docs: add PRDs for Phase 8 Tailscale-inspired features
5 new PRDs: - PRD-public-stun.md — RFC 5389 STUN client - PRD-portmap.md — NAT-PMP/PCP/UPnP port mapping - PRD-ice-regather.md — Mid-call ICE re-gathering - PRD-netcheck.md — Network diagnostic - PRD-relay-selection.md — Region-based relay selection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
116
docs/PRD-ice-regather.md
Normal file
116
docs/PRD-ice-regather.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# PRD: Mid-Call ICE Re-Gathering
|
||||
|
||||
> Phase: Implemented (signal plane); transport hot-swap deferred
|
||||
> Status: Partial (2026-04-14)
|
||||
> Crate: wzp-client, wzp-proto, wzp-relay
|
||||
|
||||
## Problem
|
||||
|
||||
When a mobile device transitions between networks (WiFi -> cellular, IP address change), the active QUIC connection dies. The call stays on a dead path until timeout, then the user experiences silence. There is no mechanism to re-discover candidates and re-establish a direct path mid-call.
|
||||
|
||||
Android's `NetworkMonitor.onIpChanged` already fires on `onLinkPropertiesChanged`, but nothing consumes it for candidate re-gathering or path migration.
|
||||
|
||||
## Solution
|
||||
|
||||
Implement an `IceAgent` that manages the full candidate lifecycle — initial gathering, mid-call re-gathering on network change, and peer candidate application. A new `CandidateUpdate` signal message carries refreshed candidates to the peer through the relay.
|
||||
|
||||
## Implementation
|
||||
|
||||
### New Module: `crates/wzp-client/src/ice_agent.rs`
|
||||
|
||||
**IceAgent struct**:
|
||||
- Owns `IceAgentConfig` (STUN config, portmap toggle, gather timeout, local ports)
|
||||
- Monotonic `generation: AtomicU32` — incremented on each re-gather, peers reject stale updates
|
||||
- `peer_generation: AtomicU32` — tracks last-seen peer generation for ordering
|
||||
|
||||
**Public API**:
|
||||
- `gather()` -> `CandidateSet` — runs STUN + portmap + host candidates in parallel with timeout
|
||||
- `re_gather()` -> `(CandidateSet, SignalMessage)` — increments generation, returns update to send
|
||||
- `apply_peer_update(signal)` -> `Option<PeerCandidates>` — parses `CandidateUpdate`, rejects if generation <= last-seen
|
||||
|
||||
**CandidateSet**:
|
||||
```rust
|
||||
pub struct CandidateSet {
|
||||
pub reflexive: Option<SocketAddr>,
|
||||
pub local: Vec<SocketAddr>,
|
||||
pub mapped: Option<SocketAddr>,
|
||||
pub generation: u32,
|
||||
}
|
||||
```
|
||||
|
||||
### New Signal: `CandidateUpdate`
|
||||
|
||||
```rust
|
||||
CandidateUpdate {
|
||||
call_id: String,
|
||||
reflexive_addr: Option<String>,
|
||||
local_addrs: Vec<String>,
|
||||
mapped_addr: Option<String>,
|
||||
generation: u32,
|
||||
}
|
||||
```
|
||||
|
||||
- All address fields use `#[serde(default, skip_serializing_if)]` for backward compat
|
||||
- Generation counter is mandatory — prevents stale updates from network reordering
|
||||
|
||||
### Relay Forwarding
|
||||
|
||||
`CandidateUpdate` is forwarded to the call peer using the same pattern as `MediaPathReport`:
|
||||
1. Look up peer fingerprint + `peer_relay_fp` from `CallRegistry`
|
||||
2. If cross-relay: wrap in `FederatedSignalForward` and forward via federation link
|
||||
3. If local: send via `signal_hub.send_to()`
|
||||
|
||||
### Desktop Handling
|
||||
|
||||
Signal recv loop handles `CandidateUpdate`:
|
||||
- Logs generation, reflexive, mapped, local count
|
||||
- Emits `recv:CandidateUpdate` debug event
|
||||
- Emits `signal-event` type `candidate_update` to JS frontend
|
||||
- TODO: wire into `IceAgent.apply_peer_update()` + `race_upgrade()` for transport hot-swap
|
||||
|
||||
### Deferred: Transport Hot-Swap
|
||||
|
||||
The actual mid-call transport replacement is not yet wired. The designed approach:
|
||||
- `Arc<RwLock<Arc<QuinnTransport>>>` — send/recv tasks clone inner Arc per frame
|
||||
- On upgrade, swap inner Arc under write lock — next frame picks up new transport
|
||||
- Android: `pending_ice_regather: AtomicBool` polled in recv task, triggers re-gather + swap
|
||||
- Requires live testing to validate seamless audio continuity during swap
|
||||
|
||||
## Signal Flow
|
||||
|
||||
```
|
||||
Network change (WiFi -> cellular)
|
||||
|
|
||||
v
|
||||
IceAgent::re_gather()
|
||||
|-- stun::discover_reflexive()
|
||||
|-- portmap::acquire_port_mapping()
|
||||
|-- local_host_candidates()
|
||||
|
|
||||
v
|
||||
SignalMessage::CandidateUpdate { generation: N+1 }
|
||||
|
|
||||
v (via relay)
|
||||
Peer IceAgent::apply_peer_update()
|
||||
|
|
||||
v
|
||||
PeerCandidates { reflexive, local, mapped }
|
||||
|
|
||||
v
|
||||
dual_path::race() with new candidates [NOT YET WIRED]
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `crates/wzp-client/src/ice_agent.rs` | New — IceAgent + CandidateSet |
|
||||
| `crates/wzp-proto/src/packet.rs` | `CandidateUpdate` variant |
|
||||
| `crates/wzp-relay/src/main.rs` | Forward `CandidateUpdate` to peer |
|
||||
| `crates/wzp-client/src/featherchat.rs` | Map `CandidateUpdate` to `IceCandidate` type |
|
||||
| `desktop/src-tauri/src/lib.rs` | Handle `CandidateUpdate` in signal recv loop |
|
||||
|
||||
## Testing
|
||||
|
||||
- 10 unit tests: generation monotonicity, apply_peer_update (all fields, empty fields, unparseable addrs, stale rejection, wrong signal type), default config, gather with no STUN, re_gather produces signal with incrementing generation
|
||||
- 2 protocol roundtrip tests: CandidateUpdate full + minimal
|
||||
75
docs/PRD-netcheck.md
Normal file
75
docs/PRD-netcheck.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# PRD: Network Diagnostic (Netcheck)
|
||||
|
||||
> Phase: Implemented
|
||||
> Status: Done (2026-04-14)
|
||||
> Crate: wzp-client
|
||||
|
||||
## Problem
|
||||
|
||||
When P2P connections fail or call quality is poor, there is no diagnostic tool to understand why. Users and developers must manually probe STUN, check NAT type, test relay connectivity, and verify port mapping support — all separately. Tailscale's `netcheck` consolidates all of this into a single diagnostic report.
|
||||
|
||||
## Solution
|
||||
|
||||
A comprehensive `run_netcheck()` function that probes all network capabilities in parallel and produces a structured `NetcheckReport`. Exposed as a CLI subcommand (`wzp-client --netcheck`) and available for in-app diagnostics.
|
||||
|
||||
## Implementation
|
||||
|
||||
### New Module: `crates/wzp-client/src/netcheck.rs`
|
||||
|
||||
**NetcheckReport**:
|
||||
```rust
|
||||
pub struct NetcheckReport {
|
||||
pub nat_type: NatType,
|
||||
pub reflexive_addr: Option<String>,
|
||||
pub ipv4_reachable: bool,
|
||||
pub ipv6_reachable: bool,
|
||||
pub hairpin_works: Option<bool>,
|
||||
pub port_mapping: Option<PortMapProtocol>,
|
||||
pub relay_latencies: Vec<RelayLatency>,
|
||||
pub preferred_relay: Option<String>,
|
||||
pub stun_latency_ms: Option<u32>,
|
||||
pub upnp_available: bool,
|
||||
pub pcp_available: bool,
|
||||
pub nat_pmp_available: bool,
|
||||
pub gateway: Option<String>,
|
||||
pub duration_ms: u32,
|
||||
pub stun_probes: Vec<NatProbeResult>,
|
||||
}
|
||||
```
|
||||
|
||||
**Probes (all parallel via `tokio::join!`)**:
|
||||
1. **STUN probes** — `probe_stun_servers()` to all configured STUN servers
|
||||
2. **Relay latencies** — `probe_reflect_addr()` to each configured relay
|
||||
3. **Port mapping** — `acquire_port_mapping()` to detect NAT-PMP/PCP/UPnP
|
||||
4. **Gateway** — `default_gateway()` for the router address
|
||||
5. **IPv6** — attempt to bind `[::]:0` and send to an IPv6 STUN server
|
||||
|
||||
**Derived fields**:
|
||||
- `nat_type` / `reflexive_addr` — from `classify_nat()` on STUN probes
|
||||
- `ipv4_reachable` — true if any STUN probe succeeded
|
||||
- `preferred_relay` — relay with lowest RTT
|
||||
- `port_mapping` / `nat_pmp_available` / `pcp_available` / `upnp_available` — from portmap result
|
||||
|
||||
**Human-readable output**: `format_report()` produces a formatted text report with sections for NAT info, port mapping, STUN probes, relay latencies.
|
||||
|
||||
### CLI Integration
|
||||
|
||||
`wzp-client --netcheck <relay-addr>` — runs the diagnostic using the specified relay plus default STUN servers, prints the report, and exits.
|
||||
|
||||
### Deferred
|
||||
|
||||
- **Hairpin test** — send packet from shared endpoint to own reflexive addr to test NAT hairpinning. Architecture is in place (`hairpin_works: Option<bool>`) but the actual probe is not yet implemented.
|
||||
- **Android/Desktop in-app UI** — expose via JNI (Android) and Tauri command (desktop) for user-facing diagnostics.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `crates/wzp-client/src/netcheck.rs` | New — NetcheckReport + run_netcheck + format_report |
|
||||
| `crates/wzp-client/src/lib.rs` | Add `pub mod netcheck` |
|
||||
| `crates/wzp-client/src/cli.rs` | `--netcheck` flag + handler |
|
||||
|
||||
## Testing
|
||||
|
||||
- 5 unit tests: default config, report JSON serialization + roundtrip, RelayLatency serialization, format_report with empty relays, format_report with full data (STUN probes, relay latencies, preferred relay, port mapping)
|
||||
- 1 integration test (`#[ignore]`): full netcheck run
|
||||
92
docs/PRD-portmap.md
Normal file
92
docs/PRD-portmap.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# PRD: NAT Port Mapping (PCP/PMP/UPnP)
|
||||
|
||||
> Phase: Implemented
|
||||
> Status: Done (2026-04-14)
|
||||
> Crate: wzp-client, wzp-proto, wzp-relay
|
||||
|
||||
## Problem
|
||||
|
||||
WarzonePhone falls back to relay-only when the client is behind a symmetric NAT (different external port per destination). The STUN-discovered reflexive address won't match what a peer sees, so direct hole-punching fails. Tailscale reports ~70% of consumer routers support NAT-PMP, PCP, or UPnP — protocols that let clients request explicit port mappings, making symmetric NATs traversable.
|
||||
|
||||
## Solution
|
||||
|
||||
Implement all three port mapping protocols, tried in sequence (NAT-PMP -> PCP -> UPnP). When a mapping is acquired, advertise the mapped address as a new candidate type alongside reflexive and host candidates. The relay cross-wires it into `CallSetup.peer_mapped_addr` so the peer can dial it.
|
||||
|
||||
## Implementation
|
||||
|
||||
### New Module: `crates/wzp-client/src/portmap.rs`
|
||||
|
||||
**NAT-PMP (RFC 6886)**:
|
||||
- UDP to gateway:5351
|
||||
- External address request (opcode 0) -> returns router's public IP
|
||||
- Map UDP request (opcode 1) -> returns mapped external port + lifetime
|
||||
- 12-byte request, 16-byte response
|
||||
|
||||
**PCP (RFC 6887)**:
|
||||
- Same gateway:5351, version 2
|
||||
- MAP opcode with client IP as IPv4-mapped IPv6
|
||||
- 60-byte request/response with 12-byte nonce for anti-spoofing
|
||||
- Superset of NAT-PMP, supports IPv6
|
||||
|
||||
**UPnP IGD**:
|
||||
- SSDP M-SEARCH to 239.255.255.250:1900 for InternetGatewayDevice discovery
|
||||
- Parse LOCATION header -> fetch device description XML -> find WANIPConnection controlURL
|
||||
- SOAP `GetExternalIPAddress` -> router's public IP
|
||||
- SOAP `AddPortMapping` -> maps the QUIC port
|
||||
|
||||
**Gateway discovery**:
|
||||
- macOS: `route -n get default` (parse `gateway:` line)
|
||||
- Linux/Android: `/proc/net/route` (parse hex gateway for 00000000 destination)
|
||||
|
||||
**Public API**:
|
||||
- `acquire_port_mapping(internal_port, local_ip)` -> tries all 3, first success wins
|
||||
- `release_port_mapping(mapping)` -> best-effort cleanup (lifetime=0 for NAT-PMP)
|
||||
- `spawn_refresh(mapping)` -> background task renewing at half-lifetime
|
||||
- `default_gateway()` -> cross-platform gateway discovery
|
||||
|
||||
### Signal Protocol Extensions
|
||||
|
||||
| Message | New Field | Purpose |
|
||||
|---------|-----------|---------|
|
||||
| `DirectCallOffer` | `caller_mapped_addr: Option<String>` | Caller's port-mapped address |
|
||||
| `DirectCallAnswer` | `callee_mapped_addr: Option<String>` | Callee's port-mapped address |
|
||||
| `CallSetup` | `peer_mapped_addr: Option<String>` | Relay cross-wires peer's mapped addr |
|
||||
|
||||
All fields use `#[serde(default, skip_serializing_if)]` for backward compatibility.
|
||||
|
||||
### Relay Cross-Wiring
|
||||
|
||||
`CallRegistry` extended with `caller_mapped_addr` / `callee_mapped_addr` fields + setter methods. The relay:
|
||||
1. Extracts `caller_mapped_addr` from `DirectCallOffer`, stores in registry
|
||||
2. Extracts `callee_mapped_addr` from `DirectCallAnswer`, stores in registry
|
||||
3. Cross-wires into `CallSetup`: caller gets callee's mapped addr as `peer_mapped_addr`, and vice versa
|
||||
|
||||
### Candidate Priority
|
||||
|
||||
`PeerCandidates.mapped` added to `dual_path.rs`. Dial order:
|
||||
1. Host (LAN) candidates — fastest on same-LAN
|
||||
2. **Port-mapped** — stable even behind symmetric NATs
|
||||
3. Server-reflexive (STUN) — standard hole-punching
|
||||
4. Relay — always-available fallback
|
||||
|
||||
### Desktop Integration
|
||||
|
||||
Both `place_call()` and `answer_call()` call `acquire_port_mapping()` using the signal endpoint's local port. Privacy-mode answers (`AcceptGeneric`) skip portmap to keep the address hidden.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `crates/wzp-client/src/portmap.rs` | New — NAT-PMP/PCP/UPnP client |
|
||||
| `crates/wzp-client/src/dual_path.rs` | `PeerCandidates.mapped` field + dial_order update |
|
||||
| `crates/wzp-proto/src/packet.rs` | `caller/callee_mapped_addr` + `peer_mapped_addr` fields |
|
||||
| `crates/wzp-relay/src/call_registry.rs` | `caller/callee_mapped_addr` fields + setters |
|
||||
| `crates/wzp-relay/src/main.rs` | Extract, store, cross-wire mapped addrs |
|
||||
| `desktop/src-tauri/src/lib.rs` | Call portmap in place_call/answer_call |
|
||||
|
||||
## Testing
|
||||
|
||||
- 18 unit tests: NAT-PMP encoding, UPnP XML parsing (5 variants including real-world router XML), URL host extraction, error Display, protocol serde, PortMapping serialization, gateway detection, constants verification
|
||||
- 2 integration tests (`#[ignore]`): gateway discovery, acquire_mapping
|
||||
- 9 PeerCandidates tests: dial_order with all types, dedup, is_empty edge cases
|
||||
- 12 protocol roundtrip tests: offer/answer/setup with mapped addr, backward compat without
|
||||
68
docs/PRD-public-stun.md
Normal file
68
docs/PRD-public-stun.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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
|
||||
88
docs/PRD-relay-selection.md
Normal file
88
docs/PRD-relay-selection.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# PRD: Region-Based Relay Selection
|
||||
|
||||
> Phase: Implemented (data model)
|
||||
> Status: Done (2026-04-14)
|
||||
> Crate: wzp-client, wzp-proto, wzp-relay
|
||||
|
||||
## Problem
|
||||
|
||||
Clients are configured with a single relay address. With multiple relays in the federation mesh, the client should automatically discover all available relays and select the lowest-latency one. Currently there is no mechanism for the relay to advertise its mesh peers to clients, and no client-side data structure to track relay health over time.
|
||||
|
||||
## Solution
|
||||
|
||||
1. Relays advertise their region and mesh peers in `RegisterPresenceAck`
|
||||
2. Clients maintain a `RelayMap` sorted by measured RTT
|
||||
3. `preferred()` returns the best relay for call setup
|
||||
|
||||
## Implementation
|
||||
|
||||
### New Module: `crates/wzp-client/src/relay_map.rs`
|
||||
|
||||
**RelayEntry**:
|
||||
```rust
|
||||
pub struct RelayEntry {
|
||||
pub name: String,
|
||||
pub addr: SocketAddr,
|
||||
pub region: Option<String>,
|
||||
pub rtt_ms: Option<u32>,
|
||||
pub last_probed: Option<Instant>,
|
||||
pub reachable: bool,
|
||||
}
|
||||
```
|
||||
|
||||
**RelayMap API**:
|
||||
- `upsert(name, addr, region)` — add or update a relay entry
|
||||
- `update_rtt(addr, rtt_ms)` — record probe result, marks reachable, re-sorts
|
||||
- `mark_unreachable(addr)` — sorts unreachable entries to end
|
||||
- `preferred()` -> `Option<&RelayEntry>` — lowest RTT reachable relay
|
||||
- `populate_from_ack(relays, region)` — parse `RegisterPresenceAck.available_relays` (format: `"name|addr"`)
|
||||
- `needs_reprobe(max_age)` — true if any entry has stale or missing probe
|
||||
- `stale_entries(max_age)` — list of entries needing fresh probes
|
||||
|
||||
### Signal Protocol Extension
|
||||
|
||||
`RegisterPresenceAck` extended:
|
||||
```rust
|
||||
RegisterPresenceAck {
|
||||
success: bool,
|
||||
error: Option<String>,
|
||||
relay_build: Option<String>,
|
||||
relay_region: Option<String>, // NEW
|
||||
available_relays: Vec<String>, // NEW — "name|addr" format
|
||||
}
|
||||
```
|
||||
|
||||
### Relay Config Extension
|
||||
|
||||
`RelayConfig` extended:
|
||||
```rust
|
||||
pub region: Option<String>, // e.g., "us-east", "eu-west"
|
||||
pub advertised_addr: Option<SocketAddr>, // for available_relays population
|
||||
```
|
||||
|
||||
### Relay Population
|
||||
|
||||
On `RegisterPresenceAck`, the relay populates:
|
||||
- `relay_region` from `config.region`
|
||||
- `available_relays` from `config.peers` (label|url format)
|
||||
|
||||
### Deferred
|
||||
|
||||
- **Automatic relay switching** — using `preferred()` to select relay during call setup instead of hardcoded config
|
||||
- **Background reprobing** — periodic RTT measurements to keep the relay map fresh
|
||||
- **Cross-relay RTT estimation** — using mesh probe data to estimate combined caller-RTT + callee-RTT for optimal relay placement
|
||||
|
||||
## Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `crates/wzp-client/src/relay_map.rs` | New — RelayMap + RelayEntry |
|
||||
| `crates/wzp-client/src/lib.rs` | Add `pub mod relay_map` |
|
||||
| `crates/wzp-proto/src/packet.rs` | `relay_region` + `available_relays` on RegisterPresenceAck |
|
||||
| `crates/wzp-relay/src/config.rs` | `region` + `advertised_addr` fields |
|
||||
| `crates/wzp-relay/src/main.rs` | Populate RegisterPresenceAck from config + peers |
|
||||
|
||||
## Testing
|
||||
|
||||
- 15 unit tests: preferred by RTT, unreachable not preferred, preferred empty/all-unreachable, populate_from_ack (valid + malformed entries), upsert updates/preserves region, needs_reprobe (empty/never/fresh), stale_entries, sort stability with equal RTT, mark_unreachable sorts to end, RelayEntry serialization
|
||||
- 2 protocol tests: RegisterPresenceAck roundtrip with new fields, backward compat without new fields
|
||||
Reference in New Issue
Block a user