app: nat.stun_5780 — NAT mapping/filtering discovery, verified vs live server

Hand-rolled RFC 5389/5780 STUN client (stdlib only) that exercises the
server's stun-5780 capability: one socket, three binding requests
(primary, OTHER-ADDRESS alternate IP, CHANGE-REQUEST port) — the
comparison classifies NAT mapping and filtering behavior.

Verified on the OnePlus: local 10.13.102.124 -> mapped
178.191.120.247:53259 (behind_nat true), alternate address answered from
the server's second IP, mapping endpoint-independent, filtering
address/port-dependent. Finding nat.symmetric (medium) for the
P2P-hostile case.

Two real bugs found by running it: port preservation was misread as "no
NAT" (compare addresses, not ports), and an unbound socket reports the
wildcard local address (resolve via a throwaway connected socket).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 09:15:29 +02:00
co-authored by Claude Opus 5
parent dd9ecf5032
commit 483de5ca54
4 changed files with 675 additions and 0 deletions
+16
View File
@@ -414,3 +414,19 @@ Ran the app on the OnePlus with Shizuku running (`echolot-app/reports/CPH2747-ap
Both privilege tiers now work end to end in the production app on real hardware, alongside the
canary-DNS loop against the live server. 6 tests/run: link.snapshot, icmp.ping4, icmp.ping6,
net.captive_portal, dns.canary, link.ip_monitor(shizuku).
## App: nat.stun_5780 — NAT behavior discovery verified against the live server (2026-08-01)
Client-side RFC 5389/5780 STUN (hand-rolled, stdlib only) exercising the server's `stun-5780`
capability. Three binding requests from ONE socket: primary, the server's OTHER-ADDRESS
(alternate IP), and CHANGE-REQUEST(port). Verified on the OnePlus
(`echolot-app/reports/CPH2747-app-run4-stun.json`):
- local `10.13.102.124` → mapped `178.191.120.247:53259`, `behind_nat: true`
- `other_address 89.185.109.151:3479` — the server's second IP answered, so RFC 5780 works
end to end (client ↔ our own STUN implementation)
- **mapping: endpoint-independent** (same external port toward a different destination → P2P
friendly); **filtering: address/port-dependent** (no reply to CHANGE-REQUEST → unsolicited
inbound is dropped). Classic full-cone-mapping + port-restricted-filtering NAT.
Finding wired: `nat.symmetric` (medium) when mapping is address/port-dependent.
Two bugs caught by running it for real: port preservation was misread as "no NAT" (now compares
ADDRESSES), and an unbound socket reports the wildcard as its local address (now resolved via a
throwaway connected socket). 7 tests/run.