Add: socks5 support. It may have problems with DoT, will see.
This commit is contained in:
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -2844,9 +2844,14 @@ dependencies = [
|
|||||||
"hickory-resolver",
|
"hickory-resolver",
|
||||||
"pnet",
|
"pnet",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rustls",
|
||||||
|
"rustls-native-certs 0.6.3",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tokio-socks",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2921,6 +2926,8 @@ dependencies = [
|
|||||||
"surge-ping",
|
"surge-ping",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-socks",
|
||||||
|
"url",
|
||||||
"wtfnet-geoip",
|
"wtfnet-geoip",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2934,6 +2941,8 @@ dependencies = [
|
|||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
|
"tokio-socks",
|
||||||
|
"url",
|
||||||
"x509-parser",
|
"x509-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -6,7 +6,7 @@ WTFnet is a pure CLI toolbox for diagnosing network problems on Linux and Window
|
|||||||
- System snapshot: interfaces, IPs, routes, DNS config.
|
- System snapshot: interfaces, IPs, routes, DNS config.
|
||||||
- Ports, neighbors, and trusted root certificates.
|
- Ports, neighbors, and trusted root certificates.
|
||||||
- Probing: ping, tcping, traceroute (best-effort).
|
- Probing: ping, tcping, traceroute (best-effort).
|
||||||
- DNS: query/detect/watch with GeoIP, DoT/DoH, and SOCKS5 for DoH.
|
- DNS: query/detect/watch with GeoIP, DoT/DoH, and SOCKS5 support.
|
||||||
- GeoIP offline lookup via GeoLite2 Country/ASN.
|
- GeoIP offline lookup via GeoLite2 Country/ASN.
|
||||||
- Subnet calculator: subnet/contains/overlap/summarize.
|
- Subnet calculator: subnet/contains/overlap/summarize.
|
||||||
|
|
||||||
@@ -34,17 +34,20 @@ wtfn neigh list --ipv6
|
|||||||
wtfn geoip lookup 8.8.8.8
|
wtfn geoip lookup 8.8.8.8
|
||||||
wtfn probe ping example.com --count 4
|
wtfn probe ping example.com --count 4
|
||||||
wtfn probe tcping example.com:443 --count 4
|
wtfn probe tcping example.com:443 --count 4
|
||||||
|
wtfn probe tcping example.com:443 --socks5 socks5://127.0.0.1:9909
|
||||||
wtfn probe trace example.com:443 --max-hops 20
|
wtfn probe trace example.com:443 --max-hops 20
|
||||||
|
|
||||||
# DNS
|
# DNS
|
||||||
wtfn dns query example.com A
|
wtfn dns query example.com A
|
||||||
wtfn dns query example.com AAAA --server 1.1.1.1
|
wtfn dns query example.com AAAA --server 1.1.1.1
|
||||||
wtfn dns query example.com A --transport doh --server 1.1.1.1 --tls-name cloudflare-dns.com
|
wtfn dns query example.com A --transport doh --server 1.1.1.1 --tls-name cloudflare-dns.com
|
||||||
|
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
|
||||||
|
|
||||||
# TLS
|
# TLS
|
||||||
wtfn tls handshake example.com:443
|
wtfn tls handshake example.com:443
|
||||||
|
wtfn tls handshake example.com:443 --socks5 socks5://127.0.0.1:9909
|
||||||
wtfn tls cert example.com:443
|
wtfn tls cert example.com:443
|
||||||
wtfn tls verify example.com:443
|
wtfn tls verify example.com:443
|
||||||
wtfn tls alpn example.com:443 --alpn h2,http/1.1
|
wtfn tls alpn example.com:443 --alpn h2,http/1.1
|
||||||
@@ -78,13 +81,13 @@ Command flags (implemented):
|
|||||||
- `ports listen`: `--tcp`, `--udp`, `--port <n>`
|
- `ports listen`: `--tcp`, `--udp`, `--port <n>`
|
||||||
- `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>`, `--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>`, `--timeout-ms <n>`, `--udp`, `--port <n>`, `--no-geoip`
|
||||||
- `dns query`: `--server <ip[:port]>`, `--transport <udp|tcp|dot|doh>`, `--tls-name <name>`, `--socks5 <url>`, `--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>`, `--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>`
|
||||||
- `http head|get`: `--timeout-ms <n>`, `--follow-redirects <n>`, `--show-headers`, `--show-body`, `--max-body-bytes <n>`, `--http1-only`, `--http2-only`, `--geoip`
|
- `http head|get`: `--timeout-ms <n>`, `--follow-redirects <n>`, `--show-headers`, `--show-body`, `--max-body-bytes <n>`, `--http1-only`, `--http2-only`, `--geoip`, `--socks5 <url>`
|
||||||
- `tls handshake|cert|verify|alpn`: `--sni <name>`, `--alpn <csv>`, `--timeout-ms <n>`, `--insecure`
|
- `tls handshake|cert|verify|alpn`: `--sni <name>`, `--alpn <csv>`, `--timeout-ms <n>`, `--insecure`, `--socks5 <url>`, `--prefer-ipv4`
|
||||||
- `discover mdns`: `--duration <Ns|Nms>`, `--service <type>`
|
- `discover mdns`: `--duration <Ns|Nms>`, `--service <type>`
|
||||||
- `discover ssdp`: `--duration <Ns|Nms>`
|
- `discover ssdp`: `--duration <Ns|Nms>`
|
||||||
- `diag`: `--out <path>`, `--bundle <path>`, `--dns-detect <domain>`, `--dns-timeout-ms <n>`, `--dns-repeat <n>`
|
- `diag`: `--out <path>`, `--bundle <path>`, `--dns-detect <domain>`, `--dns-timeout-ms <n>`, `--dns-repeat <n>`
|
||||||
@@ -139,7 +142,7 @@ Implemented:
|
|||||||
- geoip, probe, dns query/detect/watch.
|
- geoip, probe, dns query/detect/watch.
|
||||||
- http head/get with timing and GeoIP.
|
- http head/get with timing and GeoIP.
|
||||||
- tls handshake/verify/cert/alpn.
|
- tls handshake/verify/cert/alpn.
|
||||||
- DoT/DoH + SOCKS5 proxy for DoH.
|
- DoT/DoH + SOCKS5 proxy support.
|
||||||
- discover mdns/ssdp.
|
- discover mdns/ssdp.
|
||||||
- diag report + bundle.
|
- diag report + bundle.
|
||||||
- calc subcrate with subnet/contains/overlap/summarize.
|
- calc subcrate with subnet/contains/overlap/summarize.
|
||||||
|
|||||||
@@ -226,6 +226,10 @@ struct ProbeTcpingArgs {
|
|||||||
#[arg(long, default_value_t = 800)]
|
#[arg(long, default_value_t = 800)]
|
||||||
timeout_ms: u64,
|
timeout_ms: u64,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
socks5: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
prefer_ipv4: bool,
|
||||||
|
#[arg(long)]
|
||||||
no_geoip: bool,
|
no_geoip: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +260,8 @@ struct DnsQueryArgs {
|
|||||||
tls_name: Option<String>,
|
tls_name: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
socks5: Option<String>,
|
socks5: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
prefer_ipv4: bool,
|
||||||
#[arg(long, default_value_t = 2000)]
|
#[arg(long, default_value_t = 2000)]
|
||||||
timeout_ms: u64,
|
timeout_ms: u64,
|
||||||
}
|
}
|
||||||
@@ -271,6 +277,8 @@ struct DnsDetectArgs {
|
|||||||
tls_name: Option<String>,
|
tls_name: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
socks5: Option<String>,
|
socks5: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
prefer_ipv4: bool,
|
||||||
#[arg(long, default_value_t = 3)]
|
#[arg(long, default_value_t = 3)]
|
||||||
repeat: u32,
|
repeat: u32,
|
||||||
#[arg(long, default_value_t = 2000)]
|
#[arg(long, default_value_t = 2000)]
|
||||||
@@ -328,6 +336,8 @@ struct HttpRequestArgs {
|
|||||||
http2_only: bool,
|
http2_only: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
geoip: bool,
|
geoip: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
socks5: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
@@ -341,6 +351,10 @@ struct TlsArgs {
|
|||||||
timeout_ms: u64,
|
timeout_ms: u64,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
insecure: bool,
|
insecure: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
socks5: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
prefer_ipv4: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
@@ -988,14 +1002,31 @@ async fn handle_probe_tcping(cli: &Cli, args: ProbeTcpingArgs) -> i32 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match wtfnet_probe::tcp_ping(&host, port, args.count, args.timeout_ms).await {
|
match wtfnet_probe::tcp_ping(
|
||||||
|
&host,
|
||||||
|
port,
|
||||||
|
args.count,
|
||||||
|
args.timeout_ms,
|
||||||
|
args.socks5.as_deref(),
|
||||||
|
args.prefer_ipv4,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(mut report) => {
|
Ok(mut report) => {
|
||||||
if !args.no_geoip {
|
if !args.no_geoip {
|
||||||
enrich_tcp_geoip(&mut report);
|
enrich_tcp_geoip(&mut 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 command = CommandInfo::new("probe tcping", vec![args.target]);
|
let mut command_args = vec![args.target];
|
||||||
|
if let Some(proxy) = args.socks5 {
|
||||||
|
command_args.push("--socks5".to_string());
|
||||||
|
command_args.push(proxy);
|
||||||
|
}
|
||||||
|
if args.prefer_ipv4 {
|
||||||
|
command_args.push("--prefer-ipv4".to_string());
|
||||||
|
}
|
||||||
|
let command = CommandInfo::new("probe tcping", 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 {
|
||||||
@@ -1280,7 +1311,12 @@ async fn handle_dns_query(cli: &Cli, args: DnsQueryArgs) -> i32 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let server = match args.server.as_deref() {
|
let server = match args.server.as_deref() {
|
||||||
Some(value) => match parse_dns_server_target(value, transport, args.tls_name.as_deref()) {
|
Some(value) => match parse_dns_server_target(
|
||||||
|
value,
|
||||||
|
transport,
|
||||||
|
args.tls_name.as_deref(),
|
||||||
|
args.prefer_ipv4,
|
||||||
|
) {
|
||||||
Ok(addr) => Some(addr),
|
Ok(addr) => Some(addr),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("{err}");
|
eprintln!("{err}");
|
||||||
@@ -1360,7 +1396,14 @@ async fn handle_dns_detect(cli: &Cli, args: DnsDetectArgs) -> i32 {
|
|||||||
let parsed = raw
|
let parsed = raw
|
||||||
.split(',')
|
.split(',')
|
||||||
.filter(|value| !value.trim().is_empty())
|
.filter(|value| !value.trim().is_empty())
|
||||||
.map(|value| parse_dns_server_target(value.trim(), transport, args.tls_name.as_deref()))
|
.map(|value| {
|
||||||
|
parse_dns_server_target(
|
||||||
|
value.trim(),
|
||||||
|
transport,
|
||||||
|
args.tls_name.as_deref(),
|
||||||
|
args.prefer_ipv4,
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect::<Result<Vec<_>, _>>();
|
.collect::<Result<Vec<_>, _>>();
|
||||||
match parsed {
|
match parsed {
|
||||||
Ok(values) => values,
|
Ok(values) => values,
|
||||||
@@ -1617,6 +1660,7 @@ fn parse_dns_server_target(
|
|||||||
value: &str,
|
value: &str,
|
||||||
transport: wtfnet_dns::DnsTransport,
|
transport: wtfnet_dns::DnsTransport,
|
||||||
tls_name: Option<&str>,
|
tls_name: Option<&str>,
|
||||||
|
prefer_ipv4: bool,
|
||||||
) -> Result<wtfnet_dns::DnsServerTarget, String> {
|
) -> Result<wtfnet_dns::DnsServerTarget, String> {
|
||||||
let default_port = match transport {
|
let default_port = match transport {
|
||||||
wtfnet_dns::DnsTransport::Udp | wtfnet_dns::DnsTransport::Tcp => 53,
|
wtfnet_dns::DnsTransport::Udp | wtfnet_dns::DnsTransport::Tcp => 53,
|
||||||
@@ -1638,10 +1682,8 @@ fn parse_dns_server_target(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (host, port) = split_host_port_with_default(value, default_port)?;
|
let (host, port) = split_host_port_with_default(value, default_port)?;
|
||||||
let addr = format!("{host}:{port}")
|
let addr = resolve_host_port(&host, port, prefer_ipv4)
|
||||||
.to_socket_addrs()
|
|
||||||
.map_err(|_| format!("invalid server address: {value}"))?
|
.map_err(|_| format!("invalid server address: {value}"))?
|
||||||
.next()
|
|
||||||
.ok_or_else(|| format!("unable to resolve server: {value}"))?;
|
.ok_or_else(|| format!("unable to resolve server: {value}"))?;
|
||||||
|
|
||||||
let name = tls_name
|
let name = tls_name
|
||||||
@@ -1687,6 +1729,28 @@ fn split_host_port_with_default(value: &str, default_port: u16) -> Result<(Strin
|
|||||||
Ok((value.to_string(), default_port))
|
Ok((value.to_string(), default_port))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_host_port(
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
prefer_ipv4: bool,
|
||||||
|
) -> Result<Option<std::net::SocketAddr>, std::io::Error> {
|
||||||
|
let mut iter = (host, port).to_socket_addrs()?;
|
||||||
|
if prefer_ipv4 {
|
||||||
|
let mut fallback = None;
|
||||||
|
for addr in iter.by_ref() {
|
||||||
|
if addr.is_ipv4() {
|
||||||
|
return Ok(Some(addr));
|
||||||
|
}
|
||||||
|
if fallback.is_none() {
|
||||||
|
fallback = Some(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(fallback)
|
||||||
|
} else {
|
||||||
|
Ok(iter.next())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_http_request(
|
async fn handle_http_request(
|
||||||
cli: &Cli,
|
cli: &Cli,
|
||||||
args: HttpRequestArgs,
|
args: HttpRequestArgs,
|
||||||
@@ -1701,6 +1765,7 @@ async fn handle_http_request(
|
|||||||
show_body: args.show_body,
|
show_body: args.show_body,
|
||||||
http1_only: args.http1_only,
|
http1_only: args.http1_only,
|
||||||
http2_only: args.http2_only,
|
http2_only: args.http2_only,
|
||||||
|
proxy: args.socks5.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match wtfnet_http::request(&args.url, opts).await {
|
match wtfnet_http::request(&args.url, opts).await {
|
||||||
@@ -1850,6 +1915,8 @@ fn build_tls_options(args: &TlsArgs) -> wtfnet_tls::TlsOptions {
|
|||||||
alpn: parse_alpn(args.alpn.as_deref()),
|
alpn: parse_alpn(args.alpn.as_deref()),
|
||||||
timeout_ms: args.timeout_ms,
|
timeout_ms: args.timeout_ms,
|
||||||
insecure: args.insecure,
|
insecure: args.insecure,
|
||||||
|
socks5: args.socks5.clone(),
|
||||||
|
prefer_ipv4: args.prefer_ipv4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ edition = "2024"
|
|||||||
hickory-resolver = { version = "0.24", features = ["dns-over-tls", "dns-over-https", "dns-over-https-rustls", "dns-over-rustls", "native-certs"] }
|
hickory-resolver = { version = "0.24", features = ["dns-over-tls", "dns-over-https", "dns-over-https-rustls", "dns-over-rustls", "native-certs"] }
|
||||||
hickory-proto = "0.24"
|
hickory-proto = "0.24"
|
||||||
reqwest = { version = "0.11", features = ["rustls-tls", "socks"] }
|
reqwest = { version = "0.11", features = ["rustls-tls", "socks"] }
|
||||||
|
rustls = "0.21"
|
||||||
|
rustls-native-certs = "0.6"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = ["time"] }
|
tokio = { version = "1", features = ["io-util", "time"] }
|
||||||
|
tokio-rustls = "0.24"
|
||||||
|
tokio-socks = "0.5"
|
||||||
|
url = "2"
|
||||||
pnet = { version = "0.34", optional = true }
|
pnet = { version = "0.34", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -8,12 +8,18 @@ use hickory_resolver::system_conf::read_system_conf;
|
|||||||
use hickory_proto::op::{Message, MessageType, Query};
|
use hickory_proto::op::{Message, MessageType, Query};
|
||||||
use hickory_proto::rr::Name;
|
use hickory_proto::rr::Name;
|
||||||
use reqwest::Proxy;
|
use reqwest::Proxy;
|
||||||
|
use rustls::{Certificate, ClientConfig, RootCertStore, ServerName};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio_rustls::TlsConnector;
|
||||||
|
use tokio_socks::tcp::Socks5Stream;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[cfg(feature = "pcap")]
|
#[cfg(feature = "pcap")]
|
||||||
use pnet::datalink::{self, Channel, Config as DatalinkConfig, NetworkInterface};
|
use pnet::datalink::{self, Channel, Config as DatalinkConfig, NetworkInterface};
|
||||||
@@ -164,11 +170,16 @@ pub async fn query(
|
|||||||
) -> Result<DnsQueryReport, DnsError> {
|
) -> Result<DnsQueryReport, DnsError> {
|
||||||
let record_type = parse_record_type(record_type)?;
|
let record_type = parse_record_type(record_type)?;
|
||||||
if let Some(proxy) = proxy {
|
if let Some(proxy) = proxy {
|
||||||
if transport != DnsTransport::Doh {
|
|
||||||
return Err(DnsError::ProxyUnsupported(transport.to_string()));
|
|
||||||
}
|
|
||||||
let server = server.ok_or_else(|| DnsError::MissingServer(transport.to_string()))?;
|
let server = server.ok_or_else(|| DnsError::MissingServer(transport.to_string()))?;
|
||||||
return doh_query_via_proxy(domain, record_type, server, timeout_ms, proxy).await;
|
return match transport {
|
||||||
|
DnsTransport::Doh => {
|
||||||
|
doh_query_via_proxy(domain, record_type, server, timeout_ms, proxy).await
|
||||||
|
}
|
||||||
|
DnsTransport::Dot => {
|
||||||
|
dot_query_via_proxy(domain, record_type, server, timeout_ms, proxy).await
|
||||||
|
}
|
||||||
|
_ => Err(DnsError::ProxyUnsupported(transport.to_string())),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
let resolver = build_resolver(server.clone(), transport, timeout_ms)?;
|
let resolver = build_resolver(server.clone(), transport, timeout_ms)?;
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
@@ -512,6 +523,151 @@ async fn doh_query_via_proxy(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn dot_query_via_proxy(
|
||||||
|
domain: &str,
|
||||||
|
record_type: RecordType,
|
||||||
|
server: DnsServerTarget,
|
||||||
|
timeout_ms: u64,
|
||||||
|
proxy: String,
|
||||||
|
) -> Result<DnsQueryReport, DnsError> {
|
||||||
|
let tls_name = server
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| DnsError::MissingTlsName("dot".to_string()))?;
|
||||||
|
|
||||||
|
let name = Name::from_ascii(domain)
|
||||||
|
.map_err(|err| DnsError::Resolver(format!("invalid domain: {err}")))?;
|
||||||
|
let mut message = Message::new();
|
||||||
|
message
|
||||||
|
.set_id(0)
|
||||||
|
.set_message_type(MessageType::Query)
|
||||||
|
.set_recursion_desired(true)
|
||||||
|
.add_query(Query::query(name, record_type));
|
||||||
|
let body = message
|
||||||
|
.to_vec()
|
||||||
|
.map_err(|err| DnsError::Resolver(err.to_string()))?;
|
||||||
|
if body.len() > u16::MAX as usize {
|
||||||
|
return Err(DnsError::Resolver("dns message too large".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let connector = build_tls_connector()?;
|
||||||
|
let proxy_config = parse_socks5_proxy(&proxy)?;
|
||||||
|
let target = if proxy_config.remote_dns {
|
||||||
|
(tls_name.clone(), server.addr.port())
|
||||||
|
} else {
|
||||||
|
(server.addr.ip().to_string(), server.addr.port())
|
||||||
|
};
|
||||||
|
let timeout = Duration::from_millis(timeout_ms);
|
||||||
|
let tcp = tokio::time::timeout(
|
||||||
|
timeout,
|
||||||
|
Socks5Stream::connect(proxy_config.addr.as_str(), target),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| DnsError::Resolver("timeout".to_string()))?
|
||||||
|
.map_err(|err| DnsError::Proxy(err.to_string()))?
|
||||||
|
.into_inner();
|
||||||
|
let server_name = ServerName::try_from(tls_name.as_str())
|
||||||
|
.map_err(|_| DnsError::MissingTlsName(tls_name.clone()))?;
|
||||||
|
let mut stream = tokio::time::timeout(timeout, connector.connect(server_name, tcp))
|
||||||
|
.await
|
||||||
|
.map_err(|_| DnsError::Resolver("timeout".to_string()))?
|
||||||
|
.map_err(|err| DnsError::Resolver(err.to_string()))?;
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let response_bytes = tokio::time::timeout(timeout, async {
|
||||||
|
let length = (body.len() as u16).to_be_bytes();
|
||||||
|
stream.write_all(&length).await?;
|
||||||
|
stream.write_all(&body).await?;
|
||||||
|
stream.flush().await?;
|
||||||
|
let mut len_buf = [0u8; 2];
|
||||||
|
stream.read_exact(&mut len_buf).await?;
|
||||||
|
let response_len = u16::from_be_bytes(len_buf) as usize;
|
||||||
|
let mut response = vec![0u8; response_len];
|
||||||
|
stream.read_exact(&mut response).await?;
|
||||||
|
Ok::<Vec<u8>, std::io::Error>(response)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| DnsError::Resolver("timeout".to_string()))?
|
||||||
|
.map_err(|err| DnsError::Resolver(err.to_string()))?;
|
||||||
|
|
||||||
|
let response =
|
||||||
|
Message::from_vec(&response_bytes).map_err(|err| DnsError::Resolver(err.to_string()))?;
|
||||||
|
let duration_ms = start.elapsed().as_millis();
|
||||||
|
|
||||||
|
let mut answers = Vec::new();
|
||||||
|
for record in response.answers() {
|
||||||
|
let ttl = record.ttl();
|
||||||
|
let name = record.name().to_string();
|
||||||
|
let record_type = record.record_type().to_string();
|
||||||
|
if let Some(data) = record.data() {
|
||||||
|
if let Some(data) = format_rdata(data) {
|
||||||
|
answers.push(DnsAnswer {
|
||||||
|
name,
|
||||||
|
record_type,
|
||||||
|
ttl,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DnsQueryReport {
|
||||||
|
domain: domain.to_string(),
|
||||||
|
record_type: record_type.to_string(),
|
||||||
|
transport: DnsTransport::Dot.to_string(),
|
||||||
|
server: Some(server.addr.to_string()),
|
||||||
|
server_name: Some(tls_name),
|
||||||
|
proxy: Some(proxy),
|
||||||
|
rcode: response.response_code().to_string(),
|
||||||
|
answers,
|
||||||
|
duration_ms,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_tls_connector() -> Result<TlsConnector, DnsError> {
|
||||||
|
let mut roots = RootCertStore::empty();
|
||||||
|
let store = rustls_native_certs::load_native_certs()
|
||||||
|
.map_err(|err| DnsError::Io(err.to_string()))?;
|
||||||
|
for cert in store {
|
||||||
|
roots
|
||||||
|
.add(&Certificate(cert.0))
|
||||||
|
.map_err(|err| DnsError::Resolver(err.to_string()))?;
|
||||||
|
}
|
||||||
|
let config = ClientConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_root_certificates(roots)
|
||||||
|
.with_no_client_auth();
|
||||||
|
Ok(TlsConnector::from(Arc::new(config)))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Socks5Proxy {
|
||||||
|
addr: String,
|
||||||
|
remote_dns: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_socks5_proxy(value: &str) -> Result<Socks5Proxy, DnsError> {
|
||||||
|
let url = Url::parse(value).map_err(|_| DnsError::Proxy(value.to_string()))?;
|
||||||
|
let scheme = url.scheme();
|
||||||
|
let remote_dns = match scheme {
|
||||||
|
"socks5" => false,
|
||||||
|
"socks5h" => true,
|
||||||
|
_ => return Err(DnsError::ProxyUnsupported(scheme.to_string())),
|
||||||
|
};
|
||||||
|
if !url.username().is_empty() || url.password().is_some() {
|
||||||
|
return Err(DnsError::Proxy("proxy auth not supported".to_string()));
|
||||||
|
}
|
||||||
|
let host = url
|
||||||
|
.host_str()
|
||||||
|
.ok_or_else(|| DnsError::Proxy(value.to_string()))?;
|
||||||
|
let port = url
|
||||||
|
.port_or_known_default()
|
||||||
|
.ok_or_else(|| DnsError::Proxy(value.to_string()))?;
|
||||||
|
Ok(Socks5Proxy {
|
||||||
|
addr: format!("{host}:{port}"),
|
||||||
|
remote_dns,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "pcap")]
|
#[cfg(feature = "pcap")]
|
||||||
fn select_interface(name: Option<&str>) -> Option<NetworkInterface> {
|
fn select_interface(name: Option<&str>) -> Option<NetworkInterface> {
|
||||||
let interfaces = datalink::interfaces();
|
let interfaces = datalink::interfaces();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use reqwest::{Client, Method, StatusCode};
|
use reqwest::{Client, Method, Proxy, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -63,6 +63,7 @@ pub struct HttpRequestOptions {
|
|||||||
pub show_body: bool,
|
pub show_body: bool,
|
||||||
pub http1_only: bool,
|
pub http1_only: bool,
|
||||||
pub http2_only: bool,
|
pub http2_only: bool,
|
||||||
|
pub proxy: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn request(url: &str, opts: HttpRequestOptions) -> Result<HttpReport, HttpError> {
|
pub async fn request(url: &str, opts: HttpRequestOptions) -> Result<HttpReport, HttpError> {
|
||||||
@@ -100,6 +101,11 @@ pub async fn request(url: &str, opts: HttpRequestOptions) -> Result<HttpReport,
|
|||||||
builder.redirect(reqwest::redirect::Policy::none())
|
builder.redirect(reqwest::redirect::Policy::none())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(proxy) = opts.proxy.as_ref() {
|
||||||
|
let proxy = Proxy::all(proxy).map_err(|err| HttpError::Request(err.to_string()))?;
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
|
||||||
if opts.http1_only {
|
if opts.http1_only {
|
||||||
builder = builder.http1_only();
|
builder = builder.http1_only();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,3 +12,5 @@ tokio = { version = "1", features = ["net", "time"] }
|
|||||||
surge-ping = "0.8"
|
surge-ping = "0.8"
|
||||||
wtfnet-geoip = { path = "../wtfnet-geoip" }
|
wtfnet-geoip = { path = "../wtfnet-geoip" }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
tokio-socks = "0.5"
|
||||||
|
url = "2"
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ use std::time::{Duration, Instant};
|
|||||||
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;
|
||||||
|
use tokio_socks::tcp::Socks5Stream;
|
||||||
|
use url::Url;
|
||||||
use wtfnet_geoip::GeoIpRecord;
|
use wtfnet_geoip::GeoIpRecord;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -28,6 +30,10 @@ pub enum ProbeError {
|
|||||||
Resolve(String),
|
Resolve(String),
|
||||||
#[error("io error: {0}")]
|
#[error("io error: {0}")]
|
||||||
Io(String),
|
Io(String),
|
||||||
|
#[error("invalid proxy: {0}")]
|
||||||
|
InvalidProxy(String),
|
||||||
|
#[error("proxy error: {0}")]
|
||||||
|
Proxy(String),
|
||||||
#[error("timeout")]
|
#[error("timeout")]
|
||||||
Timeout,
|
Timeout,
|
||||||
#[error("ping error: {0}")]
|
#[error("ping error: {0}")]
|
||||||
@@ -184,9 +190,30 @@ pub async fn tcp_ping(
|
|||||||
port: u16,
|
port: u16,
|
||||||
count: u32,
|
count: u32,
|
||||||
timeout_ms: u64,
|
timeout_ms: u64,
|
||||||
|
proxy: Option<&str>,
|
||||||
|
prefer_ipv4: bool,
|
||||||
) -> Result<TcpPingReport, ProbeError> {
|
) -> Result<TcpPingReport, ProbeError> {
|
||||||
let addr = resolve_one(target).await?;
|
let (report_ip, target_host, proxy_addr) = if let Some(proxy) = proxy {
|
||||||
let socket_addr = SocketAddr::new(addr, port);
|
let proxy = parse_socks5_proxy(proxy)?;
|
||||||
|
if proxy.remote_dns {
|
||||||
|
(None, target.to_string(), proxy.addr)
|
||||||
|
} else {
|
||||||
|
let addr = if prefer_ipv4 {
|
||||||
|
resolve_one_prefer_ipv4(target).await?
|
||||||
|
} else {
|
||||||
|
resolve_one(target).await?
|
||||||
|
};
|
||||||
|
(Some(addr), addr.to_string(), proxy.addr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let addr = if prefer_ipv4 {
|
||||||
|
resolve_one_prefer_ipv4(target).await?
|
||||||
|
} else {
|
||||||
|
resolve_one(target).await?
|
||||||
|
};
|
||||||
|
(Some(addr), addr.to_string(), String::new())
|
||||||
|
};
|
||||||
|
let socket_addr = report_ip.map(|addr| SocketAddr::new(addr, port));
|
||||||
let timeout_dur = Duration::from_millis(timeout_ms);
|
let timeout_dur = Duration::from_millis(timeout_ms);
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
let mut received = 0u32;
|
let mut received = 0u32;
|
||||||
@@ -197,9 +224,27 @@ pub async fn tcp_ping(
|
|||||||
for seq in 0..count {
|
for seq in 0..count {
|
||||||
let seq = seq as u16;
|
let seq = seq as u16;
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let attempt = timeout(timeout_dur, TcpStream::connect(socket_addr)).await;
|
let attempt: Result<TcpStream, ProbeError> = if proxy.is_some() {
|
||||||
|
let target = (target_host.as_str(), port);
|
||||||
|
let stream = timeout(
|
||||||
|
timeout_dur,
|
||||||
|
Socks5Stream::connect(proxy_addr.as_str(), target),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ProbeError::Timeout)?
|
||||||
|
.map_err(|err| ProbeError::Proxy(err.to_string()))?;
|
||||||
|
Ok(stream.into_inner())
|
||||||
|
} else {
|
||||||
|
timeout(
|
||||||
|
timeout_dur,
|
||||||
|
TcpStream::connect(socket_addr.expect("missing socket addr")),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ProbeError::Timeout)?
|
||||||
|
.map_err(|err| ProbeError::Io(err.to_string()))
|
||||||
|
};
|
||||||
match attempt {
|
match attempt {
|
||||||
Ok(Ok(_stream)) => {
|
Ok(_stream) => {
|
||||||
let rtt = start.elapsed().as_millis();
|
let rtt = start.elapsed().as_millis();
|
||||||
received += 1;
|
received += 1;
|
||||||
min = Some(min.map_or(rtt, |value: u128| value.min(rtt)));
|
min = Some(min.map_or(rtt, |value: u128| value.min(rtt)));
|
||||||
@@ -211,27 +256,20 @@ pub async fn tcp_ping(
|
|||||||
error: None,
|
error: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(Err(err)) => {
|
Err(err) => {
|
||||||
results.push(TcpPingResult {
|
results.push(TcpPingResult {
|
||||||
seq,
|
seq,
|
||||||
rtt_ms: None,
|
rtt_ms: None,
|
||||||
error: Some(err.to_string()),
|
error: Some(err.to_string()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(_) => {
|
|
||||||
results.push(TcpPingResult {
|
|
||||||
seq,
|
|
||||||
rtt_ms: None,
|
|
||||||
error: Some("timeout".to_string()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let summary = build_summary(count, received, min, max, sum);
|
let summary = build_summary(count, received, min, max, sum);
|
||||||
Ok(TcpPingReport {
|
Ok(TcpPingReport {
|
||||||
target: target.to_string(),
|
target: target.to_string(),
|
||||||
ip: Some(addr.to_string()),
|
ip: report_ip.map(|addr| addr.to_string()),
|
||||||
geoip: None,
|
geoip: None,
|
||||||
port,
|
port,
|
||||||
timeout_ms,
|
timeout_ms,
|
||||||
@@ -389,6 +427,50 @@ async fn resolve_one(target: &str) -> Result<IpAddr, ProbeError> {
|
|||||||
.ok_or_else(|| ProbeError::Resolve("no address found".to_string()))
|
.ok_or_else(|| ProbeError::Resolve("no address found".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn resolve_one_prefer_ipv4(target: &str) -> Result<IpAddr, ProbeError> {
|
||||||
|
let mut iter = lookup_host((target, 0))
|
||||||
|
.await
|
||||||
|
.map_err(|err| ProbeError::Resolve(err.to_string()))?;
|
||||||
|
let mut fallback = None;
|
||||||
|
for addr in iter.by_ref() {
|
||||||
|
if addr.ip().is_ipv4() {
|
||||||
|
return Ok(addr.ip());
|
||||||
|
}
|
||||||
|
if fallback.is_none() {
|
||||||
|
fallback = Some(addr.ip());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fallback.ok_or_else(|| ProbeError::Resolve("no address found".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Socks5Proxy {
|
||||||
|
addr: String,
|
||||||
|
remote_dns: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_socks5_proxy(value: &str) -> Result<Socks5Proxy, ProbeError> {
|
||||||
|
let url = Url::parse(value).map_err(|_| ProbeError::InvalidProxy(value.to_string()))?;
|
||||||
|
let scheme = url.scheme();
|
||||||
|
let remote_dns = match scheme {
|
||||||
|
"socks5" => false,
|
||||||
|
"socks5h" => true,
|
||||||
|
_ => return Err(ProbeError::InvalidProxy(value.to_string())),
|
||||||
|
};
|
||||||
|
if !url.username().is_empty() || url.password().is_some() {
|
||||||
|
return Err(ProbeError::Proxy("proxy auth not supported".to_string()));
|
||||||
|
}
|
||||||
|
let host = url
|
||||||
|
.host_str()
|
||||||
|
.ok_or_else(|| ProbeError::InvalidProxy(value.to_string()))?;
|
||||||
|
let port = url
|
||||||
|
.port_or_known_default()
|
||||||
|
.ok_or_else(|| ProbeError::InvalidProxy(value.to_string()))?;
|
||||||
|
Ok(Socks5Proxy {
|
||||||
|
addr: format!("{host}:{port}"),
|
||||||
|
remote_dns,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn tcp_connect_with_ttl(addr: SocketAddr, ttl: u8, timeout: Duration) -> Result<(), ProbeError> {
|
fn tcp_connect_with_ttl(addr: SocketAddr, ttl: u8, timeout: Duration) -> Result<(), ProbeError> {
|
||||||
let domain = match addr.ip() {
|
let domain = match addr.ip() {
|
||||||
IpAddr::V4(_) => Domain::IPV4,
|
IpAddr::V4(_) => Domain::IPV4,
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ thiserror = "2"
|
|||||||
tokio = { version = "1", features = ["net", "time"] }
|
tokio = { version = "1", features = ["net", "time"] }
|
||||||
tokio-rustls = "0.24"
|
tokio-rustls = "0.24"
|
||||||
x509-parser = "0.16"
|
x509-parser = "0.16"
|
||||||
|
tokio-socks = "0.5"
|
||||||
|
url = "2"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use thiserror::Error;
|
|||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use tokio_rustls::TlsConnector;
|
use tokio_rustls::TlsConnector;
|
||||||
|
use tokio_socks::tcp::Socks5Stream;
|
||||||
|
use url::Url;
|
||||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -78,12 +80,23 @@ pub struct TlsOptions {
|
|||||||
pub alpn: Vec<String>,
|
pub alpn: Vec<String>,
|
||||||
pub timeout_ms: u64,
|
pub timeout_ms: u64,
|
||||||
pub insecure: bool,
|
pub insecure: bool,
|
||||||
|
pub socks5: Option<String>,
|
||||||
|
pub prefer_ipv4: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handshake(target: &str, options: TlsOptions) -> Result<TlsHandshakeReport, TlsError> {
|
pub async fn handshake(target: &str, options: TlsOptions) -> Result<TlsHandshakeReport, TlsError> {
|
||||||
let (addr, server_name) = parse_target(target, options.sni.as_deref())?;
|
let (host, port, server_name) = parse_target(target, options.sni.as_deref())?;
|
||||||
let connector = build_connector(options.insecure, &options.alpn)?;
|
let connector = build_connector(options.insecure, &options.alpn)?;
|
||||||
let stream = connect(addr, connector, server_name, options.timeout_ms).await?;
|
let stream = connect(
|
||||||
|
host.as_str(),
|
||||||
|
port,
|
||||||
|
options.socks5.as_deref(),
|
||||||
|
connector,
|
||||||
|
server_name,
|
||||||
|
options.timeout_ms,
|
||||||
|
options.prefer_ipv4,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let (_, session) = stream.get_ref();
|
let (_, session) = stream.get_ref();
|
||||||
|
|
||||||
Ok(TlsHandshakeReport {
|
Ok(TlsHandshakeReport {
|
||||||
@@ -102,9 +115,19 @@ pub async fn handshake(target: &str, options: TlsOptions) -> Result<TlsHandshake
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn verify(target: &str, options: TlsOptions) -> Result<TlsVerifyReport, TlsError> {
|
pub async fn verify(target: &str, options: TlsOptions) -> Result<TlsVerifyReport, TlsError> {
|
||||||
let (addr, server_name) = parse_target(target, options.sni.as_deref())?;
|
let (host, port, server_name) = parse_target(target, options.sni.as_deref())?;
|
||||||
let connector = build_connector(false, &options.alpn)?;
|
let connector = build_connector(false, &options.alpn)?;
|
||||||
match connect(addr, connector, server_name, options.timeout_ms).await {
|
match connect(
|
||||||
|
host.as_str(),
|
||||||
|
port,
|
||||||
|
options.socks5.as_deref(),
|
||||||
|
connector,
|
||||||
|
server_name,
|
||||||
|
options.timeout_ms,
|
||||||
|
options.prefer_ipv4,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
let (_, session) = stream.get_ref();
|
let (_, session) = stream.get_ref();
|
||||||
Ok(TlsVerifyReport {
|
Ok(TlsVerifyReport {
|
||||||
@@ -136,9 +159,18 @@ pub async fn verify(target: &str, options: TlsOptions) -> Result<TlsVerifyReport
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn certs(target: &str, options: TlsOptions) -> Result<TlsCertReport, TlsError> {
|
pub async fn certs(target: &str, options: TlsOptions) -> Result<TlsCertReport, TlsError> {
|
||||||
let (addr, server_name) = parse_target(target, options.sni.as_deref())?;
|
let (host, port, server_name) = parse_target(target, options.sni.as_deref())?;
|
||||||
let connector = build_connector(options.insecure, &options.alpn)?;
|
let connector = build_connector(options.insecure, &options.alpn)?;
|
||||||
let stream = connect(addr, connector, server_name, options.timeout_ms).await?;
|
let stream = connect(
|
||||||
|
host.as_str(),
|
||||||
|
port,
|
||||||
|
options.socks5.as_deref(),
|
||||||
|
connector,
|
||||||
|
server_name,
|
||||||
|
options.timeout_ms,
|
||||||
|
options.prefer_ipv4,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let (_, session) = stream.get_ref();
|
let (_, session) = stream.get_ref();
|
||||||
Ok(TlsCertReport {
|
Ok(TlsCertReport {
|
||||||
target: target.to_string(),
|
target: target.to_string(),
|
||||||
@@ -148,9 +180,18 @@ pub async fn certs(target: &str, options: TlsOptions) -> Result<TlsCertReport, T
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn alpn(target: &str, options: TlsOptions) -> Result<TlsAlpnReport, TlsError> {
|
pub async fn alpn(target: &str, options: TlsOptions) -> Result<TlsAlpnReport, TlsError> {
|
||||||
let (addr, server_name) = parse_target(target, options.sni.as_deref())?;
|
let (host, port, server_name) = parse_target(target, options.sni.as_deref())?;
|
||||||
let connector = build_connector(options.insecure, &options.alpn)?;
|
let connector = build_connector(options.insecure, &options.alpn)?;
|
||||||
let stream = connect(addr, connector, server_name, options.timeout_ms).await?;
|
let stream = connect(
|
||||||
|
host.as_str(),
|
||||||
|
port,
|
||||||
|
options.socks5.as_deref(),
|
||||||
|
connector,
|
||||||
|
server_name,
|
||||||
|
options.timeout_ms,
|
||||||
|
options.prefer_ipv4,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let (_, session) = stream.get_ref();
|
let (_, session) = stream.get_ref();
|
||||||
Ok(TlsAlpnReport {
|
Ok(TlsAlpnReport {
|
||||||
target: target.to_string(),
|
target: target.to_string(),
|
||||||
@@ -162,9 +203,8 @@ pub async fn alpn(target: &str, options: TlsOptions) -> Result<TlsAlpnReport, Tl
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_target(target: &str, sni: Option<&str>) -> Result<(SocketAddr, ServerName), TlsError> {
|
fn parse_target(target: &str, sni: Option<&str>) -> Result<(String, u16, ServerName), TlsError> {
|
||||||
let (host, port) = split_host_port(target)?;
|
let (host, port) = split_host_port(target)?;
|
||||||
let addr = resolve_addr(&host, port)?;
|
|
||||||
let server_name = if let Some(sni) = sni {
|
let server_name = if let Some(sni) = sni {
|
||||||
ServerName::try_from(sni).map_err(|_| TlsError::InvalidSni(sni.to_string()))?
|
ServerName::try_from(sni).map_err(|_| TlsError::InvalidSni(sni.to_string()))?
|
||||||
} else if let Ok(ip) = host.parse::<IpAddr>() {
|
} else if let Ok(ip) = host.parse::<IpAddr>() {
|
||||||
@@ -173,7 +213,7 @@ fn parse_target(target: &str, sni: Option<&str>) -> Result<(SocketAddr, ServerNa
|
|||||||
ServerName::try_from(host.as_str())
|
ServerName::try_from(host.as_str())
|
||||||
.map_err(|_| TlsError::InvalidSni(host.to_string()))?
|
.map_err(|_| TlsError::InvalidSni(host.to_string()))?
|
||||||
};
|
};
|
||||||
Ok((addr, server_name))
|
Ok((host, port, server_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_host_port(value: &str) -> Result<(String, u16), TlsError> {
|
fn split_host_port(value: &str) -> Result<(String, u16), TlsError> {
|
||||||
@@ -237,6 +277,24 @@ fn resolve_addr(host: &str, port: u16) -> Result<SocketAddr, TlsError> {
|
|||||||
Ok(addr)
|
Ok(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_addr_prefer_ipv4(host: &str, port: u16) -> Result<SocketAddr, TlsError> {
|
||||||
|
if let Ok(ip) = host.parse::<IpAddr>() {
|
||||||
|
return Ok(SocketAddr::new(ip, port));
|
||||||
|
}
|
||||||
|
let mut iter = std::net::ToSocketAddrs::to_socket_addrs(&(host, port))
|
||||||
|
.map_err(|err| TlsError::Io(err.to_string()))?;
|
||||||
|
let mut fallback = None;
|
||||||
|
for addr in iter.by_ref() {
|
||||||
|
if addr.is_ipv4() {
|
||||||
|
return Ok(addr);
|
||||||
|
}
|
||||||
|
if fallback.is_none() {
|
||||||
|
fallback = Some(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fallback.ok_or_else(|| TlsError::InvalidTarget(host.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
fn build_connector(insecure: bool, alpn: &[String]) -> Result<TlsConnector, TlsError> {
|
fn build_connector(insecure: bool, alpn: &[String]) -> Result<TlsConnector, TlsError> {
|
||||||
let mut config = if insecure {
|
let mut config = if insecure {
|
||||||
ClientConfig::builder()
|
ClientConfig::builder()
|
||||||
@@ -266,15 +324,46 @@ fn build_connector(insecure: bool, alpn: &[String]) -> Result<TlsConnector, TlsE
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn connect(
|
async fn connect(
|
||||||
addr: SocketAddr,
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
proxy: Option<&str>,
|
||||||
connector: TlsConnector,
|
connector: TlsConnector,
|
||||||
server_name: ServerName,
|
server_name: ServerName,
|
||||||
timeout_ms: u64,
|
timeout_ms: u64,
|
||||||
|
prefer_ipv4: bool,
|
||||||
) -> Result<tokio_rustls::client::TlsStream<TcpStream>, TlsError> {
|
) -> Result<tokio_rustls::client::TlsStream<TcpStream>, TlsError> {
|
||||||
let tcp = timeout(Duration::from_millis(timeout_ms), TcpStream::connect(addr))
|
let tcp = if let Some(proxy) = proxy {
|
||||||
|
let proxy_addr = parse_proxy_addr(proxy)?;
|
||||||
|
let (target_host, remote_dns) = socks5_target_host(proxy, host);
|
||||||
|
let target = if remote_dns {
|
||||||
|
(target_host.clone(), port)
|
||||||
|
} else {
|
||||||
|
let addr = if prefer_ipv4 {
|
||||||
|
resolve_addr_prefer_ipv4(target_host.as_str(), port)?
|
||||||
|
} else {
|
||||||
|
resolve_addr(target_host.as_str(), port)?
|
||||||
|
};
|
||||||
|
(addr.ip().to_string(), port)
|
||||||
|
};
|
||||||
|
let stream = timeout(
|
||||||
|
Duration::from_millis(timeout_ms),
|
||||||
|
Socks5Stream::connect(proxy_addr.as_str(), target),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| TlsError::Timeout)?
|
.map_err(|_| TlsError::Timeout)?
|
||||||
.map_err(|err| TlsError::Io(err.to_string()))?;
|
.map_err(|err| TlsError::Io(err.to_string()))?;
|
||||||
|
stream.into_inner()
|
||||||
|
} else {
|
||||||
|
let addr = if prefer_ipv4 {
|
||||||
|
resolve_addr_prefer_ipv4(host, port)?
|
||||||
|
} else {
|
||||||
|
resolve_addr(host, port)?
|
||||||
|
};
|
||||||
|
timeout(Duration::from_millis(timeout_ms), TcpStream::connect(addr))
|
||||||
|
.await
|
||||||
|
.map_err(|_| TlsError::Timeout)?
|
||||||
|
.map_err(|err| TlsError::Io(err.to_string()))?
|
||||||
|
};
|
||||||
let stream = timeout(
|
let stream = timeout(
|
||||||
Duration::from_millis(timeout_ms),
|
Duration::from_millis(timeout_ms),
|
||||||
connector.connect(server_name, tcp),
|
connector.connect(server_name, tcp),
|
||||||
@@ -285,6 +374,22 @@ async fn connect(
|
|||||||
Ok(stream)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_proxy_addr(value: &str) -> Result<String, TlsError> {
|
||||||
|
let url = Url::parse(value).map_err(|_| TlsError::InvalidTarget(value.to_string()))?;
|
||||||
|
let host = url
|
||||||
|
.host_str()
|
||||||
|
.ok_or_else(|| TlsError::InvalidTarget(value.to_string()))?;
|
||||||
|
let port = url
|
||||||
|
.port_or_known_default()
|
||||||
|
.ok_or_else(|| TlsError::InvalidTarget(value.to_string()))?;
|
||||||
|
Ok(format!("{host}:{port}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socks5_target_host(proxy: &str, host: &str) -> (String, bool) {
|
||||||
|
let remote_dns = proxy.starts_with("socks5h://");
|
||||||
|
(host.to_string(), remote_dns)
|
||||||
|
}
|
||||||
|
|
||||||
fn extract_cert_chain(certs: Option<&[Certificate]>) -> Result<Vec<TlsCertSummary>, TlsError> {
|
fn extract_cert_chain(certs: Option<&[Certificate]>) -> Result<Vec<TlsCertSummary>, TlsError> {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
if let Some(certs) = certs {
|
if let Some(certs) = certs {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ This document tracks current implementation status against the original design i
|
|||||||
- Diag: bundle export in zip.
|
- Diag: bundle export in zip.
|
||||||
|
|
||||||
## Deviations or refinements
|
## Deviations or refinements
|
||||||
- DNS adds DoT/DoH and SOCKS5 proxy support (beyond initial scope).
|
- DNS adds DoT/DoH and SOCKS5 proxy support.
|
||||||
|
- HTTP/TLS/TCP ping include SOCKS5 proxy support.
|
||||||
- HTTP timing breakdown is best-effort: `dns_ms` and `ttfb_ms` are captured; `connect_ms`/`tls_ms` remain placeholders.
|
- HTTP timing breakdown is best-effort: `dns_ms` and `ttfb_ms` are captured; `connect_ms`/`tls_ms` remain placeholders.
|
||||||
- DNS watch uses `pnet` and is feature-gated as best-effort.
|
- DNS watch uses `pnet` and is feature-gated as best-effort.
|
||||||
|
|
||||||
|
|||||||
@@ -54,12 +54,13 @@ This document tracks the planned roadmap alongside the current implementation st
|
|||||||
- `wtfnet-dns` crate with query/detect support wired to CLI.
|
- `wtfnet-dns` crate with query/detect support wired to CLI.
|
||||||
- DNS query/detect output includes GeoIP enrichment for server and answer IPs.
|
- DNS query/detect output includes GeoIP enrichment for server and answer IPs.
|
||||||
- DNS query/detect supports DoT and DoH transports.
|
- DNS query/detect supports DoT and DoH transports.
|
||||||
- DNS query/detect supports SOCKS5 proxying for DoH.
|
- DNS query/detect supports SOCKS5 proxying for DoH/DoT.
|
||||||
- DNS watch (passive, best-effort) implemented.
|
- DNS watch (passive, best-effort) implemented.
|
||||||
- Calc subcrate with subnet/contains/overlap/summarize wired to CLI.
|
- Calc subcrate with subnet/contains/overlap/summarize wired to CLI.
|
||||||
- CMake/Makefile build, install, and package targets for release packaging.
|
- CMake/Makefile build, install, and package targets for release packaging.
|
||||||
- HTTP crate with head/get support, timing breakdown, and optional GeoIP.
|
- HTTP crate with head/get support, timing breakdown, optional GeoIP, and SOCKS5 proxy.
|
||||||
- TLS crate with handshake/verify/cert/alpn support in CLI.
|
- TLS crate with handshake/verify/cert/alpn support in CLI (SOCKS5 proxy supported).
|
||||||
|
- TCP ping supports SOCKS5 proxy.
|
||||||
- Discover crate with mdns/ssdp commands.
|
- Discover crate with mdns/ssdp commands.
|
||||||
- Diag crate with report and bundle export.
|
- Diag crate with report and bundle export.
|
||||||
- Basic unit tests for calc and TLS parsing.
|
- Basic unit tests for calc and TLS parsing.
|
||||||
|
|||||||
Reference in New Issue
Block a user