From 1632d82b6a04195ad7dc5864491650e50ba478e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 9 Oct 2026 10:33:17 +0200 Subject: [PATCH 01/14] Re-organise --- Cargo.lock | 220 ++--- .../user_agent_header_regexes.yaml | 70 +- .../defguard_gateway_manager/src/handler.rs | 898 +----------------- .../src/handler/tests.rs | 415 ++++++++ crates/defguard_gateway_manager/src/lib.rs | 1 + .../defguard_gateway_manager/src/updates.rs | 455 +++++++++ 6 files changed, 1040 insertions(+), 1019 deletions(-) create mode 100644 crates/defguard_gateway_manager/src/handler/tests.rs create mode 100644 crates/defguard_gateway_manager/src/updates.rs diff --git a/Cargo.lock b/Cargo.lock index 73d0653da4..50b834b34e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,9 +176,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" dependencies = [ "derive_arbitrary", ] @@ -271,7 +271,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.20", + "thiserror 2.0.21", "time", ] @@ -389,7 +389,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.20", + "thiserror 2.0.21", "v_frame", "y4m", ] @@ -637,7 +637,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -1619,7 +1619,7 @@ dependencies = [ "chrono", "rcgen", "rustls-pki-types", - "thiserror 2.0.20", + "thiserror 2.0.21", "time", "x509-parser 0.18.1", ] @@ -1654,7 +1654,7 @@ dependencies = [ "sha2 0.11.0", "sqlx", "struct-patch", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "totp-lite", "tracing", @@ -1734,7 +1734,7 @@ dependencies = [ "strum", "strum_macros", "tera", - "thiserror 2.0.20", + "thiserror 2.0.21", "time", "tokio", "tokio-stream", @@ -1773,7 +1773,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tracing", ] @@ -1796,7 +1796,7 @@ dependencies = [ "semver", "serde_json", "sqlx", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tokio-stream", "tonic", @@ -1831,7 +1831,7 @@ dependencies = [ "http", "hyper-rustls", "rustls", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tonic", "tower-service", @@ -1908,7 +1908,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "sqlx", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tokio-stream", "tonic", @@ -1929,7 +1929,7 @@ dependencies = [ "ipnetwork", "sqlx", "strum", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tracing", ] @@ -1968,7 +1968,7 @@ dependencies = [ "defguard_common", "serde", "sqlx", - "thiserror 2.0.20", + "thiserror 2.0.21", "tracing", ] @@ -1981,7 +1981,7 @@ dependencies = [ "os_info", "semver", "serde", - "thiserror 2.0.20", + "thiserror 2.0.21", "tonic", "tower", "tracing", @@ -2037,7 +2037,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -2090,13 +2090,13 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -2373,9 +2373,9 @@ dependencies = [ [[package]] name = "either" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" dependencies = [ "serde", ] @@ -2423,13 +2423,12 @@ checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" [[package]] name = "encoding_rs" -version = "0.8.41" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" dependencies = [ "cfg-if", "core_detect", - "multiversion", "multiversion_no_op", "rustversion", "scopeguard", @@ -2943,9 +2942,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" dependencies = [ "atomic-waker", "bytes", @@ -3009,8 +3008,6 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.2.0", ] @@ -3025,9 +3022,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +checksum = "fc2700e3fe555c3310aa7286cac6167449f2c87e0eb58769c9208a1c58a1d106" dependencies = [ "base64 0.22.1", "bytes", @@ -3208,9 +3205,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", @@ -3277,16 +3274,17 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", "futures-channel", "futures-util", "http", "http-body", + "httparse", "hyper", "ipnet", "libc", @@ -3599,9 +3597,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.37" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" dependencies = [ "defmt", "jiff-core", @@ -3626,9 +3624,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.37" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" dependencies = [ "jiff-core", "proc-macro2", @@ -3638,9 +3636,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" [[package]] name = "jiff-tzdb-platform" @@ -3663,9 +3661,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", @@ -3683,7 +3681,7 @@ dependencies = [ "num-bigint", "serde", "serde_json", - "thiserror 2.0.20", + "thiserror 2.0.21", "yasna 0.5.2", "zeroize", ] @@ -3767,7 +3765,7 @@ dependencies = [ "native-tls", "nom 7.1.3", "percent-encoding", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tokio-native-tls", "tokio-stream", @@ -3908,9 +3906,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.4" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" dependencies = [ "hashbrown 0.17.1", ] @@ -4084,7 +4082,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -4093,27 +4091,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "multiversion" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" -dependencies = [ - "multiversion-macros", -] - -[[package]] -name = "multiversion-macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" -dependencies = [ - "proc-macro2", - "quote", - "rustversion", - "syn 3.0.6", -] - [[package]] name = "multiversion_no_op" version = "1.0.0" @@ -5352,9 +5329,9 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "pulldown-cmark-to-cmark" -version = "22.0.1" +version = "22.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab1ad36992cead65f02aa399a373a42730922f1525d988172634fdefdecb8a60" +checksum = "84bbb29c624230c4bd1047bbdb2aa47e41c860e9665ce62ba9504eebe91bf867" dependencies = [ "pulldown-cmark", ] @@ -5441,7 +5418,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tracing", "web-time", @@ -5463,7 +5440,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.20", + "thiserror 2.0.21", "tinyvec", "tracing", "web-time", @@ -5631,7 +5608,7 @@ dependencies = [ "rand 0.9.5", "rand_chacha 0.9.0", "simd_helpers", - "thiserror 2.0.20", + "thiserror 2.0.21", "v_frame", "wasm-bindgen", ] @@ -5877,9 +5854,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.12.0" +version = "8.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +checksum = "19afa5b4b6a611de00bd1bdae6ae6f39084c9399f0679c3f52d8469cf335cc23" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -5888,9 +5865,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.12.0" +version = "8.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +checksum = "e0d8afda6374eac59e066abee06d265247ebbaf3006cf878e2879e8356e34053" dependencies = [ "mime_guess", "proc-macro2", @@ -5902,9 +5879,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.12.0" +version = "8.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +checksum = "0d84e8ba78bd384263e5922f084cbe1b081c3b7e69add59c8fb097b879ba968a" dependencies = [ "globset", "sha2 0.11.0", @@ -6275,9 +6252,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" dependencies = [ "base64 0.23.1", "bs58", @@ -6296,9 +6273,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" dependencies = [ "darling 0.24.1", "proc-macro2", @@ -6520,15 +6497,15 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.20", + "thiserror 2.0.21", "time", ] [[package]] name = "siphasher" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" [[package]] name = "slab" @@ -6548,9 +6525,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.16.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" dependencies = [ "serde", ] @@ -6657,7 +6634,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tokio-stream", "tracing", @@ -6699,7 +6676,7 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn 2.0.119", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "url", ] @@ -6727,7 +6704,7 @@ dependencies = [ "sha1 0.11.0", "sha2 0.11.0", "sqlx-core", - "thiserror 2.0.20", + "thiserror 2.0.21", "tracing", "uuid", ] @@ -6764,7 +6741,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.20", + "thiserror 2.0.21", "tracing", "uuid", "whoami", @@ -6790,7 +6767,7 @@ dependencies = [ "percent-encoding", "serde", "sqlx-core", - "thiserror 2.0.20", + "thiserror 2.0.21", "tracing", "url", "uuid", @@ -7066,11 +7043,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl 2.0.20", + "thiserror-impl 2.0.21", ] [[package]] @@ -7086,9 +7063,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", @@ -7215,9 +7192,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ "rustls", "tokio", @@ -7431,7 +7408,7 @@ dependencies = [ "governor", "http", "pin-project", - "thiserror 2.0.20", + "thiserror 2.0.21", "tonic", "tower", "tracing", @@ -7820,9 +7797,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -7833,19 +7810,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.78" +version = "0.4.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" dependencies = [ "js-sys", + "tokio", "wasm-bindgen", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7853,9 +7831,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", @@ -7866,9 +7844,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] @@ -7888,9 +7866,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" dependencies = [ "js-sys", "wasm-bindgen", @@ -8322,7 +8300,7 @@ dependencies = [ "oid-registry 0.8.1", "ring", "rusticata-macros", - "thiserror 2.0.20", + "thiserror 2.0.21", "time", ] @@ -8376,18 +8354,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.60" +version = "0.8.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6400fc4bb7426f6faac3fb07566122bb18622e3022bd2c36426a92ed489b1dbe" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.60" +version = "0.8.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99c27cc7525bad4df59a45e073c45b2c2aa820efb588234cc23a547ec69a34db" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" dependencies = [ "proc-macro2", "quote", @@ -8417,9 +8395,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" dependencies = [ "zeroize_derive", ] diff --git a/crates/defguard_core/user_agent_header_regexes.yaml b/crates/defguard_core/user_agent_header_regexes.yaml index 8cfda16704..5e269a97af 100644 --- a/crates/defguard_core/user_agent_header_regexes.yaml +++ b/crates/defguard_core/user_agent_header_regexes.yaml @@ -57,6 +57,8 @@ user_agent_parsers: - regex: X11; (Datanyze); Linux - regex: (NewRelicPinger)/(\d+)\.(\d+) family_replacement: NewRelicPingerBot + - regex: (RuxitSynthetic)/(\d+)\.(\d+) + family_replacement: Ruxit Synthetic - regex: (Tableau)/(\d+)\.(\d+) family_replacement: Tableau - regex: AppleWebKit/\d{1,10}\.\d{1,10}.{0,200} Safari.{0,200} (CreativeCloud)/(\d+)\.(\d+).(\d+) @@ -128,6 +130,7 @@ user_agent_parsers: - regex: \[(Pinterest)/[^\]]{1,50}\] - regex: '(Pinterest)(?: for Android(?: Tablet|)|)/(\d+)(?:\.(\d+)|)(?:\.(\d+)|)' - regex: Mozilla.{1,200}Mobile.{1,100}(Instagram).(\d+)\.(\d+)\.(\d+) + - regex: (Instagram) (\d+)\.(\d+)\.(\d+) - regex: Mozilla.{1,200}Mobile.{1,100}(Flipboard).(\d+)\.(\d+)\.(\d+) - regex: Mozilla.{1,200}Mobile.{1,100}(Flipboard-Briefing).(\d+)\.(\d+)\.(\d+) - regex: Mozilla.{1,200}Mobile.{1,100}(Onefootball)\/Android.(\d+)\.(\d+)\.(\d+) @@ -142,6 +145,8 @@ user_agent_parsers: family_replacement: TikTok - regex: Mozilla.{1,200}Mobile.{1,100}(KAKAOTALK)/(\d+)\.(\d+)\.(\d+) family_replacement: KakaoTalk + - regex: (Telegram-Android)/(\d+)\.(\d+)\.(\d+) + family_replacement: Telegram - regex: Mozilla.{1,200}Mobile.{1,100}(Phantom\/ios|Phantom\/android).(\d+)\.(\d+)\.(\d+) family_replacement: Phantom - regex: Mozilla.{1,100}Mobile.{1,100}(AspiegelBot|PetalBot) @@ -152,6 +157,7 @@ user_agent_parsers: family_replacement: Basilisk - regex: (PaleMoon)/(\d+)\.(\d+)(?:\.(\d+)|) family_replacement: Pale Moon + - regex: (Camoufox) Camoufox (\d+)\.(\d+) - regex: (Fennec)/(\d+)\.(\d+)\.?([ab]?\d+[a-z]*) family_replacement: Firefox Mobile - regex: (Fennec)/(\d+)\.(\d+)(pre) @@ -187,7 +193,7 @@ user_agent_parsers: family_replacement: Netscape - regex: (MyIBrow)/(\d+)\.(\d+) family_replacement: My Internet Browser - - regex: (UC? ?Browser|UCWEB|U3)[ /]?(\d+)\.(\d+)\.(\d+) + - regex: (UC? ?Browser|UCWEB|UCMobile|U3)[ /]?(\d+)\.(\d+)\.(\d+) family_replacement: UC Browser - regex: (Opera Tablet).{0,200}Version/(\d+)\.(\d+)(?:\.(\d+)|) - regex: (Opera Mini)(?:/att|)/?(\d+|)(?:\.(\d+)|)(?:\.(\d+)|) @@ -204,6 +210,10 @@ user_agent_parsers: family_replacement: Opera Mobile - regex: (?:Chrome).{1,300}(OPR)/(\d+)\.(\d+)\.(\d+) family_replacement: Opera + - regex: (OPX)/(\d+)\.(\d+)(?:\.(\d+)|) + family_replacement: Opera GX + - regex: (OPT)/(\d+)\.(\d+)(?:\.(\d+)|) + family_replacement: Opera Touch - regex: (Coast)/(\d+).(\d+).(\d+) family_replacement: Opera Coast - regex: (OPiOS)/(\d+).(\d+).(\d+) @@ -255,6 +265,8 @@ user_agent_parsers: family_replacement: Aloha Browser - regex: ((?:B|b)rave(?:\sChrome)?)/(\d+)(?:\.(\d+)|)(?:\.(\d+)|)(?:\.(\d+)|) family_replacement: Brave + - regex: (?:\()?Brave(?:\))?\s*$ + family_replacement: Brave - regex: (Silk)/(\d+)\.(\d+)(?:\.([0-9\-]+)|) family_replacement: Amazon Silk - regex: (Puffin)/(\d+)\.(\d+)(?:\.(\d+)|) @@ -310,8 +322,12 @@ user_agent_parsers: - regex: (VivoBrowser)\/(\d+)\.(\d+)\.(\d+)(?:\.(\d+)|) - regex: (H[Ii]Browser)\/v(\d+)\.(\d+)\.(\d+)\.(\d+) family_replacement: HiBrowser + - regex: (HonorBrowser)/(\d+)\.(\d+)\.(\d+)(?:\.(\d+)|) + family_replacement: Honor Browser - regex: (bdhonorbrowser)/(\d+)\.(\d+)\.(\d+)\.(\d+) family_replacement: Honor Browser + - regex: (HeyTapBrowser)/(\d+)\.(\d+)\.(\d+)\.(\d+) + family_replacement: HeyTap Browser - regex: (weibo)__(\d+)\.(\d+)\.(\d+) family_replacement: Weibo - regex: (WeiboliteiOS|WeiboIntliOS) @@ -370,7 +386,7 @@ user_agent_parsers: family_replacement: Quark - regex: (QuarkPC)/(\d+)\.(\d+)\.(\d+) family_replacement: Quark PC - - regex: (SLBrowser)/(\d+)\.(\d+)\.(\d+)\.(\d+) SLBChan/(\d+) + - regex: (SLBrowser)/(\d+)\.(\d+)\.(\d+) family_replacement: Smart Lenovo Browser - regex: (Atom)/(\d+)\.(\d+)\.(\d+)\.(\d+) family_replacement: Atom Browser @@ -400,12 +416,25 @@ user_agent_parsers: family_replacement: JiSu Browser - regex: (Wolvic)/(\d+)\.(\d+)\.(\d+) family_replacement: Wolvic Browser - - regex: (HeyTapBrowser)/(\d+)\.(\d+)\.(\d+)\.(\d+) - family_replacement: HeyTap Browser - regex: (Thano)/(\d+)\.(\d+) family_replacement: SmartTV WebBrowser - - regex: (MicroMessenger)/(\d+)\.(\d+)\.(\d+)\.(\d+) + - regex: (MicroMessenger)/(\d+)\.(\d+)(?:\.(\d+)|) family_replacement: WeChat Browser + - regex: (Odin)/(\d+)\.(\d+)\.(\d+)\.(\d+) + family_replacement: Odin + - regex: (Colt)/(\d+)\.(\d+) + family_replacement: NetCast Smart TV + - regex: (Lite Browser)/(\d+)\.(\d+) + family_replacement: Lite Browser + - regex: (OMI)/(\d+)\.(\d+)\.(\d+) + family_replacement: Vewd Browser + - regex: (Mypal)/(\d+)\.(\d+)\.(\d+) + family_replacement: Mypal Browser + - regex: (Chesscom-Android)/(\d+)\.(\d+)\.(\d+) + - regex: (RobloxApp)/(\d+)\.(\d+)\.(\d+) + family_replacement: Roblox App + - regex: (Roadrunner)/IOS/\d+/(\d+)\.(\d+)\.(\d+) + - regex: (AncestryAndroid)/(\d+)\.(\d+)(?:\.(\d+)|) - regex: (Slack_SSB)/(\d+)\.(\d+)\.(\d+) family_replacement: Slack Desktop Client - regex: (HipChat)/?(\d+|) @@ -440,6 +469,8 @@ user_agent_parsers: - regex: Superhuman family_replacement: Superhuman - regex: (Vivaldi)/(\d+)(?:\.(\d+)|)(?:\.(\d+)|) + - regex: .{0,200}(Teams)/(\d+)\.(\d+)\.(\d+) + family_replacement: Microsoft Teams - regex: (Edge?)/(\d+)(?:\.(\d+)|)(?:\.(\d+)|)(?:\.(\d+)|) family_replacement: Edge - regex: (Chrome)/(\d+)\.(\d+)\.(\d+)[\d.]{0,100} Iron[^/] @@ -452,6 +483,31 @@ user_agent_parsers: Mini|Opera|NetFront|Netfront|Konqueror|Googlebot|SeaMonkey|Kazehakase|Vienna|Iceape|Iceweasel|IceWeasel|Iron|K-Meleon|Sleipnir|Galeon|GranParadiso|iCab|iTunes|MacAppStore|NetNewsWire|Space Bison|Stainless|Orca|Dolfin|BOLT|Minimo|Tizen Browser|Polaris|Abrowser|Planetweb|ICE Browser|mDolphin|qutebrowser|Otter|QupZilla|MailBar|kmail2|YahooMobileMail|ExchangeWebServices|ExchangeServicesClient|Dragon|Outlook-iOS-Android)/(\d+)\.(\d+)(?:\.(\d+)|) + - regex: (QtWebEngine)/(\d+)\.(\d+)\.(\d+) + family_replacement: Qt Web Engine + - regex: (OpenWave)/(\d+)\.(\d+)\.(\d+) + family_replacement: Open Wave + - regex: (AtContent)/(\d+)\.(\d+)\.(\d+) + - regex: (Trailer)/(\d+)\.(\d+)\.(\d+) + - regex: (Agency)/(\d+)\.(\d+)\.(\d+) + - regex: (Herring)/(\d+)\.(\d+)\.(\d+) + - regex: (Config)/(\d+)\.(\d+)\.(\d+) + - regex: (Viewer)/(\d+)\.(\d+)\.(\d+) + - regex: (LikeWise)/(\d+)\.(\d+)\.(\d+) + - regex: (Unique)/(\d+)\.(\d+)\.(\d+) + - regex: (CitizenFX)/(\d+)\.(\d+)\.(\d+) + - regex: (R2Client)/(\d+)\.(\d+)(?:\.(\d+)|) + - regex: (OBS)/(\d+)\.(\d+)\.(\d+) + family_replacement: OBS Studio + - regex: (AdobeCEP)/(\d+)\.(\d+)\.(\d+) + family_replacement: Adobe CEP + - regex: Valve Steam (GameOverlay).{1,200}Chrome/(\d+)\.(\d+)\.(\d+) + family_replacement: Steam GameOverlay + - regex: Valve Steam (Gamepad)/Steam Deck.{1,200}Chrome/(\d+)\.(\d+)\.(\d+) + family_replacement: Steam Deck + - regex: '(Valve(?: Steam|) Client).{1,200}Chrome/(\d+)\.(\d+)\.(\d+)' + family_replacement: Steam Client + - regex: (Ladybird)/(\d+)\.(\d+)(?:\.(\d+)|) - regex: (Chromium|Chrome)/(\d+)\.(\d+)(?:\.(\d+)|)(?:\.(\d+)|) - regex: (IEMobile)[ /](\d+)\.(\d+) family_replacement: IE Mobile @@ -2613,11 +2669,11 @@ device_parsers: device_replacement: Odys $1 brand_replacement: Odys model_replacement: $1 - - regex: '; (ONE [a-zA-Z]\d+)(?: Build|\) AppleWebKit)' + - regex: '; (ONE [a-zA-Z]\d+)(?: Build|;? {0,2}\) AppleWebKit)' device_replacement: OnePlus $1 brand_replacement: OnePlus model_replacement: $1 - - regex: '; (ONEPLUS [a-zA-Z]\d+)(?: Build|\) AppleWebKit)' + - regex: '; (ONEPLUS [a-zA-Z]\d+)(?: Build|;? {0,2}\) AppleWebKit)' device_replacement: OnePlus $1 brand_replacement: OnePlus model_replacement: $1 diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index a4ffef1b4f..7894411637 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -2,7 +2,6 @@ use std::path::PathBuf; use std::{ collections::HashMap, - net::IpAddr, str::FromStr, sync::{ Arc, Mutex, @@ -16,13 +15,9 @@ use defguard_common::{ VERSION, db::{ Id, - models::{ - Certificates, DeviceNetworkInfo, Settings, WireguardNetwork, gateway::Gateway, - wireguard::DEFAULT_WIREGUARD_MTU, - }, + models::{Certificates, Settings, WireguardNetwork, gateway::Gateway}, }, gateway_event::GatewayCommand, - gateway_types::{FirewallConfig, WireguardPeer}, messages::peer_stats_update::PeerStatsUpdate, }; use defguard_core::{ @@ -32,12 +27,8 @@ use defguard_core::{ location_management::allowed_peers::get_location_allowed_peers, }; use defguard_grpc_tls::{certs as tls_certs, connector::HttpsSchemeConnector}; -use defguard_proto::{ - enterprise::firewall::FirewallConfig as ProtoFirewallConfig, - gateway::{ - Configuration, CoreResponse, Peer, PeerStats, Update, UpdateType, core_request, - core_response, gateway_client, update, - }, +use defguard_proto::gateway::{ + Configuration, CoreResponse, PeerStats, core_request, core_response, gateway_client, }; use defguard_version::client::ClientVersionInterceptor; use hyper_rustls::HttpsConnectorBuilder; @@ -46,7 +37,7 @@ use semver::Version; use sqlx::PgPool; use tokio::{ sync::{ - broadcast::{self, Sender}, + broadcast::Sender, mpsc::{self, UnboundedSender}, watch, }, @@ -54,14 +45,11 @@ use tokio::{ time::sleep, }; use tokio_stream::wrappers::UnboundedReceiverStream; -use tonic::{ - Code, Status, - transport::{Channel, Endpoint}, -}; +use tonic::transport::{Channel, Endpoint}; #[cfg(test)] use crate::GatewayManagerTestSupport; -use crate::{Client, TEN_SECS, error::GatewayError}; +use crate::{Client, TEN_SECS, error::GatewayError, updates::GatewayUpdatesHandler}; #[cfg(test)] #[derive(Default)] @@ -781,459 +769,6 @@ impl GatewayHandler { } } -/// Helper struct for handling gateway events. -struct GatewayUpdatesHandler { - network_id: Id, - network: WireguardNetwork, - gateway_name: String, - pool: Option, - session_authorization_required: bool, - events_rx: broadcast::Receiver, - tx: UnboundedSender, -} - -impl GatewayUpdatesHandler { - #[must_use] - fn new( - network_id: Id, - network: WireguardNetwork, - gateway_name: String, - pool: Option, - events_rx: broadcast::Receiver, - tx: UnboundedSender, - ) -> Self { - Self { - network_id, - network, - gateway_name, - pool, - session_authorization_required: false, - events_rx, - tx, - } - } - - async fn authorization_required_for( - network: &WireguardNetwork, - pool: Option<&PgPool>, - ) -> bool { - if network.mfa_enabled { - return true; - } - - let Some(pool) = pool else { - return false; - }; - - match network.has_postures(pool).await { - Ok(has_postures) => has_postures, - Err(err) => { - error!("Failed to fetch postures for location {network}: {err}"); - false - } - } - } - - async fn refresh_session_authorization_required(&mut self) { - self.session_authorization_required = - Self::authorization_required_for(&self.network, self.pool.as_ref()).await; - } - - #[must_use] - fn runtime_peer_update( - &self, - peer_label: &str, - peer_pubkey: String, - allowed_ips: Vec, - is_authorized: bool, - preshared_key: Option, - ) -> Option { - if !self.session_authorization_required { - return Some(Peer { - pubkey: peer_pubkey, - allowed_ips, - preshared_key: None, - keepalive_interval: Some(self.network.keepalive_interval.cast_unsigned()), - }); - } - - if !is_authorized { - debug!( - "Skipping gateway peer update for WireGuard device {peer_label} in runtime-authorized location {} because there is no active VPN session", - self.network.name - ); - return None; - } - - let Some(preshared_key) = preshared_key else { - debug!( - "Skipping gateway peer update for WireGuard device {peer_label} in location {} because the runtime preshared key is missing", - self.network.name - ); - return None; - }; - - Some(Peer { - pubkey: peer_pubkey, - allowed_ips, - preshared_key: Some(preshared_key), - keepalive_interval: Some(self.network.keepalive_interval.cast_unsigned()), - }) - } - - fn send_runtime_device_update( - &self, - peer_label: &str, - peer_pubkey: String, - network_info: &DeviceNetworkInfo, - update_type: i32, - ) -> Result<(), Status> { - let allowed_ips = network_info - .device_wireguard_ips - .iter() - .map(IpAddr::to_string) - .collect(); - - let Some(peer) = self.runtime_peer_update( - peer_label, - peer_pubkey, - allowed_ips, - network_info.is_authorized, - network_info.preshared_key.clone(), - ) else { - return Ok(()); - }; - - self.send_peer_update(peer, update_type) - } - - /// Process incoming Gateway events - /// - /// Main gRPC server uses a shared channel for broadcasting all gateway events - /// so the handler must determine if an event is relevant for the network being serviced - async fn run(&mut self) { - info!( - "Starting update stream to gateway: {}, network {}", - self.gateway_name, self.network - ); - self.refresh_session_authorization_required().await; - while let Ok(update) = self.events_rx.recv().await { - debug!("Received WireGuard update: {update:?}"); - let result = match update { - GatewayCommand::NetworkCreated(network_id, network) => { - if network_id == self.network_id { - self.send_network_update( - &network, - Vec::new(), - None, - UpdateType::Create as i32, - ) - } else { - Ok(()) - } - } - GatewayCommand::NetworkModified( - network_id, - network, - peers, - maybe_firewall_config, - ) => { - if network_id == self.network_id { - let result = self.send_network_update( - &network, - peers, - maybe_firewall_config, - UpdateType::Modify as i32, - ); - // update stored network data - self.network = network; - self.refresh_session_authorization_required().await; - result - } else { - Ok(()) - } - } - GatewayCommand::NetworkDeleted(network_id, network_name) => { - if network_id == self.network_id { - self.send_network_delete(&network_name) - } else { - Ok(()) - } - } - GatewayCommand::DeviceCreated(device) => { - // check if a peer has to be added in the current network - match device - .network_info - .iter() - .find(|info| info.network_id == self.network_id) - { - Some(network_info) => self.send_runtime_device_update( - &device.device.name, - device.device.wireguard_pubkey, - network_info, - UpdateType::Create as i32, - ), - None => Ok(()), - } - } - GatewayCommand::DeviceModified(device) => { - // check if a peer has to be updated in the current network - match device - .network_info - .iter() - .find(|info| info.network_id == self.network_id) - { - Some(network_info) => self.send_runtime_device_update( - &device.device.name, - device.device.wireguard_pubkey, - network_info, - UpdateType::Modify as i32, - ), - None => Ok(()), - } - } - GatewayCommand::DeviceDeleted(device) => { - // check if a peer has to be updated in the current network - match device - .network_info - .iter() - .find(|info| info.network_id == self.network_id) - { - Some(_) => self.send_peer_delete(&device.device.wireguard_pubkey), - None => Ok(()), - } - } - GatewayCommand::FirewallConfigChanged(location_id, firewall_config) => { - if location_id == self.network_id { - self.send_firewall_update(firewall_config) - } else { - Ok(()) - } - } - GatewayCommand::FirewallDisabled(location_id) => { - if location_id == self.network_id { - self.send_firewall_disable() - } else { - Ok(()) - } - } - GatewayCommand::VpnSessionDeauthorized(location_id, device) => { - if location_id == self.network_id { - self.send_peer_delete(&device.wireguard_pubkey) - } else { - Ok(()) - } - } - GatewayCommand::VpnSessionAuthorized(location_id, device, network_info) => { - if location_id == self.network_id { - if network_info.network_id != location_id { - error!( - "Received VPN authorization success event for location {location_id} with invalid runtime network info: {network_info:?}" - ); - continue; - } - - self.send_runtime_device_update( - &device.name, - device.wireguard_pubkey, - &network_info, - UpdateType::Create as i32, - ) - } else { - Ok(()) - } - } - }; - if result.is_err() { - error!( - "Closing update steam to gateway: {}, network {}", - self.gateway_name, self.network - ); - break; - } - } - } - - /// Sends updated network configuration - fn send_network_update( - &self, - network: &WireguardNetwork, - peers: Vec, - firewall_config: Option, - update_type: i32, - ) -> Result<(), Status> { - debug!("Sending network update for network {network}"); - let proto_peers: Vec = peers.into_iter().map(Into::into).collect(); - let proto_firewall: Option = firewall_config.map(Into::into); - if let Err(err) = self.tx.send(CoreResponse { - id: 0, - payload: Some(core_response::Payload::Update(Update { - update_type, - update: Some(update::Update::Network(Configuration { - name: network.name.clone(), - private_key: network.prvkey.clone(), - addresses: network.address().iter().map(ToString::to_string).collect(), - port: network.port.cast_unsigned(), - peers: proto_peers, - firewall_config: proto_firewall, - mtu: network.mtu.cast_unsigned(), - fwmark: network.fwmark as u32, - })), - })), - }) { - let msg = format!( - "Failed to send network update, network {network}, update type: {update_type} \ - ({}), error: {err}", - if update_type == UpdateType::Create as i32 { - "CREATE" - } else { - "MODIFY" - }, - ); - error!(msg); - return Err(Status::new(Code::Internal, msg)); - } - debug!("Network update sent for network {network}"); - Ok(()) - } - - /// Sends delete network command to gateway - fn send_network_delete(&self, network_name: &str) -> Result<(), Status> { - debug!( - "Sending network delete command for network {}", - self.network - ); - if let Err(err) = self.tx.send(CoreResponse { - id: 0, - payload: Some(core_response::Payload::Update(Update { - update_type: UpdateType::Delete as i32, - update: Some(update::Update::Network(Configuration { - name: network_name.to_owned(), - private_key: String::new(), - addresses: Vec::new(), - port: 0, - peers: Vec::new(), - firewall_config: None, - mtu: DEFAULT_WIREGUARD_MTU.cast_unsigned(), - fwmark: 0, - })), - })), - }) { - let msg = format!( - "Failed to send network update, network {}, update type: 3 (DELETE), error: {err}", - self.network, - ); - error!(msg); - return Err(Status::new(Code::Internal, msg)); - } - debug!("Network delete command sent for network {}", self.network); - Ok(()) - } - - /// Send update peer command to gateway - fn send_peer_update(&self, peer: Peer, update_type: i32) -> Result<(), Status> { - debug!("Sending peer update for network {}", self.network); - if let Err(err) = self.tx.send(CoreResponse { - id: 0, - payload: Some(core_response::Payload::Update(Update { - update_type, - update: Some(update::Update::Peer(peer)), - })), - }) { - let msg = format!( - "Failed to send peer update for network {}, update type: {update_type} ({}), \ - error: {err}", - self.network, - if update_type == UpdateType::Create as i32 { - "CREATE" - } else { - "MODIFY" - }, - ); - error!(msg); - return Err(Status::new(Code::Internal, msg)); - } - debug!("Peer update sent for network {}", self.network); - Ok(()) - } - - /// Send delete peer command to gateway - fn send_peer_delete(&self, peer_pubkey: &str) -> Result<(), Status> { - debug!("Sending peer delete for network {}", self.network); - if let Err(err) = self.tx.send(CoreResponse { - id: 0, - payload: Some(core_response::Payload::Update(Update { - update_type: UpdateType::Delete as i32, - update: Some(update::Update::Peer(Peer { - pubkey: peer_pubkey.into(), - allowed_ips: Vec::new(), - preshared_key: None, - keepalive_interval: None, - })), - })), - }) { - let msg = format!( - "Failed to send peer update for network {}, peer {peer_pubkey}, update type: 3 \ - (DELETE), error: {err}", - self.network, - ); - error!(msg); - return Err(Status::new(Code::Internal, msg)); - } - debug!("Peer delete command sent for network {}", self.network); - Ok(()) - } - - /// Send firewall config update command to gateway - fn send_firewall_update(&self, firewall_config: FirewallConfig) -> Result<(), Status> { - debug!( - "Sending firewall config update for network {} with config {firewall_config:?}", - self.network - ); - let proto_firewall: ProtoFirewallConfig = firewall_config.into(); - if let Err(err) = self.tx.send(CoreResponse { - id: 0, - payload: Some(core_response::Payload::Update(Update { - update_type: UpdateType::Modify as i32, - update: Some(update::Update::FirewallConfig(proto_firewall)), - })), - }) { - let msg = format!( - "Failed to send firewall config update for network {}, error: {err}", - self.network, - ); - error!(msg); - return Err(Status::new(Code::Internal, msg)); - } - debug!("Firewall config update sent for network {}", self.network); - Ok(()) - } - - /// Send firewall disable command to gateway - fn send_firewall_disable(&self) -> Result<(), Status> { - debug!( - "Sending firewall disable command for network {}", - self.network - ); - if let Err(err) = self.tx.send(CoreResponse { - id: 0, - payload: Some(core_response::Payload::Update(Update { - update_type: UpdateType::Delete as i32, - update: Some(update::Update::DisableFirewall(())), - })), - }) { - let msg = format!( - "Failed to send firewall disable command for network {}, error: {err}", - self.network, - ); - error!(msg); - return Err(Status::new(Code::Internal, msg)); - } - debug!("Firewall disable command sent for network {}", self.network); - Ok(()) - } -} - /// Helper used to convert peer stats coming from gRPC client /// into an internal representation fn try_protos_into_stats_message( @@ -1241,7 +776,6 @@ fn try_protos_into_stats_message( location_id: Id, gateway_id: Id, ) -> Option { - // try to parse endpoint let endpoint = proto_stats.endpoint.parse().ok()?; let latest_handshake = proto_stats @@ -1261,422 +795,4 @@ fn try_protos_into_stats_message( } #[cfg(test)] -mod tests { - use std::{ - collections::HashMap, - net::IpAddr, - str::FromStr, - sync::{Arc, atomic::AtomicBool}, - }; - - use chrono::{DateTime, Utc}; - use defguard_common::{ - db::{ - Id, - models::{ - Device, DeviceType, User, - device::WireguardNetworkDevice, - gateway::Gateway, - vpn_client_session::VpnClientSession, - wireguard::{ServiceLocationMode, WireguardNetwork}, - }, - setup_pool, - }, - gateway_event::GatewayCommand, - }; - use defguard_proto::gateway::{Configuration, PeerStats, core_response}; - use prost_types::Timestamp; - use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - use tokio::sync::{broadcast, mpsc::unbounded_channel, watch}; - - use super::{ - GatewayHandler, GatewayUpdatesHandler, WireguardPeer, try_protos_into_stats_message, - }; - - fn test_network(mfa_enabled: bool) -> WireguardNetwork { - WireguardNetwork::new( - "test-network".into(), - 51820, - "127.0.0.1".into(), - None, - Vec::new(), - true, - false, - false, - false, - mfa_enabled, - ServiceLocationMode::Disabled, - ) - .with_id(1) - } - - fn build_peer_stats(endpoint: &str) -> PeerStats { - PeerStats { - public_key: "peer-public-key".to_owned(), - endpoint: endpoint.to_owned(), - upload: 123, - download: 456, - keepalive_interval: 25, - latest_handshake: Some(prost_types::Timestamp { - seconds: 1_700_000_000, - nanos: 0, - }), - allowed_ips: "10.10.0.2/32".to_owned(), - } - } - - fn build_network() -> WireguardNetwork { - let mut network = WireguardNetwork::new( - "test-network".to_owned(), - 51820, - "198.51.100.10".to_owned(), - Some("1.1.1.1".to_owned()), - ["0.0.0.0/0".parse().expect("valid allowed IP network")], - false, - false, - false, - false, - false, // mfa_enabled - ServiceLocationMode::default(), - ) - .set_address([ - "10.10.0.1/24".parse().expect("valid IPv4 network"), - "fd00::1/64".parse().expect("valid IPv6 network"), - ]) - .expect("valid network addresses") - .with_id(1); - network.pubkey = "network-public-key".to_owned(); - network.prvkey = "network-private-key".to_owned(); - network.mtu = 1420; - network.fwmark = 4321; - network.keepalive_interval = 25; - network.peer_disconnect_threshold = 180; - network - } - - #[test] - fn try_protos_into_stats_message_maps_valid_peer_stats() { - let stats = try_protos_into_stats_message(build_peer_stats("203.0.113.10:51820"), 11, 22) - .expect("valid peer stats should be converted"); - - assert_eq!(stats.location_id, 11); - assert_eq!(stats.gateway_id, 22); - assert_eq!(stats.device_pubkey, "peer-public-key"); - assert_eq!(stats.endpoint.to_string(), "203.0.113.10:51820"); - assert_eq!(stats.upload, 123); - assert_eq!(stats.download, 456); - assert_eq!( - stats.latest_handshake, - DateTime::from_timestamp(1_700_000_000, 0) - .expect("valid handshake timestamp") - .naive_utc() - ); - } - - #[test] - fn try_protos_into_stats_message_rejects_invalid_endpoint() { - let stats = try_protos_into_stats_message(build_peer_stats("not-a-socket-address"), 11, 22); - - assert!(stats.is_none()); - } - - #[test] - fn try_protos_into_stats_message_returns_none_for_missing_handshake() { - let stats = try_protos_into_stats_message( - PeerStats { - latest_handshake: None, - ..build_peer_stats("203.0.113.10:51820") - }, - 11, - 22, - ); - - assert!(stats.is_none()); - } - - #[test] - fn try_protos_into_stats_message_returns_none_for_invalid_timestamp() { - let stats = try_protos_into_stats_message( - PeerStats { - latest_handshake: Some(Timestamp { - seconds: i64::MAX, - nanos: 0, - }), - ..build_peer_stats("203.0.113.10:51820") - }, - 11, - 22, - ); - - assert!(stats.is_none()); - } - - #[test] - fn gen_config_maps_network_fields() { - use defguard_common::gateway_types::{ - FirewallConfig as NativeFirewallConfig, FirewallPolicy, - }; - use defguard_proto::enterprise::firewall::FirewallPolicy as ProtoFirewallPolicy; - let config = Configuration::new( - &build_network(), - vec![WireguardPeer { - pubkey: "peer-public-key".to_owned(), - allowed_ips: vec!["10.10.0.2/32".to_owned()], - preshared_key: Some("peer-preshared-key".to_owned()), - keepalive_interval: Some(25), - }], - Some(NativeFirewallConfig { - default_policy: FirewallPolicy::Unspecified, - rules: Vec::new(), - snat_bindings: Vec::new(), - }), - ); - - assert_eq!(config.name, "test-network"); - assert_eq!(config.port, 51820); - assert_eq!(config.private_key, "network-private-key"); - assert_eq!(config.addresses, vec!["10.10.0.1/24", "fd00::1/64"]); - assert_eq!(config.mtu, 1420); - assert_eq!(config.fwmark, 4321); - - let peer = config - .peers - .first() - .expect("generated config should include peer"); - assert_eq!(peer.pubkey, "peer-public-key"); - assert_eq!(peer.allowed_ips, vec!["10.10.0.2/32"]); - assert_eq!(peer.preshared_key.as_deref(), Some("peer-preshared-key")); - assert_eq!(peer.keepalive_interval, Some(25)); - - let firewall_config = config - .firewall_config - .expect("generated config should include firewall config"); - assert_eq!( - firewall_config.default_policy, - ProtoFirewallPolicy::Unspecified as i32 - ); - assert_eq!( - firewall_config.rules, - [] as [defguard_proto::enterprise::firewall::FirewallRule; 0] - ); - assert_eq!( - firewall_config.snat_bindings, - [] as [defguard_proto::enterprise::firewall::SnatBinding; 0] - ); - } - - #[test] - fn gen_config_preserves_absent_firewall_config_and_empty_peers() { - let config = Configuration::new(&build_network(), Vec::new(), None); - - assert_eq!(config.peers, [] as [defguard_proto::gateway::Peer; 0]); - assert!(config.firewall_config.is_none()); - } - - fn test_handler(mfa_enabled: bool) -> GatewayUpdatesHandler { - let network = test_network(mfa_enabled); - let (events_tx, events_rx) = broadcast::channel(1); - let (tx, _rx) = unbounded_channel(); - drop(events_tx); - - let mut handler = - GatewayUpdatesHandler::new(network.id, network, "gateway".into(), None, events_rx, tx); - handler.session_authorization_required = handler.network.mfa_enabled; - handler - } - - #[test] - fn test_runtime_peer_update_strips_preshared_key_for_non_mfa_locations() { - let handler = test_handler(false); - - let peer = handler - .runtime_peer_update( - "device", - "device-pubkey".into(), - vec!["10.1.1.2".into()], - true, - Some("legacy-psk".into()), - ) - .unwrap(); - - assert_eq!(peer.pubkey, "device-pubkey"); - assert_eq!(peer.allowed_ips, ["10.1.1.2"]); - assert_eq!(peer.preshared_key, None); - assert_eq!(peer.keepalive_interval, Some(25)); - } - - #[test] - fn test_runtime_peer_update_skips_authorized_mfa_peer_without_session_preshared_key() { - let handler = test_handler(true); - - let peer = handler.runtime_peer_update( - "device", - "device-pubkey".into(), - vec!["10.1.1.2".into()], - true, - None, - ); - - assert_eq!(peer, None); - } - - #[test] - fn test_runtime_peer_update_preserves_session_preshared_key_for_authorized_mfa_peer() { - let handler = test_handler(true); - - let peer = handler - .runtime_peer_update( - "device", - "device-pubkey".into(), - vec!["10.1.1.2".into()], - true, - Some("session-psk".into()), - ) - .unwrap(); - - assert_eq!(peer.preshared_key, Some("session-psk".into())); - } - - #[test] - fn test_runtime_peer_update_preserves_session_preshared_key_for_authorized_posture_peer() { - let mut handler = test_handler(false); - handler.session_authorization_required = true; - - let peer = handler - .runtime_peer_update( - "device", - "device-pubkey".into(), - vec!["10.1.1.2".into()], - true, - Some("session-psk".into()), - ) - .unwrap(); - - assert_eq!(peer.preshared_key, Some("session-psk".into())); - } - - #[sqlx::test] - async fn test_send_configuration_includes_mfa_peers_with_session_preshared_key( - _: PgPoolOptions, - options: PgConnectOptions, - ) { - let pool = setup_pool(options).await; - - let user = User::new( - "testuser", - Some("password123"), - "Test", - "User", - "test@example.com", - None, - ) - .save(&pool) - .await - .unwrap(); - - let new_device = Device::new( - "device-new".into(), - "pubkey-new".into(), - user.id, - DeviceType::User, - None, - true, - ) - .save(&pool) - .await - .unwrap(); - - let connected_device = Device::new( - "device-connected".into(), - "pubkey-connected".into(), - user.id, - DeviceType::User, - None, - true, - ) - .save(&pool) - .await - .unwrap(); - - let mut network = WireguardNetwork::default() - .try_set_address("10.7.1.1/24") - .unwrap(); - network.name = "mfa-full-config-location".to_owned(); - network.mfa_enabled = true; - network.service_location_mode = ServiceLocationMode::Disabled; - let network = network.save(&pool).await.unwrap(); - - WireguardNetworkDevice::new( - network.id, - new_device.id, - vec![IpAddr::from_str("10.7.1.2").unwrap()], - ) - .insert(&pool) - .await - .unwrap(); - - WireguardNetworkDevice::new( - network.id, - connected_device.id, - vec![IpAddr::from_str("10.7.1.3").unwrap()], - ) - .insert(&pool) - .await - .unwrap(); - - let mut new_session = - VpnClientSession::new(network.id, user.id, new_device.id, None, false); - new_session.preshared_key = Some("new-session-psk".into()); - new_session.save(&pool).await.unwrap(); - - let mut connected_session = VpnClientSession::new( - network.id, - user.id, - connected_device.id, - Some(Utc::now().naive_utc()), - false, - ); - connected_session.preshared_key = Some("connected-session-psk".into()); - connected_session.save(&pool).await.unwrap(); - - let gateway = Gateway::new(network.id, "gateway", "127.0.0.1", 50051, "test") - .save(&pool) - .await - .unwrap(); - let (events_tx, _events_rx) = broadcast::channel::(1); - let (connection_events_tx, _connection_events_rx) = unbounded_channel(); - let (peer_stats_tx, _peer_stats_rx) = unbounded_channel(); - let (_certs_tx, certs_rx) = watch::channel(Arc::new(HashMap::::new())); - let handler = GatewayHandler::new( - gateway, - pool.clone(), - events_tx, - connection_events_tx, - peer_stats_tx, - certs_rx, - Arc::new(AtomicBool::new(false)), - ) - .unwrap(); - let (tx, mut rx) = unbounded_channel(); - - handler.send_configuration(&tx).await.unwrap(); - - let response = rx.recv().await.unwrap(); - let Some(core_response::Payload::Config(configuration)) = response.payload else { - panic!("expected gateway config payload"); - }; - - assert_eq!(configuration.peers.len(), 2); - assert_eq!( - configuration - .peers - .iter() - .map(|peer| (peer.pubkey.as_str(), peer.preshared_key.as_deref())) - .collect::>(), - [ - ("pubkey-new", Some("new-session-psk")), - ("pubkey-connected", Some("connected-session-psk")), - ] - ); - } -} +mod tests; diff --git a/crates/defguard_gateway_manager/src/handler/tests.rs b/crates/defguard_gateway_manager/src/handler/tests.rs new file mode 100644 index 0000000000..b36b8f9130 --- /dev/null +++ b/crates/defguard_gateway_manager/src/handler/tests.rs @@ -0,0 +1,415 @@ +use std::{ + collections::HashMap, + net::IpAddr, + str::FromStr, + sync::{Arc, atomic::AtomicBool}, +}; + +use chrono::{DateTime, Utc}; +use defguard_common::{ + db::{ + Id, + models::{ + Device, DeviceType, User, + device::WireguardNetworkDevice, + gateway::Gateway, + vpn_client_session::VpnClientSession, + wireguard::{ServiceLocationMode, WireguardNetwork}, + }, + setup_pool, + }, + gateway_event::GatewayCommand, + gateway_types::WireguardPeer, +}; +use defguard_proto::gateway::{Configuration, PeerStats, core_response}; +use prost_types::Timestamp; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use tokio::sync::{broadcast, mpsc::unbounded_channel, watch}; + +use crate::updates::GatewayUpdatesHandler; + +use super::{GatewayHandler, try_protos_into_stats_message}; + +fn test_network(mfa_enabled: bool) -> WireguardNetwork { + WireguardNetwork::new( + "test-network".into(), + 51820, + "127.0.0.1".into(), + None, + Vec::new(), + true, + false, + false, + false, + mfa_enabled, + ServiceLocationMode::Disabled, + ) + .with_id(1) +} + +fn build_peer_stats(endpoint: &str) -> PeerStats { + PeerStats { + public_key: "peer-public-key".to_owned(), + endpoint: endpoint.to_owned(), + upload: 123, + download: 456, + keepalive_interval: 25, + latest_handshake: Some(prost_types::Timestamp { + seconds: 1_700_000_000, + nanos: 0, + }), + allowed_ips: "10.10.0.2/32".to_owned(), + } +} + +fn build_network() -> WireguardNetwork { + let mut network = WireguardNetwork::new( + "test-network".to_owned(), + 51820, + "198.51.100.10".to_owned(), + Some("1.1.1.1".to_owned()), + ["0.0.0.0/0".parse().expect("valid allowed IP network")], + false, + false, + false, + false, + false, // mfa_enabled + ServiceLocationMode::default(), + ) + .set_address([ + "10.10.0.1/24".parse().expect("valid IPv4 network"), + "fd00::1/64".parse().expect("valid IPv6 network"), + ]) + .expect("valid network addresses") + .with_id(1); + network.pubkey = "network-public-key".to_owned(); + network.prvkey = "network-private-key".to_owned(); + network.mtu = 1420; + network.fwmark = 4321; + network.keepalive_interval = 25; + network.peer_disconnect_threshold = 180; + network +} + +#[test] +fn try_protos_into_stats_message_maps_valid_peer_stats() { + let stats = try_protos_into_stats_message(build_peer_stats("203.0.113.10:51820"), 11, 22) + .expect("valid peer stats should be converted"); + + assert_eq!(stats.location_id, 11); + assert_eq!(stats.gateway_id, 22); + assert_eq!(stats.device_pubkey, "peer-public-key"); + assert_eq!(stats.endpoint.to_string(), "203.0.113.10:51820"); + assert_eq!(stats.upload, 123); + assert_eq!(stats.download, 456); + assert_eq!( + stats.latest_handshake, + DateTime::from_timestamp(1_700_000_000, 0) + .expect("valid handshake timestamp") + .naive_utc() + ); +} + +#[test] +fn try_protos_into_stats_message_rejects_invalid_endpoint() { + let stats = try_protos_into_stats_message(build_peer_stats("not-a-socket-address"), 11, 22); + + assert!(stats.is_none()); +} + +#[test] +fn try_protos_into_stats_message_returns_none_for_missing_handshake() { + let stats = try_protos_into_stats_message( + PeerStats { + latest_handshake: None, + ..build_peer_stats("203.0.113.10:51820") + }, + 11, + 22, + ); + + assert!(stats.is_none()); +} + +#[test] +fn try_protos_into_stats_message_returns_none_for_invalid_timestamp() { + let stats = try_protos_into_stats_message( + PeerStats { + latest_handshake: Some(Timestamp { + seconds: i64::MAX, + nanos: 0, + }), + ..build_peer_stats("203.0.113.10:51820") + }, + 11, + 22, + ); + + assert!(stats.is_none()); +} + +#[test] +fn gen_config_maps_network_fields() { + use defguard_common::gateway_types::{FirewallConfig as NativeFirewallConfig, FirewallPolicy}; + use defguard_proto::enterprise::firewall::FirewallPolicy as ProtoFirewallPolicy; + let config = Configuration::new( + &build_network(), + vec![WireguardPeer { + pubkey: "peer-public-key".to_owned(), + allowed_ips: vec!["10.10.0.2/32".to_owned()], + preshared_key: Some("peer-preshared-key".to_owned()), + keepalive_interval: Some(25), + }], + Some(NativeFirewallConfig { + default_policy: FirewallPolicy::Unspecified, + rules: Vec::new(), + snat_bindings: Vec::new(), + }), + ); + + assert_eq!(config.name, "test-network"); + assert_eq!(config.port, 51820); + assert_eq!(config.private_key, "network-private-key"); + assert_eq!(config.addresses, vec!["10.10.0.1/24", "fd00::1/64"]); + assert_eq!(config.mtu, 1420); + assert_eq!(config.fwmark, 4321); + + let peer = config + .peers + .first() + .expect("generated config should include peer"); + assert_eq!(peer.pubkey, "peer-public-key"); + assert_eq!(peer.allowed_ips, vec!["10.10.0.2/32"]); + assert_eq!(peer.preshared_key.as_deref(), Some("peer-preshared-key")); + assert_eq!(peer.keepalive_interval, Some(25)); + + let firewall_config = config + .firewall_config + .expect("generated config should include firewall config"); + assert_eq!( + firewall_config.default_policy, + ProtoFirewallPolicy::Unspecified as i32 + ); + assert_eq!( + firewall_config.rules, + [] as [defguard_proto::enterprise::firewall::FirewallRule; 0] + ); + assert_eq!( + firewall_config.snat_bindings, + [] as [defguard_proto::enterprise::firewall::SnatBinding; 0] + ); +} + +#[test] +fn gen_config_preserves_absent_firewall_config_and_empty_peers() { + let config = Configuration::new(&build_network(), Vec::new(), None); + + assert_eq!(config.peers, [] as [defguard_proto::gateway::Peer; 0]); + assert!(config.firewall_config.is_none()); +} + +fn test_handler(mfa_enabled: bool) -> GatewayUpdatesHandler { + let network = test_network(mfa_enabled); + let (events_tx, events_rx) = broadcast::channel(1); + let (tx, _rx) = unbounded_channel(); + drop(events_tx); + + let mut handler = + GatewayUpdatesHandler::new(network.id, network, "gateway".into(), None, events_rx, tx); + handler.session_authorization_required = handler.network.mfa_enabled; + handler +} + +#[test] +fn test_runtime_peer_update_strips_preshared_key_for_non_mfa_locations() { + let handler = test_handler(false); + + let peer = handler + .runtime_peer_update( + "device", + "device-pubkey".into(), + vec!["10.1.1.2".into()], + true, + Some("legacy-psk".into()), + ) + .unwrap(); + + assert_eq!(peer.pubkey, "device-pubkey"); + assert_eq!(peer.allowed_ips, ["10.1.1.2"]); + assert_eq!(peer.preshared_key, None); + assert_eq!(peer.keepalive_interval, Some(25)); +} + +#[test] +fn test_runtime_peer_update_skips_authorized_mfa_peer_without_session_preshared_key() { + let handler = test_handler(true); + + let peer = handler.runtime_peer_update( + "device", + "device-pubkey".into(), + vec!["10.1.1.2".into()], + true, + None, + ); + + assert_eq!(peer, None); +} + +#[test] +fn test_runtime_peer_update_preserves_session_preshared_key_for_authorized_mfa_peer() { + let handler = test_handler(true); + + let peer = handler + .runtime_peer_update( + "device", + "device-pubkey".into(), + vec!["10.1.1.2".into()], + true, + Some("session-psk".into()), + ) + .unwrap(); + + assert_eq!(peer.preshared_key, Some("session-psk".into())); +} + +#[test] +fn test_runtime_peer_update_preserves_session_preshared_key_for_authorized_posture_peer() { + let mut handler = test_handler(false); + handler.session_authorization_required = true; + + let peer = handler + .runtime_peer_update( + "device", + "device-pubkey".into(), + vec!["10.1.1.2".into()], + true, + Some("session-psk".into()), + ) + .unwrap(); + + assert_eq!(peer.preshared_key, Some("session-psk".into())); +} + +#[sqlx::test] +async fn test_send_configuration_includes_mfa_peers_with_session_preshared_key( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password123"), + "Test", + "User", + "test@example.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let new_device = Device::new( + "device-new".into(), + "pubkey-new".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let connected_device = Device::new( + "device-connected".into(), + "pubkey-connected".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let mut network = WireguardNetwork::default() + .try_set_address("10.7.1.1/24") + .unwrap(); + network.name = "mfa-full-config-location".to_owned(); + network.mfa_enabled = true; + network.service_location_mode = ServiceLocationMode::Disabled; + let network = network.save(&pool).await.unwrap(); + + WireguardNetworkDevice::new( + network.id, + new_device.id, + vec![IpAddr::from_str("10.7.1.2").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + WireguardNetworkDevice::new( + network.id, + connected_device.id, + vec![IpAddr::from_str("10.7.1.3").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + let mut new_session = VpnClientSession::new(network.id, user.id, new_device.id, None, false); + new_session.preshared_key = Some("new-session-psk".into()); + new_session.save(&pool).await.unwrap(); + + let mut connected_session = VpnClientSession::new( + network.id, + user.id, + connected_device.id, + Some(Utc::now().naive_utc()), + false, + ); + connected_session.preshared_key = Some("connected-session-psk".into()); + connected_session.save(&pool).await.unwrap(); + + let gateway = Gateway::new(network.id, "gateway", "127.0.0.1", 50051, "test") + .save(&pool) + .await + .unwrap(); + let (events_tx, _events_rx) = broadcast::channel::(1); + let (connection_events_tx, _connection_events_rx) = unbounded_channel(); + let (peer_stats_tx, _peer_stats_rx) = unbounded_channel(); + let (_certs_tx, certs_rx) = watch::channel(Arc::new(HashMap::::new())); + let handler = GatewayHandler::new( + gateway, + pool.clone(), + events_tx, + connection_events_tx, + peer_stats_tx, + certs_rx, + Arc::new(AtomicBool::new(false)), + ) + .unwrap(); + let (tx, mut rx) = unbounded_channel(); + + handler.send_configuration(&tx).await.unwrap(); + + let response = rx.recv().await.unwrap(); + let Some(core_response::Payload::Config(configuration)) = response.payload else { + panic!("expected gateway config payload"); + }; + + assert_eq!(configuration.peers.len(), 2); + assert_eq!( + configuration + .peers + .iter() + .map(|peer| (peer.pubkey.as_str(), peer.preshared_key.as_deref())) + .collect::>(), + [ + ("pubkey-new", Some("new-session-psk")), + ("pubkey-connected", Some("connected-session-psk")), + ] + ); +} diff --git a/crates/defguard_gateway_manager/src/lib.rs b/crates/defguard_gateway_manager/src/lib.rs index c89b922093..55752b6167 100644 --- a/crates/defguard_gateway_manager/src/lib.rs +++ b/crates/defguard_gateway_manager/src/lib.rs @@ -36,6 +36,7 @@ extern crate tracing; mod certs; mod error; mod handler; +mod updates; #[cfg(test)] mod tests; diff --git a/crates/defguard_gateway_manager/src/updates.rs b/crates/defguard_gateway_manager/src/updates.rs new file mode 100644 index 0000000000..2841bf66da --- /dev/null +++ b/crates/defguard_gateway_manager/src/updates.rs @@ -0,0 +1,455 @@ +use std::net::IpAddr; + +use defguard_common::{ + db::{ + Id, + models::{DeviceNetworkInfo, WireguardNetwork, wireguard::DEFAULT_WIREGUARD_MTU}, + }, + gateway_event::GatewayCommand, + gateway_types::{FirewallConfig, WireguardPeer}, +}; +use defguard_proto::{ + enterprise::firewall::FirewallConfig as ProtoFirewallConfig, + gateway::{Configuration, CoreResponse, Peer, Update, UpdateType, core_response, update}, +}; +use sqlx::PgPool; +use tokio::sync::{broadcast, mpsc::UnboundedSender}; +use tonic::{Code, Status}; + +/// Helper struct for handling gateway events. +pub(crate) struct GatewayUpdatesHandler { + network_id: Id, + pub(crate) network: WireguardNetwork, + gateway_name: String, + pool: Option, + pub(crate) session_authorization_required: bool, + events_rx: broadcast::Receiver, + tx: UnboundedSender, +} + +impl GatewayUpdatesHandler { + #[must_use] + pub(crate) fn new( + network_id: Id, + network: WireguardNetwork, + gateway_name: String, + pool: Option, + events_rx: broadcast::Receiver, + tx: UnboundedSender, + ) -> Self { + Self { + network_id, + network, + gateway_name, + pool, + session_authorization_required: false, + events_rx, + tx, + } + } + + async fn authorization_required_for( + network: &WireguardNetwork, + pool: Option<&PgPool>, + ) -> bool { + if network.mfa_enabled { + return true; + } + + let Some(pool) = pool else { + return false; + }; + + match network.has_postures(pool).await { + Ok(has_postures) => has_postures, + Err(err) => { + error!("Failed to fetch postures for location {network}: {err}"); + false + } + } + } + + async fn refresh_session_authorization_required(&mut self) { + self.session_authorization_required = + Self::authorization_required_for(&self.network, self.pool.as_ref()).await; + } + + #[must_use] + pub(crate) fn runtime_peer_update( + &self, + peer_label: &str, + peer_pubkey: String, + allowed_ips: Vec, + is_authorized: bool, + preshared_key: Option, + ) -> Option { + if !self.session_authorization_required { + return Some(Peer { + pubkey: peer_pubkey, + allowed_ips, + preshared_key: None, + keepalive_interval: Some(self.network.keepalive_interval.cast_unsigned()), + }); + } + + if !is_authorized { + debug!( + "Skipping gateway peer update for WireGuard device {peer_label} in runtime-authorized location {} because there is no active VPN session", + self.network.name + ); + return None; + } + + let Some(preshared_key) = preshared_key else { + debug!( + "Skipping gateway peer update for WireGuard device {peer_label} in location {} because the runtime preshared key is missing", + self.network.name + ); + return None; + }; + + Some(Peer { + pubkey: peer_pubkey, + allowed_ips, + preshared_key: Some(preshared_key), + keepalive_interval: Some(self.network.keepalive_interval.cast_unsigned()), + }) + } + + fn send_runtime_device_update( + &self, + peer_label: &str, + peer_pubkey: String, + network_info: &DeviceNetworkInfo, + update_type: UpdateType, + ) -> Result<(), Status> { + let allowed_ips = network_info + .device_wireguard_ips + .iter() + .map(IpAddr::to_string) + .collect(); + + let Some(peer) = self.runtime_peer_update( + peer_label, + peer_pubkey, + allowed_ips, + network_info.is_authorized, + network_info.preshared_key.clone(), + ) else { + return Ok(()); + }; + + self.send_peer_update(peer, update_type) + } + + /// Process incoming Gateway events + /// + /// Main gRPC server uses a shared channel for broadcasting all gateway events + /// so the handler must determine if an event is relevant for the network being serviced + pub(crate) async fn run(&mut self) { + info!( + "Starting update stream to gateway: {}, network {}", + self.gateway_name, self.network + ); + self.refresh_session_authorization_required().await; + while let Ok(update) = self.events_rx.recv().await { + debug!("Received WireGuard update: {update:?}"); + let result = match update { + GatewayCommand::NetworkCreated(network_id, network) => { + if network_id == self.network_id { + self.send_network_update(&network, Vec::new(), None, UpdateType::Create) + } else { + Ok(()) + } + } + GatewayCommand::NetworkModified( + network_id, + network, + peers, + maybe_firewall_config, + ) => { + if network_id == self.network_id { + let result = self.send_network_update( + &network, + peers, + maybe_firewall_config, + UpdateType::Modify, + ); + // update stored network data + self.network = network; + self.refresh_session_authorization_required().await; + result + } else { + Ok(()) + } + } + GatewayCommand::NetworkDeleted(network_id, network_name) => { + if network_id == self.network_id { + self.send_network_delete(&network_name) + } else { + Ok(()) + } + } + GatewayCommand::DeviceCreated(device) => { + // check if a peer has to be added in the current network + match device + .network_info + .iter() + .find(|info| info.network_id == self.network_id) + { + Some(network_info) => self.send_runtime_device_update( + &device.device.name, + device.device.wireguard_pubkey, + network_info, + UpdateType::Create, + ), + None => Ok(()), + } + } + GatewayCommand::DeviceModified(device) => { + // check if a peer has to be updated in the current network + match device + .network_info + .iter() + .find(|info| info.network_id == self.network_id) + { + Some(network_info) => self.send_runtime_device_update( + &device.device.name, + device.device.wireguard_pubkey, + network_info, + UpdateType::Modify, + ), + None => Ok(()), + } + } + GatewayCommand::DeviceDeleted(device) => { + // check if a peer has to be updated in the current network + match device + .network_info + .iter() + .find(|info| info.network_id == self.network_id) + { + Some(_) => self.send_peer_delete(&device.device.wireguard_pubkey), + None => Ok(()), + } + } + GatewayCommand::FirewallConfigChanged(location_id, firewall_config) => { + if location_id == self.network_id { + self.send_firewall_update(firewall_config) + } else { + Ok(()) + } + } + GatewayCommand::FirewallDisabled(location_id) => { + if location_id == self.network_id { + self.send_firewall_disable() + } else { + Ok(()) + } + } + GatewayCommand::VpnSessionDeauthorized(location_id, device) => { + if location_id == self.network_id { + self.send_peer_delete(&device.wireguard_pubkey) + } else { + Ok(()) + } + } + GatewayCommand::VpnSessionAuthorized(location_id, device, network_info) => { + if location_id == self.network_id { + if network_info.network_id != location_id { + error!( + "Received VPN authorization success event for location {location_id} with invalid runtime network info: {network_info:?}" + ); + continue; + } + + self.send_runtime_device_update( + &device.name, + device.wireguard_pubkey, + &network_info, + UpdateType::Create, + ) + } else { + Ok(()) + } + } + }; + if result.is_err() { + error!( + "Closing update steam to gateway: {}, network {}", + self.gateway_name, self.network + ); + break; + } + } + } + + /// Sends updated network configuration + fn send_network_update( + &self, + network: &WireguardNetwork, + peers: Vec, + firewall_config: Option, + update_type: UpdateType, + ) -> Result<(), Status> { + debug!("Sending network update for network {network}"); + let proto_peers = peers.into_iter().map(Into::into).collect(); + let proto_firewall = firewall_config.map(ProtoFirewallConfig::from); + if let Err(err) = self.tx.send(CoreResponse { + id: 0, + payload: Some(core_response::Payload::Update(Update { + update_type: update_type as i32, + update: Some(update::Update::Network(Configuration { + name: network.name.clone(), + private_key: network.prvkey.clone(), + addresses: network.address().iter().map(ToString::to_string).collect(), + port: network.port.cast_unsigned(), + peers: proto_peers, + firewall_config: proto_firewall, + mtu: network.mtu.cast_unsigned(), + fwmark: network.fwmark as u32, + })), + })), + }) { + let msg = format!( + "Failed to send network update, network {network}, update type: {update_type:?}, \ + error: {err}", + ); + error!(msg); + return Err(Status::new(Code::Internal, msg)); + } + debug!("Network update sent for network {network}"); + Ok(()) + } + + /// Sends delete network command to gateway + fn send_network_delete(&self, network_name: &str) -> Result<(), Status> { + debug!( + "Sending network delete command for network {}", + self.network + ); + if let Err(err) = self.tx.send(CoreResponse { + id: 0, + payload: Some(core_response::Payload::Update(Update { + update_type: UpdateType::Delete as i32, + update: Some(update::Update::Network(Configuration { + name: network_name.to_owned(), + private_key: String::new(), + addresses: Vec::new(), + port: 0, + peers: Vec::new(), + firewall_config: None, + mtu: DEFAULT_WIREGUARD_MTU.cast_unsigned(), + fwmark: 0, + })), + })), + }) { + let msg = format!( + "Failed to send network update, network {}, update type: 3 (DELETE), error: {err}", + self.network, + ); + error!(msg); + return Err(Status::new(Code::Internal, msg)); + } + debug!("Network delete command sent for network {}", self.network); + Ok(()) + } + + /// Send update peer command to gateway + fn send_peer_update(&self, peer: Peer, update_type: UpdateType) -> Result<(), Status> { + debug!("Sending peer update for network {}", self.network); + if let Err(err) = self.tx.send(CoreResponse { + id: 0, + payload: Some(core_response::Payload::Update(Update { + update_type: update_type as i32, + update: Some(update::Update::Peer(peer)), + })), + }) { + let msg = format!( + "Failed to send peer update for network {}, update type: {update_type:?}, \ + error: {err}", + self.network, + ); + error!(msg); + return Err(Status::new(Code::Internal, msg)); + } + debug!("Peer update sent for network {}", self.network); + Ok(()) + } + + /// Send delete peer command to gateway + fn send_peer_delete(&self, peer_pubkey: &str) -> Result<(), Status> { + debug!("Sending peer delete for network {}", self.network); + if let Err(err) = self.tx.send(CoreResponse { + id: 0, + payload: Some(core_response::Payload::Update(Update { + update_type: UpdateType::Delete as i32, + update: Some(update::Update::Peer(Peer { + pubkey: peer_pubkey.into(), + allowed_ips: Vec::new(), + preshared_key: None, + keepalive_interval: None, + })), + })), + }) { + let msg = format!( + "Failed to send peer update for network {}, peer {peer_pubkey}, update type: 3 \ + (DELETE), error: {err}", + self.network, + ); + error!(msg); + return Err(Status::new(Code::Internal, msg)); + } + debug!("Peer delete command sent for network {}", self.network); + Ok(()) + } + + /// Send firewall config update command to gateway + fn send_firewall_update(&self, firewall_config: FirewallConfig) -> Result<(), Status> { + debug!( + "Sending firewall config update for network {} with config {firewall_config:?}", + self.network + ); + let proto_firewall: ProtoFirewallConfig = firewall_config.into(); + if let Err(err) = self.tx.send(CoreResponse { + id: 0, + payload: Some(core_response::Payload::Update(Update { + update_type: UpdateType::Modify as i32, + update: Some(update::Update::FirewallConfig(proto_firewall)), + })), + }) { + let msg = format!( + "Failed to send firewall config update for network {}, error: {err}", + self.network, + ); + error!(msg); + return Err(Status::new(Code::Internal, msg)); + } + debug!("Firewall config update sent for network {}", self.network); + Ok(()) + } + + /// Send firewall disable command to gateway + fn send_firewall_disable(&self) -> Result<(), Status> { + debug!( + "Sending firewall disable command for network {}", + self.network + ); + if let Err(err) = self.tx.send(CoreResponse { + id: 0, + payload: Some(core_response::Payload::Update(Update { + update_type: UpdateType::Delete as i32, + update: Some(update::Update::DisableFirewall(())), + })), + }) { + let msg = format!( + "Failed to send firewall disable command for network {}, error: {err}", + self.network, + ); + error!(msg); + return Err(Status::new(Code::Internal, msg)); + } + debug!("Firewall disable command sent for network {}", self.network); + Ok(()) + } +} From 28afc20a06511538e627c4befa04cd2391887717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 9 Oct 2026 12:22:10 +0200 Subject: [PATCH 02/14] broadcast to mpsc --- crates/defguard/src/main.rs | 7 +- crates/defguard_common/src/gateway_event.rs | 8 +- crates/defguard_core/src/appstate.rs | 37 ++++---- .../src/enterprise/db/models/acl.rs | 6 +- .../src/enterprise/directory_sync/mod.rs | 12 +-- .../src/enterprise/directory_sync/tests.rs | 56 ++++++------- .../src/enterprise/handlers/openid_login.rs | 2 +- .../defguard_core/src/enterprise/ldap/mod.rs | 6 +- .../defguard_core/src/enterprise/ldap/sync.rs | 10 +-- .../src/enterprise/ldap/tests.rs | 16 ++-- .../src/enterprise/ldap/utils.rs | 8 +- .../defguard_core/src/enterprise/license.rs | 6 +- .../src/enterprise/license/tests.rs | 4 +- .../src/grpc/proxy/client_mfa.rs | 4 +- .../src/grpc/proxy/client_mfa/tests.rs | 53 ++++++------ crates/defguard_core/src/lib.rs | 41 +++++---- .../src/location_management/mod.rs | 4 +- .../defguard_core/src/mfa_engine/authorize.rs | 11 +-- crates/defguard_core/src/mfa_engine/mod.rs | 4 +- crates/defguard_core/src/mfa_engine/tests.rs | 8 +- crates/defguard_core/src/user_management.rs | 4 +- crates/defguard_core/src/utility_thread.rs | 8 +- .../tests/integration/api/common/client.rs | 4 +- .../tests/integration/api/common/mod.rs | 21 ++--- .../tests/integration/api/component_setup.rs | 9 +- .../tests/integration/api/device_posture.rs | 5 +- .../tests/integration/api/proxy_certs.rs | 23 +++-- .../tests/integration/api/wireguard.rs | 8 +- .../api/wireguard_network_import.rs | 2 +- .../tests/integration/ldap/mod.rs | 14 ++-- crates/defguard_event_logger/src/lib.rs | 39 +++++---- .../defguard_gateway_manager/src/handler.rs | 20 +++-- .../src/handler/tests.rs | 10 +-- crates/defguard_gateway_manager/src/lib.rs | 84 ++++++++++++++++--- .../src/tests/common/mod.rs | 53 ++++++++---- .../src/tests/gateway_manager/handler.rs | 5 +- .../gateway_manager/handler/device_events.rs | 6 +- .../handler/firewall_events.rs | 4 +- .../src/tests/gateway_manager/handler/mfa.rs | 10 +-- .../gateway_manager/handler/network_events.rs | 21 ++--- .../tests/gateway_manager/handler/support.rs | 9 +- .../defguard_gateway_manager/src/updates.rs | 11 ++- crates/defguard_proxy_manager/src/handler.rs | 7 +- crates/defguard_proxy_manager/src/lib.rs | 5 +- .../src/servers/enrollment.rs | 15 ++-- .../src/tests/common/mod.rs | 69 +++++++-------- .../tests/proxy_manager/handler/enrollment.rs | 2 +- .../src/tests/proxy_manager/handler/mfa.rs | 39 ++++----- .../tests/proxy_manager/handler/mfa_legacy.rs | 21 ++--- .../src/tests/proxy_manager/handler/oidc.rs | 3 - crates/defguard_session_manager/src/error.rs | 8 +- crates/defguard_session_manager/src/lib.rs | 11 +-- .../tests/common/mod.rs | 21 ++--- crates/defguard_setup/src/auto_adoption.rs | 10 +-- crates/defguard_setup/src/migration.rs | 4 +- 55 files changed, 467 insertions(+), 421 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 5d574857ce..7586100d0c 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -300,8 +300,7 @@ async fn main() -> Result<(), anyhow::Error> { // setup communication channels for services let (webhook_tx, webhook_rx) = unbounded_channel::(); - // RX is discarded here since it can be derived from TX later on - let (gateway_tx, _gateway_rx) = broadcast::channel::(256); + let (gateway_tx, gateway_rx) = unbounded_channel::(); let (peer_stats_tx, peer_stats_rx) = unbounded_channel::(); let worker_state = Arc::new(Mutex::new(WorkerState::new(webhook_tx.clone()))); @@ -345,8 +344,8 @@ async fn main() -> Result<(), anyhow::Error> { let mut gateway_manager = GatewayManager::new( pool.clone(), - GatewayTxSet::new(gateway_tx.clone(), peer_stats_tx) - .with_connection_events(gateway_connection_event_tx), + GatewayTxSet::new(peer_stats_tx).with_connection_events(gateway_connection_event_tx), + gateway_rx, ); debug!("Resetting proxy connection state on startup"); diff --git a/crates/defguard_common/src/gateway_event.rs b/crates/defguard_common/src/gateway_event.rs index 426504d3a9..be5d76a1f2 100644 --- a/crates/defguard_common/src/gateway_event.rs +++ b/crates/defguard_common/src/gateway_event.rs @@ -1,10 +1,10 @@ //! Gateway command types and helpers for communicating with the gateway manager service. //! //! [`GatewayCommand`] is the primary type sent from core to the gateway manager over -//! an in-process broadcast channel. The gateway manager converts each command to the +//! an in-process MPSC channel. The gateway manager converts each command to the //! appropriate protobuf wire message before forwarding it to the gateway daemon. -use tokio::sync::broadcast::Sender; +use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, error}; use crate::{ @@ -45,7 +45,7 @@ pub enum GatewayCommand { /// Sends a [`GatewayCommand`] to the gateway manager service. /// /// In API handler context prefer `AppState::send_gateway_command`. -pub fn send_gateway_command(command: GatewayCommand, gateway_tx: &Sender) { +pub fn send_gateway_command(command: GatewayCommand, gateway_tx: &UnboundedSender) { debug!("Sending the following command to Gateway Manager: {command:?}"); if let Err(err) = gateway_tx.send(command) { error!("Error sending gateway command: {err}"); @@ -57,7 +57,7 @@ pub fn send_gateway_command(command: GatewayCommand, gateway_tx: &Sender, - gateway_tx: &Sender, + gateway_tx: &UnboundedSender, ) { debug!("Sending {} gateway commands", commands.len()); for command in commands { diff --git a/crates/defguard_core/src/appstate.rs b/crates/defguard_core/src/appstate.rs index c34c86a250..67ad204ea7 100644 --- a/crates/defguard_core/src/appstate.rs +++ b/crates/defguard_core/src/appstate.rs @@ -7,10 +7,7 @@ use reqwest::Client; use serde_json::json; use sqlx::PgPool; use tokio::{ - sync::{ - broadcast::Sender, - mpsc::{UnboundedReceiver, UnboundedSender}, - }, + sync::{broadcast, mpsc}, task::spawn, }; @@ -27,15 +24,15 @@ const X_DEFGUARD_EVENT: &str = "x-defguard-event"; #[derive(Clone)] pub struct AppState { pub pool: PgPool, - tx: UnboundedSender, - pub gateway_tx: Sender, - pub web_reload_tx: tokio::sync::broadcast::Sender<()>, + tx: mpsc::UnboundedSender, + pub gateway_tx: mpsc::UnboundedSender, + pub web_reload_tx: broadcast::Sender<()>, key: Key, - pub event_tx: UnboundedSender, - pub ldap_tx: UnboundedSender, - pub dirsync_tx: UnboundedSender, + pub event_tx: mpsc::UnboundedSender, + pub ldap_tx: mpsc::UnboundedSender, + pub dirsync_tx: mpsc::UnboundedSender, pub incompatible_components: Arc>, - pub proxy_control_tx: tokio::sync::mpsc::Sender, + pub proxy_control_tx: mpsc::Sender, /// Reflects whether the HTTP server is currently running with TLS pub tls_active: Arc, } @@ -50,7 +47,7 @@ impl AppState { } /// Handle webhook events - async fn handle_triggers(pool: PgPool, mut rx: UnboundedReceiver) { + async fn handle_triggers(pool: PgPool, mut rx: mpsc::UnboundedReceiver) { let reqwest_client = Client::builder().user_agent("reqwest").build().unwrap(); while let Some(msg) = rx.recv().await { debug!("WebHook triggered"); @@ -116,16 +113,16 @@ impl AppState { /// Create application state pub fn new( pool: PgPool, - tx: UnboundedSender, - rx: UnboundedReceiver, - gateway_tx: Sender, - web_reload_tx: tokio::sync::broadcast::Sender<()>, + tx: mpsc::UnboundedSender, + rx: mpsc::UnboundedReceiver, + gateway_tx: mpsc::UnboundedSender, + web_reload_tx: broadcast::Sender<()>, key: Key, - event_tx: UnboundedSender, - ldap_tx: UnboundedSender, - dirsync_tx: UnboundedSender, + event_tx: mpsc::UnboundedSender, + ldap_tx: mpsc::UnboundedSender, + dirsync_tx: mpsc::UnboundedSender, incompatible_components: Arc>, - proxy_control_tx: tokio::sync::mpsc::Sender, + proxy_control_tx: mpsc::Sender, tls_active: Arc, ) -> Self { spawn(Self::handle_triggers(pool.clone(), rx)); diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index d435c0ce7d..3448e92996 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -17,7 +17,7 @@ use sqlx::{ query, query_as, query_scalar, }; use thiserror::Error; -use tokio::sync::broadcast::Sender; +use tokio::sync::mpsc::UnboundedSender; use crate::{ enterprise::{ @@ -555,7 +555,7 @@ impl AclRule { rules: &[Id], actor: &str, pool: &PgPool, - gateway_tx: &Sender, + gateway_tx: &UnboundedSender, ) -> Result<(), AclError> { debug!("Applying {} ACL rules: {rules:?}", rules.len()); let mut transaction = pool.begin().await?; @@ -1830,7 +1830,7 @@ impl AclAlias { kind: AliasKind, actor: &str, pool: &PgPool, - gateway_tx: &Sender, + gateway_tx: &UnboundedSender, ) -> Result<(), AclError> { debug!( "Applying {} ACL aliases of kind {kind:?}: {aliases:?}", diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index a4aa4d44f7..8197eb6498 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -12,7 +12,7 @@ use paste::paste; use reqwest::header::AUTHORIZATION; use sqlx::{PgConnection, PgPool}; use thiserror::Error; -use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; +use tokio::sync::mpsc::UnboundedSender; use super::{ db::models::{ @@ -351,7 +351,7 @@ async fn sync_user_groups( directory_sync: &T, user: &User, pool: &PgPool, - gateway_tx: &Sender, + gateway_tx: &UnboundedSender, ldap_tx: &UnboundedSender, dirsync_tx: &UnboundedSender, provider_name: &str, @@ -467,7 +467,7 @@ pub(crate) async fn test_directory_sync_connection( pub async fn sync_user_groups_if_configured( user: &User, pool: &PgPool, - gateway_tx: &Sender, + gateway_tx: &UnboundedSender, ldap_tx: &UnboundedSender, dirsync_tx: &UnboundedSender, ) -> Result<(), DirectorySyncError> { @@ -567,7 +567,7 @@ async fn create_and_add_to_group( async fn sync_all_users_groups( directory_sync: &T, pool: &PgPool, - gateway_tx: &Sender, + gateway_tx: &UnboundedSender, ldap_tx: &UnboundedSender, dirsync_tx: &UnboundedSender, provider_name: &str, @@ -739,7 +739,7 @@ fn is_directory_sync_enabled(provider: Option<&OpenIdProvider>) -> bool { async fn sync_all_users_state( pool: &PgPool, - gateway_tx: &Sender, + gateway_tx: &UnboundedSender, ldap_tx: &UnboundedSender, dirsync_tx: &UnboundedSender, all_users: &[DirectoryUser], @@ -1292,7 +1292,7 @@ pub(crate) async fn get_directory_sync_interval(pool: &PgPool) -> u64 { // Performs the directory sync job. This function is called by the utility thread. pub(crate) async fn do_directory_sync( pool: &PgPool, - gateway_tx: &Sender, + gateway_tx: &UnboundedSender, ldap_tx: &UnboundedSender, dirsync_tx: &UnboundedSender, ) -> Result<(), DirectorySyncError> { diff --git a/crates/defguard_core/src/enterprise/directory_sync/tests.rs b/crates/defguard_core/src/enterprise/directory_sync/tests.rs index 5580f01c3a..63b6b6bca3 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/tests.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/tests.rs @@ -14,7 +14,7 @@ mod test { }; use ipnetwork::IpNetwork; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - use tokio::sync::{broadcast, mpsc}; + use tokio::sync::mpsc; use super::super::{testprovider::FAILING_GROUP, *}; use crate::{ @@ -47,7 +47,7 @@ mod test { ))); } - async fn do_test_directory_sync(pool: &PgPool, gateway_tx: &broadcast::Sender) { + async fn do_test_directory_sync(pool: &PgPool, gateway_tx: &UnboundedSender) { let (ldap_tx, _ldap_rx) = mpsc::unbounded_channel::(); let (dirsync_tx, _dirsync_rx) = dirsync_test_channel(); do_directory_sync(pool, gateway_tx, &ldap_tx, &dirsync_tx) @@ -199,7 +199,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Keep, @@ -243,7 +243,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Keep, @@ -339,7 +339,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Delete, @@ -384,7 +384,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); User::init_admin_user(&pool, "pass123").await.unwrap(); let _ = make_test_provider( @@ -441,7 +441,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Delete, @@ -512,7 +512,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Disable, @@ -597,7 +597,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); // Added mut gateway_rx + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Keep, @@ -677,7 +677,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _) = broadcast::channel::(16); + let (gateway_tx, _) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Delete, @@ -744,7 +744,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _) = broadcast::channel::(16); + let (gateway_tx, _) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Delete, @@ -777,7 +777,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _) = broadcast::channel::(16); + let (gateway_tx, _) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Delete, @@ -807,7 +807,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _) = broadcast::channel::(16); + let (gateway_tx, _) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Delete, @@ -832,7 +832,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Delete, @@ -886,7 +886,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _) = broadcast::channel::(16); + let (gateway_tx, _) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Delete, @@ -914,7 +914,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _) = broadcast::channel::(16); + let (gateway_tx, _) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Delete, @@ -961,7 +961,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _) = broadcast::channel::(16); + let (gateway_tx, _) = mpsc::unbounded_channel::(); make_test_provider( &pool, DirectorySyncUserBehavior::Delete, @@ -1001,7 +1001,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); // disable prefetching users make_test_provider( @@ -1035,7 +1035,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); // enable prefetching users make_test_provider( @@ -1069,7 +1069,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); // enable prefetching users, import only members of group1 let mut provider = make_test_provider( @@ -1110,7 +1110,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); // enable prefetching users, import only members of a group that doesn't exist // in the directory @@ -1148,7 +1148,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _gateway_rx) = broadcast::channel::(16); + let (gateway_tx, _gateway_rx) = mpsc::unbounded_channel::(); // limit the sync to a group whose member query always fails let mut provider = make_test_provider( @@ -1184,7 +1184,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); // enable prefetching users make_test_provider( @@ -1238,7 +1238,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _gateway_rx) = broadcast::channel::(16); + let (gateway_tx, _gateway_rx) = mpsc::unbounded_channel::(); // enable prefetching users make_test_provider( @@ -1329,7 +1329,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _gateway_rx) = broadcast::channel::(16); + let (gateway_tx, _gateway_rx) = mpsc::unbounded_channel::(); // enable prefetching users make_test_provider( @@ -1432,7 +1432,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _gateway_rx) = broadcast::channel::(16); + let (gateway_tx, _gateway_rx) = mpsc::unbounded_channel::(); make_test_provider( &pool, @@ -1550,7 +1550,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config); - let (gateway_tx, mut gateway_rx) = broadcast::channel::(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel::(); // enable prefetching users make_test_provider( @@ -1605,7 +1605,7 @@ mod test { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); - let (gateway_tx, _gateway_rx) = broadcast::channel::(16); + let (gateway_tx, _gateway_rx) = mpsc::unbounded_channel::(); // prefetch disabled, restrict sync to a group that has no members in the directory let mut provider = make_test_provider( diff --git a/crates/defguard_core/src/enterprise/handlers/openid_login.rs b/crates/defguard_core/src/enterprise/handlers/openid_login.rs index 0dabd1065f..62b6a9488a 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_login.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_login.rs @@ -1198,7 +1198,7 @@ mod test { fn make_app_state(pool: PgPool, key: Key) -> AppState { let (webhook_tx, webhook_rx) = mpsc::unbounded_channel(); - let (gateway_tx, _gateway_rx) = broadcast::channel(16); + let (gateway_tx, _gateway_rx) = mpsc::unbounded_channel(); let (web_reload_tx, _web_reload_rx) = broadcast::channel(8); let (event_tx, _event_rx) = mpsc::unbounded_channel(); let (ldap_tx, _ldap_rx) = mpsc::unbounded_channel(); diff --git a/crates/defguard_core/src/enterprise/ldap/mod.rs b/crates/defguard_core/src/enterprise/ldap/mod.rs index e94b1ede8f..9ba48e00e0 100644 --- a/crates/defguard_core/src/enterprise/ldap/mod.rs +++ b/crates/defguard_core/src/enterprise/ldap/mod.rs @@ -15,7 +15,7 @@ use model::UserObjectClass; use rand::Rng; use sqlx::PgPool; use sync::{get_ldap_sync_status, is_ldap_desynced, set_ldap_sync_status}; -use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; +use tokio::sync::mpsc::UnboundedSender; use self::error::LdapError; use crate::{ @@ -52,7 +52,7 @@ pub mod utils; /// Sets LDAP sync status to OutOfSync if any errors occur during the process. pub(crate) async fn do_ldap_sync( pool: &PgPool, - wg_tx: &Sender, + wg_tx: &UnboundedSender, ldap_tx: &UnboundedSender, ) -> Result<(), LdapError> { debug!("Starting LDAP sync, if enabled"); @@ -417,7 +417,7 @@ impl LDAPConnection { &mut self, users: Vec<&mut User>, pool: &PgPool, - wg_tx: &Sender, + wg_tx: &UnboundedSender, ldap_tx: &UnboundedSender, ) -> Result<(), LdapError> { debug!("Updating users state in LDAP"); diff --git a/crates/defguard_core/src/enterprise/ldap/sync.rs b/crates/defguard_core/src/enterprise/ldap/sync.rs index 2b88b7705a..2519efecc4 100644 --- a/crates/defguard_core/src/enterprise/ldap/sync.rs +++ b/crates/defguard_core/src/enterprise/ldap/sync.rs @@ -80,7 +80,7 @@ use defguard_common::db::{ }; use serde::Serialize; use sqlx::{PgConnection, PgPool}; -use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; +use tokio::sync::mpsc::UnboundedSender; use super::{LDAPConfig, error::LdapError}; use crate::{ @@ -572,7 +572,7 @@ impl super::LDAPConnection { mut intersecting_users: Vec<(User, User)>, authority: Authority, pool: &PgPool, - wg_tx: &Sender, + wg_tx: &UnboundedSender, ldap_tx: &UnboundedSender, ) -> Result<(), LdapError> { let sync_account_status = self.config.ldap_uses_ad && self.config.ldap_sync_account_status; @@ -665,7 +665,7 @@ impl super::LDAPConnection { &mut self, user: &User, pool: &PgPool, - wg_tx: &Sender, + wg_tx: &UnboundedSender, ldap_tx: &UnboundedSender, ) -> Result<(), LdapError> { debug!("Syncing user data for {user}"); @@ -812,7 +812,7 @@ impl super::LDAPConnection { &mut self, pool: &PgPool, full: bool, - wg_tx: &Sender, + wg_tx: &UnboundedSender, ldap_tx: &UnboundedSender, ) -> Result<(), LdapError> { let settings = Settings::get_current_settings(); @@ -1141,7 +1141,7 @@ impl super::LDAPConnection { async fn apply_user_sync_changes( &mut self, pool: &PgPool, - gateway_tx: &Sender, + gateway_tx: &UnboundedSender, mut changes: UserSyncChanges, ldap_tx: &UnboundedSender, ) -> Result<(), LdapError> { diff --git a/crates/defguard_core/src/enterprise/ldap/tests.rs b/crates/defguard_core/src/enterprise/ldap/tests.rs index cf20ac8178..855e879105 100644 --- a/crates/defguard_core/src/enterprise/ldap/tests.rs +++ b/crates/defguard_core/src/enterprise/ldap/tests.rs @@ -14,10 +14,7 @@ use defguard_common::{ testing::smtp::configure_working_smtp, }; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use tokio::sync::{ - broadcast::{Receiver, Sender, channel}, - mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, -}; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}; use super::{ model::{ @@ -96,8 +93,11 @@ fn configure_smtp_and_ldap(settings: &mut Settings) { } /// Bind both halves: a dropped receiver makes `Sender::send` fail and swallow events. -fn wg_test_channel() -> (Sender, Receiver) { - channel(256) +fn wg_test_channel() -> ( + UnboundedSender, + UnboundedReceiver, +) { + unbounded_channel() } fn ldap_test_channel() -> ( @@ -115,7 +115,7 @@ fn drain_ldap_sync_events(rx: &mut UnboundedReceiver) -> Vec< events } -fn drain_gateway_commands(rx: &mut Receiver) -> Vec { +fn drain_gateway_commands(rx: &mut UnboundedReceiver) -> Vec { let mut events = Vec::new(); while let Ok(event) = rx.try_recv() { events.push(event); @@ -204,7 +204,7 @@ async fn defguard_sync_snapshot( async fn assert_incremental_sync_converges( ldap_conn: &mut super::LDAPConnection, pool: &PgPool, - wg_tx: &Sender, + wg_tx: &UnboundedSender, ) { let before = defguard_sync_snapshot(pool).await; ldap_conn.test_client_mut().clear_events(); diff --git a/crates/defguard_core/src/enterprise/ldap/utils.rs b/crates/defguard_core/src/enterprise/ldap/utils.rs index 5a973220fd..b31f4a6895 100644 --- a/crates/defguard_core/src/enterprise/ldap/utils.rs +++ b/crates/defguard_core/src/enterprise/ldap/utils.rs @@ -8,7 +8,7 @@ use defguard_common::db::{ models::{User, group::Group}, }; use sqlx::PgPool; -use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; +use tokio::sync::mpsc::UnboundedSender; use super::{LDAPConnection, error::LdapError}; use crate::{ @@ -122,7 +122,7 @@ pub(crate) async fn login_through_ldap_with_connection( pub async fn ldap_update_user_state( user: &mut User, pool: &PgPool, - wg_tx: &Sender, + wg_tx: &UnboundedSender, ldap_tx: &UnboundedSender, ) { let vec = vec![user]; @@ -133,7 +133,7 @@ pub async fn ldap_update_user_state( pub(crate) async fn ldap_update_users_state( users: Vec<&mut User>, pool: &PgPool, - wg_tx: &Sender, + wg_tx: &UnboundedSender, ldap_tx: &UnboundedSender, ) { let _ = Box::pin(with_ldap_status(pool, async { @@ -241,7 +241,7 @@ pub(crate) async fn ldap_handle_user_modify( old_username: &str, current_user: &mut User, pool: &PgPool, - wg_tx: &Sender, + wg_tx: &UnboundedSender, ldap_tx: &UnboundedSender, ) { let _: Result<(), LdapError> = Box::pin(with_ldap_status(pool, async { diff --git a/crates/defguard_core/src/enterprise/license.rs b/crates/defguard_core/src/enterprise/license.rs index 3a6f1d10ed..91ede2a17d 100644 --- a/crates/defguard_core/src/enterprise/license.rs +++ b/crates/defguard_core/src/enterprise/license.rs @@ -24,7 +24,7 @@ use prost::Message; use sqlx::PgPool; use strum::VariantArray; use thiserror::Error; -use tokio::time::sleep; +use tokio::{sync::mpsc, time::sleep}; use crate::{ enterprise::{ @@ -613,7 +613,7 @@ const MAX_OVERDUE_TIME: TimeDelta = TimeDelta::days(14); /// Keep one enabled Gateway per location and one Edge per instance. async fn trim_gateways_and_edges( pool: &PgPool, - proxy_control_tx: &tokio::sync::mpsc::Sender, + proxy_control_tx: &mpsc::Sender, ) -> Result<(), LicenseError> { Gateway::leave_one_enabled(pool).await?; @@ -641,7 +641,7 @@ async fn trim_gateways_and_edges( #[instrument(skip_all)] pub async fn run_periodic_license_check( pool: &PgPool, - proxy_control_tx: tokio::sync::mpsc::Sender, + proxy_control_tx: mpsc::Sender, ) -> Result<(), LicenseError> { let config = server_config(); let mut check_period: Duration = *config.check_period; diff --git a/crates/defguard_core/src/enterprise/license/tests.rs b/crates/defguard_core/src/enterprise/license/tests.rs index 1fbcaa8a4d..dab5e5a64c 100644 --- a/crates/defguard_core/src/enterprise/license/tests.rs +++ b/crates/defguard_core/src/enterprise/license/tests.rs @@ -4,6 +4,7 @@ use defguard_common::db::{ setup_pool, }; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use tokio::sync::mpsc; use super::*; @@ -350,8 +351,7 @@ async fn test_trim_gateways_and_edges(_: PgPoolOptions, options: PgConnectOption .await .unwrap(); - let (proxy_control_tx, mut proxy_control_rx) = - tokio::sync::mpsc::channel::(8); + let (proxy_control_tx, mut proxy_control_rx) = mpsc::channel::(8); trim_gateways_and_edges(&pool, &proxy_control_tx) .await diff --git a/crates/defguard_core/src/grpc/proxy/client_mfa.rs b/crates/defguard_core/src/grpc/proxy/client_mfa.rs index d2e7defd4f..6cad3f68ef 100644 --- a/crates/defguard_core/src/grpc/proxy/client_mfa.rs +++ b/crates/defguard_core/src/grpc/proxy/client_mfa.rs @@ -42,7 +42,7 @@ use defguard_proto::{ }; use sqlx::{PgConnection, PgPool, Postgres, pool::PoolConnection}; use tokio::{ - sync::{broadcast::Sender, mpsc::UnboundedSender, oneshot}, + sync::{mpsc::UnboundedSender, oneshot}, time, }; use tonic::{Code, Status}; @@ -445,7 +445,7 @@ impl ClientMfaServer { #[must_use] pub fn new( pool: PgPool, - gateway_tx: Sender, + gateway_tx: UnboundedSender, bidi_event_tx: UnboundedSender, remote_mfa_responses: RemoteAuthWaiters, ) -> Self { diff --git a/crates/defguard_core/src/grpc/proxy/client_mfa/tests.rs b/crates/defguard_core/src/grpc/proxy/client_mfa/tests.rs index 1aed590af1..0fc1e89de9 100644 --- a/crates/defguard_core/src/grpc/proxy/client_mfa/tests.rs +++ b/crates/defguard_core/src/grpc/proxy/client_mfa/tests.rs @@ -44,7 +44,10 @@ use sqlx::{ PgPool, postgres::{PgConnectOptions, PgPoolOptions}, }; -use tokio::sync::{broadcast, mpsc}; +use tokio::{ + sync::{mpsc, oneshot}, + time::{sleep, timeout}, +}; use tonic::Code; use totp_lite::{Sha1, totp_custom}; @@ -1034,7 +1037,7 @@ async fn test_replacing_connected_mfa_session_emits_session_superseded_event( .await .expect("failed to create existing MFA session"); - let (gateway_tx, mut gateway_rx) = broadcast::channel(8); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel(); let (bidi_event_tx, mut event_rx) = mpsc::unbounded_channel(); let mut conn = pool.acquire().await.expect("failed to acquire connection"); @@ -1105,7 +1108,7 @@ async fn test_replacing_new_mfa_session_marks_session_disconnected_without_disco .await .expect("failed to create existing new MFA session"); - let (gateway_tx, mut gateway_rx) = broadcast::channel(8); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel(); let (bidi_event_tx, mut event_rx) = mpsc::unbounded_channel(); let mut conn = pool.acquire().await.expect("failed to acquire connection"); @@ -1135,7 +1138,7 @@ async fn test_replacing_new_mfa_session_marks_session_disconnected_without_disco assert!(matches!( event_rx.try_recv(), - Err(tokio::sync::mpsc::error::TryRecvError::Empty) + Err(mpsc::error::TryRecvError::Empty) )); let old_session = VpnClientSession::find_by_id(&pool, old_session.id) @@ -1149,10 +1152,10 @@ fn make_server( pool: PgPool, ) -> ( ClientMfaServer, - tokio::sync::mpsc::UnboundedReceiver, - tokio::sync::broadcast::Receiver, + mpsc::UnboundedReceiver, + mpsc::UnboundedReceiver, ) { - let (gateway_tx, gateway_rx) = broadcast::channel(8); + let (gateway_tx, gateway_rx) = mpsc::unbounded_channel(); let (bidi_event_tx, bidi_event_rx) = mpsc::unbounded_channel(); let remote_mfa_responses: RemoteAuthWaiters = Arc::default(); @@ -1234,7 +1237,7 @@ async fn test_create_new_mfa_session_disconnects_previous_active_session( .await .expect("failed to create previous active MFA session"); - let (gateway_tx, mut gateway_rx) = broadcast::channel(4); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel(); let (bidi_event_tx, _bidi_event_rx) = mpsc::unbounded_channel(); let mut conn = pool .acquire() @@ -1395,8 +1398,8 @@ async fn setup_mobile_mfa_flow_server( String, String, SigningKey, - tokio::sync::mpsc::UnboundedReceiver, - tokio::sync::broadcast::Receiver, + mpsc::UnboundedReceiver, + mpsc::UnboundedReceiver, ) { set_enterprise_license(); let pool = setup_pool(options).await; @@ -1435,8 +1438,8 @@ async fn setup_totp_mfa_server( Id, String, String, - tokio::sync::mpsc::UnboundedReceiver, - tokio::sync::broadcast::Receiver, + mpsc::UnboundedReceiver, + mpsc::UnboundedReceiver, ) { set_enterprise_license(); let pool = setup_pool(options).await; @@ -1504,7 +1507,7 @@ fn assert_superseded_response(response: Option, request_id: u64) { #[test] fn test_remote_waiters_are_scoped_by_contract_and_attempt() { let waiters: RemoteAuthWaiters = Arc::default(); - let (multi_signal_tx, _multi_signal_rx) = tokio::sync::oneshot::channel(); + let (multi_signal_tx, _multi_signal_rx) = oneshot::channel(); waiters.write().unwrap().insert( hash_token("multi-token"), super::RemoteAuthWaiter { @@ -1526,7 +1529,7 @@ fn test_remote_waiters_are_scoped_by_contract_and_attempt() { .is_some() ); - let (legacy_signal_tx, _legacy_signal_rx) = tokio::sync::oneshot::channel(); + let (legacy_signal_tx, _legacy_signal_rx) = oneshot::channel(); waiters.write().unwrap().insert( hash_token("legacy-token"), super::RemoteAuthWaiter { @@ -1549,7 +1552,7 @@ fn test_remove_remote_mfa_waiter_only_removes_matching_identity() { let waiters: RemoteAuthWaiters = Arc::default(); let stale_waiter_identity = Arc::new(()); let current_waiter_identity = Arc::new(()); - let (signal_tx, _signal_rx) = tokio::sync::oneshot::channel(); + let (signal_tx, _signal_rx) = oneshot::channel(); waiters.write().unwrap().insert( "test-hash".to_owned(), @@ -1604,7 +1607,7 @@ async fn test_duplicate_remote_mfa_park_supersedes_old_waiter_and_preserves_newe .await .expect("second waiter should replace the first"); - let first_response = tokio::time::timeout(Duration::from_secs(1), first_response_rx.recv()) + let first_response = timeout(Duration::from_secs(1), first_response_rx.recv()) .await .expect("old waiter should finish after replacement"); let newer_waiter_registered = server @@ -1618,7 +1621,7 @@ async fn test_duplicate_remote_mfa_park_supersedes_old_waiter_and_preserves_newe .write() .expect("failed to write remote MFA waiters") .remove(&hash); - let second_response = tokio::time::timeout(Duration::from_secs(1), second_response_rx.recv()) + let second_response = timeout(Duration::from_secs(1), second_response_rx.recv()) .await .expect("newer waiter cleanup should finish"); @@ -1655,7 +1658,7 @@ async fn test_legacy_mfa_remote_timeout_sends_terminal_error( .await .expect("remote waiter should park"); - let response = tokio::time::timeout(Duration::from_secs(1), response_rx.recv()) + let response = timeout(Duration::from_secs(1), response_rx.recv()) .await .expect("timed-out legacy waiter should finish"); assert_core_error_response( @@ -1716,7 +1719,7 @@ async fn test_legacy_mfa_start_supersedes_parked_waiter_with_defined_result( .expect("replacement start should succeed"); assert!(!replacement.token.is_empty()); - let response = tokio::time::timeout(Duration::from_secs(1), response_rx.recv()) + let response = timeout(Duration::from_secs(1), response_rx.recv()) .await .expect("superseded waiter should finish after replacement start"); assert_superseded_response(response, 1); @@ -2052,7 +2055,7 @@ async fn test_mfa_flow_remote_wakes_matching_attempt_and_delivers_psk( .await .expect("matching approval should wake the remote waiter"); - let response = tokio::time::timeout(Duration::from_secs(2), response_rx.recv()) + let response = timeout(Duration::from_secs(2), response_rx.recv()) .await .expect("remote waiter should finish after approval") .expect("remote response sender should remain available"); @@ -2180,7 +2183,7 @@ async fn test_mfa_flow_remote_observes_approval_before_registration( ) .await .expect("remote waiter should observe the durable approval"); - let response = tokio::time::timeout(Duration::from_secs(2), response_rx.recv()) + let response = timeout(Duration::from_secs(2), response_rx.recv()) .await .expect("remote waiter should finish") .expect("remote response sender should remain available"); @@ -2246,7 +2249,7 @@ async fn test_mfa_flow_remote_timeout_cleans_its_waiter( .await .expect("remote waiter should park"); - tokio::time::timeout(Duration::from_secs(1), async { + timeout(Duration::from_secs(1), async { loop { if server .remote_mfa_responses @@ -2256,12 +2259,12 @@ async fn test_mfa_flow_remote_timeout_cleans_its_waiter( { break; } - tokio::time::sleep(Duration::from_millis(5)).await; + sleep(Duration::from_millis(5)).await; } }) .await .expect("timeout cleanup should remove the owning waiter"); - let response = tokio::time::timeout(Duration::from_secs(1), response_rx.recv()) + let response = timeout(Duration::from_secs(1), response_rx.recv()) .await .expect("timed-out remote waiter should send a response"); assert_core_error_response( @@ -2292,7 +2295,7 @@ async fn test_mfa_flow_remote_timeout_cleans_its_waiter( .remove(&hash_token(&token)) .expect("parked waiter should be registered"); drop(waiter); - let response = tokio::time::timeout(Duration::from_secs(1), response_rx.recv()) + let response = timeout(Duration::from_secs(1), response_rx.recv()) .await .expect("closed signal receiver should send a response"); assert_core_error_response( diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index c7b6086f59..d6b2c310f4 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -77,10 +77,7 @@ use semver::Version; use sqlx::PgPool; use tokio::{ spawn, - sync::{ - broadcast::Sender, - mpsc::{UnboundedReceiver, UnboundedSender}, - }, + sync::{broadcast, mpsc}, time::sleep, }; use tower_governor::{ @@ -299,18 +296,18 @@ fn openapi_route() -> Router { } pub fn build_webapp( - webhook_tx: UnboundedSender, - webhook_rx: UnboundedReceiver, - gateway_tx: Sender, - web_reload_tx: tokio::sync::broadcast::Sender<()>, + webhook_tx: mpsc::UnboundedSender, + webhook_rx: mpsc::UnboundedReceiver, + gateway_tx: mpsc::UnboundedSender, + web_reload_tx: broadcast::Sender<()>, worker_state: Arc>, pool: PgPool, key: Key, - event_tx: UnboundedSender, - ldap_tx: UnboundedSender, - dirsync_tx: UnboundedSender, + event_tx: mpsc::UnboundedSender, + ldap_tx: mpsc::UnboundedSender, + dirsync_tx: mpsc::UnboundedSender, incompatible_components: Arc>, - proxy_control_tx: tokio::sync::mpsc::Sender, + proxy_control_tx: mpsc::Sender, tls_active: Arc, server_config: &DefGuardConfig, ) -> Router { @@ -890,16 +887,16 @@ pub fn apply_security_layers(router: Router, tls_active: Arc) -> Rou #[instrument(skip_all)] pub async fn run_web_server( worker_state: Arc>, - webhook_tx: UnboundedSender, - webhook_rx: UnboundedReceiver, - gateway_tx: Sender, - web_reload_tx: tokio::sync::broadcast::Sender<()>, + webhook_tx: mpsc::UnboundedSender, + webhook_rx: mpsc::UnboundedReceiver, + gateway_tx: mpsc::UnboundedSender, + web_reload_tx: broadcast::Sender<()>, pool: PgPool, - event_tx: UnboundedSender, - ldap_tx: UnboundedSender, - dirsync_tx: UnboundedSender, + event_tx: mpsc::UnboundedSender, + ldap_tx: mpsc::UnboundedSender, + dirsync_tx: mpsc::UnboundedSender, incompatible_components: Arc>, - proxy_control_tx: tokio::sync::mpsc::Sender, + proxy_control_tx: mpsc::Sender, ) -> Result<(), anyhow::Error> { let settings = Settings::get_current_settings(); let key = Key::from(settings.secret_key_required()?.as_bytes()); @@ -983,12 +980,12 @@ pub async fn run_web_server( handle.graceful_shutdown(Some(Duration::from_secs(30))); let _ = server_task.await; } - Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { + Err(broadcast::error::RecvError::Lagged(_)) => { info!("Missed core web server reload signal, restarting listener"); handle.graceful_shutdown(Some(Duration::from_secs(30))); let _ = server_task.await; } - Err(tokio::sync::broadcast::error::RecvError::Closed) => { + Err(broadcast::error::RecvError::Closed) => { return Err(anyhow!("Core web reload channel closed unexpectedly")); } } diff --git a/crates/defguard_core/src/location_management/mod.rs b/crates/defguard_core/src/location_management/mod.rs index 642d29f3b1..31260f784b 100644 --- a/crates/defguard_core/src/location_management/mod.rs +++ b/crates/defguard_core/src/location_management/mod.rs @@ -16,7 +16,7 @@ use defguard_common::{ }; use sqlx::PgConnection; use thiserror::Error; -use tokio::sync::broadcast::Sender; +use tokio::sync::mpsc::UnboundedSender; use crate::{ device_access::join_device_to_all_networks, @@ -60,7 +60,7 @@ impl LocationManager { } /// Send all commands to Gateway. Use this method *after* database transaction is committed. - pub(crate) fn send(self, gateway_tx: &Sender) { + pub(crate) fn send(self, gateway_tx: &UnboundedSender) { send_multiple_gateway_commands(self.gateway_commands, gateway_tx); } } diff --git a/crates/defguard_core/src/mfa_engine/authorize.rs b/crates/defguard_core/src/mfa_engine/authorize.rs index 83c143c8b4..893ac1477c 100644 --- a/crates/defguard_core/src/mfa_engine/authorize.rs +++ b/crates/defguard_core/src/mfa_engine/authorize.rs @@ -12,10 +12,7 @@ use defguard_common::{ }; use sqlx::PgConnection; use thiserror::Error; -use tokio::sync::{ - broadcast::Sender, - mpsc::{UnboundedSender, error::SendError}, -}; +use tokio::sync::mpsc::{UnboundedSender, error::SendError}; use crate::events::{ BidiRequestContext, BidiStreamEvent, BidiStreamEventType, DesktopClientMfaEvent, @@ -35,7 +32,7 @@ pub enum AuthorizeError { #[error(transparent)] Event(#[from] ClientMfaServerError), #[error("gateway event channel error: {0}")] - Gateway(Box>), + Gateway(Box>), } pub enum SessionDisconnectReason { @@ -48,14 +45,14 @@ pub enum SessionDisconnectReason { /// The two outbound channels the MFA engine and the posture path push to. #[derive(Clone)] pub struct EventChannels { - pub gateway_tx: Sender, + pub gateway_tx: UnboundedSender, pub bidi_event_tx: UnboundedSender, } impl EventChannels { #[must_use] pub fn new( - gateway_tx: Sender, + gateway_tx: UnboundedSender, bidi_event_tx: UnboundedSender, ) -> Self { Self { diff --git a/crates/defguard_core/src/mfa_engine/mod.rs b/crates/defguard_core/src/mfa_engine/mod.rs index 20808c1613..b2e8886542 100644 --- a/crates/defguard_core/src/mfa_engine/mod.rs +++ b/crates/defguard_core/src/mfa_engine/mod.rs @@ -27,7 +27,7 @@ use defguard_common::db::{ wireguard_key::WireguardKey, }; use sqlx::{PgConnection, PgPool}; -use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; +use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, error, warn}; use crate::{ @@ -150,7 +150,7 @@ impl MfaEngine { #[must_use] pub fn new( pool: PgPool, - gateway_tx: Sender, + gateway_tx: UnboundedSender, bidi_event_tx: UnboundedSender, ) -> Self { Self { diff --git a/crates/defguard_core/src/mfa_engine/tests.rs b/crates/defguard_core/src/mfa_engine/tests.rs index 7a58d1728f..75e7972b36 100644 --- a/crates/defguard_core/src/mfa_engine/tests.rs +++ b/crates/defguard_core/src/mfa_engine/tests.rs @@ -39,7 +39,7 @@ use sqlx::{ PgPool, postgres::{PgConnectOptions, PgPoolOptions}, }; -use tokio::sync::{broadcast, mpsc}; +use tokio::sync::mpsc; use tonic::{Code, Status}; use totp_lite::{Sha1, totp_custom}; use uuid::Uuid; @@ -98,9 +98,9 @@ fn make_engine( ) -> ( MfaEngine, mpsc::UnboundedReceiver, - broadcast::Receiver, + mpsc::UnboundedReceiver, ) { - let (gateway_tx, gateway_rx) = broadcast::channel(8); + let (gateway_tx, gateway_rx) = mpsc::unbounded_channel(); let (bidi_event_tx, bidi_event_rx) = mpsc::unbounded_channel(); ( MfaEngine::new(pool, gateway_tx, bidi_event_tx), @@ -1401,7 +1401,7 @@ async fn start_fido2_test_attempt( ) -> ( MfaEngine, mpsc::UnboundedReceiver, - broadcast::Receiver, + mpsc::UnboundedReceiver, String, String, ) { diff --git a/crates/defguard_core/src/user_management.rs b/crates/defguard_core/src/user_management.rs index aa9a88d94b..6ef2e9f3c3 100644 --- a/crates/defguard_core/src/user_management.rs +++ b/crates/defguard_core/src/user_management.rs @@ -9,7 +9,7 @@ use defguard_common::db::{ }; use sqlx::PgConnection; use thiserror::Error; -use tokio::sync::broadcast::Sender; +use tokio::sync::mpsc::UnboundedSender; use crate::{ enterprise::{ @@ -152,7 +152,7 @@ impl UserManager { } /// Send all commands to Gateway. Use this method *after* database transaction is committed. - pub fn send(self, gateway_tx: &Sender) { + pub fn send(self, gateway_tx: &UnboundedSender) { send_multiple_gateway_commands(self.gateway_commands, gateway_tx); } } diff --git a/crates/defguard_core/src/utility_thread.rs b/crates/defguard_core/src/utility_thread.rs index a64674809d..e783da86a9 100644 --- a/crates/defguard_core/src/utility_thread.rs +++ b/crates/defguard_core/src/utility_thread.rs @@ -49,7 +49,7 @@ const ACL_EXPIRY_SYSTEM_ACTOR: &str = "system:acl-expiry"; #[instrument(skip_all)] pub async fn run_utility_thread( pool: &PgPool, - gateway_tx: broadcast::Sender, + gateway_tx: mpsc::UnboundedSender, proxy_control_tx: mpsc::Sender, web_reload_tx: broadcast::Sender<()>, ldap_tx: mpsc::UnboundedSender, @@ -241,7 +241,7 @@ impl LicenseGates { async fn license_status_check( pool: &PgPool, - gateway_tx: broadcast::Sender, + gateway_tx: mpsc::UnboundedSender, ) -> Result<(), anyhow::Error> { let mut conn = pool.acquire().await?; for location in WireguardNetwork::all(pool).await? { @@ -286,7 +286,7 @@ async fn license_status_check( /// Find newly expired ACL rules and update their status. async fn expired_acl_rules_check( pool: &PgPool, - gateway_tx: broadcast::Sender, + gateway_tx: mpsc::UnboundedSender, ) -> Result<(), anyhow::Error> { // mark relevant rules as expired let updated_rules = query_as!( @@ -573,7 +573,7 @@ mod test { /// Reconcile gateway state and return the peer list for `location_id`. async fn reconcile_peers(pool: &PgPool, location_id: Id) -> Vec { - let (gateway_tx, mut gateway_rx) = broadcast::channel(16); + let (gateway_tx, mut gateway_rx) = mpsc::unbounded_channel(); license_status_check(pool, gateway_tx).await.unwrap(); while let Ok(command) = gateway_rx.try_recv() { diff --git a/crates/defguard_core/tests/integration/api/common/client.rs b/crates/defguard_core/tests/integration/api/common/client.rs index 0ee201b6d6..12e783dd23 100644 --- a/crates/defguard_core/tests/integration/api/common/client.rs +++ b/crates/defguard_core/tests/integration/api/common/client.rs @@ -213,11 +213,11 @@ impl TestClient { loop { match self.api_event_rx.try_recv() { Ok(msg) => all_events.push((*msg.event, msg.context.user_id, msg.context.username)), - Err(tokio::sync::mpsc::error::TryRecvError::Empty) => { + Err(TryRecvError::Empty) => { // No more messages available right now break; } - Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => { + Err(TryRecvError::Disconnected) => { // Channel is closed break; } diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index 2a4edc3cb2..ba2ad13435 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -37,10 +37,7 @@ use serde_json::{Value, json}; use sqlx::PgPool; use tokio::{ net::TcpListener, - sync::{ - broadcast::{self, Receiver}, - mpsc::{self, channel, unbounded_channel}, - }, + sync::{broadcast, mpsc}, }; use self::client::TestClient; @@ -59,7 +56,7 @@ pub const X_FORWARDED_URI: HeaderName = HeaderName::from_static("x-forwarded-uri pub(crate) struct ClientState { pub pool: PgPool, pub worker_state: Arc>, - pub gateway_rx: Receiver, + pub gateway_rx: mpsc::UnboundedReceiver, pub proxy_control_rx: mpsc::Receiver, pub test_user: User, #[allow(dead_code)] @@ -70,7 +67,7 @@ impl ClientState { pub fn new( pool: PgPool, worker_state: Arc>, - gateway_rx: Receiver, + gateway_rx: mpsc::UnboundedReceiver, proxy_control_rx: mpsc::Receiver, test_user: User, config: DefGuardConfig, @@ -91,10 +88,10 @@ pub(crate) async fn make_base_client( config: DefGuardConfig, listener: TcpListener, ) -> (TestClient, ClientState) { - let (api_event_tx, api_event_rx) = unbounded_channel::(); - let (tx, rx) = unbounded_channel::(); + let (api_event_tx, api_event_rx) = mpsc::unbounded_channel::(); + let (tx, rx) = mpsc::unbounded_channel::(); let worker_state = Arc::new(Mutex::new(WorkerState::new(tx.clone()))); - let (gateway_tx, gateway_rx) = broadcast::channel::(16); + let (gateway_tx, gateway_rx) = mpsc::unbounded_channel::(); let license = License::new( "test_customer".to_owned(), @@ -110,7 +107,7 @@ pub(crate) async fn make_base_client( set_cached_license(Some(license)); - let (proxy_control_tx, proxy_control_rx) = channel(10); + let (proxy_control_tx, proxy_control_rx) = mpsc::channel(8); let client_state = ClientState::new( pool.clone(), @@ -142,8 +139,8 @@ pub(crate) async fn make_base_client( .as_bytes(), ); let (web_reload_tx, _web_reload_rx) = broadcast::channel::<()>(8); - let (ldap_tx, _ldap_rx) = tokio::sync::mpsc::unbounded_channel(); - let (dirsync_tx, _dirsync_rx) = tokio::sync::mpsc::unbounded_channel(); + let (ldap_tx, _ldap_rx) = mpsc::unbounded_channel(); + let (dirsync_tx, _dirsync_rx) = mpsc::unbounded_channel(); let tls_active = Arc::new(AtomicBool::new(false)); let webapp = build_webapp( diff --git a/crates/defguard_core/tests/integration/api/component_setup.rs b/crates/defguard_core/tests/integration/api/component_setup.rs index 50dfa9fe2c..ae5834b785 100644 --- a/crates/defguard_core/tests/integration/api/component_setup.rs +++ b/crates/defguard_core/tests/integration/api/component_setup.rs @@ -18,7 +18,12 @@ use sqlx::{ PgPool, postgres::{PgConnectOptions, PgPoolOptions}, }; -use tokio::{net::TcpListener, sync::Notify, task::JoinHandle, time::timeout}; +use tokio::{ + net::TcpListener, + sync::{Notify, mpsc}, + task::JoinHandle, + time::timeout, +}; use tokio_stream::wrappers::UnboundedReceiverStream; use tonic::{Request, Response, Status, transport::Server}; use tracing::{debug, info}; @@ -226,7 +231,7 @@ impl GatewaySetup for MockGatewaySetupService { type StartStream = UnboundedReceiverStream>; async fn start(&self, _request: Request<()>) -> Result, Status> { - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let (tx, rx) = mpsc::unbounded_channel(); tokio::spawn(async move { for i in 0..3u32 { diff --git a/crates/defguard_core/tests/integration/api/device_posture.rs b/crates/defguard_core/tests/integration/api/device_posture.rs index d0a0919c7b..5d72133e75 100644 --- a/crates/defguard_core/tests/integration/api/device_posture.rs +++ b/crates/defguard_core/tests/integration/api/device_posture.rs @@ -18,6 +18,7 @@ use defguard_core::{ use reqwest::StatusCode; use serde_json::{Value, json}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use tokio::sync::mpsc; use super::{ PaginatedApiResponse, @@ -48,12 +49,12 @@ async fn setup(options: PgConnectOptions) -> (TestClient, ClientState) { (client, state) } -fn drain_gateway_events(gateway_rx: &mut tokio::sync::broadcast::Receiver) { +fn drain_gateway_events(gateway_rx: &mut mpsc::UnboundedReceiver) { while gateway_rx.try_recv().is_ok() {} } fn expect_network_modified_peers( - gateway_rx: &mut tokio::sync::broadcast::Receiver, + gateway_rx: &mut mpsc::UnboundedReceiver, location_id: Id, expected_pubkeys: &[&str], ) { diff --git a/crates/defguard_core/tests/integration/api/proxy_certs.rs b/crates/defguard_core/tests/integration/api/proxy_certs.rs index 353bd7e0a6..396e68afe0 100644 --- a/crates/defguard_core/tests/integration/api/proxy_certs.rs +++ b/crates/defguard_core/tests/integration/api/proxy_certs.rs @@ -40,10 +40,7 @@ use sqlx::{ }; use tokio::{ net::TcpListener, - sync::{ - broadcast, - mpsc::{Receiver, Sender, channel, unbounded_channel}, - }, + sync::{broadcast, mpsc}, time::{sleep, timeout}, }; @@ -55,7 +52,7 @@ const BROADCAST_TIMEOUT: Duration = Duration::from_secs(5); // Mock: captures messages sent to the proxy manager channel. struct ProxyBroadcastCapture { - rx: Receiver, + rx: mpsc::Receiver, } impl ProxyBroadcastCapture { @@ -135,14 +132,14 @@ async fn make_test_client_with_proxy_rx( // Use a channel large enough that sends never block in tests. let (proxy_control_tx, proxy_control_rx): ( - Sender, - Receiver, - ) = channel(32); + mpsc::Sender, + mpsc::Receiver, + ) = mpsc::channel(32); - let (api_event_tx, api_event_rx) = unbounded_channel::(); - let (tx, rx) = unbounded_channel::(); + let (api_event_tx, api_event_rx) = mpsc::unbounded_channel::(); + let (tx, rx) = mpsc::unbounded_channel::(); let worker_state = Arc::new(Mutex::new(WorkerState::new(tx.clone()))); - let (gateway_tx, _wg_rx) = broadcast::channel::(16); + let (gateway_tx, _wg_rx) = mpsc::unbounded_channel::(); let license = License::new( "test_customer".to_owned(), @@ -163,8 +160,8 @@ async fn make_test_client_with_proxy_rx( .as_bytes(), ); let (web_reload_tx, _web_reload_rx) = broadcast::channel::<()>(8); - let (ldap_tx, _ldap_rx) = tokio::sync::mpsc::unbounded_channel(); - let (dirsync_tx, _dirsync_rx) = tokio::sync::mpsc::unbounded_channel(); + let (ldap_tx, _ldap_rx) = mpsc::unbounded_channel(); + let (dirsync_tx, _dirsync_rx) = mpsc::unbounded_channel(); let webapp = build_webapp( tx, diff --git a/crates/defguard_core/tests/integration/api/wireguard.rs b/crates/defguard_core/tests/integration/api/wireguard.rs index 716a37906c..b576f78550 100644 --- a/crates/defguard_core/tests/integration/api/wireguard.rs +++ b/crates/defguard_core/tests/integration/api/wireguard.rs @@ -29,6 +29,7 @@ use ipnetwork::IpNetwork; use reqwest::StatusCode; use serde_json::{Value, json}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use tokio::sync::mpsc; use super::common::{ authenticate_admin, @@ -553,7 +554,7 @@ async fn test_create_network_with_posture_checks_requires_enterprise_license( client.assert_event_queue_is_empty(); assert_matches!( client_state.gateway_rx.try_recv(), - Err(tokio::sync::broadcast::error::TryRecvError::Empty) + Err(mpsc::error::TryRecvError::Empty) ); let response = client.get("/api/v1/network").send().await; @@ -881,10 +882,7 @@ async fn test_modify_network_does_not_notify_gateway_when_commit_fails( .send() .await; assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); - assert_matches!( - gateway_rx.try_recv(), - Err(tokio::sync::broadcast::error::TryRecvError::Empty) - ); + assert_matches!(gateway_rx.try_recv(), Err(mpsc::error::TryRecvError::Empty)); let response = client .get(format!("/api/v1/network/{}", location.id)) diff --git a/crates/defguard_core/tests/integration/api/wireguard_network_import.rs b/crates/defguard_core/tests/integration/api/wireguard_network_import.rs index 9f6221f03e..d8a2cfcd87 100644 --- a/crates/defguard_core/tests/integration/api/wireguard_network_import.rs +++ b/crates/defguard_core/tests/integration/api/wireguard_network_import.rs @@ -11,7 +11,7 @@ use defguard_core::{ use reqwest::StatusCode; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use tokio::sync::broadcast::error::TryRecvError; +use tokio::sync::mpsc::error::TryRecvError; use super::common::{fetch_user_details, make_test_client, setup_pool}; diff --git a/crates/defguard_core/tests/integration/ldap/mod.rs b/crates/defguard_core/tests/integration/ldap/mod.rs index e41e30cbfe..d975dd9388 100644 --- a/crates/defguard_core/tests/integration/ldap/mod.rs +++ b/crates/defguard_core/tests/integration/ldap/mod.rs @@ -25,20 +25,20 @@ use sqlx::{ PgPool, postgres::{PgConnectOptions, PgPoolOptions}, }; -use tokio::sync::{ - broadcast::{Receiver, Sender, channel}, - mpsc, -}; +use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender, unbounded_channel}; -fn wg_test_channel() -> (Sender, Receiver) { - channel(256) +fn wg_test_channel() -> ( + UnboundedSender, + UnboundedReceiver, +) { + unbounded_channel() } async fn sync_ldap( ldap_conn: &mut LDAPConnection, pool: &PgPool, full: bool, - wg_tx: &Sender, + wg_tx: &UnboundedSender, ) { let (ldap_tx, _ldap_rx) = mpsc::unbounded_channel::(); ldap_conn.sync(pool, full, wg_tx, &ldap_tx).await.unwrap(); diff --git a/crates/defguard_event_logger/src/lib.rs b/crates/defguard_event_logger/src/lib.rs index 208eb10f8b..0eb910a2be 100644 --- a/crates/defguard_event_logger/src/lib.rs +++ b/crates/defguard_event_logger/src/lib.rs @@ -38,7 +38,7 @@ use description::{get_api_event_description, get_enrollment_event_description}; use error::EventLoggerError; use message::{Event, EventContext, EventLoggerMessage}; use sqlx::PgPool; -use tokio::sync::{Notify, mpsc::UnboundedReceiver}; +use tokio::sync::{Notify, broadcast, mpsc}; use tracing::{debug, error, info, trace}; pub mod description; @@ -51,18 +51,17 @@ const MESSAGE_LIMIT: usize = 100; #[allow(clippy::too_many_arguments)] pub async fn run_event_logger( pool: PgPool, - api_event_rx: UnboundedReceiver, - bidi_event_rx: UnboundedReceiver, - session_manager_event_rx: UnboundedReceiver, - ldap_sync_event_rx: UnboundedReceiver, - directory_sync_event_rx: UnboundedReceiver, - gateway_connection_event_rx: UnboundedReceiver, - proxy_connection_event_rx: UnboundedReceiver, + api_event_rx: mpsc::UnboundedReceiver, + bidi_event_rx: mpsc::UnboundedReceiver, + session_manager_event_rx: mpsc::UnboundedReceiver, + ldap_sync_event_rx: mpsc::UnboundedReceiver, + directory_sync_event_rx: mpsc::UnboundedReceiver, + gateway_connection_event_rx: mpsc::UnboundedReceiver, + proxy_connection_event_rx: mpsc::UnboundedReceiver, activity_log_stream_reload_notify: Arc, - activity_log_messages_tx: tokio::sync::broadcast::Sender, + activity_log_messages_tx: broadcast::Sender, ) -> Result<(), EventLoggerError> { - let (event_logger_tx, mut event_logger_rx) = - tokio::sync::mpsc::unbounded_channel::(); + let (event_logger_tx, mut event_logger_rx) = mpsc::unbounded_channel::(); // Spawn a task that reads from all source channels and forwards // translated messages to the internal channel. @@ -105,15 +104,15 @@ pub async fn run_event_logger( /// is dropped, causing the batch loop to shut down gracefully. #[allow(clippy::too_many_arguments)] async fn translate_and_forward( - mut api_event_rx: UnboundedReceiver, - mut bidi_event_rx: UnboundedReceiver, - mut session_manager_event_rx: UnboundedReceiver, - mut ldap_sync_event_rx: UnboundedReceiver, - mut directory_sync_event_rx: UnboundedReceiver, - mut gateway_connection_event_rx: UnboundedReceiver, - mut proxy_connection_event_rx: UnboundedReceiver, + mut api_event_rx: mpsc::UnboundedReceiver, + mut bidi_event_rx: mpsc::UnboundedReceiver, + mut session_manager_event_rx: mpsc::UnboundedReceiver, + mut ldap_sync_event_rx: mpsc::UnboundedReceiver, + mut directory_sync_event_rx: mpsc::UnboundedReceiver, + mut gateway_connection_event_rx: mpsc::UnboundedReceiver, + mut proxy_connection_event_rx: mpsc::UnboundedReceiver, reload_notify: Arc, - event_logger_tx: tokio::sync::mpsc::UnboundedSender, + event_logger_tx: mpsc::UnboundedSender, ) { loop { let message = tokio::select! { @@ -1239,7 +1238,7 @@ fn map_to_activity_log_event(message: EventLoggerMessage) -> ActivityLogEvent, - activity_log_messages_tx: &tokio::sync::broadcast::Sender, + activity_log_messages_tx: &broadcast::Sender, ) -> Result<(), EventLoggerError> { let mut transaction = pool.begin().await?; let mut serialized_activity_log_events = String::new(); diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index 7894411637..baf117c457 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -17,7 +17,6 @@ use defguard_common::{ Id, models::{Certificates, Settings, WireguardNetwork, gateway::Gateway}, }, - gateway_event::GatewayCommand, messages::peer_stats_update::PeerStatsUpdate, }; use defguard_core::{ @@ -37,7 +36,6 @@ use semver::Version; use sqlx::PgPool; use tokio::{ sync::{ - broadcast::Sender, mpsc::{self, UnboundedSender}, watch, }, @@ -49,7 +47,9 @@ use tonic::transport::{Channel, Endpoint}; #[cfg(test)] use crate::GatewayManagerTestSupport; -use crate::{Client, TEN_SECS, error::GatewayError, updates::GatewayUpdatesHandler}; +use crate::{ + Client, GatewayEventRouter, TEN_SECS, error::GatewayError, updates::GatewayUpdatesHandler, +}; #[cfg(test)] #[derive(Default)] @@ -77,7 +77,7 @@ pub(crate) struct GatewayHandler { gateway: Gateway, message_id: AtomicU64, pool: PgPool, - events_tx: Sender, + event_router: GatewayEventRouter, connection_events_tx: UnboundedSender, peer_stats_tx: UnboundedSender, certs_rx: watch::Receiver>>, @@ -98,7 +98,7 @@ impl GatewayHandler { pub fn new( gateway: Gateway, pool: PgPool, - events_tx: Sender, + event_router: GatewayEventRouter, connection_events_tx: UnboundedSender, peer_stats_tx: UnboundedSender, certs_rx: watch::Receiver>>, @@ -116,7 +116,7 @@ impl GatewayHandler { gateway, message_id: AtomicU64::new(0), pool, - events_tx, + event_router, connection_events_tx, peer_stats_tx, certs_rx, @@ -576,17 +576,19 @@ impl GatewayHandler { info!("Sent configuration to {}", self.gateway); config_sent = true; self.mark_connected_and_maybe_notify(&network.name).await; + let (events_tx, events_rx) = mpsc::unbounded_channel(); let mut updates_handler = GatewayUpdatesHandler::new( self.gateway.location_id, network, self.gateway.name.clone(), Some(self.pool.clone()), - self.events_tx.subscribe(), + events_rx, tx.clone(), ); let handle = tokio::spawn(async move { updates_handler.run().await; }); + self.event_router.register(self.gateway.id, events_tx); self.updates_handler_handle = Some(handle); } Err(err) => { @@ -694,7 +696,7 @@ impl GatewayHandler { pub(crate) fn new_with_test_socket( gateway: Gateway, pool: PgPool, - events_tx: Sender, + event_router: GatewayEventRouter, connection_events_tx: UnboundedSender, peer_stats_tx: UnboundedSender, certs_rx: watch::Receiver>>, @@ -704,7 +706,7 @@ impl GatewayHandler { let mut handler = Self::new( gateway, pool, - events_tx, + event_router, connection_events_tx, peer_stats_tx, certs_rx, diff --git a/crates/defguard_gateway_manager/src/handler/tests.rs b/crates/defguard_gateway_manager/src/handler/tests.rs index b36b8f9130..eaada67c38 100644 --- a/crates/defguard_gateway_manager/src/handler/tests.rs +++ b/crates/defguard_gateway_manager/src/handler/tests.rs @@ -18,15 +18,14 @@ use defguard_common::{ }, setup_pool, }, - gateway_event::GatewayCommand, gateway_types::WireguardPeer, }; use defguard_proto::gateway::{Configuration, PeerStats, core_response}; use prost_types::Timestamp; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use tokio::sync::{broadcast, mpsc::unbounded_channel, watch}; +use tokio::sync::{mpsc::unbounded_channel, watch}; -use crate::updates::GatewayUpdatesHandler; +use crate::{GatewayEventRouter, updates::GatewayUpdatesHandler}; use super::{GatewayHandler, try_protos_into_stats_message}; @@ -210,7 +209,7 @@ fn gen_config_preserves_absent_firewall_config_and_empty_peers() { fn test_handler(mfa_enabled: bool) -> GatewayUpdatesHandler { let network = test_network(mfa_enabled); - let (events_tx, events_rx) = broadcast::channel(1); + let (events_tx, events_rx) = unbounded_channel(); let (tx, _rx) = unbounded_channel(); drop(events_tx); @@ -377,14 +376,13 @@ async fn test_send_configuration_includes_mfa_peers_with_session_preshared_key( .save(&pool) .await .unwrap(); - let (events_tx, _events_rx) = broadcast::channel::(1); let (connection_events_tx, _connection_events_rx) = unbounded_channel(); let (peer_stats_tx, _peer_stats_rx) = unbounded_channel(); let (_certs_tx, certs_rx) = watch::channel(Arc::new(HashMap::::new())); let handler = GatewayHandler::new( gateway, pool.clone(), - events_tx, + GatewayEventRouter::default(), connection_events_tx, peer_stats_tx, certs_rx, diff --git a/crates/defguard_gateway_manager/src/lib.rs b/crates/defguard_gateway_manager/src/lib.rs index 55752b6167..7609b02ae2 100644 --- a/crates/defguard_gateway_manager/src/lib.rs +++ b/crates/defguard_gateway_manager/src/lib.rs @@ -19,8 +19,7 @@ use sqlx::{PgPool, postgres::PgListener}; use tokio::sync::Notify; use tokio::{ sync::{ - broadcast::Sender, - mpsc::{UnboundedSender, unbounded_channel}, + mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, watch::Receiver, }, task::{AbortHandle, JoinHandle, JoinSet}, @@ -299,24 +298,38 @@ pub struct GatewayManager { #[cfg(test)] test_support: GatewayManagerTestSupport, tx: GatewayTxSet, + /// Taken by [`Self::run`]. + events_rx: Option>, + event_router: GatewayEventRouter, } impl GatewayManager { #[cfg(not(test))] #[must_use] - pub fn new(pool: PgPool, tx: GatewayTxSet) -> Self { + pub fn new( + pool: PgPool, + tx: GatewayTxSet, + events_rx: UnboundedReceiver, + ) -> Self { Self { clients: Arc::default(), handlers: JoinSet::new(), pool, disconnect_notification_sent_by_gateway: Arc::default(), tx, + events_rx: Some(events_rx), + event_router: GatewayEventRouter::default(), } } #[cfg(test)] #[must_use] - fn new(pool: PgPool, tx: GatewayTxSet, test_support: GatewayManagerTestSupport) -> Self { + fn new( + pool: PgPool, + tx: GatewayTxSet, + events_rx: UnboundedReceiver, + test_support: GatewayManagerTestSupport, + ) -> Self { Self { clients: Arc::default(), handlers: JoinSet::new(), @@ -324,6 +337,8 @@ impl GatewayManager { disconnect_notification_sent_by_gateway: Arc::default(), test_support, tx, + events_rx: Some(events_rx), + event_router: GatewayEventRouter::default(), } } @@ -384,7 +399,7 @@ impl GatewayManager { GatewayHandler::new_with_test_socket( gateway, self.pool.clone(), - self.tx.events.clone(), + self.event_router.clone(), self.tx.connection_events.clone(), self.tx.peer_stats.clone(), certs_rx, @@ -395,7 +410,7 @@ impl GatewayManager { GatewayHandler::new( gateway, self.pool.clone(), - self.tx.events.clone(), + self.event_router.clone(), self.tx.connection_events.clone(), self.tx.peer_stats.clone(), certs_rx, @@ -411,7 +426,7 @@ impl GatewayManager { GatewayHandler::new( gateway, self.pool.clone(), - self.tx.events.clone(), + self.event_router.clone(), self.tx.connection_events.clone(), self.tx.peer_stats.clone(), certs_rx, @@ -430,6 +445,9 @@ impl GatewayManager { sleep(TEN_SECS).await; } })); + let _dispatch_events_task = self.events_rx.take().map(|events_rx| { + AbortTaskOnDrop::new(tokio::spawn(self.event_router.clone().run(events_rx))) + }); // Stores the abort handle and a snapshot of the gateway at the time the handler was last // started. The snapshot is used by the Update arm to detect connection-relevant changes. let mut abort_handles: HashMap)> = HashMap::new(); @@ -737,20 +755,15 @@ mod unit_tests { /// events, notifications, and side effects to Core components. #[derive(Clone)] pub struct GatewayTxSet { - events: Sender, peer_stats: UnboundedSender, connection_events: UnboundedSender, } impl GatewayTxSet { #[must_use] - pub fn new( - events: Sender, - peer_stats: UnboundedSender, - ) -> Self { + pub fn new(peer_stats: UnboundedSender) -> Self { let (connection_events, _receiver) = unbounded_channel(); Self { - events, peer_stats, connection_events, } @@ -765,3 +778,48 @@ impl GatewayTxSet { self } } + +/// Delivers Gateway events to all updates handlers. +#[derive(Clone, Default)] +pub(crate) struct GatewayEventRouter { + senders: Arc>>>, +} + +impl GatewayEventRouter { + /// Replaces any previously registered channel for this Gateway. + pub(crate) fn register(&self, gateway_id: Id, events_tx: UnboundedSender) { + self.senders + .lock() + .expect("Failed to lock GatewayEventRouter::senders") + .insert(gateway_id, events_tx); + } + + pub(crate) async fn run(self, mut events_rx: UnboundedReceiver) { + while let Some(event) = events_rx.recv().await { + self.dispatch(&event); + } + } + + /// Sends the event to all handlers, dropping closed channels. + fn dispatch(&self, event: &GatewayCommand) { + self.senders + .lock() + .expect("Failed to lock GatewayEventRouter::senders") + .retain(|gateway_id, events_tx| { + let sent = events_tx.send(event.clone()).is_ok(); + if !sent { + debug!("Updates handler for Gateway id={gateway_id} has stopped"); + } + sent + }); + } + + #[cfg(test)] + pub(crate) fn registered(&self, gateway_id: Id) -> Option> { + self.senders + .lock() + .expect("Failed to lock GatewayEventRouter::senders") + .get(&gateway_id) + .cloned() + } +} diff --git a/crates/defguard_gateway_manager/src/tests/common/mod.rs b/crates/defguard_gateway_manager/src/tests/common/mod.rs index d6e186d922..0c386a5602 100644 --- a/crates/defguard_gateway_manager/src/tests/common/mod.rs +++ b/crates/defguard_gateway_manager/src/tests/common/mod.rs @@ -35,7 +35,7 @@ use sqlx::{PgPool, postgres::PgConnectOptions}; use tokio::{ net::UnixListener, sync::{ - Notify, broadcast, + Notify, mpsc::{self, UnboundedReceiver, UnboundedSender}, oneshot, watch, }, @@ -45,7 +45,10 @@ use tokio::{ use tokio_stream::{once, wrappers::UnboundedReceiverStream}; use tonic::{Request, Response, Status, Streaming, transport::Server}; -use crate::{GatewayManager, GatewayManagerTestSupport, GatewayTxSet, handler::GatewayHandler}; +use crate::{ + GatewayEventRouter, GatewayManager, GatewayManagerTestSupport, GatewayTxSet, + handler::GatewayHandler, +}; const TEST_TIMEOUT: Duration = Duration::from_secs(10); pub(crate) const FAST_RETRY_DELAY: Duration = Duration::from_millis(20); @@ -474,10 +477,11 @@ impl ManagerTestContext { self.set_retry_delay(FAST_RETRY_DELAY); - let (events_tx, _) = broadcast::channel(16); + let (_events_tx, events_rx) = mpsc::unbounded_channel(); let (peer_stats_tx, _peer_stats_rx) = mpsc::unbounded_channel(); - let tx = GatewayTxSet::new(events_tx, peer_stats_tx); - let mut manager = GatewayManager::new(self.pool.clone(), tx, self.control.clone()); + let tx = GatewayTxSet::new(peer_stats_tx); + let mut manager = + GatewayManager::new(self.pool.clone(), tx, events_rx, self.control.clone()); let manager_task = tokio::spawn(async move { manager.run().await }); timeout(TEST_TIMEOUT, self.control.wait_until_listener_ready()) @@ -524,21 +528,25 @@ pub(crate) struct HandlerTestContext { pub(crate) gateway: Gateway, pub(crate) peer_stats_rx: UnboundedReceiver, pub(crate) connection_events_rx: UnboundedReceiver, - events_tx: Option>, + events_tx: Option>, + event_router: GatewayEventRouter, + dispatch_task: JoinHandle<()>, pub(crate) mock_gateway: Option, handler_task: Option>>, } impl HandlerTestContext { pub(crate) async fn new(options: PgConnectOptions) -> Self { - let (events_tx, _) = broadcast::channel(16); - Self::new_with_events_tx(options, events_tx).await + Self::new_with_event_router(options, GatewayEventRouter::default()).await } - pub(crate) async fn new_with_events_tx( + /// Contexts sharing a router receive each other's events. + pub(crate) async fn new_with_event_router( options: PgConnectOptions, - events_tx: broadcast::Sender, + event_router: GatewayEventRouter, ) -> Self { + let (events_tx, events_rx) = mpsc::unbounded_channel(); + let dispatch_task = tokio::spawn(event_router.clone().run(events_rx)); let pool = setup_pool(options).await; initialize_current_settings(&pool) .await @@ -552,7 +560,7 @@ impl HandlerTestContext { let mut handler = GatewayHandler::new_with_test_socket( gateway.clone(), pool.clone(), - events_tx.clone(), + event_router.clone(), connection_events_tx, peer_stats_tx, certs_rx, @@ -571,12 +579,18 @@ impl HandlerTestContext { peer_stats_rx, connection_events_rx, events_tx: Some(events_tx), + event_router, + dispatch_task, mock_gateway: Some(mock_gateway), handler_task: Some(handler_task), } } - pub(crate) fn events_tx(&self) -> &broadcast::Sender { + pub(crate) fn is_events_handler_registered(&self) -> bool { + self.event_router.registered(self.gateway.id).is_some() + } + + pub(crate) fn events_tx(&self) -> &UnboundedSender { self.events_tx .as_ref() .expect("events sender already taken from context") @@ -628,18 +642,26 @@ impl HandlerTestContext { } pub(crate) async fn complete_config_handshake(&mut self) -> Gateway { - let initial_event_receivers = self.events_tx().receiver_count(); + let initial_events_tx = self.event_router.registered(self.gateway.id); self.mock_gateway().send_config_request(); let _ = self.mock_gateway_mut().recv_outbound().await; let connected_gateway = wait_for_gateway_connection_state(&self.pool, self.gateway.id, true).await; timeout(TEST_TIMEOUT, async { - while self.events_tx().receiver_count() <= initial_event_receivers { + while self + .event_router + .registered(self.gateway.id) + .is_none_or(|events_tx| { + initial_events_tx + .as_ref() + .is_some_and(|initial| initial.same_channel(&events_tx)) + }) + { sleep(Duration::from_millis(20)).await; } }) .await - .expect("timed out waiting for gateway updates handler subscription"); + .expect("timed out waiting for gateway updates handler registration"); tokio::task::yield_now().await; connected_gateway } @@ -686,6 +708,7 @@ impl HandlerTestContext { impl Drop for HandlerTestContext { fn drop(&mut self) { + self.dispatch_task.abort(); if let Some(handler_task) = self.handler_task.take() { handler_task.abort(); } diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler.rs index 85e705797d..aeeb5044a7 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler.rs @@ -21,7 +21,10 @@ use self::support::{ create_device_info_for_current_network, enable_internal_mfa_for_network, enable_linux_posture_for_network, expected_keepalive_interval, panic_unexpected, parse_test_ip, }; -use crate::tests::common::{HandlerTestContext, build_peer_stats, reload_gateway}; +use crate::{ + GatewayEventRouter, + tests::common::{HandlerTestContext, build_peer_stats, reload_gateway}, +}; include!("handler/handshake.rs"); include!("handler/lifecycle.rs"); diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/device_events.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/device_events.rs index 6008540cf8..4ce809eb77 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/device_events.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/device_events.rs @@ -19,7 +19,7 @@ async fn test_device_created_for_network_produces_peer_create_update( context .events_tx() .send(GatewayCommand::DeviceCreated(device_info)), - "failed to broadcast created device event" + "failed to send created device event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; @@ -88,7 +88,7 @@ async fn test_device_modified_for_network_produces_peer_modify_update( context .events_tx() .send(GatewayCommand::DeviceModified(device_info)), - "failed to broadcast modified device event" + "failed to send modified device event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; @@ -140,7 +140,7 @@ async fn test_device_deleted_for_network_produces_peer_delete_update( context .events_tx() .send(GatewayCommand::DeviceDeleted(device_info)), - "failed to broadcast deleted device event" + "failed to send deleted device event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/firewall_events.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/firewall_events.rs index 608f92262c..923f8281a8 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/firewall_events.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/firewall_events.rs @@ -15,7 +15,7 @@ async fn test_matching_location_firewall_config_changed_event_produces_update( context.network.id, expected_firewall_config.clone(), )), - "failed to broadcast firewall config changed event" + "failed to send firewall config changed event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; @@ -38,7 +38,7 @@ async fn test_matching_location_firewall_disabled_event_produces_disable_update( context .events_tx() .send(GatewayCommand::FirewallDisabled(context.network.id)), - "failed to broadcast firewall disabled event" + "failed to send firewall disabled event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/mfa.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/mfa.rs index 9b17a67c63..6c4ce23040 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/mfa.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/mfa.rs @@ -23,7 +23,7 @@ async fn test_matching_location_posture_vpn_session_authorized_produces_peer_cre device, network_device, )), - "failed to broadcast posture VPN session authorized event" + "failed to send posture VPN session authorized event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; @@ -65,7 +65,7 @@ async fn test_matching_location_posture_vpn_session_authorized_without_psk_is_sk device, network_device, )), - "failed to broadcast posture VPN session authorized event without PSK" + "failed to send posture VPN session authorized event without PSK" ); context.mock_gateway_mut().expect_no_outbound().await; @@ -98,7 +98,7 @@ async fn test_matching_location_vpn_session_authorized_produces_peer_create( device, network_device, )), - "failed to broadcast VPN session authorized event" + "failed to send VPN session authorized event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; @@ -144,7 +144,7 @@ async fn test_vpn_session_authorized_with_mismatched_network_id_is_ignored( device, network_device, )), - "failed to broadcast mismatched VPN session authorized event" + "failed to send mismatched VPN session authorized event" ); context.mock_gateway_mut().expect_no_outbound().await; @@ -177,7 +177,7 @@ async fn test_matching_location_vpn_session_deauthorized_produces_peer_delete( context.network.id, device, )), - "failed to broadcast VPN session deauthorized event" + "failed to send VPN session deauthorized event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs index 1763191513..d0b5d21e7f 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs @@ -12,7 +12,7 @@ async fn test_matching_location_network_deleted_event_produces_delete_update( context.network.id, context.network.name.clone(), )), - "failed to broadcast gateway event" + "failed to send gateway event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; @@ -51,7 +51,7 @@ async fn test_matching_location_network_modified_event_produces_modify_update( Vec::new(), None, )), - "failed to broadcast modified gateway event" + "failed to send modified gateway event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; @@ -96,7 +96,7 @@ async fn test_matching_location_network_created_event_produces_create_update( context.network.id, created_network, )), - "failed to broadcast created gateway event" + "failed to send created gateway event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; @@ -118,10 +118,11 @@ async fn test_only_matching_handler_receives_network_modified_update( _: PgPoolOptions, options: PgConnectOptions, ) { - let (events_tx, _) = tokio::sync::broadcast::channel(16); + let event_router = GatewayEventRouter::default(); let mut matching_context = - HandlerTestContext::new_with_events_tx(options.clone(), events_tx.clone()).await; - let mut unrelated_context = HandlerTestContext::new_with_events_tx(options, events_tx).await; + HandlerTestContext::new_with_event_router(options.clone(), event_router.clone()).await; + let mut unrelated_context = + HandlerTestContext::new_with_event_router(options, event_router).await; assert_ne!(matching_context.network.id, unrelated_context.network.id); @@ -151,7 +152,7 @@ async fn test_only_matching_handler_receives_network_modified_update( Vec::new(), None, )), - "failed to broadcast modified gateway event" + "failed to send modified gateway event" ); let outbound = matching_context.mock_gateway_mut().recv_outbound().await; @@ -193,7 +194,7 @@ async fn test_different_location_network_created_event_is_ignored( other_network.id, other_network, )), - "failed to broadcast unrelated created gateway event" + "failed to send unrelated created gateway event" ); context.mock_gateway_mut().expect_no_outbound().await; @@ -216,7 +217,7 @@ async fn test_different_location_network_deleted_event_is_ignored( other_network.id, other_network.name.clone(), )), - "failed to broadcast unrelated gateway event" + "failed to send unrelated gateway event" ); context.mock_gateway_mut().expect_no_outbound().await; @@ -226,7 +227,7 @@ async fn test_different_location_network_deleted_event_is_ignored( context.network.id, context.network.name.clone(), )), - "failed to broadcast owned gateway event" + "failed to send owned gateway event" ); let outbound = context.mock_gateway_mut().recv_outbound().await; diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/support.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/support.rs index 96de0a302f..1071894a92 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/support.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/support.rs @@ -269,16 +269,15 @@ pub(crate) async fn assert_device_event_is_ignored_before_config_handshake( build_event: fn(DeviceInfo) -> GatewayCommand, ) { let mut context = HandlerTestContext::new(options).await; - assert_eq!(context.events_tx().receiver_count(), 0); + assert!(!context.is_events_handler_registered()); - let _broadcast_guard = context.events_tx().subscribe(); let device_info = create_device_info_for_current_network(&context, device_name, device_pubkey, device_ip) .await; assert_send_ok!( context.events_tx().send(build_event(device_info)), - "failed to broadcast ignored device event" + "failed to send ignored device event" ); context.mock_gateway_mut().expect_no_outbound().await; @@ -309,7 +308,7 @@ pub(crate) async fn assert_device_event_for_different_network_is_ignored( assert_send_ok!( context.events_tx().send(build_event(device_info)), - "failed to broadcast ignored device event" + "failed to send ignored device event" ); context.mock_gateway_mut().expect_no_outbound().await; @@ -329,7 +328,7 @@ pub(crate) async fn assert_firewall_event_for_different_network_is_ignored( assert_send_ok!( context.events_tx().send(build_event(other_network.id)), - "failed to broadcast ignored firewall event" + "failed to send ignored firewall event" ); context.mock_gateway_mut().expect_no_outbound().await; diff --git a/crates/defguard_gateway_manager/src/updates.rs b/crates/defguard_gateway_manager/src/updates.rs index 2841bf66da..37d723e123 100644 --- a/crates/defguard_gateway_manager/src/updates.rs +++ b/crates/defguard_gateway_manager/src/updates.rs @@ -13,7 +13,7 @@ use defguard_proto::{ gateway::{Configuration, CoreResponse, Peer, Update, UpdateType, core_response, update}, }; use sqlx::PgPool; -use tokio::sync::{broadcast, mpsc::UnboundedSender}; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tonic::{Code, Status}; /// Helper struct for handling gateway events. @@ -23,7 +23,7 @@ pub(crate) struct GatewayUpdatesHandler { gateway_name: String, pool: Option, pub(crate) session_authorization_required: bool, - events_rx: broadcast::Receiver, + events_rx: UnboundedReceiver, tx: UnboundedSender, } @@ -34,7 +34,7 @@ impl GatewayUpdatesHandler { network: WireguardNetwork, gateway_name: String, pool: Option, - events_rx: broadcast::Receiver, + events_rx: UnboundedReceiver, tx: UnboundedSender, ) -> Self { Self { @@ -144,15 +144,14 @@ impl GatewayUpdatesHandler { /// Process incoming Gateway events /// - /// Main gRPC server uses a shared channel for broadcasting all gateway events - /// so the handler must determine if an event is relevant for the network being serviced + /// Every handler receives all events, so it must skip those for other networks. pub(crate) async fn run(&mut self) { info!( "Starting update stream to gateway: {}, network {}", self.gateway_name, self.network ); self.refresh_session_authorization_required().await; - while let Ok(update) = self.events_rx.recv().await { + while let Some(update) = self.events_rx.recv().await { debug!("Received WireGuard update: {update:?}"); let result = match update { GatewayCommand::NetworkCreated(network_id, network) => { diff --git a/crates/defguard_proxy_manager/src/handler.rs b/crates/defguard_proxy_manager/src/handler.rs index 64a1e09670..f36266e059 100644 --- a/crates/defguard_proxy_manager/src/handler.rs +++ b/crates/defguard_proxy_manager/src/handler.rs @@ -72,7 +72,6 @@ use tokio::{ select, sync::{ Mutex, Semaphore, TryAcquireError, - broadcast::Sender, mpsc::{self, UnboundedSender}, watch, }, @@ -643,7 +642,7 @@ impl ProxyHandler { received: CoreRequest, response_tx: UnboundedSender, services: Arc, - gateway_tx: Sender, + gateway_tx: UnboundedSender, handler_tx_map: HandlerTxMap, ) -> Result<(), ProxyError> { let payload = match received.payload { @@ -1173,7 +1172,7 @@ impl ProxyHandler { async fn message_loop( &mut self, tx: UnboundedSender, - gateway_tx: Sender, + gateway_tx: UnboundedSender, resp_stream: &mut Streaming, ) -> Result<(), ProxyError> { let mut tasks = JoinSet::new(); @@ -1327,7 +1326,7 @@ impl ProxyHandler { async fn handle_auth_callback( pool: &PgPool, request: AuthCallbackRequest, - gateway_tx: &Sender, + gateway_tx: &UnboundedSender, services: &Arc, ) -> Result, ProxyError> { let payload = match Settings::get_current_settings() diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index 4931fc0bb8..0c602f577b 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -27,7 +27,6 @@ use tokio::{ select, sync::{ Mutex, Semaphore, - broadcast::Sender, mpsc::{Receiver, UnboundedSender, unbounded_channel}, oneshot, watch, }, @@ -428,7 +427,7 @@ impl ProxyManager { /// events, notifications, and side effects to Core components. #[derive(Clone)] pub struct ProxyTxSet { - wireguard: Sender, + wireguard: UnboundedSender, bidi_events: UnboundedSender, pub(crate) ldap: UnboundedSender, pub(crate) dirsync: UnboundedSender, @@ -439,7 +438,7 @@ pub struct ProxyTxSet { impl ProxyTxSet { #[must_use] pub fn new( - wireguard: Sender, + wireguard: UnboundedSender, bidi_events: UnboundedSender, ldap: UnboundedSender, dirsync: UnboundedSender, diff --git a/crates/defguard_proxy_manager/src/servers/enrollment.rs b/crates/defguard_proxy_manager/src/servers/enrollment.rs index e9fdebb5b9..8af5927e12 100644 --- a/crates/defguard_proxy_manager/src/servers/enrollment.rs +++ b/crates/defguard_proxy_manager/src/servers/enrollment.rs @@ -48,15 +48,12 @@ use defguard_proto::client_types::{ NewDevice, RegisterMobileAuthRequest, }; use sqlx::{PgConnection, PgPool, query_scalar}; -use tokio::sync::{ - broadcast::Sender, - mpsc::{UnboundedSender, error::SendError}, -}; +use tokio::sync::mpsc::{UnboundedSender, error::SendError}; use tonic::Status; pub(crate) struct EnrollmentServer { pool: PgPool, - gateway_tx: Sender, + gateway_tx: UnboundedSender, bidi_event_tx: UnboundedSender, ldap_tx: UnboundedSender, event_tx: UnboundedSender, @@ -66,7 +63,7 @@ impl EnrollmentServer { #[must_use] pub(crate) fn new( pool: PgPool, - gateway_tx: Sender, + gateway_tx: UnboundedSender, bidi_event_tx: UnboundedSender, ldap_tx: UnboundedSender, event_tx: UnboundedSender, @@ -1331,7 +1328,7 @@ mod test { use defguard_core::db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token}; use defguard_proto::{client_types::EnrollmentStartRequest, proxy::DeviceInfo}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - use tokio::sync::{broadcast, mpsc::unbounded_channel}; + use tokio::sync::mpsc::unbounded_channel; use super::EnrollmentServer; @@ -1369,7 +1366,7 @@ mod test { settings.enrollment_send_welcome_email = false; update_current_settings(&pool, settings).await.unwrap(); - let (gateway_tx, _) = broadcast::channel(1); + let (gateway_tx, _) = unbounded_channel(); let (bidi_event_tx, _) = unbounded_channel(); let (ldap_tx, _) = unbounded_channel(); let (event_tx, _) = unbounded_channel(); @@ -1426,7 +1423,7 @@ mod test { settings.enrollment_display_welcome_message = false; update_current_settings(&pool, settings).await.unwrap(); - let (gateway_tx, _gateway_rx) = broadcast::channel(1); + let (gateway_tx, _gateway_rx) = unbounded_channel(); let (bidi_event_tx, _bidi_events_rx) = unbounded_channel(); let (ldap_tx, _ldap_rx) = unbounded_channel(); let (event_tx, _event_rx) = unbounded_channel(); diff --git a/crates/defguard_proxy_manager/src/tests/common/mod.rs b/crates/defguard_proxy_manager/src/tests/common/mod.rs index a714d09de7..5558bc1363 100644 --- a/crates/defguard_proxy_manager/src/tests/common/mod.rs +++ b/crates/defguard_proxy_manager/src/tests/common/mod.rs @@ -42,12 +42,8 @@ use rsa::{RsaPrivateKey, pkcs8::EncodePrivateKey, traits::PublicKeyParts}; use sqlx::{PgPool, postgres::PgConnectOptions}; use tokio::{ net::{TcpListener, UnixListener}, - sync::{ - Notify, Semaphore, broadcast, - mpsc::{self, UnboundedReceiver, UnboundedSender}, - oneshot, watch, - }, - task::JoinHandle, + sync::{Notify, Semaphore, mpsc, oneshot, watch}, + task::{JoinHandle, yield_now}, time::{sleep, timeout}, }; use tokio_stream::{once, wrappers::UnboundedReceiverStream}; @@ -105,9 +101,9 @@ pub(crate) fn mock_proxy_socket_path() -> PathBuf { /// - sends `CoreRequest` messages back (injected via `inbound_tx`) struct MockProxyState { /// Messages sent by the handler (CoreResponse) forwarded here for assertions. - outbound_tx: UnboundedSender, + outbound_tx: mpsc::UnboundedSender, /// Receiver side for requests we want to inject into the handler (CoreRequest). - inbound_rx: Mutex>>>, + inbound_rx: Mutex>>>, /// One-shot notifier that fires once on the first connection. connected_tx: Mutex>>, connection_count: AtomicU16, @@ -131,7 +127,9 @@ impl MockProxyState { } } - fn take_inbound_rx(&self) -> Result>, Status> { + fn take_inbound_rx( + &self, + ) -> Result>, Status> { self.inbound_rx .lock() .expect("failed to lock inbound receiver") @@ -203,9 +201,9 @@ pub(crate) struct MockProxyHarness { state: Arc, socket_path: PathBuf, /// Sender to inject `CoreRequest` messages into the live stream. - inbound_tx: Option>>, + inbound_tx: Option>>, /// Receiver of `CoreResponse` messages forwarded from the handler. - outbound_rx: UnboundedReceiver, + outbound_rx: mpsc::UnboundedReceiver, connected_rx: oneshot::Receiver<()>, server_task: Option>>, } @@ -402,10 +400,10 @@ impl Drop for MockProxyHarness { pub(crate) struct HandlerTestContext { pub(crate) pool: PgPool, pub(crate) proxy: Proxy, - pub(crate) gateway_tx: broadcast::Sender, - pub(crate) bidi_events_rx: UnboundedReceiver, - pub(crate) event_rx: UnboundedReceiver, - pub(crate) connection_events_rx: UnboundedReceiver, + gateway_rx: mpsc::UnboundedReceiver, + pub(crate) bidi_events_rx: mpsc::UnboundedReceiver, + pub(crate) event_rx: mpsc::UnboundedReceiver, + pub(crate) connection_events_rx: mpsc::UnboundedReceiver, pub(crate) mock_proxy: Option, remote_mfa_responses: RemoteAuthWaiters, handler_task: Option>>, @@ -421,7 +419,7 @@ impl HandlerTestContext { pub(crate) async fn new_with_semaphore( options: PgConnectOptions, - semaphore: Arc, + semaphore: Arc, ) -> Self { let pool = setup_pool(options).await; initialize_current_settings(&pool) @@ -437,20 +435,14 @@ impl HandlerTestContext { let proxy = create_proxy(&pool).await; - let (gateway_tx, _) = broadcast::channel(16); + let (gateway_tx, gateway_rx) = mpsc::unbounded_channel(); let (bidi_events_tx, bidi_events_rx) = mpsc::unbounded_channel::(); let (ldap_tx, _ldap_rx) = mpsc::unbounded_channel(); let (dirsync_tx, _dirsync_rx) = mpsc::unbounded_channel(); let (event_tx, event_rx) = mpsc::unbounded_channel(); let (connection_events_tx, connection_events_rx) = mpsc::unbounded_channel(); - let tx_set = ProxyTxSet::new( - gateway_tx.clone(), - bidi_events_tx, - ldap_tx, - dirsync_tx, - event_tx, - ) - .with_connection_events(connection_events_tx); + let tx_set = ProxyTxSet::new(gateway_tx, bidi_events_tx, ldap_tx, dirsync_tx, event_tx) + .with_connection_events(connection_events_tx); let (_, certs_rx) = watch::channel(Arc::new(HashMap::new())); let incompatible_components = Arc::new(std::sync::RwLock::new( @@ -491,7 +483,7 @@ impl HandlerTestContext { Self { pool, proxy, - gateway_tx, + gateway_rx, bidi_events_rx, event_rx, connection_events_rx, @@ -502,6 +494,14 @@ impl HandlerTestContext { } } + /// Discards events received so far. + pub(crate) fn take_gateway_rx(&mut self) -> mpsc::UnboundedReceiver { + let (_, closed_rx) = mpsc::unbounded_channel(); + let mut gateway_rx = std::mem::replace(&mut self.gateway_rx, closed_rx); + while gateway_rx.try_recv().is_ok() {} + gateway_rx + } + pub(crate) fn mock_proxy(&self) -> &MockProxyHarness { self.mock_proxy .as_ref() @@ -545,7 +545,7 @@ impl HandlerTestContext { timeout(TEST_TIMEOUT, async { while Arc::strong_count(&self.remote_mfa_responses) > 1 { - tokio::task::yield_now().await; + yield_now().await; } }) .await @@ -554,7 +554,9 @@ impl HandlerTestContext { mock_proxy } - pub(crate) fn take_connection_events_rx(&mut self) -> UnboundedReceiver { + pub(crate) fn take_connection_events_rx( + &mut self, + ) -> mpsc::UnboundedReceiver { let (_, receiver) = mpsc::unbounded_channel(); std::mem::replace(&mut self.connection_events_rx, receiver) } @@ -594,8 +596,7 @@ pub(crate) struct ManagerTestContext { pub(crate) pool: PgPool, control: ProxyManagerTestSupport, /// Sender for the proxy control channel used by `ProxyManager`. - pub(crate) proxy_control_tx: - tokio::sync::mpsc::Sender, + pub(crate) proxy_control_tx: mpsc::Sender, manager_task: Option>>, } @@ -607,7 +608,7 @@ impl ManagerTestContext { .expect("failed to initialize global settings for proxy manager tests"); let (proxy_control_tx, _proxy_control_rx_placeholder) = - tokio::sync::mpsc::channel::(16); + mpsc::channel::(16); let control = ProxyManagerTestSupport::default(); @@ -649,7 +650,7 @@ impl ManagerTestContext { pub(crate) async fn start(&mut self) { assert!(self.manager_task.is_none(), "proxy manager already started"); - let (gateway_tx, _) = broadcast::channel(16); + let (gateway_tx, _) = mpsc::unbounded_channel(); let (bidi_events_tx, _bidi_events_rx) = mpsc::unbounded_channel::(); let (ldap_tx, _ldap_rx) = mpsc::unbounded_channel(); let (dirsync_tx, _dirsync_rx) = mpsc::unbounded_channel(); @@ -661,7 +662,7 @@ impl ManagerTestContext { )); let (proxy_control_tx, proxy_control_rx) = - tokio::sync::mpsc::channel::(16); + mpsc::channel::(16); self.proxy_control_tx = proxy_control_tx; let manager = ProxyManager::new_for_test( @@ -675,7 +676,7 @@ impl ManagerTestContext { let manager_task = tokio::spawn(async move { manager.run().await }); // No PgListener in proxy manager - just yield to let the manager start. - tokio::task::yield_now().await; + yield_now().await; self.manager_task = Some(manager_task); } diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs index 90454ae63d..ee69df3c0f 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs @@ -517,7 +517,7 @@ async fn test_new_device_sends_gateway_device_created_event( start_enrollment_session(&mut context, &token.id).await; // Subscribe to gateway commands BEFORE sending the request. - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let pubkey = "DhsoNUJPXGl2g5CdqrfE0d7r+AUSHyw5RlNgbXqHlKE="; context.mock_proxy().send_request(CoreRequest { diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs index cef5a2382f..4b5d8ee91b 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs @@ -643,7 +643,7 @@ async fn test_legacy_mfa_finish_rejects_valid_multi_step_mobile_proof( .await, ); let challenge = challenge.expect("mobile approval must include a signature challenge"); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); context.mock_proxy().send_request(CoreRequest { id: 2, @@ -747,7 +747,7 @@ async fn test_legacy_mfa_finish_rejects_valid_multi_step_mobile_proof( ); assert!(matches!( timeout(RECEIVE_TIMEOUT, gateway_rx.recv()).await, - Ok(Ok(GatewayCommand::VpnSessionAuthorized(location_id, _, _))) if location_id == network.id + Ok(Some(GatewayCommand::VpnSessionAuthorized(location_id, _, _))) if location_id == network.id )); context.finish().await.expect_server_finished().await; @@ -785,7 +785,7 @@ async fn test_legacy_mfa_finish_rejects_valid_proof_without_waiter_on_two_step_f .await, ); let challenge = challenge.expect("mobile approval must include a signature challenge"); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); context.mock_proxy().send_request(CoreRequest { id: 2, @@ -917,7 +917,7 @@ async fn test_legacy_mfa_finish_rejects_previous_mobile_attempt_challenge( panic!("second MobileApprove attempt must include a signature challenge"); }; assert_ne!(current_challenge.challenge, first_challenge); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); context.mock_proxy().send_request(CoreRequest { id: 5, @@ -1049,7 +1049,7 @@ async fn test_legacy_mfa_finish_hides_bad_mobile_signature( let challenge = challenge.expect("MobileApprove must include a challenge"); let invalid_signature = sign_challenge(&signing_key, &format!("{challenge} is not the challenge")); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); context.mock_proxy().send_request(CoreRequest { id: 2, @@ -1300,9 +1300,6 @@ async fn test_mfa_token_valid_before_finish_invalid_after( let valid = send_token_validation(&mut context, &token).await; assert!(valid, "token must be valid after start"); - // Subscribe before finish so the handler's gateway_tx.send() has a receiver - let _gateway_rx = context.gateway_tx.subscribe(); - let code = user.generate_email_mfa_code().expect("generate email code"); send_mfa_finish(&mut context, &token, Some(&code)).await; @@ -1410,8 +1407,6 @@ async fn test_mfa_finish_replaces_existing_session_disconnects_old( setup_user_totp_mfa(&context.pool, &mut user).await; // ---- First MFA cycle ---- - // Must subscribe before finish so the send has a receiver. - let _gw_rx1 = context.gateway_tx.subscribe(); let (_, token1) = send_mfa_start( &mut context, @@ -1452,7 +1447,7 @@ async fn test_mfa_finish_replaces_existing_session_disconnects_old( // Subscribe before finish so both VpnSessionDeauthorized and // VpnSessionAuthorized have an active receiver. - let mut gw_rx2 = context.gateway_tx.subscribe(); + let mut gw_rx2 = context.take_gateway_rx(); let code2 = generate_totp_code(&user); let (_, psk2) = send_mfa_finish(&mut context, &token2, Some(&code2)).await; @@ -1461,7 +1456,7 @@ async fn test_mfa_finish_replaces_existing_session_disconnects_old( "second MFA cycle must return a non-empty PSK" ); - // Receive events from the gateway broadcast channel. The handler sends + // Receive events from the gateway channel. The handler sends // VpnSessionDeauthorized (for the old session) and then VpnSessionAuthorized // (for the new session) in that order. let mut got_disconnected = false; @@ -1515,9 +1510,9 @@ async fn test_multi_step_mfa_full_flow(_: PgPoolOptions, options: PgConnectOptio .await; assert_ne!(token, ""); - // Subscribe to the gateway broadcast before finishing so the collect path's + // Subscribe to the gateway channel before finishing so the collect path's // gateway send has a live receiver. - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); // Step 0 (TOTP) advances without authorizing. let totp = generate_totp_code(&user); @@ -1615,7 +1610,7 @@ async fn test_mfa_flow_oidc_awaits_external_completion( .await; let started = send_mfa_step_start(&mut context, &token, MfaMethod::Oidc).await; let attempt_id = started.step_attempt_id; - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let stale_response = send_flow_step_finish(&mut context, &token, "superseded-attempt", None).await; @@ -1727,7 +1722,7 @@ async fn test_new_protocol_mobile_approve_marks_and_collects_by_poll( let challenge = started .challenge .expect("mobile approve StepStart must return a challenge"); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let response = send_flow_step_finish(&mut context, &token, &attempt_id, None).await; assert!(matches!( @@ -1910,7 +1905,7 @@ async fn test_new_protocol_mobile_approve_advances_non_final_step( let challenge = started .challenge .expect("mobile approve StepStart must return a challenge"); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let signature = sign_challenge(&signing_key, &challenge); let response = @@ -2003,7 +1998,7 @@ async fn test_new_protocol_mobile_approve_non_final_device_name_reaches_success_ let challenge = started .challenge .expect("mobile approve StepStart must return a challenge"); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let signature = sign_challenge(&signing_key, &challenge); let response = @@ -2135,7 +2130,7 @@ async fn test_parked_mobile_approval_completes_final_step( .challenge .expect("mobile approval needs a challenge"); let signature = sign_challenge(&signing_key, &challenge); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); context.mock_proxy().send_request(CoreRequest { id: 7001, @@ -2215,7 +2210,7 @@ async fn test_parked_mobile_approval_completes_final_step( assert_vpn_session_exists(&context.pool, network.id, device.id).await; assert!(matches!( timeout(RECEIVE_TIMEOUT, gateway_rx.recv()).await, - Ok(Ok(GatewayCommand::VpnSessionAuthorized(id, _, _))) if id == network.id + Ok(Some(GatewayCommand::VpnSessionAuthorized(id, _, _))) if id == network.id )); let event = context .bidi_events_rx @@ -2273,7 +2268,7 @@ async fn test_parked_mobile_approval_advances_non_final_step( .challenge .expect("mobile approval needs a challenge"), ); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); context.mock_proxy().send_request(CoreRequest { id: 7101, @@ -2371,7 +2366,7 @@ async fn test_multi_step_biometric_flow_completes(_: PgPoolOptions, options: PgC &[MfaMethod::Totp, MfaMethod::Biometric], ) .await; - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let response = send_flow_code_finish( &mut context, diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_legacy.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_legacy.rs index aa8a2e9198..ca9e76d7f4 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_legacy.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_legacy.rs @@ -103,7 +103,7 @@ async fn test_mfa_finish_succeeds_with_totp_code(_: PgPoolOptions, options: PgCo .await; // Subscribe before finish so the gateway send has a receiver. - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let code = generate_totp_code(&user); let (_, psk) = send_mfa_finish(&mut context, &token, Some(&code)).await; @@ -152,7 +152,7 @@ async fn test_legacy_totp_login_completes_with_fido2_on_location( MfaMethod::Totp, ) .await; - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let code = generate_totp_code(&user); let (response, psk) = send_mfa_finish(&mut context, &token, Some(&code)).await; assert!( @@ -198,7 +198,7 @@ async fn test_mfa_finish_succeeds_with_biometric_signature( let challenge = challenge.expect("biometric start must return a challenge to sign"); // Subscribe before finish so the handler's gateway_tx.send() has a receiver. - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let signature = sign_challenge(&signing_key, &challenge); let (_, psk) = send_mfa_finish(&mut context, &token, Some(&signature)).await; @@ -245,7 +245,7 @@ async fn test_mfa_finish_rejects_empty_legacy_mobile_approve_proof( MfaMethod::MobileApprove, ) .await; - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let response = send_mfa_finish_raw(&mut context, &token, None).await; let (code, message) = assert_error_response_details(&response); @@ -286,7 +286,7 @@ async fn test_mfa_finish_succeeds_with_mobile_approve_signature( .await; let challenge = challenge.expect("mobile approve start must return a challenge to sign"); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); context.mock_proxy().send_request(CoreRequest { id: AWAIT_ID, @@ -389,7 +389,7 @@ async fn test_legacy_mobile_approve_completes_with_fido2_on_location( ) .await; let challenge = challenge.expect("legacy mobile-approve start should return a challenge"); - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); context.mock_proxy().send_request(CoreRequest { id: AWAIT_ID, @@ -483,7 +483,7 @@ async fn test_mfa_finish_succeeds_and_creates_session(_: PgPoolOptions, options: .await; // Subscribe before finish so the gateway send has a receiver. - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let _ = code; // keep binding so the setup_user_email_mfa call is not dead // Generate the finish code from the same secret. @@ -531,7 +531,7 @@ async fn test_legacy_email_login_completes_with_fido2_on_location( MfaMethod::Email, ) .await; - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let code = user.generate_email_mfa_code().expect("generate email code"); let (response, psk) = send_mfa_finish(&mut context, &token, Some(&code)).await; assert!( @@ -670,7 +670,7 @@ async fn test_mfa_finish_succeeds_after_oidc_completion( MfaMethod::Oidc, ) .await; - let mut gateway_rx = context.gateway_tx.subscribe(); + let mut gateway_rx = context.take_gateway_rx(); let response = send_mfa_finish_raw(&mut context, &token, None).await; let (code, message) = assert_error_response_details(&response); @@ -779,9 +779,6 @@ async fn test_mfa_await_remote_does_not_receive_psk_after_email_finish( // Let the handler register the waiter. task::yield_now().await; - // Subscribe before finish so the gateway send has a receiver. - let _gateway_rx = context.gateway_tx.subscribe(); - // Finish without receiving so the response can be checked below. let code = user.generate_email_mfa_code().expect("generate email code"); send_mfa_finish_no_recv(&mut context, &token, Some(&code)).await; diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs index 1f0b73aa18..55f7a4b1a8 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs @@ -751,9 +751,6 @@ async fn test_mfa_oidc_full_flow(_: PgPoolOptions, options: PgConnectOptions) { let _provider = create_oidc_provider(&context.pool, &mock).await; set_public_proxy_url(&context.pool, &mock.base_url).await; - // Subscribe to gateway events before sending MFA finish. - let _gateway_rx = context.gateway_tx.subscribe(); - // ---- Step 1: ClientMfaStart with Oidc method ---- let (_, mfa_token) = send_mfa_start( &mut context, diff --git a/crates/defguard_session_manager/src/error.rs b/crates/defguard_session_manager/src/error.rs index 9a3f798bcb..21308c1ea3 100644 --- a/crates/defguard_session_manager/src/error.rs +++ b/crates/defguard_session_manager/src/error.rs @@ -1,6 +1,6 @@ use defguard_common::{db::Id, gateway_event::GatewayCommand}; use thiserror::Error; -use tokio::sync::{broadcast::error::SendError as BroadcastSendError, mpsc::error::SendError}; +use tokio::sync::mpsc::error::SendError; use crate::events::SessionManagerEvent; @@ -27,7 +27,7 @@ pub enum SessionManagerError { #[error("Failed to send session manager event: {0}")] SessionManagerEventError(Box>), #[error("Failed to send gateway manager event: {0}")] - GatewayManagerEventError(Box>), + GatewayManagerEventError(Box>), } impl From> for SessionManagerError { @@ -35,8 +35,8 @@ impl From> for SessionManagerError { Self::SessionManagerEventError(Box::new(error)) } } -impl From> for SessionManagerError { - fn from(error: BroadcastSendError) -> Self { +impl From> for SessionManagerError { + fn from(error: SendError) -> Self { Self::GatewayManagerEventError(Box::new(error)) } } diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index d3f0e74fa3..e36cebf0ef 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -12,10 +12,7 @@ use defguard_common::{ }; use sqlx::{PgConnection, PgPool}; use tokio::{ - sync::{ - broadcast::Sender, - mpsc::{UnboundedReceiver, UnboundedSender}, - }, + sync::mpsc::{UnboundedReceiver, UnboundedSender}, time::{Duration, Interval, interval}, }; use tracing::{debug, error, info, trace}; @@ -42,7 +39,7 @@ pub async fn run_session_manager( pool: PgPool, mut peer_stats_rx: UnboundedReceiver, session_manager_event_tx: UnboundedSender, - gateway_tx: Sender, + gateway_tx: UnboundedSender, ) -> Result<(), SessionManagerError> { info!("Starting VPN client session manager service"); let mut session_update_timer = interval(SESSION_UPDATE_INTERVAL); @@ -103,7 +100,7 @@ pub async fn run_session_manager_iteration( pub struct SessionManager { pool: PgPool, session_manager_event_tx: UnboundedSender, - gateway_tx: Sender, + gateway_tx: UnboundedSender, } impl SessionManager { @@ -111,7 +108,7 @@ impl SessionManager { pub fn new( pool: PgPool, session_manager_event_tx: UnboundedSender, - gateway_tx: Sender, + gateway_tx: UnboundedSender, ) -> Self { Self { pool, diff --git a/crates/defguard_session_manager/tests/common/mod.rs b/crates/defguard_session_manager/tests/common/mod.rs index 7ffa56f198..682757d3d7 100644 --- a/crates/defguard_session_manager/tests/common/mod.rs +++ b/crates/defguard_session_manager/tests/common/mod.rs @@ -28,20 +28,14 @@ use defguard_session_manager::{ }; use ipnetwork::IpNetwork; use sqlx::{PgExecutor, PgPool, query, query_scalar}; -use tokio::{ - sync::{ - broadcast, - mpsc::{self}, - }, - time::interval, -}; +use tokio::{sync::mpsc, time::interval}; pub(crate) struct SessionManagerHarness { pub(crate) manager: SessionManager, stats_tx: mpsc::UnboundedSender, pub(crate) stats_rx: mpsc::UnboundedReceiver, pub(crate) event_rx: mpsc::UnboundedReceiver, - pub(crate) gateway_rx: broadcast::Receiver, + pub(crate) gateway_rx: mpsc::UnboundedReceiver, } pub(crate) fn assert_no_session_manager_events(harness: &mut SessionManagerHarness) { @@ -56,12 +50,9 @@ pub(crate) fn assert_no_session_manager_events(harness: &mut SessionManagerHarne pub(crate) fn assert_no_gateway_events(harness: &mut SessionManagerHarness) { match harness.gateway_rx.try_recv() { - Err(broadcast::error::TryRecvError::Empty) => {} - Err(broadcast::error::TryRecvError::Closed) => { - panic!("gateway event channel closed unexpectedly") - } - Err(broadcast::error::TryRecvError::Lagged(skipped)) => { - panic!("gateway event channel lagged and skipped {skipped} events") + Err(mpsc::error::TryRecvError::Empty) => {} + Err(mpsc::error::TryRecvError::Disconnected) => { + panic!("gateway event channel disconnected unexpectedly") } Ok(event) => panic!("unexpected gateway event: {event:?}"), } @@ -71,7 +62,7 @@ impl SessionManagerHarness { pub(crate) fn new(pool: PgPool) -> Self { let (stats_tx, stats_rx) = mpsc::unbounded_channel(); let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (gateway_tx, gateway_rx) = broadcast::channel(16); + let (gateway_tx, gateway_rx) = mpsc::unbounded_channel(); let manager = SessionManager::new(pool, event_tx, gateway_tx); Self { diff --git a/crates/defguard_setup/src/auto_adoption.rs b/crates/defguard_setup/src/auto_adoption.rs index 8a36da341c..ae771500bf 100644 --- a/crates/defguard_setup/src/auto_adoption.rs +++ b/crates/defguard_setup/src/auto_adoption.rs @@ -36,7 +36,7 @@ use defguard_version::{Version, client::ClientVersionInterceptor}; use ipnetwork::IpNetwork; use reqwest::Url; use sqlx::PgPool; -use tokio::{sync::mpsc::UnboundedReceiver, time::timeout}; +use tokio::{sync::mpsc, time::timeout}; use tonic::{ Request, Status, service::Interceptor, @@ -171,7 +171,7 @@ fn format_component_log(timestamp: &str, level: &str, target: &str, message: &st format!("{timestamp} {level} {target}: message={message}") } -fn collect_stream_logs(log_rx: &mut UnboundedReceiver) -> Vec { +fn collect_stream_logs(log_rx: &mut mpsc::UnboundedReceiver) -> Vec { let mut logs = Vec::new(); while let Ok(log) = log_rx.try_recv() { logs.push(log); @@ -189,7 +189,7 @@ fn collect_core_logs(log_buffer: &SetupLogBuffer) -> Vec { fn merge_failure_logs( message: impl Into, log_buffer: &SetupLogBuffer, - log_rx: &mut UnboundedReceiver, + log_rx: &mut mpsc::UnboundedReceiver, ) -> (bool, Vec, Option) { let msg = message.into(); error!("{msg}"); @@ -250,7 +250,7 @@ async fn run_edge_adoption_attempt_scoped( ca_key_der: Vec, ) -> (bool, Vec, Option) { debug!("Starting edge adoption attempt host={host} port={port}"); - let (log_tx, mut log_rx) = tokio::sync::mpsc::unbounded_channel::(); + let (log_tx, mut log_rx) = mpsc::unbounded_channel::(); let endpoint_str = format!("http://{host}:{port}"); let url = match Url::parse(&endpoint_str) { Ok(url) => url, @@ -573,7 +573,7 @@ async fn run_gateway_adoption_attempt_scoped( ca_key_der: Vec, ) -> (bool, Vec, Option) { debug!("Starting gateway adoption attempt host={host} port={port}"); - let (log_tx, mut log_rx) = tokio::sync::mpsc::unbounded_channel::(); + let (log_tx, mut log_rx) = mpsc::unbounded_channel::(); let endpoint_str = format!("http://{host}:{port}"); let url = match Url::parse(&endpoint_str) { diff --git a/crates/defguard_setup/src/migration.rs b/crates/defguard_setup/src/migration.rs index 93a8265323..39871cf7cd 100644 --- a/crates/defguard_setup/src/migration.rs +++ b/crates/defguard_setup/src/migration.rs @@ -60,7 +60,7 @@ use crate::handlers::{ pub struct MigrationWebapp { pub router: Router, _event_rx: mpsc::UnboundedReceiver, - _gateway_rx: broadcast::Receiver, + _gateway_rx: mpsc::UnboundedReceiver, _proxy_control_rx: mpsc::Receiver, } @@ -73,7 +73,7 @@ pub fn build_migration_webapp( let (event_tx, event_rx) = mpsc::unbounded_channel::(); let (ldap_tx, _ldap_rx) = mpsc::unbounded_channel(); let (dirsync_tx, _dirsync_rx) = mpsc::unbounded_channel(); - let (gateway_tx, gateway_rx) = broadcast::channel::(64); + let (gateway_tx, gateway_rx) = mpsc::unbounded_channel::(); let (web_reload_tx, _web_reload_rx) = broadcast::channel::<()>(8); let (proxy_control_tx, proxy_control_rx) = mpsc::channel(32); let incompatible_components = Arc::new(RwLock::new(IncompatibleComponents::default())); From 305bcfc43871f37b0b34cae992d504a4d9583a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 9 Oct 2026 12:32:48 +0200 Subject: [PATCH 03/14] Clippy happy --- .../tests/integration/api/openid_login.rs | 17 ++++++++++------- .../src/tests/proxy_manager/handler/mfa.rs | 8 ++++---- .../tests/proxy_manager/handler/mfa_legacy.rs | 8 ++++---- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/defguard_core/tests/integration/api/openid_login.rs b/crates/defguard_core/tests/integration/api/openid_login.rs index 49779788ba..15e372b21a 100644 --- a/crates/defguard_core/tests/integration/api/openid_login.rs +++ b/crates/defguard_core/tests/integration/api/openid_login.rs @@ -1,7 +1,10 @@ use chrono::{Duration, Utc}; -use defguard_common::db::{ - Id, - models::{oauth2client::OAuth2Client, settings::OpenIdUsernameHandling}, +use defguard_common::{ + db::{ + Id, + models::{oauth2client::OAuth2Client, settings::OpenIdUsernameHandling}, + }, + secret::SecretStringWrapper, }; use defguard_core::{ enterprise::{ @@ -708,21 +711,21 @@ async fn test_modify_openid_provider_without_secrets_keeps_them( provider .google_service_account_key .as_ref() - .map(|key| key.expose_secret()), + .map(SecretStringWrapper::expose_secret), Some(GOOGLE_KEY) ); assert_eq!( provider .okta_private_jwk .as_ref() - .map(|key| key.expose_secret()), + .map(SecretStringWrapper::expose_secret), Some(OKTA_JWK) ); assert_eq!( provider .jumpcloud_api_key .as_ref() - .map(|key| key.expose_secret()), + .map(SecretStringWrapper::expose_secret), Some(JUMPCLOUD_KEY) ); } @@ -765,7 +768,7 @@ async fn test_modify_openid_provider_with_new_secret_replaces_it( provider .jumpcloud_api_key .as_ref() - .map(|key| key.expose_secret()), + .map(SecretStringWrapper::expose_secret), Some(JUMPCLOUD_KEY) ); } diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs index 4b5d8ee91b..869b75053c 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs @@ -739,7 +739,7 @@ async fn test_legacy_mfa_finish_rejects_valid_multi_step_mobile_proof( else { panic!("2.2 mobile approval must complete the flow"); }; - assert!(!completed.preshared_key.is_empty()); + assert_ne!(completed.preshared_key, ""); let session = assert_vpn_session_exists(&context.pool, network.id, device.id).await; assert_eq!( session.preshared_key.as_ref(), @@ -2196,7 +2196,7 @@ async fn test_parked_mobile_approval_completes_final_step( let Some(mfa_step_result::Outcome::Completed(completed)) = result.outcome else { panic!("expected completed parked result"); }; - assert!(!completed.preshared_key.is_empty()); + assert_ne!(completed.preshared_key, ""); parked_key = Some(completed.preshared_key); } } @@ -2399,7 +2399,7 @@ async fn test_multi_step_biometric_flow_completes(_: PgPoolOptions, options: PgC let challenge = step_started .challenge .expect("biometric StepStart must return a challenge"); - assert!(!challenge.is_empty()); + assert_ne!(challenge, ""); let invalid_response = send_flow_step_finish( &mut context, &token, @@ -2443,7 +2443,7 @@ async fn test_multi_step_biometric_flow_completes(_: PgPoolOptions, options: PgC Some(mfa_step_result::Outcome::Completed(completed)) => completed.preshared_key, _ => panic!("expected completed biometric response"), }; - assert!(!preshared_key.is_empty()); + assert_ne!(preshared_key, ""); assert_vpn_session_exists(&context.pool, network.id, device.id).await; assert!(matches!( timeout(RECEIVE_TIMEOUT, gateway_rx.recv()) diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_legacy.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_legacy.rs index ca9e76d7f4..4a95c78af1 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_legacy.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_legacy.rs @@ -319,7 +319,7 @@ async fn test_mfa_finish_succeeds_with_mobile_approve_signature( match &response.payload { Some(core_response::Payload::ClientMfaFinish(result)) => { assert_eq!(response.id, AWAIT_ID + 1); - assert!(result.preshared_key.is_empty()); + assert_eq!(result.preshared_key, ""); } Some(core_response::Payload::AwaitRemoteMfaFinish(result)) => { assert_eq!(response.id, AWAIT_ID); @@ -422,11 +422,11 @@ async fn test_legacy_mobile_approve_completes_with_fido2_on_location( match &response.payload { Some(core_response::Payload::ClientMfaFinish(result)) => { assert_eq!(response.id, AWAIT_ID + 1); - assert!(result.preshared_key.is_empty()); + assert_eq!(result.preshared_key, ""); } Some(core_response::Payload::AwaitRemoteMfaFinish(result)) => { assert_eq!(response.id, AWAIT_ID); - assert!(!result.preshared_key.is_empty()); + assert_ne!(result.preshared_key, ""); parked_key = Some(result.preshared_key.clone()); } _ => panic!("unexpected response"), @@ -786,7 +786,7 @@ async fn test_mfa_await_remote_does_not_receive_psk_after_email_finish( let response = context.mock_proxy_mut().recv_outbound().await; match response.payload { Some(core_response::Payload::ClientMfaFinish(response)) => { - assert!(!response.preshared_key.is_empty()); + assert_ne!(response.preshared_key, ""); } other => panic!( "expected ClientMfaFinish response, got {:?}", From ec7d49526030f8fb344e14940629164a351cd185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 9 Oct 2026 13:32:36 +0200 Subject: [PATCH 04/14] Abandon SecretStringWrapper::from_str in favour of SecretStringWrapper::from --- .../src/db/models/settings/tests.rs | 4 +-- crates/defguard_common/src/db/models/user.rs | 4 +-- crates/defguard_common/src/secret.rs | 10 +++--- crates/defguard_core/Cargo.toml | 2 +- .../src/enterprise/ldap/tests.rs | 4 +-- crates/defguard_core/src/letsencrypt.rs | 3 +- crates/defguard_core/src/mail/tests.rs | 13 +++---- .../tests/integration/api/common/mod.rs | 3 +- .../tests/integration/api/settings.rs | 16 ++++----- .../tests/integration/ldap/mod.rs | 4 +-- crates/defguard_gateway_manager/Cargo.toml | 2 +- .../defguard_gateway_manager/src/handler.rs | 35 +++++-------------- crates/defguard_proxy_manager/src/handler.rs | 4 +-- .../tests/proxy_manager/handler/support.rs | 3 +- 14 files changed, 39 insertions(+), 68 deletions(-) diff --git a/crates/defguard_common/src/db/models/settings/tests.rs b/crates/defguard_common/src/db/models/settings/tests.rs index 5ad417b4c4..c51a42aa86 100644 --- a/crates/defguard_common/src/db/models/settings/tests.rs +++ b/crates/defguard_common/src/db/models/settings/tests.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use humantime::Duration; use reqwest::Url; use rsa::{RsaPrivateKey, pkcs8::EncodePrivateKey}; @@ -29,7 +27,7 @@ fn test_smtp_config() { // add auth info settings.smtp.user = Some("smtp_user".into()); - settings.smtp.password = Some(SecretStringWrapper::from_str("hunter2").unwrap()); + settings.smtp.password = Some(SecretStringWrapper::from("hunter2")); assert!(settings.smtp_configured()); } diff --git a/crates/defguard_common/src/db/models/user.rs b/crates/defguard_common/src/db/models/user.rs index abde2fc1ce..748e6acc2a 100644 --- a/crates/defguard_common/src/db/models/user.rs +++ b/crates/defguard_common/src/db/models/user.rs @@ -1349,8 +1349,6 @@ impl Distribution> for Standard { #[cfg(test)] mod test { - use std::str::FromStr; - use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; @@ -1829,7 +1827,7 @@ mod test { settings.smtp.sender = Some("noreply@example.com".into()); settings.ldap_url = Some("ldap://localhost".into()); settings.ldap_bind_username = Some("cn=admin,dc=example,dc=com".into()); - settings.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); + settings.ldap_bind_password = Some(SecretStringWrapper::from("secret")); settings.ldap_username_attr = Some("uid".into()); settings.ldap_user_search_base = Some("ou=users,dc=example,dc=com".into()); settings.ldap_user_obj_class = Some("inetOrgPerson".into()); diff --git a/crates/defguard_common/src/secret.rs b/crates/defguard_common/src/secret.rs index 476dc9b567..26e5badc76 100644 --- a/crates/defguard_common/src/secret.rs +++ b/crates/defguard_common/src/secret.rs @@ -1,4 +1,4 @@ -use std::{convert::Infallible, error::Error, str::FromStr}; +use std::error::Error; use secrecy::{ExposeSecret, SecretString}; use serde::Deserialize; @@ -31,11 +31,9 @@ impl From for SecretStringWrapper { } } -impl FromStr for SecretStringWrapper { - type Err = Infallible; - - fn from_str(src: &str) -> Result { - Ok(Self(SecretString::from(src))) +impl From<&str> for SecretStringWrapper { + fn from(value: &str) -> Self { + Self(SecretString::from(value)) } } diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml index 8aaea77ada..9a70eb5fb2 100644 --- a/crates/defguard_core/Cargo.toml +++ b/crates/defguard_core/Cargo.toml @@ -116,7 +116,7 @@ defguard_common = { workspace = true, features = ["test-support"] } claims.workspace = true ed25519-dalek.workspace = true getrandom.workspace = true -hyper-util = "0.1" +hyper-util.workspace = true reqwest = { version = "0.12", features = [ "cookies", "json", diff --git a/crates/defguard_core/src/enterprise/ldap/tests.rs b/crates/defguard_core/src/enterprise/ldap/tests.rs index 855e879105..1d2df175fb 100644 --- a/crates/defguard_core/src/enterprise/ldap/tests.rs +++ b/crates/defguard_core/src/enterprise/ldap/tests.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, str::FromStr}; +use std::collections::HashMap; use defguard_common::{ db::{ @@ -81,7 +81,7 @@ fn configure_smtp_and_ldap(settings: &mut Settings) { settings.smtp.sender = Some("noreply@example.com".into()); settings.ldap_url = Some("ldap://localhost".into()); settings.ldap_bind_username = Some("cn=admin,dc=example,dc=com".into()); - settings.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); + settings.ldap_bind_password = Some(SecretStringWrapper::from("secret")); settings.ldap_username_attr = Some("uid".into()); settings.ldap_user_search_base = Some("ou=users,dc=example,dc=com".into()); settings.ldap_user_obj_class = Some("inetOrgPerson".into()); diff --git a/crates/defguard_core/src/letsencrypt.rs b/crates/defguard_core/src/letsencrypt.rs index 4c7f2af570..5af7551e2c 100644 --- a/crates/defguard_core/src/letsencrypt.rs +++ b/crates/defguard_core/src/letsencrypt.rs @@ -334,7 +334,6 @@ mod tests { use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, pin::Pin, - str::FromStr, sync::{Arc, Once}, time::Duration, }; @@ -545,7 +544,7 @@ mod tests { settings.smtp.port = Some(587); settings.smtp.sender = Some("noreply@example.com".into()); settings.smtp.user = Some(String::new()); - settings.smtp.password = Some(SecretStringWrapper::from_str("").unwrap()); + settings.smtp.password = Some(SecretStringWrapper::from("")); defguard_common::db::models::settings::set_settings(Some(settings)); } diff --git a/crates/defguard_core/src/mail/tests.rs b/crates/defguard_core/src/mail/tests.rs index 5a16043193..5fe331f0b8 100644 --- a/crates/defguard_core/src/mail/tests.rs +++ b/crates/defguard_core/src/mail/tests.rs @@ -1,4 +1,4 @@ -use std::{env, str::FromStr, time::Duration}; +use std::{env, time::Duration}; use chrono::Utc; use defguard_common::{ @@ -111,15 +111,16 @@ async fn set_smtp_settings(pool: &PgPool) { if let Ok(refresh_token) = env::var("SMTP_OAUTH_REFRESH_TOKEN") { settings.smtp.oauth_issuer_url = env::var("SMTP_OAUTH_ISSUER_URL").ok(); settings.smtp.oauth_client_id = env::var("SMTP_OAUTH_CLIENT_ID").ok(); - settings.smtp.oauth_client_secret = Some( - SecretStringWrapper::from_str(&env::var("SMTP_OAUTH_CLIENT_SECRET").unwrap()).unwrap(), - ); + settings.smtp.oauth_client_secret = Some(SecretStringWrapper::from( + env::var("SMTP_OAUTH_CLIENT_SECRET").unwrap(), + )); settings.smtp.oauth_refresh_token = Some(refresh_token.into()); settings.smtp.authentication = SmtpAuthentication::XOAuth2; } else { settings.smtp.user = env::var("SMTP_USER").ok(); - settings.smtp.password = - Some(SecretStringWrapper::from_str(&env::var("SMTP_PASSWORD").unwrap()).unwrap()); + settings.smtp.password = Some(SecretStringWrapper::from( + env::var("SMTP_PASSWORD").unwrap(), + )); settings.smtp.encryption = SmtpEncryption::StartTls; settings.smtp.authentication = SmtpAuthentication::Login; } diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index ba2ad13435..9b29886f1e 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -2,7 +2,6 @@ pub(crate) mod client; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, - str::FromStr, sync::{Arc, Mutex, atomic::AtomicBool}, }; @@ -406,7 +405,7 @@ pub(crate) fn configure_smtp(settings: &mut Settings) { pub(crate) fn configure_ldap(settings: &mut Settings) { settings.ldap_url = Some("ldap://localhost".into()); settings.ldap_bind_username = Some("cn=admin,dc=example,dc=com".into()); - settings.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); + settings.ldap_bind_password = Some(SecretStringWrapper::from("secret")); settings.ldap_username_attr = Some("uid".into()); settings.ldap_user_search_base = Some("ou=users,dc=example,dc=com".into()); settings.ldap_user_obj_class = Some("inetOrgPerson".into()); diff --git a/crates/defguard_core/tests/integration/api/settings.rs b/crates/defguard_core/tests/integration/api/settings.rs index 5af8dd6907..9cdffdf456 100644 --- a/crates/defguard_core/tests/integration/api/settings.rs +++ b/crates/defguard_core/tests/integration/api/settings.rs @@ -1,4 +1,4 @@ -use std::{str::FromStr, time::Duration}; +use std::time::Duration; use defguard_common::{ db::models::{ @@ -252,7 +252,7 @@ async fn test_ldap_connection_test_with_submitted_settings( let mut submitted = saved_settings.clone(); submitted.ldap_url = Some("ldap://127.0.0.1:1".to_owned()); submitted.ldap_bind_username = Some("cn=admin,dc=example,dc=com".to_owned()); - submitted.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); + submitted.ldap_bind_password = Some(SecretStringWrapper::from("secret")); submitted.ldap_username_attr = Some("uid".to_owned()); submitted.ldap_user_search_base = Some("ou=users,dc=example,dc=com".to_owned()); submitted.ldap_user_obj_class = Some("inetOrgPerson".to_owned()); @@ -716,12 +716,10 @@ async fn test_get_settings_does_not_expose_secrets(_: PgPoolOptions, options: Pg assert_eq!(response.status(), StatusCode::OK); let mut settings = Settings::get_current_settings(); - settings.ldap_bind_password = Some(SecretStringWrapper::from_str("ldap-secret").unwrap()); - settings.smtp.password = Some(SecretStringWrapper::from_str("smtp-secret").unwrap()); - settings.smtp.oauth_client_secret = - Some(SecretStringWrapper::from_str("oauth-client-secret").unwrap()); - settings.smtp.oauth_refresh_token = - Some(SecretStringWrapper::from_str("oauth-refresh-token").unwrap()); + settings.ldap_bind_password = Some(SecretStringWrapper::from("ldap-secret")); + settings.smtp.password = Some(SecretStringWrapper::from("smtp-secret")); + settings.smtp.oauth_client_secret = Some(SecretStringWrapper::from("oauth-client-secret")); + settings.smtp.oauth_refresh_token = Some(SecretStringWrapper::from("oauth-refresh-token")); update_current_settings(&client_state.pool, settings) .await .unwrap(); @@ -769,7 +767,7 @@ async fn test_get_settings_does_not_expose_secrets(_: PgPoolOptions, options: Pg } fn secret(value: &str) -> Option { - Some(SecretStringWrapper::from_str(value).unwrap()) + Some(SecretStringWrapper::from(value)) } async fn store_all_secrets(pool: &PgPool) { diff --git a/crates/defguard_core/tests/integration/ldap/mod.rs b/crates/defguard_core/tests/integration/ldap/mod.rs index d975dd9388..5009737840 100644 --- a/crates/defguard_core/tests/integration/ldap/mod.rs +++ b/crates/defguard_core/tests/integration/ldap/mod.rs @@ -1,6 +1,6 @@ //! Integration tests that require a running LDAP server. -use std::{collections::HashSet, env, str::FromStr}; +use std::{collections::HashSet, env}; use defguard_common::{ config::{DefGuardConfig, SERVER_CONFIG}, @@ -54,7 +54,7 @@ async fn set_ldap_settings(pool: &PgPool) { settings.ldap_url = env::var("LDAP_URL").ok(); settings.ldap_bind_username = env::var("LDAP_BIND_USERNAME").ok(); settings.ldap_bind_password = env::var("LDAP_BIND_PASSWORD") - .map(|pass| SecretStringWrapper::from_str(&pass).unwrap()) + .map(SecretStringWrapper::from) .ok(); settings.ldap_group_search_base = env::var("LDAP_GROUP_SEARCH_BASE").ok(); settings.ldap_user_search_base = env::var("LDAP_USER_SEARCH_BASE").ok(); diff --git a/crates/defguard_gateway_manager/Cargo.toml b/crates/defguard_gateway_manager/Cargo.toml index b6c0d6b2a3..792edfb220 100644 --- a/crates/defguard_gateway_manager/Cargo.toml +++ b/crates/defguard_gateway_manager/Cargo.toml @@ -30,4 +30,4 @@ tower.workspace = true tracing.workspace = true [dev-dependencies] -hyper-util = "0.1" +hyper-util.workspace = true diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index baf117c457..f966562b14 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -51,25 +51,6 @@ use crate::{ Client, GatewayEventRouter, TEN_SECS, error::GatewayError, updates::GatewayUpdatesHandler, }; -#[cfg(test)] -#[derive(Default)] -struct GatewayTestTransport { - socket_path: Option, -} - -#[cfg(test)] -impl GatewayTestTransport { - fn with_socket_path(socket_path: PathBuf) -> Self { - Self { - socket_path: Some(socket_path), - } - } - - fn socket_path(&self) -> Option<&PathBuf> { - self.socket_path.as_ref() - } -} - /// One instance per connected Gateway. pub(crate) struct GatewayHandler { // Gateway server endpoint URL. @@ -89,7 +70,7 @@ pub(crate) struct GatewayHandler { /// reconnect email is sent if and only if a disconnect email went out for that outage. disconnect_notification_sent: Arc, #[cfg(test)] - test_transport: GatewayTestTransport, + test_socket_path: Option, #[cfg(test)] test_support: Option, } @@ -124,7 +105,7 @@ impl GatewayHandler { pending_disconnect_notification: None, disconnect_notification_sent, #[cfg(test)] - test_transport: GatewayTestTransport::default(), + test_socket_path: None, #[cfg(test)] test_support: None, }) @@ -712,7 +693,7 @@ impl GatewayHandler { certs_rx, disconnect_notification_sent, )?; - handler.test_transport = GatewayTestTransport::with_socket_path(socket_path); + handler.test_socket_path = Some(socket_path); Ok(handler) } @@ -747,8 +728,8 @@ impl GatewayHandler { } async fn connect_channel(&self, endpoint: &Endpoint) -> Result { - if let Some(socket_path) = self.test_transport.socket_path().cloned() { - return Ok(endpoint.connect_with_connector_lazy(tower::service_fn( + if let Some(socket_path) = self.test_socket_path.clone() { + Ok(endpoint.connect_with_connector_lazy(tower::service_fn( move |_: tonic::transport::Uri| { let socket_path = socket_path.clone(); async move { @@ -757,10 +738,10 @@ impl GatewayHandler { )) } }, - ))); + ))) + } else { + self.connect_tls_channel(endpoint).await } - - self.connect_tls_channel(endpoint).await } pub(crate) async fn handle_connection_once(&mut self) -> anyhow::Result<()> { diff --git a/crates/defguard_proxy_manager/src/handler.rs b/crates/defguard_proxy_manager/src/handler.rs index f36266e059..4fabb44302 100644 --- a/crates/defguard_proxy_manager/src/handler.rs +++ b/crates/defguard_proxy_manager/src/handler.rs @@ -73,7 +73,7 @@ use tokio::{ sync::{ Mutex, Semaphore, TryAcquireError, mpsc::{self, UnboundedSender}, - watch, + oneshot, watch, }, task::JoinSet, time::sleep, @@ -201,7 +201,7 @@ fn build_auth_info_payload( }) } -type ShutdownReceiver = tokio::sync::oneshot::Receiver; +type ShutdownReceiver = oneshot::Receiver; #[cfg(test)] #[derive(Default)] diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs index 6db26912ca..59b63f8c06 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs @@ -1,6 +1,5 @@ use std::{ mem::discriminant, - str::FromStr, sync::atomic::{AtomicU16, AtomicU64, Ordering}, time::SystemTime, }; @@ -1377,7 +1376,7 @@ pub(crate) fn configure_smtp(settings: &mut Settings) { pub(crate) fn configure_ldap(settings: &mut Settings) { settings.ldap_url = Some("ldap://localhost".into()); settings.ldap_bind_username = Some("cn=admin,dc=example,dc=com".into()); - settings.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); + settings.ldap_bind_password = Some(SecretStringWrapper::from("secret")); settings.ldap_username_attr = Some("uid".into()); settings.ldap_user_search_base = Some("ou=users,dc=example,dc=com".into()); settings.ldap_user_obj_class = Some("inetOrgPerson".into()); From d0d1091cd625f865b4e489aa4820793b6b256a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 12 Oct 2026 09:21:57 +0200 Subject: [PATCH 05/14] Cargo.lock --- Cargo.lock | 120 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50b834b34e..a0208a425d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,9 +99,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ammonia" -version = "4.2.1" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "850babe4998b8b9e2dad65a56be4ee1bb977bc3cb3934fa2b28feb2297c30a55" +checksum = "061e83b03c2681c18a6787d956e355c74e0b98ba7ba3d69b0822ade1e6f1d716" dependencies = [ "cssparser", "html5ever", @@ -350,7 +350,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -769,7 +769,7 @@ dependencies = [ "prettyplease 0.3.0", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -869,9 +869,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ "find-msvc-tools", "jobserver", @@ -1031,7 +1031,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -1368,7 +1368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" dependencies = [ "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -1516,7 +1516,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -1538,7 +1538,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core 0.24.1", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -2096,7 +2096,7 @@ checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -2223,7 +2223,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -2751,7 +2751,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -4055,7 +4055,7 @@ version = "0.0.0" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -4837,19 +4837,21 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" +checksum = "b568374ba38b33a6c627141f891faf16902b08d2db26b8ede1bcb0a15b1919fa" dependencies = [ "memchr", + "psm", + "stacker", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf" +checksum = "b66e184b924cebaaff20ab2256ca52f12332d528a39aa76553b5d96f92aacf7f" dependencies = [ "pest", "pest_generator", @@ -4857,22 +4859,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109" +checksum = "a87478d267e4de54a626af9754f2f0f58e927aac6ed0575fe89bc05ad6851694" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.8", ] [[package]] name = "pest_meta" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb" +checksum = "4f986f248b4241ac359b831f6139aaa34e03b08a37b6caf7e201a33f95c869e1" dependencies = [ "pest", ] @@ -5199,7 +5201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -5298,6 +5300,15 @@ version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + [[package]] name = "publicsuffix" version = "2.3.0" @@ -5703,7 +5714,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -6177,7 +6188,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -6195,9 +6206,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -6280,7 +6291,7 @@ dependencies = [ "darling 0.24.1", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -6525,9 +6536,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" dependencies = [ "serde", ] @@ -6820,6 +6831,19 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + [[package]] name = "string_cache" version = "0.11.0" @@ -6921,9 +6945,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622" dependencies = [ "proc-macro2", "quote", @@ -6958,7 +6982,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -7069,7 +7093,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -7177,7 +7201,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -7214,9 +7238,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" dependencies = [ "bytes", "futures-core", @@ -7542,9 +7566,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-normalization" @@ -7684,9 +7708,9 @@ checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" [[package]] name = "uuid" -version = "1.27.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -7838,7 +7862,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", "wasm-bindgen-shared", ] @@ -8348,7 +8372,7 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", "synstructure 0.14.0", ] @@ -8389,7 +8413,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", "synstructure 0.14.0", ] @@ -8443,7 +8467,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.8", ] [[package]] @@ -8474,9 +8498,9 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" dependencies = [ "bumpalo", "crc32fast", From 3bc05bd4e025c08f14de2261e4666e602164c1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 12 Oct 2026 09:39:09 +0200 Subject: [PATCH 06/14] Cleanup --- crates/defguard_gateway_manager/src/lib.rs | 110 +++--------------- .../src/tests/common/mod.rs | 4 +- .../defguard_gateway_manager/src/tests/mod.rs | 69 +++++++++++ 3 files changed, 88 insertions(+), 95 deletions(-) diff --git a/crates/defguard_gateway_manager/src/lib.rs b/crates/defguard_gateway_manager/src/lib.rs index 7609b02ae2..502f908d39 100644 --- a/crates/defguard_gateway_manager/src/lib.rs +++ b/crates/defguard_gateway_manager/src/lib.rs @@ -20,7 +20,7 @@ use tokio::sync::Notify; use tokio::{ sync::{ mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, - watch::Receiver, + watch, }, task::{AbortHandle, JoinHandle, JoinSet}, time::sleep, @@ -384,7 +384,7 @@ impl GatewayManager { fn build_handler( &self, gateway: Gateway, - certs_rx: Receiver>>, + certs_rx: watch::Receiver>>, ) -> Result { // Reuse any outstanding "disconnect sent" flag for this Gateway so a reconnect email is // not lost when the handler is rebuilt after connection-relevant fields change. @@ -436,7 +436,7 @@ impl GatewayManager { /// Bi-directional gRPC stream for communication with Defguard Gateway. pub async fn run(&mut self) -> Result<(), anyhow::Error> { - let (certs_tx, certs_rx) = tokio::sync::watch::channel(Arc::new(HashMap::new())); + let (certs_tx, certs_rx) = watch::channel(Arc::new(HashMap::new())); certs::refresh_certs(&self.pool, &certs_tx).await; let refresh_pool = self.pool.clone(); let _refresh_certs_task = AbortTaskOnDrop::new(tokio::spawn(async move { @@ -450,7 +450,7 @@ impl GatewayManager { }); // Stores the abort handle and a snapshot of the gateway at the time the handler was last // started. The snapshot is used by the Update arm to detect connection-relevant changes. - let mut abort_handles: HashMap)> = HashMap::new(); + let mut abort_handles = HashMap::new(); for gateway in Gateway::all(&self.pool).await? { if !gateway.enabled { debug!("Existing Gateway is disabled, so it won't be handled"); @@ -644,7 +644,7 @@ impl GatewayManager { &mut self, gateway: Gateway, clients: Arc>>, - certs_rx: Receiver>>, + certs_rx: watch::Receiver>>, ) -> Result { let mut gateway_handler = self.build_handler(gateway, certs_rx)?; let manager_reconnect_delay = self.manager_reconnect_delay(); @@ -679,78 +679,6 @@ fn needs_restart(old: &Gateway, new: &Gateway) -> bool { || old.core_client_cert_key_der != new.core_client_cert_key_der } -#[cfg(test)] -mod unit_tests { - use chrono::Utc; - use defguard_common::db::{Id, models::gateway::Gateway}; - - use super::needs_restart; - - fn base_gateway() -> Gateway { - Gateway { - id: 1, - location_id: 1, - name: "test".to_owned(), - address: "127.0.0.1".to_owned(), - port: 50051, - connected_at: None, - disconnected_at: None, - certificate_serial: None, - certificate_expiry: None, - version: None, - enabled: true, - modified_at: Utc::now().naive_utc(), - modified_by: "test".to_owned(), - core_client_cert_der: None, - core_client_cert_key_der: None, - core_client_cert_expiry: None, - } - } - - #[test] - fn test_needs_restart_detects_connection_relevant_field_changes() { - let base = base_gateway(); - - // Identical gateways - no restart needed. - assert!(!needs_restart(&base, &base.clone())); - - // Non-connection-relevant fields - no restart. - let mut no_restart = base.clone(); - no_restart.version = Some("2.0.0".to_owned()); - no_restart.modified_by = "someone-else".to_owned(); - no_restart.connected_at = Some(Utc::now().naive_utc()); - no_restart.disconnected_at = Some(Utc::now().naive_utc()); - no_restart.certificate_serial = Some("abc".to_owned()); - no_restart.core_client_cert_expiry = Some(Utc::now().naive_utc()); - assert!(!needs_restart(&base, &no_restart)); - - // address change - restart required. - let mut changed = base.clone(); - changed.address = "10.0.0.1".to_owned(); - assert!(needs_restart(&base, &changed)); - - // port change - restart required. - let mut changed = base.clone(); - changed.port = 9999; - assert!(needs_restart(&base, &changed)); - - // enabled change - restart required. - let mut changed = base.clone(); - changed.enabled = false; - assert!(needs_restart(&base, &changed)); - - // core_client_cert_der change - restart required. - let mut changed = base.clone(); - changed.core_client_cert_der = Some(vec![1, 2, 3]); - assert!(needs_restart(&base, &changed)); - - // core_client_cert_key_der change - restart required. - let mut changed = base.clone(); - changed.core_client_cert_key_der = Some(vec![4, 5, 6]); - assert!(needs_restart(&base, &changed)); - } -} - /// Shared set of outbound channels that gateway instances use to forward /// events, notifications, and side effects to Core components. #[derive(Clone)] @@ -795,25 +723,21 @@ impl GatewayEventRouter { } pub(crate) async fn run(self, mut events_rx: UnboundedReceiver) { - while let Some(event) = events_rx.recv().await { - self.dispatch(&event); + while let Some(ref event) = events_rx.recv().await { + // Sends the event to all handlers, dropping closed channels. + self.senders + .lock() + .expect("Failed to lock GatewayEventRouter::senders") + .retain(|gateway_id, events_tx| { + let sent = events_tx.send(event.clone()).is_ok(); + if !sent { + debug!("Updates handler for Gateway id={gateway_id} has stopped"); + } + sent + }); } } - /// Sends the event to all handlers, dropping closed channels. - fn dispatch(&self, event: &GatewayCommand) { - self.senders - .lock() - .expect("Failed to lock GatewayEventRouter::senders") - .retain(|gateway_id, events_tx| { - let sent = events_tx.send(event.clone()).is_ok(); - if !sent { - debug!("Updates handler for Gateway id={gateway_id} has stopped"); - } - sent - }); - } - #[cfg(test)] pub(crate) fn registered(&self, gateway_id: Id) -> Option> { self.senders diff --git a/crates/defguard_gateway_manager/src/tests/common/mod.rs b/crates/defguard_gateway_manager/src/tests/common/mod.rs index 0c386a5602..efc2a9cbf3 100644 --- a/crates/defguard_gateway_manager/src/tests/common/mod.rs +++ b/crates/defguard_gateway_manager/src/tests/common/mod.rs @@ -39,7 +39,7 @@ use tokio::{ mpsc::{self, UnboundedReceiver, UnboundedSender}, oneshot, watch, }, - task::JoinHandle, + task::{JoinHandle, yield_now}, time::{sleep, timeout}, }; use tokio_stream::{once, wrappers::UnboundedReceiverStream}; @@ -662,7 +662,7 @@ impl HandlerTestContext { }) .await .expect("timed out waiting for gateway updates handler registration"); - tokio::task::yield_now().await; + yield_now().await; connected_gateway } diff --git a/crates/defguard_gateway_manager/src/tests/mod.rs b/crates/defguard_gateway_manager/src/tests/mod.rs index ef5f24f2c4..71172c69fa 100644 --- a/crates/defguard_gateway_manager/src/tests/mod.rs +++ b/crates/defguard_gateway_manager/src/tests/mod.rs @@ -1,2 +1,71 @@ mod common; mod gateway_manager; + +use chrono::Utc; +use defguard_common::db::{Id, models::gateway::Gateway}; + +use super::needs_restart; + +fn base_gateway() -> Gateway { + Gateway { + id: 1, + location_id: 1, + name: "test".to_owned(), + address: "127.0.0.1".to_owned(), + port: 50051, + connected_at: None, + disconnected_at: None, + certificate_serial: None, + certificate_expiry: None, + version: None, + enabled: true, + modified_at: Utc::now().naive_utc(), + modified_by: "test".to_owned(), + core_client_cert_der: None, + core_client_cert_key_der: None, + core_client_cert_expiry: None, + } +} + +#[test] +fn test_needs_restart_detects_connection_relevant_field_changes() { + let base = base_gateway(); + + // Identical gateways - no restart needed. + assert!(!needs_restart(&base, &base.clone())); + + // Non-connection-relevant fields - no restart. + let mut no_restart = base.clone(); + no_restart.version = Some("2.0.0".to_owned()); + no_restart.modified_by = "someone-else".to_owned(); + no_restart.connected_at = Some(Utc::now().naive_utc()); + no_restart.disconnected_at = Some(Utc::now().naive_utc()); + no_restart.certificate_serial = Some("abc".to_owned()); + no_restart.core_client_cert_expiry = Some(Utc::now().naive_utc()); + assert!(!needs_restart(&base, &no_restart)); + + // address change - restart required. + let mut changed = base.clone(); + changed.address = "10.0.0.1".to_owned(); + assert!(needs_restart(&base, &changed)); + + // port change - restart required. + let mut changed = base.clone(); + changed.port = 9999; + assert!(needs_restart(&base, &changed)); + + // enabled change - restart required. + let mut changed = base.clone(); + changed.enabled = false; + assert!(needs_restart(&base, &changed)); + + // core_client_cert_der change - restart required. + let mut changed = base.clone(); + changed.core_client_cert_der = Some(vec![1, 2, 3]); + assert!(needs_restart(&base, &changed)); + + // core_client_cert_key_der change - restart required. + let mut changed = base.clone(); + changed.core_client_cert_key_der = Some(vec![4, 5, 6]); + assert!(needs_restart(&base, &changed)); +} From 4194beba6086379da2a3e517d43b12922f09c681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 12 Oct 2026 11:14:44 +0200 Subject: [PATCH 07/14] Remove GatewayEventRouter --- .../src/grpc/proxy/client_mfa/tests.rs | 33 ++- .../defguard_gateway_manager/src/handler.rs | 49 ++-- .../src/handler/tests.rs | 9 +- crates/defguard_gateway_manager/src/lib.rs | 75 ++---- .../src/tests/common/mod.rs | 48 +--- .../src/tests/gateway_manager/handler.rs | 5 +- .../gateway_manager/handler/network_events.rs | 27 +- .../tests/gateway_manager/handler/support.rs | 2 - .../defguard_gateway_manager/src/updates.rs | 238 +++++++++--------- 9 files changed, 202 insertions(+), 284 deletions(-) diff --git a/crates/defguard_core/src/grpc/proxy/client_mfa/tests.rs b/crates/defguard_core/src/grpc/proxy/client_mfa/tests.rs index 0fc1e89de9..01633b7a08 100644 --- a/crates/defguard_core/src/grpc/proxy/client_mfa/tests.rs +++ b/crates/defguard_core/src/grpc/proxy/client_mfa/tests.rs @@ -626,7 +626,7 @@ async fn test_posture_check_pass_emits_posture_check_passed_event( .expect("posture check should pass") { super::PostureCheckOutcome::Approved { preshared_key } => { - assert!(!preshared_key.is_empty()); + assert_ne!(preshared_key, ""); } super::PostureCheckOutcome::Rejected { failed_checks } => { panic!("posture check unexpectedly failed: {failed_checks:?}") @@ -1717,7 +1717,7 @@ async fn test_legacy_mfa_start_supersedes_parked_waiter_with_defined_result( ) .await .expect("replacement start should succeed"); - assert!(!replacement.token.is_empty()); + assert_ne!(replacement.token, ""); let response = timeout(Duration::from_secs(1), response_rx.recv()) .await @@ -1783,7 +1783,7 @@ async fn test_legacy_mfa_finish_totp_authorizes_session( ) .await .expect("finish should succeed"); - assert!(!response.preshared_key.is_empty()); + assert_ne!(response.preshared_key, ""); // The successful finish is audited. let event = event_rx @@ -1989,7 +1989,7 @@ async fn test_mfa_flow_remote_wakes_matching_attempt_and_delivers_psk( .expect("MobileApprove must include a signature challenge") { mfa_step_started::Challenge::Signature(challenge) => challenge.challenge, - other => panic!("unexpected challenge: {other:?}"), + other @ mfa_step_started::Challenge::Fido2(_) => panic!("unexpected challenge: {other:?}"), }; let token = accepted.token; let step_attempt_id = first_step.step_attempt_id; @@ -2068,7 +2068,7 @@ async fn test_mfa_flow_remote_wakes_matching_attempt_and_delivers_psk( let Some(mfa_step_result::Outcome::Completed(completed)) = result.outcome else { panic!("expected a completed remote result"); }; - assert!(!completed.preshared_key.is_empty()); + assert_ne!(completed.preshared_key, ""); let remote_preshared_key = completed.preshared_key; assert!( server @@ -2129,7 +2129,7 @@ async fn test_mfa_flow_remote_observes_approval_before_registration( .expect("MobileApprove must include a signature challenge") { mfa_step_started::Challenge::Signature(challenge) => challenge.challenge, - other => panic!("unexpected challenge: {other:?}"), + other @ mfa_step_started::Challenge::Fido2(_) => panic!("unexpected challenge: {other:?}"), }; let token = accepted.token; let step_attempt_id = first_step.step_attempt_id; @@ -2197,7 +2197,7 @@ async fn test_mfa_flow_remote_observes_approval_before_registration( else { panic!("expected a completed remote result"); }; - assert!(!completed.preshared_key.is_empty()); + assert_ne!(completed.preshared_key, ""); assert!( VpnClientMfaSession::::find_active_by_token(&pool, &token) .await @@ -2348,8 +2348,8 @@ async fn test_mfa_flow_start_returns_initial_attempt(_: PgPoolOptions, options: let first_step = accepted .first_step .expect("accepted flow start must include the first step"); - assert!(!accepted.token.is_empty()); - assert!(!first_step.step_attempt_id.is_empty()); + assert_ne!(accepted.token, ""); + assert_ne!(first_step.step_attempt_id, ""); assert!(first_step.challenge.is_none()); let session = VpnClientMfaSession::::find_active_by_token(&pool, &accepted.token) @@ -2440,12 +2440,12 @@ async fn test_mfa_flow_step_start_returns_typed_attempt( .await .expect("flow step start should succeed"); let started = step.started.expect("step start must include its result"); - assert!(!started.step_attempt_id.is_empty()); + assert_ne!(started.step_attempt_id, ""); assert_ne!(started.step_attempt_id, initial_attempt); let Some(mfa_step_started::Challenge::Signature(challenge)) = started.challenge else { panic!("mobile-approve step start should return a signature challenge"); }; - assert!(!challenge.challenge.is_empty()); + assert_ne!(challenge.challenge, ""); let session = VpnClientMfaSession::::find_active_by_token(&pool, &accepted.token) .await @@ -2528,9 +2528,8 @@ async fn test_mfa_flow_step_finish_rejects_untyped_totp_submission( device_info(), ) .await; - let error = match error { - Ok(_) => panic!("a TOTP step must reject an empty typed submission"), - Err(error) => error, + let Err(error) = error else { + panic!("a TOTP step must reject an empty typed submission"); }; assert_eq!(error.code(), Code::InvalidArgument); assert_eq!( @@ -2581,14 +2580,14 @@ async fn test_mfa_flow_remote_rejects_non_mobile_step(_: PgPoolOptions, options: .first_step .expect("flow start must include an attempt") .step_attempt_id; - let (_response_tx, _response_rx) = mpsc::unbounded_channel(); + let (response_tx, _response_rx) = mpsc::unbounded_channel(); let error = server .mfa_flow_remote( MfaFlowRemoteRequest { token: accepted.token, step_attempt_id: attempt_id, }, - _response_tx, + response_tx, 1, device_info(), ) @@ -2795,7 +2794,7 @@ async fn test_finish_survives_server_restart(_: PgPoolOptions, options: PgConnec ) .await .expect("finish should succeed after restart"); - assert!(!response.preshared_key.is_empty()); + assert_ne!(response.preshared_key, ""); } #[sqlx::test] diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index f966562b14..3255d62351 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -17,6 +17,7 @@ use defguard_common::{ Id, models::{Certificates, Settings, WireguardNetwork, gateway::Gateway}, }, + gateway_event::GatewayCommand, messages::peer_stats_update::PeerStatsUpdate, }; use defguard_core::{ @@ -36,7 +37,7 @@ use semver::Version; use sqlx::PgPool; use tokio::{ sync::{ - mpsc::{self, UnboundedSender}, + mpsc::{self, UnboundedReceiver, UnboundedSender}, watch, }, task::JoinHandle, @@ -47,9 +48,7 @@ use tonic::transport::{Channel, Endpoint}; #[cfg(test)] use crate::GatewayManagerTestSupport; -use crate::{ - Client, GatewayEventRouter, TEN_SECS, error::GatewayError, updates::GatewayUpdatesHandler, -}; +use crate::{Client, TEN_SECS, error::GatewayError, updates::GatewayUpdatesHandler}; /// One instance per connected Gateway. pub(crate) struct GatewayHandler { @@ -58,11 +57,10 @@ pub(crate) struct GatewayHandler { gateway: Gateway, message_id: AtomicU64, pool: PgPool, - event_router: GatewayEventRouter, + events_rx: UnboundedReceiver, connection_events_tx: UnboundedSender, peer_stats_tx: UnboundedSender, certs_rx: watch::Receiver>>, - updates_handler_handle: Option>, /// Disconnect email notification waiting out the inactivity threshold. Aborted when the /// Gateway reconnects inside the window, so short outages produce no email at all. pending_disconnect_notification: Option>, @@ -79,7 +77,7 @@ impl GatewayHandler { pub fn new( gateway: Gateway, pool: PgPool, - event_router: GatewayEventRouter, + events_rx: UnboundedReceiver, connection_events_tx: UnboundedSender, peer_stats_tx: UnboundedSender, certs_rx: watch::Receiver>>, @@ -97,11 +95,10 @@ impl GatewayHandler { gateway, message_id: AtomicU64::new(0), pool, - event_router, + events_rx, connection_events_tx, peer_stats_tx, certs_rx, - updates_handler_handle: None, pending_disconnect_notification: None, disconnect_notification_sent, #[cfg(test)] @@ -525,9 +522,23 @@ impl GatewayHandler { let mut resp_stream = response.into_inner(); let mut config_sent = false; + let mut updates_handler: Option = None; loop { - match resp_stream.message().await { + let message = tokio::select! { + message = resp_stream.message() => message, + Some(event) = self.events_rx.recv() => { + // Events are discarded until configuration has been sent. + if let Some(handler) = updates_handler.as_mut() + && handler.handle_event(event).await.is_err() + { + error!("Closing update stream to {}", self.gateway); + updates_handler = None; + } + continue; + } + }; + match message { Ok(None) => { info!("Stream was closed by the sender."); self.handle_stream_disconnection( @@ -557,20 +568,15 @@ impl GatewayHandler { info!("Sent configuration to {}", self.gateway); config_sent = true; self.mark_connected_and_maybe_notify(&network.name).await; - let (events_tx, events_rx) = mpsc::unbounded_channel(); - let mut updates_handler = GatewayUpdatesHandler::new( + let mut handler = GatewayUpdatesHandler::new( self.gateway.location_id, network, self.gateway.name.clone(), Some(self.pool.clone()), - events_rx, tx.clone(), ); - let handle = tokio::spawn(async move { - updates_handler.run().await; - }); - self.event_router.register(self.gateway.id, events_tx); - self.updates_handler_handle = Some(handle); + handler.start().await; + updates_handler = Some(handler); } Err(err) => { error!( @@ -647,9 +653,6 @@ impl GatewayHandler { impl Drop for GatewayHandler { fn drop(&mut self) { - if let Some(handle) = self.updates_handler_handle.take() { - handle.abort(); - } if let Some(handle) = self.pending_disconnect_notification.take() { handle.abort(); } @@ -677,7 +680,7 @@ impl GatewayHandler { pub(crate) fn new_with_test_socket( gateway: Gateway, pool: PgPool, - event_router: GatewayEventRouter, + events_rx: UnboundedReceiver, connection_events_tx: UnboundedSender, peer_stats_tx: UnboundedSender, certs_rx: watch::Receiver>>, @@ -687,7 +690,7 @@ impl GatewayHandler { let mut handler = Self::new( gateway, pool, - event_router, + events_rx, connection_events_tx, peer_stats_tx, certs_rx, diff --git a/crates/defguard_gateway_manager/src/handler/tests.rs b/crates/defguard_gateway_manager/src/handler/tests.rs index eaada67c38..ff54b061d6 100644 --- a/crates/defguard_gateway_manager/src/handler/tests.rs +++ b/crates/defguard_gateway_manager/src/handler/tests.rs @@ -25,7 +25,7 @@ use prost_types::Timestamp; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use tokio::sync::{mpsc::unbounded_channel, watch}; -use crate::{GatewayEventRouter, updates::GatewayUpdatesHandler}; +use crate::updates::GatewayUpdatesHandler; use super::{GatewayHandler, try_protos_into_stats_message}; @@ -209,12 +209,9 @@ fn gen_config_preserves_absent_firewall_config_and_empty_peers() { fn test_handler(mfa_enabled: bool) -> GatewayUpdatesHandler { let network = test_network(mfa_enabled); - let (events_tx, events_rx) = unbounded_channel(); let (tx, _rx) = unbounded_channel(); - drop(events_tx); - let mut handler = - GatewayUpdatesHandler::new(network.id, network, "gateway".into(), None, events_rx, tx); + let mut handler = GatewayUpdatesHandler::new(network.id, network, "gateway".into(), None, tx); handler.session_authorization_required = handler.network.mfa_enabled; handler } @@ -382,7 +379,7 @@ async fn test_send_configuration_includes_mfa_peers_with_session_preshared_key( let handler = GatewayHandler::new( gateway, pool.clone(), - GatewayEventRouter::default(), + unbounded_channel().1, connection_events_tx, peer_stats_tx, certs_rx, diff --git a/crates/defguard_gateway_manager/src/lib.rs b/crates/defguard_gateway_manager/src/lib.rs index 502f908d39..5307eadd80 100644 --- a/crates/defguard_gateway_manager/src/lib.rs +++ b/crates/defguard_gateway_manager/src/lib.rs @@ -300,7 +300,8 @@ pub struct GatewayManager { tx: GatewayTxSet, /// Taken by [`Self::run`]. events_rx: Option>, - event_router: GatewayEventRouter, + /// Per-Gateway channels feeding each `GatewayHandler`. + event_senders: Arc>>>, } impl GatewayManager { @@ -318,7 +319,7 @@ impl GatewayManager { disconnect_notification_sent_by_gateway: Arc::default(), tx, events_rx: Some(events_rx), - event_router: GatewayEventRouter::default(), + event_senders: Arc::default(), } } @@ -338,7 +339,7 @@ impl GatewayManager { test_support, tx, events_rx: Some(events_rx), - event_router: GatewayEventRouter::default(), + event_senders: Arc::default(), } } @@ -384,6 +385,7 @@ impl GatewayManager { fn build_handler( &self, gateway: Gateway, + events_rx: UnboundedReceiver, certs_rx: watch::Receiver>>, ) -> Result { // Reuse any outstanding "disconnect sent" flag for this Gateway so a reconnect email is @@ -399,7 +401,7 @@ impl GatewayManager { GatewayHandler::new_with_test_socket( gateway, self.pool.clone(), - self.event_router.clone(), + events_rx, self.tx.connection_events.clone(), self.tx.peer_stats.clone(), certs_rx, @@ -410,7 +412,7 @@ impl GatewayManager { GatewayHandler::new( gateway, self.pool.clone(), - self.event_router.clone(), + events_rx, self.tx.connection_events.clone(), self.tx.peer_stats.clone(), certs_rx, @@ -426,7 +428,7 @@ impl GatewayManager { GatewayHandler::new( gateway, self.pool.clone(), - self.event_router.clone(), + events_rx, self.tx.connection_events.clone(), self.tx.peer_stats.clone(), certs_rx, @@ -445,8 +447,17 @@ impl GatewayManager { sleep(TEN_SECS).await; } })); - let _dispatch_events_task = self.events_rx.take().map(|events_rx| { - AbortTaskOnDrop::new(tokio::spawn(self.event_router.clone().run(events_rx))) + let _dispatch_events_task = self.events_rx.take().map(|mut events_rx| { + let event_senders = Arc::clone(&self.event_senders); + AbortTaskOnDrop::new(tokio::spawn(async move { + while let Some(ref event) = events_rx.recv().await { + // Send the event to all handlers, dropping closed channels. + event_senders + .lock() + .expect("Failed to lock GatewayManager::event_senders") + .retain(|_, events_tx| events_tx.send(event.clone()).is_ok()); + } + })) }); // Stores the abort handle and a snapshot of the gateway at the time the handler was last // started. The snapshot is used by the Update arm to detect connection-relevant changes. @@ -646,7 +657,12 @@ impl GatewayManager { clients: Arc>>, certs_rx: watch::Receiver>>, ) -> Result { - let mut gateway_handler = self.build_handler(gateway, certs_rx)?; + let (events_tx, events_rx) = unbounded_channel(); + self.event_senders + .lock() + .expect("Failed to lock GatewayManager::event_senders") + .insert(gateway.id, events_tx); + let mut gateway_handler = self.build_handler(gateway, events_rx, certs_rx)?; let manager_reconnect_delay = self.manager_reconnect_delay(); let abort_handle = self.handlers.spawn(async move { gateway_handler @@ -706,44 +722,3 @@ impl GatewayTxSet { self } } - -/// Delivers Gateway events to all updates handlers. -#[derive(Clone, Default)] -pub(crate) struct GatewayEventRouter { - senders: Arc>>>, -} - -impl GatewayEventRouter { - /// Replaces any previously registered channel for this Gateway. - pub(crate) fn register(&self, gateway_id: Id, events_tx: UnboundedSender) { - self.senders - .lock() - .expect("Failed to lock GatewayEventRouter::senders") - .insert(gateway_id, events_tx); - } - - pub(crate) async fn run(self, mut events_rx: UnboundedReceiver) { - while let Some(ref event) = events_rx.recv().await { - // Sends the event to all handlers, dropping closed channels. - self.senders - .lock() - .expect("Failed to lock GatewayEventRouter::senders") - .retain(|gateway_id, events_tx| { - let sent = events_tx.send(event.clone()).is_ok(); - if !sent { - debug!("Updates handler for Gateway id={gateway_id} has stopped"); - } - sent - }); - } - } - - #[cfg(test)] - pub(crate) fn registered(&self, gateway_id: Id) -> Option> { - self.senders - .lock() - .expect("Failed to lock GatewayEventRouter::senders") - .get(&gateway_id) - .cloned() - } -} diff --git a/crates/defguard_gateway_manager/src/tests/common/mod.rs b/crates/defguard_gateway_manager/src/tests/common/mod.rs index efc2a9cbf3..1cedcb3102 100644 --- a/crates/defguard_gateway_manager/src/tests/common/mod.rs +++ b/crates/defguard_gateway_manager/src/tests/common/mod.rs @@ -39,16 +39,13 @@ use tokio::{ mpsc::{self, UnboundedReceiver, UnboundedSender}, oneshot, watch, }, - task::{JoinHandle, yield_now}, + task::JoinHandle, time::{sleep, timeout}, }; use tokio_stream::{once, wrappers::UnboundedReceiverStream}; use tonic::{Request, Response, Status, Streaming, transport::Server}; -use crate::{ - GatewayEventRouter, GatewayManager, GatewayManagerTestSupport, GatewayTxSet, - handler::GatewayHandler, -}; +use crate::{GatewayManager, GatewayManagerTestSupport, GatewayTxSet, handler::GatewayHandler}; const TEST_TIMEOUT: Duration = Duration::from_secs(10); pub(crate) const FAST_RETRY_DELAY: Duration = Duration::from_millis(20); @@ -529,24 +526,13 @@ pub(crate) struct HandlerTestContext { pub(crate) peer_stats_rx: UnboundedReceiver, pub(crate) connection_events_rx: UnboundedReceiver, events_tx: Option>, - event_router: GatewayEventRouter, - dispatch_task: JoinHandle<()>, pub(crate) mock_gateway: Option, handler_task: Option>>, } impl HandlerTestContext { pub(crate) async fn new(options: PgConnectOptions) -> Self { - Self::new_with_event_router(options, GatewayEventRouter::default()).await - } - - /// Contexts sharing a router receive each other's events. - pub(crate) async fn new_with_event_router( - options: PgConnectOptions, - event_router: GatewayEventRouter, - ) -> Self { let (events_tx, events_rx) = mpsc::unbounded_channel(); - let dispatch_task = tokio::spawn(event_router.clone().run(events_rx)); let pool = setup_pool(options).await; initialize_current_settings(&pool) .await @@ -560,7 +546,7 @@ impl HandlerTestContext { let mut handler = GatewayHandler::new_with_test_socket( gateway.clone(), pool.clone(), - event_router.clone(), + events_rx, connection_events_tx, peer_stats_tx, certs_rx, @@ -579,17 +565,11 @@ impl HandlerTestContext { peer_stats_rx, connection_events_rx, events_tx: Some(events_tx), - event_router, - dispatch_task, mock_gateway: Some(mock_gateway), handler_task: Some(handler_task), } } - pub(crate) fn is_events_handler_registered(&self) -> bool { - self.event_router.registered(self.gateway.id).is_some() - } - pub(crate) fn events_tx(&self) -> &UnboundedSender { self.events_tx .as_ref() @@ -642,28 +622,9 @@ impl HandlerTestContext { } pub(crate) async fn complete_config_handshake(&mut self) -> Gateway { - let initial_events_tx = self.event_router.registered(self.gateway.id); self.mock_gateway().send_config_request(); let _ = self.mock_gateway_mut().recv_outbound().await; - let connected_gateway = - wait_for_gateway_connection_state(&self.pool, self.gateway.id, true).await; - timeout(TEST_TIMEOUT, async { - while self - .event_router - .registered(self.gateway.id) - .is_none_or(|events_tx| { - initial_events_tx - .as_ref() - .is_some_and(|initial| initial.same_channel(&events_tx)) - }) - { - sleep(Duration::from_millis(20)).await; - } - }) - .await - .expect("timed out waiting for gateway updates handler registration"); - yield_now().await; - connected_gateway + wait_for_gateway_connection_state(&self.pool, self.gateway.id, true).await } pub(crate) async fn finish(mut self) -> MockGatewayHarness { @@ -708,7 +669,6 @@ impl HandlerTestContext { impl Drop for HandlerTestContext { fn drop(&mut self) { - self.dispatch_task.abort(); if let Some(handler_task) = self.handler_task.take() { handler_task.abort(); } diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler.rs index aeeb5044a7..85e705797d 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler.rs @@ -21,10 +21,7 @@ use self::support::{ create_device_info_for_current_network, enable_internal_mfa_for_network, enable_linux_posture_for_network, expected_keepalive_interval, panic_unexpected, parse_test_ip, }; -use crate::{ - GatewayEventRouter, - tests::common::{HandlerTestContext, build_peer_stats, reload_gateway}, -}; +use crate::tests::common::{HandlerTestContext, build_peer_stats, reload_gateway}; include!("handler/handshake.rs"); include!("handler/lifecycle.rs"); diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs index d0b5d21e7f..ee378a37a9 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs @@ -118,11 +118,8 @@ async fn test_only_matching_handler_receives_network_modified_update( _: PgPoolOptions, options: PgConnectOptions, ) { - let event_router = GatewayEventRouter::default(); - let mut matching_context = - HandlerTestContext::new_with_event_router(options.clone(), event_router.clone()).await; - let mut unrelated_context = - HandlerTestContext::new_with_event_router(options, event_router).await; + let mut matching_context = HandlerTestContext::new(options.clone()).await; + let mut unrelated_context = HandlerTestContext::new(options).await; assert_ne!(matching_context.network.id, unrelated_context.network.id); @@ -143,15 +140,19 @@ async fn test_only_matching_handler_receives_network_modified_update( modified_network.mtu = 1400; modified_network.fwmark = 7; + // Every handler receives all events. + let event = GatewayCommand::NetworkModified( + matching_context.network.id, + modified_network, + Vec::new(), + None, + ); + assert_send_ok!( + unrelated_context.events_tx().send(event.clone()), + "failed to send modified gateway event" + ); assert_send_ok!( - matching_context - .events_tx() - .send(GatewayCommand::NetworkModified( - matching_context.network.id, - modified_network, - Vec::new(), - None, - )), + matching_context.events_tx().send(event), "failed to send modified gateway event" ); diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/support.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/support.rs index 1071894a92..8ed8698c95 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/support.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/support.rs @@ -269,8 +269,6 @@ pub(crate) async fn assert_device_event_is_ignored_before_config_handshake( build_event: fn(DeviceInfo) -> GatewayCommand, ) { let mut context = HandlerTestContext::new(options).await; - assert!(!context.is_events_handler_registered()); - let device_info = create_device_info_for_current_network(&context, device_name, device_pubkey, device_ip) .await; diff --git a/crates/defguard_gateway_manager/src/updates.rs b/crates/defguard_gateway_manager/src/updates.rs index 37d723e123..4c0b1b7f84 100644 --- a/crates/defguard_gateway_manager/src/updates.rs +++ b/crates/defguard_gateway_manager/src/updates.rs @@ -13,7 +13,7 @@ use defguard_proto::{ gateway::{Configuration, CoreResponse, Peer, Update, UpdateType, core_response, update}, }; use sqlx::PgPool; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio::sync::mpsc::UnboundedSender; use tonic::{Code, Status}; /// Helper struct for handling gateway events. @@ -23,7 +23,6 @@ pub(crate) struct GatewayUpdatesHandler { gateway_name: String, pool: Option, pub(crate) session_authorization_required: bool, - events_rx: UnboundedReceiver, tx: UnboundedSender, } @@ -34,7 +33,6 @@ impl GatewayUpdatesHandler { network: WireguardNetwork, gateway_name: String, pool: Option, - events_rx: UnboundedReceiver, tx: UnboundedSender, ) -> Self { Self { @@ -43,7 +41,6 @@ impl GatewayUpdatesHandler { gateway_name, pool, session_authorization_required: false, - events_rx, tx, } } @@ -142,143 +139,134 @@ impl GatewayUpdatesHandler { self.send_peer_update(peer, update_type) } - /// Process incoming Gateway events - /// - /// Every handler receives all events, so it must skip those for other networks. - pub(crate) async fn run(&mut self) { + /// Must be called before handling events. + pub(crate) async fn start(&mut self) { info!( "Starting update stream to gateway: {}, network {}", self.gateway_name, self.network ); self.refresh_session_authorization_required().await; - while let Some(update) = self.events_rx.recv().await { - debug!("Received WireGuard update: {update:?}"); - let result = match update { - GatewayCommand::NetworkCreated(network_id, network) => { - if network_id == self.network_id { - self.send_network_update(&network, Vec::new(), None, UpdateType::Create) - } else { - Ok(()) - } + } + + /// Process incoming Gateway event + /// + /// Every handler receives all events, so it must skip those for other networks. + pub(crate) async fn handle_event(&mut self, update: GatewayCommand) -> Result<(), Status> { + debug!("Received WireGuard update: {update:?}"); + match update { + GatewayCommand::NetworkCreated(network_id, network) => { + if network_id == self.network_id { + self.send_network_update(&network, Vec::new(), None, UpdateType::Create) + } else { + Ok(()) } - GatewayCommand::NetworkModified( - network_id, - network, - peers, - maybe_firewall_config, - ) => { - if network_id == self.network_id { - let result = self.send_network_update( - &network, - peers, - maybe_firewall_config, - UpdateType::Modify, - ); - // update stored network data - self.network = network; - self.refresh_session_authorization_required().await; - result - } else { - Ok(()) - } + } + GatewayCommand::NetworkModified(network_id, network, peers, maybe_firewall_config) => { + if network_id == self.network_id { + let result = self.send_network_update( + &network, + peers, + maybe_firewall_config, + UpdateType::Modify, + ); + // update stored network data + self.network = network; + self.refresh_session_authorization_required().await; + result + } else { + Ok(()) } - GatewayCommand::NetworkDeleted(network_id, network_name) => { - if network_id == self.network_id { - self.send_network_delete(&network_name) - } else { - Ok(()) - } + } + GatewayCommand::NetworkDeleted(network_id, network_name) => { + if network_id == self.network_id { + self.send_network_delete(&network_name) + } else { + Ok(()) } - GatewayCommand::DeviceCreated(device) => { - // check if a peer has to be added in the current network - match device - .network_info - .iter() - .find(|info| info.network_id == self.network_id) - { - Some(network_info) => self.send_runtime_device_update( - &device.device.name, - device.device.wireguard_pubkey, - network_info, - UpdateType::Create, - ), - None => Ok(()), - } + } + GatewayCommand::DeviceCreated(device) => { + // check if a peer has to be added in the current network + match device + .network_info + .iter() + .find(|info| info.network_id == self.network_id) + { + Some(network_info) => self.send_runtime_device_update( + &device.device.name, + device.device.wireguard_pubkey, + network_info, + UpdateType::Create, + ), + None => Ok(()), } - GatewayCommand::DeviceModified(device) => { - // check if a peer has to be updated in the current network - match device - .network_info - .iter() - .find(|info| info.network_id == self.network_id) - { - Some(network_info) => self.send_runtime_device_update( - &device.device.name, - device.device.wireguard_pubkey, - network_info, - UpdateType::Modify, - ), - None => Ok(()), - } + } + GatewayCommand::DeviceModified(device) => { + // check if a peer has to be updated in the current network + match device + .network_info + .iter() + .find(|info| info.network_id == self.network_id) + { + Some(network_info) => self.send_runtime_device_update( + &device.device.name, + device.device.wireguard_pubkey, + network_info, + UpdateType::Modify, + ), + None => Ok(()), } - GatewayCommand::DeviceDeleted(device) => { - // check if a peer has to be updated in the current network - match device - .network_info - .iter() - .find(|info| info.network_id == self.network_id) - { - Some(_) => self.send_peer_delete(&device.device.wireguard_pubkey), - None => Ok(()), - } + } + GatewayCommand::DeviceDeleted(device) => { + // check if a peer has to be updated in the current network + match device + .network_info + .iter() + .find(|info| info.network_id == self.network_id) + { + Some(_) => self.send_peer_delete(&device.device.wireguard_pubkey), + None => Ok(()), } - GatewayCommand::FirewallConfigChanged(location_id, firewall_config) => { - if location_id == self.network_id { - self.send_firewall_update(firewall_config) - } else { - Ok(()) - } + } + GatewayCommand::FirewallConfigChanged(location_id, firewall_config) => { + if location_id == self.network_id { + self.send_firewall_update(firewall_config) + } else { + Ok(()) } - GatewayCommand::FirewallDisabled(location_id) => { - if location_id == self.network_id { - self.send_firewall_disable() - } else { - Ok(()) - } + } + GatewayCommand::FirewallDisabled(location_id) => { + if location_id == self.network_id { + self.send_firewall_disable() + } else { + Ok(()) } - GatewayCommand::VpnSessionDeauthorized(location_id, device) => { - if location_id == self.network_id { - self.send_peer_delete(&device.wireguard_pubkey) - } else { - Ok(()) - } + } + GatewayCommand::VpnSessionDeauthorized(location_id, device) => { + if location_id == self.network_id { + self.send_peer_delete(&device.wireguard_pubkey) + } else { + Ok(()) } - GatewayCommand::VpnSessionAuthorized(location_id, device, network_info) => { - if location_id == self.network_id { - if network_info.network_id != location_id { - error!( - "Received VPN authorization success event for location {location_id} with invalid runtime network info: {network_info:?}" - ); - continue; - } - - self.send_runtime_device_update( - &device.name, - device.wireguard_pubkey, - &network_info, - UpdateType::Create, - ) - } else { - Ok(()) + } + GatewayCommand::VpnSessionAuthorized(location_id, device, network_info) => { + if location_id == self.network_id { + if network_info.network_id != location_id { + error!( + "Received VPN authorization success event for location {location_id} \ + with invalid runtime network info: {network_info:?}" + ); + return Ok(()); } + + self.send_runtime_device_update( + &device.name, + device.wireguard_pubkey, + &network_info, + UpdateType::Create, + ) + } else { + Ok(()) } - }; - if result.is_err() { - error!( - "Closing update steam to gateway: {}, network {}", - self.gateway_name, self.network - ); - break; } } } From a0600c290a8c40ab481e8c2b0479bad0894f99e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 12 Oct 2026 11:57:35 +0200 Subject: [PATCH 08/14] cleanup --- crates/defguard_core/src/enterprise/db/models/acl.rs | 3 +-- crates/defguard_gateway_manager/src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index 3448e92996..4770546c64 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -575,8 +575,7 @@ impl AclRule { } info!("Applied {} ACL rules: {rules:?}", rules.len()); - let affected_locations: Vec> = - affected_locations.into_iter().collect(); + let affected_locations = affected_locations.into_iter().collect::>(); debug!( "{} locations affected by applied ACL rules. Sending gateway firewall update events \ for each location", diff --git a/crates/defguard_gateway_manager/src/lib.rs b/crates/defguard_gateway_manager/src/lib.rs index 5307eadd80..12c1214960 100644 --- a/crates/defguard_gateway_manager/src/lib.rs +++ b/crates/defguard_gateway_manager/src/lib.rs @@ -695,8 +695,8 @@ fn needs_restart(old: &Gateway, new: &Gateway) -> bool { || old.core_client_cert_key_der != new.core_client_cert_key_der } -/// Shared set of outbound channels that gateway instances use to forward -/// events, notifications, and side effects to Core components. +/// Shared set of outbound channels that gateway instances use to forward events, notifications, +/// and side effects to Core components. #[derive(Clone)] pub struct GatewayTxSet { peer_stats: UnboundedSender, @@ -706,7 +706,7 @@ pub struct GatewayTxSet { impl GatewayTxSet { #[must_use] pub fn new(peer_stats: UnboundedSender) -> Self { - let (connection_events, _receiver) = unbounded_channel(); + let (connection_events, _) = unbounded_channel(); Self { peer_stats, connection_events, From e1abbfe04ca009a1a4644b4f0f854b5a6e177bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 12 Oct 2026 12:13:52 +0200 Subject: [PATCH 09/14] Simplify GatewayCommand --- crates/defguard_common/src/gateway_event.rs | 3 +- .../src/enterprise/handlers/device_posture.rs | 1 - .../defguard_core/src/handlers/wireguard.rs | 6 +- crates/defguard_core/src/utility_thread.rs | 5 +- .../tests/integration/api/device_posture.rs | 4 +- .../gateway_manager/handler/network_events.rs | 56 ++++++++----------- .../defguard_gateway_manager/src/updates.rs | 8 +-- 7 files changed, 36 insertions(+), 47 deletions(-) diff --git a/crates/defguard_common/src/gateway_event.rs b/crates/defguard_common/src/gateway_event.rs index be5d76a1f2..7008d92b62 100644 --- a/crates/defguard_common/src/gateway_event.rs +++ b/crates/defguard_common/src/gateway_event.rs @@ -25,9 +25,8 @@ use crate::{ /// protobuf wire types happens at the serialization boundary in the gateway manager. #[derive(Clone, Debug)] pub enum GatewayCommand { - NetworkCreated(Id, WireguardNetwork), + NetworkCreated(WireguardNetwork), NetworkModified( - Id, WireguardNetwork, Vec, Option, diff --git a/crates/defguard_core/src/enterprise/handlers/device_posture.rs b/crates/defguard_core/src/enterprise/handlers/device_posture.rs index aada3ec1fa..157175e2af 100644 --- a/crates/defguard_core/src/enterprise/handlers/device_posture.rs +++ b/crates/defguard_core/src/enterprise/handlers/device_posture.rs @@ -65,7 +65,6 @@ async fn build_location_peer_refresh_commands( let peers = get_location_allowed_peers(&location, &mut *conn).await?; let maybe_firewall_config = try_get_location_firewall_config(&location, &mut *conn).await?; commands.push(GatewayCommand::NetworkModified( - location.id, location, peers, maybe_firewall_config, diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index 795c7ac9eb..86fad99135 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -471,7 +471,7 @@ pub(crate) async fn create_network( } transaction.commit().await?; - appstate.send_gateway_command(GatewayCommand::NetworkCreated(network.id, network.clone())); + appstate.send_gateway_command(GatewayCommand::NetworkCreated(network.clone())); info!( "User {} created WireGuard network {network_name}", @@ -701,7 +701,7 @@ pub(crate) async fn modify_network( let maybe_firewall_config = try_get_location_firewall_config(&network, &mut transaction).await?; let gateway_command = - GatewayCommand::NetworkModified(network.id, network.clone(), peers, maybe_firewall_config); + GatewayCommand::NetworkModified(network.clone(), peers, maybe_firewall_config); // commit DB transaction transaction.commit().await?; @@ -1034,7 +1034,7 @@ pub(crate) async fn import_network( .await?; info!("New network {network} created"); - appstate.send_gateway_command(GatewayCommand::NetworkCreated(network.id, network.clone())); + appstate.send_gateway_command(GatewayCommand::NetworkCreated(network.clone())); let reserved_ips = imported_devices .iter() diff --git a/crates/defguard_core/src/utility_thread.rs b/crates/defguard_core/src/utility_thread.rs index e783da86a9..a1601d5f19 100644 --- a/crates/defguard_core/src/utility_thread.rs +++ b/crates/defguard_core/src/utility_thread.rs @@ -258,7 +258,6 @@ async fn license_status_check( let disable_firewall = location.acl_enabled && firewall_config.is_none(); let location_id = location.id; gateway_tx.send(GatewayCommand::NetworkModified( - location_id, location, peers, firewall_config, @@ -577,8 +576,8 @@ mod test { license_status_check(pool, gateway_tx).await.unwrap(); while let Ok(command) = gateway_rx.try_recv() { - if let GatewayCommand::NetworkModified(id, _, peers, _) = command - && id == location_id + if let GatewayCommand::NetworkModified(location, peers, _) = command + && location.id == location_id { return peers; } diff --git a/crates/defguard_core/tests/integration/api/device_posture.rs b/crates/defguard_core/tests/integration/api/device_posture.rs index 5d72133e75..35e66daa20 100644 --- a/crates/defguard_core/tests/integration/api/device_posture.rs +++ b/crates/defguard_core/tests/integration/api/device_posture.rs @@ -59,10 +59,10 @@ fn expect_network_modified_peers( expected_pubkeys: &[&str], ) { let event = gateway_rx.try_recv().unwrap(); - let GatewayCommand::NetworkModified(id, _, peers, _) = event else { + let GatewayCommand::NetworkModified(location, peers, _) = event else { panic!("expected NetworkModified, got {event:?}"); }; - assert_eq!(id, location_id); + assert_eq!(location.id, location_id); let actual_pubkeys = peers .iter() .map(|peer| peer.pubkey.as_str()) diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs index ee378a37a9..a5c66790ca 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/handler/network_events.rs @@ -33,11 +33,9 @@ async fn test_matching_location_network_modified_event_produces_modify_update( let mut modified_network = context .network .clone() - .set_address([ - "10.20.0.1/24" - .parse() - .expect("failed to parse modified network address"), - ]) + .set_address(["10.20.0.1/24" + .parse() + .expect("failed to parse modified network address")]) .expect("failed to set modified network address"); modified_network.name = format!("{}-modified", context.network.name); modified_network.port = 51821; @@ -46,7 +44,6 @@ async fn test_matching_location_network_modified_event_produces_modify_update( assert_send_ok!( context.events_tx().send(GatewayCommand::NetworkModified( - context.network.id, modified_network, Vec::new(), None, @@ -80,11 +77,9 @@ async fn test_matching_location_network_created_event_produces_create_update( let mut created_network = context .network .clone() - .set_address([ - "10.40.0.1/24" - .parse() - .expect("failed to parse created network address"), - ]) + .set_address(["10.40.0.1/24" + .parse() + .expect("failed to parse created network address")]) .expect("failed to set created network address"); created_network.name = format!("{}-created", context.network.name); created_network.port = 51841; @@ -92,10 +87,9 @@ async fn test_matching_location_network_created_event_produces_create_update( created_network.fwmark = 17; assert_send_ok!( - context.events_tx().send(GatewayCommand::NetworkCreated( - context.network.id, - created_network, - )), + context + .events_tx() + .send(GatewayCommand::NetworkCreated(created_network)), "failed to send created gateway event" ); @@ -129,11 +123,9 @@ async fn test_only_matching_handler_receives_network_modified_update( let mut modified_network = matching_context .network .clone() - .set_address([ - "10.30.0.1/24" - .parse() - .expect("failed to parse modified network address"), - ]) + .set_address(["10.30.0.1/24" + .parse() + .expect("failed to parse modified network address")]) .expect("failed to set modified network address"); modified_network.name = format!("{}-modified", matching_context.network.name); modified_network.port = 51831; @@ -141,12 +133,7 @@ async fn test_only_matching_handler_receives_network_modified_update( modified_network.fwmark = 7; // Every handler receives all events. - let event = GatewayCommand::NetworkModified( - matching_context.network.id, - modified_network, - Vec::new(), - None, - ); + let event = GatewayCommand::NetworkModified(modified_network, Vec::new(), None); assert_send_ok!( unrelated_context.events_tx().send(event.clone()), "failed to send modified gateway event" @@ -165,8 +152,14 @@ async fn test_only_matching_handler_receives_network_modified_update( 1400, 7, ); - matching_context.mock_gateway_mut().expect_no_outbound().await; - unrelated_context.mock_gateway_mut().expect_no_outbound().await; + matching_context + .mock_gateway_mut() + .expect_no_outbound() + .await; + unrelated_context + .mock_gateway_mut() + .expect_no_outbound() + .await; matching_context .finish() @@ -191,10 +184,9 @@ async fn test_different_location_network_created_event_is_ignored( let _ = context.complete_config_handshake().await; assert_send_ok!( - context.events_tx().send(GatewayCommand::NetworkCreated( - other_network.id, - other_network, - )), + context + .events_tx() + .send(GatewayCommand::NetworkCreated(other_network)), "failed to send unrelated created gateway event" ); diff --git a/crates/defguard_gateway_manager/src/updates.rs b/crates/defguard_gateway_manager/src/updates.rs index 4c0b1b7f84..dcd6bcd58c 100644 --- a/crates/defguard_gateway_manager/src/updates.rs +++ b/crates/defguard_gateway_manager/src/updates.rs @@ -154,15 +154,15 @@ impl GatewayUpdatesHandler { pub(crate) async fn handle_event(&mut self, update: GatewayCommand) -> Result<(), Status> { debug!("Received WireGuard update: {update:?}"); match update { - GatewayCommand::NetworkCreated(network_id, network) => { - if network_id == self.network_id { + GatewayCommand::NetworkCreated(network) => { + if network.id == self.network_id { self.send_network_update(&network, Vec::new(), None, UpdateType::Create) } else { Ok(()) } } - GatewayCommand::NetworkModified(network_id, network, peers, maybe_firewall_config) => { - if network_id == self.network_id { + GatewayCommand::NetworkModified(network, peers, maybe_firewall_config) => { + if network.id == self.network_id { let result = self.send_network_update( &network, peers, From 06cd702b1125879a5c6e7e3308e872313038f879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 12 Oct 2026 12:46:25 +0200 Subject: [PATCH 10/14] Simplify GatewayUpdatesHandler --- .../defguard_gateway_manager/src/handler.rs | 1 - .../src/handler/tests.rs | 4 +-- .../defguard_gateway_manager/src/updates.rs | 29 +++++++++---------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index 3255d62351..edb4428266 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -569,7 +569,6 @@ impl GatewayHandler { config_sent = true; self.mark_connected_and_maybe_notify(&network.name).await; let mut handler = GatewayUpdatesHandler::new( - self.gateway.location_id, network, self.gateway.name.clone(), Some(self.pool.clone()), diff --git a/crates/defguard_gateway_manager/src/handler/tests.rs b/crates/defguard_gateway_manager/src/handler/tests.rs index ff54b061d6..34a465ab9a 100644 --- a/crates/defguard_gateway_manager/src/handler/tests.rs +++ b/crates/defguard_gateway_manager/src/handler/tests.rs @@ -211,9 +211,7 @@ fn test_handler(mfa_enabled: bool) -> GatewayUpdatesHandler { let network = test_network(mfa_enabled); let (tx, _rx) = unbounded_channel(); - let mut handler = GatewayUpdatesHandler::new(network.id, network, "gateway".into(), None, tx); - handler.session_authorization_required = handler.network.mfa_enabled; - handler + GatewayUpdatesHandler::new(network, "gateway".into(), None, tx) } #[test] diff --git a/crates/defguard_gateway_manager/src/updates.rs b/crates/defguard_gateway_manager/src/updates.rs index dcd6bcd58c..ec3894b04f 100644 --- a/crates/defguard_gateway_manager/src/updates.rs +++ b/crates/defguard_gateway_manager/src/updates.rs @@ -18,8 +18,7 @@ use tonic::{Code, Status}; /// Helper struct for handling gateway events. pub(crate) struct GatewayUpdatesHandler { - network_id: Id, - pub(crate) network: WireguardNetwork, + network: WireguardNetwork, gateway_name: String, pool: Option, pub(crate) session_authorization_required: bool, @@ -29,18 +28,18 @@ pub(crate) struct GatewayUpdatesHandler { impl GatewayUpdatesHandler { #[must_use] pub(crate) fn new( - network_id: Id, network: WireguardNetwork, gateway_name: String, pool: Option, tx: UnboundedSender, ) -> Self { + // Sane default. + let session_authorization_required = network.mfa_enabled; Self { - network_id, network, gateway_name, pool, - session_authorization_required: false, + session_authorization_required, tx, } } @@ -155,14 +154,14 @@ impl GatewayUpdatesHandler { debug!("Received WireGuard update: {update:?}"); match update { GatewayCommand::NetworkCreated(network) => { - if network.id == self.network_id { + if network.id == self.network.id { self.send_network_update(&network, Vec::new(), None, UpdateType::Create) } else { Ok(()) } } GatewayCommand::NetworkModified(network, peers, maybe_firewall_config) => { - if network.id == self.network_id { + if network.id == self.network.id { let result = self.send_network_update( &network, peers, @@ -178,7 +177,7 @@ impl GatewayUpdatesHandler { } } GatewayCommand::NetworkDeleted(network_id, network_name) => { - if network_id == self.network_id { + if network_id == self.network.id { self.send_network_delete(&network_name) } else { Ok(()) @@ -189,7 +188,7 @@ impl GatewayUpdatesHandler { match device .network_info .iter() - .find(|info| info.network_id == self.network_id) + .find(|info| info.network_id == self.network.id) { Some(network_info) => self.send_runtime_device_update( &device.device.name, @@ -205,7 +204,7 @@ impl GatewayUpdatesHandler { match device .network_info .iter() - .find(|info| info.network_id == self.network_id) + .find(|info| info.network_id == self.network.id) { Some(network_info) => self.send_runtime_device_update( &device.device.name, @@ -221,35 +220,35 @@ impl GatewayUpdatesHandler { match device .network_info .iter() - .find(|info| info.network_id == self.network_id) + .find(|info| info.network_id == self.network.id) { Some(_) => self.send_peer_delete(&device.device.wireguard_pubkey), None => Ok(()), } } GatewayCommand::FirewallConfigChanged(location_id, firewall_config) => { - if location_id == self.network_id { + if location_id == self.network.id { self.send_firewall_update(firewall_config) } else { Ok(()) } } GatewayCommand::FirewallDisabled(location_id) => { - if location_id == self.network_id { + if location_id == self.network.id { self.send_firewall_disable() } else { Ok(()) } } GatewayCommand::VpnSessionDeauthorized(location_id, device) => { - if location_id == self.network_id { + if location_id == self.network.id { self.send_peer_delete(&device.wireguard_pubkey) } else { Ok(()) } } GatewayCommand::VpnSessionAuthorized(location_id, device, network_info) => { - if location_id == self.network_id { + if location_id == self.network.id { if network_info.network_id != location_id { error!( "Received VPN authorization success event for location {location_id} \ From 11ace984b33a1f3c2ebe543c793192dcf8af3833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 12 Oct 2026 18:03:42 +0200 Subject: [PATCH 11/14] optimise --- Cargo.lock | 8 +- crates/defguard_common/src/db/models/group.rs | 2 +- .../src/enterprise/allowed_ips/tests.rs | 8 +- crates/defguard_core/src/events.rs | 4 +- .../defguard_gateway_manager/src/handler.rs | 12 +-- .../src/handler/tests.rs | 6 +- .../defguard_gateway_manager/src/updates.rs | 73 ++++++++++--------- .../tests/proxy_manager/handler/mfa_config.rs | 15 ++-- .../handler/mfa_config_authorize.rs | 4 +- .../tests/proxy_manager/handler/polling.rs | 4 +- 10 files changed, 69 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0208a425d..4b7576dd15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3852,9 +3852,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.29" +version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +checksum = "f710a23e6dbf193214fd46ca56a9d6864e550abe86202184532ae7275e46de19" dependencies = [ "cc", "libc", @@ -3876,9 +3876,9 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" [[package]] name = "lock_api" diff --git a/crates/defguard_common/src/db/models/group.rs b/crates/defguard_common/src/db/models/group.rs index 7ba5d6cc42..ac4e340b2c 100644 --- a/crates/defguard_common/src/db/models/group.rs +++ b/crates/defguard_common/src/db/models/group.rs @@ -232,7 +232,7 @@ mod test { assert!(!user.remove_from_group(&pool, &group).await.unwrap()); let members = group.member_usernames(&pool).await.unwrap(); - assert!(members.is_empty()); + assert_eq!(members, [] as [String; 0]); } #[sqlx::test] diff --git a/crates/defguard_core/src/enterprise/allowed_ips/tests.rs b/crates/defguard_core/src/enterprise/allowed_ips/tests.rs index 7747d3d0ce..c1b6dbc6c2 100644 --- a/crates/defguard_core/src/enterprise/allowed_ips/tests.rs +++ b/crates/defguard_core/src/enterprise/allowed_ips/tests.rs @@ -191,7 +191,7 @@ async fn test_explicit_user_allow(_: PgPoolOptions, options: PgConnectOptions) { let result = get_allowed_ips_from_acl_rules(&mut conn, &location, &other_user) .await .unwrap(); - assert!(result.is_empty()); + assert_eq!(result, [] as [ipnetwork::IpNetwork; 0]); } #[sqlx::test] @@ -253,7 +253,7 @@ async fn test_group_membership_allow(_: PgPoolOptions, options: PgConnectOptions let result = get_allowed_ips_from_acl_rules(&mut conn, &location, &non_member) .await .unwrap(); - assert!(result.is_empty()); + assert_eq!(result, [] as [ipnetwork::IpNetwork; 0]); } #[sqlx::test] @@ -546,7 +546,7 @@ async fn test_non_matching_rule_excluded(_: PgPoolOptions, options: PgConnectOpt let result = get_allowed_ips_from_acl_rules(&mut conn, &location, &user) .await .unwrap(); - assert!(result.is_empty()); + assert_eq!(result, [] as [ipnetwork::IpNetwork; 0]); } #[sqlx::test] @@ -699,7 +699,7 @@ async fn test_predefined_destination_included(_: PgPoolOptions, options: PgConne .await .unwrap(); - assert_eq!(result, vec!["172.16.0.0/12".parse().unwrap()]); + assert_eq!(result, ["172.16.0.0/12".parse().unwrap()]); } #[sqlx::test] diff --git a/crates/defguard_core/src/events.rs b/crates/defguard_core/src/events.rs index 69f0cf29d9..e56f43f670 100644 --- a/crates/defguard_core/src/events.rs +++ b/crates/defguard_core/src/events.rs @@ -512,7 +512,7 @@ pub enum DesktopClientMfaEvent { }, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub enum GatewayConnectionEvent { Connected { gateway_id: Id, @@ -524,7 +524,7 @@ pub enum GatewayConnectionEvent { }, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub enum ProxyConnectionEvent { Connected { proxy_id: Id, proxy_name: String }, Disconnected { proxy_id: Id, proxy_name: String }, diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index edb4428266..7e98935cdb 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -170,7 +170,7 @@ impl GatewayHandler { if let Err(()) = url.set_scheme("http") { return Err(GatewayError::EndpointError(format!( - "Failed to set http scheme for Gateway URL {:?}", + "Failed to set HTTP scheme for Gateway URL {:?}", self.url ))); } @@ -201,7 +201,7 @@ impl GatewayHandler { let mut network = WireguardNetwork::find_by_id(&mut *conn, network_id) .await? .ok_or_else(|| { - GatewayError::NotFound(format!("Network with id {network_id} not found")) + GatewayError::NotFound(format!("Network with ID {network_id} not found")) })?; debug!( @@ -540,7 +540,7 @@ impl GatewayHandler { }; match message { Ok(None) => { - info!("Stream was closed by the sender."); + info!("Stream was closed by the sender"); self.handle_stream_disconnection( &clients, retry_on_connect_failure, @@ -550,7 +550,7 @@ impl GatewayHandler { return Ok(()); } Ok(Some(received)) => { - info!("Received message from Gateway."); + info!("Received message from Gateway"); debug!("Message from Gateway {uri}"); match received.payload { @@ -570,8 +570,8 @@ impl GatewayHandler { self.mark_connected_and_maybe_notify(&network.name).await; let mut handler = GatewayUpdatesHandler::new( network, - self.gateway.name.clone(), - Some(self.pool.clone()), + &self.gateway.name, + Some(&self.pool), tx.clone(), ); handler.start().await; diff --git a/crates/defguard_gateway_manager/src/handler/tests.rs b/crates/defguard_gateway_manager/src/handler/tests.rs index 34a465ab9a..b2210a597d 100644 --- a/crates/defguard_gateway_manager/src/handler/tests.rs +++ b/crates/defguard_gateway_manager/src/handler/tests.rs @@ -207,11 +207,11 @@ fn gen_config_preserves_absent_firewall_config_and_empty_peers() { assert!(config.firewall_config.is_none()); } -fn test_handler(mfa_enabled: bool) -> GatewayUpdatesHandler { +fn test_handler<'a>(mfa_enabled: bool) -> GatewayUpdatesHandler<'a> { let network = test_network(mfa_enabled); let (tx, _rx) = unbounded_channel(); - GatewayUpdatesHandler::new(network, "gateway".into(), None, tx) + GatewayUpdatesHandler::new(network, "gateway", None, tx) } #[test] @@ -269,7 +269,7 @@ fn test_runtime_peer_update_preserves_session_preshared_key_for_authorized_mfa_p #[test] fn test_runtime_peer_update_preserves_session_preshared_key_for_authorized_posture_peer() { let mut handler = test_handler(false); - handler.session_authorization_required = true; + handler.set_session_authorization_required(true); let peer = handler .runtime_peer_update( diff --git a/crates/defguard_gateway_manager/src/updates.rs b/crates/defguard_gateway_manager/src/updates.rs index ec3894b04f..84144219b3 100644 --- a/crates/defguard_gateway_manager/src/updates.rs +++ b/crates/defguard_gateway_manager/src/updates.rs @@ -17,23 +17,23 @@ use tokio::sync::mpsc::UnboundedSender; use tonic::{Code, Status}; /// Helper struct for handling gateway events. -pub(crate) struct GatewayUpdatesHandler { +pub(crate) struct GatewayUpdatesHandler<'a> { network: WireguardNetwork, - gateway_name: String, - pool: Option, - pub(crate) session_authorization_required: bool, + gateway_name: &'a str, + pool: Option<&'a PgPool>, + session_authorization_required: bool, tx: UnboundedSender, } -impl GatewayUpdatesHandler { +impl<'a> GatewayUpdatesHandler<'a> { #[must_use] pub(crate) fn new( network: WireguardNetwork, - gateway_name: String, - pool: Option, + gateway_name: &'a str, + pool: Option<&'a PgPool>, tx: UnboundedSender, ) -> Self { - // Sane default. + // Sane default. It will be updated in `start()`. let session_authorization_required = network.mfa_enabled; Self { network, @@ -44,30 +44,32 @@ impl GatewayUpdatesHandler { } } - async fn authorization_required_for( - network: &WireguardNetwork, - pool: Option<&PgPool>, - ) -> bool { - if network.mfa_enabled { - return true; + #[cfg(test)] + pub(crate) fn set_session_authorization_required(&mut self, value: bool) { + self.session_authorization_required = value; + } + + async fn refresh_session_authorization_required(&mut self) { + if self.network.mfa_enabled { + self.session_authorization_required = true; + return; } - let Some(pool) = pool else { - return false; + let Some(pool) = self.pool else { + self.session_authorization_required = false; + return; }; - match network.has_postures(pool).await { + self.session_authorization_required = match self.network.has_postures(pool).await { Ok(has_postures) => has_postures, Err(err) => { - error!("Failed to fetch postures for location {network}: {err}"); + error!( + "Failed to fetch postures for location {}: {err}", + self.network + ); false } - } - } - - async fn refresh_session_authorization_required(&mut self) { - self.session_authorization_required = - Self::authorization_required_for(&self.network, self.pool.as_ref()).await; + }; } #[must_use] @@ -90,7 +92,8 @@ impl GatewayUpdatesHandler { if !is_authorized { debug!( - "Skipping gateway peer update for WireGuard device {peer_label} in runtime-authorized location {} because there is no active VPN session", + "Skipping gateway peer update for WireGuard device {peer_label} in \ + runtime-authorized location {} because there is no active VPN session", self.network.name ); return None; @@ -98,7 +101,8 @@ impl GatewayUpdatesHandler { let Some(preshared_key) = preshared_key else { debug!( - "Skipping gateway peer update for WireGuard device {peer_label} in location {} because the runtime preshared key is missing", + "Skipping gateway peer update for WireGuard device {peer_label} in location {} \ + because the runtime preshared key is missing", self.network.name ); return None; @@ -279,8 +283,8 @@ impl GatewayUpdatesHandler { update_type: UpdateType, ) -> Result<(), Status> { debug!("Sending network update for network {network}"); - let proto_peers = peers.into_iter().map(Into::into).collect(); - let proto_firewall = firewall_config.map(ProtoFirewallConfig::from); + let peers = peers.into_iter().map(Into::into).collect(); + let firewall_config = firewall_config.map(ProtoFirewallConfig::from); if let Err(err) = self.tx.send(CoreResponse { id: 0, payload: Some(core_response::Payload::Update(Update { @@ -290,8 +294,8 @@ impl GatewayUpdatesHandler { private_key: network.prvkey.clone(), addresses: network.address().iter().map(ToString::to_string).collect(), port: network.port.cast_unsigned(), - peers: proto_peers, - firewall_config: proto_firewall, + peers, + firewall_config, mtu: network.mtu.cast_unsigned(), fwmark: network.fwmark as u32, })), @@ -302,10 +306,11 @@ impl GatewayUpdatesHandler { error: {err}", ); error!(msg); - return Err(Status::new(Code::Internal, msg)); + Err(Status::new(Code::Internal, msg)) + } else { + debug!("Network update sent for network {network}"); + Ok(()) } - debug!("Network update sent for network {network}"); - Ok(()) } /// Sends delete network command to gateway @@ -396,7 +401,7 @@ impl GatewayUpdatesHandler { "Sending firewall config update for network {} with config {firewall_config:?}", self.network ); - let proto_firewall: ProtoFirewallConfig = firewall_config.into(); + let proto_firewall = ProtoFirewallConfig::from(firewall_config); if let Err(err) = self.tx.send(CoreResponse { id: 0, payload: Some(core_response::Payload::Update(Update { diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_config.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_config.rs index ef0cfab8d1..f8f75678f9 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_config.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_config.rs @@ -30,9 +30,8 @@ async fn test_mfa_config_session_gates_factor_setup(_: PgPoolOptions, options: P let start_response = send_mfa_config_start(&mut context, &polling_token, &device.wireguard_pubkey).await; - let session = match &start_response.payload { - Some(core_response::Payload::MfaConfigStart(response)) => response, - _ => panic!("expected MfaConfigStartResponse"), + let Some(core_response::Payload::MfaConfigStart(session)) = &start_response.payload else { + panic!("expected MfaConfigStartResponse") }; assert_eq!(session.available_methods, vec![MfaMethod::Totp as i32]); assert!(!session.email_fallback); @@ -97,9 +96,8 @@ async fn test_email_fallback_enables_email_factor(_: PgPoolOptions, options: PgC let start_response = send_mfa_config_start(&mut context, &polling_token, &device.wireguard_pubkey).await; - let session = match &start_response.payload { - Some(core_response::Payload::MfaConfigStart(response)) => response, - _ => panic!("expected MfaConfigStartResponse"), + let Some(core_response::Payload::MfaConfigStart(session)) = &start_response.payload else { + panic!("expected MfaConfigStartResponse") }; assert_eq!(session.available_methods, [] as [i32; 0]); assert!(session.email_fallback); @@ -237,9 +235,8 @@ async fn test_fido2_only_user_is_not_treated_as_no_factor( let start_response = send_mfa_config_start(&mut context, &polling_token, &device.wireguard_pubkey).await; - let session = match &start_response.payload { - Some(core_response::Payload::MfaConfigStart(response)) => response, - _ => panic!("expected MfaConfigStartResponse"), + let Some(core_response::Payload::MfaConfigStart(session)) = &start_response.payload else { + panic!("expected MfaConfigStartResponse") }; assert_eq!(session.available_methods, vec![MfaMethod::Fido2 as i32]); assert!( diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_config_authorize.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_config_authorize.rs index f1787176e1..a12066547b 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_config_authorize.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa_config_authorize.rs @@ -377,8 +377,8 @@ async fn send_oidc_callback( state_payload: &str, sub: &str, ) -> CoreResponse { - let nonce = "mfa-config-oidc-nonce"; static OIDC_CALLBACK_CTR: AtomicU64 = AtomicU64::new(6250); + let nonce = "mfa-config-oidc-nonce"; context.mock_proxy().send_request(CoreRequest { id: OIDC_CALLBACK_CTR.fetch_add(1, Ordering::Relaxed), device_info: Some(make_device_info()), @@ -412,7 +412,7 @@ async fn test_oidc_authorizes_session(_: PgPoolOptions, options: PgConnectOption link_user_oidc_identity(pool, user).await; }) .await; - assert_eq!(session.available_methods, vec![MfaMethod::Oidc as i32]); + assert_eq!(session.available_methods, [MfaMethod::Oidc as i32]); assert!( !session.email_fallback, "OIDC must replace the email fallback" diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/polling.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/polling.rs index 2c8f2f9749..945168edd1 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/polling.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/polling.rs @@ -302,7 +302,7 @@ async fn test_polling_reflects_network_changes(_: PgPoolOptions, options: PgConn set_test_license_business(); let _network = create_network(&context.pool).await; - let (_user, device) = create_user_with_device(&context.pool).await; + let (user, device) = create_user_with_device(&context.pool).await; let token_str = create_polling_token(&context.pool, device.id).await; // First poll. @@ -326,7 +326,7 @@ async fn test_polling_reflects_network_changes(_: PgPoolOptions, options: PgConn // Re-run add_to_all_networks so the device gets a WireguardNetworkDevice // row for the newly created network (required for config-building). let mut conn = context.pool.acquire().await.expect("acquire connection"); - join_device_to_all_networks(&mut conn, &device, &_user) + join_device_to_all_networks(&mut conn, &device, &user) .await .expect("add device to all networks"); From a228fce02d3444da973ce463a60eeae2bce6934b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Tue, 13 Oct 2026 09:38:18 +0200 Subject: [PATCH 12/14] Re-organise --- Cargo.lock | 80 ++-- crates/defguard/src/main.rs | 2 +- crates/defguard_gateway_manager/src/lib.rs | 422 +----------------- .../defguard_gateway_manager/src/manager.rs | 422 ++++++++++++++++++ .../src/tests/common/mod.rs | 4 +- 5 files changed, 478 insertions(+), 452 deletions(-) create mode 100644 crates/defguard_gateway_manager/src/manager.rs diff --git a/Cargo.lock b/Cargo.lock index 4b7576dd15..624dd0aa95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,7 +350,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -419,9 +419,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.18.1" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" dependencies = [ "aws-lc-sys", "zeroize", @@ -429,9 +429,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" dependencies = [ "cc", "cmake", @@ -769,7 +769,7 @@ dependencies = [ "prettyplease 0.3.0", "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -1031,7 +1031,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -1368,7 +1368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" dependencies = [ "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -1516,7 +1516,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -1538,7 +1538,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core 0.24.1", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -1651,7 +1651,7 @@ dependencies = [ "serde", "serde_cbor_2", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "sqlx", "struct-patch", "thiserror 2.0.21", @@ -1726,7 +1726,7 @@ dependencies = [ "serde_qs", "serde_urlencoded", "sha-1", - "sha2 0.11.0", + "sha2 0.11.1", "sha256", "sqlx", "ssh-key", @@ -1906,7 +1906,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "sqlx", "thiserror 2.0.21", "tokio", @@ -2096,7 +2096,7 @@ checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -2223,7 +2223,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -2365,7 +2365,7 @@ dependencies = [ "ed25519 3.0.0", "rand_core 0.10.1", "serde", - "sha2 0.11.0", + "sha2 0.11.1", "signature 3.0.0", "subtle", "zeroize", @@ -2751,7 +2751,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -4055,7 +4055,7 @@ version = "0.0.0" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -4867,7 +4867,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -5201,7 +5201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -5714,7 +5714,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -5895,7 +5895,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d84e8ba78bd384263e5922f084cbe1b081c3b7e69add59c8fb097b879ba968a" dependencies = [ "globset", - "sha2 0.11.0", + "sha2 0.11.1", "walkdir", ] @@ -6040,9 +6040,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" dependencies = [ "dyn-clone", "ref-cast", @@ -6188,7 +6188,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -6275,7 +6275,7 @@ dependencies = [ "indexmap 2.14.2", "jiff", "schemars 0.9.0", - "schemars 1.2.2", + "schemars 1.2.3", "serde_core", "serde_json", "serde_with_macros", @@ -6291,7 +6291,7 @@ dependencies = [ "darling 0.24.1", "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -6403,9 +6403,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", "cpufeatures 0.3.1", @@ -6713,7 +6713,7 @@ dependencies = [ "percent-encoding", "serde", "sha1 0.11.0", - "sha2 0.11.0", + "sha2 0.11.1", "sqlx-core", "thiserror 2.0.21", "tracing", @@ -6748,7 +6748,7 @@ dependencies = [ "rand 0.10.3", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "smallvec", "sqlx-core", "stringprep", @@ -6945,9 +6945,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -6982,7 +6982,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -7093,7 +7093,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -7201,7 +7201,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] @@ -7862,7 +7862,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -8372,7 +8372,7 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", "synstructure 0.14.0", ] @@ -8413,7 +8413,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", "synstructure 0.14.0", ] @@ -8467,7 +8467,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.8", + "syn 3.0.9", ] [[package]] diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 7586100d0c..3ecd252d6f 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -41,7 +41,7 @@ use defguard_core::{ version::IncompatibleComponents, }; use defguard_event_logger::run_event_logger; -use defguard_gateway_manager::{GatewayManager, GatewayTxSet}; +use defguard_gateway_manager::{GatewayTxSet, manager::GatewayManager}; use defguard_proxy_manager::{ProxyManager, ProxyTxSet}; use defguard_session_manager::{events::SessionManagerEvent, run_session_manager}; use defguard_setup::{ diff --git a/crates/defguard_gateway_manager/src/lib.rs b/crates/defguard_gateway_manager/src/lib.rs index 12c1214960..7feb614a5e 100644 --- a/crates/defguard_gateway_manager/src/lib.rs +++ b/crates/defguard_gateway_manager/src/lib.rs @@ -1,33 +1,28 @@ +use std::time::Duration; +#[cfg(test)] use std::{ collections::HashMap, - sync::{Arc, Mutex, atomic::AtomicBool}, - time::Duration, + path::PathBuf, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, }; -#[cfg(test)] -use std::{path::PathBuf, sync::atomic::Ordering}; use defguard_common::{ - db::{ChangeNotification, Id, TriggerOperation, models::gateway::Gateway}, - gateway_event::GatewayCommand, + db::{Id, models::gateway::Gateway}, messages::peer_stats_update::PeerStatsUpdate, }; use defguard_core::events::GatewayConnectionEvent; use defguard_proto::gateway::gateway_client::GatewayClient; use defguard_version::client::ClientVersionInterceptor; -use sqlx::{PgPool, postgres::PgListener}; #[cfg(test)] use tokio::sync::Notify; use tokio::{ - sync::{ - mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, - watch, - }, - task::{AbortHandle, JoinHandle, JoinSet}, - time::sleep, + sync::mpsc::{UnboundedSender, unbounded_channel}, + task::JoinHandle, }; -use tonic::{Request, service::interceptor::InterceptedService, transport::Channel}; - -use crate::{error::GatewayError, handler::GatewayHandler}; +use tonic::{service::interceptor::InterceptedService, transport::Channel}; #[macro_use] extern crate tracing; @@ -35,12 +30,12 @@ extern crate tracing; mod certs; mod error; mod handler; +pub mod manager; mod updates; #[cfg(test)] mod tests; -const GATEWAY_TABLE_TRIGGER: &str = "gateway_change"; const GATEWAY_RECONNECT_DELAY: Duration = Duration::from_secs(5); const TEN_SECS: Duration = Duration::from_secs(10); @@ -287,399 +282,6 @@ impl GatewayManagerTestSupport { } } -pub struct GatewayManager { - clients: Arc>>, - pool: PgPool, - handlers: JoinSet>, - /// Per-Gateway flag tracking whether a disconnect email went out and is still awaiting its - /// matching reconnect email. Owned by the manager rather than the handler so it survives a - /// handler rebuild when connection-relevant fields change. - disconnect_notification_sent_by_gateway: Arc>>>, - #[cfg(test)] - test_support: GatewayManagerTestSupport, - tx: GatewayTxSet, - /// Taken by [`Self::run`]. - events_rx: Option>, - /// Per-Gateway channels feeding each `GatewayHandler`. - event_senders: Arc>>>, -} - -impl GatewayManager { - #[cfg(not(test))] - #[must_use] - pub fn new( - pool: PgPool, - tx: GatewayTxSet, - events_rx: UnboundedReceiver, - ) -> Self { - Self { - clients: Arc::default(), - handlers: JoinSet::new(), - pool, - disconnect_notification_sent_by_gateway: Arc::default(), - tx, - events_rx: Some(events_rx), - event_senders: Arc::default(), - } - } - - #[cfg(test)] - #[must_use] - fn new( - pool: PgPool, - tx: GatewayTxSet, - events_rx: UnboundedReceiver, - test_support: GatewayManagerTestSupport, - ) -> Self { - Self { - clients: Arc::default(), - handlers: JoinSet::new(), - pool, - disconnect_notification_sent_by_gateway: Arc::default(), - test_support, - tx, - events_rx: Some(events_rx), - event_senders: Arc::default(), - } - } - - /// Records that the manager finished processing a pg_notify for the given gateway. - /// Tests block on `wait_for_gateway_notification_count` to synchronize against the - /// manager's async loop before asserting side effects. - #[cfg(test)] - fn note_gateway_notification_for_tests(&self, gateway_id: Id) { - self.test_support.note_gateway_notification(gateway_id); - } - - fn manager_reconnect_delay(&self) -> Duration { - #[cfg(test)] - { - self.test_support.manager_reconnect_delay() - } - - #[cfg(not(test))] - { - GATEWAY_RECONNECT_DELAY - } - } - - /// Returns the per-Gateway "disconnect email sent, awaiting reconnect" flag, creating it if - /// necessary. - fn disconnect_notification_flag(&self, gateway_id: Id) -> Arc { - self.disconnect_notification_sent_by_gateway - .lock() - .expect("Failed to lock GatewayManager disconnect notification flags") - .entry(gateway_id) - .or_insert_with(|| Arc::new(AtomicBool::new(false))) - .clone() - } - - /// Removes the per-Gateway "disconnect email sent" flag (on Gateway deletion). - fn clear_disconnect_notification_flag(&self, gateway_id: Id) { - self.disconnect_notification_sent_by_gateway - .lock() - .expect("Failed to lock GatewayManager disconnect notification flags") - .remove(&gateway_id); - } - - fn build_handler( - &self, - gateway: Gateway, - events_rx: UnboundedReceiver, - certs_rx: watch::Receiver>>, - ) -> Result { - // Reuse any outstanding "disconnect sent" flag for this Gateway so a reconnect email is - // not lost when the handler is rebuilt after connection-relevant fields change. - let disconnect_notification_sent = self.disconnect_notification_flag(gateway.id); - - #[cfg(test)] - { - self.test_support.note_handler_spawn_attempt(gateway.id); - - let mut gateway_handler = - if let Some(socket_path) = self.test_support.socket_path_for(&gateway) { - GatewayHandler::new_with_test_socket( - gateway, - self.pool.clone(), - events_rx, - self.tx.connection_events.clone(), - self.tx.peer_stats.clone(), - certs_rx, - socket_path, - disconnect_notification_sent, - )? - } else { - GatewayHandler::new( - gateway, - self.pool.clone(), - events_rx, - self.tx.connection_events.clone(), - self.tx.peer_stats.clone(), - certs_rx, - disconnect_notification_sent, - )? - }; - gateway_handler.attach_test_support(self.test_support.clone()); - - Ok(gateway_handler) - } - - #[cfg(not(test))] - GatewayHandler::new( - gateway, - self.pool.clone(), - events_rx, - self.tx.connection_events.clone(), - self.tx.peer_stats.clone(), - certs_rx, - disconnect_notification_sent, - ) - } - - /// Bi-directional gRPC stream for communication with Defguard Gateway. - pub async fn run(&mut self) -> Result<(), anyhow::Error> { - let (certs_tx, certs_rx) = watch::channel(Arc::new(HashMap::new())); - certs::refresh_certs(&self.pool, &certs_tx).await; - let refresh_pool = self.pool.clone(); - let _refresh_certs_task = AbortTaskOnDrop::new(tokio::spawn(async move { - loop { - certs::refresh_certs(&refresh_pool, &certs_tx).await; - sleep(TEN_SECS).await; - } - })); - let _dispatch_events_task = self.events_rx.take().map(|mut events_rx| { - let event_senders = Arc::clone(&self.event_senders); - AbortTaskOnDrop::new(tokio::spawn(async move { - while let Some(ref event) = events_rx.recv().await { - // Send the event to all handlers, dropping closed channels. - event_senders - .lock() - .expect("Failed to lock GatewayManager::event_senders") - .retain(|_, events_tx| events_tx.send(event.clone()).is_ok()); - } - })) - }); - // Stores the abort handle and a snapshot of the gateway at the time the handler was last - // started. The snapshot is used by the Update arm to detect connection-relevant changes. - let mut abort_handles = HashMap::new(); - for gateway in Gateway::all(&self.pool).await? { - if !gateway.enabled { - debug!("Existing Gateway is disabled, so it won't be handled"); - continue; - } - - let id = gateway.id; - let snapshot = gateway.clone(); - let abort_handle = - self.run_handler(gateway, Arc::clone(&self.clients), certs_rx.clone())?; - abort_handles.insert(id, (abort_handle, snapshot)); - } - - // Observe gateway changes. - let mut listener = PgListener::connect_with(&self.pool).await?; - listener.listen(GATEWAY_TABLE_TRIGGER).await?; - - #[cfg(test)] - self.test_support.mark_listener_ready(); - - while let Ok(notification) = listener.recv().await { - let payload = notification.payload(); - match serde_json::from_str::(payload) { - Ok(gateway_notification) => { - let gateway_id = gateway_notification.id; - - match gateway_notification.operation { - TriggerOperation::Insert => { - let gateway = match Gateway::find_by_id(&self.pool, gateway_id).await { - Ok(Some(gateway)) => gateway, - Ok(None) => { - warn!( - "Received Insert notification for Gateway \ - id={gateway_id} but it was not found in the database" - ); - #[cfg(test)] - self.note_gateway_notification_for_tests(gateway_id); - continue; - } - Err(err) => { - error!("Failed to fetch Gateway id={gateway_id}: {err}"); - continue; - } - }; - - if gateway.enabled { - let snapshot = gateway.clone(); - let abort_handle = self.run_handler( - gateway, - Arc::clone(&self.clients), - certs_rx.clone(), - )?; - abort_handles.insert(gateway_id, (abort_handle, snapshot)); - } else { - debug!( - "New Gateway id={gateway_id} is disabled, so it won't be \ - handled" - ); - } - - #[cfg(test)] - self.note_gateway_notification_for_tests(gateway_id); - } - TriggerOperation::Update => { - let mut gateway = - match Gateway::find_by_id(&self.pool, gateway_id).await { - Ok(Some(gateway)) => gateway, - Ok(None) => { - warn!( - "Received Update notification for Gateway \ - id={gateway_id} but it was not found in the database" - ); - #[cfg(test)] - self.note_gateway_notification_for_tests(gateway_id); - continue; - } - Err(err) => { - error!("Failed to fetch Gateway id={gateway_id}: {err}"); - continue; - } - }; - - // Only restart the handler when connection-relevant fields have actually changed - let should_restart = match abort_handles.get(&gateway_id) { - Some((_, snapshot)) => needs_restart(snapshot, &gateway), - // Gateway not currently handled - treat as needing a (re)start. - None => true, - }; - - if should_restart { - self.remove_client(gateway_id); - if let Some((abort_handle, _)) = abort_handles.remove(&gateway_id) { - info!( - "Aborting connection to Gateway id={gateway_id}, \ - connection-relevant fields have changed" - ); - abort_handle.abort(); - } - - // Only mark disconnected if the gateway was actually connected - if gateway.is_connected() - && let Err(err) = gateway.touch_disconnected(&self.pool).await - { - error!( - "Failed to update disconnection time for Gateway \ - id={gateway_id} after database change: {err}" - ); - } - - if gateway.enabled { - let snapshot = gateway.clone(); - let abort_handle = self.run_handler( - gateway, - Arc::clone(&self.clients), - certs_rx.clone(), - )?; - abort_handles.insert(gateway_id, (abort_handle, snapshot)); - } else { - debug!( - "Updated Gateway id={gateway_id} is disabled, so it \ - won't be handled" - ); - } - } else { - // Non-connection-relevant update (e.g. version bump from handler - // save). Refresh the stored snapshot so future comparisons use - // up-to-date baseline values. - if let Some((_, snapshot)) = abort_handles.get_mut(&gateway_id) { - *snapshot = gateway; - } - } - - #[cfg(test)] - self.note_gateway_notification_for_tests(gateway_id); - } - TriggerOperation::Delete => { - // Send purge request to Gateway. - let maybe_client = self.remove_client(gateway_id); - - if let Some(mut client) = maybe_client { - debug!("Sending purge request to Gateway id={gateway_id}"); - if let Err(err) = client.purge(Request::new(())).await { - error!( - "Error sending purge request to Gateway id={gateway_id}: \ - {err}" - ); - } else { - info!("Sent purge request to Gateway id={gateway_id}"); - } - } else { - warn!( - "Cannot find gRPC client for Gateway id={gateway_id}; \ - skipping purge request" - ); - } - - // Kill the `GatewayHandler` and the connection. - if let Some((abort_handle, _)) = abort_handles.remove(&gateway_id) { - info!( - "Aborting connection to Gateway id={gateway_id}, it has \ - disappeared from the database" - ); - abort_handle.abort(); - } else { - warn!( - "Cannot find Gateway id={gateway_id} on the list of \ - connected gateways" - ); - } - - // Drop the per-Gateway reconnect-pairing state. - self.clear_disconnect_notification_flag(gateway_id); - - #[cfg(test)] - self.note_gateway_notification_for_tests(gateway_id); - } - } - } - Err(err) => error!("Failed to de-serialize database notification object: {err}"), - } - } - - while let Some(Ok(_result)) = self.handlers.join_next().await { - debug!("Gateway gRPC task has ended"); - } - - Ok(()) - } - - fn run_handler( - &mut self, - gateway: Gateway, - clients: Arc>>, - certs_rx: watch::Receiver>>, - ) -> Result { - let (events_tx, events_rx) = unbounded_channel(); - self.event_senders - .lock() - .expect("Failed to lock GatewayManager::event_senders") - .insert(gateway.id, events_tx); - let mut gateway_handler = self.build_handler(gateway, events_rx, certs_rx)?; - let manager_reconnect_delay = self.manager_reconnect_delay(); - let abort_handle = self.handlers.spawn(async move { - gateway_handler - .handle_connection(clients, manager_reconnect_delay) - .await - }); - Ok(abort_handle) - } - - fn remove_client(&self, gateway_id: Id) -> Option { - self.clients - .lock() - .expect("Failed to lock GatewayManager::clients") - .remove(&gateway_id) - } -} - /// Returns true if the change from `old` to `new` requires the gateway handler to be /// restarted - i.e. if any field that directly affects the gRPC connection or TLS identity /// has changed. diff --git a/crates/defguard_gateway_manager/src/manager.rs b/crates/defguard_gateway_manager/src/manager.rs new file mode 100644 index 0000000000..a4c9ba98c7 --- /dev/null +++ b/crates/defguard_gateway_manager/src/manager.rs @@ -0,0 +1,422 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex, atomic::AtomicBool}, + time::Duration, +}; + +use defguard_common::{ + db::{ChangeNotification, Id, TriggerOperation, models::gateway::Gateway}, + gateway_event::GatewayCommand, +}; +use sqlx::{PgPool, postgres::PgListener}; +use tokio::{ + sync::{ + mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, + watch, + }, + task::{AbortHandle, JoinSet}, + time::sleep, +}; +use tonic::Request; + +#[cfg(test)] +use crate::GatewayManagerTestSupport; +use crate::{ + AbortTaskOnDrop, Client, GatewayTxSet, TEN_SECS, certs, error::GatewayError, + handler::GatewayHandler, needs_restart, +}; + +const GATEWAY_TABLE_TRIGGER: &str = "gateway_change"; + +pub struct GatewayManager { + clients: Arc>>, + pool: PgPool, + handlers: JoinSet>, + /// Per-Gateway flag tracking whether a disconnect email went out and is still awaiting its + /// matching reconnect email. Owned by the manager rather than the handler so it survives a + /// handler rebuild when connection-relevant fields change. + disconnect_notification_sent_by_gateway: Arc>>>, + #[cfg(test)] + test_support: GatewayManagerTestSupport, + tx: GatewayTxSet, + /// Taken by [`Self::run`]. + events_rx: Option>, + /// Per-Gateway channels feeding each `GatewayHandler`. + event_senders: Arc>>>, +} + +impl GatewayManager { + #[cfg(not(test))] + #[must_use] + pub fn new( + pool: PgPool, + tx: GatewayTxSet, + events_rx: UnboundedReceiver, + ) -> Self { + Self { + clients: Arc::default(), + handlers: JoinSet::new(), + pool, + disconnect_notification_sent_by_gateway: Arc::default(), + tx, + events_rx: Some(events_rx), + event_senders: Arc::default(), + } + } + + #[cfg(test)] + #[must_use] + pub(crate) fn new( + pool: PgPool, + tx: GatewayTxSet, + events_rx: UnboundedReceiver, + test_support: GatewayManagerTestSupport, + ) -> Self { + Self { + clients: Arc::default(), + handlers: JoinSet::new(), + pool, + disconnect_notification_sent_by_gateway: Arc::default(), + test_support, + tx, + events_rx: Some(events_rx), + event_senders: Arc::default(), + } + } + + /// Records that the manager finished processing a pg_notify for the given gateway. + /// Tests block on `wait_for_gateway_notification_count` to synchronize against the + /// manager's async loop before asserting side effects. + #[cfg(test)] + fn note_gateway_notification_for_tests(&self, gateway_id: Id) { + self.test_support.note_gateway_notification(gateway_id); + } + + fn manager_reconnect_delay(&self) -> Duration { + #[cfg(test)] + { + self.test_support.manager_reconnect_delay() + } + + #[cfg(not(test))] + { + crate::GATEWAY_RECONNECT_DELAY + } + } + + /// Returns the per-Gateway "disconnect email sent, awaiting reconnect" flag, creating it if + /// necessary. + fn disconnect_notification_flag(&self, gateway_id: Id) -> Arc { + self.disconnect_notification_sent_by_gateway + .lock() + .expect("Failed to lock GatewayManager disconnect notification flags") + .entry(gateway_id) + .or_insert_with(|| Arc::new(AtomicBool::new(false))) + .clone() + } + + /// Removes the per-Gateway "disconnect email sent" flag (on Gateway deletion). + fn clear_disconnect_notification_flag(&self, gateway_id: Id) { + self.disconnect_notification_sent_by_gateway + .lock() + .expect("Failed to lock GatewayManager disconnect notification flags") + .remove(&gateway_id); + } + + fn build_handler( + &self, + gateway: Gateway, + events_rx: UnboundedReceiver, + certs_rx: watch::Receiver>>, + ) -> Result { + // Reuse any outstanding "disconnect sent" flag for this Gateway so a reconnect email is + // not lost when the handler is rebuilt after connection-relevant fields change. + let disconnect_notification_sent = self.disconnect_notification_flag(gateway.id); + + #[cfg(test)] + { + self.test_support.note_handler_spawn_attempt(gateway.id); + + let mut gateway_handler = + if let Some(socket_path) = self.test_support.socket_path_for(&gateway) { + GatewayHandler::new_with_test_socket( + gateway, + self.pool.clone(), + events_rx, + self.tx.connection_events.clone(), + self.tx.peer_stats.clone(), + certs_rx, + socket_path, + disconnect_notification_sent, + )? + } else { + GatewayHandler::new( + gateway, + self.pool.clone(), + events_rx, + self.tx.connection_events.clone(), + self.tx.peer_stats.clone(), + certs_rx, + disconnect_notification_sent, + )? + }; + gateway_handler.attach_test_support(self.test_support.clone()); + + Ok(gateway_handler) + } + + #[cfg(not(test))] + GatewayHandler::new( + gateway, + self.pool.clone(), + events_rx, + self.tx.connection_events.clone(), + self.tx.peer_stats.clone(), + certs_rx, + disconnect_notification_sent, + ) + } + + /// Bi-directional gRPC stream for communication with Defguard Gateway. + pub async fn run(&mut self) -> Result<(), anyhow::Error> { + let (certs_tx, certs_rx) = watch::channel(Arc::new(HashMap::new())); + certs::refresh_certs(&self.pool, &certs_tx).await; + let refresh_pool = self.pool.clone(); + let _refresh_certs_task = AbortTaskOnDrop::new(tokio::spawn(async move { + loop { + certs::refresh_certs(&refresh_pool, &certs_tx).await; + sleep(TEN_SECS).await; + } + })); + let _dispatch_events_task = self.events_rx.take().map(|mut events_rx| { + let event_senders = Arc::clone(&self.event_senders); + AbortTaskOnDrop::new(tokio::spawn(async move { + while let Some(ref event) = events_rx.recv().await { + // Send the event to all handlers, dropping closed channels. + event_senders + .lock() + .expect("Failed to lock GatewayManager::event_senders") + .retain(|_, events_tx| events_tx.send(event.clone()).is_ok()); + } + })) + }); + // Stores the abort handle and a snapshot of the gateway at the time the handler was last + // started. The snapshot is used by the Update arm to detect connection-relevant changes. + let mut abort_handles = HashMap::new(); + for gateway in Gateway::all(&self.pool).await? { + if !gateway.enabled { + debug!("Existing Gateway is disabled, so it won't be handled"); + continue; + } + + let id = gateway.id; + let snapshot = gateway.clone(); + let abort_handle = + self.run_handler(gateway, Arc::clone(&self.clients), certs_rx.clone())?; + abort_handles.insert(id, (abort_handle, snapshot)); + } + + // Observe gateway changes. + let mut listener = PgListener::connect_with(&self.pool).await?; + listener.listen(GATEWAY_TABLE_TRIGGER).await?; + + #[cfg(test)] + self.test_support.mark_listener_ready(); + + while let Ok(notification) = listener.recv().await { + let payload = notification.payload(); + match serde_json::from_str::(payload) { + Ok(gateway_notification) => { + let gateway_id = gateway_notification.id; + + match gateway_notification.operation { + TriggerOperation::Insert => { + let gateway = match Gateway::find_by_id(&self.pool, gateway_id).await { + Ok(Some(gateway)) => gateway, + Ok(None) => { + warn!( + "Received Insert notification for Gateway \ + id={gateway_id} but it was not found in the database" + ); + #[cfg(test)] + self.note_gateway_notification_for_tests(gateway_id); + continue; + } + Err(err) => { + error!("Failed to fetch Gateway id={gateway_id}: {err}"); + continue; + } + }; + + if gateway.enabled { + let snapshot = gateway.clone(); + let abort_handle = self.run_handler( + gateway, + Arc::clone(&self.clients), + certs_rx.clone(), + )?; + abort_handles.insert(gateway_id, (abort_handle, snapshot)); + } else { + debug!( + "New Gateway id={gateway_id} is disabled, so it won't be \ + handled" + ); + } + + #[cfg(test)] + self.note_gateway_notification_for_tests(gateway_id); + } + TriggerOperation::Update => { + let mut gateway = + match Gateway::find_by_id(&self.pool, gateway_id).await { + Ok(Some(gateway)) => gateway, + Ok(None) => { + warn!( + "Received Update notification for Gateway \ + id={gateway_id} but it was not found in the database" + ); + #[cfg(test)] + self.note_gateway_notification_for_tests(gateway_id); + continue; + } + Err(err) => { + error!("Failed to fetch Gateway id={gateway_id}: {err}"); + continue; + } + }; + + // Only restart the handler when connection-relevant fields have actually changed + let should_restart = match abort_handles.get(&gateway_id) { + Some((_, snapshot)) => needs_restart(snapshot, &gateway), + // Gateway not currently handled - treat as needing a (re)start. + None => true, + }; + + if should_restart { + self.remove_client(gateway_id); + if let Some((abort_handle, _)) = abort_handles.remove(&gateway_id) { + info!( + "Aborting connection to Gateway id={gateway_id}, \ + connection-relevant fields have changed" + ); + abort_handle.abort(); + } + + // Only mark disconnected if the gateway was actually connected + if gateway.is_connected() + && let Err(err) = gateway.touch_disconnected(&self.pool).await + { + error!( + "Failed to update disconnection time for Gateway \ + id={gateway_id} after database change: {err}" + ); + } + + if gateway.enabled { + let snapshot = gateway.clone(); + let abort_handle = self.run_handler( + gateway, + Arc::clone(&self.clients), + certs_rx.clone(), + )?; + abort_handles.insert(gateway_id, (abort_handle, snapshot)); + } else { + debug!( + "Updated Gateway id={gateway_id} is disabled, so it \ + won't be handled" + ); + } + } else { + // Non-connection-relevant update (e.g. version bump from handler + // save). Refresh the stored snapshot so future comparisons use + // up-to-date baseline values. + if let Some((_, snapshot)) = abort_handles.get_mut(&gateway_id) { + *snapshot = gateway; + } + } + + #[cfg(test)] + self.note_gateway_notification_for_tests(gateway_id); + } + TriggerOperation::Delete => { + // Send purge request to Gateway. + let maybe_client = self.remove_client(gateway_id); + + if let Some(mut client) = maybe_client { + debug!("Sending purge request to Gateway id={gateway_id}"); + if let Err(err) = client.purge(Request::new(())).await { + error!( + "Error sending purge request to Gateway id={gateway_id}: \ + {err}" + ); + } else { + info!("Sent purge request to Gateway id={gateway_id}"); + } + } else { + warn!( + "Cannot find gRPC client for Gateway id={gateway_id}; \ + skipping purge request" + ); + } + + // Kill the `GatewayHandler` and the connection. + if let Some((abort_handle, _)) = abort_handles.remove(&gateway_id) { + info!( + "Aborting connection to Gateway id={gateway_id}, it has \ + disappeared from the database" + ); + abort_handle.abort(); + } else { + warn!( + "Cannot find Gateway id={gateway_id} on the list of \ + connected gateways" + ); + } + + // Drop the per-Gateway reconnect-pairing state. + self.clear_disconnect_notification_flag(gateway_id); + + #[cfg(test)] + self.note_gateway_notification_for_tests(gateway_id); + } + } + } + Err(err) => error!("Failed to de-serialize database notification object: {err}"), + } + } + + while let Some(Ok(_result)) = self.handlers.join_next().await { + debug!("Gateway gRPC task has ended"); + } + + Ok(()) + } + + fn run_handler( + &mut self, + gateway: Gateway, + clients: Arc>>, + certs_rx: watch::Receiver>>, + ) -> Result { + let (events_tx, events_rx) = unbounded_channel(); + self.event_senders + .lock() + .expect("Failed to lock GatewayManager::event_senders") + .insert(gateway.id, events_tx); + let mut gateway_handler = self.build_handler(gateway, events_rx, certs_rx)?; + let manager_reconnect_delay = self.manager_reconnect_delay(); + let abort_handle = self.handlers.spawn(async move { + gateway_handler + .handle_connection(clients, manager_reconnect_delay) + .await + }); + Ok(abort_handle) + } + + fn remove_client(&self, gateway_id: Id) -> Option { + self.clients + .lock() + .expect("Failed to lock GatewayManager::clients") + .remove(&gateway_id) + } +} diff --git a/crates/defguard_gateway_manager/src/tests/common/mod.rs b/crates/defguard_gateway_manager/src/tests/common/mod.rs index 1cedcb3102..3b8df5701c 100644 --- a/crates/defguard_gateway_manager/src/tests/common/mod.rs +++ b/crates/defguard_gateway_manager/src/tests/common/mod.rs @@ -45,7 +45,9 @@ use tokio::{ use tokio_stream::{once, wrappers::UnboundedReceiverStream}; use tonic::{Request, Response, Status, Streaming, transport::Server}; -use crate::{GatewayManager, GatewayManagerTestSupport, GatewayTxSet, handler::GatewayHandler}; +use crate::{ + GatewayManagerTestSupport, GatewayTxSet, handler::GatewayHandler, manager::GatewayManager, +}; const TEST_TIMEOUT: Duration = Duration::from_secs(10); pub(crate) const FAST_RETRY_DELAY: Duration = Duration::from_millis(20); From f14acb5459f9514e58e8ef0b63201dc6ac31b68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Tue, 13 Oct 2026 10:55:13 +0200 Subject: [PATCH 13/14] uaparser to ua_parser --- Cargo.lock | 55 +++--- .../src/db/models/wireguard/tests.rs | 6 +- crates/defguard_common/src/types/user_info.rs | 10 +- crates/defguard_core/Cargo.toml | 3 +- crates/defguard_core/src/handlers/auth.rs | 5 +- crates/defguard_core/src/headers.rs | 177 ++++++++++++------ 6 files changed, 155 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 624dd0aa95..296753c46b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1120,12 +1120,6 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "convert_case" version = "0.10.0" @@ -1725,6 +1719,7 @@ dependencies = [ "serde_json", "serde_qs", "serde_urlencoded", + "serde_yaml", "sha-1", "sha2 0.11.1", "sha256", @@ -1749,7 +1744,7 @@ dependencies = [ "tracing", "tracing-subscriber", "trait-variant", - "uaparser", + "ua-parser", "utoipa", "utoipa-swagger-ui", "uuid", @@ -2081,9 +2076,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" dependencies = [ "serde_core", ] @@ -2130,19 +2125,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.119", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -2158,7 +2140,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case 0.10.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -4917,7 +4899,7 @@ dependencies = [ "curve25519-dalek 4.1.3", "cx448", "derive_builder", - "derive_more 2.1.1", + "derive_more", "des", "digest 0.10.7", "dsa", @@ -5740,6 +5722,18 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-filtered" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5f7b31fbef748cc46643c1f9ba17f6d5c7c6f0ba5e372fc9c48d31ad1c8612" +dependencies = [ + "aho-corasick", + "itertools 0.14.0", + "regex", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.11" @@ -6112,7 +6106,7 @@ checksum = "8322aebea94a6cc288ff884e8a49bdbceef60206c400c6c46d22e62fc4fd91a6" dependencies = [ "bitflags 2.13.2", "cssparser", - "derive_more 2.1.1", + "derive_more", "log", "new_debug_unreachable", "precomputed-hash", @@ -7533,17 +7527,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] -name = "uaparser" -version = "0.6.4" +name = "ua-parser" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c9e1c3f893758f154004195fc2d2c52fbda462df725220ceaef830ac29affa" +checksum = "f01b7ba339f8874b643216c6c957d792047143abe848568131e5d91d2ae2ada1" dependencies = [ - "derive_more 0.99.20", - "lazy_static", "regex", + "regex-filtered", "serde", - "serde_derive", - "serde_yaml", ] [[package]] diff --git a/crates/defguard_common/src/db/models/wireguard/tests.rs b/crates/defguard_common/src/db/models/wireguard/tests.rs index 25771b0017..2d886d644e 100644 --- a/crates/defguard_common/src/db/models/wireguard/tests.rs +++ b/crates/defguard_common/src/db/models/wireguard/tests.rs @@ -3,7 +3,7 @@ use std::{assert_matches, net::Ipv6Addr, str::FromStr}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; -use crate::db::setup_pool; +use crate::db::{models::device, setup_pool}; #[test] fn test_set_address() { @@ -135,7 +135,7 @@ async fn test_get_allowed_devices_for_user(_: PgPoolOptions, options: PgConnectO .get_allowed_devices_for_user(&mut pool.acquire().await.unwrap(), Id::from(999)) .await .unwrap(); - assert!(devices.is_empty()); + assert_eq!(devices, [] as [device::Device; 0]); } #[sqlx::test] @@ -223,7 +223,7 @@ async fn test_get_allowed_devices_for_user_with_groups( .get_allowed_devices_for_user(&mut transaction, user2.id) .await .unwrap(); - assert!(devices.is_empty()); + assert_eq!(devices, [] as [device::Device; 0]); } #[sqlx::test] diff --git a/crates/defguard_common/src/types/user_info.rs b/crates/defguard_common/src/types/user_info.rs index 98ee00e05c..6de3badd13 100644 --- a/crates/defguard_common/src/types/user_info.rs +++ b/crates/defguard_common/src/types/user_info.rs @@ -311,8 +311,14 @@ mod test { assert_eq!(group1.member_usernames(&pool).await.unwrap(), ["hpotter"]); assert_eq!(group3.member_usernames(&pool).await.unwrap(), ["hpotter"]); - assert!(group2.member_usernames(&pool).await.unwrap().is_empty()); - assert!(group4.member_usernames(&pool).await.unwrap().is_empty()); + assert_eq!( + group2.member_usernames(&pool).await.unwrap(), + [] as [std::string::String; 0] + ); + assert_eq!( + group4.member_usernames(&pool).await.unwrap(), + [] as [std::string::String; 0] + ); } // Admin updating another user must be able to change all profile diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml index 9a70eb5fb2..440d141ca3 100644 --- a/crates/defguard_core/Cargo.toml +++ b/crates/defguard_core/Cargo.toml @@ -94,7 +94,8 @@ webauthn-rs-proto.workspace = true x25519-dalek.workspace = true ammonia = "4.1" regex = "1.10" -uaparser = "0.6" +serde_yaml = "0.9" +ua-parser = "0.2" async-stream = "0.3" [features] diff --git a/crates/defguard_core/src/handlers/auth.rs b/crates/defguard_core/src/handlers/auth.rs index ab386cff64..656639442d 100644 --- a/crates/defguard_core/src/handlers/auth.rs +++ b/crates/defguard_core/src/handlers/auth.rs @@ -21,7 +21,6 @@ use defguard_common::{ }; use sqlx::{PgPool, types::Uuid}; use time::Duration; -use uaparser::Parser; use webauthn_rs::prelude::PublicKeyCredential; use webauthn_rs_proto::options::CollectedClientData; @@ -44,7 +43,7 @@ use crate::{ error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, handlers::{ClientIpAddr, SIGN_IN_COOKIE_NAME, cookie_domain, user_for_admin_or_self}, - headers::{USER_AGENT_PARSER, check_new_device_login, get_user_agent_device}, + headers::{ParsedUserAgent, check_new_device_login, get_user_agent_device}, mail::templates::{mfa_activation_mail, mfa_code_mail, mfa_configured_mail}, server_config, }; @@ -57,7 +56,7 @@ pub async fn create_session( user_agent: &str, user: &mut User, ) -> Result<(Session, Option, Option), WebError> { - let agent = USER_AGENT_PARSER.parse(user_agent); + let agent = ParsedUserAgent::parse(user_agent); let device_info = get_user_agent_device(&agent); debug!("Cleaning up expired sessions..."); Session::delete_expired(pool).await?; diff --git a/crates/defguard_core/src/headers.rs b/crates/defguard_core/src/headers.rs index 5df00a26af..540fad6ec5 100644 --- a/crates/defguard_core/src/headers.rs +++ b/crates/defguard_core/src/headers.rs @@ -1,9 +1,6 @@ -use std::{ - borrow::Borrow, - sync::{ - Arc, LazyLock, - atomic::{AtomicBool, Ordering}, - }, +use std::sync::{ + Arc, LazyLock, + atomic::{AtomicBool, Ordering}, }; use axum::{ @@ -17,7 +14,7 @@ use defguard_common::db::{ models::{DeviceLoginEvent, User}, }; use sqlx::PgPool; -use uaparser::{Client, Parser, UserAgentParser}; +use ua_parser::{Extractor, Regexes, device, os, user_agent}; use crate::mail::templates::{SessionContext, TemplateError, new_device_login_mail}; @@ -94,74 +91,103 @@ pub(crate) async fn security_headers_middleware( response } -pub static USER_AGENT_PARSER: LazyLock = LazyLock::new(|| { - let regexes = include_bytes!("../user_agent_header_regexes.yaml"); - UserAgentParser::from_bytes(regexes).expect("Parser creation failed") +/// Family reported by uap-core when nothing matches. +const UNKNOWN_FAMILY: &str = "Other"; + +pub static USER_AGENT_PARSER: LazyLock> = LazyLock::new(|| { + let regexes: Regexes = + serde_yaml::from_slice(include_bytes!("../user_agent_header_regexes.yaml")) + .expect("Failed to load user agent regexes"); + Extractor::try_from(regexes).expect("Parser creation failed") }); -#[must_use] -pub fn get_device_info(user_agent: &str) -> String { - let escaped = tera::escape_html(user_agent); - let client = USER_AGENT_PARSER.parse(&escaped); - get_user_agent_device(&client) +/// User agent split into browser, OS and device by [`USER_AGENT_PARSER`]. +pub(crate) struct ParsedUserAgent<'a> { + browser: Option>, + os: Option>, + device: Option>, } -#[must_use] -pub(crate) fn get_user_agent_device(user_agent_client: &Client) -> String { - let device_type = user_agent_client - .device - .model - .as_ref() - .map_or("unknown model", Borrow::borrow); - - let mut device_version = String::new(); - if let Some(major) = &user_agent_client.os.major { - device_version.push_str(major); - - if let Some(minor) = &user_agent_client.os.minor { - device_version.push('.'); - device_version.push_str(minor); - - if let Some(patch) = &user_agent_client.os.patch { - device_version.push('.'); - device_version.push_str(patch); - } +impl<'a> ParsedUserAgent<'a> { + #[must_use] + pub(crate) fn parse(user_agent: &'a str) -> Self { + let (browser, os, device) = USER_AGENT_PARSER.extract(user_agent); + Self { + browser, + os, + device, } } - let mut device_os = user_agent_client.os.family.to_string(); - device_os.push(' '); - device_os.push_str(&device_version); - device_os.push_str(", "); - device_os.push_str(&user_agent_client.user_agent.family); + fn browser_family(&self) -> &str { + self.browser + .as_ref() + .map_or(UNKNOWN_FAMILY, |browser| &browser.family) + } + + fn os_family(&self) -> &str { + self.os.as_ref().map_or(UNKNOWN_FAMILY, |os| &os.os) + } - format!("{device_type}, OS: {device_os}") + fn device_family(&self) -> &str { + self.device + .as_ref() + .map_or(UNKNOWN_FAMILY, |device| &device.device) + } + + fn device_model(&self) -> Option<&str> { + self.device.as_ref()?.model.as_deref() + } + + fn device_brand(&self) -> Option<&str> { + self.device.as_ref()?.brand.as_deref() + } + + /// OS version as "major[.minor[.patch]]", or empty if unknown. + fn os_version(&self) -> String { + let Some(os) = &self.os else { + return String::new(); + }; + [&os.major, &os.minor, &os.patch] + .into_iter() + .map_while(|part| part.as_deref()) + .collect::>() + .join(".") + } +} + +#[must_use] +pub fn get_device_info(user_agent: &str) -> String { + let escaped = tera::escape_html(user_agent); + get_user_agent_device(&ParsedUserAgent::parse(&escaped)) +} + +#[must_use] +pub(crate) fn get_user_agent_device(user_agent: &ParsedUserAgent) -> String { + format!( + "{}, OS: {} {}, {}", + user_agent.device_model().unwrap_or("unknown model"), + user_agent.os_family(), + user_agent.os_version(), + user_agent.browser_family(), + ) } fn get_user_agent_device_login_data( user_id: Id, ip_address: String, event_type: String, - user_agent_client: &Client, + user_agent: &ParsedUserAgent, ) -> DeviceLoginEvent { - let model = user_agent_client - .device - .model - .as_ref() - .map(ToString::to_string); - - let brand = user_agent_client - .device - .brand - .as_ref() - .map(ToString::to_string); - - let family = user_agent_client.device.family.to_string(); - let os_family = user_agent_client.os.family.to_string(); - let browser = user_agent_client.user_agent.family.to_string(); - DeviceLoginEvent::new( - user_id, ip_address, model, family, brand, os_family, browser, event_type, + user_id, + ip_address, + user_agent.device_model().map(ToString::to_string), + user_agent.device_family().to_string(), + user_agent.device_brand().map(ToString::to_string), + user_agent.os_family().to_string(), + user_agent.browser_family().to_string(), + event_type, ) } @@ -171,7 +197,7 @@ pub(crate) async fn check_new_device_login( user: &User, ip_address: String, event_type: String, - agent: Client<'_>, + agent: ParsedUserAgent<'_>, ) -> Result<(), TemplateError> { let device_login_event = get_user_agent_device_login_data(user.id, ip_address, event_type, &agent); @@ -192,3 +218,34 @@ pub(crate) async fn check_new_device_login( Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_user_agent() { + let agent = ParsedUserAgent::parse( + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 \ + (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1", + ); + assert_eq!(agent.device_model(), Some("iPhone")); + assert_eq!(agent.device_brand(), Some("Apple")); + assert_eq!(agent.device_family(), "iPhone"); + assert_eq!(agent.os_family(), "iOS"); + assert_eq!(agent.os_version(), "17.6.1"); + assert_eq!(agent.browser_family(), "Mobile Safari"); + assert_eq!( + get_user_agent_device(&agent), + "iPhone, OS: iOS 17.6.1, Mobile Safari" + ); + + let agent = ParsedUserAgent::parse(""); + assert_eq!(agent.device_model(), None); + assert_eq!(agent.device_family(), "Other"); + assert_eq!( + get_user_agent_device(&agent), + "unknown model, OS: Other , Other" + ); + } +} From 4a18fa70923e98935bf290bd623bdfa5edbc2e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Tue, 13 Oct 2026 11:19:44 +0200 Subject: [PATCH 14/14] serde-saphyr --- Cargo.lock | 80 ++++++++++++++----- crates/defguard_core/Cargo.toml | 2 +- .../src/enterprise/ldap/tests.rs | 28 +++---- crates/defguard_core/src/headers.rs | 2 +- 4 files changed, 76 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 296753c46b..fbe94cdcee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,17 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width 0.2.2", +] + [[package]] name = "anstream" version = "1.0.0" @@ -228,6 +239,12 @@ dependencies = [ "password-hash 0.6.1", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.8" @@ -1715,11 +1732,11 @@ dependencies = [ "secrecy", "semver", "serde", + "serde-saphyr", "serde_cbor_2", "serde_json", "serde_qs", "serde_urlencoded", - "serde_yaml", "sha-1", "sha2 0.11.1", "sha256", @@ -2417,6 +2434,15 @@ dependencies = [ "simdutf8", ] +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + [[package]] name = "enum-as-inner" version = "0.7.0" @@ -2911,6 +2937,16 @@ dependencies = [ "web-time", ] +[[package]] +name = "granit-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0" +dependencies = [ + "arraydeque", + "smallvec", +] + [[package]] name = "group" version = "0.13.0" @@ -4123,6 +4159,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -6135,6 +6177,23 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-saphyr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8" +dependencies = [ + "annotate-snippets", + "base64 0.23.1", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + [[package]] name = "serde-value" version = "0.7.0" @@ -6288,19 +6347,6 @@ dependencies = [ "syn 3.0.9", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.14.2", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "serdect" version = "0.2.0" @@ -7610,12 +7656,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml index 440d141ca3..330598c6c2 100644 --- a/crates/defguard_core/Cargo.toml +++ b/crates/defguard_core/Cargo.toml @@ -94,7 +94,7 @@ webauthn-rs-proto.workspace = true x25519-dalek.workspace = true ammonia = "4.1" regex = "1.10" -serde_yaml = "0.9" +serde-saphyr = "1.3" ua-parser = "0.2" async-stream = "0.3" diff --git a/crates/defguard_core/src/enterprise/ldap/tests.rs b/crates/defguard_core/src/enterprise/ldap/tests.rs index 1d2df175fb..f7fe830043 100644 --- a/crates/defguard_core/src/enterprise/ldap/tests.rs +++ b/crates/defguard_core/src/enterprise/ldap/tests.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use defguard_common::{ db::{ models::{ - Device, DeviceType, WireguardNetwork, + Device, DeviceType, User, WireguardNetwork, device::WireguardNetworkDevice, group::Permission, settings::{LdapSyncStatus, initialize_current_settings}, @@ -868,8 +868,8 @@ async fn test_user_in_ldap_sync_groups() { #[test] fn test_compute_user_sync_changes_empty_lists() { - let mut ldap_users: Vec = Vec::new(); - let mut defguard_users: Vec> = Vec::new(); + let mut ldap_users = Vec::new(); + let mut defguard_users = Vec::new(); let changes = compute_user_sync_changes( &mut ldap_users, @@ -879,10 +879,10 @@ fn test_compute_user_sync_changes_empty_lists() { ); // No users in either system, should result in no changes - assert!(changes.delete_defguard.is_empty()); - assert!(changes.add_defguard.is_empty()); - assert!(changes.delete_ldap.is_empty()); - assert!(changes.add_ldap.is_empty()); + assert_eq!(changes.delete_defguard, [] as [User; 0]); + assert_eq!(changes.add_defguard, [] as [User; 0]); + assert_eq!(changes.delete_ldap, [] as [User; 0]); + assert_eq!(changes.add_ldap, [] as [User; 0]); } #[test] @@ -897,7 +897,7 @@ fn test_ldap_authority_add_to_defguard() { ); let mut ldap_users = vec![ldap_user]; - let mut defguard_users: Vec> = Vec::new(); + let mut defguard_users = Vec::new(); let changes = compute_user_sync_changes( &mut ldap_users, @@ -907,11 +907,11 @@ fn test_ldap_authority_add_to_defguard() { ); // When LDAP is authoritative, users in LDAP but not Defguard should be added to Defguard - assert!(changes.delete_defguard.is_empty()); + assert_eq!(changes.delete_defguard, [] as [User; 0]); assert_eq!(changes.add_defguard.len(), 1); assert_eq!(changes.add_defguard[0].username, "test_user"); - assert!(changes.delete_ldap.is_empty()); - assert!(changes.add_ldap.is_empty()); + assert_eq!(changes.delete_ldap, [] as [User; 0]); + assert_eq!(changes.add_ldap, [] as [User; 0]); } #[sqlx::test] @@ -930,7 +930,7 @@ fn test_ldap_authority_delete_from_defguard(_: PgPoolOptions, options: PgConnect .await .unwrap(); - let mut ldap_users: Vec = Vec::new(); + let mut ldap_users = Vec::new(); let mut defguard_users = vec![defguard_user]; let changes = compute_user_sync_changes( @@ -964,7 +964,7 @@ fn test_defguard_authority_add_to_ldap(_: PgPoolOptions, options: PgConnectOptio .await .unwrap(); - let mut ldap_users: Vec = Vec::new(); + let mut ldap_users = Vec::new(); let mut defguard_users = vec![defguard_user]; let changes = compute_user_sync_changes( @@ -994,7 +994,7 @@ fn test_defguard_authority_delete_from_ldap() { ); let mut ldap_users = vec![ldap_user]; - let mut defguard_users: Vec> = Vec::new(); + let mut defguard_users = Vec::new(); let changes = compute_user_sync_changes( &mut ldap_users, diff --git a/crates/defguard_core/src/headers.rs b/crates/defguard_core/src/headers.rs index 540fad6ec5..9856ec3dac 100644 --- a/crates/defguard_core/src/headers.rs +++ b/crates/defguard_core/src/headers.rs @@ -96,7 +96,7 @@ const UNKNOWN_FAMILY: &str = "Other"; pub static USER_AGENT_PARSER: LazyLock> = LazyLock::new(|| { let regexes: Regexes = - serde_yaml::from_slice(include_bytes!("../user_agent_header_regexes.yaml")) + serde_saphyr::from_slice(include_bytes!("../user_agent_header_regexes.yaml")) .expect("Failed to load user agent regexes"); Extractor::try_from(regexes).expect("Parser creation failed") });