diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 09fa12a..eaeebb2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,9 +16,9 @@ jobs: uses: actions/checkout@v4 - name: Install Nix - uses: cachix/install-nix-action@v27 + uses: cachix/install-nix-action@v31 with: - nix_path: nixpkgs=channel:nixos-24.05 + nix_path: nixpkgs=channel:nixos-25.05 - name: Cargo cache uses: actions/cache@v4 @@ -30,6 +30,9 @@ jobs: ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + - + name: Install dev dependencies + run: nix-shell --command "make dev-dependencies" - name: Run tests run: nix-shell --command "make test" @@ -53,9 +56,9 @@ jobs: uses: actions/checkout@v4 - name: Install Nix - uses: cachix/install-nix-action@v27 + uses: cachix/install-nix-action@v31 with: - nix_path: nixpkgs=channel:nixos-24.05 + nix_path: nixpkgs=channel:nixos-25.05 - name: Cargo cache uses: actions/cache@v4 diff --git a/Cargo.lock b/Cargo.lock index fd322ee..b184787 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" @@ -39,21 +39,21 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", @@ -75,15 +75,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -102,9 +102,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -116,22 +116,22 @@ dependencies = [ ] [[package]] -name = "byteorder" -version = "1.5.0" +name = "bumpalo" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.13" +version = "1.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" +checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" dependencies = [ "jobserver", "libc", @@ -150,13 +150,13 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chirpstack-gateway-mesh" -version = "4.0.1" +version = "4.1.0-test.2" dependencies = [ "aes", "anyhow", @@ -170,30 +170,29 @@ dependencies = [ "humantime-serde", "log", "lrwn_filters", - "prost-types", - "rand 0.9.0", + "rand 0.9.2", "serde", "signal-hook", "signal-hook-tokio", "simple_logger", "syslog", "tokio", - "toml", + "toml 0.9.4", "zeromq", "zmq", ] [[package]] name = "chirpstack_api" -version = "4.11.1" +version = "4.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd068c5bb5c6d359416af5f011c8ee35f0578c57dd16f3319b0c6405c91c078" +checksum = "93cfc11003ad9d3d763dd5ad3753cd31baf211f52e7c326b266e3d32fdf0fe23" dependencies = [ "hex", "pbjson-build", "prost", "prost-types", - "rand 0.8.5", + "rand 0.9.2", "tonic-build", ] @@ -209,9 +208,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.29" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" +checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" dependencies = [ "clap_builder", "clap_derive", @@ -219,9 +218,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.29" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" +checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" dependencies = [ "anstyle", "clap_lex", @@ -229,9 +228,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", @@ -241,9 +240,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cmac" @@ -290,9 +289,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -343,9 +342,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -353,9 +352,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", @@ -367,9 +366,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", @@ -400,9 +399,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -462,24 +461,24 @@ dependencies = [ [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -601,25 +600,25 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -630,9 +629,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "handlebars" -version = "6.3.1" +version = "6.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d752747ddabc4c1a70dd28e72f2e3c218a816773e0d7faf67433f1acfa6cba7c" +checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" dependencies = [ "derive_builder", "log", @@ -641,7 +640,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -652,9 +651,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "heck" @@ -670,20 +669,20 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hostname" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ "cfg-if", "libc", - "windows", + "windows-link", ] [[package]] name = "humantime" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" [[package]] name = "humantime-serde" @@ -703,23 +702,34 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.7.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.4", ] [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "generic-array", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + [[package]] name = "itertools" version = "0.13.0" @@ -740,19 +750,30 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.3", "libc", ] +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "jwalk" version = "0.8.1" @@ -771,21 +792,21 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -793,52 +814,52 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lrwn_filters" -version = "4.9.0" +version = "4.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470ec7a1a238a47a5ed8b7a0f765ccb870b812dea76621c35b0bae1118b831cb" +checksum = "5da176e42cd97f9f298624ccb6266c0d24591ba9ed83b7d85519d1024a901c53" dependencies = [ "hex", "serde", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "miniz_oxide" -version = "0.8.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] name = "multimap" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "num-conv" @@ -890,15 +911,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -906,9 +927,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -931,20 +952,20 @@ dependencies = [ [[package]] name = "pest" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.11", + "thiserror 2.0.12", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -952,9 +973,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", @@ -965,11 +986,10 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", "sha2", ] @@ -998,9 +1018,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "powerfmt" @@ -1010,18 +1030,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", ] [[package]] name = "prettyplease" -version = "0.2.29" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", "syn", @@ -1029,9 +1049,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -1090,13 +1110,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -1110,13 +1136,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.0", - "zerocopy 0.8.17", + "rand_core 0.9.3", ] [[package]] @@ -1136,7 +1161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1145,17 +1170,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] name = "rand_core" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", - "zerocopy 0.8.17", + "getrandom 0.3.3", ] [[package]] @@ -1180,11 +1204,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", ] [[package]] @@ -1218,28 +1242,34 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustix" -version = "0.38.44" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -1258,18 +1288,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -1278,9 +1308,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -1290,18 +1320,27 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ "serde", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -1316,9 +1355,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -1326,9 +1365,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -1359,27 +1398,24 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1396,9 +1432,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.98" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -1426,7 +1462,7 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml", + "toml 0.8.23", "version-compare", ] @@ -1438,13 +1474,12 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.16.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -1461,11 +1496,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -1481,9 +1516,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -1492,9 +1527,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -1509,15 +1544,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -1525,20 +1560,22 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1554,9 +1591,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -1568,43 +1605,82 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "tonic-build" -version = "0.12.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847" dependencies = [ "prettyplease", "proc-macro2", @@ -1616,9 +1692,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "ucd-trie" @@ -1628,17 +1704,19 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "uuid" -version = "1.13.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -1665,47 +1743,92 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] -name = "winapi-util" -version = "0.1.9" +name = "wasm-bindgen" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "windows-sys 0.59.0", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", ] [[package]] -name = "windows" -version = "0.52.0" +name = "wasm-bindgen-backend" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ - "windows-core", - "windows-targets 0.52.6", + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", ] [[package]] -name = "windows-core" -version = "0.52.0" +name = "wasm-bindgen-macro" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ - "windows-targets 0.52.6", + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", ] +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-sys" version = "0.48.0" @@ -1717,20 +1840,20 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.3", ] [[package]] @@ -1757,13 +1880,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1776,6 +1916,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1788,6 +1934,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1800,12 +1952,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1818,6 +1982,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1830,6 +2000,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1842,6 +2018,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1855,58 +2037,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.7.2" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" -dependencies = [ - "memchr", -] +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" +name = "winnow" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ - "bitflags 2.8.0", + "memchr", ] [[package]] -name = "zerocopy" -version = "0.7.35" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", + "bitflags 2.9.1", ] [[package]] name = "zerocopy" -version = "0.8.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" -dependencies = [ - "zerocopy-derive 0.8.17", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.17" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0f74525..f1f0f4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ repository = "https://github.com/chirpstack/chirpstack-gateway-mesh" homepage = "https://www.chirpstack.io/" license = "MIT" - version = "4.0.1" + version = "4.1.0-test.2" authors = ["Orne Brocaar "] edition = "2021" publish = false @@ -16,32 +16,33 @@ "usage", "derive", ] } - chirpstack_api = { version = "4.11.1", default-features = false } - lrwn_filters = { version = "4.9", features = ["serde"] } + chirpstack_api = { version = "4.13", default-features = false } + lrwn_filters = { version = "4.12", features = ["serde"] } log = "0.4" simple_logger = "5.0" syslog = "7.0" - toml = "0.8" + toml = "0.9" handlebars = "6.3" anyhow = "1.0" humantime-serde = "1.1" serde = { version = "1.0", features = ["derive"] } - tokio = { version = "1.43", features = [ + tokio = { version = "1.47", features = [ "macros", "rt-multi-thread", "time", "fs", "sync", + "process", + "io-util", ] } hex = "0.4" rand = "0.9" signal-hook = "0.3" signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } futures = "0.3" - prost-types = "0.13" zmq = "0.10" - cmac = { version = "0.7" } - aes = { version = "0.8" } + cmac = "0.7" + aes = "0.8" [dev-dependencies] zeromq = "0.4" diff --git a/configuration/region_au915.toml b/configuration/region_au915.toml index ddad1da..c4ace04 100644 --- a/configuration/region_au915.toml +++ b/configuration/region_au915.toml @@ -75,6 +75,8 @@ 927100000, ] + tx_power = [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27] + [[mappings.data_rates]] modulation = "LORA" spreading_factor = 12 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 604045c..a98f086 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] - channel = "1.84.1" + channel = "1.85.0" components = ["rustfmt", "clippy"] profile = "default" diff --git a/shell.nix b/shell.nix index 87c3cbd..5ea59e2 100644 --- a/shell.nix +++ b/shell.nix @@ -1,4 +1,4 @@ -{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.11.tar.gz") {} }: +{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-25.05.tar.gz") {} }: pkgs.mkShell { buildInputs = [ diff --git a/src/aes128.rs b/src/aes128.rs index be22fed..1b1784b 100644 --- a/src/aes128.rs +++ b/src/aes128.rs @@ -1,6 +1,10 @@ use std::fmt; use std::str::FromStr; +use aes::{ + cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}, + Aes128, Block, +}; use anyhow::{Error, Result}; use serde::{ de::{self, Visitor}, @@ -95,3 +99,26 @@ impl Visitor<'_> for Aes128KeyVisitor { Aes128Key::from_str(value).map_err(|e| E::custom(format!("{}", e))) } } + +pub fn get_signing_key(key: Aes128Key) -> Aes128Key { + let b: [u8; 16] = [0; 16]; + get_key(key, b) +} + +pub fn get_encryption_key(key: Aes128Key) -> Aes128Key { + let mut b: [u8; 16] = [0; 16]; + b[0] = 0x01; + get_key(key, b) +} + +fn get_key(key: Aes128Key, b: [u8; 16]) -> Aes128Key { + let key_bytes = key.to_bytes(); + let key = GenericArray::from_slice(&key_bytes); + let cipher = Aes128::new(key); + + let mut b = b; + let block = Block::from_mut_slice(&mut b); + cipher.encrypt_block(block); + + Aes128Key(b) +} diff --git a/src/backend.rs b/src/backend.rs index fc9e6f7..34eeef1 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -16,8 +16,7 @@ static RELAY_ID: OnceLock> = OnceLock::new(); static CONCENTRATORD_CMD_CHAN: OnceLock = OnceLock::new(); static MESH_CONCENTRATORD_CMD_CHAN: OnceLock = OnceLock::new(); -type Event = (String, Vec); -type Command = ((String, Vec), oneshot::Sender>>); +type Command = (gw::Command, oneshot::Sender>>); type CommandChannel = mpsc::UnboundedSender; pub async fn setup(conf: &Configuration) -> Result<()> { @@ -47,7 +46,7 @@ async fn setup_concentratord(conf: &Configuration) -> Result<()> { sock.connect(&command_url).unwrap(); while let Some(cmd) = cmd_rx.blocking_recv() { - let resp = send_zmq_command(&mut sock, &cmd); + let resp = send_zmq_command(&mut sock, &cmd.0); cmd.1.send(resp).unwrap(); } @@ -60,23 +59,30 @@ async fn setup_concentratord(conf: &Configuration) -> Result<()> { trace!("Reading Gateway ID"); let mut gateway_id: [u8; 8] = [0; 8]; let (gateway_id_tx, gateway_id_rx) = oneshot::channel::>>(); - cmd_tx.send((("gateway_id".to_string(), vec![]), gateway_id_tx))?; + cmd_tx.send(( + gw::Command { + command: Some(gw::command::Command::GetGatewayId( + gw::GetGatewayIdRequest {}, + )), + }, + gateway_id_tx, + ))?; let resp = gateway_id_rx.await??; - gateway_id.copy_from_slice(&resp); - info!("Retrieved Gateway ID: {}", hex::encode(gateway_id)); + + let resp = gw::GetGatewayIdResponse::decode(resp.as_slice())?; + gateway_id.copy_from_slice(&hex::decode(&resp.gateway_id)?); + info!("Retrieved Gateway ID: {}", resp.gateway_id); GATEWAY_ID .set(Mutex::new(gateway_id)) .map_err(|e| anyhow!("OnceLock error: {:?}", e))?; // Set CMD channel. - CONCENTRATORD_CMD_CHAN .set(cmd_tx) .map_err(|e| anyhow!("OnceLock error: {:?}", e))?; // Setup ZMQ event. - - let (event_tx, event_rx) = mpsc::unbounded_channel::(); + let (event_tx, event_rx) = mpsc::unbounded_channel::(); // Spawn the zmq event handler to a dedicated thread. thread::spawn({ @@ -143,7 +149,7 @@ async fn setup_mesh_conncentratord(conf: &Configuration) -> Result<()> { sock.connect(&command_url).unwrap(); while let Some(cmd) = cmd_rx.blocking_recv() { - let resp = send_zmq_command(&mut sock, &cmd); + let resp = send_zmq_command(&mut sock, &cmd.0); cmd.1.send(resp).unwrap(); } @@ -155,12 +161,29 @@ async fn setup_mesh_conncentratord(conf: &Configuration) -> Result<()> { trace!("Reading Gateway ID"); let (gateway_id_tx, gateway_id_rx) = oneshot::channel::>>(); - cmd_tx.send((("gateway_id".to_string(), vec![]), gateway_id_tx))?; + cmd_tx.send(( + gw::Command { + command: Some(gw::command::Command::GetGatewayId( + gw::GetGatewayIdRequest {}, + )), + }, + gateway_id_tx, + ))?; let resp = gateway_id_rx.await??; - info!("Retrieved Gateway ID: {}", hex::encode(&resp)); + let resp = gw::GetGatewayIdResponse::decode(resp.as_slice())?; + info!("Retrieved Gateway ID: {}", resp.gateway_id); let mut relay_id: [u8; 4] = [0; 4]; - relay_id.copy_from_slice(&resp[4..]); + if conf.mesh.relay_id.is_empty() { + relay_id.copy_from_slice(&hex::decode(&resp.gateway_id)?[4..]); + } else { + info!("Using relay_id from configuration file"); + let b = hex::decode(&conf.mesh.relay_id)?; + if b.len() != 4 { + return Err(anyhow!("relay_id must be exactly 4 bytes!")); + } + relay_id.copy_from_slice(&b); + } RELAY_ID .set(Mutex::new(relay_id)) .map_err(|e| anyhow!("OnceLock error: {:?}", e))?; @@ -173,7 +196,7 @@ async fn setup_mesh_conncentratord(conf: &Configuration) -> Result<()> { // Setup ZMQ event. - let (event_tx, event_rx) = mpsc::unbounded_channel::(); + let (event_tx, event_rx) = mpsc::unbounded_channel::(); // Spawn the zmq event handler to a dedicated thread; thread::spawn({ @@ -211,7 +234,7 @@ async fn setup_mesh_conncentratord(conf: &Configuration) -> Result<()> { async fn event_loop( border_gateway: bool, border_gateway_ignore_direct_uplinks: bool, - mut event_rx: mpsc::UnboundedReceiver, + mut event_rx: mpsc::UnboundedReceiver, filters: lrwn_filters::Filters, ) { trace!("Starting event loop"); @@ -219,7 +242,7 @@ async fn event_loop( if let Err(e) = handle_event_msg( border_gateway, border_gateway_ignore_direct_uplinks, - &event, + event, &filters, ) .await @@ -230,10 +253,10 @@ async fn event_loop( } } -async fn mesh_event_loop(border_gateway: bool, mut event_rx: mpsc::UnboundedReceiver) { +async fn mesh_event_loop(border_gateway: bool, mut event_rx: mpsc::UnboundedReceiver) { trace!("Starting mesh event loop"); while let Some(event) = event_rx.recv().await { - if let Err(e) = handle_mesh_event_msg(border_gateway, &event).await { + if let Err(e) = handle_mesh_event_msg(border_gateway, event).await { error!("Handle mesh event error: {}", e); continue; } @@ -243,20 +266,14 @@ async fn mesh_event_loop(border_gateway: bool, mut event_rx: mpsc::UnboundedRece async fn handle_event_msg( border_gateway: bool, border_gateway_ignore_direct_uplinks: bool, - event: &Event, + event: gw::Event, filters: &lrwn_filters::Filters, ) -> Result<()> { - trace!( - "Handling event, event: {}, data: {}", - event.0, - hex::encode(&event.1) - ); - - match event.0.as_str() { - "up" => { - let pl = gw::UplinkFrame::decode(event.1.as_slice())?; + trace!("Handling event, event: {:?}", event,); - if let Some(rx_info) = &pl.rx_info { + match &event.event { + Some(gw::event::Event::UplinkFrame(v)) => { + if let Some(rx_info) = &v.rx_info { // Filter out frames with invalid CRC. if rx_info.crc_status() != gw::CrcStatus::CrcOk { debug!( @@ -267,7 +284,7 @@ async fn handle_event_msg( } // Filter out proprietary payloads. - if pl.phy_payload.first().cloned().unwrap_or_default() & 0xe0 == 0xe0 { + if v.phy_payload.first().cloned().unwrap_or_default() & 0xe0 == 0xe0 { debug!( "Discarding proprietary uplink, uplink_id: {}", rx_info.uplink_id @@ -282,128 +299,115 @@ async fn handle_event_msg( } // Filter uplinks based on DevAddr and JoinEUI filters. - if !lrwn_filters::matches(&pl.phy_payload, filters) { + if !lrwn_filters::matches(&v.phy_payload, filters) { debug!( "Discarding uplink because of dev_addr and join_eui filters, uplink_id: {}", rx_info.uplink_id ) } - info!("Frame received - {}", helpers::format_uplink(&pl)?); - mesh::handle_uplink(border_gateway, pl).await?; + info!("Frame received - {}", helpers::format_uplink(v)?); + mesh::handle_uplink(border_gateway, v).await?; } } - "stats" => { + Some(gw::event::Event::GatewayStats(_)) => { if border_gateway { - let pl = gw::GatewayStats::decode(event.1.as_slice())?; - info!("Gateway stats received, gateway_id: {}", pl.gateway_id); - proxy::send_stats(&pl).await?; + proxy::send_event(event).await?; } } - _ => { - return Ok(()); - } + _ => {} } Ok(()) } -async fn handle_mesh_event_msg(border_gateway: bool, event: &Event) -> Result<()> { - trace!( - "Handling mesh event, event: {}, data: {}", - event.0, - hex::encode(&event.1) - ); - - match event.0.as_str() { - "up" => { - let pl = gw::UplinkFrame::decode(event.1.as_slice())?; - - if let Some(rx_info) = &pl.rx_info { - // Filter out frames with invalid CRC. - if rx_info.crc_status() != gw::CrcStatus::CrcOk { - debug!( - "Discarding uplink, CRC != OK, uplink_id: {}", - rx_info.uplink_id - ); - return Ok(()); - } - } - - // The mesh event msg must always be a proprietary payload. - if pl.phy_payload.first().cloned().unwrap_or_default() & 0xe0 == 0xe0 { - info!("Mesh frame received - {}", helpers::format_uplink(&pl)?); - mesh::handle_mesh(border_gateway, pl).await?; +async fn handle_mesh_event_msg(border_gateway: bool, event: gw::Event) -> Result<()> { + trace!("Handling mesh event, event: {:?}", event); + + if let Some(gw::event::Event::UplinkFrame(v)) = &event.event { + if let Some(rx_info) = &v.rx_info { + // Filter out frames with invalid CRC. + if rx_info.crc_status() != gw::CrcStatus::CrcOk { + debug!( + "Discarding uplink, CRC != OK, uplink_id: {}", + rx_info.uplink_id + ); + return Ok(()); } } - _ => { - return Ok(()); + + // The mesh event msg must always be a proprietary payload. + if v.phy_payload.first().cloned().unwrap_or_default() & 0xe0 == 0xe0 { + info!("Mesh frame received - {}", helpers::format_uplink(v)?); + mesh::handle_mesh(border_gateway, v).await?; } } Ok(()) } -async fn send_command(cmd: &str, b: &[u8]) -> Result> { - trace!( - "Sending command, command: {}, data: {}", - cmd, - hex::encode(b) - ); +async fn send_command(cmd: gw::Command) -> Result> { + trace!("Sending command, command: {:?}", cmd,); let cmd_chan = CONCENTRATORD_CMD_CHAN .get() .ok_or_else(|| anyhow!("CONCENTRATORD_CMD_CHAN is not set"))?; let (cmd_tx, cmd_rx) = oneshot::channel::>>(); - cmd_chan.send(((cmd.to_string(), b.to_vec()), cmd_tx))?; + cmd_chan.send((cmd, cmd_tx))?; cmd_rx.await? } -async fn send_mesh_command(cmd: &str, b: &[u8]) -> Result> { - trace!( - "Sending mesh command, command: {}, data: {}", - cmd, - hex::encode(b) - ); +async fn send_mesh_command(cmd: gw::Command) -> Result> { + trace!("Sending mesh command, command: {:?}", cmd); let cmd_chan = MESH_CONCENTRATORD_CMD_CHAN .get() .ok_or_else(|| anyhow!("MESH_CONCENTRATORD_CMD_CHAN is not set"))?; let (cmd_tx, cmd_rx) = oneshot::channel::>>(); - cmd_chan.send(((cmd.to_string(), b.to_vec()), cmd_tx))?; + cmd_chan.send((cmd, cmd_tx))?; cmd_rx.await? } -pub async fn mesh(pl: &gw::DownlinkFrame) -> Result<()> { - info!("Sending mesh frame - {}", helpers::format_downlink(pl)?); +pub async fn mesh(pl: gw::DownlinkFrame) -> Result<()> { + info!("Sending mesh frame - {}", helpers::format_downlink(&pl)?); + let downlink_id = pl.downlink_id; let tx_ack = { - let b = pl.encode_to_vec(); - let resp_b = send_mesh_command("down", &b).await?; + let pl = gw::Command { + command: Some(gw::command::Command::SendDownlinkFrame(pl)), + }; + let resp_b = send_mesh_command(pl).await?; gw::DownlinkTxAck::decode(resp_b.as_slice())? }; helpers::tx_ack_to_err(&tx_ack)?; - info!("Enqueue acknowledged, downlink_id: {}", pl.downlink_id); + info!("Enqueue acknowledged, downlink_id: {}", downlink_id); Ok(()) } -pub async fn send_downlink(pl: &gw::DownlinkFrame) -> Result { - info!("Sending downlink frame - {}", helpers::format_downlink(pl)?); +pub async fn send_downlink(pl: gw::DownlinkFrame) -> Result { + info!( + "Sending downlink frame - {}", + helpers::format_downlink(&pl)? + ); - let b = pl.encode_to_vec(); - let resp_b = send_command("down", &b).await?; + let pl = gw::Command { + command: Some(gw::command::Command::SendDownlinkFrame(pl)), + }; + let resp_b = send_command(pl).await?; let tx_ack = gw::DownlinkTxAck::decode(resp_b.as_slice())?; Ok(tx_ack) } -pub async fn send_gateway_configuration(pl: &gw::GatewayConfiguration) -> Result<()> { +pub async fn send_gateway_configuration(pl: gw::GatewayConfiguration) -> Result<()> { info!("Sending gateway configuration, version: {}", pl.version); - let b = pl.encode_to_vec(); - let _ = send_command("config", &b).await?; + let pl = gw::Command { + command: Some(gw::command::Command::SetGatewayConfiguration(pl)), + }; + let _ = send_command(pl).await?; Ok(()) } @@ -428,15 +432,9 @@ pub async fn get_gateway_id() -> Result<[u8; 8]> { .await) } -fn send_zmq_command(sock: &mut zmq::Socket, cmd: &Command) -> Result> { - debug!( - "Sending command to socket, command: {}, payload: {}", - &cmd.0 .0, - hex::encode(&cmd.0 .1) - ); - - sock.send(&cmd.0 .0, zmq::SNDMORE)?; - sock.send(&cmd.0 .1, 0)?; +fn send_zmq_command(sock: &mut zmq::Socket, cmd: &gw::Command) -> Result> { + debug!("Sending command to socket, command: {:?}", &cmd,); + sock.send(cmd.encode_to_vec(), 0)?; // set poller so that we can timeout after 100ms let mut items = [sock.as_poll_item(zmq::POLLIN)]; @@ -445,19 +443,13 @@ fn send_zmq_command(sock: &mut zmq::Socket, cmd: &Command) -> Result> { return Err(anyhow!("Could not read down response")); } - // red tx ack response + // read tx ack response let resp_b: &[u8] = &sock.recv_bytes(0)?; Ok(resp_b.to_vec()) } -fn receive_zmq_event(sock: &mut zmq::Socket) -> Result { - let msg = sock.recv_multipart(0)?; - if msg.len() != 2 { - return Err(anyhow!("Event must have 2 frames")); - } - - let event = String::from_utf8(msg[0].to_vec())?; - let b = msg[1].to_vec(); - - Ok((event, b)) +fn receive_zmq_event(sock: &mut zmq::Socket) -> Result { + let b = sock.recv_bytes(0)?; + let event = gw::Event::decode(b.as_slice())?; + Ok(event) } diff --git a/src/cache.rs b/src/cache.rs index 3a6a50b..1c46af0 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -59,7 +59,17 @@ impl From<&packets::MeshPacket> for PayloadCache { relay_id: v.relay_id, timestamp: 0, }, - packets::Payload::Heartbeat(v) => PayloadCache { + packets::Payload::Event(v) => PayloadCache { + p_type, + uplink_id: 0, + relay_id: v.relay_id, + timestamp: v + .timestamp + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as u32, + }, + packets::Payload::Command(v) => PayloadCache { p_type, uplink_id: 0, relay_id: v.relay_id, diff --git a/src/cmd/configfile.rs b/src/cmd/configfile.rs index 659ad7c..0647a1b 100644 --- a/src/cmd/configfile.rs +++ b/src/cmd/configfile.rs @@ -24,12 +24,28 @@ pub fn run() { # Mesh configuration. [mesh] - # Signing key (AES128, HEX encoded). + # Mesh root key (AES128, HEX encoded). + # + # This key is used to derive the signing and encryption keys. The same key + # must be configured on every Border and Relay gateway. + root_key="{{ mesh.root_key }}" + + # Signing key (AES128, HEX encoded) (deprecated). # # This key is used to sign and validate each mesh packet. This key must be # configured on every Border / Relay gateway equally. + # + # Deprecation note: If set, the signing key will not be derrived from the + # above root_key, but this key will be used. signing_key="{{ mesh.signing_key }}" + # Relay ID. + # + # If set, this will override the Relay ID that is derived from the + # Gateway ID provided by the Concentratord backend (using the 4 least + # significant bytes). Example: "01020304". + relay_id="{{ mesh.relay_id }}" + # Border Gateway. # # If this is set to true, then the ChirpStack Gateway Mesh will consider @@ -37,12 +53,6 @@ pub fn run() { # uplinks and forward these to the proxy API, rather than relaying these. border_gateway={{ mesh.border_gateway }} - # Heartbeat interval (Relay Gateway only). - # - # This defines the interval in which a Relay Gateway (border_gateway=false) - # will emit heartbeat messages. - heartbeat_interval="{{ mesh.heartbeat_interval }}" - # Max hop count. # # This defines the maximum number of hops a relayed payload will pass. @@ -142,6 +152,63 @@ pub fn run() { # Command API URL. command_url="{{ backend.mesh_concentratord.command_url }}" + + +# Events configuration (Relay only). +[events] + + # Heartbeat interval (Relay Gateway only). + # + # This defines the interval in which a Relay Gateway (border_gateway=false) + # will emit heartbeat messages. + heartbeat_interval="{{ events.heartbeat_interval }}" + + # Commands. + # + # This configures for each event type the command that must be executed. The + # stdout of the command will be used as event payload. Example: + # + # 128 = ["/path/to/command", "arg1", "arg2"] + # + [events.commands] + + {{#each events.commands}} + {{@key}} = [{{#each this}}"{{this}}", {{/each}}] + {{/each}} + + # Event sets (can be repeated). + # + # This configures sets of events that will be periodically sent by the + # relay. Example: + # + # [[events.sets]] + # interval = "5min" + # events = [128, 129, 130] + # + {{#each events.sets}} + [[events.sets]] + interval = "{{this.interval}}" + events = [{{#each this.events}}{{this}}, {{/each}}] + {{/each}} + + +# Commands configuration (Relay only). +[commands] + + # Commands. + # + # On receiving a command, the Gateway Mesh will execute the command matching + # the command type (128 - 255 is for proprietary commands). The payload will + # be provided to the command using stdin. The returned stdout will be sent + # back as event (using the same type). Example: + # + # "129" = ["/path/to/command", "arg1", "arg2"] + # + [commands.commands] + + {{#each commands.commands}} + {{@key}} = [{{#each this}}"{{this}}", {{/each}}] + {{/each}} "#; let conf = config::get(); diff --git a/src/cmd/root.rs b/src/cmd/root.rs index 028c3e7..ea7b421 100644 --- a/src/cmd/root.rs +++ b/src/cmd/root.rs @@ -4,12 +4,13 @@ use signal_hook::consts::signal::*; use signal_hook_tokio::Signals; use crate::config::Configuration; -use crate::{backend, heartbeat, proxy}; +use crate::{backend, commands, events, proxy}; pub async fn run(conf: &Configuration) -> Result<()> { proxy::setup(conf).await?; backend::setup(conf).await?; - heartbeat::setup(conf).await?; + events::setup(conf).await?; + commands::setup(conf).await?; let mut signals = Signals::new([SIGINT, SIGTERM])?; let handle = signals.handle(); diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..36b6627 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; +use std::process::Stdio; +use std::time::SystemTime; + +use anyhow::Result; +use log::error; +use tokio::io::AsyncWriteExt; +use tokio::process::Command; +use tokio::sync::{Mutex, OnceCell}; + +use crate::{config::Configuration, packets}; + +static COMMANDS: OnceCell>> = OnceCell::const_new(); +static LAST_TIMESTAMP: OnceCell>> = OnceCell::const_new(); + +pub async fn setup(conf: &Configuration) -> Result<()> { + // Only Relay Gateways process commands. + if conf.mesh.border_gateway { + return Ok(()); + } + + // Set commands. + COMMANDS + .set( + conf.commands + .commands + .iter() + .map(|(k, v)| (k.parse().unwrap(), v.clone())) + .collect(), + ) + .map_err(|_| anyhow!("OnceCell set error"))?; + + Ok(()) +} + +pub async fn execute_commands(pl: &packets::CommandPayload) -> Result> { + // Validate that the command timestamp did increment, compared to previous + // command payload. + if let Some(ts) = get_last_timestamp().await { + if ts >= pl.timestamp { + return Err(anyhow!( + "Command timestamp did not increment compared to previous command payload" + )); + } + } + + // Store the command timestamp. + set_last_timestamp(pl.timestamp).await; + + // Execute the commands and capture the response events. + let mut out = vec![]; + for cmd in &pl.commands { + let resp = match cmd { + packets::Command::Proprietary((t, v)) => execute_proprietary(*t, v).await, + packets::Command::Encrypted(_) => panic!("Commands must be decrypted first"), + }; + + match resp { + Ok(v) => out.push(v), + Err(e) => error!("Execute command error: {}", e), + } + } + + Ok(out) +} + +async fn execute_proprietary(typ: u8, value: &[u8]) -> Result { + let args = COMMANDS + .get() + .ok_or_else(|| anyhow!("COMMANDS is not set"))? + .get(&typ) + .ok_or_else(|| anyhow!("Command type {} is not configured", typ))?; + + if args.is_empty() { + return Err(anyhow!("Command for command type {} is empty", typ,)); + } + + let mut cmd = Command::new(&args[0]); + cmd.stdin(Stdio::piped()); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + // Add addition args. + if args.len() > 1 { + cmd.args(&args[1..]); + } + + // Spawn process + let mut child = cmd.spawn()?; + + // Write stdin + let mut stdin = child.stdin.take().unwrap(); + tokio::spawn({ + let b = value.to_vec(); + async move { stdin.write(&b).await } + }); + + // Wait for output + let out = child.wait_with_output().await?; + Ok(packets::Event::Proprietary((typ, out.stdout))) +} + +async fn get_last_timestamp() -> Option { + *LAST_TIMESTAMP + .get_or_init(|| async { Mutex::new(None) }) + .await + .lock() + .await +} + +async fn set_last_timestamp(ts: SystemTime) { + let mut last_ts = LAST_TIMESTAMP + .get_or_init(|| async { Mutex::new(None) }) + .await + .lock() + .await; + + *last_ts = Some(ts); +} diff --git a/src/config.rs b/src/config.rs index 41e8532..06ff20d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; use std::{env, fs}; @@ -17,6 +18,8 @@ pub struct Configuration { pub mesh: Mesh, pub backend: Backend, pub mappings: Mappings, + pub events: Events, + pub commands: Commands, } impl Configuration { @@ -55,9 +58,9 @@ impl Default for Logging { #[derive(Serialize, Deserialize)] #[serde(default)] pub struct Mesh { + pub relay_id: String, + pub root_key: Aes128Key, pub signing_key: Aes128Key, - #[serde(with = "humantime_serde")] - pub heartbeat_interval: Duration, pub frequencies: Vec, pub data_rate: DataRate, pub tx_power: i32, @@ -71,8 +74,9 @@ pub struct Mesh { impl Default for Mesh { fn default() -> Self { Mesh { + relay_id: "".into(), + root_key: Aes128Key::null(), signing_key: Aes128Key::null(), - heartbeat_interval: Duration::from_secs(300), frequencies: vec![868100000, 868300000, 868500000], data_rate: DataRate { modulation: Modulation::LORA, @@ -155,6 +159,39 @@ pub struct DataRate { pub bitrate: u32, } +#[derive(Serialize, Deserialize, PartialEq, Eq)] +#[serde(default)] +pub struct Events { + #[serde(with = "humantime_serde")] + pub heartbeat_interval: Duration, + pub commands: HashMap>, + pub sets: Vec, +} + +impl Default for Events { + fn default() -> Self { + Events { + heartbeat_interval: Duration::from_secs(300), + commands: Default::default(), + sets: Vec::new(), + } + } +} + +#[derive(Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default)] +pub struct EventsSet { + #[serde(with = "humantime_serde")] + pub interval: Duration, + pub events: Vec, +} + +#[derive(Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default)] +pub struct Commands { + pub commands: HashMap>, +} + #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)] #[allow(non_camel_case_types)] #[allow(clippy::upper_case_acronyms)] diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..7cc0a60 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,173 @@ +use std::collections::HashMap; +use std::time::SystemTime; + +use anyhow::Result; +use chirpstack_api::gw; +use log::{error, info}; +use rand::random; +use tokio::process::Command; +use tokio::sync::OnceCell; +use tokio::time::sleep; + +use crate::aes128::{get_encryption_key, get_signing_key, Aes128Key}; +use crate::backend; +use crate::config::{self, Configuration}; +use crate::helpers; +use crate::mesh::get_mesh_frequency; +use crate::packets; + +static COMMANDS: OnceCell>> = OnceCell::const_new(); + +pub async fn setup(conf: &Configuration) -> Result<()> { + // Only Relay Gateways report events. + if conf.mesh.border_gateway { + return Ok(()); + } + + // Set commands. + COMMANDS + .set( + conf.events + .commands + .iter() + .map(|(k, v)| (k.parse().unwrap(), v.clone())) + .collect(), + ) + .map_err(|_| anyhow!("OnceCell set error"))?; + + // Setup heartbeat event loop. + if !conf.events.heartbeat_interval.is_zero() { + info!( + "Starting heartbeat loop, heartbeat_interval: {:?}", + conf.events.heartbeat_interval + ); + + tokio::spawn({ + let heartbeat_interval = conf.events.heartbeat_interval; + + async move { + loop { + if let Err(e) = report_heartbeat().await { + error!("Report heartbeat error, error: {}", e); + } + sleep(heartbeat_interval).await; + } + } + }); + } + + // Setup event-set loops. + for event_set in &conf.events.sets { + info!( + "Starting event-set loop, events: {:?}, interval: {:?}", + event_set.events, event_set.interval + ); + + tokio::spawn({ + let events = event_set.events.clone(); + let interval = event_set.interval; + + async move { + loop { + sleep(interval).await; + if let Err(e) = report_events(&events).await { + error!("Report event-set error, error: {}", e); + } + } + } + }); + } + + Ok(()) +} + +pub async fn report_heartbeat() -> Result<()> { + info!("Sending heartbeat event"); + send_events(vec![packets::Event::Heartbeat(packets::HeartbeatPayload { + relay_path: vec![], + })]) + .await +} + +pub async fn report_events(typs: &[u8]) -> Result<()> { + let mut events = Vec::new(); + for typ in typs { + events.push(get_event(*typ).await?); + } + info!("Sending events, events: {:?}", typs); + send_events(events).await +} + +async fn get_event(typ: u8) -> Result { + let args = COMMANDS + .get() + .ok_or_else(|| anyhow!("COMMANDS is not set"))? + .get(&typ) + .ok_or_else(|| anyhow!("Event type {} is not configured", typ))?; + + if args.is_empty() { + return Err(anyhow!("Command for event type {} is empty", typ)); + } + + let mut cmd = Command::new(&args[0]); + if args.len() > 1 { + cmd.args(&args[1..]); + } + + let output = cmd.output().await?; + + Ok(packets::Event::Proprietary((typ, output.stdout))) +} + +pub async fn send_events(events: Vec) -> Result<()> { + let conf = config::get(); + + let mut packet = packets::MeshPacket { + mhdr: packets::MHDR { + payload_type: packets::PayloadType::Event, + hop_count: 1, + }, + payload: packets::Payload::Event(packets::EventPayload { + timestamp: SystemTime::now(), + relay_id: backend::get_relay_id().await?, + events, + }), + mic: None, + }; + packet.encrypt(get_encryption_key(conf.mesh.root_key))?; + packet.set_mic(if conf.mesh.signing_key != Aes128Key::null() { + conf.mesh.signing_key + } else { + get_signing_key(conf.mesh.root_key) + })?; + + let pl = gw::DownlinkFrame { + downlink_id: random(), + items: vec![gw::DownlinkFrameItem { + phy_payload: packet.to_vec()?, + tx_info: Some(gw::DownlinkTxInfo { + frequency: get_mesh_frequency(&conf)?, + modulation: Some(helpers::data_rate_to_gw_modulation( + &conf.mesh.data_rate, + false, + )), + power: conf.mesh.tx_power, + timing: Some(gw::Timing { + parameters: Some(gw::timing::Parameters::Immediately( + gw::ImmediatelyTimingInfo {}, + )), + }), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + info!( + "Sending event packet, downlink_id: {}, mesh_packet: {}", + pl.downlink_id, packet + ); + + backend::mesh(pl).await +} diff --git a/src/heartbeat.rs b/src/heartbeat.rs deleted file mode 100644 index aa1bdb8..0000000 --- a/src/heartbeat.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::time::SystemTime; - -use anyhow::Result; -use chirpstack_api::gw; -use log::{error, info}; -use rand::random; -use tokio::time::sleep; - -use crate::backend; -use crate::config::{self, Configuration}; -use crate::helpers; -use crate::mesh::get_mesh_frequency; -use crate::packets; - -pub async fn setup(conf: &Configuration) -> Result<()> { - // Only Relay gatewways need to report heartbeat as the Border Gateway is already internet - // connected and reports status through the Concentratord. - if conf.mesh.border_gateway || conf.mesh.heartbeat_interval.is_zero() { - return Ok(()); - } - - info!( - "Starting heartbeat loop, heartbeat_interval: {:?}", - conf.mesh.heartbeat_interval - ); - - tokio::spawn({ - let heartbeat_interval = conf.mesh.heartbeat_interval; - - async move { - loop { - if let Err(e) = report_heartbeat().await { - error!("Report heartbeat error, error: {}", e); - } - sleep(heartbeat_interval).await; - } - } - }); - - Ok(()) -} - -pub async fn report_heartbeat() -> Result<()> { - let conf = config::get(); - - let mut packet = packets::MeshPacket { - mhdr: packets::MHDR { - payload_type: packets::PayloadType::Heartbeat, - hop_count: 1, - }, - payload: packets::Payload::Heartbeat(packets::HeartbeatPayload { - timestamp: SystemTime::now(), - relay_id: backend::get_relay_id().await.unwrap_or_default(), - relay_path: vec![], - }), - mic: None, - }; - packet.set_mic(conf.mesh.signing_key)?; - - let pl = gw::DownlinkFrame { - downlink_id: random(), - items: vec![gw::DownlinkFrameItem { - phy_payload: packet.to_vec()?, - tx_info: Some(gw::DownlinkTxInfo { - frequency: get_mesh_frequency(&conf)?, - modulation: Some(helpers::data_rate_to_gw_modulation( - &conf.mesh.data_rate, - false, - )), - power: conf.mesh.tx_power, - timing: Some(gw::Timing { - parameters: Some(gw::timing::Parameters::Immediately( - gw::ImmediatelyTimingInfo {}, - )), - }), - ..Default::default() - }), - ..Default::default() - }], - ..Default::default() - }; - - info!( - "Sending heartbeat packet, downlink_id: {}, mesh_packet: {}", - pl.downlink_id, packet - ); - backend::mesh(&pl).await -} diff --git a/src/lib.rs b/src/lib.rs index 8d58cf1..e893ca7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,9 @@ pub mod aes128; pub mod backend; pub mod cache; pub mod cmd; +pub mod commands; pub mod config; -pub mod heartbeat; +pub mod events; pub mod helpers; pub mod logging; pub mod mesh; diff --git a/src/mesh.rs b/src/mesh.rs index c46702a..5df99c8 100644 --- a/src/mesh.rs +++ b/src/mesh.rs @@ -1,20 +1,22 @@ use std::collections::HashMap; -use std::sync::LazyLock; -use std::sync::Mutex; +use std::sync::{LazyLock, Mutex}; +use std::time::SystemTime; use anyhow::Result; -use chirpstack_api::gw; +use chirpstack_api::{gw, prost_types}; use log::{info, trace, warn}; use rand::random; use crate::{ + aes128::{get_encryption_key, get_signing_key, Aes128Key}, backend, cache::{Cache, PayloadCache}, + commands, config::{self, Configuration}, - helpers, + events, helpers, packets::{ - self, DownlinkMetadata, MeshPacket, Payload, PayloadType, UplinkMetadata, UplinkPayload, - MHDR, + self, DownlinkMetadata, Event, MeshPacket, Payload, PayloadType, UplinkMetadata, + UplinkPayload, MHDR, }, proxy, }; @@ -28,18 +30,22 @@ static PAYLOAD_CACHE: LazyLock>> = LazyLock::new(|| Mutex::new(Cache::new(64))); // Handle LoRaWAN payload (non-proprietary). -pub async fn handle_uplink(border_gateway: bool, pl: gw::UplinkFrame) -> Result<()> { +pub async fn handle_uplink(border_gateway: bool, pl: &gw::UplinkFrame) -> Result<()> { match border_gateway { - true => proxy_uplink_lora_packet(&pl).await, - false => relay_uplink_lora_packet(&pl).await, + true => proxy_uplink_lora_packet(pl).await, + false => relay_uplink_lora_packet(pl).await, } } // Handle Proprietary LoRaWAN payload (mesh encapsulated). -pub async fn handle_mesh(border_gateway: bool, pl: gw::UplinkFrame) -> Result<()> { +pub async fn handle_mesh(border_gateway: bool, pl: &gw::UplinkFrame) -> Result<()> { let conf = config::get(); - let packet = MeshPacket::from_slice(&pl.phy_payload)?; - if !packet.validate_mic(conf.mesh.signing_key)? { + let mut packet = MeshPacket::from_slice(&pl.phy_payload)?; + if !packet.validate_mic(if conf.mesh.signing_key != Aes128Key::null() { + conf.mesh.signing_key + } else { + get_signing_key(conf.mesh.root_key) + })? { warn!("Dropping packet, invalid MIC, mesh_packet: {}", packet); return Ok(()); } @@ -54,14 +60,17 @@ pub async fn handle_mesh(border_gateway: bool, pl: gw::UplinkFrame) -> Result<() return Ok(()); }; + // Decrypt the packet (in case it contains an encrypted payload). + packet.decrypt(get_encryption_key(conf.mesh.root_key))?; + match border_gateway { // Proxy relayed uplink true => match packet.mhdr.payload_type { - PayloadType::Uplink => proxy_uplink_mesh_packet(&pl, packet).await, - PayloadType::Heartbeat => proxy_heartbeat_mesh_packet(&pl, packet).await, + PayloadType::Uplink => proxy_uplink_mesh_packet(pl, packet).await, + PayloadType::Event => proxy_event_mesh_packet(pl, packet).await, _ => Ok(()), }, - false => relay_mesh_packet(&pl, packet).await, + false => relay_mesh_packet(pl, packet).await, } } @@ -76,17 +85,83 @@ pub async fn handle_downlink(pl: gw::DownlinkFrame) -> Result if tx_info.context.len() != CTX_PREFIX.len() + 6 || !tx_info.context[0..CTX_PREFIX.len()].eq(&CTX_PREFIX) { - return proxy_downlink_lora_packet(&pl).await; + return proxy_downlink_lora_packet(pl).await; } } relay_downlink_lora_packet(&pl).await } -async fn proxy_downlink_lora_packet(pl: &gw::DownlinkFrame) -> Result { +pub async fn send_mesh_command(pl: gw::MeshCommand) -> Result<()> { + let conf = config::get(); + + let mut packet = packets::MeshPacket { + mhdr: packets::MHDR { + payload_type: packets::PayloadType::Command, + hop_count: 1, + }, + payload: packets::Payload::Command(packets::CommandPayload { + timestamp: SystemTime::now(), + relay_id: { + let mut relay_id: [u8; 4] = [0; 4]; + hex::decode_to_slice(&pl.relay_id, &mut relay_id)?; + relay_id + }, + commands: pl + .commands + .iter() + .filter_map(|v| { + v.command + .as_ref() + .map(|gw::mesh_command_item::Command::Proprietary(v)| { + packets::Command::Proprietary((v.command_type as u8, v.payload.clone())) + }) + }) + .collect(), + }), + mic: None, + }; + packet.encrypt(get_encryption_key(conf.mesh.root_key))?; + packet.set_mic(if conf.mesh.signing_key != Aes128Key::null() { + conf.mesh.signing_key + } else { + get_signing_key(conf.mesh.root_key) + })?; + + let pl = gw::DownlinkFrame { + downlink_id: random(), + items: vec![gw::DownlinkFrameItem { + phy_payload: packet.to_vec()?, + tx_info: Some(gw::DownlinkTxInfo { + frequency: get_mesh_frequency(&conf)?, + modulation: Some(helpers::data_rate_to_gw_modulation( + &conf.mesh.data_rate, + false, + )), + power: conf.mesh.tx_power, + timing: Some(gw::Timing { + parameters: Some(gw::timing::Parameters::Immediately( + gw::ImmediatelyTimingInfo {}, + )), + }), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + info!( + "Sending mesh packet, downlink_id: {}, mesh_packet: {}", + pl.downlink_id, packet + ); + backend::mesh(pl).await +} + +async fn proxy_downlink_lora_packet(pl: gw::DownlinkFrame) -> Result { info!( "Proxying LoRaWAN downlink, downlink: {}", - helpers::format_downlink(pl)? + helpers::format_downlink(&pl)? ); backend::send_downlink(pl).await } @@ -96,7 +171,12 @@ async fn proxy_uplink_lora_packet(pl: &gw::UplinkFrame) -> Result<()> { "Proxying LoRaWAN uplink, uplink: {}", helpers::format_uplink(pl)? ); - proxy::send_uplink(pl).await + + let pl = gw::Event { + event: Some(gw::event::Event::UplinkFrame(pl.clone())), + }; + + proxy::send_event(pl).await } async fn proxy_uplink_mesh_packet(pl: &gw::UplinkFrame, packet: MeshPacket) -> Result<()> { @@ -150,39 +230,66 @@ async fn proxy_uplink_mesh_packet(pl: &gw::UplinkFrame, packet: MeshPacket) -> R // Set original PHYPayload. pl.phy_payload.clone_from(&mesh_pl.phy_payload); - proxy::send_uplink(&pl).await + let pl = gw::Event { + event: Some(gw::event::Event::UplinkFrame(pl)), + }; + + proxy::send_event(pl).await } -async fn proxy_heartbeat_mesh_packet(pl: &gw::UplinkFrame, packet: MeshPacket) -> Result<()> { +async fn proxy_event_mesh_packet(pl: &gw::UplinkFrame, packet: MeshPacket) -> Result<()> { let mesh_pl = match &packet.payload { - Payload::Heartbeat(v) => v, + Payload::Event(v) => v, _ => { return Err(anyhow!("Expected Heartbeat payload")); } }; info!( - "Unwrapping relay heartbeat packet, uplink_id: {}, mesh_packet: {}", + "Unwrapping relay event packet, uplink_id: {}, mesh_packet: {}", pl.rx_info.as_ref().map(|v| v.uplink_id).unwrap_or_default(), packet ); - let heartbeat_pl = gw::MeshHeartbeat { - gateway_id: hex::encode(backend::get_gateway_id().await?), - relay_id: hex::encode(mesh_pl.relay_id), - relay_path: mesh_pl - .relay_path - .iter() - .map(|v| gw::MeshHeartbeatRelayPath { - relay_id: hex::encode(v.relay_id), - rssi: v.rssi.into(), - snr: v.snr.into(), - }) - .collect(), - time: Some(mesh_pl.timestamp.into()), + let event = gw::Event { + event: Some(gw::event::Event::Mesh(gw::MeshEvent { + gateway_id: hex::encode(backend::get_gateway_id().await?), + relay_id: hex::encode(mesh_pl.relay_id), + time: Some(mesh_pl.timestamp.into()), + events: mesh_pl + .events + .iter() + .map(|e| gw::MeshEventItem { + event: Some(match e { + Event::Heartbeat(v) => { + gw::mesh_event_item::Event::Heartbeat(gw::MeshEventHeartbeat { + relay_path: v + .relay_path + .iter() + .map(|v| gw::MeshEventHeartbeatRelayPath { + relay_id: hex::encode(v.relay_id), + rssi: v.rssi.into(), + snr: v.snr.into(), + }) + .collect(), + }) + } + Event::Proprietary(v) => { + gw::mesh_event_item::Event::Proprietary(gw::MeshEventProprietary { + event_type: v.0.into(), + payload: v.1.clone(), + }) + } + Event::Encrypted(_) => panic!("Events must be decrypted first"), + }), + }) + .collect(), + })), }; - proxy::send_mesh_heartbeat(&heartbeat_pl).await + proxy::send_event(event).await?; + + Ok(()) } async fn relay_mesh_packet(pl: &gw::UplinkFrame, mut packet: MeshPacket) -> Result<()> { @@ -238,10 +345,10 @@ async fn relay_mesh_packet(pl: &gw::UplinkFrame, mut packet: MeshPacket) -> Resu "Unwrapping relayed downlink, downlink_id: {}, mesh_packet: {}", pl.downlink_id, packet ); - return helpers::tx_ack_to_err(&backend::send_downlink(&pl).await?); + return helpers::tx_ack_to_err(&backend::send_downlink(pl).await?); } } - packets::Payload::Heartbeat(pl) => { + packets::Payload::Event(pl) => { if pl.relay_id == relay_id { trace!("Dropping packet as this relay was the sender"); @@ -249,12 +356,30 @@ async fn relay_mesh_packet(pl: &gw::UplinkFrame, mut packet: MeshPacket) -> Resu return Ok(()); } - // Add our Relay ID to the path. - pl.relay_path.push(packets::RelayPath { - relay_id, - rssi: rx_info.rssi as i16, - snr: rx_info.snr as i8, - }); + for event in &mut pl.events { + // Add our Relay ID to the path in case of heartbeat event. + if let Event::Heartbeat(v) = event { + v.relay_path.push(packets::RelayPath { + relay_id, + rssi: rx_info.rssi as i16, + snr: rx_info.snr as i8, + }); + } + } + } + packets::Payload::Command(pl) => { + if pl.relay_id == relay_id { + // The command payload was intended for this gateway, execute + // the commands. + let resp = commands::execute_commands(pl).await?; + + // Send back the responses (events). + if !resp.is_empty() { + events::send_events(resp).await?; + } + + return Ok(()); + } } } @@ -264,9 +389,16 @@ async fn relay_mesh_packet(pl: &gw::UplinkFrame, mut packet: MeshPacket) -> Resu // Increment hop count. packet.mhdr.hop_count += 1; + // Encrypt. + packet.encrypt(get_encryption_key(conf.mesh.root_key))?; + // We need to re-set the MIC as we have changed the payload by incrementing // the hop count (and in casee of heartbeat, we have modified the Relay path). - packet.set_mic(conf.mesh.signing_key)?; + packet.set_mic(if conf.mesh.signing_key != Aes128Key::null() { + conf.mesh.signing_key + } else { + get_signing_key(conf.mesh.root_key) + })?; if packet.mhdr.hop_count > conf.mesh.max_hop_count { return Err(anyhow!("Max hop count exceeded")); @@ -299,7 +431,7 @@ async fn relay_mesh_packet(pl: &gw::UplinkFrame, mut packet: MeshPacket) -> Resu "Re-relaying mesh packet, downlink_id: {}, mesh_packet: {}", pl.downlink_id, packet ); - backend::mesh(&pl).await + backend::mesh(pl).await } async fn relay_uplink_lora_packet(pl: &gw::UplinkFrame) -> Result<()> { @@ -336,7 +468,11 @@ async fn relay_uplink_lora_packet(pl: &gw::UplinkFrame) -> Result<()> { }), mic: None, }; - packet.set_mic(conf.mesh.signing_key)?; + packet.set_mic(if conf.mesh.signing_key != Aes128Key::null() { + conf.mesh.signing_key + } else { + get_signing_key(conf.mesh.root_key) + })?; let pl = gw::DownlinkFrame { downlink_id: random(), @@ -366,7 +502,7 @@ async fn relay_uplink_lora_packet(pl: &gw::UplinkFrame) -> Result<()> { rx_info.uplink_id, pl.downlink_id, packet, ); - backend::mesh(&pl).await + backend::mesh(pl).await } async fn relay_downlink_lora_packet(pl: &gw::DownlinkFrame) -> Result { @@ -435,7 +571,11 @@ async fn relay_downlink_lora_packet(pl: &gw::DownlinkFrame) -> Result Result { tx_ack_items[i].status = gw::TxAckStatus::Ok.into(); break; diff --git a/src/packets.rs b/src/packets.rs index 43002e4..6788992 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -1,9 +1,14 @@ use std::fmt; +use std::io::{Cursor, Read}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use aes::Aes128; +use aes::{ + cipher::{generic_array::GenericArray, BlockEncrypt}, + Aes128, Block, +}; use anyhow::Result; use cmac::{Cmac, Mac}; +use log::warn; use crate::aes128::Aes128Key; @@ -62,8 +67,9 @@ impl MeshPacket { PayloadType::Downlink => { Payload::Downlink(DownlinkPayload::from_slice(&b[1..len - 4])?) } - PayloadType::Heartbeat => { - Payload::Heartbeat(HeartbeatPayload::from_slice(&b[1..len - 4])?) + PayloadType::Event => Payload::Event(EventPayload::from_slice(&b[1..len - 4])?), + PayloadType::Command => { + Payload::Command(CommandPayload::from_slice(&b[1..len - 4])?) } }, mic: Some(mic), @@ -76,7 +82,8 @@ impl MeshPacket { b.extend_from_slice(&match &self.payload { Payload::Uplink(v) => v.to_vec()?, Payload::Downlink(v) => v.to_vec()?, - Payload::Heartbeat(v) => v.to_vec()?, + Payload::Event(v) => v.to_vec()?, + Payload::Command(v) => v.to_vec()?, }); if let Some(mic) = self.mic { @@ -93,12 +100,31 @@ impl MeshPacket { b.extend_from_slice(&match &self.payload { Payload::Uplink(v) => v.to_vec()?, Payload::Downlink(v) => v.to_vec()?, - Payload::Heartbeat(v) => v.to_vec()?, + Payload::Event(v) => v.to_vec()?, + Payload::Command(v) => v.to_vec()?, }); Ok(b) } + pub fn encrypt(&mut self, key: Aes128Key) -> Result<()> { + match &mut self.payload { + Payload::Event(pl) => pl.encrypt(key), + Payload::Command(pl) => pl.encrypt(key), + _ => Ok(()), + } + } + + pub fn decrypt(&mut self, key: Aes128Key) -> Result<()> { + match &mut self.payload { + Payload::Event(pl) => pl.decrypt(key), + Payload::Command(pl) => pl.decrypt(key), + _ => Ok(()), + } + } + + // Calculate and set the message integrity code. + // Note: If applicable, the payload must be encrypted first! pub fn set_mic(&mut self, key: Aes128Key) -> Result<()> { self.mic = Some(self.calculate_mic(key)?); Ok(()) @@ -152,7 +178,15 @@ impl fmt::Display for MeshPacket { hex::encode(v.relay_id), self.mic.map(hex::encode).unwrap_or_default(), ), - Payload::Heartbeat(v) => write!( + Payload::Event(v) => write!( + f, + "[{:?} hop_count: {}, timestamp: {:?}, relay_id: {}]", + self.mhdr.payload_type, + self.mhdr.hop_count, + v.timestamp, + hex::encode(v.relay_id), + ), + Payload::Command(v) => write!( f, "[{:?} hop_count: {}, timestamp: {:?}, relay_id: {}]", self.mhdr.payload_type, @@ -191,7 +225,7 @@ impl MHDR { return Err(anyhow!("Max hop_count is 8")); } - Ok(0x07 << 5 | self.payload_type.to_byte() << 3 | (self.hop_count - 1)) + Ok((0x07 << 5) | (self.payload_type.to_byte() << 3) | (self.hop_count - 1)) } } @@ -199,7 +233,8 @@ impl MHDR { pub enum PayloadType { Uplink, Downlink, - Heartbeat, + Event, + Command, } impl PayloadType { @@ -207,7 +242,8 @@ impl PayloadType { Ok(match b { 0x00 => PayloadType::Uplink, 0x01 => PayloadType::Downlink, - 0x02 => PayloadType::Heartbeat, + 0x02 => PayloadType::Event, + 0x03 => PayloadType::Command, _ => return Err(anyhow!("Unexpected PayloadType: {}", b)), }) } @@ -216,7 +252,8 @@ impl PayloadType { match self { PayloadType::Uplink => 0x00, PayloadType::Downlink => 0x01, - PayloadType::Heartbeat => 0x02, + PayloadType::Event => 0x02, + PayloadType::Command => 0x03, } } } @@ -225,7 +262,8 @@ impl PayloadType { pub enum Payload { Uplink(UplinkPayload), Downlink(DownlinkPayload), - Heartbeat(HeartbeatPayload), + Event(EventPayload), + Command(CommandPayload), } #[derive(Debug, PartialEq, Eq, Clone)] @@ -417,33 +455,181 @@ impl DownlinkMetadata { } #[derive(Debug, PartialEq, Eq, Clone)] -pub struct HeartbeatPayload { +pub struct EventPayload { pub timestamp: SystemTime, pub relay_id: [u8; 4], - pub relay_path: Vec, + pub events: Vec, } -impl HeartbeatPayload { - pub fn from_slice(b: &[u8]) -> Result { - if b.len() < 8 { - return Err(anyhow!("At least 8 bytes are expected")); - } - - if (b.len() - 8) % 6 != 0 { - return Err(anyhow!("Invalid amount of Relay path bytes")); - } - +impl EventPayload { + pub fn from_slice(b: &[u8]) -> Result { + let b_len = b.len() as u64; + let mut cur = Cursor::new(b); let mut ts_b: [u8; 4] = [0; 4]; - ts_b.copy_from_slice(&b[0..4]); + + cur.read_exact(&mut ts_b)?; let timestamp = u32::from_be_bytes(ts_b); let timestamp = UNIX_EPOCH .checked_add(Duration::from_secs(timestamp.into())) .ok_or_else(|| anyhow!("Invalid timestamp"))?; let mut relay_id: [u8; 4] = [0; 4]; - relay_id.copy_from_slice(&b[4..8]); + cur.read_exact(&mut relay_id)?; + + let mut events = Vec::new(); - let relay_path: Vec = b[8..] + if cur.position() < b_len { + let mut b = Vec::new(); + cur.read_to_end(&mut b)?; + events.push(Event::Encrypted(b)); + } + + Ok(EventPayload { + timestamp, + relay_id, + events, + }) + } + + pub fn to_vec(&self) -> Result> { + let timestamp = self.timestamp.duration_since(UNIX_EPOCH)?.as_secs() as u32; + let mut b = timestamp.to_be_bytes().to_vec(); + b.extend_from_slice(&self.relay_id); + + for event in &self.events { + b.extend_from_slice(&event.encode()?); + } + + Ok(b) + } + + pub fn encrypt(&mut self, key: Aes128Key) -> Result<()> { + if self.events.is_empty() { + return Ok(()); + } + + // Buffer to encrypt + let mut b = Vec::new(); + for event in &self.events { + b.extend_from_slice(&event.encode()?); + } + + self.events = vec![Event::Encrypted(encrypt_events_commands( + key, + false, + &self.relay_id, + self.timestamp, + &b, + )?)]; + + Ok(()) + } + + pub fn decrypt(&mut self, key: Aes128Key) -> Result<()> { + if self.events.is_empty() { + return Ok(()); + } + + if self.events.len() != 1 { + return Err(anyhow!("Exactly 1 event item expected")); + } + + if let Event::Encrypted(b) = &self.events[0] { + let b = encrypt_events_commands(key, false, &self.relay_id, self.timestamp, b)?; + let b_len = b.len() as u64; + + let mut cur = Cursor::new(b.as_slice()); + let mut events = Vec::new(); + + while cur.position() < b_len { + match Event::decode(&mut cur) { + Ok(v) => events.push(v), + Err(e) => warn!("Decode event error: {}", e), + } + } + + self.events = events; + } else { + return Err(anyhow!("Encrypted event expected")); + } + + Ok(()) + } + + pub fn decode(&mut self) -> Result<()> { + if self.events.is_empty() { + return Ok(()); + } + + if let Event::Encrypted(b) = &self.events[0] { + let b_len = b.len() as u64; + + let mut cur = Cursor::new(b.as_slice()); + let mut events = Vec::new(); + + while cur.position() < b_len { + match Event::decode(&mut cur) { + Ok(v) => events.push(v), + Err(e) => warn!("Decode event error: {}", e), + } + } + + self.events = events; + } + + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Event { + Encrypted(Vec), + Heartbeat(HeartbeatPayload), + Proprietary((u8, Vec)), +} + +impl Event { + pub fn decode(cur: &mut Cursor<&[u8]>) -> Result { + let mut tag_length: [u8; 2] = [0; 2]; + cur.read_exact(&mut tag_length)?; + + let mut value = vec![0; tag_length[1] as usize]; + cur.read_exact(&mut value)?; + + Ok(match tag_length[0] { + 0x00 => Event::Heartbeat(HeartbeatPayload::from_slice(&value)?), + _ => Event::Proprietary((tag_length[0], value)), + }) + } + + pub fn encode(&self) -> Result> { + let (t, v) = match self { + Event::Encrypted(v) => return Ok(v.clone()), + Event::Heartbeat(v) => (0x00, v.to_vec()?), + Event::Proprietary((t, v)) => (*t, v.clone()), + }; + + let mut b = Vec::with_capacity(2 + v.len()); + b.push(t); + b.push(v.len() as u8); + b.extend_from_slice(&v); + + Ok(b) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct HeartbeatPayload { + pub relay_path: Vec, +} + +impl HeartbeatPayload { + pub fn from_slice(b: &[u8]) -> Result { + if b.len() % 6 != 0 { + return Err(anyhow!("Invalid amount of Relay path bytes")); + } + + let relay_path: Vec = b .chunks(6) .map(|v| { let mut b: [u8; 6] = [0; 6]; @@ -452,17 +638,11 @@ impl HeartbeatPayload { }) .collect(); - Ok(HeartbeatPayload { - timestamp, - relay_id, - relay_path, - }) + Ok(HeartbeatPayload { relay_path }) } pub fn to_vec(&self) -> Result> { - let timestamp = self.timestamp.duration_since(UNIX_EPOCH)?.as_secs() as u32; - let mut b = timestamp.to_be_bytes().to_vec(); - b.extend_from_slice(&self.relay_id); + let mut b = Vec::with_capacity(self.relay_path.len() * 6); for relay_path in &self.relay_path { b.extend_from_slice(&relay_path.to_bytes()?); } @@ -525,6 +705,172 @@ impl RelayPath { } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct CommandPayload { + pub timestamp: SystemTime, + pub relay_id: [u8; 4], + pub commands: Vec, +} + +impl CommandPayload { + pub fn from_slice(b: &[u8]) -> Result { + let b_len = b.len() as u64; + let mut cur = Cursor::new(b); + let mut ts_b: [u8; 4] = [0; 4]; + + cur.read_exact(&mut ts_b)?; + let timestamp = u32::from_be_bytes(ts_b); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(timestamp.into())) + .ok_or_else(|| anyhow!("Invalid timestamp"))?; + + let mut relay_id: [u8; 4] = [0; 4]; + cur.read_exact(&mut relay_id)?; + + let mut commands = Vec::new(); + + if cur.position() < b_len { + let mut b = Vec::new(); + cur.read_to_end(&mut b)?; + commands.push(Command::Encrypted(b)); + } + + Ok(CommandPayload { + timestamp, + relay_id, + commands, + }) + } + + pub fn to_vec(&self) -> Result> { + let timestamp = self.timestamp.duration_since(UNIX_EPOCH)?.as_secs() as u32; + let mut b = timestamp.to_be_bytes().to_vec(); + b.extend_from_slice(&self.relay_id); + + for cmd in &self.commands { + b.extend_from_slice(&cmd.encode()?); + } + + Ok(b) + } + + pub fn encrypt(&mut self, key: Aes128Key) -> Result<()> { + if self.commands.is_empty() { + return Ok(()); + } + + // Buffer to encrypt + let mut b = Vec::new(); + for command in &self.commands { + b.extend_from_slice(&command.encode()?); + } + + self.commands = vec![Command::Encrypted(encrypt_events_commands( + key, + true, + &self.relay_id, + self.timestamp, + &b, + )?)]; + + Ok(()) + } + + pub fn decrypt(&mut self, key: Aes128Key) -> Result<()> { + if self.commands.is_empty() { + return Ok(()); + } + + if self.commands.len() != 1 { + return Err(anyhow!("Exactly 1 command item expected")); + } + + if let Command::Encrypted(b) = &self.commands[0] { + let b = encrypt_events_commands(key, true, &self.relay_id, self.timestamp, b)?; + let b_len = b.len() as u64; + + let mut cur = Cursor::new(b.as_slice()); + let mut commands = Vec::new(); + + while cur.position() < b_len { + match Command::decode(&mut cur) { + Ok(v) => commands.push(v), + Err(e) => warn!("Decode command error: {}", e), + } + } + + self.commands = commands; + } else { + return Err(anyhow!("Encrypted command exepcted")); + } + + Ok(()) + } + + pub fn decode(&mut self) -> Result<()> { + if self.commands.is_empty() { + return Ok(()); + } + + if let Command::Encrypted(b) = &self.commands[0] { + let b_len = b.len() as u64; + + let mut cur = Cursor::new(b.as_slice()); + let mut commands = Vec::new(); + + while cur.position() < b_len { + match Command::decode(&mut cur) { + Ok(v) => commands.push(v), + Err(e) => warn!("Decode command error: {}", e), + } + } + + self.commands = commands; + } + + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Command { + Encrypted(Vec), + Proprietary((u8, Vec)), +} + +impl Command { + pub fn decode(cur: &mut Cursor<&[u8]>) -> Result { + let mut tag_length: [u8; 2] = [0; 2]; + cur.read_exact(&mut tag_length)?; + + let mut value = vec![0; tag_length[1] as usize]; + cur.read_exact(&mut value)?; + + Ok(Command::Proprietary((tag_length[0], value))) + } + + pub fn encode(&self) -> Result> { + let (t, v) = match self { + Command::Encrypted(v) => return Ok(v.clone()), + Command::Proprietary((t, v)) => (*t, v.clone()), + }; + + let mut b = Vec::with_capacity(2 + v.len()); + b.push(t); + b.push(v.len() as u8); + b.extend_from_slice(&v); + + Ok(b) + } + + pub fn get_type(&self) -> u8 { + match self { + Command::Encrypted(_) => 0x00, // this should never be the case + Command::Proprietary((typ, _)) => *typ, + } + } +} + pub fn encode_freq(freq: u32) -> Result<[u8; 3]> { let mut freq = freq; // Support LoRaWAN 2.4GHz, in which case the stepping is 200Hz: @@ -564,6 +910,55 @@ pub fn decode_freq(b: &[u8]) -> Result { Ok(freq) } +pub fn encrypt_events_commands( + key: Aes128Key, + is_command: bool, + relay_id: &[u8], + timestamp: SystemTime, + payload: &[u8], +) -> Result> { + use aes::cipher::KeyInit; + + if payload.is_empty() { + return Ok(Vec::new()); + } + + let mut b = payload.to_vec(); + let b_len = b.len(); + + // Make buffer length multiple of 16. + b.append(&mut vec![0; 16 - (b_len % 16)]); + + // Encode timestamp. + let timestamp = timestamp.duration_since(UNIX_EPOCH)?.as_secs() as u32; + + let key_bytes = key.to_bytes(); + let key = GenericArray::from_slice(&key_bytes); + let cipher = Aes128::new(key); + + let mut a = vec![0; 16]; + a[0] = 0x01; + if is_command { + a[5] = 0x01; + } + a[6..10].clone_from_slice(relay_id); + a[10..14].clone_from_slice(×tamp.to_be_bytes()); + + // Encrypt blocks + for i in 0..(b.len() / 16) { + a[15] = (i + 1) as u8; + + let mut block = Block::clone_from_slice(&a); + cipher.encrypt_block(&mut block); + + for j in 0..16 { + b[(i * 16) + j] ^= block[j]; + } + } + + Ok(b[0..b_len].to_vec()) +} + #[cfg(test)] mod test { use super::*; @@ -1018,17 +1413,46 @@ mod test { } #[test] - fn test_heartbeat_payload_from_slice() { + fn test_event_heartbeat_payload_from_slice() { let b = vec![ - 59, 154, 202, 0, 1, 2, 3, 4, 5, 6, 7, 8, 120, 52, 9, 10, 11, 12, 120, 52, + 59, 154, 202, 0, 1, 2, 3, 4, 0, 12, 5, 6, 7, 8, 120, 52, 9, 10, 11, 12, 120, 52, ]; - let heartbeat_pl = HeartbeatPayload::from_slice(&b).unwrap(); + let mut event_pl = EventPayload::from_slice(&b).unwrap(); + event_pl.decode().unwrap(); + assert_eq!( - HeartbeatPayload { + EventPayload { timestamp: UNIX_EPOCH .checked_add(Duration::from_secs(1_000_000_000)) .unwrap(), relay_id: [1, 2, 3, 4], + events: vec![Event::Heartbeat(HeartbeatPayload { + relay_path: vec![ + RelayPath { + relay_id: [5, 6, 7, 8], + rssi: -120, + snr: -12, + }, + RelayPath { + relay_id: [9, 10, 11, 12], + rssi: -120, + snr: -12, + }, + ] + }),], + }, + event_pl, + ); + } + + #[test] + fn test_heartbeat_payload_to_vec() { + let event_pl = EventPayload { + timestamp: UNIX_EPOCH + .checked_add(Duration::from_secs(1_000_000_000)) + .unwrap(), + relay_id: [1, 2, 3, 4], + events: vec![Event::Heartbeat(HeartbeatPayload { relay_path: vec![ RelayPath { relay_id: [5, 6, 7, 8], @@ -1041,36 +1465,44 @@ mod test { snr: -12, }, ], + })], + }; + let b = event_pl.to_vec().unwrap(); + assert_eq!( + vec![59, 154, 202, 0, 1, 2, 3, 4, 0, 12, 5, 6, 7, 8, 120, 52, 9, 10, 11, 12, 120, 52], + b + ); + } + + #[test] + fn test_proprietary_command_from_slice() { + let b = vec![59, 154, 202, 0, 1, 2, 3, 4, 128, 4, 4, 3, 2, 1]; + let mut cmd_pl = CommandPayload::from_slice(&b).unwrap(); + cmd_pl.decode().unwrap(); + + assert_eq!( + CommandPayload { + timestamp: UNIX_EPOCH + .checked_add(Duration::from_secs(1_000_000_000)) + .unwrap(), + relay_id: [1, 2, 3, 4], + commands: vec![Command::Proprietary((128, vec![4, 3, 2, 1]))], }, - heartbeat_pl, + cmd_pl ); } #[test] - fn test_heartbeat_payload_to_vec() { - let heartbeat_pl = HeartbeatPayload { + fn test_proprietary_command_to_vec() { + let cmd_pl = CommandPayload { timestamp: UNIX_EPOCH .checked_add(Duration::from_secs(1_000_000_000)) .unwrap(), relay_id: [1, 2, 3, 4], - relay_path: vec![ - RelayPath { - relay_id: [5, 6, 7, 8], - rssi: -120, - snr: -12, - }, - RelayPath { - relay_id: [9, 10, 11, 12], - rssi: -120, - snr: -12, - }, - ], + commands: vec![Command::Proprietary((128, vec![4, 3, 2, 1]))], }; - let b = heartbeat_pl.to_vec().unwrap(); - assert_eq!( - vec![59, 154, 202, 0, 1, 2, 3, 4, 5, 6, 7, 8, 120, 52, 9, 10, 11, 12, 120, 52], - b - ); + let b = cmd_pl.to_vec().unwrap(); + assert_eq!(vec![59, 154, 202, 0, 1, 2, 3, 4, 128, 4, 4, 3, 2, 1], b); } #[test] diff --git a/src/proxy.rs b/src/proxy.rs index 62111d2..e79544e 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -14,9 +14,8 @@ use crate::mesh; static EVENT_CHAN: OnceLock = OnceLock::new(); -type Event = (String, Vec); -type Command = ((String, Vec), oneshot::Sender>); -type EventChannel = mpsc::UnboundedSender; +type EventChannel = mpsc::UnboundedSender; +type Command = (gw::Command, oneshot::Sender>); type CommandChannel = mpsc::UnboundedReceiver; pub async fn setup(conf: &Configuration) -> Result<()> { @@ -32,7 +31,7 @@ pub async fn setup(conf: &Configuration) -> Result<()> { // Setup ZMQ event. // As the zmq::Context can't be shared between threads, we use a channel. - let (event_tx, mut event_rx) = mpsc::unbounded_channel::(); + let (event_tx, mut event_rx) = mpsc::unbounded_channel::(); // Spawn the zmq event handler to a dedicated thread. thread::spawn({ @@ -44,20 +43,17 @@ pub async fn setup(conf: &Configuration) -> Result<()> { sock.bind(&event_bind).unwrap(); while let Some(event) = event_rx.blocking_recv() { - sock.send(&event.0, zmq::SNDMORE).unwrap(); - sock.send(&event.1, 0).unwrap(); + sock.send(event.encode_to_vec(), 0).unwrap(); } } }); // Set event channel. - EVENT_CHAN .set(event_tx) .map_err(|e| anyhow!("OnceLock error: {:?}", e))?; // Setup ZMQ command. - let (command_tx, command_rx) = mpsc::unbounded_channel::(); // Spawn the zmq command handler to a dedicated thread. @@ -73,7 +69,7 @@ pub async fn setup(conf: &Configuration) -> Result<()> { match receive_zmq_command(&mut sock) { Ok(v) => { let (resp_tx, resp_rx) = oneshot::channel::>(); - command_tx.send(((v.0, v.1), resp_tx)).unwrap(); + command_tx.send((v, resp_tx)).unwrap(); match resp_rx.blocking_recv() { Ok(v) => sock.send(&v, 0).unwrap(), @@ -102,38 +98,14 @@ pub async fn setup(conf: &Configuration) -> Result<()> { Ok(()) } -pub async fn send_uplink(pl: &gw::UplinkFrame) -> Result<()> { - info!("Sending uplink event - {}", helpers::format_uplink(pl)?); +pub async fn send_event(pl: gw::Event) -> Result<()> { + info!("Sending event"); let event_chan = EVENT_CHAN .get() .ok_or_else(|| anyhow!("EVENT_CHAN is not set"))?; - event_chan.send(("up".to_string(), pl.encode_to_vec()))?; - - Ok(()) -} - -pub async fn send_stats(pl: &gw::GatewayStats) -> Result<()> { - info!("Sending gateway stats event"); - - let event_chan = EVENT_CHAN - .get() - .ok_or_else(|| anyhow!("EVENT_CHAN is not set"))?; - - event_chan.send(("stats".to_string(), pl.encode_to_vec()))?; - - Ok(()) -} - -pub async fn send_mesh_heartbeat(pl: &gw::MeshHeartbeat) -> Result<()> { - info!("Sending mesh heartbeat event"); - - let event_chan = EVENT_CHAN - .get() - .ok_or_else(|| anyhow!("EVENT_CHAN is not set"))?; - - event_chan.send(("mesh_heartbeat".to_string(), pl.encode_to_vec()))?; + event_chan.send(pl)?; Ok(()) } @@ -142,7 +114,7 @@ async fn command_loop(mut command_rx: CommandChannel) { trace!("Starting command loop"); while let Some(cmd) = command_rx.recv().await { - match handle_command(&cmd).await { + match handle_command(cmd.0).await { Ok(v) => { _ = cmd.1.send(v); } @@ -156,40 +128,38 @@ async fn command_loop(mut command_rx: CommandChannel) { error!("Command loop has been interrupted"); } -async fn handle_command(cmd: &Command) -> Result> { - Ok(match cmd.0 .0.as_str() { - "config" => { - let pl = gw::GatewayConfiguration::decode(cmd.0 .1.as_slice())?; - info!("Configuration command received, version: {}", pl.version); - backend::send_gateway_configuration(&pl).await?; +async fn handle_command(cmd: gw::Command) -> Result> { + Ok(match cmd.command { + Some(gw::command::Command::SetGatewayConfiguration(v)) => { + info!("Configuration command received, version: {}", v.version); + backend::send_gateway_configuration(v).await?; Vec::new() } - "down" => { - let pl = gw::DownlinkFrame::decode(cmd.0 .1.as_slice())?; + Some(gw::command::Command::SendDownlinkFrame(v)) => { info!( "Downlink command received - {}", - helpers::format_downlink(&pl)? + helpers::format_downlink(&v)? ); - mesh::handle_downlink(pl).await.map(|v| v.encode_to_vec())? + mesh::handle_downlink(v).await.map(|v| v.encode_to_vec())? } - "gateway_id" => { + Some(gw::command::Command::GetGatewayId(_)) => { info!("Get gateway id command received"); - backend::get_gateway_id().await.map(|v| v.to_vec())? + gw::GetGatewayIdResponse { + gateway_id: hex::encode(backend::get_gateway_id().await.unwrap_or_default()), + } + .encode_to_vec() } - _ => { - return Err(anyhow!("Unexpected command: {}", cmd.0 .0)); + Some(gw::command::Command::Mesh(v)) => { + info!("Mesh command received"); + mesh::send_mesh_command(v).await?; + Vec::new() } + _ => return Err(anyhow!("Unexpected command: {:?}", cmd.command)), }) } -fn receive_zmq_command(sock: &mut zmq::Socket) -> Result<(String, Vec)> { - let msg = sock.recv_multipart(0).unwrap(); - if msg.len() != 2 { - return Err(anyhow!("Command must have 2 frames")); - } - - let cmd = String::from_utf8(msg[0].to_vec())?; - let b = msg[1].to_vec(); - - Ok((cmd, b)) +fn receive_zmq_command(sock: &mut zmq::Socket) -> Result { + let b = sock.recv_bytes(0)?; + let cmd = gw::Command::decode(b.as_slice())?; + Ok(cmd) } diff --git a/tests/border_gateway_downlink_lora.rs b/tests/border_gateway_downlink_lora.rs index 3d08a8f..6a55342 100644 --- a/tests/border_gateway_downlink_lora.rs +++ b/tests/border_gateway_downlink_lora.rs @@ -2,7 +2,7 @@ extern crate anyhow; use chirpstack_api::gw; -use chirpstack_api::prost::Message; +use chirpstack_api::{prost::Message, prost_types}; use zeromq::{SocketRecv, SocketSend}; mod common; @@ -52,14 +52,14 @@ async fn test_border_gateway_downlink_lora() { // Publish downlink command. { let mut cmd_sock = common::FORWARDER_COMMAND_SOCK.get().unwrap().lock().await; + let cmd = gw::Command { + command: Some(gw::command::Command::SendDownlinkFrame(down.clone())), + }; cmd_sock .send( - vec![ - bytes::Bytes::from("down"), - bytes::Bytes::from(down.encode_to_vec()), - ] - .try_into() - .unwrap(), + vec![bytes::Bytes::from(cmd.encode_to_vec())] + .try_into() + .unwrap(), ) .await .unwrap(); @@ -70,10 +70,12 @@ async fn test_border_gateway_downlink_lora() { let mut cmd_sock = common::BACKEND_COMMAND_SOCK.get().unwrap().lock().await; let msg = cmd_sock.recv().await.unwrap(); - let cmd = String::from_utf8(msg.get(0).map(|v| v.to_vec()).unwrap()).unwrap(); - assert_eq!("down", cmd); - - gw::DownlinkFrame::decode(msg.get(1).cloned().unwrap()).unwrap() + let cmd = gw::Command::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::command::Command::SendDownlinkFrame(v)) = cmd.command { + v + } else { + panic!("No DownlinkFrame"); + } }; assert_eq!(down, down_received); diff --git a/tests/border_gateway_downlink_mesh.rs b/tests/border_gateway_downlink_mesh.rs index 7c21d66..2e1308e 100644 --- a/tests/border_gateway_downlink_mesh.rs +++ b/tests/border_gateway_downlink_mesh.rs @@ -2,10 +2,10 @@ extern crate anyhow; use chirpstack_api::gw; -use chirpstack_api::prost::Message; +use chirpstack_api::{prost::Message, prost_types}; use zeromq::{SocketRecv, SocketSend}; -use chirpstack_gateway_mesh::aes128::Aes128Key; +use chirpstack_gateway_mesh::aes128::{get_signing_key, Aes128Key}; use chirpstack_gateway_mesh::packets; mod common; @@ -54,14 +54,14 @@ async fn test_border_gateway_downlink_mesh() { // Publish downlink command. { let mut cmd_sock = common::FORWARDER_COMMAND_SOCK.get().unwrap().lock().await; + let cmd = gw::Command { + command: Some(gw::command::Command::SendDownlinkFrame(down.clone())), + }; cmd_sock .send( - vec![ - bytes::Bytes::from("down"), - bytes::Bytes::from(down.encode_to_vec()), - ] - .try_into() - .unwrap(), + vec![bytes::Bytes::from(cmd.encode_to_vec())] + .try_into() + .unwrap(), ) .await .unwrap(); @@ -76,10 +76,12 @@ async fn test_border_gateway_downlink_mesh() { .await; let msg = cmd_sock.recv().await.unwrap(); - let cmd = String::from_utf8(msg.get(0).map(|v| v.to_vec()).unwrap()).unwrap(); - assert_eq!("down", cmd); - - gw::DownlinkFrame::decode(msg.get(1).cloned().unwrap()).unwrap() + let cmd = gw::Command::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::command::Command::SendDownlinkFrame(v)) = cmd.command { + v + } else { + panic!("No DownlinkFrame"); + } }; let down_item = down.items.first().unwrap(); @@ -105,7 +107,7 @@ async fn test_border_gateway_downlink_mesh() { }), mic: None, }; - packet.set_mic(Aes128Key::null()).unwrap(); + packet.set_mic(get_signing_key(Aes128Key::null())).unwrap(); packet }, mesh_packet diff --git a/tests/border_gateway_mesh_command_proprietary.rs b/tests/border_gateway_mesh_command_proprietary.rs new file mode 100644 index 0000000..fd23d25 --- /dev/null +++ b/tests/border_gateway_mesh_command_proprietary.rs @@ -0,0 +1,107 @@ +#[macro_use] +extern crate anyhow; + +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use chirpstack_api::gw; +use chirpstack_api::prost::Message; +use zeromq::{SocketRecv, SocketSend}; + +use chirpstack_gateway_mesh::aes128::{get_encryption_key, Aes128Key}; +use chirpstack_gateway_mesh::packets; + +mod common; + +/* + This tests the scenario when the Border Gateway receives a Mesh Command which + must be sent to a Relay Gateway. +*/ +#[tokio::test] +async fn border_gateway_mesh_command_proprietary() { + common::setup(true).await; + + let cmd = gw::MeshCommand { + gateway_id: "0101010101010101".into(), + relay_id: "02020202".into(), + commands: vec![gw::MeshCommandItem { + command: Some(gw::mesh_command_item::Command::Proprietary( + gw::MeshCommandProprietary { + command_type: 200, + payload: vec![4, 3, 2, 1], + }, + )), + }], + }; + + // Publish command. + { + let mut cmd_sock = common::FORWARDER_COMMAND_SOCK.get().unwrap().lock().await; + let cmd = gw::Command { + command: Some(gw::command::Command::Mesh(cmd.clone())), + }; + cmd_sock + .send( + vec![bytes::Bytes::from(cmd.encode_to_vec())] + .try_into() + .unwrap(), + ) + .await + .unwrap(); + } + + // We expect the wrapped downlink to be received by the mesh concentratord. + let down: gw::DownlinkFrame = { + let mut cmd_sock = common::MESH_BACKEND_COMMAND_SOCK + .get() + .unwrap() + .lock() + .await; + let msg = cmd_sock.recv().await.unwrap(); + + let cmd = gw::Command::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::command::Command::SendDownlinkFrame(v)) = cmd.command { + v + } else { + panic!("No DownlinkFrame"); + } + }; + + let down_item = down.items.first().unwrap(); + let mut mesh_packet = packets::MeshPacket::from_slice(&down_item.phy_payload).unwrap(); + + // MIC check. + assert_ne!([0, 0, 0, 0], mesh_packet.mic.unwrap()); + mesh_packet.mic = None; + + // Decrypt. + mesh_packet + .decrypt(get_encryption_key(Aes128Key::null())) + .unwrap(); + + if let packets::Payload::Command(v) = &mut mesh_packet.payload { + // Asser time is ~ now() + assert!( + SystemTime::now() + .duration_since(v.timestamp) + .unwrap_or_default() + < Duration::from_secs(5) + ); + v.timestamp = UNIX_EPOCH; + } + + assert_eq!( + packets::MeshPacket { + mhdr: packets::MHDR { + payload_type: packets::PayloadType::Command, + hop_count: 1 + }, + payload: packets::Payload::Command(packets::CommandPayload { + timestamp: UNIX_EPOCH, + relay_id: [2, 2, 2, 2], + commands: vec![packets::Command::Proprietary((200, vec![4, 3, 2, 1])),] + }), + mic: None, + }, + mesh_packet + ); +} diff --git a/tests/border_gateway_mesh_event_heartbeat.rs b/tests/border_gateway_mesh_event_heartbeat.rs new file mode 100644 index 0000000..699c690 --- /dev/null +++ b/tests/border_gateway_mesh_event_heartbeat.rs @@ -0,0 +1,128 @@ +use std::time::UNIX_EPOCH; + +#[macro_use] +extern crate anyhow; + +use chirpstack_api::gw; +use chirpstack_api::prost::Message; +use zeromq::{SocketRecv, SocketSend}; + +use chirpstack_gateway_mesh::aes128::{get_encryption_key, get_signing_key, Aes128Key}; +use chirpstack_gateway_mesh::packets; + +mod common; + +/* + Thsi tests the scenario that the Border Gateway receives a mesh heartbeat + packet. The Border Gateway will forward this to the Forwarder application. +*/ +#[tokio::test] +async fn test_border_gateway_mesh_heartbeat() { + common::setup(true).await; + + let mut packet = packets::MeshPacket { + mhdr: packets::MHDR { + payload_type: packets::PayloadType::Event, + hop_count: 1, + }, + payload: packets::Payload::Event(packets::EventPayload { + relay_id: [2, 2, 2, 2], + timestamp: UNIX_EPOCH, + events: vec![packets::Event::Heartbeat(packets::HeartbeatPayload { + relay_path: vec![ + packets::RelayPath { + relay_id: [1, 2, 3, 4], + rssi: -120, + snr: -12, + }, + packets::RelayPath { + relay_id: [5, 6, 7, 8], + rssi: -120, + snr: -12, + }, + ], + })], + }), + mic: None, + }; + packet + .encrypt(get_encryption_key(Aes128Key::null())) + .unwrap(); + packet.set_mic(get_signing_key(Aes128Key::null())).unwrap(); + + let up = gw::UplinkFrame { + phy_payload: packet.to_vec().unwrap(), + tx_info: Some(gw::UplinkTxInfo { + frequency: 868100000, + modulation: Some(gw::Modulation { + parameters: Some(gw::modulation::Parameters::Lora(gw::LoraModulationInfo { + bandwidth: 125000, + spreading_factor: 12, + code_rate: gw::CodeRate::Cr45.into(), + ..Default::default() + })), + }), + }), + rx_info: Some(gw::UplinkRxInfo { + crc_status: gw::CrcStatus::CrcOk.into(), + ..Default::default() + }), + ..Default::default() + }; + + // Publish uplink event. + { + let mut event_sock = common::MESH_BACKEND_EVENT_SOCK.get().unwrap().lock().await; + let event = gw::Event { + event: Some(gw::event::Event::UplinkFrame(up.clone())), + }; + event_sock + .send( + vec![bytes::Bytes::from(event.encode_to_vec())] + .try_into() + .unwrap(), + ) + .await + .unwrap(); + } + + // We expect to receive a Mesh Event. + let mesh_event: gw::MeshEvent = { + let mut event_sock = common::FORWARDER_EVENT_SOCK.get().unwrap().lock().await; + let msg = event_sock.recv().await.unwrap(); + let event = gw::Event::decode(msg.get(0).cloned().unwrap()).unwrap(); + + if let Some(gw::event::Event::Mesh(v)) = event.event { + v + } else { + panic!("Event does not contain MeshEvent"); + } + }; + + assert_eq!( + gw::MeshEvent { + gateway_id: "0101010101010101".to_string(), + time: Some(UNIX_EPOCH.into()), + relay_id: "02020202".to_string(), + events: vec![gw::MeshEventItem { + event: Some(gw::mesh_event_item::Event::Heartbeat( + gw::MeshEventHeartbeat { + relay_path: vec![ + gw::MeshEventHeartbeatRelayPath { + relay_id: "01020304".into(), + rssi: -120, + snr: -12, + }, + gw::MeshEventHeartbeatRelayPath { + relay_id: "05060708".into(), + rssi: -120, + snr: -12, + }, + ], + }, + )), + },], + }, + mesh_event + ); +} diff --git a/tests/border_gateway_mesh_heartbeat.rs b/tests/border_gateway_mesh_heartbeat.rs deleted file mode 100644 index 58daa2e..0000000 --- a/tests/border_gateway_mesh_heartbeat.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::time::UNIX_EPOCH; - -#[macro_use] -extern crate anyhow; - -use chirpstack_api::gw; -use chirpstack_api::prost::Message; -use zeromq::{SocketRecv, SocketSend}; - -use chirpstack_gateway_mesh::aes128::Aes128Key; -use chirpstack_gateway_mesh::packets; - -mod common; - -/* - Thsi tests the scenario that the Border Gateway receives a mesh heartbeat - packet. The Border Gateway will forward this to the Forwarder application. -*/ -#[tokio::test] -async fn test_border_gateway_mesh_heartbeat() { - common::setup(true).await; - - let mut packet = packets::MeshPacket { - mhdr: packets::MHDR { - payload_type: packets::PayloadType::Heartbeat, - hop_count: 1, - }, - payload: packets::Payload::Heartbeat(packets::HeartbeatPayload { - relay_id: [2, 2, 2, 2], - timestamp: UNIX_EPOCH, - relay_path: vec![ - packets::RelayPath { - relay_id: [1, 2, 3, 4], - rssi: -120, - snr: -12, - }, - packets::RelayPath { - relay_id: [5, 6, 7, 8], - rssi: -120, - snr: -12, - }, - ], - }), - mic: None, - }; - packet.set_mic(Aes128Key::null()).unwrap(); - - let up = gw::UplinkFrame { - phy_payload: packet.to_vec().unwrap(), - tx_info: Some(gw::UplinkTxInfo { - frequency: 868100000, - modulation: Some(gw::Modulation { - parameters: Some(gw::modulation::Parameters::Lora(gw::LoraModulationInfo { - bandwidth: 125000, - spreading_factor: 12, - code_rate: gw::CodeRate::Cr45.into(), - ..Default::default() - })), - }), - }), - rx_info: Some(gw::UplinkRxInfo { - crc_status: gw::CrcStatus::CrcOk.into(), - ..Default::default() - }), - ..Default::default() - }; - - // Publish uplink event. - { - let mut event_sock = common::MESH_BACKEND_EVENT_SOCK.get().unwrap().lock().await; - event_sock - .send( - vec![ - bytes::Bytes::from("up"), - bytes::Bytes::from(up.encode_to_vec()), - ] - .try_into() - .unwrap(), - ) - .await - .unwrap(); - } - - // We expect to receive the MeshHeartbeat to be received by the forwarder. - let mesh_heartbeat: gw::MeshHeartbeat = { - let mut event_sock = common::FORWARDER_EVENT_SOCK.get().unwrap().lock().await; - let msg = event_sock.recv().await.unwrap(); - - let cmd = String::from_utf8(msg.get(0).map(|v| v.to_vec()).unwrap()).unwrap(); - assert_eq!("mesh_heartbeat", cmd); - - gw::MeshHeartbeat::decode(msg.get(1).cloned().unwrap()).unwrap() - }; - - assert_eq!( - gw::MeshHeartbeat { - gateway_id: "0101010101010101".to_string(), - time: Some(UNIX_EPOCH.into()), - relay_id: "02020202".to_string(), - relay_path: vec![ - gw::MeshHeartbeatRelayPath { - relay_id: "01020304".into(), - rssi: -120, - snr: -12, - }, - gw::MeshHeartbeatRelayPath { - relay_id: "05060708".into(), - rssi: -120, - snr: -12, - }, - ], - }, - mesh_heartbeat - ); -} diff --git a/tests/border_gateway_uplink_lora.rs b/tests/border_gateway_uplink_lora.rs index 113179b..0f95a56 100644 --- a/tests/border_gateway_uplink_lora.rs +++ b/tests/border_gateway_uplink_lora.rs @@ -39,29 +39,30 @@ async fn test_border_gateway_uplink_lora() { // Publish uplink event. { let mut event_sock = common::BACKEND_EVENT_SOCK.get().unwrap().lock().await; + let event = gw::Event { + event: Some(gw::event::Event::UplinkFrame(up.clone())), + }; event_sock .send( - vec![ - bytes::Bytes::from("up"), - bytes::Bytes::from(up.encode_to_vec()), - ] - .try_into() - .unwrap(), + vec![bytes::Bytes::from(event.encode_to_vec())] + .try_into() + .unwrap(), ) .await .unwrap(); } // We expect to receive the same uplink. - let up_received = { + let up_received: gw::UplinkFrame = { let mut event_sock = common::FORWARDER_EVENT_SOCK.get().unwrap().lock().await; - let msg = event_sock.recv().await.unwrap(); - let cmd = String::from_utf8(msg.get(0).map(|v| v.to_vec()).unwrap()).unwrap(); - assert_eq!("up", cmd); - - gw::UplinkFrame::decode(msg.get(1).cloned().unwrap()).unwrap() + let event = gw::Event::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::event::Event::UplinkFrame(v)) = event.event { + v + } else { + panic!("No UplinkFrame"); + } }; // Validate they are equal. diff --git a/tests/border_gateway_uplink_mesh.rs b/tests/border_gateway_uplink_mesh.rs index 9a0c025..1b91395 100644 --- a/tests/border_gateway_uplink_mesh.rs +++ b/tests/border_gateway_uplink_mesh.rs @@ -5,7 +5,7 @@ use chirpstack_api::gw; use chirpstack_api::prost::Message; use zeromq::{SocketRecv, SocketSend}; -use chirpstack_gateway_mesh::aes128::Aes128Key; +use chirpstack_gateway_mesh::aes128::{get_signing_key, Aes128Key}; use chirpstack_gateway_mesh::packets; mod common; @@ -37,7 +37,7 @@ async fn test_border_gateway_uplink_mesh() { }), mic: None, }; - packet.set_mic(Aes128Key::null()).unwrap(); + packet.set_mic(get_signing_key(Aes128Key::null())).unwrap(); let up = gw::UplinkFrame { phy_payload: packet.to_vec().unwrap(), @@ -62,14 +62,14 @@ async fn test_border_gateway_uplink_mesh() { // Publish uplink event. { let mut event_sock = common::MESH_BACKEND_EVENT_SOCK.get().unwrap().lock().await; + let event = gw::Event { + event: Some(gw::event::Event::UplinkFrame(up.clone())), + }; event_sock .send( - vec![ - bytes::Bytes::from("up"), - bytes::Bytes::from(up.encode_to_vec()), - ] - .try_into() - .unwrap(), + vec![bytes::Bytes::from(event.encode_to_vec())] + .try_into() + .unwrap(), ) .await .unwrap(); @@ -80,10 +80,12 @@ async fn test_border_gateway_uplink_mesh() { let mut event_sock = common::FORWARDER_EVENT_SOCK.get().unwrap().lock().await; let msg = event_sock.recv().await.unwrap(); - let cmd = String::from_utf8(msg.get(0).map(|v| v.to_vec()).unwrap()).unwrap(); - assert_eq!("up", cmd); - - gw::UplinkFrame::decode(msg.get(1).cloned().unwrap()).unwrap() + let event = gw::Event::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::event::Event::UplinkFrame(v)) = event.event { + v + } else { + panic!("No UplinkFrame"); + } }; // Validate PHYPayload diff --git a/tests/common/mod.rs b/tests/common/mod.rs index abab5f7..b66cf61 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -6,6 +6,7 @@ use tokio::sync::Mutex; use tokio::time::sleep; use zeromq::{Socket, SocketRecv, SocketSend}; +use chirpstack_api::{gw, prost::Message}; use chirpstack_gateway_mesh::config::{self, Configuration}; pub static FORWARDER_EVENT_SOCK: OnceLock> = OnceLock::new(); @@ -29,7 +30,6 @@ pub fn get_config(border_gateway: bool) -> Configuration { Configuration { mesh: config::Mesh { border_gateway, - heartbeat_interval: Duration::ZERO, frequencies: vec![868100000], data_rate: config::DataRate { modulation: config::Modulation::LORA, @@ -67,6 +67,23 @@ pub fn get_config(border_gateway: bool) -> Configuration { }], tx_power: vec![27, 16], }, + events: config::Events { + heartbeat_interval: Duration::ZERO, + commands: [ + ("128".into(), vec!["echo".into(), "foo".into()]), + ("129".into(), vec!["echo".into(), "bar".into()]), + ] + .iter() + .cloned() + .collect(), + ..Default::default() + }, + commands: config::Commands { + commands: [("130".into(), vec!["wc".into(), "-m".into()])] + .iter() + .cloned() + .collect(), + }, ..Default::default() } } @@ -177,7 +194,13 @@ async fn init_mesh() { let mut cmd_sock = BACKEND_COMMAND_SOCK.get().unwrap().lock().await; let _ = cmd_sock.recv().await; cmd_sock - .send(vec![1, 1, 1, 1, 1, 1, 1, 1].into()) + .send( + gw::GetGatewayIdResponse { + gateway_id: "0101010101010101".into(), + } + .encode_to_vec() + .into(), + ) .await .unwrap(); }); @@ -186,7 +209,13 @@ async fn init_mesh() { let mut cmd_sock = MESH_BACKEND_COMMAND_SOCK.get().unwrap().lock().await; let _ = cmd_sock.recv().await; cmd_sock - .send(vec![2, 2, 2, 2, 2, 2, 2, 2].into()) + .send( + gw::GetGatewayIdResponse { + gateway_id: "0202020202020202".into(), + } + .encode_to_vec() + .into(), + ) .await .unwrap(); }); diff --git a/tests/relay_gateway_downlink_lora.rs b/tests/relay_gateway_downlink_lora.rs index a8b0f5d..87d8e59 100644 --- a/tests/relay_gateway_downlink_lora.rs +++ b/tests/relay_gateway_downlink_lora.rs @@ -2,10 +2,10 @@ extern crate anyhow; use chirpstack_api::gw; -use chirpstack_api::prost::Message; +use chirpstack_api::{prost::Message, prost_types}; use zeromq::{SocketRecv, SocketSend}; -use chirpstack_gateway_mesh::aes128::Aes128Key; +use chirpstack_gateway_mesh::aes128::{get_signing_key, Aes128Key}; use chirpstack_gateway_mesh::{mesh, packets}; mod common; @@ -39,7 +39,9 @@ async fn test_relay_gateway_downlink_lora() { }), mic: None, }; - down_packet.set_mic(Aes128Key::null()).unwrap(); + down_packet + .set_mic(get_signing_key(Aes128Key::null())) + .unwrap(); // The packet that we received from the Border Gateway that must be relayed to // the End Device. @@ -68,28 +70,30 @@ async fn test_relay_gateway_downlink_lora() { // (we simulate that we receive the Mesh encapsulated downlink) { let mut event_sock = common::MESH_BACKEND_EVENT_SOCK.get().unwrap().lock().await; + let event = gw::Event { + event: Some(gw::event::Event::UplinkFrame(up.clone())), + }; event_sock .send( - vec![ - bytes::Bytes::from("up"), - bytes::Bytes::from(up.encode_to_vec()), - ] - .try_into() - .unwrap(), + vec![bytes::Bytes::from(event.encode_to_vec())] + .try_into() + .unwrap(), ) .await .unwrap(); } // We expect that the unwrapped downlink was sent to the concentratord. - let mut down = { + let mut down: gw::DownlinkFrame = { let mut cmd_sock = common::BACKEND_COMMAND_SOCK.get().unwrap().lock().await; let msg = cmd_sock.recv().await.unwrap(); - let cmd = String::from_utf8(msg.get(0).map(|v| v.to_vec()).unwrap()).unwrap(); - assert_eq!("down", cmd); - - gw::DownlinkFrame::decode(msg.get(1).cloned().unwrap()).unwrap() + let cmd = gw::Command::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::command::Command::SendDownlinkFrame(v)) = cmd.command { + v + } else { + panic!("No DownlinkFrame"); + } }; assert_ne!(0, down.downlink_id); diff --git a/tests/relay_gateway_mesh_command_proprietary.rs b/tests/relay_gateway_mesh_command_proprietary.rs new file mode 100644 index 0000000..9663ad5 --- /dev/null +++ b/tests/relay_gateway_mesh_command_proprietary.rs @@ -0,0 +1,144 @@ +#[macro_use] +extern crate anyhow; + +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use chirpstack_api::gw; +use chirpstack_api::prost::Message; +use zeromq::{SocketRecv, SocketSend}; + +use chirpstack_gateway_mesh::aes128::{get_encryption_key, get_signing_key, Aes128Key}; +use chirpstack_gateway_mesh::packets; + +mod common; + +/* + This test the scenario when the Relay Gateway receives a Mesh Command. THe + Relay Gateway will execute this command and then sends back the response as + a Mesh Event. +*/ +#[tokio::test] +async fn test_relay_gateway_mesh_command_proprietary() { + common::setup(false).await; + + let mut cmd_packet = packets::MeshPacket { + mhdr: packets::MHDR { + payload_type: packets::PayloadType::Command, + hop_count: 1, + }, + payload: packets::Payload::Command(packets::CommandPayload { + timestamp: SystemTime::now(), + relay_id: [2, 2, 2, 2], + commands: vec![packets::Command::Proprietary(( + 130, + "hello".as_bytes().to_vec(), + ))], + }), + mic: None, + }; + cmd_packet + .encrypt(get_encryption_key(Aes128Key::null())) + .unwrap(); + cmd_packet + .set_mic(get_signing_key(Aes128Key::null())) + .unwrap(); + + // The packet that we received from the Border Gateway. + let up = gw::UplinkFrame { + phy_payload: cmd_packet.to_vec().unwrap(), + tx_info: Some(gw::UplinkTxInfo { + frequency: 868100000, + modulation: Some(gw::Modulation { + parameters: Some(gw::modulation::Parameters::Lora(gw::LoraModulationInfo { + bandwidth: 125000, + spreading_factor: 12, + code_rate: gw::CodeRate::Cr45.into(), + ..Default::default() + })), + }), + }), + rx_info: Some(gw::UplinkRxInfo { + gateway_id: "0101010101010101".into(), + crc_status: gw::CrcStatus::CrcOk.into(), + ..Default::default() + }), + ..Default::default() + }; + + // Publish uplink. + // (we simulate that we receive the Mesh command) + { + let mut event_sock = common::MESH_BACKEND_EVENT_SOCK.get().unwrap().lock().await; + let event = gw::Event { + event: Some(gw::event::Event::UplinkFrame(up.clone())), + }; + event_sock + .send( + vec![bytes::Bytes::from(event.encode_to_vec())] + .try_into() + .unwrap(), + ) + .await + .unwrap(); + } + + // We expect that the Relay Gateway responds to the Mesh Command by sending + // a Mesh Event back. + let mut down: gw::DownlinkFrame = { + let mut cmd_sock = common::MESH_BACKEND_COMMAND_SOCK + .get() + .unwrap() + .lock() + .await; + let msg = cmd_sock.recv().await.unwrap(); + + let cmd = gw::Command::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::command::Command::SendDownlinkFrame(v)) = cmd.command { + v + } else { + panic!("No DownlinkFrame"); + } + }; + + assert_ne!(0, down.downlink_id); + down.downlink_id = 0; + + let down_item = down.items.first().unwrap(); + let mut mesh_packet = packets::MeshPacket::from_slice(&down_item.phy_payload).unwrap(); + + // Decrypt. + mesh_packet + .decrypt(get_encryption_key(Aes128Key::null())) + .unwrap(); + + // MIC. + assert_ne!([0, 0, 0, 0], mesh_packet.mic.unwrap()); + mesh_packet.mic = None; + + if let packets::Payload::Event(v) = &mut mesh_packet.payload { + // Assert the time is ~ now() + assert!( + SystemTime::now() + .duration_since(v.timestamp) + .unwrap_or_default() + < Duration::from_secs(5) + ); + v.timestamp = UNIX_EPOCH; + } + + assert_eq!( + packets::MeshPacket { + mhdr: packets::MHDR { + payload_type: packets::PayloadType::Event, + hop_count: 1, + }, + payload: packets::Payload::Event(packets::EventPayload { + relay_id: [2, 2, 2, 2], + timestamp: UNIX_EPOCH, + events: vec![packets::Event::Proprietary((130, vec![53, 10])),], // 53 = 5 in ascii + }), + mic: None, + }, + mesh_packet + ); +} diff --git a/tests/relay_gateway_mesh_heartbeat.rs b/tests/relay_gateway_mesh_event_heartbeat.rs similarity index 62% rename from tests/relay_gateway_mesh_heartbeat.rs rename to tests/relay_gateway_mesh_event_heartbeat.rs index 8997982..a5760d1 100644 --- a/tests/relay_gateway_mesh_heartbeat.rs +++ b/tests/relay_gateway_mesh_event_heartbeat.rs @@ -8,7 +8,8 @@ use chirpstack_api::prost::Message; use chirpstack_gateway_mesh::packets; use zeromq::SocketRecv; -use chirpstack_gateway_mesh::heartbeat; +use chirpstack_gateway_mesh::aes128::{get_encryption_key, Aes128Key}; +use chirpstack_gateway_mesh::events; mod common; @@ -16,9 +17,9 @@ mod common; This tests the scenario when the Relay Gateway sends its periodic heartbeat. */ #[tokio::test] -async fn test_relay_gateway_mesh_heartbeat() { +async fn test_relay_gateway_mesh_event_heartbeat() { common::setup(false).await; - let _ = heartbeat::report_heartbeat().await; + let _ = events::report_heartbeat().await; // We expect the heartbeat to be received by the mesh concentratord as // a downlink frame. @@ -30,18 +31,25 @@ async fn test_relay_gateway_mesh_heartbeat() { .await; let msg = cmd_sock.recv().await.unwrap(); - let cmd = String::from_utf8(msg.get(0).map(|v| v.to_vec()).unwrap()).unwrap(); - assert_eq!("down", cmd); - - gw::DownlinkFrame::decode(msg.get(1).cloned().unwrap()).unwrap() + let cmd = gw::Command::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::command::Command::SendDownlinkFrame(v)) = cmd.command { + v + } else { + panic!("No DownlinkFrame"); + } }; let down_item = down.items.first().unwrap(); let mut mesh_packet = packets::MeshPacket::from_slice(&down_item.phy_payload).unwrap(); + + mesh_packet + .decrypt(get_encryption_key(Aes128Key::null())) + .unwrap(); + assert_ne!([0, 0, 0, 0], mesh_packet.mic.unwrap()); mesh_packet.mic = None; - if let packets::Payload::Heartbeat(v) = &mut mesh_packet.payload { + if let packets::Payload::Event(v) = &mut mesh_packet.payload { // Assert the time is ~ now() assert!( SystemTime::now() @@ -55,13 +63,15 @@ async fn test_relay_gateway_mesh_heartbeat() { assert_eq!( packets::MeshPacket { mhdr: packets::MHDR { - payload_type: packets::PayloadType::Heartbeat, + payload_type: packets::PayloadType::Event, hop_count: 1, }, - payload: packets::Payload::Heartbeat(packets::HeartbeatPayload { + payload: packets::Payload::Event(packets::EventPayload { relay_id: [2, 2, 2, 2], timestamp: UNIX_EPOCH, - relay_path: vec![], + events: vec![packets::Event::Heartbeat(packets::HeartbeatPayload { + relay_path: vec![] + }),], }), mic: None, }, diff --git a/tests/relay_gateway_mesh_event_proprietary.rs b/tests/relay_gateway_mesh_event_proprietary.rs new file mode 100644 index 0000000..e07a9ea --- /dev/null +++ b/tests/relay_gateway_mesh_event_proprietary.rs @@ -0,0 +1,82 @@ +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +#[macro_use] +extern crate anyhow; + +use chirpstack_api::gw; +use chirpstack_api::prost::Message; +use chirpstack_gateway_mesh::packets; +use zeromq::SocketRecv; + +use chirpstack_gateway_mesh::aes128::{get_encryption_key, Aes128Key}; +use chirpstack_gateway_mesh::events; + +mod common; + +/* + This tests the scenario when the Relay Gateway sends proprietary events. +*/ +#[tokio::test] +async fn test_relay_gateway_mesh_event_proprietary() { + common::setup(false).await; + let _ = events::report_events(&[128, 129]).await; + + // We expect the proprietary events to be received by the mesh concentratord + // as a downlink frame. + + let down: gw::DownlinkFrame = { + let mut cmd_sock = common::MESH_BACKEND_COMMAND_SOCK + .get() + .unwrap() + .lock() + .await; + let msg = cmd_sock.recv().await.unwrap(); + + let cmd = gw::Command::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::command::Command::SendDownlinkFrame(v)) = cmd.command { + v + } else { + panic!("No DownlinkFrame"); + } + }; + + let down_item = down.items.first().unwrap(); + let mut mesh_packet = packets::MeshPacket::from_slice(&down_item.phy_payload).unwrap(); + + mesh_packet + .decrypt(get_encryption_key(Aes128Key::null())) + .unwrap(); + + assert_ne!([0, 0, 0, 0], mesh_packet.mic.unwrap()); + mesh_packet.mic = None; + + if let packets::Payload::Event(v) = &mut mesh_packet.payload { + // Assert the time is ~ now() + assert!( + SystemTime::now() + .duration_since(v.timestamp) + .unwrap_or_default() + < Duration::from_secs(5) + ); + v.timestamp = UNIX_EPOCH; + } + + assert_eq!( + packets::MeshPacket { + mhdr: packets::MHDR { + payload_type: packets::PayloadType::Event, + hop_count: 1, + }, + payload: packets::Payload::Event(packets::EventPayload { + relay_id: [2, 2, 2, 2], + timestamp: UNIX_EPOCH, + events: vec![ + packets::Event::Proprietary((128, vec![102, 111, 111, 10])), + packets::Event::Proprietary((129, vec![98, 97, 114, 10])), + ], + }), + mic: None, + }, + mesh_packet + ); +} diff --git a/tests/relay_gateway_relay_mesh_heartbeat.rs b/tests/relay_gateway_relay_mesh_event.rs similarity index 57% rename from tests/relay_gateway_relay_mesh_heartbeat.rs rename to tests/relay_gateway_relay_mesh_event.rs index 8fe9cfe..0b15d82 100644 --- a/tests/relay_gateway_relay_mesh_heartbeat.rs +++ b/tests/relay_gateway_relay_mesh_event.rs @@ -9,7 +9,7 @@ use chirpstack_gateway_mesh::packets; use tokio::time::{timeout, Duration}; use zeromq::{SocketRecv, SocketSend}; -use chirpstack_gateway_mesh::aes128::Aes128Key; +use chirpstack_gateway_mesh::aes128::{get_encryption_key, get_signing_key, Aes128Key}; mod common; @@ -23,17 +23,22 @@ async fn test_relay_gateway_relay_mesh_heartbeat() { let mut packet = packets::MeshPacket { mhdr: packets::MHDR { - payload_type: packets::PayloadType::Heartbeat, + payload_type: packets::PayloadType::Event, hop_count: 1, }, - payload: packets::Payload::Heartbeat(packets::HeartbeatPayload { + payload: packets::Payload::Event(packets::EventPayload { relay_id: [1, 2, 3, 4], timestamp: UNIX_EPOCH, - relay_path: vec![], + events: vec![packets::Event::Heartbeat(packets::HeartbeatPayload { + relay_path: vec![], + })], }), mic: None, }; - packet.set_mic(Aes128Key::null()).unwrap(); + packet + .encrypt(get_encryption_key(Aes128Key::null())) + .unwrap(); + packet.set_mic(get_signing_key(Aes128Key::null())).unwrap(); let up = gw::UplinkFrame { phy_payload: packet.to_vec().unwrap(), @@ -61,14 +66,14 @@ async fn test_relay_gateway_relay_mesh_heartbeat() { // Publish Uplink { let mut event_sock = common::MESH_BACKEND_EVENT_SOCK.get().unwrap().lock().await; + let event = gw::Event { + event: Some(gw::event::Event::UplinkFrame(up.clone())), + }; event_sock .send( - vec![ - bytes::Bytes::from("up"), - bytes::Bytes::from(up.encode_to_vec()), - ] - .try_into() - .unwrap(), + vec![bytes::Bytes::from(event.encode_to_vec())] + .try_into() + .unwrap(), ) .await .unwrap(); @@ -84,38 +89,55 @@ async fn test_relay_gateway_relay_mesh_heartbeat() { .await; let msg = cmd_sock.recv().await.unwrap(); - let cmd = String::from_utf8(msg.get(0).map(|v| v.to_vec()).unwrap()).unwrap(); - assert_eq!("down", cmd); - - gw::DownlinkFrame::decode(msg.get(1).cloned().unwrap()).unwrap() + let cmd = gw::Command::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::command::Command::SendDownlinkFrame(v)) = cmd.command { + v + } else { + panic!("No DownlinkFrame"); + } }; let down_item = down.items.first().unwrap(); - let mesh_packet = packets::Packet::from_slice(&down_item.phy_payload).unwrap(); + let mut mesh_packet = packets::Packet::from_slice(&down_item.phy_payload).unwrap(); + if let packets::Packet::Mesh(pl) = &mut mesh_packet { + pl.decrypt(get_encryption_key(Aes128Key::null())).unwrap(); + } + packet + .decrypt(get_encryption_key(Aes128Key::null())) + .unwrap(); packet.mhdr.hop_count += 1; - if let packets::Payload::Heartbeat(v) = &mut packet.payload { - v.relay_path.push(packets::RelayPath { - relay_id: [2, 2, 2, 2], - rssi: -60, - snr: 12, - }); + if let packets::Payload::Event(v) = &mut packet.payload { + for event in &mut v.events { + if let packets::Event::Heartbeat(v) = event { + v.relay_path.push(packets::RelayPath { + relay_id: [2, 2, 2, 2], + rssi: -60, + snr: 12, + }); + } + } } - packet.set_mic(Aes128Key::null()).unwrap(); - + packet + .encrypt(get_encryption_key(Aes128Key::null())) + .unwrap(); + packet.set_mic(get_signing_key(Aes128Key::null())).unwrap(); + packet + .decrypt(get_encryption_key(Aes128Key::null())) + .unwrap(); assert_eq!(packets::Packet::Mesh(packet), mesh_packet); // Publish the uplink one more time, this time we expect that it will be discarded. { let mut event_sock = common::MESH_BACKEND_EVENT_SOCK.get().unwrap().lock().await; + let event = gw::Event { + event: Some(gw::event::Event::UplinkFrame(up.clone())), + }; event_sock .send( - vec![ - bytes::Bytes::from("up"), - bytes::Bytes::from(up.encode_to_vec()), - ] - .try_into() - .unwrap(), + vec![bytes::Bytes::from(event.encode_to_vec())] + .try_into() + .unwrap(), ) .await .unwrap(); diff --git a/tests/relay_gateway_uplink_lora.rs b/tests/relay_gateway_uplink_lora.rs index ff3491a..fb98bb1 100644 --- a/tests/relay_gateway_uplink_lora.rs +++ b/tests/relay_gateway_uplink_lora.rs @@ -6,7 +6,7 @@ use chirpstack_api::prost::Message; use chirpstack_gateway_mesh::packets; use zeromq::{SocketRecv, SocketSend}; -use chirpstack_gateway_mesh::aes128::Aes128Key; +use chirpstack_gateway_mesh::aes128::{get_signing_key, Aes128Key}; mod common; @@ -45,14 +45,14 @@ async fn test_relay_gateway_uplink_lora() { // Publish uplink event. { let mut event_sock = common::BACKEND_EVENT_SOCK.get().unwrap().lock().await; + let event = gw::Event { + event: Some(gw::event::Event::UplinkFrame(up.clone())), + }; event_sock .send( - vec![ - bytes::Bytes::from("up"), - bytes::Bytes::from(up.encode_to_vec()), - ] - .try_into() - .unwrap(), + vec![bytes::Bytes::from(event.encode_to_vec())] + .try_into() + .unwrap(), ) .await .unwrap(); @@ -68,10 +68,12 @@ async fn test_relay_gateway_uplink_lora() { .await; let msg = cmd_sock.recv().await.unwrap(); - let cmd = String::from_utf8(msg.get(0).map(|v| v.to_vec()).unwrap()).unwrap(); - assert_eq!("down", cmd); - - gw::DownlinkFrame::decode(msg.get(1).cloned().unwrap()).unwrap() + let cmd = gw::Command::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::command::Command::SendDownlinkFrame(v)) = cmd.command { + v + } else { + panic!("No DownlinkFrame"); + } }; let down_item = down.items.first().unwrap(); @@ -97,7 +99,7 @@ async fn test_relay_gateway_uplink_lora() { }), mic: None, }; - packet.set_mic(Aes128Key::null()).unwrap(); + packet.set_mic(get_signing_key(Aes128Key::null())).unwrap(); packet }, mesh_packet diff --git a/tests/relay_gateway_uplink_mesh.rs b/tests/relay_gateway_uplink_mesh.rs index 7b5e1d6..d987c99 100644 --- a/tests/relay_gateway_uplink_mesh.rs +++ b/tests/relay_gateway_uplink_mesh.rs @@ -7,7 +7,7 @@ use chirpstack_gateway_mesh::packets; use tokio::time::{timeout, Duration}; use zeromq::{SocketRecv, SocketSend}; -use chirpstack_gateway_mesh::aes128::Aes128Key; +use chirpstack_gateway_mesh::aes128::{get_signing_key, Aes128Key}; mod common; @@ -38,7 +38,7 @@ async fn test_relay_gateway_uplink_mesh() { }), mic: None, }; - packet.set_mic(Aes128Key::null()).unwrap(); + packet.set_mic(get_signing_key(Aes128Key::null())).unwrap(); packet }); @@ -68,14 +68,14 @@ async fn test_relay_gateway_uplink_mesh() { // Publish uplink event { let mut event_sock = common::MESH_BACKEND_EVENT_SOCK.get().unwrap().lock().await; + let event = gw::Event { + event: Some(gw::event::Event::UplinkFrame(up.clone())), + }; event_sock .send( - vec![ - bytes::Bytes::from("up"), - bytes::Bytes::from(up.encode_to_vec()), - ] - .try_into() - .unwrap(), + vec![bytes::Bytes::from(event.encode_to_vec())] + .try_into() + .unwrap(), ) .await .unwrap(); @@ -91,10 +91,12 @@ async fn test_relay_gateway_uplink_mesh() { .await; let msg = cmd_sock.recv().await.unwrap(); - let cmd = String::from_utf8(msg.get(0).map(|v| v.to_vec()).unwrap()).unwrap(); - assert_eq!("down", cmd); - - gw::DownlinkFrame::decode(msg.get(1).cloned().unwrap()).unwrap() + let cmd = gw::Command::decode(msg.get(0).cloned().unwrap()).unwrap(); + if let Some(gw::command::Command::SendDownlinkFrame(v)) = cmd.command { + v + } else { + panic!("No DownlinkFrame"); + } }; let down_item = down.items.first().unwrap(); @@ -103,7 +105,7 @@ async fn test_relay_gateway_uplink_mesh() { // The hop_count must be incremented. if let packets::Packet::Mesh(v) = &mut packet { v.mhdr.hop_count += 1; - v.set_mic(Aes128Key::null()).unwrap(); + v.set_mic(get_signing_key(Aes128Key::null())).unwrap(); } assert_eq!(packet, mesh_packet); @@ -111,14 +113,14 @@ async fn test_relay_gateway_uplink_mesh() { // Publish the uplink one more time, this time we expect that it will be discarded. { let mut event_sock = common::MESH_BACKEND_EVENT_SOCK.get().unwrap().lock().await; + let event = gw::Event { + event: Some(gw::event::Event::UplinkFrame(up.clone())), + }; event_sock .send( - vec![ - bytes::Bytes::from("up"), - bytes::Bytes::from(up.encode_to_vec()), - ] - .try_into() - .unwrap(), + vec![bytes::Bytes::from(event.encode_to_vec())] + .try_into() + .unwrap(), ) .await .unwrap();