228 lines
8.2 KiB
Go
228 lines
8.2 KiB
Go
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")
|
|
}
|
|
}
|