From c538e31174118500a7d8cd244c7f7bd2d3c4ef8c Mon Sep 17 00:00:00 2001 From: DaZuo0122 <1085701449@qq.com> Date: Sat, 17 Jan 2026 12:51:41 +0800 Subject: [PATCH] Add: per-hop and rdns for probe trace --- Cargo.lock | 1 + README.md | 10 +- crates/wtfnet-cli/src/main.rs | 82 +++++++-- crates/wtfnet-probe/Cargo.toml | 1 + crates/wtfnet-probe/src/lib.rs | 294 +++++++++++++++++++++++++++------ docs/RELEASE_v0.3.0.md | 171 +++++++++++++++++++ docs/WORK_ITEMS_v0.3.0.md | 44 +++++ docs/requirement_docs_v0.3.md | 267 ++++++++++++++++++++++++++++++ docs/status.md | 12 +- 9 files changed, 809 insertions(+), 73 deletions(-) create mode 100644 docs/RELEASE_v0.3.0.md create mode 100644 docs/WORK_ITEMS_v0.3.0.md create mode 100644 docs/requirement_docs_v0.3.md diff --git a/Cargo.lock b/Cargo.lock index 683ae47..fd52d49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2921,6 +2921,7 @@ dependencies = [ name = "wtfnet-probe" version = "0.1.0" dependencies = [ + "hickory-resolver", "libc", "pnet", "serde", diff --git a/README.md b/README.md index 5c41b50..6fbe051 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Command flags (implemented): - `neigh list`: `--ipv4`, `--ipv6`, `--iface ` - `probe ping`: `--count `, `--timeout-ms `, `--interval-ms `, `--no-geoip` - `probe tcping`: `--count `, `--timeout-ms `, `--socks5 `, `--prefer-ipv4`, `--no-geoip` -- `probe trace`: `--max-hops `, `--timeout-ms `, `--udp`, `--port `, `--no-geoip` +- `probe trace`: `--max-hops `, `--per-hop `, `--timeout-ms `, `--udp`, `--port `, `--rdns`, `--no-geoip` - `dns query`: `--server `, `--transport `, `--tls-name `, `--socks5 `, `--prefer-ipv4`, `--timeout-ms ` - `dns detect`: `--servers `, `--transport `, `--tls-name `, `--socks5 `, `--prefer-ipv4`, `--repeat `, `--timeout-ms ` - `dns watch`: `--duration `, `--iface `, `--filter ` @@ -129,11 +129,13 @@ cmake --build build --target install - diag: bundle export (zip) ### v0.3 (future upgrades) -- richer trace output (reverse lookup, per-hop loss) -- TLS extras: OCSP stapling indicator, more chain parsing +- richer trace output (reverse lookup, per-hop loss, per-hop stats) +- HTTP timing accuracy (connect/tls) +- TLS extras: OCSP stapling indicator, richer cert parsing - ports conns improvements (top talkers / summary) - better baseline/diff for system roots -- smarter "diagnose " workflow mode +- optional HTTP/3 (feature-gated) +- optional LLMNR/NBNS discovery ## Current stage Implemented: diff --git a/crates/wtfnet-cli/src/main.rs b/crates/wtfnet-cli/src/main.rs index a9a4f07..39b3462 100644 --- a/crates/wtfnet-cli/src/main.rs +++ b/crates/wtfnet-cli/src/main.rs @@ -238,6 +238,8 @@ struct ProbeTraceArgs { target: String, #[arg(long, default_value_t = 30)] max_hops: u8, + #[arg(long, default_value_t = 3)] + per_hop: u32, #[arg(long, default_value_t = 800)] timeout_ms: u64, #[arg(long)] @@ -245,6 +247,8 @@ struct ProbeTraceArgs { #[arg(long, default_value_t = 33434)] port: u16, #[arg(long)] + rdns: bool, + #[arg(long)] no_geoip: bool, } @@ -1076,9 +1080,25 @@ async fn handle_probe_trace(cli: &Cli, args: ProbeTraceArgs) -> i32 { }; let result = if args.udp { - wtfnet_probe::udp_trace(&host, port, args.max_hops, args.timeout_ms).await + wtfnet_probe::udp_trace( + &host, + port, + args.max_hops, + args.timeout_ms, + args.per_hop, + args.rdns, + ) + .await } else { - wtfnet_probe::tcp_trace(&host, port, args.max_hops, args.timeout_ms).await + wtfnet_probe::tcp_trace( + &host, + port, + args.max_hops, + args.timeout_ms, + args.per_hop, + args.rdns, + ) + .await }; match result { @@ -1088,7 +1108,30 @@ async fn handle_probe_trace(cli: &Cli, args: ProbeTraceArgs) -> i32 { } if cli.json { let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false); - let command = CommandInfo::new("probe trace", vec![args.target]); + let mut command_args = vec![args.target]; + if args.udp { + command_args.push("--udp".to_string()); + } + if args.rdns { + command_args.push("--rdns".to_string()); + } + if args.per_hop != 3 { + command_args.push("--per-hop".to_string()); + command_args.push(args.per_hop.to_string()); + } + if args.max_hops != 30 { + command_args.push("--max-hops".to_string()); + command_args.push(args.max_hops.to_string()); + } + if args.timeout_ms != 800 { + command_args.push("--timeout-ms".to_string()); + command_args.push(args.timeout_ms.to_string()); + } + if args.udp && args.port != 33434 { + command_args.push("--port".to_string()); + command_args.push(args.port.to_string()); + } + let command = CommandInfo::new("probe trace", command_args); let envelope = CommandEnvelope::new(meta, command, report); emit_json(cli, &envelope) } else { @@ -1102,17 +1145,30 @@ async fn handle_probe_trace(cli: &Cli, args: ProbeTraceArgs) -> i32 { } for hop in report.hops { let geoip = hop.geoip.as_ref().map(format_geoip); + let addr = hop.addr.unwrap_or_else(|| "*".to_string()); + let rtt = if let (Some(min), Some(avg), Some(max)) = + (hop.min_ms, hop.avg_ms, hop.max_ms) + { + format!("{min}/{avg:.1}/{max}ms") + } else { + "-".to_string() + }; + let rdns = hop + .rdns + .as_ref() + .map(|value| format!(" rdns={value}")) + .unwrap_or_default(); + let note = hop + .note + .as_ref() + .map(|value| format!(" note={value}")) + .unwrap_or_default(); + let geoip = geoip + .map(|value| format!(" geoip={value}")) + .unwrap_or_default(); println!( - "ttl={} addr={} rtt={}ms {}{}", - hop.ttl, - hop.addr.unwrap_or_else(|| "*".to_string()), - hop.rtt_ms - .map(|v| v.to_string()) - .unwrap_or_else(|| "-".to_string()), - hop.note.unwrap_or_default(), - geoip - .map(|value| format!(" geoip={value}")) - .unwrap_or_default() + "ttl={} addr={} loss={:.1}% rtt(min/avg/max)={}{}{}{}", + hop.ttl, addr, hop.loss_pct, rtt, rdns, note, geoip ); } ExitKind::Ok.code() diff --git a/crates/wtfnet-probe/Cargo.toml b/crates/wtfnet-probe/Cargo.toml index be07eff..15c9f11 100644 --- a/crates/wtfnet-probe/Cargo.toml +++ b/crates/wtfnet-probe/Cargo.toml @@ -15,3 +15,4 @@ libc = "0.2" tokio-socks = "0.5" url = "2" tracing = "0.1" +hickory-resolver = { version = "0.24", features = ["system-config"] } diff --git a/crates/wtfnet-probe/src/lib.rs b/crates/wtfnet-probe/src/lib.rs index 904c08f..c9c47e0 100644 --- a/crates/wtfnet-probe/src/lib.rs +++ b/crates/wtfnet-probe/src/lib.rs @@ -13,10 +13,14 @@ use pnet::transport::{ use std::os::unix::io::AsRawFd; use serde::{Deserialize, Serialize}; use socket2::{Domain, Protocol, Socket, Type}; +use std::collections::{HashMap, HashSet}; use std::net::{IpAddr, SocketAddr}; #[cfg(unix)] use std::mem::size_of_val; use std::time::{Duration, Instant}; +use hickory_resolver::config::{ResolverConfig, ResolverOpts}; +use hickory_resolver::system_conf::read_system_conf; +use hickory_resolver::TokioAsyncResolver; use thiserror::Error; use tokio::net::{TcpStream, lookup_host}; use tokio::time::timeout; @@ -93,6 +97,12 @@ pub struct TraceHop { pub ttl: u8, pub addr: Option, pub rtt_ms: Option, + pub rtt_samples: Vec>, + pub min_ms: Option, + pub avg_ms: Option, + pub max_ms: Option, + pub loss_pct: f64, + pub rdns: Option, pub note: Option, pub geoip: Option, } @@ -105,6 +115,8 @@ pub struct TraceReport { pub port: u16, pub max_hops: u8, pub timeout_ms: u64, + pub per_hop: u32, + pub rdns: bool, pub protocol: String, pub hops: Vec, } @@ -308,6 +320,8 @@ pub async fn tcp_trace( port: u16, max_hops: u8, timeout_ms: u64, + per_hop: u32, + rdns: bool, ) -> Result { debug!( target, @@ -321,37 +335,81 @@ pub async fn tcp_trace( let socket_addr = SocketAddr::new(addr, port); let timeout_dur = Duration::from_millis(timeout_ms); let mut hops = Vec::new(); + let mut rdns_lookup = if rdns { + Some(ReverseDns::new(timeout_dur)?) + } else { + None + }; for ttl in 1..=max_hops { - let addr = socket_addr; - let start = Instant::now(); - let result = - tokio::task::spawn_blocking(move || tcp_connect_with_ttl(addr, ttl, timeout_dur)) - .await - .map_err(|err| ProbeError::Io(err.to_string()))?; + debug!(ttl, per_hop, "probe tcp trace hop start"); + let mut samples = Vec::new(); + let mut last_error = None; + for _ in 0..per_hop.max(1) { + let addr = socket_addr; + let start = Instant::now(); + let result = + tokio::task::spawn_blocking(move || tcp_connect_with_ttl(addr, ttl, timeout_dur)) + .await + .map_err(|err| ProbeError::Io(err.to_string()))?; - match result { - Ok(()) => { - let rtt = start.elapsed().as_millis(); - hops.push(TraceHop { - ttl, - addr: Some(socket_addr.ip().to_string()), - rtt_ms: Some(rtt), - note: None, - geoip: None, - }); - break; + match result { + Ok(()) => { + let rtt = start.elapsed().as_millis(); + debug!(ttl, rtt_ms = rtt, "probe tcp trace hop reply"); + samples.push(Some(rtt)); + } + Err(err) => { + let message = err.to_string(); + debug!(ttl, error = %message, "probe tcp trace hop error"); + last_error = Some(message); + samples.push(None); + } } - Err(err) => { - let rtt = start.elapsed().as_millis(); - hops.push(TraceHop { - ttl, - addr: None, - rtt_ms: Some(rtt), - note: Some(err.to_string()), - geoip: None, - }); + } + + let (min_ms, avg_ms, max_ms, loss_pct) = stats_from_samples(&samples); + let rtt_ms = avg_ms.map(|value| value.round() as u128); + let rdns_name = if rdns { + if let Some(lookup) = rdns_lookup.as_mut() { + lookup.lookup(socket_addr.ip()).await + } else { + None } + } else { + None + }; + let note = if loss_pct >= 100.0 { + last_error + } else { + None + }; + + hops.push(TraceHop { + ttl, + addr: Some(socket_addr.ip().to_string()), + rtt_ms, + rtt_samples: samples, + min_ms, + avg_ms, + max_ms, + loss_pct, + rdns: rdns_name, + note, + geoip: None, + }); + + debug!( + ttl, + loss_pct, + min_ms = ?min_ms, + avg_ms = ?avg_ms, + max_ms = ?max_ms, + "probe tcp trace hop summary" + ); + + if loss_pct < 100.0 { + break; } } @@ -362,6 +420,8 @@ pub async fn tcp_trace( port, max_hops, timeout_ms, + per_hop, + rdns, protocol: "tcp".to_string(), hops, }) @@ -372,6 +432,8 @@ pub async fn udp_trace( port: u16, max_hops: u8, timeout_ms: u64, + per_hop: u32, + rdns: bool, ) -> Result { debug!( target, @@ -385,37 +447,102 @@ pub async fn udp_trace( let timeout_dur = Duration::from_millis(timeout_ms); let mut hops = Vec::new(); + let mut rdns_lookup = if rdns { + Some(ReverseDns::new(timeout_dur)?) + } else { + None + }; for ttl in 1..=max_hops { - let addr = SocketAddr::new(addr, port); - let start = Instant::now(); - let result = tokio::task::spawn_blocking(move || udp_trace_hop(addr, ttl, timeout_dur)) - .await - .map_err(|err| ProbeError::Io(err.to_string()))?; + debug!(ttl, per_hop, "probe udp trace hop start"); + let mut samples = Vec::new(); + let mut hop_addr = None; + let mut reached_any = false; + let mut last_error = None; + let mut addr_set = HashSet::new(); - match result { - Ok((hop_addr, reached)) => { - let rtt = start.elapsed().as_millis(); - hops.push(TraceHop { - ttl, - addr: hop_addr.map(|ip| ip.to_string()), - rtt_ms: Some(rtt), - note: None, - geoip: None, - }); - if reached { - break; + for _ in 0..per_hop.max(1) { + let addr = SocketAddr::new(addr, port); + let start = Instant::now(); + let result = tokio::task::spawn_blocking(move || udp_trace_hop(addr, ttl, timeout_dur)) + .await + .map_err(|err| ProbeError::Io(err.to_string()))?; + + match result { + Ok((addr, reached)) => { + let rtt = start.elapsed().as_millis(); + debug!( + ttl, + addr = ?addr, + rtt_ms = rtt, + reached, + "probe udp trace hop reply" + ); + samples.push(Some(rtt)); + if let Some(ip) = addr { + addr_set.insert(ip); + if hop_addr.is_none() { + hop_addr = Some(ip); + } + } + if reached { + reached_any = true; + } + } + Err(err) => { + let message = err.to_string(); + debug!(ttl, error = %message, "probe udp trace hop error"); + last_error = Some(message); + samples.push(None); } } - Err(err) => { - hops.push(TraceHop { - ttl, - addr: None, - rtt_ms: None, - note: Some(err.to_string()), - geoip: None, - }); + } + + let (min_ms, avg_ms, max_ms, loss_pct) = stats_from_samples(&samples); + let rtt_ms = avg_ms.map(|value| value.round() as u128); + let rdns_name = if rdns { + if let (Some(ip), Some(lookup)) = (hop_addr, rdns_lookup.as_mut()) { + lookup.lookup(ip).await + } else { + None } + } else { + None + }; + let note = if loss_pct >= 100.0 { + last_error + } else if addr_set.len() > 1 { + Some("multiple hop addresses".to_string()) + } else { + None + }; + + hops.push(TraceHop { + ttl, + addr: hop_addr.map(|ip| ip.to_string()), + rtt_ms, + rtt_samples: samples, + min_ms, + avg_ms, + max_ms, + loss_pct, + rdns: rdns_name, + note, + geoip: None, + }); + + debug!( + ttl, + loss_pct, + min_ms = ?min_ms, + avg_ms = ?avg_ms, + max_ms = ?max_ms, + reached_any, + "probe udp trace hop summary" + ); + + if reached_any { + break; } } @@ -426,6 +553,8 @@ pub async fn udp_trace( port, max_hops, timeout_ms, + per_hop, + rdns, protocol: "udp".to_string(), hops, }) @@ -458,6 +587,35 @@ fn build_summary( } } +fn stats_from_samples( + samples: &[Option], +) -> (Option, Option, Option, f64) { + let mut min = None; + let mut max = None; + let mut sum = 0u128; + let mut received = 0u32; + for sample in samples { + if let Some(rtt) = sample { + received += 1; + min = Some(min.map_or(*rtt, |value: u128| value.min(*rtt))); + max = Some(max.map_or(*rtt, |value: u128| value.max(*rtt))); + sum += *rtt; + } + } + let sent = samples.len() as u32; + let loss_pct = if sent == 0 { + 0.0 + } else { + ((sent - received) as f64 / sent as f64) * 100.0 + }; + let avg_ms = if received == 0 { + None + } else { + Some(sum as f64 / received as f64) + }; + (min, avg_ms, max, loss_pct) +} + async fn resolve_one(target: &str) -> Result { let mut iter = lookup_host((target, 0)) .await @@ -483,6 +641,40 @@ async fn resolve_one_prefer_ipv4(target: &str) -> Result { fallback.ok_or_else(|| ProbeError::Resolve("no address found".to_string())) } +struct ReverseDns { + resolver: TokioAsyncResolver, + cache: HashMap>, + timeout: Duration, +} + +impl ReverseDns { + fn new(timeout: Duration) -> Result { + let (config, opts) = match read_system_conf() { + Ok((config, opts)) => (config, opts), + Err(_) => (ResolverConfig::default(), ResolverOpts::default()), + }; + let resolver = TokioAsyncResolver::tokio(config, opts); + Ok(Self { + resolver, + cache: HashMap::new(), + timeout, + }) + } + + async fn lookup(&mut self, ip: IpAddr) -> Option { + if let Some(value) = self.cache.get(&ip) { + return value.clone(); + } + let result = timeout(self.timeout, self.resolver.reverse_lookup(ip)).await; + let value = match result { + Ok(Ok(response)) => response.iter().next().map(|name| name.to_utf8()), + _ => None, + }; + self.cache.insert(ip, value.clone()); + value + } +} + struct Socks5Proxy { addr: String, remote_dns: bool, diff --git a/docs/RELEASE_v0.3.0.md b/docs/RELEASE_v0.3.0.md new file mode 100644 index 0000000..476b68f --- /dev/null +++ b/docs/RELEASE_v0.3.0.md @@ -0,0 +1,171 @@ +# WTFnet v0.3.0 - Release Plan + +Binary name in examples: `wtfn` (current CLI examples use this form). +Project scope: Linux (Debian/Ubuntu) + Windows first-class. + +## 0. Summary + +v0.3.0 focuses on improving diagnostic depth and fidelity of existing commands rather than adding a "smart doctor" workflow. + +Major upgrades in this release: +- richer traceroute output and per-hop statistics +- HTTP timing breakdown accuracy (connect/tls stages) +- optional HTTP/3 support (best-effort) +- TLS diagnostics upgrades (OCSP stapling indicator, richer certificate parsing) +- ports connections view and summaries +- improved cert baseline/diff for system roots +- optional discovery expansion (LLMNR/NBNS) + +## 1. Goals + +### G1. Make existing outputs more trustworthy +- Replace placeholder timing fields with real measured values where feasible. +- Improve trace reliability and readability. + +### G2. Expand diagnostics depth, not workflow complexity +- Keep subcommands explicit (no `doctor`, no guided flow). +- Focus on "give me evidence" tools. + +### G3. Keep v0.2 compatibility +- Add flags and fields in an additive way. +- Keep default behavior safe and bounded. + +## 2. Non-goals (explicitly out of scope) + +- No `wtfn doctor ...` / one-shot diagnosis command (move to v0.4+). +- No shell completion scripts or man page generation. +- No new output modes like JSONL streaming / schema negotiation changes (stay stable). +- No OS-native TLS verifier in v0.3.0 (optional future enhancement). + +## 3. Feature scope + +### 3.1 probe trace: richer output (MUST) +Current: trace exists best-effort. +Target improvements: +- `--rdns`: reverse DNS lookup per hop (best-effort; cached; time-bounded) +- `--per-hop `: send N probes per hop (default 3) to compute: + - avg/min/max RTT per hop + - loss % per hop +- `--icmp` and `--udp` modes remain best-effort; document privilege requirements +- Keep `--geoip` integration: hop IP -> Country/ASN + +Acceptance: +- output includes per-hop loss and stable hop formatting +- JSON output contains hop arrays with RTT series + +### 3.2 HTTP timing breakdown accuracy (MUST) +Current: `dns_ms` + `ttfb_ms` exist, but connect/tls are placeholders. +Target: +- implement `connect_ms` and `tls_ms` (best-effort) for HTTP/1.1 and HTTP/2 +- keep total duration correct and stable +- when measurement unavailable (library limitation), report: + - `null` + add warning, never fake numbers + +Acceptance: +- `wtfn http head|get` JSON contains: + - `dns_ms`, `connect_ms`, `tls_ms`, `ttfb_ms`, `total_ms` +- on timeout / failure, partial timing must still be meaningful. + +### 3.3 HTTP/3 (optional feature flag) (SHOULD) +Current: HTTP/3 not implemented. +Target: +- add `--http3` support behind Cargo feature `http3` +- behavior: + - `--http3-only`: fail if HTTP/3 cannot be used + - `--http3`: try HTTP/3, fallback to HTTP/2 unless `--http3-only` +- provide clear error classes: + - UDP blocked, QUIC handshake timeout, TLS/ALPN mismatch, etc. + +Acceptance: +- builds without `http3` feature still work +- with feature enabled, HTTP/3 works on at least one known compatible endpoint + +### 3.4 TLS extras: OCSP + richer cert parsing (MUST) +Current: `tls handshake/verify/cert/alpn` exists. +Target: +- show OCSP stapling presence (if exposed by library) +- richer certificate parsing for leaf and intermediates: + - SANs (DNS/IP) + - key usage / extended key usage (best-effort) + - signature algorithm (best-effort) +- new flags: + - `--show-extensions` (prints richer X.509 info) + - `--ocsp` (show stapling info if present) + +Acceptance: +- TLS output includes richer leaf cert details when requested +- `--show-chain` remains fast and bounded + +### 3.5 ports conns: active connection view + summaries (SHOULD) +Current: `ports listen/who`. +Target: +- add `wtfn ports conns` + - show active TCP connections with: + - local addr:port + - remote addr:port + - state (ESTABLISHED/TIME_WAIT/etc) + - PID/process name (best-effort) +- add summary mode: + - `--top ` show top remote IPs by count + - `--by-process` group by process + +Acceptance: +- works on Linux + Windows best-effort +- never requires admin by default; if needed, return partial with warnings + +### 3.6 cert roots: stronger baseline/diff (MUST) +Current: cert roots listing exists; baseline/diff exists. +Target improvements: +- normalize matching key: SHA256 fingerprint +- diff categories: + - added / removed + - changed validity (newly expired) + - subject/issuer changes +- add stable JSON schema for baseline files (include schema version) + +Acceptance: +- baseline diff is stable across platforms (best-effort fields allowed) +- diff output is human-friendly and JSON-friendly + +### 3.7 discover: LLMNR/NBNS (optional) (NICE) +Current: mDNS + SSDP exist; LLMNR/NBNS missing. +Target: +- add `wtfn discover llmnr --duration 3s` +- add `wtfn discover nbns --duration 3s` +- bounded, low-noise, rate-limited + +Acceptance: +- best-effort implementation on Windows-first networks +- if unsupported on OS, show "not supported" error with exit code 5 (partial) + +## 4. Compatibility & behavior rules + +- Command names must remain stable. +- Existing flags must retain meaning. +- JSON output fields are additive only. +- Logging remains stderr-only; JSON output remains clean stdout. + +## 5. Deliverables checklist + +MUST: +- trace richer output + per-hop loss stats +- HTTP connect/tls timing best-effort with warnings when unknown +- TLS extras: OCSP indicator + richer x509 parsing +- ports conns basic implementation +- cert baseline/diff improvements + +SHOULD: +- HTTP/3 behind feature flag + +NICE: +- LLMNR/NBNS discovery + +## 6. Definition of Done (v0.3.0) + +- v0.3.0 builds on Linux (Debian/Ubuntu) + Windows. +- `wtfn probe trace` provides per-hop loss and optional rdns. +- `wtfn http head|get` reports accurate timing breakdown where possible. +- `wtfn tls ...` provides OCSP + SAN/extensions when requested. +- `wtfn ports conns` works best-effort and produces useful output. +- cert baseline/diff is stable and readable. +- No doctor command, no completions, no new output modes. diff --git a/docs/WORK_ITEMS_v0.3.0.md b/docs/WORK_ITEMS_v0.3.0.md new file mode 100644 index 0000000..386ba9c --- /dev/null +++ b/docs/WORK_ITEMS_v0.3.0.md @@ -0,0 +1,44 @@ +# WTFnet v0.3.0 - Work Items + +This is a practical checklist to execute v0.3.0. + +## 1) probe/trace upgrades +- [ ] add `--per-hop ` and store RTT samples per hop +- [ ] compute loss% per hop +- [ ] add `--rdns` best-effort reverse lookup (cached + time-bounded) +- [ ] improve hop formatting + JSON schema + +## 2) http timing improvements +- [ ] implement `connect_ms` and `tls_ms` timing +- [ ] report `null` + warning when measurement unavailable +- [ ] keep current `dns_ms` and `ttfb_ms` + +## 3) optional HTTP/3 +- [ ] add `http3` cargo feature + deps +- [ ] implement `--http3` / `--http3-only` +- [ ] define error classification for QUIC failures + +## 4) tls extras +- [ ] add OCSP stapling presence indicator (if available) +- [ ] parse SANs and key usage / EKU best-effort +- [ ] add `--show-extensions` and `--ocsp` flags + +## 5) ports conns +- [ ] implement `wtfn ports conns` +- [ ] add `--top ` and `--by-process` +- [ ] best-effort PID mapping with warnings + +## 6) cert baseline/diff improvements +- [ ] baseline schema version +- [ ] match by SHA256 fingerprint +- [ ] diff categories: add/remove/expired/changed + +## 7) optional LLMNR/NBNS +- [ ] implement `discover llmnr` +- [ ] implement `discover nbns` +- [ ] bounded collection, low-noise + +## 8) docs updates +- [ ] update README roadmap +- [ ] update COMMANDS.md with new flags/commands +- [ ] add RELEASE_v0.3.0.md diff --git a/docs/requirement_docs_v0.3.md b/docs/requirement_docs_v0.3.md new file mode 100644 index 0000000..fddb6ab --- /dev/null +++ b/docs/requirement_docs_v0.3.md @@ -0,0 +1,267 @@ +Below are **repo-ready v0.3.0 docs**. +They’re written to match your **current implemented feature set** (v0.2 mostly done: sys/ports/neigh/certs/geoip/probe/dns/http/tls/discover/diag/calc). +They also respect your decision to **NOT include** in v0.3.0: + +* shell completions / man pages +* the “smart one-shot doctor” command +* extra output ergonomics for automation + +And they target the **real gaps / next upgrades** you still have: **HTTP/3**, deeper **HTTP timing**, **TLS extras**, richer **trace**, **ports conns**, better **cert baseline/diff**, and optionally **LLMNR/NBNS discovery** (currently missing). + +--- + +## 1) `docs/RELEASE_v0.3.0.md` + +```markdown +# WTFnet v0.3.0 — Release Plan + +Binary name in examples: `wtfn` (current CLI examples use this form). +Project scope: Linux (Debian/Ubuntu) + Windows first-class. + +## 0. Summary + +v0.3.0 focuses on improving **diagnostic depth and fidelity** of existing commands rather than adding a "smart doctor" workflow. + +Major upgrades in this release: +- richer traceroute output and per-hop statistics +- HTTP timing breakdown accuracy (connect/tls stages) +- optional HTTP/3 support (best-effort) +- TLS diagnostics upgrades (OCSP stapling indicator, richer certificate parsing) +- ports connections view and summaries +- improved cert baseline/diff for system roots +- optional discovery expansion (LLMNR/NBNS) + +--- + +## 1. Goals + +### G1. Make existing outputs more trustworthy +- Replace placeholder timing fields with real measured values where feasible. +- Improve trace reliability and readability. + +### G2. Expand diagnostics depth, not workflow complexity +- Keep subcommands explicit (no `doctor`, no guided flow). +- Focus on "give me evidence" tools. + +### G3. Keep v0.2 compatibility +- Add flags and fields in an additive way. +- Keep default behavior safe and bounded. + +--- + +## 2. Non-goals (explicitly out of scope) + +- No `wtfn doctor ...` / one-shot diagnosis command (move to v0.4+). +- No shell completion scripts or man page generation. +- No new output modes like JSONL streaming / schema negotiation changes (stay stable). +- No OS-native TLS verifier in v0.3.0 (optional future enhancement). + +--- + +## 3. Feature scope + +### 3.1 probe trace: richer output (MUST) +Current: trace exists best-effort. +Target improvements: +- `--rdns`: reverse DNS lookup per hop (best-effort; cached; time-bounded) +- `--per-hop `: send N probes per hop (default 3) to compute: + - avg/min/max RTT per hop + - loss % per hop +- `--icmp` and `--udp` modes remain best-effort; document privilege requirements +- Keep `--geoip` integration: hop IP → Country/ASN + +Acceptance: +- output includes per-hop loss and stable hop formatting +- JSON output contains hop arrays with RTT series + +--- + +### 3.2 HTTP timing breakdown accuracy (MUST) +Current: `dns_ms` + `ttfb_ms` exist, but connect/tls are placeholders. +Target: +- implement `connect_ms` and `tls_ms` (best-effort) for HTTP/1.1 and HTTP/2 +- keep total duration correct and stable +- when measurement unavailable (library limitation), report: + - `null` + add warning, never fake numbers + +Acceptance: +- `wtfn http head|get` JSON contains: + - `dns_ms`, `connect_ms`, `tls_ms`, `ttfb_ms`, `total_ms` +- on timeout / failure, partial timing must still be meaningful. + +--- + +### 3.3 HTTP/3 (optional feature flag) (SHOULD) +Current: HTTP/3 not implemented. +Target: +- add `--http3` support behind Cargo feature `http3` +- behavior: + - `--http3-only`: fail if HTTP/3 cannot be used + - `--http3`: try HTTP/3, fallback to HTTP/2 unless `--http3-only` +- provide clear error classes: + - UDP blocked, QUIC handshake timeout, TLS/ALPN mismatch, etc. + +Acceptance: +- builds without `http3` feature still work +- with feature enabled, HTTP/3 works on at least one known compatible endpoint + +--- + +### 3.4 TLS extras: OCSP + richer cert parsing (MUST) +Current: `tls handshake/verify/cert/alpn` exists. +Target: +- show OCSP stapling presence (if exposed by library) +- richer certificate parsing for leaf and intermediates: + - SANs (DNS/IP) + - key usage / extended key usage (best-effort) + - signature algorithm (best-effort) +- new flags: + - `--show-extensions` (prints richer X.509 info) + - `--ocsp` (show stapling info if present) + +Acceptance: +- TLS output includes richer leaf cert details when requested +- `--show-chain` remains fast and bounded + +--- + +### 3.5 ports conns: active connection view + summaries (SHOULD) +Current: `ports listen/who`. +Target: +- add `wtfn ports conns` + - show active TCP connections with: + - local addr:port + - remote addr:port + - state (ESTABLISHED/TIME_WAIT/etc) + - PID/process name (best-effort) +- add summary mode: + - `--top ` show top remote IPs by count + - `--by-process` group by process + +Acceptance: +- works on Linux + Windows best-effort +- never requires admin by default; if needed, return partial with warnings + +--- + +### 3.6 cert roots: stronger baseline/diff (MUST) +Current: cert roots listing exists; baseline/diff exists. +Target improvements: +- normalize matching key: SHA256 fingerprint +- diff categories: + - added / removed + - changed validity (newly expired) + - subject/issuer changes +- add stable JSON schema for baseline files (include schema version) + +Acceptance: +- baseline diff is stable across platforms (best-effort fields allowed) +- diff output is human-friendly and JSON-friendly + +--- + +### 3.7 discover: LLMNR/NBNS (optional) (NICE) +Current: mDNS + SSDP exist; LLMNR/NBNS missing. +Target: +- add `wtfn discover llmnr --duration 3s` +- add `wtfn discover nbns --duration 3s` +- bounded, low-noise, rate-limited + +Acceptance: +- best-effort implementation on Windows-first networks +- if unsupported on OS, show "not supported" error with exit code 5 (partial) + +--- + +## 4. Compatibility & behavior rules + +- Command names must remain stable. +- Existing flags must retain meaning. +- JSON output fields are additive only. +- Logging remains stderr-only; JSON output remains clean stdout. + +--- + +## 5. Deliverables checklist + +MUST: +- trace richer output + per-hop loss stats +- HTTP connect/tls timing best-effort with warnings when unknown +- TLS extras: OCSP indicator + richer x509 parsing +- ports conns basic implementation +- cert baseline/diff improvements + +SHOULD: +- HTTP/3 behind feature flag + +NICE: +- LLMNR/NBNS discovery + +--- + +## 6. Definition of Done (v0.3.0) + +- v0.3.0 builds on Linux (Debian/Ubuntu) + Windows. +- `wtfn probe trace` provides per-hop loss and optional rdns. +- `wtfn http head|get` reports accurate timing breakdown where possible. +- `wtfn tls ...` provides OCSP + SAN/extensions when requested. +- `wtfn ports conns` works best-effort and produces useful output. +- cert baseline/diff is stable and readable. +- No doctor command, no completions, no new output modes. + +``` + +--- + +## 3) `docs/WORK_ITEMS_v0.3.0.md` (engineering task list) + +```markdown +# WTFnet v0.3.0 — Work Items + +This is a practical checklist to execute v0.3.0. + +## 1) probe/trace upgrades +- [ ] add `--per-hop ` and store RTT samples per hop +- [ ] compute loss% per hop +- [ ] add `--rdns` best-effort reverse lookup (cached + time-bounded) +- [ ] improve hop formatting + JSON schema + +## 2) http timing improvements +- [ ] implement `connect_ms` and `tls_ms` timing +- [ ] report `null` + warning when measurement unavailable +- [ ] keep current `dns_ms` and `ttfb_ms` + +## 3) optional HTTP/3 +- [ ] add `http3` cargo feature + deps +- [ ] implement `--http3` / `--http3-only` +- [ ] define error classification for QUIC failures + +## 4) tls extras +- [ ] add OCSP stapling presence indicator (if available) +- [ ] parse SANs and key usage / EKU best-effort +- [ ] add `--show-extensions` and `--ocsp` flags + +## 5) ports conns +- [ ] implement `wtfn ports conns` +- [ ] add `--top ` and `--by-process` +- [ ] best-effort PID mapping with warnings + +## 6) cert baseline/diff improvements +- [ ] baseline schema version +- [ ] match by SHA256 fingerprint +- [ ] diff categories: add/remove/expired/changed + +## 7) optional LLMNR/NBNS +- [ ] implement `discover llmnr` +- [ ] implement `discover nbns` +- [ ] bounded collection, low-noise + +## 8) docs updates +- [ ] update README roadmap +- [ ] update COMMANDS.md with new flags/commands +- [ ] add RELEASE_v0.3.0.md + +``` + +--- + diff --git a/docs/status.md b/docs/status.md index e35e9fb..d682e06 100644 --- a/docs/status.md +++ b/docs/status.md @@ -21,11 +21,13 @@ This document tracks the planned roadmap alongside the current implementation st - diag: bundle export (zip) ### v0.3 (future upgrades) -- richer trace output (reverse lookup, per-hop loss) -- TLS extras: OCSP stapling indicator, more chain parsing +- richer trace output (reverse lookup, per-hop loss, per-hop stats) +- HTTP timing accuracy (connect/tls) +- TLS extras: OCSP stapling indicator, richer cert parsing - ports conns improvements (top talkers / summary) - better baseline/diff for system roots -- smarter "diagnose " workflow mode +- optional HTTP/3 (feature-gated) +- optional LLMNR/NBNS discovery ## Current stage @@ -66,8 +68,8 @@ This document tracks the planned roadmap alongside the current implementation st - Basic unit tests for calc and TLS parsing. ### In progress -- None. +- v0.3: probe trace upgrades (per-hop stats + rdns). ### Next +- Complete v0.3 trace upgrades and update CLI output. - Add v0.2 tests (dns detect, basic http/tls smoke). -- Validate http/tls timings and improve breakdown if possible.