feat(ui): selectable NAT detection mode + netcheck Tauri command
detect_nat_type now accepts optional `mode` parameter:
- "relay" — relay-based Reflect only (original behavior)
- "stun" — public STUN servers only (no relay needed)
- "both" — relay + STUN in parallel (default, highest confidence)
New run_netcheck Tauri command exposes the full network diagnostic
(NAT type, IPv4/v6, port mapping, relay latencies, port allocation)
to the JS frontend.
JS usage:
await invoke('detect_nat_type', { relays, mode: 'stun' })
await invoke('run_netcheck', { relays })
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2092,17 +2092,18 @@ async fn get_reflected_address(
|
|||||||
/// would make a symmetric NAT look like a cone NAT, which is
|
/// would make a symmetric NAT look like a cone NAT, which is
|
||||||
/// exactly the failure mode we're trying to detect.
|
/// exactly the failure mode we're trying to detect.
|
||||||
///
|
///
|
||||||
/// Takes the relay list from JS because the GUI owns the relay
|
/// NAT detection with selectable mode.
|
||||||
/// config (localStorage `wzp-settings.relays`). Frontend passes it
|
///
|
||||||
/// in; Rust side just does the network work.
|
/// `mode`:
|
||||||
|
/// - `"relay"` — relay-based Reflect only (original Phase 1-2 behavior)
|
||||||
|
/// - `"stun"` — public STUN servers only (no relay needed)
|
||||||
|
/// - `"both"` (default) — relay + STUN in parallel (highest confidence)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn detect_nat_type(
|
async fn detect_nat_type(
|
||||||
state: tauri::State<'_, Arc<AppState>>,
|
state: tauri::State<'_, Arc<AppState>>,
|
||||||
relays: Vec<RelayArg>,
|
relays: Vec<RelayArg>,
|
||||||
|
mode: Option<String>,
|
||||||
) -> Result<serde_json::Value, String> {
|
) -> Result<serde_json::Value, String> {
|
||||||
// Parse relay args up front so a single malformed entry fails
|
|
||||||
// the whole call cleanly instead of surfacing as a probe error
|
|
||||||
// at the end.
|
|
||||||
let mut parsed = Vec::with_capacity(relays.len());
|
let mut parsed = Vec::with_capacity(relays.len());
|
||||||
for r in relays {
|
for r in relays {
|
||||||
let addr: std::net::SocketAddr = r
|
let addr: std::net::SocketAddr = r
|
||||||
@@ -2112,29 +2113,71 @@ async fn detect_nat_type(
|
|||||||
parsed.push((r.name, addr));
|
parsed.push((r.name, addr));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 5: share the signal endpoint across all probes so
|
|
||||||
// they emit from the same source port. Port-preserving NATs
|
|
||||||
// (MikroTik, most consumer routers) give a stable external
|
|
||||||
// port → classifier correctly sees cone instead of falsely
|
|
||||||
// labeling SymmetricPort. Falls back to None (per-probe fresh
|
|
||||||
// endpoint) when not registered.
|
|
||||||
let shared_endpoint = state.signal.lock().await.endpoint.clone();
|
let shared_endpoint = state.signal.lock().await.endpoint.clone();
|
||||||
|
|
||||||
// 1500ms per probe is generous: a same-host probe is < 10ms,
|
|
||||||
// a cross-continent probe is typically < 300ms, and we want
|
|
||||||
// to tolerate a one-off packet loss during connect.
|
|
||||||
//
|
|
||||||
// Phase 8 (Tailscale-inspired): also probe public STUN servers
|
|
||||||
// in parallel with relay-based reflection. More probes = higher
|
|
||||||
// confidence in NAT classification. Falls back gracefully if
|
|
||||||
// STUN servers are unreachable.
|
|
||||||
let stun_config = wzp_client::stun::StunConfig::default();
|
let stun_config = wzp_client::stun::StunConfig::default();
|
||||||
let detection = wzp_client::reflect::detect_nat_type_with_stun(
|
|
||||||
parsed, 1500, shared_endpoint, &stun_config,
|
let mode_str = mode.as_deref().unwrap_or("both");
|
||||||
).await;
|
tracing::info!(mode = mode_str, relay_count = parsed.len(), "detect_nat_type: starting");
|
||||||
|
|
||||||
|
let detection = match mode_str {
|
||||||
|
"relay" => {
|
||||||
|
// Original behavior: relay-based Reflect only
|
||||||
|
wzp_client::reflect::detect_nat_type(parsed, 1500, shared_endpoint).await
|
||||||
|
}
|
||||||
|
"stun" => {
|
||||||
|
// Public STUN servers only — no relay connection needed
|
||||||
|
let probes = wzp_client::stun::probe_stun_servers(&stun_config).await;
|
||||||
|
let (nat_type, consensus_addr) = wzp_client::reflect::classify_nat(&probes);
|
||||||
|
wzp_client::reflect::NatDetection {
|
||||||
|
probes,
|
||||||
|
nat_type,
|
||||||
|
consensus_addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// "both" — relay + STUN in parallel (default, highest confidence)
|
||||||
|
wzp_client::reflect::detect_nat_type_with_stun(
|
||||||
|
parsed, 1500, shared_endpoint, &stun_config,
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
};
|
||||||
serde_json::to_value(&detection).map_err(|e| format!("serialize: {e}"))
|
serde_json::to_value(&detection).map_err(|e| format!("serialize: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run comprehensive network diagnostic (STUN + relay + portmap + IPv6).
|
||||||
|
#[tauri::command]
|
||||||
|
async fn run_netcheck(
|
||||||
|
state: tauri::State<'_, Arc<AppState>>,
|
||||||
|
relays: Vec<RelayArg>,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
let mut relay_addrs = Vec::with_capacity(relays.len());
|
||||||
|
for r in relays {
|
||||||
|
let addr: std::net::SocketAddr = r
|
||||||
|
.address
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("bad relay address {:?}: {e}", r.address))?;
|
||||||
|
relay_addrs.push((r.name, addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_port = state.signal.lock().await.endpoint
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|ep| ep.local_addr().ok())
|
||||||
|
.map(|la| la.port())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let config = wzp_client::netcheck::NetcheckConfig {
|
||||||
|
stun_config: wzp_client::stun::StunConfig::default(),
|
||||||
|
relays: relay_addrs,
|
||||||
|
timeout: std::time::Duration::from_secs(5),
|
||||||
|
test_portmap: true,
|
||||||
|
test_ipv6: true,
|
||||||
|
local_port,
|
||||||
|
};
|
||||||
|
|
||||||
|
let report = wzp_client::netcheck::run_netcheck(&config).await;
|
||||||
|
serde_json::to_value(&report).map_err(|e| format!("serialize: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Deserialization shim for the relay list coming from JS. The
|
/// Deserialization shim for the relay list coming from JS. The
|
||||||
/// `wzp-settings.relays` array in localStorage has more fields
|
/// `wzp-settings.relays` array in localStorage has more fields
|
||||||
/// (rtt, serverFingerprint, knownFingerprint) but we only need
|
/// (rtt, serverFingerprint, knownFingerprint) but we only need
|
||||||
@@ -2299,7 +2342,7 @@ pub fn run() {
|
|||||||
ping_relay, get_identity, get_app_info,
|
ping_relay, get_identity, get_app_info,
|
||||||
connect, disconnect, toggle_mic, toggle_speaker, get_status,
|
connect, disconnect, toggle_mic, toggle_speaker, get_status,
|
||||||
register_signal, place_call, answer_call, get_signal_status,
|
register_signal, place_call, answer_call, get_signal_status,
|
||||||
get_reflected_address, detect_nat_type,
|
get_reflected_address, detect_nat_type, run_netcheck,
|
||||||
hangup_call,
|
hangup_call,
|
||||||
deregister,
|
deregister,
|
||||||
set_speakerphone, is_speakerphone_on,
|
set_speakerphone, is_speakerphone_on,
|
||||||
|
|||||||
Reference in New Issue
Block a user