package wgtunnel // Group B — the manager state machine (Scenarios A-D). Non-hollow: every test asserts EXEC // COUNTS on the recording runner (the no-exec negatives are the load-bearing ones), single // registration, marker semantics, and the absence of key material in the captured log buffer. import ( "bytes" "context" "errors" "io" "log/slog" "net/netip" "os" "strings" "sync" "testing" "time" "gitea.dooplex.hu/admin/felhom-agent/internal/hub" ) // fakeResolver drives endpoint resolution deterministically + counts lookups (the "no DNS while // healthy" negative asserts count stays flat). type fakeResolver struct { mu sync.Mutex addrs []netip.Addr err error calls int } func (f *fakeResolver) LookupIPv4(_ context.Context, _ string) ([]netip.Addr, error) { f.mu.Lock() defer f.mu.Unlock() f.calls++ if f.err != nil { return nil, f.err } return append([]netip.Addr(nil), f.addrs...), nil } func (f *fakeResolver) count() int { f.mu.Lock(); defer f.mu.Unlock(); return f.calls } func (f *fakeResolver) set(a ...netip.Addr) { f.mu.Lock() f.addrs, f.err = a, nil f.mu.Unlock() } func (f *fakeResolver) fail(e error) { f.mu.Lock(); f.err = e; f.mu.Unlock() } // recordingRunner records every privileged exec; scripted errors per command name. type recordingRunner struct { mu sync.Mutex calls [][]string fail map[string]error // command name → error } func (r *recordingRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) { r.mu.Lock() r.calls = append(r.calls, append([]string{name}, args...)) err := r.fail[name] r.mu.Unlock() if err != nil { return nil, []byte("scripted failure"), err } if name == "wg" { return []byte("SERVERPUB\t1783107115\n"), nil, nil } return nil, nil, nil } func (r *recordingRunner) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) { return r.Run(ctx, name, args...) } func (r *recordingRunner) count(name string) int { r.mu.Lock() defer r.mu.Unlock() n := 0 for _, c := range r.calls { if c[0] == name { n++ } } return n } func (r *recordingRunner) total() int { r.mu.Lock() defer r.mu.Unlock() return len(r.calls) } func (r *recordingRunner) lastSystemctl() []string { r.mu.Lock() defer r.mu.Unlock() for i := len(r.calls) - 1; i >= 0; i-- { if r.calls[i][0] == "systemctl" { return r.calls[i] } } return nil } // fakeHub scripts RegisterWG. type fakeHub struct { mu sync.Mutex calls int resp *hub.WGRegisterResponse err error } func (f *fakeHub) RegisterWG(_ context.Context, pubkey string) (*hub.WGRegisterResponse, error) { f.mu.Lock() defer f.mu.Unlock() f.calls++ if f.err != nil { return nil, f.err } if f.resp != nil { return f.resp, nil } return &hub.WGRegisterResponse{Pubkey: pubkey, AssignedIP: "10.77.0.2/32", Generation: 3, Sync: "ok"}, nil } func (f *fakeHub) count() int { f.mu.Lock(); defer f.mu.Unlock(); return f.calls } // testManager builds a Manager over temp state with all external probes faked. // active is a pointer so tests flip service state mid-scenario. func testManager(t *testing.T, fh *fakeHub) (*Manager, *recordingRunner, *bool, *bytes.Buffer) { t.Helper() rr := &recordingRunner{fail: map[string]error{}} active := false logBuf := &bytes.Buffer{} logger := slog.New(slog.NewTextHandler(logBuf, &slog.HandlerOptions{Level: slog.LevelDebug})) m := NewManager(rr, fh, t.TempDir(), logger) m.isActive = func(ctx context.Context) bool { return active } m.wgPresent = func() bool { return true } // Default resolver: one A record, no real DNS. Tests exercising the watchdog swap in their own. m.resolver = &fakeResolver{addrs: []netip.Addr{netip.MustParseAddr("167.233.158.164")}} return m, rr, &active, logBuf } func testBlock(pub string) *hub.WireWireguard { return &hub.WireWireguard{ Endpoint: hub.WireWireguardEndpoint{ DNSName: "ep0.felhom.eu", WGPort: 443, ServerPubkey: "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=", PBSTunnelIP: "10.77.0.1", }, Pubkey: pub, AssignedIP: "10.77.0.2/32", } } func localPub(t *testing.T, m *Manager) string { t.Helper() pub, _, err := EnsureKey(m.stateDir) if err != nil { t.Fatal(err) } return pub } // TestRenderConf_Golden pins the EXACT conf bytes, including "MTU = 1280" (the IPv6-minimum // floor, doc 06 §4.3) and the v4-pinned "Endpoint = 167.233.158.164:443" (doc 06 §4.2 — the A // LITERAL, never the DNS name, never an AAAA). Red-proofed: flipping clientMTU back to 1420, or // rendering the dns_name instead of the resolved literal, fails this on the mismatch — non-vacuous. func TestRenderConf_Golden(t *testing.T) { block := testBlock("ignored") conf, err := renderConf(block, vectorPrivB64, "167.233.158.164") if err != nil { t.Fatal(err) } want := `# felhom offsite tunnel — agent-managed (S3); DO NOT EDIT [Interface] PrivateKey = ` + vectorPrivB64 + ` Address = 10.77.0.2/32 MTU = 1280 [Peer] PublicKey = CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk= Endpoint = 167.233.158.164:443 AllowedIPs = 10.77.0.1/32 PersistentKeepalive = 25 ` if conf != want { t.Errorf("conf mismatch:\n--- got ---\n%s\n--- want ---\n%s", conf, want) } // Refusals: bad server key, bad ip, hostile dns_name, non-v4 endpoint literal. bad := *block bad.Endpoint.ServerPubkey = "short" if _, err := renderConf(&bad, vectorPrivB64, "167.233.158.164"); err == nil { t.Error("bad server pubkey accepted") } bad = *block bad.AssignedIP = "10.77.0.2" // no /32 if _, err := renderConf(&bad, vectorPrivB64, "167.233.158.164"); err == nil { t.Error("assigned_ip without /32 accepted") } bad = *block bad.Endpoint.DNSName = "evil host\ninjected" if _, err := renderConf(&bad, vectorPrivB64, "167.233.158.164"); err == nil { t.Error("hostile dns_name accepted") } // The endpoint literal must be IPv4 — a v6 (or the DNS name) is refused. if _, err := renderConf(block, vectorPrivB64, "2a01:4f8:1c16:7aa1::1"); err == nil { t.Error("IPv6 endpoint literal accepted (v4-pin violated)") } if _, err := renderConf(block, vectorPrivB64, "ep0.felhom.eu"); err == nil { t.Error("DNS name accepted as endpoint literal") } } // TestRenderConf_OOBPeerAppendedSortedAndStable is the [OF-1] core: when oob_peer_ip is set the // operator /32 joins AllowedIPs, DETERMINISTICALLY SORTED, and two renders are BYTE-IDENTICAL (the // conf-hash stability the reconcile loop depends on — trap 1). Absent → byte-identical to pre-H1. func TestRenderConf_OOBPeerAppendedSortedAndStable(t *testing.T) { // operator IP that sorts AFTER the pbs IP (10.77.0.1 < 10.77.0.250). block := testBlock("ignored") block.OOBPeerIP = "10.77.0.250" c1, err := renderConf(block, vectorPrivB64, "167.233.158.164") if err != nil { t.Fatal(err) } if !strings.Contains(c1, "AllowedIPs = 10.77.0.1/32, 10.77.0.250/32\n") { t.Fatalf("OOB /32 not appended sorted:\n%s", c1) } // byte-stable across renders (no per-tick flapping). c2, _ := renderConf(block, vectorPrivB64, "167.233.158.164") if c1 != c2 { t.Fatal("two renders of the same block differ — conf-hash would flap every tick") } // operator IP that sorts BEFORE the pbs IP → it must come FIRST (proves the sort, not append order). lower := testBlock("ignored") lower.OOBPeerIP = "10.76.0.9" cl, err := renderConf(lower, vectorPrivB64, "167.233.158.164") if err != nil { t.Fatal(err) } if !strings.Contains(cl, "AllowedIPs = 10.76.0.9/32, 10.77.0.1/32\n") { t.Fatalf("lower OOB IP not sorted first:\n%s", cl) } // absent oob_peer_ip → byte-identical to the pre-H1 single-/32 line. none := testBlock("ignored") cn, _ := renderConf(none, vectorPrivB64, "167.233.158.164") if !strings.Contains(cn, "AllowedIPs = 10.77.0.1/32\n") { t.Fatalf("absent oob must render the single pbs /32:\n%s", cn) } // invalid oob_peer_ip is a HARD error (never a silently-dropped widening). bad := testBlock("ignored") bad.OOBPeerIP = "not-an-ip" if _, err := renderConf(bad, vectorPrivB64, "167.233.158.164"); err == nil { t.Error("invalid oob_peer_ip accepted") } bad.OOBPeerIP = "2a01:4f8::1" // v6 → refused (arc is v4-pinned) if _, err := renderConf(bad, vectorPrivB64, "167.233.158.164"); err == nil { t.Error("IPv6 oob_peer_ip accepted") } } func TestScenarioA_RegisterThenApplyOrdering(t *testing.T) { fh := &fakeHub{} m, rr, active, _ := testManager(t, fh) ctx := context.Background() // Tick 1: no desired data yet → keygen + ONE registration, NO conf/systemctl. m.Apply(ctx, false, nil) if fh.count() != 1 { t.Fatalf("register calls = %d, want 1", fh.count()) } if rr.total() != 0 { t.Fatalf("privileged execs before the block arrived: %v", rr.calls) } if m.loadMarker() == nil { t.Fatal("marker not written after successful registration") } // Tick 2 (still nothing fetched): NO second registration (marker gate). m.Apply(ctx, false, nil) if fh.count() != 1 { t.Fatalf("register calls after marker = %d, want still 1 (marker gate)", fh.count()) } // Block arrives → conf staged + installed + enabled. pub := localPub(t, m) m.Apply(ctx, true, testBlock(pub)) if rr.count("install") != 1 || rr.count("systemctl") != 1 { t.Fatalf("apply execs = %v", rr.calls) } if got := rr.lastSystemctl(); strings.Join(got, " ") != "systemctl enable --now wg-quick@wg-felhom" { t.Errorf("systemctl argv = %v", got) } if _, err := os.Stat(m.stagedConfPath()); err != nil { t.Error("staged conf missing") } // Steady state with service now active: NO further execs. *active = true before := rr.total() m.Apply(ctx, true, testBlock(pub)) if rr.total() != before { t.Errorf("steady-state tick ran execs: %v", rr.calls[before:]) } } func TestScenarioB_ConfChangeRestartsAndSelfHeal(t *testing.T) { fh := &fakeHub{} m, rr, active, _ := testManager(t, fh) ctx := context.Background() m.Apply(ctx, false, nil) // register pub := localPub(t, m) m.Apply(ctx, true, testBlock(pub)) // first apply (enable) *active = true // Endpoint re-key: server_pubkey changes → re-render + install + RESTART (not reload/enable). changed := testBlock(pub) changed.Endpoint.ServerPubkey = "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=" m.Apply(ctx, true, changed) if got := rr.lastSystemctl(); strings.Join(got, " ") != "systemctl restart wg-quick@wg-felhom" { t.Errorf("conf change systemctl = %v, want restart", got) } // Crash/manual stop with unchanged conf → self-heal enable --now. *active = false m.Apply(ctx, true, changed) if got := rr.lastSystemctl(); strings.Join(got, " ") != "systemctl enable --now wg-quick@wg-felhom" { t.Errorf("self-heal systemctl = %v, want enable --now", got) } } func TestScenarioC_RevocationDisablesAndNeverReregisters(t *testing.T) { fh := &fakeHub{} m, rr, active, _ := testManager(t, fh) ctx := context.Background() m.Apply(ctx, false, nil) pub := localPub(t, m) m.Apply(ctx, true, testBlock(pub)) *active = true // The hub removes the peer: PRESENT desired-state, absent block. m.Apply(ctx, true, nil) if got := rr.lastSystemctl(); strings.Join(got, " ") != "systemctl disable --now wg-quick@wg-felhom" { t.Fatalf("revocation systemctl = %v, want disable --now", got) } if m.loadMarker() == nil { t.Fatal("marker removed on revocation — it must be KEPT (revoked stays revoked)") } *active = false // Multiple later ticks: NO re-register, NO install, NO enable (the load-bearing negatives). regBefore, execBefore := fh.count(), rr.total() for i := 0; i < 5; i++ { m.Apply(ctx, true, nil) } if fh.count() != regBefore { t.Errorf("re-registration after revocation: %d calls", fh.count()-regBefore) } if rr.total() != execBefore { t.Errorf("execs after teardown: %v", rr.calls[execBefore:]) } // Operator re-adds the peer → block returns → tunnel back WITHOUT registration. m.Apply(ctx, true, testBlock(pub)) if fh.count() != regBefore { t.Errorf("re-add triggered a registration: %d", fh.count()-regBefore) } if got := rr.lastSystemctl(); strings.Join(got, " ") != "systemctl enable --now wg-quick@wg-felhom" { t.Errorf("re-add systemctl = %v, want enable --now", got) } } func TestScenarioD_MismatchReregisterWithBackoff(t *testing.T) { fh := &fakeHub{} m, _, _, _ := testManager(t, fh) ctx := context.Background() now := time.Unix(1_800_000_000, 0) m.now = func() time.Time { return now } m.Apply(ctx, false, nil) // initial registration pub := localPub(t, m) // The hub serves a block for ANOTHER pubkey → exactly one re-register per backoff window. other := testBlock("hHwNLDdSNPNl5mCVUYejc1oPdhPRYJ06ak2MU66qWiI=") fh.err = errors.New("hub 500") m.Apply(ctx, true, other) if fh.count() != 2 { t.Fatalf("mismatch register calls = %d, want 2 (initial + one retry)", fh.count()) } // Same instant: backoff gates the hot loop. m.Apply(ctx, true, other) m.Apply(ctx, true, other) if fh.count() != 2 { t.Fatalf("backoff not applied: %d calls", fh.count()) } // After the backoff window: one more attempt, this time succeeding → marker updated. fh.err = nil now = now.Add(2 * time.Minute) m.Apply(ctx, true, other) if fh.count() != 3 { t.Fatalf("post-backoff register calls = %d, want 3", fh.count()) } if mk := m.loadMarker(); mk == nil || mk.Pubkey != pub { t.Errorf("marker after re-key = %+v, want local pubkey %s", m.loadMarker(), pub) } } func TestAdoptLostMarkerWithKnownKey(t *testing.T) { fh := &fakeHub{} m, rr, _, _ := testManager(t, fh) ctx := context.Background() pub := localPub(t, m) // key exists, NO marker (lost state), hub already has the block m.Apply(ctx, true, testBlock(pub)) if fh.count() != 0 { t.Fatalf("adopt path re-registered: %d calls", fh.count()) } mk := m.loadMarker() if mk == nil || mk.Pubkey != pub || mk.AssignedIP != "10.77.0.2/32" { t.Fatalf("adoption marker = %+v", mk) } if rr.count("install") != 1 { t.Errorf("adopt did not proceed to conf apply: %v", rr.calls) } } func TestNoTeardownOnAbsentData(t *testing.T) { fh := &fakeHub{} m, rr, active, _ := testManager(t, fh) ctx := context.Background() m.Apply(ctx, false, nil) pub := localPub(t, m) m.Apply(ctx, true, testBlock(pub)) *active = true before := rr.total() // Desired fetch stale/unavailable (fetched=false): keep last-applied state — NO teardown, // NO execs (red-proof (d) anchor). for i := 0; i < 3; i++ { m.Apply(ctx, false, nil) } if rr.total() != before { t.Errorf("absent-data tick ran execs (teardown-on-absent-data bug): %v", rr.calls[before:]) } } func TestPartialStateAndCorruptKeyIdle(t *testing.T) { fh := &fakeHub{} m, rr, _, _ := testManager(t, fh) ctx := context.Background() // Marker without key file → idle, no registration, no execs, key NOT created. if err := m.writeMarker(marker{Pubkey: "X", AssignedIP: "10.77.0.9/32"}); err != nil { t.Fatal(err) } m.Apply(ctx, false, nil) if fh.count() != 0 || rr.total() != 0 { t.Fatalf("partial state acted: reg=%d execs=%d", fh.count(), rr.total()) } if _, err := os.Stat(KeyFilePath(m.stateDir)); !os.IsNotExist(err) { t.Fatal("partial state minted a fresh key — never-guess violated") } // Corrupt key file → idle likewise. os.Remove(m.markerPath()) os.MkdirAll(m.wgDir(), 0o700) os.WriteFile(KeyFilePath(m.stateDir), []byte("garbage"), 0o600) m.Apply(ctx, false, nil) if fh.count() != 0 || rr.total() != 0 { t.Fatalf("corrupt key acted: reg=%d execs=%d", fh.count(), rr.total()) } } func TestStatusStanzaAndHandshakeParse(t *testing.T) { fh := &fakeHub{} m, rr, active, _ := testManager(t, fh) ctx := context.Background() m.now = func() time.Time { return time.Unix(1783107215, 0) } // 100s after the scripted handshake m.Apply(ctx, false, nil) // keygen + register *active = true st := m.Status(ctx) if st.Pubkey == "" || !st.Registered || !st.Active || st.AssignedIP != "10.77.0.2/32" { t.Fatalf("status = %+v", st) } if st.LastHandshakeAgeS == nil || *st.LastHandshakeAgeS != 100 { t.Fatalf("handshake age = %v, want 100", st.LastHandshakeAgeS) } // The ONLY wg invocation is latest-handshakes — never `dump`. rr.mu.Lock() for _, c := range rr.calls { if c[0] == "wg" && strings.Join(c, " ") != "wg show wg-felhom latest-handshakes" { t.Errorf("unexpected wg invocation: %v", c) } } rr.mu.Unlock() // Inactive service → no wg exec, nil age. *active = false wgBefore := rr.count("wg") st = m.Status(ctx) if st.Active || st.LastHandshakeAgeS != nil { t.Errorf("inactive status = %+v", st) } if rr.count("wg") != wgBefore { t.Error("handshake read ran against an inactive service") } } func TestNoKeyMaterialInLogs(t *testing.T) { fh := &fakeHub{} m, _, active, logBuf := testManager(t, fh) ctx := context.Background() m.Apply(ctx, false, nil) pub := localPub(t, m) m.Apply(ctx, true, testBlock(pub)) *active = true m.Apply(ctx, true, nil) // revocation path too m.Status(ctx) raw, err := os.ReadFile(KeyFilePath(m.stateDir)) if err != nil { t.Fatal(err) } privB64 := strings.TrimSpace(string(raw)) if privB64 == "" || len(privB64) != 44 { t.Fatalf("unexpected key file %q", privB64) } logs := logBuf.String() if strings.Contains(logs, privB64) { t.Fatal("PRIVATE KEY found in the log buffer") } // base64 of the raw key without padding variants too (defense against partial logging) if trimmed := strings.TrimRight(privB64, "="); len(trimmed) > 20 && strings.Contains(logs, trimmed) { t.Fatal("private key material (unpadded) found in the log buffer") } if !strings.Contains(logs, pub) { t.Error("pubkey absent from logs — expected (pubkeys are fine to log)") } } // --- S4: v4-pin + re-resolve watchdog (doc 06 §4.2) --- // TestV4Pin_LowestARecordInConf: Apply renders the DETERMINISTIC-lowest A record as the Endpoint // literal (never the DNS name), resolves exactly once, and a steady-state re-Apply hits the cache // (no second DNS call — the "no DNS while healthy" negative at the Apply level). func TestV4Pin_LowestARecordInConf(t *testing.T) { fh := &fakeHub{} m, _, active, _ := testManager(t, fh) fr := &fakeResolver{} fr.set(netip.MustParseAddr("167.233.158.164"), netip.MustParseAddr("10.0.0.5")) // lowest = 10.0.0.5 m.resolver = fr ctx := context.Background() m.Apply(ctx, false, nil) pub := localPub(t, m) m.Apply(ctx, true, testBlock(pub)) *active = true staged, err := os.ReadFile(m.stagedConfPath()) if err != nil { t.Fatal(err) } if !strings.Contains(string(staged), "Endpoint = 10.0.0.5:443") { t.Errorf("conf did not pin the lowest A record:\n%s", staged) } if strings.Contains(string(staged), "ep0.felhom.eu") { t.Error("DNS name leaked into the conf (v4-pin violated)") } m.Apply(ctx, true, testBlock(pub)) // steady state if fr.count() != 1 { t.Errorf("resolver calls = %d, want exactly 1 (cached after first apply — no DNS while healthy)", fr.count()) } } // establishWG brings a manager to applied+active over the given resolver; returns the local pubkey. func establishWG(t *testing.T, m *Manager, active *bool) string { t.Helper() ctx := context.Background() m.Apply(ctx, false, nil) pub := localPub(t, m) m.Apply(ctx, true, testBlock(pub)) *active = true return pub } func TestWatchdog_HealthyNoReResolve(t *testing.T) { fh := &fakeHub{} m, rr, active, _ := testManager(t, fh) fr := &fakeResolver{} fr.set(netip.MustParseAddr("167.233.158.164")) m.resolver = fr pub := establishWG(t, m, active) ctx := context.Background() m.now = func() time.Time { return time.Unix(1783107115+30, 0) } // handshake age 30s < 180 → fresh dnsBefore, scBefore := fr.count(), rr.count("systemctl") m.Watchdog(ctx, true, testBlock(pub)) if fr.count() != dnsBefore { t.Errorf("watchdog re-resolved a HEALTHY tunnel: %d extra DNS calls", fr.count()-dnsBefore) } if rr.count("systemctl") != scBefore { t.Errorf("watchdog took mutating action on a healthy tunnel: %v", rr.calls) } } func TestWatchdog_StaleReIPRestarts(t *testing.T) { fh := &fakeHub{} m, rr, active, _ := testManager(t, fh) fr := &fakeResolver{} fr.set(netip.MustParseAddr("167.233.158.164")) m.resolver = fr pub := establishWG(t, m, active) ctx := context.Background() m.now = func() time.Time { return time.Unix(1783107115+300, 0) } // age 300s > 180 → stale fr.set(netip.MustParseAddr("192.0.2.9")) // endpoint re-IP m.Watchdog(ctx, true, testBlock(pub)) if got := rr.lastSystemctl(); strings.Join(got, " ") != "systemctl restart wg-quick@wg-felhom" { t.Errorf("re-IP watchdog systemctl = %v, want restart", got) } staged, err := os.ReadFile(m.stagedConfPath()) if err != nil { t.Fatal(err) } if !strings.Contains(string(staged), "Endpoint = 192.0.2.9:443") { t.Errorf("conf not re-pinned to the new endpoint IP:\n%s", staged) } } func TestWatchdog_StaleSameIPNoChurn(t *testing.T) { fh := &fakeHub{} m, rr, active, logBuf := testManager(t, fh) fr := &fakeResolver{} fr.set(netip.MustParseAddr("167.233.158.164")) m.resolver = fr pub := establishWG(t, m, active) ctx := context.Background() m.now = func() time.Time { return time.Unix(1783107115+300, 0) } // stale, but resolver returns SAME IP scBefore := rr.count("systemctl") m.Watchdog(ctx, true, testBlock(pub)) m.Watchdog(ctx, true, testBlock(pub)) // second stale tick if rr.count("systemctl") != scBefore { t.Errorf("watchdog restarted on stale-but-unchanged IP (churn): %v", rr.calls) } if n := strings.Count(logBuf.String(), "endpoint IP unchanged"); n != 1 { t.Errorf("stale-same-IP warning logged %d times, want throttled to 1", n) } } func TestWatchdog_ResolverFailureKeepsConf(t *testing.T) { fh := &fakeHub{} m, rr, active, _ := testManager(t, fh) fr := &fakeResolver{} fr.set(netip.MustParseAddr("167.233.158.164")) m.resolver = fr pub := establishWG(t, m, active) ctx := context.Background() orig, _ := os.ReadFile(m.stagedConfPath()) m.now = func() time.Time { return time.Unix(1783107115+300, 0) } // stale fr.fail(errors.New("resolver down")) scBefore := rr.count("systemctl") m.Watchdog(ctx, true, testBlock(pub)) if rr.count("systemctl") != scBefore { t.Errorf("watchdog acted despite a resolver failure: %v", rr.calls) } after, _ := os.ReadFile(m.stagedConfPath()) if !bytes.Equal(orig, after) { t.Error("conf changed on resolver failure — must keep the last-applied conf") } } // TestInitialResolveFailureNoTeardown: first apply with a failing resolver cannot render — so it // applies NOTHING and NEVER tears down (DNS failure ≠ revocation); it recovers when DNS returns. func TestInitialResolveFailureNoTeardown(t *testing.T) { fh := &fakeHub{} m, rr, _, _ := testManager(t, fh) fr := &fakeResolver{} fr.fail(errors.New("resolver down")) m.resolver = fr ctx := context.Background() m.Apply(ctx, false, nil) // register (no DNS) pub := localPub(t, m) m.Apply(ctx, true, testBlock(pub)) // resolve fails → cannot apply if rr.count("install") != 0 || rr.count("systemctl") != 0 { t.Errorf("applied/tore-down despite a resolve failure: %v", rr.calls) } // DNS returns → the tunnel comes up on the next tick. fr.set(netip.MustParseAddr("167.233.158.164")) m.Apply(ctx, true, testBlock(pub)) if rr.count("install") != 1 { t.Errorf("did not recover after DNS returned: %v", rr.calls) } }