From 3a1f29fa4172621131661cf333feb730923bd076 Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Thu, 11 Jan 2024 13:56:14 +0000 Subject: [PATCH 1/3] WIP. --- .vscode/settings.json | 4 + Cargo.lock | 1390 +++++++++++++++++ Cargo.toml | 31 + Cross.toml | 17 + LICENSE | 21 + cross/Dockerfile.aarch64-unknown-linux-musl | 6 + .../Dockerfile.armv5te-unknown-linux-musleabi | 6 + .../Dockerfile.armv7-unknown-linux-musleabihf | 6 + cross/Dockerfile.mips-unknown-linux-gnu | 6 + cross/Dockerfile.mips-unknown-linux-musl | 6 + cross/Dockerfile.mipsel-unknown-linux-musl | 6 + cross/Dockerfile.x86_64-unknown-linux-musl | 6 + rust-toolchain.toml | 4 + shell.nix | 23 + src/backend.rs | 178 +++ src/cmd/border_gateway.rs | 10 + src/cmd/configfile.rs | 92 ++ src/cmd/mod.rs | 3 + src/cmd/root.rs | 10 + src/config.rs | 112 ++ src/logging.rs | 22 + src/main.rs | 76 + src/packets.rs | 982 ++++++++++++ 23 files changed, 3017 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Cross.toml create mode 100644 LICENSE create mode 100644 cross/Dockerfile.aarch64-unknown-linux-musl create mode 100644 cross/Dockerfile.armv5te-unknown-linux-musleabi create mode 100644 cross/Dockerfile.armv7-unknown-linux-musleabihf create mode 100644 cross/Dockerfile.mips-unknown-linux-gnu create mode 100644 cross/Dockerfile.mips-unknown-linux-musl create mode 100644 cross/Dockerfile.mipsel-unknown-linux-musl create mode 100644 cross/Dockerfile.x86_64-unknown-linux-musl create mode 100644 rust-toolchain.toml create mode 100644 shell.nix create mode 100644 src/backend.rs create mode 100644 src/cmd/border_gateway.rs create mode 100644 src/cmd/configfile.rs create mode 100644 src/cmd/mod.rs create mode 100644 src/cmd/root.rs create mode 100644 src/config.rs create mode 100644 src/logging.rs create mode 100644 src/main.rs create mode 100644 src/packets.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0a6e8ed --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix", + "rust-analyzer.showUnlinkedFileNotification": false +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..95c4560 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1390 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-expr" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chirpstack-gateway-relay" +version = "4.0.0" +dependencies = [ + "anyhow", + "chirpstack_api", + "clap", + "handlebars", + "hex", + "log", + "lrwn_filters", + "once_cell", + "serde", + "simple_logger", + "syslog", + "tokio", + "toml", + "zmq", +] + +[[package]] +name = "chirpstack_api" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d467e02b0ebcf71ef7812dfa847620735616c6fdbd9984d9f31c95a007a50bb" +dependencies = [ + "hex", + "pbjson-build", + "prost", + "prost-types", + "rand", + "tonic-build", +] + +[[package]] +name = "clap" +version = "4.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c3242926edf34aec4ac3a77108ad4854bffaa2e4ddc1824124ce59231302d5" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dircpy" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8466f8d28ca6da4c9dfbbef6ad4bff6f2fdd5e412d821025b0d3f0a9d74a8c1e" +dependencies = [ + "jwalk", + "log", + "walkdir", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "handlebars" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "jwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" +dependencies = [ + "crossbeam", + "rayon", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "lrwn_filters" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e2edbfcdb340de9a711502b0b06387b1cea1d99cdfbf0b5e59f640f75fdb7" +dependencies = [ + "hex", + "serde", + "thiserror", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pbjson-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" +dependencies = [ + "heck", + "itertools", + "prost", + "prost-types", +] + +[[package]] +name = "pest" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f200d8d83c44a45b21764d1916299752ca035d15ecd46faca3e9a2a2bf6ad06" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcd6ab1236bbdb3a49027e920e693192ebfe8913f6d60e294de57463a493cfde" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a31940305ffc96863a735bef7c7994a00b325a7138fdbc5bda0f1a0476d3275" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ff62f5259e53b78d1af898941cdcdccfae7385cf7d793a6e55de5d05bb4b7d" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +dependencies = [ + "bytes", + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "simple_logger" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0ca6504625ee1aa5fda33913d2005eab98c7a42dd85f116ecce3ff54c9d3ef" +dependencies = [ + "colored", + "log", + "time", + "windows-sys 0.48.0", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "syn" +version = "2.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syslog" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7434e95bcccce1215d30f4bf84fe8c00e8de1b9be4fb736d747ca53d36e7f96f" +dependencies = [ + "error-chain", + "hostname", + "libc", + "log", + "time", +] + +[[package]] +name = "system-deps" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "thiserror" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "itoa", + "libc", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "num_cpus", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tonic-build" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeromq-src" +version = "0.2.6+4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc120b771270365d5ed0dfb4baf1005f2243ae1ae83703265cb3504070f4160b" +dependencies = [ + "cc", + "dircpy", +] + +[[package]] +name = "zmq" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd3091dd571fb84a9b3e5e5c6a807d186c411c812c8618786c3c30e5349234e7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "zmq-sys", +] + +[[package]] +name = "zmq-sys" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8351dc72494b4d7f5652a681c33634063bbad58046c1689e75270908fdc864" +dependencies = [ + "libc", + "system-deps", + "zeromq-src", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..09b380e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] + name = "chirpstack-gateway-relay" + description = "ChirpStack Gateway Relay" + repository = "https://github.com/chirpstack/chirpstack-gateway-relay" + homepage = "https://www.chirpstack.io/" + license = "MIT" + version = "4.0.0" + authors = ["Orne Brocaar "] + edition = "2021" + publish = false + +[dependencies] + clap = { version = "4.4", default-features = false, features = [ + "std", + "help", + "usage", + "derive", + ] } + chirpstack_api = { version = "4.6", default-features = false } + lrwn_filters = { version = "4.6", features = ["serde"] } + log = "0.4" + simple_logger = "4.2" + syslog = "6.1" + toml = "0.8" + handlebars = "4.4" + zmq = { version = "0.10" } + anyhow = "1.0" + serde = { version = "1.0", features = ["derive"] } + tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] } + once_cell = "1.19" + hex = "0.4.3" diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..8a0cde5 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,17 @@ +[target.x86_64-unknown-linux-musl] +dockerfile="cross/Dockerfile.x86_64-unknown-linux-musl" + +[target.aarch64-unknown-linux-musl] +dockerfile="cross/Dockerfile.aarch64-unknown-linux-musl" + +[target.armv7-unknown-linux-musleabihf] +dockerfile="cross/Dockerfile.armv7-unknown-linux-musleabihf" + +[target.mips-unknown-linux-musl] +dockerfile="cross/Dockerfile.mips-unknown-linux-musl" + +[target.mipsel-unknown-linux-musl] +dockerfile="cross/Dockerfile.mipsel-unknown-linux-musl" + +[target.armv5te-unknown-linux-musleabi] +dockerfile="cross/Dockerfile.armv5te-unknown-linux-musleabi" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b0bf363 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Orne Brocaar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cross/Dockerfile.aarch64-unknown-linux-musl b/cross/Dockerfile.aarch64-unknown-linux-musl new file mode 100644 index 0000000..1d3915e --- /dev/null +++ b/cross/Dockerfile.aarch64-unknown-linux-musl @@ -0,0 +1,6 @@ +FROM ghcr.io/cross-rs/aarch64-unknown-linux-musl:latest + +RUN apt-get update && \ + apt-get --assume-yes install \ + protobuf-compiler \ + libprotobuf-dev diff --git a/cross/Dockerfile.armv5te-unknown-linux-musleabi b/cross/Dockerfile.armv5te-unknown-linux-musleabi new file mode 100644 index 0000000..79dfde4 --- /dev/null +++ b/cross/Dockerfile.armv5te-unknown-linux-musleabi @@ -0,0 +1,6 @@ +FROM ghcr.io/cross-rs/armv5te-unknown-linux-musleabi:latest + +RUN apt-get update && \ + apt-get --assume-yes install \ + protobuf-compiler \ + libprotobuf-dev diff --git a/cross/Dockerfile.armv7-unknown-linux-musleabihf b/cross/Dockerfile.armv7-unknown-linux-musleabihf new file mode 100644 index 0000000..1a6682e --- /dev/null +++ b/cross/Dockerfile.armv7-unknown-linux-musleabihf @@ -0,0 +1,6 @@ +FROM ghcr.io/cross-rs/armv7-unknown-linux-musleabihf:latest + +RUN apt-get update && \ + apt-get --assume-yes install \ + protobuf-compiler \ + libprotobuf-dev diff --git a/cross/Dockerfile.mips-unknown-linux-gnu b/cross/Dockerfile.mips-unknown-linux-gnu new file mode 100644 index 0000000..fdc8ada --- /dev/null +++ b/cross/Dockerfile.mips-unknown-linux-gnu @@ -0,0 +1,6 @@ +FROM ghcr.io/cross-rs/mips-unknown-linux-gnu:latest + +RUN apt-get update && \ + apt-get --assume-yes install \ + protobuf-compiler \ + libprotobuf-dev diff --git a/cross/Dockerfile.mips-unknown-linux-musl b/cross/Dockerfile.mips-unknown-linux-musl new file mode 100644 index 0000000..23072e8 --- /dev/null +++ b/cross/Dockerfile.mips-unknown-linux-musl @@ -0,0 +1,6 @@ +FROM ghcr.io/cross-rs/mips-unknown-linux-musl:latest + +RUN apt-get update && \ + apt-get --assume-yes install \ + protobuf-compiler \ + libprotobuf-dev diff --git a/cross/Dockerfile.mipsel-unknown-linux-musl b/cross/Dockerfile.mipsel-unknown-linux-musl new file mode 100644 index 0000000..6f56bf9 --- /dev/null +++ b/cross/Dockerfile.mipsel-unknown-linux-musl @@ -0,0 +1,6 @@ +FROM ghcr.io/cross-rs/mipsel-unknown-linux-musl:latest + +RUN apt-get update && \ + apt-get --assume-yes install \ + protobuf-compiler \ + libprotobuf-dev diff --git a/cross/Dockerfile.x86_64-unknown-linux-musl b/cross/Dockerfile.x86_64-unknown-linux-musl new file mode 100644 index 0000000..30ac69e --- /dev/null +++ b/cross/Dockerfile.x86_64-unknown-linux-musl @@ -0,0 +1,6 @@ +FROM ghcr.io/cross-rs/x86_64-unknown-linux-musl:latest + +RUN apt-get update && \ + apt-get --assume-yes install \ + protobuf-compiler \ + libprotobuf-dev diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..cb0fe30 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.74.1" +components = ["rustfmt", "clippy"] +profile = "default" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..30052c5 --- /dev/null +++ b/shell.nix @@ -0,0 +1,23 @@ +{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-23.05.tar.gz") {} }: + +pkgs.mkShell { + nativeBuildInputs = [ + pkgs.pkg-config + ]; + buildInputs = [ + pkgs.cacert + pkgs.rustup + pkgs.protobuf + pkgs.perl + pkgs.cmake + pkgs.clang + pkgs.opkg-utils + pkgs.jq + pkgs.cargo-cross + pkgs.cargo-deb + ]; + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + BINDGEN_EXTRA_CLANG_ARGS = "-I${pkgs.llvmPackages.libclang.lib}/lib/clang/${pkgs.llvmPackages.libclang.version}/include"; + DOCKER_BUILDKIT = "1"; + NIX_STORE = "/nix/store"; +} \ No newline at end of file diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..7a4d205 --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,178 @@ +use std::sync::{Arc, Mutex}; + +use anyhow::Result; +use log::{error, info, trace}; +use once_cell::sync::OnceCell; +use tokio::task; + +use crate::config::Configuration; + +static CONCENTRATORD: OnceCell = OnceCell::new(); +static RELAY_CONCENTRATORD: OnceCell = OnceCell::new(); + +pub async fn setup(conf: &Configuration) -> Result<()> { + setup_concentratord(conf).await?; + setup_relay_concentratord(conf).await?; + Ok(()) +} + +async fn setup_concentratord(conf: &Configuration) -> Result<()> { + info!( + "Setting up Concentratord backend, event_url: {}, command_url: {}", + conf.backend.concentratord.event_url, conf.backend.concentratord.command_url + ); + + let zmq_ctx = zmq::Context::new(); + let event_sock = zmq_ctx.socket(zmq::SUB)?; + event_sock.connect(&conf.backend.concentratord.event_url)?; + event_sock.set_subscribe("".as_bytes())?; + + let cmd_sock = zmq_ctx.socket(zmq::REQ)?; + cmd_sock.connect(&conf.backend.concentratord.command_url)?; + + let mut b = Backend { + ctx: zmq_ctx, + cmd_url: conf.backend.concentratord.command_url.clone(), + cmd_sock: Mutex::new(cmd_sock), + gateway_id: None, + }; + b.read_gateway_id()?; + + tokio::spawn({ + let filters = lrwn_filters::Filters { + dev_addr_prefixes: conf.relay.filters.dev_addr_prefixes.clone(), + join_eui_prefixes: conf.relay.filters.join_eui_prefixes.clone(), + }; + + async move { + event_loop(event_sock, filters).await; + } + }); + + CONCENTRATORD + .set(b) + .map_err(|_| anyhow!("OnceCell set error"))?; + + Ok(()) +} + +async fn setup_relay_concentratord(conf: &Configuration) -> Result<()> { + info!( + "Setting up Relay Concentratord backend, event_url: {}, command_url: {}", + conf.backend.relay_concentratord.event_url, conf.backend.relay_concentratord.command_url + ); + + let zmq_ctx = zmq::Context::new(); + let event_sock = zmq_ctx.socket(zmq::SUB)?; + event_sock.connect(&conf.backend.relay_concentratord.event_url)?; + event_sock.set_subscribe("".as_bytes())?; + + let cmd_sock = zmq_ctx.socket(zmq::REQ)?; + cmd_sock.connect(&conf.backend.relay_concentratord.command_url)?; + + let mut b = Backend { + ctx: zmq_ctx, + cmd_url: conf.backend.concentratord.command_url.clone(), + cmd_sock: Mutex::new(cmd_sock), + gateway_id: None, + }; + b.read_gateway_id()?; + + tokio::spawn(async move { + relay_event_loop(event_sock).await; + }); + + RELAY_CONCENTRATORD + .set(b) + .map_err(|_| anyhow!("OnceCell set error"))?; + + Ok(()) +} + +struct Backend { + ctx: zmq::Context, + cmd_url: String, + cmd_sock: Mutex, + gateway_id: Option, +} + +impl Backend { + fn read_gateway_id(&mut self) -> Result<()> { + let cmd_sock = self.cmd_sock.lock().unwrap(); + + // send 'gateway_id' command with empty payload. + cmd_sock.send("gateway_id", zmq::SNDMORE)?; + cmd_sock.send("", 0)?; + + // set poller so that we can timeout after 100ms + let mut items = [cmd_sock.as_poll_item(zmq::POLLIN)]; + zmq::poll(&mut items, 100)?; + if !items[0].is_readable() { + return Err(anyhow!("Could not read gateway id")); + } + let gateway_id = cmd_sock.recv_bytes(0)?; + self.gateway_id = Some(hex::encode(gateway_id)); + + Ok(()) + } +} + +async fn event_loop(event_sock: zmq::Socket, filters: lrwn_filters::Filters) { + trace!("Starting event loop"); + let event_sock = Arc::new(Mutex::new(event_sock)); + + loop { + let event = match read_event(event_sock.clone()).await { + Ok(v) => v, + Err(err) => { + error!("Receive event error, error: {}", err); + continue; + } + }; + + if event.len() != 2 { + continue; + } + } +} + +async fn relay_event_loop(event_sock: zmq::Socket) { + trace!("Starting relay event loop"); + let event_sock = Arc::new(Mutex::new(event_sock)); + + loop { + let event = match read_event(event_sock.clone()).await { + Ok(v) => v, + Err(err) => { + error!("Receive event error, error: {}", err); + continue; + } + }; + + if event.len() != 2 { + continue; + } + } +} + +async fn read_event(event_sock: Arc>) -> Result>> { + task::spawn_blocking({ + move || -> Result>> { + let event_sock = event_sock.lock().unwrap(); + + // set poller so that we can timeout after 100ms + let mut items = [event_sock.as_poll_item(zmq::POLLIN)]; + zmq::poll(&mut items, 100)?; + if !items[0].is_readable() { + return Ok(vec![]); + } + + let msg = event_sock.recv_multipart(0)?; + if msg.len() != 2 { + return Err(anyhow!("Event must have two frames")); + } + Ok(msg) + } + }) + .await? +} diff --git a/src/cmd/border_gateway.rs b/src/cmd/border_gateway.rs new file mode 100644 index 0000000..177d251 --- /dev/null +++ b/src/cmd/border_gateway.rs @@ -0,0 +1,10 @@ +use anyhow::Result; + +use crate::backend; +use crate::config::Configuration; + +pub async fn run(conf: &Configuration) -> Result<()> { + backend::setup(conf).await?; + + Ok(()) +} diff --git a/src/cmd/configfile.rs b/src/cmd/configfile.rs new file mode 100644 index 0000000..d4d4ac0 --- /dev/null +++ b/src/cmd/configfile.rs @@ -0,0 +1,92 @@ +use crate::config; +use handlebars::{no_escape, Handlebars}; + +pub fn run(conf: &config::Configuration) { + let template = r#" +# Logging settings. +[logging] + + # Log level. + # + # Valid options are: + # * TRACE + # * DEBUG + # * INFO + # * WARN + # * ERROR + # * OFF + log_level="INFO" + + # Log to syslog. + # + # When set to true, log messages are being written to syslog instead of stdout. + log_to_syslog=false + + +# Relay configuration. +[relay] + + # Relay frequencies. + # + # The ChirpStack Gateway Relay will randomly use one of the configured + # frequencies when relaying uplink and downlink messages. + frequencies=[ + {{#each relay.frequencies}} + {{this}}, + {{/each}} + ] + + # Proxy API configuration (Border Gateway mode). + # + # If the Gateway Relay is configured to operate as Border Gateway. It + # will unwrap relayed uplink frames, and will wrap downlink payloads that + # must be relayed. In this case the ChirpStack MQTT Forwarder must be + # configured to use the proxy API instead of the Concentratord API. + # + # Payloads of devices that are under the direct coverage of this gateway + # are transparently proxied between the ChirpStack MQTT Forwarder and + # ChirpStack Concentratord. + [relay.proxy_api] + + # Event PUB socket bind. + event_bind="{{ relay.proxy_api.event_bind }}" + + # Command REP socket bind. + command_bind="{{ relay.proxy_api.command_bind }}" + + +# Backend configuration. +[backend] + + # ChirpStack Concentratord configuration (Relay <> End Device). + [backend.concentratord] + + # Event API URL. + event_url="{{ backend.concentratord.event_url }}" + + # Command API URL. + command_url="{{ backend.concentratord.command_url }}" + + + # ChirpStack Concentratord configuration (Relay <> Relay). + # + # While not required, this configuration makes it possible to use a different + # Concentratord instance for the Relay <> Relay communication. E.g. this + # makes it possible to use ISM2400 for Relay <> Relay communication. + [backend.relay_concentratord] + + # Event API URL. + event_url="{{ backend.relay_concentratord.event_url }}" + + # Command API URL. + command_url="{{ backend.relay_concentratord.command_url }}" +"#; + + let mut reg = Handlebars::new(); + reg.register_escape_fn(no_escape); + println!( + "{}", + reg.render_template(template, &conf) + .expect("Render configfile error") + ); +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs new file mode 100644 index 0000000..43c16ee --- /dev/null +++ b/src/cmd/mod.rs @@ -0,0 +1,3 @@ +pub mod border_gateway; +pub mod configfile; +pub mod root; diff --git a/src/cmd/root.rs b/src/cmd/root.rs new file mode 100644 index 0000000..177d251 --- /dev/null +++ b/src/cmd/root.rs @@ -0,0 +1,10 @@ +use anyhow::Result; + +use crate::backend; +use crate::config::Configuration; + +pub async fn run(conf: &Configuration) -> Result<()> { + backend::setup(conf).await?; + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a6e2365 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,112 @@ +use std::fs; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Default)] +#[serde(default)] +pub struct Configuration { + pub logging: Logging, + pub relay: Relay, + pub backend: Backend, + pub channels: Vec, + pub data_rates: Vec, +} + +impl Configuration { + pub fn get(filenames: &[String]) -> Result { + let mut content = String::new(); + for file_name in filenames { + content.push_str(&fs::read_to_string(file_name)?); + } + Ok(toml::from_str(&content)?) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(default)] +pub struct Logging { + pub level: String, + pub log_to_syslog: bool, +} + +impl Default for Logging { + fn default() -> Self { + Logging { + level: "info".into(), + log_to_syslog: false, + } + } +} + +#[derive(Serialize, Deserialize, Default)] +#[serde(default)] +pub struct Relay { + pub frequencies: Vec, + pub data_rate: DataRate, + pub proxy_api: ProxyApi, + pub filters: Filters, +} + +#[derive(Serialize, Deserialize, Default)] +#[serde(default)] +pub struct Backend { + pub concentratord: Concentratord, + pub relay_concentratord: Concentratord, +} + +#[derive(Serialize, Deserialize)] +#[serde(default)] +pub struct Concentratord { + pub event_url: String, + pub command_url: String, +} + +impl Default for Concentratord { + fn default() -> Self { + Concentratord { + event_url: "ipc:///tmp/concentratord_event".into(), + command_url: "ipc:///tmp/concentratord_command".into(), + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(default)] +pub struct ProxyApi { + pub event_bind: String, + pub command_bind: String, +} + +impl Default for ProxyApi { + fn default() -> Self { + ProxyApi { + event_bind: "ipc:///tmp/chirpstack_gateway_relay_event".into(), + command_bind: "ipc:///tmp/chirpstack_gateway_relay_command".into(), + } + } +} + +#[derive(Serialize, Deserialize, Default)] +#[serde(default)] +pub struct Filters { + pub dev_addr_prefixes: Vec, + pub join_eui_prefixes: Vec, +} + +#[derive(Serialize, Deserialize, Default)] +#[serde(default)] +pub struct Channel { + pub frequency: u32, + pub min_dr: u8, + pub max_dr: u8, +} + +#[derive(Serialize, Deserialize, Default)] +#[serde(default)] +pub struct DataRate { + spreading_factor: u8, + bandwidth: u32, + coding_rate: String, + bitrate: u32, +} diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..bd86a09 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,22 @@ +use std::process; + +use anyhow::Result; +use syslog::{BasicLogger, Facility, Formatter3164}; + +pub fn setup(name: &str, level: log::Level, syslog: bool) -> Result<()> { + if syslog { + let formatter = Formatter3164 { + facility: Facility::LOG_USER, + hostname: None, + process: name.to_string(), + pid: process::id(), + }; + let logger = syslog::unix(formatter).map_err(|e| anyhow!("{}", e))?; + log::set_boxed_logger(Box::new(BasicLogger::new(logger))) + .map(|()| log::set_max_level(level.to_level_filter()))?; + } else { + simple_logger::init_with_level(level)?; + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cfcb823 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,76 @@ +#[macro_use] +extern crate anyhow; + +use std::thread::sleep; +use std::time::Duration; +use std::{process, str::FromStr}; + +use clap::{Parser, Subcommand}; +use log::info; + +mod backend; +mod cmd; +mod config; +mod logging; +mod packets; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[arg(short, long, value_name = "FILE")] + config: Vec, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// Print the configuration template + Configfile {}, + /// Operate the Relay as Border Gateway + BorderGateway {}, +} + +#[tokio::main] +async fn main() { + let cli = Cli::parse(); + let conf = config::Configuration::get(&cli.config).expect("Read configuration error"); + + if let Some(Commands::Configfile {}) = &cli.command { + cmd::configfile::run(&conf); + process::exit(0); + } + + let log_level = log::Level::from_str(&conf.logging.level).expect("Parse log_level error"); + + // Loop until success, as this will fail when syslog hasn't been fully started. + while let Err(e) = logging::setup( + env!("CARGO_PKG_NAME"), + log_level, + conf.logging.log_to_syslog, + ) { + println!("Setup log error: {}", e); + sleep(Duration::from_secs(1)) + } + + let border_gateway = if let Some(Commands::BorderGateway {}) = &cli.command { + true + } else { + false + }; + + info!( + "Starting {} (border_gateway: {},version: {}, docs: {})", + env!("CARGO_PKG_DESCRIPTION"), + border_gateway, + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_HOMEPAGE"), + ); + + if border_gateway { + cmd::border_gateway::run(&conf).await.unwrap(); + } else { + cmd::root::run(&conf).await.unwrap(); + } +} diff --git a/src/packets.rs b/src/packets.rs new file mode 100644 index 0000000..866bb92 --- /dev/null +++ b/src/packets.rs @@ -0,0 +1,982 @@ +use anyhow::Result; +use chirpstack_api::gw; + +#[derive(Debug, PartialEq, Eq)] +pub enum Packet { + Relay(RelayPacket), + Lora(Vec), +} + +impl Packet { + pub fn from_slice(b: &[u8]) -> Result { + if b.is_empty() { + return Err(anyhow!("Input is empty")); + } + + // Check for proprietary "111" bits prefix. + if b[0] & 0xe0 != 0 { + Ok(Packet::Relay(RelayPacket::from_slice(b)?)) + } else { + Ok(Packet::Lora(b.to_vec())) + } + } + + pub fn to_vec(&self) -> Result> { + match self { + Packet::Relay(v) => v.to_vec(), + Packet::Lora(v) => Ok(v.clone()), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct RelayPacket { + pub mhdr: MHDR, + pub payload: Payload, +} + +impl RelayPacket { + pub fn from_slice(b: &[u8]) -> Result { + if b.is_empty() { + return Err(anyhow!("Input is empty")); + } + + let mhdr = MHDR::from_byte(b[0])?; + + Ok(RelayPacket { + payload: match mhdr.payload_type { + PayloadType::Uplink => Payload::Uplink(UplinkPayload::from_slice(&b[1..])?), + PayloadType::Downlink => Payload::Downlink(DownlinkPayload::from_slice(&b[1..])?), + }, + mhdr, + }) + } + + pub fn to_vec(&self) -> Result> { + let mut b = vec![self.mhdr.to_byte()?]; + b.extend_from_slice(&match &self.payload { + Payload::Uplink(v) => v.to_vec()?, + Payload::Downlink(v) => v.to_vec()?, + }); + Ok(b) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct MHDR { + pub payload_type: PayloadType, + pub hop_count: u8, +} + +impl MHDR { + pub fn from_byte(b: u8) -> Result { + if (b >> 5) != 0x07 { + return Err(anyhow!("Invalid MType")); + } + + Ok(MHDR { + payload_type: PayloadType::from_byte((b >> 3) & 0x03)?, + hop_count: b & 0x07, + }) + } + + pub fn to_byte(&self) -> Result { + if self.hop_count > 7 { + return Err(anyhow!("Max hop_count is 7")); + } + + Ok(0x07 << 5 | self.payload_type.to_byte() << 3 | self.hop_count) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum PayloadType { + Uplink, + Downlink, +} + +impl PayloadType { + pub fn from_byte(b: u8) -> Result { + Ok(match b { + 0x00 => PayloadType::Uplink, + 0x01 => PayloadType::Downlink, + _ => return Err(anyhow!("Unexpected PayloadType: {}", b)), + }) + } + + pub fn to_byte(&self) -> u8 { + match self { + PayloadType::Uplink => 0x00, + PayloadType::Downlink => 0x01, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Payload { + Uplink(UplinkPayload), + Downlink(DownlinkPayload), +} + +#[derive(Debug, PartialEq, Eq)] +pub struct UplinkPayload { + pub metadata: UplinkMetadata, + pub relay_gateway_id: [u8; 4], + pub phy_payload: Vec, +} + +impl UplinkPayload { + pub fn from_slice(b: &[u8]) -> Result { + if b.len() < 9 { + return Err(anyhow!("At least 9 bytes are expected")); + } + + let mut md = [0; 5]; + let mut gw_id = [0; 4]; + md.copy_from_slice(&b[0..5]); + gw_id.copy_from_slice(&b[5..9]); + + Ok(UplinkPayload { + metadata: UplinkMetadata::from_bytes(md), + relay_gateway_id: gw_id, + phy_payload: b[9..].to_vec(), + }) + } + + pub fn to_vec(&self) -> Result> { + let mut b = self.metadata.to_bytes()?.to_vec(); + b.extend_from_slice(&self.relay_gateway_id); + b.extend_from_slice(&self.phy_payload); + Ok(b) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct UplinkMetadata { + pub uplink_id: u16, + pub dr: u8, + pub rssi: i16, + pub snr: i8, + pub channel: u8, +} + +impl UplinkMetadata { + pub fn from_bytes(b: [u8; 5]) -> Self { + let snr = b[3] & 0x3f; + let snr = if snr > 31 { + (snr as i8) - 64 + } else { + snr as i8 + }; + + UplinkMetadata { + uplink_id: u16::from_be_bytes([b[0], b[1]]) >> 4, + dr: b[1] & 0x0f, + rssi: -1 * (b[2] as i16), + snr, + channel: b[4], + } + } + + pub fn to_bytes(&self) -> Result<[u8; 5]> { + if self.uplink_id > 4095 { + return Err(anyhow!("Max uplink_id value is 4095")); + } + + if self.dr > 15 { + return Err(anyhow!("Max dr value is 15")); + } + + if self.rssi > 0 { + return Err(anyhow!("Max rssi value is 0")); + } + + if self.rssi < -255 { + return Err(anyhow!("Min rssi value is -255")); + } + + if self.snr < -32 { + return Err(anyhow!("Min snr value is -32")); + } + if self.snr > 31 { + return Err(anyhow!("Max snr value is 31")); + } + + let uplink_id_b = (self.uplink_id << 4).to_be_bytes(); + + Ok([ + uplink_id_b[0], + uplink_id_b[1] | self.dr, + (-1 * self.rssi) as u8, + if self.snr < 0 { + (self.snr + 64) as u8 + } else { + self.snr as u8 + }, + self.channel, + ]) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct DownlinkPayload { + pub metadata: DownlinkMetadata, + pub relay_gateway_id: [u8; 4], + pub phy_payload: Vec, +} + +impl DownlinkPayload { + pub fn from_slice(b: &[u8]) -> Result { + if b.len() < 10 { + return Err(anyhow!("At least 10 bytes are expected")); + } + + let mut md = [0; 6]; + let mut gw_id = [0; 4]; + md.copy_from_slice(&b[0..6]); + gw_id.copy_from_slice(&b[6..10]); + + Ok(DownlinkPayload { + metadata: DownlinkMetadata::from_bytes(md), + relay_gateway_id: gw_id, + phy_payload: b[10..].to_vec(), + }) + } + + pub fn to_vec(&self) -> Result> { + let mut b = self.metadata.to_bytes()?.to_vec(); + b.extend_from_slice(&self.relay_gateway_id); + b.extend_from_slice(&self.phy_payload); + Ok(b) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct DownlinkMetadata { + pub uplink_id: u16, + pub dr: u8, + pub frequency: u32, + pub delay: u8, +} + +impl DownlinkMetadata { + pub fn from_bytes(b: [u8; 6]) -> Self { + DownlinkMetadata { + uplink_id: u16::from_be_bytes([b[0], b[1]]) >> 4, + dr: b[1] & 0x0f, + frequency: decode_freq(&b[2..5]).unwrap(), + delay: (b[5] & 0x0f) + 1, + } + } + + pub fn to_bytes(&self) -> Result<[u8; 6]> { + if self.uplink_id > 4095 { + return Err(anyhow!("Max uplink_id value is 4095")); + } + + if self.dr > 15 { + return Err(anyhow!("Max dr value is 15")); + } + + if self.delay < 1 { + return Err(anyhow!("Min delay value is 1")); + } + + if self.delay > 16 { + return Err(anyhow!("Max delay value is 16")); + } + + let uplink_id_b = (self.uplink_id << 4).to_be_bytes(); + let freq_b = encode_freq(self.frequency)?; + + Ok([ + uplink_id_b[0], + uplink_id_b[1] | self.dr, + freq_b[0], + freq_b[1], + freq_b[2], + self.delay - 1, + ]) + } +} + +pub fn encode_freq(freq: u32) -> Result<[u8; 3]> { + let mut freq = freq; + // Support LoRaWAN 2.4GHz, in which case the stepping is 200Hz: + // See Frequency Encoding in MAC Commands + // https://lora-developers.semtech.com/documentation/tech-papers-and-guides/physical-layer-proposal-2.4ghz/ + if freq >= 2400000000 { + freq /= 2; + } + + if freq / 100 >= (1 << 24) { + return Err(anyhow!("Max frequency value is 2^24 - 1")); + } + if freq % 100 != 0 { + return Err(anyhow!("Frequency must be multiple of 100")); + } + + let mut b = [0; 3]; + b[0..3].copy_from_slice(&(freq / 100).to_be_bytes()[1..4]); + Ok(b) +} + +pub fn decode_freq(b: &[u8]) -> Result { + if b.len() != 3 { + return Err(anyhow!("3 bytes expected for frequency")); + } + let mut freq_b: [u8; 4] = [0; 4]; + freq_b[1..4].copy_from_slice(&b[0..3]); + let mut freq = u32::from_be_bytes(freq_b); + + if freq >= 12000000 { + // 2.4GHz frequency + freq *= 200 + } else { + freq *= 100 + } + + Ok(freq) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_mhdr_from_byte() { + struct Test { + name: String, + byte: u8, + expected_mhdr: Option, + expected_error: Option, + } + + let tests = vec![ + Test { + name: "uplink + hop count 3".to_string(), + byte: 0xe3, + expected_mhdr: Some(MHDR { + payload_type: PayloadType::Uplink, + hop_count: 3, + }), + expected_error: None, + }, + Test { + name: "downlink + hop count 7".to_string(), + byte: 0xef, + expected_mhdr: Some(MHDR { + payload_type: PayloadType::Downlink, + hop_count: 7, + }), + expected_error: None, + }, + Test { + name: "invalid MType".to_string(), + byte: 0x00, + expected_mhdr: None, + expected_error: Some("Invalid MType".into()), + }, + ]; + + for tst in &tests { + println!("> {}", tst.name); + let res = MHDR::from_byte(tst.byte); + + if let Some(mhdr) = &tst.expected_mhdr { + assert_eq!(mhdr, &res.unwrap()); + } else if let Some(err) = &tst.expected_error { + assert_eq!(err.to_string(), res.unwrap_err().to_string()); + } + } + } + + #[test] + fn test_mhdr_to_byte() { + struct Test { + name: String, + mhdr: MHDR, + expected_byte: Option, + expected_error: Option, + } + + let tests = vec![ + Test { + name: "uplink + hop count 3".to_string(), + mhdr: MHDR { + payload_type: PayloadType::Uplink, + hop_count: 3, + }, + expected_byte: Some(0xe3), + expected_error: None, + }, + Test { + name: "downlink + hop count 7".to_string(), + mhdr: MHDR { + payload_type: PayloadType::Downlink, + hop_count: 7, + }, + expected_byte: Some(0xef), + expected_error: None, + }, + Test { + name: "hop count exceeds max value".to_string(), + mhdr: MHDR { + payload_type: PayloadType::Uplink, + hop_count: 8, + }, + expected_byte: None, + expected_error: Some("Max hop_count is 7".into()), + }, + ]; + + for tst in &tests { + println!("> {}", tst.name); + let res = tst.mhdr.to_byte(); + + if let Some(b) = &tst.expected_byte { + assert_eq!(b, &res.unwrap()); + } else if let Some(err) = &tst.expected_error { + assert_eq!(err.to_string(), res.unwrap_err().to_string()); + } + } + } + + #[test] + fn test_uplink_metadata_to_bytes() { + struct Test { + name: String, + metadata: UplinkMetadata, + expected_bytes: Option<[u8; 5]>, + expected_error: Option, + } + + let tests = vec![ + Test { + name: "Uplink ID exceeds max value".into(), + metadata: UplinkMetadata { + uplink_id: 4096, + dr: 0, + rssi: 0, + snr: 0, + channel: 0, + }, + expected_bytes: None, + expected_error: Some("Max uplink_id value is 4095".into()), + }, + Test { + name: "DR exceeds max value".into(), + metadata: UplinkMetadata { + uplink_id: 0, + dr: 16, + rssi: 0, + snr: 0, + channel: 0, + }, + expected_bytes: None, + expected_error: Some("Max dr value is 15".into()), + }, + Test { + name: "RSSI exceeds max value".into(), + metadata: UplinkMetadata { + uplink_id: 0, + dr: 0, + rssi: 1, + snr: 0, + channel: 0, + }, + expected_bytes: None, + expected_error: Some("Max rssi value is 0".into()), + }, + Test { + name: "RSSI exceeds min value".into(), + metadata: UplinkMetadata { + uplink_id: 0, + dr: 0, + rssi: -256, + snr: 0, + channel: 0, + }, + expected_bytes: None, + expected_error: Some("Min rssi value is -255".into()), + }, + Test { + name: "SNR exceeds max value".into(), + metadata: UplinkMetadata { + uplink_id: 0, + dr: 0, + rssi: 0, + snr: 32, + channel: 0, + }, + expected_bytes: None, + expected_error: Some("Max snr value is 31".into()), + }, + Test { + name: "SNR exceeds min value".into(), + metadata: UplinkMetadata { + uplink_id: 0, + dr: 0, + rssi: 0, + snr: -33, + channel: 0, + }, + expected_bytes: None, + expected_error: Some("Min snr value is -32".into()), + }, + Test { + name: "Uplink id: 1024, dr: 3, rssi: -120, snr: -12, channel: 64".into(), + metadata: UplinkMetadata { + uplink_id: 1024, + dr: 3, + rssi: -120, + snr: -12, + channel: 64, + }, + expected_bytes: Some([0x40, 0x03, 0x78, 0x34, 0x40]), + expected_error: None, + }, + ]; + + for tst in &tests { + println!("> {}", tst.name); + let res = tst.metadata.to_bytes(); + + if let Some(b) = &tst.expected_bytes { + assert_eq!(b, &res.unwrap()); + } else if let Some(err) = &tst.expected_error { + assert_eq!(err.to_string(), res.unwrap_err().to_string()); + } + } + } + + #[test] + fn test_uplink_metadata_from_bytes() { + struct Test { + name: String, + bytes: [u8; 5], + expected_metadata: UplinkMetadata, + } + + let tests = vec![Test { + name: "Uplink id: 1024, dr: 3, rssi: -120, snr: -12, channel: 64".into(), + bytes: [0x40, 0x03, 0x78, 0x34, 0x40], + expected_metadata: UplinkMetadata { + uplink_id: 1024, + dr: 3, + rssi: -120, + snr: -12, + channel: 64, + }, + }]; + + for tst in &tests { + println!("> {}", tst.name); + let res = UplinkMetadata::from_bytes(tst.bytes); + assert_eq!(res, tst.expected_metadata); + } + } + + #[test] + fn test_uplink_payload_from_vec() { + let b = vec![0x40, 0x03, 0x78, 0x34, 0x40, 0x01, 0x02, 0x03, 0x04, 0x05]; + let up_pl = UplinkPayload::from_slice(&b).unwrap(); + assert_eq!( + UplinkPayload { + metadata: UplinkMetadata { + uplink_id: 1024, + dr: 3, + rssi: -120, + snr: -12, + channel: 64, + }, + relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + phy_payload: vec![0x05], + }, + up_pl, + ); + } + + #[test] + fn test_uplink_payload_to_vec() { + let up_pl = UplinkPayload { + metadata: UplinkMetadata { + uplink_id: 1024, + dr: 3, + rssi: -120, + snr: -12, + channel: 64, + }, + relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + phy_payload: vec![0x05], + }; + let b = up_pl.to_vec().unwrap(); + assert_eq!( + vec![0x40, 0x03, 0x78, 0x34, 0x40, 0x01, 0x02, 0x03, 0x04, 0x05], + b + ); + } + + #[test] + fn test_downlink_metadata_from_bytes() { + struct Test { + name: String, + bytes: [u8; 6], + expected_metadata: DownlinkMetadata, + } + + let tests = vec![Test { + name: "Uplink id: 1024, dr: 3, frequency: 868100000, delay: 16".into(), + bytes: [0x40, 0x03, 0x84, 0x76, 0x28, 0x0f], + expected_metadata: DownlinkMetadata { + uplink_id: 1024, + dr: 3, + frequency: 868100000, + delay: 16, + }, + }]; + + for tst in &tests { + println!("> {}", tst.name); + let res = DownlinkMetadata::from_bytes(tst.bytes); + assert_eq!(res, tst.expected_metadata); + } + } + + #[test] + fn test_downlink_metaata_to_bytes() { + struct Test { + name: String, + metadata: DownlinkMetadata, + expected_bytes: Option<[u8; 6]>, + expected_error: Option, + } + + let tests = vec![ + Test { + name: "Uplink ID exceeds max value".into(), + metadata: DownlinkMetadata { + uplink_id: 4096, + dr: 0, + frequency: 868100000, + delay: 1, + }, + expected_bytes: None, + expected_error: Some("Max uplink_id value is 4095".into()), + }, + Test { + name: "DR exceeds max value".into(), + metadata: DownlinkMetadata { + uplink_id: 0, + dr: 16, + frequency: 868100000, + delay: 1, + }, + expected_bytes: None, + expected_error: Some("Max dr value is 15".into()), + }, + Test { + name: "Frequency not multiple of 100".into(), + metadata: DownlinkMetadata { + uplink_id: 0, + dr: 0, + frequency: 868100001, + delay: 1, + }, + expected_bytes: None, + expected_error: Some("Frequency must be multiple of 100".into()), + }, + Test { + name: "Delay exceeds max value".into(), + metadata: DownlinkMetadata { + uplink_id: 0, + dr: 0, + frequency: 868100000, + delay: 17, + }, + expected_bytes: None, + expected_error: Some("Max delay value is 16".into()), + }, + Test { + name: "Uplink id: 1024, dr: 3, frequency: 868100000, delay: 16".into(), + metadata: DownlinkMetadata { + uplink_id: 1024, + dr: 3, + frequency: 868100000, + delay: 16, + }, + expected_bytes: Some([0x40, 0x03, 0x84, 0x76, 0x28, 0x0f]), + expected_error: None, + }, + ]; + + for tst in &tests { + println!("> {}", tst.name); + let res = tst.metadata.to_bytes(); + + if let Some(b) = &tst.expected_bytes { + assert_eq!(b, &res.unwrap()); + } else if let Some(err) = &tst.expected_error { + assert_eq!(err.to_string(), res.unwrap_err().to_string()); + } + } + } + + #[test] + fn test_downlink_payload_from_slice() { + let b = vec![ + 0x40, 0x03, 0x84, 0x76, 0x28, 0x0f, 0x01, 0x02, 0x03, 0x04, 0x05, + ]; + let dn_pl = DownlinkPayload::from_slice(&b).unwrap(); + assert_eq!( + DownlinkPayload { + metadata: DownlinkMetadata { + uplink_id: 1024, + dr: 3, + frequency: 868100000, + delay: 16, + }, + relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + phy_payload: vec![0x05], + }, + dn_pl, + ); + } + + #[test] + fn test_downlink_payload_to_vec() { + let dn_pl = DownlinkPayload { + metadata: DownlinkMetadata { + uplink_id: 1024, + dr: 3, + frequency: 868100000, + delay: 16, + }, + relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + phy_payload: vec![0x05], + }; + let b = dn_pl.to_vec().unwrap(); + assert_eq!( + vec![0x40, 0x03, 0x84, 0x76, 0x28, 0x0f, 0x01, 0x02, 0x03, 0x04, 0x05,], + b + ); + } + + #[test] + fn test_relay_packet_from_slice() { + struct Test { + name: String, + bytes: Vec, + expected_relay_packet: RelayPacket, + } + + let tests = vec![ + Test { + name: "uplink".into(), + bytes: vec![ + 0xe3, 0x40, 0x03, 0x78, 0x34, 0x40, 0x01, 0x02, 0x03, 0x04, 0x05, + ], + expected_relay_packet: RelayPacket { + mhdr: MHDR { + payload_type: PayloadType::Uplink, + hop_count: 3, + }, + payload: Payload::Uplink(UplinkPayload { + metadata: UplinkMetadata { + uplink_id: 1024, + dr: 3, + rssi: -120, + snr: -12, + channel: 64, + }, + relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + phy_payload: vec![0x05], + }), + }, + }, + Test { + name: "downlink".into(), + bytes: vec![ + 0xef, 0x40, 0x03, 0x84, 0x76, 0x28, 0x0f, 0x01, 0x02, 0x03, 0x04, 0x05, + ], + expected_relay_packet: RelayPacket { + mhdr: MHDR { + payload_type: PayloadType::Downlink, + hop_count: 7, + }, + payload: Payload::Downlink(DownlinkPayload { + metadata: DownlinkMetadata { + uplink_id: 1024, + dr: 3, + frequency: 868100000, + delay: 16, + }, + relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + phy_payload: vec![0x05], + }), + }, + }, + ]; + + for tst in &tests { + println!("> {}", tst.name); + let pl = RelayPacket::from_slice(&tst.bytes).unwrap(); + assert_eq!(tst.expected_relay_packet, pl); + } + } + + #[test] + fn test_relay_packet_to_vec() { + struct Test { + name: String, + relay_packet: RelayPacket, + expected_bytes: Vec, + } + + let tests = vec![ + Test { + name: "uplink".into(), + expected_bytes: vec![ + 0xe3, 0x40, 0x03, 0x78, 0x34, 0x40, 0x01, 0x02, 0x03, 0x04, 0x05, + ], + relay_packet: RelayPacket { + mhdr: MHDR { + payload_type: PayloadType::Uplink, + hop_count: 3, + }, + payload: Payload::Uplink(UplinkPayload { + metadata: UplinkMetadata { + uplink_id: 1024, + dr: 3, + rssi: -120, + snr: -12, + channel: 64, + }, + relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + phy_payload: vec![0x05], + }), + }, + }, + Test { + name: "downlink".into(), + expected_bytes: vec![ + 0xef, 0x40, 0x03, 0x84, 0x76, 0x28, 0x0f, 0x01, 0x02, 0x03, 0x04, 0x05, + ], + relay_packet: RelayPacket { + mhdr: MHDR { + payload_type: PayloadType::Downlink, + hop_count: 7, + }, + payload: Payload::Downlink(DownlinkPayload { + metadata: DownlinkMetadata { + uplink_id: 1024, + dr: 3, + frequency: 868100000, + delay: 16, + }, + relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + phy_payload: vec![0x05], + }), + }, + }, + ]; + + for tst in &tests { + println!("> {}", tst.name); + let b = tst.relay_packet.to_vec().unwrap(); + assert_eq!(tst.expected_bytes, b); + } + } + + #[test] + fn test_packet_from_slice() { + struct Test { + name: String, + bytes: Vec, + expected_packet: Packet, + } + + let tests = vec![ + Test { + name: "relay packet".into(), + bytes: vec![ + 0xe3, 0x40, 0x03, 0x78, 0x34, 0x40, 0x01, 0x02, 0x03, 0x04, 0x05, + ], + expected_packet: Packet::Relay(RelayPacket { + mhdr: MHDR { + payload_type: PayloadType::Uplink, + hop_count: 3, + }, + payload: Payload::Uplink(UplinkPayload { + metadata: UplinkMetadata { + uplink_id: 1024, + dr: 3, + rssi: -120, + snr: -12, + channel: 64, + }, + relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + phy_payload: vec![0x05], + }), + }), + }, + Test { + name: "lora packet".into(), + bytes: vec![0x01, 0x02, 0x03], + expected_packet: Packet::Lora(vec![0x01, 0x02, 0x03]), + }, + ]; + + for tst in &tests { + println!("> {}", tst.name); + let pkt = Packet::from_slice(&tst.bytes).unwrap(); + assert_eq!(tst.expected_packet, pkt); + } + } + + #[test] + fn test_packet_to_vec() { + struct Test { + name: String, + expected_bytes: Vec, + packet: Packet, + } + + let tests = vec![ + Test { + name: "relay packet".into(), + expected_bytes: vec![ + 0xe3, 0x40, 0x03, 0x78, 0x34, 0x40, 0x01, 0x02, 0x03, 0x04, 0x05, + ], + packet: Packet::Relay(RelayPacket { + mhdr: MHDR { + payload_type: PayloadType::Uplink, + hop_count: 3, + }, + payload: Payload::Uplink(UplinkPayload { + metadata: UplinkMetadata { + uplink_id: 1024, + dr: 3, + rssi: -120, + snr: -12, + channel: 64, + }, + relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + phy_payload: vec![0x05], + }), + }), + }, + Test { + name: "lora packet".into(), + expected_bytes: vec![0x01, 0x02, 0x03], + packet: Packet::Lora(vec![0x01, 0x02, 0x03]), + }, + ]; + + for tst in &tests { + println!("> {}", tst.name); + let b = tst.packet.to_vec().unwrap(); + assert_eq!(tst.expected_bytes, b); + } + } +} From 0f2a7eecaef687797f5ea34a0084137c29982bd5 Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Tue, 16 Jan 2024 14:37:18 +0000 Subject: [PATCH 2/3] WIP. --- Cargo.lock | 1 + Cargo.toml | 1 + src/backend.rs | 277 ++++++++++++++++++++++++++++++++++++-- src/cmd/border_gateway.rs | 10 -- src/cmd/configfile.rs | 14 +- src/cmd/mod.rs | 1 - src/cmd/root.rs | 3 +- src/config.rs | 126 ++++++++++++++--- src/helpers.rs | 121 +++++++++++++++++ src/lib.rs | 17 +++ src/main.rs | 30 +---- src/packets.rs | 49 ++++--- src/proxy.rs | 86 ++++++++++++ src/relay.rs | 216 +++++++++++++++++++++++++++++ 14 files changed, 858 insertions(+), 94 deletions(-) delete mode 100644 src/cmd/border_gateway.rs create mode 100644 src/helpers.rs create mode 100644 src/lib.rs create mode 100644 src/proxy.rs create mode 100644 src/relay.rs diff --git a/Cargo.lock b/Cargo.lock index 95c4560..afed6a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,7 @@ dependencies = [ "log", "lrwn_filters", "once_cell", + "rand", "serde", "simple_logger", "syslog", diff --git a/Cargo.toml b/Cargo.toml index 09b380e..1549963 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,4 @@ tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] } once_cell = "1.19" hex = "0.4.3" + rand = "0.8" \ No newline at end of file diff --git a/src/backend.rs b/src/backend.rs index 7a4d205..306d89b 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,11 +1,16 @@ use std::sync::{Arc, Mutex}; +use std::thread::sleep; +use std::time::Duration; use anyhow::Result; -use log::{error, info, trace}; +use chirpstack_api::prost::Message; +use log::{debug, error, info, trace, warn}; use once_cell::sync::OnceCell; use tokio::task; use crate::config::Configuration; +use crate::{relay, ZMQ_CONTEXT}; +use chirpstack_api::gw; static CONCENTRATORD: OnceCell = OnceCell::new(); static RELAY_CONCENTRATORD: OnceCell = OnceCell::new(); @@ -22,7 +27,7 @@ async fn setup_concentratord(conf: &Configuration) -> Result<()> { conf.backend.concentratord.event_url, conf.backend.concentratord.command_url ); - let zmq_ctx = zmq::Context::new(); + let zmq_ctx = ZMQ_CONTEXT.lock().unwrap(); let event_sock = zmq_ctx.socket(zmq::SUB)?; event_sock.connect(&conf.backend.concentratord.event_url)?; event_sock.set_subscribe("".as_bytes())?; @@ -31,7 +36,6 @@ async fn setup_concentratord(conf: &Configuration) -> Result<()> { cmd_sock.connect(&conf.backend.concentratord.command_url)?; let mut b = Backend { - ctx: zmq_ctx, cmd_url: conf.backend.concentratord.command_url.clone(), cmd_sock: Mutex::new(cmd_sock), gateway_id: None, @@ -39,13 +43,14 @@ async fn setup_concentratord(conf: &Configuration) -> Result<()> { b.read_gateway_id()?; tokio::spawn({ + let border_gateway = conf.relay.border_gateway; let filters = lrwn_filters::Filters { dev_addr_prefixes: conf.relay.filters.dev_addr_prefixes.clone(), join_eui_prefixes: conf.relay.filters.join_eui_prefixes.clone(), }; async move { - event_loop(event_sock, filters).await; + event_loop(border_gateway, event_sock, filters).await; } }); @@ -62,7 +67,7 @@ async fn setup_relay_concentratord(conf: &Configuration) -> Result<()> { conf.backend.relay_concentratord.event_url, conf.backend.relay_concentratord.command_url ); - let zmq_ctx = zmq::Context::new(); + let zmq_ctx = ZMQ_CONTEXT.lock().unwrap(); let event_sock = zmq_ctx.socket(zmq::SUB)?; event_sock.connect(&conf.backend.relay_concentratord.event_url)?; event_sock.set_subscribe("".as_bytes())?; @@ -71,15 +76,18 @@ async fn setup_relay_concentratord(conf: &Configuration) -> Result<()> { cmd_sock.connect(&conf.backend.relay_concentratord.command_url)?; let mut b = Backend { - ctx: zmq_ctx, cmd_url: conf.backend.concentratord.command_url.clone(), cmd_sock: Mutex::new(cmd_sock), gateway_id: None, }; b.read_gateway_id()?; - tokio::spawn(async move { - relay_event_loop(event_sock).await; + tokio::spawn({ + let border_gateway = conf.relay.border_gateway; + + async move { + relay_event_loop(border_gateway, event_sock).await; + } }); RELAY_CONCENTRATORD @@ -90,10 +98,9 @@ async fn setup_relay_concentratord(conf: &Configuration) -> Result<()> { } struct Backend { - ctx: zmq::Context, cmd_url: String, cmd_sock: Mutex, - gateway_id: Option, + gateway_id: Option<[u8; 8]>, } impl Backend { @@ -110,14 +117,68 @@ impl Backend { if !items[0].is_readable() { return Err(anyhow!("Could not read gateway id")); } - let gateway_id = cmd_sock.recv_bytes(0)?; - self.gateway_id = Some(hex::encode(gateway_id)); + let mut gateway_id: [u8; 8] = [0; 8]; + gateway_id.copy_from_slice(&cmd_sock.recv_bytes(0)?); + self.gateway_id = Some(gateway_id); + + Ok(()) + } + + fn send_command(&self, cmd: &str, b: &[u8]) -> Result> { + let res = || -> Result> { + let cmd_sock = self.cmd_sock.lock().unwrap(); + cmd_sock.send(cmd, zmq::SNDMORE)?; + cmd_sock.send(b, 0)?; + + // set poller so that we can timeout after 100ms + let mut items = [cmd_sock.as_poll_item(zmq::POLLIN)]; + zmq::poll(&mut items, 100)?; + if !items[0].is_readable() { + return Err(anyhow!("Could not read down response")); + } + + // red tx ack response + let resp_b: &[u8] = &cmd_sock.recv_bytes(0)?; + Ok(resp_b.to_vec()) + }(); + + if res.is_err() { + loop { + // Reconnect the CMD socket in case we received an error. + // In case there was an issue with receiving data from the socket, it could mean + // it is in a 'dirty' state. E.g. due to the error we did not read the full + // response. + if let Err(e) = self.reconnect_cmd_sock() { + error!( + "Re-connecting to Concentratord command API error, error: {}", + e + ); + sleep(Duration::from_secs(1)); + continue; + } + + break; + } + } + + res + } + + fn reconnect_cmd_sock(&self) -> Result<()> { + warn!( + "Re-connecting to Concentratord command API, command_url: {}", + self.cmd_url + ); + let zmq_ctx = ZMQ_CONTEXT.lock().unwrap(); + let mut cmd_sock = self.cmd_sock.lock().unwrap(); + *cmd_sock = zmq_ctx.socket(zmq::REQ)?; + cmd_sock.connect(&self.cmd_url)?; Ok(()) } } -async fn event_loop(event_sock: zmq::Socket, filters: lrwn_filters::Filters) { +async fn event_loop(border_gateway: bool, event_sock: zmq::Socket, filters: lrwn_filters::Filters) { trace!("Starting event loop"); let event_sock = Arc::new(Mutex::new(event_sock)); @@ -133,10 +194,15 @@ async fn event_loop(event_sock: zmq::Socket, filters: lrwn_filters::Filters) { if event.len() != 2 { continue; } + + if let Err(err) = handle_event_msg(border_gateway, &event[0], &event[1], &filters).await { + error!("Handle event error: {}", err); + continue; + } } } -async fn relay_event_loop(event_sock: zmq::Socket) { +async fn relay_event_loop(border_gateway: bool, event_sock: zmq::Socket) { trace!("Starting relay event loop"); let event_sock = Arc::new(Mutex::new(event_sock)); @@ -152,7 +218,89 @@ async fn relay_event_loop(event_sock: zmq::Socket) { if event.len() != 2 { continue; } + + if let Err(err) = handle_relay_event_msg(border_gateway, &event[0], &event[1]).await { + error!("Handle relay event error: {}", err); + continue; + } + } +} + +async fn handle_event_msg( + border_gateway: bool, + event: &[u8], + pl: &[u8], + filters: &lrwn_filters::Filters, +) -> Result<()> { + let event = String::from_utf8(event.to_vec())?; + + match event.as_str() { + "up" => { + let pl = gw::UplinkFrame::decode(pl)?; + + 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(()); + } + + // Note that proprietary frames will always pass as these can't be + // filtered. + if !lrwn_filters::matches(&pl.phy_payload, filters) { + debug!( + "Discarding uplink because of dev_addr and join_eui filters, uplink_id: {}", + rx_info.uplink_id + ) + } + + relay::handle_uplink(border_gateway, pl).await?; + } + } + "stats" => { + let pl = gw::GatewayStats::decode(pl)?; + relay::handle_stats(border_gateway, pl).await?; + } + _ => { + return Ok(()); + } + } + + Ok(()) +} + +async fn handle_relay_event_msg(border_gateway: bool, event: &[u8], pl: &[u8]) -> Result<()> { + let event = String::from_utf8(event.to_vec())?; + + match event.as_str() { + "up" => { + let pl = gw::UplinkFrame::decode(pl)?; + + 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 relay event msg must always be a proprietary payload. + if pl.phy_payload.first().cloned().unwrap_or_default() & 0xe0 != 0 { + relay::handle_uplink(border_gateway, pl).await?; + } + } + _ => { + return Ok(()); + } } + + Ok(()) } async fn read_event(event_sock: Arc>) -> Result>> { @@ -176,3 +324,104 @@ async fn read_event(event_sock: Arc>) -> Result>> }) .await? } + +async fn send_command(cmd: &str, b: &[u8]) -> Result> { + task::spawn_blocking({ + let cmd = cmd.to_string(); + let b = b.to_vec(); + + move || -> Result> { + if let Some(backend) = CONCENTRATORD.get() { + return backend.send_command(&cmd, &b); + } + + Err(anyhow!("CONCENTRATORD is not set")) + } + }) + .await? +} + +async fn send_relay_command(cmd: &str, b: &[u8]) -> Result> { + task::spawn_blocking({ + let cmd = cmd.to_string(); + let b = b.to_vec(); + + move || -> Result> { + if let Some(backend) = RELAY_CONCENTRATORD.get() { + return backend.send_command(&cmd, &b); + } + + Err(anyhow!("RELAY_CONCENTRATORD is not set")) + } + }) + .await? +} + +pub async fn relay(pl: &gw::DownlinkFrame) -> Result<()> { + let tx_ack = { + let b = pl.encode_to_vec(); + let resp_b = send_relay_command("down", &b).await?; + gw::DownlinkTxAck::decode(resp_b.as_slice())? + }; + + let tx_ack_ok: Vec = tx_ack + .items + .iter() + .filter(|v| v.status() == gw::TxAckStatus::Ok) + .cloned() + .collect(); + + if !tx_ack_ok.is_empty() { + return Ok(()); + } + + Err(anyhow!( + "Relay failed: {}", + tx_ack + .items + .last() + .cloned() + .unwrap_or_default() + .status() + .as_str_name() + )) +} + +pub async fn send_downlink(pl: &gw::DownlinkFrame) -> Result<()> { + let tx_ack = { + let b = pl.encode_to_vec(); + let resp_b = send_command("down", &b).await?; + gw::DownlinkTxAck::decode(resp_b.as_slice())? + }; + + let tx_ack_ok: Vec = tx_ack + .items + .iter() + .filter(|v| v.status() == gw::TxAckStatus::Ok) + .cloned() + .collect(); + + if !tx_ack_ok.is_empty() { + return Ok(()); + } + + Err(anyhow!( + "Send downlink failed: {}", + tx_ack + .items + .last() + .cloned() + .unwrap_or_default() + .status() + .as_str_name() + )) +} + +pub fn get_relay_id() -> Result<[u8; 4]> { + if let Some(rc) = RELAY_CONCENTRATORD.get() { + let mut relay_id: [u8; 4] = [0; 4]; + relay_id.copy_from_slice(&rc.gateway_id.unwrap_or_default()[4..]) + } + + Err(anyhow!("RELAY_CONCENTRATORD is not (yet) initialized")) +} diff --git a/src/cmd/border_gateway.rs b/src/cmd/border_gateway.rs deleted file mode 100644 index 177d251..0000000 --- a/src/cmd/border_gateway.rs +++ /dev/null @@ -1,10 +0,0 @@ -use anyhow::Result; - -use crate::backend; -use crate::config::Configuration; - -pub async fn run(conf: &Configuration) -> Result<()> { - backend::setup(conf).await?; - - Ok(()) -} diff --git a/src/cmd/configfile.rs b/src/cmd/configfile.rs index d4d4ac0..4e44bf7 100644 --- a/src/cmd/configfile.rs +++ b/src/cmd/configfile.rs @@ -1,7 +1,7 @@ use crate::config; use handlebars::{no_escape, Handlebars}; -pub fn run(conf: &config::Configuration) { +pub fn run() { let template = r#" # Logging settings. [logging] @@ -26,6 +26,13 @@ pub fn run(conf: &config::Configuration) { # Relay configuration. [relay] + # Border Gateway. + # + # If this is set to true, then the ChirpStack Gateway Relay will consider + # this gateway as a Border Gateway, meaning that it will unwrap relayed + # uplinks and forward these to the proxy API, rather than relaying these. + border_gateway={{ relay.border_gateway }} + # Relay frequencies. # # The ChirpStack Gateway Relay will randomly use one of the configured @@ -36,7 +43,7 @@ pub fn run(conf: &config::Configuration) { {{/each}} ] - # Proxy API configuration (Border Gateway mode). + # Proxy API configuration. # # If the Gateway Relay is configured to operate as Border Gateway. It # will unwrap relayed uplink frames, and will wrap downlink payloads that @@ -82,11 +89,12 @@ pub fn run(conf: &config::Configuration) { command_url="{{ backend.relay_concentratord.command_url }}" "#; + let conf = config::get(); let mut reg = Handlebars::new(); reg.register_escape_fn(no_escape); println!( "{}", - reg.render_template(template, &conf) + reg.render_template(template, &(*conf)) .expect("Render configfile error") ); } diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 43c16ee..485ebf0 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,3 +1,2 @@ -pub mod border_gateway; pub mod configfile; pub mod root; diff --git a/src/cmd/root.rs b/src/cmd/root.rs index 177d251..b7019ca 100644 --- a/src/cmd/root.rs +++ b/src/cmd/root.rs @@ -1,9 +1,10 @@ use anyhow::Result; -use crate::backend; use crate::config::Configuration; +use crate::{backend, proxy}; pub async fn run(conf: &Configuration) -> Result<()> { + proxy::setup(conf)?; backend::setup(conf).await?; Ok(()) diff --git a/src/config.rs b/src/config.rs index a6e2365..6ff95fb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,12 @@ use std::fs; +use std::sync::{Arc, Mutex}; use anyhow::Result; -use serde::{Deserialize, Serialize}; +use once_cell::sync::OnceCell; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +static CONFIG: OnceCell>> = OnceCell::new(); #[derive(Serialize, Deserialize, Default)] #[serde(default)] @@ -9,17 +14,19 @@ pub struct Configuration { pub logging: Logging, pub relay: Relay, pub backend: Backend, - pub channels: Vec, + pub channels: Vec, pub data_rates: Vec, } impl Configuration { - pub fn get(filenames: &[String]) -> Result { + pub fn load(filenames: &[String]) -> Result<()> { let mut content = String::new(); for file_name in filenames { content.push_str(&fs::read_to_string(file_name)?); } - Ok(toml::from_str(&content)?) + + let conf: Configuration = toml::from_str(&content)?; + set(conf) } } @@ -44,8 +51,11 @@ impl Default for Logging { pub struct Relay { pub frequencies: Vec, pub data_rate: DataRate, + pub tx_power: i32, pub proxy_api: ProxyApi, pub filters: Filters, + pub border_gateway: bool, + pub max_hop_count: u8, } #[derive(Serialize, Deserialize, Default)] @@ -94,19 +104,103 @@ pub struct Filters { pub join_eui_prefixes: Vec, } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, PartialEq, Eq)] #[serde(default)] -pub struct Channel { - pub frequency: u32, - pub min_dr: u8, - pub max_dr: u8, +pub struct DataRate { + pub modulation: Modulation, + pub spreading_factor: u8, + pub bandwidth: u32, + pub code_rate: Option, + pub bitrate: u32, } -#[derive(Serialize, Deserialize, Default)] -#[serde(default)] -pub struct DataRate { - spreading_factor: u8, - bandwidth: u32, - coding_rate: String, - bitrate: u32, +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[allow(non_camel_case_types)] +#[allow(clippy::upper_case_acronyms)] +pub enum Modulation { + LORA, + FSK, +} + +impl Default for Modulation { + fn default() -> Self { + Modulation::LORA + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum CodeRate { + Cr45, + Cr46, + Cr47, + Cr48, + Cr38, + Cr26, + Cr14, + Cr16, + Cr56, + CrLi45, + CrLi46, + CrLi48, +} + +impl Serialize for CodeRate { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + CodeRate::Cr45 => serializer.serialize_str("4/5"), + CodeRate::Cr46 => serializer.serialize_str("4/6"), + CodeRate::Cr47 => serializer.serialize_str("4/7"), + CodeRate::Cr48 => serializer.serialize_str("4/8"), + CodeRate::Cr38 => serializer.serialize_str("3/8"), + CodeRate::Cr26 => serializer.serialize_str("2/6"), + CodeRate::Cr14 => serializer.serialize_str("1/4"), + CodeRate::Cr16 => serializer.serialize_str("1/6"), + CodeRate::Cr56 => serializer.serialize_str("5/6"), + CodeRate::CrLi45 => serializer.serialize_str("4/5LI"), + CodeRate::CrLi46 => serializer.serialize_str("4/6LI"), + CodeRate::CrLi48 => serializer.serialize_str("4/5LI"), + } + } +} + +impl<'de> Deserialize<'de> for CodeRate { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(match s.as_str() { + "4/5" => CodeRate::Cr45, + "4/6" | "2/3" => CodeRate::Cr46, + "4/7" => CodeRate::Cr47, + "4/8" | "2/4" | "1/2" => CodeRate::Cr48, + "3/8" => CodeRate::Cr38, + "2/6" | "1/3" => CodeRate::Cr26, + "1/4" => CodeRate::Cr14, + "1/6" => CodeRate::Cr16, + "5/6" => CodeRate::Cr56, + "4/5LI" => CodeRate::CrLi45, + "4/6LI" => CodeRate::CrLi46, + "4/8LI" => CodeRate::CrLi48, + _ => return Err(Error::custom(format!("Unexpected code_rate: {}", s))), + }) + } +} + +pub fn set(c: Configuration) -> Result<()> { + CONFIG + .set(Mutex::new(Arc::new(c))) + .map_err(|_| anyhow!("Set OnceCell error")) +} + +pub fn get() -> Arc { + let conf = CONFIG + .get() + .ok_or_else(|| anyhow!("OnceCell is not set")) + .unwrap(); + + conf.lock().unwrap().clone() } diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..6dda5ec --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,121 @@ +use anyhow::Result; + +use crate::config; +use chirpstack_api::gw; + +pub fn frequency_to_chan(freq: u32) -> Result { + let conf = config::get(); + for (i, f) in conf.channels.iter().enumerate() { + if freq == *f { + return Ok(i as u8); + } + } + + Err(anyhow!("Frequency {} does not map to a channel", freq)) +} + +pub fn chan_to_frequency(chan: u8) -> Result { + let conf = config::get(); + conf.channels + .get(chan as usize) + .cloned() + .ok_or_else(|| anyhow!("Channel {} does not map to a frequency", chan)) +} + +pub fn modulation_to_dr(modulation: &gw::Modulation) -> Result { + let mod_params = modulation + .parameters + .as_ref() + .ok_or_else(|| anyhow!("parameters must not be None"))?; + + let dr = match mod_params { + gw::modulation::Parameters::Lora(v) => config::DataRate { + modulation: config::Modulation::LORA, + bandwidth: v.bandwidth, + code_rate: Some(match v.code_rate() { + gw::CodeRate::Cr45 => config::CodeRate::Cr45, + gw::CodeRate::Cr46 => config::CodeRate::Cr46, + gw::CodeRate::Cr47 => config::CodeRate::Cr47, + gw::CodeRate::Cr48 => config::CodeRate::Cr48, + gw::CodeRate::Cr38 => config::CodeRate::Cr38, + gw::CodeRate::Cr26 => config::CodeRate::Cr26, + gw::CodeRate::Cr14 => config::CodeRate::Cr14, + gw::CodeRate::Cr16 => config::CodeRate::Cr16, + gw::CodeRate::Cr56 => config::CodeRate::Cr56, + gw::CodeRate::CrLi45 => config::CodeRate::CrLi45, + gw::CodeRate::CrLi46 => config::CodeRate::CrLi46, + gw::CodeRate::CrLi48 => config::CodeRate::CrLi48, + gw::CodeRate::CrUndefined => { + return Err(anyhow!("code_rate is CrUndefined")); + } + }), + spreading_factor: v.spreading_factor as u8, + ..Default::default() + }, + gw::modulation::Parameters::Fsk(v) => config::DataRate { + modulation: config::Modulation::FSK, + bitrate: v.datarate, + ..Default::default() + }, + gw::modulation::Parameters::LrFhss(_) => { + return Err(anyhow!("LR-FHSS is not supported")); + } + }; + + let conf = config::get(); + for (i, d) in conf.data_rates.iter().enumerate() { + if dr == *d { + return Ok(i as u8); + } + } + + Err(anyhow!( + "Modulation: {:?} does not map to a data-rate", + modulation + )) +} + +pub fn dr_to_modulation(dr: u8, ipol: bool) -> Result { + let conf = config::get(); + let dr = conf + .data_rates + .get(dr as usize) + .ok_or_else(|| anyhow!("Data-rate {} does not map to a modulation", dr))?; + + Ok(data_rate_to_gw_modulation(&dr, ipol)) +} + +pub fn data_rate_to_gw_modulation(dr: &config::DataRate, ipol: bool) -> gw::Modulation { + match dr.modulation { + config::Modulation::LORA => gw::Modulation { + parameters: Some(gw::modulation::Parameters::Lora(gw::LoraModulationInfo { + bandwidth: dr.bandwidth, + spreading_factor: dr.spreading_factor as u32, + code_rate: match dr.code_rate { + None => gw::CodeRate::CrUndefined, + Some(config::CodeRate::Cr45) => gw::CodeRate::Cr45, + Some(config::CodeRate::Cr46) => gw::CodeRate::Cr46, + Some(config::CodeRate::Cr47) => gw::CodeRate::Cr47, + Some(config::CodeRate::Cr48) => gw::CodeRate::Cr48, + Some(config::CodeRate::Cr38) => gw::CodeRate::Cr38, + Some(config::CodeRate::Cr26) => gw::CodeRate::Cr26, + Some(config::CodeRate::Cr14) => gw::CodeRate::Cr14, + Some(config::CodeRate::Cr16) => gw::CodeRate::Cr16, + Some(config::CodeRate::Cr56) => gw::CodeRate::Cr56, + Some(config::CodeRate::CrLi45) => gw::CodeRate::CrLi45, + Some(config::CodeRate::CrLi46) => gw::CodeRate::CrLi46, + Some(config::CodeRate::CrLi48) => gw::CodeRate::CrLi48, + } + .into(), + polarization_inversion: ipol, + ..Default::default() + })), + }, + config::Modulation::FSK => gw::Modulation { + parameters: Some(gw::modulation::Parameters::Fsk(gw::FskModulationInfo { + frequency_deviation: dr.bitrate / 2, + datarate: dr.bitrate, + })), + }, + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..009b3f8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,17 @@ +#[macro_use] +extern crate anyhow; + +use std::sync::Mutex; + +use once_cell::sync::Lazy; + +pub mod backend; +pub mod cmd; +pub mod config; +pub mod helpers; +pub mod logging; +pub mod packets; +pub mod proxy; +pub mod relay; + +pub static ZMQ_CONTEXT: Lazy> = Lazy::new(|| Mutex::new(zmq::Context::new())); diff --git a/src/main.rs b/src/main.rs index cfcb823..85cc2d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,3 @@ -#[macro_use] -extern crate anyhow; - use std::thread::sleep; use std::time::Duration; use std::{process, str::FromStr}; @@ -8,11 +5,7 @@ use std::{process, str::FromStr}; use clap::{Parser, Subcommand}; use log::info; -mod backend; -mod cmd; -mod config; -mod logging; -mod packets; +use chirpstack_gateway_relay::{cmd, config, logging}; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -28,20 +21,19 @@ struct Cli { enum Commands { /// Print the configuration template Configfile {}, - /// Operate the Relay as Border Gateway - BorderGateway {}, } #[tokio::main] async fn main() { let cli = Cli::parse(); - let conf = config::Configuration::get(&cli.config).expect("Read configuration error"); + config::Configuration::load(&cli.config).expect("Read configuration error"); if let Some(Commands::Configfile {}) = &cli.command { - cmd::configfile::run(&conf); + cmd::configfile::run(); process::exit(0); } + let conf = config::get(); let log_level = log::Level::from_str(&conf.logging.level).expect("Parse log_level error"); // Loop until success, as this will fail when syslog hasn't been fully started. @@ -54,23 +46,13 @@ async fn main() { sleep(Duration::from_secs(1)) } - let border_gateway = if let Some(Commands::BorderGateway {}) = &cli.command { - true - } else { - false - }; - info!( "Starting {} (border_gateway: {},version: {}, docs: {})", env!("CARGO_PKG_DESCRIPTION"), - border_gateway, + conf.relay.border_gateway, env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_HOMEPAGE"), ); - if border_gateway { - cmd::border_gateway::run(&conf).await.unwrap(); - } else { - cmd::root::run(&conf).await.unwrap(); - } + cmd::root::run(&conf).await.unwrap(); } diff --git a/src/packets.rs b/src/packets.rs index 866bb92..3c8acf5 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use chirpstack_api::gw; #[derive(Debug, PartialEq, Eq)] pub enum Packet { @@ -29,7 +28,7 @@ impl Packet { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct RelayPacket { pub mhdr: MHDR, pub payload: Payload, @@ -62,7 +61,7 @@ impl RelayPacket { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct MHDR { pub payload_type: PayloadType, pub hop_count: u8, @@ -89,7 +88,7 @@ impl MHDR { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum PayloadType { Uplink, Downlink, @@ -112,16 +111,16 @@ impl PayloadType { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum Payload { Uplink(UplinkPayload), Downlink(DownlinkPayload), } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct UplinkPayload { pub metadata: UplinkMetadata, - pub relay_gateway_id: [u8; 4], + pub relay_id: [u8; 4], pub phy_payload: Vec, } @@ -138,20 +137,20 @@ impl UplinkPayload { Ok(UplinkPayload { metadata: UplinkMetadata::from_bytes(md), - relay_gateway_id: gw_id, + relay_id: gw_id, phy_payload: b[9..].to_vec(), }) } pub fn to_vec(&self) -> Result> { let mut b = self.metadata.to_bytes()?.to_vec(); - b.extend_from_slice(&self.relay_gateway_id); + b.extend_from_slice(&self.relay_id); b.extend_from_slice(&self.phy_payload); Ok(b) } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct UplinkMetadata { pub uplink_id: u16, pub dr: u8, @@ -218,10 +217,10 @@ impl UplinkMetadata { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct DownlinkPayload { pub metadata: DownlinkMetadata, - pub relay_gateway_id: [u8; 4], + pub relay_id: [u8; 4], pub phy_payload: Vec, } @@ -238,20 +237,20 @@ impl DownlinkPayload { Ok(DownlinkPayload { metadata: DownlinkMetadata::from_bytes(md), - relay_gateway_id: gw_id, + relay_id: gw_id, phy_payload: b[10..].to_vec(), }) } pub fn to_vec(&self) -> Result> { let mut b = self.metadata.to_bytes()?.to_vec(); - b.extend_from_slice(&self.relay_gateway_id); + b.extend_from_slice(&self.relay_id); b.extend_from_slice(&self.phy_payload); Ok(b) } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct DownlinkMetadata { pub uplink_id: u16, pub dr: u8, @@ -590,7 +589,7 @@ mod test { snr: -12, channel: 64, }, - relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + relay_id: [0x01, 0x02, 0x03, 0x04], phy_payload: vec![0x05], }, up_pl, @@ -607,7 +606,7 @@ mod test { snr: -12, channel: 64, }, - relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + relay_id: [0x01, 0x02, 0x03, 0x04], phy_payload: vec![0x05], }; let b = up_pl.to_vec().unwrap(); @@ -736,7 +735,7 @@ mod test { frequency: 868100000, delay: 16, }, - relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + relay_id: [0x01, 0x02, 0x03, 0x04], phy_payload: vec![0x05], }, dn_pl, @@ -752,7 +751,7 @@ mod test { frequency: 868100000, delay: 16, }, - relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + relay_id: [0x01, 0x02, 0x03, 0x04], phy_payload: vec![0x05], }; let b = dn_pl.to_vec().unwrap(); @@ -789,7 +788,7 @@ mod test { snr: -12, channel: 64, }, - relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + relay_id: [0x01, 0x02, 0x03, 0x04], phy_payload: vec![0x05], }), }, @@ -811,7 +810,7 @@ mod test { frequency: 868100000, delay: 16, }, - relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + relay_id: [0x01, 0x02, 0x03, 0x04], phy_payload: vec![0x05], }), }, @@ -852,7 +851,7 @@ mod test { snr: -12, channel: 64, }, - relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + relay_id: [0x01, 0x02, 0x03, 0x04], phy_payload: vec![0x05], }), }, @@ -874,7 +873,7 @@ mod test { frequency: 868100000, delay: 16, }, - relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + relay_id: [0x01, 0x02, 0x03, 0x04], phy_payload: vec![0x05], }), }, @@ -915,7 +914,7 @@ mod test { snr: -12, channel: 64, }, - relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + relay_id: [0x01, 0x02, 0x03, 0x04], phy_payload: vec![0x05], }), }), @@ -961,7 +960,7 @@ mod test { snr: -12, channel: 64, }, - relay_gateway_id: [0x01, 0x02, 0x03, 0x04], + relay_id: [0x01, 0x02, 0x03, 0x04], phy_payload: vec![0x05], }), }), diff --git a/src/proxy.rs b/src/proxy.rs new file mode 100644 index 0000000..d9c566b --- /dev/null +++ b/src/proxy.rs @@ -0,0 +1,86 @@ +use std::sync::Mutex; + +use anyhow::Result; +use chirpstack_api::gw; +use chirpstack_api::prost::Message; +use log::{info, warn}; +use once_cell::sync::OnceCell; +use tokio::task; + +use crate::config::Configuration; +use crate::ZMQ_CONTEXT; + +static EVENT_SOCKET: OnceCell> = OnceCell::new(); +static COMMAND_SOCKET: OnceCell> = OnceCell::new(); + +pub fn setup(conf: &Configuration) -> Result<()> { + if !conf.relay.border_gateway { + return Ok(()); + } + + info!( + "Setting up Concentratord proxy API, event_bind: {}, command_bind: {}", + conf.relay.proxy_api.event_bind, conf.relay.proxy_api.command_bind + ); + + let zmq_ctx = ZMQ_CONTEXT.lock().unwrap(); + let sock = zmq_ctx.socket(zmq::PUB)?; + sock.bind(&conf.relay.proxy_api.event_bind)?; + EVENT_SOCKET + .set(Mutex::new(sock)) + .map_err(|_| anyhow!("OnceCell set error"))?; + + let sock = zmq_ctx.socket(zmq::REP)?; + sock.bind(&conf.relay.proxy_api.command_bind)?; + COMMAND_SOCKET + .set(Mutex::new(sock)) + .map_err(|_| anyhow!("OnceCell set error"))?; + + Ok(()) +} + +pub async fn send_uplink(pl: &gw::UplinkFrame) -> Result<()> { + task::spawn_blocking({ + let b = pl.encode_to_vec(); + + move || -> Result<()> { + let event_sock = match EVENT_SOCKET.get() { + Some(v) => v, + None => { + warn!("Proxy API is not (yet) initialized"); + return Ok(()); + } + }; + + let sock = event_sock.lock().unwrap(); + sock.send("up", zmq::SNDMORE).unwrap(); + sock.send(b, 0).unwrap(); + + Ok(()) + } + }) + .await? +} + +pub async fn send_stats(pl: &gw::GatewayStats) -> Result<()> { + task::spawn_blocking({ + let b = pl.encode_to_vec(); + + move || -> Result<()> { + let event_sock = match EVENT_SOCKET.get() { + Some(v) => v, + None => { + warn!("Proxy API is not (yet) initialized"); + return Ok(()); + } + }; + + let sock = event_sock.lock().unwrap(); + sock.send("stats", zmq::SNDMORE).unwrap(); + sock.send(b, 0).unwrap(); + + Ok(()) + } + }) + .await? +} diff --git a/src/relay.rs b/src/relay.rs new file mode 100644 index 0000000..fa760b3 --- /dev/null +++ b/src/relay.rs @@ -0,0 +1,216 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use anyhow::Result; +use chirpstack_api::gw; +use once_cell::sync::Lazy; +use rand::random; + +use crate::{ + backend, + config::{self, Configuration}, + helpers, + packets::{Packet, Payload, PayloadType, RelayPacket, UplinkMetadata, UplinkPayload, MHDR}, + proxy, +}; + +static RELAY_CHANNEL: Mutex = Mutex::new(0); +static UPLINK_ID: Mutex = Mutex::new(0); +static UPLINK_CONTEXT: Lazy>>> = + Lazy::new(|| Mutex::new(HashMap::new())); + +pub async fn handle_uplink(border_gateway: bool, pl: gw::UplinkFrame) -> Result<()> { + let packet = Packet::from_slice(&pl.phy_payload)?; + + match packet { + Packet::Relay(v) => match border_gateway { + true => proxy_uplink_relay_packet(&pl, v).await?, + false => relay_relay_packet(&pl, v).await?, + }, + Packet::Lora(_) => match border_gateway { + true => proxy_uplink_lora_packet(&pl).await?, + false => relay_uplink_lora_packet(&pl).await?, + }, + } + + Ok(()) +} + +pub async fn handle_stats(border_gateway: bool, pl: gw::GatewayStats) -> Result<()> { + if !border_gateway { + return Ok(()); + } + proxy::send_stats(&pl).await +} + +async fn proxy_uplink_lora_packet(pl: &gw::UplinkFrame) -> Result<()> { + proxy::send_uplink(pl).await +} + +async fn proxy_uplink_relay_packet(pl: &gw::UplinkFrame, packet: RelayPacket) -> Result<()> { + let relay_pl = match &packet.payload { + Payload::Uplink(v) => v, + _ => { + return Err(anyhow!("Expected Uplink payload")); + } + }; + + let mut pl = pl.clone(); + + if let Some(rx_info) = &mut pl.rx_info { + // Set metadata. + rx_info + .metadata + .insert("hop_count".to_string(), packet.mhdr.hop_count.to_string()); + rx_info + .metadata + .insert("relay_id".to_string(), hex::encode(&relay_pl.relay_id)); + + // Set RSSI and SNR. + rx_info.snr = relay_pl.metadata.snr.into(); + rx_info.rssi = relay_pl.metadata.rssi.into(); + + // Set context. + rx_info.context = relay_pl.metadata.uplink_id.to_be_bytes().to_vec(); + } + + // Set TxInfo. + if let Some(tx_info) = &mut pl.tx_info { + tx_info.frequency = helpers::chan_to_frequency(relay_pl.metadata.channel)?; + tx_info.modulation = Some(helpers::dr_to_modulation(relay_pl.metadata.dr, false)?); + } + + // Set original PHYPayload. + pl.phy_payload = relay_pl.phy_payload.clone(); + + proxy::send_uplink(&pl).await +} + +async fn relay_relay_packet(_: &gw::UplinkFrame, mut packet: RelayPacket) -> Result<()> { + let conf = config::get(); + + // Increment hop count. + packet.mhdr.hop_count += 1; + + if packet.mhdr.hop_count > conf.relay.max_hop_count { + return Err(anyhow!("Max hop count exceeded")); + } + + let pl = gw::DownlinkFrame { + downlink_id: random(), + items: vec![gw::DownlinkFrameItem { + phy_payload: packet.to_vec()?, + tx_info: Some(gw::DownlinkTxInfo { + frequency: get_relay_frequency(&conf)?, + modulation: Some(helpers::data_rate_to_gw_modulation( + &conf.relay.data_rate, + true, + )), + power: conf.relay.tx_power, + timing: Some(gw::Timing { + parameters: Some(gw::timing::Parameters::Immediately( + gw::ImmediatelyTimingInfo {}, + )), + }), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + backend::relay(&pl).await +} + +async fn relay_uplink_lora_packet(pl: &gw::UplinkFrame) -> Result<()> { + let conf = config::get(); + + let rx_info = pl + .rx_info + .as_ref() + .ok_or_else(|| anyhow!("rx_info is None"))?; + let tx_info = pl + .tx_info + .as_ref() + .ok_or_else(|| anyhow!("tx_info is None"))?; + let modulation = tx_info + .modulation + .as_ref() + .ok_or_else(|| anyhow!("modulation is None"))?; + + let packet = RelayPacket { + mhdr: MHDR { + payload_type: PayloadType::Uplink, + hop_count: 0, + }, + payload: Payload::Uplink(UplinkPayload { + metadata: UplinkMetadata { + uplink_id: store_uplink_context(&rx_info.context), + dr: helpers::modulation_to_dr(&modulation)?, + channel: helpers::frequency_to_chan(tx_info.frequency)?, + rssi: rx_info.rssi as i16, + snr: rx_info.snr as i8, + }, + relay_id: backend::get_relay_id()?, + phy_payload: pl.phy_payload.clone(), + }), + }; + + let pl = gw::DownlinkFrame { + downlink_id: random(), + items: vec![gw::DownlinkFrameItem { + phy_payload: packet.to_vec()?, + tx_info: Some(gw::DownlinkTxInfo { + frequency: get_relay_frequency(&conf)?, + power: conf.relay.tx_power, + modulation: Some(helpers::data_rate_to_gw_modulation( + &conf.relay.data_rate, + true, + )), + timing: Some(gw::Timing { + parameters: Some(gw::timing::Parameters::Immediately( + gw::ImmediatelyTimingInfo {}, + )), + }), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + backend::relay(&pl).await +} + +fn get_relay_frequency(conf: &Configuration) -> Result { + if conf.relay.frequencies.is_empty() { + return Err(anyhow!("No relay frequencies are configured")); + } + + let mut relay_channel = RELAY_CHANNEL.lock().unwrap(); + *relay_channel += 1; + + if *relay_channel >= conf.relay.frequencies.len() { + *relay_channel = 0; + } + + Ok(conf.relay.frequencies[*relay_channel]) +} + +fn get_uplink_id() -> u16 { + let mut uplink_id = UPLINK_ID.lock().unwrap(); + *uplink_id += 1; + + if *uplink_id > 4095 { + *uplink_id = 0; + } + + *uplink_id +} + +fn store_uplink_context(ctx: &[u8]) -> u16 { + let uplink_id = get_uplink_id(); + let mut uplink_ctx = UPLINK_CONTEXT.lock().unwrap(); + uplink_ctx.insert(uplink_id, ctx.to_vec()); + uplink_id +} From f4b2a04bb59674821a09d9f367f482ec2aa37246 Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Fri, 19 Jan 2024 12:19:40 +0000 Subject: [PATCH 3/3] WIP first test. --- Cargo.lock | 163 +++++++++++++++++++ Cargo.toml | 14 +- src/backend.rs | 171 +++++++++++++------- src/cmd/configfile.rs | 24 ++- src/cmd/root.rs | 9 ++ src/config.rs | 13 +- src/helpers.rs | 2 +- src/lib.rs | 5 +- src/packets.rs | 4 +- src/proxy.rs | 172 +++++++++++++++++--- src/relay.rs | 188 +++++++++++++++++++--- tests/border_gateway_test.rs | 301 +++++++++++++++++++++++++++++++++++ tests/relay_gateway_test.rs | 8 + 13 files changed, 952 insertions(+), 122 deletions(-) create mode 100644 tests/border_gateway_test.rs create mode 100644 tests/relay_gateway_test.rs diff --git a/Cargo.lock b/Cargo.lock index afed6a0..f046821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,7 @@ dependencies = [ "anyhow", "chirpstack_api", "clap", + "futures", "handlebars", "hex", "log", @@ -126,6 +127,8 @@ dependencies = [ "once_cell", "rand", "serde", + "signal-hook", + "signal-hook-tokio", "simple_logger", "syslog", "tokio", @@ -353,6 +356,95 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -547,6 +639,17 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "multimap" version = "0.8.3" @@ -660,6 +763,12 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.27" @@ -933,6 +1042,37 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signal-hook-tokio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e" +dependencies = [ + "futures-core", + "libc", + "signal-hook", + "tokio", +] + [[package]] name = "simple_logger" version = "4.3.0" @@ -945,12 +1085,31 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "syn" version = "2.0.41" @@ -1065,9 +1224,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", + "libc", + "mio", "num_cpus", "pin-project-lite", + "socket2", "tokio-macros", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1549963..7c4e52e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,17 @@ zmq = { version = "0.10" } anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } - tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] } + tokio = { version = "1.35", features = ["macros", "rt-multi-thread", "time"] } once_cell = "1.19" hex = "0.4.3" - rand = "0.8" \ No newline at end of file + rand = "0.8" + signal-hook = "0.3" + signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } + futures = "0.3" + +[profile.release] + strip = true + opt-level = "z" + lto = true + codegen-units = 1 + panic = "abort" diff --git a/src/backend.rs b/src/backend.rs index 306d89b..7c3c18b 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -9,7 +9,7 @@ use once_cell::sync::OnceCell; use tokio::task; use crate::config::Configuration; -use crate::{relay, ZMQ_CONTEXT}; +use crate::{proxy, relay}; use chirpstack_api::gw; static CONCENTRATORD: OnceCell = OnceCell::new(); @@ -27,7 +27,7 @@ async fn setup_concentratord(conf: &Configuration) -> Result<()> { conf.backend.concentratord.event_url, conf.backend.concentratord.command_url ); - let zmq_ctx = ZMQ_CONTEXT.lock().unwrap(); + let zmq_ctx = zmq::Context::new(); let event_sock = zmq_ctx.socket(zmq::SUB)?; event_sock.connect(&conf.backend.concentratord.event_url)?; event_sock.set_subscribe("".as_bytes())?; @@ -37,10 +37,10 @@ async fn setup_concentratord(conf: &Configuration) -> Result<()> { let mut b = Backend { cmd_url: conf.backend.concentratord.command_url.clone(), - cmd_sock: Mutex::new(cmd_sock), + cmd_sock: Arc::new(Mutex::new(cmd_sock)), gateway_id: None, }; - b.read_gateway_id()?; + b.read_gateway_id().await?; tokio::spawn({ let border_gateway = conf.relay.border_gateway; @@ -67,7 +67,7 @@ async fn setup_relay_concentratord(conf: &Configuration) -> Result<()> { conf.backend.relay_concentratord.event_url, conf.backend.relay_concentratord.command_url ); - let zmq_ctx = ZMQ_CONTEXT.lock().unwrap(); + let zmq_ctx = zmq::Context::new(); let event_sock = zmq_ctx.socket(zmq::SUB)?; event_sock.connect(&conf.backend.relay_concentratord.event_url)?; event_sock.set_subscribe("".as_bytes())?; @@ -77,10 +77,10 @@ async fn setup_relay_concentratord(conf: &Configuration) -> Result<()> { let mut b = Backend { cmd_url: conf.backend.concentratord.command_url.clone(), - cmd_sock: Mutex::new(cmd_sock), + cmd_sock: Arc::new(Mutex::new(cmd_sock)), gateway_id: None, }; - b.read_gateway_id()?; + b.read_gateway_id().await?; tokio::spawn({ let border_gateway = conf.relay.border_gateway; @@ -99,33 +99,48 @@ async fn setup_relay_concentratord(conf: &Configuration) -> Result<()> { struct Backend { cmd_url: String, - cmd_sock: Mutex, + cmd_sock: Arc>, gateway_id: Option<[u8; 8]>, } impl Backend { - fn read_gateway_id(&mut self) -> Result<()> { - let cmd_sock = self.cmd_sock.lock().unwrap(); - - // send 'gateway_id' command with empty payload. - cmd_sock.send("gateway_id", zmq::SNDMORE)?; - cmd_sock.send("", 0)?; - - // set poller so that we can timeout after 100ms - let mut items = [cmd_sock.as_poll_item(zmq::POLLIN)]; - zmq::poll(&mut items, 100)?; - if !items[0].is_readable() { - return Err(anyhow!("Could not read gateway id")); - } + async fn read_gateway_id(&mut self) -> Result<()> { + trace!("Reading gateway ID"); + + let gateway_id = task::spawn_blocking({ + let cmd_sock = self.cmd_sock.clone(); + + move || -> Result<[u8; 8]> { + let cmd_sock = cmd_sock.lock().unwrap(); + + // send 'gateway_id' command with empty payload. + cmd_sock.send("gateway_id", zmq::SNDMORE)?; + cmd_sock.send("", 0)?; + + // set poller so that we can timeout after 100ms + let mut items = [cmd_sock.as_poll_item(zmq::POLLIN)]; + zmq::poll(&mut items, 100)?; + if !items[0].is_readable() { + return Err(anyhow!("Could not read gateway id")); + } + + let mut gateway_id: [u8; 8] = [0; 8]; + gateway_id.copy_from_slice(&cmd_sock.recv_bytes(0)?); + info!("Retrieved gateway_id: {}", hex::encode(gateway_id)); + + Ok(gateway_id) + } + }) + .await??; - let mut gateway_id: [u8; 8] = [0; 8]; - gateway_id.copy_from_slice(&cmd_sock.recv_bytes(0)?); self.gateway_id = Some(gateway_id); Ok(()) } fn send_command(&self, cmd: &str, b: &[u8]) -> Result> { + trace!("Sending command, cmd: {}, bytes: {}", cmd, hex::encode(b)); + let res = || -> Result> { let cmd_sock = self.cmd_sock.lock().unwrap(); cmd_sock.send(cmd, zmq::SNDMORE)?; @@ -170,7 +185,7 @@ impl Backend { "Re-connecting to Concentratord command API, command_url: {}", self.cmd_url ); - let zmq_ctx = ZMQ_CONTEXT.lock().unwrap(); + let zmq_ctx = zmq::Context::new(); let mut cmd_sock = self.cmd_sock.lock().unwrap(); *cmd_sock = zmq_ctx.socket(zmq::REQ)?; cmd_sock.connect(&self.cmd_url)?; @@ -234,6 +249,12 @@ async fn handle_event_msg( ) -> Result<()> { let event = String::from_utf8(event.to_vec())?; + trace!( + "Handling event, event: {}, data: {}", + event, + hex::encode(pl) + ); + match event.as_str() { "up" => { let pl = gw::UplinkFrame::decode(pl)?; @@ -248,8 +269,16 @@ async fn handle_event_msg( return Ok(()); } - // Note that proprietary frames will always pass as these can't be - // filtered. + // Filter out proprietary payloads. + if pl.phy_payload.first().cloned().unwrap_or_default() & 0xe0 != 0 { + debug!( + "Discarding proprietary uplink, uplink_id: {}", + rx_info.uplink_id + ); + return Ok(()); + } + + // Filter uplinks based on DevAddr and JoinEUI filters. if !lrwn_filters::matches(&pl.phy_payload, filters) { debug!( "Discarding uplink because of dev_addr and join_eui filters, uplink_id: {}", @@ -261,8 +290,10 @@ async fn handle_event_msg( } } "stats" => { - let pl = gw::GatewayStats::decode(pl)?; - relay::handle_stats(border_gateway, pl).await?; + if border_gateway { + let pl = gw::GatewayStats::decode(pl)?; + proxy::send_stats(&pl).await?; + } } _ => { return Ok(()); @@ -275,6 +306,12 @@ async fn handle_event_msg( async fn handle_relay_event_msg(border_gateway: bool, event: &[u8], pl: &[u8]) -> Result<()> { let event = String::from_utf8(event.to_vec())?; + trace!( + "Handling relay event, event: {}, data: {}", + event, + hex::encode(pl) + ); + match event.as_str() { "up" => { let pl = gw::UplinkFrame::decode(pl)?; @@ -292,7 +329,7 @@ async fn handle_relay_event_msg(border_gateway: bool, event: &[u8], pl: &[u8]) - // The relay event msg must always be a proprietary payload. if pl.phy_payload.first().cloned().unwrap_or_default() & 0xe0 != 0 { - relay::handle_uplink(border_gateway, pl).await?; + relay::handle_relay(border_gateway, pl).await?; } } _ => { @@ -326,6 +363,12 @@ async fn read_event(event_sock: Arc>) -> Result>> } async fn send_command(cmd: &str, b: &[u8]) -> Result> { + trace!( + "Sending command, command: {}, data: {}", + cmd, + hex::encode(b) + ); + task::spawn_blocking({ let cmd = cmd.to_string(); let b = b.to_vec(); @@ -342,6 +385,12 @@ async fn send_command(cmd: &str, b: &[u8]) -> Result> { } async fn send_relay_command(cmd: &str, b: &[u8]) -> Result> { + trace!( + "Sending relay command, command: {}, data: {}", + cmd, + hex::encode(b) + ); + task::spawn_blocking({ let cmd = cmd.to_string(); let b = b.to_vec(); @@ -358,6 +407,8 @@ async fn send_relay_command(cmd: &str, b: &[u8]) -> Result> { } pub async fn relay(pl: &gw::DownlinkFrame) -> Result<()> { + info!("Sending relay payload, downlink_id: {}", pl.downlink_id); + let tx_ack = { let b = pl.encode_to_vec(); let resp_b = send_relay_command("down", &b).await?; @@ -372,9 +423,15 @@ pub async fn relay(pl: &gw::DownlinkFrame) -> Result<()> { .collect(); if !tx_ack_ok.is_empty() { + info!("Enqueue acknowledged, downlink_id: {}", pl.downlink_id); return Ok(()); } + warn!( + "Enqueue not acknowledged, downlink_id: {}, acks: {:?}", + pl.downlink_id, tx_ack.items + ); + Err(anyhow!( "Relay failed: {}", tx_ack @@ -387,41 +444,45 @@ pub async fn relay(pl: &gw::DownlinkFrame) -> Result<()> { )) } -pub async fn send_downlink(pl: &gw::DownlinkFrame) -> Result<()> { - let tx_ack = { - let b = pl.encode_to_vec(); - let resp_b = send_command("down", &b).await?; - gw::DownlinkTxAck::decode(resp_b.as_slice())? - }; +pub async fn send_downlink(pl: &gw::DownlinkFrame) -> Result { + info!("Sending downlink, downlink_id: {}", pl.downlink_id); - let tx_ack_ok: Vec = tx_ack - .items - .iter() - .filter(|v| v.status() == gw::TxAckStatus::Ok) - .cloned() - .collect(); + let b = pl.encode_to_vec(); + let resp_b = send_command("down", &b).await?; + let tx_ack = gw::DownlinkTxAck::decode(resp_b.as_slice())?; - if !tx_ack_ok.is_empty() { - return Ok(()); - } + Ok(tx_ack) +} - Err(anyhow!( - "Send downlink failed: {}", - tx_ack - .items - .last() - .cloned() - .unwrap_or_default() - .status() - .as_str_name() - )) +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?; + + Ok(()) } pub fn get_relay_id() -> Result<[u8; 4]> { + trace!("Getting relay ID"); + if let Some(rc) = RELAY_CONCENTRATORD.get() { let mut relay_id: [u8; 4] = [0; 4]; - relay_id.copy_from_slice(&rc.gateway_id.unwrap_or_default()[4..]) + relay_id.copy_from_slice(&rc.gateway_id.unwrap_or_default()[4..]); + return Ok(relay_id); } Err(anyhow!("RELAY_CONCENTRATORD is not (yet) initialized")) } + +pub fn get_gateway_id() -> Result<[u8; 8]> { + trace!("Getting gateway ID"); + + if let Some(c) = CONCENTRATORD.get() { + let mut gateway_id: [u8; 8] = [0; 8]; + gateway_id.copy_from_slice(&c.gateway_id.unwrap_or_default()); + return Ok(gateway_id); + } + + Err(anyhow!("CONCENTRATORD is not (yet) initialized")) +} diff --git a/src/cmd/configfile.rs b/src/cmd/configfile.rs index 4e44bf7..eaafb2f 100644 --- a/src/cmd/configfile.rs +++ b/src/cmd/configfile.rs @@ -15,7 +15,7 @@ pub fn run() { # * WARN # * ERROR # * OFF - log_level="INFO" + level="INFO" # Log to syslog. # @@ -43,6 +43,28 @@ pub fn run() { {{/each}} ] + # Tx-power (EIRP). + tx_power={{ relay.tx_power }} + + # Data-rate properties. + [relay.data_rate] + + # Modulation. + modulation="{{ relay.data_rate.modulation }}" + + # Spreading-factor. + spreading_factor={{ relay.data_rate.spreading_factor }} + + # Bandwidth. + bandwidth={{ relay.data_rate.bandwidth }} + + # Code-rate. + code_rate="{{ relay.data_rate.code_rate }}" + + # Bitrate. + bitrate={{ relay_data_rate.bitrate }} + + # Proxy API configuration. # # If the Gateway Relay is configured to operate as Border Gateway. It diff --git a/src/cmd/root.rs b/src/cmd/root.rs index b7019ca..94ef6d3 100644 --- a/src/cmd/root.rs +++ b/src/cmd/root.rs @@ -1,4 +1,7 @@ use anyhow::Result; +use futures::stream::StreamExt; +use signal_hook::consts::signal::*; +use signal_hook_tokio::Signals; use crate::config::Configuration; use crate::{backend, proxy}; @@ -7,5 +10,11 @@ pub async fn run(conf: &Configuration) -> Result<()> { proxy::setup(conf)?; backend::setup(conf).await?; + let mut signals = Signals::new(&[SIGINT, SIGTERM])?; + let handle = signals.handle(); + + let _ = signals.next().await; + handle.close(); + Ok(()) } diff --git a/src/config.rs b/src/config.rs index 6ff95fb..eea4318 100644 --- a/src/config.rs +++ b/src/config.rs @@ -91,8 +91,8 @@ pub struct ProxyApi { impl Default for ProxyApi { fn default() -> Self { ProxyApi { - event_bind: "ipc:///tmp/chirpstack_gateway_relay_event".into(), - command_bind: "ipc:///tmp/chirpstack_gateway_relay_command".into(), + event_bind: "ipc:///tmp/gateway_relay_event".into(), + command_bind: "ipc:///tmp/gateway_relay_command".into(), } } } @@ -114,20 +114,15 @@ pub struct DataRate { pub bitrate: u32, } -#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)] #[allow(non_camel_case_types)] #[allow(clippy::upper_case_acronyms)] pub enum Modulation { + #[default] LORA, FSK, } -impl Default for Modulation { - fn default() -> Self { - Modulation::LORA - } -} - #[derive(Clone, Copy, PartialEq, Eq)] pub enum CodeRate { Cr45, diff --git a/src/helpers.rs b/src/helpers.rs index 6dda5ec..4cf9f9e 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -82,7 +82,7 @@ pub fn dr_to_modulation(dr: u8, ipol: bool) -> Result { .get(dr as usize) .ok_or_else(|| anyhow!("Data-rate {} does not map to a modulation", dr))?; - Ok(data_rate_to_gw_modulation(&dr, ipol)) + Ok(data_rate_to_gw_modulation(dr, ipol)) } pub fn data_rate_to_gw_modulation(dr: &config::DataRate, ipol: bool) -> gw::Modulation { diff --git a/src/lib.rs b/src/lib.rs index 009b3f8..e6f31dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #[macro_use] extern crate anyhow; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use once_cell::sync::Lazy; @@ -14,4 +14,5 @@ pub mod packets; pub mod proxy; pub mod relay; -pub static ZMQ_CONTEXT: Lazy> = Lazy::new(|| Mutex::new(zmq::Context::new())); +// pub static ZMQ_CONTEXT: Lazy>> = +// Lazy::new(|| Arc::new(Mutex::new(zmq::Context::new()))); diff --git a/src/packets.rs b/src/packets.rs index 3c8acf5..b577728 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -171,7 +171,7 @@ impl UplinkMetadata { UplinkMetadata { uplink_id: u16::from_be_bytes([b[0], b[1]]) >> 4, dr: b[1] & 0x0f, - rssi: -1 * (b[2] as i16), + rssi: -(b[2] as i16), snr, channel: b[4], } @@ -206,7 +206,7 @@ impl UplinkMetadata { Ok([ uplink_id_b[0], uplink_id_b[1] | self.dr, - (-1 * self.rssi) as u8, + -self.rssi as u8, if self.snr < 0 { (self.snr + 64) as u8 } else { diff --git a/src/proxy.rs b/src/proxy.rs index d9c566b..0509960 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,17 +1,25 @@ -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use anyhow::Result; use chirpstack_api::gw; use chirpstack_api::prost::Message; -use log::{info, warn}; +use log::{error, info, trace, warn}; use once_cell::sync::OnceCell; use tokio::task; +use crate::backend; use crate::config::Configuration; -use crate::ZMQ_CONTEXT; +use crate::relay; static EVENT_SOCKET: OnceCell> = OnceCell::new(); -static COMMAND_SOCKET: OnceCell> = OnceCell::new(); + +pub enum Command { + Timeout, + Unknown(String, Vec), + Downlink(gw::DownlinkFrame), + GatewayID, + Configuration(gw::GatewayConfiguration), +} pub fn setup(conf: &Configuration) -> Result<()> { if !conf.relay.border_gateway { @@ -23,7 +31,7 @@ pub fn setup(conf: &Configuration) -> Result<()> { conf.relay.proxy_api.event_bind, conf.relay.proxy_api.command_bind ); - let zmq_ctx = ZMQ_CONTEXT.lock().unwrap(); + let zmq_ctx = zmq::Context::new(); let sock = zmq_ctx.socket(zmq::PUB)?; sock.bind(&conf.relay.proxy_api.event_bind)?; EVENT_SOCKET @@ -32,29 +40,36 @@ pub fn setup(conf: &Configuration) -> Result<()> { let sock = zmq_ctx.socket(zmq::REP)?; sock.bind(&conf.relay.proxy_api.command_bind)?; - COMMAND_SOCKET - .set(Mutex::new(sock)) - .map_err(|_| anyhow!("OnceCell set error"))?; + + tokio::spawn({ + async move { + command_loop(sock).await; + } + }); Ok(()) } pub async fn send_uplink(pl: &gw::UplinkFrame) -> Result<()> { + info!( + "Sending uplink event, uplink_id: {}", + pl.rx_info + .as_ref() + .ok_or_else(|| anyhow!("rx_info is None"))? + .uplink_id + ); + task::spawn_blocking({ let b = pl.encode_to_vec(); move || -> Result<()> { - let event_sock = match EVENT_SOCKET.get() { - Some(v) => v, - None => { - warn!("Proxy API is not (yet) initialized"); - return Ok(()); - } - }; + let event_sock = EVENT_SOCKET + .get() + .ok_or_else(|| anyhow!("Proxy API is not (yet) initialized"))?; let sock = event_sock.lock().unwrap(); - sock.send("up", zmq::SNDMORE).unwrap(); - sock.send(b, 0).unwrap(); + sock.send("up", zmq::SNDMORE)?; + sock.send(b, 0)?; Ok(()) } @@ -63,24 +78,129 @@ pub async fn send_uplink(pl: &gw::UplinkFrame) -> Result<()> { } pub async fn send_stats(pl: &gw::GatewayStats) -> Result<()> { + info!("Sending stats event"); + task::spawn_blocking({ let b = pl.encode_to_vec(); move || -> Result<()> { - let event_sock = match EVENT_SOCKET.get() { - Some(v) => v, - None => { - warn!("Proxy API is not (yet) initialized"); - return Ok(()); - } - }; + let event_sock = EVENT_SOCKET + .get() + .ok_or_else(|| anyhow!("Proxy API is not (yet) initialized"))?; let sock = event_sock.lock().unwrap(); - sock.send("stats", zmq::SNDMORE).unwrap(); - sock.send(b, 0).unwrap(); + sock.send("stats", zmq::SNDMORE)?; + sock.send(b, 0)?; Ok(()) } }) .await? } + +async fn command_loop(rep_sock: zmq::Socket) { + trace!("Starting command loop"); + let rep_sock = Arc::new(Mutex::new(rep_sock)); + + loop { + let cmd = match read_command(rep_sock.clone()).await { + Ok(v) => v, + Err(err) => { + error!("Receive command error, error: {}", err); + continue; + } + }; + + let resp = match cmd { + Command::Timeout => continue, + Command::Unknown(_, _) => Vec::new(), + Command::Configuration(v) => { + info!("Configuration command received, version: {}", v.version); + + if let Err(e) = backend::send_gateway_configuration(&v).await { + error!("Send gateway configuration error: {}", e); + } + Vec::new() + } + Command::Downlink(v) => { + info!("Downlink command received, downlink_id: {}", v.downlink_id); + + match relay::handle_downlink(v).await { + Ok(v) => v.encode_to_vec(), + Err(e) => { + error!("Handle downlink error: {}", e); + Vec::new() + } + } + } + Command::GatewayID => { + info!("Get gateway id command received"); + + match backend::get_gateway_id() { + Ok(v) => v.to_vec(), + Err(e) => { + error!("Get gateway ID error: {}", e); + Vec::new() + } + } + } + }; + + let resp = task::spawn_blocking({ + let rep_sock = rep_sock.clone(); + + move || -> Result<()> { + rep_sock + .lock() + .unwrap() + .send(resp, 0) + .map_err(anyhow::Error::new) + } + }) + .await; + + if let Err(e) = &resp { + error!("Sending to ZMQ REP socket error: {}", e); + } + + if let Ok(Err(e)) = &resp { + error!("Sending to ZMQ REP socket error: {}", e); + } + } +} + +async fn read_command(rep_sock: Arc>) -> Result { + trace!("Reading next command from zmq socket"); + + task::spawn_blocking({ + move || -> Result { + let rep_sock = rep_sock.lock().unwrap(); + + let mut items = [rep_sock.as_poll_item(zmq::POLLIN)]; + zmq::poll(&mut items, 100)?; + if !items[0].is_readable() { + return Ok(Command::Timeout); + } + + let msg = rep_sock.recv_multipart(0)?; + + if msg.len() != 2 { + return Err(anyhow!("Command must have two frames")); + } + + let command = String::from_utf8(msg[0].clone())?; + + match command.as_str() { + "down" => gw::DownlinkFrame::decode(&*msg[1]) + .map(Command::Downlink) + .map_err(anyhow::Error::new), + "config" => gw::GatewayConfiguration::decode(&*msg[1]) + .map(Command::Configuration) + .map_err(anyhow::Error::new), + "gateway_id" => Ok(Command::GatewayID), + _ => Err(anyhow!("Unknown command: {}", command)), + } + } + }) + .await? +} diff --git a/src/relay.rs b/src/relay.rs index fa760b3..1056399 100644 --- a/src/relay.rs +++ b/src/relay.rs @@ -3,6 +3,7 @@ use std::sync::Mutex; use anyhow::Result; use chirpstack_api::gw; +use log::warn; use once_cell::sync::Lazy; use rand::random; @@ -10,37 +11,61 @@ use crate::{ backend, config::{self, Configuration}, helpers, - packets::{Packet, Payload, PayloadType, RelayPacket, UplinkMetadata, UplinkPayload, MHDR}, + packets::{ + self, DownlinkMetadata, Payload, PayloadType, RelayPacket, UplinkMetadata, UplinkPayload, + MHDR, + }, proxy, }; +static CTX_PREFIX: [u8; 3] = [1, 2, 3]; static RELAY_CHANNEL: Mutex = Mutex::new(0); static UPLINK_ID: Mutex = Mutex::new(0); static UPLINK_CONTEXT: Lazy>>> = Lazy::new(|| Mutex::new(HashMap::new())); +// Handle LoRaWAN payload (non-proprietary). pub async fn handle_uplink(border_gateway: bool, pl: gw::UplinkFrame) -> Result<()> { - let packet = Packet::from_slice(&pl.phy_payload)?; + match border_gateway { + true => proxy_uplink_lora_packet(&pl).await, + false => relay_uplink_lora_packet(&pl).await, + } +} - match packet { - Packet::Relay(v) => match border_gateway { - true => proxy_uplink_relay_packet(&pl, v).await?, - false => relay_relay_packet(&pl, v).await?, - }, - Packet::Lora(_) => match border_gateway { - true => proxy_uplink_lora_packet(&pl).await?, - false => relay_uplink_lora_packet(&pl).await?, +// Handle Proprietary LoRaWAN payload (relay encapsulated). +pub async fn handle_relay(border_gateway: bool, pl: gw::UplinkFrame) -> Result<()> { + let packet = RelayPacket::from_slice(&pl.phy_payload)?; + + match border_gateway { + // In this case we only care about proxy-ing relayed uplinks + true => match packet.mhdr.payload_type { + PayloadType::Uplink => proxy_uplink_relay_packet(&pl, packet).await, + _ => Ok(()), }, + false => relay_relay_packet(&pl, packet).await, } - - Ok(()) } -pub async fn handle_stats(border_gateway: bool, pl: gw::GatewayStats) -> Result<()> { - if !border_gateway { - return Ok(()); +pub async fn handle_downlink(pl: gw::DownlinkFrame) -> Result { + if let Some(first_item) = pl.items.first() { + let tx_info = first_item + .tx_info + .as_ref() + .ok_or_else(|| anyhow!("tx_info is None"))?; + + // Check if context has the CTX_PREFIX, if not we just proxy the downlink payload. + if tx_info.context.len() < CTX_PREFIX.len() + || !tx_info.context[0..CTX_PREFIX.len()].eq(&CTX_PREFIX) + { + return proxy_downlink_lora_packet(&pl).await; + } } - proxy::send_stats(&pl).await + + relay_downlink_lora_packet(&pl).await +} + +async fn proxy_downlink_lora_packet(pl: &gw::DownlinkFrame) -> Result { + backend::send_downlink(pl).await } async fn proxy_uplink_lora_packet(pl: &gw::UplinkFrame) -> Result<()> { @@ -59,19 +84,26 @@ async fn proxy_uplink_relay_packet(pl: &gw::UplinkFrame, packet: RelayPacket) -> if let Some(rx_info) = &mut pl.rx_info { // Set metadata. + rx_info.metadata.insert( + "hop_count".to_string(), + (packet.mhdr.hop_count + 1).to_string(), + ); rx_info .metadata - .insert("hop_count".to_string(), packet.mhdr.hop_count.to_string()); - rx_info - .metadata - .insert("relay_id".to_string(), hex::encode(&relay_pl.relay_id)); + .insert("relay_id".to_string(), hex::encode(relay_pl.relay_id)); // Set RSSI and SNR. rx_info.snr = relay_pl.metadata.snr.into(); rx_info.rssi = relay_pl.metadata.rssi.into(); // Set context. - rx_info.context = relay_pl.metadata.uplink_id.to_be_bytes().to_vec(); + rx_info.context = { + let mut ctx = Vec::with_capacity(CTX_PREFIX.len() + 6); // Relay ID = 4 + Uplink ID = 2 + ctx.extend_from_slice(&CTX_PREFIX); + ctx.extend_from_slice(&relay_pl.relay_id); + ctx.extend_from_slice(&relay_pl.metadata.uplink_id.to_be_bytes()); + ctx + }; } // Set TxInfo. @@ -104,7 +136,7 @@ async fn relay_relay_packet(_: &gw::UplinkFrame, mut packet: RelayPacket) -> Res frequency: get_relay_frequency(&conf)?, modulation: Some(helpers::data_rate_to_gw_modulation( &conf.relay.data_rate, - true, + false, )), power: conf.relay.tx_power, timing: Some(gw::Timing { @@ -146,7 +178,7 @@ async fn relay_uplink_lora_packet(pl: &gw::UplinkFrame) -> Result<()> { payload: Payload::Uplink(UplinkPayload { metadata: UplinkMetadata { uplink_id: store_uplink_context(&rx_info.context), - dr: helpers::modulation_to_dr(&modulation)?, + dr: helpers::modulation_to_dr(modulation)?, channel: helpers::frequency_to_chan(tx_info.frequency)?, rssi: rx_info.rssi as i16, snr: rx_info.snr as i8, @@ -165,7 +197,7 @@ async fn relay_uplink_lora_packet(pl: &gw::UplinkFrame) -> Result<()> { power: conf.relay.tx_power, modulation: Some(helpers::data_rate_to_gw_modulation( &conf.relay.data_rate, - true, + false, )), timing: Some(gw::Timing { parameters: Some(gw::timing::Parameters::Immediately( @@ -182,6 +214,114 @@ async fn relay_uplink_lora_packet(pl: &gw::UplinkFrame) -> Result<()> { backend::relay(&pl).await } +async fn relay_downlink_lora_packet(pl: &gw::DownlinkFrame) -> Result { + let conf = config::get(); + + let mut tx_ack_items: Vec = pl + .items + .iter() + .map(|_| gw::DownlinkTxAckItem { + status: gw::TxAckStatus::Ignored.into(), + }) + .collect(); + + for (i, downlink_item) in pl.items.iter().enumerate() { + let tx_info = downlink_item + .tx_info + .as_ref() + .ok_or_else(|| anyhow!("tx_info is None"))?; + let modulation = tx_info + .modulation + .as_ref() + .ok_or_else(|| anyhow!("modulation is None"))?; + let timing = tx_info + .timing + .as_ref() + .ok_or_else(|| anyhow!("timing is None"))?; + let delay = match &timing.parameters { + Some(gw::timing::Parameters::Delay(v)) => v + .delay + .as_ref() + .map(|v| v.seconds as u8) + .unwrap_or_default(), + _ => { + return Err(anyhow!("Only Delay timing is supported")); + } + }; + + let ctx = tx_info + .context + .get(CTX_PREFIX.len()..CTX_PREFIX.len() + 6) + .ok_or_else(|| anyhow!("context does not contain enough bytes"))?; + + let packet = packets::RelayPacket { + mhdr: packets::MHDR { + payload_type: packets::PayloadType::Downlink, + hop_count: 0, + }, + payload: packets::Payload::Downlink(packets::DownlinkPayload { + phy_payload: downlink_item.phy_payload.clone(), + relay_id: { + let mut b: [u8; 4] = [0; 4]; + b.copy_from_slice(&ctx[0..4]); + b + }, + metadata: DownlinkMetadata { + uplink_id: { + let mut b: [u8; 2] = [0; 2]; + b.copy_from_slice(&ctx[4..6]); + u16::from_be_bytes(b) + }, + dr: helpers::modulation_to_dr(modulation)?, + frequency: tx_info.frequency, + delay, + }, + }), + }; + + let pl = gw::DownlinkFrame { + downlink_id: pl.downlink_id, + items: vec![gw::DownlinkFrameItem { + phy_payload: packet.to_vec()?, + tx_info: Some(gw::DownlinkTxInfo { + frequency: get_relay_frequency(&conf)?, + power: conf.relay.tx_power, + modulation: Some(helpers::data_rate_to_gw_modulation( + &conf.relay.data_rate, + false, + )), + timing: Some(gw::Timing { + parameters: Some(gw::timing::Parameters::Immediately( + gw::ImmediatelyTimingInfo {}, + )), + }), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + match backend::relay(&pl).await { + Ok(_) => { + tx_ack_items[i].status = gw::TxAckStatus::Ok.into(); + break; + } + Err(e) => { + warn!("Relay downlink failed, error: {}", e); + tx_ack_items[i].status = gw::TxAckStatus::InternalError.into(); + } + } + } + + Ok(gw::DownlinkTxAck { + gateway_id: pl.gateway_id.clone(), + downlink_id: pl.downlink_id, + items: tx_ack_items, + ..Default::default() + }) +} + fn get_relay_frequency(conf: &Configuration) -> Result { if conf.relay.frequencies.is_empty() { return Err(anyhow!("No relay frequencies are configured")); diff --git a/tests/border_gateway_test.rs b/tests/border_gateway_test.rs new file mode 100644 index 0000000..6303adf --- /dev/null +++ b/tests/border_gateway_test.rs @@ -0,0 +1,301 @@ +#[macro_use] +extern crate anyhow; + +use std::sync::Mutex; +use std::time::Duration; + +use once_cell::sync::{Lazy, OnceCell}; +use tokio::task; +use tokio::time::sleep; + +use chirpstack_api::gw::{self, modulation}; +use chirpstack_api::prost::Message; +use chirpstack_gateway_relay::config::{self, Configuration}; +use chirpstack_gateway_relay::packets; + +static TEST_SYNC: Lazy> = Lazy::new(|| Mutex::new(())); +static RELAY_INIT: OnceCell = OnceCell::new(); + +static FORWARDER_EVENT_SOCK: OnceCell> = OnceCell::new(); +static FORWARDER_COMMAND_SOCK: OnceCell> = OnceCell::new(); + +static BACKEND_EVENT_SOCK: OnceCell> = OnceCell::new(); +static BACKEND_COMMAND_SOCK: OnceCell> = OnceCell::new(); + +static RELAY_BACKEND_EVENT_SOCK: OnceCell> = OnceCell::new(); +static RELAY_BACKEND_COMMAND_SOCK: OnceCell> = OnceCell::new(); + +fn get_config() -> Configuration { + Configuration { + relay: config::Relay { + frequencies: vec![868100000], + data_rate: config::DataRate { + modulation: config::Modulation::LORA, + spreading_factor: 7, + bandwidth: 125000, + code_rate: Some(config::CodeRate::Cr45), + ..Default::default() + }, + tx_power: 16, + border_gateway: true, + proxy_api: config::ProxyApi { + event_bind: "ipc:///tmp/gateway_relay_event".into(), + command_bind: "ipc:///tmp/gateway_relay_command".into(), + }, + ..Default::default() + }, + backend: config::Backend { + concentratord: config::Concentratord { + event_url: "ipc:///tmp/concentratord_event".into(), + command_url: "ipc:///tmp/concentratord_command".into(), + }, + relay_concentratord: config::Concentratord { + event_url: "ipc:///tmp/relay_concentratord_event".into(), + command_url: "ipc:///tmp/relay_concentratord_command".into(), + }, + }, + channels: vec![868100000, 868300000, 868500000], + data_rates: vec![config::DataRate { + modulation: config::Modulation::LORA, + spreading_factor: 12, + bandwidth: 125000, + code_rate: Some(config::CodeRate::Cr45), + ..Default::default() + }], + ..Default::default() + } +} + +fn init_forwarder() { + let conf = get_config(); + let zmq_ctx = zmq::Context::new(); + + if FORWARDER_EVENT_SOCK.get().is_none() { + let event_sock = zmq_ctx.socket(zmq::SUB).unwrap(); + event_sock + .connect(&conf.relay.proxy_api.event_bind) + .unwrap(); + event_sock.set_subscribe("".as_bytes()).unwrap(); + FORWARDER_EVENT_SOCK + .set(Mutex::new(event_sock)) + .map_err(|_| anyhow!("OnceCell error")) + .unwrap(); + } + + if FORWARDER_COMMAND_SOCK.get().is_none() { + let cmd_sock = zmq_ctx.socket(zmq::REQ).unwrap(); + cmd_sock + .connect(&conf.relay.proxy_api.command_bind) + .unwrap(); + FORWARDER_COMMAND_SOCK + .set(Mutex::new(cmd_sock)) + .map_err(|_| anyhow!("OnceCell error")) + .unwrap(); + } +} + +fn init_backend() { + let conf = get_config(); + let zmq_ctx = zmq::Context::new(); + + if BACKEND_EVENT_SOCK.get().is_none() { + let event_sock = zmq_ctx.socket(zmq::PUB).unwrap(); + event_sock + .bind(&conf.backend.concentratord.event_url) + .unwrap(); + BACKEND_EVENT_SOCK + .set(Mutex::new(event_sock)) + .map_err(|_| anyhow!("OnceCell error")) + .unwrap(); + } + + if BACKEND_COMMAND_SOCK.get().is_none() { + let cmd_sock = zmq_ctx.socket(zmq::REP).unwrap(); + cmd_sock + .bind(&conf.backend.concentratord.command_url) + .unwrap(); + BACKEND_COMMAND_SOCK + .set(Mutex::new(cmd_sock)) + .map_err(|_| anyhow!("OnceCell error")) + .unwrap(); + } + + if RELAY_BACKEND_EVENT_SOCK.get().is_none() { + let event_sock = zmq_ctx.socket(zmq::PUB).unwrap(); + event_sock + .bind(&conf.backend.relay_concentratord.event_url) + .unwrap(); + RELAY_BACKEND_EVENT_SOCK + .set(Mutex::new(event_sock)) + .map_err(|_| anyhow!("OnceCell error")) + .unwrap(); + } + + if RELAY_BACKEND_COMMAND_SOCK.get().is_none() { + let cmd_sock = zmq_ctx.socket(zmq::REP).unwrap(); + cmd_sock + .bind(&conf.backend.relay_concentratord.command_url) + .unwrap(); + RELAY_BACKEND_COMMAND_SOCK + .set(Mutex::new(cmd_sock)) + .map_err(|_| anyhow!("OnceCell error")) + .unwrap(); + } +} + +async fn init_relay() { + if RELAY_INIT.get().is_none() { + chirpstack_gateway_relay::logging::setup( + "chirpstack-gateway-relay", + log::Level::Debug, + false, + ) + .unwrap(); + + tokio::spawn({ + let conf = config::get(); + + async move { + chirpstack_gateway_relay::cmd::root::run(&conf) + .await + .unwrap(); + } + }); + + RELAY_INIT + .set(true) + .map_err(|_| anyhow!("OnceCell error")) + .unwrap(); + + tokio::task::spawn_blocking({ + move || { + let cmd_sock = BACKEND_COMMAND_SOCK.get().unwrap().lock().unwrap(); + let _ = cmd_sock.recv_multipart(0).unwrap(); + let gw_id: Vec = vec![1, 1, 1, 1, 1, 1, 1, 1]; + cmd_sock.send(&gw_id, 0).unwrap(); + } + }) + .await + .unwrap(); + + tokio::task::spawn_blocking({ + move || { + let cmd_sock = RELAY_BACKEND_COMMAND_SOCK.get().unwrap().lock().unwrap(); + let _ = cmd_sock.recv_multipart(0).unwrap(); + let gw_id: Vec = vec![2, 2, 2, 2, 2, 2, 2, 2]; + cmd_sock.send(&gw_id, 0).unwrap(); + } + }) + .await + .unwrap(); + + sleep(Duration::from_millis(100)).await; + } +} + +// The Relay Gateway receives a relayed uplink. +// We expect that the "unwrapped" uplink is proxied to the forwarder using the +// relay context. +#[tokio::test] +async fn test_uplink_relay_frame() { + let _guard = TEST_SYNC.lock().unwrap(); + let _ = config::set(get_config()); + init_backend(); + init_relay().await; + init_forwarder(); + + let packet = packets::RelayPacket { + mhdr: packets::MHDR { + payload_type: packets::PayloadType::Uplink, + hop_count: 0, + }, + payload: packets::Payload::Uplink(packets::UplinkPayload { + metadata: packets::UplinkMetadata { + uplink_id: 123, + dr: 0, + rssi: -60, + snr: 6, + channel: 2, + }, + relay_id: [1, 2, 3, 4], + phy_payload: vec![9, 8, 7, 6], + }), + }; + + 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. + task::spawn_blocking({ + let up = up.clone(); + + move || { + let event_sock = RELAY_BACKEND_EVENT_SOCK.get().unwrap().lock().unwrap(); + event_sock.send("up", zmq::SNDMORE).unwrap(); + event_sock.send(up.encode_to_vec(), 0).unwrap(); + } + }) + .await + .unwrap(); + + // We expect to receive the unwrapped uplink to be received by the forwarder. + let up: gw::UplinkFrame = task::spawn_blocking({ + move || -> gw::UplinkFrame { + let event_sock = FORWARDER_EVENT_SOCK.get().unwrap().lock().unwrap(); + let msg = event_sock.recv_multipart(0).unwrap(); + let cmd = String::from_utf8(msg[0].clone()).unwrap(); + assert_eq!("up", cmd); + gw::UplinkFrame::decode(&*msg[1]).unwrap() + } + }) + .await + .unwrap(); + + // Validate PHYPayload + assert_eq!(vec![9, 8, 7, 6], up.phy_payload); + + // Validate TxInfo + + // Validate RxInfo (RSSI & SNR) +} + +// #[tokio::test] +// async fn test_uplink_lora_frame() { +// let _guard = TEST_SYNC.lock().unwrap(); +// let _ = config::set(get_config()); +// init_forwarder(); +// init_backend(); +// } + +// #[tokio::test] +// async fn test_downlink_relay_frame() { +// let _guard = TEST_SYNC.lock().unwrap(); +// let _ = config::set(get_config()); +// init_forwarder(); +// init_backend(); +// } + +// #[tokio::test] +// async fn test_downlink_lora_frame() { +// let _guard = TEST_SYNC.lock().unwrap(); +// let _ = config::set(get_config()); +// init_forwarder(); +// init_backend(); +// } diff --git a/tests/relay_gateway_test.rs b/tests/relay_gateway_test.rs new file mode 100644 index 0000000..58e77f2 --- /dev/null +++ b/tests/relay_gateway_test.rs @@ -0,0 +1,8 @@ +#[tokio::test] +async fn test_relay_uplink_lora() {} + +#[tokio::test] +async fn test_filter_uplink_lora() {} + +#[tokio::test] +async fn test_relay_downlink_lora() {}