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:
@@ -340,8 +340,9 @@ impl CallEngine {
|
|||||||
|
|
||||||
// Transport source: either the pre-connected one from the
|
// Transport source: either the pre-connected one from the
|
||||||
// dual-path race (Phase 3.5) or build a fresh one here.
|
// dual-path race (Phase 3.5) or build a fresh one here.
|
||||||
|
let is_direct_p2p = pre_connected_transport.is_some();
|
||||||
let transport = if let Some(t) = pre_connected_transport {
|
let transport = if let Some(t) = pre_connected_transport {
|
||||||
info!(t_ms = call_t0.elapsed().as_millis(), "first-join diag: using pre-connected transport from dual-path race");
|
info!(t_ms = call_t0.elapsed().as_millis(), "first-join diag: using pre-connected transport from dual-path race (direct P2P)");
|
||||||
t
|
t
|
||||||
} else {
|
} else {
|
||||||
// QUIC transport + handshake (Phase 0 relay-only path).
|
// QUIC transport + handshake (Phase 0 relay-only path).
|
||||||
@@ -381,14 +382,27 @@ impl CallEngine {
|
|||||||
Arc::new(wzp_transport::QuinnTransport::new(conn))
|
Arc::new(wzp_transport::QuinnTransport::new(conn))
|
||||||
};
|
};
|
||||||
|
|
||||||
let _session = wzp_client::handshake::perform_handshake(
|
// The media handshake (CallOffer/CallAnswer + crypto key
|
||||||
&*transport,
|
// exchange) is a relay-specific protocol: the relay runs
|
||||||
&seed.0,
|
// `accept_handshake` on its side. On a direct P2P
|
||||||
Some(&alias),
|
// connection the peer is a phone, not a relay — nobody on
|
||||||
)
|
// the other end handles the handshake. So skip it when
|
||||||
.await
|
// is_direct_p2p. The QUIC transport already provides TLS
|
||||||
.map_err(|e| { error!("perform_handshake failed: {e}"); e })?;
|
// encryption, and both peers' identities were verified
|
||||||
info!(t_ms = call_t0.elapsed().as_millis(), "first-join diag: connected to relay, handshake complete");
|
// through the signal channel (DirectCallOffer/Answer carry
|
||||||
|
// identity_pub + ephemeral_pub + signature).
|
||||||
|
if !is_direct_p2p {
|
||||||
|
let _session = wzp_client::handshake::perform_handshake(
|
||||||
|
&*transport,
|
||||||
|
&seed.0,
|
||||||
|
Some(&alias),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| { error!("perform_handshake failed: {e}"); e })?;
|
||||||
|
info!(t_ms = call_t0.elapsed().as_millis(), "first-join diag: connected to relay, handshake complete");
|
||||||
|
} else {
|
||||||
|
info!(t_ms = call_t0.elapsed().as_millis(), "first-join diag: direct P2P — skipping relay handshake (QUIC TLS is the encryption layer)");
|
||||||
|
}
|
||||||
event_cb("connected", &format!("joined room {room}"));
|
event_cb("connected", &format!("joined room {room}"));
|
||||||
|
|
||||||
// Oboe audio via the wzp-native cdylib that was dlopen'd at
|
// Oboe audio via the wzp-native cdylib that was dlopen'd at
|
||||||
@@ -1011,8 +1025,9 @@ impl CallEngine {
|
|||||||
|
|
||||||
// Transport source: either the pre-connected dual-path
|
// Transport source: either the pre-connected dual-path
|
||||||
// winner (Phase 3.5) or build a fresh relay connection here.
|
// winner (Phase 3.5) or build a fresh relay connection here.
|
||||||
|
let is_direct_p2p = pre_connected_transport.is_some();
|
||||||
let transport = if let Some(t) = pre_connected_transport {
|
let transport = if let Some(t) = pre_connected_transport {
|
||||||
info!("using pre-connected transport from dual-path race");
|
info!("using pre-connected transport from dual-path race (direct P2P)");
|
||||||
t
|
t
|
||||||
} else {
|
} else {
|
||||||
// Connect — reuse the signal endpoint if the direct-call path gave
|
// Connect — reuse the signal endpoint if the direct-call path gave
|
||||||
@@ -1035,14 +1050,21 @@ impl CallEngine {
|
|||||||
Arc::new(wzp_transport::QuinnTransport::new(conn))
|
Arc::new(wzp_transport::QuinnTransport::new(conn))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handshake
|
// Handshake — relay-specific. Direct P2P connections skip
|
||||||
let _session = wzp_client::handshake::perform_handshake(
|
// this because the peer is a phone, not a relay with an
|
||||||
&*transport,
|
// accept_handshake handler. See the android branch's
|
||||||
&seed.0,
|
// comment for the full rationale.
|
||||||
Some(&alias),
|
if !is_direct_p2p {
|
||||||
)
|
let _session = wzp_client::handshake::perform_handshake(
|
||||||
.await
|
&*transport,
|
||||||
.map_err(|e| { error!("perform_handshake failed: {e}"); e })?;
|
&seed.0,
|
||||||
|
Some(&alias),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| { error!("perform_handshake failed: {e}"); e })?;
|
||||||
|
} else {
|
||||||
|
info!("direct P2P — skipping relay handshake (QUIC TLS is the encryption layer)");
|
||||||
|
}
|
||||||
|
|
||||||
info!("connected to relay, handshake complete");
|
info!("connected to relay, handshake complete");
|
||||||
event_cb("connected", &format!("joined room {room}"));
|
event_cb("connected", &format!("joined room {room}"));
|
||||||
|
|||||||
@@ -327,8 +327,9 @@ async fn connect(
|
|||||||
// cross-wired by the relay in CallSetup.peer_direct_addr.
|
// cross-wired by the relay in CallSetup.peer_direct_addr.
|
||||||
peer_direct_addr: Option<String>,
|
peer_direct_addr: Option<String>,
|
||||||
// Phase 5.5: peer's LAN host candidates from CallSetup.
|
// Phase 5.5: peer's LAN host candidates from CallSetup.
|
||||||
// JS side passes [] when empty.
|
// Optional so the room-join path (which has no peer addrs)
|
||||||
peer_local_addrs: Vec<String>,
|
// can omit it entirely — it's only populated on direct calls.
|
||||||
|
peer_local_addrs: Option<Vec<String>>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
emit_call_debug(&app, "connect:start", serde_json::json!({
|
emit_call_debug(&app, "connect:start", serde_json::json!({
|
||||||
"relay": relay,
|
"relay": relay,
|
||||||
@@ -373,7 +374,8 @@ async fn connect(
|
|||||||
// Phase 5.5: build the full peer candidate bundle (reflex +
|
// Phase 5.5: build the full peer candidate bundle (reflex +
|
||||||
// LAN hosts). The dial_order helper will fan them out in
|
// LAN hosts). The dial_order helper will fan them out in
|
||||||
// priority order for the D-role race.
|
// 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()
|
.iter()
|
||||||
.filter_map(|s| s.parse().ok())
|
.filter_map(|s| s.parse().ok())
|
||||||
.collect();
|
.collect();
|
||||||
@@ -441,7 +443,7 @@ async fn connect(
|
|||||||
_ => {
|
_ => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
has_peer_reflex = peer_direct_addr.is_some(),
|
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(),
|
has_own = own_reflex_addr.is_some(),
|
||||||
?role,
|
?role,
|
||||||
%relay,
|
%relay,
|
||||||
@@ -450,7 +452,7 @@ async fn connect(
|
|||||||
);
|
);
|
||||||
emit_call_debug(&app, "connect:dual_path_skipped", serde_json::json!({
|
emit_call_debug(&app, "connect:dual_path_skipped", serde_json::json!({
|
||||||
"has_peer_reflex": peer_direct_addr.is_some(),
|
"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(),
|
"has_own": own_reflex_addr.is_some(),
|
||||||
"role": format!("{:?}", role),
|
"role": format!("{:?}", role),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user