hub v0.167.0 source (not built, not deployed): D31 — the F-14 PBS auto re-issue reads the newest deletion of a host that held a key, a keyless install between no longer blocks it (R-1205); D30 — the new key's handshake clears wg_tunnel_stale (pinned), wording; wire gate entry; Part 1 stray-copy 9202 evidence
gates / gates (push) Successful in 15m46s

This commit is contained in:
2026-10-22 21:04:42 +02:00
parent 96ac07e168
commit 84751cb0ce
18 changed files with 517 additions and 13 deletions
+7 -4
View File
@@ -10,8 +10,11 @@ import (
// than the last one accepted from that peer), so the box's tunnel to ep0 — its PBS whole-box copies and the
// operator's door — stays dead until the real date passes the wrong one, while the box itself reports normally.
// Nothing said why. The hub now tells the OPERATOR once per host, edge-triggered from the host's newest tunnel event
// (the host_swap shape), and once when it is healthy again. Operator-only (notify.operatorOnlyEvents): the household
// cannot fix it. The repair (re-key the peer, or ep0 forgets the stored timestamp) is a separate decision.
// (the host_swap shape), and records once when it is healthy again (wg_tunnel_ok is an info event: stored, not
// mailed). Operator-only (notify.operatorOnlyEvents): the household cannot fix it. The repair is D30 (operator
// 2026-10-22): an agent with it makes a new key after 15 minutes of a future-stamped handshake and registers it here
// (re-key in place, the full-list push to ep0); the new key's first handshake clears this alarm — pinned by
// TestD30_TheAlarmClearsWhenTheNewKeyShakesHands.
const (
eventWGTunnelStale = "wg_tunnel_stale"
eventWGTunnelOK = "wg_tunnel_ok"
@@ -36,7 +39,7 @@ func wgTunnelStale(w *hostWireguard) (bool, string) {
return false, ""
}
if w.HandshakeFutureS != nil && *w.HandshakeFutureS > 0 {
return true, fmt.Sprintf("its last handshake is stamped %s in the FUTURE by the box's own clock — the clock ran ahead once, and ep0 refuses every newer handshake until that time passes", humanDuration(*w.HandshakeFutureS))
return true, fmt.Sprintf("its last handshake is stamped %s in the FUTURE by the box's own clock — the clock ran ahead once, and ep0 refuses every newer handshake until that time passes (an agent with the D30 repair makes a new key by itself within about 15 minutes)", humanDuration(*w.HandshakeFutureS))
}
if w.LastHandshakeAgeS != nil && *w.LastHandshakeAgeS > wgTunnelStaleAfterS {
return true, fmt.Sprintf("it has not shaken hands for %s while the box reports — the box's clock may have run ahead (then ep0 refuses it), or ep0 dropped the peer", humanDuration(*w.LastHandshakeAgeS))
@@ -78,7 +81,7 @@ func (h *Handler) checkWGTunnel(hostID, custID string, w *hostWireguard) {
default:
return
}
h.logger.Printf("[WARN] host %s: %s", hostID, typ)
h.logger.Printf("[%s] host %s: %s", map[bool]string{true: "WARN", false: "INFO"}[stale], hostID, typ)
det := map[string]any{"host_id": hostID}
if w.LastHandshakeAgeS != nil {
det["last_handshake_age_s"] = *w.LastHandshakeAgeS
+29
View File
@@ -76,3 +76,32 @@ func TestR1204_WhatIsJudged(t *testing.T) {
}
}
}
// D30 (R-1204): after the agent re-keys, the box reports a restarted tunnel with no handshake yet (nothing to judge),
// then the new key's first handshake — which clears the alarm once, and raises nothing new.
// RED-PROOF (2026-10-22 night): with the `!stale && wasStale` case removed from checkWGTunnel, this fails (no ok event).
func TestD30_TheAlarmClearsWhenTheNewKeyShakesHands(t *testing.T) {
h, st, _ := newTestHandler(t)
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "p"})
st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})
post := func(wg string) {
t.Helper()
if rr := do(h, http.MethodPost, "/host-report", "HKEY", tunnelReportBody("h1", wg)); rr.Code != http.StatusOK {
t.Fatalf("host-report = %d %s", rr.Code, rr.Body.String())
}
}
count := func(typ string) int {
evs, _ := st.GetEventsByType("c1", typ, time.Time{})
return len(evs)
}
post(`{"pubkey":"OLD","registered":true,"active":true,"last_handshake_age_s":0,"handshake_future_s":10800}`) // 3 h ahead
post(`{"pubkey":"NEW","registered":true,"active":true}`) // re-keyed, no handshake yet
if count(eventWGTunnelStale) != 1 || count(eventWGTunnelOK) != 0 {
t.Fatalf("stale %d ok %d, want 1 / 0 before the new key shakes hands", count(eventWGTunnelStale), count(eventWGTunnelOK))
}
post(`{"pubkey":"NEW","registered":true,"active":true,"last_handshake_age_s":20}`)
post(`{"pubkey":"NEW","registered":true,"active":true,"last_handshake_age_s":110}`)
if count(eventWGTunnelOK) != 1 || count(eventWGTunnelStale) != 1 {
t.Fatalf("stale %d ok %d, want 1 / 1 — the new key's handshake clears the alarm once", count(eventWGTunnelStale), count(eventWGTunnelOK))
}
}