Add: flag to make watch keep running

This commit is contained in:
DaZuo0122
2026-01-17 20:07:13 +08:00
parent f349d4b4fa
commit 94762d139a
7 changed files with 77 additions and 11 deletions

10
Cargo.lock generated
View File

@@ -2230,6 +2230,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 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]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.8" version = "0.3.8"
@@ -2504,6 +2513,7 @@ dependencies = [
"libc", "libc",
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2 0.6.1", "socket2 0.6.1",
"tokio-macros", "tokio-macros",
"windows-sys 0.61.2", "windows-sys 0.61.2",

View File

@@ -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 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 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 --duration 10s --filter example.com
wtfn dns watch --follow
wtfn dns leak status wtfn dns leak status
wtfn dns leak watch --duration 10s --profile proxy-stub wtfn dns leak watch --duration 10s --profile proxy-stub
wtfn dns leak watch --follow
wtfn dns leak report report.json wtfn dns leak report report.json
# TLS # TLS

View File

@@ -12,7 +12,7 @@ clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
time = { version = "0.3", features = ["formatting", "parsing"] } 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-core = { path = "../wtfnet-core" }
wtfnet-calc = { path = "../wtfnet-calc" } wtfnet-calc = { path = "../wtfnet-calc" }
wtfnet-geoip = { path = "../wtfnet-geoip" } wtfnet-geoip = { path = "../wtfnet-geoip" }

View File

@@ -386,6 +386,8 @@ struct DnsDetectArgs {
struct DnsWatchArgs { struct DnsWatchArgs {
#[arg(long, default_value = "30s", help = "Capture duration")] #[arg(long, default_value = "30s", help = "Capture duration")]
duration: String, duration: String,
#[arg(long, help = "Keep running until Ctrl-C")]
follow: bool,
#[arg(long, help = "Capture interface name")] #[arg(long, help = "Capture interface name")]
iface: Option<String>, iface: Option<String>,
#[arg(long, help = "Filter by domain substring")] #[arg(long, help = "Filter by domain substring")]
@@ -404,6 +406,8 @@ struct DnsLeakStatusArgs {
struct DnsLeakWatchArgs { struct DnsLeakWatchArgs {
#[arg(long, default_value = "10s", help = "Capture duration")] #[arg(long, default_value = "10s", help = "Capture duration")]
duration: String, duration: String,
#[arg(long, help = "Keep running until Ctrl-C")]
follow: bool,
#[arg(long, help = "Capture interface name")] #[arg(long, help = "Capture interface name")]
iface: Option<String>, iface: Option<String>,
#[arg(long, help = "Policy profile (full-tunnel|proxy-stub|split)")] #[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 { 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, Ok(value) => value,
Err(err) => { Err(err) => {
eprintln!("{err}"); eprintln!("{err}");
@@ -2019,11 +2023,27 @@ async fn handle_dns_watch(cli: &Cli, args: DnsWatchArgs) -> i32 {
filter: args.filter.clone(), 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) => { Ok(report) => {
if cli.json { if cli.json {
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false); let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
let mut command_args = vec!["--duration".to_string(), args.duration]; 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 { if let Some(iface) = args.iface {
command_args.push("--iface".to_string()); command_args.push("--iface".to_string());
command_args.push(iface); command_args.push(iface);
@@ -2154,7 +2174,7 @@ async fn handle_dns_leak_watch(cli: &Cli, args: DnsLeakWatchArgs) -> i32 {
if args.iface_diag { if args.iface_diag {
return handle_dns_leak_iface_diag(cli).await; 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, Ok(value) => value,
Err(err) => { Err(err) => {
eprintln!("{err}"); eprintln!("{err}");
@@ -2189,12 +2209,29 @@ async fn handle_dns_leak_watch(cli: &Cli, args: DnsLeakWatchArgs) -> i32 {
include_events: !args.summary_only, include_events: !args.summary_only,
}; };
let report = match wtfnet_dnsleak::watch(options, Some(&*platform.flow_owner)).await { 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, Ok(report) => report,
Err(err) => { Err(err) => {
eprintln!("dns leak watch failed: {err}"); eprintln!("dns leak watch failed: {err}");
return ExitKind::Failed.code(); return ExitKind::Failed.code();
} }
}
} else {
match watch_task.await {
Ok(report) => report,
Err(err) => {
eprintln!("dns leak watch failed: {err}");
return ExitKind::Failed.code();
}
}
}; };
if let Some(path) = args.out.as_ref() { if let Some(path) = args.out.as_ref() {
@@ -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".to_string());
command_args.push(iface); command_args.push(iface);
} }
if args.follow {
command_args.push("--follow".to_string());
}
if let Some(profile) = args.profile { if let Some(profile) = args.profile {
command_args.push("--profile".to_string()); command_args.push("--profile".to_string());
command_args.push(profile); command_args.push(profile);
@@ -3122,6 +3162,13 @@ fn parse_duration_ms(value: &str) -> Result<u64, String> {
Ok(ms) Ok(ms)
} }
fn resolve_follow_duration(follow: bool, duration: &str) -> Result<u64, String> {
if follow {
return Ok(u64::MAX / 4);
}
parse_duration_ms(duration)
}
fn format_answers_geoip(answers: &[DnsAnswerGeoIp]) -> String { fn format_answers_geoip(answers: &[DnsAnswerGeoIp]) -> String {
if answers.is_empty() { if answers.is_empty() {
return "-".to_string(); return "-".to_string();

View File

@@ -42,9 +42,9 @@ This document lists CLI commands and supported flags. Output defaults to text; u
## dns ## dns
- `dns query <domain> <type>` flags: `--server <ip[:port]>`, `--transport <udp|tcp|dot|doh>`, `--tls-name <name>`, `--socks5 <url>`, `--prefer-ipv4`, `--timeout-ms <n>` - `dns query <domain> <type>` flags: `--server <ip[:port]>`, `--transport <udp|tcp|dot|doh>`, `--tls-name <name>`, `--socks5 <url>`, `--prefer-ipv4`, `--timeout-ms <n>`
- `dns detect <domain>` flags: `--servers <csv>`, `--transport <udp|tcp|dot|doh>`, `--tls-name <name>`, `--socks5 <url>`, `--prefer-ipv4`, `--repeat <n>`, `--timeout-ms <n>` - `dns detect <domain>` flags: `--servers <csv>`, `--transport <udp|tcp|dot|doh>`, `--tls-name <name>`, `--socks5 <url>`, `--prefer-ipv4`, `--repeat <n>`, `--timeout-ms <n>`
- `dns watch` flags: `--duration <Ns|Nms>`, `--iface <name>`, `--filter <pattern>` - `dns watch` flags: `--duration <Ns|Nms>`, `--follow` (run until Ctrl-C), `--iface <name>`, `--filter <pattern>`
- `dns leak status` flags: `--profile <full-tunnel|proxy-stub|split>`, `--policy <path>` - `dns leak status` flags: `--profile <full-tunnel|proxy-stub|split>`, `--policy <path>`
- `dns leak watch` flags: `--duration <Ns|Nms>`, `--iface <name>`, `--profile <full-tunnel|proxy-stub|split>`, `--policy <path>`, `--privacy <full|redacted|minimal>`, `--out <path>`, `--summary-only`, `--iface-diag` (list capture-capable interfaces) - `dns leak watch` flags: `--duration <Ns|Nms>`, `--follow` (run until Ctrl-C), `--iface <name>`, `--profile <full-tunnel|proxy-stub|split>`, `--policy <path>`, `--privacy <full|redacted|minimal>`, `--out <path>`, `--summary-only`, `--iface-diag` (list capture-capable interfaces)
- `dns leak report` flags: `<path>`, `--privacy <full|redacted|minimal>` - `dns leak report` flags: `<path>`, `--privacy <full|redacted|minimal>`
## http ## http

View File

@@ -156,6 +156,11 @@ Add under `dns` command group:
- summary report (human) by default - summary report (human) by default
- `--json` returns structured report with events list - `--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 ## 9) Recommended incremental build plan
Phase 1 (core passive detection): Phase 1 (core passive detection):

View File

@@ -24,6 +24,8 @@ This document tracks the current DNS leak detector implementation against the de
- `dns leak watch` - `dns leak watch`
- `dns leak report` - `dns leak report`
- `dns leak watch --iface-diag` (diagnostics for capture-capable interfaces). - `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: - Interface selection:
- per-interface open timeout to avoid capture hangs - per-interface open timeout to avoid capture hangs
- ordered scan prefers non-loopback + named ethernet/wlan and interfaces with IPs - ordered scan prefers non-loopback + named ethernet/wlan and interfaces with IPs