Fix: main thread timeout early than work thread

This commit is contained in:
DaZuo0122
2026-01-17 19:07:10 +08:00
parent 144e801e13
commit d5b92ede7b
2 changed files with 52 additions and 32 deletions

View File

@@ -14,6 +14,11 @@ use pnet::datalink::{self, Channel, Config as DatalinkConfig};
#[cfg(feature = "pcap")] #[cfg(feature = "pcap")]
use std::sync::mpsc; 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"))] #[cfg(not(feature = "pcap"))]
pub async fn capture_events(_options: &LeakWatchOptions) -> Result<Vec<ClassifiedEvent>, DnsLeakError> { pub async fn capture_events(_options: &LeakWatchOptions) -> Result<Vec<ClassifiedEvent>, DnsLeakError> {
Err(DnsLeakError::NotSupported( Err(DnsLeakError::NotSupported(
@@ -24,8 +29,13 @@ pub async fn capture_events(_options: &LeakWatchOptions) -> Result<Vec<Classifie
#[cfg(feature = "pcap")] #[cfg(feature = "pcap")]
pub async fn capture_events(options: &LeakWatchOptions) -> Result<Vec<ClassifiedEvent>, DnsLeakError> { pub async fn capture_events(options: &LeakWatchOptions) -> Result<Vec<ClassifiedEvent>, DnsLeakError> {
let options = options.clone(); let options = options.clone();
let candidates = format_iface_list(&datalink::interfaces()); let iface_list = datalink::interfaces();
let timeout_ms = options.duration_ms.saturating_add(2000); 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)); let handle = tokio::task::spawn_blocking(move || capture_events_blocking(options));
match tokio::time::timeout(Duration::from_millis(timeout_ms), handle).await { match tokio::time::timeout(Duration::from_millis(timeout_ms), handle).await {
Ok(joined) => joined.map_err(|err| DnsLeakError::Io(err.to_string()))?, Ok(joined) => joined.map_err(|err| DnsLeakError::Io(err.to_string()))?,
@@ -88,16 +98,28 @@ fn capture_events_blocking(options: LeakWatchOptions) -> Result<Vec<ClassifiedEv
let local_ips = iface.ips.iter().map(|ip| ip.ip()).collect::<Vec<_>>(); let local_ips = iface.ips.iter().map(|ip| ip.ip()).collect::<Vec<_>>();
let iface_name = iface.name.clone(); 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 deadline = Instant::now() + Duration::from_millis(options.duration_ms);
let mut events = Vec::new(); let mut events = Vec::new();
let mut seen = HashSet::new(); let mut seen = HashSet::new();
while Instant::now() < deadline { 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, Ok(frame) => frame,
Err(_) => continue, Err(_) => continue,
}; };
let ethernet = match EthernetPacket::new(frame) { let ethernet = match EthernetPacket::new(&frame) {
Some(packet) => packet, Some(packet) => packet,
None => continue, None => continue,
}; };
@@ -284,14 +306,8 @@ fn select_interface(
}); });
} }
if let Some(iface) = pick_stable_iface(&interfaces) { let ordered = order_interfaces(&interfaces);
debug!("dns leak iface pick: stable={}", iface.name); for iface in ordered.iter() {
if let Ok(channel) = open_channel_with_timeout(iface, config) {
return Ok(channel);
}
}
for iface in interfaces.iter() {
debug!("dns leak iface pick: try={}", iface.name); debug!("dns leak iface pick: try={}", iface.name);
if let Ok(channel) = open_channel_with_timeout(iface.clone(), config) { if let Ok(channel) = open_channel_with_timeout(iface.clone(), config) {
return Ok(channel); return Ok(channel);
@@ -320,7 +336,7 @@ fn open_channel_with_timeout(
let _ = tx.send((iface, result)); 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) { match rx.recv_timeout(timeout) {
Ok((iface, Ok(rx))) => Ok((iface, rx)), Ok((iface, Ok(rx))) => Ok((iface, rx)),
Ok((_iface, Err(err))) => Err(err), Ok((_iface, Err(err))) => Err(err),
@@ -340,26 +356,27 @@ fn is_named_fallback(name: &str) -> bool {
} }
#[cfg(feature = "pcap")] #[cfg(feature = "pcap")]
fn pick_stable_iface( fn order_interfaces(
interfaces: &[datalink::NetworkInterface], interfaces: &[datalink::NetworkInterface],
) -> Option<datalink::NetworkInterface> { ) -> Vec<datalink::NetworkInterface> {
let mut preferred = interfaces let mut preferred = Vec::new();
.iter() let mut others = Vec::new();
.filter(|iface| { for iface in interfaces.iter() {
iface.is_up() if iface.is_loopback() {
&& !iface.is_loopback() continue;
&& (is_named_fallback(&iface.name) || !iface.ips.is_empty()) }
}) if is_named_fallback(&iface.name) || !iface.ips.is_empty() {
.cloned() preferred.push(iface.clone());
.collect::<Vec<_>>(); } else {
if preferred.is_empty() { others.push(iface.clone());
preferred = interfaces }
.iter() }
.filter(|iface| !iface.is_loopback()) preferred.extend(others);
.cloned() if preferred.is_empty() {
.collect(); interfaces.to_vec()
} else {
preferred
} }
preferred.into_iter().next()
} }
#[cfg(feature = "pcap")] #[cfg(feature = "pcap")]

View File

@@ -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). - `dns leak watch --iface-diag` (diagnostics for capture-capable interfaces).
- Interface selection: - Interface selection:
- per-interface open timeout to avoid capture hangs - 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`) - 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 ## Partially implemented
- Route/interface classification: heuristic only (loopback/tunnel/physical by iface name). - Route/interface classification: heuristic only (loopback/tunnel/physical by iface name).