package wgtunnel import ( "context" "errors" "fmt" "os" "path/filepath" "strings" "testing" "time" ) // D30 (R-1204): a handshake stamped in the future that STAYS for rekeyAfter → a new key pair, registered on the // existing peer path (the hub re-keys in place; its full-list push to ep0 swaps the key). The fake `wg` reports a // handshake at 1783107115; a clock three hours behind that sees it three hours in the future. // RED-PROOF (2026-10-22 night): with the clockAheadRekeyLocked call removed from Watchdog, // TestD30_AFutureHandshakeThatStaysReKeys and …AFailedRegistration… fail (no new key, no registration); with the // rekeyAfter wait removed, …ThatStaysReKeys fails (a re-key at 10 min); with the isActive check removed, // TestD30_NoReKeyWhileTheTunnelIsDown fails. const hs = 1783107115 func at(m *Manager, unix int64) { m.now = func() time.Time { return time.Unix(unix, 0) } } func readKey(t *testing.T, m *Manager, name string) string { t.Helper() b, err := os.ReadFile(filepath.Join(m.stateDir, "wg", name)) if err != nil { t.Fatal(err) } return strings.TrimSpace(string(b)) } func TestD30_AFutureHandshakeThatStaysReKeys(t *testing.T) { fh := &fakeHub{} m, _, active, logs := testManager(t, fh) oldPub := establishWG(t, m, active) oldPriv := readKey(t, m, keyFileName) ctx := context.Background() calls := fh.count() t0 := int64(hs - 3*3600) // the clock is right again; the last handshake (made while it ran ahead) is 3 h ahead at(m, t0) m.Watchdog(ctx, true, testBlock(oldPub)) at(m, t0+10*60) m.Watchdog(ctx, true, testBlock(oldPub)) if fh.count() != calls || localPub(t, m) != oldPub { t.Fatal("no re-key before the future stamp has stayed rekeyAfter") } at(m, t0+16*60) m.Watchdog(ctx, true, testBlock(oldPub)) newPub := localPub(t, m) if newPub == oldPub { t.Fatal("a future stamp that stayed 16 min must give a new key") } if fh.count() != calls+1 { t.Fatalf("the new key must be registered on the existing peer path (RegisterWG calls %d → %d)", calls, fh.count()) } if mk := m.loadMarker(); mk == nil || mk.Pubkey != newPub { t.Fatalf("the registration marker must carry the new key: %+v", mk) } if readKey(t, m, prevKeyName) != oldPriv { t.Fatal("the old private key must be kept as private.key.prev (the escrow may hold it)") } if strings.Contains(logs.String(), oldPriv) || strings.Contains(logs.String(), readKey(t, m, keyFileName)) { t.Fatal("a private key reached the log") } // The served block still names the OLD key until the next fetch: no second re-key over that. at(m, t0+17*60) m.Watchdog(ctx, true, testBlock(oldPub)) if localPub(t, m) != newPub { t.Fatal("a second re-key ran before the hub served the new key") } // The hub serves the new key → the tunnel is applied with the NEW private key. m.Apply(ctx, true, testBlock(newPub)) conf, err := os.ReadFile(m.stagedConfPath()) if err != nil { t.Fatal(err) } if !strings.Contains(string(conf), readKey(t, m, keyFileName)) || strings.Contains(string(conf), oldPriv) { t.Fatal("the applied tunnel must use the new private key, not the old one") } } func TestD30_AShortFutureSpellDoesNotReKey(t *testing.T) { fh := &fakeHub{} m, _, active, _ := testManager(t, fh) pub := establishWG(t, m, active) ctx := context.Background() t0 := int64(hs - 3*3600) at(m, t0) m.Watchdog(ctx, true, testBlock(pub)) at(m, hs+100) // a handshake now lies in the past: the sign ended (a working tunnel) m.Watchdog(ctx, true, testBlock(pub)) at(m, t0+20*60) // in the future again: a NEW window starts, it does not continue the old one m.Watchdog(ctx, true, testBlock(pub)) if localPub(t, m) != pub { t.Fatal("a future stamp that did not STAY for rekeyAfter must not re-key") } } func TestD30_NoReKeyWhileTheTunnelIsDown(t *testing.T) { fh := &fakeHub{} m, _, active, _ := testManager(t, fh) pub := establishWG(t, m, active) *active = false ctx := context.Background() t0 := int64(hs - 3*3600) for _, d := range []int64{0, 20 * 60, 40 * 60} { at(m, t0+d) m.Watchdog(ctx, true, testBlock(pub)) } if localPub(t, m) != pub { t.Fatal("no re-key while the tunnel unit is not active") } } func TestD30_AFailedRegistrationIsRetriedByTheExistingPath(t *testing.T) { fh := &fakeHub{} m, _, active, _ := testManager(t, fh) oldPub := establishWG(t, m, active) ctx := context.Background() t0 := int64(hs - 3*3600) at(m, t0) m.Watchdog(ctx, true, testBlock(oldPub)) fh.mu.Lock() fh.err = errors.New("hub down") fh.mu.Unlock() at(m, t0+16*60) m.Watchdog(ctx, true, testBlock(oldPub)) newPub := localPub(t, m) if newPub == oldPub { t.Fatal("the key rotates before the registration") } if mk := m.loadMarker(); mk == nil || mk.Pubkey != oldPub { t.Fatal("a failed registration leaves the marker on the old key") } fh.mu.Lock() fh.err = nil fh.mu.Unlock() at(m, t0+30*60) // past the backoff: Apply sees the served (old) key ≠ the local key → re-key-in-place registers m.Apply(ctx, true, testBlock(oldPub)) if mk := m.loadMarker(); mk == nil || mk.Pubkey != newPub { t.Fatalf("the existing re-key-in-place path must register the new key: %+v", mk) } } // Review 2026-10-23 night. N7: a second rotation keeps the FIRST old key as `private.key.prev` (the escrow may hold it) // and the later one beside it; N6: the current key is replaced by a rename OVER it, so `private.key` is never missing. // RED-PROOF: with the „prev exists → a dated name" branch removed, TwoRotations fails (the first key overwritten) — // seen failing 2026-10-23 night. func TestD30_TwoRotationsKeepTheFirstOldKey(t *testing.T) { fh := &fakeHub{} m, _, active, _ := testManager(t, fh) establishWG(t, m, active) first := readKey(t, m, keyFileName) old := rekeyNow defer func() { rekeyNow = old }() rekeyNow = func() time.Time { return time.Unix(hs, 0) } if _, err := rotateKey(m.stateDir); err != nil { t.Fatal(err) } second := readKey(t, m, keyFileName) rekeyNow = func() time.Time { return time.Unix(hs+7200, 0) } if _, err := rotateKey(m.stateDir); err != nil { t.Fatal(err) } if readKey(t, m, prevKeyName) != first { t.Fatal("private.key.prev must keep the FIRST old key") } if readKey(t, m, fmt.Sprintf("%s.%d", prevKeyName, hs+7200)) != second { t.Fatal("the second old key must be kept beside it") } if cur := readKey(t, m, keyFileName); cur == first || cur == second { t.Fatal("the current key must be new") } } // N7: the once-an-hour bound survives an agent restart (it was in memory only). RED-PROOF: with loadLastRekey removed // from clockAheadRekeyLocked, this fails — seen failing 2026-10-23 night. func TestD30_TheHourBoundSurvivesARestart(t *testing.T) { fh := &fakeHub{} m, rr, active, _ := testManager(t, fh) pub := establishWG(t, m, active) ctx := context.Background() t0 := int64(hs - 3*3600) at(m, t0) m.Watchdog(ctx, true, testBlock(pub)) at(m, t0+16*60) m.Watchdog(ctx, true, testBlock(pub)) newPub := localPub(t, m) if newPub == pub { t.Fatal("fixture: the first re-key must happen") } // The agent restarts: a NEW manager on the same state; the served block now names the new key; the sign is back. m2 := NewManager(rr, fh, m.stateDir, m.logger) m2.isActive, m2.wgPresent, m2.resolver = m.isActive, m.wgPresent, m.resolver m2.Apply(ctx, true, testBlock(newPub)) at(m2, t0+20*60) m2.Watchdog(ctx, true, testBlock(newPub)) at(m2, t0+40*60) m2.Watchdog(ctx, true, testBlock(newPub)) if localPub(t, m2) != newPub { t.Fatal("a restart must not reset the once-an-hour bound") } _ = active } // Commit security review 2026-10-23 night: a last re-key saved while the clock ran ahead (a later date than now) must // not block the next re-key until that date. RED-PROOF: with the „lastRekey after now → dropped" guard removed, this // fails — seen failing 2026-10-23 night. func TestD30_ALastReKeyFromTheFutureDoesNotBlock(t *testing.T) { fh := &fakeHub{} m, _, active, _ := testManager(t, fh) pub := establishWG(t, m, active) m.saveLastRekey(time.Unix(hs+2*365*86400, 0)) // written while the clock was two years ahead ctx := context.Background() t0 := int64(hs - 3*3600) at(m, t0) m.Watchdog(ctx, true, testBlock(pub)) at(m, t0+16*60) m.Watchdog(ctx, true, testBlock(pub)) if localPub(t, m) == pub { t.Fatal("a last re-key dated in the future must not block the re-key") } }