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" "os" "strings" "sync" "testing" "time" "gitea.dooplex.hu/admin/felhom-agent/internal/hub" ) // 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 } 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). Red-proofed 2026-07-04: flipping clientMTU back to 1420 fails this test // on the MTU line mismatch — the golden is non-vacuous, not a "contains MTU" check. func TestRenderConf_Golden(t *testing.T) { block := testBlock("ignored") conf, err := renderConf(block, vectorPrivB64) 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 = ep0.felhom.eu: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. bad := *block bad.Endpoint.ServerPubkey = "short" if _, err := renderConf(&bad, vectorPrivB64); err == nil { t.Error("bad server pubkey accepted") } bad = *block bad.AssignedIP = "10.77.0.2" // no /32 if _, err := renderConf(&bad, vectorPrivB64); err == nil { t.Error("assigned_ip without /32 accepted") } bad = *block bad.Endpoint.DNSName = "evil host\ninjected" if _, err := renderConf(&bad, vectorPrivB64); err == nil { t.Error("hostile dns_name 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)") } }