feat(nat): wire birthday attack end-to-end into connect flow
Some checks failed
Mirror to GitHub / mirror (push) Failing after 32s
Build Release Binaries / build-amd64 (push) Failing after 3m19s

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:
Siavash Sameni
2026-04-14 16:50:11 +04:00
parent f06f9073ae
commit 8c360186df

View File

@@ -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,
})), })),
}); });