Files
wz-phone/vault/PRDs/PRD-public-stun.md
Siavash Sameni ed8a7ae5aa docs: protocol audit 2026-05-25, update architecture + Obsidian vault
Audit:
- docs/AUDIT-2026-05-25.md: full protocol audit covering 8 findings
  (4 critical, 2 high, 5 medium, 4 low) with code references and fix
  effort estimates
- vault/Audit/Tasks.md: Obsidian Tasks plugin file tracking all audit
  items with priorities, due dates, and per-step checklists

Architecture docs updated for Wire format v2 and Wave 5/6 features:
- ARCHITECTURE.md: adds wzp-video to dependency graph and project
  structure; wire format updated to v2 (16B header, 5B MiniHeader);
  relay concurrency section corrected (DashMap+RwLock is current, not
  a future optimization); test count 571→702; Android note
- PROGRESS.md: Wave 5 and Wave 6 sections appended; test count 372→702;
  current status and open blockers as of 2026-05-25
- ROAD-TO-VIDEO.md: implementation status table inserted (/🟡/🔴/🔲
  per phase); 6-step critical path to first video call
- WZP-SPEC.md: MediaHeader updated to v2 (16B byte-aligned); MiniHeader
  updated to 5B with seq_delta; codec IDs 9-12 added (H.264/H.265/AV1);
  version negotiation section added

Obsidian vault (vault/):
- 114 files across Architecture/, PRDs/, Reports/, Android/,
  Reference/, Audit/ with YAML frontmatter
- 00 - Home.md index note with wiki links
- .obsidian/app.json config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 06:00:17 +04:00

74 lines
3.8 KiB
Markdown

---
tags: [prd, wzp]
type: prd
---
# PRD: Public STUN Client
> Phase: Implemented
> Status: Done (2026-04-14)
> Crate: wzp-client
## Problem
WarzonePhone's reflexive address discovery depends entirely on relay-based `Reflect` messages over an authenticated QUIC signal channel. If the relay is unreachable, overloaded, or not yet connected, the client cannot discover its public IP:port for P2P hole-punching. This single point of failure means call setup is delayed or falls back to relay-only unnecessarily.
Tailscale solves this by querying multiple public STUN servers in parallel, independent of its DERP relay infrastructure.
## Solution
Implement a minimal RFC 5389 STUN Binding client over raw UDP that queries public STUN servers (Google, Cloudflare) in parallel. This provides:
1. **Independent reflexive discovery** — works without any relay connection
2. **Redundancy** — STUN fallback when relay reflection fails
3. **Better NAT classification** — more probes = higher confidence in Cone vs Symmetric detection
4. **Faster call setup** — STUN can run before signal registration completes
## Implementation
### New Module: `crates/wzp-client/src/stun.rs`
**Wire format** (RFC 5389):
- 20-byte header: type (u16) + length (u16) + magic cookie (0x2112A442) + transaction ID (12 bytes)
- Binding Request (0x0001): no attributes, just the header
- Binding Response (0x0101): parses XOR-MAPPED-ADDRESS (0x0020, preferred) and MAPPED-ADDRESS (0x0001, fallback)
- XOR decoding: port XOR'd with top 16 bits of magic cookie, IPv4 XOR'd with cookie, IPv6 XOR'd with cookie || txn ID
**Public API**:
- `stun_reflect(socket, server, timeout)` — single-server probe with one retry on first-packet timeout
- `discover_reflexive(config)` — parallel probe of N servers, first success wins
- `probe_stun_servers(config)` — all-server probe returning `Vec<NatProbeResult>` for NAT classification
- `resolve_stun_server(host_port)` — DNS resolution preferring IPv4
**Default servers**: `stun.l.google.com:19302`, `stun1.l.google.com:19302`, `stun.cloudflare.com:3478`
**Error handling**: `StunError` enum — Io, Timeout, Malformed, TxnMismatch, ErrorResponse, NoMappedAddress, DnsError
### Integration Points
1. **`reflect.rs`**: New `detect_nat_type_with_stun()` runs relay probes and STUN probes concurrently via `tokio::join!`, merges results, re-classifies
2. **Desktop `lib.rs`**: `try_reflect_own_addr()` falls back to `try_stun_fallback()` when relay reflection fails or times out
3. **Desktop `detect_nat_type` command**: Uses `detect_nat_type_with_stun()` for combined relay + STUN classification
### Design Decisions
- **Separate UDP socket** per STUN probe — can't share the QUIC socket (quinn owns its I/O driver)
- **No external crate** — RFC 5389 Binding is ~200 lines of code, no need for `stun-rs` or `webrtc-rs`
- **Retry once** at half-timeout — handles the "first-packet problem" where some NATs drop the initial UDP packet to a new destination
- **IPv4 preferred** for DNS resolution — Phase 7 IPv6 is still flaky
## Files
| File | Change |
|------|--------|
| `crates/wzp-client/src/stun.rs` | New — STUN client |
| `crates/wzp-client/src/lib.rs` | Add `pub mod stun` |
| `crates/wzp-client/src/reflect.rs` | Add `detect_nat_type_with_stun()` |
| `crates/wzp-client/Cargo.toml` | Add `rand` dependency |
| `desktop/src-tauri/src/lib.rs` | STUN fallback in `try_reflect_own_addr()`, STUN in `detect_nat_type` |
## Testing
- 22 unit tests: encode/decode roundtrips, XOR-MAPPED-ADDRESS (IPv4, IPv6, high port), MAPPED-ADDRESS fallback (IPv4, IPv6), unknown family, attribute padding, unknown attributes skipped, truncated attributes, error response, bad cookie, txn mismatch, too short, no mapped address, XOR preferred over mapped, error Display, default config, empty servers
- 2 integration tests (`#[ignore]`): query `stun.l.google.com`, multi-server probe