diff --git a/Cargo.lock b/Cargo.lock index 5855fd0..2e62010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2230,6 +2230,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -2504,6 +2513,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", diff --git a/README.md b/README.md index 86aa6fd..f0c6cc9 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,10 @@ wtfn dns query example.com A --transport doh --server 1.1.1.1 --tls-name cloudfl wtfn dns query example.com A --transport dot --server 1.1.1.1 --tls-name cloudflare-dns.com --socks5 socks5://127.0.0.1:9909 wtfn dns detect example.com --transport doh --servers 1.1.1.1 --tls-name cloudflare-dns.com wtfn dns watch --duration 10s --filter example.com +wtfn dns watch --follow wtfn dns leak status wtfn dns leak watch --duration 10s --profile proxy-stub +wtfn dns leak watch --follow wtfn dns leak report report.json # TLS diff --git a/crates/wtfnet-cli/Cargo.toml b/crates/wtfnet-cli/Cargo.toml index 4ac5334..4de12c3 100644 --- a/crates/wtfnet-cli/Cargo.toml +++ b/crates/wtfnet-cli/Cargo.toml @@ -12,7 +12,7 @@ clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } serde_json = "1" time = { version = "0.3", features = ["formatting", "parsing"] } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } wtfnet-core = { path = "../wtfnet-core" } wtfnet-calc = { path = "../wtfnet-calc" } wtfnet-geoip = { path = "../wtfnet-geoip" } diff --git a/crates/wtfnet-cli/src/main.rs b/crates/wtfnet-cli/src/main.rs index ed9be0b..3c8b8f6 100644 --- a/crates/wtfnet-cli/src/main.rs +++ b/crates/wtfnet-cli/src/main.rs @@ -386,6 +386,8 @@ struct DnsDetectArgs { struct DnsWatchArgs { #[arg(long, default_value = "30s", help = "Capture duration")] duration: String, + #[arg(long, help = "Keep running until Ctrl-C")] + follow: bool, #[arg(long, help = "Capture interface name")] iface: Option, #[arg(long, help = "Filter by domain substring")] @@ -404,6 +406,8 @@ struct DnsLeakStatusArgs { struct DnsLeakWatchArgs { #[arg(long, default_value = "10s", help = "Capture duration")] duration: String, + #[arg(long, help = "Keep running until Ctrl-C")] + follow: bool, #[arg(long, help = "Capture interface name")] iface: Option, #[arg(long, help = "Policy profile (full-tunnel|proxy-stub|split)")] @@ -2006,7 +2010,7 @@ async fn handle_dns_detect(cli: &Cli, args: DnsDetectArgs) -> i32 { } async fn handle_dns_watch(cli: &Cli, args: DnsWatchArgs) -> i32 { - let duration_ms = match parse_duration_ms(&args.duration) { + let duration_ms = match resolve_follow_duration(args.follow, &args.duration) { Ok(value) => value, Err(err) => { eprintln!("{err}"); @@ -2019,11 +2023,27 @@ async fn handle_dns_watch(cli: &Cli, args: DnsWatchArgs) -> i32 { filter: args.filter.clone(), }; - match wtfnet_dns::watch(options).await { + let watch_task = wtfnet_dns::watch(options); + let report = if args.follow { + tokio::select! { + result = watch_task => result, + _ = tokio::signal::ctrl_c() => { + eprintln!("dns watch interrupted by Ctrl-C"); + return ExitKind::Ok.code(); + } + } + } else { + watch_task.await + }; + + match report { Ok(report) => { if cli.json { let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false); let mut command_args = vec!["--duration".to_string(), args.duration]; + if args.follow { + command_args.push("--follow".to_string()); + } if let Some(iface) = args.iface { command_args.push("--iface".to_string()); command_args.push(iface); @@ -2154,7 +2174,7 @@ async fn handle_dns_leak_watch(cli: &Cli, args: DnsLeakWatchArgs) -> i32 { if args.iface_diag { return handle_dns_leak_iface_diag(cli).await; } - let duration_ms = match parse_duration_ms(&args.duration) { + let duration_ms = match resolve_follow_duration(args.follow, &args.duration) { Ok(value) => value, Err(err) => { eprintln!("{err}"); @@ -2189,11 +2209,28 @@ async fn handle_dns_leak_watch(cli: &Cli, args: DnsLeakWatchArgs) -> i32 { include_events: !args.summary_only, }; - let report = match wtfnet_dnsleak::watch(options, Some(&*platform.flow_owner)).await { - Ok(report) => report, - Err(err) => { - eprintln!("dns leak watch failed: {err}"); - return ExitKind::Failed.code(); + let watch_task = wtfnet_dnsleak::watch(options, Some(&*platform.flow_owner)); + let report = if args.follow { + match tokio::select! { + result = watch_task => result, + _ = tokio::signal::ctrl_c() => { + eprintln!("dns leak watch interrupted by Ctrl-C"); + return ExitKind::Ok.code(); + } + } { + Ok(report) => report, + Err(err) => { + eprintln!("dns leak watch failed: {err}"); + return ExitKind::Failed.code(); + } + } + } else { + match watch_task.await { + Ok(report) => report, + Err(err) => { + eprintln!("dns leak watch failed: {err}"); + return ExitKind::Failed.code(); + } } }; @@ -2213,6 +2250,9 @@ async fn handle_dns_leak_watch(cli: &Cli, args: DnsLeakWatchArgs) -> i32 { command_args.push("--iface".to_string()); command_args.push(iface); } + if args.follow { + command_args.push("--follow".to_string()); + } if let Some(profile) = args.profile { command_args.push("--profile".to_string()); command_args.push(profile); @@ -3122,6 +3162,13 @@ fn parse_duration_ms(value: &str) -> Result { Ok(ms) } +fn resolve_follow_duration(follow: bool, duration: &str) -> Result { + if follow { + return Ok(u64::MAX / 4); + } + parse_duration_ms(duration) +} + fn format_answers_geoip(answers: &[DnsAnswerGeoIp]) -> String { if answers.is_empty() { return "-".to_string(); diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index e6f5f41..f5405ff 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -42,9 +42,9 @@ This document lists CLI commands and supported flags. Output defaults to text; u ## dns - `dns query ` flags: `--server `, `--transport `, `--tls-name `, `--socks5 `, `--prefer-ipv4`, `--timeout-ms ` - `dns detect ` flags: `--servers `, `--transport `, `--tls-name `, `--socks5 `, `--prefer-ipv4`, `--repeat `, `--timeout-ms ` -- `dns watch` flags: `--duration `, `--iface `, `--filter ` +- `dns watch` flags: `--duration `, `--follow` (run until Ctrl-C), `--iface `, `--filter ` - `dns leak status` flags: `--profile `, `--policy ` -- `dns leak watch` flags: `--duration `, `--iface `, `--profile `, `--policy `, `--privacy `, `--out `, `--summary-only`, `--iface-diag` (list capture-capable interfaces) +- `dns leak watch` flags: `--duration `, `--follow` (run until Ctrl-C), `--iface `, `--profile `, `--policy `, `--privacy `, `--out `, `--summary-only`, `--iface-diag` (list capture-capable interfaces) - `dns leak report` flags: ``, `--privacy ` ## http diff --git a/docs/DNS_LEAK_DETECTOR_IMPLEMENTATION.md b/docs/DNS_LEAK_DETECTOR_IMPLEMENTATION.md index a3bc13d..f2dc43b 100644 --- a/docs/DNS_LEAK_DETECTOR_IMPLEMENTATION.md +++ b/docs/DNS_LEAK_DETECTOR_IMPLEMENTATION.md @@ -156,6 +156,11 @@ Add under `dns` command group: - summary report (human) by default - `--json` returns structured report with events list +`--follow` keeps the watch running by resolving the duration to a large +placeholder (one year in milliseconds) and then racing the watch against +`tokio::signal::ctrl_c()`; Ctrl-C returns early with a clean exit code so the +outer loop stops. + ## 9) Recommended incremental build plan Phase 1 (core passive detection): diff --git a/docs/dns_leak_implementation_status.md b/docs/dns_leak_implementation_status.md index 5600bad..243c209 100644 --- a/docs/dns_leak_implementation_status.md +++ b/docs/dns_leak_implementation_status.md @@ -24,6 +24,8 @@ This document tracks the current DNS leak detector implementation against the de - `dns leak watch` - `dns leak report` - `dns leak watch --iface-diag` (diagnostics for capture-capable interfaces). +- `dns leak watch --follow` runs until Ctrl-C by combining a long duration with + a `tokio::signal::ctrl_c()` early-exit path. - Interface selection: - per-interface open timeout to avoid capture hangs - ordered scan prefers non-loopback + named ethernet/wlan and interfaces with IPs