Add: H3 support - incomplete
This commit is contained in:
@@ -11,6 +11,7 @@ path = "src/main.rs"
|
||||
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"] }
|
||||
wtfnet-core = { path = "../wtfnet-core" }
|
||||
wtfnet-calc = { path = "../wtfnet-calc" }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::path::PathBuf;
|
||||
use wtfnet_core::{
|
||||
@@ -97,6 +97,7 @@ enum SysCommand {
|
||||
enum PortsCommand {
|
||||
Listen(PortsListenArgs),
|
||||
Who(PortsWhoArgs),
|
||||
Conns(PortsConnsArgs),
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
@@ -107,6 +108,8 @@ enum NeighCommand {
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum CertCommand {
|
||||
Roots,
|
||||
Baseline(CertBaselineArgs),
|
||||
Diff(CertDiffArgs),
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
@@ -190,6 +193,14 @@ struct PortsWhoArgs {
|
||||
target: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct PortsConnsArgs {
|
||||
#[arg(long)]
|
||||
top: Option<usize>,
|
||||
#[arg(long)]
|
||||
by_process: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct NeighListArgs {
|
||||
#[arg(long)]
|
||||
@@ -205,6 +216,16 @@ struct GeoIpLookupArgs {
|
||||
target: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct CertBaselineArgs {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct CertDiffArgs {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct ProbePingArgs {
|
||||
target: String,
|
||||
@@ -339,6 +360,10 @@ struct HttpRequestArgs {
|
||||
#[arg(long)]
|
||||
http2_only: bool,
|
||||
#[arg(long)]
|
||||
http3: bool,
|
||||
#[arg(long)]
|
||||
http3_only: bool,
|
||||
#[arg(long)]
|
||||
geoip: bool,
|
||||
#[arg(long)]
|
||||
socks5: Option<String>,
|
||||
@@ -359,6 +384,10 @@ struct TlsArgs {
|
||||
socks5: Option<String>,
|
||||
#[arg(long)]
|
||||
prefer_ipv4: bool,
|
||||
#[arg(long)]
|
||||
show_extensions: bool,
|
||||
#[arg(long)]
|
||||
ocsp: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
@@ -450,6 +479,7 @@ struct HttpReportGeoIp {
|
||||
pub geoip: Vec<wtfnet_geoip::GeoIpRecord>,
|
||||
pub headers: Vec<(String, String)>,
|
||||
pub body: Option<String>,
|
||||
pub warnings: Vec<String>,
|
||||
pub timing: wtfnet_http::HttpTiming,
|
||||
}
|
||||
|
||||
@@ -481,12 +511,21 @@ async fn main() {
|
||||
Commands::Ports {
|
||||
command: PortsCommand::Who(args),
|
||||
} => handle_ports_who(&cli, args.clone()).await,
|
||||
Commands::Ports {
|
||||
command: PortsCommand::Conns(args),
|
||||
} => handle_ports_conns(&cli, args.clone()).await,
|
||||
Commands::Neigh {
|
||||
command: NeighCommand::List(args),
|
||||
} => handle_neigh_list(&cli, args.clone()).await,
|
||||
Commands::Cert {
|
||||
command: CertCommand::Roots,
|
||||
} => handle_cert_roots(&cli).await,
|
||||
Commands::Cert {
|
||||
command: CertCommand::Baseline(args),
|
||||
} => handle_cert_baseline(&cli, args.clone()).await,
|
||||
Commands::Cert {
|
||||
command: CertCommand::Diff(args),
|
||||
} => handle_cert_diff(&cli, args.clone()).await,
|
||||
Commands::Geoip {
|
||||
command: GeoIpCommand::Lookup(args),
|
||||
} => handle_geoip_lookup(&cli, args.clone()).await,
|
||||
@@ -820,6 +859,99 @@ async fn handle_ports_who(cli: &Cli, args: PortsWhoArgs) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ports_conns(cli: &Cli, args: PortsConnsArgs) -> i32 {
|
||||
let result = platform().ports.connections().await;
|
||||
match result {
|
||||
Ok(conns) => {
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let mut command_args = Vec::new();
|
||||
if let Some(top) = args.top {
|
||||
command_args.push("--top".to_string());
|
||||
command_args.push(top.to_string());
|
||||
}
|
||||
if args.by_process {
|
||||
command_args.push("--by-process".to_string());
|
||||
}
|
||||
let command = CommandInfo::new("ports conns", command_args);
|
||||
let envelope = CommandEnvelope::new(meta, command, conns);
|
||||
emit_json(cli, &envelope)
|
||||
} else if args.by_process {
|
||||
let summary = summarize_by_process(&conns);
|
||||
for (name, count) in summary {
|
||||
println!("{name} {count}");
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
} else if let Some(top) = args.top {
|
||||
let summary = summarize_top_remote(&conns, top);
|
||||
for (addr, count) in summary {
|
||||
println!("{addr} {count}");
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
} else {
|
||||
for conn in conns {
|
||||
let state = conn.state.unwrap_or_else(|| "-".to_string());
|
||||
let pid = conn
|
||||
.pid
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let proc = conn
|
||||
.process_name
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
println!(
|
||||
"{} {} -> {} {} pid={} proc={}",
|
||||
conn.proto, conn.local_addr, conn.remote_addr, state, pid, proc
|
||||
);
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => emit_platform_error(cli, err),
|
||||
}
|
||||
}
|
||||
|
||||
fn summarize_top_remote(
|
||||
conns: &[wtfnet_platform::ConnSocket],
|
||||
top: usize,
|
||||
) -> Vec<(String, usize)> {
|
||||
let mut counts = std::collections::HashMap::new();
|
||||
for conn in conns {
|
||||
let host = parse_host_from_socket(&conn.remote_addr);
|
||||
*counts.entry(host).or_insert(0usize) += 1;
|
||||
}
|
||||
let mut items = counts.into_iter().collect::<Vec<_>>();
|
||||
items.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
||||
items.truncate(top);
|
||||
items
|
||||
}
|
||||
|
||||
fn summarize_by_process(conns: &[wtfnet_platform::ConnSocket]) -> Vec<(String, usize)> {
|
||||
let mut counts = std::collections::HashMap::new();
|
||||
for conn in conns {
|
||||
let name = conn
|
||||
.process_name
|
||||
.clone()
|
||||
.or_else(|| conn.pid.map(|value| format!("pid:{value}")))
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
*counts.entry(name).or_insert(0usize) += 1;
|
||||
}
|
||||
let mut items = counts.into_iter().collect::<Vec<_>>();
|
||||
items.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
||||
items
|
||||
}
|
||||
|
||||
fn parse_host_from_socket(value: &str) -> String {
|
||||
if let Some(stripped) = value.strip_prefix('[') {
|
||||
if let Some(end) = stripped.find(']') {
|
||||
return stripped[..end].to_string();
|
||||
}
|
||||
}
|
||||
if let Some((host, _port)) = value.rsplit_once(':') {
|
||||
return host.to_string();
|
||||
}
|
||||
value.to_string()
|
||||
}
|
||||
|
||||
async fn handle_neigh_list(cli: &Cli, args: NeighListArgs) -> i32 {
|
||||
let result = platform().neigh.neighbors().await;
|
||||
match result {
|
||||
@@ -882,6 +1014,212 @@ async fn handle_cert_roots(cli: &Cli) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CertBaseline {
|
||||
schema_version: u32,
|
||||
created_at: String,
|
||||
roots: Vec<wtfnet_platform::RootCert>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct CertChange {
|
||||
sha256: String,
|
||||
field: String,
|
||||
baseline: String,
|
||||
current: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct CertDiffReport {
|
||||
baseline_path: String,
|
||||
baseline_count: usize,
|
||||
current_count: usize,
|
||||
added: Vec<wtfnet_platform::RootCert>,
|
||||
removed: Vec<wtfnet_platform::RootCert>,
|
||||
changed: Vec<CertChange>,
|
||||
newly_expired: Vec<wtfnet_platform::RootCert>,
|
||||
schema_version: u32,
|
||||
}
|
||||
|
||||
async fn handle_cert_baseline(cli: &Cli, args: CertBaselineArgs) -> i32 {
|
||||
let result = platform().cert.trusted_roots().await;
|
||||
match result {
|
||||
Ok(roots) => {
|
||||
let baseline = CertBaseline {
|
||||
schema_version: 1,
|
||||
created_at: now_rfc3339(),
|
||||
roots,
|
||||
};
|
||||
match serde_json::to_string_pretty(&baseline) {
|
||||
Ok(payload) => match std::fs::write(&args.path, payload) {
|
||||
Ok(()) => ExitKind::Ok.code(),
|
||||
Err(err) => {
|
||||
eprintln!("failed to write baseline: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("failed to serialize baseline: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => emit_platform_error(cli, err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_cert_diff(cli: &Cli, args: CertDiffArgs) -> i32 {
|
||||
let baseline = match std::fs::read_to_string(&args.path) {
|
||||
Ok(contents) => match serde_json::from_str::<CertBaseline>(&contents) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
eprintln!("failed to parse baseline: {err}");
|
||||
return ExitKind::Failed.code();
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("failed to read baseline: {err}");
|
||||
return ExitKind::Failed.code();
|
||||
}
|
||||
};
|
||||
|
||||
let current = match platform().cert.trusted_roots().await {
|
||||
Ok(value) => value,
|
||||
Err(err) => return emit_platform_error(cli, err),
|
||||
};
|
||||
|
||||
let baseline_map = baseline
|
||||
.roots
|
||||
.iter()
|
||||
.map(|cert| (cert.sha256.clone(), cert))
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
let current_map = current
|
||||
.iter()
|
||||
.map(|cert| (cert.sha256.clone(), cert))
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
|
||||
let mut added = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
let mut changed = Vec::new();
|
||||
let mut newly_expired = Vec::new();
|
||||
|
||||
for cert in ¤t {
|
||||
if !baseline_map.contains_key(&cert.sha256) {
|
||||
added.push(cert.clone());
|
||||
}
|
||||
}
|
||||
for cert in &baseline.roots {
|
||||
if !current_map.contains_key(&cert.sha256) {
|
||||
removed.push(cert.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (sha, base) in &baseline_map {
|
||||
if let Some(curr) = current_map.get(sha) {
|
||||
if base.subject != curr.subject {
|
||||
changed.push(CertChange {
|
||||
sha256: sha.clone(),
|
||||
field: "subject".to_string(),
|
||||
baseline: base.subject.clone(),
|
||||
current: curr.subject.clone(),
|
||||
});
|
||||
}
|
||||
if base.issuer != curr.issuer {
|
||||
changed.push(CertChange {
|
||||
sha256: sha.clone(),
|
||||
field: "issuer".to_string(),
|
||||
baseline: base.issuer.clone(),
|
||||
current: curr.issuer.clone(),
|
||||
});
|
||||
}
|
||||
if base.not_after != curr.not_after {
|
||||
changed.push(CertChange {
|
||||
sha256: sha.clone(),
|
||||
field: "not_after".to_string(),
|
||||
baseline: base.not_after.clone(),
|
||||
current: curr.not_after.clone(),
|
||||
});
|
||||
}
|
||||
if base.not_before != curr.not_before {
|
||||
changed.push(CertChange {
|
||||
sha256: sha.clone(),
|
||||
field: "not_before".to_string(),
|
||||
baseline: base.not_before.clone(),
|
||||
current: curr.not_before.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if let (Some(created), Some(expiry)) = (
|
||||
parse_cert_time(&baseline.created_at),
|
||||
parse_cert_time(&curr.not_after),
|
||||
) {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
if created < expiry && now >= expiry {
|
||||
newly_expired.push((*curr).clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let report = CertDiffReport {
|
||||
baseline_path: args.path.to_string_lossy().to_string(),
|
||||
baseline_count: baseline.roots.len(),
|
||||
current_count: current.len(),
|
||||
added,
|
||||
removed,
|
||||
changed,
|
||||
newly_expired,
|
||||
schema_version: baseline.schema_version,
|
||||
};
|
||||
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let command = CommandInfo::new("cert diff", vec![report.baseline_path.clone()]);
|
||||
let envelope = CommandEnvelope::new(meta, command, report);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
println!(
|
||||
"baseline_count={} current_count={} added={} removed={} changed={} newly_expired={}",
|
||||
report.baseline_count,
|
||||
report.current_count,
|
||||
report.added.len(),
|
||||
report.removed.len(),
|
||||
report.changed.len(),
|
||||
report.newly_expired.len()
|
||||
);
|
||||
for cert in report.added {
|
||||
println!("added {} {}", cert.sha256, cert.subject);
|
||||
}
|
||||
for cert in report.removed {
|
||||
println!("removed {} {}", cert.sha256, cert.subject);
|
||||
}
|
||||
for change in report.changed {
|
||||
println!(
|
||||
"changed {} {} {} -> {}",
|
||||
change.sha256, change.field, change.baseline, change.current
|
||||
);
|
||||
}
|
||||
for cert in report.newly_expired {
|
||||
println!("expired {} {}", cert.sha256, cert.subject);
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cert_time(value: &str) -> Option<time::OffsetDateTime> {
|
||||
if let Ok(dt) = time::OffsetDateTime::parse(value, &time::format_description::well_known::Rfc3339) {
|
||||
return Some(dt);
|
||||
}
|
||||
let format = time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second] UTC").ok()?;
|
||||
time::OffsetDateTime::parse(value, &format).ok()
|
||||
}
|
||||
|
||||
fn now_rfc3339() -> String {
|
||||
time::OffsetDateTime::now_utc()
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
|
||||
}
|
||||
|
||||
async fn handle_geoip_lookup(cli: &Cli, args: GeoIpLookupArgs) -> i32 {
|
||||
let ip = match args.target.parse::<std::net::IpAddr>() {
|
||||
Ok(ip) => ip,
|
||||
@@ -1821,6 +2159,8 @@ async fn handle_http_request(
|
||||
show_body: args.show_body,
|
||||
http1_only: args.http1_only,
|
||||
http2_only: args.http2_only,
|
||||
http3: args.http3,
|
||||
http3_only: args.http3_only,
|
||||
proxy: args.socks5.clone(),
|
||||
};
|
||||
|
||||
@@ -1844,6 +2184,7 @@ async fn handle_http_request(
|
||||
geoip,
|
||||
headers: report.headers.clone(),
|
||||
body: report.body.clone(),
|
||||
warnings: report.warnings.clone(),
|
||||
timing: report.timing.clone(),
|
||||
}
|
||||
} else {
|
||||
@@ -1857,6 +2198,7 @@ async fn handle_http_request(
|
||||
geoip: Vec::new(),
|
||||
headers: report.headers.clone(),
|
||||
body: report.body.clone(),
|
||||
warnings: report.warnings.clone(),
|
||||
timing: report.timing.clone(),
|
||||
}
|
||||
};
|
||||
@@ -1883,6 +2225,11 @@ async fn handle_http_request(
|
||||
if !report.resolved_ips.is_empty() {
|
||||
println!("resolved: {}", report.resolved_ips.join(", "));
|
||||
}
|
||||
if !report.warnings.is_empty() {
|
||||
for warning in &report.warnings {
|
||||
println!("warning: {warning}");
|
||||
}
|
||||
}
|
||||
println!("total_ms: {}", report.timing.total_ms);
|
||||
if let Some(ms) = report.timing.dns_ms {
|
||||
println!("dns_ms: {ms}");
|
||||
@@ -1973,6 +2320,8 @@ fn build_tls_options(args: &TlsArgs) -> wtfnet_tls::TlsOptions {
|
||||
insecure: args.insecure,
|
||||
socks5: args.socks5.clone(),
|
||||
prefer_ipv4: args.prefer_ipv4,
|
||||
show_extensions: args.show_extensions,
|
||||
ocsp: args.ocsp,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user