From 8da009097a2b988099dfa2f2e8222d9b548c7e87 Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 13 Apr 2026 14:58:40 +0900 Subject: [PATCH 1/5] Update dependencies, DB and server code; add test Update Cargo.lock with multiple dependency bumps and additions, update the hbb_common submodule, and adjust source code across src/common.rs, src/database.rs, src/peer.rs, src/relay_server.rs and src/rendezvous_server.rs to match the updates. Commit also updates the local SQLite DB (db_v2.sqlite3) and adds a new integration test (tests/server_protection_process.rs) to exercise server protection behavior. These changes combine dependency maintenance with corresponding code and test updates to ensure compatibility and improved coverage. Signed-off-by: Vlad --- Cargo.lock | 2717 +++++++++++++++++++++++++--- db_v2.sqlite3 | Bin 24576 -> 24576 bytes libs/hbb_common | 2 +- src/common.rs | 311 +++- src/database.rs | 142 +- src/peer.rs | 163 +- src/relay_server.rs | 161 +- src/rendezvous_server.rs | 224 ++- tests/server_protection_process.rs | 500 +++++ 9 files changed, 3889 insertions(+), 331 deletions(-) create mode 100644 tests/server_protection_process.rs diff --git a/Cargo.lock b/Cargo.lock index 8077c21dc..8dac55353 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,13 +17,48 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", ] @@ -61,11 +96,70 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] [[package]] name = "arrayvec" @@ -73,6 +167,45 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.1", + "num-traits", + "rusticata-macros", + "thiserror 1.0.31", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "async-compression" version = "0.4.18" @@ -86,6 +219,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "async-speed-limit" version = "0.3.1" @@ -104,7 +248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", + "quote 1.0.45", "syn 1.0.93", ] @@ -117,6 +261,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -146,7 +296,7 @@ dependencies = [ "bytes", "futures-util", "headers", - "http", + "http 0.2.7", "http-body", "hyper", "itoa", @@ -175,7 +325,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 0.2.7", "http-body", "mime", ] @@ -195,6 +345,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.0" @@ -213,6 +369,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bcrypt" version = "0.13.0" @@ -221,10 +383,19 @@ checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641" dependencies = [ "base64 0.13.0", "blowfish", - "getrandom", + "getrandom 0.2.15", "zeroize", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.59.2" @@ -234,14 +405,14 @@ dependencies = [ "bitflags 1.3.2", "cexpr", "clang-sys", - "clap", + "clap 2.34.0", "env_logger 0.9.0", "lazy_static", "lazycell", "log", "peeking_take_while", "proc-macro2 1.0.93", - "quote 1.0.38", + "quote 1.0.45", "regex", "rustc-hash", "shlex", @@ -256,9 +427,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.7.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -269,6 +440,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "blowfish" version = "0.9.1" @@ -293,13 +473,47 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.9.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.11.0", + "polling", + "rustix", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.9" @@ -311,6 +525,18 @@ dependencies = [ "shlex", ] +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + [[package]] name = "cesu8" version = "1.1.0" @@ -332,6 +558,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fc89c7c5b9e7a02dfe45cd2367bae382f9ed31c61ca8debe5f827c420a2f08" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.39" @@ -354,6 +604,7 @@ checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -376,12 +627,45 @@ dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -392,6 +676,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.11.0" @@ -410,10 +703,16 @@ source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc dependencies = [ "directories-next", "serde", - "thiserror", + "thiserror 1.0.31", "toml 0.5.9", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -424,6 +723,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -432,27 +741,27 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc" -version = "3.0.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" -version = "2.1.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" @@ -508,7 +817,7 @@ dependencies = [ "cfg-if", "crossbeam-utils", "lazy_static", - "memoffset", + "memoffset 0.6.5", "scopeguard", ] @@ -524,24 +833,80 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "cfg-if", - "lazy_static", + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", ] [[package]] name = "crypto-common" -version = "0.1.3" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + [[package]] name = "deadpool" version = "0.8.2" @@ -565,14 +930,51 @@ dependencies = [ "winapi", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.1", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "quickcheck", +] + [[package]] name = "digest" -version = "0.10.3" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -606,6 +1008,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "dlopen" version = "0.1.8" @@ -653,13 +1066,69 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dtls" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f531dd7c181beaf3cebab3716afa4d0d41ab888be85232583f56bbaf07ca208a" +dependencies = [ + "aes", + "aes-gcm", + "async-trait", + "bincode", + "byteorder", + "cbc", + "ccm", + "chacha20poly1305", + "der-parser", + "hmac", + "log", + "p256", + "p384", + "portable-atomic", + "rand 0.9.3", + "rand_core 0.6.4", + "rcgen", + "ring 0.17.14", + "rustls 0.23.37", + "sec1", + "serde", + "sha1", + "sha2", + "thiserror 1.0.31", + "tokio", + "webrtc-util", + "x25519-dalek", + "x509-parser", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", + "spki", +] + [[package]] name = "ed25519" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d916019f70ae3a1faa1195685e290287f39207d38e6dfee727197cffcc002214" dependencies = [ - "signature", + "signature 1.5.0", ] [[package]] @@ -668,6 +1137,27 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -677,6 +1167,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.9.0" @@ -692,15 +1192,15 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.10.2" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ - "humantime", - "is-terminal", + "anstream", + "anstyle", + "env_filter", + "jiff", "log", - "regex", - "termcolor", ] [[package]] @@ -709,6 +1209,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "event-listener" version = "2.5.2" @@ -724,6 +1234,22 @@ dependencies = [ "instant", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.16" @@ -732,7 +1258,7 @@ checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.13", "winapi", ] @@ -761,7 +1287,7 @@ dependencies = [ "log", "regex", "rustversion", - "thiserror", + "thiserror 1.0.31", "time", ] @@ -780,7 +1306,7 @@ dependencies = [ "log", "nu-ansi-term", "regex", - "thiserror", + "thiserror 1.0.31", ] [[package]] @@ -801,6 +1327,12 @@ 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 = "foreign-types" version = "0.3.2" @@ -818,11 +1350,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "matches", "percent-encoding", ] @@ -843,9 +1374,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -853,9 +1384,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" @@ -881,32 +1412,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", - "syn 1.0.93", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -916,9 +1447,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -928,18 +1459,18 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -953,6 +1484,41 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[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 = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -965,6 +1531,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -976,7 +1553,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.7", "indexmap 2.7.0", "slab", "tokio", @@ -1004,6 +1581,9 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] [[package]] name = "hashlink" @@ -1019,16 +1599,18 @@ name = "hbb_common" version = "0.1.0" dependencies = [ "anyhow", + "async-recursion", "backtrace", "base64 0.22.1", "bytes", "chrono", + "clap 4.6.0", "confy", "default_net", "directories-next", "dirs-next", "dlopen", - "env_logger 0.10.2", + "env_logger 0.11.10", "filetime", "flexi_logger 0.27.4", "futures", @@ -1036,33 +1618,43 @@ dependencies = [ "httparse", "lazy_static", "libc", + "libloading", "log", "mac_address", "machine-uid 0.3.0", "osascript", "protobuf", "protobuf-codegen", - "rand", + "rand 0.8.5", "regex", + "rustls-native-certs 0.8.3", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_derive", "serde_json", "sha2", + "smithay-client-toolkit", "socket2 0.3.19", "sodiumoxide", "sysinfo", - "thiserror", + "thiserror 1.0.31", "tokio", "tokio-native-tls", "tokio-rustls 0.26.1", "tokio-socks 0.5.2-1", + "tokio-tungstenite 0.26.2", "tokio-util", "toml 0.7.8", + "tungstenite 0.26.2", "url", + "users", "uuid", + "webpki-roots 1.0.6", + "webrtc", + "whoami", "winapi", + "x11", "zstd", ] @@ -1076,14 +1668,14 @@ dependencies = [ "base64 0.13.0", "bcrypt", "chrono", - "clap", + "clap 2.34.0", "deadpool", "dns-lookup", "flate2", "flexi_logger 0.22.3", "hbb_common", "headers", - "http", + "http 0.2.7", "ipnetwork", "jsonwebtoken", "lazy_static", @@ -1101,9 +1693,9 @@ dependencies = [ "serde_json", "sodiumoxide", "sqlx", - "tokio-tungstenite", + "tokio-tungstenite 0.17.1", "tower-http", - "tungstenite", + "tungstenite 0.17.2", "uuid", "whoami", ] @@ -1118,7 +1710,7 @@ dependencies = [ "bitflags 1.3.2", "bytes", "headers-core", - "http", + "http 0.2.7", "httpdate", "mime", "sha-1", @@ -1130,7 +1722,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" dependencies = [ - "http", + "http 0.2.7", ] [[package]] @@ -1142,6 +1734,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1157,12 +1755,36 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "0.2.7" @@ -1174,6 +1796,16 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + [[package]] name = "http-body" version = "0.4.4" @@ -1181,7 +1813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ "bytes", - "http", + "http 0.2.7", "pin-project-lite", ] @@ -1193,9 +1825,9 @@ checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1220,7 +1852,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.7", "http-body", "httparse", "httpdate", @@ -1240,7 +1872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.7", "hyper", "rustls 0.21.12", "tokio", @@ -1283,15 +1915,113 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" -version = "0.2.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1312,6 +2042,7 @@ checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown 0.15.2", + "serde", ] [[package]] @@ -1320,6 +2051,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ + "block-padding", "generic-array", ] @@ -1332,6 +2064,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interceptor" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea51375727680dc15f06e8ad90fa31df75d79dd030100e8ad60eef1c27fe2c98" +dependencies = [ + "async-trait", + "bytes", + "futures", + "log", + "portable-atomic", + "rand 0.9.3", + "rtcp", + "rtp", + "thiserror 1.0.31", + "tokio", + "waitgroup", + "webrtc-srtp", + "webrtc-util", +] + [[package]] name = "ipnet" version = "2.10.1" @@ -1358,6 +2111,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.3" @@ -1373,18 +2132,44 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "jni" -version = "0.19.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", + "cfg-if", "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.31", "walkdir", + "windows-sys 0.45.0", ] [[package]] @@ -1419,7 +2204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc9051c17f81bae79440afa041b3a278e1de71bfb96d32454b477fd4703ccb6f" dependencies = [ "base64 0.13.0", - "pem", + "pem 1.0.2", "ring 0.16.20", "serde", "serde_json", @@ -1428,9 +2213,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" @@ -1438,6 +2223,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lexical-core" version = "0.7.6" @@ -1453,9 +2244,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libloading" @@ -1467,6 +2258,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.4", +] + [[package]] name = "libsodium-sys" version = "0.2.7" @@ -1490,6 +2293,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "local-ip-address" version = "0.5.3" @@ -1498,7 +2313,7 @@ checksum = "2815836665de176ba66deaa449ada98fdf208d84730d1a84a22cbeed6151a6fa" dependencies = [ "libc", "neli", - "thiserror", + "thiserror 1.0.31", "windows-sys 0.48.0", ] @@ -1514,12 +2329,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mac_address" @@ -1527,7 +2339,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4863ee94f19ed315bf3bc00299338d857d4b5bc856af375cc97d237382ad3856" dependencies = [ - "nix", + "nix 0.23.1", "winapi", ] @@ -1550,12 +2362,6 @@ dependencies = [ "winreg 0.11.0", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "matchit" version = "0.5.0" @@ -1563,11 +2369,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" [[package]] -name = "memchr" +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -1577,6 +2402,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" @@ -1620,13 +2454,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1638,10 +2472,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.5", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -1666,7 +2500,7 @@ checksum = "c168194d373b1e134786274020dae7fc5513d565ea2ebb9bc9ff17ffb69106d4" dependencies = [ "either", "proc-macro2 1.0.93", - "quote 1.0.38", + "quote 1.0.45", "serde", "syn 1.0.93", ] @@ -1681,7 +2515,20 @@ dependencies = [ "cc", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", ] [[package]] @@ -1733,6 +2580,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-integer" version = "0.1.46" @@ -1779,19 +2632,40 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -1807,8 +2681,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", - "syn 2.0.96", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -1817,6 +2691,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" version = "0.9.104" @@ -1850,6 +2730,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1880,7 +2784,7 @@ dependencies = [ "cfg-if", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.13", "smallvec", "winapi", ] @@ -1893,7 +2797,7 @@ checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.13", "smallvec", "windows-sys 0.36.1", ] @@ -1919,11 +2823,30 @@ dependencies = [ "base64 0.13.0", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" @@ -1941,8 +2864,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", - "syn 2.0.96", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -1963,9 +2886,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69044d1c00894fc1f43d9485aadb6ab6e68df90608fa52cf1074cda6420c6b76" dependencies = [ - "rand", + "rand 0.8.5", "socket2 0.4.4", - "thiserror", + "thiserror 1.0.31", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", ] [[package]] @@ -1974,12 +2907,104 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2 1.0.93", + "syn 2.0.117", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "0.4.30" @@ -2007,7 +3032,7 @@ dependencies = [ "bytes", "once_cell", "protobuf-support", - "thiserror", + "thiserror 1.0.31", ] [[package]] @@ -2022,7 +3047,7 @@ dependencies = [ "protobuf-parse", "regex", "tempfile", - "thiserror", + "thiserror 1.0.31", ] [[package]] @@ -2037,7 +3062,7 @@ dependencies = [ "protobuf", "protobuf-support", "tempfile", - "thiserror", + "thiserror 1.0.31", "which", ] @@ -2047,7 +3072,7 @@ version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252" dependencies = [ - "thiserror", + "thiserror 1.0.31", ] [[package]] @@ -2056,13 +3081,22 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9e1dcb320d6839f6edb64f7a4a59d39b30480d4d1765b56873f7c858538a5fe" +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + [[package]] name = "quickcheck" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] @@ -2076,13 +3110,25 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2 1.0.93", ] +[[package]] +name = "r-efi" +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 = "rand" version = "0.8.5" @@ -2090,8 +3136,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -2101,16 +3157,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] @@ -2133,6 +3208,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem 3.0.6", + "ring 0.17.14", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.2.13" @@ -2142,15 +3231,24 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "redox_users" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", + "getrandom 0.2.15", + "redox_syscall 0.2.13", + "thiserror 1.0.31", ] [[package]] @@ -2203,7 +3301,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.7", "http-body", "hyper", "hyper-rustls", @@ -2218,7 +3316,7 @@ dependencies = [ "pin-project-lite", "rustls 0.21.12", "rustls-native-certs 0.6.2", - "rustls-pemfile 1.0.0", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", @@ -2238,6 +3336,16 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -2255,16 +3363,42 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.3" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "getrandom", + "cfg-if", + "getrandom 0.2.15", "libc", - "spin 0.9.3", "untrusted 0.9.0", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rtcp" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d30d1c4091644431c22acf9f8be6191b56805e0e977f15ca7104b4a6d6eaec" +dependencies = [ + "bytes", + "thiserror 1.0.31", + "webrtc-util", +] + +[[package]] +name = "rtp" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f126f38ea84c02480e32e547c1459a939052f74fb92117ac3eef23fdac6b023" +dependencies = [ + "bytes", + "memchr", + "portable-atomic", + "rand 0.9.3", + "serde", + "thiserror 1.0.31", + "webrtc-util", ] [[package]] @@ -2289,6 +3423,37 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.1", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustls" version = "0.20.4" @@ -2308,22 +3473,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring 0.17.3", + "ring 0.17.14", "rustls-webpki 0.101.7", "sct", ] [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", - "ring 0.17.3", + "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.102.8", + "rustls-webpki 0.103.11", "subtle", "zeroize", ] @@ -2334,23 +3499,22 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.0", + "openssl-probe 0.1.5", + "rustls-pemfile", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] name = "rustls-native-certs" -version = "0.7.3" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", - "rustls-pemfile 2.2.0", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.7.0", ] [[package]] @@ -2363,39 +3527,33 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls-pki-types" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "rustls-pki-types", + "zeroize", ] -[[package]] -name = "rustls-pki-types" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" - [[package]] name = "rustls-platform-verifier" -version = "0.3.4" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", "once_cell", - "rustls 0.23.21", - "rustls-native-certs 0.7.3", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", "rustls-platform-verifier-android", - "rustls-webpki 0.102.8", - "security-framework", + "rustls-webpki 0.103.11", + "security-framework 3.7.0", "security-framework-sys", - "webpki-roots 0.26.7", - "winapi", + "webpki-root-certs", + "windows-sys 0.59.0", ] [[package]] @@ -2410,17 +3568,17 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.3", + "ring 0.17.14", "untrusted 0.9.0", ] [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" dependencies = [ - "ring 0.17.3", + "ring 0.17.14", "rustls-pki-types", "untrusted 0.9.0", ] @@ -2471,48 +3629,102 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "sdp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c374dceda16965d541c8800ce9cc4e1c14acfd661ddf7952feeedc3411e5c6" +dependencies = [ + "rand 0.9.3", + "substring", + "thiserror 1.0.31", + "url", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.7.0", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", - "num-bigint", "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", - "syn 2.0.96", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -2558,6 +3770,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.2" @@ -2590,6 +3813,16 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simple_asn1" version = "0.6.1" @@ -2598,21 +3831,57 @@ checksum = "4a762b1c38b9b990c694b9c2f8abe3372ce6a9ceaae6bca39cfc46e054f45745" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.31", "time", ] [[package]] name = "slab" -version = "0.4.6" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.8.0" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.11.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -2645,6 +3914,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "sodiumoxide" version = "0.2.7" @@ -2672,6 +3951,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "sqlformat" version = "0.1.8" @@ -2727,7 +4016,7 @@ dependencies = [ "paste", "percent-encoding", "rustls 0.20.4", - "rustls-pemfile 1.0.0", + "rustls-pemfile", "serde", "serde_json", "sha2", @@ -2735,7 +4024,7 @@ dependencies = [ "sqlformat", "sqlx-rt", "stringprep", - "thiserror", + "thiserror 1.0.31", "tokio-stream", "url", "webpki-roots 0.22.4", @@ -2749,10 +4038,10 @@ checksum = "f40c63177cf23d356b159b60acd27c54af7423f1736988502e36bae9a712118f" dependencies = [ "dotenv", "either", - "heck", + "heck 0.4.0", "once_cell", "proc-macro2 1.0.93", - "quote 1.0.38", + "quote 1.0.45", "serde_json", "sha2", "sqlx-core", @@ -2772,6 +4061,12 @@ dependencies = [ "tokio-rustls 0.23.4", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2794,6 +4089,40 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "stun" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a512c5d501e3e3b5a4bb3e8e31462d56d54a66b95a28b8596e14422bf21c32b" +dependencies = [ + "base64 0.22.1", + "crc", + "lazy_static", + "md-5", + "rand 0.9.3", + "ring 0.17.14", + "subtle", + "thiserror 1.0.31", + "tokio", + "url", + "webrtc-util", +] + +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2818,18 +4147,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04066589568b72ec65f42d65a1a52436e954b168773148893c020269563decf2" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", + "quote 1.0.45", "unicode-xid 0.2.3", ] [[package]] name = "syn" -version = "2.0.96" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", + "quote 1.0.45", "unicode-ident", ] @@ -2839,6 +4168,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "sysinfo" version = "0.29.10" @@ -2860,7 +4200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2883,7 +4223,7 @@ dependencies = [ "cfg-if", "fastrand", "libc", - "redox_syscall", + "redox_syscall 0.2.13", "remove_dir_all", "winapi", ] @@ -2912,7 +4252,16 @@ version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.31", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -2922,28 +4271,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", + "quote 1.0.45", "syn 1.0.93", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "time" -version = "0.3.9" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ + "deranged", "itoa", "libc", + "num-conv", "num_threads", + "powerfmt", "quickcheck", + "serde_core", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] [[package]] name = "tinyvec" @@ -2962,31 +4347,30 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.43.0" +version = "1.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ - "backtrace", "bytes", "libc", "mio", "parking_lot 0.12.0", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.8", + "socket2 0.6.3", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", - "syn 2.0.96", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -3026,7 +4410,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.21", + "rustls 0.23.37", "tokio", ] @@ -3041,7 +4425,7 @@ dependencies = [ "futures-sink", "futures-util", "pin-project", - "thiserror", + "thiserror 1.0.31", "tokio", "tokio-util", ] @@ -3054,7 +4438,7 @@ checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", - "thiserror", + "thiserror 1.0.31", "tokio", ] @@ -3078,7 +4462,26 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.17.2", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.26.1", + "tungstenite 0.26.2", + "webpki-roots 0.26.7", ] [[package]] @@ -3168,7 +4571,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", + "http 0.2.7", "http-body", "http-range-header", "httpdate", @@ -3215,8 +4618,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", - "syn 2.0.96", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -3243,16 +4646,59 @@ dependencies = [ "base64 0.13.0", "byteorder", "bytes", - "http", + "http 0.2.7", "httparse", "log", - "rand", + "rand 0.8.5", "sha-1", - "thiserror", + "thiserror 1.0.31", "url", "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "native-tls", + "rand 0.9.3", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", + "rustls-pki-types", + "sha1", + "thiserror 2.0.18", + "utf-8", + "webpki-roots 0.26.7", +] + +[[package]] +name = "turn" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed995882f66ab94238de77c62e5e778389698ab700afa4696f4754da8f457cb" +dependencies = [ + "async-trait", + "base64 0.22.1", + "futures", + "log", + "md-5", + "portable-atomic", + "rand 0.9.3", + "ring 0.17.14", + "stun", + "thiserror 1.0.31", + "tokio", + "tokio-util", + "webrtc-util", +] + [[package]] name = "typenum" version = "1.15.0" @@ -3261,12 +4707,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicase" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" @@ -3319,6 +4762,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -3333,14 +4786,24 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.2.2" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", + "serde", +] + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", ] [[package]] @@ -3349,13 +4812,27 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" -version = "1.12.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom", + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -3376,6 +4853,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + [[package]] name = "walkdir" version = "2.3.2" @@ -3403,6 +4889,30 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[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", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -3424,8 +4934,8 @@ dependencies = [ "bumpalo", "log", "proc-macro2 1.0.93", - "quote 1.0.38", - "syn 2.0.96", + "quote 1.0.45", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -3448,7 +4958,7 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ - "quote 1.0.38", + "quote 1.0.45", "wasm-bindgen-macro-support", ] @@ -3459,8 +4969,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2 1.0.93", - "quote 1.0.38", - "syn 2.0.96", + "quote 1.0.45", + "syn 2.0.117", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3475,41 +4985,202 @@ dependencies = [ ] [[package]] -name = "web-sys" -version = "0.3.77" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ - "js-sys", - "wasm-bindgen", + "leb128fmt", + "wasmparser", ] [[package]] -name = "webpki" -version = "0.22.0" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "anyhow", + "indexmap 2.7.0", + "wasm-encoder", + "wasmparser", ] [[package]] -name = "webpki-roots" -version = "0.22.4" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "webpki", + "bitflags 2.11.0", + "hashbrown 0.15.2", + "indexmap 2.7.0", + "semver", ] [[package]] -name = "webpki-roots" -version = "0.25.4" +name = "wayland-backend" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - -[[package]] +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.11.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2 1.0.93", + "quick-xml", + "quote 1.0.45", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +dependencies = [ + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] name = "webpki-roots" version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3518,6 +5189,184 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webrtc" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08fd686c0920ac08f3a57eacc48e31f0e4ca1ffefba4478784606f78c14e83ad" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "dtls", + "hex", + "interceptor", + "lazy_static", + "log", + "portable-atomic", + "rand 0.9.3", + "rcgen", + "regex", + "ring 0.17.14", + "rtcp", + "rtp", + "sdp", + "serde", + "serde_json", + "sha2", + "smol_str", + "stun", + "thiserror 1.0.31", + "tokio", + "turn", + "unicase", + "url", + "waitgroup", + "webrtc-data", + "webrtc-ice", + "webrtc-mdns", + "webrtc-media", + "webrtc-sctp", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "webrtc-data" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062a5438d63bb0756a221693d76cc0dd6119affee1dfdfe57abe3a2a8c8b3eea" +dependencies = [ + "bytes", + "log", + "portable-atomic", + "thiserror 1.0.31", + "tokio", + "webrtc-sctp", + "webrtc-util", +] + +[[package]] +name = "webrtc-ice" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cb13fd1a373e68addc4bba0c8ca058627518e54342583d024bdcbb8ae5d97d" +dependencies = [ + "arc-swap", + "async-trait", + "crc", + "log", + "portable-atomic", + "rand 0.9.3", + "serde", + "serde_json", + "stun", + "thiserror 1.0.31", + "tokio", + "turn", + "url", + "uuid", + "waitgroup", + "webrtc-mdns", + "webrtc-util", +] + +[[package]] +name = "webrtc-mdns" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17279a067e75df72ce923fdeb7f04cd808f6f5aa4910dc6bcb4fbe66b396ace" +dependencies = [ + "log", + "socket2 0.5.8", + "thiserror 1.0.31", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-media" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a84c910fec0848fd5a0d8a5651e0ddbdedaf25a7d3ae3f0b15f71ac73a1773" +dependencies = [ + "byteorder", + "bytes", + "rand 0.9.3", + "rtp", + "thiserror 1.0.31", +] + +[[package]] +name = "webrtc-sctp" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f985465467d8910c1f8ac4382cd64f83b1f6a1a75021a82b221546f6fb3b856f" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "crc", + "log", + "portable-atomic", + "rand 0.9.3", + "thiserror 1.0.31", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-srtp" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d8cdc33413f1d0192670a80ce93d17cb78d57fe3a2414be30d6f6dff121123" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "byteorder", + "bytes", + "ctr", + "hmac", + "log", + "rtcp", + "rtp", + "sha1", + "subtle", + "thiserror 1.0.31", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-util" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c0c7e0c8f280f2bbfae442701465777ac07adaf46ce0c5863cd58e13fe472a" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bytes", + "ipnet", + "lazy_static", + "log", + "nix 0.26.4", + "portable-atomic", + "rand 0.9.3", + "thiserror 1.0.31", + "tokio", + "winapi", +] + [[package]] name = "which" version = "4.2.5" @@ -3531,11 +5380,12 @@ dependencies = [ [[package]] name = "whoami" -version = "1.2.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524b58fa5a20a2fb3014dd6358b70e6579692a56ef6fce928834e488f42f65e8" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "wasm-bindgen", + "libredox", + "wasite", "web-sys", ] @@ -3598,6 +5448,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.36.1" @@ -3611,6 +5467,15 @@ dependencies = [ "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3638,6 +5503,30 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3669,6 +5558,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3687,6 +5582,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3705,6 +5606,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3729,6 +5636,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3747,6 +5660,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3759,6 +5678,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3777,6 +5702,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3827,11 +5758,257 @@ dependencies = [ "windows-sys 0.48.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-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 2.7.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 1.0.93", + "quote 1.0.45", + "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.11.0", + "indexmap 2.7.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.7.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid 0.2.3", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.1", + "oid-registry", + "ring 0.17.14", + "rusticata-macros", + "thiserror 1.0.31", + "time", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.45", + "syn 2.0.117", +] [[package]] name = "zstd" diff --git a/db_v2.sqlite3 b/db_v2.sqlite3 index 93d9801b0ce72813f71240b9adf5642926bfc6c0..5e1c7814dfcca556036530cd4955da8ac6ed7325 100644 GIT binary patch delta 44 zcmZoTz}RqrQ6@OhC$l6~AuYcsH?c&)m_dMnk&(ecL4kpRL42Z&Go$#%gaz>cA*TzD delta 44 zcmZoTz}RqrQ6@OhC$l6~AuYcsH?c&)m_dMniHX5ML4kpRL2ROoGo#qXgaz>cA-xNZ diff --git a/libs/hbb_common b/libs/hbb_common index 83419b654..26aae293a 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 83419b6549636ee39dacef7776c473f5802e08d6 +Subproject commit 26aae293a87e93a01c7681ee428d8f7e1773db1b diff --git a/src/common.rs b/src/common.rs index 9c76f41b8..2a611908f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2,15 +2,41 @@ use clap::App; use hbb_common::{ allow_err, anyhow::{Context, Result}, get_version_number, log, tokio, ResultType }; +use http::HeaderMap; use ini::Ini; +use once_cell::sync::Lazy; use sodiumoxide::crypto::sign; use std::{ + collections::HashMap, io::prelude::*, io::Read, - net::SocketAddr, + net::{IpAddr, SocketAddr}, + sync::Mutex, time::{Instant, SystemTime}, }; +const TRUST_PROXY_HEADERS_ENV: &str = "TRUST_PROXY_HEADERS"; +const CONN_RATE_WINDOW_SECONDS_ENV: &str = "CONNECTION_RATE_WINDOW_SECONDS"; +const MAX_CONN_PER_IP_PER_WINDOW_ENV: &str = "MAX_CONNECTIONS_PER_IP_PER_WINDOW"; +const UDP_RATE_WINDOW_SECONDS_ENV: &str = "UDP_RATE_WINDOW_SECONDS"; +const MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV: &str = "MAX_UDP_PACKETS_PER_IP_PER_WINDOW"; +const DEFAULT_CONN_RATE_WINDOW_SECONDS: usize = 60; +const DEFAULT_MAX_CONN_PER_IP_PER_WINDOW: usize = 120; +const DEFAULT_UDP_RATE_WINDOW_SECONDS: usize = 60; +const DEFAULT_MAX_UDP_PACKETS_PER_IP_PER_WINDOW: usize = 240; + +#[derive(Clone, Copy)] +struct ConnectionRateEntry { + window_started_at: Instant, + last_seen_at: Instant, + count: usize, +} + +static CONNECTION_RATE_LIMITS: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); +static PROTECTION_STATS: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); + #[allow(dead_code)] pub(crate) fn get_expired_time() -> Instant { let now = Instant::now(); @@ -95,6 +121,175 @@ pub fn get_arg_or(name: &str, default: String) -> String { std::env::var(arg_name(name)).unwrap_or(default) } +#[allow(dead_code)] +pub fn trust_proxy_headers() -> bool { + matches!( + std::env::var(TRUST_PROXY_HEADERS_ENV) + .unwrap_or_default() + .trim() + .to_ascii_lowercase() + .as_str(), + "y" | "yes" | "true" | "1" + ) +} + +fn env_usize_or(name: &str, default: usize) -> usize { + std::env::var(name) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(default) +} + +fn conn_rate_window_seconds() -> usize { + env_usize_or( + CONN_RATE_WINDOW_SECONDS_ENV, + DEFAULT_CONN_RATE_WINDOW_SECONDS, + ) +} + +fn max_conn_per_ip_per_window() -> usize { + env_usize_or( + MAX_CONN_PER_IP_PER_WINDOW_ENV, + DEFAULT_MAX_CONN_PER_IP_PER_WINDOW, + ) +} + +fn udp_rate_window_seconds() -> usize { + env_usize_or( + UDP_RATE_WINDOW_SECONDS_ENV, + DEFAULT_UDP_RATE_WINDOW_SECONDS, + ) +} + +fn max_udp_packets_per_ip_per_window() -> usize { + env_usize_or( + MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, + DEFAULT_MAX_UDP_PACKETS_PER_IP_PER_WINDOW, + ) +} + +fn prune_connection_rate_limits( + entries: &mut HashMap, + now: Instant, + window_secs: usize, +) { + entries.retain(|_, entry| { + now.duration_since(entry.last_seen_at).as_secs() < (window_secs * 2) as u64 + }); +} + +#[allow(dead_code)] +fn allow_ip_activity(scope: &str, addr: SocketAddr, window_secs: usize, max_events: usize) -> bool { + if addr.ip().is_loopback() { + return true; + } + let now = Instant::now(); + let mut lock = CONNECTION_RATE_LIMITS.lock().unwrap(); + prune_connection_rate_limits(&mut lock, now, window_secs); + let key = format!("{scope}|{}", addr.ip()); + let entry = lock.entry(key).or_insert(ConnectionRateEntry { + window_started_at: now, + last_seen_at: now, + count: 0, + }); + if now.duration_since(entry.window_started_at).as_secs() >= window_secs as u64 { + entry.window_started_at = now; + entry.count = 0; + } + entry.last_seen_at = now; + if entry.count >= max_events { + return false; + } + entry.count += 1; + true +} + +#[allow(dead_code)] +pub fn allow_connection_from_ip(scope: &str, addr: SocketAddr) -> bool { + let allowed = allow_ip_activity( + scope, + addr, + conn_rate_window_seconds(), + max_conn_per_ip_per_window(), + ); + if !allowed { + record_protection_event("connection_rate_limit_hits"); + } + allowed +} + +#[allow(dead_code)] +pub fn allow_udp_packet_from_ip(scope: &str, addr: SocketAddr) -> bool { + let allowed = allow_ip_activity( + scope, + addr, + udp_rate_window_seconds(), + max_udp_packets_per_ip_per_window(), + ); + if !allowed { + record_protection_event("udp_rate_limit_hits"); + } + allowed +} + +#[allow(dead_code)] +pub fn record_protection_event(name: &'static str) { + let mut lock = PROTECTION_STATS.lock().unwrap(); + *lock.entry(name).or_insert(0) += 1; +} + +#[allow(dead_code)] +pub fn protection_stats_snapshot() -> Vec<(String, u64)> { + let mut entries: Vec<(String, u64)> = PROTECTION_STATS + .lock() + .unwrap() + .iter() + .map(|(name, value)| ((*name).to_owned(), *value)) + .collect(); + entries.sort_by(|a, b| a.0.cmp(&b.0)); + entries +} + +#[allow(dead_code)] +pub fn protection_limits_summary() -> Vec { + vec![ + format!( + "connections_per_ip_per_window={}/{}s", + max_conn_per_ip_per_window(), + conn_rate_window_seconds() + ), + format!( + "udp_packets_per_ip_per_window={}/{}s", + max_udp_packets_per_ip_per_window(), + udp_rate_window_seconds() + ), + format!("trust_proxy_headers={}", trust_proxy_headers()), + ] +} + +#[allow(dead_code)] +pub fn apply_trusted_proxy_addr(addr: SocketAddr, headers: &HeaderMap) -> SocketAddr { + if !trust_proxy_headers() { + return addr; + } + let forwarded_ip = headers + .get("X-Real-IP") + .or_else(|| headers.get("X-Forwarded-For")) + .and_then(|header_value| header_value.to_str().ok()) + .and_then(parse_forwarded_ip); + forwarded_ip.map(|ip| SocketAddr::new(ip, 0)).unwrap_or(addr) +} + +fn parse_forwarded_ip(value: &str) -> Option { + value + .split(',') + .next() + .map(str::trim) + .filter(|value| !value.is_empty()) + .and_then(|value| value.parse::().ok()) +} + #[allow(dead_code)] #[inline] pub fn now() -> u64 { @@ -215,4 +410,116 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { log::info!("new version is available: {}", latest_release_version); } Ok(()) -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use super::{ + allow_connection_from_ip, allow_udp_packet_from_ip, apply_trusted_proxy_addr, + conn_rate_window_seconds, max_conn_per_ip_per_window, max_udp_packets_per_ip_per_window, + protection_limits_summary, protection_stats_snapshot, record_protection_event, + trust_proxy_headers, udp_rate_window_seconds, CONNECTION_RATE_LIMITS, + CONN_RATE_WINDOW_SECONDS_ENV, MAX_CONN_PER_IP_PER_WINDOW_ENV, + MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, PROTECTION_STATS, TRUST_PROXY_HEADERS_ENV, + UDP_RATE_WINDOW_SECONDS_ENV, + }; + use http::HeaderMap; + use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::Mutex, + }; + + static TEST_PROXY_HEADERS_LOCK: Mutex<()> = Mutex::new(()); + + #[test] + fn trusted_proxy_headers_are_disabled_by_default() { + let _guard = TEST_PROXY_HEADERS_LOCK.lock().unwrap(); + std::env::remove_var(TRUST_PROXY_HEADERS_ENV); + assert!(!trust_proxy_headers()); + } + + #[test] + fn apply_trusted_proxy_addr_only_changes_addr_when_enabled() { + let _guard = TEST_PROXY_HEADERS_LOCK.lock().unwrap(); + let original = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 4)), 21117); + let mut headers = HeaderMap::new(); + headers.insert("X-Forwarded-For", "198.51.100.10, 10.0.0.1".parse().unwrap()); + + std::env::remove_var(TRUST_PROXY_HEADERS_ENV); + assert_eq!(apply_trusted_proxy_addr(original, &headers), original); + + std::env::set_var(TRUST_PROXY_HEADERS_ENV, "Y"); + assert_eq!( + apply_trusted_proxy_addr(original, &headers), + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 10)), 0) + ); + + std::env::remove_var(TRUST_PROXY_HEADERS_ENV); + } + + #[test] + fn connection_rate_limiter_enforces_per_ip_window_and_exempts_loopback() { + let _guard = TEST_PROXY_HEADERS_LOCK.lock().unwrap(); + CONNECTION_RATE_LIMITS.lock().unwrap().clear(); + PROTECTION_STATS.lock().unwrap().clear(); + std::env::set_var(MAX_CONN_PER_IP_PER_WINDOW_ENV, "2"); + std::env::set_var(CONN_RATE_WINDOW_SECONDS_ENV, "60"); + std::env::set_var(MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, "3"); + std::env::set_var(UDP_RATE_WINDOW_SECONDS_ENV, "60"); + + let remote = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 10)), 21117); + assert!(allow_connection_from_ip("hbbs-main", remote)); + assert!(allow_connection_from_ip("hbbs-main", remote)); + assert!(!allow_connection_from_ip("hbbs-main", remote)); + assert!(allow_udp_packet_from_ip("hbbs-udp", remote)); + assert!(allow_udp_packet_from_ip("hbbs-udp", remote)); + assert!(allow_udp_packet_from_ip("hbbs-udp", remote)); + assert!(!allow_udp_packet_from_ip("hbbs-udp", remote)); + + let loopback = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 21117); + assert!(allow_connection_from_ip("hbbs-main", loopback)); + assert!(allow_connection_from_ip("hbbs-main", loopback)); + assert!(allow_connection_from_ip("hbbs-main", loopback)); + assert_eq!( + protection_stats_snapshot(), + vec![ + ("connection_rate_limit_hits".to_owned(), 1), + ("udp_rate_limit_hits".to_owned(), 1), + ] + ); + record_protection_event("peer_records_pruned"); + assert_eq!( + protection_stats_snapshot(), + vec![ + ("connection_rate_limit_hits".to_owned(), 1), + ("peer_records_pruned".to_owned(), 1), + ("udp_rate_limit_hits".to_owned(), 1), + ] + ); + + std::env::remove_var(MAX_CONN_PER_IP_PER_WINDOW_ENV); + std::env::remove_var(CONN_RATE_WINDOW_SECONDS_ENV); + std::env::remove_var(MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV); + std::env::remove_var(UDP_RATE_WINDOW_SECONDS_ENV); + CONNECTION_RATE_LIMITS.lock().unwrap().clear(); + PROTECTION_STATS.lock().unwrap().clear(); + assert_eq!(max_conn_per_ip_per_window(), super::DEFAULT_MAX_CONN_PER_IP_PER_WINDOW); + assert_eq!(conn_rate_window_seconds(), super::DEFAULT_CONN_RATE_WINDOW_SECONDS); + assert_eq!( + max_udp_packets_per_ip_per_window(), + super::DEFAULT_MAX_UDP_PACKETS_PER_IP_PER_WINDOW + ); + assert_eq!( + udp_rate_window_seconds(), + super::DEFAULT_UDP_RATE_WINDOW_SECONDS + ); + assert_eq!( + protection_limits_summary(), + vec![ + "connections_per_ip_per_window=120/60s".to_owned(), + "udp_packets_per_ip_per_window=240/60s".to_owned(), + "trust_proxy_headers=false".to_owned(), + ] + ); + } +} diff --git a/src/database.rs b/src/database.rs index fa1b6edcf..9ed6355c3 100644 --- a/src/database.rs +++ b/src/database.rs @@ -8,6 +8,8 @@ use std::{ops::DerefMut, str::FromStr}; //use sqlx::mysql::MySqlPoolOptions; type Pool = deadpool::managed::Pool; +const DEFAULT_MAX_TOTAL_PEER_RECORDS: usize = 100_000; +const DEFAULT_PEER_RECORD_RETENTION_DAYS: usize = 180; pub struct DbPool { url: String, @@ -33,6 +35,33 @@ impl deadpool::managed::Manager for DbPool { #[derive(Clone)] pub struct Database { pool: Pool, + max_total_peers: usize, + peer_record_retention_days: usize, +} + +pub enum InsertPeerResult { + Inserted(Vec), + PeerLimitReached, +} + +fn env_usize_or(name: &str, default: usize) -> usize { + std::env::var(name) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(default) +} + +fn peer_limit_reached(total_peers: i64, max_total_peers: usize) -> bool { + max_total_peers > 0 && total_peers >= max_total_peers as i64 +} + +fn peer_retention_prune_enabled(days: usize) -> bool { + days > 0 +} + +fn peer_retention_cutoff_arg(days: usize) -> String { + format!("-{days} days") } #[derive(Default)] @@ -56,6 +85,13 @@ impl Database { .parse() .unwrap_or(1); log::debug!("MAX_DATABASE_CONNECTIONS={}", n); + let max_total_peers = env_usize_or("MAX_TOTAL_PEER_RECORDS", DEFAULT_MAX_TOTAL_PEER_RECORDS); + let peer_record_retention_days = env_usize_or( + "PEER_RECORD_RETENTION_DAYS", + DEFAULT_PEER_RECORD_RETENTION_DAYS, + ); + log::info!("MAX_TOTAL_PEER_RECORDS={}", max_total_peers); + log::info!("PEER_RECORD_RETENTION_DAYS={}", peer_record_retention_days); let pool = Pool::new( DbPool { url: url.to_owned(), @@ -63,7 +99,11 @@ impl Database { n, ); let _ = pool.get().await?; // test - let db = Database { pool }; + let db = Database { + pool, + max_total_peers, + peer_record_retention_days, + }; db.create_tables().await?; Ok(db) } @@ -109,7 +149,21 @@ impl Database { uuid: &[u8], pk: &[u8], info: &str, - ) -> ResultType> { + ) -> ResultType { + if peer_limit_reached(self.peer_count().await?, self.max_total_peers) { + if peer_retention_prune_enabled(self.peer_record_retention_days) { + let deleted = self.prune_old_peer_records().await?; + if deleted > 0 { + crate::common::record_protection_event("peer_records_pruned"); + log::info!("pruned {} old peer records before inserting {}", deleted, id); + } + } + } + if peer_limit_reached(self.peer_count().await?, self.max_total_peers) { + crate::common::record_protection_event("peer_limit_reached"); + log::warn!("peer record limit reached, rejecting new peer {}", id); + return Ok(InsertPeerResult::PeerLimitReached); + } let guid = uuid::Uuid::new_v4().as_bytes().to_vec(); sqlx::query!( "insert into peer(guid, id, uuid, pk, info) values(?, ?, ?, ?, ?)", @@ -121,7 +175,7 @@ impl Database { ) .execute(self.pool.get().await?.deref_mut()) .await?; - Ok(guid) + Ok(InsertPeerResult::Inserted(guid)) } pub async fn update_pk( @@ -142,16 +196,98 @@ impl Database { .await?; Ok(()) } + + async fn peer_count(&self) -> ResultType { + let row = sqlx::query!("select count(*) as count from peer") + .fetch_one(self.pool.get().await?.deref_mut()) + .await?; + Ok(row.count as i64) + } + + async fn prune_old_peer_records(&self) -> ResultType { + let cutoff = peer_retention_cutoff_arg(self.peer_record_retention_days); + let result = sqlx::query("delete from peer where created_at < datetime('now', ?)") + .bind(cutoff) + .execute(self.pool.get().await?.deref_mut()) + .await?; + Ok(result.rows_affected()) + } } #[cfg(test)] mod tests { + use super::{peer_limit_reached, peer_retention_cutoff_arg, peer_retention_prune_enabled}; use hbb_common::tokio; + use sqlx::Connection as _; + use std::{path::PathBuf, time::{SystemTime, UNIX_EPOCH}}; + + #[test] + fn peer_limit_helper_rejects_when_total_reaches_cap() { + assert!(!peer_limit_reached(99, 100)); + assert!(peer_limit_reached(100, 100)); + assert!(peer_limit_reached(101, 100)); + } + + #[test] + fn peer_retention_helpers_use_configured_day_window() { + assert!(peer_retention_prune_enabled(1)); + assert_eq!(peer_retention_cutoff_arg(180), "-180 days"); + } + + #[test] + fn insert_peer_prunes_expired_records_before_enforcing_cap() { + insert_peer_prunes_expired_records_before_enforcing_cap_(); + } + #[test] fn test_insert() { insert(); } + fn temp_db_path(name: &str) -> PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + std::env::temp_dir().join(format!("rustdesk-server-{name}-{unique}.sqlite")) + } + + #[tokio::main(flavor = "multi_thread")] + async fn insert_peer_prunes_expired_records_before_enforcing_cap_() { + let path = temp_db_path("peer-prune"); + let path_str = path.to_string_lossy().to_string(); + let mut db = super::Database::new(&path_str).await.unwrap(); + db.max_total_peers = 1; + db.peer_record_retention_days = 1; + + let guid = uuid::Uuid::new_v4().as_bytes().to_vec(); + let empty_uuid = Vec::::new(); + let empty_pk = Vec::::new(); + let mut conn = sqlx::SqliteConnection::connect(&path_str).await.unwrap(); + sqlx::query!( + "insert into peer(guid, id, uuid, pk, created_at, info) values(?, ?, ?, ?, datetime('now', '-2 days'), ?)", + guid, + "old-peer", + empty_uuid, + empty_pk, + "" + ) + .execute(&mut conn) + .await + .unwrap(); + + let result = db + .insert_peer("new-peer", &Vec::::new(), &Vec::::new(), "") + .await + .unwrap(); + assert!(matches!(result, super::InsertPeerResult::Inserted(_))); + assert_eq!(db.peer_count().await.unwrap(), 1); + assert!(db.get_peer("old-peer").await.unwrap().is_none()); + assert!(db.get_peer("new-peer").await.unwrap().is_some()); + + std::fs::remove_file(path).ok(); + } + #[tokio::main(flavor = "multi_thread")] async fn insert() { let db = super::Database::new("test.sqlite3").await.unwrap(); diff --git a/src/peer.rs b/src/peer.rs index 4ca87cfad..dd25b30dd 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -22,6 +22,9 @@ pub const IP_CHANGE_DUR: u64 = 180; pub const IP_CHANGE_DUR_X2: u64 = IP_CHANGE_DUR * 2; pub const DAY_SECONDS: u64 = 3600 * 24; pub const IP_BLOCK_DUR: u64 = 60; +const DEFAULT_MAX_PEER_CACHE_SIZE: usize = 16_384; +const DEFAULT_MAX_PENDING_REGISTRATIONS_PER_IP: usize = 64; +const PEER_CACHE_INACTIVE_TIMEOUT_MS: i32 = 30_000; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub(crate) struct PeerInfo { @@ -63,6 +66,43 @@ pub(crate) type LockPeer = Arc>; pub(crate) struct PeerMap { map: Arc>>, pub(crate) db: database::Database, + max_cached_peers: usize, + max_pending_registrations_per_ip: usize, +} + +struct PeerEvictionEntry { + id: String, + inactive: bool, + last_reg_time: Instant, +} + +fn env_usize_or(name: &str, default: usize) -> usize { + std::env::var(name) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(default) +} + +fn pending_registration_limit_exceeded(pending_for_ip: usize, max_pending: usize) -> bool { + max_pending > 0 && pending_for_ip >= max_pending +} + +fn select_peer_ids_to_evict( + entries: Vec, + max_cached_peers: usize, +) -> Vec { + if max_cached_peers == 0 || entries.len() < max_cached_peers { + return vec![]; + } + let to_remove = entries.len() + 1 - max_cached_peers; + let mut entries = entries; + entries.sort_by_key(|entry| (!entry.inactive, entry.last_reg_time)); + entries + .into_iter() + .take(to_remove) + .map(|entry| entry.id) + .collect() } impl PeerMap { @@ -82,9 +122,22 @@ impl PeerMap { db }); log::info!("DB_URL={}", db); + let max_cached_peers = + env_usize_or("MAX_PEER_CACHE_SIZE", DEFAULT_MAX_PEER_CACHE_SIZE); + let max_pending_registrations_per_ip = env_usize_or( + "MAX_PENDING_REGISTRATIONS_PER_IP", + DEFAULT_MAX_PENDING_REGISTRATIONS_PER_IP, + ); + log::info!("MAX_PEER_CACHE_SIZE={}", max_cached_peers); + log::info!( + "MAX_PENDING_REGISTRATIONS_PER_IP={}", + max_pending_registrations_per_ip + ); let pm = Self { map: Default::default(), db: database::Database::new(&db).await?, + max_cached_peers, + max_pending_registrations_per_ip, }; Ok(pm) } @@ -118,9 +171,12 @@ impl PeerMap { log::error!("db.insert_peer failed: {}", err); return register_pk_response::Result::SERVER_ERROR; } - Ok(guid) => { + Ok(database::InsertPeerResult::Inserted(guid)) => { peer.write().await.guid = guid; } + Ok(database::InsertPeerResult::PeerLimitReached) => { + return register_pk_response::Result::PEER_LIMIT_REACHED; + } } } else { if let Err(err) = self.db.update_pk(&guid, &id, &pk, &info_str).await { @@ -138,6 +194,7 @@ impl PeerMap { if p.is_some() { return p; } else if let Ok(Some(v)) = self.db.get_peer(id).await { + self.prune_cache_for_insert().await; let peer = Peer { guid: v.guid, uuid: v.uuid.into(), @@ -154,18 +211,24 @@ impl PeerMap { None } - #[inline] - pub(crate) async fn get_or(&self, id: &str) -> LockPeer { + pub(crate) async fn get_or_for_registration(&self, id: &str, ip: &str) -> Option { if let Some(p) = self.get(id).await { - return p; + return Some(p); + } + if !self.can_cache_pending_registration(ip).await { + return None; } + self.prune_cache_for_insert().await; let mut w = self.map.write().await; if let Some(p) = w.get(id) { - return p.clone(); + return Some(p.clone()); } - let tmp = LockPeer::default(); + let tmp = Arc::new(RwLock::new(Peer { + info: PeerInfo { ip: ip.to_owned() }, + ..Default::default() + })); w.insert(id.to_owned(), tmp.clone()); - tmp + Some(tmp) } #[inline] @@ -177,4 +240,90 @@ impl PeerMap { pub(crate) async fn is_in_memory(&self, id: &str) -> bool { self.map.read().await.contains_key(id) } + + async fn can_cache_pending_registration(&self, ip: &str) -> bool { + let snapshot: Vec = self.map.read().await.values().cloned().collect(); + let mut pending_for_ip = 0usize; + for peer in snapshot { + let peer = peer.read().await; + if peer.info.ip == ip && peer.guid.is_empty() { + pending_for_ip += 1; + } + } + !pending_registration_limit_exceeded( + pending_for_ip, + self.max_pending_registrations_per_ip, + ) + } + + async fn prune_cache_for_insert(&self) { + let snapshot: Vec<(String, LockPeer)> = self + .map + .read() + .await + .iter() + .map(|(id, peer)| (id.clone(), peer.clone())) + .collect(); + let mut entries = Vec::with_capacity(snapshot.len()); + for (id, peer) in snapshot { + let peer = peer.read().await; + entries.push(PeerEvictionEntry { + id, + inactive: peer.last_reg_time.elapsed().as_millis() as i32 + >= PEER_CACHE_INACTIVE_TIMEOUT_MS, + last_reg_time: peer.last_reg_time, + }); + } + let remove_ids = select_peer_ids_to_evict(entries, self.max_cached_peers); + if remove_ids.is_empty() { + return; + } + let mut map = self.map.write().await; + for id in remove_ids { + map.remove(&id); + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + pending_registration_limit_exceeded, select_peer_ids_to_evict, PeerEvictionEntry, + }; + use std::time::{Duration, Instant}; + + #[test] + fn pending_registration_limit_flags_excessive_per_ip_growth() { + assert!(!pending_registration_limit_exceeded(0, 64)); + assert!(!pending_registration_limit_exceeded(63, 64)); + assert!(pending_registration_limit_exceeded(64, 64)); + } + + #[test] + fn peer_cache_eviction_prefers_inactive_then_oldest_entries() { + let now = Instant::now(); + let old = now.checked_sub(Duration::from_secs(120)).unwrap_or(now); + let newer = now.checked_sub(Duration::from_secs(30)).unwrap_or(now); + let remove = select_peer_ids_to_evict( + vec![ + PeerEvictionEntry { + id: "inactive-old".to_owned(), + inactive: true, + last_reg_time: old, + }, + PeerEvictionEntry { + id: "active-old".to_owned(), + inactive: false, + last_reg_time: old, + }, + PeerEvictionEntry { + id: "inactive-new".to_owned(), + inactive: true, + last_reg_time: newer, + }, + ], + 2, + ); + assert_eq!(remove, vec!["inactive-old".to_owned(), "inactive-new".to_owned()]); + } } diff --git a/src/relay_server.rs b/src/relay_server.rs index 0ec190ace..c8eb83392 100644 --- a/src/relay_server.rs +++ b/src/relay_server.rs @@ -26,12 +26,29 @@ use std::{ io::Error, net::SocketAddr, sync::atomic::{AtomicUsize, Ordering}, + time::Instant, }; type Usage = (usize, usize, usize, usize); +struct PendingRelay { + stream: Box, + ip: String, + created_at: Instant, +} + +impl PendingRelay { + fn new(stream: Box, ip: String) -> Self { + Self { + stream, + ip, + created_at: Instant::now(), + } + } +} + lazy_static::lazy_static! { - static ref PEERS: Mutex>> = Default::default(); + static ref PEERS: Mutex> = Default::default(); static ref USAGE: RwLock> = Default::default(); static ref BLACKLIST: RwLock> = Default::default(); static ref BLOCKLIST: RwLock> = Default::default(); @@ -42,8 +59,11 @@ static DOWNGRADE_START_CHECK: AtomicUsize = AtomicUsize::new(1_800_000); // in m static LIMIT_SPEED: AtomicUsize = AtomicUsize::new(32 * 1024 * 1024); // in bit/s static TOTAL_BANDWIDTH: AtomicUsize = AtomicUsize::new(1024 * 1024 * 1024); // in bit/s static SINGLE_BANDWIDTH: AtomicUsize = AtomicUsize::new(128 * 1024 * 1024); // in bit/s +static MAX_PENDING_RELAYS: AtomicUsize = AtomicUsize::new(4096); +static MAX_PENDING_RELAYS_PER_IP: AtomicUsize = AtomicUsize::new(64); const BLACKLIST_FILE: &str = "blacklist.txt"; const BLOCKLIST_FILE: &str = "blocklist.txt"; +const PENDING_RELAY_HOLD_SECS: u64 = 30; #[tokio::main(flavor = "multi_thread")] pub async fn start(port: &str, key: &str) -> ResultType<()> { @@ -146,7 +166,27 @@ fn check_params() { log::info!( "SINGLE_BANDWIDTH: {}Mb/s", SINGLE_BANDWIDTH.load(Ordering::SeqCst) as f64 / 1024. / 1024. - ) + ); + let tmp = std::env::var("MAX_PENDING_RELAYS") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + if tmp > 0 { + MAX_PENDING_RELAYS.store(tmp, Ordering::SeqCst); + } + log::info!( + "MAX_PENDING_RELAYS: {}", + MAX_PENDING_RELAYS.load(Ordering::SeqCst) + ); + let tmp = std::env::var("MAX_PENDING_RELAYS_PER_IP") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + if tmp > 0 { + MAX_PENDING_RELAYS_PER_IP.store(tmp, Ordering::SeqCst); + } + log::info!( + "MAX_PENDING_RELAYS_PER_IP: {}", + MAX_PENDING_RELAYS_PER_IP.load(Ordering::SeqCst) + ); } async fn check_cmd(cmd: &str, limiter: Limiter) -> String { @@ -157,7 +197,7 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String { match fds.next() { Some("h") => { res = format!( - "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", "blacklist-add(ba) ", "blacklist-remove(br) ", "blacklist(b) ", @@ -169,6 +209,7 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String { "limit-speed(ls) [value(Mb/s)]", "total-bandwidth(tb) [value(Mb/s)]", "single-bandwidth(sb) [value(Mb/s)]", + "protection-stats(ps)", "usage(u)" ) } @@ -318,6 +359,14 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String { ); } } + Some("protection-stats" | "ps") => { + for line in crate::common::protection_limits_summary() { + let _ = writeln!(res, "{line}"); + } + for (name, value) in crate::common::protection_stats_snapshot() { + let _ = writeln!(res, "{name}={value}"); + } + } _ => {} } res @@ -331,6 +380,10 @@ async fn io_loop(listener: TcpListener, listener2: TcpListener, key: &str) { res = listener.accept() => { match res { Ok((stream, addr)) => { + if !crate::common::allow_connection_from_ip("hbbr-tcp", addr) { + log::warn!("Rate limit exceeded for hbbr-tcp from {}", addr.ip()); + continue; + } stream.set_nodelay(true).ok(); handle_connection(stream, addr, &limiter, key, false).await; } @@ -343,6 +396,10 @@ async fn io_loop(listener: TcpListener, listener2: TcpListener, key: &str) { res = listener2.accept() => { match res { Ok((stream, addr)) => { + if !crate::common::allow_connection_from_ip("hbbr-ws", addr) { + log::warn!("Rate limit exceeded for hbbr-ws from {}", addr.ip()); + continue; + } stream.set_nodelay(true).ok(); handle_connection(stream, addr, &limiter, key, true).await; } @@ -400,18 +457,7 @@ async fn make_pair( if ws { use tokio_tungstenite::tungstenite::handshake::server::{Request, Response}; let callback = |req: &Request, response: Response| { - let headers = req.headers(); - let real_ip = headers - .get("X-Real-IP") - .or_else(|| headers.get("X-Forwarded-For")) - .and_then(|header_value| header_value.to_str().ok()); - if let Some(ip) = real_ip { - if ip.contains('.') { - addr = format!("{ip}:0").parse().unwrap_or(addr); - } else { - addr = format!("[{ip}]:0").parse().unwrap_or(addr); - } - } + addr = crate::common::apply_trusted_proxy_addr(addr, req.headers()); Ok(response) }; let ws_stream = tokio_tungstenite::accept_hdr_async(stream, callback).await?; @@ -429,14 +475,16 @@ async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limit if let Some(rendezvous_message::Union::RequestRelay(rf)) = msg_in.union { if !key.is_empty() && rf.licence_key != key { log::warn!("Relay authentication failed from {} - invalid key", addr); + send_relay_refuse(&mut stream, "Key mismatch").await; return; } if !rf.uuid.is_empty() { - let mut peer = PEERS.lock().await.remove(&rf.uuid); - if let Some(peer) = peer.as_mut() { + let mut pending = PEERS.lock().await.remove(&rf.uuid); + if let Some(pending) = pending.as_mut() { log::info!("Relayrequest {} from {} got paired", rf.uuid, addr); let id = format!("{}:{}", addr.ip(), addr.port()); USAGE.write().await.insert(id.clone(), Default::default()); + let peer = &mut pending.stream; if !stream.is_ws() && !peer.is_ws() { peer.set_raw(); stream.set_raw(); @@ -451,8 +499,27 @@ async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limit USAGE.write().await.remove(&id); } else { log::info!("New relay request {} from {}", rf.uuid, addr); - PEERS.lock().await.insert(rf.uuid.clone(), Box::new(stream)); - sleep(30.).await; + let ip = addr.ip().to_string(); + let mut peers = PEERS.lock().await; + prune_expired_pending_relays(&mut peers); + let pending_for_ip = peers.values().filter(|peer| peer.ip == ip).count(); + if let Some(reason) = + pending_relay_limit_reason(peers.len(), pending_for_ip) + { + crate::common::record_protection_event("relay_pending_rejected"); + log::warn!( + "Rejecting relay request {} from {}: {}", + rf.uuid, + addr, + reason + ); + drop(peers); + send_relay_refuse(&mut stream, reason).await; + return; + } + peers.insert(rf.uuid.clone(), PendingRelay::new(Box::new(stream), ip)); + drop(peers); + sleep(PENDING_RELAY_HOLD_SECS as f32).await; PEERS.lock().await.remove(&rf.uuid); } } @@ -461,6 +528,31 @@ async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limit } } +fn prune_expired_pending_relays(peers: &mut HashMap) { + peers.retain(|_, pending| pending.created_at.elapsed().as_secs() < PENDING_RELAY_HOLD_SECS); +} + +fn pending_relay_limit_reason(total_pending: usize, pending_for_ip: usize) -> Option<&'static str> { + if total_pending >= MAX_PENDING_RELAYS.load(Ordering::SeqCst) { + return Some("Relay server busy"); + } + if pending_for_ip >= MAX_PENDING_RELAYS_PER_IP.load(Ordering::SeqCst) { + return Some("Too many pending relay requests"); + } + None +} + +async fn send_relay_refuse(stream: &mut impl StreamTrait, reason: &str) { + let mut msg_out = RendezvousMessage::new(); + msg_out.set_relay_response(RelayResponse { + refuse_reason: reason.to_owned(), + ..Default::default() + }); + if let Ok(bytes) = msg_out.write_to_bytes() { + let _ = stream.send_raw(bytes.into()).await; + } +} + async fn relay( addr: SocketAddr, stream: &mut impl StreamTrait, @@ -645,3 +737,34 @@ impl StreamTrait for tokio_tungstenite::WebSocketStream { fn set_raw(&mut self) {} } + +#[cfg(test)] +mod tests { + use super::{pending_relay_limit_reason, MAX_PENDING_RELAYS, MAX_PENDING_RELAYS_PER_IP}; + use std::sync::{atomic::Ordering, Mutex}; + + static TEST_RELAY_LIMITS_LOCK: Mutex<()> = Mutex::new(()); + + #[test] + fn pending_relay_limits_enforce_global_and_per_ip_caps() { + let _guard = TEST_RELAY_LIMITS_LOCK.lock().unwrap(); + let saved_total = MAX_PENDING_RELAYS.load(Ordering::SeqCst); + let saved_per_ip = MAX_PENDING_RELAYS_PER_IP.load(Ordering::SeqCst); + + MAX_PENDING_RELAYS.store(2, Ordering::SeqCst); + MAX_PENDING_RELAYS_PER_IP.store(1, Ordering::SeqCst); + + assert_eq!(pending_relay_limit_reason(0, 0), None); + assert_eq!( + pending_relay_limit_reason(1, 1), + Some("Too many pending relay requests") + ); + assert_eq!( + pending_relay_limit_reason(2, 0), + Some("Relay server busy") + ); + + MAX_PENDING_RELAYS.store(saved_total, Ordering::SeqCst); + MAX_PENDING_RELAYS_PER_IP.store(saved_per_ip, Ordering::SeqCst); + } +} diff --git a/src/rendezvous_server.rs b/src/rendezvous_server.rs index ff68441b6..a23106e05 100644 --- a/src/rendezvous_server.rs +++ b/src/rendezvous_server.rs @@ -13,7 +13,9 @@ use hbb_common::{ log, protobuf::{Message as _, MessageField}, rendezvous_proto::{ - register_pk_response::Result::{TOO_FREQUENT, UUID_MISMATCH}, + register_pk_response::Result::{ + INVALID_ID_FORMAT, LICENSE_MISMATCH, TOO_FREQUENT, UUID_MISMATCH, + }, *, }, tcp::{listen_any, FramedStream}, @@ -48,6 +50,8 @@ enum Data { } const REG_TIMEOUT: i32 = 30_000; +const MIN_REGISTRATION_ID_LEN: usize = 6; +const MAX_REGISTRATION_ID_LEN: usize = 100; type TcpStreamSink = SplitSink, Bytes>; type WsSink = SplitSink, tungstenite::Message>; enum Sink { @@ -68,6 +72,8 @@ use tokio::sync::Mutex as TokioMutex; // differentiate if needed struct PunchReqEntry { tm: Instant, from_ip: String, to_ip: String, to_id: String } static PUNCH_REQS: Lazy>> = Lazy::new(|| TokioMutex::new(Vec::new())); const PUNCH_REQ_DEDUPE_SEC: u64 = 60; +const PUNCH_REQ_RETENTION_SECS: u64 = 600; +const MAX_PUNCH_REQS: usize = 8192; #[derive(Clone)] struct Inner { @@ -172,12 +178,13 @@ impl RendezvousServer { } else { test_addr.parse()? }; + let test_key = key.to_owned(); tokio::spawn(async move { - if let Err(err) = test_hbbs(test_addr).await { + if let Err(err) = test_hbbs(test_addr, test_key.clone()).await { if test_addr.is_ipv6() && test_addr.ip().is_unspecified() { let mut test_addr = test_addr; test_addr.set_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); - if let Err(err) = test_hbbs(test_addr).await { + if let Err(err) = test_hbbs(test_addr, test_key).await { log::error!("Failed to run hbbs test with {test_addr}: {err}"); std::process::exit(1); } @@ -259,7 +266,12 @@ impl RendezvousServer { res = socket.next() => { match res { Some(Ok((bytes, addr))) => { - if let Err(err) = self.handle_udp(&bytes, addr.into(), socket, key).await { + let addr: SocketAddr = addr.into(); + if !crate::common::allow_udp_packet_from_ip("hbbs-udp", addr) { + log::warn!("Rate limit exceeded for hbbs-udp from {}", addr.ip()); + continue; + } + if let Err(err) = self.handle_udp(&bytes, addr, socket, key).await { log::error!("udp failure: {}", err); return LoopFailure::UdpSocket; } @@ -276,8 +288,12 @@ impl RendezvousServer { res = listener2.accept() => { match res { Ok((stream, addr)) => { + if !crate::common::allow_connection_from_ip("hbbs-nat", addr) { + log::warn!("Rate limit exceeded for hbbs-nat from {}", addr.ip()); + continue; + } stream.set_nodelay(true).ok(); - self.handle_listener2(stream, addr).await; + self.handle_listener2(stream, addr, key).await; } Err(err) => { log::error!("listener2.accept failed: {}", err); @@ -288,6 +304,10 @@ impl RendezvousServer { res = listener3.accept() => { match res { Ok((stream, addr)) => { + if !crate::common::allow_connection_from_ip("hbbs-ws", addr) { + log::warn!("Rate limit exceeded for hbbs-ws from {}", addr.ip()); + continue; + } stream.set_nodelay(true).ok(); self.handle_listener(stream, addr, key, true).await; } @@ -300,6 +320,10 @@ impl RendezvousServer { res = listener.accept() => { match res { Ok((stream, addr)) => { + if !crate::common::allow_connection_from_ip("hbbs-main", addr) { + log::warn!("Rate limit exceeded for hbbs-main from {}", addr.ip()); + continue; + } stream.set_nodelay(true).ok(); self.handle_listener(stream, addr, key, false).await; } @@ -326,6 +350,27 @@ impl RendezvousServer { Some(rendezvous_message::Union::RegisterPeer(rp)) => { // B registered if !rp.id.is_empty() { + if !is_valid_server_key(key, &rp.licence_key) { + log::warn!( + "Authentication failed from {} for peer {} - invalid key", + addr, + rp.id + ); + return Ok(()); + } + if !is_valid_registration_id(&rp.id) { + log::warn!("Invalid peer registration id from {}: {:?}", addr, rp.id); + return Ok(()); + } + let ip = addr.ip().to_string(); + if !self.check_ip_blocker(&ip, &rp.id).await { + log::warn!( + "Peer registration rate-limited from {} for id {}", + addr, + rp.id + ); + return Ok(()); + } log::trace!("New peer registered: {:?} {:?}", &rp.id, &addr); self.update_addr(rp.id, addr, socket).await?; if self.inner.serial > rp.serial { @@ -343,14 +388,29 @@ impl RendezvousServer { if rk.uuid.is_empty() || rk.pk.is_empty() { return Ok(()); } + if !is_valid_server_key(key, &rk.licence_key) { + log::warn!( + "Authentication failed from {} for peer {} - invalid key", + addr, + rk.id + ); + return send_rk_res(socket, addr, LICENSE_MISMATCH).await; + } let id = rk.id; let ip = addr.ip().to_string(); - if id.len() < 6 { - return send_rk_res(socket, addr, UUID_MISMATCH).await; + if !is_valid_registration_id(&id) { + return send_rk_res(socket, addr, INVALID_ID_FORMAT).await; } else if !self.check_ip_blocker(&ip, &id).await { return send_rk_res(socket, addr, TOO_FREQUENT).await; } - let peer = self.pm.get_or(&id).await; + let Some(peer) = self.pm.get_or_for_registration(&id, &ip).await else { + log::warn!( + "Pending registration cache limit reached from {} for id {}", + addr, + id + ); + return send_rk_res(socket, addr, TOO_FREQUENT).await; + }; let (changed, ip_changed) = { let peer = peer.read().await; if peer.uuid.is_empty() { @@ -414,12 +474,14 @@ impl RendezvousServer { ); } } - if changed { - self.pm.update_pk(id, peer, addr, rk.uuid, rk.pk, ip).await; - } + let result = if changed { + self.pm.update_pk(id, peer, addr, rk.uuid, rk.pk, ip).await + } else { + register_pk_response::Result::OK + }; let mut msg_out = RendezvousMessage::new(); msg_out.set_register_pk_response(RegisterPkResponse { - result: register_pk_response::Result::OK.into(), + result: result.into(), ..Default::default() }); socket.send(&msg_out, addr).await? @@ -539,6 +601,10 @@ impl RendezvousServer { allow_err!(self.handle_local_addr(la, addr, None).await); } Some(rendezvous_message::Union::TestNatRequest(tar)) => { + if !is_valid_server_key(key, &tar.licence_key) { + log::warn!("Authentication failed from {} for nat probe", addr); + return true; + } let mut msg_out = RendezvousMessage::new(); let mut res = TestNatResponse { port: addr.port() as _, @@ -722,6 +788,7 @@ impl RendezvousServer { let to_ip = try_into_v4(peer_addr).ip().to_string(); let to_id_clone = id.clone(); let mut lock = PUNCH_REQS.lock().await; + prune_punch_requests(&mut lock); let mut dup = false; for e in lock.iter().rev().take(30) { // only check recent tail subset for speed if e.from_ip == from_ip && e.to_id == to_id_clone { @@ -729,7 +796,15 @@ impl RendezvousServer { break; } } - if !dup { lock.push(PunchReqEntry { tm: Instant::now(), from_ip, to_ip, to_id: to_id_clone }); } + if !dup { + lock.push(PunchReqEntry { + tm: Instant::now(), + from_ip, + to_ip, + to_id: to_id_clone, + }); + prune_punch_requests(&mut lock); + } } let mut msg_out = RendezvousMessage::new(); @@ -942,13 +1017,14 @@ impl RendezvousServer { match fds.next() { Some("h") => { res = format!( - "{}\n{}\n{}\n{}\n{}\n{}\n{}\n", + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", "relay-servers(rs) ", "reload-geo(rg)", "ip-blocker(ib) [|] [-]", "ip-changes(ic) [|] [-]", "punch-requests(pr) [] [-]", "always-use-relay(aur)", + "protection-stats(ps)", "test-geo(tg) " ) } @@ -1053,6 +1129,7 @@ impl RendezvousServer { let arg = fds.next(); if let Some("-") = arg { lock.clear(); } else { + prune_punch_requests(&mut lock); let mut start = arg.and_then(|x| x.parse::().ok()).unwrap_or(0); let mut page_size = fds.next().and_then(|x| x.parse::().ok()).unwrap_or(10); if page_size == 0 { page_size = 10; } @@ -1081,6 +1158,14 @@ impl RendezvousServer { ); } } + Some("protection-stats" | "ps") => { + for line in crate::common::protection_limits_summary() { + let _ = writeln!(res, "{line}"); + } + for (name, value) in crate::common::protection_stats_snapshot() { + let _ = writeln!(res, "{name}={value}"); + } + } Some("test-geo" | "tg") => { if let Some(rs) = fds.next() { if let Ok(a) = rs.parse::() { @@ -1099,8 +1184,9 @@ impl RendezvousServer { res } - async fn handle_listener2(&self, stream: TcpStream, addr: SocketAddr) { + async fn handle_listener2(&self, stream: TcpStream, addr: SocketAddr, key: &str) { let mut rs = self.clone(); + let key = key.to_owned(); let ip = try_into_v4(addr).ip(); if ip.is_loopback() { tokio::spawn(async move { @@ -1121,7 +1207,11 @@ impl RendezvousServer { if let Some(Ok(bytes)) = stream.next_timeout(30_000).await { if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { match msg_in.union { - Some(rendezvous_message::Union::TestNatRequest(_)) => { + Some(rendezvous_message::Union::TestNatRequest(tar)) => { + if !is_valid_server_key(&key, &tar.licence_key) { + log::warn!("Authentication failed from {} for nat probe", addr); + return; + } let mut msg_out = RendezvousMessage::new(); msg_out.set_test_nat_response(TestNatResponse { port: addr.port() as _, @@ -1130,6 +1220,10 @@ impl RendezvousServer { stream.send(&msg_out).await.ok(); } Some(rendezvous_message::Union::OnlineRequest(or)) => { + if !is_valid_server_key(&key, &or.licence_key) { + log::warn!("Authentication failed from {} for online query", addr); + return; + } allow_err!(rs.handle_online_request(&mut stream, or.peers).await); } _ => {} @@ -1160,18 +1254,7 @@ impl RendezvousServer { if ws { use tokio_tungstenite::tungstenite::handshake::server::{Request, Response}; let callback = |req: &Request, response: Response| { - let headers = req.headers(); - let real_ip = headers - .get("X-Real-IP") - .or_else(|| headers.get("X-Forwarded-For")) - .and_then(|header_value| header_value.to_str().ok()); - if let Some(ip) = real_ip { - if ip.contains('.') { - addr = format!("{ip}:0").parse().unwrap_or(addr); - } else { - addr = format!("[{ip}]:0").parse().unwrap_or(addr); - } - } + addr = crate::common::apply_trusted_proxy_addr(addr, req.headers()); Ok(response) }; let ws_stream = tokio_tungstenite::accept_hdr_async(stream, callback).await?; @@ -1300,7 +1383,7 @@ async fn check_relay_servers(rs0: Arc, tx: Sender) { } // temp solution to solve udp socket failure -async fn test_hbbs(addr: SocketAddr) -> ResultType<()> { +async fn test_hbbs(addr: SocketAddr, key: String) -> ResultType<()> { let mut addr = addr; if addr.ip().is_unspecified() { addr.set_ip(if addr.is_ipv4() { @@ -1314,6 +1397,7 @@ async fn test_hbbs(addr: SocketAddr) -> ResultType<()> { let mut msg_out = RendezvousMessage::new(); msg_out.set_register_peer(RegisterPeer { id: "(:test_hbbs:)".to_owned(), + licence_key: key, ..Default::default() }); let mut last_time_recv = Instant::now(); @@ -1337,6 +1421,88 @@ async fn test_hbbs(addr: SocketAddr) -> ResultType<()> { } } +fn prune_punch_requests(entries: &mut Vec) { + let before = entries.len(); + entries.retain(|entry| entry.tm.elapsed().as_secs() < PUNCH_REQ_RETENTION_SECS); + if entries.len() > MAX_PUNCH_REQS { + let excess = entries.len() - MAX_PUNCH_REQS; + entries.drain(0..excess); + } + if before > entries.len() { + crate::common::record_protection_event("punch_requests_pruned"); + } +} + +#[inline] +fn is_valid_registration_id(id: &str) -> bool { + let len = id.chars().count(); + (MIN_REGISTRATION_ID_LEN..=MAX_REGISTRATION_ID_LEN).contains(&len) +} + +#[inline] +fn is_valid_server_key(configured_key: &str, supplied_key: &str) -> bool { + configured_key.is_empty() || supplied_key == configured_key +} + +#[cfg(test)] +mod tests { + use super::{ + is_valid_registration_id, is_valid_server_key, prune_punch_requests, PunchReqEntry, + MAX_PUNCH_REQS, PUNCH_REQ_RETENTION_SECS, + }; + use std::time::{Duration, Instant}; + + #[test] + fn server_key_validation_accepts_open_server_or_matching_key() { + assert!(is_valid_server_key("", "")); + assert!(is_valid_server_key("", "anything")); + assert!(is_valid_server_key("shared-secret", "shared-secret")); + } + + #[test] + fn server_key_validation_rejects_mismatched_key() { + assert!(!is_valid_server_key("shared-secret", "")); + assert!(!is_valid_server_key("shared-secret", "wrong")); + } + + #[test] + fn registration_id_validation_enforces_basic_length_bounds() { + assert!(!is_valid_registration_id("")); + assert!(!is_valid_registration_id("12345")); + assert!(is_valid_registration_id("123456")); + assert!(is_valid_registration_id(&"a".repeat(100))); + assert!(!is_valid_registration_id(&"a".repeat(101))); + } + + #[test] + fn prune_punch_requests_removes_old_entries_and_caps_growth() { + let now = Instant::now(); + let old = now + .checked_sub(Duration::from_secs(PUNCH_REQ_RETENTION_SECS + 1)) + .unwrap_or(now); + let mut entries = vec![PunchReqEntry { + tm: old, + from_ip: "192.0.2.10".to_owned(), + to_ip: "198.51.100.10".to_owned(), + to_id: "old".to_owned(), + }]; + for i in 0..(MAX_PUNCH_REQS + 5) { + entries.push(PunchReqEntry { + tm: now, + from_ip: format!("192.0.2.{i}"), + to_ip: "198.51.100.10".to_owned(), + to_id: format!("peer-{i}"), + }); + } + + prune_punch_requests(&mut entries); + + assert_eq!(entries.len(), MAX_PUNCH_REQS); + assert!(entries.iter().all(|entry| entry.tm.elapsed().as_secs() < PUNCH_REQ_RETENTION_SECS)); + assert_eq!(entries.first().map(|entry| entry.to_id.as_str()), Some("peer-5")); + } +} + #[inline] async fn send_rk_res( socket: &mut FramedSocket, diff --git a/tests/server_protection_process.rs b/tests/server_protection_process.rs new file mode 100644 index 000000000..e8c5b1bfe --- /dev/null +++ b/tests/server_protection_process.rs @@ -0,0 +1,500 @@ +use hbb_common::{ + anyhow::{bail, Context, Result}, + protobuf::Message, + rendezvous_proto::{ + register_pk_response, rendezvous_message, OnlineRequest, RegisterPk, RegisterPkResponse, + RendezvousMessage, RequestRelay, + }, + tcp::FramedStream, + udp::FramedSocket, +}; +use sqlx::{Connection, Row, SqliteConnection}; +use std::{ + fs, + io::{Read, Write}, + net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream, UdpSocket}, + path::{Path, PathBuf}, + process::{Child, Command, Stdio}, + thread, + time::{Duration, Instant, SystemTime}, +}; + +struct ChildGuard { + child: Child, + temp_dir: PathBuf, +} + +impl ChildGuard { + fn try_wait(&mut self) -> Result> { + Ok(self.child.try_wait()?) + } +} + +impl Drop for ChildGuard { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + let _ = fs::remove_dir_all(&self.temp_dir); + } +} + +fn unique_temp_dir(prefix: &str) -> Result { + let mut path = std::env::temp_dir(); + let stamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + path.push(format!("{prefix}-{}-{stamp}", std::process::id())); + fs::create_dir_all(&path)?; + Ok(path) +} + +fn reserve_hbbs_port() -> Result { + for _ in 0..64 { + let main = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0))?; + let port = main.local_addr()?.port(); + if port <= 1024 || port >= (u16::MAX - 2) { + continue; + } + if TcpListener::bind((Ipv4Addr::UNSPECIFIED, port - 1)).is_ok() + && TcpListener::bind((Ipv4Addr::UNSPECIFIED, port + 2)).is_ok() + && UdpSocket::bind((Ipv4Addr::UNSPECIFIED, port)).is_ok() + { + return Ok(port); + } + } + bail!("failed to reserve hbbs port triplet"); +} + +fn reserve_hbbr_port() -> Result { + for _ in 0..64 { + let main = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0))?; + let port = main.local_addr()?.port(); + if port >= (u16::MAX - 2) { + continue; + } + if TcpListener::bind((Ipv4Addr::UNSPECIFIED, port + 2)).is_ok() { + return Ok(port); + } + } + bail!("failed to reserve hbbr port pair"); +} + +fn wait_for_tcp_ready(child: &mut ChildGuard, addr: SocketAddr) -> Result<()> { + let deadline = Instant::now() + Duration::from_secs(10); + while Instant::now() < deadline { + match TcpStream::connect_timeout(&addr, Duration::from_millis(200)) { + Ok(_) => return Ok(()), + Err(_) => { + if let Some(status) = child.try_wait()? { + bail!("server exited before becoming ready: {status}"); + } + thread::sleep(Duration::from_millis(100)); + } + } + } + bail!("timed out waiting for {addr} to accept connections"); +} + +fn find_bin_path(bin_env: &str, bin_name: &str) -> Result { + if let Some(path) = std::env::var_os(bin_env) { + return Ok(PathBuf::from(path)); + } + let mut path = std::env::current_exe()?; + path.pop(); + path.pop(); + path.push(bin_name); + #[cfg(windows)] + path.set_extension("exe"); + if path.is_file() { + return Ok(path); + } + bail!("missing cargo bin env and fallback binary path for {bin_name}"); +} + +fn spawn_server( + bin_env: &str, + bin_name: &str, + args: &[String], + current_dir: &Path, + envs: &[(&str, &str)], +) -> Result { + let bin = find_bin_path(bin_env, bin_name)?; + let mut command = Command::new(bin); + command + .current_dir(current_dir) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + for (key, value) in envs { + command.env(key, value); + } + let child = command.spawn()?; + Ok(ChildGuard { + child, + temp_dir: current_dir.to_path_buf(), + }) +} + +fn spawn_hbbs(port: u16, key: &str) -> Result { + let temp_dir = unique_temp_dir("hbbs-process-test")?; + spawn_hbbs_in_dir(port, key, temp_dir, &[]) +} + +fn spawn_hbbs_in_dir( + port: u16, + key: &str, + temp_dir: PathBuf, + envs: &[(&str, &str)], +) -> Result { + let args = vec![ + "--port".to_owned(), + port.to_string(), + "--key".to_owned(), + key.to_owned(), + ]; + let mut child_envs = vec![("TEST_HBBS", "no")]; + child_envs.extend_from_slice(envs); + let mut child = spawn_server( + "CARGO_BIN_EXE_hbbs", + "hbbs", + &args, + &temp_dir, + &child_envs, + )?; + wait_for_tcp_ready( + &mut child, + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port - 1), + )?; + Ok(child) +} + +fn spawn_hbbr(port: u16, key: &str) -> Result { + let temp_dir = unique_temp_dir("hbbr-process-test")?; + let args = vec![ + "--port".to_owned(), + port.to_string(), + "--key".to_owned(), + key.to_owned(), + ]; + let mut child = spawn_server("CARGO_BIN_EXE_hbbr", "hbbr", &args, &temp_dir, &[])?; + wait_for_tcp_ready( + &mut child, + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), + )?; + Ok(child) +} + +fn admin_command(addr: SocketAddr, command: &str) -> Result { + let mut stream = TcpStream::connect_timeout(&addr, Duration::from_secs(2))?; + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + stream.write_all(command.as_bytes())?; + stream.shutdown(std::net::Shutdown::Write)?; + let mut out = String::new(); + stream.read_to_string(&mut out)?; + Ok(out) +} + +fn non_loopback_local_ip() -> Option { + match local_ip_address::local_ip() { + Ok(ip) if !ip.is_loopback() && !ip.is_unspecified() => Some(ip), + _ => None, + } +} + +async fn send_online_request(addr: SocketAddr, key: &str) -> Result> { + let mut stream = FramedStream::new(addr, None, 1_500).await?; + let mut message = RendezvousMessage::new(); + message.set_online_request(OnlineRequest { + licence_key: key.to_owned(), + ..Default::default() + }); + stream.send(&message).await?; + let response = match stream.next_timeout(1_000).await { + Some(Ok(bytes)) => Some(RendezvousMessage::parse_from_bytes(&bytes)?), + Some(Err(err)) => return Err(err.into()), + None => None, + }; + Ok(response) +} + +async fn send_register_pk(addr: SocketAddr, id: &str, key: &str) -> Result> { + let mut socket = FramedSocket::new((Ipv4Addr::UNSPECIFIED, 0)).await?; + let mut message = RendezvousMessage::new(); + message.set_register_pk(RegisterPk { + id: id.to_owned(), + uuid: vec![1, 2, 3, 4].into(), + pk: vec![9, 8, 7, 6].into(), + licence_key: key.to_owned(), + ..Default::default() + }); + socket.send(&message, addr).await?; + let response = match socket.next_timeout(1_000).await { + Some(Ok((bytes, _))) => Some(RendezvousMessage::parse_from_bytes(&bytes)?), + Some(Err(err)) => return Err(err), + None => None, + }; + Ok(response) +} + +async fn send_relay_request(addr: SocketAddr, key: &str) -> Result> { + let mut stream = FramedStream::new(addr, None, 1_500).await?; + let mut message = RendezvousMessage::new(); + message.set_request_relay(RequestRelay { + id: "peer-a".to_owned(), + uuid: "relay-test-uuid".to_owned(), + licence_key: key.to_owned(), + ..Default::default() + }); + stream.send(&message).await?; + let response = match stream.next_timeout(1_000).await { + Some(Ok(bytes)) => Some(RendezvousMessage::parse_from_bytes(&bytes)?), + Some(Err(err)) => return Err(err.into()), + None => None, + }; + Ok(response) +} + +fn runtime() -> Result { + Ok(hbb_common::tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build()?) +} + +fn temp_db_path(dir: &Path) -> PathBuf { + dir.join("db_v2.sqlite3") +} + +async fn create_peer_schema_and_insert_stale_row(db_path: &Path, id: &str) -> Result<()> { + if !db_path.exists() { + fs::File::create(db_path)?; + } + let mut conn = SqliteConnection::connect(db_path.to_string_lossy().as_ref()).await?; + sqlx::query( + " + create table if not exists peer ( + guid blob primary key not null, + id varchar(100) not null, + uuid blob not null, + pk blob not null, + created_at datetime not null default(current_timestamp), + user blob, + status tinyint, + note varchar(300), + info text not null + ) without rowid; + ", + ) + .execute(&mut conn) + .await?; + sqlx::query("create unique index if not exists index_peer_id on peer (id);") + .execute(&mut conn) + .await?; + sqlx::query("create index if not exists index_peer_created_at on peer (created_at);") + .execute(&mut conn) + .await?; + sqlx::query( + "insert into peer(guid, id, uuid, pk, created_at, info) values(?, ?, ?, ?, datetime('now', '-2 days'), ?)", + ) + .bind(vec![0u8; 16]) + .bind(id) + .bind(vec![1u8; 4]) + .bind(vec![2u8; 4]) + .bind("") + .execute(&mut conn) + .await?; + Ok(()) +} + +async fn peer_exists(db_path: &Path, id: &str) -> Result { + let mut conn = SqliteConnection::connect(db_path.to_string_lossy().as_ref()).await?; + let row = sqlx::query("select count(*) from peer where id = ?") + .bind(id) + .fetch_one(&mut conn) + .await?; + let count: i64 = row.try_get(0)?; + Ok(count > 0) +} + +#[test] +fn hbbs_admin_protection_stats_reports_limits() -> Result<()> { + let port = reserve_hbbs_port()?; + let _child = spawn_hbbs(port, "server-key")?; + let output = admin_command( + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port - 1), + "ps", + )?; + assert!(output.contains("connections_per_ip_per_window=")); + assert!(output.contains("udp_packets_per_ip_per_window=")); + assert!(output.contains("trust_proxy_headers=")); + Ok(()) +} + +#[test] +fn hbbr_admin_protection_stats_reports_limits() -> Result<()> { + let port = reserve_hbbr_port()?; + let _child = spawn_hbbr(port, "server-key")?; + let output = admin_command( + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), + "ps", + )?; + assert!(output.contains("connections_per_ip_per_window=")); + assert!(output.contains("udp_packets_per_ip_per_window=")); + assert!(output.contains("trust_proxy_headers=")); + Ok(()) +} + +#[test] +fn hbbs_online_request_requires_configured_key_process() -> Result<()> { + let Some(ip) = non_loopback_local_ip() else { + eprintln!("skipping non-loopback hbbs auth process test: no non-loopback local IP"); + return Ok(()); + }; + let port = reserve_hbbs_port()?; + let _child = spawn_hbbs(port, "server-key")?; + let addr = SocketAddr::new(ip, port - 1); + + let runtime = runtime()?; + runtime.block_on(async move { + let unauthorized = send_online_request(addr, "").await?; + assert!( + unauthorized.is_none(), + "unauthorized online request unexpectedly received a response" + ); + + let authorized = send_online_request(addr, "server-key") + .await? + .context("authorized online request should receive a response")?; + match authorized.union { + Some(rendezvous_message::Union::OnlineResponse(response)) => { + assert!(response.states.is_empty()); + } + other => bail!("unexpected response to authorized online request: {other:?}"), + } + Ok::<(), hbb_common::anyhow::Error>(()) + })?; + Ok(()) +} + +#[test] +fn hbbs_register_pk_requires_configured_key_process() -> Result<()> { + let port = reserve_hbbs_port()?; + let _child = spawn_hbbs(port, "server-key")?; + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port); + let runtime = runtime()?; + runtime.block_on(async move { + let unauthorized = send_register_pk(addr, "peeruna1", "").await? + .context("unauthorized register_pk should receive a response")?; + match unauthorized.union { + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { result, .. })) => { + assert_eq!(result, register_pk_response::Result::LICENSE_MISMATCH.into()); + } + other => bail!("unexpected response to unauthorized register_pk: {other:?}"), + } + + let authorized = send_register_pk(addr, "peerauth1", "server-key").await? + .context("authorized register_pk should receive a response")?; + match authorized.union { + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { result, .. })) => { + assert_eq!(result, register_pk_response::Result::OK.into()); + } + other => bail!("unexpected response to authorized register_pk: {other:?}"), + } + Ok::<(), hbb_common::anyhow::Error>(()) + })?; + Ok(()) +} + +#[test] +fn hbbr_request_relay_reports_key_mismatch_process() -> Result<()> { + let Some(ip) = non_loopback_local_ip() else { + eprintln!("skipping non-loopback hbbr auth process test: no non-loopback local IP"); + return Ok(()); + }; + let port = reserve_hbbr_port()?; + let _child = spawn_hbbr(port, "server-key")?; + let addr = SocketAddr::new(ip, port); + let runtime = runtime()?; + runtime.block_on(async move { + let response = send_relay_request(addr, "").await? + .context("relay key mismatch should receive a refusal response")?; + match response.union { + Some(rendezvous_message::Union::RelayResponse(response)) => { + assert_eq!(response.refuse_reason, "Key mismatch"); + } + other => bail!("unexpected response to unauthorized relay request: {other:?}"), + } + Ok::<(), hbb_common::anyhow::Error>(()) + })?; + Ok(()) +} + +#[test] +fn hbbs_register_pk_reports_peer_limit_reached_process() -> Result<()> { + let port = reserve_hbbs_port()?; + let temp_dir = unique_temp_dir("hbbs-peer-limit-process-test")?; + let db_path = temp_db_path(&temp_dir); + let db_url = db_path.to_string_lossy().to_string(); + let _child = spawn_hbbs_in_dir( + port, + "server-key", + temp_dir, + &[("DB_URL", db_url.as_str()), ("MAX_TOTAL_PEER_RECORDS", "1"), ("PEER_RECORD_RETENTION_DAYS", "3650")], + )?; + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port); + let runtime = runtime()?; + runtime.block_on(async move { + let first = send_register_pk(addr, "limitpeer1", "server-key").await? + .context("first register_pk should receive a response")?; + match first.union { + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { result, .. })) => { + assert_eq!(result, register_pk_response::Result::OK.into()); + } + other => bail!("unexpected first register_pk response: {other:?}"), + } + + let second = send_register_pk(addr, "limitpeer2", "server-key").await? + .context("second register_pk should receive a response")?; + match second.union { + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { result, .. })) => { + assert_eq!(result, register_pk_response::Result::PEER_LIMIT_REACHED.into()); + } + other => bail!("unexpected second register_pk response: {other:?}"), + } + Ok::<(), hbb_common::anyhow::Error>(()) + })?; + Ok(()) +} + +#[test] +fn hbbs_register_pk_prunes_stale_peer_before_enforcing_cap_process() -> Result<()> { + let port = reserve_hbbs_port()?; + let temp_dir = unique_temp_dir("hbbs-peer-retention-process-test")?; + let db_path = temp_db_path(&temp_dir); + let runtime = runtime()?; + runtime.block_on(create_peer_schema_and_insert_stale_row(&db_path, "oldpeer1"))?; + let db_url = db_path.to_string_lossy().to_string(); + let _child = spawn_hbbs_in_dir( + port, + "server-key", + temp_dir, + &[("DB_URL", db_url.as_str()), ("MAX_TOTAL_PEER_RECORDS", "1"), ("PEER_RECORD_RETENTION_DAYS", "1")], + )?; + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port); + runtime.block_on(async move { + let response = send_register_pk(addr, "newpeer1", "server-key").await? + .context("register_pk should receive a response")?; + match response.union { + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { result, .. })) => { + assert_eq!(result, register_pk_response::Result::OK.into()); + } + other => bail!("unexpected register_pk response after stale prune: {other:?}"), + } + Ok::<(), hbb_common::anyhow::Error>(()) + })?; + assert!(!runtime.block_on(peer_exists(&db_path, "oldpeer1"))?); + assert!(runtime.block_on(peer_exists(&db_path, "newpeer1"))?); + Ok(()) +} From 8aa43d30e321b15720a244223ce589f3fb8b3808 Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 13 Apr 2026 15:53:03 +0900 Subject: [PATCH 2/5] Add connection-rate cap; improve cache & test spawn Introduce MAX_CONNECTION_RATE_ENTRIES with a default and enforce a cap on connection-rate entries: evict the oldest entry when the cap is reached, record eviction/rejection protection events, and expose the cap in protection summaries. Fix forwarded IP port handling and add websocket-forwarded rate-limit checks in relay and rendezvous servers. Refactor peer caching insertion: compute eviction candidates, remove excess peers before inserting pending registrations, and enforce per-IP pending registration limits without the previous helper. Update select_peer_ids_to_evict usage and minor formatting/await handling. Revamp test process spawn helpers: replace single-port reservation with retrying pick_*_port_candidate functions, make spawn_hbbs/spawn_hbbr return the chosen port and child, preserve temp dirs on certain errors, and add/adjust tests (including a new test to assert connection-rate entry bounding). Miscellaneous whitespace, logging, and small API-call formatting improvements across files. Signed-off-by: Vlad --- src/common.rs | 145 ++++++++++++++++---- src/peer.rs | 63 +++++---- src/relay_server.rs | 15 ++- src/rendezvous_server.rs | 61 +++++++-- tests/server_protection_process.rs | 204 +++++++++++++++++++---------- 5 files changed, 355 insertions(+), 133 deletions(-) diff --git a/src/common.rs b/src/common.rs index 2a611908f..665d8afc3 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,6 +1,8 @@ use clap::App; use hbb_common::{ - allow_err, anyhow::{Context, Result}, get_version_number, log, tokio, ResultType + allow_err, + anyhow::{Context, Result}, + get_version_number, log, tokio, ResultType, }; use http::HeaderMap; use ini::Ini; @@ -18,10 +20,12 @@ use std::{ const TRUST_PROXY_HEADERS_ENV: &str = "TRUST_PROXY_HEADERS"; const CONN_RATE_WINDOW_SECONDS_ENV: &str = "CONNECTION_RATE_WINDOW_SECONDS"; const MAX_CONN_PER_IP_PER_WINDOW_ENV: &str = "MAX_CONNECTIONS_PER_IP_PER_WINDOW"; +const MAX_CONNECTION_RATE_ENTRIES_ENV: &str = "MAX_CONNECTION_RATE_ENTRIES"; const UDP_RATE_WINDOW_SECONDS_ENV: &str = "UDP_RATE_WINDOW_SECONDS"; const MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV: &str = "MAX_UDP_PACKETS_PER_IP_PER_WINDOW"; const DEFAULT_CONN_RATE_WINDOW_SECONDS: usize = 60; const DEFAULT_MAX_CONN_PER_IP_PER_WINDOW: usize = 120; +const DEFAULT_MAX_CONNECTION_RATE_ENTRIES: usize = 8_192; const DEFAULT_UDP_RATE_WINDOW_SECONDS: usize = 60; const DEFAULT_MAX_UDP_PACKETS_PER_IP_PER_WINDOW: usize = 240; @@ -155,13 +159,17 @@ fn max_conn_per_ip_per_window() -> usize { ) } -fn udp_rate_window_seconds() -> usize { +fn max_connection_rate_entries() -> usize { env_usize_or( - UDP_RATE_WINDOW_SECONDS_ENV, - DEFAULT_UDP_RATE_WINDOW_SECONDS, + MAX_CONNECTION_RATE_ENTRIES_ENV, + DEFAULT_MAX_CONNECTION_RATE_ENTRIES, ) } +fn udp_rate_window_seconds() -> usize { + env_usize_or(UDP_RATE_WINDOW_SECONDS_ENV, DEFAULT_UDP_RATE_WINDOW_SECONDS) +} + fn max_udp_packets_per_ip_per_window() -> usize { env_usize_or( MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, @@ -179,6 +187,18 @@ fn prune_connection_rate_limits( }); } +fn evict_oldest_connection_rate_entry(entries: &mut HashMap) -> bool { + let oldest_key = entries + .iter() + .min_by_key(|(_, entry)| entry.last_seen_at) + .map(|(key, _)| key.clone()); + if let Some(key) = oldest_key { + entries.remove(&key); + return true; + } + false +} + #[allow(dead_code)] fn allow_ip_activity(scope: &str, addr: SocketAddr, window_secs: usize, max_events: usize) -> bool { if addr.ip().is_loopback() { @@ -188,11 +208,28 @@ fn allow_ip_activity(scope: &str, addr: SocketAddr, window_secs: usize, max_even let mut lock = CONNECTION_RATE_LIMITS.lock().unwrap(); prune_connection_rate_limits(&mut lock, now, window_secs); let key = format!("{scope}|{}", addr.ip()); - let entry = lock.entry(key).or_insert(ConnectionRateEntry { - window_started_at: now, - last_seen_at: now, - count: 0, - }); + if !lock.contains_key(&key) { + let max_entries = max_connection_rate_entries(); + if max_entries > 0 + && lock.len() >= max_entries + && evict_oldest_connection_rate_entry(&mut lock) + { + record_protection_event("connection_rate_entries_evicted"); + } + if max_entries > 0 && lock.len() >= max_entries { + record_protection_event("connection_rate_entries_rejected"); + return false; + } + lock.insert( + key.clone(), + ConnectionRateEntry { + window_started_at: now, + last_seen_at: now, + count: 0, + }, + ); + } + let entry = lock.get_mut(&key).expect("entry inserted above"); if now.duration_since(entry.window_started_at).as_secs() >= window_secs as u64 { entry.window_started_at = now; entry.count = 0; @@ -259,6 +296,10 @@ pub fn protection_limits_summary() -> Vec { max_conn_per_ip_per_window(), conn_rate_window_seconds() ), + format!( + "connection_rate_entry_cap={}", + max_connection_rate_entries() + ), format!( "udp_packets_per_ip_per_window={}/{}s", max_udp_packets_per_ip_per_window(), @@ -278,7 +319,9 @@ pub fn apply_trusted_proxy_addr(addr: SocketAddr, headers: &HeaderMap) -> Socket .or_else(|| headers.get("X-Forwarded-For")) .and_then(|header_value| header_value.to_str().ok()) .and_then(parse_forwarded_ip); - forwarded_ip.map(|ip| SocketAddr::new(ip, 0)).unwrap_or(addr) + forwarded_ip + .map(|ip| SocketAddr::new(ip, addr.port())) + .unwrap_or(addr) } fn parse_forwarded_ip(value: &str) -> Option { @@ -384,7 +427,6 @@ pub async fn listen_signal() -> Result<()> { unreachable!(); } - pub fn check_software_update() { const ONE_DAY_IN_SECONDS: u64 = 60 * 60 * 24; std::thread::spawn(move || loop { @@ -395,8 +437,10 @@ pub fn check_software_update() { #[tokio::main(flavor = "current_thread")] async fn check_software_update_() -> hbb_common::ResultType<()> { - let (request, url) = hbb_common::version_check_request(hbb_common::VER_TYPE_RUSTDESK_SERVER.to_string()); - let latest_release_response = reqwest::Client::builder().build()? + let (request, url) = + hbb_common::version_check_request(hbb_common::VER_TYPE_RUSTDESK_SERVER.to_string()); + let latest_release_response = reqwest::Client::builder() + .build()? .post(url) .json(&request) .send() @@ -407,7 +451,7 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { let response_url = resp.url; let latest_release_version = response_url.rsplit('/').next().unwrap_or_default(); if get_version_number(&latest_release_version) > get_version_number(crate::version::VERSION) { - log::info!("new version is available: {}", latest_release_version); + log::info!("new version is available: {}", latest_release_version); } Ok(()) } @@ -416,17 +460,18 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { mod tests { use super::{ allow_connection_from_ip, allow_udp_packet_from_ip, apply_trusted_proxy_addr, - conn_rate_window_seconds, max_conn_per_ip_per_window, max_udp_packets_per_ip_per_window, - protection_limits_summary, protection_stats_snapshot, record_protection_event, - trust_proxy_headers, udp_rate_window_seconds, CONNECTION_RATE_LIMITS, - CONN_RATE_WINDOW_SECONDS_ENV, MAX_CONN_PER_IP_PER_WINDOW_ENV, - MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, PROTECTION_STATS, TRUST_PROXY_HEADERS_ENV, - UDP_RATE_WINDOW_SECONDS_ENV, + conn_rate_window_seconds, max_conn_per_ip_per_window, max_connection_rate_entries, + max_udp_packets_per_ip_per_window, protection_limits_summary, protection_stats_snapshot, + record_protection_event, trust_proxy_headers, udp_rate_window_seconds, + CONNECTION_RATE_LIMITS, CONN_RATE_WINDOW_SECONDS_ENV, MAX_CONNECTION_RATE_ENTRIES_ENV, + MAX_CONN_PER_IP_PER_WINDOW_ENV, MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, PROTECTION_STATS, + TRUST_PROXY_HEADERS_ENV, UDP_RATE_WINDOW_SECONDS_ENV, }; use http::HeaderMap; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, sync::Mutex, + time::Duration, }; static TEST_PROXY_HEADERS_LOCK: Mutex<()> = Mutex::new(()); @@ -443,7 +488,10 @@ mod tests { let _guard = TEST_PROXY_HEADERS_LOCK.lock().unwrap(); let original = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 4)), 21117); let mut headers = HeaderMap::new(); - headers.insert("X-Forwarded-For", "198.51.100.10, 10.0.0.1".parse().unwrap()); + headers.insert( + "X-Forwarded-For", + "198.51.100.10, 10.0.0.1".parse().unwrap(), + ); std::env::remove_var(TRUST_PROXY_HEADERS_ENV); assert_eq!(apply_trusted_proxy_addr(original, &headers), original); @@ -451,7 +499,7 @@ mod tests { std::env::set_var(TRUST_PROXY_HEADERS_ENV, "Y"); assert_eq!( apply_trusted_proxy_addr(original, &headers), - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 10)), 0) + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 10)), 21117) ); std::env::remove_var(TRUST_PROXY_HEADERS_ENV); @@ -464,6 +512,7 @@ mod tests { PROTECTION_STATS.lock().unwrap().clear(); std::env::set_var(MAX_CONN_PER_IP_PER_WINDOW_ENV, "2"); std::env::set_var(CONN_RATE_WINDOW_SECONDS_ENV, "60"); + std::env::set_var(MAX_CONNECTION_RATE_ENTRIES_ENV, "2"); std::env::set_var(MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, "3"); std::env::set_var(UDP_RATE_WINDOW_SECONDS_ENV, "60"); @@ -499,12 +548,23 @@ mod tests { std::env::remove_var(MAX_CONN_PER_IP_PER_WINDOW_ENV); std::env::remove_var(CONN_RATE_WINDOW_SECONDS_ENV); + std::env::remove_var(MAX_CONNECTION_RATE_ENTRIES_ENV); std::env::remove_var(MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV); std::env::remove_var(UDP_RATE_WINDOW_SECONDS_ENV); CONNECTION_RATE_LIMITS.lock().unwrap().clear(); PROTECTION_STATS.lock().unwrap().clear(); - assert_eq!(max_conn_per_ip_per_window(), super::DEFAULT_MAX_CONN_PER_IP_PER_WINDOW); - assert_eq!(conn_rate_window_seconds(), super::DEFAULT_CONN_RATE_WINDOW_SECONDS); + assert_eq!( + max_conn_per_ip_per_window(), + super::DEFAULT_MAX_CONN_PER_IP_PER_WINDOW + ); + assert_eq!( + conn_rate_window_seconds(), + super::DEFAULT_CONN_RATE_WINDOW_SECONDS + ); + assert_eq!( + max_connection_rate_entries(), + super::DEFAULT_MAX_CONNECTION_RATE_ENTRIES + ); assert_eq!( max_udp_packets_per_ip_per_window(), super::DEFAULT_MAX_UDP_PACKETS_PER_IP_PER_WINDOW @@ -517,9 +577,46 @@ mod tests { protection_limits_summary(), vec![ "connections_per_ip_per_window=120/60s".to_owned(), + "connection_rate_entry_cap=8192".to_owned(), "udp_packets_per_ip_per_window=240/60s".to_owned(), "trust_proxy_headers=false".to_owned(), ] ); } + + #[test] + fn connection_rate_limit_entries_are_bounded() { + let _guard = TEST_PROXY_HEADERS_LOCK.lock().unwrap(); + CONNECTION_RATE_LIMITS.lock().unwrap().clear(); + PROTECTION_STATS.lock().unwrap().clear(); + std::env::set_var(MAX_CONNECTION_RATE_ENTRIES_ENV, "2"); + std::env::set_var(MAX_CONN_PER_IP_PER_WINDOW_ENV, "10"); + std::env::set_var(CONN_RATE_WINDOW_SECONDS_ENV, "60"); + + let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)), 21117); + let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 2)), 21117); + let addr3 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 3)), 21117); + assert!(allow_connection_from_ip("hbbs-main", addr1)); + std::thread::sleep(Duration::from_millis(2)); + assert!(allow_connection_from_ip("hbbs-main", addr2)); + std::thread::sleep(Duration::from_millis(2)); + assert!(allow_connection_from_ip("hbbs-main", addr3)); + + let snapshot = CONNECTION_RATE_LIMITS.lock().unwrap(); + assert_eq!(snapshot.len(), 2); + assert!(!snapshot.contains_key("hbbs-main|198.51.100.1")); + assert!(snapshot.contains_key("hbbs-main|198.51.100.2")); + assert!(snapshot.contains_key("hbbs-main|198.51.100.3")); + drop(snapshot); + assert_eq!( + protection_stats_snapshot(), + vec![("connection_rate_entries_evicted".to_owned(), 1)] + ); + + std::env::remove_var(MAX_CONNECTION_RATE_ENTRIES_ENV); + std::env::remove_var(MAX_CONN_PER_IP_PER_WINDOW_ENV); + std::env::remove_var(CONN_RATE_WINDOW_SECONDS_ENV); + CONNECTION_RATE_LIMITS.lock().unwrap().clear(); + PROTECTION_STATS.lock().unwrap().clear(); + } } diff --git a/src/peer.rs b/src/peer.rs index dd25b30dd..f015c437d 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -122,8 +122,7 @@ impl PeerMap { db }); log::info!("DB_URL={}", db); - let max_cached_peers = - env_usize_or("MAX_PEER_CACHE_SIZE", DEFAULT_MAX_PEER_CACHE_SIZE); + let max_cached_peers = env_usize_or("MAX_PEER_CACHE_SIZE", DEFAULT_MAX_PEER_CACHE_SIZE); let max_pending_registrations_per_ip = env_usize_or( "MAX_PENDING_REGISTRATIONS_PER_IP", DEFAULT_MAX_PENDING_REGISTRATIONS_PER_IP, @@ -215,14 +214,42 @@ impl PeerMap { if let Some(p) = self.get(id).await { return Some(p); } - if !self.can_cache_pending_registration(ip).await { - return None; - } - self.prune_cache_for_insert().await; let mut w = self.map.write().await; if let Some(p) = w.get(id) { return Some(p.clone()); } + let snapshot: Vec<(String, LockPeer)> = w + .iter() + .map(|(id, peer)| (id.clone(), peer.clone())) + .collect(); + let mut entries = Vec::with_capacity(snapshot.len()); + let mut pending_ids_for_ip = HashSet::new(); + for (peer_id, peer) in snapshot { + let peer = peer.read().await; + if peer.info.ip == ip && peer.guid.is_empty() { + pending_ids_for_ip.insert(peer_id.clone()); + } + entries.push(PeerEvictionEntry { + id: peer_id, + inactive: peer.last_reg_time.elapsed().as_millis() as i32 + >= PEER_CACHE_INACTIVE_TIMEOUT_MS, + last_reg_time: peer.last_reg_time, + }); + } + let remove_ids = select_peer_ids_to_evict(entries, self.max_cached_peers); + if !remove_ids.is_empty() { + let remove_ids_set: HashSet = remove_ids.iter().cloned().collect(); + for id in &remove_ids { + w.remove(id); + } + pending_ids_for_ip.retain(|id| !remove_ids_set.contains(id)); + } + if pending_registration_limit_exceeded( + pending_ids_for_ip.len(), + self.max_pending_registrations_per_ip, + ) { + return None; + } let tmp = Arc::new(RwLock::new(Peer { info: PeerInfo { ip: ip.to_owned() }, ..Default::default() @@ -241,21 +268,6 @@ impl PeerMap { self.map.read().await.contains_key(id) } - async fn can_cache_pending_registration(&self, ip: &str) -> bool { - let snapshot: Vec = self.map.read().await.values().cloned().collect(); - let mut pending_for_ip = 0usize; - for peer in snapshot { - let peer = peer.read().await; - if peer.info.ip == ip && peer.guid.is_empty() { - pending_for_ip += 1; - } - } - !pending_registration_limit_exceeded( - pending_for_ip, - self.max_pending_registrations_per_ip, - ) - } - async fn prune_cache_for_insert(&self) { let snapshot: Vec<(String, LockPeer)> = self .map @@ -287,9 +299,7 @@ impl PeerMap { #[cfg(test)] mod tests { - use super::{ - pending_registration_limit_exceeded, select_peer_ids_to_evict, PeerEvictionEntry, - }; + use super::{pending_registration_limit_exceeded, select_peer_ids_to_evict, PeerEvictionEntry}; use std::time::{Duration, Instant}; #[test] @@ -324,6 +334,9 @@ mod tests { ], 2, ); - assert_eq!(remove, vec!["inactive-old".to_owned(), "inactive-new".to_owned()]); + assert_eq!( + remove, + vec!["inactive-old".to_owned(), "inactive-new".to_owned()] + ); } } diff --git a/src/relay_server.rs b/src/relay_server.rs index c8eb83392..08243ad3d 100644 --- a/src/relay_server.rs +++ b/src/relay_server.rs @@ -455,12 +455,22 @@ async fn make_pair( ws: bool, ) -> ResultType<()> { if ws { + let peer_addr = addr; use tokio_tungstenite::tungstenite::handshake::server::{Request, Response}; let callback = |req: &Request, response: Response| { addr = crate::common::apply_trusted_proxy_addr(addr, req.headers()); Ok(response) }; let ws_stream = tokio_tungstenite::accept_hdr_async(stream, callback).await?; + if addr.ip() != peer_addr.ip() + && !crate::common::allow_connection_from_ip("hbbr-ws-forwarded", addr) + { + log::warn!( + "Rate limit exceeded for hbbr-ws-forwarded from {}", + addr.ip() + ); + return Ok(()); + } make_pair_(ws_stream, addr, key, limiter).await; } else { make_pair_(FramedStream::from(stream, addr), addr, key, limiter).await; @@ -759,10 +769,7 @@ mod tests { pending_relay_limit_reason(1, 1), Some("Too many pending relay requests") ); - assert_eq!( - pending_relay_limit_reason(2, 0), - Some("Relay server busy") - ); + assert_eq!(pending_relay_limit_reason(2, 0), Some("Relay server busy")); MAX_PENDING_RELAYS.store(saved_total, Ordering::SeqCst); MAX_PENDING_RELAYS_PER_IP.store(saved_per_ip, Ordering::SeqCst); diff --git a/src/rendezvous_server.rs b/src/rendezvous_server.rs index a23106e05..8580a56db 100644 --- a/src/rendezvous_server.rs +++ b/src/rendezvous_server.rs @@ -69,7 +69,12 @@ static ALWAYS_USE_RELAY: AtomicBool = AtomicBool::new(false); use once_cell::sync::Lazy; use tokio::sync::Mutex as TokioMutex; // differentiate if needed #[derive(Clone)] -struct PunchReqEntry { tm: Instant, from_ip: String, to_ip: String, to_id: String } +struct PunchReqEntry { + tm: Instant, + from_ip: String, + to_ip: String, + to_id: String, +} static PUNCH_REQS: Lazy>> = Lazy::new(|| TokioMutex::new(Vec::new())); const PUNCH_REQ_DEDUPE_SEC: u64 = 60; const PUNCH_REQ_RETENTION_SECS: u64 = 600; @@ -754,7 +759,11 @@ impl RendezvousServer { ) -> ResultType<(RendezvousMessage, Option)> { let mut ph = ph; if !key.is_empty() && ph.licence_key != key { - log::warn!("Authentication failed from {} for peer {} - invalid key", addr, ph.id); + log::warn!( + "Authentication failed from {} for peer {} - invalid key", + addr, + ph.id + ); let mut msg_out = RendezvousMessage::new(); msg_out.set_punch_hole_response(PunchHoleResponse { failure: punch_hole_response::Failure::LICENSE_MISMATCH.into(), @@ -781,7 +790,7 @@ impl RendezvousServer { }); return Ok((msg_out, None)); } - + // record punch hole request (from addr -> peer id/peer_addr) { let from_ip = try_into_v4(addr).ip().to_string(); @@ -790,9 +799,12 @@ impl RendezvousServer { let mut lock = PUNCH_REQS.lock().await; prune_punch_requests(&mut lock); let mut dup = false; - for e in lock.iter().rev().take(30) { // only check recent tail subset for speed + for e in lock.iter().rev().take(30) { + // only check recent tail subset for speed if e.from_ip == from_ip && e.to_id == to_id_clone { - if e.tm.elapsed().as_secs() < PUNCH_REQ_DEDUPE_SEC { dup = true; } + if e.tm.elapsed().as_secs() < PUNCH_REQ_DEDUPE_SEC { + dup = true; + } break; } } @@ -1127,18 +1139,28 @@ impl RendezvousServer { use std::fmt::Write as _; let mut lock = PUNCH_REQS.lock().await; let arg = fds.next(); - if let Some("-") = arg { lock.clear(); } - else { + if let Some("-") = arg { + lock.clear(); + } else { prune_punch_requests(&mut lock); let mut start = arg.and_then(|x| x.parse::().ok()).unwrap_or(0); - let mut page_size = fds.next().and_then(|x| x.parse::().ok()).unwrap_or(10); - if page_size == 0 { page_size = 10; } + let mut page_size = fds + .next() + .and_then(|x| x.parse::().ok()) + .unwrap_or(10); + if page_size == 0 { + page_size = 10; + } for (_, e) in lock.iter().enumerate().skip(start).take(page_size) { let age = e.tm.elapsed(); let event_system = std::time::SystemTime::now() - age; let event_iso = chrono::DateTime::::from(event_system) .to_rfc3339_opts(chrono::SecondsFormat::Secs, true); - let _ = writeln!(res, "{} {} -> {}@{}", event_iso, e.from_ip, e.to_id, e.to_ip); + let _ = writeln!( + res, + "{} {} -> {}@{}", + event_iso, e.from_ip, e.to_id, e.to_ip + ); } } } @@ -1252,12 +1274,22 @@ impl RendezvousServer { ) -> ResultType<()> { let mut sink; if ws { + let peer_addr = addr; use tokio_tungstenite::tungstenite::handshake::server::{Request, Response}; let callback = |req: &Request, response: Response| { addr = crate::common::apply_trusted_proxy_addr(addr, req.headers()); Ok(response) }; let ws_stream = tokio_tungstenite::accept_hdr_async(stream, callback).await?; + if addr.ip() != peer_addr.ip() + && !crate::common::allow_connection_from_ip("hbbs-ws-forwarded", addr) + { + log::warn!( + "Rate limit exceeded for hbbs-ws-forwarded from {}", + addr.ip() + ); + return Ok(()); + } let (a, mut b) = ws_stream.split(); sink = Some(Sink::Ws(a)); while let Ok(Some(Ok(msg))) = timeout(30_000, b.next()).await { @@ -1498,8 +1530,13 @@ mod tests { prune_punch_requests(&mut entries); assert_eq!(entries.len(), MAX_PUNCH_REQS); - assert!(entries.iter().all(|entry| entry.tm.elapsed().as_secs() < PUNCH_REQ_RETENTION_SECS)); - assert_eq!(entries.first().map(|entry| entry.to_id.as_str()), Some("peer-5")); + assert!(entries + .iter() + .all(|entry| entry.tm.elapsed().as_secs() < PUNCH_REQ_RETENTION_SECS)); + assert_eq!( + entries.first().map(|entry| entry.to_id.as_str()), + Some("peer-5") + ); } } diff --git a/tests/server_protection_process.rs b/tests/server_protection_process.rs index e8c5b1bfe..25db822a3 100644 --- a/tests/server_protection_process.rs +++ b/tests/server_protection_process.rs @@ -34,7 +34,9 @@ impl Drop for ChildGuard { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); - let _ = fs::remove_dir_all(&self.temp_dir); + if !self.temp_dir.as_os_str().is_empty() { + let _ = fs::remove_dir_all(&self.temp_dir); + } } } @@ -49,7 +51,7 @@ fn unique_temp_dir(prefix: &str) -> Result { Ok(path) } -fn reserve_hbbs_port() -> Result { +fn pick_hbbs_port_candidate() -> Result { for _ in 0..64 { let main = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0))?; let port = main.local_addr()?.port(); @@ -63,10 +65,10 @@ fn reserve_hbbs_port() -> Result { return Ok(port); } } - bail!("failed to reserve hbbs port triplet"); + bail!("failed to find hbbs port triplet"); } -fn reserve_hbbr_port() -> Result { +fn pick_hbbr_port_candidate() -> Result { for _ in 0..64 { let main = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0))?; let port = main.local_addr()?.port(); @@ -77,7 +79,7 @@ fn reserve_hbbr_port() -> Result { return Ok(port); } } - bail!("failed to reserve hbbr port pair"); + bail!("failed to find hbbr port pair"); } fn wait_for_tcp_ready(child: &mut ChildGuard, addr: SocketAddr) -> Result<()> { @@ -137,16 +139,41 @@ fn spawn_server( }) } -fn spawn_hbbs(port: u16, key: &str) -> Result { - let temp_dir = unique_temp_dir("hbbs-process-test")?; - spawn_hbbs_in_dir(port, key, temp_dir, &[]) +fn spawn_hbbs(key: &str) -> Result<(u16, ChildGuard)> { + let mut last_err = None; + for _ in 0..16 { + let port = pick_hbbs_port_candidate()?; + let temp_dir = unique_temp_dir("hbbs-process-test")?; + match spawn_hbbs_in_dir_with_port(port, key, temp_dir, &[], false) { + Ok(child) => return Ok((port, child)), + Err(err) => last_err = Some(err), + } + } + Err(last_err.unwrap_or_else(|| hbb_common::anyhow::anyhow!("failed to spawn hbbs"))) } fn spawn_hbbs_in_dir( + key: &str, + temp_dir: PathBuf, + envs: &[(&str, &str)], +) -> Result<(u16, ChildGuard)> { + let mut last_err = None; + for _ in 0..16 { + let port = pick_hbbs_port_candidate()?; + match spawn_hbbs_in_dir_with_port(port, key, temp_dir.clone(), envs, true) { + Ok(child) => return Ok((port, child)), + Err(err) => last_err = Some(err), + } + } + Err(last_err.unwrap_or_else(|| hbb_common::anyhow::anyhow!("failed to spawn hbbs"))) +} + +fn spawn_hbbs_in_dir_with_port( port: u16, key: &str, temp_dir: PathBuf, envs: &[(&str, &str)], + preserve_temp_dir_on_error: bool, ) -> Result { let args = vec![ "--port".to_owned(), @@ -156,34 +183,46 @@ fn spawn_hbbs_in_dir( ]; let mut child_envs = vec![("TEST_HBBS", "no")]; child_envs.extend_from_slice(envs); - let mut child = spawn_server( - "CARGO_BIN_EXE_hbbs", - "hbbs", - &args, - &temp_dir, - &child_envs, - )?; - wait_for_tcp_ready( + let mut child = spawn_server("CARGO_BIN_EXE_hbbs", "hbbs", &args, &temp_dir, &child_envs)?; + if let Err(err) = wait_for_tcp_ready( &mut child, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port - 1), - )?; + ) { + if preserve_temp_dir_on_error { + child.temp_dir = PathBuf::new(); + } + return Err(err); + } Ok(child) } -fn spawn_hbbr(port: u16, key: &str) -> Result { - let temp_dir = unique_temp_dir("hbbr-process-test")?; - let args = vec![ - "--port".to_owned(), - port.to_string(), - "--key".to_owned(), - key.to_owned(), - ]; - let mut child = spawn_server("CARGO_BIN_EXE_hbbr", "hbbr", &args, &temp_dir, &[])?; - wait_for_tcp_ready( - &mut child, - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), - )?; - Ok(child) +fn spawn_hbbr(key: &str) -> Result<(u16, ChildGuard)> { + let mut last_err = None; + for _ in 0..16 { + let port = pick_hbbr_port_candidate()?; + let temp_dir = unique_temp_dir("hbbr-process-test")?; + let args = vec![ + "--port".to_owned(), + port.to_string(), + "--key".to_owned(), + key.to_owned(), + ]; + let mut child = match spawn_server("CARGO_BIN_EXE_hbbr", "hbbr", &args, &temp_dir, &[]) { + Ok(child) => child, + Err(err) => { + last_err = Some(err); + continue; + } + }; + match wait_for_tcp_ready( + &mut child, + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), + ) { + Ok(()) => return Ok((port, child)), + Err(err) => last_err = Some(err), + } + } + Err(last_err.unwrap_or_else(|| hbb_common::anyhow::anyhow!("failed to spawn hbbr"))) } fn admin_command(addr: SocketAddr, command: &str) -> Result { @@ -219,7 +258,11 @@ async fn send_online_request(addr: SocketAddr, key: &str) -> Result Result> { +async fn send_register_pk( + addr: SocketAddr, + id: &str, + key: &str, +) -> Result> { let mut socket = FramedSocket::new((Ipv4Addr::UNSPECIFIED, 0)).await?; let mut message = RendezvousMessage::new(); message.set_register_pk(RegisterPk { @@ -320,8 +363,7 @@ async fn peer_exists(db_path: &Path, id: &str) -> Result { #[test] fn hbbs_admin_protection_stats_reports_limits() -> Result<()> { - let port = reserve_hbbs_port()?; - let _child = spawn_hbbs(port, "server-key")?; + let (port, _child) = spawn_hbbs("server-key")?; let output = admin_command( SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port - 1), "ps", @@ -334,12 +376,8 @@ fn hbbs_admin_protection_stats_reports_limits() -> Result<()> { #[test] fn hbbr_admin_protection_stats_reports_limits() -> Result<()> { - let port = reserve_hbbr_port()?; - let _child = spawn_hbbr(port, "server-key")?; - let output = admin_command( - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), - "ps", - )?; + let (port, _child) = spawn_hbbr("server-key")?; + let output = admin_command(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), "ps")?; assert!(output.contains("connections_per_ip_per_window=")); assert!(output.contains("udp_packets_per_ip_per_window=")); assert!(output.contains("trust_proxy_headers=")); @@ -352,8 +390,7 @@ fn hbbs_online_request_requires_configured_key_process() -> Result<()> { eprintln!("skipping non-loopback hbbs auth process test: no non-loopback local IP"); return Ok(()); }; - let port = reserve_hbbs_port()?; - let _child = spawn_hbbs(port, "server-key")?; + let (port, _child) = spawn_hbbs("server-key")?; let addr = SocketAddr::new(ip, port - 1); let runtime = runtime()?; @@ -380,24 +417,34 @@ fn hbbs_online_request_requires_configured_key_process() -> Result<()> { #[test] fn hbbs_register_pk_requires_configured_key_process() -> Result<()> { - let port = reserve_hbbs_port()?; - let _child = spawn_hbbs(port, "server-key")?; + let (port, _child) = spawn_hbbs("server-key")?; let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port); let runtime = runtime()?; runtime.block_on(async move { - let unauthorized = send_register_pk(addr, "peeruna1", "").await? + let unauthorized = send_register_pk(addr, "peeruna1", "") + .await? .context("unauthorized register_pk should receive a response")?; match unauthorized.union { - Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { result, .. })) => { - assert_eq!(result, register_pk_response::Result::LICENSE_MISMATCH.into()); + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { + result, + .. + })) => { + assert_eq!( + result, + register_pk_response::Result::LICENSE_MISMATCH.into() + ); } other => bail!("unexpected response to unauthorized register_pk: {other:?}"), } - let authorized = send_register_pk(addr, "peerauth1", "server-key").await? + let authorized = send_register_pk(addr, "peerauth1", "server-key") + .await? .context("authorized register_pk should receive a response")?; match authorized.union { - Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { result, .. })) => { + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { + result, + .. + })) => { assert_eq!(result, register_pk_response::Result::OK.into()); } other => bail!("unexpected response to authorized register_pk: {other:?}"), @@ -413,12 +460,12 @@ fn hbbr_request_relay_reports_key_mismatch_process() -> Result<()> { eprintln!("skipping non-loopback hbbr auth process test: no non-loopback local IP"); return Ok(()); }; - let port = reserve_hbbr_port()?; - let _child = spawn_hbbr(port, "server-key")?; + let (port, _child) = spawn_hbbr("server-key")?; let addr = SocketAddr::new(ip, port); let runtime = runtime()?; runtime.block_on(async move { - let response = send_relay_request(addr, "").await? + let response = send_relay_request(addr, "") + .await? .context("relay key mismatch should receive a refusal response")?; match response.union { Some(rendezvous_message::Union::RelayResponse(response)) => { @@ -433,33 +480,46 @@ fn hbbr_request_relay_reports_key_mismatch_process() -> Result<()> { #[test] fn hbbs_register_pk_reports_peer_limit_reached_process() -> Result<()> { - let port = reserve_hbbs_port()?; let temp_dir = unique_temp_dir("hbbs-peer-limit-process-test")?; let db_path = temp_db_path(&temp_dir); let db_url = db_path.to_string_lossy().to_string(); - let _child = spawn_hbbs_in_dir( - port, + let (port, _child) = spawn_hbbs_in_dir( "server-key", temp_dir, - &[("DB_URL", db_url.as_str()), ("MAX_TOTAL_PEER_RECORDS", "1"), ("PEER_RECORD_RETENTION_DAYS", "3650")], + &[ + ("DB_URL", db_url.as_str()), + ("MAX_TOTAL_PEER_RECORDS", "1"), + ("PEER_RECORD_RETENTION_DAYS", "3650"), + ], )?; let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port); let runtime = runtime()?; runtime.block_on(async move { - let first = send_register_pk(addr, "limitpeer1", "server-key").await? + let first = send_register_pk(addr, "limitpeer1", "server-key") + .await? .context("first register_pk should receive a response")?; match first.union { - Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { result, .. })) => { + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { + result, + .. + })) => { assert_eq!(result, register_pk_response::Result::OK.into()); } other => bail!("unexpected first register_pk response: {other:?}"), } - let second = send_register_pk(addr, "limitpeer2", "server-key").await? + let second = send_register_pk(addr, "limitpeer2", "server-key") + .await? .context("second register_pk should receive a response")?; match second.union { - Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { result, .. })) => { - assert_eq!(result, register_pk_response::Result::PEER_LIMIT_REACHED.into()); + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { + result, + .. + })) => { + assert_eq!( + result, + register_pk_response::Result::PEER_LIMIT_REACHED.into() + ); } other => bail!("unexpected second register_pk response: {other:?}"), } @@ -470,24 +530,32 @@ fn hbbs_register_pk_reports_peer_limit_reached_process() -> Result<()> { #[test] fn hbbs_register_pk_prunes_stale_peer_before_enforcing_cap_process() -> Result<()> { - let port = reserve_hbbs_port()?; let temp_dir = unique_temp_dir("hbbs-peer-retention-process-test")?; let db_path = temp_db_path(&temp_dir); let runtime = runtime()?; - runtime.block_on(create_peer_schema_and_insert_stale_row(&db_path, "oldpeer1"))?; + runtime.block_on(create_peer_schema_and_insert_stale_row( + &db_path, "oldpeer1", + ))?; let db_url = db_path.to_string_lossy().to_string(); - let _child = spawn_hbbs_in_dir( - port, + let (port, _child) = spawn_hbbs_in_dir( "server-key", temp_dir, - &[("DB_URL", db_url.as_str()), ("MAX_TOTAL_PEER_RECORDS", "1"), ("PEER_RECORD_RETENTION_DAYS", "1")], + &[ + ("DB_URL", db_url.as_str()), + ("MAX_TOTAL_PEER_RECORDS", "1"), + ("PEER_RECORD_RETENTION_DAYS", "1"), + ], )?; let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port); runtime.block_on(async move { - let response = send_register_pk(addr, "newpeer1", "server-key").await? + let response = send_register_pk(addr, "newpeer1", "server-key") + .await? .context("register_pk should receive a response")?; match response.union { - Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { result, .. })) => { + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { + result, + .. + })) => { assert_eq!(result, register_pk_response::Result::OK.into()); } other => bail!("unexpected register_pk response after stale prune: {other:?}"), From a7ad2389249b31a0a68a132106e2b9f2d5799252 Mon Sep 17 00:00:00 2001 From: "Vlad (Kuzmin) Erium" Date: Mon, 13 Apr 2026 19:10:37 +0900 Subject: [PATCH 3/5] fix(security): update hbb_common with upstream file-transfer validation Signed-off-by: Vlad (Kuzmin) Erium --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 26aae293a..36bf31de1 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 26aae293a87e93a01c7681ee428d8f7e1773db1b +Subproject commit 36bf31de1b8b40a257cff07d47e3da1c2e0497da From 2401abdff559c9997143bcea59e853d2173bd1df Mon Sep 17 00:00:00 2001 From: "Vlad (Kuzmin) Erium" Date: Mon, 20 Apr 2026 14:20:17 +0900 Subject: [PATCH 4/5] fix(security): add rendezvous and relay abuse protections Signed-off-by: Vlad (Kuzmin) Erium --- src/common.rs | 74 +++- src/database.rs | 14 +- src/peer.rs | 216 +++++++++++- src/relay_server.rs | 199 ++++++++++- src/rendezvous_server.rs | 532 +++++++++++++++++++++++++---- tests/server_protection_process.rs | 123 ++++++- 6 files changed, 1076 insertions(+), 82 deletions(-) diff --git a/src/common.rs b/src/common.rs index 665d8afc3..9ebeff446 100644 --- a/src/common.rs +++ b/src/common.rs @@ -21,11 +21,15 @@ const TRUST_PROXY_HEADERS_ENV: &str = "TRUST_PROXY_HEADERS"; const CONN_RATE_WINDOW_SECONDS_ENV: &str = "CONNECTION_RATE_WINDOW_SECONDS"; const MAX_CONN_PER_IP_PER_WINDOW_ENV: &str = "MAX_CONNECTIONS_PER_IP_PER_WINDOW"; const MAX_CONNECTION_RATE_ENTRIES_ENV: &str = "MAX_CONNECTION_RATE_ENTRIES"; +const CONTROL_MSG_RATE_WINDOW_SECONDS_ENV: &str = "CONTROL_MESSAGE_RATE_WINDOW_SECONDS"; +const MAX_CONTROL_MSGS_PER_IP_PER_WINDOW_ENV: &str = "MAX_CONTROL_MESSAGES_PER_IP_PER_WINDOW"; const UDP_RATE_WINDOW_SECONDS_ENV: &str = "UDP_RATE_WINDOW_SECONDS"; const MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV: &str = "MAX_UDP_PACKETS_PER_IP_PER_WINDOW"; const DEFAULT_CONN_RATE_WINDOW_SECONDS: usize = 60; const DEFAULT_MAX_CONN_PER_IP_PER_WINDOW: usize = 120; const DEFAULT_MAX_CONNECTION_RATE_ENTRIES: usize = 8_192; +const DEFAULT_CONTROL_MSG_RATE_WINDOW_SECONDS: usize = 60; +const DEFAULT_MAX_CONTROL_MSGS_PER_IP_PER_WINDOW: usize = 1_200; const DEFAULT_UDP_RATE_WINDOW_SECONDS: usize = 60; const DEFAULT_MAX_UDP_PACKETS_PER_IP_PER_WINDOW: usize = 240; @@ -170,6 +174,20 @@ fn udp_rate_window_seconds() -> usize { env_usize_or(UDP_RATE_WINDOW_SECONDS_ENV, DEFAULT_UDP_RATE_WINDOW_SECONDS) } +fn control_msg_rate_window_seconds() -> usize { + env_usize_or( + CONTROL_MSG_RATE_WINDOW_SECONDS_ENV, + DEFAULT_CONTROL_MSG_RATE_WINDOW_SECONDS, + ) +} + +fn max_control_msgs_per_ip_per_window() -> usize { + env_usize_or( + MAX_CONTROL_MSGS_PER_IP_PER_WINDOW_ENV, + DEFAULT_MAX_CONTROL_MSGS_PER_IP_PER_WINDOW, + ) +} + fn max_udp_packets_per_ip_per_window() -> usize { env_usize_or( MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, @@ -270,6 +288,20 @@ pub fn allow_udp_packet_from_ip(scope: &str, addr: SocketAddr) -> bool { allowed } +#[allow(dead_code)] +pub fn allow_control_message_from_ip(scope: &str, addr: SocketAddr) -> bool { + let allowed = allow_ip_activity( + scope, + addr, + control_msg_rate_window_seconds(), + max_control_msgs_per_ip_per_window(), + ); + if !allowed { + record_protection_event("control_message_rate_limit_hits"); + } + allowed +} + #[allow(dead_code)] pub fn record_protection_event(name: &'static str) { let mut lock = PROTECTION_STATS.lock().unwrap(); @@ -300,6 +332,11 @@ pub fn protection_limits_summary() -> Vec { "connection_rate_entry_cap={}", max_connection_rate_entries() ), + format!( + "control_messages_per_ip_per_window={}/{}s", + max_control_msgs_per_ip_per_window(), + control_msg_rate_window_seconds() + ), format!( "udp_packets_per_ip_per_window={}/{}s", max_udp_packets_per_ip_per_window(), @@ -459,13 +496,16 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { #[cfg(test)] mod tests { use super::{ - allow_connection_from_ip, allow_udp_packet_from_ip, apply_trusted_proxy_addr, - conn_rate_window_seconds, max_conn_per_ip_per_window, max_connection_rate_entries, - max_udp_packets_per_ip_per_window, protection_limits_summary, protection_stats_snapshot, - record_protection_event, trust_proxy_headers, udp_rate_window_seconds, - CONNECTION_RATE_LIMITS, CONN_RATE_WINDOW_SECONDS_ENV, MAX_CONNECTION_RATE_ENTRIES_ENV, - MAX_CONN_PER_IP_PER_WINDOW_ENV, MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, PROTECTION_STATS, - TRUST_PROXY_HEADERS_ENV, UDP_RATE_WINDOW_SECONDS_ENV, + allow_connection_from_ip, allow_control_message_from_ip, allow_udp_packet_from_ip, + apply_trusted_proxy_addr, conn_rate_window_seconds, control_msg_rate_window_seconds, + max_conn_per_ip_per_window, max_connection_rate_entries, + max_control_msgs_per_ip_per_window, max_udp_packets_per_ip_per_window, + protection_limits_summary, protection_stats_snapshot, record_protection_event, + trust_proxy_headers, udp_rate_window_seconds, CONNECTION_RATE_LIMITS, + CONN_RATE_WINDOW_SECONDS_ENV, CONTROL_MSG_RATE_WINDOW_SECONDS_ENV, + MAX_CONNECTION_RATE_ENTRIES_ENV, MAX_CONN_PER_IP_PER_WINDOW_ENV, + MAX_CONTROL_MSGS_PER_IP_PER_WINDOW_ENV, MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, + PROTECTION_STATS, TRUST_PROXY_HEADERS_ENV, UDP_RATE_WINDOW_SECONDS_ENV, }; use http::HeaderMap; use std::{ @@ -512,7 +552,9 @@ mod tests { PROTECTION_STATS.lock().unwrap().clear(); std::env::set_var(MAX_CONN_PER_IP_PER_WINDOW_ENV, "2"); std::env::set_var(CONN_RATE_WINDOW_SECONDS_ENV, "60"); - std::env::set_var(MAX_CONNECTION_RATE_ENTRIES_ENV, "2"); + std::env::set_var(MAX_CONNECTION_RATE_ENTRIES_ENV, "4"); + std::env::set_var(MAX_CONTROL_MSGS_PER_IP_PER_WINDOW_ENV, "2"); + std::env::set_var(CONTROL_MSG_RATE_WINDOW_SECONDS_ENV, "60"); std::env::set_var(MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV, "3"); std::env::set_var(UDP_RATE_WINDOW_SECONDS_ENV, "60"); @@ -520,6 +562,9 @@ mod tests { assert!(allow_connection_from_ip("hbbs-main", remote)); assert!(allow_connection_from_ip("hbbs-main", remote)); assert!(!allow_connection_from_ip("hbbs-main", remote)); + assert!(allow_control_message_from_ip("hbbs-control", remote)); + assert!(allow_control_message_from_ip("hbbs-control", remote)); + assert!(!allow_control_message_from_ip("hbbs-control", remote)); assert!(allow_udp_packet_from_ip("hbbs-udp", remote)); assert!(allow_udp_packet_from_ip("hbbs-udp", remote)); assert!(allow_udp_packet_from_ip("hbbs-udp", remote)); @@ -533,6 +578,7 @@ mod tests { protection_stats_snapshot(), vec![ ("connection_rate_limit_hits".to_owned(), 1), + ("control_message_rate_limit_hits".to_owned(), 1), ("udp_rate_limit_hits".to_owned(), 1), ] ); @@ -541,6 +587,7 @@ mod tests { protection_stats_snapshot(), vec![ ("connection_rate_limit_hits".to_owned(), 1), + ("control_message_rate_limit_hits".to_owned(), 1), ("peer_records_pruned".to_owned(), 1), ("udp_rate_limit_hits".to_owned(), 1), ] @@ -549,6 +596,8 @@ mod tests { std::env::remove_var(MAX_CONN_PER_IP_PER_WINDOW_ENV); std::env::remove_var(CONN_RATE_WINDOW_SECONDS_ENV); std::env::remove_var(MAX_CONNECTION_RATE_ENTRIES_ENV); + std::env::remove_var(MAX_CONTROL_MSGS_PER_IP_PER_WINDOW_ENV); + std::env::remove_var(CONTROL_MSG_RATE_WINDOW_SECONDS_ENV); std::env::remove_var(MAX_UDP_PACKETS_PER_IP_PER_WINDOW_ENV); std::env::remove_var(UDP_RATE_WINDOW_SECONDS_ENV); CONNECTION_RATE_LIMITS.lock().unwrap().clear(); @@ -565,6 +614,14 @@ mod tests { max_connection_rate_entries(), super::DEFAULT_MAX_CONNECTION_RATE_ENTRIES ); + assert_eq!( + max_control_msgs_per_ip_per_window(), + super::DEFAULT_MAX_CONTROL_MSGS_PER_IP_PER_WINDOW + ); + assert_eq!( + control_msg_rate_window_seconds(), + super::DEFAULT_CONTROL_MSG_RATE_WINDOW_SECONDS + ); assert_eq!( max_udp_packets_per_ip_per_window(), super::DEFAULT_MAX_UDP_PACKETS_PER_IP_PER_WINDOW @@ -578,6 +635,7 @@ mod tests { vec![ "connections_per_ip_per_window=120/60s".to_owned(), "connection_rate_entry_cap=8192".to_owned(), + "control_messages_per_ip_per_window=1200/60s".to_owned(), "udp_packets_per_ip_per_window=240/60s".to_owned(), "trust_proxy_headers=false".to_owned(), ] diff --git a/src/database.rs b/src/database.rs index 9ed6355c3..f56b4e20e 100644 --- a/src/database.rs +++ b/src/database.rs @@ -85,7 +85,8 @@ impl Database { .parse() .unwrap_or(1); log::debug!("MAX_DATABASE_CONNECTIONS={}", n); - let max_total_peers = env_usize_or("MAX_TOTAL_PEER_RECORDS", DEFAULT_MAX_TOTAL_PEER_RECORDS); + let max_total_peers = + env_usize_or("MAX_TOTAL_PEER_RECORDS", DEFAULT_MAX_TOTAL_PEER_RECORDS); let peer_record_retention_days = env_usize_or( "PEER_RECORD_RETENTION_DAYS", DEFAULT_PEER_RECORD_RETENTION_DAYS, @@ -155,7 +156,11 @@ impl Database { let deleted = self.prune_old_peer_records().await?; if deleted > 0 { crate::common::record_protection_event("peer_records_pruned"); - log::info!("pruned {} old peer records before inserting {}", deleted, id); + log::info!( + "pruned {} old peer records before inserting {}", + deleted, + id + ); } } } @@ -219,7 +224,10 @@ mod tests { use super::{peer_limit_reached, peer_retention_cutoff_arg, peer_retention_prune_enabled}; use hbb_common::tokio; use sqlx::Connection as _; - use std::{path::PathBuf, time::{SystemTime, UNIX_EPOCH}}; + use std::{ + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, + }; #[test] fn peer_limit_helper_rejects_when_total_reaches_cap() { diff --git a/src/peer.rs b/src/peer.rs index f015c437d..bab3c4f10 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -24,6 +24,9 @@ pub const DAY_SECONDS: u64 = 3600 * 24; pub const IP_BLOCK_DUR: u64 = 60; const DEFAULT_MAX_PEER_CACHE_SIZE: usize = 16_384; const DEFAULT_MAX_PENDING_REGISTRATIONS_PER_IP: usize = 64; +const DEFAULT_MAX_IP_BLOCKER_ENTRIES: usize = 8_192; +const DEFAULT_MAX_IP_CHANGES_ENTRIES: usize = 8_192; +const DEFAULT_MAX_UNIQUE_IP_CHANGES_PER_ID: usize = 32; const PEER_CACHE_INACTIVE_TIMEOUT_MS: i32 = 30_000; #[derive(Debug, Default, Serialize, Deserialize, Clone)] @@ -88,6 +91,126 @@ fn pending_registration_limit_exceeded(pending_for_ip: usize, max_pending: usize max_pending > 0 && pending_for_ip >= max_pending } +fn max_ip_blocker_entries() -> usize { + env_usize_or("MAX_IP_BLOCKER_ENTRIES", DEFAULT_MAX_IP_BLOCKER_ENTRIES) +} + +fn max_ip_changes_entries() -> usize { + env_usize_or("MAX_IP_CHANGES_ENTRIES", DEFAULT_MAX_IP_CHANGES_ENTRIES) +} + +fn max_unique_ip_changes_per_id() -> usize { + env_usize_or( + "MAX_UNIQUE_IP_CHANGES_PER_ID", + DEFAULT_MAX_UNIQUE_IP_CHANGES_PER_ID, + ) +} + +pub(crate) fn prune_ip_blocker_entries(entries: &mut IpBlockMap) { + entries.retain(|_, (short_window, daily_window)| { + short_window.1.elapsed().as_secs() <= IP_BLOCK_DUR + || daily_window.1.elapsed().as_secs() <= DAY_SECONDS + }); +} + +fn evict_oldest_ip_blocker_entry(entries: &mut IpBlockMap) -> bool { + let oldest_ip = entries + .iter() + .min_by_key(|(_, (short_window, daily_window))| short_window.1.max(daily_window.1)) + .map(|(ip, _)| ip.clone()); + if let Some(ip) = oldest_ip { + entries.remove(&ip); + return true; + } + false +} + +pub(crate) fn allow_ip_registration_attempt(entries: &mut IpBlockMap, ip: &str, id: &str) -> bool { + prune_ip_blocker_entries(entries); + if !entries.contains_key(ip) { + let max_entries = max_ip_blocker_entries(); + if max_entries > 0 && entries.len() >= max_entries && evict_oldest_ip_blocker_entry(entries) + { + record_protection_event("ip_blocker_entries_evicted"); + } + } + if let Some(old) = entries.get_mut(ip) { + let now = Instant::now(); + let counter = &mut old.0; + if counter.1.elapsed().as_secs() > IP_BLOCK_DUR { + counter.0 = 0; + } else if counter.0 > 30 { + return false; + } + counter.0 += 1; + counter.1 = now; + + let counter = &mut old.1; + let is_new = counter.0.get(id).is_none(); + if counter.1.elapsed().as_secs() > DAY_SECONDS { + counter.0.clear(); + } else if counter.0.len() > 300 { + return !is_new; + } + if is_new { + counter.0.insert(id.to_owned()); + } + counter.1 = now; + } else { + entries.insert( + ip.to_owned(), + ((0, Instant::now()), (Default::default(), Instant::now())), + ); + } + true +} + +pub(crate) fn prune_ip_change_entries(entries: &mut IpChangesMap) { + entries.retain(|_, value| value.0.elapsed().as_secs() < IP_CHANGE_DUR_X2 && value.1.len() > 1); +} + +fn evict_oldest_ip_change_entry(entries: &mut IpChangesMap) -> bool { + let oldest_id = entries + .iter() + .min_by_key(|(_, (tm, _))| *tm) + .map(|(id, _)| id.clone()); + if let Some(id) = oldest_id { + entries.remove(&id); + return true; + } + false +} + +pub(crate) fn track_ip_change(entries: &mut IpChangesMap, id: &str, ip: &str) { + prune_ip_change_entries(entries); + if let Some((tm, ips)) = entries.get_mut(id) { + if tm.elapsed().as_secs() > IP_CHANGE_DUR { + *tm = Instant::now(); + ips.clear(); + ips.insert(ip.to_owned(), 1); + return; + } + if let Some(value) = ips.get_mut(ip) { + *value += 1; + return; + } + if max_unique_ip_changes_per_id() > 0 && ips.len() >= max_unique_ip_changes_per_id() { + record_protection_event("ip_change_ip_limit_hits"); + return; + } + ips.insert(ip.to_owned(), 1); + return; + } + let max_entries = max_ip_changes_entries(); + if max_entries > 0 && entries.len() >= max_entries && evict_oldest_ip_change_entry(entries) { + record_protection_event("ip_changes_entries_evicted"); + } + entries.insert( + id.to_owned(), + (Instant::now(), HashMap::from([(ip.to_owned(), 1)])), + ); +} + fn select_peer_ids_to_evict( entries: Vec, max_cached_peers: usize, @@ -299,8 +422,15 @@ impl PeerMap { #[cfg(test)] mod tests { - use super::{pending_registration_limit_exceeded, select_peer_ids_to_evict, PeerEvictionEntry}; - use std::time::{Duration, Instant}; + use super::{ + allow_ip_registration_attempt, pending_registration_limit_exceeded, + prune_ip_change_entries, select_peer_ids_to_evict, track_ip_change, IpBlockMap, + IpChangesMap, PeerEvictionEntry, DAY_SECONDS, IP_CHANGE_DUR_X2, + }; + use std::{ + collections::{HashMap, HashSet}, + time::{Duration, Instant}, + }; #[test] fn pending_registration_limit_flags_excessive_per_ip_growth() { @@ -339,4 +469,86 @@ mod tests { vec!["inactive-old".to_owned(), "inactive-new".to_owned()] ); } + + #[test] + fn allow_ip_registration_attempt_prunes_and_caps_entries() { + std::env::set_var("MAX_IP_BLOCKER_ENTRIES", "2"); + let now = Instant::now(); + let stale = now + .checked_sub(Duration::from_secs(DAY_SECONDS + 5)) + .unwrap_or(now); + let older = now.checked_sub(Duration::from_secs(10)).unwrap_or(now); + let newer = now.checked_sub(Duration::from_secs(1)).unwrap_or(now); + let mut entries: IpBlockMap = HashMap::from([ + ( + "198.51.100.1".to_owned(), + ((0, stale), (HashSet::new(), stale)), + ), + ( + "198.51.100.2".to_owned(), + ((1, older), (HashSet::from(["peer-a".to_owned()]), older)), + ), + ( + "198.51.100.3".to_owned(), + ((1, newer), (HashSet::from(["peer-b".to_owned()]), newer)), + ), + ]); + assert!(allow_ip_registration_attempt( + &mut entries, + "198.51.100.4", + "peer-c" + )); + assert_eq!(entries.len(), 2); + assert!(!entries.contains_key("198.51.100.1")); + assert!(!entries.contains_key("198.51.100.2")); + assert!(entries.contains_key("198.51.100.3")); + assert!(entries.contains_key("198.51.100.4")); + std::env::remove_var("MAX_IP_BLOCKER_ENTRIES"); + } + + #[test] + fn track_ip_change_prunes_and_limits_unique_ips() { + std::env::set_var("MAX_IP_CHANGES_ENTRIES", "2"); + std::env::set_var("MAX_UNIQUE_IP_CHANGES_PER_ID", "2"); + let now = Instant::now(); + let stale = now + .checked_sub(Duration::from_secs(IP_CHANGE_DUR_X2 + 5)) + .unwrap_or(now); + let mut entries: IpChangesMap = HashMap::from([ + ( + "stale-id".to_owned(), + (stale, HashMap::from([("198.51.100.1".to_owned(), 1)])), + ), + ( + "peer-a".to_owned(), + ( + now, + HashMap::from([ + ("198.51.100.2".to_owned(), 1), + ("198.51.100.3".to_owned(), 1), + ]), + ), + ), + ( + "peer-b".to_owned(), + ( + now, + HashMap::from([ + ("198.51.100.4".to_owned(), 1), + ("198.51.100.5".to_owned(), 1), + ]), + ), + ), + ]); + prune_ip_change_entries(&mut entries); + assert_eq!(entries.len(), 2); + track_ip_change(&mut entries, "peer-a", "198.51.100.9"); + assert_eq!(entries["peer-a"].1.len(), 2); + track_ip_change(&mut entries, "peer-c", "198.51.100.6"); + assert_eq!(entries.len(), 2); + assert!(entries.contains_key("peer-a")); + assert!(entries.contains_key("peer-c")); + std::env::remove_var("MAX_IP_CHANGES_ENTRIES"); + std::env::remove_var("MAX_UNIQUE_IP_CHANGES_PER_ID"); + } } diff --git a/src/relay_server.rs b/src/relay_server.rs index 08243ad3d..18ed28095 100644 --- a/src/relay_server.rs +++ b/src/relay_server.rs @@ -47,8 +47,15 @@ impl PendingRelay { } } +struct ActiveRelay { + peer_a_ip: String, + peer_b_ip: String, + started_at: Instant, +} + lazy_static::lazy_static! { static ref PEERS: Mutex> = Default::default(); + static ref ACTIVE_RELAYS: Mutex> = Default::default(); static ref USAGE: RwLock> = Default::default(); static ref BLACKLIST: RwLock> = Default::default(); static ref BLOCKLIST: RwLock> = Default::default(); @@ -61,6 +68,10 @@ static TOTAL_BANDWIDTH: AtomicUsize = AtomicUsize::new(1024 * 1024 * 1024); // i static SINGLE_BANDWIDTH: AtomicUsize = AtomicUsize::new(128 * 1024 * 1024); // in bit/s static MAX_PENDING_RELAYS: AtomicUsize = AtomicUsize::new(4096); static MAX_PENDING_RELAYS_PER_IP: AtomicUsize = AtomicUsize::new(64); +static MAX_ACTIVE_RELAYS: AtomicUsize = AtomicUsize::new(2048); +static MAX_ACTIVE_RELAYS_PER_IP: AtomicUsize = AtomicUsize::new(128); +static RELAY_IDLE_TIMEOUT_SECS: AtomicUsize = AtomicUsize::new(30); +static MAX_RELAY_SESSION_SECS: AtomicUsize = AtomicUsize::new(14_400); const BLACKLIST_FILE: &str = "blacklist.txt"; const BLOCKLIST_FILE: &str = "blocklist.txt"; const PENDING_RELAY_HOLD_SECS: u64 = 30; @@ -187,6 +198,46 @@ fn check_params() { "MAX_PENDING_RELAYS_PER_IP: {}", MAX_PENDING_RELAYS_PER_IP.load(Ordering::SeqCst) ); + let tmp = std::env::var("MAX_ACTIVE_RELAYS") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + if tmp > 0 { + MAX_ACTIVE_RELAYS.store(tmp, Ordering::SeqCst); + } + log::info!( + "MAX_ACTIVE_RELAYS: {}", + MAX_ACTIVE_RELAYS.load(Ordering::SeqCst) + ); + let tmp = std::env::var("MAX_ACTIVE_RELAYS_PER_IP") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + if tmp > 0 { + MAX_ACTIVE_RELAYS_PER_IP.store(tmp, Ordering::SeqCst); + } + log::info!( + "MAX_ACTIVE_RELAYS_PER_IP: {}", + MAX_ACTIVE_RELAYS_PER_IP.load(Ordering::SeqCst) + ); + let tmp = std::env::var("RELAY_IDLE_TIMEOUT_SECS") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + if tmp > 0 { + RELAY_IDLE_TIMEOUT_SECS.store(tmp, Ordering::SeqCst); + } + log::info!( + "RELAY_IDLE_TIMEOUT_SECS: {}", + RELAY_IDLE_TIMEOUT_SECS.load(Ordering::SeqCst) + ); + let tmp = std::env::var("MAX_RELAY_SESSION_SECS") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + if tmp > 0 { + MAX_RELAY_SESSION_SECS.store(tmp, Ordering::SeqCst); + } + log::info!( + "MAX_RELAY_SESSION_SECS: {}", + MAX_RELAY_SESSION_SECS.load(Ordering::SeqCst) + ); } async fn check_cmd(cmd: &str, limiter: Limiter) -> String { @@ -197,7 +248,7 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String { match fds.next() { Some("h") => { res = format!( - "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", "blacklist-add(ba) ", "blacklist-remove(br) ", "blacklist(b) ", @@ -209,6 +260,7 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String { "limit-speed(ls) [value(Mb/s)]", "total-bandwidth(tb) [value(Mb/s)]", "single-bandwidth(sb) [value(Mb/s)]", + "active-relays(ar)", "protection-stats(ps)", "usage(u)" ) @@ -359,10 +411,44 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String { ); } } + Some("active-relays" | "ar") => { + let active_relays = ACTIVE_RELAYS.lock().await; + let _ = writeln!(res, "{}", active_relays.len()); + for (uuid, relay) in active_relays.iter() { + let _ = writeln!( + res, + "{}: {} <-> {} ({}s)", + uuid, + relay.peer_a_ip, + relay.peer_b_ip, + relay.started_at.elapsed().as_secs() + ); + } + } Some("protection-stats" | "ps") => { for line in crate::common::protection_limits_summary() { let _ = writeln!(res, "{line}"); } + let _ = writeln!( + res, + "max_active_relays={}", + MAX_ACTIVE_RELAYS.load(Ordering::SeqCst) + ); + let _ = writeln!( + res, + "max_active_relays_per_ip={}", + MAX_ACTIVE_RELAYS_PER_IP.load(Ordering::SeqCst) + ); + let _ = writeln!( + res, + "relay_idle_timeout_secs={}", + RELAY_IDLE_TIMEOUT_SECS.load(Ordering::SeqCst) + ); + let _ = writeln!( + res, + "max_relay_session_secs={}", + MAX_RELAY_SESSION_SECS.load(Ordering::SeqCst) + ); for (name, value) in crate::common::protection_stats_snapshot() { let _ = writeln!(res, "{name}={value}"); } @@ -492,6 +578,40 @@ async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limit let mut pending = PEERS.lock().await.remove(&rf.uuid); if let Some(pending) = pending.as_mut() { log::info!("Relayrequest {} from {} got paired", rf.uuid, addr); + let peer_a_ip = pending.ip.clone(); + let peer_b_ip = addr.ip().to_string(); + let mut active_relays = ACTIVE_RELAYS.lock().await; + let active_for_peer_a = + count_active_relays_for_ip(&active_relays, &peer_a_ip); + let active_for_peer_b = + count_active_relays_for_ip(&active_relays, &peer_b_ip); + if let Some(reason) = active_relay_limit_reason( + active_relays.len(), + active_for_peer_a, + active_for_peer_b, + ) { + drop(active_relays); + crate::common::record_protection_event("active_relay_rejected"); + log::warn!( + "Rejecting active relay {} between {} and {}: {}", + rf.uuid, + peer_a_ip, + peer_b_ip, + reason + ); + send_relay_refuse(&mut stream, reason).await; + send_relay_refuse(&mut *pending.stream, reason).await; + return; + } + active_relays.insert( + rf.uuid.clone(), + ActiveRelay { + peer_a_ip, + peer_b_ip, + started_at: Instant::now(), + }, + ); + drop(active_relays); let id = format!("{}:{}", addr.ip(), addr.port()); USAGE.write().await.insert(id.clone(), Default::default()); let peer = &mut pending.stream; @@ -500,12 +620,14 @@ async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limit stream.set_raw(); log::info!("Both are raw"); } - if let Err(err) = relay(addr, &mut stream, peer, limiter, id.clone()).await + if let Err(err) = + relay(&rf.uuid, addr, &mut stream, peer, limiter, id.clone()).await { log::info!("Relay of {} closed: {}", addr, err); } else { log::info!("Relay of {} closed", addr); } + ACTIVE_RELAYS.lock().await.remove(&rf.uuid); USAGE.write().await.remove(&id); } else { log::info!("New relay request {} from {}", rf.uuid, addr); @@ -552,7 +674,30 @@ fn pending_relay_limit_reason(total_pending: usize, pending_for_ip: usize) -> Op None } -async fn send_relay_refuse(stream: &mut impl StreamTrait, reason: &str) { +fn count_active_relays_for_ip(active_relays: &HashMap, ip: &str) -> usize { + active_relays + .values() + .filter(|relay| relay.peer_a_ip == ip || relay.peer_b_ip == ip) + .count() +} + +fn active_relay_limit_reason( + total_active: usize, + active_for_peer_a: usize, + active_for_peer_b: usize, +) -> Option<&'static str> { + if total_active >= MAX_ACTIVE_RELAYS.load(Ordering::SeqCst) { + return Some("Relay server busy"); + } + if active_for_peer_a >= MAX_ACTIVE_RELAYS_PER_IP.load(Ordering::SeqCst) + || active_for_peer_b >= MAX_ACTIVE_RELAYS_PER_IP.load(Ordering::SeqCst) + { + return Some("Too many active relay sessions"); + } + None +} + +async fn send_relay_refuse(stream: &mut dyn StreamTrait, reason: &str) { let mut msg_out = RendezvousMessage::new(); msg_out.set_relay_response(RelayResponse { refuse_reason: reason.to_owned(), @@ -564,6 +709,7 @@ async fn send_relay_refuse(stream: &mut impl StreamTrait, reason: &str) { } async fn relay( + relay_uuid: &str, addr: SocketAddr, stream: &mut impl StreamTrait, peer: &mut Box, @@ -585,6 +731,7 @@ async fn relay( (sb * DOWNGRADE_THRESHOLD_100.load(Ordering::SeqCst) as f64 / 100. / 1000.) as usize; // in bit/ms let mut timer = interval(Duration::from_secs(3)); let mut last_recv_time = std::time::Instant::now(); + let session_started_at = std::time::Instant::now(); loop { tokio::select! { res = peer.recv() => { @@ -626,9 +773,18 @@ async fn relay( } }, _ = timer.tick() => { - if last_recv_time.elapsed().as_secs() > 30 { + if last_recv_time.elapsed().as_secs() + > RELAY_IDLE_TIMEOUT_SECS.load(Ordering::SeqCst) as u64 + { bail!("Timeout"); } + let max_session_secs = MAX_RELAY_SESSION_SECS.load(Ordering::SeqCst); + if max_session_secs > 0 + && session_started_at.elapsed().as_secs() > max_session_secs as u64 + { + crate::common::record_protection_event("relay_session_duration_limit_hits"); + bail!("Relay session duration limit reached"); + } } } @@ -656,7 +812,8 @@ async fn relay( { downgrade = true; log::info!( - "Downgrade {}, exceed downgrade threshold {}bit/ms in {}ms", + "Downgrade relay {} ({}), exceed downgrade threshold {}bit/ms in {}ms", + relay_uuid, id, downgrade_threshold, elapsed @@ -750,7 +907,10 @@ impl StreamTrait for tokio_tungstenite::WebSocketStream { #[cfg(test)] mod tests { - use super::{pending_relay_limit_reason, MAX_PENDING_RELAYS, MAX_PENDING_RELAYS_PER_IP}; + use super::{ + active_relay_limit_reason, pending_relay_limit_reason, MAX_ACTIVE_RELAYS, + MAX_ACTIVE_RELAYS_PER_IP, MAX_PENDING_RELAYS, MAX_PENDING_RELAYS_PER_IP, + }; use std::sync::{atomic::Ordering, Mutex}; static TEST_RELAY_LIMITS_LOCK: Mutex<()> = Mutex::new(()); @@ -774,4 +934,31 @@ mod tests { MAX_PENDING_RELAYS.store(saved_total, Ordering::SeqCst); MAX_PENDING_RELAYS_PER_IP.store(saved_per_ip, Ordering::SeqCst); } + + #[test] + fn active_relay_limits_enforce_global_and_per_ip_caps() { + let _guard = TEST_RELAY_LIMITS_LOCK.lock().unwrap(); + let saved_total = MAX_ACTIVE_RELAYS.load(Ordering::SeqCst); + let saved_per_ip = MAX_ACTIVE_RELAYS_PER_IP.load(Ordering::SeqCst); + + MAX_ACTIVE_RELAYS.store(2, Ordering::SeqCst); + MAX_ACTIVE_RELAYS_PER_IP.store(1, Ordering::SeqCst); + + assert_eq!(active_relay_limit_reason(0, 0, 0), None); + assert_eq!( + active_relay_limit_reason(1, 1, 0), + Some("Too many active relay sessions") + ); + assert_eq!( + active_relay_limit_reason(1, 0, 1), + Some("Too many active relay sessions") + ); + assert_eq!( + active_relay_limit_reason(2, 0, 0), + Some("Relay server busy") + ); + + MAX_ACTIVE_RELAYS.store(saved_total, Ordering::SeqCst); + MAX_ACTIVE_RELAYS_PER_IP.store(saved_per_ip, Ordering::SeqCst); + } } diff --git a/src/rendezvous_server.rs b/src/rendezvous_server.rs index 8580a56db..a8bedc2c6 100644 --- a/src/rendezvous_server.rs +++ b/src/rendezvous_server.rs @@ -35,7 +35,7 @@ use hbb_common::{ use ipnetwork::Ipv4Network; use sodiumoxide::crypto::sign; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, sync::atomic::{AtomicBool, AtomicUsize, Ordering}, sync::Arc, @@ -52,12 +52,30 @@ enum Data { const REG_TIMEOUT: i32 = 30_000; const MIN_REGISTRATION_ID_LEN: usize = 6; const MAX_REGISTRATION_ID_LEN: usize = 100; +const MAX_ONLINE_REQUEST_PEERS_ENV: &str = "MAX_ONLINE_REQUEST_PEERS"; +const FANOUT_WINDOW_SECONDS_ENV: &str = "FANOUT_WINDOW_SECONDS"; +const MAX_FANOUT_TRACKED_SOURCES_ENV: &str = "MAX_FANOUT_TRACKED_SOURCES"; +const MAX_PUNCH_TARGETS_PER_IP_PER_WINDOW_ENV: &str = "MAX_PUNCH_TARGETS_PER_IP_PER_WINDOW"; +const MAX_RELAY_TARGETS_PER_IP_PER_WINDOW_ENV: &str = "MAX_RELAY_TARGETS_PER_IP_PER_WINDOW"; +const TCP_PUNCH_ENTRY_TTL_SECS_ENV: &str = "TCP_PUNCH_ENTRY_TTL_SECS"; +const MAX_TCP_PUNCH_ENTRIES_ENV: &str = "MAX_TCP_PUNCH_ENTRIES"; +const DEFAULT_MAX_ONLINE_REQUEST_PEERS: usize = 4_096; +const DEFAULT_FANOUT_WINDOW_SECONDS: usize = 60; +const DEFAULT_MAX_FANOUT_TRACKED_SOURCES: usize = 8_192; +const DEFAULT_MAX_PUNCH_TARGETS_PER_IP_PER_WINDOW: usize = 256; +const DEFAULT_MAX_RELAY_TARGETS_PER_IP_PER_WINDOW: usize = 256; +const DEFAULT_TCP_PUNCH_ENTRY_TTL_SECS: usize = 30; +const DEFAULT_MAX_TCP_PUNCH_ENTRIES: usize = 4_096; type TcpStreamSink = SplitSink, Bytes>; type WsSink = SplitSink, tungstenite::Message>; enum Sink { TcpStream(TcpStreamSink), Ws(WsSink), } +struct TcpPunchEntry { + sink: Sink, + created_at: Instant, +} type Sender = mpsc::UnboundedSender; type Receiver = mpsc::UnboundedReceiver; static ROTATION_RELAY_SERVER: AtomicUsize = AtomicUsize::new(0); @@ -80,6 +98,16 @@ const PUNCH_REQ_DEDUPE_SEC: u64 = 60; const PUNCH_REQ_RETENTION_SECS: u64 = 600; const MAX_PUNCH_REQS: usize = 8192; +struct FanoutEntry { + window_started_at: Instant, + last_seen_at: Instant, + targets: HashSet, +} + +type FanoutMap = HashMap; +static PUNCH_FANOUT: Lazy> = Lazy::new(|| TokioMutex::new(HashMap::new())); +static RELAY_FANOUT: Lazy> = Lazy::new(|| TokioMutex::new(HashMap::new())); + #[derive(Clone)] struct Inner { serial: i32, @@ -92,7 +120,7 @@ struct Inner { #[derive(Clone)] pub struct RendezvousServer { - tcp_punch: Arc>>, + tcp_punch: Arc>>, pm: PeerMap, tx: Sender, relay_servers: Arc, @@ -462,22 +490,7 @@ impl RendezvousServer { peer.write().await.reg_pk = req_pk; if ip_changed { let mut lock = IP_CHANGES.lock().await; - if let Some((tm, ips)) = lock.get_mut(&id) { - if tm.elapsed().as_secs() > IP_CHANGE_DUR { - *tm = Instant::now(); - ips.clear(); - ips.insert(ip.clone(), 1); - } else if let Some(v) = ips.get_mut(&ip) { - *v += 1; - } else { - ips.insert(ip.clone(), 1); - } - } else { - lock.insert( - id.clone(), - (Instant::now(), HashMap::from([(ip.clone(), 1)])), - ); - } + track_ip_change(&mut lock, &id, &ip); } let result = if changed { self.pm.update_pk(id, peer, addr, rk.uuid, rk.pk, ip).await @@ -556,19 +569,96 @@ impl RendezvousServer { ws: bool, ) -> bool { if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(bytes) { + let should_rate_limit = matches!( + msg_in.union.as_ref(), + Some(rendezvous_message::Union::PunchHoleRequest(_)) + | Some(rendezvous_message::Union::RequestRelay(_)) + | Some(rendezvous_message::Union::RelayResponse(_)) + | Some(rendezvous_message::Union::PunchHoleSent(_)) + | Some(rendezvous_message::Union::LocalAddr(_)) + | Some(rendezvous_message::Union::TestNatRequest(_)) + ); + if should_rate_limit + && !crate::common::allow_control_message_from_ip("hbbs-control", addr) + { + log::warn!("Control message rate limit exceeded from {}", addr.ip()); + return false; + } match msg_in.union { Some(rendezvous_message::Union::PunchHoleRequest(ph)) => { // there maybe several attempt, so sink can be none if let Some(sink) = sink.take() { - self.tcp_punch.lock().await.insert(try_into_v4(addr), sink); + let rejected_sink = { + let mut lock = self.tcp_punch.lock().await; + insert_tcp_punch_entry(&mut lock, try_into_v4(addr), sink) + }; + if let Some(rejected_sink) = rejected_sink { + crate::common::record_protection_event("tcp_punch_entry_limit_hits"); + log::warn!("tcp_punch entry limit exceeded for {}", addr); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_punch_hole_response(PunchHoleResponse { + other_failure: "Too many pending TCP punch sessions".to_owned(), + ..Default::default() + }); + Self::send_to_sink(&mut Some(rejected_sink), msg_out).await; + return false; + } } allow_err!(self.handle_tcp_punch_hole_request(addr, ph, key, ws).await); return true; } Some(rendezvous_message::Union::RequestRelay(mut rf)) => { + let source_ip = try_into_v4(addr).ip().to_string(); + let target_id = rf.id.clone(); + let allowed = { + let mut lock = RELAY_FANOUT.lock().await; + allow_target_fanout( + &mut lock, + &source_ip, + &target_id, + max_relay_targets_per_ip_per_window(), + "relay_fanout_entries_evicted", + "relay_fanout_entries_rejected", + ) + }; + if !allowed { + crate::common::record_protection_event("relay_target_fanout_limit_hits"); + log::warn!( + "Relay target fan-out limit exceeded from {} toward {}", + addr, + target_id + ); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_relay_response(RelayResponse { + uuid: rf.uuid.clone(), + refuse_reason: "Too many distinct relay targets".to_owned(), + ..Default::default() + }); + if sink.is_some() { + Self::send_to_sink(sink, msg_out).await; + } else { + allow_err!(self.send_to_tcp_sync(msg_out, addr).await); + } + return true; + } // there maybe several attempt, so sink can be none if let Some(sink) = sink.take() { - self.tcp_punch.lock().await.insert(try_into_v4(addr), sink); + let rejected_sink = { + let mut lock = self.tcp_punch.lock().await; + insert_tcp_punch_entry(&mut lock, try_into_v4(addr), sink) + }; + if let Some(rejected_sink) = rejected_sink { + crate::common::record_protection_event("tcp_punch_entry_limit_hits"); + log::warn!("tcp_punch entry limit exceeded for {}", addr); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_relay_response(RelayResponse { + uuid: rf.uuid.clone(), + refuse_reason: "Too many pending TCP punch sessions".to_owned(), + ..Default::default() + }); + Self::send_to_sink(&mut Some(rejected_sink), msg_out).await; + return true; + } } if let Some(peer) = self.pm.get_in_memory(&rf.id).await { let mut msg_out = RendezvousMessage::new(); @@ -772,6 +862,32 @@ impl RendezvousServer { return Ok((msg_out, None)); } let id = ph.id; + let source_ip = try_into_v4(addr).ip().to_string(); + let allowed = { + let mut lock = PUNCH_FANOUT.lock().await; + allow_target_fanout( + &mut lock, + &source_ip, + &id, + max_punch_targets_per_ip_per_window(), + "punch_fanout_entries_evicted", + "punch_fanout_entries_rejected", + ) + }; + if !allowed { + crate::common::record_protection_event("punch_target_fanout_limit_hits"); + log::warn!( + "Punch target fan-out limit exceeded from {} toward {}", + addr, + id + ); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_punch_hole_response(PunchHoleResponse { + other_failure: "Too many distinct punch targets".to_owned(), + ..Default::default() + }); + return Ok((msg_out, None)); + } // punch hole request from A, relay to B, // check if in same intranet first, // fetch local addrs if in same intranet. @@ -882,8 +998,17 @@ impl RendezvousServer { stream: &mut FramedStream, peers: Vec, ) -> ResultType<()> { + let peer_lookup_limit = clamped_online_request_peer_count(peers.len()); + if peer_lookup_limit < peers.len() { + crate::common::record_protection_event("online_request_peer_limit_hits"); + log::warn!( + "Capping online request lookup from {} to {} peers", + peers.len(), + peer_lookup_limit + ); + } let mut states = BytesMut::zeroed((peers.len() + 7) / 8); - for (i, peer_id) in peers.iter().enumerate() { + for (i, peer_id) in peers.iter().take(peer_lookup_limit).enumerate() { if let Some(peer) = self.pm.get_in_memory(peer_id).await { let elapsed = peer.read().await.last_reg_time.elapsed().as_millis() as i32; // bytes index from left to right @@ -907,7 +1032,11 @@ impl RendezvousServer { #[inline] async fn send_to_tcp(&mut self, msg: RendezvousMessage, addr: SocketAddr) { - let mut tcp = self.tcp_punch.lock().await.remove(&try_into_v4(addr)); + let mut tcp = { + let mut lock = self.tcp_punch.lock().await; + prune_tcp_punch_entries(&mut lock); + lock.remove(&try_into_v4(addr)).map(|entry| entry.sink) + }; tokio::spawn(async move { Self::send_to_sink(&mut tcp, msg).await; }); @@ -935,7 +1064,11 @@ impl RendezvousServer { msg: RendezvousMessage, addr: SocketAddr, ) -> ResultType<()> { - let mut sink = self.tcp_punch.lock().await.remove(&try_into_v4(addr)); + let mut sink = { + let mut lock = self.tcp_punch.lock().await; + prune_tcp_punch_entries(&mut lock); + lock.remove(&try_into_v4(addr)).map(|entry| entry.sink) + }; Self::send_to_sink(&mut sink, msg).await; Ok(()) } @@ -977,32 +1110,7 @@ impl RendezvousServer { async fn check_ip_blocker(&self, ip: &str, id: &str) -> bool { let mut lock = IP_BLOCKER.lock().await; - let now = Instant::now(); - if let Some(old) = lock.get_mut(ip) { - let counter = &mut old.0; - if counter.1.elapsed().as_secs() > IP_BLOCK_DUR { - counter.0 = 0; - } else if counter.0 > 30 { - return false; - } - counter.0 += 1; - counter.1 = now; - - let counter = &mut old.1; - let is_new = counter.0.get(id).is_none(); - if counter.1.elapsed().as_secs() > DAY_SECONDS { - counter.0.clear(); - } else if counter.0.len() > 300 { - return !is_new; - } - if is_new { - counter.0.insert(id.to_owned()); - } - counter.1 = now; - } else { - lock.insert(ip.to_owned(), ((0, now), (Default::default(), now))); - } - true + allow_ip_registration_attempt(&mut lock, ip, id) } fn parse_relay_servers(&mut self, relay_servers: &str) { @@ -1051,10 +1159,7 @@ impl RendezvousServer { } Some("ip-blocker" | "ib") => { let mut lock = IP_BLOCKER.lock().await; - lock.retain(|&_, (a, b)| { - a.1.elapsed().as_secs() <= IP_BLOCK_DUR - || b.1.elapsed().as_secs() <= DAY_SECONDS - }); + prune_ip_blocker_entries(&mut lock); res = format!("{}\n", lock.len()); let ip = fds.next(); let mut start = ip.map(|x| x.parse::().unwrap_or(-1)).unwrap_or(-1); @@ -1103,7 +1208,7 @@ impl RendezvousServer { } Some("ip-changes" | "ic") => { let mut lock = IP_CHANGES.lock().await; - lock.retain(|&_, v| v.0.elapsed().as_secs() < IP_CHANGE_DUR_X2 && v.1.len() > 1); + prune_ip_change_entries(&mut lock); res = format!("{}\n", lock.len()); let id = fds.next(); let mut start = id.map(|x| x.parse::().unwrap_or(-1)).unwrap_or(-1); @@ -1143,7 +1248,7 @@ impl RendezvousServer { lock.clear(); } else { prune_punch_requests(&mut lock); - let mut start = arg.and_then(|x| x.parse::().ok()).unwrap_or(0); + let start = arg.and_then(|x| x.parse::().ok()).unwrap_or(0); let mut page_size = fds .next() .and_then(|x| x.parse::().ok()) @@ -1184,6 +1289,39 @@ impl RendezvousServer { for line in crate::common::protection_limits_summary() { let _ = writeln!(res, "{line}"); } + let tcp_punch_entries = { + let mut lock = self.tcp_punch.lock().await; + prune_tcp_punch_entries(&mut lock); + lock.len() + }; + let _ = writeln!( + res, + "tcp_punch_entry_ttl_secs={}", + tcp_punch_entry_ttl_secs() + ); + let _ = writeln!(res, "max_tcp_punch_entries={}", max_tcp_punch_entries()); + let _ = writeln!(res, "tcp_punch_entries={tcp_punch_entries}"); + let _ = writeln!(res, "fanout_window_seconds={}", fanout_window_seconds()); + let _ = writeln!( + res, + "max_fanout_tracked_sources={}", + max_fanout_tracked_sources() + ); + let _ = writeln!( + res, + "max_punch_targets_per_ip_per_window={}", + max_punch_targets_per_ip_per_window() + ); + let _ = writeln!( + res, + "max_relay_targets_per_ip_per_window={}", + max_relay_targets_per_ip_per_window() + ); + let _ = writeln!( + res, + "max_online_request_peers={}", + max_online_request_peers() + ); for (name, value) in crate::common::protection_stats_snapshot() { let _ = writeln!(res, "{name}={value}"); } @@ -1465,6 +1603,179 @@ fn prune_punch_requests(entries: &mut Vec) { } } +fn fanout_window_seconds() -> usize { + std::env::var(FANOUT_WINDOW_SECONDS_ENV) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_FANOUT_WINDOW_SECONDS) +} + +fn max_fanout_tracked_sources() -> usize { + std::env::var(MAX_FANOUT_TRACKED_SOURCES_ENV) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_MAX_FANOUT_TRACKED_SOURCES) +} + +fn max_punch_targets_per_ip_per_window() -> usize { + std::env::var(MAX_PUNCH_TARGETS_PER_IP_PER_WINDOW_ENV) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_MAX_PUNCH_TARGETS_PER_IP_PER_WINDOW) +} + +fn max_relay_targets_per_ip_per_window() -> usize { + std::env::var(MAX_RELAY_TARGETS_PER_IP_PER_WINDOW_ENV) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_MAX_RELAY_TARGETS_PER_IP_PER_WINDOW) +} + +fn tcp_punch_entry_ttl_secs() -> usize { + std::env::var(TCP_PUNCH_ENTRY_TTL_SECS_ENV) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_TCP_PUNCH_ENTRY_TTL_SECS) +} + +fn max_tcp_punch_entries() -> usize { + std::env::var(MAX_TCP_PUNCH_ENTRIES_ENV) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_MAX_TCP_PUNCH_ENTRIES) +} + +fn prune_tcp_punch_entries(entries: &mut HashMap) { + let before = entries.len(); + let ttl_secs = tcp_punch_entry_ttl_secs() as u64; + entries.retain(|_, entry| entry.created_at.elapsed().as_secs() < ttl_secs); + if before > entries.len() { + crate::common::record_protection_event("tcp_punch_entries_pruned"); + } +} + +fn evict_oldest_tcp_punch_entry(entries: &mut HashMap) -> bool { + let oldest_addr = entries + .iter() + .min_by_key(|(_, entry)| entry.created_at) + .map(|(addr, _)| *addr); + if let Some(addr) = oldest_addr { + entries.remove(&addr); + return true; + } + false +} + +fn insert_tcp_punch_entry( + entries: &mut HashMap, + addr: SocketAddr, + sink: Sink, +) -> Option { + prune_tcp_punch_entries(entries); + if !entries.contains_key(&addr) { + let max_entries = max_tcp_punch_entries(); + if max_entries > 0 && entries.len() >= max_entries && evict_oldest_tcp_punch_entry(entries) + { + crate::common::record_protection_event("tcp_punch_entries_evicted"); + } + if max_entries > 0 && entries.len() >= max_entries { + crate::common::record_protection_event("tcp_punch_entries_rejected"); + return Some(sink); + } + } + entries.insert( + addr, + TcpPunchEntry { + sink, + created_at: Instant::now(), + }, + ); + None +} + +fn prune_target_fanout(entries: &mut FanoutMap) { + let window_secs = fanout_window_seconds() as u64; + entries + .retain(|_, entry| entry.last_seen_at.elapsed().as_secs() < window_secs.saturating_mul(2)); +} + +fn evict_oldest_target_fanout(entries: &mut FanoutMap) -> bool { + let oldest_ip = entries + .iter() + .min_by_key(|(_, entry)| entry.last_seen_at) + .map(|(ip, _)| ip.clone()); + if let Some(ip) = oldest_ip { + entries.remove(&ip); + return true; + } + false +} + +fn allow_target_fanout( + entries: &mut FanoutMap, + source_ip: &str, + target_id: &str, + max_targets_per_window: usize, + evicted_event: &'static str, + rejected_event: &'static str, +) -> bool { + let now = Instant::now(); + let window_secs = fanout_window_seconds() as u64; + prune_target_fanout(entries); + if let Some(entry) = entries.get_mut(source_ip) { + if now.duration_since(entry.window_started_at).as_secs() >= window_secs { + entry.window_started_at = now; + entry.targets.clear(); + } + entry.last_seen_at = now; + if entry.targets.contains(target_id) { + return true; + } + if max_targets_per_window > 0 && entry.targets.len() >= max_targets_per_window { + return false; + } + entry.targets.insert(target_id.to_owned()); + return true; + } + + let max_entries = max_fanout_tracked_sources(); + if max_entries > 0 && entries.len() >= max_entries && evict_oldest_target_fanout(entries) { + crate::common::record_protection_event(evicted_event); + } + if max_entries > 0 && entries.len() >= max_entries { + crate::common::record_protection_event(rejected_event); + return false; + } + + entries.insert( + source_ip.to_owned(), + FanoutEntry { + window_started_at: now, + last_seen_at: now, + targets: HashSet::from([target_id.to_owned()]), + }, + ); + true +} + +fn max_online_request_peers() -> usize { + std::env::var(MAX_ONLINE_REQUEST_PEERS_ENV) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_MAX_ONLINE_REQUEST_PEERS) +} + +fn clamped_online_request_peer_count(total_peers: usize) -> usize { + total_peers.min(max_online_request_peers()) +} + #[inline] fn is_valid_registration_id(id: &str) -> bool { let len = id.chars().count(); @@ -1479,10 +1790,16 @@ fn is_valid_server_key(configured_key: &str, supplied_key: &str) -> bool { #[cfg(test)] mod tests { use super::{ - is_valid_registration_id, is_valid_server_key, prune_punch_requests, PunchReqEntry, - MAX_PUNCH_REQS, PUNCH_REQ_RETENTION_SECS, + allow_target_fanout, clamped_online_request_peer_count, is_valid_registration_id, + is_valid_server_key, prune_punch_requests, FanoutMap, PunchReqEntry, + DEFAULT_MAX_ONLINE_REQUEST_PEERS, FANOUT_WINDOW_SECONDS_ENV, + MAX_FANOUT_TRACKED_SOURCES_ENV, MAX_ONLINE_REQUEST_PEERS_ENV, MAX_PUNCH_REQS, + PUNCH_REQ_RETENTION_SECS, + }; + use std::{ + collections::HashMap, + time::{Duration, Instant}, }; - use std::time::{Duration, Instant}; #[test] fn server_key_validation_accepts_open_server_or_matching_key() { @@ -1538,6 +1855,105 @@ mod tests { Some("peer-5") ); } + + #[test] + fn online_request_peer_lookup_count_is_bounded() { + std::env::remove_var(MAX_ONLINE_REQUEST_PEERS_ENV); + assert_eq!( + clamped_online_request_peer_count(DEFAULT_MAX_ONLINE_REQUEST_PEERS + 1), + DEFAULT_MAX_ONLINE_REQUEST_PEERS + ); + + std::env::set_var(MAX_ONLINE_REQUEST_PEERS_ENV, "3"); + assert_eq!(clamped_online_request_peer_count(2), 2); + assert_eq!(clamped_online_request_peer_count(3), 3); + assert_eq!(clamped_online_request_peer_count(4), 3); + std::env::remove_var(MAX_ONLINE_REQUEST_PEERS_ENV); + } + + #[test] + fn target_fanout_caps_distinct_targets_but_allows_repeats() { + std::env::set_var(FANOUT_WINDOW_SECONDS_ENV, "60"); + let mut entries: FanoutMap = HashMap::new(); + + assert!(allow_target_fanout( + &mut entries, + "198.51.100.10", + "peer-a", + 2, + "fanout_evicted", + "fanout_rejected" + )); + assert!(allow_target_fanout( + &mut entries, + "198.51.100.10", + "peer-a", + 2, + "fanout_evicted", + "fanout_rejected" + )); + assert!(allow_target_fanout( + &mut entries, + "198.51.100.10", + "peer-b", + 2, + "fanout_evicted", + "fanout_rejected" + )); + assert!(!allow_target_fanout( + &mut entries, + "198.51.100.10", + "peer-c", + 2, + "fanout_evicted", + "fanout_rejected" + )); + + std::env::remove_var(FANOUT_WINDOW_SECONDS_ENV); + } + + #[test] + fn target_fanout_tracked_sources_are_bounded() { + std::env::set_var(FANOUT_WINDOW_SECONDS_ENV, "60"); + std::env::set_var(MAX_FANOUT_TRACKED_SOURCES_ENV, "2"); + let now = Instant::now(); + let older = now.checked_sub(Duration::from_secs(10)).unwrap_or(now); + let newer = now.checked_sub(Duration::from_secs(1)).unwrap_or(now); + let mut entries: FanoutMap = HashMap::from([ + ( + "198.51.100.1".to_owned(), + super::FanoutEntry { + window_started_at: older, + last_seen_at: older, + targets: std::collections::HashSet::from(["peer-a".to_owned()]), + }, + ), + ( + "198.51.100.2".to_owned(), + super::FanoutEntry { + window_started_at: newer, + last_seen_at: newer, + targets: std::collections::HashSet::from(["peer-b".to_owned()]), + }, + ), + ]); + + assert!(allow_target_fanout( + &mut entries, + "198.51.100.3", + "peer-c", + 2, + "fanout_evicted", + "fanout_rejected" + )); + assert_eq!(entries.len(), 2); + assert!(!entries.contains_key("198.51.100.1")); + assert!(entries.contains_key("198.51.100.2")); + assert!(entries.contains_key("198.51.100.3")); + + std::env::remove_var(FANOUT_WINDOW_SECONDS_ENV); + std::env::remove_var(MAX_FANOUT_TRACKED_SOURCES_ENV); + } } #[inline] diff --git a/tests/server_protection_process.rs b/tests/server_protection_process.rs index 25db822a3..2acf0c030 100644 --- a/tests/server_protection_process.rs +++ b/tests/server_protection_process.rs @@ -2,8 +2,8 @@ use hbb_common::{ anyhow::{bail, Context, Result}, protobuf::Message, rendezvous_proto::{ - register_pk_response, rendezvous_message, OnlineRequest, RegisterPk, RegisterPkResponse, - RendezvousMessage, RequestRelay, + register_pk_response, rendezvous_message, OnlineRequest, PunchHoleRequest, RegisterPk, + RegisterPkResponse, RendezvousMessage, RequestRelay, }, tcp::FramedStream, udp::FramedSocket, @@ -281,12 +281,41 @@ async fn send_register_pk( Ok(response) } +async fn send_relay_request_on_stream( + stream: &mut FramedStream, + id: &str, + uuid: &str, + key: &str, +) -> Result> { + let mut message = RendezvousMessage::new(); + message.set_request_relay(RequestRelay { + id: id.to_owned(), + uuid: uuid.to_owned(), + licence_key: key.to_owned(), + ..Default::default() + }); + stream.send(&message).await?; + let response = match stream.next_timeout(1_000).await { + Some(Ok(bytes)) => Some(RendezvousMessage::parse_from_bytes(&bytes)?), + Some(Err(err)) => return Err(err.into()), + None => None, + }; + Ok(response) +} + async fn send_relay_request(addr: SocketAddr, key: &str) -> Result> { let mut stream = FramedStream::new(addr, None, 1_500).await?; + send_relay_request_on_stream(&mut stream, "peer-a", "relay-test-uuid", key).await +} + +async fn send_punch_hole_request_on_stream( + stream: &mut FramedStream, + id: &str, + key: &str, +) -> Result> { let mut message = RendezvousMessage::new(); - message.set_request_relay(RequestRelay { - id: "peer-a".to_owned(), - uuid: "relay-test-uuid".to_owned(), + message.set_punch_hole_request(PunchHoleRequest { + id: id.to_owned(), licence_key: key.to_owned(), ..Default::default() }); @@ -566,3 +595,87 @@ fn hbbs_register_pk_prunes_stale_peer_before_enforcing_cap_process() -> Result<( assert!(runtime.block_on(peer_exists(&db_path, "newpeer1"))?); Ok(()) } + +#[test] +fn hbbs_request_relay_fanout_limit_process() -> Result<()> { + let temp_dir = unique_temp_dir("hbbs-relay-fanout-process-test")?; + let (port, _child) = spawn_hbbs_in_dir( + "server-key", + temp_dir, + &[ + ("FANOUT_WINDOW_SECONDS", "60"), + ("MAX_RELAY_TARGETS_PER_IP_PER_WINDOW", "1"), + ], + )?; + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port); + let runtime = runtime()?; + runtime.block_on(async move { + let mut stream = FramedStream::new(addr, None, 1_500).await?; + let first = + send_relay_request_on_stream(&mut stream, "peer-a", "relay-a", "server-key").await?; + assert!( + first.is_none(), + "first relay fan-out request should not be refused" + ); + + let second = send_relay_request_on_stream(&mut stream, "peer-b", "relay-b", "server-key") + .await? + .context("second relay fan-out request should receive a refusal")?; + match second.union { + Some(rendezvous_message::Union::RelayResponse(response)) => { + assert_eq!(response.refuse_reason, "Too many distinct relay targets"); + } + other => bail!("unexpected response to relay fan-out refusal: {other:?}"), + } + Ok::<(), hbb_common::anyhow::Error>(()) + })?; + Ok(()) +} + +#[test] +fn hbbs_tcp_punch_entries_prune_after_ttl_process() -> Result<()> { + let temp_dir = unique_temp_dir("hbbs-tcp-punch-prune-process-test")?; + let (port, _child) = spawn_hbbs_in_dir( + "server-key", + temp_dir, + &[ + ("TCP_PUNCH_ENTRY_TTL_SECS", "1"), + ("MAX_TCP_PUNCH_ENTRIES", "16"), + ], + )?; + let udp_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port); + let admin_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port - 1); + let tcp_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port); + let runtime = runtime()?; + runtime.block_on(async { + let response = send_register_pk(udp_addr, "target11", "server-key") + .await? + .context("register_pk should receive a response")?; + match response.union { + Some(rendezvous_message::Union::RegisterPkResponse(RegisterPkResponse { + result, + .. + })) => { + assert_eq!(result, register_pk_response::Result::OK.into()); + } + other => bail!("unexpected register_pk response: {other:?}"), + } + + let mut stream = FramedStream::new(tcp_addr, None, 1_500).await?; + let response = + send_punch_hole_request_on_stream(&mut stream, "target11", "server-key").await?; + assert!( + response.is_none(), + "live punch-hole request should not receive an immediate response" + ); + + std::thread::sleep(Duration::from_millis(1_250)); + let stats = admin_command(admin_addr, "ps")?; + assert!( + stats.contains("tcp_punch_entries=0"), + "unexpected protection stats: {stats}" + ); + Ok::<(), hbb_common::anyhow::Error>(()) + })?; + Ok(()) +} From 8de597c69ac0f439b5c95eaea1e89d7cd3406f69 Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 20 Apr 2026 15:17:28 +0900 Subject: [PATCH 5/5] Prevent UUID reuse and fix IP prune logic Adjust prune_ip_change_entries to retain entries that are either recent or have multiple IPs (use || instead of &&) to avoid prematurely dropping recent single-IP records, and add a unit test covering that behavior. In relay_server.rs, add checks to detect and reject relay requests when the relay UUID is already active (both for immediate and pending flows), log warnings, record protection events, and send refuse responses. Add corresponding tests and minor test imports to validate UUID reuse detection. --- src/peer.rs | 28 +++++++++++++++++++++++++++- src/relay_server.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/peer.rs b/src/peer.rs index bab3c4f10..730deda76 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -166,7 +166,7 @@ pub(crate) fn allow_ip_registration_attempt(entries: &mut IpBlockMap, ip: &str, } pub(crate) fn prune_ip_change_entries(entries: &mut IpChangesMap) { - entries.retain(|_, value| value.0.elapsed().as_secs() < IP_CHANGE_DUR_X2 && value.1.len() > 1); + entries.retain(|_, value| value.0.elapsed().as_secs() < IP_CHANGE_DUR_X2 || value.1.len() > 1); } fn evict_oldest_ip_change_entry(entries: &mut IpChangesMap) -> bool { @@ -551,4 +551,30 @@ mod tests { std::env::remove_var("MAX_IP_CHANGES_ENTRIES"); std::env::remove_var("MAX_UNIQUE_IP_CHANGES_PER_ID"); } + + #[test] + fn prune_ip_change_entries_keeps_recent_single_ip_entries() { + let now = Instant::now(); + let recent = now + .checked_sub(Duration::from_secs(IP_CHANGE_DUR_X2 / 2)) + .unwrap_or(now); + let stale = now + .checked_sub(Duration::from_secs(IP_CHANGE_DUR_X2 + 5)) + .unwrap_or(now); + let mut entries: IpChangesMap = HashMap::from([ + ( + "recent-single".to_owned(), + (recent, HashMap::from([("198.51.100.1".to_owned(), 1)])), + ), + ( + "stale-single".to_owned(), + (stale, HashMap::from([("198.51.100.2".to_owned(), 1)])), + ), + ]); + + prune_ip_change_entries(&mut entries); + + assert!(entries.contains_key("recent-single")); + assert!(!entries.contains_key("stale-single")); + } } diff --git a/src/relay_server.rs b/src/relay_server.rs index 18ed28095..56ad61503 100644 --- a/src/relay_server.rs +++ b/src/relay_server.rs @@ -581,6 +581,18 @@ async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limit let peer_a_ip = pending.ip.clone(); let peer_b_ip = addr.ip().to_string(); let mut active_relays = ACTIVE_RELAYS.lock().await; + if active_relays.contains_key(&rf.uuid) { + drop(active_relays); + crate::common::record_protection_event("relay_uuid_in_use"); + log::warn!( + "Rejecting relay request {} from {}: uuid already active", + rf.uuid, + addr + ); + send_relay_refuse(&mut stream, "uuid_in_use").await; + send_relay_refuse(&mut *pending.stream, "uuid_in_use").await; + return; + } let active_for_peer_a = count_active_relays_for_ip(&active_relays, &peer_a_ip); let active_for_peer_b = @@ -632,6 +644,19 @@ async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limit } else { log::info!("New relay request {} from {}", rf.uuid, addr); let ip = addr.ip().to_string(); + let active_relays = ACTIVE_RELAYS.lock().await; + if active_relays.contains_key(&rf.uuid) { + drop(active_relays); + crate::common::record_protection_event("relay_uuid_in_use"); + log::warn!( + "Rejecting pending relay request {} from {}: uuid already active", + rf.uuid, + addr + ); + send_relay_refuse(&mut stream, "uuid_in_use").await; + return; + } + drop(active_relays); let mut peers = PEERS.lock().await; prune_expired_pending_relays(&mut peers); let pending_for_ip = peers.values().filter(|peer| peer.ip == ip).count(); @@ -908,10 +933,11 @@ impl StreamTrait for tokio_tungstenite::WebSocketStream { #[cfg(test)] mod tests { use super::{ - active_relay_limit_reason, pending_relay_limit_reason, MAX_ACTIVE_RELAYS, + active_relay_limit_reason, pending_relay_limit_reason, ACTIVE_RELAYS, MAX_ACTIVE_RELAYS, MAX_ACTIVE_RELAYS_PER_IP, MAX_PENDING_RELAYS, MAX_PENDING_RELAYS_PER_IP, }; use std::sync::{atomic::Ordering, Mutex}; + use std::time::Instant; static TEST_RELAY_LIMITS_LOCK: Mutex<()> = Mutex::new(()); @@ -961,4 +987,21 @@ mod tests { MAX_ACTIVE_RELAYS.store(saved_total, Ordering::SeqCst); MAX_ACTIVE_RELAYS_PER_IP.store(saved_per_ip, Ordering::SeqCst); } + + #[tokio::test(flavor = "current_thread")] + async fn active_relay_uuid_reuse_is_detected() { + let _guard = TEST_RELAY_LIMITS_LOCK.lock().unwrap(); + let mut active_relays = ACTIVE_RELAYS.lock().await; + active_relays.clear(); + active_relays.insert( + "uuid-1".to_owned(), + super::ActiveRelay { + peer_a_ip: "198.51.100.1".to_owned(), + peer_b_ip: "198.51.100.2".to_owned(), + started_at: Instant::now(), + }, + ); + assert!(active_relays.contains_key("uuid-1")); + active_relays.clear(); + } }