diff --git a/Cargo.lock b/Cargo.lock index fd52d49..d11a57b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,12 +217,24 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "cipher" version = "0.4.4" @@ -279,6 +291,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "concurrent-queue" version = "2.5.0" @@ -304,6 +326,16 @@ dependencies = [ "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]] name = "core-foundation-sys" version = "0.8.7" @@ -447,6 +479,18 @@ dependencies = [ "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]] name = "fastrand" version = "2.3.0" @@ -511,6 +555,21 @@ dependencies = [ "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]] name = "futures-channel" version = "0.3.31" @@ -518,6 +577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -526,12 +586,34 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "futures-sink" version = "0.3.31" @@ -550,8 +632,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -574,8 +661,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -585,9 +674,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -607,7 +698,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap", "slab", "tokio", @@ -615,6 +706,34 @@ dependencies = [ "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]] name = "hashbrown" version = "0.16.1" @@ -648,12 +767,12 @@ dependencies = [ "futures-io", "futures-util", "h2", - "http", + "http 0.2.12", "idna", "ipnet", "once_cell", "rand 0.8.5", - "rustls", + "rustls 0.21.12", "rustls-native-certs 0.6.3", "rustls-pemfile 1.0.4", "thiserror 1.0.69", @@ -679,7 +798,7 @@ dependencies = [ "parking_lot", "rand 0.8.5", "resolv-conf", - "rustls", + "rustls 0.21.12", "rustls-native-certs 0.6.3", "smallvec", "thiserror 1.0.69", @@ -708,6 +827,16 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -715,7 +844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", "pin-project-lite", ] @@ -742,7 +871,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "httparse", "httpdate", @@ -762,9 +891,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.12", "hyper", - "rustls", + "rustls 0.21.12", "tokio", "tokio-rustls", ] @@ -952,6 +1081,28 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "jobserver" version = "0.1.34" @@ -984,6 +1135,12 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1026,6 +1183,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -1108,10 +1271,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -1240,6 +1403,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" + [[package]] name = "openssl-sys" version = "0.9.111" @@ -1482,6 +1651,64 @@ dependencies = [ "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]] name = "quote" version = "1.0.43" @@ -1606,7 +1833,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "hyper", "hyper-rustls", @@ -1619,7 +1846,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -1635,7 +1862,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.25.4", "winreg", ] @@ -1659,6 +1886,12 @@ dependencies = [ "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]] name = "rusticata-macros" version = "4.1.0" @@ -1689,20 +1922,34 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "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]] name = "rustls-native-certs" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ - "openssl-probe", + "openssl-probe 0.1.6", "rustls-pemfile 1.0.4", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] @@ -1711,11 +1958,23 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ - "openssl-probe", + "openssl-probe 0.1.6", "rustls-pemfile 2.2.0", "rustls-pki-types", "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]] @@ -1742,9 +2001,37 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ + "web-time", "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]] name = "rustls-webpki" version = "0.101.7" @@ -1755,6 +2042,17 @@ dependencies = [ "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]] name = "rustversion" version = "1.0.22" @@ -1767,6 +2065,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "schannel" version = "0.1.28" @@ -1799,7 +2106,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "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", "libc", "security-framework-sys", @@ -1913,6 +2233,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.11" @@ -2033,7 +2359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2207,7 +2533,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.12", "tokio", ] @@ -2248,6 +2574,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2394,6 +2721,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "want" version = "0.3.1" @@ -2487,12 +2824,40 @@ dependencies = [ "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]] name = "webpki-roots" version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "widestring" version = "1.2.1" @@ -2515,6 +2880,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2527,6 +2901,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows-sys" version = "0.48.0" @@ -2563,6 +2946,21 @@ dependencies = [ "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]] name = "windows-targets" version = "0.48.5" @@ -2611,6 +3009,12 @@ dependencies = [ "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]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2629,6 +3033,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2647,6 +3057,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2677,6 +3093,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2695,6 +3117,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2713,6 +3141,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2731,6 +3165,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2787,6 +3227,7 @@ dependencies = [ "clap", "serde", "serde_json", + "time", "tokio", "wtfnet-calc", "wtfnet-core", @@ -2844,7 +3285,7 @@ dependencies = [ "hickory-resolver", "pnet", "reqwest", - "rustls", + "rustls 0.21.12", "rustls-native-certs 0.6.3", "serde", "thiserror 2.0.17", @@ -2868,12 +3309,22 @@ dependencies = [ name = "wtfnet-http" version = "0.1.0" dependencies = [ + "bytes", + "h3", + "h3-quinn", + "http 1.4.0", + "quinn", "reqwest", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", "serde", "thiserror 2.0.17", "tokio", + "tokio-rustls", + "tokio-socks", "tracing", "url", + "webpki-roots 1.0.5", ] [[package]] @@ -2939,7 +3390,7 @@ dependencies = [ name = "wtfnet-tls" version = "0.1.0" dependencies = [ - "rustls", + "rustls 0.21.12", "rustls-native-certs 0.6.3", "serde", "thiserror 2.0.17", diff --git a/README.md b/README.md index 8737769..3666a54 100644 --- a/README.md +++ b/README.md @@ -81,14 +81,17 @@ Command flags (implemented): - `sys route`: `--ipv4`, `--ipv6`, `--to ` - `ports listen`: `--tcp`, `--udp`, `--port ` - `neigh list`: `--ipv4`, `--ipv6`, `--iface ` +- `ports conns`: `--top `, `--by-process` +- `cert baseline`: `` +- `cert diff`: `` - `probe ping`: `--count `, `--timeout-ms `, `--interval-ms `, `--no-geoip` - `probe tcping`: `--count `, `--timeout-ms `, `--socks5 `, `--prefer-ipv4`, `--no-geoip` - `probe trace`: `--max-hops `, `--per-hop `, `--timeout-ms `, `--udp`, `--port `, `--rdns`, `--no-geoip` - `dns query`: `--server `, `--transport `, `--tls-name `, `--socks5 `, `--prefer-ipv4`, `--timeout-ms ` - `dns detect`: `--servers `, `--transport `, `--tls-name `, `--socks5 `, `--prefer-ipv4`, `--repeat `, `--timeout-ms ` - `dns watch`: `--duration `, `--iface `, `--filter ` -- `http head|get`: `--timeout-ms `, `--follow-redirects `, `--show-headers`, `--show-body`, `--max-body-bytes `, `--http1-only`, `--http2-only`, `--geoip`, `--socks5 ` -- `tls handshake|cert|verify|alpn`: `--sni `, `--alpn `, `--timeout-ms `, `--insecure`, `--socks5 `, `--prefer-ipv4` +- `http head|get`: `--timeout-ms `, `--follow-redirects `, `--show-headers`, `--show-body`, `--max-body-bytes `, `--http1-only`, `--http2-only`, `--http3` (feature `http3`), `--http3-only` (feature `http3`), `--geoip`, `--socks5 ` +- `tls handshake|cert|verify|alpn`: `--sni `, `--alpn `, `--timeout-ms `, `--insecure`, `--socks5 `, `--prefer-ipv4`, `--show-extensions`, `--ocsp` - `discover mdns`: `--duration `, `--service ` - `discover ssdp`: `--duration ` - `diag`: `--out `, `--bundle `, `--dns-detect `, `--dns-timeout-ms `, `--dns-repeat ` @@ -112,6 +115,14 @@ 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 ### v0.1 (MVP) - sys: ifaces/ip/route/dns @@ -135,8 +146,8 @@ cmake --build build --target install - TLS extras: OCSP stapling indicator, richer cert parsing - ports conns improvements (top talkers / summary) - better baseline/diff for system roots -- optional HTTP/3 (feature-gated) - optional LLMNR/NBNS discovery +- optional HTTP/3 (feature-gated; experimental, incomplete) ## Current stage Implemented: diff --git a/crates/wtfnet-cli/Cargo.toml b/crates/wtfnet-cli/Cargo.toml index 6cff6e7..243b91f 100644 --- a/crates/wtfnet-cli/Cargo.toml +++ b/crates/wtfnet-cli/Cargo.toml @@ -11,6 +11,7 @@ path = "src/main.rs" clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +time = { version = "0.3", features = ["formatting", "parsing"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } wtfnet-core = { path = "../wtfnet-core" } wtfnet-calc = { path = "../wtfnet-calc" } diff --git a/crates/wtfnet-cli/src/main.rs b/crates/wtfnet-cli/src/main.rs index 39b3462..4d97c6e 100644 --- a/crates/wtfnet-cli/src/main.rs +++ b/crates/wtfnet-cli/src/main.rs @@ -1,5 +1,5 @@ use clap::{Parser, Subcommand}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::net::ToSocketAddrs; use std::path::PathBuf; use wtfnet_core::{ @@ -97,6 +97,7 @@ enum SysCommand { enum PortsCommand { Listen(PortsListenArgs), Who(PortsWhoArgs), + Conns(PortsConnsArgs), } #[derive(Subcommand, Debug)] @@ -107,6 +108,8 @@ enum NeighCommand { #[derive(Subcommand, Debug)] enum CertCommand { Roots, + Baseline(CertBaselineArgs), + Diff(CertDiffArgs), } #[derive(Subcommand, Debug)] @@ -190,6 +193,14 @@ struct PortsWhoArgs { target: String, } +#[derive(Parser, Debug, Clone)] +struct PortsConnsArgs { + #[arg(long)] + top: Option, + #[arg(long)] + by_process: bool, +} + #[derive(Parser, Debug, Clone)] struct NeighListArgs { #[arg(long)] @@ -205,6 +216,16 @@ struct GeoIpLookupArgs { target: String, } +#[derive(Parser, Debug, Clone)] +struct CertBaselineArgs { + path: PathBuf, +} + +#[derive(Parser, Debug, Clone)] +struct CertDiffArgs { + path: PathBuf, +} + #[derive(Parser, Debug, Clone)] struct ProbePingArgs { target: String, @@ -339,6 +360,10 @@ struct HttpRequestArgs { #[arg(long)] http2_only: bool, #[arg(long)] + http3: bool, + #[arg(long)] + http3_only: bool, + #[arg(long)] geoip: bool, #[arg(long)] socks5: Option, @@ -359,6 +384,10 @@ struct TlsArgs { socks5: Option, #[arg(long)] prefer_ipv4: bool, + #[arg(long)] + show_extensions: bool, + #[arg(long)] + ocsp: bool, } #[derive(Parser, Debug, Clone)] @@ -450,6 +479,7 @@ struct HttpReportGeoIp { pub geoip: Vec, pub headers: Vec<(String, String)>, pub body: Option, + pub warnings: Vec, pub timing: wtfnet_http::HttpTiming, } @@ -481,12 +511,21 @@ async fn main() { Commands::Ports { command: PortsCommand::Who(args), } => handle_ports_who(&cli, args.clone()).await, + Commands::Ports { + command: PortsCommand::Conns(args), + } => handle_ports_conns(&cli, args.clone()).await, Commands::Neigh { command: NeighCommand::List(args), } => handle_neigh_list(&cli, args.clone()).await, Commands::Cert { command: CertCommand::Roots, } => handle_cert_roots(&cli).await, + Commands::Cert { + command: CertCommand::Baseline(args), + } => handle_cert_baseline(&cli, args.clone()).await, + Commands::Cert { + command: CertCommand::Diff(args), + } => handle_cert_diff(&cli, args.clone()).await, Commands::Geoip { command: GeoIpCommand::Lookup(args), } => handle_geoip_lookup(&cli, args.clone()).await, @@ -820,6 +859,99 @@ async fn handle_ports_who(cli: &Cli, args: PortsWhoArgs) -> i32 { } } +async fn handle_ports_conns(cli: &Cli, args: PortsConnsArgs) -> i32 { + let result = platform().ports.connections().await; + match result { + Ok(conns) => { + if cli.json { + let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false); + let mut command_args = Vec::new(); + if let Some(top) = args.top { + command_args.push("--top".to_string()); + command_args.push(top.to_string()); + } + if args.by_process { + command_args.push("--by-process".to_string()); + } + let command = CommandInfo::new("ports conns", command_args); + let envelope = CommandEnvelope::new(meta, command, conns); + emit_json(cli, &envelope) + } else if args.by_process { + let summary = summarize_by_process(&conns); + for (name, count) in summary { + println!("{name} {count}"); + } + ExitKind::Ok.code() + } else if let Some(top) = args.top { + let summary = summarize_top_remote(&conns, top); + for (addr, count) in summary { + println!("{addr} {count}"); + } + ExitKind::Ok.code() + } else { + for conn in conns { + let state = conn.state.unwrap_or_else(|| "-".to_string()); + let pid = conn + .pid + .map(|value| value.to_string()) + .unwrap_or_else(|| "-".to_string()); + let proc = conn + .process_name + .unwrap_or_else(|| "-".to_string()); + println!( + "{} {} -> {} {} pid={} proc={}", + conn.proto, conn.local_addr, conn.remote_addr, state, pid, proc + ); + } + ExitKind::Ok.code() + } + } + Err(err) => emit_platform_error(cli, err), + } +} + +fn summarize_top_remote( + conns: &[wtfnet_platform::ConnSocket], + top: usize, +) -> Vec<(String, usize)> { + let mut counts = std::collections::HashMap::new(); + for conn in conns { + let host = parse_host_from_socket(&conn.remote_addr); + *counts.entry(host).or_insert(0usize) += 1; + } + let mut items = counts.into_iter().collect::>(); + 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::>(); + items.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0))); + items +} + +fn parse_host_from_socket(value: &str) -> String { + if let Some(stripped) = value.strip_prefix('[') { + if let Some(end) = stripped.find(']') { + return stripped[..end].to_string(); + } + } + if let Some((host, _port)) = value.rsplit_once(':') { + return host.to_string(); + } + value.to_string() +} + async fn handle_neigh_list(cli: &Cli, args: NeighListArgs) -> i32 { let result = platform().neigh.neighbors().await; match result { @@ -882,6 +1014,212 @@ async fn handle_cert_roots(cli: &Cli) -> i32 { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CertBaseline { + schema_version: u32, + created_at: String, + roots: Vec, +} + +#[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, + removed: Vec, + changed: Vec, + newly_expired: Vec, + 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::(&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::>(); + let current_map = current + .iter() + .map(|cert| (cert.sha256.clone(), cert)) + .collect::>(); + + let mut added = Vec::new(); + let mut removed = Vec::new(); + let mut changed = Vec::new(); + let mut newly_expired = Vec::new(); + + for cert in ¤t { + if !baseline_map.contains_key(&cert.sha256) { + added.push(cert.clone()); + } + } + for cert in &baseline.roots { + if !current_map.contains_key(&cert.sha256) { + removed.push(cert.clone()); + } + } + + for (sha, base) in &baseline_map { + if let Some(curr) = current_map.get(sha) { + if base.subject != curr.subject { + changed.push(CertChange { + sha256: sha.clone(), + field: "subject".to_string(), + baseline: base.subject.clone(), + current: curr.subject.clone(), + }); + } + if base.issuer != curr.issuer { + changed.push(CertChange { + sha256: sha.clone(), + field: "issuer".to_string(), + baseline: base.issuer.clone(), + current: curr.issuer.clone(), + }); + } + if base.not_after != curr.not_after { + changed.push(CertChange { + sha256: sha.clone(), + field: "not_after".to_string(), + baseline: base.not_after.clone(), + current: curr.not_after.clone(), + }); + } + if base.not_before != curr.not_before { + changed.push(CertChange { + sha256: sha.clone(), + field: "not_before".to_string(), + baseline: base.not_before.clone(), + current: curr.not_before.clone(), + }); + } + + if let (Some(created), Some(expiry)) = ( + parse_cert_time(&baseline.created_at), + parse_cert_time(&curr.not_after), + ) { + let now = time::OffsetDateTime::now_utc(); + if created < expiry && now >= expiry { + newly_expired.push((*curr).clone()); + } + } + } + } + + let report = CertDiffReport { + baseline_path: args.path.to_string_lossy().to_string(), + baseline_count: baseline.roots.len(), + current_count: current.len(), + added, + removed, + changed, + newly_expired, + schema_version: baseline.schema_version, + }; + + if cli.json { + let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false); + let command = CommandInfo::new("cert diff", vec![report.baseline_path.clone()]); + let envelope = CommandEnvelope::new(meta, command, report); + emit_json(cli, &envelope) + } else { + println!( + "baseline_count={} current_count={} added={} removed={} changed={} newly_expired={}", + report.baseline_count, + report.current_count, + report.added.len(), + report.removed.len(), + report.changed.len(), + report.newly_expired.len() + ); + for cert in report.added { + println!("added {} {}", cert.sha256, cert.subject); + } + for cert in report.removed { + println!("removed {} {}", cert.sha256, cert.subject); + } + for change in report.changed { + println!( + "changed {} {} {} -> {}", + change.sha256, change.field, change.baseline, change.current + ); + } + for cert in report.newly_expired { + println!("expired {} {}", cert.sha256, cert.subject); + } + ExitKind::Ok.code() + } +} + +fn parse_cert_time(value: &str) -> Option { + if let Ok(dt) = time::OffsetDateTime::parse(value, &time::format_description::well_known::Rfc3339) { + return Some(dt); + } + let format = time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second] UTC").ok()?; + time::OffsetDateTime::parse(value, &format).ok() +} + +fn now_rfc3339() -> String { + time::OffsetDateTime::now_utc() + .format(&time::format_description::well_known::Rfc3339) + .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()) +} + async fn handle_geoip_lookup(cli: &Cli, args: GeoIpLookupArgs) -> i32 { let ip = match args.target.parse::() { Ok(ip) => ip, @@ -1821,6 +2159,8 @@ async fn handle_http_request( show_body: args.show_body, http1_only: args.http1_only, http2_only: args.http2_only, + http3: args.http3, + http3_only: args.http3_only, proxy: args.socks5.clone(), }; @@ -1844,6 +2184,7 @@ async fn handle_http_request( geoip, headers: report.headers.clone(), body: report.body.clone(), + warnings: report.warnings.clone(), timing: report.timing.clone(), } } else { @@ -1857,6 +2198,7 @@ async fn handle_http_request( geoip: Vec::new(), headers: report.headers.clone(), body: report.body.clone(), + warnings: report.warnings.clone(), timing: report.timing.clone(), } }; @@ -1883,6 +2225,11 @@ async fn handle_http_request( if !report.resolved_ips.is_empty() { println!("resolved: {}", report.resolved_ips.join(", ")); } + if !report.warnings.is_empty() { + for warning in &report.warnings { + println!("warning: {warning}"); + } + } println!("total_ms: {}", report.timing.total_ms); if let Some(ms) = report.timing.dns_ms { println!("dns_ms: {ms}"); @@ -1973,6 +2320,8 @@ fn build_tls_options(args: &TlsArgs) -> wtfnet_tls::TlsOptions { insecure: args.insecure, socks5: args.socks5.clone(), prefer_ipv4: args.prefer_ipv4, + show_extensions: args.show_extensions, + ocsp: args.ocsp, } } diff --git a/crates/wtfnet-http/Cargo.toml b/crates/wtfnet-http/Cargo.toml index fc6dfbc..d9f852c 100644 --- a/crates/wtfnet-http/Cargo.toml +++ b/crates/wtfnet-http/Cargo.toml @@ -5,8 +5,21 @@ edition = "2024" [dependencies] reqwest = { version = "0.11", features = ["rustls-tls"] } +rustls = "0.21" +rustls-native-certs = "0.6" serde = { version = "1", features = ["derive"] } thiserror = "2" tokio = { version = "1", features = ["net", "time"] } +tokio-rustls = "0.24" +tokio-socks = "0.5" url = "2" 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"] diff --git a/crates/wtfnet-http/src/lib.rs b/crates/wtfnet-http/src/lib.rs index c572340..aaaac30 100644 --- a/crates/wtfnet-http/src/lib.rs +++ b/crates/wtfnet-http/src/lib.rs @@ -1,12 +1,28 @@ use reqwest::{Client, Method, Proxy, StatusCode}; +use rustls::{Certificate, ClientConfig, RootCertStore, ServerName}; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, SocketAddr}; +use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::net::lookup_host; use thiserror::Error; +use tokio::time::timeout; +use tokio_rustls::TlsConnector; +use tokio_socks::tcp::Socks5Stream; use tracing::debug; 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)] pub enum HttpError { #[error("invalid url: {0}")] @@ -36,6 +52,7 @@ pub struct HttpReport { pub resolved_ips: Vec, pub headers: Vec<(String, String)>, pub body: Option, + pub warnings: Vec, pub timing: HttpTiming, } @@ -64,6 +81,8 @@ pub struct HttpRequestOptions { pub show_body: bool, pub http1_only: bool, pub http2_only: bool, + pub http3: bool, + pub http3_only: bool, pub proxy: Option, } @@ -105,6 +124,39 @@ pub async fn request(url: &str, opts: HttpRequestOptions) -> Result { + 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)); builder = if let Some(max) = opts.follow_redirects { builder.redirect(reqwest::redirect::Policy::limited(max as usize)) @@ -132,6 +184,16 @@ pub async fn request(url: &str, opts: HttpRequestOptions) -> Result Result Result Option { Some(status.as_u16()) } + +struct Socks5Proxy { + addr: String, + remote_dns: bool, +} + +fn parse_socks5_proxy(value: &str) -> Result { + 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, Option, Vec) { + 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::().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 { + 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), 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::().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::>() + } 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 { + 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) +} diff --git a/crates/wtfnet-platform-linux/src/lib.rs b/crates/wtfnet-platform-linux/src/lib.rs index f17eb4f..4c539a2 100644 --- a/crates/wtfnet-platform-linux/src/lib.rs +++ b/crates/wtfnet-platform-linux/src/lib.rs @@ -5,8 +5,8 @@ use std::collections::HashMap; use std::sync::Arc; use wtfnet_core::ErrorCode; use wtfnet_platform::{ - CertProvider, DnsConfigSnapshot, ListenSocket, NeighborEntry, NeighProvider, NetInterface, - Platform, PlatformError, PortsProvider, RootCert, RouteEntry, SysProvider, + CertProvider, ConnSocket, DnsConfigSnapshot, ListenSocket, NeighborEntry, NeighProvider, + NetInterface, Platform, PlatformError, PortsProvider, RootCert, RouteEntry, SysProvider, }; use x509_parser::oid_registry::{ 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) } +fn parse_linux_tcp_conns( + path: &str, + is_v6: bool, + inode_map: &HashMap, +) -> Result, 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( path: &str, is_v6: bool, @@ -286,6 +343,24 @@ fn parse_linux_udp_with_inode_map( 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 { let mut parts = value.split(':'); let addr_hex = parts.next()?; @@ -518,6 +593,22 @@ impl PortsProvider for LinuxPortsProvider { .filter(|socket| extract_port(&socket.local_addr) == Some(port)) .collect()) } + + async fn connections(&self) -> Result, 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] diff --git a/crates/wtfnet-platform-windows/src/lib.rs b/crates/wtfnet-platform-windows/src/lib.rs index e0d8b9c..ef702cb 100644 --- a/crates/wtfnet-platform-windows/src/lib.rs +++ b/crates/wtfnet-platform-windows/src/lib.rs @@ -10,8 +10,8 @@ use x509_parser::oid_registry::{ use std::sync::Arc; use wtfnet_core::ErrorCode; use wtfnet_platform::{ - CertProvider, DnsConfigSnapshot, ListenSocket, NeighborEntry, NeighProvider, NetInterface, - Platform, PlatformError, PortsProvider, RootCert, RouteEntry, SysProvider, + CertProvider, ConnSocket, DnsConfigSnapshot, ListenSocket, NeighborEntry, NeighProvider, + NetInterface, Platform, PlatformError, PortsProvider, RootCert, RouteEntry, SysProvider, }; pub fn platform() -> Platform { @@ -333,6 +333,33 @@ fn parse_windows_listeners() -> Result, PlatformError> { Ok(sockets) } +fn parse_windows_connections() -> Result, 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 { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 5 { @@ -358,6 +385,32 @@ fn parse_netstat_tcp_line(line: &str) -> Option { }) } +fn parse_netstat_tcp_conn_line(line: &str) -> Option { + 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::().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 { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 4 { @@ -429,6 +482,17 @@ fn enrich_socket(socket: &mut ListenSocket, map: &HashMap) { } } +fn enrich_conn_socket(socket: &mut ConnSocket, map: &HashMap) { + 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)] struct ProcInfo { name: Option, @@ -605,6 +669,10 @@ impl PortsProvider for WindowsPortsProvider { .filter(|socket| extract_port(&socket.local_addr) == Some(port)) .collect()) } + + async fn connections(&self) -> Result, PlatformError> { + parse_windows_connections() + } } #[async_trait] diff --git a/crates/wtfnet-platform/src/lib.rs b/crates/wtfnet-platform/src/lib.rs index 0c5241e..8b2a698 100644 --- a/crates/wtfnet-platform/src/lib.rs +++ b/crates/wtfnet-platform/src/lib.rs @@ -46,6 +46,18 @@ pub struct ListenSocket { pub owner: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConnSocket { + pub proto: String, + pub local_addr: String, + pub remote_addr: String, + pub state: Option, + pub pid: Option, + pub ppid: Option, + pub process_name: Option, + pub process_path: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RootCert { pub subject: String, @@ -98,6 +110,7 @@ pub trait SysProvider: Send + Sync { pub trait PortsProvider: Send + Sync { async fn listening(&self) -> Result, PlatformError>; async fn who_owns(&self, port: u16) -> Result, PlatformError>; + async fn connections(&self) -> Result, PlatformError>; } #[async_trait] diff --git a/crates/wtfnet-tls/src/lib.rs b/crates/wtfnet-tls/src/lib.rs index b528e82..067080d 100644 --- a/crates/wtfnet-tls/src/lib.rs +++ b/crates/wtfnet-tls/src/lib.rs @@ -35,6 +35,9 @@ pub struct TlsCertSummary { pub not_before: String, pub not_after: String, pub san: Vec, + pub signature_algorithm: Option, + pub key_usage: Option>, + pub extended_key_usage: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -45,6 +48,7 @@ pub struct TlsHandshakeReport { pub alpn_negotiated: Option, pub tls_version: Option, pub cipher: Option, + pub ocsp_stapled: Option, pub cert_chain: Vec, } @@ -56,6 +60,7 @@ pub struct TlsVerifyReport { pub alpn_negotiated: Option, pub tls_version: Option, pub cipher: Option, + pub ocsp_stapled: Option, pub verified: bool, pub error: Option, } @@ -64,6 +69,7 @@ pub struct TlsVerifyReport { pub struct TlsCertReport { pub target: String, pub sni: Option, + pub ocsp_stapled: Option, pub cert_chain: Vec, } @@ -83,6 +89,8 @@ pub struct TlsOptions { pub insecure: bool, pub socks5: Option, pub prefer_ipv4: bool, + pub show_extensions: bool, + pub ocsp: bool, } pub async fn handshake(target: &str, options: TlsOptions) -> Result { @@ -120,7 +128,8 @@ pub async fn handshake(target: &str, options: TlsOptions) -> Result Result Result Result (String, bool) { (host.to_string(), remote_dns) } -fn extract_cert_chain(certs: Option<&[Certificate]>) -> Result, TlsError> { +fn extract_cert_chain( + certs: Option<&[Certificate]>, + show_extensions: bool, +) -> Result, TlsError> { let mut results = Vec::new(); if let Some(certs) = certs { for cert in certs { - let summary = parse_cert(&cert.0)?; + let summary = parse_cert(&cert.0, show_extensions)?; results.push(summary); } } Ok(results) } -fn parse_cert(der: &[u8]) -> Result { +fn parse_cert(der: &[u8], show_extensions: bool) -> Result { let (_, cert) = 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 { subject: cert.subject().to_string(), issuer: cert.issuer().to_string(), not_before: cert.validity().not_before.to_string(), not_after: cert.validity().not_after.to_string(), san: extract_san(&cert), + signature_algorithm, + key_usage, + extended_key_usage, }) } @@ -460,6 +487,85 @@ fn extract_san(cert: &X509Certificate<'_>) -> Vec { result } +fn extract_key_usage(cert: &X509Certificate<'_>) -> Option> { + 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> { + 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 { + if enabled { + None + } else { + None + } +} + struct NoVerifier; impl rustls::client::ServerCertVerifier for NoVerifier { diff --git a/docs/RELEASE_v0.3.0.md b/docs/RELEASE_v0.3.0.md index 476b68f..5f63969 100644 --- a/docs/RELEASE_v0.3.0.md +++ b/docs/RELEASE_v0.3.0.md @@ -10,7 +10,7 @@ v0.3.0 focuses on improving diagnostic depth and fidelity of existing commands r Major upgrades in this release: - richer traceroute output and per-hop statistics - HTTP timing breakdown accuracy (connect/tls stages) -- optional HTTP/3 support (best-effort) +- optional HTTP/3 support (feature-gated; experimental) - TLS diagnostics upgrades (OCSP stapling indicator, richer certificate parsing) - ports connections view and summaries - improved cert baseline/diff for system roots @@ -67,7 +67,7 @@ Acceptance: - on timeout / failure, partial timing must still be meaningful. ### 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: - add `--http3` support behind Cargo feature `http3` - behavior: @@ -79,6 +79,7 @@ Target: Acceptance: - builds without `http3` feature still work - 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) Current: `tls handshake/verify/cert/alpn` exists. diff --git a/docs/WORK_ITEMS_v0.3.0.md b/docs/WORK_ITEMS_v0.3.0.md index 386ba9c..18fe056 100644 --- a/docs/WORK_ITEMS_v0.3.0.md +++ b/docs/WORK_ITEMS_v0.3.0.md @@ -3,42 +3,43 @@ This is a practical checklist to execute v0.3.0. ## 1) probe/trace upgrades -- [ ] add `--per-hop ` and store RTT samples per hop -- [ ] compute loss% per hop -- [ ] add `--rdns` best-effort reverse lookup (cached + time-bounded) -- [ ] improve hop formatting + JSON schema +- [x] add `--per-hop ` and store RTT samples per hop +- [x] compute loss% per hop +- [x] add `--rdns` best-effort reverse lookup (cached + time-bounded) +- [x] improve hop formatting + JSON schema ## 2) http timing improvements -- [ ] implement `connect_ms` and `tls_ms` timing -- [ ] report `null` + warning when measurement unavailable +- [x] implement `connect_ms` and `tls_ms` timing +- [x] report `null` + warning when measurement unavailable - [ ] keep current `dns_ms` and `ttfb_ms` -## 3) optional HTTP/3 -- [ ] add `http3` cargo feature + deps -- [ ] implement `--http3` / `--http3-only` -- [ ] define error classification for QUIC failures +## 3) tls extras +- [x] add OCSP stapling presence indicator (if available) +- [x] parse SANs and key usage / EKU best-effort +- [x] add `--show-extensions` and `--ocsp` flags -## 4) tls extras -- [ ] add OCSP stapling presence indicator (if available) -- [ ] parse SANs and key usage / EKU best-effort -- [ ] add `--show-extensions` and `--ocsp` flags +## 4) ports conns +- [x] implement `wtfn ports conns` +- [x] add `--top ` and `--by-process` +- [x] best-effort PID mapping with warnings -## 5) ports conns -- [ ] implement `wtfn ports conns` -- [ ] add `--top ` and `--by-process` -- [ ] best-effort PID mapping with warnings +## 5) cert baseline/diff improvements +- [x] baseline schema version +- [x] match by SHA256 fingerprint +- [x] diff categories: add/remove/expired/changed -## 6) cert baseline/diff improvements -- [ ] baseline schema version -- [ ] match by SHA256 fingerprint -- [ ] diff categories: add/remove/expired/changed - -## 7) optional LLMNR/NBNS +## 6) optional LLMNR/NBNS - [ ] implement `discover llmnr` - [ ] implement `discover nbns` - [ ] bounded collection, low-noise -## 8) docs updates -- [ ] update README roadmap +## 7) docs updates +- [x] update README roadmap - [ ] update COMMANDS.md with new flags/commands - [ ] 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 diff --git a/docs/implementation_status.md b/docs/implementation_status.md index 5320f24..0404064 100644 --- a/docs/implementation_status.md +++ b/docs/implementation_status.md @@ -9,7 +9,7 @@ This document tracks current implementation status against the original design i - GeoIP: local GeoLite2 Country + ASN support. - Probe: ping/tcping/trace with GeoIP enrichment. - 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. - Discover: mDNS/SSDP bounded collection. - Diag: bundle export in zip. @@ -17,11 +17,11 @@ This document tracks current implementation status against the original design i ## Deviations or refinements - 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`/`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. ## 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). - Discover does not include LLMNR/NBNS. diff --git a/docs/status.md b/docs/status.md index d682e06..5d944e7 100644 --- a/docs/status.md +++ b/docs/status.md @@ -26,8 +26,8 @@ This document tracks the planned roadmap alongside the current implementation st - TLS extras: OCSP stapling indicator, richer cert parsing - ports conns improvements (top talkers / summary) - better baseline/diff for system roots -- optional HTTP/3 (feature-gated) - optional LLMNR/NBNS discovery +- optional HTTP/3 (feature-gated; experimental, incomplete) ## 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. - TLS crate with handshake/verify/cert/alpn support in CLI (SOCKS5 proxy supported). - 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. - Diag crate with report and bundle export. - Basic unit tests for calc and TLS parsing. ### In progress -- v0.3: probe trace upgrades (per-hop stats + rdns). +- v0.3: optional HTTP/3 (feature-gated; keep disabled until stabilized). ### Next - Complete v0.3 trace upgrades and update CLI output.