diff --git a/Cargo.lock b/Cargo.lock index c0f2a4c..23c95a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,7 +158,7 @@ dependencies = [ "ark-serialize", "ark-std", "arrayvec", - "digest", + "digest 0.10.7", "educe", "itertools 0.13.0", "num-bigint 0.4.6", @@ -214,7 +214,7 @@ dependencies = [ "ark-serialize-derive", "ark-std", "arrayvec", - "digest", + "digest 0.10.7", "num-bigint 0.4.6", ] @@ -236,7 +236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -299,6 +299,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -307,15 +318,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "zeroize", @@ -323,9 +334,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", @@ -383,15 +394,15 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn 2.0.117", ] [[package]] name = "bitflags" -version = "2.11.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "bitvec" @@ -414,6 +425,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bls12_381" version = "0.8.0" @@ -428,56 +448,65 @@ dependencies = [ ] [[package]] -name = "boring-sys2" -version = "5.0.0-alpha.13" +name = "brotli" +version = "8.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455d79965f5155dcc88a7abce112c3590883889131b799beda10bf9a813ed669" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" dependencies = [ - "bindgen", - "cmake", - "fs_extra", - "fslock", + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", ] [[package]] -name = "boring2" -version = "5.0.0-alpha.13" +name = "brotli-decompressor" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183ccc3854411c035410dcdbffafca62084f3a6c33f013c77e83c025d2a08a28" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" dependencies = [ - "bitflags", - "boring-sys2", - "foreign-types", - "libc", - "openssl-macros", + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] -name = "brotli" -version = "8.0.2" +name = "bs58" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", + "tinyvec", ] [[package]] -name = "brotli-decompressor" -version = "5.0.0" +name = "btls" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "2c5e60b8c8d282c86360cab651ded04ab0335a7b5390c8d34145cbeab8cacf5f" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", + "bitflags", + "btls-sys", + "foreign-types", + "libc", + "openssl-macros", +] + +[[package]] +name = "btls-sys" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b1b8638a2e1c38a5ae4efa90ae57e643baec35a30d03fc5b399b893adc4954b" +dependencies = [ + "bindgen", + "cmake", + "fs_extra", + "fslock", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -525,14 +554,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -589,9 +618,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -611,9 +640,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -629,9 +658,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -648,7 +677,7 @@ dependencies = [ "group", "hex", "lazy_static", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_bytes", "serde_with", @@ -705,6 +734,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "core-foundation" version = "0.9.4" @@ -740,6 +775,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -749,6 +793,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -763,14 +837,23 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -778,9 +861,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "group", "rand_core 0.6.4", @@ -870,13 +953,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -897,17 +986,28 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.2", +] + [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -933,7 +1033,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -954,9 +1054,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" @@ -966,7 +1066,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -987,6 +1087,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -1020,7 +1132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1079,6 +1191,18 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.5.0" @@ -1193,9 +1317,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -1233,11 +1357,24 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "glob" version = "0.3.3" @@ -1268,9 +1405,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -1278,7 +1415,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -1291,12 +1428,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" - [[package]] name = "hashbrown" version = "0.15.5" @@ -1304,13 +1435,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", + "foldhash 0.1.5", ] [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -1327,20 +1464,66 @@ dependencies = [ "serde", ] +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.4", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.4", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1371,9 +1554,9 @@ dependencies = [ [[package]] name = "http2" -version = "0.5.13" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ae4d57b46c9fdbf38b738dc8f62da3d7f5de3c77934f734b65bdc8b1dc9ce7" +checksum = "569ef7a780e853c4e1768f58a3c8168193b82cdcbab66638a0b1c6583ec5995e" dependencies = [ "atomic-waker", "bytes", @@ -1381,8 +1564,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.0", - "parking_lot", + "indexmap 2.14.0", "slab", "smallvec", "tokio", @@ -1395,11 +1577,20 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1411,7 +1602,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1419,15 +1609,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -1484,12 +1673,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1497,9 +1687,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1510,9 +1700,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1524,15 +1714,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1544,15 +1734,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1563,6 +1753,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1582,9 +1778,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1603,31 +1799,34 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] -name = "ipnet" -version = "2.12.0" +name = "ipconfig" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] [[package]] -name = "iri-string" -version = "0.7.11" +name = "ipnet" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -1675,6 +1874,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -1715,10 +1944,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1733,7 +1964,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2", + "sha2 0.10.9", "signature", ] @@ -1743,7 +1974,17 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", ] [[package]] @@ -1752,11 +1993,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -1776,9 +2023,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1791,9 +2038,18 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] [[package]] name = "lru-slab" @@ -1803,9 +2059,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mime" @@ -1841,15 +2097,32 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "nom" version = "7.1.3" @@ -1883,9 +2156,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -1911,6 +2184,10 @@ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -1921,7 +2198,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "ooniauth-core" version = "0.1.0" -source = "git+https://github.com/ooni/userauth#71af1c74633f4ccc3c9d88ca94ffab6a3b803451" +source = "git+https://github.com/ooni/userauth#187594915bf2450b244a3bb1e7907b34453e01b6" dependencies = [ "bincode", "chrono", @@ -1930,11 +2207,11 @@ dependencies = [ "ff", "group", "hex", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_bytes", "serde_with", - "sha2", + "sha2 0.10.9", "subtle", "syn 2.0.117", "thiserror 2.0.18", @@ -1942,6 +2219,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "ooniprobe-experiments" +version = "0.1.0" +dependencies = [ + "bytes", + "chrono", + "http", + "http-body-util", + "ooniprobe-network", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", +] + [[package]] name = "ooniprobe-ffi" version = "0.1.0" @@ -1956,11 +2249,11 @@ dependencies = [ "mime", "ooniauth-core", "ooniprobe-services", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", "time", "tokio", @@ -1968,6 +2261,31 @@ dependencies = [ "wreq", ] +[[package]] +name = "ooniprobe-network" +version = "0.1.0" +dependencies = [ + "base64", + "bytes", + "chrono", + "hickory-proto", + "hickory-resolver", + "http", + "http-body-util", + "hyper", + "hyper-util", + "pin-project", + "rand 0.8.6", + "rustls", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "url", + "webpki-roots", +] + [[package]] name = "ooniprobe-services" version = "0.1.0" @@ -1978,7 +2296,7 @@ dependencies = [ "http", "hyper", "hyper-util", - "jni", + "jni 0.21.1", "mime", "reqwest", "serde", @@ -2016,14 +2334,14 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] name = "p3-challenger" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0b490c745a7d2adeeafff06411814c8078c432740162332b3cd71be0158a76" +checksum = "8972ccd1d5dc90e46cdb1f2ab4ee2bae49b3917e5e98aa533f0c2b779c010445" dependencies = [ "p3-field", "p3-maybe-rayon", @@ -2035,9 +2353,9 @@ dependencies = [ [[package]] name = "p3-dft" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55301e91544440254977108b85c32c09d7ea05f2f0dd61092a2825339906a4a7" +checksum = "17771aca44632f9cc11f2718d7ea7ec06794946c4190ef3a985bfc893f14c18a" dependencies = [ "itertools 0.14.0", "p3-field", @@ -2050,25 +2368,25 @@ dependencies = [ [[package]] name = "p3-field" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85affca7fc983889f260655c4cf74163eebb94605f702e4b6809ead707cba54f" +checksum = "6f3eb24d0591fd4d282d89cbe4e4efba5571c699375006f80b2cbf53ce83461c" dependencies = [ "itertools 0.14.0", "num-bigint 0.4.6", "p3-maybe-rayon", "p3-util", "paste", - "rand 0.10.0", + "rand 0.10.1", "serde", "tracing", ] [[package]] name = "p3-koala-bear" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7369a8eb2a27b314338f6b4b77e6a701ad31f1deff25b75bd302fc08c924c9b3" +checksum = "61e31d7adaecf270811e3f4b52dc12214235a5427b976a49a45f73d542fc0a2d" dependencies = [ "p3-challenger", "p3-field", @@ -2077,48 +2395,48 @@ dependencies = [ "p3-poseidon1", "p3-poseidon2", "p3-symmetric", - "rand 0.10.0", + "rand 0.10.1", ] [[package]] name = "p3-matrix" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53428126b009071563d1d07305a9de8be0d21de00b57d2475289ee32ffca6577" +checksum = "ea9c94c0714944e7b8a9a62e6340b1e3e1d3f8ecfd3e35c08798360200e73eff" dependencies = [ "itertools 0.14.0", "p3-field", "p3-maybe-rayon", "p3-util", - "rand 0.10.0", + "rand 0.10.1", "serde", "tracing", ] [[package]] name = "p3-maybe-rayon" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "082bf467011c06c768c579ec6eb9accb5e1e62108891634cc770396e917f978a" +checksum = "eebc233a34b1ab0273f35b4052fa2eeb3114b22ba4575bd7da00716e878ffb77" [[package]] name = "p3-mds" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35209e6214102ea6ec6b8cb1b9c15a9b8e597a39f9173597c957f123bced81b3" +checksum = "6b5441fa8116246ec9e6c835f15273cb27777ca572960ec87476b67fef13e01e" dependencies = [ "p3-dft", "p3-field", "p3-symmetric", "p3-util", - "rand 0.10.0", + "rand 0.10.1", ] [[package]] name = "p3-monty-31" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffa8c99ec50c035020bbf5457c6a729ba6a975719c1a8dd3f16421081e4f650c" +checksum = "8724f330ea6d19dd4f2436aa0f88b5fcbf88f0f55ca7fccd3fea8b736dbcddad" dependencies = [ "itertools 0.14.0", "num-bigint 0.4.6", @@ -2132,7 +2450,7 @@ dependencies = [ "p3-symmetric", "p3-util", "paste", - "rand 0.10.0", + "rand 0.10.1", "serde", "spin", "tracing", @@ -2140,33 +2458,33 @@ dependencies = [ [[package]] name = "p3-poseidon1" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a018b618e3fa0aec8be933b1d8e404edd23f46991f6bf3f5c2f3f95e9413fe9" +checksum = "04e2a562fea210baae390a32f9ecf0dd8724ae3f4352d1c8e413077b6f00a162" dependencies = [ "p3-field", "p3-symmetric", - "rand 0.10.0", + "rand 0.10.1", ] [[package]] name = "p3-poseidon2" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256a668a9ba916f8767552f13d0ba50d18968bc74a623bfdafa41e2970c944d0" +checksum = "06394851c161d17e4aa4ad2aad5557d32f14cadd1dc838f965d8e1821a63b8c5" dependencies = [ "p3-field", "p3-mds", "p3-symmetric", "p3-util", - "rand 0.10.0", + "rand 0.10.1", ] [[package]] name = "p3-symmetric" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c60a71a1507c13611b0f2b0b6e83669fd5b76f8e3115bcbced5ccfdf3ca7807" +checksum = "9ac1a276d421f8ef3361bb7d8c39a02c93c6b3f10eeaa559cc4c50222f9a5b82" dependencies = [ "itertools 0.14.0", "p3-field", @@ -2176,9 +2494,9 @@ dependencies = [ [[package]] name = "p3-util" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b766b9e9254bf3fa98d76e42cf8a5b30628c182dfd5272d270076ee12f0fc0" +checksum = "d08a58162a4c264269ef454f0b28dcda89939490eecacb2b2cf5b00f719b80f6" dependencies = [ "serde", "transpose", @@ -2238,16 +2556,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "pin-project" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] [[package]] -name = "pin-utils" -version = "0.1.0" +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkcs8" @@ -2261,9 +2593,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -2271,11 +2603,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2353,7 +2691,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -2376,7 +2714,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -2394,6 +2732,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -2402,9 +2746,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2413,9 +2757,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -2423,11 +2767,11 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -2470,9 +2814,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "redox_syscall" @@ -2534,9 +2878,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2572,6 +2916,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfc6979" version = "0.4.0" @@ -2598,9 +2948,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -2613,12 +2963,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2627,9 +2979,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2639,9 +2991,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -2649,13 +3001,13 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -2665,7 +3017,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2676,9 +3028,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -2740,17 +3092,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "schnellru" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" -dependencies = [ - "ahash", - "cfg-if", - "hashbrown 0.13.2", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -2816,9 +3157,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -2864,11 +3205,24 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_html_form" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0946d52b4b7e28823148aebbeceb901012c595ad737920d504fa8634bb099e6f" +dependencies = [ + "form_urlencoded", + "indexmap 2.14.0", + "itoa", + "ryu", + "serde_core", +] + [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2891,15 +3245,16 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -2910,9 +3265,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -2927,18 +3282,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", ] [[package]] name = "sha3" -version = "0.10.8" +version = "0.11.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "0b233a7d59d7bfc027208506a33ffc9532b2acb24ddc61fe7e758dc2250db431" dependencies = [ - "digest", - "keccak", + "digest 0.11.3", + "keccak 0.2.0", ] [[package]] @@ -2947,14 +3323,20 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "sigma-compiler" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ae064a7fd9e041cf24c33fe085a66aa4d0b740aee45ff9541214ea8a3712f7" +checksum = "feb764bbde12cb0d91e64d17efc78849a5226e9c05dd7b6f9cbdb62b8f33141d" dependencies = [ "group", - "rand 0.8.5", + "rand 0.8.6", "sigma-compiler-derive", "sigma-proofs", "subtle", @@ -2962,9 +3344,9 @@ dependencies = [ [[package]] name = "sigma-compiler-core" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c36b1be2c9a0c538bb1066cd0c08b4c5e3e5199685d7105b206466228a570eed" +checksum = "2ebc92e66c5553d145131032c7905267efe4d28cde07823901cb03f2779a42bb" dependencies = [ "clap", "prettyplease", @@ -2975,9 +3357,9 @@ dependencies = [ [[package]] name = "sigma-compiler-derive" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d53c32e54fb037517ab1ead720f7c3a51de58dc0e76155ac901febf11682357" +checksum = "24d6625ea15f83542841bddf3eccbb93b9cd9e5504572e7a0cd72bf63a87e29e" dependencies = [ "sigma-compiler-core", "syn 2.0.117", @@ -2985,27 +3367,27 @@ dependencies = [ [[package]] name = "sigma-proofs" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ad8e588ea87247ad3672164dfe4eae41862693fd3b6c933c258d406e29b5ef" +checksum = "5a8298949b71fec0ac49585c07ccd6c0d0d82eeaba7decae5de810ebb6690016" dependencies = [ "ahash", "bls12_381", "curve25519-dalek", "ff", "group", - "hashbrown 0.15.5", + "hashbrown 0.17.1", + "itertools 0.14.0", "k256", - "keccak", + "keccak 0.2.0", "num-bigint 0.4.6", "num-traits", "p256", - "rand 0.8.5", "rand_core 0.6.4", - "sha3", + "sha3 0.10.9", "spongefish", "subtle", - "thiserror 1.0.69", + "thiserror 2.0.18", "zerocopy", "zeroize", ] @@ -3026,15 +3408,31 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" @@ -3062,9 +3460,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3091,31 +3489,31 @@ dependencies = [ [[package]] name = "spongefish" -version = "0.4.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b272d43b44123f692db90b5e9c2ffaa31eb885194eb7f74be5ab15fda1b8a4ae" +checksum = "074f823019979d89e8d46a966feb3d173f3db9a21c6764f8c2282e137017bba5" dependencies = [ "ark-ec", "ark-ff", "ark-serialize", "bls12_381", "curve25519-dalek", - "digest", + "digest 0.11.3", "k256", "p256", "p3-koala-bear", - "rand 0.8.5", - "sha2", - "sha3", + "rand 0.8.6", + "sha2 0.11.0", + "sha3 0.11.0-rc.9", "spongefish-derive", "zeroize", ] [[package]] name = "spongefish-derive" -version = "0.4.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91b7014144fb488f831acb9ef0de3cc9e1b07ca8c0945aa4fcc85c3394164d8" +checksum = "d60e339e6913c02844921b60ab035e40704f9fb57d96663d734f8ae410756a44" dependencies = [ "proc-macro2", "quote", @@ -3215,6 +3613,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -3303,9 +3707,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3328,9 +3732,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3344,20 +3748,20 @@ dependencies = [ ] [[package]] -name = "tokio-boring2" -version = "5.0.0-alpha.13" +name = "tokio-btls" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f81df1210d791f31d72d840de8fbd80b9c3cb324956523048b1413e2bd55756" +checksum = "2e1fd638ec35427faf3b8f412e0fdd6fae76591d79dba40f38fa667d22bc44dd" dependencies = [ - "boring2", + "btls", "tokio", ] [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -3413,20 +3817,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3510,9 +3914,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicase" @@ -3526,6 +3930,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "uniffi" version = "0.28.3" @@ -3694,6 +4104,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3727,18 +4148,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -3749,23 +4179,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3773,9 +4199,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -3786,18 +4212,52 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -3815,9 +4275,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -3831,6 +4300,12 @@ dependencies = [ "nom", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -3853,7 +4328,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3950,6 +4425,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -3983,13 +4467,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -4002,6 +4503,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +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" @@ -4014,6 +4521,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +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" @@ -4026,12 +4539,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +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" @@ -4044,6 +4569,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +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" @@ -4056,6 +4587,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +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" @@ -4068,6 +4605,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +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" @@ -4080,24 +4623,115 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "wreq" -version = "6.0.0-rc.28" +version = "6.0.0-rc.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79937f6c4df65b3f6f78715b9de2977afe9ee3b3436483c7949a24511e25935" +checksum = "3f0eba5f5814a94e5f1a99156f187133464e525b66bdbc69a9627d46530af2e1" dependencies = [ - "ahash", - "boring2", - "brotli", + "btls", + "btls-sys", "bytes", - "flate2", - "futures-channel", "futures-util", "http", "http-body", @@ -4106,37 +4740,70 @@ dependencies = [ "httparse", "ipnet", "libc", + "lru", "percent-encoding", "pin-project-lite", - "schnellru", "serde", - "serde_urlencoded", - "smallvec", + "serde_html_form", "socket2", "tokio", - "tokio-boring2", + "tokio-btls", "tower", "url", - "want", "webpki-root-certs", - "zstd", + "wreq-proto", + "wreq-rt", +] + +[[package]] +name = "wreq-proto" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a43942f024bb303f1042c9aa3c87fa1d9149f507c65db6e5220a11ccdb207387" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "http2", + "httparse", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", + "want", +] + +[[package]] +name = "wreq-rt" +version = "0.2.2-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e9bce67a3fa3dd3f1503f066d86661c9caf399a763d3bd184da7afaf886c8b" +dependencies = [ + "pin-project-lite", + "tokio", + "wreq-proto", ] [[package]] name = "wreq-util" -version = "3.0.0-rc.10" +version = "3.0.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6bbe24d28beb9ceb58b514bd6a613c759d3b706f768b9d2950d5d35b543c04" +checksum = "baa5d2ab72139256916ca352a3d05c53d74e1dd360052eb5ba7691033c417c65" dependencies = [ + "brotli", + "flate2", "typed-builder", "wreq", + "zstd", ] [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -4149,9 +4816,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4160,9 +4827,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -4172,18 +4839,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", @@ -4192,18 +4859,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -4233,9 +4900,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -4244,9 +4911,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -4255,9 +4922,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9d0898b..b5ca9a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,8 @@ [workspace] members = [ "ooniprobe-services", + "ooniprobe-network", + "ooniprobe-experiments", "ooniprobe-ffi", "uniffi-bindgen" ] @@ -16,15 +18,21 @@ ooniprobe-services = { version = "0.1.0", path = "./ooniprobe-services" } ooniauth-core = { git = "https://github.com/ooni/userauth" } rand = "0.8.5" bincode = "1" +chrono = "0.4.40" thiserror = "2.0.12" serde = { version = "1.0.216", features = ["serde_derive"] } serde_json = "1.0.133" serde_derive = "1.0" +rustls = { version = "0.23.37", features = ["ring"] } tokio = { version = "1.42.0", features = ["full"] } +tokio-rustls = "0.26.4" +webpki-roots = "1.0.6" +hickory-resolver = "0.25.2" +hickory-proto = "0.25.2" url = "2.5.4" bytes = "1.9.0" -http-body-util = "0.1.2" -http = "1.2.0" +http-body-util = "0.1.3" +http = "1.4.0" pin-project = "1" jni = "0.21.1" hyper-util = { version = "0.1.10", features = ["http1", "http2", "tokio"] } diff --git a/ooniprobe-core/src/lib.rs b/ooniprobe-core/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ooniprobe-core/src/lib.rs @@ -0,0 +1 @@ + diff --git a/ooniprobe-experiments/Cargo.toml b/ooniprobe-experiments/Cargo.toml index 19eb995..207cab7 100644 --- a/ooniprobe-experiments/Cargo.toml +++ b/ooniprobe-experiments/Cargo.toml @@ -1,22 +1,17 @@ [package] name = "ooniprobe-experiments" -version = { workspace = true } -edition = { workspace = true } +version = "0.1.0" +edition = "2021" [dependencies] -ooniprobe-services = { workspace = true } - -boring = { workspace = true } +ooniprobe-network = { path = "../ooniprobe-network" } +chrono = {workspace = true} bytes = { workspace = true } -http = { workspace = true } -http-body-util = { workspace = true } -hyper = { workspace = true } -hyper-util = { workspace = true } -tokio = { workspace = true } -tokio-boring = { workspace = true } -pin-project = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -quanta = { workspace = true } -url = { workspace = true } +tokio = {workspace = true} +tokio-rustls = {workspace = true} +http = {workspace = true} +http-body-util = {workspace = true} +thiserror = {workspace = true} diff --git a/ooniprobe-experiments/src/dataformats.rs b/ooniprobe-experiments/src/dataformats.rs deleted file mode 100644 index 9dd699c..0000000 --- a/ooniprobe-experiments/src/dataformats.rs +++ /dev/null @@ -1,176 +0,0 @@ -use boring::{base64, derive}; - -use crate::tracing::network_event::NetworkEvent; - -pub trait FromNetworkEvent { - fn add_network_event(&mut self, network_event: &NetworkEvent); -} - -#[derive(Default, Debug)] -pub struct DnsQuery { - pub answers: Vec, - pub engine: String, - pub failure: String, - pub getaddrinfo_error: i32, - pub hostname: String, - pub query_type: String, - pub raw_response: String, - pub rcode: i32, - pub resolver_address: String, - pub t0: f64, - pub t: f64, - pub tags: Vec, - pub transaction_id: i32, -} - -#[derive(Default, Debug)] -pub struct DnsAnswer { - pub answer_type: String, - pub asn: i32, - pub as_org_name: String, - pub hostname: String, - pub ipv4: String, - pub ipv6: String, - pub ttl: Option, -} - -#[derive(Default, Debug)] -pub struct HttpRequest { - pub body: String, - pub body_is_truncated: bool, - pub headers_list: Vec<(String, String)>, - pub headers: std::collections::HashMap, - pub method: String, - pub tor: TorInfo, - pub x_transport: String, - pub url: String, -} - -#[derive(Default, Debug)] -pub struct HttpResponse { - pub body: String, - pub body_is_truncated: bool, - pub code: i32, - pub headers_list: Vec<(String, String)>, - pub headers: std::collections::HashMap, -} - -#[derive(Default, Debug)] -pub struct TorInfo { - pub exit_ip: Option, - pub exit_name: Option, - pub is_tor: bool, -} - -#[derive(Default, Debug)] -pub struct HttpTransaction { - pub network: String, - pub address: String, - pub alpn: String, - pub failure: Option, - pub request: HttpRequest, - pub response: HttpResponse, - pub t0: f64, - pub t: f64, - pub transaction_id: i32, -} - -#[derive(Default, Debug)] -pub struct Measurement { - pub annotations: std::collections::HashMap, - pub extensions: std::collections::HashMap, - pub input: String, - pub measurement_start_time: String, - pub probe_asn: String, - pub probe_network_name: String, - pub probe_cc: String, - pub probe_city: String, - pub probe_ip: String, - pub report_filename: String, - pub report_id: String, - pub resolver_asn: String, - pub resolver_ip: String, - pub resolver_network_name: String, - pub software_name: String, - pub software_version: String, - pub test_helpers: std::collections::HashMap, - pub test_keys: std::collections::HashMap, - pub test_name: String, - pub test_runtime: f64, - pub test_start_time: String, - pub test_version: String, -} - -#[derive(Default, Debug)] -pub struct TCPConnectStatus { - pub failure: String, - pub success: bool, -} - -#[derive(Default, Debug)] -pub struct TCPConnect { - pub ip: String, - pub port: i32, - pub status: TCPConnectStatus, - pub t0: f64, - pub t: f64, - pub tags: Vec, - pub transaction_id: i32, -} - -#[derive(Default, Debug)] -pub struct TlsHandshake { - pub network: String, - pub address: String, - pub failure: String, - pub t0: f64, - pub t: f64, - pub transaction_id: u32, - - pub no_tls_verify: bool, - - pub negotiated_protocol: String, - pub echconfig: String, - pub peer_certificates: Vec, - pub server_name: String, - pub tls_version: String, - pub cipher_suite: String, - - pub tags: Vec, -} - -impl TlsHandshake { - pub fn new(address: &str) -> TlsHandshake { - let mut tls = TlsHandshake::default(); - tls.address = address.to_string(); - tls - } - - pub fn add_ssl(&mut self, ssl: &boring::ssl::SslRef) { - self.cipher_suite = ssl - .current_cipher() - .map_or(String::new(), |c| c.name().to_string()); - self.negotiated_protocol = ssl - .selected_alpn_protocol() - .map_or(String::new(), |p| String::from_utf8_lossy(p).to_string()); - self.peer_certificates = ssl.peer_cert_chain().map_or(Vec::new(), |chain| { - chain - .iter() - .map(|cert| base64::encode_block(cert.to_der().unwrap_or_default().as_ref())) - .collect() - }); - self.server_name = ssl - .servername(boring::ssl::NameType::HOST_NAME) - .map_or(String::new(), |name| name.to_string()); - self.tls_version = ssl.version_str().to_string(); - } -} - -impl FromNetworkEvent for TlsHandshake { - fn add_network_event(&mut self, network_event: &NetworkEvent) { - self.t0 = network_event.t0.unwrap(); - self.t = network_event.t.unwrap(); - self.network = network_event.proto.clone().unwrap(); - self.transaction_id = network_event.transaction_id; - } -} diff --git a/ooniprobe-experiments/src/experiment.rs b/ooniprobe-experiments/src/experiment.rs index a616098..3300003 100644 --- a/ooniprobe-experiments/src/experiment.rs +++ b/ooniprobe-experiments/src/experiment.rs @@ -1,136 +1,250 @@ +//! Experiment executor. +//! +//! [`Experiment`] walks an [`ExperimentConfig`]'s steps in order, threading +//! each step's output into the next step's input via a [`StepContext`]. + +use crate::parser::{DnsStep, DnsTransport, ExperimentConfig, HttpVersion, Step, TcpStep}; use bytes::Bytes; -use http_body_util::{BodyExt, Full}; -use hyper_util::rt::TokioIo; - -use tokio::net::TcpStream; - -use hyper::client::conn::http2; - -use crate::dataformats; -use crate::dataformats::FromNetworkEvent; -use crate::tracing::network_event::NetworkEventCollector; -use crate::tracing::stream::TracingTcpStream; -use ooniprobe_services::parrot; -use tokio::net::lookup_host; -use url::Url; - -#[derive(Default, Debug)] -pub struct TestKeys { - tcp_connect: Vec, - requests: Vec, - queries: Vec, - tls_handshakes: Vec, +use http::{Method, Request}; +use http_body_util::Full; +use ooniprobe_network::{ + dns::TracingResolver, + errors::OoniError, + http::{get_request, http_user_agent, TracingHttpClient}, + tcp::{TracingDialer, TracingStream}, + tls::TracingTlsHandshaker, + trace::Trace, +}; +use std::net::{IpAddr, SocketAddr}; +use tokio_rustls::client::TlsStream; + +/// Streams the executor holds between steps. +struct StepContext { + resolved_ips: Vec, + dns_hostname: Option, + tcp_streams: Vec<(SocketAddr, TracingStream)>, + tls_streams: Vec<(SocketAddr, TlsStream)>, } -#[derive(Debug)] -pub enum ExperimentError { - GenericError, +impl StepContext { + fn new() -> Self { + Self { + resolved_ips: vec![], + dns_hostname: None, + tcp_streams: vec![], + tls_streams: vec![], + } + } + + fn clear_streams(&mut self) { + self.tcp_streams.clear(); + self.tls_streams.clear(); + } } -pub struct Target { - input: String, +fn parse_addr(server: Option<&str>, default_port: u16) -> Result { + let s = server.ok_or_else(|| OoniError::Unknown("missing server field".into()))?; + if let Ok(sa) = s.parse::() { + return Ok(sa); + } + if let Ok(ip) = s.parse::() { + return Ok(SocketAddr::new(ip, default_port)); + } + Err(OoniError::Unknown(format!("cannot parse address '{s}'"))) } -pub struct Config {} +pub struct Experiment { + cfg: ExperimentConfig, + trace: Trace, +} -pub trait Experiment { - async fn run(target: Target, config: Config) -> TestKeys; +fn explicit_to_addr(s: &str, fallback_port: u16) -> Result { + if let Ok(sa) = s.parse::() { + return Ok(sa); + } + if let Ok(ip) = s.parse::() { + return Ok(SocketAddr::new(ip, fallback_port)); + } + Err(OoniError::Unknown(format!( + "cannot parse tcp address '{s}'" + ))) } -pub struct WebsiteExperiment; - -impl WebsiteExperiment { - pub async fn run(target: Target) -> Result { - let mut test_keys = TestKeys::default(); - - let measurement_start_time = quanta::Instant::now(); - let mut tracing_collector = NetworkEventCollector::new(measurement_start_time); - - let url = Url::parse(&target.input).unwrap(); - let host = url.host_str().unwrap().to_string(); - let port = match url.scheme() { - "http" => 80, - "https" => 443, - // TODO: what do we do if now match? - _ => 80, - }; - - let addr = format!("{}:{}", host, port); - - let transaction = tracing_collector.new_transaction(); - transaction.new_network_event(); - - let mut addrs = lookup_host(addr.clone()).await.unwrap(); - let socket = addrs.next().unwrap(); - - let stream = TcpStream::connect(&socket).await.unwrap(); - let stream_wrapper = TracingTcpStream::from_stream(stream, transaction.clone()); - - let mut network_event = transaction.new_network_event(); - network_event.set_proto("tcp"); - network_event.enter(); - let mut tls_handshake = dataformats::TlsHandshake::new(addr.as_str()); - let config = parrot::make_chrome_config(); - let stream = tokio_boring::connect(config, host.as_ref(), stream_wrapper) - .await - .unwrap(); - network_event.exit(); - tls_handshake.add_ssl(stream.ssl()); - tls_handshake.add_network_event(&network_event); - test_keys.tls_handshakes.push(tls_handshake); - - let io: TokioIo> = TokioIo::new(stream); - let executor = hyper_util::rt::tokio::TokioExecutor::new(); - - let (sender, conn) = http2::handshake::<_, _, Full>(executor, io) - .await - .map_err(|_| ExperimentError::GenericError)?; - - //stream_wrapper.print_network_events(); - tokio::task::spawn(async move { - if let Err(e) = conn.await { - println!("Error: {:?}", e); - } - }); - - let req = http::Request::builder() - .method(http::Method::GET) - .uri(target.input) - .version(hyper::Version::HTTP_2) - .body(Full::default()) - .map_err(|_| ExperimentError::GenericError)?; - - let res = sender - .clone() - .send_request(req) - .await - .map_err(|_| ExperimentError::GenericError)?; - - println!("Response: {:#?}", res); - - let body = res - .collect() - .await - .map_err(|_| ExperimentError::GenericError)? - .to_bytes(); - - println!("{}", String::from_utf8_lossy(&body)); - println!("test_keys: {:#?}", test_keys); - Ok(test_keys) +fn alpn_for(version: &HttpVersion) -> &'static str { + match version { + HttpVersion::H1 => "http/1.1", + HttpVersion::H2 | HttpVersion::Auto => "h2", + } +} + +fn build_request(url: &str, method: &str) -> Result>, OoniError> { + if method.eq_ignore_ascii_case("GET") { + return get_request(url); } + let m = Method::from_bytes(method.as_bytes()) + .map_err(|e| OoniError::Unknown(format!("invalid HTTP method '{method}': {e}")))?; + let uri: http::Uri = url + .parse() + .map_err(|e| OoniError::Unknown(format!("invalid URL '{url}': {e}")))?; + let host = uri + .host() + .ok_or_else(|| OoniError::Unknown("URL has no host".into()))? + .to_owned(); + Request::builder() + .method(m) + .uri(uri) + .header("Host", &host) + .header("User-Agent", http_user_agent()) + .body(Full::new(Bytes::new())) + .map_err(|e| OoniError::Unknown(format!("request build error: {e}"))) } -#[cfg(test)] -mod tests { +impl Experiment { + pub fn new(cfg: ExperimentConfig, trace: Trace) -> Self { + Self { cfg, trace } + } - use super::*; + async fn build_resolver(&self, s: &DnsStep) -> Result { + match &s.transport { + DnsTransport::System => Ok(TracingResolver::system(self.trace.clone())), + DnsTransport::Udp => { + let addr = parse_addr(s.server.as_deref(), 53)?; + Ok(TracingResolver::udp(addr, self.trace.clone())) + } + DnsTransport::Dot => { + let addr = parse_addr(s.server.as_deref(), 853)?; + Ok(TracingResolver::dot( + addr, + s.server.clone().unwrap_or_default(), + self.trace.clone(), + )) + } + DnsTransport::Doh => { + let url = s + .url + .as_deref() + .ok_or_else(|| OoniError::Unknown("DoH step missing 'url'".into()))?; + TracingResolver::from_url(url, self.trace.clone()).await + } + } + } + + fn resolve_tcp_addrs( + &self, + s: &TcpStep, + ctx: &StepContext, + ) -> Result, OoniError> { + match &s.address { + Some(explicit) => Ok(vec![explicit_to_addr(explicit, s.port)?]), + None => Ok(ctx + .resolved_ips + .iter() + .map(|ip| SocketAddr::new(*ip, s.port)) + .collect()), + } + } + + /// Execute every step, recording all observations into the trace. + pub async fn run(self) -> Result<(), OoniError> { + let mut ctx = StepContext::new(); + + for step in &self.cfg.steps { + match step { + Step::Dns(s) => { + ctx.clear_streams(); + let resolver = self.build_resolver(s).await?; + let ips = resolver.lookup_host(&s.hostname).await?; + if ips.is_empty() { + return Err(OoniError::DnsNoAnswer); + } + ctx.resolved_ips = ips; + ctx.dns_hostname = Some(s.hostname.clone()); + } + + Step::Tcp(s) => { + let addrs = self.resolve_tcp_addrs(s, &ctx)?; + let dialer = TracingDialer::new(self.trace.clone()); + ctx.tcp_streams.clear(); + ctx.tls_streams.clear(); + + for addr in addrs { + let tx_id = self.trace.next_transaction_id(); + if let Ok(stream) = dialer.connect(addr, tx_id).await { + ctx.tcp_streams.push((addr, stream)); + } + } + + if ctx.tcp_streams.is_empty() { + return Err(OoniError::ConnectionRefused); + } + } + + Step::Tls(s) => { + let sni = s + .sni + .clone() + .or_else(|| ctx.dns_hostname.clone()) + .unwrap_or_default(); + + let handshaker = if s.skip_verify { + TracingTlsHandshaker::insecure(self.trace.clone())? + } else { + TracingTlsHandshaker::new(self.trace.clone())? + }; + + let tcp_streams = std::mem::take(&mut ctx.tcp_streams); + for (addr, tcp) in tcp_streams { + let tx_id = self.trace.next_transaction_id(); + if let Ok(tls) = handshaker + .handshake(tcp, &sni, &addr.to_string(), tx_id) + .await + { + ctx.tls_streams.push((addr, tls)); + } + } + + if ctx.tls_streams.is_empty() { + return Err(OoniError::TlsFailedHandshake); + } + } + + Step::Http(s) => { + let http = TracingHttpClient::new(self.trace.clone()); + + if !ctx.tls_streams.is_empty() { + let streams = std::mem::take(&mut ctx.tls_streams); + for (addr, tls) in streams { + let tx_id = self.trace.next_transaction_id(); + if let Ok(req) = build_request(&s.url, &s.method) { + let alpn = alpn_for(&s.version); + let _ = match s.version { + HttpVersion::H1 => { + http.send_http1(tls, req, &addr.to_string(), alpn, tx_id) + .await + } + HttpVersion::H2 | HttpVersion::Auto => { + http.send_http2(tls, req, &addr.to_string(), alpn, tx_id) + .await + } + }; + } + } + } else { + let streams = std::mem::take(&mut ctx.tcp_streams); + for (addr, tcp) in streams { + let tx_id = self.trace.next_transaction_id(); + if let Ok(req) = build_request(&s.url, &s.method) { + let _ = http + .send_http1(tcp, req, &addr.to_string(), "http/1.1", tx_id) + .await; + } + } + } + // http is terminal — no abort check needed + } + } + } - #[tokio::test] - async fn test_experiment() { - WebsiteExperiment::run(Target { - input: "https://www.google.com/humans.txt".to_string(), - }) - .await - .unwrap(); + Ok(()) } } diff --git a/ooniprobe-experiments/src/lib.rs b/ooniprobe-experiments/src/lib.rs index 967841c..0a94f74 100644 --- a/ooniprobe-experiments/src/lib.rs +++ b/ooniprobe-experiments/src/lib.rs @@ -1,3 +1,3 @@ -pub mod dataformats; pub mod experiment; -pub mod tracing; +pub mod measurement; +pub mod parser; diff --git a/ooniprobe-experiments/src/measurement.rs b/ooniprobe-experiments/src/measurement.rs new file mode 100644 index 0000000..2c73fa0 --- /dev/null +++ b/ooniprobe-experiments/src/measurement.rs @@ -0,0 +1,89 @@ +//! OONI measurement assembly. +//! +//! This module bridges the experiment executor and the OONI archival format. + +use std::time::{Instant, SystemTime}; + +use serde::Serialize; + +use crate::{ + experiment::Experiment, + parser::{ExperimentConfig, ParseError}, +}; + +use ooniprobe_network::{ + archival::{ + DnsLookupResult, HttpTransaction, Measurement, NetworkEvent, TcpConnectResult, + TlsHandshakeResult, + }, + trace::Trace, +}; + +/// The `test_keys` object written into every OONI measurement. +#[derive(Debug, Default, Serialize)] +pub struct TestKeys { + pub queries: Vec, + pub tcp_connect: Vec, + pub tls_handshakes: Vec, + pub network_events: Vec, + pub requests: Vec, +} + +impl TestKeys { + /// Drain all observations out of `trace` into a new `TestKeys`. + pub fn from_trace(trace: &Trace) -> Self { + Self { + queries: trace.drain_dns_lookups(), + tcp_connect: trace.drain_tcp_connects(), + tls_handshakes: trace.drain_tls_handshakes(), + network_events: trace.drain_network_events(), + requests: trace.drain_http_requests(), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum RunError { + #[error("invalid config: {0}")] + Parse(#[from] ParseError), + + #[error("serialisation failed: {0}")] + Serialise(#[from] serde_json::Error), +} + +pub async fn run(config: &str) -> Result { + let cfg = ExperimentConfig::from_json(config)?; + + let trace = Trace::new(1); + let start_time = SystemTime::now(); + let start = Instant::now(); + + let _ = Experiment::new(cfg.clone(), trace.clone()).run().await; + + let test_keys = TestKeys::from_trace(&trace); + // TODO: populate resolver asn and resolver ip correctly + let measurement = Measurement { + annotations: Default::default(), + data_format_version: "0.2.0".into(), + extensions: None, + input: cfg.input, + measurement_start_time: start_time, + probe_asn: cfg.probe_config.probe_asn, + probe_cc: cfg.probe_config.probe_cc, + probe_ip: cfg.probe_config.probe_ip, + probe_network_name: cfg.probe_config.probe_network_name, + resolver_asn: String::new(), + resolver_ip: String::new(), + resolver_network_name: String::new(), + software_name: cfg.probe_config.software_name, + software_version: cfg.probe_config.software_version, + test_helpers: serde_json::Value::Object(Default::default()), + test_keys: serde_json::to_value(test_keys).unwrap_or_default(), + test_name: cfg.name, + test_runtime: start.elapsed().as_secs_f64(), + test_start_time: start_time, + test_version: "0.1.0".into(), + }; + + Ok(serde_json::to_string(&measurement)?) +} diff --git a/ooniprobe-experiments/src/parser.rs b/ooniprobe-experiments/src/parser.rs new file mode 100644 index 0000000..d0c7ce7 --- /dev/null +++ b/ooniprobe-experiments/src/parser.rs @@ -0,0 +1,221 @@ +//! Experiment configuration parser. +//! +//! An [`ExperimentConfig`] is the single type that both the parser and the +//! executor work with. + +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum DnsTransport { + System, + Udp, + Dot, + Doh, +} + +impl Default for DnsTransport { + fn default() -> Self { + Self::System + } +} + +impl fmt::Display for DnsTransport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::System => f.write_str("system"), + Self::Udp => f.write_str("udp"), + Self::Dot => f.write_str("dot"), + Self::Doh => f.write_str("doh"), + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct DnsStep { + pub hostname: String, + #[serde(default)] + pub transport: DnsTransport, + + /// Required for Udp and Dot transports (`"ip:port"`). + pub server: Option, + + /// Required for Doh transport + pub url: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TcpStep { + pub port: u16, + pub address: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TlsStep { + pub sni: Option, + #[serde(default)] + pub skip_verify: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum HttpVersion { + H1, + H2, + /// Negotiate via ALPN (use h2 if available, else h1). + Auto, +} + +impl Default for HttpVersion { + fn default() -> Self { + Self::Auto + } +} + +fn default_http_method() -> String { + "GET".into() +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct HttpStep { + pub url: String, + + /// HTTP method + #[serde(default = "default_http_method")] + pub method: String, + + /// HTTP version to use + #[serde(default)] + pub version: HttpVersion, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum Step { + Dns(DnsStep), + Tcp(TcpStep), + Tls(TlsStep), + Http(HttpStep), +} + +impl Step { + pub fn kind(&self) -> &'static str { + match self { + Self::Dns(_) => "dns", + Self::Tcp(_) => "tcp", + Self::Tls(_) => "tls", + Self::Http(_) => "http", + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error("JSON parse error: {0}")] + Json(#[from] serde_json::Error), + + #[error("validation error: {0}")] + Validation(String), +} + +/// Assembles a complete OONI [`Measurement`] from an experiment run. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct ProbeConfig { + pub probe_asn: String, + pub probe_cc: String, + pub probe_ip: String, + pub probe_network_name: String, + + pub software_name: String, + pub software_version: String, +} + +/// A fully-parsed, validated experiment description. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ExperimentConfig { + #[serde(rename = "experiment")] + pub name: String, + pub input: Option, + pub probe_config: ProbeConfig, + pub steps: Vec, +} + +impl ExperimentConfig { + /// Parse from a JSON string and validate step dependencies. + pub fn from_json(input: &str) -> Result { + let cfg: Self = serde_json::from_str(input)?; + cfg.validate()?; + Ok(cfg) + } + + fn validate(&self) -> Result<(), ParseError> { + if self.steps.is_empty() { + return Err(ParseError::Validation("steps list is empty".into())); + } + + let mut has_ips = false; + let mut has_tcp = false; + let mut has_tls = false; + // Remember the last DNS hostname so TLS steps can fall back to it. + let mut last_dns_hostname: Option<&str> = None; + + for (i, step) in self.steps.iter().enumerate() { + match step { + Step::Dns(s) => { + if s.transport != DnsTransport::System && s.server.is_none() { + return Err(ParseError::Validation(format!( + "step {i} (dns): transport '{}' requires a 'server' field", + s.transport + ))); + } + if s.transport == DnsTransport::Doh && s.url.is_none() { + return Err(ParseError::Validation(format!( + "step {i} (dns/doh): 'url' is required for DoH" + ))); + } + has_ips = true; + has_tcp = false; + has_tls = false; + last_dns_hostname = Some(&s.hostname); + } + + Step::Tcp(s) => { + if s.address.is_none() && !has_ips { + return Err(ParseError::Validation(format!( + "step {i} (tcp): no 'address' and no preceding dns step to supply addresses" + ))); + } + has_ips = false; + has_tcp = true; + } + + Step::Tls(s) => { + if !has_tcp { + return Err(ParseError::Validation(format!( + "step {i} (tls): no preceding tcp step to supply an open stream" + ))); + } + // SNI must be resolvable — either explicit or from a prior dns step. + if s.sni.is_none() && last_dns_hostname.is_none() { + return Err(ParseError::Validation(format!( + "step {i} (tls): no 'sni' and no preceding dns step to derive SNI from" + ))); + } + has_tcp = false; + has_tls = true; + } + + Step::Http(_) => { + if !has_tcp && !has_tls { + return Err(ParseError::Validation(format!( + "step {i} (http): requires an open stream from a preceding tcp or tls step" + ))); + } + } + } + } + + Ok(()) + } +} diff --git a/ooniprobe-experiments/src/tracing/mod.rs b/ooniprobe-experiments/src/tracing/mod.rs deleted file mode 100644 index d29fb9c..0000000 --- a/ooniprobe-experiments/src/tracing/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod network_event; -pub mod stream; diff --git a/ooniprobe-experiments/src/tracing/network_event.rs b/ooniprobe-experiments/src/tracing/network_event.rs deleted file mode 100644 index f3bd0fe..0000000 --- a/ooniprobe-experiments/src/tracing/network_event.rs +++ /dev/null @@ -1,194 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use quanta::Instant; -pub enum TracingError { - GenericError, - SpanAlreadyEntered, - SpanNotEntered, - SpanAlreadyExited, -} - -#[derive(Clone, Serialize, Debug)] -pub struct NetworkEvent { - pub address: Option, - pub failure: Option, - pub num_bytes: Option, - pub operation: Option, - pub proto: Option, - pub transaction_id: u32, - pub t0: Option, - pub t: Option, - - #[serde(skip_serializing)] - measurement_start_time: quanta::Instant, -} - -impl NetworkEvent { - pub fn set_address(&mut self, value: &str) -> Result<(), TracingError> { - self.address = Some(value.to_string()); - Ok(()) - } - - pub fn set_failure(&mut self, value: &str) -> Result<(), TracingError> { - self.failure = Some(value.to_string()); - Ok(()) - } - pub fn set_operation(&mut self, value: &str) -> Result<(), TracingError> { - self.operation = Some(value.to_string()); - Ok(()) - } - pub fn set_proto(&mut self, value: &str) -> Result<(), TracingError> { - self.proto = Some(value.to_string()); - Ok(()) - } - - pub fn set_num_bytes(&mut self, value: u32) -> Result<(), TracingError> { - self.num_bytes = Some(value); - Ok(()) - } - - pub fn enter(&mut self) -> Result<(), TracingError> { - if self.t0.is_some() { - return Err(TracingError::SpanAlreadyEntered); - } - self.t0 = Some(self.measurement_start_time.elapsed().as_secs_f64()); - Ok(()) - } - - pub fn exit(&mut self) -> Result<(), TracingError> { - if self.t0.is_none() { - return Err(TracingError::SpanNotEntered); - } - if self.t.is_some() { - return Err(TracingError::SpanAlreadyExited); - } - self.t = Some(self.measurement_start_time.elapsed().as_secs_f64()); - Ok(()) - } -} - -#[derive(Clone)] -pub struct NetworkEventCollector { - measurement_start_time: Instant, - current_transaction_idx: u32, - transactions: Vec, -} - -impl<'a> NetworkEventCollector { - pub fn new(measurement_start_time: Instant) -> Self { - Self { - measurement_start_time, - current_transaction_idx: 5000, - transactions: vec![], - } - } - - pub fn new_transaction(&mut self) -> NetworkEventTransaction { - let span = NetworkEventTransaction::new( - self.new_transaction_idx(), - self.measurement_start_time.clone(), - ); - span - } - - pub fn print_network_events(&mut self) { - for span in &self.transactions { - match serde_json::to_string(&span.network_events) { - Ok(json) => println!("{}", json), - Err(e) => eprintln!("Error serializing span: {}", e), - } - } - } - - pub fn collect_transaction(&mut self, network_event_transaction: NetworkEventTransaction) { - self.transactions.push(network_event_transaction) - } - - fn new_transaction_idx(&mut self) -> u32 { - self.current_transaction_idx += 1; - self.current_transaction_idx - } -} - -#[derive(Clone, Debug)] -pub struct NetworkEventTransaction { - measurement_start_time: Instant, - transaction_id: u32, - network_events: Vec, -} - -impl NetworkEventTransaction { - fn new(transaction_id: u32, measurement_start_time: Instant) -> Self { - Self { - measurement_start_time, - transaction_id, - network_events: vec![], - } - } - - pub fn new_network_event(&self) -> NetworkEvent { - NetworkEvent { - measurement_start_time: self.measurement_start_time, - address: None, - failure: None, - num_bytes: None, - operation: None, - proto: None, - t0: None, - t: None, - transaction_id: self.transaction_id, - } - } - - pub fn collect_network_event(&mut self, network_event: NetworkEvent) { - self.network_events.push(network_event) - } -} - -#[cfg(test)] -mod tests { - use std::{thread, time::Duration}; - - use super::*; - - #[test] - fn test_tracing() { - let measurement_start_time = quanta::Instant::now(); - let mut collector = NetworkEventCollector::new(measurement_start_time); - - let mut transaction1 = collector.new_transaction(); - let mut network_event1 = transaction1.new_network_event(); - network_event1.enter(); - network_event1.set_address("1.1.1.1:443"); - thread::sleep(Duration::from_micros(42)); - network_event1.exit(); - transaction1.collect_network_event(network_event1); - - let mut network_event2 = transaction1.new_network_event(); - network_event2.enter(); - network_event2.set_address("2.2.2.2:443"); - thread::sleep(Duration::from_micros(100)); - network_event2.exit(); - transaction1.collect_network_event(network_event2); - - collector.collect_transaction(transaction1); - - let mut transaction2 = collector.new_transaction(); - let mut network_event1 = transaction2.new_network_event(); - network_event1.enter(); - network_event1.set_address("1.1.1.1:443"); - thread::sleep(Duration::from_micros(42)); - network_event1.exit(); - transaction2.collect_network_event(network_event1); - - let mut network_event2 = transaction2.new_network_event(); - network_event2.enter(); - network_event2.set_address("2.2.2.2:443"); - thread::sleep(Duration::from_micros(100)); - network_event2.exit(); - transaction2.collect_network_event(network_event2); - - collector.collect_transaction(transaction2); - collector.print_network_events(); - } -} diff --git a/ooniprobe-experiments/src/tracing/stream.rs b/ooniprobe-experiments/src/tracing/stream.rs deleted file mode 100644 index b26ea39..0000000 --- a/ooniprobe-experiments/src/tracing/stream.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::io; -use std::{ - pin::Pin, - task::{Context, Poll}, -}; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -use tokio::net::TcpStream; - -use super::network_event::{NetworkEvent, NetworkEventTransaction}; - -#[derive(Debug)] -#[pin_project::pin_project] -pub struct TracingTcpStream { - #[pin] - inner: TcpStream, - network_events: Vec, - transaction: NetworkEventTransaction, -} - -impl TracingTcpStream { - pub fn from_stream(inner: TcpStream, transaction: NetworkEventTransaction) -> Self { - Self { - inner, - transaction, - network_events: vec![], - } - } - - pub fn print_network_events(self) { - println!("{:?}", self.network_events); - } -} - -impl AsyncWrite for TracingTcpStream { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - let this = self.project(); - let ret = this.inner.poll_write(cx, buf); - let num_bytes = match &ret { - Poll::Ready(Ok(n)) => *n, - _ => 0, - }; - let mut ne = this.transaction.new_network_event(); - ne.set_operation("write"); - ne.set_num_bytes(num_bytes.try_into().unwrap()); - this.network_events.push(ne); - ret - } - - fn poll_write_vectored( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - bufs: &[io::IoSlice<'_>], - ) -> Poll> { - self.project().inner.poll_write_vectored(cx, bufs) - } - - fn is_write_vectored(&self) -> bool { - self.inner.is_write_vectored() - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().inner.poll_flush(cx) - } - - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().inner.poll_shutdown(cx) - } -} - -impl AsyncRead for TracingTcpStream { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - let initial_filled = buf.filled().len(); - let this = self.project(); - let ret = this.inner.poll_read(cx, buf); - let num_bytes = buf.filled().len() - initial_filled; - let mut ne = this.transaction.new_network_event(); - ne.set_operation("read"); - ne.set_num_bytes(num_bytes.try_into().unwrap()); - this.network_events.push(ne); - ret - } -} diff --git a/ooniprobe-experiments/tests/test_websteps.rs b/ooniprobe-experiments/tests/test_websteps.rs new file mode 100644 index 0000000..6ec73b9 --- /dev/null +++ b/ooniprobe-experiments/tests/test_websteps.rs @@ -0,0 +1,34 @@ +#[cfg(test)] +mod tests { + use ooniprobe_experiments::measurement::run; + use std::fs; + + #[tokio::test] + async fn tcping_success() { + let config = r#"{ + "experiment": "web_connectivity", + "input": "example.com:443", + "probe_config": { + "probe_asn": "AS0", + "probe_cc": "ZZ", + "probe_ip": "127.0.0.1", + "probe_network_name": "Test Network", + "software_name": "test", + "software_version": "0.0.1" + }, + "steps": [ + {"type": "dns", "hostname": "example.com"}, + {"type": "tcp", "port": 443}, + {"type": "tls"}, + {"type": "http", "url": "https://example.com"} + ] + }"#; + + let json = run(config).await.unwrap(); + + let m: serde_json::Value = serde_json::from_str(&json).unwrap(); + let pretty = serde_json::to_string_pretty(&m).unwrap(); + + fs::write("webconnectivity_measurement.json", &pretty).unwrap(); + } +} diff --git a/ooniprobe-network/Cargo.toml b/ooniprobe-network/Cargo.toml new file mode 100644 index 0000000..1107ca1 --- /dev/null +++ b/ooniprobe-network/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ooniprobe-network" +version = { workspace = true } +edition = { workspace = true } + +[dependencies] +chrono = {workspace = true} +serde = { workspace = true } +serde_json = { workspace = true } +base64 = {workspace = true} +bytes = {workspace = true} +thiserror = {workspace = true} +tokio = {workspace = true} +tokio-rustls = {workspace = true} +pin-project= {workspace = true} +rustls = {workspace = true, features = ["ring"]} +webpki-roots = {workspace = true} +hickory-resolver = {workspace = true, features = ["tokio"]} +hickory-proto = {workspace = true} +http = {workspace = true} +http-body-util = {workspace = true} +hyper = {workspace = true} +hyper-util = {workspace = true} +rand = {workspace = true} +url = {workspace = true} diff --git a/ooniprobe-network/src/archival.rs b/ooniprobe-network/src/archival.rs new file mode 100644 index 0000000..bf31e93 --- /dev/null +++ b/ooniprobe-network/src/archival.rs @@ -0,0 +1,319 @@ +//! OONI archival data-format types. +//! +//! . +//! +//! All types implement `Serialize`/`Deserialize` with field names that +//! match the JSON keys mandated by the spec. + +use std::time::SystemTime; + +use crate::utils::{b64_decode, b64_encode}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// A byte string that serialises as `{"format":"base64","data":"…"}` per +/// df-001-httpt § MaybeBinaryData. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct BinaryData(pub Vec); + +impl Serialize for BinaryData { + fn serialize(&self, s: S) -> Result { + use serde::ser::SerializeMap; + if self.0.is_empty() { + return s.serialize_none(); + } + let mut map = s.serialize_map(Some(2))?; + map.serialize_entry("format", "base64")?; + map.serialize_entry("data", &b64_encode(&self.0))?; + map.end() + } +} + +impl<'de> Deserialize<'de> for BinaryData { + fn deserialize>(d: D) -> Result { + use serde::de::Error; + #[derive(Deserialize)] + struct Repr { + format: String, + data: String, + } + let repr = Option::::deserialize(d)?; + match repr { + None => Ok(BinaryData(vec![])), + Some(r) => { + if r.format != "base64" { + return Err(D::Error::custom(format!( + "unknown binary data format: {}", + r.format + ))); + } + let bytes = b64_decode(&r.data).map_err(D::Error::custom)?; + Ok(BinaryData(bytes)) + } + } + } +} + +/// A body that is either valid UTF-8 text or raw bytes (base64-encoded). +/// Mirrors the MaybeBinaryData logic in the Go implementation. +#[derive(Debug, Clone, Default)] +pub struct MaybeBinaryData(pub Vec); + +impl Serialize for MaybeBinaryData { + fn serialize(&self, s: S) -> Result { + match std::str::from_utf8(&self.0) { + Ok(text) => s.serialize_str(text), + Err(_) => BinaryData(self.0.clone()).serialize(s), + } + } +} + +impl<'de> Deserialize<'de> for MaybeBinaryData { + fn deserialize>(d: D) -> Result { + use serde::de::Error; + let v = serde_json::Value::deserialize(d)?; + match v { + serde_json::Value::String(s) => Ok(MaybeBinaryData(s.into_bytes())), + serde_json::Value::Object(_) => { + let bd: BinaryData = serde_json::from_value(v).map_err(D::Error::custom)?; + Ok(MaybeBinaryData(bd.0)) + } + serde_json::Value::Null => Ok(MaybeBinaryData(vec![])), + other => Err(D::Error::custom(format!( + "unexpected body value: {}", + other + ))), + } + } +} + +/// DnsAnswer: https://github.com/ooni/spec/blob/master/data-formats/df-002-dnst.md#answer +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DnsAnswer { + pub answer_type: String, + pub asn: i64, + pub as_org_name: String, + pub expiration_limit: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub hostname: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ipv4: Option, + pub ipv6: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub minimum_ttl: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_interval: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub responsible_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub retry_interval: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub serial_number: Option, + pub ttl: Option, +} + +/// DnsLookupResult: https://github.com/ooni/spec/blob/master/data-formats/df-002-dnst.md#dns-data-format +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DnsLookupResult { + pub answers: Vec, + pub engine: String, + pub failure: Option, + pub hostname: String, + pub query_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub raw_response: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rcode: Option, + pub resolver_hostname: Option, + pub resolver_port: Option, + pub resolver_address: String, + pub t0: f64, + pub t: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_id: Option, +} + +/// TcpConnectStatus: https://github.com/ooni/spec/blob/master/data-formats/df-005-tcpconnect.md#status +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct TcpConnectStatus { + #[serde(skip_serializing_if = "Option::is_none")] + pub blocked: Option, + pub failure: Option, + pub success: bool, +} + +/// TcpConnectResult: https://github.com/ooni/spec/blob/master/data-formats/df-005-tcpconnect.md#tcpconnect +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct TcpConnectResult { + #[serde(skip_serializing_if = "Option::is_none")] + pub conn_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dial_id: Option, + pub ip: String, + pub port: u16, + pub status: TcpConnectStatus, + pub t0: f64, + pub t: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TlsNetwork { + Tcp, + Udp, +} + +/// TlsHandshakeResult: https://github.com/ooni/spec/blob/master/data-formats/df-006-tlshandshake.md +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TlsHandshakeResult { + #[serde(skip_serializing_if = "Option::is_none")] + pub network: Option, + pub address: String, + pub cipher_suite: String, + pub failure: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub so_error: Option, + pub negotiated_protocol: String, + pub no_tls_verify: bool, + pub peer_certificates: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub server_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub outer_server_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ech_config: Option, + pub t0: f64, + pub t: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + pub tls_version: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum NetworkOperation { + BytesReceivedCumulative, + Connect, + Read, + ReadFrom, + Write, + WriteTo, + TlsHandshakeStart, + TlsHandshakeDone, + Close, // NOTE: addition to ooni/spec +} + +/// NetworkEvent: https://github.com/ooni/spec/blob/master/data-formats/df-008-netevents.md +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkEvent { + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub conn_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dial_id: Option, + pub failure: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub num_bytes: Option, + pub operation: NetworkOperation, + #[serde(skip_serializing_if = "Option::is_none")] + pub proto: Option, + pub t0: f64, + pub t: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_id: Option, +} + +/// HttpRequest: https://github.com/ooni/spec/blob/master/data-formats/df-001-httpt.md#request +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct HttpRequest { + pub url: String, + pub body: MaybeBinaryData, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_is_truncated: Option, + pub headers_list: Vec<(String, String)>, + pub headers: std::collections::HashMap, + pub method: String, + pub x_transport: String, +} + +/// HttpResponse: https://github.com/ooni/spec/blob/master/data-formats/df-001-httpt.md#response +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct HttpResponse { + pub body: MaybeBinaryData, + pub body_is_truncated: bool, + pub code: u16, + pub headers_list: Vec<(String, String)>, + pub headers: std::collections::HashMap, + + #[serde(skip)] + pub raw_body: Vec, +} + +/// HttpTransaction: https://github.com/ooni/spec/blob/master/data-formats/df-001-httpt.md +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct HttpTransaction { + #[serde(skip_serializing_if = "Option::is_none")] + pub network: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub alpn: Option, + pub failure: Option, + pub request: HttpRequest, + pub response: HttpResponse, + #[serde(skip_serializing_if = "Option::is_none")] + pub response_length: Option, + pub t0: f64, + pub t: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_id: Option, +} + +/// Top-level OONI measurement envelope. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Measurement { + pub annotations: std::collections::HashMap, + pub data_format_version: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub extensions: Option>, + pub input: Option, + #[serde(serialize_with = "serialize_time")] + pub measurement_start_time: SystemTime, + pub probe_asn: String, + pub probe_cc: String, + pub probe_ip: String, + pub probe_network_name: String, + pub resolver_asn: String, + pub resolver_ip: String, + pub resolver_network_name: String, + pub software_name: String, + pub software_version: String, + pub test_helpers: serde_json::Value, + pub test_keys: serde_json::Value, + pub test_name: String, + pub test_runtime: f64, + #[serde(serialize_with = "serialize_time")] + pub test_start_time: SystemTime, + pub test_version: String, +} + +fn serialize_time(time: &SystemTime, serializer: S) -> Result +where + S: serde::Serializer, +{ + let dt: DateTime = (*time).into(); + serializer.serialize_str(&dt.format("%Y-%m-%d %H:%M:%S").to_string()) +} diff --git a/ooniprobe-network/src/dns.rs b/ooniprobe-network/src/dns.rs new file mode 100644 index 0000000..c98753a --- /dev/null +++ b/ooniprobe-network/src/dns.rs @@ -0,0 +1,481 @@ +//! DNS resolution with full transport-layer tracing. + +use std::net::{IpAddr, SocketAddr}; +use std::time::Duration; + +use bytes::Bytes; +use hickory_proto::ProtoErrorKind; +use http::{Method, Request}; +use http_body_util::Full; + +use hickory_proto::{ + op::{Message, MessageType, OpCode, Query, ResponseCode}, + rr::{DNSClass, Name, RData, RecordType}, + serialize::binary::BinDecodable, +}; + +use hickory_resolver::ResolveError; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use url::Url; + +use crate::{ + archival::{DnsAnswer, DnsLookupResult}, + errors::OoniError, + http::{http_user_agent, TracingHttpClient}, + tcp::TracingDialer, + tls::TracingTlsHandshaker, + trace::Trace, + udp::TracingUdpSocket, +}; + +/// Build a DNS query wire message for `name`/`qtype`. +/// Returns `(message_id, wire_bytes)`. +fn build_query(name: &str, qtype: RecordType) -> Result<(u16, Vec), OoniError> { + let id: u16 = rand::random(); + let mut msg = Message::new(); + msg.set_id(id) + .set_message_type(MessageType::Query) + .set_op_code(OpCode::Query) + .set_recursion_desired(true); + + let parsed = Name::from_utf8(name) + .map_err(|e| OoniError::DnsUnexpected(format!("invalid name: {e}")))?; + let mut q = Query::new(); + q.set_name(parsed) + .set_query_type(qtype) + .set_query_class(DNSClass::IN); + msg.add_query(q); + + let bytes = msg + .to_vec() + .map_err(|e| OoniError::DnsUnexpected(format!("encode error: {e}")))?; + Ok((id, bytes)) +} + +/// Parse a raw DNS response into `(Vec, rcode)`. +/// Returns `Err` for NXDOMAIN, SERVFAIL, REFUSED, or empty answer sections. +fn parse_response(data: &[u8], qtype: RecordType) -> Result<(Vec, i64), OoniError> { + let msg = Message::from_bytes(data) + .map_err(|e| OoniError::DnsUnexpected(format!("decode error: {e}")))?; + + let rcode = i64::from(u16::from(msg.response_code())); + + match msg.response_code() { + ResponseCode::NXDomain => return Err(OoniError::DnsNxDomain), + ResponseCode::ServFail => return Err(OoniError::DnsServFail), + ResponseCode::Refused => return Err(OoniError::DnsRefused), + _ => {} + } + + let answers: Vec = msg + .answers() + .iter() + .filter_map(|rec| match rec.data() { + RData::A(a) if qtype == RecordType::A => Some(DnsAnswer { + answer_type: "A".into(), + ipv4: Some(a.to_string()), + ttl: Some(rec.ttl()), + ..Default::default() + }), + RData::AAAA(a) if qtype == RecordType::AAAA => Some(DnsAnswer { + answer_type: "AAAA".into(), + ipv6: Some(a.to_string()), + ttl: Some(rec.ttl()), + ..Default::default() + }), + RData::CNAME(c) => Some(DnsAnswer { + answer_type: "CNAME".into(), + hostname: Some(c.to_string()), + ttl: Some(rec.ttl()), + ..Default::default() + }), + _ => None, + }) + .collect(); + + if answers.is_empty() { + return Err(OoniError::DnsNoAnswer); + } + + Ok((answers, rcode)) +} + +fn classify_hickory(e: &ResolveError) -> OoniError { + use hickory_resolver::ResolveErrorKind; + match e.kind() { + ResolveErrorKind::Proto(proto_err) => match proto_err.kind() { + ProtoErrorKind::NoRecordsFound { .. } => OoniError::DnsNoAnswer, + ProtoErrorKind::Timeout { .. } => OoniError::DnsTimeout, + _ => OoniError::DnsUnexpected(proto_err.to_string()), + }, + _ => OoniError::DnsUnexpected(e.to_string()), + } +} + +/// Extract IP addresses from a parsed answer list. +fn answers_to_ips(answers: &[DnsAnswer]) -> Vec { + answers + .iter() + .filter_map(|a| { + a.ipv4 + .as_deref() + .and_then(|s| s.parse().ok()) + .map(IpAddr::V4) + .or_else(|| { + a.ipv6 + .as_deref() + .and_then(|s| s.parse().ok()) + .map(IpAddr::V6) + }) + }) + .collect() +} + +// DnsTransport — carries the wire message to the resolver +pub enum DnsTransport { + System, + Udp { + server: SocketAddr, + }, + Dot { + server: SocketAddr, + hostname: String, + }, + Doh { + server: SocketAddr, + hostname: String, + url: String, + }, +} + +impl DnsTransport { + /// Exchange a raw DNS query with the server using our traced primitives. + async fn exchange( + &self, + query: &[u8], + trace: &Trace, + tx_id: i64, + ) -> Result, OoniError> { + match self { + Self::System => unreachable!("System transport has no wire exchange"), + + Self::Udp { server } => { + let sock = TracingUdpSocket::connect(*server, trace.clone()).await?; + sock.exchange(query, Duration::from_secs(5), tx_id).await + } + + Self::Dot { server, hostname } => { + let dialer = TracingDialer::new(trace.clone()); + let stream = dialer.connect(*server, tx_id).await?; + let addr = stream.address().to_owned(); + + let hs = TracingTlsHandshaker::new(trace.clone())?; + let mut tls = hs.handshake(stream, hostname, &addr, tx_id).await?; + + // Send: length-prefixed query. + let len_bytes = (query.len() as u16).to_be_bytes(); + tls.write_all(&len_bytes) + .await + .map_err(OoniError::from_io)?; + tls.write_all(query).await.map_err(OoniError::from_io)?; + tls.flush().await.map_err(OoniError::from_io)?; + + // Recv: 2-byte length then payload. + let mut len_buf = [0u8; 2]; + tls.read_exact(&mut len_buf) + .await + .map_err(OoniError::from_io)?; + let resp_len = u16::from_be_bytes(len_buf) as usize; + let mut resp = vec![0u8; resp_len]; + tls.read_exact(&mut resp) + .await + .map_err(OoniError::from_io)?; + + Ok(resp) + } + + Self::Doh { + server, + hostname, + url, + } => { + let dialer = TracingDialer::new(trace.clone()); + let stream = dialer.connect(*server, tx_id).await?; + let addr = stream.address().to_owned(); + + let hs = TracingTlsHandshaker::new(trace.clone())?; + let tls = hs.handshake(stream, hostname, &addr, tx_id).await?; + + let http = TracingHttpClient::new(trace.clone()); + let req = Request::builder() + .method(Method::POST) + .uri(url.as_str()) + .header("Host", hostname.as_str()) + .header("Content-Type", "application/dns-message") + .header("Accept", "application/dns-message") + .header("User-Agent", http_user_agent()) + .body(Full::new(Bytes::copy_from_slice(query))) + .map_err(|e| OoniError::Unknown(format!("DoH req build: {e}")))?; + + let resp = http.send_http2(tls, req, &addr, "h2", tx_id).await?; + Ok(resp.raw_body) + } + } + } + + pub fn engine(&self) -> &'static str { + match self { + Self::System => "system", + Self::Udp { .. } => "udp", + Self::Dot { .. } => "dot", + Self::Doh { .. } => "doh", + } + } + + pub fn resolver_address(&self) -> String { + match self { + Self::System => String::new(), + Self::Udp { server } => server.to_string(), + Self::Dot { server, .. } => server.to_string(), + Self::Doh { url, .. } => url.clone(), + } + } +} + +// TracingResolver + +/// A DNS resolver that records every lookup **and all transport-layer events** into a [`Trace`]. +pub struct TracingResolver { + transport: DnsTransport, + trace: Trace, +} + +impl TracingResolver { + pub fn system(trace: Trace) -> Self { + Self { + transport: DnsTransport::System, + trace, + } + } + + /// Plain DNS over UDP. + pub fn udp(server: SocketAddr, trace: Trace) -> Self { + Self { + transport: DnsTransport::Udp { server }, + trace, + } + } + + /// DNS over TLS (port 853 by convention). + pub fn dot(server: SocketAddr, hostname: impl Into, trace: Trace) -> Self { + Self { + transport: DnsTransport::Dot { + server, + hostname: hostname.into(), + }, + trace, + } + } + + /// DNS over HTTPS. + pub fn doh( + server: SocketAddr, + hostname: impl Into, + url: impl Into, + trace: Trace, + ) -> Self { + Self { + transport: DnsTransport::Doh { + server, + hostname: hostname.into(), + url: url.into(), + }, + trace, + } + } + + /// Parse a resolver URL and build the appropriate transport. + pub async fn from_url(url: &str, trace: Trace) -> Result { + let u = Url::parse(url) + .map_err(|e| OoniError::Unknown(format!("invalid resolver URL: {e}")))?; + + let host = u + .host_str() + .ok_or_else(|| OoniError::Unknown("URL has no host".into()))? + .to_owned(); + + let default_port = match u.scheme() { + "udp" => 53, + "dot" | "tls" => 853, + "https" | "doh" => 443, + other => return Err(OoniError::Unknown(format!("unsupported scheme: {other}"))), + }; + let port = u.port().unwrap_or(default_port); + + let server = resolve_to_socket_addr(&host, port).await?; + + match u.scheme() { + "udp" => Ok(Self::udp(server, trace)), + "dot" | "tls" => Ok(Self::dot(server, &host, trace)), + "https" | "doh" => Ok(Self::doh(server, &host, url, trace)), + other => Err(OoniError::Unknown(format!("unsupported scheme: {other}"))), + } + } + + /// Resolve `hostname`, issuing both A and AAAA queries. + /// Records all events into the trace. + pub async fn lookup_host(&self, hostname: &str) -> Result, OoniError> { + let tx_id = self.trace.next_transaction_id(); + let mut addrs = Vec::new(); + addrs.extend( + self.do_lookup(hostname, RecordType::A, tx_id) + .await + .unwrap_or_default(), + ); + addrs.extend( + self.do_lookup(hostname, RecordType::AAAA, tx_id) + .await + .unwrap_or_default(), + ); + if addrs.is_empty() { + Err(OoniError::DnsNoAnswer) + } else { + Ok(addrs) + } + } + + async fn do_lookup( + &self, + hostname: &str, + qtype: RecordType, + tx_id: i64, + ) -> Result, OoniError> { + let t0 = self.trace.elapsed_secs(); + + // Perform the lookup via whichever transport is configured. + let outcome: Result<(Vec, i64), OoniError> = match &self.transport { + DnsTransport::System => self.system_lookup(hostname, qtype).await, + transport => { + let (_id, wire) = build_query(hostname, qtype)?; + transport + .exchange(&wire, &self.trace, tx_id) + .await + .and_then(|resp| parse_response(&resp, qtype)) + } + }; + + let t = self.trace.elapsed_secs(); + let qtype_str = match qtype { + RecordType::A => "A", + RecordType::AAAA => "AAAA", + _ => "UNKNOWN", + }; + + let (answers, rcode, ips, failure) = match outcome { + Ok((ans, rc)) => { + let ips = answers_to_ips(&ans); + (ans, Some(rc), ips, None) + } + Err(ref e) => (vec![], None, vec![], Some(e.failure().0.clone())), + }; + + // Always record one DnsLookupResult per query, even on failure. + self.trace.record_dns_lookup(DnsLookupResult { + answers, + engine: self.transport.engine().to_owned(), + failure: failure.clone(), + hostname: hostname.to_owned(), + query_type: qtype_str.to_owned(), + resolver_address: self.transport.resolver_address(), + resolver_hostname: None, + resolver_port: None, + raw_response: None, + rcode, + t0, + t, + tags: None, + transaction_id: Some(tx_id), + }); + + if failure.is_some() { + Err(OoniError::DnsNoAnswer) + } else { + Ok(ips) + } + } + + /// Issue a single A query. + pub async fn lookup_a(&self, hostname: &str) -> Result, OoniError> { + let tx_id = self.trace.next_transaction_id(); + self.do_lookup(hostname, RecordType::A, tx_id).await + } + + /// Issue a single AAAA query. + pub async fn lookup_aaaa(&self, hostname: &str) -> Result, OoniError> { + let tx_id = self.trace.next_transaction_id(); + self.do_lookup(hostname, RecordType::AAAA, tx_id).await + } + + /// System getaddrinfo via hickory resolver + async fn system_lookup( + &self, + hostname: &str, + qtype: RecordType, + ) -> Result<(Vec, i64), OoniError> { + use hickory_resolver::TokioResolver; + + let r = TokioResolver::builder_tokio() + .map_err(|e| OoniError::DnsUnexpected(format!("failed to create resolver: {e}")))? + .build(); + + let result: Result, _> = if qtype == RecordType::AAAA { + r.ipv6_lookup(hostname) + .await + .map(|l| l.iter().map(|a| IpAddr::V6(a.0)).collect()) + } else { + r.ipv4_lookup(hostname) + .await + .map(|l| l.iter().map(|a| IpAddr::V4(a.0)).collect()) + }; + + match result { + Ok(ips) => { + let answers = ips + .iter() + .map(|ip| match ip { + IpAddr::V4(v4) => DnsAnswer { + answer_type: "A".into(), + ipv4: Some(v4.to_string()), + ..Default::default() + }, + IpAddr::V6(v6) => DnsAnswer { + answer_type: "AAAA".into(), + ipv6: Some(v6.to_string()), + ..Default::default() + }, + }) + .collect(); + Ok((answers, 0)) + } + Err(e) => Err(classify_hickory(&e)), + } + } +} + +/// Resolve `host:port` to a `SocketAddr` using the OS resolver. +/// Returns the first address found. +async fn resolve_to_socket_addr(host: &str, port: u16) -> Result { + let target = format!("{}:{}", host, port); + // Try parsing as an IP literal first (no lookup needed). + if let Ok(addr) = target.parse::() { + return Ok(addr); + } + + // Fall back to OS resolution. + tokio::net::lookup_host(target) + .await + .map_err(|e| OoniError::DnsUnexpected(e.to_string()))? + .next() + .ok_or(OoniError::DnsNoAnswer) +} diff --git a/ooniprobe-network/src/errors.rs b/ooniprobe-network/src/errors.rs new file mode 100644 index 0000000..8c202e1 --- /dev/null +++ b/ooniprobe-network/src/errors.rs @@ -0,0 +1,132 @@ +//! OONI error types. +//! +//! https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md + +use serde::{Deserialize, Serialize}; +use std::io::ErrorKind; +use thiserror::Error; + +/// Canonical OONI failure string. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FailureString(pub String); + +impl FailureString { + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for FailureString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// All errors that `ooniprobe-network` can produce. +#[derive(Debug, Error)] +pub enum OoniError { + // DNS + #[error("dns_nxdomain_error")] + DnsNxDomain, + #[error("dns_no_answer")] + DnsNoAnswer, + #[error("dns_refused_error")] + DnsRefused, + #[error("dns_servfail_error")] + DnsServFail, + #[error("dns_lookup_timeout_error")] + DnsTimeout, + #[error("dns_bogon_error")] + DnsBogon, + #[error("dns_temporary_failure")] + DnsTemporaryFailure, + #[error("dns_unexpected_failure: {0}")] + DnsUnexpected(String), + + // TCP + #[error("connection_refused")] + ConnectionRefused, + #[error("connection_reset")] + ConnectionReset, + #[error("generic_timeout_error")] + GenericTimeout, + #[error("network_unreachable")] + NetworkUnreachable, + #[error("host_unreachable")] + HostUnreachable, + #[error("connection_already_closed")] + ConnectionAlreadyClosed, + + // TLS + #[error("ssl_invalid_certificate")] + TlsInvalidCertificate, + #[error("ssl_unknown_authority")] + TlsUnknownAuthority, + #[error("ssl_invalid_hostname")] + TlsInvalidHostname, + #[error("ssl_handshake_timeout")] + TlsHandshakeTimeout, + #[error("ssl_failed_handshake")] + TlsFailedHandshake, + #[error("eof_error")] + Eof, + + // HTTP + #[error("http_request_failed")] + HttpRequestFailed, + #[error("http_invalid_redirect_location_host")] + HttpInvalidRedirectLocationHost, + #[error("http_unexpected_redirect_url")] + HttpUnexpectedRedirectUrl, + #[error("http_unexpected_status_code")] + HttpUnexpectedStatusCode, + + // QUIC + #[error("quic_incompatible_version")] + QuicIncompatibleVersion, + + // Generic + #[error("unknown_failure: {0}")] + Unknown(String), + #[error("internal: {0}")] + Internal(String), +} + +impl OoniError { + /// Return the canonical OONI failure string for this error. + pub fn failure(&self) -> FailureString { + FailureString(self.to_string()) + } + + /// Wrap an arbitrary `std::io::Error` into the most specific `OoniError`. + pub fn from_io(e: std::io::Error) -> Self { + match e.kind() { + ErrorKind::ConnectionRefused => Self::ConnectionRefused, + ErrorKind::ConnectionReset => Self::ConnectionReset, + ErrorKind::TimedOut | ErrorKind::WouldBlock => Self::GenericTimeout, + ErrorKind::NetworkUnreachable => Self::NetworkUnreachable, + ErrorKind::HostUnreachable => Self::HostUnreachable, + ErrorKind::UnexpectedEof => Self::Eof, + ErrorKind::BrokenPipe => Self::ConnectionAlreadyClosed, + _ => Self::Unknown(e.to_string()), + } + } + + /// Classify a `rustls::Error` into an OONI failure string. + pub fn from_tls(e: rustls::Error) -> Self { + match &e { + rustls::Error::InvalidCertificate(_) => Self::TlsInvalidCertificate, + rustls::Error::NoCertificatesPresented => Self::TlsUnknownAuthority, + rustls::Error::General(s) if s.contains("hostname") => Self::TlsInvalidHostname, + _ => Self::TlsFailedHandshake, + } + } +} + +pub fn failure_to_option(e: &Option) -> Option { + e.as_ref().map(|err| err.failure().0) +} diff --git a/ooniprobe-network/src/http.rs b/ooniprobe-network/src/http.rs new file mode 100644 index 0000000..4a1239e --- /dev/null +++ b/ooniprobe-network/src/http.rs @@ -0,0 +1,323 @@ +//! HTTP transport with tracing. +//! +//! [`TracingHttpClient`] sends HTTP/1.1 or HTTP/2 requests and records +//! the full request/response cycle as an [`HttpTransaction`] + +use std::collections::HashMap; + +use bytes::Bytes; +use http::{Method, Request, Response, Uri}; +use http_body_util::{BodyExt, Full}; +use hyper::body::Incoming; +use hyper_util::rt::TokioIo; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::{ + archival::{HttpRequest, HttpResponse, HttpTransaction, MaybeBinaryData}, + errors::OoniError, + trace::Trace, +}; + +const MAX_BODY: usize = 524_288; + +/// collect_headers +fn collect_headers(map: &http::HeaderMap) -> (Vec<(String, String)>, HashMap) { + let list: Vec<(String, String)> = map + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_owned())) + .collect(); + let map: HashMap = list.iter().cloned().collect(); + (list, map) +} + +/// HTTP user agent string. +pub fn http_user_agent() -> &'static str { + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.1" +} + +/// Request metadata captured before the body is consumed. +struct RequestMeta { + url: String, + method: String, + headers_list: Vec<(String, String)>, + headers: HashMap, + body: Vec, +} + +impl RequestMeta { + fn capture(req: &Request>) -> Self { + let url = req.uri().to_string(); + let method = req.method().to_string(); + let (hdrs_list, hdrs) = collect_headers(req.headers()); + + let body = req + .body() + .clone() + .collect() + .now_or_never() + .and_then(|r| r.ok()) + .map(|c| c.to_bytes().to_vec()) + .unwrap_or_default(); + Self { + url, + method, + headers_list: hdrs_list, + headers: hdrs, + body, + } + } + + fn to_http_request(&self) -> HttpRequest { + HttpRequest { + body: MaybeBinaryData(self.body.clone()), + body_is_truncated: Some(false), + headers_list: self.headers_list.clone(), + headers: self.headers.clone(), + method: self.method.clone(), + url: self.url.clone(), + x_transport: "tcp".into(), + } + } +} + +/// Collect and optionally truncate the body of a response. +pub async fn read_body(response: Response, max_bytes: usize) -> (Vec, bool) { + match response.into_body().collect().await { + Ok(c) => { + let b = c.to_bytes(); + if b.len() > max_bytes { + (b[..max_bytes].to_vec(), true) + } else { + (b.to_vec(), false) + } + } + Err(_) => (vec![], false), + } +} + +// TracingHttpClient +pub struct TracingHttpClient { + trace: Trace, + max_body_size: usize, +} + +impl TracingHttpClient { + pub fn new(trace: Trace) -> Self { + Self { + trace, + max_body_size: MAX_BODY, + } + } + + pub fn with_max_body_size(mut self, n: usize) -> Self { + self.max_body_size = n; + self + } + + pub async fn send_http1( + &self, + stream: S, + request: Request>, + address: &str, + alpn: &str, + tx_id: i64, + ) -> Result + where + S: AsyncRead + AsyncWrite + Unpin + Send + 'static, + { + let meta = RequestMeta::capture(&request); + let io = TokioIo::new(stream); + let t0 = self.trace.elapsed_secs(); + + let send_result = hyper::client::conn::http1::handshake(io) + .await + .map_err(|_| OoniError::HttpRequestFailed) + .and_then(|(sender, conn)| { + tokio::spawn(async move { + let _ = conn.await; + }); + Ok(sender) + }); + + match send_result { + Ok(mut sender) => { + let resp = sender + .send_request(request) + .await + .map_err(|_| OoniError::HttpRequestFailed); + let t = self.trace.elapsed_secs(); + self.record_result(meta, address, alpn, tx_id, t0, t, resp) + .await + } + Err(e) => { + self.record_failure(&meta, address, alpn, tx_id, t0, &e); + Err(e) + } + } + } + + pub async fn send_http2( + &self, + stream: S, + request: Request>, + address: &str, + alpn: &str, + tx_id: i64, + ) -> Result + where + S: AsyncRead + AsyncWrite + Unpin + Send + 'static, + { + let meta = RequestMeta::capture(&request); + let io = TokioIo::new(stream); + let exec = hyper_util::rt::TokioExecutor::new(); + let t0 = self.trace.elapsed_secs(); + + let send_result = hyper::client::conn::http2::handshake::<_, _, Full>(exec, io) + .await + .map_err(|_| OoniError::HttpRequestFailed) + .and_then(|(sender, conn)| { + tokio::spawn(async move { + let _ = conn.await; + }); + Ok(sender) + }); + + match send_result { + Ok(mut sender) => { + let resp = sender + .send_request(request) + .await + .map_err(|_| OoniError::HttpRequestFailed); + let t = self.trace.elapsed_secs(); + self.record_result(meta, address, alpn, tx_id, t0, t, resp) + .await + } + Err(e) => { + self.record_failure(&meta, address, alpn, tx_id, t0, &e); + Err(e) + } + } + } + + fn record_failure( + &self, + meta: &RequestMeta, + address: &str, + alpn: &str, + tx_id: i64, + t0: f64, + err: &OoniError, + ) { + let t = self.trace.elapsed_secs(); + self.trace.record_http_request(HttpTransaction { + network: Some("tcp".into()), + address: Some(address.to_owned()), + alpn: Some(alpn.to_owned()), + failure: Some(err.failure().0.clone()), + request: meta.to_http_request(), + response: HttpResponse::default(), + t0, + t, + tags: None, + transaction_id: Some(tx_id), + response_length: None, + }); + } + + async fn record_result( + &self, + meta: RequestMeta, + address: &str, + alpn: &str, + tx_id: i64, + t0: f64, + t: f64, + result: Result, OoniError>, + ) -> Result { + match result { + Ok(resp) => { + let status = resp.status().as_u16(); + let (hdrs_list, hdrs) = collect_headers(resp.headers()); + let (body, is_truncated) = read_body(resp, MAX_BODY).await; + let http_response = HttpResponse { + body: MaybeBinaryData(body.clone()), + body_is_truncated: is_truncated, + code: status, + headers_list: hdrs_list, + headers: hdrs, + raw_body: body, + }; + + self.trace.record_http_request(HttpTransaction { + network: Some("tcp".into()), + address: Some(address.to_owned()), + alpn: Some(alpn.to_owned()), + failure: None, + request: meta.to_http_request(), + response: http_response.clone(), + t0, + t, + tags: None, + transaction_id: Some(tx_id), + response_length: None, // TODO: populate this accurately from the content-length header + }); + Ok(http_response) + } + Err(e) => { + self.record_failure(&meta, address, alpn, tx_id, t0, &e); + Err(e) + } + } + } +} + +// Trait to call `collect()` synchronously on `Full` (it's always ready). +trait NowOrNever: std::future::Future + Sized { + fn now_or_never(self) -> Option { + use std::pin::pin; + use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + + fn noop(_: *const ()) {} + fn noop_clone(_: *const ()) -> RawWaker { + make_waker() + } + fn make_waker() -> RawWaker { + static VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop); + RawWaker::new(std::ptr::null(), &VTABLE) + } + let waker = unsafe { Waker::from_raw(make_waker()) }; + let mut cx = Context::from_waker(&waker); + + let mut future = pin!(self); + + match future.as_mut().poll(&mut cx) { + Poll::Ready(v) => Some(v), + Poll::Pending => None, + } + } +} +impl NowOrNever for F {} + +/// Build a simple GET request with OONI's default User-Agent. +pub fn get_request(url: &str) -> Result>, OoniError> { + let uri: Uri = url + .parse() + .map_err(|e| OoniError::Unknown(format!("invalid URL: {e}")))?; + let host = uri + .host() + .ok_or_else(|| OoniError::Unknown("URL has no host".into()))? + .to_owned(); + + Request::builder() + .method(Method::GET) + .uri(uri) + .header("Host", &host) + .header("User-Agent", http_user_agent()) + .header( + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + ) + .header("Accept-Language", "en-US,en;q=0.9") + .body(Full::new(Bytes::new())) + .map_err(|e| OoniError::Unknown(format!("request build error: {e}"))) +} diff --git a/ooniprobe-network/src/lib.rs b/ooniprobe-network/src/lib.rs new file mode 100644 index 0000000..11a82a2 --- /dev/null +++ b/ooniprobe-network/src/lib.rs @@ -0,0 +1,9 @@ +pub mod archival; +pub mod dns; +pub mod errors; +pub mod http; +pub mod tcp; +pub mod tls; +pub mod trace; +pub mod udp; +pub mod utils; diff --git a/ooniprobe-network/src/tcp.rs b/ooniprobe-network/src/tcp.rs new file mode 100644 index 0000000..f6688d6 --- /dev/null +++ b/ooniprobe-network/src/tcp.rs @@ -0,0 +1,259 @@ +//! TCP connection establishment with tracing. +//! +//! [`TracingDialer`] connects to a remote endpoint and emits a +//! [`TcpConnectResult`] into the trace + +use std::net::SocketAddr; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use pin_project::pin_project; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::net::TcpStream; + +use crate::{ + archival::{NetworkEvent, NetworkOperation, TcpConnectResult, TcpConnectStatus}, + errors::OoniError, + trace::Trace, +}; + +/// Connects TCP sockets and records the result in a [`Trace`]. +pub struct TracingDialer { + trace: Trace, +} + +impl TracingDialer { + pub fn new(trace: Trace) -> Self { + Self { trace } + } + + /// Dial `addr`, record the result, return a [`TracingStream`] on success. + pub async fn connect(&self, addr: SocketAddr, tx_id: i64) -> Result { + let t0 = self.trace.elapsed_secs(); + let result = TcpStream::connect(addr).await; + let t = self.trace.elapsed_secs(); + + let (ip, port) = (addr.ip().to_string(), addr.port()); + + match result { + Ok(stream) => { + self.trace.record_tcp_connect(TcpConnectResult { + conn_id: None, + dial_id: None, + ip: ip.clone(), + port, + status: TcpConnectStatus { + failure: None, + success: true, + blocked: None, + }, + t0, + t, + tags: Some(vec![]), + transaction_id: Some(tx_id), + }); + + self.trace.record_network_event(NetworkEvent { + address: Some(addr.to_string()), + conn_id: None, + dial_id: None, + failure: None, + num_bytes: None, + operation: NetworkOperation::Connect, + proto: Some("tcp".into()), + t0, + t, + tags: None, + transaction_id: Some(tx_id), + }); + + Ok(TracingStream::new( + stream, + addr.to_string(), + self.trace.clone(), + tx_id, + )) + } + Err(e) => { + let err = OoniError::from_io(e); + self.trace.record_tcp_connect(TcpConnectResult { + conn_id: None, + dial_id: None, + ip, + port, + status: TcpConnectStatus { + failure: Some(err.failure().0.clone()), + success: false, + blocked: None, + }, + t0, + t, + tags: Some(vec![]), + transaction_id: Some(tx_id), + }); + Err(err) + } + } + } + + /// Convenience: try each address in `addrs` in order, returning the first + /// successful connection + pub async fn connect_first( + &self, + addrs: &[SocketAddr], + tx_id: i64, + ) -> Result { + let mut last_err = OoniError::DnsNoAnswer; + for &addr in addrs { + match self.connect(addr, tx_id).await { + Ok(s) => return Ok(s), + Err(e) => last_err = e, + } + } + Err(last_err) + } +} + +/// A `TcpStream` wrapper that emits [`NetworkEvent`]s for every I/O operation. +#[pin_project] +pub struct TracingStream { + #[pin] + inner: TcpStream, + address: String, + trace: Trace, + tx_id: i64, +} + +impl TracingStream { + pub(crate) fn new(inner: TcpStream, address: String, trace: Trace, tx_id: i64) -> Self { + Self { + inner, + address, + trace, + tx_id, + } + } + + /// Access the underlying `TcpStream` (e.g. to hand it to a TLS layer). + pub fn into_inner(self) -> TcpStream { + self.inner + } + + /// Peer address string `"ip:port"`. + pub fn address(&self) -> &str { + &self.address + } +} + +impl AsyncRead for TracingStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + let before_len = buf.filled().len(); + let t0 = this.trace.elapsed_secs(); + let result = this.inner.poll_read(cx, buf); + let t = this.trace.elapsed_secs(); + let num_bytes = (buf.filled().len() - before_len) as i64; + + let (failure, success) = match &result { + Poll::Ready(Err(e)) => ( + Some( + OoniError::from_io(std::io::Error::new(e.kind(), e.to_string())) + .failure() + .0, + ), + false, + ), + Poll::Ready(Ok(())) if num_bytes == 0 => (Some("eof_error".to_owned()), false), + _ => (None, true), + }; + + if matches!(result, Poll::Ready(_)) { + this.trace.record_network_event(NetworkEvent { + conn_id: None, + dial_id: None, + address: Some(this.address.clone()), + failure, + num_bytes: if success { Some(num_bytes) } else { None }, + operation: NetworkOperation::Read, + proto: Some("tcp".into()), + t0, + t, + tags: None, + transaction_id: Some(*this.tx_id), + }); + } + result + } +} + +impl AsyncWrite for TracingStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + let t0 = this.trace.elapsed_secs(); + let result = this.inner.poll_write(cx, buf); + let t = this.trace.elapsed_secs(); + + if let Poll::Ready(ref r) = result { + let (num_bytes, failure) = match r { + Ok(n) => (Some(*n as i64), None), + Err(e) => ( + None, + Some( + OoniError::from_io(std::io::Error::new(e.kind(), e.to_string())) + .failure() + .0, + ), + ), + }; + this.trace.record_network_event(NetworkEvent { + conn_id: None, + dial_id: None, + address: Some(this.address.clone()), + failure, + num_bytes, + operation: NetworkOperation::Write, + proto: Some("tcp".into()), + t0, + t, + tags: None, + transaction_id: Some(*this.tx_id), + }); + } + result + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().inner.poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + let t0 = this.trace.elapsed_secs(); + let result = this.inner.poll_shutdown(cx); + let t = this.trace.elapsed_secs(); + if matches!(result, Poll::Ready(_)) { + this.trace.record_network_event(NetworkEvent { + conn_id: None, + dial_id: None, + address: Some(this.address.clone()), + failure: None, + num_bytes: None, + operation: NetworkOperation::Close, + proto: Some("tcp".into()), + t0, + t, + tags: None, + transaction_id: Some(*this.tx_id), + }); + } + result + } +} diff --git a/ooniprobe-network/src/tls.rs b/ooniprobe-network/src/tls.rs new file mode 100644 index 0000000..28f922d --- /dev/null +++ b/ooniprobe-network/src/tls.rs @@ -0,0 +1,232 @@ +//! TLS handshake with tracing. +//! +//! [`TracingTlsHandshaker`] performs a TLS 1.2/1.3 handshake over any +//! `AsyncRead + AsyncWrite` stream and emits a [`TlsHandshakeResult`] + +use std::sync::Arc; + +use rustls::{crypto::ring, ClientConfig, RootCertStore}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_rustls::{client::TlsStream, TlsConnector}; + +use crate::{ + archival::{BinaryData, NetworkEvent, NetworkOperation, TlsHandshakeResult, TlsNetwork}, + errors::OoniError, + trace::Trace, +}; + +/// Build a `ClientConfig` with the system root store. +pub fn system_tls_config(alpns: &[&str]) -> Result, OoniError> { + let mut root_store = RootCertStore::empty(); + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + + let mut config = ClientConfig::builder_with_provider(ring::default_provider().into()) + .with_safe_default_protocol_versions() + .map_err(|e| OoniError::Unknown(format!("TLS config error: {e}")))? + .with_root_certificates(root_store) + .with_no_client_auth(); + + config.alpn_protocols = alpns.iter().map(|s| s.as_bytes().to_vec()).collect(); + + Ok(Arc::new(config)) +} + +/// Build a `ClientConfig` that skips certificate verification. +pub fn insecure_tls_config() -> Result, OoniError> { + #[derive(Debug)] + struct NoVerifier; + + impl rustls::client::danger::ServerCertVerifier for NoVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _msg: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _msg: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + rustls::crypto::ring::default_provider() + .signature_verification_algorithms + .supported_schemes() + } + } + + let config = ClientConfig::builder_with_provider(ring::default_provider().into()) + .with_safe_default_protocol_versions() + .map_err(|e| OoniError::Unknown(format!("TLS config error: {e}")))? + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoVerifier)) + .with_no_client_auth(); + + Ok(Arc::new(config)) +} + +// TracingTlsHandshaker + +/// Performs TLS handshakes and records results in a [`Trace`]. +pub struct TracingTlsHandshaker { + config: Arc, + trace: Trace, + no_tls_verify: bool, +} + +impl TracingTlsHandshaker { + /// Create a handshaker using system CA roots. + pub fn new(trace: Trace) -> Result { + Ok(Self { + config: system_tls_config(&["h2", "http/1.1"])?, + trace, + no_tls_verify: false, + }) + } + + /// Create a handshaker that skips certificate validation. + pub fn insecure(trace: Trace) -> Result { + Ok(Self { + config: insecure_tls_config()?, + trace, + no_tls_verify: true, + }) + } + + /// Create a handshaker with a custom [`ClientConfig`]. + pub fn with_config(config: Arc, trace: Trace) -> Self { + Self { + config, + trace, + no_tls_verify: false, + } + } + + /// Perform a TLS handshake over `stream`. + pub async fn handshake( + &self, + stream: S, + server_name: &str, + address: &str, + tx_id: i64, + ) -> Result, OoniError> + where + S: AsyncRead + AsyncWrite + Unpin, + { + let connector = TlsConnector::from(self.config.clone()); + let sni = rustls::pki_types::ServerName::try_from(server_name.to_owned()) + .map_err(|e| OoniError::Unknown(format!("invalid SNI: {e}")))?; + + let t0 = self.trace.elapsed_secs(); + self.trace.record_network_event(NetworkEvent { + address: Some(address.to_string()), + conn_id: None, + dial_id: None, + failure: None, + num_bytes: None, + operation: NetworkOperation::TlsHandshakeStart, + proto: Some("tls".into()), + t0, + t: t0, + tags: None, + transaction_id: Some(tx_id), + }); + + let result = connector.connect(sni, stream).await; + let t = self.trace.elapsed_secs(); + + match result { + Ok(tls_stream) => { + let (_, session) = tls_stream.get_ref(); + let cipher = session + .negotiated_cipher_suite() + .map(|cs| format!("{:?}", cs.suite())) + .unwrap_or_default(); + let alpn = session + .alpn_protocol() + .map(|a| String::from_utf8_lossy(a).into_owned()) + .unwrap_or_default(); + let tls_version = session + .protocol_version() + .map(|v| format!("{:?}", v)) + .unwrap_or_default(); + let peer_certs = session + .peer_certificates() + .unwrap_or_default() + .iter() + .map(|c| BinaryData(c.as_ref().to_vec())) + .collect(); + + self.trace.record_tls_handshake(TlsHandshakeResult { + network: Some(TlsNetwork::Tcp), + address: address.to_owned(), + cipher_suite: cipher, + failure: None, + negotiated_protocol: alpn, + no_tls_verify: self.no_tls_verify, + peer_certificates: peer_certs, + server_name: Some(server_name.to_owned()), + t0, + t, + tags: None, + tls_version, + transaction_id: Some(tx_id), + outer_server_name: None, + ech_config: None, + so_error: None, + }); + + Ok(tls_stream) + } + Err(e) => { + // Try to classify as rustls vs I/O error. + let ooni_err = if let Some(tls_err) = + e.get_ref().and_then(|e| e.downcast_ref::()) + { + OoniError::from_tls(tls_err.clone()) + } else { + OoniError::from_io(e) + }; + + self.trace.record_tls_handshake(TlsHandshakeResult { + network: Some(TlsNetwork::Tcp), + address: address.to_owned(), + cipher_suite: String::new(), + failure: Some(ooni_err.failure().0.clone()), + negotiated_protocol: String::new(), + no_tls_verify: self.no_tls_verify, + peer_certificates: vec![], + server_name: Some(server_name.to_owned()), + t0, + t, + tags: None, + tls_version: String::new(), + transaction_id: Some(tx_id), + outer_server_name: None, + ech_config: None, + so_error: None, + }); + + Err(ooni_err) + } + } + } +} diff --git a/ooniprobe-network/src/trace.rs b/ooniprobe-network/src/trace.rs new file mode 100644 index 0000000..641f987 --- /dev/null +++ b/ooniprobe-network/src/trace.rs @@ -0,0 +1,220 @@ +//! Measurement trace. +//! +//! A [`Trace`] is the central object for a single "step" of a measurement. +//! +//! Every network primitive (DNS, TCP, TLS, HTTP) receives a reference to +//! the trace. When an operation completes (or fails) the primitive calls +//! one of the `record_*` methods, which push an archival event into a +//! bounded channel inside the trace. The experiment then drains those +//! channels at whatever cadence it chooses. + +use std::sync::{ + atomic::{AtomicI64, Ordering}, + Arc, Mutex, +}; +use std::time::Instant; + +use crate::archival::{ + DnsLookupResult, HttpTransaction, NetworkEvent, TcpConnectResult, TlsHandshakeResult, +}; + +// Buffer sizes matching Go's measurexlite constants. +const DNS_LOOKUP_BUF: usize = 8; +const TCP_CONNECT_BUF: usize = 8; +const TLS_HANDSHAKE_BUF: usize = 8; +const NETWORK_EVENT_BUF: usize = 64; +const HTTP_REQUEST_BUF: usize = 8; + +/// TraceInner - Inner shared state +struct TraceInner { + /// Monotonic start instant for relative timing. + zero_time: Instant, + + index: i64, + tx_counter: AtomicI64, + tags: Vec, + + dns_lookups: Mutex>, + tcp_connects: Mutex>, + tls_handshakes: Mutex>, + network_events: Mutex>, + http_requests: Mutex>, +} + +/// A measurement trace. +#[derive(Clone)] +pub struct Trace(Arc); + +impl Trace { + /// Create a new trace with the given index. + /// + /// `index` is used to form transaction IDs and should be unique + /// across all traces within a single measurement run. + pub fn new(index: i64) -> Self { + Self(Arc::new(TraceInner { + zero_time: Instant::now(), + index, + tx_counter: AtomicI64::new(0), + tags: vec![], + dns_lookups: Mutex::new(Vec::with_capacity(DNS_LOOKUP_BUF)), + tcp_connects: Mutex::new(Vec::with_capacity(TCP_CONNECT_BUF)), + tls_handshakes: Mutex::new(Vec::with_capacity(TLS_HANDSHAKE_BUF)), + network_events: Mutex::new(Vec::with_capacity(NETWORK_EVENT_BUF)), + http_requests: Mutex::new(Vec::with_capacity(HTTP_REQUEST_BUF)), + })) + } + + /// Create a new trace with custom tags attached to every observation. + pub fn new_with_tags(index: i64, tags: Vec) -> Self { + Self(Arc::new(TraceInner { + zero_time: Instant::now(), + index, + tx_counter: AtomicI64::new(0), + tags, + dns_lookups: Mutex::new(Vec::with_capacity(DNS_LOOKUP_BUF)), + tcp_connects: Mutex::new(Vec::with_capacity(TCP_CONNECT_BUF)), + tls_handshakes: Mutex::new(Vec::with_capacity(TLS_HANDSHAKE_BUF)), + network_events: Mutex::new(Vec::with_capacity(NETWORK_EVENT_BUF)), + http_requests: Mutex::new(Vec::with_capacity(HTTP_REQUEST_BUF)), + })) + } + + // Timing + + /// Elapsed time since the trace was created, as seconds (for `t0`/`t` fields). + pub fn elapsed_secs(&self) -> f64 { + self.0.zero_time.elapsed().as_secs_f64() + } + + /// Return the `Instant` at which measurement began (zero-time). + pub fn zero_time(&self) -> Instant { + self.0.zero_time + } + + /// Convenience: duration from zero-time to a given instant, in seconds. + pub fn secs_since(&self, t: Instant) -> f64 { + t.saturating_duration_since(self.0.zero_time).as_secs_f64() + } + + // Transaction IDs + + /// Allocate the next unique transaction ID for this trace. + pub fn next_transaction_id(&self) -> i64 { + let n = self.0.tx_counter.fetch_add(1, Ordering::Relaxed); + self.0.index * 10_000 + n + } + + /// Return the trace index. + pub fn index(&self) -> i64 { + self.0.index + } + + /// Tags that are automatically appended to every recorded observation. + pub fn tags(&self) -> &[String] { + &self.0.tags + } + + /// Record a DNS lookup result. + pub fn record_dns_lookup(&self, mut obs: DnsLookupResult) { + obs.tags + .get_or_insert_with(Vec::new) + .extend_from_slice(&self.0.tags); + let mut guard = self.0.dns_lookups.lock().unwrap(); + if guard.len() < DNS_LOOKUP_BUF * 4 { + guard.push(obs); + } + } + + /// Record a TCP connect result. + pub fn record_tcp_connect(&self, mut obs: TcpConnectResult) { + obs.tags + .get_or_insert_with(Vec::new) + .extend_from_slice(&self.0.tags); + let mut guard = self.0.tcp_connects.lock().unwrap(); + if guard.len() < TCP_CONNECT_BUF * 4 { + guard.push(obs); + } + } + + /// Record a TLS (or QUIC) handshake result. + pub fn record_tls_handshake(&self, mut obs: TlsHandshakeResult) { + obs.tags + .get_or_insert_with(Vec::new) + .extend_from_slice(&self.0.tags); + let mut guard = self.0.tls_handshakes.lock().unwrap(); + if guard.len() < TLS_HANDSHAKE_BUF * 4 { + guard.push(obs); + } + } + + /// Record a low-level network I/O event. + pub fn record_network_event(&self, mut obs: NetworkEvent) { + obs.tags + .get_or_insert_with(Vec::new) + .extend_from_slice(&self.0.tags); + let mut guard = self.0.network_events.lock().unwrap(); + if guard.len() < NETWORK_EVENT_BUF * 4 { + guard.push(obs); + } + } + + /// Record an HTTP request/response pair. + pub fn record_http_request(&self, mut obs: HttpTransaction) { + obs.tags + .get_or_insert_with(Vec::new) + .extend_from_slice(&self.0.tags); + let mut guard = self.0.http_requests.lock().unwrap(); + if guard.len() < HTTP_REQUEST_BUF * 4 { + guard.push(obs); + } + } + + // Drain + + pub fn drain_dns_lookups(&self) -> Vec { + let mut guard = self.0.dns_lookups.lock().unwrap(); + std::mem::take(&mut *guard) + } + + pub fn drain_tcp_connects(&self) -> Vec { + let mut guard = self.0.tcp_connects.lock().unwrap(); + std::mem::take(&mut *guard) + } + + pub fn drain_tls_handshakes(&self) -> Vec { + let mut guard = self.0.tls_handshakes.lock().unwrap(); + std::mem::take(&mut *guard) + } + + pub fn drain_network_events(&self) -> Vec { + let mut guard = self.0.network_events.lock().unwrap(); + std::mem::take(&mut *guard) + } + + pub fn drain_http_requests(&self) -> Vec { + let mut guard = self.0.http_requests.lock().unwrap(); + std::mem::take(&mut *guard) + } + + // Introspect (non-draining) + + /// Snapshot of DNS lookups without draining. + pub fn dns_lookups(&self) -> Vec { + self.0.dns_lookups.lock().unwrap().clone() + } + + /// Snapshot of TCP connects without draining. + pub fn tcp_connects(&self) -> Vec { + self.0.tcp_connects.lock().unwrap().clone() + } + + /// Snapshot of TLS handshakes without draining. + pub fn tls_handshakes(&self) -> Vec { + self.0.tls_handshakes.lock().unwrap().clone() + } + + /// Snapshot of network events without draining. + pub fn network_events(&self) -> Vec { + self.0.network_events.lock().unwrap().clone() + } +} diff --git a/ooniprobe-network/src/udp.rs b/ooniprobe-network/src/udp.rs new file mode 100644 index 0000000..b8410b5 --- /dev/null +++ b/ooniprobe-network/src/udp.rs @@ -0,0 +1,129 @@ +//! UDP socket with tracing. +//! +//! [`TracingUdpSocket`] wraps `tokio::net::UdpSocket` and records every +//! `send`/`recv` as a [`NetworkEvent`] in the trace, matching the same +//! style as [`TracingStream`] in `tcp.rs`. + +use std::net::SocketAddr; +use std::time::Duration; + +use tokio::net::UdpSocket; + +use crate::{ + archival::{NetworkEvent, NetworkOperation}, + errors::OoniError, + trace::Trace, +}; + +/// A UDP socket that records every send/recv as a [`NetworkEvent`]. +pub struct TracingUdpSocket { + inner: UdpSocket, + address: String, + trace: Trace, +} + +impl TracingUdpSocket { + pub async fn connect(server: SocketAddr, trace: Trace) -> Result { + let bind_addr = if server.is_ipv6() { + ":::0" + } else { + "0.0.0.0:0" + }; + let sock = UdpSocket::bind(bind_addr) + .await + .map_err(OoniError::from_io)?; + + sock.connect(server).await.map_err(OoniError::from_io)?; + Ok(Self { + inner: sock, + address: server.to_string(), + trace, + }) + } + + /// Send `data`, wait for a response (bounded by `timeout`), return raw bytes. + pub async fn exchange( + &self, + data: &[u8], + timeout: Duration, + tx_id: i64, + ) -> Result, OoniError> { + self.record_send(data, tx_id).await?; + tokio::time::timeout(timeout, self.record_recv(tx_id)) + .await + .map_err(|_| OoniError::GenericTimeout)? + } + + async fn record_send(&self, data: &[u8], tx_id: i64) -> Result<(), OoniError> { + let t0 = self.trace.elapsed_secs(); + let res = self.inner.send(data).await; + let t = self.trace.elapsed_secs(); + + let (num_bytes, failure) = match &res { + Ok(n) => (Some(*n as i64), None), // usize → i64: safe since UDP payload < 65535 bytes + Err(e) => ( + None, + Some( + OoniError::from_io(std::io::Error::new(e.kind(), e.to_string())) + .failure() + .0, + ), + ), + }; + + self.trace.record_network_event(NetworkEvent { + conn_id: None, + dial_id: None, + address: Some(self.address.clone()), + failure: failure, + num_bytes: num_bytes, + operation: NetworkOperation::Write, + proto: Some("udp".into()), + t0, + t, + tags: None, + transaction_id: Some(tx_id), + }); + + res.map(|_| ()).map_err(OoniError::from_io) + } + + async fn record_recv(&self, tx_id: i64) -> Result, OoniError> { + let t0 = self.trace.elapsed_secs(); + let mut buf = vec![0u8; 4096]; + let res = self.inner.recv(&mut buf).await; + let t = self.trace.elapsed_secs(); + + let (num_bytes, failure) = match &res { + Ok(n) => (Some(*n as i64), None), // usize → i64: safe since UDP payload < 65535 bytes + Err(e) => ( + None, + Some( + OoniError::from_io(std::io::Error::new(e.kind(), e.to_string())) + .failure() + .0, + ), + ), + }; + + self.trace.record_network_event(NetworkEvent { + conn_id: None, + dial_id: None, + address: Some(self.address.clone()), + failure: failure, + num_bytes: num_bytes, + operation: NetworkOperation::Read, + proto: Some("udp".into()), + t0, + t, + tags: None, + transaction_id: Some(tx_id), + }); + + res.map(|n| { + buf.truncate(n); + buf + }) + .map_err(OoniError::from_io) + } +} diff --git a/ooniprobe-network/src/utils.rs b/ooniprobe-network/src/utils.rs new file mode 100644 index 0000000..c88ad18 --- /dev/null +++ b/ooniprobe-network/src/utils.rs @@ -0,0 +1,10 @@ +use base64::prelude::BASE64_STANDARD; +use base64::{DecodeError, Engine}; + +pub fn b64_encode(b: &[u8]) -> String { + BASE64_STANDARD.encode(b) +} + +pub fn b64_decode(s: &str) -> Result, DecodeError> { + BASE64_STANDARD.decode(s).map_err(Into::into) +} diff --git a/ooniprobe-services/src/client/wreq_impl.rs b/ooniprobe-services/src/client/wreq_impl.rs index 06c4b71..6e3dbcf 100644 --- a/ooniprobe-services/src/client/wreq_impl.rs +++ b/ooniprobe-services/src/client/wreq_impl.rs @@ -4,7 +4,7 @@ use mime::Mime; use std::sync::Arc; use std::time::Duration; use tokio::runtime::Runtime; -use wreq::tls::CertStore; +use wreq::tls::trust::CertStore; use wreq_util::Emulation; use super::{b64_encode, ClientOptions, Error, Response}; @@ -124,7 +124,7 @@ impl ClientBuilder { pub fn build(self) -> Result { let mut client_builder = wreq::Client::builder() - .cert_store(CertStore::from_der_certs( + .tls_cert_store(CertStore::from_der_certs( webpki_root_certs::TLS_SERVER_ROOT_CERTS, )?) .emulation(Emulation::Chrome118);