feat(nat): wire birthday attack end-to-end into connect flow
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<std::net::SocketAddr> = 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();
|
let mut all_local = peer_local_parsed.clone();
|
||||||
all_local.extend(predicted_addrs);
|
all_local.extend(predicted_addrs);
|
||||||
|
all_local.extend(birthday_addrs);
|
||||||
|
|
||||||
let candidates = wzp_client::dual_path::PeerCandidates {
|
let candidates = wzp_client::dual_path::PeerCandidates {
|
||||||
reflexive: peer_addr_parsed,
|
reflexive: peer_addr_parsed,
|
||||||
@@ -1012,6 +1059,15 @@ struct SignalState {
|
|||||||
/// command reads this to generate predicted port candidates for
|
/// command reads this to generate predicted port candidates for
|
||||||
/// sequential NATs.
|
/// sequential NATs.
|
||||||
peer_hard_nat_probe: Option<PeerHardNatInfo>,
|
peer_hard_nat_probe: Option<PeerHardNatInfo>,
|
||||||
|
/// Phase 8.6: peer's birthday attack ports, if received.
|
||||||
|
peer_birthday_ports: Option<PeerBirthdayInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed data from a peer's HardNatBirthdayStart signal.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct PeerBirthdayInfo {
|
||||||
|
external_ip: std::net::IpAddr,
|
||||||
|
ports: Vec<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parsed data from a peer's HardNatProbe signal.
|
/// Parsed data from a peer's HardNatProbe signal.
|
||||||
@@ -1404,8 +1460,15 @@ fn do_register_signal(
|
|||||||
"acceptor_ports": acceptor_ports,
|
"acceptor_ports": acceptor_ports,
|
||||||
"external_ip": external_ip,
|
"external_ip": external_ip,
|
||||||
}));
|
}));
|
||||||
// TODO: trigger dialer spray when birthday attack
|
// Stash for the connect command (if still running)
|
||||||
// is integrated into the race waterfall
|
// or for a background spray after relay fallback.
|
||||||
|
if let Ok(ip) = external_ip.parse::<std::net::IpAddr>() {
|
||||||
|
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 })) => {
|
Ok(Some(SignalMessage::ReflectResponse { observed_addr })) => {
|
||||||
// "STUN for QUIC" response — the relay told us our
|
// "STUN for QUIC" response — the relay told us our
|
||||||
@@ -2364,6 +2427,7 @@ pub fn run() {
|
|||||||
reconnect_in_progress: false,
|
reconnect_in_progress: false,
|
||||||
pending_path_report: None,
|
pending_path_report: None,
|
||||||
peer_hard_nat_probe: None,
|
peer_hard_nat_probe: None,
|
||||||
|
peer_birthday_ports: None,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user