R-982 (D4): root SSH from the LAN by key only — a bundle drop-in, checked and reloaded by felhom-os-apply; R-1204: the heartbeat reports a future-stamped tunnel handshake; unreleased
gates / gates (push) Successful in 2m23s
gates / gates (push) Successful in 2m23s
This commit is contained in:
1 parent
2d5840ba11
commit
a0c88e5183
7 files changed
+144
-4
No files matched your search
@@ -292,7 +292,11 @@ type WireguardStatus struct {
|
||||
Registered bool `json:"registered"` // the registration marker exists
|
||||
Active bool `json:"active"` // wg-quick@wg-felhom is-active
|
||||
LastHandshakeAgeS *int64 `json:"last_handshake_age_s,omitempty"`
|
||||
AssignedIP string `json:"assigned_ip,omitempty"` // from the marker, e.g. "10.77.0.2/32"
|
||||
// HandshakeFutureS (R-1204): seconds the latest handshake lies in the FUTURE by this box's clock — set (> 0) only
|
||||
// then. A box that ran once with its clock ahead keeps a handshake stamped in the future; WireGuard on ep0 then
|
||||
// refuses every newer handshake, and LastHandshakeAgeS (clamped at 0) reads „just now" for ever. The hub alarms on it.
|
||||
HandshakeFutureS *int64 `json:"handshake_future_s,omitempty"`
|
||||
AssignedIP string `json:"assigned_ip,omitempty"` // from the marker, e.g. "10.77.0.2/32"
|
||||
}
|
||||
|
||||
// SystemInfo is the `system` stanza (see HostReport.System).
|
||||
|
||||
@@ -661,6 +661,9 @@ func (m *Manager) Status(ctx context.Context) *hub.WireguardStatus {
|
||||
if age, ok := parseHandshakeAge(string(out), m.now()); ok {
|
||||
st.LastHandshakeAgeS = &age
|
||||
}
|
||||
if fut, ok := handshakeFuture(string(out), m.now()); ok {
|
||||
st.HandshakeFutureS = &fut
|
||||
}
|
||||
}
|
||||
}
|
||||
if st.Pubkey == "" && !st.Registered && !st.Active {
|
||||
@@ -669,6 +672,28 @@ func (m *Manager) Status(ctx context.Context) *hub.WireguardStatus {
|
||||
return st
|
||||
}
|
||||
|
||||
// handshakeFuture (R-1204) reports how many seconds the latest handshake lies in the future, ok only when it does
|
||||
// (by more than a minute — clock jitter is not the case). parseHandshakeAge clamps that case to 0 („just now"), which
|
||||
// is what made a box whose clock had run ahead look healthy while ep0 refused its tunnel. Pinned by
|
||||
// TestR1204_AFutureHandshakeIsReported.
|
||||
func handshakeFuture(out string, now time.Time) (int64, bool) {
|
||||
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) != 2 {
|
||||
continue
|
||||
}
|
||||
epoch, err := strconv.ParseInt(f[1], 10, 64)
|
||||
if err != nil || epoch <= 0 {
|
||||
continue
|
||||
}
|
||||
if fut := epoch - now.Unix(); fut > 60 {
|
||||
return fut, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// parseHandshakeAge parses `wg show <if> latest-handshakes` output ("<peer-pub>\t<epoch>").
|
||||
// epoch 0 = no handshake yet → not ok (nil in the report; nil ≠ 0).
|
||||
func parseHandshakeAge(out string, now time.Time) (int64, bool) {
|
||||
|
||||
@@ -696,3 +696,31 @@ func TestInitialResolveFailureNoTeardown(t *testing.T) {
|
||||
t.Errorf("did not recover after DNS returned: %v", rr.calls)
|
||||
}
|
||||
}
|
||||
|
||||
// R-1204: a box that ran once with its clock ahead keeps a handshake stamped in the FUTURE — ep0 then refuses every
|
||||
// newer one. The age stays clamped at 0 (unchanged contract), and the new field says how far ahead it lies; a
|
||||
// handshake in the past (or within a minute of now) never sets it.
|
||||
// RED-PROOF: with Status not setting HandshakeFutureS the first assertion fails — seen failing 2026-10-22.
|
||||
func TestR1204_AFutureHandshakeIsReported(t *testing.T) {
|
||||
fh := &fakeHub{}
|
||||
m, _, active, _ := testManager(t, fh)
|
||||
ctx := context.Background()
|
||||
m.Apply(ctx, false, nil)
|
||||
*active = true
|
||||
m.now = func() time.Time { return time.Unix(1783107115-2*24*3600, 0) } // the clock came back two days earlier
|
||||
st := m.Status(ctx)
|
||||
if st.HandshakeFutureS == nil || *st.HandshakeFutureS != 2*24*3600 {
|
||||
t.Fatalf("handshake_future_s = %v, want %d", st.HandshakeFutureS, 2*24*3600)
|
||||
}
|
||||
if st.LastHandshakeAgeS == nil || *st.LastHandshakeAgeS != 0 {
|
||||
t.Fatalf("the age stays clamped at 0: %v", st.LastHandshakeAgeS)
|
||||
}
|
||||
m.now = func() time.Time { return time.Unix(1783107115+100, 0) }
|
||||
if st = m.Status(ctx); st.HandshakeFutureS != nil {
|
||||
t.Fatalf("a past handshake must not set handshake_future_s: %v", *st.HandshakeFutureS)
|
||||
}
|
||||
m.now = func() time.Time { return time.Unix(1783107115-30, 0) } // 30 s of jitter is not the case
|
||||
if st = m.Status(ctx); st.HandshakeFutureS != nil {
|
||||
t.Fatalf("clock jitter must not set handshake_future_s")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user