From d5b92ede7b04f1a59dab8c4c52f1fedde80611b2 Mon Sep 17 00:00:00 2001 From: DaZuo0122 <1085701449@qq.com> Date: Sat, 17 Jan 2026 19:07:10 +0800 Subject: [PATCH] Fix: main thread timeout early than work thread --- crates/wtfnet-dnsleak/src/sensor.rs | 79 ++++++++++++++++---------- docs/dns_leak_implementation_status.md | 5 +- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/crates/wtfnet-dnsleak/src/sensor.rs b/crates/wtfnet-dnsleak/src/sensor.rs index ced374e..777520c 100644 --- a/crates/wtfnet-dnsleak/src/sensor.rs +++ b/crates/wtfnet-dnsleak/src/sensor.rs @@ -14,6 +14,11 @@ use pnet::datalink::{self, Channel, Config as DatalinkConfig}; #[cfg(feature = "pcap")] use std::sync::mpsc; +#[cfg(feature = "pcap")] +const OPEN_IFACE_TIMEOUT_MS: u64 = 700; +#[cfg(feature = "pcap")] +const FRAME_RECV_TIMEOUT_MS: u64 = 200; + #[cfg(not(feature = "pcap"))] pub async fn capture_events(_options: &LeakWatchOptions) -> Result, DnsLeakError> { Err(DnsLeakError::NotSupported( @@ -24,8 +29,13 @@ pub async fn capture_events(_options: &LeakWatchOptions) -> Result Result, DnsLeakError> { let options = options.clone(); - let candidates = format_iface_list(&datalink::interfaces()); - let timeout_ms = options.duration_ms.saturating_add(2000); + let iface_list = datalink::interfaces(); + let candidates = format_iface_list(&iface_list); + let select_budget_ms = (iface_list.len().max(1) as u64).saturating_mul(OPEN_IFACE_TIMEOUT_MS); + let timeout_ms = options + .duration_ms + .saturating_add(select_budget_ms) + .saturating_add(2000); let handle = tokio::task::spawn_blocking(move || capture_events_blocking(options)); match tokio::time::timeout(Duration::from_millis(timeout_ms), handle).await { Ok(joined) => joined.map_err(|err| DnsLeakError::Io(err.to_string()))?, @@ -88,16 +98,28 @@ fn capture_events_blocking(options: LeakWatchOptions) -> Result>(); let iface_name = iface.name.clone(); + let (frame_tx, frame_rx) = mpsc::channel(); + std::thread::spawn(move || loop { + match rx.next() { + Ok(frame) => { + if frame_tx.send(frame.to_vec()).is_err() { + break; + } + } + Err(_) => continue, + } + }); + let deadline = Instant::now() + Duration::from_millis(options.duration_ms); let mut events = Vec::new(); let mut seen = HashSet::new(); while Instant::now() < deadline { - let frame = match rx.next() { + let frame = match frame_rx.recv_timeout(Duration::from_millis(FRAME_RECV_TIMEOUT_MS)) { Ok(frame) => frame, Err(_) => continue, }; - let ethernet = match EthernetPacket::new(frame) { + let ethernet = match EthernetPacket::new(&frame) { Some(packet) => packet, None => continue, }; @@ -284,14 +306,8 @@ fn select_interface( }); } - if let Some(iface) = pick_stable_iface(&interfaces) { - debug!("dns leak iface pick: stable={}", iface.name); - if let Ok(channel) = open_channel_with_timeout(iface, config) { - return Ok(channel); - } - } - - for iface in interfaces.iter() { + let ordered = order_interfaces(&interfaces); + for iface in ordered.iter() { debug!("dns leak iface pick: try={}", iface.name); if let Ok(channel) = open_channel_with_timeout(iface.clone(), config) { return Ok(channel); @@ -320,7 +336,7 @@ fn open_channel_with_timeout( let _ = tx.send((iface, result)); }); - let timeout = Duration::from_millis(700); + let timeout = Duration::from_millis(OPEN_IFACE_TIMEOUT_MS); match rx.recv_timeout(timeout) { Ok((iface, Ok(rx))) => Ok((iface, rx)), Ok((_iface, Err(err))) => Err(err), @@ -340,26 +356,27 @@ fn is_named_fallback(name: &str) -> bool { } #[cfg(feature = "pcap")] -fn pick_stable_iface( +fn order_interfaces( interfaces: &[datalink::NetworkInterface], -) -> Option { - let mut preferred = interfaces - .iter() - .filter(|iface| { - iface.is_up() - && !iface.is_loopback() - && (is_named_fallback(&iface.name) || !iface.ips.is_empty()) - }) - .cloned() - .collect::>(); - if preferred.is_empty() { - preferred = interfaces - .iter() - .filter(|iface| !iface.is_loopback()) - .cloned() - .collect(); +) -> Vec { + let mut preferred = Vec::new(); + let mut others = Vec::new(); + for iface in interfaces.iter() { + if iface.is_loopback() { + continue; + } + if is_named_fallback(&iface.name) || !iface.ips.is_empty() { + preferred.push(iface.clone()); + } else { + others.push(iface.clone()); + } + } + preferred.extend(others); + if preferred.is_empty() { + interfaces.to_vec() + } else { + preferred } - preferred.into_iter().next() } #[cfg(feature = "pcap")] diff --git a/docs/dns_leak_implementation_status.md b/docs/dns_leak_implementation_status.md index 4536473..d54b6e3 100644 --- a/docs/dns_leak_implementation_status.md +++ b/docs/dns_leak_implementation_status.md @@ -25,8 +25,11 @@ This document tracks the current DNS leak detector implementation against the de - `dns leak watch --iface-diag` (diagnostics for capture-capable interfaces). - Interface selection: - per-interface open timeout to avoid capture hangs - - stable default pick (up, non-loopback, named ethernet/wlan) before fallback scan + - ordered scan prefers non-loopback + named ethernet/wlan and interfaces with IPs - verbose logging of interface selection attempts (use `-v` / `-vv`) + - overall watch timeout accounts for worst-case interface scan time +- Capture loop: + - receiver runs in a worker thread; main loop polls with a short timeout to avoid blocking ## Partially implemented - Route/interface classification: heuristic only (loopback/tunnel/physical by iface name).