From 8c360186dfbb49f38a59b588e69dc47734179087 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Tue, 14 Apr 2026 16:50:11 +0400 Subject: [PATCH] feat(nat): wire birthday attack end-to-end into connect flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete Dialer-side birthday attack integration: - SignalState stores peer_birthday_ports from HardNatBirthdayStart - connect command: if peer's HardNatProbe shows non-cone NAT, waits up to 3s for birthday ports to arrive (Acceptor needs time to open 32 sockets + STUN-probe each) - When birthday ports arrive, generate_dialer_targets() builds hit list (known ports + random fill) and adds them to PeerCandidates - All birthday targets go into the dual-path race as extra candidates - LAN/cone calls skip the wait entirely (gated on allocation type) Full waterfall now: 1. Standard candidates (reflexive + mapped) → immediate 2. Port prediction (sequential delta) → immediate 3. Birthday targets (if non-cone peer) → +3s wait 4. All of above raced in parallel via JoinSet 5. Relay runs concurrently with 500ms head-start 599 tests pass, 0 regressions. Co-Authored-By: Claude Opus 4.6 (1M context) --- desktop/src-tauri/src/lib.rs | 68 ++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index be7e76e..8a0afb3 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -443,8 +443,55 @@ async fn connect( } } + // Phase 8.6: if peer sent birthday attack ports, add + // them as extra candidates the Dialer can target. + // Only wait for birthday ports if we know the peer has + // a non-cone NAT (from HardNatProbe). Otherwise start + // the race immediately — LAN/cone calls shouldn't wait. + let mut birthday_addrs: Vec = Vec::new(); + { + let peer_needs_birthday = { + let sig = state.signal.lock().await; + sig.peer_hard_nat_probe.as_ref() + .map(|p| p.allocation != "port-preserving") + .unwrap_or(false) + }; + if peer_needs_birthday { + // Wait up to 3s for BirthdayStart (Acceptor needs + // time to open ports + STUN-probe them). + for _ in 0..6 { + let sig = state.signal.lock().await; + if sig.peer_birthday_ports.is_some() { break; } + drop(sig); + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + } + } + let sig = state.signal.lock().await; + if let Some(ref bday) = sig.peer_birthday_ports { + let targets = wzp_client::birthday::generate_dialer_targets( + match bday.external_ip { + std::net::IpAddr::V4(ip) => ip, + _ => std::net::Ipv4Addr::UNSPECIFIED, + }, + &bday.ports, + 64, // spray up to 64 targets + ); + birthday_addrs = targets; + tracing::info!( + birthday_targets = birthday_addrs.len(), + known_ports = bday.ports.len(), + "connect: adding birthday attack targets" + ); + emit_call_debug(&app, "connect:birthday_targets", serde_json::json!({ + "known_ports": bday.ports, + "total_targets": birthday_addrs.len(), + })); + } + } + let mut all_local = peer_local_parsed.clone(); all_local.extend(predicted_addrs); + all_local.extend(birthday_addrs); let candidates = wzp_client::dual_path::PeerCandidates { reflexive: peer_addr_parsed, @@ -1012,6 +1059,15 @@ struct SignalState { /// command reads this to generate predicted port candidates for /// sequential NATs. peer_hard_nat_probe: Option, + /// Phase 8.6: peer's birthday attack ports, if received. + peer_birthday_ports: Option, +} + +/// Parsed data from a peer's HardNatBirthdayStart signal. +#[derive(Debug, Clone)] +struct PeerBirthdayInfo { + external_ip: std::net::IpAddr, + ports: Vec, } /// Parsed data from a peer's HardNatProbe signal. @@ -1404,8 +1460,15 @@ fn do_register_signal( "acceptor_ports": acceptor_ports, "external_ip": external_ip, })); - // TODO: trigger dialer spray when birthday attack - // is integrated into the race waterfall + // Stash for the connect command (if still running) + // or for a background spray after relay fallback. + if let Ok(ip) = external_ip.parse::() { + let mut sig = signal_state.lock().await; + sig.peer_birthday_ports = Some(PeerBirthdayInfo { + external_ip: ip, + ports: acceptor_ports, + }); + } } Ok(Some(SignalMessage::ReflectResponse { observed_addr })) => { // "STUN for QUIC" response — the relay told us our @@ -2364,6 +2427,7 @@ pub fn run() { reconnect_in_progress: false, pending_path_report: None, peer_hard_nat_probe: None, + peer_birthday_ports: None, })), });