diff --git a/Cargo.lock b/Cargo.lock index 6c00510f..c38a2b7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,7 +32,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.13", + "cpufeatures", ] [[package]] @@ -243,7 +243,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures 0.2.13", + "cpufeatures", "password-hash", ] @@ -502,18 +502,6 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" -[[package]] -name = "attohttpc" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" -dependencies = [ - "base64 0.22.1", - "http", - "log", - "url", -] - [[package]] name = "auto_impl" version = "1.2.1" @@ -1228,18 +1216,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.13", -] - -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", + "cpufeatures", ] [[package]] @@ -1249,7 +1226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20 0.9.1", + "chacha20", "cipher", "poly1305", "zeroize", @@ -1609,15 +1586,6 @@ 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 = "crc" version = "3.2.1" @@ -1838,7 +1806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures 0.2.13", + "cpufeatures", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -2338,9 +2306,11 @@ dependencies = [ "hickory-server", "hkdf", "http", + "http-body-util", "humansize", "humantime-serde", - "igd-next", + "hyper", + "hyper-util", "indoc", "kcp-sys", "log", @@ -2406,6 +2376,7 @@ dependencies = [ "windows-service", "winreg 0.52.0", "x25519-dalek", + "xmltree", "zerocopy 0.7.35", ] @@ -3501,25 +3472,11 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 5.2.0", + "r-efi", "wasi 0.14.2+wasi-0.2.4", "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", - "rand_core 0.10.1", - "wasip2", - "wasip3", -] - [[package]] name = "ghash" version = "0.5.1" @@ -4349,12 +4306,6 @@ dependencies = [ "syn 2.0.117", ] -[[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" @@ -4392,26 +4343,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "igd-next" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac9a3c8278f43b4cd8463380f4a25653ac843e5b177e1d3eaf849cc9ba10d4d" -dependencies = [ - "attohttpc", - "bytes", - "futures", - "http", - "http-body-util", - "hyper", - "hyper-util", - "log", - "rand 0.10.1", - "tokio", - "url", - "xmltree", -] - [[package]] name = "ignore" version = "0.4.22" @@ -4840,12 +4771,6 @@ dependencies = [ "spin", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libappindicator" version = "0.9.0" @@ -6690,7 +6615,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures 0.2.13", + "cpufeatures", "opaque-debug", "universal-hash", ] @@ -6702,7 +6627,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures 0.2.13", + "cpufeatures", "opaque-debug", "universal-hash", ] @@ -7157,12 +7082,6 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" -[[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" @@ -7214,17 +7133,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20 0.10.0", - "getrandom 0.4.2", - "rand_core 0.10.1", -] - [[package]] name = "rand_chacha" version = "0.2.2" @@ -7282,12 +7190,6 @@ dependencies = [ "getrandom 0.3.2", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - [[package]] name = "rand_distr" version = "0.2.2" @@ -8588,7 +8490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.13", + "cpufeatures", "digest", ] @@ -8599,7 +8501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", - "cpufeatures 0.2.13", + "cpufeatures", "digest", ] @@ -10872,24 +10774,6 @@ dependencies = [ "wit-bindgen-rt", ] -[[package]] -name = "wasip2" -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 = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - [[package]] name = "wasite" version = "0.1.0" @@ -10952,28 +10836,6 @@ 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 = "wasm-streams" version = "0.5.0" @@ -10987,18 +10849,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.8.0", - "hashbrown 0.15.3", - "indexmap 2.14.0", - "semver", -] - [[package]] name = "wayland-backend" version = "0.3.15" @@ -12019,32 +11869,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[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 0.5.0", - "wit-parser", -] - [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -12054,74 +11878,6 @@ dependencies = [ "bitflags 2.8.0", ] -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "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 2.8.0", - "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 = "wl-clipboard-rs" version = "0.9.3" diff --git a/easytier-contrib/easytier-ohrs/Cargo.lock b/easytier-contrib/easytier-ohrs/Cargo.lock index 5d868035..178f33bf 100644 --- a/easytier-contrib/easytier-ohrs/Cargo.lock +++ b/easytier-contrib/easytier-ohrs/Cargo.lock @@ -26,7 +26,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpufeatures", ] [[package]] @@ -224,24 +224,22 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomic-write-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb1e2c1d58618bea806ccca5bbe65dc4e868be16f69ff118a39049389687548" +dependencies = [ + "nix 0.29.0", + "rand 0.8.5", +] + [[package]] name = "atomic_refcell" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" -[[package]] -name = "attohttpc" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" -dependencies = [ - "base64 0.22.1", - "http", - "log", - "url", -] - [[package]] name = "auto_impl" version = "1.3.0" @@ -495,18 +493,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", + "cpufeatures", ] [[package]] @@ -516,7 +503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20 0.9.1", + "chacha20", "cipher", "poly1305", "zeroize", @@ -722,15 +709,6 @@ 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 = "crc" version = "3.3.0" @@ -860,7 +838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "curve25519-dalek-derive", "fiat-crypto", "rustc_version", @@ -1132,6 +1110,7 @@ dependencies = [ "async-recursion", "async-trait", "atomic-shim", + "atomic-write-file", "atomic_refcell", "auto_impl", "base64 0.22.1", @@ -1163,10 +1142,13 @@ dependencies = [ "hickory-proto", "hickory-resolver", "hickory-server", + "hkdf", "http", + "http-body-util", "humansize", "humantime-serde", - "igd-next", + "hyper", + "hyper-util", "indoc", "kcp-sys", "log", @@ -1195,6 +1177,7 @@ dependencies = [ "serde", "serde_json", "service-manager", + "sha2", "shellexpand", "smoltcp", "socket2 0.5.10", @@ -1219,6 +1202,7 @@ dependencies = [ "windows 0.62.2", "windows-service", "winreg 0.52.0", + "xmltree", "zerocopy 0.7.35", ] @@ -1791,25 +1775,11 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 5.3.0", + "r-efi", "wasi 0.14.7+wasi-0.2.4", "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", - "rand_core 0.10.1", - "wasip2", - "wasip3", -] - [[package]] name = "ghash" version = "0.5.1" @@ -2075,6 +2045,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2348,12 +2327,6 @@ 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" @@ -2381,26 +2354,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "igd-next" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac9a3c8278f43b4cd8463380f4a25653ac843e5b177e1d3eaf849cc9ba10d4d" -dependencies = [ - "attohttpc", - "bytes", - "futures", - "http", - "http-body-util", - "hyper", - "hyper-util", - "log", - "rand 0.10.1", - "tokio", - "url", - "xmltree", -] - [[package]] name = "ignore" version = "0.4.23" @@ -2425,8 +2378,6 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", - "serde", - "serde_core", ] [[package]] @@ -2618,12 +2569,6 @@ 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 = "libbz2-rs-sys" version = "0.2.2" @@ -3382,7 +3327,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures 0.2.17", + "cpufeatures", "opaque-debug", "universal-hash", ] @@ -3394,7 +3339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "opaque-debug", "universal-hash", ] @@ -3777,12 +3722,6 @@ 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 = "radix_trie" version = "0.2.1" @@ -3814,17 +3753,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20 0.10.0", - "getrandom 0.4.2", - "rand_core 0.10.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -3863,12 +3791,6 @@ dependencies = [ "getrandom 0.3.3", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - [[package]] name = "raw-cpuid" version = "11.6.0" @@ -4384,7 +4306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -4395,7 +4317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -5358,16 +5280,7 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen 0.46.0", -] - -[[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 = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -5442,40 +5355,6 @@ 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", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.9.4", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" version = "0.3.80" @@ -6183,94 +6062,6 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[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-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "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 0.5.0", - "indexmap", - "prettyplease", - "syn 2.0.106", - "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.106", - "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 2.9.4", - "indexmap", - "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", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.1" diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 99860c01..22c89a04 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -223,8 +223,13 @@ shellexpand = "3.1.1" # for fake tcp flume = { version = "0.12", optional = true } -igd-next = { version = "0.17.0", features = ["aio_tokio"], optional = true } + +# for upnp +http-body-util = { version = "0.1", optional = true } +hyper = { version = "1", default-features = false, features = ["client", "http1"], optional = true } +hyper-util = { version = "0.1", default-features = false, features = ["tokio"], optional = true } natpmp = { version = "0.5.0", optional = true } +xmltree = { version = "0.10", optional = true } [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies] machine-uid = "0.5.3" @@ -399,7 +404,13 @@ faketcp = [ "easytier-proto/faketcp", ] zstd = ["easytier-core/zstd", "easytier-proto/zstd"] -upnp = ["dep:igd-next", "dep:natpmp"] +upnp = [ + "dep:http-body-util", + "dep:hyper", + "dep:hyper-util", + "dep:natpmp", + "dep:xmltree", +] endpoint-discovery = [ "dns-resolver", "easytier-core/endpoint-discovery", diff --git a/easytier/src/common/upnp.rs b/easytier/src/common/upnp.rs index ff0aa71e..de245d53 100644 --- a/easytier/src/common/upnp.rs +++ b/easytier/src/common/upnp.rs @@ -10,17 +10,14 @@ use easytier_core::connectivity::hole_punch::port_mapping::{ ActiveUdpPortMapping as CoreActiveUdpPortMapping, UdpPortMappingAttemptError, UdpPortMappingBackend, UdpPortMappingLifecycle, }; -use igd_next::{ - AddAnyPortError, PortMappingProtocol, SearchOptions, - aio::{ - Gateway, - tokio::{Tokio, search_gateway}, - }, -}; use natpmp::{ Protocol as NatPmpProtocol, Response as NatPmpResponse, new_tokio_natpmp, new_tokio_natpmp_with, }; +use crate::igd_next::{ + AddAnyPortError, Gateway, PortMappingProtocol, SearchOptions, search_gateway, +}; + use super::netns::NetNS; const UPNP_SEARCH_TIMEOUT: Duration = Duration::from_secs(1); @@ -29,11 +26,9 @@ const NAT_PMP_RESPONSE_TIMEOUT: Duration = Duration::from_secs(1); const UPNP_LEASE_DURATION_SECS: u32 = 300; const UPNP_DESCRIPTION: &str = "EasyTier udp hole punch"; -type TokioGateway = Gateway; - enum PortMappingBackend { NatPmp { gateway: Ipv4Addr }, - Igd { gateway: TokioGateway }, + Igd { gateway: Gateway }, } struct ActiveUdpPortMapping { @@ -95,7 +90,7 @@ impl ActiveUdpPortMapping { async fn discover_igd_gateway( net_ns: &NetNS, local_listener: &url::Url, - ) -> anyhow::Result<(TokioGateway, SocketAddr)> { + ) -> anyhow::Result<(Gateway, SocketAddr)> { let _g = net_ns.guard(); let gateway = search_gateway(SearchOptions { timeout: Some(UPNP_SEARCH_TIMEOUT), @@ -111,7 +106,7 @@ impl ActiveUdpPortMapping { async fn establish_via_igd( local_listener: &url::Url, - gateway: TokioGateway, + gateway: Gateway, local_addr: SocketAddr, ) -> anyhow::Result { let gateway_external_port = add_udp_mapping_port_igd(&gateway, local_addr, local_listener) @@ -252,7 +247,7 @@ pub(crate) fn spawn_udp_port_mapping_lifecycle( async fn discover_igd_gateway_in_netns( net_ns: NetNS, local_listener: url::Url, -) -> anyhow::Result<(TokioGateway, SocketAddr)> { +) -> anyhow::Result<(Gateway, SocketAddr)> { if !should_run_port_mapping_in_dedicated_thread(&net_ns) { return ActiveUdpPortMapping::discover_igd_gateway(&net_ns, &local_listener).await; } @@ -275,7 +270,7 @@ async fn discover_igd_gateway_in_netns( async fn establish_igd_mapping_in_netns( net_ns: NetNS, local_listener: url::Url, - gateway: TokioGateway, + gateway: Gateway, local_addr: SocketAddr, ) -> anyhow::Result { if !should_run_port_mapping_in_dedicated_thread(&net_ns) { @@ -352,13 +347,13 @@ fn should_run_port_mapping_in_dedicated_thread(net_ns: &NetNS) -> bool { } async fn add_udp_mapping_port_igd( - gateway: &TokioGateway, + gateway: &Gateway, local_addr: SocketAddr, local_listener: &url::Url, ) -> anyhow::Result { match gateway .add_any_port( - PortMappingProtocol::UDP, + PortMappingProtocol::Udp, local_addr, UPNP_LEASE_DURATION_SECS, UPNP_DESCRIPTION, @@ -377,7 +372,7 @@ async fn add_udp_mapping_port_igd( gateway .add_port( - PortMappingProtocol::UDP, + PortMappingProtocol::Udp, local_addr.port(), local_addr, UPNP_LEASE_DURATION_SECS, @@ -533,14 +528,14 @@ async fn resolve_internal_addr( } async fn renew_udp_mapping_igd( - gateway: &TokioGateway, + gateway: &Gateway, local_addr: SocketAddr, external_port: u16, local_listener: &url::Url, ) -> anyhow::Result<()> { gateway .add_port( - PortMappingProtocol::UDP, + PortMappingProtocol::Udp, external_port, local_addr, UPNP_LEASE_DURATION_SECS, @@ -551,12 +546,12 @@ async fn renew_udp_mapping_igd( } async fn remove_udp_mapping_igd( - gateway: &TokioGateway, + gateway: &Gateway, external_port: u16, local_listener: &url::Url, ) -> anyhow::Result<()> { gateway - .remove_port(PortMappingProtocol::UDP, external_port) + .remove_port(PortMappingProtocol::Udp, external_port) .await .with_context(|| format!("remove udp port mapping {local_listener}")) } diff --git a/easytier/src/igd_next/LICENSE b/easytier/src/igd_next/LICENSE new file mode 100644 index 00000000..68a7db00 --- /dev/null +++ b/easytier/src/igd_next/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Simon Bernier St-Pierre + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/easytier/src/igd_next/errors.rs b/easytier/src/igd_next/errors.rs new file mode 100644 index 00000000..1bffe0f2 --- /dev/null +++ b/easytier/src/igd_next/errors.rs @@ -0,0 +1,126 @@ +use std::{io, string::FromUtf8Error}; + +use easytier_core::tunnel::TunnelError; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum HttpTransportError { + #[error("socket error: {0}")] + Socket(#[from] TunnelError), + #[error("HTTP request error: {0}")] + Http(#[from] hyper::http::Error), + #[error("HTTP connection error: {0}")] + Hyper(#[from] hyper::Error), + #[error("HTTP response is not UTF-8: {0}")] + Utf8(#[from] FromUtf8Error), + #[error("HTTP request timed out")] + TimedOut, + #[error("invalid HTTP response: {0}")] + InvalidResponse(String), +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum RequestError { + #[error(transparent)] + Http(#[from] HttpTransportError), + #[error("invalid response from gateway: {0}")] + InvalidResponse(String), + #[error("gateway response error {0}: {1}")] + ErrorCode(u16, String), + #[error("gateway does not support action: {0}")] + UnsupportedAction(String), +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum SearchError { + #[error("invalid response")] + InvalidResponse, + #[error("no response within timeout")] + NoResponseWithinTimeout, + #[error("I/O error: {0}")] + Io(#[from] io::Error), + #[error("UTF-8 error: {0}")] + Utf8(#[from] std::str::Utf8Error), + #[error("XML error: {0}")] + Xml(#[from] xmltree::ParseError), + #[error(transparent)] + Http(#[from] HttpTransportError), +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum AddAnyPortError { + #[error("the client is not authorized to map this port")] + ActionNotAuthorized, + #[error("cannot add a mapping for local port 0")] + InternalPortZeroInvalid, + #[error("the gateway does not have any free ports")] + NoPortsAvailable, + #[error("the required same-numbered external port is in use")] + ExternalPortInUse, + #[error("the gateway only supports permanent leases")] + OnlyPermanentLeasesSupported, + #[error("the description was too long for the gateway")] + DescriptionTooLong, + #[error("request error: {0}")] + RequestError(#[from] RequestError), +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum AddPortError { + #[error("the client is not authorized to map this port")] + ActionNotAuthorized, + #[error("cannot add a mapping for local port 0")] + InternalPortZeroInvalid, + #[error("external port 0 is invalid")] + ExternalPortZeroInvalid, + #[error("the requested port is in use")] + PortInUse, + #[error("the gateway requires matching internal and external ports")] + SamePortValuesRequired, + #[error("the gateway only supports permanent leases")] + OnlyPermanentLeasesSupported, + #[error("the description was too long for the gateway")] + DescriptionTooLong, + #[error("request error: {0}")] + RequestError(#[source] RequestError), +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum RemovePortError { + #[error("the client is not authorized to remove the port")] + ActionNotAuthorized, + #[error("the port was not mapped")] + NoSuchPortMapping, + #[error("request error: {0}")] + RequestError(#[source] RequestError), +} + +#[derive(Debug, thiserror::Error)] +#[cfg(test)] +pub(crate) enum GetExternalIpError { + #[error("the client is not authorized to get the external IP address")] + ActionNotAuthorized, + #[error("request error: {0}")] + RequestError(#[source] RequestError), +} + +#[derive(Debug, thiserror::Error)] +#[cfg(test)] +pub(crate) enum GetGenericPortMappingEntryError { + #[error("the client is not authorized to look up port mappings")] + ActionNotAuthorized, + #[error("the provided mapping index is invalid")] + SpecifiedArrayIndexInvalid, + #[error("request error: {0}")] + RequestError(#[source] RequestError), +} + +#[cfg(test)] +impl From for GetGenericPortMappingEntryError { + fn from(error: RequestError) -> Self { + match error { + RequestError::ErrorCode(606, _) => Self::ActionNotAuthorized, + RequestError::ErrorCode(713, _) => Self::SpecifiedArrayIndexInvalid, + other => Self::RequestError(other), + } + } +} diff --git a/easytier/src/igd_next/messages.rs b/easytier/src/igd_next/messages.rs new file mode 100644 index 00000000..8e019f53 --- /dev/null +++ b/easytier/src/igd_next/messages.rs @@ -0,0 +1,218 @@ +use std::net::SocketAddr; + +use super::PortMappingProtocol; + +pub(super) const SEARCH_REQUEST: &str = "M-SEARCH * HTTP/1.1\r +Host:239.255.255.250:1900\r +ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r +Man:\"ssdp:discover\"\r +MX:3\r\n\r\n"; + +#[cfg(test)] +pub(super) const GET_EXTERNAL_IP_ACTION: &str = "GetExternalIPAddress"; +pub(super) const ADD_ANY_PORT_MAPPING_ACTION: &str = "AddAnyPortMapping"; +pub(super) const ADD_PORT_MAPPING_ACTION: &str = "AddPortMapping"; +pub(super) const DELETE_PORT_MAPPING_ACTION: &str = "DeletePortMapping"; +#[cfg(test)] +pub(super) const GET_GENERIC_PORT_MAPPING_ENTRY_ACTION: &str = "GetGenericPortMappingEntry"; + +const MESSAGE_HEAD: &str = r#" + +"#; +const MESSAGE_TAIL: &str = r#" +"#; + +pub(super) fn soap_action(service_type: &str, action: &str) -> String { + format!("\"{service_type}#{action}\"") +} + +fn format_message(body: String) -> String { + format!("{MESSAGE_HEAD}{body}{MESSAGE_TAIL}") +} + +fn xml_escape(input: &str) -> String { + let mut output = String::with_capacity(input.len()); + for character in input.chars() { + match character { + '&' => output.push_str("&"), + '<' => output.push_str("<"), + '>' => output.push_str(">"), + '"' => output.push_str("""), + '\'' => output.push_str("'"), + _ => output.push(character), + } + } + output +} + +#[cfg(test)] +pub(super) fn format_get_external_ip_message(service_type: &str) -> String { + format_message(format!( + r#" + "# + )) +} + +pub(super) fn format_add_any_port_mapping_message( + service_type: &str, + schema: &[String], + protocol: PortMappingProtocol, + external_port: u16, + local_addr: SocketAddr, + lease_duration: u32, + description: &str, +) -> String { + let arguments = format_mapping_arguments( + schema, + protocol, + external_port, + local_addr, + lease_duration, + description, + ); + format_message(format!( + r#" + {arguments} + "# + )) +} + +pub(super) fn format_add_port_mapping_message( + service_type: &str, + schema: &[String], + protocol: PortMappingProtocol, + external_port: u16, + local_addr: SocketAddr, + lease_duration: u32, + description: &str, +) -> String { + let arguments = format_mapping_arguments( + schema, + protocol, + external_port, + local_addr, + lease_duration, + description, + ); + format_message(format!( + r#" + {arguments} + "# + )) +} + +fn format_mapping_arguments( + schema: &[String], + protocol: PortMappingProtocol, + external_port: u16, + local_addr: SocketAddr, + lease_duration: u32, + description: &str, +) -> String { + schema + .iter() + .filter_map(|argument| { + let value = match argument.as_str() { + "NewEnabled" => "1".to_owned(), + "NewExternalPort" => external_port.to_string(), + "NewInternalClient" => local_addr.ip().to_string(), + "NewInternalPort" => local_addr.port().to_string(), + "NewLeaseDuration" => lease_duration.to_string(), + "NewPortMappingDescription" => description.to_owned(), + "NewProtocol" => protocol.to_string(), + "NewRemoteHost" => String::new(), + unknown => { + tracing::warn!(argument = unknown, "unknown IGD SOAP argument"); + return None; + } + }; + Some(format!("<{argument}>{}", xml_escape(&value))) + }) + .collect::>() + .join("\n") +} + +pub(super) fn format_delete_port_message( + service_type: &str, + schema: &[String], + protocol: PortMappingProtocol, + external_port: u16, +) -> String { + let arguments = schema + .iter() + .filter_map(|argument| { + let value = match argument.as_str() { + "NewExternalPort" => external_port.to_string(), + "NewProtocol" => protocol.to_string(), + "NewRemoteHost" => String::new(), + unknown => { + tracing::warn!(argument = unknown, "unknown IGD SOAP argument"); + return None; + } + }; + Some(format!("<{argument}>{}", xml_escape(&value))) + }) + .collect::>() + .join("\n"); + + format_message(format!( + r#" + {arguments} + "# + )) +} + +#[cfg(test)] +pub(super) fn format_get_generic_port_mapping_entry_message( + service_type: &str, + port_mapping_index: u32, +) -> String { + format_message(format!( + r#" + {port_mapping_index} + "# + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + const PPP_SERVICE: &str = "urn:schemas-upnp-org:service:WANPPPConnection:1"; + + #[test] + fn soap_action_and_body_use_discovered_service_type() { + assert_eq!( + soap_action(PPP_SERVICE, ADD_PORT_MAPPING_ACTION), + "\"urn:schemas-upnp-org:service:WANPPPConnection:1#AddPortMapping\"" + ); + + let body = format_add_port_mapping_message( + PPP_SERVICE, + &["NewProtocol".to_owned(), "NewExternalPort".to_owned()], + PortMappingProtocol::Udp, + 12345, + "192.168.1.5:80".parse().unwrap(), + 0, + "test", + ); + assert!(body.contains(r#"xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1""#)); + assert!(body.contains("UDP")); + assert!(body.contains("12345")); + } + + #[test] + fn mapping_description_is_xml_escaped() { + let body = format_add_port_mapping_message( + PPP_SERVICE, + &["NewPortMappingDescription".to_owned()], + PortMappingProtocol::Udp, + 12345, + "192.168.1.5:80".parse().unwrap(), + 0, + "Bob & Alice ", + ); + assert!(body.contains("Bob & Alice </NewPortMappingDescription><evil>")); + assert!(!body.contains("")); + } +} diff --git a/easytier/src/igd_next/mod.rs b/easytier/src/igd_next/mod.rs new file mode 100644 index 00000000..2d906483 --- /dev/null +++ b/easytier/src/igd_next/mod.rs @@ -0,0 +1,485 @@ +// Derived from igd-next 0.17.0 and upstream commit +// bb335d60348386526684d9249d7ae733772279e4, with its network transport +// replaced by EasyTier's socket factories. See LICENSE in this +// directory for the upstream license. + +mod errors; +mod messages; +mod parsing; + +use std::{collections::HashMap, fmt, net::SocketAddr, time::Duration}; + +#[cfg(test)] +use std::net::IpAddr; + +use bytes::Bytes; +use easytier_core::socket::{ + tcp::TcpConnectOptions, + udp::{UdpBindOptions, VirtualUdpSocket, VirtualUdpSocketFactory}, +}; +use http_body_util::{BodyExt as _, Full, Limited}; +use hyper::{ + Method, Request, + client::conn::http1, + header::{CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, HOST}, +}; +use hyper_util::rt::TokioIo; +use rand::Rng as _; +use tokio::time::timeout; + +pub(crate) use errors::AddAnyPortError; +#[cfg(test)] +use errors::GetExternalIpError; +#[cfg(test)] +pub(crate) use errors::GetGenericPortMappingEntryError; +use errors::{AddPortError, HttpTransportError, RemovePortError, RequestError, SearchError}; + +use crate::socket::{tcp::connect_tcp, udp::RuntimeUdpSocketFactory}; + +const DEFAULT_SEARCH_TIMEOUT: Duration = Duration::from_secs(10); +const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5); +const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); +const MAX_SSDP_RESPONSE_SIZE: usize = 1500; +const MAX_HTTP_RESPONSE_SIZE: usize = 1024 * 1024; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum PortMappingProtocol { + #[cfg(test)] + Tcp, + Udp, +} + +impl fmt::Display for PortMappingProtocol { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + #[cfg(test)] + Self::Tcp => "TCP", + Self::Udp => "UDP", + }) + } +} + +pub(crate) struct SearchOptions { + pub(crate) bind_addr: SocketAddr, + pub(crate) broadcast_address: SocketAddr, + pub(crate) timeout: Option, + pub(crate) single_search_timeout: Option, +} + +impl Default for SearchOptions { + fn default() -> Self { + Self { + bind_addr: "0.0.0.0:0".parse().unwrap(), + broadcast_address: "239.255.255.250:1900".parse().unwrap(), + timeout: Some(DEFAULT_SEARCH_TIMEOUT), + single_search_timeout: Some(DEFAULT_RESPONSE_TIMEOUT), + } + } +} + +#[derive(Debug)] +pub(crate) struct Gateway { + pub(crate) addr: SocketAddr, + control_url: String, + control_schema: HashMap>, + service_type: String, +} + +#[cfg(test)] +pub(crate) struct PortMappingEntry { + pub(crate) external_port: u16, + pub(crate) protocol: PortMappingProtocol, + pub(crate) internal_port: u16, + pub(crate) internal_client: String, + pub(crate) port_mapping_description: String, +} + +pub(crate) async fn search_gateway(options: SearchOptions) -> Result { + let search_timeout = options.timeout.unwrap_or(DEFAULT_SEARCH_TIMEOUT); + timeout(search_timeout, search_gateway_inner(options)) + .await + .map_err(|_| SearchError::NoResponseWithinTimeout)? +} + +async fn search_gateway_inner(options: SearchOptions) -> Result { + let bind_options = UdpBindOptions::direct_connect().with_local_addr(Some(options.bind_addr)); + let socket = RuntimeUdpSocketFactory::new() + .bind_udp(bind_options) + .await + .map_err(|error| SearchError::Io(std::io::Error::other(error.to_string())))?; + socket + .send_to( + messages::SEARCH_REQUEST.as_bytes(), + options.broadcast_address, + ) + .await?; + + let response_timeout = options + .single_search_timeout + .unwrap_or(DEFAULT_RESPONSE_TIMEOUT); + loop { + let mut buffer = [0_u8; MAX_SSDP_RESPONSE_SIZE]; + let (length, from) = match timeout(response_timeout, socket.recv_from(&mut buffer)).await { + Ok(Ok(response)) => response, + Ok(Err(error)) => { + tracing::debug!(?error, "failed to receive IGD discovery response"); + continue; + } + Err(_) => { + tracing::debug!("timed out waiting for IGD discovery response"); + continue; + } + }; + let response = match std::str::from_utf8(&buffer[..length]) { + Ok(response) => response, + Err(error) => { + tracing::debug!(?error, %from, "invalid IGD discovery response encoding"); + continue; + } + }; + let (addr, root_url) = match parsing::parse_search_result(response) { + Ok(result) => result, + Err(error) => { + tracing::debug!(?error, %from, "invalid IGD discovery response"); + continue; + } + }; + let (service_type, schema_url, control_url) = match get_control_urls(addr, &root_url).await + { + Ok(urls) => urls, + Err(error) => { + tracing::debug!(?error, %addr, "failed to read IGD device description"); + continue; + } + }; + let control_schema = match get_control_schema(addr, &schema_url).await { + Ok(schema) => schema, + Err(error) => { + tracing::debug!(?error, %addr, "failed to read IGD control schema"); + continue; + } + }; + return Ok(Gateway { + addr, + control_url, + control_schema, + service_type, + }); + } +} + +async fn get_control_urls( + addr: SocketAddr, + root_url: &str, +) -> Result<(String, String, String), SearchError> { + let response = send_http_request(addr, root_url, Method::GET, None, String::new()).await?; + parsing::parse_control_urls(response.as_ref()) +} + +async fn get_control_schema( + addr: SocketAddr, + schema_url: &str, +) -> Result>, SearchError> { + let response = send_http_request(addr, schema_url, Method::GET, None, String::new()).await?; + parsing::parse_schemas(response.as_ref()) +} + +async fn send_http_request( + addr: SocketAddr, + path: &str, + method: Method, + soap_action: Option<&str>, + body: String, +) -> Result { + timeout( + DEFAULT_REQUEST_TIMEOUT, + send_http_request_inner(addr, path, method, soap_action, body), + ) + .await + .map_err(|_| HttpTransportError::TimedOut)? +} + +async fn send_http_request_inner( + addr: SocketAddr, + path: &str, + method: Method, + soap_action: Option<&str>, + body: String, +) -> Result { + let stream = connect_tcp(TcpConnectOptions::direct_connect(addr)).await?; + let (mut sender, connection) = http1::handshake(TokioIo::new(stream)).await?; + let body = Bytes::from(body); + let mut request = Request::builder() + .method(method) + .uri(path) + .header(HOST, addr.to_string()) + .header(CONNECTION, "close") + .header(CONTENT_LENGTH, body.len() as u64); + if let Some(action) = soap_action { + request = request + .header("SOAPAction", action) + .header(CONTENT_TYPE, "text/xml; charset=utf-8"); + } + let request = request.body(Full::new(body))?; + + let response = async move { + let response = sender.send_request(request).await?; + let body = Limited::new(response.into_body(), MAX_HTTP_RESPONSE_SIZE) + .collect() + .await + .map_err(|error| HttpTransportError::InvalidResponse(error.to_string()))? + .to_bytes(); + Ok::<_, HttpTransportError>(body) + }; + tokio::pin!(response); + tokio::pin!(connection); + tokio::select! { + biased; + result = &mut response => result, + result = &mut connection => { + result?; + response.await + } + } +} + +impl Gateway { + async fn perform_request( + &self, + action: &str, + body: &str, + expected_response: &str, + ) -> parsing::RequestResult { + let soap_action = messages::soap_action(&self.service_type, action); + let response = send_http_request( + self.addr, + &self.control_url, + Method::POST, + Some(&soap_action), + body.to_owned(), + ) + .await?; + let response = String::from_utf8(response.to_vec()).map_err(HttpTransportError::from)?; + parsing::parse_response(response, expected_response) + } + + #[cfg(test)] + pub(crate) async fn get_external_ip(&self) -> Result { + let result = self + .perform_request( + messages::GET_EXTERNAL_IP_ACTION, + &messages::format_get_external_ip_message(&self.service_type), + "GetExternalIPAddressResponse", + ) + .await; + parsing::parse_get_external_ip_response(result) + } + + pub(crate) async fn add_any_port( + &self, + protocol: PortMappingProtocol, + local_addr: SocketAddr, + lease_duration: u32, + description: &str, + ) -> Result { + if local_addr.port() == 0 { + return Err(AddAnyPortError::InternalPortZeroInvalid); + } + + if let Some(schema) = self + .control_schema + .get(messages::ADD_ANY_PORT_MAPPING_ACTION) + { + let external_port = random_port(); + let result = self + .perform_request( + messages::ADD_ANY_PORT_MAPPING_ACTION, + &messages::format_add_any_port_mapping_message( + &self.service_type, + schema, + protocol, + external_port, + local_addr, + lease_duration, + description, + ), + "AddAnyPortMappingResponse", + ) + .await; + return parsing::parse_add_any_port_mapping_response(result); + } + + self.retry_add_random_port_mapping(protocol, local_addr, lease_duration, description) + .await + } + + async fn retry_add_random_port_mapping( + &self, + protocol: PortMappingProtocol, + local_addr: SocketAddr, + lease_duration: u32, + description: &str, + ) -> Result { + for _ in 0..20 { + match self + .add_random_port_mapping(protocol, local_addr, lease_duration, description) + .await + { + Err(AddAnyPortError::NoPortsAvailable) => continue, + result => return result, + } + } + Err(AddAnyPortError::NoPortsAvailable) + } + + async fn add_random_port_mapping( + &self, + protocol: PortMappingProtocol, + local_addr: SocketAddr, + lease_duration: u32, + description: &str, + ) -> Result { + let external_port = random_port(); + match self + .add_port_mapping( + protocol, + external_port, + local_addr, + lease_duration, + description, + ) + .await + { + Ok(()) => Ok(external_port), + Err(error) => match parsing::convert_add_random_port_mapping_error(error) { + Some(error) => Err(error), + None => { + self.add_same_port_mapping(protocol, local_addr, lease_duration, description) + .await + } + }, + } + } + + async fn add_same_port_mapping( + &self, + protocol: PortMappingProtocol, + local_addr: SocketAddr, + lease_duration: u32, + description: &str, + ) -> Result { + self.add_port_mapping( + protocol, + local_addr.port(), + local_addr, + lease_duration, + description, + ) + .await + .map(|()| local_addr.port()) + .map_err(parsing::convert_add_same_port_mapping_error) + } + + async fn add_port_mapping( + &self, + protocol: PortMappingProtocol, + external_port: u16, + local_addr: SocketAddr, + lease_duration: u32, + description: &str, + ) -> Result<(), RequestError> { + let schema = self + .control_schema + .get(messages::ADD_PORT_MAPPING_ACTION) + .ok_or_else(|| { + RequestError::UnsupportedAction(messages::ADD_PORT_MAPPING_ACTION.to_owned()) + })?; + self.perform_request( + messages::ADD_PORT_MAPPING_ACTION, + &messages::format_add_port_mapping_message( + &self.service_type, + schema, + protocol, + external_port, + local_addr, + lease_duration, + description, + ), + "AddPortMappingResponse", + ) + .await?; + Ok(()) + } + + pub(crate) async fn add_port( + &self, + protocol: PortMappingProtocol, + external_port: u16, + local_addr: SocketAddr, + lease_duration: u32, + description: &str, + ) -> Result<(), AddPortError> { + if external_port == 0 { + return Err(AddPortError::ExternalPortZeroInvalid); + } + if local_addr.port() == 0 { + return Err(AddPortError::InternalPortZeroInvalid); + } + self.add_port_mapping( + protocol, + external_port, + local_addr, + lease_duration, + description, + ) + .await + .map_err(parsing::convert_add_port_error) + } + + pub(crate) async fn remove_port( + &self, + protocol: PortMappingProtocol, + external_port: u16, + ) -> Result<(), RemovePortError> { + let schema = self + .control_schema + .get(messages::DELETE_PORT_MAPPING_ACTION) + .ok_or_else(|| { + RemovePortError::RequestError(RequestError::UnsupportedAction( + messages::DELETE_PORT_MAPPING_ACTION.to_owned(), + )) + })?; + let result = self + .perform_request( + messages::DELETE_PORT_MAPPING_ACTION, + &messages::format_delete_port_message( + &self.service_type, + schema, + protocol, + external_port, + ), + "DeletePortMappingResponse", + ) + .await; + parsing::parse_delete_port_mapping_response(result) + } + + #[cfg(test)] + pub(crate) async fn get_generic_port_mapping_entry( + &self, + index: u32, + ) -> Result { + let result = self + .perform_request( + messages::GET_GENERIC_PORT_MAPPING_ENTRY_ACTION, + &messages::format_get_generic_port_mapping_entry_message(&self.service_type, index), + "GetGenericPortMappingEntryResponse", + ) + .await; + parsing::parse_get_generic_port_mapping_entry(result) + } +} + +fn random_port() -> u16 { + rand::thread_rng().gen_range(32_768..65_535) +} diff --git a/easytier/src/igd_next/parsing.rs b/easytier/src/igd_next/parsing.rs new file mode 100644 index 00000000..8e11e9df --- /dev/null +++ b/easytier/src/igd_next/parsing.rs @@ -0,0 +1,401 @@ +use std::{collections::HashMap, io, net::SocketAddr}; + +#[cfg(test)] +use std::str::FromStr; + +use url::Url; +use xmltree::Element; + +use super::errors::{AddAnyPortError, AddPortError, RemovePortError, RequestError, SearchError}; +#[cfg(test)] +use super::{ + PortMappingEntry, PortMappingProtocol, + errors::{GetExternalIpError, GetGenericPortMappingEntryError}, +}; + +pub(super) fn parse_search_result(text: &str) -> Result<(SocketAddr, String), SearchError> { + for line in text.lines().map(str::trim) { + let Some((name, value)) = line.split_once(':') else { + continue; + }; + if !name.eq_ignore_ascii_case("location") { + continue; + } + + let url = Url::parse(value.trim()).map_err(|_| SearchError::InvalidResponse)?; + let host = url.host_str().ok_or(SearchError::InvalidResponse)?; + let ip = host.parse().map_err(|_| SearchError::InvalidResponse)?; + let port = url + .port_or_known_default() + .ok_or(SearchError::InvalidResponse)?; + let mut path = url.path().to_owned(); + if let Some(query) = url.query() { + path.push('?'); + path.push_str(query); + } + return Ok((SocketAddr::new(ip, port), path)); + } + Err(SearchError::InvalidResponse) +} + +pub(super) fn parse_control_urls(response: R) -> Result<(String, String, String), SearchError> +where + R: io::Read, +{ + let root = Element::parse(response)?; + root.children + .iter() + .filter_map(|child| child.as_element()) + .find(|child| child.name == "device") + .and_then(parse_device) + .ok_or(SearchError::InvalidResponse) +} + +fn parse_device(device: &Element) -> Option<(String, String, String)> { + let service = device.get_child("serviceList").and_then(|list| { + list.children + .iter() + .filter_map(|child| child.as_element()) + .filter(|child| child.name == "service") + .find_map(parse_service) + }); + service.or_else(|| { + device.get_child("deviceList").and_then(|list| { + list.children + .iter() + .filter_map(|child| child.as_element()) + .filter(|child| child.name == "device") + .find_map(parse_device) + }) + }) +} + +fn parse_service(service: &Element) -> Option<(String, String, String)> { + let service_type = service.get_child("serviceType")?.get_text()?.into_owned(); + if ![ + "urn:schemas-upnp-org:service:WANPPPConnection:1", + "urn:schemas-upnp-org:service:WANIPConnection:1", + "urn:schemas-upnp-org:service:WANIPConnection:2", + ] + .contains(&service_type.as_str()) + { + return None; + } + + let schema_url = service.get_child("SCPDURL")?.get_text()?.into_owned(); + let control_url = service.get_child("controlURL")?.get_text()?.into_owned(); + Some((service_type, schema_url, control_url)) +} + +pub(super) fn parse_schemas(response: R) -> Result>, SearchError> +where + R: io::Read, +{ + let root = Element::parse(response)?; + root.children + .iter() + .filter_map(|child| child.as_element()) + .find(|child| child.name == "actionList") + .map(parse_action_list) + .ok_or(SearchError::InvalidResponse) +} + +fn parse_action_list(action_list: &Element) -> HashMap> { + action_list + .children + .iter() + .filter_map(|child| child.as_element()) + .filter(|child| child.name == "action") + .filter_map(parse_action) + .collect() +} + +fn parse_action(action: &Element) -> Option<(String, Vec)> { + let name = action.get_child("name")?.get_text()?.into_owned(); + let arguments = action + .get_child("argumentList")? + .children + .iter() + .filter_map(|child| child.as_element()) + .filter(|child| child.name == "argument") + .filter_map(parse_input_argument) + .collect(); + Some((name, arguments)) +} + +fn parse_input_argument(argument: &Element) -> Option { + if argument.get_child("direction")?.get_text()?.as_ref() != "in" { + return None; + } + argument + .get_child("name")? + .get_text() + .map(|name| name.into_owned()) +} + +pub(super) struct RequestResponse { + text: String, + xml: Element, +} + +pub(super) type RequestResult = Result; + +pub(super) fn parse_response(text: String, expected: &str) -> RequestResult { + let mut xml = + Element::parse(text.as_bytes()).map_err(|_| RequestError::InvalidResponse(text.clone()))?; + let body = xml + .get_mut_child("Body") + .ok_or_else(|| RequestError::InvalidResponse(text.clone()))?; + if let Some(response) = body.take_child(expected) { + return Ok(RequestResponse { + text, + xml: response, + }); + } + + let upnp_error = body + .get_child("Fault") + .and_then(|fault| fault.get_child("detail")) + .and_then(|detail| detail.get_child("UPnPError")) + .ok_or_else(|| RequestError::InvalidResponse(text.clone()))?; + let code = upnp_error + .get_child("errorCode") + .and_then(Element::get_text) + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| RequestError::InvalidResponse(text.clone()))?; + let description = upnp_error + .get_child("errorDescription") + .and_then(Element::get_text) + .map(|value| value.into_owned()) + .ok_or_else(|| RequestError::InvalidResponse(text.clone()))?; + Err(RequestError::ErrorCode(code, description)) +} + +#[cfg(test)] +pub(super) fn parse_get_external_ip_response( + result: RequestResult, +) -> Result { + match result { + Ok(response) => response + .xml + .get_child("NewExternalIPAddress") + .and_then(Element::get_text) + .and_then(|text| text.parse().ok()) + .ok_or_else(|| { + GetExternalIpError::RequestError(RequestError::InvalidResponse(response.text)) + }), + Err(RequestError::ErrorCode(606, _)) => Err(GetExternalIpError::ActionNotAuthorized), + Err(error) => Err(GetExternalIpError::RequestError(error)), + } +} + +pub(super) fn parse_add_any_port_mapping_response( + result: RequestResult, +) -> Result { + match result { + Ok(response) => response + .xml + .get_child("NewReservedPort") + .and_then(Element::get_text) + .and_then(|text| text.parse().ok()) + .ok_or_else(|| { + AddAnyPortError::RequestError(RequestError::InvalidResponse(response.text)) + }), + Err(error) => Err(match error { + RequestError::ErrorCode(605, _) => AddAnyPortError::DescriptionTooLong, + RequestError::ErrorCode(606, _) => AddAnyPortError::ActionNotAuthorized, + RequestError::ErrorCode(728, _) => AddAnyPortError::NoPortsAvailable, + other => AddAnyPortError::RequestError(other), + }), + } +} + +pub(super) fn convert_add_random_port_mapping_error( + error: RequestError, +) -> Option { + match error { + RequestError::ErrorCode(724, _) => None, + RequestError::ErrorCode(605, _) => Some(AddAnyPortError::DescriptionTooLong), + RequestError::ErrorCode(606, _) => Some(AddAnyPortError::ActionNotAuthorized), + RequestError::ErrorCode(718, _) => Some(AddAnyPortError::NoPortsAvailable), + RequestError::ErrorCode(725, _) => Some(AddAnyPortError::OnlyPermanentLeasesSupported), + other => Some(AddAnyPortError::RequestError(other)), + } +} + +pub(super) fn convert_add_same_port_mapping_error(error: RequestError) -> AddAnyPortError { + match error { + RequestError::ErrorCode(606, _) => AddAnyPortError::ActionNotAuthorized, + RequestError::ErrorCode(718, _) => AddAnyPortError::ExternalPortInUse, + RequestError::ErrorCode(725, _) => AddAnyPortError::OnlyPermanentLeasesSupported, + other => AddAnyPortError::RequestError(other), + } +} + +pub(super) fn convert_add_port_error(error: RequestError) -> AddPortError { + match error { + RequestError::ErrorCode(605, _) => AddPortError::DescriptionTooLong, + RequestError::ErrorCode(606, _) => AddPortError::ActionNotAuthorized, + RequestError::ErrorCode(718, _) => AddPortError::PortInUse, + RequestError::ErrorCode(724, _) => AddPortError::SamePortValuesRequired, + RequestError::ErrorCode(725, _) => AddPortError::OnlyPermanentLeasesSupported, + other => AddPortError::RequestError(other), + } +} + +pub(super) fn parse_delete_port_mapping_response( + result: RequestResult, +) -> Result<(), RemovePortError> { + match result { + Ok(_) => Ok(()), + Err(RequestError::ErrorCode(606, _)) => Err(RemovePortError::ActionNotAuthorized), + Err(RequestError::ErrorCode(714, _)) => Err(RemovePortError::NoSuchPortMapping), + Err(error) => Err(RemovePortError::RequestError(error)), + } +} + +#[cfg(test)] +pub(super) fn parse_get_generic_port_mapping_entry( + result: RequestResult, +) -> Result { + let response = result?; + let xml = response.xml; + let invalid = |message: String| { + GetGenericPortMappingEntryError::RequestError(RequestError::InvalidResponse(message)) + }; + let field = |name: &str| { + xml.get_child(name) + .ok_or_else(|| invalid(format!("{name} is missing"))) + }; + field("NewRemoteHost")?; + let external_port = parse_number(&xml, "NewExternalPort")?; + let protocol = match field("NewProtocol")?.get_text().as_deref() { + Some("UDP") => PortMappingProtocol::Udp, + Some("TCP") => PortMappingProtocol::Tcp, + _ => return Err(invalid("NewProtocol is invalid".to_owned())), + }; + let internal_port = parse_number(&xml, "NewInternalPort")?; + let internal_client = field("NewInternalClient")? + .get_text() + .map(|text| text.into_owned()) + .ok_or_else(|| invalid("NewInternalClient is empty".to_owned()))?; + match parse_number::(&xml, "NewEnabled")? { + 0 | 1 => {} + _ => return Err(invalid("NewEnabled is invalid".to_owned())), + } + let port_mapping_description = field("NewPortMappingDescription")? + .get_text() + .map(|text| text.into_owned()) + .unwrap_or_default(); + parse_number::(&xml, "NewLeaseDuration")?; + + Ok(PortMappingEntry { + external_port, + protocol, + internal_port, + internal_client, + port_mapping_description, + }) +} + +#[cfg(test)] +fn parse_number( + xml: &Element, + name: &str, +) -> Result { + xml.get_child(name) + .and_then(Element::get_text) + .and_then(|text| text.parse().ok()) + .ok_or_else(|| { + GetGenericPortMappingEntryError::RequestError(RequestError::InvalidResponse(format!( + "{name} is missing or invalid" + ))) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn search_location_is_case_insensitive_and_keeps_query() { + let (address, path) = parse_search_result( + "HTTP/1.1 200 OK\r\nLOCATION: http://192.168.1.1:5431/root.xml?v=2\r\n", + ) + .unwrap(); + assert_eq!(address, "192.168.1.1:5431".parse().unwrap()); + assert_eq!(path, "/root.xml?v=2"); + } + + #[test] + fn nested_ppp_service_is_discovered() { + let description = r#" + + + urn:schemas-upnp-org:service:WANPPPConnection:1 + /control + /schema.xml + + + "#; + assert_eq!( + parse_control_urls(description.as_bytes()).unwrap(), + ( + "urn:schemas-upnp-org:service:WANPPPConnection:1".to_owned(), + "/schema.xml".to_owned(), + "/control".to_owned(), + ) + ); + } + + #[test] + fn device_description_uses_declared_xml_encoding() { + let description = r#" + + urn:schemas-upnp-org:service:WANIPConnection:1 + /control + /schema.xml + "#; + let mut encoded = vec![0xff, 0xfe]; + for code_unit in description.encode_utf16() { + encoded.extend_from_slice(&code_unit.to_le_bytes()); + } + + assert_eq!( + parse_control_urls(encoded.as_slice()).unwrap(), + ( + "urn:schemas-upnp-org:service:WANIPConnection:1".to_owned(), + "/schema.xml".to_owned(), + "/control".to_owned(), + ) + ); + } + + #[test] + fn schema_contains_only_input_arguments() { + let schema = r#" + AddPortMapping + NewExternalPortin + Resultout + NewProtocolin + + "#; + assert_eq!( + parse_schemas(schema.as_bytes()).unwrap()["AddPortMapping"], + ["NewExternalPort", "NewProtocol"] + ); + } + + #[test] + fn soap_fault_preserves_error_code() { + let response = r#" + + 713SpecifiedArrayIndexInvalid + + "#; + assert!(matches!( + parse_response(response.to_owned(), "GetGenericPortMappingEntryResponse"), + Err(RequestError::ErrorCode(713, _)) + )); + } +} diff --git a/easytier/src/lib.rs b/easytier/src/lib.rs index 8a5cc00c..a8634ac4 100644 --- a/easytier/src/lib.rs +++ b/easytier/src/lib.rs @@ -12,6 +12,8 @@ mod vpn_portal; pub mod common; #[cfg(feature = "management")] pub mod core; +#[cfg(feature = "upnp")] +pub(crate) mod igd_next; pub mod proto; #[cfg(feature = "management-rpc")] pub mod rpc_service; diff --git a/easytier/src/tests/upnp_test.rs b/easytier/src/tests/upnp_test.rs index 1440555d..d888641b 100644 --- a/easytier/src/tests/upnp_test.rs +++ b/easytier/src/tests/upnp_test.rs @@ -7,15 +7,15 @@ use std::{ time::Duration, }; +use crate::igd_next::{ + GetGenericPortMappingEntryError, PortMappingEntry, PortMappingProtocol, SearchOptions, + search_gateway, +}; use anyhow::{Context, anyhow, bail}; use easytier_core::{ connectivity::stun::{StunInfoProvider, StunSocketMapper}, process_runtime::CoreProcessRuntime, }; -use igd_next::{ - GetGenericPortMappingEntryError, PortMappingEntry, PortMappingProtocol, SearchOptions, - aio::tokio::search_gateway, -}; use tempfile::TempDir; use super::{ @@ -843,7 +843,7 @@ async fn query_mappings( for index in 0..64 { match gateway.get_generic_port_mapping_entry(index).await { Ok(entry) => { - if entry.protocol == PortMappingProtocol::UDP + if entry.protocol == PortMappingProtocol::Udp && entry.port_mapping_description == TEST_IGD_DESCRIPTION { entries.push(entry);