feat(p2p): Phase 6 — ICE-style path negotiation
Before Phase 6, each side's dual-path race ran independently and
committed to whichever transport completed first. When one side
picked Direct and the other picked Relay, they sent media to
different places — TX > 0 RX: 0 on both, completely silent call.
Phase 6 adds a negotiation step: after the local race completes,
each side sends a MediaPathReport { call_id, direct_ok, winner }
to the peer through the relay. Both wait for the other's report
before committing a transport to the CallEngine. The decision
rule is simple: if BOTH report direct_ok = true, use direct; if
EITHER reports false, BOTH use relay.
## Wire protocol
New `SignalMessage::MediaPathReport { call_id, direct_ok,
race_winner }`. The relay forwards it to the call peer via the
same signal_hub routing used for DirectCallOffer/Answer. The
cross-relay dispatcher also forwards it.
## dual_path::race restructured
Returns `RaceResult` instead of `(Arc<QuinnTransport>, WinningPath)`:
- `direct_transport: Option<Arc<QuinnTransport>>`
- `relay_transport: Option<Arc<QuinnTransport>>`
- `local_winner: WinningPath`
Both paths are run as spawned tasks. After the first completes,
a 1s grace period lets the loser also finish. The connect
command gets BOTH transports (when available) and picks the
right one based on the negotiation outcome. The unused transport
is dropped.
## connect command flow (revised)
1. Run race() → RaceResult with both transports
2. Send MediaPathReport to relay with our direct_ok
3. Install oneshot; wait for peer's report (3s timeout)
4. Decision: both direct_ok → use direct; else → use relay
5. Start CallEngine with the agreed transport
If the peer never responds (old build, timeout), falls back to
relay — backward compatible.
## Relay forwarding
MediaPathReport is forwarded like DirectCallOffer/Answer: via
signal_hub.send_to(peer_fp) for same-relay calls, and via
cross-relay dispatcher for federated calls.
## Debug log events
- `connect:dual_path_race_done` — local race result
- `connect:path_report_sent` — our report to the peer
- `connect:peer_report_received` — peer's report
- `connect:peer_report_timeout` — peer didn't respond (3s)
- `connect:path_negotiated` — final agreed path with reasons
Full workspace test: 423 passing (no regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -846,6 +846,31 @@ pub enum SignalMessage {
|
||||
observed_addr: String,
|
||||
},
|
||||
|
||||
// ── Phase 6: ICE-style path negotiation ─────────────────────
|
||||
|
||||
/// Phase 6: each side reports the result of its local dual-
|
||||
/// path race to the other side through the relay. Both peers
|
||||
/// send this after their race completes; both wait for the
|
||||
/// other's report before committing a transport to the
|
||||
/// CallEngine.
|
||||
///
|
||||
/// The decision rule is: if BOTH sides report `direct_ok =
|
||||
/// true`, use the direct P2P connection. If EITHER reports
|
||||
/// `direct_ok = false`, BOTH fall back to relay. This
|
||||
/// eliminates the race condition where one side picks Direct
|
||||
/// and the other picks Relay — they now agree on the path
|
||||
/// before any media flows.
|
||||
MediaPathReport {
|
||||
call_id: String,
|
||||
/// Did the direct QUIC connection (P2P dial or accept)
|
||||
/// complete successfully on this side?
|
||||
direct_ok: bool,
|
||||
/// Which future won the local tokio::select race?
|
||||
/// "Direct" or "Relay" — informational for debug logs.
|
||||
#[serde(default)]
|
||||
race_winner: String,
|
||||
},
|
||||
|
||||
// ── Phase 4: cross-relay direct-call signaling ────────────────────
|
||||
|
||||
/// Phase 4: relay-to-relay envelope for forwarding direct-call
|
||||
|
||||
Reference in New Issue
Block a user