fix(connect): make peerLocalAddrs optional + skip handshake on direct P2P

Two regressions from Phase 5.5/5.6:

1. Room connect broken: the connect Tauri command required
   peerLocalAddrs as a Vec<String>, but the room-join JS path
   doesn't pass it (only the direct-call setup handler does).
   Error: "invalid args 'peerLocalAddrs' for command 'connect':
   command connect missing required key peerLocalAddrs".

   Fix: change to Option<Vec<String>>, unwrap_or_default() at
   usage sites. Room connect works again with zero peer addrs.

2. Direct P2P call connects but then CallEngine fails with
   "expected CallAnswer, got Discriminant(0)". Root cause: after
   the dual-path race picked a direct P2P transport, CallEngine
   still ran perform_handshake() on it. That handshake is a
   relay-specific protocol — sends a CallOffer signal and waits
   for CallAnswer back. On a direct QUIC connection to a phone,
   there's nobody running accept_handshake, so the handshake
   reads garbage from the peer's first media packet and errors.

   Fix: track is_direct_p2p = pre_connected_transport.is_some()
   and skip perform_handshake when true. The direct connection
   is already TLS-encrypted by QUIC, and both peers' identities
   were verified through the signal channel (DirectCallOffer/
   Answer carry identity_pub + ephemeral_pub + signature). Both
   android and desktop branches updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-12 08:09:32 +04:00
parent 16793be36f
commit 2427630472
2 changed files with 47 additions and 23 deletions

View File

@@ -327,8 +327,9 @@ async fn connect(
// cross-wired by the relay in CallSetup.peer_direct_addr.
peer_direct_addr: Option<String>,
// Phase 5.5: peer's LAN host candidates from CallSetup.
// JS side passes [] when empty.
peer_local_addrs: Vec<String>,
// Optional so the room-join path (which has no peer addrs)
// can omit it entirely — it's only populated on direct calls.
peer_local_addrs: Option<Vec<String>>,
) -> Result<String, String> {
emit_call_debug(&app, "connect:start", serde_json::json!({
"relay": relay,
@@ -373,7 +374,8 @@ async fn connect(
// Phase 5.5: build the full peer candidate bundle (reflex +
// LAN hosts). The dial_order helper will fan them out in
// priority order for the D-role race.
let peer_local_parsed: Vec<std::net::SocketAddr> = peer_local_addrs
let peer_local_addrs_vec = peer_local_addrs.unwrap_or_default();
let peer_local_parsed: Vec<std::net::SocketAddr> = peer_local_addrs_vec
.iter()
.filter_map(|s| s.parse().ok())
.collect();
@@ -441,7 +443,7 @@ async fn connect(
_ => {
tracing::info!(
has_peer_reflex = peer_direct_addr.is_some(),
has_peer_local = !peer_local_addrs.is_empty(),
has_peer_local = !peer_local_addrs_vec.is_empty(),
has_own = own_reflex_addr.is_some(),
?role,
%relay,
@@ -450,7 +452,7 @@ async fn connect(
);
emit_call_debug(&app, "connect:dual_path_skipped", serde_json::json!({
"has_peer_reflex": peer_direct_addr.is_some(),
"has_peer_local": !peer_local_addrs.is_empty(),
"has_peer_local": !peer_local_addrs_vec.is_empty(),
"has_own": own_reflex_addr.is_some(),
"role": format!("{:?}", role),
}));