Fix: main thread timeout early than work thread
This commit is contained in:
@@ -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")]
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
Reference in New Issue
Block a user