diff --git a/hub/internal/web/hosts.go b/hub/internal/web/hosts.go index 48c37e9b..d3e5b0cd 100644 --- a/hub/internal/web/hosts.go +++ b/hub/internal/web/hosts.go @@ -955,7 +955,10 @@ func (s *Server) handleHostDelete(w http.ResponseWriter, r *http.Request, hostID http.Error(w, "Internal error", http.StatusInternalServerError) return } - s.logger.Printf("[INFO] host deleted: %s (%s)", hostID, hostDeleteEscrowEffect(escrowBefore != nil, escErr)) + // R-276: DeleteHost removed the host's wg_peers row; ask for the full-list push NOW (as the customer + // delete does, R-600), so an uninstalled box's tunnel stops being accepted by the off-site endpoint + // at once instead of on the reconciler's next 5-minute tick. + s.logger.Printf("[INFO] host deleted: %s (%s; %s)", hostID, hostDeleteEscrowEffect(escrowBefore != nil, escErr), s.requestWGPeerPush()) // R-509: the customer record stays and now waits for a box → send the connect link. if host.CustomerID != "" { s.autoMintSelfBindIfWaiting(host.CustomerID, "host delete") diff --git a/hub/internal/web/r276_host_delete_wg_test.go b/hub/internal/web/r276_host_delete_wg_test.go new file mode 100644 index 00000000..9da539c6 --- /dev/null +++ b/hub/internal/web/r276_host_delete_wg_test.go @@ -0,0 +1,53 @@ +package web + +import ( + "log" + "net/http" + "net/url" + "strings" + "testing" + + "gitea.dooplex.hu/admin/felhom-hub/internal/store" +) + +// R-276 (hub half): deleting a host removes its WireGuard peer row AND asks the peer-sync reconciler for +// an immediate full-list push (the seam is SetWGPeerSync — no SSH, no ep0), so the endpoint stops +// accepting the uninstalled box's tunnel now. The log says the push was REQUESTED, never "removed". +// RED-PROOF: drop s.requestWGPeerPush() from handleHostDelete → triggers = 0 → FAIL. +func TestR276_HostDeleteRemovesPeerAndPushes(t *testing.T) { + s, st := newTestServer(t) + var logBuf strings.Builder + s.logger = log.New(&logBuf, "", 0) + triggers := 0 + s.SetWGPeerSync(func() { triggers++ }) + if err := st.UpsertHost(&store.Host{HostID: "gone-1", CustomerID: "c9", APIKey: "k"}); err != nil { + t.Fatal(err) + } + if err := st.SetWGEndpoint(&store.WGEndpoint{EndpointID: "ep0", DNSName: "ep0.example", WGPort: 51820, + ServerPubkey: "srv", TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1"}); err != nil { + t.Fatal(err) + } + if _, _, err := st.AddWGPeer("pk-gone-1", "gone-1", "test"); err != nil { + t.Fatal(err) + } + + rr := postHostDelete(t, s, "gone-1", url.Values{"confirm_host_id": {"gone-1"}}) + if rr.Code != http.StatusSeeOther { + t.Fatalf("delete = %d (%s)", rr.Code, rr.Body.String()) + } + peers, err := st.ListWGPeers() + if err != nil { + t.Fatal(err) + } + for _, p := range peers { + if p.HostID == "gone-1" || p.Pubkey == "pk-gone-1" { + t.Fatal("the deleted host's WireGuard peer is still registered") + } + } + if triggers != 1 { + t.Fatalf("WG peer-sync triggers = %d, want 1 (the endpoint must drop the peer now, not on the next tick)", triggers) + } + if !strings.Contains(logBuf.String(), "WG peer removal push requested") { + t.Errorf("the delete line must name the pending push:\n%s", logBuf.String()) + } +}