You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Expected: to resist port-based blocking, the active port(s) change over time on a schedule both ends derive independently — so a blocklist of "the subnetra ports" goes stale.
Actual: after the multi-port PR a node binds an explicit, static set (default [18020, 18023, 18026]). That already reduces single-port blast radius and drops the 51820 fingerprint, but the set is fixed for the process lifetime — once enumerated it is a stable target. There is no port hopping.
Implementation Path (design sketch — static-config only, no handshake)
Time-synced port schedule (no negotiation). Derive the active port(s) for slot t from a per-link secret already shared, e.g. port = base + (Blake2b(link_key, floor(now/slot)) mod span) over a configured candidate space — both ends compute it from the wall clock + link_key, so there is no on-wire exchange (iron law [test] Enable the gated multi-point + relay e2e netns test #8). All inputs are static config (hop_slot_secs, candidate range/list, enable flag).
Receiver coverage during slot edges. To tolerate clock skew, bind the current AND adjacent slot's port(s) (a small window), reusing the multi-port socket set already added here. Egress targets the peer's current-slot port; the listen side already returns out the socket a peer was last heard on.
Interaction with NAT. A NATed spoke's source port is NAT-mapped anyway; hopping primarily helps the hub's reachable port and spoke→hub destination. Scope carefully (hub-side hop most valuable).
Counters/observability:udp_port_hop transitions; subnetra status shows the current slot's port(s).
Explicit candidate space, no unbounded range scan (consistent with the "list ports explicitly" decision in the multi-port PR — the schedule may index a configured span, but the bind set per slot stays small and bounded).
Two nodes with hopping enabled stay connected across ≥3 slot transitions with no manual reconfig and no on-wire schedule exchange; zig build + zig build test green; PROTOCOL.md updated.
Hopping OFF leaves today's static listen_ports behaviour byte-identical.
Blast Radius
Affected paths: deployments facing active port-based blocking/enumeration.
Severity rationale: medium — meaningful censorship-resistance upgrade, but adds clock-sync coupling and complexity; a small-scale-network posture may prefer the simpler static set.
Priority: P2 (depends on multi-port listen, which this builds on).
Context Boundary
jamiesun/subnetra—reactor.zig(listen-socket set, egress socket selection),config.zig(listen_ports),crypto.zig(KDF —deriveLinkKey/Blake2b). Docs:configuration/reference.md,docs/PROTOCOL.md.feat/multi-port.AGENT.mdiron law [test] Enable the gated multi-point + relay e2e netns test #8 (handshake-free; transport mode chosen by static per-link config, never on-wire negotiation), iron law [reactor] Task 6: epoll edge-triggered run loop is stubbed — daemon moves no packets #2 (zero data-plane allocation), iron law [config] Task 2: config.json is never parsed (Config.fromJson stubbed) #3 (single-threaded reactor). Builds on multi-port listen (this PR) and header obfuscation (#PR-144 /docs/PROTOCOL.md§3.4).listen_portsset. Distinct from [transport] Peer-side endpoint failover: spokes can't move to an alternate hub endpoint (multi-port LISTEN shipped; failover needs a trigger) #145 (peer endpoint failover) and the TCP/QUIC fallback issue.Expectation vs. Reality
Expected: to resist port-based blocking, the active port(s) change over time on a schedule both ends derive independently — so a blocklist of "the subnetra ports" goes stale.
Actual: after the multi-port PR a node binds an explicit, static set (default
[18020, 18023, 18026]). That already reduces single-port blast radius and drops the 51820 fingerprint, but the set is fixed for the process lifetime — once enumerated it is a stable target. There is no port hopping.Implementation Path (design sketch — static-config only, no handshake)
tfrom a per-link secret already shared, e.g.port = base + (Blake2b(link_key, floor(now/slot)) mod span)over a configured candidate space — both ends compute it from the wall clock +link_key, so there is no on-wire exchange (iron law [test] Enable the gated multi-point + relay e2e netns test #8). All inputs are static config (hop_slot_secs, candidate range/list, enable flag).udp_port_hoptransitions;subnetra statusshows the current slot's port(s).Constraints / Non-Goals
key_id/epoch).Acceptance Criteria
zig build+zig build testgreen;PROTOCOL.mdupdated.listen_portsbehaviour byte-identical.Blast Radius