diff --git a/desktop/index.html b/desktop/index.html
index 0a2713a..10e0c92 100644
--- a/desktop/index.html
+++ b/desktop/index.html
@@ -187,6 +187,10 @@
Call flow debug logs (trace every step of a call)
+
Call Debug Log
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index bf5f9d2..20f081c 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -333,13 +333,19 @@ async fn connect(
// Phase 8 (Tailscale-inspired): peer's port-mapped external
// address from NAT-PMP/PCP/UPnP, carried in CallSetup.
peer_mapped_addr: Option,
+ // Debug: when true, skip relay fallback entirely — the call
+ // fails if direct P2P doesn't connect. Useful for testing NAT
+ // traversal without the relay masking failures.
+ direct_only: Option,
) -> Result {
+ let force_direct = direct_only.unwrap_or(false);
emit_call_debug(&app, "connect:start", serde_json::json!({
"relay": relay,
"room": room,
"peer_direct_addr": peer_direct_addr,
"peer_local_addrs": peer_local_addrs,
"peer_mapped_addr": peer_mapped_addr,
+ "direct_only": force_direct,
}));
let mut engine_lock = state.engine.lock().await;
if engine_lock.is_some() {
@@ -572,7 +578,20 @@ async fn connect(
"local_direct_ok": local_direct_ok,
"peer_direct_ok": peer_direct_ok,
"chosen_path": format!("{:?}", chosen_path),
+ "direct_only": force_direct,
}));
+
+ // direct_only mode: refuse relay fallback
+ if force_direct && !use_direct {
+ let reason = format!(
+ "direct_only: P2P failed (local_ok={local_direct_ok}, peer_ok={peer_direct_ok})"
+ );
+ emit_call_debug(&app, "connect:direct_only_failed", serde_json::json!({
+ "reason": reason,
+ "candidate_diags": race_result.candidate_diags,
+ }));
+ return Err(reason);
+ }
tracing::info!(
?chosen_path,
use_direct,
diff --git a/desktop/src/main.ts b/desktop/src/main.ts
index df1c1f1..f9580a4 100644
--- a/desktop/src/main.ts
+++ b/desktop/src/main.ts
@@ -208,6 +208,7 @@ const sAlias = document.getElementById("s-alias") as HTMLInputElement;
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 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;
@@ -287,6 +288,9 @@ interface Settings {
/// renders into the rolling Debug Log panel in settings. Off in
/// normal mode keeps the GUI quiet but logcat always has a copy.
callDebugLogs: boolean;
+ /// Debug: skip relay fallback on direct calls — fail if P2P
+ /// doesn't connect. Useful for testing NAT traversal.
+ directOnly: boolean;
}
function loadSettings(): Settings {
@@ -301,6 +305,7 @@ function loadSettings(): Settings {
osAec: true, agc: true, quality: "auto", recentRooms: [],
dredDebugLogs: false,
callDebugLogs: false,
+ directOnly: false,
};
try {
const raw = localStorage.getItem("wzp-settings");
@@ -1175,6 +1180,7 @@ function openSettings() {
sRoom.value = s.room; sAlias.value = s.alias; sOsAec.checked = s.osAec;
sDredDebug.checked = !!s.dredDebugLogs;
sCallDebug.checked = !!s.callDebugLogs;
+ sDirectOnly.checked = !!s.directOnly;
// 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";
@@ -1337,6 +1343,7 @@ settingsSave.addEventListener("click", () => {
s.quality = QUALITY_STEPS[parseInt(sQuality.value)] || "auto";
s.dredDebugLogs = sDredDebug.checked;
s.callDebugLogs = sCallDebug.checked;
+ s.directOnly = sDirectOnly.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.
@@ -1692,6 +1699,7 @@ listen("signal-event", (event: any) => {
peerDirectAddr: data.peer_direct_addr ?? null,
peerLocalAddrs: data.peer_local_addrs ?? [],
peerMappedAddr: data.peer_mapped_addr ?? null,
+ directOnly: loadSettings().directOnly || false,
});
showCallScreen();
} catch (e: any) {