Add: per-hop and rdns for probe trace
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2921,6 +2921,7 @@ dependencies = [
|
|||||||
name = "wtfnet-probe"
|
name = "wtfnet-probe"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"hickory-resolver",
|
||||||
"libc",
|
"libc",
|
||||||
"pnet",
|
"pnet",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -82,7 +82,7 @@ Command flags (implemented):
|
|||||||
- `neigh list`: `--ipv4`, `--ipv6`, `--iface <name>`
|
- `neigh list`: `--ipv4`, `--ipv6`, `--iface <name>`
|
||||||
- `probe ping`: `--count <n>`, `--timeout-ms <n>`, `--interval-ms <n>`, `--no-geoip`
|
- `probe ping`: `--count <n>`, `--timeout-ms <n>`, `--interval-ms <n>`, `--no-geoip`
|
||||||
- `probe tcping`: `--count <n>`, `--timeout-ms <n>`, `--socks5 <url>`, `--prefer-ipv4`, `--no-geoip`
|
- `probe tcping`: `--count <n>`, `--timeout-ms <n>`, `--socks5 <url>`, `--prefer-ipv4`, `--no-geoip`
|
||||||
- `probe trace`: `--max-hops <n>`, `--timeout-ms <n>`, `--udp`, `--port <n>`, `--no-geoip`
|
- `probe trace`: `--max-hops <n>`, `--per-hop <n>`, `--timeout-ms <n>`, `--udp`, `--port <n>`, `--rdns`, `--no-geoip`
|
||||||
- `dns query`: `--server <ip[:port]>`, `--transport <udp|tcp|dot|doh>`, `--tls-name <name>`, `--socks5 <url>`, `--prefer-ipv4`, `--timeout-ms <n>`
|
- `dns query`: `--server <ip[:port]>`, `--transport <udp|tcp|dot|doh>`, `--tls-name <name>`, `--socks5 <url>`, `--prefer-ipv4`, `--timeout-ms <n>`
|
||||||
- `dns detect`: `--servers <csv>`, `--transport <udp|tcp|dot|doh>`, `--tls-name <name>`, `--socks5 <url>`, `--prefer-ipv4`, `--repeat <n>`, `--timeout-ms <n>`
|
- `dns detect`: `--servers <csv>`, `--transport <udp|tcp|dot|doh>`, `--tls-name <name>`, `--socks5 <url>`, `--prefer-ipv4`, `--repeat <n>`, `--timeout-ms <n>`
|
||||||
- `dns watch`: `--duration <Ns|Nms>`, `--iface <name>`, `--filter <pattern>`
|
- `dns watch`: `--duration <Ns|Nms>`, `--iface <name>`, `--filter <pattern>`
|
||||||
@@ -129,11 +129,13 @@ cmake --build build --target install
|
|||||||
- diag: bundle export (zip)
|
- diag: bundle export (zip)
|
||||||
|
|
||||||
### v0.3 (future upgrades)
|
### v0.3 (future upgrades)
|
||||||
- richer trace output (reverse lookup, per-hop loss)
|
- richer trace output (reverse lookup, per-hop loss, per-hop stats)
|
||||||
- TLS extras: OCSP stapling indicator, more chain parsing
|
- HTTP timing accuracy (connect/tls)
|
||||||
|
- TLS extras: OCSP stapling indicator, richer cert parsing
|
||||||
- ports conns improvements (top talkers / summary)
|
- ports conns improvements (top talkers / summary)
|
||||||
- better baseline/diff for system roots
|
- better baseline/diff for system roots
|
||||||
- smarter "diagnose <domain>" workflow mode
|
- optional HTTP/3 (feature-gated)
|
||||||
|
- optional LLMNR/NBNS discovery
|
||||||
|
|
||||||
## Current stage
|
## Current stage
|
||||||
Implemented:
|
Implemented:
|
||||||
|
|||||||
@@ -238,6 +238,8 @@ struct ProbeTraceArgs {
|
|||||||
target: String,
|
target: String,
|
||||||
#[arg(long, default_value_t = 30)]
|
#[arg(long, default_value_t = 30)]
|
||||||
max_hops: u8,
|
max_hops: u8,
|
||||||
|
#[arg(long, default_value_t = 3)]
|
||||||
|
per_hop: u32,
|
||||||
#[arg(long, default_value_t = 800)]
|
#[arg(long, default_value_t = 800)]
|
||||||
timeout_ms: u64,
|
timeout_ms: u64,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -245,6 +247,8 @@ struct ProbeTraceArgs {
|
|||||||
#[arg(long, default_value_t = 33434)]
|
#[arg(long, default_value_t = 33434)]
|
||||||
port: u16,
|
port: u16,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
rdns: bool,
|
||||||
|
#[arg(long)]
|
||||||
no_geoip: bool,
|
no_geoip: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,9 +1080,25 @@ async fn handle_probe_trace(cli: &Cli, args: ProbeTraceArgs) -> i32 {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = if args.udp {
|
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 {
|
} 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 {
|
match result {
|
||||||
@@ -1088,7 +1108,30 @@ async fn handle_probe_trace(cli: &Cli, args: ProbeTraceArgs) -> i32 {
|
|||||||
}
|
}
|
||||||
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 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);
|
let envelope = CommandEnvelope::new(meta, command, report);
|
||||||
emit_json(cli, &envelope)
|
emit_json(cli, &envelope)
|
||||||
} else {
|
} else {
|
||||||
@@ -1102,17 +1145,30 @@ async fn handle_probe_trace(cli: &Cli, args: ProbeTraceArgs) -> i32 {
|
|||||||
}
|
}
|
||||||
for hop in report.hops {
|
for hop in report.hops {
|
||||||
let geoip = hop.geoip.as_ref().map(format_geoip);
|
let geoip = hop.geoip.as_ref().map(format_geoip);
|
||||||
println!(
|
let addr = hop.addr.unwrap_or_else(|| "*".to_string());
|
||||||
"ttl={} addr={} rtt={}ms {}{}",
|
let rtt = if let (Some(min), Some(avg), Some(max)) =
|
||||||
hop.ttl,
|
(hop.min_ms, hop.avg_ms, hop.max_ms)
|
||||||
hop.addr.unwrap_or_else(|| "*".to_string()),
|
{
|
||||||
hop.rtt_ms
|
format!("{min}/{avg:.1}/{max}ms")
|
||||||
.map(|v| v.to_string())
|
} else {
|
||||||
.unwrap_or_else(|| "-".to_string()),
|
"-".to_string()
|
||||||
hop.note.unwrap_or_default(),
|
};
|
||||||
geoip
|
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}"))
|
.map(|value| format!(" geoip={value}"))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default();
|
||||||
|
println!(
|
||||||
|
"ttl={} addr={} loss={:.1}% rtt(min/avg/max)={}{}{}{}",
|
||||||
|
hop.ttl, addr, hop.loss_pct, rtt, rdns, note, geoip
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ExitKind::Ok.code()
|
ExitKind::Ok.code()
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ libc = "0.2"
|
|||||||
tokio-socks = "0.5"
|
tokio-socks = "0.5"
|
||||||
url = "2"
|
url = "2"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
hickory-resolver = { version = "0.24", features = ["system-config"] }
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ use pnet::transport::{
|
|||||||
use std::os::unix::io::AsRawFd;
|
use std::os::unix::io::AsRawFd;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use socket2::{Domain, Protocol, Socket, Type};
|
use socket2::{Domain, Protocol, Socket, Type};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::mem::size_of_val;
|
use std::mem::size_of_val;
|
||||||
use std::time::{Duration, Instant};
|
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 thiserror::Error;
|
||||||
use tokio::net::{TcpStream, lookup_host};
|
use tokio::net::{TcpStream, lookup_host};
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
@@ -93,6 +97,12 @@ pub struct TraceHop {
|
|||||||
pub ttl: u8,
|
pub ttl: u8,
|
||||||
pub addr: Option<String>,
|
pub addr: Option<String>,
|
||||||
pub rtt_ms: Option<u128>,
|
pub rtt_ms: Option<u128>,
|
||||||
|
pub rtt_samples: Vec<Option<u128>>,
|
||||||
|
pub min_ms: Option<u128>,
|
||||||
|
pub avg_ms: Option<f64>,
|
||||||
|
pub max_ms: Option<u128>,
|
||||||
|
pub loss_pct: f64,
|
||||||
|
pub rdns: Option<String>,
|
||||||
pub note: Option<String>,
|
pub note: Option<String>,
|
||||||
pub geoip: Option<GeoIpRecord>,
|
pub geoip: Option<GeoIpRecord>,
|
||||||
}
|
}
|
||||||
@@ -105,6 +115,8 @@ pub struct TraceReport {
|
|||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub max_hops: u8,
|
pub max_hops: u8,
|
||||||
pub timeout_ms: u64,
|
pub timeout_ms: u64,
|
||||||
|
pub per_hop: u32,
|
||||||
|
pub rdns: bool,
|
||||||
pub protocol: String,
|
pub protocol: String,
|
||||||
pub hops: Vec<TraceHop>,
|
pub hops: Vec<TraceHop>,
|
||||||
}
|
}
|
||||||
@@ -308,6 +320,8 @@ pub async fn tcp_trace(
|
|||||||
port: u16,
|
port: u16,
|
||||||
max_hops: u8,
|
max_hops: u8,
|
||||||
timeout_ms: u64,
|
timeout_ms: u64,
|
||||||
|
per_hop: u32,
|
||||||
|
rdns: bool,
|
||||||
) -> Result<TraceReport, ProbeError> {
|
) -> Result<TraceReport, ProbeError> {
|
||||||
debug!(
|
debug!(
|
||||||
target,
|
target,
|
||||||
@@ -321,8 +335,17 @@ pub async fn tcp_trace(
|
|||||||
let socket_addr = SocketAddr::new(addr, port);
|
let socket_addr = SocketAddr::new(addr, port);
|
||||||
let timeout_dur = Duration::from_millis(timeout_ms);
|
let timeout_dur = Duration::from_millis(timeout_ms);
|
||||||
let mut hops = Vec::new();
|
let mut hops = Vec::new();
|
||||||
|
let mut rdns_lookup = if rdns {
|
||||||
|
Some(ReverseDns::new(timeout_dur)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
for ttl in 1..=max_hops {
|
for ttl in 1..=max_hops {
|
||||||
|
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 addr = socket_addr;
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let result =
|
let result =
|
||||||
@@ -333,25 +356,60 @@ pub async fn tcp_trace(
|
|||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let rtt = start.elapsed().as_millis();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
hops.push(TraceHop {
|
||||||
ttl,
|
ttl,
|
||||||
addr: Some(socket_addr.ip().to_string()),
|
addr: Some(socket_addr.ip().to_string()),
|
||||||
rtt_ms: Some(rtt),
|
rtt_ms,
|
||||||
note: None,
|
rtt_samples: samples,
|
||||||
|
min_ms,
|
||||||
|
avg_ms,
|
||||||
|
max_ms,
|
||||||
|
loss_pct,
|
||||||
|
rdns: rdns_name,
|
||||||
|
note,
|
||||||
geoip: None,
|
geoip: None,
|
||||||
});
|
});
|
||||||
break;
|
|
||||||
}
|
debug!(
|
||||||
Err(err) => {
|
|
||||||
let rtt = start.elapsed().as_millis();
|
|
||||||
hops.push(TraceHop {
|
|
||||||
ttl,
|
ttl,
|
||||||
addr: None,
|
loss_pct,
|
||||||
rtt_ms: Some(rtt),
|
min_ms = ?min_ms,
|
||||||
note: Some(err.to_string()),
|
avg_ms = ?avg_ms,
|
||||||
geoip: None,
|
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,
|
port,
|
||||||
max_hops,
|
max_hops,
|
||||||
timeout_ms,
|
timeout_ms,
|
||||||
|
per_hop,
|
||||||
|
rdns,
|
||||||
protocol: "tcp".to_string(),
|
protocol: "tcp".to_string(),
|
||||||
hops,
|
hops,
|
||||||
})
|
})
|
||||||
@@ -372,6 +432,8 @@ pub async fn udp_trace(
|
|||||||
port: u16,
|
port: u16,
|
||||||
max_hops: u8,
|
max_hops: u8,
|
||||||
timeout_ms: u64,
|
timeout_ms: u64,
|
||||||
|
per_hop: u32,
|
||||||
|
rdns: bool,
|
||||||
) -> Result<TraceReport, ProbeError> {
|
) -> Result<TraceReport, ProbeError> {
|
||||||
debug!(
|
debug!(
|
||||||
target,
|
target,
|
||||||
@@ -385,8 +447,21 @@ pub async fn udp_trace(
|
|||||||
|
|
||||||
let timeout_dur = Duration::from_millis(timeout_ms);
|
let timeout_dur = Duration::from_millis(timeout_ms);
|
||||||
let mut hops = Vec::new();
|
let mut hops = Vec::new();
|
||||||
|
let mut rdns_lookup = if rdns {
|
||||||
|
Some(ReverseDns::new(timeout_dur)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
for ttl in 1..=max_hops {
|
for ttl in 1..=max_hops {
|
||||||
|
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();
|
||||||
|
|
||||||
|
for _ in 0..per_hop.max(1) {
|
||||||
let addr = SocketAddr::new(addr, port);
|
let addr = SocketAddr::new(addr, port);
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let result = tokio::task::spawn_blocking(move || udp_trace_hop(addr, ttl, timeout_dur))
|
let result = tokio::task::spawn_blocking(move || udp_trace_hop(addr, ttl, timeout_dur))
|
||||||
@@ -394,28 +469,80 @@ pub async fn udp_trace(
|
|||||||
.map_err(|err| ProbeError::Io(err.to_string()))?;
|
.map_err(|err| ProbeError::Io(err.to_string()))?;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok((hop_addr, reached)) => {
|
Ok((addr, reached)) => {
|
||||||
let rtt = start.elapsed().as_millis();
|
let rtt = start.elapsed().as_millis();
|
||||||
hops.push(TraceHop {
|
debug!(
|
||||||
ttl,
|
ttl,
|
||||||
addr: hop_addr.map(|ip| ip.to_string()),
|
addr = ?addr,
|
||||||
rtt_ms: Some(rtt),
|
rtt_ms = rtt,
|
||||||
note: None,
|
reached,
|
||||||
geoip: None,
|
"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 {
|
if reached {
|
||||||
break;
|
reached_any = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
let message = err.to_string();
|
||||||
|
debug!(ttl, error = %message, "probe udp trace hop error");
|
||||||
|
last_error = Some(message);
|
||||||
|
samples.push(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 {
|
hops.push(TraceHop {
|
||||||
ttl,
|
ttl,
|
||||||
addr: None,
|
addr: hop_addr.map(|ip| ip.to_string()),
|
||||||
rtt_ms: None,
|
rtt_ms,
|
||||||
note: Some(err.to_string()),
|
rtt_samples: samples,
|
||||||
|
min_ms,
|
||||||
|
avg_ms,
|
||||||
|
max_ms,
|
||||||
|
loss_pct,
|
||||||
|
rdns: rdns_name,
|
||||||
|
note,
|
||||||
geoip: None,
|
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,
|
port,
|
||||||
max_hops,
|
max_hops,
|
||||||
timeout_ms,
|
timeout_ms,
|
||||||
|
per_hop,
|
||||||
|
rdns,
|
||||||
protocol: "udp".to_string(),
|
protocol: "udp".to_string(),
|
||||||
hops,
|
hops,
|
||||||
})
|
})
|
||||||
@@ -458,6 +587,35 @@ fn build_summary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stats_from_samples(
|
||||||
|
samples: &[Option<u128>],
|
||||||
|
) -> (Option<u128>, Option<f64>, Option<u128>, 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<IpAddr, ProbeError> {
|
async fn resolve_one(target: &str) -> Result<IpAddr, ProbeError> {
|
||||||
let mut iter = lookup_host((target, 0))
|
let mut iter = lookup_host((target, 0))
|
||||||
.await
|
.await
|
||||||
@@ -483,6 +641,40 @@ async fn resolve_one_prefer_ipv4(target: &str) -> Result<IpAddr, ProbeError> {
|
|||||||
fallback.ok_or_else(|| ProbeError::Resolve("no address found".to_string()))
|
fallback.ok_or_else(|| ProbeError::Resolve("no address found".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ReverseDns {
|
||||||
|
resolver: TokioAsyncResolver,
|
||||||
|
cache: HashMap<IpAddr, Option<String>>,
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReverseDns {
|
||||||
|
fn new(timeout: Duration) -> Result<Self, ProbeError> {
|
||||||
|
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<String> {
|
||||||
|
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 {
|
struct Socks5Proxy {
|
||||||
addr: String,
|
addr: String,
|
||||||
remote_dns: bool,
|
remote_dns: bool,
|
||||||
|
|||||||
171
docs/RELEASE_v0.3.0.md
Normal file
171
docs/RELEASE_v0.3.0.md
Normal file
@@ -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 <n>`: 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 <n>` 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.
|
||||||
44
docs/WORK_ITEMS_v0.3.0.md
Normal file
44
docs/WORK_ITEMS_v0.3.0.md
Normal file
@@ -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 <n>` 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 <n>` 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
|
||||||
267
docs/requirement_docs_v0.3.md
Normal file
267
docs/requirement_docs_v0.3.md
Normal file
@@ -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 <n>`: 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 <n>` 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 <n>` 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 <n>` 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
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
@@ -21,11 +21,13 @@ This document tracks the planned roadmap alongside the current implementation st
|
|||||||
- diag: bundle export (zip)
|
- diag: bundle export (zip)
|
||||||
|
|
||||||
### v0.3 (future upgrades)
|
### v0.3 (future upgrades)
|
||||||
- richer trace output (reverse lookup, per-hop loss)
|
- richer trace output (reverse lookup, per-hop loss, per-hop stats)
|
||||||
- TLS extras: OCSP stapling indicator, more chain parsing
|
- HTTP timing accuracy (connect/tls)
|
||||||
|
- TLS extras: OCSP stapling indicator, richer cert parsing
|
||||||
- ports conns improvements (top talkers / summary)
|
- ports conns improvements (top talkers / summary)
|
||||||
- better baseline/diff for system roots
|
- better baseline/diff for system roots
|
||||||
- smarter "diagnose <domain>" workflow mode
|
- optional HTTP/3 (feature-gated)
|
||||||
|
- optional LLMNR/NBNS discovery
|
||||||
|
|
||||||
## Current stage
|
## 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.
|
- Basic unit tests for calc and TLS parsing.
|
||||||
|
|
||||||
### In progress
|
### In progress
|
||||||
- None.
|
- v0.3: probe trace upgrades (per-hop stats + rdns).
|
||||||
|
|
||||||
### Next
|
### Next
|
||||||
|
- Complete v0.3 trace upgrades and update CLI output.
|
||||||
- Add v0.2 tests (dns detect, basic http/tls smoke).
|
- Add v0.2 tests (dns detect, basic http/tls smoke).
|
||||||
- Validate http/tls timings and improve breakdown if possible.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user