// Package poke is the agent-plane immediate-sync LISTENER (Direction-2a, // SPIKE-immediate-sync-transport-2026-07-16). It receives a CONTENTLESS UDP "sync now" poke // relayed hub → ep0 forced-command → wg0-origin → here, and fires ONE immediate desired-state // cycle (the hub control loop's out-of-band report trigger). This collapses a user-triggered // agent-plane config change (e.g. a pbsdr descriptor) from the 15-min report cycle to the // spike-measured ~0.42 s path — while the 15-min cycle remains the guarantee (a lost poke is // harmless by construction). // // The security posture is the spike's, unchanged: // - BIND CONFINEMENT: the socket binds EXCLUSIVELY to the box's WireGuard /32 (10.77.0.x from // registered.json). Never 0.0.0.0, never the LAN interface — a datagram to the LAN address // reaches no socket. The only way in is over the tunnel, and WireGuard refuses to encrypt to a // /32 no registered peer owns (spike P1 EKEYREJECTED), so a poke can only originate from ep0. // - CONTENTLESS: the payload is ignored ENTIRELY — any datagram means only "tick now". A forged // or replayed poke costs at most one extra debounced tick; it carries no config, no auth // handshake, no version. There is nothing in a poke to trust. // - DEBOUNCE: a burst of pokes within DebounceWindow yields ≤1 extra tick (leading-edge). The // downstream trigger channel is itself cap-1 coalescing, so a tick already running collapses // further pokes into one follow-up — never N queued. package poke import ( "context" "fmt" "log/slog" "net" "net/netip" "sync" "time" ) // Port is the FIXED UDP port the listener binds on the box's WG address and the hub/ep0 // forced-command targets. It is a shared cross-repo contract (documented in REUSE.md and the ep0 // felhom-poke forced-command): change it in one place → change it in all three. const Port = 51822 // DebounceWindow coalesces a burst of pokes into a single immediate tick (leading-edge). Two // distinct operator changes inside one window still converge in one tick because desired-state is // level-triggered (the tick fetches the LATEST state, not a delta). const DebounceWindow = 2 * time.Second // rebindInterval is how long the listener waits before re-attempting a bind after WG is // unregistered/down or a serve loop ends. const rebindInterval = 5 * time.Second // readBufSize bounds a single datagram read. A poke is contentless; the buffer only needs to be // large enough to drain whatever a (harmless) sender emits. const readBufSize = 512 // Listener binds the poke socket and fires the report trigger on each (debounced) poke. type Listener struct { // resolve returns the box's current WG address (ok=false until registered). Called before each // bind attempt so a late registration / rare re-address is picked up on the next cycle. resolve func() (netip.Addr, bool) // trigger is the non-blocking nudge into the hub control loop's out-of-band report channel. trigger func() port int window time.Duration logger *slog.Logger mu sync.Mutex lastFire time.Time local netip.AddrPort // set once bound (for tests / logging) boundCh chan struct{} // closed once the first bind succeeds now func() time.Time } // NewListener builds a listener. port<=0 uses the fixed Port. A nil trigger is a no-op (safe). func NewListener(resolve func() (netip.Addr, bool), trigger func(), port int, logger *slog.Logger) *Listener { if logger == nil { logger = slog.Default() } if trigger == nil { trigger = func() {} } if port <= 0 { port = Port } return &Listener{ resolve: resolve, trigger: trigger, port: port, window: DebounceWindow, logger: logger, boundCh: make(chan struct{}), now: time.Now, } } // onPoke applies the leading-edge debounce and fires the trigger at most once per window. Returns // true iff it fired (the test hook for the debounce red-proof). func (l *Listener) onPoke() bool { l.mu.Lock() now := l.now() if !l.lastFire.IsZero() && now.Sub(l.lastFire) < l.window { l.mu.Unlock() return false } l.lastFire = now l.mu.Unlock() l.trigger() return true } // Run resolves the WG address and serves pokes until ctx is cancelled, rebinding across a WG // down/re-address. It NEVER returns nil early (the daemon treats a returned goroutine as a reason // to exit) — only ctx cancellation ends it. func (l *Listener) Run(ctx context.Context) error { for { if err := ctx.Err(); err != nil { return err } addr, ok := l.resolve() if !ok { l.logger.Debug("poke: no WG address yet (box not registered) — will retry") if !l.sleep(ctx, rebindInterval) { return ctx.Err() } continue } if err := l.serve(ctx, addr); err != nil && ctx.Err() == nil { l.logger.Warn("poke: listener stopped; will rebind", "err", err, "wg_addr", addr.String()) if !l.sleep(ctx, rebindInterval) { return ctx.Err() } } } } // serve binds EXCLUSIVELY to addr:port and delivers each datagram to onPoke until ctx or a read // error ends it. func (l *Listener) serve(ctx context.Context, addr netip.Addr) error { bind := netip.AddrPortFrom(addr, uint16(l.port)).String() var lc net.ListenConfig pc, err := lc.ListenPacket(ctx, "udp", bind) if err != nil { return fmt.Errorf("bind %s: %w", bind, err) } defer pc.Close() l.mu.Lock() if ap, ok := netip.ParseAddrPort(pc.LocalAddr().String()); ok == nil { l.local = ap } select { case <-l.boundCh: // already closed default: close(l.boundCh) } l.mu.Unlock() l.logger.Info("poke: listening for hub sync-pokes (WG-confined, contentless)", "addr", pc.LocalAddr().String()) // Unblock ReadFrom on shutdown. go func() { <-ctx.Done() pc.Close() }() buf := make([]byte, readBufSize) for { _, from, err := pc.ReadFrom(buf) if err != nil { if ctx.Err() != nil { return ctx.Err() } return fmt.Errorf("read: %w", err) } // Contentless: the payload is discarded. ANY datagram = "tick now". if l.onPoke() { l.logger.Info("poke received → triggering an immediate desired-state cycle", "from", from.String()) } else { l.logger.Debug("poke received but coalesced within the debounce window", "from", from.String()) } } } // BoundAddrPort waits (bounded by ctx) until the socket is bound and returns its address. Test // helper; production never needs it. func (l *Listener) BoundAddrPort(ctx context.Context) (netip.AddrPort, bool) { select { case <-l.boundCh: l.mu.Lock() defer l.mu.Unlock() return l.local, true case <-ctx.Done(): return netip.AddrPort{}, false } } func (l *Listener) sleep(ctx context.Context, d time.Duration) bool { t := time.NewTimer(d) defer t.Stop() select { case <-ctx.Done(): return false case <-t.C: return true } }