Add: H3 support - incomplete

This commit is contained in:
DaZuo0122
2026-01-17 13:47:37 +08:00
parent 840ceec38f
commit ccd4a31d21
14 changed files with 1553 additions and 71 deletions

497
Cargo.lock generated
View File

@@ -217,12 +217,24 @@ dependencies = [
"shlex", "shlex",
] ]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@@ -279,6 +291,16 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -304,6 +326,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@@ -447,6 +479,18 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "fastbloom"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4"
dependencies = [
"getrandom 0.3.4",
"libm",
"rand 0.9.2",
"siphasher",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@@ -511,6 +555,21 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@@ -518,6 +577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@@ -526,12 +586,34 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.31" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.31" version = "0.3.31"
@@ -550,8 +632,13 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab", "slab",
@@ -574,8 +661,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -585,9 +674,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasip2", "wasip2",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -607,7 +698,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http 0.2.12",
"indexmap", "indexmap",
"slab", "slab",
"tokio", "tokio",
@@ -615,6 +706,34 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "h3"
version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10872b55cfb02a821b69dc7cf8dc6a71d6af25eb9a79662bec4a9d016056b3be"
dependencies = [
"bytes",
"fastrand",
"futures-util",
"http 1.4.0",
"pin-project-lite",
"tokio",
]
[[package]]
name = "h3-quinn"
version = "0.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2e732c8d91a74731663ac8479ab505042fbf547b9a207213ab7fbcbfc4f8b4"
dependencies = [
"bytes",
"futures",
"h3",
"quinn",
"tokio",
"tokio-util",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.1" version = "0.16.1"
@@ -648,12 +767,12 @@ dependencies = [
"futures-io", "futures-io",
"futures-util", "futures-util",
"h2", "h2",
"http", "http 0.2.12",
"idna", "idna",
"ipnet", "ipnet",
"once_cell", "once_cell",
"rand 0.8.5", "rand 0.8.5",
"rustls", "rustls 0.21.12",
"rustls-native-certs 0.6.3", "rustls-native-certs 0.6.3",
"rustls-pemfile 1.0.4", "rustls-pemfile 1.0.4",
"thiserror 1.0.69", "thiserror 1.0.69",
@@ -679,7 +798,7 @@ dependencies = [
"parking_lot", "parking_lot",
"rand 0.8.5", "rand 0.8.5",
"resolv-conf", "resolv-conf",
"rustls", "rustls 0.21.12",
"rustls-native-certs 0.6.3", "rustls-native-certs 0.6.3",
"smallvec", "smallvec",
"thiserror 1.0.69", "thiserror 1.0.69",
@@ -708,6 +827,16 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"itoa",
]
[[package]] [[package]]
name = "http-body" name = "http-body"
version = "0.4.6" version = "0.4.6"
@@ -715,7 +844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http 0.2.12",
"pin-project-lite", "pin-project-lite",
] ]
@@ -742,7 +871,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2",
"http", "http 0.2.12",
"http-body", "http-body",
"httparse", "httparse",
"httpdate", "httpdate",
@@ -762,9 +891,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"http", "http 0.2.12",
"hyper", "hyper",
"rustls", "rustls 0.21.12",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
] ]
@@ -952,6 +1081,28 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jni"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"log",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.34" version = "0.1.34"
@@ -984,6 +1135,12 @@ version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]] [[package]]
name = "linked-hash-map" name = "linked-hash-map"
version = "0.5.6" version = "0.5.6"
@@ -1026,6 +1183,12 @@ dependencies = [
"linked-hash-map", "linked-hash-map",
] ]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.2.0" version = "0.2.0"
@@ -1108,10 +1271,10 @@ dependencies = [
"libc", "libc",
"log", "log",
"openssl", "openssl",
"openssl-probe", "openssl-probe 0.1.6",
"openssl-sys", "openssl-sys",
"schannel", "schannel",
"security-framework", "security-framework 2.11.1",
"security-framework-sys", "security-framework-sys",
"tempfile", "tempfile",
] ]
@@ -1240,6 +1403,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-probe"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.111" version = "0.9.111"
@@ -1482,6 +1651,64 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"futures-io",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls 0.23.36",
"socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [
"bytes",
"fastbloom",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.2",
"ring",
"rustc-hash",
"rustls 0.23.36",
"rustls-pki-types",
"rustls-platform-verifier",
"slab",
"thiserror 2.0.17",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.1",
"tracing",
"windows-sys 0.60.2",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.43" version = "1.0.43"
@@ -1606,7 +1833,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2",
"http", "http 0.2.12",
"http-body", "http-body",
"hyper", "hyper",
"hyper-rustls", "hyper-rustls",
@@ -1619,7 +1846,7 @@ dependencies = [
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls", "rustls 0.21.12",
"rustls-pemfile 1.0.4", "rustls-pemfile 1.0.4",
"serde", "serde",
"serde_json", "serde_json",
@@ -1635,7 +1862,7 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots", "webpki-roots 0.25.4",
"winreg", "winreg",
] ]
@@ -1659,6 +1886,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rusticata-macros" name = "rusticata-macros"
version = "4.1.0" version = "4.1.0"
@@ -1689,20 +1922,34 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [ dependencies = [
"log", "log",
"ring", "ring",
"rustls-webpki", "rustls-webpki 0.101.7",
"sct", "sct",
] ]
[[package]]
name = "rustls"
version = "0.23.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.9",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rustls-native-certs" name = "rustls-native-certs"
version = "0.6.3" version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [ dependencies = [
"openssl-probe", "openssl-probe 0.1.6",
"rustls-pemfile 1.0.4", "rustls-pemfile 1.0.4",
"schannel", "schannel",
"security-framework", "security-framework 2.11.1",
] ]
[[package]] [[package]]
@@ -1711,11 +1958,23 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [ dependencies = [
"openssl-probe", "openssl-probe 0.1.6",
"rustls-pemfile 2.2.0", "rustls-pemfile 2.2.0",
"rustls-pki-types", "rustls-pki-types",
"schannel", "schannel",
"security-framework", "security-framework 2.11.1",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe 0.2.0",
"rustls-pki-types",
"schannel",
"security-framework 3.5.1",
] ]
[[package]] [[package]]
@@ -1742,9 +2001,37 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
dependencies = [ dependencies = [
"web-time",
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls 0.23.36",
"rustls-native-certs 0.8.3",
"rustls-platform-verifier-android",
"rustls-webpki 0.103.9",
"security-framework 3.5.1",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.101.7" version = "0.101.7"
@@ -1755,6 +2042,17 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "rustls-webpki"
version = "0.103.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.22"
@@ -1767,6 +2065,15 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.28" version = "0.1.28"
@@ -1799,7 +2106,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"core-foundation", "core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
dependencies = [
"bitflags 2.10.0",
"core-foundation 0.10.1",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
"security-framework-sys", "security-framework-sys",
@@ -1913,6 +2233,12 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.11" version = "0.4.11"
@@ -2033,7 +2359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation 0.9.4",
"system-configuration-sys", "system-configuration-sys",
] ]
@@ -2207,7 +2533,7 @@ version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [ dependencies = [
"rustls", "rustls 0.21.12",
"tokio", "tokio",
] ]
@@ -2248,6 +2574,7 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [ dependencies = [
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@@ -2394,6 +2721,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@@ -2487,12 +2824,40 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.25.4" version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webpki-roots"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "widestring" name = "widestring"
version = "1.2.1" version = "1.2.1"
@@ -2515,6 +2880,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
@@ -2527,6 +2901,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@@ -2563,6 +2946,21 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.48.5" version = "0.48.5"
@@ -2611,6 +3009,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.1", "windows_x86_64_msvc 0.53.1",
] ]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.48.5" version = "0.48.5"
@@ -2629,6 +3033,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.5" version = "0.48.5"
@@ -2647,6 +3057,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.5" version = "0.48.5"
@@ -2677,6 +3093,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.5" version = "0.48.5"
@@ -2695,6 +3117,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.5" version = "0.48.5"
@@ -2713,6 +3141,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.48.5" version = "0.48.5"
@@ -2731,6 +3165,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.5" version = "0.48.5"
@@ -2787,6 +3227,7 @@ dependencies = [
"clap", "clap",
"serde", "serde",
"serde_json", "serde_json",
"time",
"tokio", "tokio",
"wtfnet-calc", "wtfnet-calc",
"wtfnet-core", "wtfnet-core",
@@ -2844,7 +3285,7 @@ dependencies = [
"hickory-resolver", "hickory-resolver",
"pnet", "pnet",
"reqwest", "reqwest",
"rustls", "rustls 0.21.12",
"rustls-native-certs 0.6.3", "rustls-native-certs 0.6.3",
"serde", "serde",
"thiserror 2.0.17", "thiserror 2.0.17",
@@ -2868,12 +3309,22 @@ dependencies = [
name = "wtfnet-http" name = "wtfnet-http"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bytes",
"h3",
"h3-quinn",
"http 1.4.0",
"quinn",
"reqwest", "reqwest",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
"serde", "serde",
"thiserror 2.0.17", "thiserror 2.0.17",
"tokio", "tokio",
"tokio-rustls",
"tokio-socks",
"tracing", "tracing",
"url", "url",
"webpki-roots 1.0.5",
] ]
[[package]] [[package]]
@@ -2939,7 +3390,7 @@ dependencies = [
name = "wtfnet-tls" name = "wtfnet-tls"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"rustls", "rustls 0.21.12",
"rustls-native-certs 0.6.3", "rustls-native-certs 0.6.3",
"serde", "serde",
"thiserror 2.0.17", "thiserror 2.0.17",

View File

@@ -81,14 +81,17 @@ Command flags (implemented):
- `sys route`: `--ipv4`, `--ipv6`, `--to <ip>` - `sys route`: `--ipv4`, `--ipv6`, `--to <ip>`
- `ports listen`: `--tcp`, `--udp`, `--port <n>` - `ports listen`: `--tcp`, `--udp`, `--port <n>`
- `neigh list`: `--ipv4`, `--ipv6`, `--iface <name>` - `neigh list`: `--ipv4`, `--ipv6`, `--iface <name>`
- `ports conns`: `--top <n>`, `--by-process`
- `cert baseline`: `<path>`
- `cert diff`: `<path>`
- `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>`, `--per-hop <n>`, `--timeout-ms <n>`, `--udp`, `--port <n>`, `--rdns`, `--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>`
- `http head|get`: `--timeout-ms <n>`, `--follow-redirects <n>`, `--show-headers`, `--show-body`, `--max-body-bytes <n>`, `--http1-only`, `--http2-only`, `--geoip`, `--socks5 <url>` - `http head|get`: `--timeout-ms <n>`, `--follow-redirects <n>`, `--show-headers`, `--show-body`, `--max-body-bytes <n>`, `--http1-only`, `--http2-only`, `--http3` (feature `http3`), `--http3-only` (feature `http3`), `--geoip`, `--socks5 <url>`
- `tls handshake|cert|verify|alpn`: `--sni <name>`, `--alpn <csv>`, `--timeout-ms <n>`, `--insecure`, `--socks5 <url>`, `--prefer-ipv4` - `tls handshake|cert|verify|alpn`: `--sni <name>`, `--alpn <csv>`, `--timeout-ms <n>`, `--insecure`, `--socks5 <url>`, `--prefer-ipv4`, `--show-extensions`, `--ocsp`
- `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>`
@@ -112,6 +115,14 @@ Install:
cmake --build build --target install cmake --build build --target install
``` ```
## HTTP/3 (experimental)
HTTP/3 support is feature-gated and incomplete. Do not enable it in production builds yet.
To enable locally for testing:
```bash
cargo run -p wtfnet-cli --features wtfnet-http/http3 -- http head https://cloudflare-quic.com --http3
```
## Roadmap ## Roadmap
### v0.1 (MVP) ### v0.1 (MVP)
- sys: ifaces/ip/route/dns - sys: ifaces/ip/route/dns
@@ -135,8 +146,8 @@ cmake --build build --target install
- TLS extras: OCSP stapling indicator, richer cert parsing - 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
- optional HTTP/3 (feature-gated)
- optional LLMNR/NBNS discovery - optional LLMNR/NBNS discovery
- optional HTTP/3 (feature-gated; experimental, incomplete)
## Current stage ## Current stage
Implemented: Implemented:

View File

@@ -11,6 +11,7 @@ path = "src/main.rs"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
time = { version = "0.3", features = ["formatting", "parsing"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
wtfnet-core = { path = "../wtfnet-core" } wtfnet-core = { path = "../wtfnet-core" }
wtfnet-calc = { path = "../wtfnet-calc" } wtfnet-calc = { path = "../wtfnet-calc" }

View File

@@ -1,5 +1,5 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use serde::Serialize; use serde::{Deserialize, Serialize};
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
use std::path::PathBuf; use std::path::PathBuf;
use wtfnet_core::{ use wtfnet_core::{
@@ -97,6 +97,7 @@ enum SysCommand {
enum PortsCommand { enum PortsCommand {
Listen(PortsListenArgs), Listen(PortsListenArgs),
Who(PortsWhoArgs), Who(PortsWhoArgs),
Conns(PortsConnsArgs),
} }
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
@@ -107,6 +108,8 @@ enum NeighCommand {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum CertCommand { enum CertCommand {
Roots, Roots,
Baseline(CertBaselineArgs),
Diff(CertDiffArgs),
} }
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
@@ -190,6 +193,14 @@ struct PortsWhoArgs {
target: String, target: String,
} }
#[derive(Parser, Debug, Clone)]
struct PortsConnsArgs {
#[arg(long)]
top: Option<usize>,
#[arg(long)]
by_process: bool,
}
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
struct NeighListArgs { struct NeighListArgs {
#[arg(long)] #[arg(long)]
@@ -205,6 +216,16 @@ struct GeoIpLookupArgs {
target: String, target: String,
} }
#[derive(Parser, Debug, Clone)]
struct CertBaselineArgs {
path: PathBuf,
}
#[derive(Parser, Debug, Clone)]
struct CertDiffArgs {
path: PathBuf,
}
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
struct ProbePingArgs { struct ProbePingArgs {
target: String, target: String,
@@ -339,6 +360,10 @@ struct HttpRequestArgs {
#[arg(long)] #[arg(long)]
http2_only: bool, http2_only: bool,
#[arg(long)] #[arg(long)]
http3: bool,
#[arg(long)]
http3_only: bool,
#[arg(long)]
geoip: bool, geoip: bool,
#[arg(long)] #[arg(long)]
socks5: Option<String>, socks5: Option<String>,
@@ -359,6 +384,10 @@ struct TlsArgs {
socks5: Option<String>, socks5: Option<String>,
#[arg(long)] #[arg(long)]
prefer_ipv4: bool, prefer_ipv4: bool,
#[arg(long)]
show_extensions: bool,
#[arg(long)]
ocsp: bool,
} }
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
@@ -450,6 +479,7 @@ struct HttpReportGeoIp {
pub geoip: Vec<wtfnet_geoip::GeoIpRecord>, pub geoip: Vec<wtfnet_geoip::GeoIpRecord>,
pub headers: Vec<(String, String)>, pub headers: Vec<(String, String)>,
pub body: Option<String>, pub body: Option<String>,
pub warnings: Vec<String>,
pub timing: wtfnet_http::HttpTiming, pub timing: wtfnet_http::HttpTiming,
} }
@@ -481,12 +511,21 @@ async fn main() {
Commands::Ports { Commands::Ports {
command: PortsCommand::Who(args), command: PortsCommand::Who(args),
} => handle_ports_who(&cli, args.clone()).await, } => handle_ports_who(&cli, args.clone()).await,
Commands::Ports {
command: PortsCommand::Conns(args),
} => handle_ports_conns(&cli, args.clone()).await,
Commands::Neigh { Commands::Neigh {
command: NeighCommand::List(args), command: NeighCommand::List(args),
} => handle_neigh_list(&cli, args.clone()).await, } => handle_neigh_list(&cli, args.clone()).await,
Commands::Cert { Commands::Cert {
command: CertCommand::Roots, command: CertCommand::Roots,
} => handle_cert_roots(&cli).await, } => 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 { Commands::Geoip {
command: GeoIpCommand::Lookup(args), command: GeoIpCommand::Lookup(args),
} => handle_geoip_lookup(&cli, args.clone()).await, } => 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 { async fn handle_neigh_list(cli: &Cli, args: NeighListArgs) -> i32 {
let result = platform().neigh.neighbors().await; let result = platform().neigh.neighbors().await;
match result { 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 &current {
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 { async fn handle_geoip_lookup(cli: &Cli, args: GeoIpLookupArgs) -> i32 {
let ip = match args.target.parse::<std::net::IpAddr>() { let ip = match args.target.parse::<std::net::IpAddr>() {
Ok(ip) => ip, Ok(ip) => ip,
@@ -1821,6 +2159,8 @@ 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,
http3: args.http3,
http3_only: args.http3_only,
proxy: args.socks5.clone(), proxy: args.socks5.clone(),
}; };
@@ -1844,6 +2184,7 @@ async fn handle_http_request(
geoip, geoip,
headers: report.headers.clone(), headers: report.headers.clone(),
body: report.body.clone(), body: report.body.clone(),
warnings: report.warnings.clone(),
timing: report.timing.clone(), timing: report.timing.clone(),
} }
} else { } else {
@@ -1857,6 +2198,7 @@ async fn handle_http_request(
geoip: Vec::new(), geoip: Vec::new(),
headers: report.headers.clone(), headers: report.headers.clone(),
body: report.body.clone(), body: report.body.clone(),
warnings: report.warnings.clone(),
timing: report.timing.clone(), timing: report.timing.clone(),
} }
}; };
@@ -1883,6 +2225,11 @@ async fn handle_http_request(
if !report.resolved_ips.is_empty() { if !report.resolved_ips.is_empty() {
println!("resolved: {}", report.resolved_ips.join(", ")); 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); println!("total_ms: {}", report.timing.total_ms);
if let Some(ms) = report.timing.dns_ms { if let Some(ms) = report.timing.dns_ms {
println!("dns_ms: {ms}"); println!("dns_ms: {ms}");
@@ -1973,6 +2320,8 @@ fn build_tls_options(args: &TlsArgs) -> wtfnet_tls::TlsOptions {
insecure: args.insecure, insecure: args.insecure,
socks5: args.socks5.clone(), socks5: args.socks5.clone(),
prefer_ipv4: args.prefer_ipv4, prefer_ipv4: args.prefer_ipv4,
show_extensions: args.show_extensions,
ocsp: args.ocsp,
} }
} }

View File

@@ -5,8 +5,21 @@ edition = "2024"
[dependencies] [dependencies]
reqwest = { version = "0.11", features = ["rustls-tls"] } reqwest = { version = "0.11", features = ["rustls-tls"] }
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 = ["net", "time"] } tokio = { version = "1", features = ["net", "time"] }
tokio-rustls = "0.24"
tokio-socks = "0.5"
url = "2" url = "2"
tracing = "0.1" tracing = "0.1"
h3 = { version = "0.0.8", optional = true }
h3-quinn = { version = "0.0.10", optional = true }
quinn = { version = "0.11", optional = true }
http = "1"
webpki-roots = "1"
bytes = "1"
[features]
http3 = ["dep:h3", "dep:h3-quinn", "dep:quinn"]

View File

@@ -1,12 +1,28 @@
use reqwest::{Client, Method, Proxy, StatusCode}; use reqwest::{Client, Method, Proxy, StatusCode};
use rustls::{Certificate, ClientConfig, RootCertStore, ServerName};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::net::lookup_host; use tokio::net::lookup_host;
use thiserror::Error; use thiserror::Error;
use tokio::time::timeout;
use tokio_rustls::TlsConnector;
use tokio_socks::tcp::Socks5Stream;
use tracing::debug; use tracing::debug;
use url::Url; use url::Url;
#[cfg(feature = "http3")]
use bytes::Buf;
#[cfg(feature = "http3")]
use http::Request;
#[cfg(feature = "http3")]
use quinn::ClientConfig as QuinnClientConfig;
#[cfg(feature = "http3")]
use quinn::Endpoint;
#[cfg(feature = "http3")]
use webpki_roots::TLS_SERVER_ROOTS;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum HttpError { pub enum HttpError {
#[error("invalid url: {0}")] #[error("invalid url: {0}")]
@@ -36,6 +52,7 @@ pub struct HttpReport {
pub resolved_ips: Vec<String>, pub resolved_ips: Vec<String>,
pub headers: Vec<(String, String)>, pub headers: Vec<(String, String)>,
pub body: Option<String>, pub body: Option<String>,
pub warnings: Vec<String>,
pub timing: HttpTiming, pub timing: HttpTiming,
} }
@@ -64,6 +81,8 @@ 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 http3: bool,
pub http3_only: bool,
pub proxy: Option<String>, pub proxy: Option<String>,
} }
@@ -105,6 +124,39 @@ pub async fn request(url: &str, opts: HttpRequestOptions) -> Result<HttpReport,
} }
let dns_ms = dns_start.elapsed().as_millis(); let dns_ms = dns_start.elapsed().as_millis();
let mut warnings = Vec::new();
if opts.http3 || opts.http3_only {
if !cfg!(feature = "http3") {
warnings.push("http3 feature not enabled in build".to_string());
if opts.http3_only {
return Err(HttpError::Request(
"http3-only requested but feature is not enabled".to_string(),
));
}
}
}
#[cfg(feature = "http3")]
{
if opts.http3 || opts.http3_only {
match http3_request(url, &opts, &resolved_ips, dns_ms).await {
Ok((report, mut h3_warnings)) => {
warnings.append(&mut h3_warnings);
return Ok(HttpReport {
warnings,
..report
});
}
Err(err) => {
warnings.push(format!("http3 failed: {err}"));
if opts.http3_only {
return Err(err);
}
}
}
}
}
let mut builder = Client::builder().timeout(Duration::from_millis(opts.timeout_ms)); let mut builder = Client::builder().timeout(Duration::from_millis(opts.timeout_ms));
builder = if let Some(max) = opts.follow_redirects { builder = if let Some(max) = opts.follow_redirects {
builder.redirect(reqwest::redirect::Policy::limited(max as usize)) builder.redirect(reqwest::redirect::Policy::limited(max as usize))
@@ -132,6 +184,16 @@ pub async fn request(url: &str, opts: HttpRequestOptions) -> Result<HttpReport,
} }
let client = builder.build().map_err(|err| HttpError::Request(err.to_string()))?; let client = builder.build().map_err(|err| HttpError::Request(err.to_string()))?;
let (connect_ms, tls_ms, timing_warnings) = measure_connect_tls(
&parsed,
host,
port,
&resolved_ips,
opts.proxy.as_deref(),
opts.timeout_ms,
)
.await;
warnings.extend(timing_warnings);
let start = Instant::now(); let start = Instant::now();
let response = client let response = client
.request(opts.method.to_reqwest(), parsed.clone()) .request(opts.method.to_reqwest(), parsed.clone())
@@ -184,11 +246,12 @@ pub async fn request(url: &str, opts: HttpRequestOptions) -> Result<HttpReport,
resolved_ips, resolved_ips,
headers, headers,
body, body,
warnings,
timing: HttpTiming { timing: HttpTiming {
total_ms, total_ms,
dns_ms: Some(dns_ms), dns_ms: Some(dns_ms),
connect_ms: None, connect_ms,
tls_ms: None, tls_ms,
ttfb_ms: Some(ttfb_ms), ttfb_ms: Some(ttfb_ms),
}, },
}) })
@@ -197,3 +260,311 @@ pub async fn request(url: &str, opts: HttpRequestOptions) -> Result<HttpReport,
fn status_code(status: StatusCode) -> Option<u16> { fn status_code(status: StatusCode) -> Option<u16> {
Some(status.as_u16()) Some(status.as_u16())
} }
struct Socks5Proxy {
addr: String,
remote_dns: bool,
}
fn parse_socks5_proxy(value: &str) -> Result<Socks5Proxy, HttpError> {
let url = Url::parse(value).map_err(|err| HttpError::Request(err.to_string()))?;
let scheme = url.scheme();
let remote_dns = match scheme {
"socks5" => false,
"socks5h" => true,
_ => {
return Err(HttpError::Request(format!(
"unsupported proxy scheme: {scheme}"
)))
}
};
let host = url
.host_str()
.ok_or_else(|| HttpError::Request("invalid proxy host".to_string()))?;
let port = url
.port_or_known_default()
.ok_or_else(|| HttpError::Request("invalid proxy port".to_string()))?;
Ok(Socks5Proxy {
addr: format!("{host}:{port}"),
remote_dns,
})
}
async fn measure_connect_tls(
parsed: &Url,
host: &str,
port: u16,
resolved_ips: &[String],
proxy: Option<&str>,
timeout_ms: u64,
) -> (Option<u128>, Option<u128>, Vec<String>) {
let mut warnings = Vec::new();
let scheme = parsed.scheme();
if scheme != "http" && scheme != "https" {
warnings.push(format!("timing unavailable for scheme: {scheme}"));
return (None, None, warnings);
}
let timeout_dur = Duration::from_millis(timeout_ms);
let connect_start = Instant::now();
let tcp = if let Some(proxy) = proxy {
match parse_socks5_proxy(proxy) {
Ok(proxy) => {
let target = if proxy.remote_dns {
(host, port)
} else if let Some(ip) = resolved_ips.first() {
(ip.as_str(), port)
} else {
warnings.push("no resolved IPs for proxy connect".to_string());
return (None, None, warnings);
};
match timeout(timeout_dur, Socks5Stream::connect(proxy.addr.as_str(), target))
.await
{
Ok(Ok(stream)) => stream.into_inner(),
Ok(Err(err)) => {
warnings.push(format!("proxy connect failed: {err}"));
return (None, None, warnings);
}
Err(_) => {
warnings.push("proxy connect timed out".to_string());
return (None, None, warnings);
}
}
}
Err(err) => {
warnings.push(format!("proxy timing skipped: {err}"));
return (None, None, warnings);
}
}
} else {
let addr = match resolved_ips.first().and_then(|ip| ip.parse::<IpAddr>().ok()) {
Some(ip) => SocketAddr::new(ip, port),
None => {
warnings.push("no resolved IPs for connect timing".to_string());
return (None, None, warnings);
}
};
match timeout(timeout_dur, tokio::net::TcpStream::connect(addr)).await {
Ok(Ok(stream)) => stream,
Ok(Err(err)) => {
warnings.push(format!("connect failed: {err}"));
return (None, None, warnings);
}
Err(_) => {
warnings.push("connect timed out".to_string());
return (None, None, warnings);
}
}
};
let connect_ms = connect_start.elapsed().as_millis();
if scheme == "http" {
return (Some(connect_ms), None, warnings);
}
let tls_start = Instant::now();
let tls = match build_tls_connector() {
Ok(connector) => connector,
Err(err) => {
warnings.push(format!("tls timing skipped: {err}"));
return (Some(connect_ms), None, warnings);
}
};
let server_name = match ServerName::try_from(host) {
Ok(name) => name,
Err(_) => {
warnings.push("invalid tls server name".to_string());
return (Some(connect_ms), None, warnings);
}
};
match timeout(timeout_dur, tls.connect(server_name, tcp)).await {
Ok(Ok(_)) => {}
Ok(Err(err)) => {
warnings.push(format!("tls handshake failed: {err}"));
return (Some(connect_ms), None, warnings);
}
Err(_) => {
warnings.push("tls handshake timed out".to_string());
return (Some(connect_ms), None, warnings);
}
}
let tls_ms = tls_start.elapsed().as_millis();
(Some(connect_ms), Some(tls_ms), warnings)
}
fn build_tls_connector() -> Result<TlsConnector, HttpError> {
let mut roots = RootCertStore::empty();
let store = rustls_native_certs::load_native_certs()
.map_err(|err| HttpError::Request(err.to_string()))?;
for cert in store {
roots
.add(&Certificate(cert.0))
.map_err(|err| HttpError::Request(err.to_string()))?;
}
let config = ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(roots)
.with_no_client_auth();
Ok(TlsConnector::from(Arc::new(config)))
}
#[cfg(feature = "http3")]
async fn http3_request(
url: &str,
opts: &HttpRequestOptions,
resolved_ips: &[String],
dns_ms: u128,
) -> Result<(HttpReport, Vec<String>), HttpError> {
let mut warnings = Vec::new();
let parsed = Url::parse(url).map_err(|err| HttpError::Url(err.to_string()))?;
if parsed.scheme() != "https" {
return Err(HttpError::Request("http3 requires https scheme".to_string()));
}
if opts.proxy.is_some() {
return Err(HttpError::Request(
"http3 proxying is not supported".to_string(),
));
}
let host = parsed
.host_str()
.ok_or_else(|| HttpError::Url("missing host".to_string()))?;
let port = parsed
.port_or_known_default()
.ok_or_else(|| HttpError::Url("missing port".to_string()))?;
let ip = resolved_ips
.first()
.and_then(|value| value.parse::<IpAddr>().ok())
.ok_or_else(|| HttpError::Request("no resolved IPs for http3".to_string()))?;
let quinn_config = build_quinn_config()?;
let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap())
.map_err(|err| HttpError::Request(err.to_string()))?;
endpoint.set_default_client_config(quinn_config);
let connect_start = Instant::now();
let connecting = endpoint
.connect(SocketAddr::new(ip, port), host)
.map_err(|err| HttpError::Request(err.to_string()))?;
let connection = timeout(Duration::from_millis(opts.timeout_ms), connecting)
.await
.map_err(|_| HttpError::Request("http3 connect timed out".to_string()))?
.map_err(|err| HttpError::Request(err.to_string()))?;
let connect_ms = connect_start.elapsed().as_millis();
let conn = h3_quinn::Connection::new(connection);
let (mut driver, mut send_request) = h3::client::new(conn)
.await
.map_err(|err| HttpError::Request(err.to_string()))?;
tokio::spawn(async move {
let _ = driver.wait_idle().await;
});
let start = Instant::now();
let method = match opts.method {
HttpMethod::Head => http::Method::HEAD,
HttpMethod::Get => http::Method::GET,
};
let request = Request::builder()
.method(method)
.uri(parsed.as_str())
.header("user-agent", "wtfnet")
.body(())
.map_err(|err| HttpError::Request(err.to_string()))?;
let mut stream = send_request
.send_request(request)
.await
.map_err(|err| HttpError::Request(err.to_string()))?;
stream
.finish()
.await
.map_err(|err| HttpError::Request(err.to_string()))?;
let response = stream
.recv_response()
.await
.map_err(|err| HttpError::Response(err.to_string()))?;
let ttfb_ms = start.elapsed().as_millis();
let status = response.status();
let final_url = parsed.to_string();
let headers = if opts.show_headers {
response
.headers()
.iter()
.map(|(name, value)| {
let value = value.to_str().unwrap_or("-").to_string();
(name.to_string(), value)
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
let body = if opts.show_body {
let mut buf = Vec::new();
while let Some(chunk) = stream
.recv_data()
.await
.map_err(|err| HttpError::Response(err.to_string()))?
{
let mut chunk = chunk;
while chunk.has_remaining() {
let bytes = chunk.copy_to_bytes(chunk.remaining());
buf.extend_from_slice(&bytes);
}
if buf.len() >= opts.max_body_bytes {
buf.truncate(opts.max_body_bytes);
break;
}
}
Some(String::from_utf8_lossy(&buf).to_string())
} else {
None
};
let total_ms = start.elapsed().as_millis();
warnings.push("http3 timing for tls/connect is best-effort".to_string());
Ok((
HttpReport {
url: url.to_string(),
final_url: Some(final_url),
method: match opts.method {
HttpMethod::Head => "HEAD".to_string(),
HttpMethod::Get => "GET".to_string(),
},
status: Some(status.as_u16()),
http_version: Some("HTTP/3".to_string()),
resolved_ips: resolved_ips.to_vec(),
headers,
body,
warnings: Vec::new(),
timing: HttpTiming {
total_ms,
dns_ms: Some(dns_ms),
connect_ms: Some(connect_ms),
tls_ms: None,
ttfb_ms: Some(ttfb_ms),
},
},
warnings,
))
}
#[cfg(feature = "http3")]
fn build_quinn_config() -> Result<QuinnClientConfig, HttpError> {
let mut roots = quinn::rustls::RootCertStore::empty();
roots.extend(TLS_SERVER_ROOTS.iter().cloned());
let mut client_config =
QuinnClientConfig::with_root_certificates(Arc::new(roots)).map_err(|err| {
HttpError::Request(format!("quinn config error: {err}"))
})?;
let mut transport = quinn::TransportConfig::default();
transport.keep_alive_interval(Some(Duration::from_secs(5)));
client_config.transport_config(Arc::new(transport));
Ok(client_config)
}

View File

@@ -5,8 +5,8 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use wtfnet_core::ErrorCode; use wtfnet_core::ErrorCode;
use wtfnet_platform::{ use wtfnet_platform::{
CertProvider, DnsConfigSnapshot, ListenSocket, NeighborEntry, NeighProvider, NetInterface, CertProvider, ConnSocket, DnsConfigSnapshot, ListenSocket, NeighborEntry, NeighProvider,
Platform, PlatformError, PortsProvider, RootCert, RouteEntry, SysProvider, NetInterface, Platform, PlatformError, PortsProvider, RootCert, RouteEntry, SysProvider,
}; };
use x509_parser::oid_registry::{ use x509_parser::oid_registry::{
OID_KEY_TYPE_DSA, OID_KEY_TYPE_EC_PUBLIC_KEY, OID_KEY_TYPE_GOST_R3410_2012_256, OID_KEY_TYPE_DSA, OID_KEY_TYPE_EC_PUBLIC_KEY, OID_KEY_TYPE_GOST_R3410_2012_256,
@@ -240,6 +240,63 @@ fn parse_linux_tcp_with_inode_map(
Ok(sockets) Ok(sockets)
} }
fn parse_linux_tcp_conns(
path: &str,
is_v6: bool,
inode_map: &HashMap<String, ProcInfo>,
) -> Result<Vec<ConnSocket>, PlatformError> {
let contents = std::fs::read_to_string(path)
.map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?;
let mut sockets = Vec::new();
for (idx, line) in contents.lines().enumerate() {
if idx == 0 {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 {
continue;
}
let local = parts[1];
let remote = parts[2];
let state = parts[3];
let inode = parts.get(9).copied();
if state == "0A" {
continue;
}
let local_addr = match parse_proc_socket_addr(local, is_v6) {
Some(addr) => addr,
None => continue,
};
let remote_addr = match parse_proc_socket_addr(remote, is_v6) {
Some(addr) => addr,
None => continue,
};
let (pid, ppid, process_name, process_path) =
inode.and_then(|value| inode_map.get(value)).map_or(
(None, None, None, None),
|info| {
(
Some(info.pid),
info.ppid,
info.name.clone(),
info.path.clone(),
)
},
);
sockets.push(ConnSocket {
proto: "tcp".to_string(),
local_addr,
remote_addr,
state: Some(map_tcp_state(state)),
pid,
ppid,
process_name,
process_path,
});
}
Ok(sockets)
}
fn parse_linux_udp_with_inode_map( fn parse_linux_udp_with_inode_map(
path: &str, path: &str,
is_v6: bool, is_v6: bool,
@@ -286,6 +343,24 @@ fn parse_linux_udp_with_inode_map(
Ok(sockets) Ok(sockets)
} }
fn map_tcp_state(value: &str) -> String {
match value {
"01" => "ESTABLISHED",
"02" => "SYN_SENT",
"03" => "SYN_RECV",
"04" => "FIN_WAIT1",
"05" => "FIN_WAIT2",
"06" => "TIME_WAIT",
"07" => "CLOSE",
"08" => "CLOSE_WAIT",
"09" => "LAST_ACK",
"0A" => "LISTEN",
"0B" => "CLOSING",
_ => "UNKNOWN",
}
.to_string()
}
fn parse_proc_socket_addr(value: &str, is_v6: bool) -> Option<String> { fn parse_proc_socket_addr(value: &str, is_v6: bool) -> Option<String> {
let mut parts = value.split(':'); let mut parts = value.split(':');
let addr_hex = parts.next()?; let addr_hex = parts.next()?;
@@ -518,6 +593,22 @@ impl PortsProvider for LinuxPortsProvider {
.filter(|socket| extract_port(&socket.local_addr) == Some(port)) .filter(|socket| extract_port(&socket.local_addr) == Some(port))
.collect()) .collect())
} }
async fn connections(&self) -> Result<Vec<ConnSocket>, PlatformError> {
let inode_map = build_inode_map();
let mut sockets = Vec::new();
sockets.extend(parse_linux_tcp_conns(
"/proc/net/tcp",
false,
&inode_map,
)?);
sockets.extend(parse_linux_tcp_conns(
"/proc/net/tcp6",
true,
&inode_map,
)?);
Ok(sockets)
}
} }
#[async_trait] #[async_trait]

View File

@@ -10,8 +10,8 @@ use x509_parser::oid_registry::{
use std::sync::Arc; use std::sync::Arc;
use wtfnet_core::ErrorCode; use wtfnet_core::ErrorCode;
use wtfnet_platform::{ use wtfnet_platform::{
CertProvider, DnsConfigSnapshot, ListenSocket, NeighborEntry, NeighProvider, NetInterface, CertProvider, ConnSocket, DnsConfigSnapshot, ListenSocket, NeighborEntry, NeighProvider,
Platform, PlatformError, PortsProvider, RootCert, RouteEntry, SysProvider, NetInterface, Platform, PlatformError, PortsProvider, RootCert, RouteEntry, SysProvider,
}; };
pub fn platform() -> Platform { pub fn platform() -> Platform {
@@ -333,6 +333,33 @@ fn parse_windows_listeners() -> Result<Vec<ListenSocket>, PlatformError> {
Ok(sockets) Ok(sockets)
} }
fn parse_windows_connections() -> Result<Vec<ConnSocket>, PlatformError> {
let proc_map = load_windows_process_map();
let output = std::process::Command::new("netstat")
.arg("-ano")
.output()
.map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?;
if !output.status.success() {
return Err(PlatformError::new(ErrorCode::IoError, "netstat -ano failed"));
}
let text = String::from_utf8_lossy(&output.stdout);
let mut sockets = Vec::new();
for line in text.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("TCP") {
continue;
}
if let Some(mut socket) = parse_netstat_tcp_conn_line(trimmed) {
enrich_conn_socket(&mut socket, &proc_map);
sockets.push(socket);
}
}
Ok(sockets)
}
fn parse_netstat_tcp_line(line: &str) -> Option<ListenSocket> { fn parse_netstat_tcp_line(line: &str) -> Option<ListenSocket> {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 5 { if parts.len() < 5 {
@@ -358,6 +385,32 @@ fn parse_netstat_tcp_line(line: &str) -> Option<ListenSocket> {
}) })
} }
fn parse_netstat_tcp_conn_line(line: &str) -> Option<ConnSocket> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 5 {
return None;
}
let local = parts[1];
let remote = parts[2];
let state = parts[3];
let pid = parts[4].parse::<u32>().ok();
if state == "LISTENING" {
return None;
}
Some(ConnSocket {
proto: "tcp".to_string(),
local_addr: local.to_string(),
remote_addr: remote.to_string(),
state: Some(state.to_string()),
pid,
ppid: None,
process_name: None,
process_path: None,
})
}
fn parse_netstat_udp_line(line: &str) -> Option<ListenSocket> { fn parse_netstat_udp_line(line: &str) -> Option<ListenSocket> {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 { if parts.len() < 4 {
@@ -429,6 +482,17 @@ fn enrich_socket(socket: &mut ListenSocket, map: &HashMap<u32, ProcInfo>) {
} }
} }
fn enrich_conn_socket(socket: &mut ConnSocket, map: &HashMap<u32, ProcInfo>) {
let pid = match socket.pid {
Some(pid) => pid,
None => return,
};
if let Some(info) = map.get(&pid) {
socket.process_name = info.name.clone();
socket.process_path = info.path.clone();
}
}
#[derive(Clone)] #[derive(Clone)]
struct ProcInfo { struct ProcInfo {
name: Option<String>, name: Option<String>,
@@ -605,6 +669,10 @@ impl PortsProvider for WindowsPortsProvider {
.filter(|socket| extract_port(&socket.local_addr) == Some(port)) .filter(|socket| extract_port(&socket.local_addr) == Some(port))
.collect()) .collect())
} }
async fn connections(&self) -> Result<Vec<ConnSocket>, PlatformError> {
parse_windows_connections()
}
} }
#[async_trait] #[async_trait]

View File

@@ -46,6 +46,18 @@ pub struct ListenSocket {
pub owner: Option<String>, pub owner: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnSocket {
pub proto: String,
pub local_addr: String,
pub remote_addr: String,
pub state: Option<String>,
pub pid: Option<u32>,
pub ppid: Option<u32>,
pub process_name: Option<String>,
pub process_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RootCert { pub struct RootCert {
pub subject: String, pub subject: String,
@@ -98,6 +110,7 @@ pub trait SysProvider: Send + Sync {
pub trait PortsProvider: Send + Sync { pub trait PortsProvider: Send + Sync {
async fn listening(&self) -> Result<Vec<ListenSocket>, PlatformError>; async fn listening(&self) -> Result<Vec<ListenSocket>, PlatformError>;
async fn who_owns(&self, port: u16) -> Result<Vec<ListenSocket>, PlatformError>; async fn who_owns(&self, port: u16) -> Result<Vec<ListenSocket>, PlatformError>;
async fn connections(&self) -> Result<Vec<ConnSocket>, PlatformError>;
} }
#[async_trait] #[async_trait]

View File

@@ -35,6 +35,9 @@ pub struct TlsCertSummary {
pub not_before: String, pub not_before: String,
pub not_after: String, pub not_after: String,
pub san: Vec<String>, pub san: Vec<String>,
pub signature_algorithm: Option<String>,
pub key_usage: Option<Vec<String>>,
pub extended_key_usage: Option<Vec<String>>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -45,6 +48,7 @@ pub struct TlsHandshakeReport {
pub alpn_negotiated: Option<String>, pub alpn_negotiated: Option<String>,
pub tls_version: Option<String>, pub tls_version: Option<String>,
pub cipher: Option<String>, pub cipher: Option<String>,
pub ocsp_stapled: Option<bool>,
pub cert_chain: Vec<TlsCertSummary>, pub cert_chain: Vec<TlsCertSummary>,
} }
@@ -56,6 +60,7 @@ pub struct TlsVerifyReport {
pub alpn_negotiated: Option<String>, pub alpn_negotiated: Option<String>,
pub tls_version: Option<String>, pub tls_version: Option<String>,
pub cipher: Option<String>, pub cipher: Option<String>,
pub ocsp_stapled: Option<bool>,
pub verified: bool, pub verified: bool,
pub error: Option<String>, pub error: Option<String>,
} }
@@ -64,6 +69,7 @@ pub struct TlsVerifyReport {
pub struct TlsCertReport { pub struct TlsCertReport {
pub target: String, pub target: String,
pub sni: Option<String>, pub sni: Option<String>,
pub ocsp_stapled: Option<bool>,
pub cert_chain: Vec<TlsCertSummary>, pub cert_chain: Vec<TlsCertSummary>,
} }
@@ -83,6 +89,8 @@ pub struct TlsOptions {
pub insecure: bool, pub insecure: bool,
pub socks5: Option<String>, pub socks5: Option<String>,
pub prefer_ipv4: bool, pub prefer_ipv4: bool,
pub show_extensions: bool,
pub ocsp: bool,
} }
pub async fn handshake(target: &str, options: TlsOptions) -> Result<TlsHandshakeReport, TlsError> { pub async fn handshake(target: &str, options: TlsOptions) -> Result<TlsHandshakeReport, TlsError> {
@@ -120,7 +128,8 @@ pub async fn handshake(target: &str, options: TlsOptions) -> Result<TlsHandshake
cipher: session cipher: session
.negotiated_cipher_suite() .negotiated_cipher_suite()
.map(|suite| format!("{suite:?}")), .map(|suite| format!("{suite:?}")),
cert_chain: extract_cert_chain(session.peer_certificates())?, ocsp_stapled: ocsp_status(session, options.ocsp),
cert_chain: extract_cert_chain(session.peer_certificates(), options.show_extensions)?,
}) })
} }
@@ -160,6 +169,7 @@ pub async fn verify(target: &str, options: TlsOptions) -> Result<TlsVerifyReport
cipher: session cipher: session
.negotiated_cipher_suite() .negotiated_cipher_suite()
.map(|suite| format!("{suite:?}")), .map(|suite| format!("{suite:?}")),
ocsp_stapled: ocsp_status(session, options.ocsp),
verified: true, verified: true,
error: None, error: None,
}) })
@@ -171,6 +181,7 @@ pub async fn verify(target: &str, options: TlsOptions) -> Result<TlsVerifyReport
alpn_negotiated: None, alpn_negotiated: None,
tls_version: None, tls_version: None,
cipher: None, cipher: None,
ocsp_stapled: None,
verified: false, verified: false,
error: Some(err.to_string()), error: Some(err.to_string()),
}), }),
@@ -203,7 +214,8 @@ pub async fn certs(target: &str, options: TlsOptions) -> Result<TlsCertReport, T
Ok(TlsCertReport { Ok(TlsCertReport {
target: target.to_string(), target: target.to_string(),
sni: options.sni, sni: options.sni,
cert_chain: extract_cert_chain(session.peer_certificates())?, ocsp_stapled: ocsp_status(session, options.ocsp),
cert_chain: extract_cert_chain(session.peer_certificates(), options.show_extensions)?,
}) })
} }
@@ -427,26 +439,41 @@ fn socks5_target_host(proxy: &str, host: &str) -> (String, bool) {
(host.to_string(), remote_dns) (host.to_string(), remote_dns)
} }
fn extract_cert_chain(certs: Option<&[Certificate]>) -> Result<Vec<TlsCertSummary>, TlsError> { fn extract_cert_chain(
certs: Option<&[Certificate]>,
show_extensions: bool,
) -> Result<Vec<TlsCertSummary>, TlsError> {
let mut results = Vec::new(); let mut results = Vec::new();
if let Some(certs) = certs { if let Some(certs) = certs {
for cert in certs { for cert in certs {
let summary = parse_cert(&cert.0)?; let summary = parse_cert(&cert.0, show_extensions)?;
results.push(summary); results.push(summary);
} }
} }
Ok(results) Ok(results)
} }
fn parse_cert(der: &[u8]) -> Result<TlsCertSummary, TlsError> { fn parse_cert(der: &[u8], show_extensions: bool) -> Result<TlsCertSummary, TlsError> {
let (_, cert) = let (_, cert) =
X509Certificate::from_der(der).map_err(|err| TlsError::Parse(err.to_string()))?; X509Certificate::from_der(der).map_err(|err| TlsError::Parse(err.to_string()))?;
let (key_usage, extended_key_usage, signature_algorithm) = if show_extensions {
(
extract_key_usage(&cert),
extract_extended_key_usage(&cert),
Some(cert.signature_algorithm.algorithm.to_string()),
)
} else {
(None, None, None)
};
Ok(TlsCertSummary { Ok(TlsCertSummary {
subject: cert.subject().to_string(), subject: cert.subject().to_string(),
issuer: cert.issuer().to_string(), issuer: cert.issuer().to_string(),
not_before: cert.validity().not_before.to_string(), not_before: cert.validity().not_before.to_string(),
not_after: cert.validity().not_after.to_string(), not_after: cert.validity().not_after.to_string(),
san: extract_san(&cert), san: extract_san(&cert),
signature_algorithm,
key_usage,
extended_key_usage,
}) })
} }
@@ -460,6 +487,85 @@ fn extract_san(cert: &X509Certificate<'_>) -> Vec<String> {
result result
} }
fn extract_key_usage(cert: &X509Certificate<'_>) -> Option<Vec<String>> {
let ext = cert.key_usage().ok()??;
let mut result = Vec::new();
if ext.value.digital_signature() {
result.push("digitalSignature".to_string());
}
if ext.value.non_repudiation() {
result.push("nonRepudiation".to_string());
}
if ext.value.key_encipherment() {
result.push("keyEncipherment".to_string());
}
if ext.value.data_encipherment() {
result.push("dataEncipherment".to_string());
}
if ext.value.key_agreement() {
result.push("keyAgreement".to_string());
}
if ext.value.key_cert_sign() {
result.push("keyCertSign".to_string());
}
if ext.value.crl_sign() {
result.push("cRLSign".to_string());
}
if ext.value.encipher_only() {
result.push("encipherOnly".to_string());
}
if ext.value.decipher_only() {
result.push("decipherOnly".to_string());
}
if result.is_empty() {
None
} else {
Some(result)
}
}
fn extract_extended_key_usage(cert: &X509Certificate<'_>) -> Option<Vec<String>> {
let ext = cert.extended_key_usage().ok()??;
let mut result = Vec::new();
if ext.value.any {
result.push("any".to_string());
}
if ext.value.server_auth {
result.push("serverAuth".to_string());
}
if ext.value.client_auth {
result.push("clientAuth".to_string());
}
if ext.value.code_signing {
result.push("codeSigning".to_string());
}
if ext.value.email_protection {
result.push("emailProtection".to_string());
}
if ext.value.time_stamping {
result.push("timeStamping".to_string());
}
if ext.value.ocsp_signing {
result.push("ocspSigning".to_string());
}
for oid in &ext.value.other {
result.push(oid.to_string());
}
if result.is_empty() {
None
} else {
Some(result)
}
}
fn ocsp_status(_session: &rustls::ClientConnection, enabled: bool) -> Option<bool> {
if enabled {
None
} else {
None
}
}
struct NoVerifier; struct NoVerifier;
impl rustls::client::ServerCertVerifier for NoVerifier { impl rustls::client::ServerCertVerifier for NoVerifier {

View File

@@ -10,7 +10,7 @@ v0.3.0 focuses on improving diagnostic depth and fidelity of existing commands r
Major upgrades in this release: Major upgrades in this release:
- richer traceroute output and per-hop statistics - richer traceroute output and per-hop statistics
- HTTP timing breakdown accuracy (connect/tls stages) - HTTP timing breakdown accuracy (connect/tls stages)
- optional HTTP/3 support (best-effort) - optional HTTP/3 support (feature-gated; experimental)
- TLS diagnostics upgrades (OCSP stapling indicator, richer certificate parsing) - TLS diagnostics upgrades (OCSP stapling indicator, richer certificate parsing)
- ports connections view and summaries - ports connections view and summaries
- improved cert baseline/diff for system roots - improved cert baseline/diff for system roots
@@ -67,7 +67,7 @@ Acceptance:
- on timeout / failure, partial timing must still be meaningful. - on timeout / failure, partial timing must still be meaningful.
### 3.3 HTTP/3 (optional feature flag) (SHOULD) ### 3.3 HTTP/3 (optional feature flag) (SHOULD)
Current: HTTP/3 not implemented. Current: feature-gated HTTP/3 path exists but is incomplete; keep disabled in default builds.
Target: Target:
- add `--http3` support behind Cargo feature `http3` - add `--http3` support behind Cargo feature `http3`
- behavior: - behavior:
@@ -79,6 +79,7 @@ Target:
Acceptance: Acceptance:
- builds without `http3` feature still work - builds without `http3` feature still work
- with feature enabled, HTTP/3 works on at least one known compatible endpoint - with feature enabled, HTTP/3 works on at least one known compatible endpoint
- documented as experimental until stabilized
### 3.4 TLS extras: OCSP + richer cert parsing (MUST) ### 3.4 TLS extras: OCSP + richer cert parsing (MUST)
Current: `tls handshake/verify/cert/alpn` exists. Current: `tls handshake/verify/cert/alpn` exists.

View File

@@ -3,42 +3,43 @@
This is a practical checklist to execute v0.3.0. This is a practical checklist to execute v0.3.0.
## 1) probe/trace upgrades ## 1) probe/trace upgrades
- [ ] add `--per-hop <n>` and store RTT samples per hop - [x] add `--per-hop <n>` and store RTT samples per hop
- [ ] compute loss% per hop - [x] compute loss% per hop
- [ ] add `--rdns` best-effort reverse lookup (cached + time-bounded) - [x] add `--rdns` best-effort reverse lookup (cached + time-bounded)
- [ ] improve hop formatting + JSON schema - [x] improve hop formatting + JSON schema
## 2) http timing improvements ## 2) http timing improvements
- [ ] implement `connect_ms` and `tls_ms` timing - [x] implement `connect_ms` and `tls_ms` timing
- [ ] report `null` + warning when measurement unavailable - [x] report `null` + warning when measurement unavailable
- [ ] keep current `dns_ms` and `ttfb_ms` - [ ] keep current `dns_ms` and `ttfb_ms`
## 3) optional HTTP/3 ## 3) tls extras
- [ ] add `http3` cargo feature + deps - [x] add OCSP stapling presence indicator (if available)
- [ ] implement `--http3` / `--http3-only` - [x] parse SANs and key usage / EKU best-effort
- [ ] define error classification for QUIC failures - [x] add `--show-extensions` and `--ocsp` flags
## 4) tls extras ## 4) ports conns
- [ ] add OCSP stapling presence indicator (if available) - [x] implement `wtfn ports conns`
- [ ] parse SANs and key usage / EKU best-effort - [x] add `--top <n>` and `--by-process`
- [ ] add `--show-extensions` and `--ocsp` flags - [x] best-effort PID mapping with warnings
## 5) ports conns ## 5) cert baseline/diff improvements
- [ ] implement `wtfn ports conns` - [x] baseline schema version
- [ ] add `--top <n>` and `--by-process` - [x] match by SHA256 fingerprint
- [ ] best-effort PID mapping with warnings - [x] diff categories: add/remove/expired/changed
## 6) cert baseline/diff improvements ## 6) optional LLMNR/NBNS
- [ ] baseline schema version
- [ ] match by SHA256 fingerprint
- [ ] diff categories: add/remove/expired/changed
## 7) optional LLMNR/NBNS
- [ ] implement `discover llmnr` - [ ] implement `discover llmnr`
- [ ] implement `discover nbns` - [ ] implement `discover nbns`
- [ ] bounded collection, low-noise - [ ] bounded collection, low-noise
## 8) docs updates ## 7) docs updates
- [ ] update README roadmap - [x] update README roadmap
- [ ] update COMMANDS.md with new flags/commands - [ ] update COMMANDS.md with new flags/commands
- [ ] add RELEASE_v0.3.0.md - [ ] add RELEASE_v0.3.0.md
## 8) optional HTTP/3 (last)
- [x] add `http3` cargo feature + deps
- [x] implement `--http3` / `--http3-only`
- [ ] define error classification for QUIC failures
- [ ] keep feature disabled in default builds until stabilized

View File

@@ -9,7 +9,7 @@ This document tracks current implementation status against the original design i
- GeoIP: local GeoLite2 Country + ASN support. - GeoIP: local GeoLite2 Country + ASN support.
- Probe: ping/tcping/trace with GeoIP enrichment. - Probe: ping/tcping/trace with GeoIP enrichment.
- DNS: Hickory-based query/detect with best-effort heuristics. - DNS: Hickory-based query/detect with best-effort heuristics.
- HTTP: head/get via reqwest. - HTTP: head/get via reqwest with best-effort timing breakdown and optional HTTP/3 (feature-gated).
- TLS: rustls-based handshake/verify/cert/alpn. - TLS: rustls-based handshake/verify/cert/alpn.
- Discover: mDNS/SSDP bounded collection. - Discover: mDNS/SSDP bounded collection.
- Diag: bundle export in zip. - Diag: bundle export in zip.
@@ -17,11 +17,11 @@ This document tracks current implementation status against the original design i
## Deviations or refinements ## Deviations or refinements
- DNS adds DoT/DoH and SOCKS5 proxy support. - DNS adds DoT/DoH and SOCKS5 proxy support.
- HTTP/TLS/TCP ping include 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`/`ttfb_ms` are captured; `connect_ms`/`tls_ms` are measured via a separate probe and can be `null` with warnings.
- DNS watch uses `pnet` and is feature-gated as best-effort. - DNS watch uses `pnet` and is feature-gated as best-effort.
## Gaps vs design (as of now) ## Gaps vs design (as of now)
- HTTP/3 not implemented. - HTTP/3 is feature-gated and incomplete; not enabled in default builds.
- TLS verification is rustls-based (no OS-native verifier). - TLS verification is rustls-based (no OS-native verifier).
- Discover does not include LLMNR/NBNS. - Discover does not include LLMNR/NBNS.

View File

@@ -26,8 +26,8 @@ This document tracks the planned roadmap alongside the current implementation st
- TLS extras: OCSP stapling indicator, richer cert parsing - 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
- optional HTTP/3 (feature-gated)
- optional LLMNR/NBNS discovery - optional LLMNR/NBNS discovery
- optional HTTP/3 (feature-gated; experimental, incomplete)
## Current stage ## Current stage
@@ -63,12 +63,18 @@ This document tracks the planned roadmap alongside the current implementation st
- HTTP crate with head/get support, timing breakdown, optional GeoIP, and SOCKS5 proxy. - HTTP crate with head/get support, timing breakdown, optional GeoIP, and SOCKS5 proxy.
- TLS crate with handshake/verify/cert/alpn support in CLI (SOCKS5 proxy supported). - TLS crate with handshake/verify/cert/alpn support in CLI (SOCKS5 proxy supported).
- TCP ping supports SOCKS5 proxy. - TCP ping supports SOCKS5 proxy.
- v0.3: probe trace per-hop stats + rdns support.
- v0.3: http connect/tls timing best-effort with warnings.
- v0.3: ports conns (active TCP connections + summaries).
- v0.3: TLS extras (OCSP flag + richer cert parsing).
- v0.3: cert baseline/diff improvements.
- v0.3: HTTP/3 request path (feature-gated; experimental, incomplete).
- 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.
### In progress ### In progress
- v0.3: probe trace upgrades (per-hop stats + rdns). - v0.3: optional HTTP/3 (feature-gated; keep disabled until stabilized).
### Next ### Next
- Complete v0.3 trace upgrades and update CLI output. - Complete v0.3 trace upgrades and update CLI output.