mirror of
https://github.com/chirpstack/chirpstack.git
synced 2025-01-29 15:13:52 +00:00
Add crate for PhyPayload filtering.
This can be used by the ChirpStack UDP and MQTT Forwarder. See also: https://github.com/chirpstack/chirpstack-mqtt-forwarder/issues/25
This commit is contained in:
parent
0679f10fad
commit
21896e65d7
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -2425,6 +2425,16 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lrwn_filters"
|
||||
version = "4.3.2"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"lrwn",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.10"
|
||||
|
@ -2,6 +2,7 @@
|
||||
members = [
|
||||
"chirpstack",
|
||||
"lrwn",
|
||||
"lrwn-filters",
|
||||
"backend",
|
||||
"api/rust",
|
||||
]
|
||||
|
2
Makefile
2
Makefile
@ -10,6 +10,7 @@ version:
|
||||
sed -i 's/^version.*/version = "$(VERSION)"/g' ./chirpstack/Cargo.toml
|
||||
sed -i 's/^version.*/version = "$(VERSION)"/g' ./backend/Cargo.toml
|
||||
sed -i 's/^version.*/version = "$(VERSION)"/g' ./lrwn/Cargo.toml
|
||||
sed -i 's/^version.*/version = "$(VERSION)"/g' ./lrwn-filters/Cargo.toml
|
||||
sed -i 's/"version.*/"version": "$(VERSION)",/g' ./ui/package.json
|
||||
sed -i 's/"version.*/"version": "$(VERSION)",/g' ./api/grpc-web/package.json
|
||||
sed -i 's/"version.*/"version": "$(VERSION)",/g' ./api/js/package.json
|
||||
@ -45,6 +46,7 @@ devshell-ui:
|
||||
test:
|
||||
docker-compose run --rm chirpstack make test
|
||||
docker-compose run --rm chirpstack make test-lrwn
|
||||
docker-compose run --rm chirpstack make test-lrwn-filters
|
||||
|
||||
# Starts the ChirpStack server (for testing only).
|
||||
test-server: build-ui
|
||||
|
@ -111,6 +111,9 @@ test:
|
||||
test-lrwn:
|
||||
cd ../lrwn && make test
|
||||
|
||||
test-lrwn-filters:
|
||||
cd ../lrwn-filters && make test
|
||||
|
||||
test-server: debug-amd64
|
||||
../target/debug/chirpstack -c ./configuration
|
||||
|
||||
|
24
lrwn-filters/Cargo.toml
Normal file
24
lrwn-filters/Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "lrwn_filters"
|
||||
description = "Library for filtering LoRaWAN payloads on DevAddr and JoinEUIs prefixes"
|
||||
homepage = "https://www.chirpstack.io/"
|
||||
license = "MIT"
|
||||
version = "4.3.2"
|
||||
authors = ["Orne Brocaar <info@brocaar.com>"]
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chirpstack/chirpstack"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[dependencies]
|
||||
hex = "0.4"
|
||||
thiserror = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
lrwn = { path = "../lrwn" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
serde = ["dep:serde"]
|
7
lrwn-filters/Makefile
Normal file
7
lrwn-filters/Makefile
Normal file
@ -0,0 +1,7 @@
|
||||
.PHONY: test
|
||||
|
||||
# Runs the tests
|
||||
test:
|
||||
cargo fmt --check
|
||||
cargo clippy
|
||||
cargo test --all-features
|
13
lrwn-filters/src/error.rs
Normal file
13
lrwn-filters/src/error.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("DevAddrPrefix must be in the form 00000000/0")]
|
||||
DevAddrPrefixFormat,
|
||||
|
||||
#[error("EuiPrefix must be in the form 0000000000000000/0")]
|
||||
EuiPrefixFormat,
|
||||
|
||||
#[error(transparent)]
|
||||
FromHexError(#[from] hex::FromHexError),
|
||||
}
|
451
lrwn-filters/src/lib.rs
Normal file
451
lrwn-filters/src/lib.rs
Normal file
@ -0,0 +1,451 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{
|
||||
de::{self, Visitor},
|
||||
{Deserialize, Deserializer, Serialize, Serializer},
|
||||
};
|
||||
|
||||
pub mod error;
|
||||
|
||||
/// Configuration.
|
||||
pub struct Filters {
|
||||
/// DevAddr prefixes.
|
||||
pub dev_addr_prefixes: Vec<DevAddrPrefix>,
|
||||
|
||||
/// JoinEUIs.
|
||||
pub join_eui_prefixes: Vec<EuiPrefix>,
|
||||
}
|
||||
|
||||
/// Returns true if the given PhyPayload matches the given filters
|
||||
///
|
||||
/// If no DevAddr prefixes are given, then all data uplink frames will pass the filter.
|
||||
/// If no JoinEui prefixes are given, then all join requests will pass the filter.
|
||||
/// PhyPayloads that can't be filtered will pass the filter.
|
||||
pub fn matches(phy_payload: &[u8], config: &Filters) -> bool {
|
||||
if phy_payload.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mhdr = phy_payload[0];
|
||||
let m_type = mhdr >> 5;
|
||||
|
||||
let dev_addr: Option<u32> = match m_type {
|
||||
// DataUp
|
||||
0x02 | 0x04 => {
|
||||
// MHDR + DevAddr
|
||||
// [1] [4]
|
||||
if phy_payload.len() >= 5 {
|
||||
let mut dev_addr: [u8; 4] = [0; 4];
|
||||
dev_addr.clone_from_slice(&phy_payload[1..5]);
|
||||
Some(u32::from_le_bytes(dev_addr))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let join_eui: Option<u64> = match m_type {
|
||||
// JoinRequest
|
||||
0x00 => {
|
||||
// MHDR + JoinEUI + DevEUI
|
||||
// [1] [8] [8]
|
||||
if phy_payload.len() >= 17 {
|
||||
let mut join_eui: [u8; 8] = [0; 8];
|
||||
join_eui.clone_from_slice(&phy_payload[1..9]);
|
||||
Some(u64::from_le_bytes(join_eui))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// We could not extract the DevAddr or JoinEUI from the PhyPayload. In this case we let the
|
||||
// message pass.
|
||||
if dev_addr.is_none() && join_eui.is_none() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(dev_addr) = dev_addr {
|
||||
if config.dev_addr_prefixes.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
for p in &config.dev_addr_prefixes {
|
||||
let prefix = u32::from_be_bytes(p.prefix());
|
||||
if dev_addr >> (32 - p.size()) == prefix >> (32 - p.size()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(join_eui) = join_eui {
|
||||
if config.join_eui_prefixes.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
for p in &config.join_eui_prefixes {
|
||||
let prefix = u64::from_be_bytes(p.prefix());
|
||||
if join_eui >> (64 - p.size()) == prefix >> (64 - p.size()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// DevAddr prefix.
|
||||
pub struct DevAddrPrefix([u8; 4], u32);
|
||||
|
||||
impl DevAddrPrefix {
|
||||
pub fn new(prefix: [u8; 4], size: u32) -> Self {
|
||||
DevAddrPrefix(prefix, size)
|
||||
}
|
||||
|
||||
fn prefix(&self) -> [u8; 4] {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn size(&self) -> u32 {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DevAddrPrefix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}/{}", hex::encode(self.0), self.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DevAddrPrefix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}/{}", hex::encode(self.0), self.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DevAddrPrefix {
|
||||
type Err = error::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.to_string();
|
||||
let parts: Vec<&str> = s.split('/').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(error::Error::DevAddrPrefixFormat);
|
||||
}
|
||||
if parts[0].len() != 8 {
|
||||
return Err(error::Error::DevAddrPrefixFormat);
|
||||
}
|
||||
|
||||
let mut mask: [u8; 4] = [0; 4];
|
||||
hex::decode_to_slice(parts[0], &mut mask)?;
|
||||
let size: u32 = parts[1]
|
||||
.parse()
|
||||
.map_err(|_| error::Error::DevAddrPrefixFormat)?;
|
||||
|
||||
Ok(DevAddrPrefix(mask, size))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for DevAddrPrefix {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> Deserialize<'de> for DevAddrPrefix {
|
||||
fn deserialize<D>(deserialize: D) -> Result<DevAddrPrefix, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize.deserialize_str(DevAddrPrefixVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
struct DevAddrPrefixVisitor;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> Visitor<'de> for DevAddrPrefixVisitor {
|
||||
type Value = DevAddrPrefix;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("A DevAddrPrefix in the format 00000000/0 is expected")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
DevAddrPrefix::from_str(value).map_err(|e| E::custom(format!("{}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// JoinEUI prefix.
|
||||
pub struct EuiPrefix([u8; 8], u32);
|
||||
|
||||
impl EuiPrefix {
|
||||
pub fn new(prefix: [u8; 8], size: u32) -> Self {
|
||||
EuiPrefix(prefix, size)
|
||||
}
|
||||
|
||||
fn prefix(&self) -> [u8; 8] {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn size(&self) -> u32 {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EuiPrefix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}/{}", hex::encode(self.0), self.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for EuiPrefix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}/{}", hex::encode(self.0), self.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EuiPrefix {
|
||||
type Err = error::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.to_string();
|
||||
let parts: Vec<&str> = s.split('/').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(error::Error::EuiPrefixFormat);
|
||||
}
|
||||
if parts[0].len() != 16 {
|
||||
return Err(error::Error::EuiPrefixFormat);
|
||||
}
|
||||
|
||||
let mut mask: [u8; 8] = [0; 8];
|
||||
hex::decode_to_slice(parts[0], &mut mask)?;
|
||||
let size: u32 = parts[1]
|
||||
.parse()
|
||||
.map_err(|_| error::Error::EuiPrefixFormat)?;
|
||||
|
||||
Ok(EuiPrefix(mask, size))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for EuiPrefix {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> Deserialize<'de> for EuiPrefix {
|
||||
fn deserialize<D>(deserialize: D) -> Result<EuiPrefix, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize.deserialize_str(EuiPrefixVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
struct EuiPrefixVisitor;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> Visitor<'de> for EuiPrefixVisitor {
|
||||
type Value = EuiPrefix;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("An EuiPrefix in the format 0000000000000000/0 is expected")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
EuiPrefix::from_str(value).map_err(|e| E::custom(format!("{}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_dev_addr() {
|
||||
struct Test {
|
||||
name: String,
|
||||
filters: Filters,
|
||||
dev_addr: lrwn::DevAddr,
|
||||
passes: bool,
|
||||
}
|
||||
|
||||
let tests = vec![
|
||||
Test {
|
||||
name: "empty filters".to_string(),
|
||||
filters: Filters {
|
||||
dev_addr_prefixes: vec![],
|
||||
join_eui_prefixes: vec![],
|
||||
},
|
||||
dev_addr: lrwn::DevAddr::from_str("01020304").unwrap(),
|
||||
passes: true,
|
||||
},
|
||||
Test {
|
||||
name: "dev_addr passes filter".to_string(),
|
||||
filters: Filters {
|
||||
dev_addr_prefixes: vec![DevAddrPrefix::from_str("01000000/8").unwrap()],
|
||||
join_eui_prefixes: vec![],
|
||||
},
|
||||
dev_addr: lrwn::DevAddr::from_str("01020304").unwrap(),
|
||||
passes: true,
|
||||
},
|
||||
Test {
|
||||
name: "dev_addr does not pass filter".to_string(),
|
||||
filters: Filters {
|
||||
dev_addr_prefixes: vec![DevAddrPrefix::from_str("01000000/16").unwrap()],
|
||||
join_eui_prefixes: vec![],
|
||||
},
|
||||
dev_addr: lrwn::DevAddr::from_str("01020304").unwrap(),
|
||||
passes: false,
|
||||
},
|
||||
Test {
|
||||
name: "dev_addr passes one of the two filters".to_string(),
|
||||
filters: Filters {
|
||||
dev_addr_prefixes: vec![
|
||||
DevAddrPrefix::from_str("01000000/16").unwrap(),
|
||||
DevAddrPrefix::from_str("01000000/8").unwrap(),
|
||||
],
|
||||
join_eui_prefixes: vec![],
|
||||
},
|
||||
dev_addr: lrwn::DevAddr::from_str("01020304").unwrap(),
|
||||
passes: true,
|
||||
},
|
||||
];
|
||||
|
||||
for test in &tests {
|
||||
println!("> {}", test.name);
|
||||
let phy = lrwn::PhyPayload {
|
||||
mhdr: lrwn::MHDR {
|
||||
m_type: lrwn::MType::UnconfirmedDataUp,
|
||||
major: lrwn::Major::LoRaWANR1,
|
||||
},
|
||||
payload: lrwn::Payload::MACPayload(lrwn::MACPayload {
|
||||
fhdr: lrwn::FHDR {
|
||||
devaddr: test.dev_addr,
|
||||
f_ctrl: Default::default(),
|
||||
f_cnt: 0,
|
||||
f_opts: lrwn::MACCommandSet::new(vec![]),
|
||||
},
|
||||
f_port: None,
|
||||
frm_payload: None,
|
||||
}),
|
||||
mic: None,
|
||||
};
|
||||
let phy_b = phy.to_vec().unwrap();
|
||||
assert_eq!(test.passes, matches(&phy_b, &test.filters));
|
||||
|
||||
let phy = lrwn::PhyPayload {
|
||||
mhdr: lrwn::MHDR {
|
||||
m_type: lrwn::MType::ConfirmedDataUp,
|
||||
major: lrwn::Major::LoRaWANR1,
|
||||
},
|
||||
payload: lrwn::Payload::MACPayload(lrwn::MACPayload {
|
||||
fhdr: lrwn::FHDR {
|
||||
devaddr: test.dev_addr,
|
||||
f_ctrl: Default::default(),
|
||||
f_cnt: 0,
|
||||
f_opts: lrwn::MACCommandSet::new(vec![]),
|
||||
},
|
||||
f_port: None,
|
||||
frm_payload: None,
|
||||
}),
|
||||
mic: None,
|
||||
};
|
||||
let phy_b = phy.to_vec().unwrap();
|
||||
assert_eq!(test.passes, matches(&phy_b, &test.filters));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_eui() {
|
||||
struct Test {
|
||||
name: String,
|
||||
filters: Filters,
|
||||
join_eui: lrwn::EUI64,
|
||||
passes: bool,
|
||||
}
|
||||
|
||||
let tests = vec![
|
||||
Test {
|
||||
name: "no filters".into(),
|
||||
filters: Filters {
|
||||
dev_addr_prefixes: vec![],
|
||||
join_eui_prefixes: vec![],
|
||||
},
|
||||
join_eui: lrwn::EUI64::from_str("0102030405060708").unwrap(),
|
||||
passes: true,
|
||||
},
|
||||
Test {
|
||||
name: "passes filter".into(),
|
||||
filters: Filters {
|
||||
dev_addr_prefixes: vec![],
|
||||
join_eui_prefixes: vec![EuiPrefix::from_str("0100000000000000/8").unwrap()],
|
||||
},
|
||||
join_eui: lrwn::EUI64::from_str("0102030405060708").unwrap(),
|
||||
passes: true,
|
||||
},
|
||||
Test {
|
||||
name: "does not pass filter".into(),
|
||||
filters: Filters {
|
||||
dev_addr_prefixes: vec![],
|
||||
join_eui_prefixes: vec![EuiPrefix::from_str("0100000000000000/16").unwrap()],
|
||||
},
|
||||
join_eui: lrwn::EUI64::from_str("0102030405060708").unwrap(),
|
||||
passes: false,
|
||||
},
|
||||
Test {
|
||||
name: "passes one of two filters".into(),
|
||||
filters: Filters {
|
||||
dev_addr_prefixes: vec![],
|
||||
join_eui_prefixes: vec![
|
||||
EuiPrefix::from_str("0100000000000000/16").unwrap(),
|
||||
EuiPrefix::from_str("0100000000000000/8").unwrap(),
|
||||
],
|
||||
},
|
||||
join_eui: lrwn::EUI64::from_str("0102030405060708").unwrap(),
|
||||
passes: true,
|
||||
},
|
||||
];
|
||||
|
||||
for test in &tests {
|
||||
println!("> {}", test.name);
|
||||
let phy = lrwn::PhyPayload {
|
||||
mhdr: lrwn::MHDR {
|
||||
m_type: lrwn::MType::JoinRequest,
|
||||
major: lrwn::Major::LoRaWANR1,
|
||||
},
|
||||
payload: lrwn::Payload::JoinRequest(lrwn::JoinRequestPayload {
|
||||
join_eui: test.join_eui,
|
||||
dev_eui: Default::default(),
|
||||
dev_nonce: 0,
|
||||
}),
|
||||
mic: None,
|
||||
};
|
||||
let phy_b = phy.to_vec().unwrap();
|
||||
assert_eq!(test.passes, matches(&phy_b, &test.filters));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user