swactor/crates/simulation/HOLE_PUNCH_FINDINGS.md

77 lines
3.9 KiB
Markdown
Raw Normal View History

2026-05-25 10:33:35 +00:00
# Hole-punching never succeeded — N3 bundle re-analysis
Date: 2026-05-22. Bundles: `vastai-N3-1`, `vastai-N3-2`, `vastai-N3-stub` from
`docean:/var/lib/swactor-diag/bundles/`.
## Finding
**No node ever held a stable direct path.** Across the 430 s N3-stub run,
every directed edge was `Relay` in every snapshot. The single non-Relay
sighting in all three bundles was stage-0→orchestrator in N3-1 going
`Relay → Mixed → Relay` over a ~24 s window, then collapsing; the reverse
direction never saw Mixed, so it was likely a transient asymmetric reading,
not a real two-way direct path. **Stage↔stage was 100% Relay in every run.**
## Suspected cause: symmetric (endpoint-dependent) NAT on vast.ai
Last-snapshot iroh socket counters, N3-stub:
| node | holepunch_attempts | paths_direct | send_ipv4 | **recv_data_ipv4** |
|--------------|-------------------:|-------------:|----------:|-------------------:|
| orchestrator | 0 | 0 | 199 072 | **0** |
| stage-0 | 3 220 | 0 | 255 282 | **0** |
| stage-1 | 1 072 | 0 | 152 515 | **0** |
| stage-2 | 3 068 | 0 | 241 752 | **0** |
Cluster-wide: ~7 360 hole-punch attempts, ~850 K direct-path datagrams sent,
**zero received**. `actor_tick_direct_addr_heartbeat = 0` everywhere (the
direct-path keepalive never ticked, because no direct path ever validated).
`portmap.upnp_available = 0`, `pcp_available = 0`, `mapping_failures ≈
mapping_attempts` on every stage — no NAT control protocol reachable inside
vast.ai containers, so iroh can't request a stable external port.
Ruled out as causes:
- **Address discovery worked.** `net_report.reports_full ≥ 1` on every node;
thousands of hole-punch attempts means stages had remote candidates from
the relay's signalling path.
- **UDP isn't blocked.** Every node's `udp_echo` probes to the VPS at
146.190.110.128:9081 had 100% reply success. Outbound delivers; return on a
pre-opened mapping delivers.
- **Relay path is fine.** All 8 `DialOutcome` events in N3-stub are `Success`
in 1.2–1.9 s. iroh just never upgrades from Relay to Direct.
The signature — outbound delivers, return-on-existing-mapping delivers,
return-on-newly-punched-port never delivers — is the classic
endpoint-dependent-mapping fingerprint. vast.ai's container egress NAT
appears to pick a different external source port per destination, so the
candidate stage-A learned about stage-B (the port B used reflecting off the
VPS) is not the port B uses sending to A.
## Implication
Between vast.ai stages, Direct is structurally unreliable on this provider.
The relay is the path, not a fallback. Provision relay bandwidth/headroom
accordingly and size SWIM timeouts around relay RTT.
## Data gaps — what would convert inference to proof
1. **`Tier2Peer.direct_addresses` in snapshots.** Today the peer object
carries only `conn_type` and `relay_urls`. Adding iroh's
`direct_addresses` list (from `Endpoint::remote_info()`) would let us see
which candidates each side learned for each peer, instead of inferring
"they had some" from `holepunch_attempts > 0`.
2. **UDP-echo source-port reflection.** Have the collector's UDP echo include
the observed `srcAddr:srcPort` in its reply (currently opaque). Probe from
each node to two collector destinations and compare external ports — same
port = endpoint-independent, different = symmetric. One number per node
would answer the NAT-type question definitively rather than by signature.
3. **Per-edge bandwidth as a packaged metric.** Today reconstructed post-hoc
by summing `MessageSent.size`/`MessageReceived.size` per `(local, peer)`.
Either ship `Tier2Peer { bytes_sent_total, bytes_received_total }` (small
per-peer accumulator on the aggregator; hooks exist in `iroh_driver.rs`),
or document the post-hoc derivation so future analyses don't re-discover
it.