diff --git a/desktop/index.html b/desktop/index.html
index 10e0c92..8c0035f 100644
--- a/desktop/index.html
+++ b/desktop/index.html
@@ -191,6 +191,10 @@
Direct-only mode (no relay fallback — fails if P2P can't connect)
+
Call Debug Log
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index 8a0afb3..9d9a96e 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -337,8 +337,12 @@ async fn connect(
// fails if direct P2P doesn't connect. Useful for testing NAT
// traversal without the relay masking failures.
direct_only: Option,
+ // Enable birthday attack for hard NAT traversal. Adds ~3s to
+ // call setup when peer has symmetric NAT.
+ birthday_attack: Option,
) -> Result {
let force_direct = direct_only.unwrap_or(false);
+ let enable_birthday = birthday_attack.unwrap_or(false);
emit_call_debug(&app, "connect:start", serde_json::json!({
"relay": relay,
"room": room,
@@ -346,6 +350,7 @@ async fn connect(
"peer_local_addrs": peer_local_addrs,
"peer_mapped_addr": peer_mapped_addr,
"direct_only": force_direct,
+ "birthday_attack": enable_birthday,
}));
let mut engine_lock = state.engine.lock().await;
if engine_lock.is_some() {
@@ -450,7 +455,7 @@ async fn connect(
// the race immediately — LAN/cone calls shouldn't wait.
let mut birthday_addrs: Vec = Vec::new();
{
- let peer_needs_birthday = {
+ let peer_needs_birthday = enable_birthday && {
let sig = state.signal.lock().await;
sig.peer_hard_nat_probe.as_ref()
.map(|p| p.allocation != "port-preserving")
diff --git a/desktop/src/main.ts b/desktop/src/main.ts
index f9580a4..eb0f0e8 100644
--- a/desktop/src/main.ts
+++ b/desktop/src/main.ts
@@ -209,6 +209,7 @@ const sOsAec = document.getElementById("s-os-aec") as HTMLInputElement;
const sDredDebug = document.getElementById("s-dred-debug") as HTMLInputElement;
const sCallDebug = document.getElementById("s-call-debug") as HTMLInputElement;
const sDirectOnly = document.getElementById("s-direct-only") as HTMLInputElement;
+const sBirthdayAttack = document.getElementById("s-birthday-attack") as HTMLInputElement;
const sCallDebugSection = document.getElementById("s-call-debug-section") as HTMLDivElement;
const sCallDebugLogEl = document.getElementById("s-call-debug-log") as HTMLDivElement;
const sCallDebugClearBtn = document.getElementById("s-call-debug-clear") as HTMLButtonElement;
@@ -291,6 +292,9 @@ interface Settings {
/// Debug: skip relay fallback on direct calls — fail if P2P
/// doesn't connect. Useful for testing NAT traversal.
directOnly: boolean;
+ /// Enable birthday attack for hard NAT traversal. Adds ~3s to
+ /// call setup when peer has symmetric NAT. Off by default.
+ birthdayAttack: boolean;
}
function loadSettings(): Settings {
@@ -306,6 +310,7 @@ function loadSettings(): Settings {
dredDebugLogs: false,
callDebugLogs: false,
directOnly: false,
+ birthdayAttack: false,
};
try {
const raw = localStorage.getItem("wzp-settings");
@@ -1181,6 +1186,7 @@ function openSettings() {
sDredDebug.checked = !!s.dredDebugLogs;
sCallDebug.checked = !!s.callDebugLogs;
sDirectOnly.checked = !!s.directOnly;
+ sBirthdayAttack.checked = !!s.birthdayAttack;
// Show the debug-log panel only when the user has the flag on —
// keeps the settings panel short in normal use.
sCallDebugSection.style.display = s.callDebugLogs ? "" : "none";
@@ -1344,6 +1350,7 @@ settingsSave.addEventListener("click", () => {
s.dredDebugLogs = sDredDebug.checked;
s.callDebugLogs = sCallDebug.checked;
s.directOnly = sDirectOnly.checked;
+ s.birthdayAttack = sBirthdayAttack.checked;
saveSettingsObj(s);
// Push the new flags to the Rust side immediately so the next
// frame / call already honors them without waiting for a restart.
@@ -1700,6 +1707,7 @@ listen("signal-event", (event: any) => {
peerLocalAddrs: data.peer_local_addrs ?? [],
peerMappedAddr: data.peer_mapped_addr ?? null,
directOnly: loadSettings().directOnly || false,
+ birthdayAttack: loadSettings().birthdayAttack || false,
});
showCallScreen();
} catch (e: any) {