From f27b8da38dc772d74d40309154d9d423c33b55ab Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Thu, 30 Jun 2022 11:11:21 +0100 Subject: [PATCH] Re-implement passive-roaming. --- Cargo.lock | 12 +- api/proto/internal/internal.proto | 31 + api/rust/Cargo.lock | 2 +- .../proto/chirpstack/internal/internal.proto | 31 + backend/Cargo.toml | 5 +- backend/src/lib.rs | 786 +++++++++++++++--- chirpstack/Cargo.toml | 3 +- chirpstack/src/api/backend/mod.rs | 552 ++++++++++++ chirpstack/src/api/mod.rs | 7 +- chirpstack/src/backend/joinserver.rs | 11 +- chirpstack/src/backend/keywrap.rs | 15 + chirpstack/src/backend/mod.rs | 10 + chirpstack/src/backend/roaming.rs | 320 +++++++ chirpstack/src/cmd/root.rs | 2 +- chirpstack/src/config.rs | 58 +- chirpstack/src/downlink/data.rs | 101 ++- chirpstack/src/downlink/data_fns.rs | 174 ++++ chirpstack/src/downlink/helpers.rs | 2 +- chirpstack/src/downlink/mod.rs | 2 + chirpstack/src/downlink/roaming.rs | 215 +++++ chirpstack/src/maccommand/dev_status.rs | 1 + chirpstack/src/maccommand/device_time.rs | 1 + chirpstack/src/maccommand/link_adr.rs | 1 + chirpstack/src/maccommand/link_check.rs | 1 + chirpstack/src/maccommand/mod.rs | 1 + chirpstack/src/region.rs | 17 + chirpstack/src/storage/device_session.rs | 64 ++ chirpstack/src/storage/mod.rs | 1 + chirpstack/src/storage/passive_roaming.rs | 260 ++++++ chirpstack/src/test/class_a_pr_test.rs | 509 ++++++++++++ chirpstack/src/test/mod.rs | 2 + chirpstack/src/test/otaa_pr_test.rs | 418 ++++++++++ chirpstack/src/test/otaa_test.rs | 2 +- chirpstack/src/uplink/data.rs | 115 ++- chirpstack/src/uplink/data_fns.rs | 221 +++++ chirpstack/src/uplink/data_sns.rs | 12 + chirpstack/src/uplink/join.rs | 330 ++------ chirpstack/src/uplink/join_fns.rs | 184 ++++ chirpstack/src/uplink/join_sns.rs | 765 +++++++++++++++++ chirpstack/src/uplink/mod.rs | 34 + lrwn/src/devaddr.rs | 7 + lrwn/src/error.rs | 3 + lrwn/src/keys.rs | 256 ++++++ lrwn/src/lib.rs | 1 + lrwn/src/netid.rs | 27 +- lrwn/src/region/mod.rs | 6 +- 46 files changed, 5117 insertions(+), 461 deletions(-) create mode 100644 chirpstack/src/api/backend/mod.rs create mode 100644 chirpstack/src/backend/roaming.rs create mode 100644 chirpstack/src/downlink/data_fns.rs create mode 100644 chirpstack/src/downlink/roaming.rs create mode 100644 chirpstack/src/storage/passive_roaming.rs create mode 100644 chirpstack/src/test/class_a_pr_test.rs create mode 100644 chirpstack/src/test/otaa_pr_test.rs create mode 100644 chirpstack/src/uplink/data_fns.rs create mode 100644 chirpstack/src/uplink/data_sns.rs create mode 100644 chirpstack/src/uplink/join_fns.rs create mode 100644 chirpstack/src/uplink/join_sns.rs create mode 100644 lrwn/src/keys.rs diff --git a/Cargo.lock b/Cargo.lock index 293c7a23..e92663f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,8 +580,8 @@ dependencies = [ "anyhow", "chrono", "hex", + "httpmock", "rand", - "redis", "reqwest", "serde", "serde_json", @@ -812,6 +812,7 @@ dependencies = [ "backend", "base64", "bigdecimal", + "bytes", "chirpstack_api", "chrono", "clap 2.34.0", @@ -996,11 +997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062" dependencies = [ "bytes", - "futures-core", "memchr", - "pin-project-lite", - "tokio", - "tokio-util 0.6.9", ] [[package]] @@ -2871,19 +2868,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80b5f38d7f5a020856a0e16e40a9cfabf88ae8f0e4c2dcd8a3114c1e470852" dependencies = [ "async-trait", - "bytes", "combine", "crc16", "dtoa", - "futures-util", "itoa 0.4.8", "percent-encoding", - "pin-project-lite", "r2d2", "rand", "sha1", - "tokio", - "tokio-util 0.6.9", "url", ] diff --git a/api/proto/internal/internal.proto b/api/proto/internal/internal.proto index 33209251..46a80c28 100644 --- a/api/proto/internal/internal.proto +++ b/api/proto/internal/internal.proto @@ -233,3 +233,34 @@ message LoraCloudGeolocBufferUplink { // RxInfo set for a single uplink. repeated gw.UplinkRxInfo rx_info = 1; } + +message PassiveRoamingDeviceSession { + // Session ID (UUID). + // Unfortunately we can not use the DevEUI as unique identifier + // as the PRStartAns DevEUI field is optional. + bytes session_id = 1; + + // NetID of the hNS. + bytes net_id = 2; + + // DevAddr of the device. + bytes dev_addr = 3; + + // DevEUI of the device (optional). + bytes dev_eui = 4; + + // LoRaWAN 1.1. + bool lorawan_1_1 = 5; + + // LoRaWAN 1.0 NwkSKey / LoRaWAN 1.1 FNwkSIntKey. + bytes f_nwk_s_int_key = 6; + + // Lifetime. + google.protobuf.Timestamp lifetime = 7; + + // Uplink frame-counter. + uint32 f_cnt_up = 8; + + // Validate MIC. + bool validate_mic = 9; +} diff --git a/api/rust/Cargo.lock b/api/rust/Cargo.lock index f548b5b9..32cb0782 100644 --- a/api/rust/Cargo.lock +++ b/api/rust/Cargo.lock @@ -121,7 +121,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chirpstack_api" -version = "4.0.0-test.6" +version = "4.0.0-test.7" dependencies = [ "hex", "pbjson", diff --git a/api/rust/proto/chirpstack/internal/internal.proto b/api/rust/proto/chirpstack/internal/internal.proto index 33209251..46a80c28 100644 --- a/api/rust/proto/chirpstack/internal/internal.proto +++ b/api/rust/proto/chirpstack/internal/internal.proto @@ -233,3 +233,34 @@ message LoraCloudGeolocBufferUplink { // RxInfo set for a single uplink. repeated gw.UplinkRxInfo rx_info = 1; } + +message PassiveRoamingDeviceSession { + // Session ID (UUID). + // Unfortunately we can not use the DevEUI as unique identifier + // as the PRStartAns DevEUI field is optional. + bytes session_id = 1; + + // NetID of the hNS. + bytes net_id = 2; + + // DevAddr of the device. + bytes dev_addr = 3; + + // DevEUI of the device (optional). + bytes dev_eui = 4; + + // LoRaWAN 1.1. + bool lorawan_1_1 = 5; + + // LoRaWAN 1.0 NwkSKey / LoRaWAN 1.1 FNwkSIntKey. + bytes f_nwk_s_int_key = 6; + + // Lifetime. + google.protobuf.Timestamp lifetime = 7; + + // Uplink frame-counter. + uint32 f_cnt_up = 8; + + // Validate MIC. + bool validate_mic = 9; +} diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 60b98834..66762758 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -10,7 +10,6 @@ serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" thiserror = "1.0" anyhow = "1.0" -redis = { version = "0.21", features = ["tokio-comp"] } tracing = "0.1" hex = "0.4" rand = "0.8" @@ -18,3 +17,7 @@ aes-kw = "0.2" reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false } chrono = { version = "0.4", features = ["serde"] } tokio = { version = "1.6", features = ["macros" ] } + +# Development and testing +[dev-dependencies] +httpmock = "0.6" diff --git a/backend/src/lib.rs b/backend/src/lib.rs index ff1ed292..f1571bc5 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -3,25 +3,37 @@ extern crate anyhow; use std::fs::File; use std::io::Read; +use std::time::Duration; use aes_kw::Kek; use anyhow::{Context, Result}; -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Utc}; use reqwest::header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}; use reqwest::{Certificate, Identity}; use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc; -use tracing::trace; +use tokio::sync::oneshot::Receiver; +use tracing::{error, info, trace}; const PROTOCOL_VERSION: &str = "1.0"; +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Role { + FNS, + HNS, + SNS, +} + pub trait BasePayloadProvider { fn base_payload(&self) -> &BasePayload; } +pub trait BasePayloadResultProvider { + fn base_payload(&self) -> &BasePayloadResult; +} + pub struct ClientConfig { - pub sender_id: String, - pub receiver_id: String, + pub sender_id: Vec, + pub receiver_id: Vec, pub server: String, pub ca_cert: String, pub tls_cert: String, @@ -31,27 +43,26 @@ pub struct ClientConfig { // include a prefix, like Bearer, Key or Basic. pub authorization: Option, - // Holds the optional Redis database client. When set the client - // will use the aysnc protocol scheme. In this case the client will wait - // AsyncTimeout before returning a timeout error. - // pub redis_client: Option>, - // AsyncTimeout defines the async timeout. This must be set when RedisClient // is set. pub async_timeout: Duration, + + // Use target-role URL suffix (e.g. /fns, /sns, ...). + pub use_target_role_suffix: bool, } impl Default for ClientConfig { fn default() -> Self { ClientConfig { - sender_id: "".into(), - receiver_id: "".into(), + sender_id: vec![], + receiver_id: vec![], server: "".into(), ca_cert: "".into(), tls_cert: "".into(), tls_key: "".into(), authorization: None, - async_timeout: Duration::zero(), + async_timeout: Duration::from_secs(0), + use_target_role_suffix: false, } } } @@ -120,87 +131,235 @@ impl Client { }) } - pub fn get_sender_id(&self) -> String { + pub fn get_sender_id(&self) -> Vec { self.config.sender_id.clone() } - pub fn get_receiver_id(&self) -> String { + pub fn get_receiver_id(&self) -> Vec { self.config.receiver_id.clone() } pub fn is_async(&self) -> bool { - false + !self.config.async_timeout.is_zero() } - pub fn get_random_transaction_id(&self) -> u32 { - rand::random() + pub fn get_async_timeout(&self) -> Duration { + self.config.async_timeout } - pub async fn join_req(&self, pl: &mut JoinReqPayload) -> Result { - pl.base.protocol_version = PROTOCOL_VERSION.to_string(); + pub async fn join_req( + &self, + pl: &mut JoinReqPayload, + async_resp: Option>>, + ) -> Result { pl.base.sender_id = self.config.sender_id.clone(); pl.base.receiver_id = self.config.receiver_id.clone(); pl.base.message_type = MessageType::JoinReq; - if pl.base.transaction_id == 0 { - pl.base.transaction_id = self.get_random_transaction_id(); - } let mut ans: JoinAnsPayload = Default::default(); - self.request(&pl, &mut ans).await?; - + self.request(None, &pl, &mut ans, async_resp).await?; Ok(ans) } - async fn request(&self, pl: &S, ans: &mut D) -> Result<()> + pub async fn rejoin_req( + &self, + pl: &mut RejoinReqPayload, + async_resp: Option>>, + ) -> Result { + pl.base.sender_id = self.config.sender_id.clone(); + pl.base.receiver_id = self.config.receiver_id.clone(); + pl.base.message_type = MessageType::RejoinReq; + + let mut ans: RejoinAnsPayload = Default::default(); + self.request(None, &pl, &mut ans, async_resp).await?; + Ok(ans) + } + + pub async fn app_s_key_req( + &self, + pl: &mut AppSKeyReqPayload, + async_resp: Option>>, + ) -> Result { + pl.base.sender_id = self.config.sender_id.clone(); + pl.base.receiver_id = self.config.receiver_id.clone(); + pl.base.message_type = MessageType::AppSKeyReq; + + let mut ans: AppSKeyAnsPayload = Default::default(); + self.request(None, &pl, &mut ans, async_resp).await?; + Ok(ans) + } + + pub async fn pr_start_req( + &self, + target_role: Role, + pl: &mut PRStartReqPayload, + async_resp: Option>>, + ) -> Result { + pl.base.sender_id = self.config.sender_id.clone(); + pl.base.receiver_id = self.config.receiver_id.clone(); + pl.base.message_type = MessageType::PRStartReq; + + let mut ans: PRStartAnsPayload = Default::default(); + self.request(Some(target_role), &pl, &mut ans, async_resp) + .await?; + Ok(ans) + } + + pub async fn pr_start_ans(&self, target_role: Role, pl: &PRStartAnsPayload) -> Result<()> { + self.response_request(Some(target_role), pl).await + } + + pub async fn pr_stop_req( + &self, + target_role: Role, + pl: &mut PRStopReqPayload, + async_resp: Option>>, + ) -> Result { + pl.base.sender_id = self.config.sender_id.clone(); + pl.base.receiver_id = self.config.receiver_id.clone(); + pl.base.message_type = MessageType::PRStopReq; + + let mut ans: PRStopAnsPayload = Default::default(); + self.request(Some(target_role), &pl, &mut ans, async_resp) + .await?; + Ok(ans) + } + + pub async fn pr_stop_ans(&self, target_role: Role, pl: &PRStopAnsPayload) -> Result<()> { + self.response_request(Some(target_role), pl).await + } + + pub async fn home_ns_req( + &self, + pl: &mut HomeNSReqPayload, + async_resp: Option>>, + ) -> Result { + pl.base.sender_id = self.config.sender_id.clone(); + pl.base.receiver_id = self.config.receiver_id.clone(); + pl.base.message_type = MessageType::HomeNSReq; + + let mut ans: HomeNSAnsPayload = Default::default(); + self.request(None, &pl, &mut ans, async_resp).await?; + Ok(ans) + } + + pub async fn xmit_data_req( + &self, + target_role: Role, + pl: &mut XmitDataReqPayload, + async_resp: Option>>, + ) -> Result { + pl.base.sender_id = self.config.sender_id.clone(); + pl.base.receiver_id = self.config.receiver_id.clone(); + pl.base.message_type = MessageType::XmitDataReq; + + let mut ans: XmitDataAnsPayload = Default::default(); + self.request(Some(target_role), &pl, &mut ans, async_resp) + .await?; + Ok(ans) + } + + pub async fn xmit_data_ans(&self, target_role: Role, pl: &XmitDataAnsPayload) -> Result<()> { + self.response_request(Some(target_role), pl).await + } + + async fn response_request(&self, target_role: Option, pl: &S) -> Result<()> + where + S: ?Sized + serde::ser::Serialize + BasePayloadResultProvider, + { + let server = if self.config.use_target_role_suffix { + match target_role { + Some(Role::FNS) => format!("{}/fns", self.config.server), + Some(Role::SNS) => format!("{}/sns", self.config.server), + Some(Role::HNS) => format!("{}/hns", self.config.server), + None => self.config.server.clone(), + } + } else { + self.config.server.clone() + }; + + let bp = pl.base_payload(); + + info!(receiver_id = %hex::encode(&bp.base.receiver_id), transaction_id = bp.base.transaction_id, message_type = ?bp.base.message_type, server = %server, "Making request"); + + self.client + .post(&server) + .headers(self.headers.clone()) + .json(pl) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + async fn request( + &self, + target_role: Option, + pl: &S, + ans: &mut D, + async_resp: Option>>, + ) -> Result<()> where S: ?Sized + serde::ser::Serialize + BasePayloadProvider, - D: serde::de::DeserializeOwned, + D: serde::de::DeserializeOwned + BasePayloadResultProvider, { - let base = pl.base_payload(); - let _key = self.get_async_key(base.transaction_id); + let server = if self.config.use_target_role_suffix { + match target_role { + Some(Role::FNS) => format!("{}/fns", self.config.server), + Some(Role::SNS) => format!("{}/sns", self.config.server), + Some(Role::HNS) => format!("{}/hns", self.config.server), + None => self.config.server.clone(), + } + } else { + self.config.server.clone() + }; - let (resp_tx, mut resp_rx): (mpsc::Sender, mpsc::Receiver) = - mpsc::channel(1); - let (_err_tx, mut err_rx): (mpsc::Sender, mpsc::Receiver) = - mpsc::channel(1); + let bp = pl.base_payload().clone(); - // TODO: implement async + info!(receiver_id = %hex::encode(&bp.receiver_id), transaction_id = bp.transaction_id, message_type = ?bp.message_type, server = %server, async_interface = %async_resp.is_some(), "Making request"); let res = self .client - .post(&self.config.server) + .post(&server) .headers(self.headers.clone()) .json(pl) .send() .await? .error_for_status()?; - if !self.is_async() { - resp_tx.send(res.text().await?).await?; - } - - tokio::select! { - err = err_rx.recv() => { - if let Some(err) = err { - return Err(anyhow!("{}", err)); - } - }, - v = resp_rx.recv() => { - if let Some(v) = v { - *ans = serde_json::from_str(&v)?; - } - }, + let resp_json = match async_resp { + Some(rx) => { + let sleep = tokio::time::sleep(self.config.async_timeout); + + tokio::select! { + rx_ans = rx => { + String::from_utf8(rx_ans?)? + } + _ = sleep => { + error!(receiver_id = %hex::encode(&bp.receiver_id), transaction_id = bp.transaction_id, message_type = ?bp.message_type, "Async request timeout"); + return Err(anyhow!("Async timeout")); + } + } + } + None => res.text().await?, + }; + + let base: BasePayloadResult = serde_json::from_str(&resp_json)?; + if base.result.result_code != ResultCode::Success { + error!(result_code = ?base.result.result_code, description = %base.result.description, receiver_id = %hex::encode(&bp.receiver_id), transaction_id = bp.transaction_id, message_type = ?bp.message_type, "Response error"); + return Err(anyhow!( + "Response error, code: {:?}, description: {:?}", + base.result.result_code, + base.result.description + )); } + *ans = serde_json::from_str(&resp_json)?; Ok(()) } - - fn get_async_key(&self, transaction_id: u32) -> String { - format!("backend:async:{}", transaction_id) - } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone)] pub enum MessageType { JoinReq, JoinAns, @@ -224,7 +383,7 @@ impl Default for MessageType { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone)] pub enum ResultCode { Success, MICFailed, @@ -253,32 +412,105 @@ impl Default for ResultCode { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)] pub enum RatePolicy { Drop, Mark, } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] #[serde(default)] pub struct BasePayload { #[serde(rename = "ProtocolVersion")] pub protocol_version: String, - #[serde(rename = "SenderID")] - pub sender_id: String, - #[serde(rename = "ReceiverID")] - pub receiver_id: String, + #[serde(rename = "SenderID", with = "hex_encode")] + pub sender_id: Vec, + #[serde(rename = "ReceiverID", with = "hex_encode")] + pub receiver_id: Vec, #[serde(rename = "TransactionID")] pub transaction_id: u32, #[serde(rename = "MessageType")] pub message_type: MessageType, - #[serde(rename = "SenderToken", with = "hex_encode")] + #[serde( + default, + rename = "SenderToken", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub sender_token: Vec, - #[serde(rename = "ReceiverToken", with = "hex_encode")] + #[serde( + default, + rename = "ReceiverToken", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub receiver_token: Vec, } -#[derive(Serialize, Deserialize, Default)] +impl BasePayload { + pub fn to_base_payload_result( + &self, + res_code: ResultCode, + description: &str, + ) -> BasePayloadResult { + BasePayloadResult { + base: BasePayload { + protocol_version: self.protocol_version.clone(), + sender_id: self.receiver_id.clone(), + receiver_id: self.sender_id.clone(), + transaction_id: self.transaction_id, + message_type: match self.message_type { + MessageType::PRStartReq => MessageType::PRStartAns, + MessageType::PRStopReq => MessageType::PRStopAns, + MessageType::XmitDataReq => MessageType::XmitDataAns, + _ => self.message_type, + }, + sender_token: self.receiver_token.clone(), + receiver_token: self.sender_token.clone(), + }, + result: ResultPayload { + result_code: res_code, + description: description.to_string(), + }, + } + } + + pub fn is_answer(&self) -> bool { + match self.message_type { + MessageType::JoinAns + | MessageType::RejoinAns + | MessageType::AppSKeyAns + | MessageType::PRStartAns + | MessageType::PRStopAns + | MessageType::HomeNSAns + | MessageType::XmitDataAns => true, + + MessageType::JoinReq + | MessageType::RejoinReq + | MessageType::AppSKeyReq + | MessageType::PRStartReq + | MessageType::PRStopReq + | MessageType::HomeNSReq + | MessageType::XmitDataReq => false, + } + } +} + +impl Default for BasePayload { + fn default() -> Self { + BasePayload { + protocol_version: PROTOCOL_VERSION.into(), + sender_id: "".into(), + receiver_id: "".into(), + transaction_id: rand::random(), + message_type: MessageType::default(), + sender_token: vec![], + receiver_token: vec![], + } + } +} + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone)] #[serde(default)] pub struct BasePayloadResult { #[serde(flatten)] @@ -287,7 +519,7 @@ pub struct BasePayloadResult { pub result: ResultPayload, } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone)] pub struct ResultPayload { #[serde(rename = "ResultCode")] pub result_code: ResultCode, @@ -295,10 +527,10 @@ pub struct ResultPayload { pub description: String, } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone)] #[serde(default)] pub struct KeyEnvelope { - #[serde(rename = "KEKLabel")] + #[serde(default, rename = "KEKLabel")] pub kek_label: String, #[serde(rename = "AESKey", with = "hex_encode")] pub aes_key: Vec, @@ -333,7 +565,7 @@ impl KeyEnvelope { } } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, PartialEq, Debug, Clone)] pub struct JoinReqPayload { #[serde(flatten)] pub base: BasePayload, @@ -349,7 +581,12 @@ pub struct JoinReqPayload { pub dl_settings: Vec, #[serde(rename = "RxDelay")] pub rx_delay: u8, - #[serde(rename = "CFList", with = "hex_encode")] + #[serde( + default, + rename = "CFList", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub cf_list: Vec, } @@ -359,7 +596,7 @@ impl BasePayloadProvider for &mut JoinReqPayload { } } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone)] #[serde(default)] pub struct JoinAnsPayload { #[serde(flatten)] @@ -378,11 +615,22 @@ pub struct JoinAnsPayload { pub nwk_s_key: Option, #[serde(rename = "AppSKey")] pub app_s_key: Option, - #[serde(rename = "SessionKeyID", with = "hex_encode")] + #[serde( + default, + rename = "SessionKeyID", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub session_key_id: Vec, } -#[derive(Serialize, Deserialize)] +impl BasePayloadResultProvider for JoinAnsPayload { + fn base_payload(&self) -> &BasePayloadResult { + &self.base + } +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] pub struct RejoinReqPayload { #[serde(flatten)] pub base: BasePayload, @@ -398,11 +646,22 @@ pub struct RejoinReqPayload { pub dl_settings: Vec, #[serde(rename = "RxDelay")] pub rx_delay: u8, - #[serde(rename = "CFList", with = "hex_encode")] + #[serde( + default, + rename = "CFList", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub cf_list: Vec, } -#[derive(Serialize, Deserialize)] +impl BasePayloadProvider for &mut RejoinReqPayload { + fn base_payload(&self) -> &BasePayload { + &self.base + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)] pub struct RejoinAnsPayload { #[serde(flatten)] pub base: BasePayloadResult, @@ -420,11 +679,22 @@ pub struct RejoinAnsPayload { pub nwk_s_key: Option, #[serde(rename = "AppSKey")] pub app_s_key: Option, - #[serde(rename = "SessionKeyID", with = "hex_encode")] + #[serde( + default, + rename = "SessionKeyID", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub session_key_id: Vec, } -#[derive(Serialize, Deserialize)] +impl BasePayloadResultProvider for RejoinAnsPayload { + fn base_payload(&self) -> &BasePayloadResult { + &self.base + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct AppSKeyReqPayload { #[serde(flatten)] pub base: BasePayload, @@ -434,7 +704,13 @@ pub struct AppSKeyReqPayload { pub session_key_id: Vec, } -#[derive(Serialize, Deserialize)] +impl BasePayloadProvider for &mut AppSKeyReqPayload { + fn base_payload(&self) -> &BasePayload { + &self.base + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)] pub struct AppSKeyAnsPayload { #[serde(flatten)] pub base: BasePayloadResult, @@ -446,7 +722,13 @@ pub struct AppSKeyAnsPayload { pub session_key_id: Vec, } -#[derive(Serialize, Deserialize)] +impl BasePayloadResultProvider for AppSKeyAnsPayload { + fn base_payload(&self) -> &BasePayloadResult { + &self.base + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)] pub struct PRStartReqPayload { #[serde(flatten)] pub base: BasePayload, @@ -456,13 +738,29 @@ pub struct PRStartReqPayload { pub ul_meta_data: ULMetaData, } -#[derive(Serialize, Deserialize)] +impl BasePayloadProvider for &mut PRStartReqPayload { + fn base_payload(&self) -> &BasePayload { + &self.base + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)] pub struct PRStartAnsPayload { #[serde(flatten)] pub base: BasePayloadResult, - #[serde(rename = "PHYPayload", with = "hex_encode")] + #[serde( + default, + rename = "PHYPayload", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub phy_payload: Vec, - #[serde(rename = "DevEUI", with = "hex_encode")] + #[serde( + default, + rename = "DevEUI", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub dev_eui: Vec, #[serde(rename = "Lifetime")] pub lifetime: Option, @@ -476,11 +774,22 @@ pub struct PRStartAnsPayload { pub service_profile: Option, #[serde(rename = "DLMetaData")] pub dl_meta_data: Option, - #[serde(rename = "DevAddr", with = "hex_encode")] + #[serde( + default, + rename = "DevAddr", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub dev_addr: Vec, } -#[derive(Serialize, Deserialize)] +impl BasePayloadResultProvider for PRStartAnsPayload { + fn base_payload(&self) -> &BasePayloadResult { + &self.base + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct PRStopReqPayload { #[serde(flatten)] pub base: BasePayload, @@ -490,19 +799,41 @@ pub struct PRStopReqPayload { pub lifetime: Option, } -#[derive(Serialize, Deserialize)] +impl BasePayloadProvider for &mut PRStopReqPayload { + fn base_payload(&self) -> &BasePayload { + &self.base + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)] pub struct PRStopAnsPayload { #[serde(flatten)] pub base: BasePayloadResult, } -#[derive(Serialize, Deserialize)] +impl BasePayloadResultProvider for PRStopAnsPayload { + fn base_payload(&self) -> &BasePayloadResult { + &self.base + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)] pub struct XmitDataReqPayload { #[serde(flatten)] pub base: BasePayload, - #[serde(rename = "PHYPayload", with = "hex_encode")] + #[serde( + default, + rename = "PHYPayload", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub phy_payload: Vec, - #[serde(rename = "FRMPayload", with = "hex_encode")] + #[serde( + default, + rename = "FRMPayload", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub frm_payload: Vec, #[serde(rename = "ULMetaData")] pub ul_meta_data: Option, @@ -510,7 +841,25 @@ pub struct XmitDataReqPayload { pub dl_meta_data: Option, } -#[derive(Serialize, Deserialize)] +impl BasePayloadProvider for &mut XmitDataReqPayload { + fn base_payload(&self) -> &BasePayload { + &self.base + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)] +pub struct XmitDataAnsPayload { + #[serde(flatten)] + pub base: BasePayloadResult, +} + +impl BasePayloadResultProvider for XmitDataAnsPayload { + fn base_payload(&self) -> &BasePayloadResult { + &self.base + } +} + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone)] pub struct HomeNSReqPayload { #[serde(flatten)] pub base: BasePayload, @@ -518,25 +867,46 @@ pub struct HomeNSReqPayload { pub dev_eui: Vec, } -#[derive(Serialize, Deserialize)] +impl BasePayloadProvider for &mut HomeNSReqPayload { + fn base_payload(&self) -> &BasePayload { + &self.base + } +} + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone)] pub struct HomeNSAnsPayload { #[serde(flatten)] pub base: BasePayloadResult, - #[serde(rename = "HNetID", with = "hex_encode")] + #[serde( + default, + rename = "HNetID", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub h_net_id: Vec, } -#[derive(Serialize, Deserialize)] -pub struct XmitDataAnsPayload { - #[serde(flatten)] - pub base: BasePayloadResult, +impl BasePayloadResultProvider for HomeNSAnsPayload { + fn base_payload(&self) -> &BasePayloadResult { + &self.base + } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct ULMetaData { - #[serde(rename = "DevEUI", with = "hex_encode")] + #[serde( + default, + rename = "DevEUI", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub dev_eui: Vec, - #[serde(rename = "DevAddr", with = "hex_encode")] + #[serde( + default, + rename = "DevAddr", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub dev_addr: Vec, #[serde(rename = "FPort")] pub f_port: Option, @@ -545,50 +915,87 @@ pub struct ULMetaData { #[serde(rename = "FCntUp")] pub f_cnt_up: Option, #[serde(rename = "Confirmed")] - pub confirmed: bool, + pub confirmed: Option, #[serde(rename = "DataRate")] - pub data_rate: Option, + pub data_rate: Option, #[serde(rename = "ULFreq")] pub ul_freq: Option, #[serde(rename = "Margin")] pub margin: Option, #[serde(rename = "Battery")] pub battery: Option, - #[serde(rename = "FNSULToken", with = "hex_encode")] + #[serde( + default, + rename = "FNSULToken", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub f_ns_ul_token: Vec, #[serde(rename = "RecvTime")] pub recv_time: DateTime, - #[serde(rename = "RFRegion")] + #[serde(default, rename = "RFRegion", skip_serializing_if = "String::is_empty")] pub rf_region: String, #[serde(rename = "GWCnt")] pub gw_cnt: Option, - #[serde(rename = "GWInfoElement")] - pub gw_info_element: Vec, + #[serde(rename = "GWInfo")] + pub gw_info: Vec, } -#[derive(Serialize, Deserialize)] +impl Default for ULMetaData { + fn default() -> Self { + ULMetaData { + dev_eui: Vec::new(), + dev_addr: Vec::new(), + f_port: None, + f_cnt_down: None, + f_cnt_up: None, + confirmed: None, + data_rate: None, + ul_freq: None, + margin: None, + battery: None, + f_ns_ul_token: Vec::new(), + recv_time: Utc::now(), + rf_region: "".to_string(), + gw_cnt: None, + gw_info: Vec::new(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)] pub struct GWInfoElement { - #[serde(rename = "ID", with = "hex_encode")] + #[serde( + default, + rename = "ID", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub id: Vec, #[serde(rename = "FineRecvTime")] pub fine_recv_time: Option, - #[serde(rename = "RFRegion")] + #[serde(default, rename = "RFRegion", skip_serializing_if = "String::is_empty")] pub rf_region: String, #[serde(rename = "RSSI")] pub rssi: Option, #[serde(rename = "SNR")] - pub snr: Option, + pub snr: Option, #[serde(rename = "Lat")] pub lat: Option, #[serde(rename = "Lon")] pub lon: Option, - #[serde(rename = "ULToken", with = "hex_encode")] + #[serde( + default, + rename = "ULToken", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub ul_token: Vec, #[serde(rename = "DLAllowed")] - pub dl_allowed: bool, + pub dl_allowed: Option, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct ServiceProfile { #[serde(rename = "ServiceProfile")] pub service_profile_id: String, @@ -632,15 +1039,20 @@ pub struct ServiceProfile { pub min_gw_diversity: usize, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)] pub struct DLMetaData { - #[serde(rename = "DevEUI", with = "hex_encode")] + #[serde( + default, + rename = "DevEUI", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub dev_eui: Vec, #[serde(rename = "FPort")] pub f_port: Option, #[serde(rename = "FCntDown")] pub f_cnt_down: Option, - #[serde(rename = "Confirmed")] + #[serde(default, rename = "Confirmed")] pub confirmed: bool, #[serde(rename = "DLFreq1")] pub dl_freq_1: Option, @@ -651,14 +1063,19 @@ pub struct DLMetaData { #[serde(rename = "ClassMode")] pub class_mode: Option, #[serde(rename = "DataRate1")] - pub data_rate_1: Option, + pub data_rate_1: Option, #[serde(rename = "DataRate2")] - pub data_rate_2: Option, - #[serde(rename = "FNSULToken", with = "hex_encode")] + pub data_rate_2: Option, + #[serde( + default, + rename = "FNSULToken", + with = "hex_encode", + skip_serializing_if = "Vec::is_empty" + )] pub f_ns_ul_token: Vec, #[serde(rename = "GWInfo")] pub gw_info: Vec, - #[serde(rename = "HiPriorityFlag")] + #[serde(default, rename = "HiPriorityFlag")] pub hi_priority_flag: bool, } @@ -677,6 +1094,10 @@ mod hex_encode { D: Deserializer<'a>, { let s: &str = serde::de::Deserialize::deserialize(deserializer)?; + + // HEX encoded values may start with 0x prefix, we must strip this. + let s = s.trim_start_matches("0x"); + hex::decode(s).map_err(serde::de::Error::custom) } } @@ -684,6 +1105,8 @@ mod hex_encode { #[cfg(test)] pub mod test { use super::*; + use httpmock::prelude::*; + use tokio::sync::oneshot; #[test] fn test_key_envelope() { @@ -707,4 +1130,131 @@ pub mod test { assert_eq!("test-kek", ke.kek_label); assert_eq!(key, ke.unwrap(&kek).unwrap()); } + + #[tokio::test] + async fn test_async_request() { + let server = MockServer::start(); + + let c = Client::new(ClientConfig { + sender_id: "010203".into(), + receiver_id: "0102030405060708".into(), + server: server.url("/"), + async_timeout: Duration::from_secs(1), + ..Default::default() + }) + .unwrap(); + + let mut req = HomeNSReqPayload { + base: BasePayload { + sender_id: "010203".into(), + receiver_id: "0102030405060708".into(), + message_type: MessageType::HomeNSReq, + transaction_id: 1234, + ..Default::default() + }, + dev_eui: vec![8, 7, 6, 5, 4, 3, 2, 1], + }; + + let ans = HomeNSAnsPayload { + base: BasePayloadResult { + base: BasePayload { + sender_id: "0102030405060708".into(), + receiver_id: "010203".into(), + message_type: MessageType::HomeNSAns, + transaction_id: 1234, + ..Default::default() + }, + result: ResultPayload { + result_code: ResultCode::Success, + description: "".into(), + }, + }, + h_net_id: vec![3, 2, 1], + }; + + let mut mock = server.mock(|when, then| { + when.method(POST) + .path("/") + .body(serde_json::to_string(&req).unwrap()); + then.status(200); + }); + + // OK + let (tx, rx) = oneshot::channel(); + tx.send(serde_json::to_vec(&ans).unwrap()).unwrap(); + let resp = c.home_ns_req(&mut req, Some(rx)).await.unwrap(); + mock.assert(); + mock.delete(); + assert_eq!(resp, ans); + + // Timeout + let (_tx, rx) = oneshot::channel(); + let resp = c.home_ns_req(&mut req, Some(rx)).await; + assert!(resp.is_err()); + } + + #[tokio::test] + async fn test_sync_request() { + let server = MockServer::start(); + + let c = Client::new(ClientConfig { + sender_id: "010203".into(), + receiver_id: "0102030405060708".into(), + server: server.url("/"), + ..Default::default() + }) + .unwrap(); + + let mut req = HomeNSReqPayload { + base: BasePayload { + sender_id: "010203".into(), + receiver_id: "0102030405060708".into(), + message_type: MessageType::HomeNSReq, + transaction_id: 1234, + ..Default::default() + }, + dev_eui: vec![8, 7, 6, 5, 4, 3, 2, 1], + }; + + let ans = HomeNSAnsPayload { + base: BasePayloadResult { + base: BasePayload { + sender_id: "0102030405060708".into(), + receiver_id: "010203".into(), + message_type: MessageType::HomeNSAns, + transaction_id: 1234, + ..Default::default() + }, + result: ResultPayload { + result_code: ResultCode::Success, + description: "".into(), + }, + }, + h_net_id: vec![3, 2, 1], + }; + + // OK + let mut mock = server.mock(|when, then| { + when.method(POST) + .path("/") + .body(serde_json::to_string(&req).unwrap()); + then.body(serde_json::to_vec(&ans).unwrap()).status(200); + }); + let resp = c.home_ns_req(&mut req, None).await.unwrap(); + mock.assert(); + mock.delete(); + assert_eq!(resp, ans); + + // Error status + let mut mock = server.mock(|when, then| { + when.method(POST) + .path("/") + .body(serde_json::to_string(&req).unwrap()); + then.status(500); + }); + let resp = c.home_ns_req(&mut req, None).await; + mock.assert(); + mock.delete(); + assert!(resp.is_err()); + } } diff --git a/chirpstack/Cargo.toml b/chirpstack/Cargo.toml index db76ef4f..5eae4149 100644 --- a/chirpstack/Cargo.toml +++ b/chirpstack/Cargo.toml @@ -63,7 +63,7 @@ prost = "0.10" pbjson-types = "0.3" # gRPC and HTTP multiplexing -warp = "0.3" +warp = { version = "0.3" } hyper = "0.14" tower = "0.4" futures = "0.3" @@ -107,6 +107,7 @@ petgraph = "0.6" # Development and testing [dev-dependencies] httpmock = "0.6" +bytes = "1.1" # Debian packaging. [package.metadata.deb] diff --git a/chirpstack/src/api/backend/mod.rs b/chirpstack/src/api/backend/mod.rs new file mode 100644 index 00000000..6dd2ec38 --- /dev/null +++ b/chirpstack/src/api/backend/mod.rs @@ -0,0 +1,552 @@ +use std::collections::HashMap; +use std::net::SocketAddr; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use redis::streams::StreamReadReply; +use tokio::sync::oneshot; +use tokio::task; +use tracing::{error, info, warn}; +use uuid::Uuid; +use warp::{http::StatusCode, Filter, Reply}; + +use crate::backend::{joinserver, keywrap, roaming}; +use crate::downlink::data_fns; +use crate::storage::{ + device_session, error::Error as StorageError, get_redis_conn, passive_roaming, redis_key, +}; +use crate::uplink::{data_sns, helpers, join_sns, RoamingMetaData, UplinkFrameSet}; +use crate::{config, region}; +use backend::{BasePayload, MessageType}; +use lrwn::region::CommonName; +use lrwn::{AES128Key, NetID, EUI64}; + +pub async fn setup() -> Result<()> { + let conf = config::get(); + if conf.backend_interfaces.bind.is_empty() { + warn!("Backend interfaces API is disabled"); + return Ok(()); + } + + let addr: SocketAddr = conf.backend_interfaces.bind.parse()?; + info!(bind = %conf.backend_interfaces.bind, "Setting up backend interfaces API"); + + let routes = warp::post() + .and(warp::body::content_length_limit(1024 * 16)) + .and(warp::body::aggregate()) + .then(handle_request); + + warp::serve(routes).run(addr).await; + + Ok(()) +} + +pub async fn handle_request(mut body: impl warp::Buf) -> http::Response { + let mut b: Vec = vec![]; + + while body.has_remaining() { + b.extend_from_slice(body.chunk()); + let cnt = body.chunk().len(); + body.advance(cnt); + } + + let bp: BasePayload = match serde_json::from_slice(&b) { + Ok(v) => v, + Err(e) => { + return warp::reply::with_status(e.to_string(), StatusCode::BAD_REQUEST) + .into_response(); + } + }; + + info!(sender_id = %hex::encode(&bp.sender_id), transaction_id = %bp.transaction_id, message_type = ?bp.message_type, "Request received"); + + let sender_client = { + if bp.sender_id.len() == 8 { + // JoinEUI. + let sender_id = match EUI64::from_slice(&bp.sender_id) { + Ok(v) => v, + Err(e) => { + error!(error = %e, "Error decoding SenderID as EUI64"); + let msg = format!("Error decoding SenderID: {}", e); + let pl = bp.to_base_payload_result(backend::ResultCode::MalformedRequest, &msg); + return warp::reply::json(&pl).into_response(); + } + }; + + match joinserver::get(&sender_id) { + Ok(v) => v, + Err(_) => { + error!(sender_id = %sender_id, "Unknown SenderID"); + let msg = format!("Unknown SenderID: {}", sender_id); + let pl = bp.to_base_payload_result(backend::ResultCode::UnknownSender, &msg); + return warp::reply::json(&pl).into_response(); + } + } + } else if bp.sender_id.len() == 3 { + // NetID + let sender_id = match NetID::from_slice(&bp.sender_id) { + Ok(v) => v, + Err(e) => { + error!(error = %e, "Error decoding SenderID as NetID"); + let msg = format!("Error decoding SenderID: {}", e); + let pl = bp.to_base_payload_result(backend::ResultCode::MalformedRequest, &msg); + return warp::reply::json(&pl).into_response(); + } + }; + + match roaming::get(&sender_id) { + Ok(v) => v, + Err(_) => { + error!(sender_id = %sender_id, "Unknown SenderID"); + let msg = format!("Unknown SenderID: {}", sender_id); + let pl = bp.to_base_payload_result(backend::ResultCode::UnknownSender, &msg); + return warp::reply::json(&pl).into_response(); + } + } + } else { + // Unknown size + error!(sender_id = ?bp.sender_id, "Invalid SenderID length"); + let pl = bp.to_base_payload_result( + backend::ResultCode::MalformedRequest, + "Invalid SenderID length", + ); + return warp::reply::json(&pl).into_response(); + } + }; + + // Request is an async answer. + if bp.is_answer() { + tokio::spawn(async move { + if let Err(e) = handle_async_ans(&bp, &b).await { + error!(error = %e, "Handle async answer error"); + } + }); + return warp::reply::with_status("", StatusCode::OK).into_response(); + } + + match bp.message_type { + MessageType::PRStartReq => handle_pr_start_req(sender_client, bp, &b).await, + MessageType::PRStopReq => handle_pr_stop_req(sender_client, bp, &b).await, + MessageType::XmitDataReq => handle_xmit_data_req(sender_client, bp, &b).await, + // Unknown message + _ => warp::reply::with_status( + "Handler for {:?} is not implemented", + StatusCode::INTERNAL_SERVER_ERROR, + ) + .into_response(), + } +} + +fn err_to_response(e: anyhow::Error, bp: &backend::BasePayload) -> http::Response { + let msg = format!("{}", e); + let pl = bp.to_base_payload_result(err_to_result_code(e), &msg); + warp::reply::json(&pl).into_response() +} + +fn err_to_result_code(e: anyhow::Error) -> backend::ResultCode { + if let Some(e) = e.downcast_ref::() { + return match e { + StorageError::NotFound(_) => backend::ResultCode::UnknownDevAddr, + StorageError::InvalidMIC | StorageError::InvalidDevNonce => { + backend::ResultCode::MICFailed + } + _ => backend::ResultCode::Other, + }; + } + backend::ResultCode::Other +} + +async fn handle_pr_start_req( + sender_client: Arc, + bp: backend::BasePayload, + b: &[u8], +) -> http::Response { + if sender_client.is_async() { + let b = b.to_vec(); + task::spawn(async move { + let ans = match _handle_pr_start_req(&b).await { + Ok(v) => v, + Err(e) => { + let msg = e.to_string(); + backend::PRStartAnsPayload { + base: bp.to_base_payload_result(err_to_result_code(e), &msg), + ..Default::default() + } + } + }; + + if let Err(e) = sender_client.pr_start_ans(backend::Role::FNS, &ans).await { + error!(error = %e, "Send async PRStartAns error"); + } + }); + + warp::reply::with_status("", StatusCode::OK).into_response() + } else { + match _handle_pr_start_req(b).await { + Ok(v) => warp::reply::json(&v).into_response(), + Err(e) => err_to_response(e, &bp), + } + } +} + +async fn _handle_pr_start_req(b: &[u8]) -> Result { + let pl: backend::PRStartReqPayload = serde_json::from_slice(b)?; + let phy = lrwn::PhyPayload::from_slice(&pl.phy_payload)?; + + if phy.mhdr.m_type == lrwn::MType::JoinRequest { + _handle_pr_start_req_join(pl, phy).await + } else { + _handle_pr_start_req_data(pl, phy).await + } +} + +async fn _handle_pr_start_req_join( + pl: backend::PRStartReqPayload, + phy: lrwn::PhyPayload, +) -> Result { + let rx_info = roaming::ul_meta_data_to_rx_info(&pl.ul_meta_data)?; + let tx_info = roaming::ul_meta_data_to_tx_info(&pl.ul_meta_data)?; + let region_common_name = CommonName::from_str(&pl.ul_meta_data.rf_region)?; + let region_name = region::get_region_name(region_common_name)?; + let dr = pl.ul_meta_data.data_rate.unwrap_or_default(); + + let ufs = UplinkFrameSet { + uplink_set_id: Uuid::new_v4(), + dr, + ch: helpers::get_uplink_ch(®ion_name, tx_info.frequency, dr)?, + phy_payload: phy, + tx_info, + rx_info_set: rx_info, + gateway_private_map: HashMap::new(), + gateway_tenant_id_map: HashMap::new(), + region_common_name, + region_name, + roaming_meta_data: Some(RoamingMetaData { + base_payload: pl.base.clone(), + ul_meta_data: pl.ul_meta_data.clone(), + }), + }; + + join_sns::JoinRequest::start_pr(ufs, pl).await +} + +async fn _handle_pr_start_req_data( + pl: backend::PRStartReqPayload, + phy: lrwn::PhyPayload, +) -> Result { + let sender_id = NetID::from_slice(&pl.base.sender_id)?; + let rx_info = roaming::ul_meta_data_to_rx_info(&pl.ul_meta_data)?; + let tx_info = roaming::ul_meta_data_to_tx_info(&pl.ul_meta_data)?; + let region_common_name = CommonName::from_str(&pl.ul_meta_data.rf_region)?; + let region_name = region::get_region_name(region_common_name)?; + let dr = pl.ul_meta_data.data_rate.unwrap_or_default(); + + let ufs = UplinkFrameSet { + uplink_set_id: Uuid::new_v4(), + dr, + ch: helpers::get_uplink_ch(®ion_name, tx_info.frequency, dr)?, + phy_payload: phy, + tx_info, + rx_info_set: rx_info, + gateway_private_map: HashMap::new(), + gateway_tenant_id_map: HashMap::new(), + region_common_name, + region_name, + roaming_meta_data: Some(RoamingMetaData { + base_payload: pl.base.clone(), + ul_meta_data: pl.ul_meta_data.clone(), + }), + }; + + // get device-session + let ds = device_session::get_for_phypayload(&ufs.phy_payload, ufs.dr, ufs.ch as u8).await?; + let pr_lifetime = roaming::get_passive_roaming_lifetime(sender_id)?; + let kek_label = roaming::get_passive_roaming_kek_label(sender_id)?; + + let nwk_s_key = if ds.mac_version().to_string().starts_with("1.0") { + Some(keywrap::wrap( + &kek_label, + AES128Key::from_slice(&ds.nwk_s_enc_key)?, + )?) + } else { + None + }; + + let f_nwk_s_int_key = if ds.mac_version().to_string().starts_with("1.0") { + None + } else { + Some(keywrap::wrap( + &kek_label, + AES128Key::from_slice(&ds.f_nwk_s_int_key)?, + )?) + }; + + // In case of stateless, the payload is directly handled + if pr_lifetime.is_zero() { + data_sns::Data::handle(ufs).await; + } + + Ok(backend::PRStartAnsPayload { + base: pl + .base + .to_base_payload_result(backend::ResultCode::Success, ""), + dev_eui: ds.dev_eui.clone(), + lifetime: if pr_lifetime.is_zero() { + None + } else { + Some(pr_lifetime.as_secs() as usize) + }, + f_nwk_s_int_key, + nwk_s_key, + f_cnt_up: Some(ds.f_cnt_up), + ..Default::default() + }) +} + +async fn handle_pr_stop_req( + sender_client: Arc, + bp: backend::BasePayload, + b: &[u8], +) -> http::Response { + if sender_client.is_async() { + let b = b.to_vec(); + task::spawn(async move { + let ans = match _handle_pr_stop_req(&b).await { + Ok(v) => v, + Err(e) => { + let msg = e.to_string(); + backend::PRStopAnsPayload { + base: bp.to_base_payload_result(err_to_result_code(e), &msg), + } + } + }; + + if let Err(e) = sender_client.pr_stop_ans(backend::Role::SNS, &ans).await { + error!(error = %e, "Send async PRStopAns error"); + } + }); + + warp::reply::with_status("", StatusCode::OK).into_response() + } else { + match _handle_pr_stop_req(b).await { + Ok(v) => warp::reply::json(&v).into_response(), + Err(e) => err_to_response(e, &bp), + } + } +} + +async fn _handle_pr_stop_req(b: &[u8]) -> Result { + let pl: backend::PRStopReqPayload = serde_json::from_slice(b)?; + let dev_eui = EUI64::from_slice(&pl.dev_eui)?; + + let sess_ids = passive_roaming::get_session_ids_for_dev_eui(dev_eui).await?; + if sess_ids.is_empty() { + return Ok(backend::PRStopAnsPayload { + base: pl + .base + .to_base_payload_result(backend::ResultCode::UnknownDevEUI, ""), + }); + } + + for sess_id in sess_ids { + if let Err(e) = passive_roaming::delete(sess_id).await { + error!(error = %e, "Delete passive-roaming device-session error"); + } + } + + Ok(backend::PRStopAnsPayload { + base: pl + .base + .to_base_payload_result(backend::ResultCode::Success, ""), + }) +} + +async fn handle_xmit_data_req( + sender_client: Arc, + bp: backend::BasePayload, + b: &[u8], +) -> http::Response { + let pl: backend::XmitDataReqPayload = match serde_json::from_slice(b) { + Ok(v) => v, + Err(e) => { + return err_to_response(anyhow::Error::new(e), &bp); + } + }; + + if sender_client.is_async() { + task::spawn(async move { + let sender_role = if pl.ul_meta_data.is_some() { + backend::Role::FNS + } else { + backend::Role::SNS + }; + + let ans = match _handle_xmit_data_req(pl).await { + Ok(v) => v, + Err(e) => { + let msg = e.to_string(); + backend::XmitDataAnsPayload { + base: bp.to_base_payload_result(err_to_result_code(e), &msg), + } + } + }; + + if let Err(e) = sender_client.xmit_data_ans(sender_role, &ans).await { + error!(error = %e, "Send async XmitDataAns error"); + } + }); + + warp::reply::with_status("", StatusCode::OK).into_response() + } else { + match _handle_xmit_data_req(pl).await { + Ok(v) => warp::reply::json(&v).into_response(), + Err(e) => err_to_response(e, &bp), + } + } +} + +async fn _handle_xmit_data_req( + pl: backend::XmitDataReqPayload, +) -> Result { + if let Some(ul_meta_data) = &pl.ul_meta_data { + let rx_info = roaming::ul_meta_data_to_rx_info(ul_meta_data)?; + let tx_info = roaming::ul_meta_data_to_tx_info(ul_meta_data)?; + let region_common_name = CommonName::from_str(&ul_meta_data.rf_region)?; + let region_name = region::get_region_name(region_common_name)?; + let dr = ul_meta_data.data_rate.unwrap_or_default(); + let phy = lrwn::PhyPayload::from_slice(&pl.phy_payload)?; + + let ufs = UplinkFrameSet { + uplink_set_id: Uuid::new_v4(), + dr, + ch: helpers::get_uplink_ch(®ion_name, tx_info.frequency, dr)?, + phy_payload: phy, + tx_info, + rx_info_set: rx_info, + gateway_private_map: HashMap::new(), + gateway_tenant_id_map: HashMap::new(), + region_common_name, + region_name, + roaming_meta_data: Some(RoamingMetaData { + base_payload: pl.base.clone(), + ul_meta_data: ul_meta_data.clone(), + }), + }; + + data_sns::Data::handle(ufs).await; + } + + if let Some(dl_meta_data) = &pl.dl_meta_data { + data_fns::Data::handle(pl.clone(), dl_meta_data.clone()).await?; + } + + Ok(backend::XmitDataAnsPayload { + base: pl + .base + .to_base_payload_result(backend::ResultCode::Success, ""), + }) +} + +async fn handle_async_ans(bp: &BasePayload, b: &[u8]) -> Result> { + task::spawn_blocking({ + let b = b.to_vec(); + let transaction_id = bp.transaction_id; + move || -> Result<()> { + let mut c = get_redis_conn()?; + let key = redis_key(format!("backend:async:{}", transaction_id)); + + redis::pipe() + .atomic() + .cmd("XADD") + .arg(&key) + .arg("MAXLEN") + .arg(1_i64) + .arg("*") + .arg("pl") + .arg(&b) + .ignore() + .cmd("EXPIRE") + .arg(&key) + .arg(30_i64) + .ignore() + .query(&mut *c)?; + + Ok(()) + } + }) + .await??; + + Ok(warp::reply().into_response()) +} + +pub async fn get_async_receiver( + transaction_id: u32, + timeout: Duration, +) -> Result>> { + let (tx, rx) = oneshot::channel(); + + task::spawn_blocking(move || -> Result<()> { + let mut c = get_redis_conn()?; + let key = redis_key(format!("backend:async:{}", transaction_id)); + + let srr: StreamReadReply = redis::cmd("XREAD") + .arg("BLOCK") + .arg(timeout.as_millis() as u64) + .arg("COUNT") + .arg(1_u64) + .arg("STREAMS") + .arg(&key) + .arg("0") + .query(&mut *c)?; + + for stream_key in &srr.keys { + for stream_id in &stream_key.ids { + for (k, v) in &stream_id.map { + match k.as_ref() { + "pl" => { + if let redis::Value::Data(b) = v { + let _ = tx.send(b.to_vec()); + return Ok(()); + } + } + _ => { + error!(key = %k, "Unexpected key in async stream"); + } + } + } + } + } + + Ok(()) + }); + + Ok(rx) +} + +#[cfg(test)] +pub mod test { + use super::*; + use crate::test; + + #[tokio::test] + async fn test_async_response() { + let _guard = test::prepare().await; + + let bp = BasePayload { + transaction_id: 1234, + ..Default::default() + }; + + let b = vec![1, 2, 3, 4]; + handle_async_ans(&bp, &b).await.unwrap(); + + let rx = get_async_receiver(1234, Duration::from_millis(100)) + .await + .unwrap(); + + let rx_b = rx.await.unwrap(); + assert_eq!(b, rx_b); + } +} diff --git a/chirpstack/src/api/mod.rs b/chirpstack/src/api/mod.rs index b5cbb990..c40ccdcb 100644 --- a/chirpstack/src/api/mod.rs +++ b/chirpstack/src/api/mod.rs @@ -9,6 +9,7 @@ use anyhow::Result; use futures::future::{self, Either, TryFutureExt}; use hyper::{service::make_service_fn, Server}; use rust_embed::RustEmbed; +use tokio::try_join; use tonic::transport::Server as TonicServer; use tonic_reflection::server::Builder as TonicReflectionBuilder; use tower::{Service, ServiceBuilder}; @@ -31,6 +32,7 @@ use crate::api::auth::validator; pub mod application; pub mod auth; +pub mod backend; pub mod device; pub mod device_profile; pub mod device_profile_template; @@ -178,7 +180,10 @@ pub async fn setup() -> Result<()> { )) }); - Server::bind(&addr).serve(service).await?; + let backend_handle = tokio::spawn(backend::setup()); + let api_handle = tokio::spawn(Server::bind(&addr).serve(service)); + + let _ = try_join!(api_handle, backend_handle)?; Ok(()) } diff --git a/chirpstack/src/backend/joinserver.rs b/chirpstack/src/backend/joinserver.rs index 05337064..4b1ffe9a 100644 --- a/chirpstack/src/backend/joinserver.rs +++ b/chirpstack/src/backend/joinserver.rs @@ -23,12 +23,13 @@ pub fn setup() -> Result<()> { info!("Configuring Join Server"); let c = Client::new(ClientConfig { - sender_id: conf.network.net_id.to_string(), - receiver_id: js.join_eui.to_string(), + sender_id: conf.network.net_id.to_vec(), + receiver_id: js.join_eui.to_vec(), server: js.server.clone(), ca_cert: js.ca_cert.clone(), tls_cert: js.tls_cert.clone(), tls_key: js.tls_key.clone(), + async_timeout: js.async_timeout, ..Default::default() })?; @@ -55,3 +56,9 @@ pub fn get(join_eui: &EUI64) -> Result> { })? .clone()) } + +#[cfg(test)] +pub fn reset() { + let mut clients_w = CLIENTS.write().unwrap(); + *clients_w = HashMap::new(); +} diff --git a/chirpstack/src/backend/keywrap.rs b/chirpstack/src/backend/keywrap.rs index 87f6ff05..43fc1cdb 100644 --- a/chirpstack/src/backend/keywrap.rs +++ b/chirpstack/src/backend/keywrap.rs @@ -23,3 +23,18 @@ pub fn unwrap(ke: &KeyEnvelope) -> Result { Err(anyhow!("KEK label {} does not exist", ke.kek_label)) } + +pub fn wrap(label: &str, key: AES128Key) -> Result { + if label.is_empty() { + return KeyEnvelope::new("", None, &key.to_bytes()); + } + + let conf = config::get(); + for kek in &conf.keks { + if kek.label == *label { + return KeyEnvelope::new(label, Some(&kek.kek.to_bytes()), &key.to_bytes()); + } + } + + Err(anyhow!("KEK label {} does not exist", label)) +} diff --git a/chirpstack/src/backend/mod.rs b/chirpstack/src/backend/mod.rs index bad0e332..a950f3f8 100644 --- a/chirpstack/src/backend/mod.rs +++ b/chirpstack/src/backend/mod.rs @@ -1,2 +1,12 @@ +use anyhow::Result; + pub mod joinserver; pub mod keywrap; +pub mod roaming; + +pub fn setup() -> Result<()> { + joinserver::setup()?; + roaming::setup()?; + + Ok(()) +} diff --git a/chirpstack/src/backend/roaming.rs b/chirpstack/src/backend/roaming.rs new file mode 100644 index 00000000..961632a7 --- /dev/null +++ b/chirpstack/src/backend/roaming.rs @@ -0,0 +1,320 @@ +use std::collections::HashMap; +use std::io::Cursor; +use std::str::FromStr; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; +use chrono::{Duration, DurationRound}; +use prost::Message; +use tracing::{debug, info, span, Level}; + +use crate::config; +use crate::gpstime::ToGpsTime; +use backend::{Client, ClientConfig, GWInfoElement, ULMetaData}; +use chirpstack_api::{common, gw}; +use lrwn::{region, DevAddr, NetID, EUI64}; + +lazy_static! { + static ref CLIENTS: RwLock>> = RwLock::new(HashMap::new()); +} + +pub fn setup() -> Result<()> { + info!("Setting up roaming clients"); + let conf = config::get(); + + for s in &conf.roaming.servers { + let span = span!(Level::INFO, "setup", net_id = %s.net_id); + let _guard = span.enter(); + + let server = if s.server.is_empty() { + format!( + "https://{}{}", + s.net_id, conf.roaming.resolve_net_id_domain_suffix, + ) + } else { + s.server.clone() + }; + + info!( + passive_roaming_lifetime = ?s.passive_roaming_lifetime, + server = %server, + async_timeout = ?s.async_timeout, + "Configuring roaming client" + ); + + let c = Client::new(ClientConfig { + sender_id: conf.network.net_id.to_vec(), + receiver_id: s.net_id.to_vec(), + server, + use_target_role_suffix: s.use_target_role_suffix, + ca_cert: s.ca_cert.clone(), + tls_cert: s.tls_cert.clone(), + tls_key: s.tls_key.clone(), + authorization: if s.authorization_header.is_empty() { + None + } else { + Some(s.authorization_header.clone()) + }, + async_timeout: s.async_timeout, + })?; + + set(&s.net_id, c); + } + + Ok(()) +} + +pub fn set(net_id: &NetID, c: Client) { + let mut clients_w = CLIENTS.write().unwrap(); + clients_w.insert(*net_id, Arc::new(c)); +} + +pub fn get(net_id: &NetID) -> Result> { + let clients_r = CLIENTS.write().unwrap(); + + if let Some(client) = clients_r.get(net_id) { + return Ok(client.clone()); + } + + let conf = config::get(); + if conf.roaming.default.enabled { + debug!(net_id = %net_id, "Configuring default roaming client"); + + let server = if conf.roaming.default.server.is_empty() { + format!( + "https://{}{}", + net_id, conf.roaming.resolve_net_id_domain_suffix, + ) + } else { + conf.roaming.default.server.clone() + }; + + let c = Client::new(ClientConfig { + sender_id: conf.network.net_id.to_vec(), + receiver_id: net_id.to_vec(), + server, + use_target_role_suffix: conf.roaming.default.use_target_role_suffix, + ca_cert: conf.roaming.default.ca_cert.clone(), + tls_cert: conf.roaming.default.tls_cert.clone(), + tls_key: conf.roaming.default.tls_key.clone(), + authorization: if conf.roaming.default.authorization_header.is_empty() { + None + } else { + Some(conf.roaming.default.authorization_header.clone()) + }, + async_timeout: conf.roaming.default.async_timeout, + })?; + + return Ok(Arc::new(c)); + } + + Err(anyhow!( + "Roaming client for net_id {} does not exist", + net_id + )) +} + +pub fn get_passive_roaming_lifetime(net_id: NetID) -> Result { + let conf = config::get(); + + for s in &conf.roaming.servers { + if s.net_id == net_id { + return Ok(s.passive_roaming_lifetime); + } + } + + if conf.roaming.default.enabled { + return Ok(conf.roaming.default.passive_roaming_lifetime); + } + + Err(anyhow!( + "Passive-roaming lifetime for net_id {} does not exist", + net_id + )) +} + +pub fn get_passive_roaming_kek_label(net_id: NetID) -> Result { + let conf = config::get(); + + for s in &conf.roaming.servers { + if s.net_id == net_id { + return Ok(s.passive_roaming_kek_label.clone()); + } + } + + Err(anyhow!( + "Passive-roaming kek-label for net_id {} does not exist", + net_id + )) +} + +pub fn is_enabled() -> bool { + let clients_r = CLIENTS.read().unwrap(); + !clients_r.is_empty() +} + +pub fn is_roaming_dev_addr(dev_addr: DevAddr) -> bool { + let conf = config::get(); + + if !is_enabled() { + return false; + } + + for net_id in &[ + // Configured NetID. + conf.network.net_id, + // Test NetIDs. For roaming it is expected that non-testing NetIDs will be used. These are + // included as non-roaming NetIDs as one might start with a test-NetID and then aquires an + // official NetID to setup roaming. Not including these would mean that all devices must + // re-join to obtain a new DevAddr. + NetID::from_be_bytes([0, 0, 0]), + NetID::from_be_bytes([0, 0, 1]), + ] { + if dev_addr.is_net_id(*net_id) { + return false; + } + } + + true +} + +pub fn get_net_ids_for_dev_addr(dev_addr: DevAddr) -> Vec { + let mut out: Vec = Vec::new(); + let conf = config::get(); + + for agreement in &conf.roaming.servers { + if dev_addr.is_net_id(agreement.net_id) { + out.push(agreement.net_id); + } + } + + out +} + +pub fn rx_info_to_gw_info(rx_info_set: &[gw::UplinkRxInfo]) -> Result> { + let mut out: Vec = Vec::new(); + + for rx_info in rx_info_set { + let gw_id = EUI64::from_str(&rx_info.gateway_id)?; + + out.push(GWInfoElement { + id: gw_id.to_be_bytes()[4..8].to_vec(), + fine_recv_time: rx_info + .fine_time_since_gps_epoch + .as_ref() + .map(|v| v.nanos as usize), + rf_region: "".to_string(), + rssi: Some(rx_info.rssi as isize), + snr: Some(rx_info.snr), + lat: rx_info.location.as_ref().map(|v| v.latitude), + lon: rx_info.location.as_ref().map(|v| v.longitude), + ul_token: rx_info.encode_to_vec(), + dl_allowed: Some(true), + }); + } + + Ok(out) +} + +pub fn ul_meta_data_to_rx_info(ul_meta_data: &ULMetaData) -> Result> { + let mut out: Vec = Vec::new(); + for gw_info in &ul_meta_data.gw_info { + out.push(gw::UplinkRxInfo { + gateway_id: hex::encode(&gw_info.id), + context: gw_info.ul_token.clone(), + rssi: gw_info.rssi.unwrap_or_default() as i32, + snr: gw_info.snr.unwrap_or_default() as f32, + location: if gw_info.lat.is_some() && gw_info.lon.is_some() { + Some(common::Location { + latitude: gw_info.lat.unwrap(), + longitude: gw_info.lon.unwrap(), + ..Default::default() + }) + } else { + None + }, + fine_time_since_gps_epoch: if gw_info.fine_recv_time.is_some() { + let ts = ul_meta_data + .recv_time + .duration_round(Duration::seconds(1))?; + let ts = ts + Duration::nanoseconds(gw_info.fine_recv_time.unwrap() as i64); + + Some(ts.to_gps_time().to_std()?.into()) + } else { + None + }, + ..Default::default() + }); + } + + Ok(out) +} + +pub fn ul_meta_data_to_tx_info(ul_meta_data: &ULMetaData) -> Result { + let region_cn = region::CommonName::from_str(&ul_meta_data.rf_region)?; + let region_conf = region::get(region_cn, false, false); + let dr = match ul_meta_data.data_rate { + Some(v) => v, + None => { + return Err(anyhow!("DataRate is not set")); + } + }; + let freq = match ul_meta_data.ul_freq { + Some(v) => (v * 1_000_000.0) as u32, + None => { + return Err(anyhow!("ULFreq is not set")); + } + }; + let params = region_conf.get_data_rate(dr)?; + + Ok(gw::UplinkTxInfo { + frequency: freq, + modulation: Some(gw::Modulation { + parameters: Some(match params { + lrwn::region::DataRateModulation::Lora(v) => { + gw::modulation::Parameters::Lora(gw::LoraModulationInfo { + bandwidth: v.bandwidth, + spreading_factor: v.spreading_factor as u32, + code_rate: gw::CodeRate::Cr45.into(), + code_rate_legacy: "".into(), + polarization_inversion: true, + }) + } + lrwn::region::DataRateModulation::Fsk(v) => { + gw::modulation::Parameters::Fsk(gw::FskModulationInfo { + datarate: v.bitrate, + ..Default::default() + }) + } + lrwn::region::DataRateModulation::LrFhss(v) => { + gw::modulation::Parameters::LrFhss(gw::LrFhssModulationInfo { + operating_channel_width: v.occupied_channel_width, + code_rate: v.coding_rate, + // GridSteps: this value can't be derived from a DR? + ..Default::default() + }) + } + }), + }), + }) +} + +pub fn dl_meta_data_to_uplink_rx_info( + dl_meta: &backend::DLMetaData, +) -> Result> { + let mut out: Vec = Vec::new(); + + for gw_info in &dl_meta.gw_info { + out.push(gw::UplinkRxInfo::decode(&mut Cursor::new( + &gw_info.ul_token, + ))?); + } + + Ok(out) +} + +#[cfg(test)] +pub fn reset() { + let mut clients_w = CLIENTS.write().unwrap(); + *clients_w = HashMap::new(); +} diff --git a/chirpstack/src/cmd/root.rs b/chirpstack/src/cmd/root.rs index 4377f59a..6b308a47 100644 --- a/chirpstack/src/cmd/root.rs +++ b/chirpstack/src/cmd/root.rs @@ -12,7 +12,7 @@ pub async fn run() -> Result<()> { ); region::setup()?; - backend::joinserver::setup()?; + backend::setup()?; adr::setup().await?; integration::setup().await?; gateway::backend::setup().await?; diff --git a/chirpstack/src/config.rs b/chirpstack/src/config.rs index 1c0813df..7b120347 100644 --- a/chirpstack/src/config.rs +++ b/chirpstack/src/config.rs @@ -27,6 +27,8 @@ pub struct Configuration { pub codec: Codec, pub user_authentication: UserAuthentication, pub join_server: JoinServer, + pub backend_interfaces: BackendInterfaces, + pub roaming: Roaming, pub keks: Vec, pub regions: Vec, } @@ -45,6 +47,8 @@ impl Default for Configuration { codec: Default::default(), user_authentication: Default::default(), join_server: Default::default(), + backend_interfaces: Default::default(), + roaming: Default::default(), keks: Vec::new(), regions: vec![Default::default()], } @@ -350,14 +354,64 @@ pub struct JoinServer { pub struct JoinServerServer { pub join_eui: EUI64, pub server: String, - pub async_interface: bool, #[serde(with = "humantime_serde")] - pub async_interface_timeout: Duration, + pub async_timeout: Duration, pub ca_cert: String, pub tls_cert: String, pub tls_key: String, } +#[derive(Serialize, Deserialize, Default, Clone)] +#[serde(default)] +pub struct Roaming { + pub resolve_net_id_domain_suffix: String, + pub servers: Vec, + pub default: RoamingServerDefault, +} + +#[derive(Serialize, Deserialize, Default, Clone)] +#[serde(default)] +pub struct BackendInterfaces { + pub bind: String, + pub ca_cert: String, + pub tls_cert: String, + pub tls_key: String, +} + +#[derive(Serialize, Deserialize, Default, Clone)] +#[serde(default)] +pub struct RoamingServer { + pub net_id: NetID, + #[serde(with = "humantime_serde")] + pub async_timeout: Duration, + #[serde(with = "humantime_serde")] + pub passive_roaming_lifetime: Duration, + pub passive_roaming_kek_label: String, + pub server: String, + pub use_target_role_suffix: bool, + pub ca_cert: String, + pub tls_cert: String, + pub tls_key: String, + pub authorization_header: String, +} + +#[derive(Serialize, Deserialize, Default, Clone)] +#[serde(default)] +pub struct RoamingServerDefault { + pub enabled: bool, + #[serde(with = "humantime_serde")] + pub async_timeout: Duration, + #[serde(with = "humantime_serde")] + pub passive_roaming_lifetime: Duration, + pub passive_roaming_kek_label: String, + pub server: String, + pub use_target_role_suffix: bool, + pub ca_cert: String, + pub tls_cert: String, + pub tls_key: String, + pub authorization_header: String, +} + #[derive(Serialize, Deserialize, Default, Clone)] #[serde(default)] pub struct Kek { diff --git a/chirpstack/src/downlink/data.rs b/chirpstack/src/downlink/data.rs index 458f8f79..203c471d 100644 --- a/chirpstack/src/downlink/data.rs +++ b/chirpstack/src/downlink/data.rs @@ -7,8 +7,10 @@ use chrono::{DateTime, Utc}; use rand::Rng; use tracing::{span, trace, warn, Instrument, Level}; +use crate::api::backend::get_async_receiver; use crate::api::helpers::FromProto; -use crate::downlink::{classb, helpers}; +use crate::backend::roaming; +use crate::downlink::{classb, helpers, tx_ack}; use crate::gpstime::{ToDateTime, ToGpsTime}; use crate::storage; use crate::storage::{ @@ -18,7 +20,7 @@ use crate::storage::{ use crate::uplink::UplinkFrameSet; use crate::{adr, config, gateway, integration, maccommand, region, sensitivity}; use chirpstack_api::{gw, integration as integration_pb, internal}; -use lrwn::DevAddr; +use lrwn::{DevAddr, NetID}; struct DownlinkFrameItem { downlink_frame_item: gw::DownlinkFrameItem, @@ -139,7 +141,12 @@ impl Data { ctx.set_phy_payloads()?; ctx.update_device_queue_item().await?; ctx.save_downlink_frame().await?; - ctx.send_downlink_frame().await?; + if ctx._is_roaming() { + ctx.send_downlink_frame_passive_roaming().await?; + ctx.handle_passive_roaming_tx_ack().await?; + } else { + ctx.send_downlink_frame().await?; + } } // Some mac-commands set their state (e.g. last requested) to the device-session. @@ -415,6 +422,14 @@ impl Data { &self.device.enabled_class == "C" } + fn _is_roaming(&self) -> bool { + self.uplink_frame_set + .as_ref() + .unwrap() + .roaming_meta_data + .is_some() + } + fn set_phy_payloads(&mut self) -> Result<()> { trace!("Setting downlink PHYPayloads"); let mut f_pending = self.more_device_queue_items; @@ -658,6 +673,86 @@ impl Data { Ok(()) } + async fn send_downlink_frame_passive_roaming(&self) -> Result<()> { + trace!("Sending downlink-frame using passive-roaming"); + + let ufs = self.uplink_frame_set.as_ref().unwrap(); + + let roaming_meta = ufs.roaming_meta_data.as_ref().unwrap(); + + let net_id = NetID::from_slice(&roaming_meta.base_payload.sender_id)?; + let client = roaming::get(&net_id)?; + + let mut req = backend::XmitDataReqPayload { + phy_payload: self.downlink_frame.items[0].phy_payload.clone(), + dl_meta_data: Some(backend::DLMetaData { + class_mode: Some("A".to_string()), + dev_eui: self.device_session.dev_eui.clone(), + f_ns_ul_token: roaming_meta.ul_meta_data.f_ns_ul_token.clone(), + dl_freq_1: { + let rx1_freq = self + .region_conf + .get_rx1_frequency_for_uplink_frequency(ufs.tx_info.frequency)?; + Some(rx1_freq as f64 / 1_000_000.0) + }, + dl_freq_2: Some(self.device_session.rx2_frequency as f64 / 1_000_000.0), + data_rate_1: { + let rx1_dr = self.region_conf.get_rx1_data_rate_index( + self.device_session.dr as u8, + self.device_session.rx1_dr_offset as usize, + )?; + Some(rx1_dr) + }, + data_rate_2: Some(self.device_session.rx2_dr as u8), + rx_delay_1: Some(self.device_session.rx1_delay as usize), + gw_info: roaming_meta + .ul_meta_data + .gw_info + .iter() + .filter(|gw| gw.dl_allowed.unwrap_or_default()) + .map(|gw| backend::GWInfoElement { + ul_token: gw.ul_token.clone(), + ..Default::default() + }) + .collect(), + ..Default::default() + }), + ..Default::default() + }; + + #[cfg(test)] + { + req.base.transaction_id = 1234 + } + + let async_receiver = match client.is_async() { + false => None, + true => { + Some(get_async_receiver(req.base.transaction_id, client.get_async_timeout()).await?) + } + }; + client + .xmit_data_req(backend::Role::FNS, &mut req, async_receiver) + .await?; + + Ok(()) + } + + async fn handle_passive_roaming_tx_ack(&self) -> Result<()> { + trace!("Handle passive-roaming tx-ack"); + + tx_ack::TxAck::handle(gw::DownlinkTxAck { + downlink_id: self.downlink_frame.downlink_id, + items: vec![gw::DownlinkTxAckItem { + status: gw::TxAckStatus::Ok.into(), + }], + ..Default::default() + }) + .await; + + Ok(()) + } + async fn _request_custom_channel_reconfiguration(&mut self) -> Result<()> { trace!("Requesting custom channel re-configuration"); let mut wanted_channels: HashMap = HashMap::new(); diff --git a/chirpstack/src/downlink/data_fns.rs b/chirpstack/src/downlink/data_fns.rs new file mode 100644 index 00000000..44e2a88a --- /dev/null +++ b/chirpstack/src/downlink/data_fns.rs @@ -0,0 +1,174 @@ +use std::str::FromStr; + +use anyhow::{Context, Result}; +use rand::Rng; +use tracing::{span, trace, Instrument, Level}; + +use super::helpers; +use crate::backend::roaming; +use crate::storage::downlink_frame; +use crate::{gateway, region}; +use chirpstack_api::{gw, internal}; +use lrwn::region::CommonName; + +pub struct Data { + region_name: String, + region_common_name: CommonName, + xmit_data_req: backend::XmitDataReqPayload, + dl_meta_data: backend::DLMetaData, + uplink_rx_info: Vec, + downlink_frame: gw::DownlinkFrame, +} + +impl Data { + pub async fn handle( + pl: backend::XmitDataReqPayload, + dl_meta: backend::DLMetaData, + ) -> Result<()> { + let span = span!(Level::INFO, "xmit_data_req_pr"); + Data::_handle(pl, dl_meta).instrument(span).await + } + + async fn _handle(pl: backend::XmitDataReqPayload, dl_meta: backend::DLMetaData) -> Result<()> { + let mut uplink_rx_info = roaming::dl_meta_data_to_uplink_rx_info(&dl_meta)?; + uplink_rx_info.sort_by(|a, b| { + if a.snr == b.snr { + return a.rssi.partial_cmp(&b.rssi).unwrap(); + } + b.snr.partial_cmp(&a.snr).unwrap() + }); + + if uplink_rx_info.is_empty() { + return Err(anyhow!("DLMetaData is not set")); + } + + let region_name = uplink_rx_info[0] + .get_metadata_string("region_name") + .ok_or(anyhow!("No region_name in metadata"))?; + + let region_common_name = uplink_rx_info[0] + .get_metadata_string("region_common_name") + .ok_or(anyhow!("No region_common_name in metadata"))?; + let region_common_name = CommonName::from_str(®ion_common_name)?; + + let mut ctx = Data { + region_name, + region_common_name, + uplink_rx_info, + xmit_data_req: pl, + dl_meta_data: dl_meta, + downlink_frame: gw::DownlinkFrame { + downlink_id: rand::thread_rng().gen(), + ..Default::default() + }, + }; + + ctx.set_downlink_frame()?; + ctx.save_downlink_frame().await?; + ctx.send_downlink_frame().await?; + + Ok(()) + } + + fn set_downlink_frame(&mut self) -> Result<()> { + trace!("Setting DownlinkFrame parameters"); + let region_conf = region::get(&self.region_name)?; + + let rx_info = self + .uplink_rx_info + .first() + .cloned() + .ok_or(anyhow!("rx_info is empty"))?; + + self.downlink_frame.gateway_id = rx_info.gateway_id.clone(); + if self.dl_meta_data.dl_freq_1.is_some() + && self.dl_meta_data.data_rate_1.is_some() + && self.dl_meta_data.rx_delay_1.is_some() + { + let mut tx_info = gw::DownlinkTxInfo { + frequency: (self.dl_meta_data.dl_freq_1.unwrap() * 1_000_000.0) as u32, + board: rx_info.board, + antenna: rx_info.antenna, + context: rx_info.context.clone(), + timing: Some(gw::Timing { + parameters: Some(gw::timing::Parameters::Delay(gw::DelayTimingInfo { + delay: Some(pbjson_types::Duration { + seconds: self.dl_meta_data.rx_delay_1.unwrap() as i64, + nanos: 0, + }), + })), + }), + ..Default::default() + }; + + tx_info.power = region_conf.get_downlink_tx_power(tx_info.frequency) as i32; + + let rx1_dr = region_conf.get_data_rate(self.dl_meta_data.data_rate_1.unwrap())?; + helpers::set_tx_info_data_rate(&mut tx_info, &rx1_dr)?; + + self.downlink_frame.items.push(gw::DownlinkFrameItem { + phy_payload: self.xmit_data_req.phy_payload.clone(), + tx_info: Some(tx_info), + tx_info_legacy: None, + }); + } + + if self.dl_meta_data.dl_freq_2.is_some() + && self.dl_meta_data.data_rate_2.is_some() + && self.dl_meta_data.rx_delay_1.is_some() + { + let mut tx_info = gw::DownlinkTxInfo { + frequency: (self.dl_meta_data.dl_freq_2.unwrap() * 1_000_000.0) as u32, + board: rx_info.board, + antenna: rx_info.antenna, + context: rx_info.context, + timing: Some(gw::Timing { + parameters: Some(gw::timing::Parameters::Delay(gw::DelayTimingInfo { + delay: Some(pbjson_types::Duration { + seconds: self.dl_meta_data.rx_delay_1.unwrap() as i64 + 1, + nanos: 0, + }), + })), + }), + ..Default::default() + }; + + tx_info.power = region_conf.get_downlink_tx_power(tx_info.frequency) as i32; + + let rx2_dr = region_conf.get_data_rate(self.dl_meta_data.data_rate_2.unwrap())?; + helpers::set_tx_info_data_rate(&mut tx_info, &rx2_dr)?; + + self.downlink_frame.items.push(gw::DownlinkFrameItem { + phy_payload: self.xmit_data_req.phy_payload.clone(), + tx_info: Some(tx_info), + tx_info_legacy: None, + }); + } + + Ok(()) + } + + async fn save_downlink_frame(&self) -> Result<()> { + trace!("Saving downlink frame"); + + downlink_frame::save(&internal::DownlinkFrame { + downlink_id: self.downlink_frame.downlink_id, + downlink_frame: Some(self.downlink_frame.clone()), + ..Default::default() + }) + .await + .context("Save downlink frame")?; + + Ok(()) + } + + async fn send_downlink_frame(&self) -> Result<()> { + trace!("Sending downlink frame"); + + gateway::backend::send_downlink(&self.region_name, &self.downlink_frame) + .await + .context("Send downlink frame")?; + + Ok(()) + } +} diff --git a/chirpstack/src/downlink/helpers.rs b/chirpstack/src/downlink/helpers.rs index 964692d8..115ef994 100644 --- a/chirpstack/src/downlink/helpers.rs +++ b/chirpstack/src/downlink/helpers.rs @@ -32,7 +32,7 @@ pub fn select_downlink_gateway( // sort items by SNR or if SNR is equal between A and B, by RSSI. rx_info.items.sort_by(|a, b| { if a.lora_snr == b.lora_snr { - return a.rssi.partial_cmp(&b.rssi).unwrap(); + return b.rssi.partial_cmp(&a.rssi).unwrap(); } b.lora_snr.partial_cmp(&a.lora_snr).unwrap() }); diff --git a/chirpstack/src/downlink/mod.rs b/chirpstack/src/downlink/mod.rs index ba561b61..b445bf5d 100644 --- a/chirpstack/src/downlink/mod.rs +++ b/chirpstack/src/downlink/mod.rs @@ -2,9 +2,11 @@ use tracing::info; pub mod classb; pub mod data; +pub mod data_fns; mod helpers; pub mod join; pub mod multicast; +pub mod roaming; pub mod scheduler; pub mod tx_ack; diff --git a/chirpstack/src/downlink/roaming.rs b/chirpstack/src/downlink/roaming.rs new file mode 100644 index 00000000..0c84f62e --- /dev/null +++ b/chirpstack/src/downlink/roaming.rs @@ -0,0 +1,215 @@ +use std::sync::Arc; + +use anyhow::{Context, Result}; +use rand::Rng; +use tracing::{span, trace, Instrument, Level}; + +use super::helpers; +use crate::storage::downlink_frame; +use crate::uplink::UplinkFrameSet; +use crate::{config, gateway, region}; +use backend::DLMetaData; +use chirpstack_api::{gw, internal}; + +pub struct PassiveRoamingDownlink { + uplink_frame_set: UplinkFrameSet, + phy_payload: Vec, + dl_meta_data: DLMetaData, + network_conf: config::RegionNetwork, + region_conf: Arc>, + downlink_frame: gw::DownlinkFrame, + downlink_gateway: Option, +} + +impl PassiveRoamingDownlink { + pub async fn handle(ufs: UplinkFrameSet, phy: Vec, dl_meta: DLMetaData) -> Result<()> { + let span = span!(Level::TRACE, "passive_roaming"); + let fut = PassiveRoamingDownlink::_handle(ufs, phy, dl_meta); + fut.instrument(span).await + } + + async fn _handle(ufs: UplinkFrameSet, phy: Vec, dl_meta: DLMetaData) -> Result<()> { + let network_conf = config::get_region_network(&ufs.region_name)?; + let region_conf = region::get(&ufs.region_name)?; + + let mut ctx = PassiveRoamingDownlink { + uplink_frame_set: ufs, + phy_payload: phy, + dl_meta_data: dl_meta, + network_conf, + region_conf, + downlink_frame: gw::DownlinkFrame { + downlink_id: rand::thread_rng().gen(), + ..Default::default() + }, + downlink_gateway: None, + }; + + ctx.select_downlink_gateway()?; + ctx.set_downlink_frame()?; + ctx.save_downlink_frame().await?; + ctx.send_downlink_frame().await?; + + Ok(()) + } + + fn select_downlink_gateway(&mut self) -> Result<()> { + trace!("Selecting downlink gateway"); + + let mut dev_gw_rx_info = internal::DeviceGatewayRxInfo { + dev_eui: Vec::new(), + dr: self.uplink_frame_set.dr as u32, + items: self + .uplink_frame_set + .rx_info_set + .iter() + .map(|rx_info| internal::DeviceGatewayRxInfoItem { + gateway_id: hex::decode(&rx_info.gateway_id).unwrap(), + rssi: rx_info.rssi, + lora_snr: rx_info.snr, + antenna: rx_info.antenna, + board: rx_info.board, + context: rx_info.context.clone(), + }) + .collect(), + }; + + let gw_down = helpers::select_downlink_gateway( + &self.uplink_frame_set.region_name, + self.network_conf.gateway_prefer_min_margin, + &mut dev_gw_rx_info, + )?; + + self.downlink_frame.gateway_id = hex::encode(&gw_down.gateway_id); + self.downlink_gateway = Some(gw_down); + + Ok(()) + } + + fn set_downlink_frame(&mut self) -> Result<()> { + trace!("Setting downlink frame"); + + let gw_down = self.downlink_gateway.as_ref().unwrap(); + + if let Some(class_mode) = &self.dl_meta_data.class_mode { + match class_mode.as_ref() { + "A" => { + // RX1 + if self.dl_meta_data.dl_freq_1.is_some() + && self.dl_meta_data.data_rate_1.is_some() + && self.dl_meta_data.rx_delay_1.is_some() + { + let dl_freq_1 = self.dl_meta_data.dl_freq_1.unwrap(); + let dl_freq_1 = (dl_freq_1 * 1_000_000.0) as u32; + let data_rate_1 = self.dl_meta_data.data_rate_1.unwrap(); + let data_rate_1 = self.region_conf.get_data_rate(data_rate_1 as u8)?; + let rx_delay_1 = self.dl_meta_data.rx_delay_1.unwrap(); + + let mut tx_info = gw::DownlinkTxInfo { + board: gw_down.board, + antenna: gw_down.antenna, + context: gw_down.context.clone(), + frequency: dl_freq_1, + timing: Some(gw::Timing { + parameters: Some(gw::timing::Parameters::Delay( + gw::DelayTimingInfo { + delay: Some(pbjson_types::Duration { + seconds: rx_delay_1 as i64, + nanos: 0, + }), + }, + )), + }), + power: if self.network_conf.downlink_tx_power != -1 { + self.network_conf.downlink_tx_power + } else { + self.region_conf.get_downlink_tx_power(dl_freq_1) as i32 + }, + ..Default::default() + }; + helpers::set_tx_info_data_rate(&mut tx_info, &data_rate_1)?; + + self.downlink_frame.items.push(gw::DownlinkFrameItem { + phy_payload: self.phy_payload.clone(), + tx_info: Some(tx_info), + tx_info_legacy: None, + }); + } + + // RX2 + if self.dl_meta_data.dl_freq_2.is_some() + && self.dl_meta_data.data_rate_2.is_some() + && self.dl_meta_data.rx_delay_1.is_some() + { + let dl_freq_2 = self.dl_meta_data.dl_freq_2.unwrap(); + let dl_freq_2 = (dl_freq_2 * 1_000_000.0) as u32; + let data_rate_2 = self.dl_meta_data.data_rate_2.unwrap(); + let data_rate_2 = self.region_conf.get_data_rate(data_rate_2 as u8)?; + let rx_delay_1 = self.dl_meta_data.rx_delay_1.unwrap(); + + let mut tx_info = gw::DownlinkTxInfo { + board: gw_down.board, + antenna: gw_down.antenna, + context: gw_down.context.clone(), + frequency: dl_freq_2, + timing: Some(gw::Timing { + parameters: Some(gw::timing::Parameters::Delay( + gw::DelayTimingInfo { + delay: Some(pbjson_types::Duration { + seconds: (rx_delay_1 + 1) as i64, + nanos: 0, + }), + }, + )), + }), + power: if self.network_conf.downlink_tx_power != -1 { + self.network_conf.downlink_tx_power + } else { + self.region_conf.get_downlink_tx_power(dl_freq_2) as i32 + }, + ..Default::default() + }; + helpers::set_tx_info_data_rate(&mut tx_info, &data_rate_2)?; + + self.downlink_frame.items.push(gw::DownlinkFrameItem { + phy_payload: self.phy_payload.clone(), + tx_info: Some(tx_info), + tx_info_legacy: None, + }); + } + } + _ => { + return Err(anyhow!("ClassMode {} is not supported", class_mode)); + } + } + } else { + return Err(anyhow!("ClassMode is not set")); + } + + Ok(()) + } + + async fn save_downlink_frame(&self) -> Result<()> { + trace!("Saving downlink frame"); + + downlink_frame::save(&internal::DownlinkFrame { + downlink_id: self.downlink_frame.downlink_id, + downlink_frame: Some(self.downlink_frame.clone()), + ..Default::default() + }) + .await + .context("Save downlink frame")?; + + Ok(()) + } + + async fn send_downlink_frame(&self) -> Result<()> { + trace!("Sending downlink frame"); + + gateway::backend::send_downlink(&self.uplink_frame_set.region_name, &self.downlink_frame) + .await + .context("Send downlink frame")?; + + Ok(()) + } +} diff --git a/chirpstack/src/maccommand/dev_status.rs b/chirpstack/src/maccommand/dev_status.rs index 16182aad..2708d156 100644 --- a/chirpstack/src/maccommand/dev_status.rs +++ b/chirpstack/src/maccommand/dev_status.rs @@ -142,6 +142,7 @@ pub mod test { gateway_tenant_id_map: HashMap::new(), region_common_name: lrwn::region::CommonName::EU868, region_name: "eu868".into(), + roaming_meta_data: None, }; let tenant = tenant::create(tenant::Tenant { diff --git a/chirpstack/src/maccommand/device_time.rs b/chirpstack/src/maccommand/device_time.rs index a0471a8f..ad2dcd9d 100644 --- a/chirpstack/src/maccommand/device_time.rs +++ b/chirpstack/src/maccommand/device_time.rs @@ -72,6 +72,7 @@ pub mod test { gateway_tenant_id_map: HashMap::new(), region_common_name: lrwn::region::CommonName::EU868, region_name: "eu868".into(), + roaming_meta_data: None, }; let gps_time = rx_time.to_gps_time(); diff --git a/chirpstack/src/maccommand/link_adr.rs b/chirpstack/src/maccommand/link_adr.rs index dce58120..10e0ae52 100644 --- a/chirpstack/src/maccommand/link_adr.rs +++ b/chirpstack/src/maccommand/link_adr.rs @@ -340,6 +340,7 @@ pub mod test { gateway_tenant_id_map: HashMap::new(), region_common_name: lrwn::region::CommonName::EU868, region_name: "eu868".into(), + roaming_meta_data: None, }; for tst in &tests { diff --git a/chirpstack/src/maccommand/link_check.rs b/chirpstack/src/maccommand/link_check.rs index 2c04e0f4..51b4cebf 100644 --- a/chirpstack/src/maccommand/link_check.rs +++ b/chirpstack/src/maccommand/link_check.rs @@ -99,6 +99,7 @@ pub mod test { gateway_tenant_id_map: HashMap::new(), region_common_name: lrwn::region::CommonName::EU868, region_name: "eu868".into(), + roaming_meta_data: None, }; let dev: device::Device = Default::default(); diff --git a/chirpstack/src/maccommand/mod.rs b/chirpstack/src/maccommand/mod.rs index ac27be68..3c730d68 100644 --- a/chirpstack/src/maccommand/mod.rs +++ b/chirpstack/src/maccommand/mod.rs @@ -185,6 +185,7 @@ pub mod test { gateway_tenant_id_map: Default::default(), region_common_name: lrwn::region::CommonName::EU868, region_name: "eu868".into(), + roaming_meta_data: None, }; let t: tenant::Tenant = Default::default(); diff --git a/chirpstack/src/region.rs b/chirpstack/src/region.rs index 1f8eaa38..2236343e 100644 --- a/chirpstack/src/region.rs +++ b/chirpstack/src/region.rs @@ -75,3 +75,20 @@ pub fn get(region_name: &str) -> Result Result { + let regions_r = REGIONS.read().unwrap(); + for (k, v) in &*regions_r { + if v.get_name() == common_name { + return Ok(k.clone()); + } + } + + Err(anyhow!( + "No region configured with common-name: {}", + common_name + )) +} diff --git a/chirpstack/src/storage/device_session.rs b/chirpstack/src/storage/device_session.rs index 1c60ce09..6cc5cec3 100644 --- a/chirpstack/src/storage/device_session.rs +++ b/chirpstack/src/storage/device_session.rs @@ -119,6 +119,9 @@ pub async fn delete(dev_eui: &EUI64) -> Result<()> { // Return the device-session matching the given PhyPayload. This will fetch all device-session // associated with the used DevAddr and based on f_cont and mic, decides which one to use. +// This function will increment the uplink frame-counter and will immediately update the +// device-session in the database, to make sure that in case this function is called multiple +// times, at most one will be valid. pub async fn get_for_phypayload_and_incr_f_cnt_up( phy: &PhyPayload, tx_dr: u8, @@ -238,6 +241,67 @@ pub async fn get_for_phypayload_and_incr_f_cnt_up( Err(Error::InvalidMIC) } +// Simmilar to get_for_phypayload_and_incr_f_cnt_up, but only retrieves the device-session for the +// given PhyPayload. As it does not return the ValidationStatus, it only returns the DeviceSession +// in case of a valid frame-counter. +pub async fn get_for_phypayload( + phy: &PhyPayload, + tx_dr: u8, + tx_ch: u8, +) -> Result { + // Clone the PhyPayload, as we will update the f_cnt to the full (32bit) frame-counter value + // for calculating the MIC. + let mut phy = phy.clone(); + + // Get the dev_addr and original f_cnt. + let (dev_addr, f_cnt_orig) = if let Payload::MACPayload(pl) = &phy.payload { + (pl.fhdr.devaddr, pl.fhdr.f_cnt) + } else { + return Err(Error::InvalidPayload("MacPayload".to_string())); + }; + + let device_sessions = get_for_dev_addr(dev_addr) + .await + .context("Get device-sessions for DevAddr")?; + if device_sessions.is_empty() { + return Err(Error::NotFound(dev_addr.to_string())); + } + + for ds in device_sessions { + // Restore the original f_cnt. + if let Payload::MACPayload(pl) = &mut phy.payload { + pl.fhdr.f_cnt = f_cnt_orig; + } + + // Get the full 32bit frame-counter. + let full_f_cnt = get_full_f_cnt_up(ds.f_cnt_up, f_cnt_orig); + let f_nwk_s_int_key = AES128Key::from_slice(&ds.f_nwk_s_int_key)?; + let s_nwk_s_int_key = AES128Key::from_slice(&ds.s_nwk_s_int_key)?; + + // Set the full f_cnt + if let Payload::MACPayload(pl) = &mut phy.payload { + pl.fhdr.f_cnt = full_f_cnt; + } + + let mic_ok = phy + .validate_uplink_data_mic( + ds.mac_version().from_proto(), + ds.conf_f_cnt, + tx_dr, + tx_ch, + &f_nwk_s_int_key, + &s_nwk_s_int_key, + ) + .context("Validate MIC")?; + + if mic_ok && full_f_cnt >= ds.f_cnt_up { + return Ok(ds); + } + } + + Err(Error::InvalidMIC) +} + async fn get_dev_euis_for_dev_addr(dev_addr: DevAddr) -> Result> { task::spawn_blocking({ let dev_addr = dev_addr; diff --git a/chirpstack/src/storage/mod.rs b/chirpstack/src/storage/mod.rs index 74daf90c..8493df4e 100644 --- a/chirpstack/src/storage/mod.rs +++ b/chirpstack/src/storage/mod.rs @@ -26,6 +26,7 @@ pub mod gateway; pub mod mac_command; pub mod metrics; pub mod multicast; +pub mod passive_roaming; pub mod schema; pub mod search; pub mod tenant; diff --git a/chirpstack/src/storage/passive_roaming.rs b/chirpstack/src/storage/passive_roaming.rs new file mode 100644 index 00000000..6d1538d5 --- /dev/null +++ b/chirpstack/src/storage/passive_roaming.rs @@ -0,0 +1,260 @@ +use std::io::Cursor; +use std::str::FromStr; + +use anyhow::{Context, Result}; +use chrono::{DateTime, Duration, Utc}; +use prost::Message; +use tokio::task; +use tracing::{debug, info}; +use uuid::Uuid; + +use super::error::Error; +use super::{get_redis_conn, redis_key}; +use crate::config; +use chirpstack_api::internal; +use lrwn::{AES128Key, DevAddr, EUI64}; + +pub async fn save(ds: &internal::PassiveRoamingDeviceSession) -> Result<()> { + let sess_id = Uuid::from_slice(&ds.session_id)?; + let dev_addr = DevAddr::from_slice(&ds.dev_addr)?; + let dev_eui = if ds.dev_eui.is_empty() { + EUI64::default() + } else { + EUI64::from_slice(&ds.dev_eui)? + }; + + let lifetime: DateTime = match ds.lifetime.clone() { + Some(v) => v.try_into()?, + None => { + debug!("Not saving passive-roaming device-session, no passive-roaming lifetime set"); + return Ok(()); + } + }; + + let lifetime = lifetime - Utc::now(); + if lifetime <= Duration::seconds(0) { + debug!("Not saving passive-roaming device-session, lifetime of passive-roaming session expired"); + return Ok(()); + } + + task::spawn_blocking({ + let ds = ds.clone(); + move || -> Result<()> { + let conf = config::get(); + + let dev_addr_key = redis_key(format!("pr:devaddr:{{{}}}", dev_addr)); + let dev_eui_key = redis_key(format!("pr:dev:{{{}}}", dev_eui)); + let sess_key = redis_key(format!("pr:sess:{{{}}}", sess_id)); + let b = ds.encode_to_vec(); + let ttl = conf.network.device_session_ttl.as_millis() as usize; + let pr_ttl = lifetime.num_milliseconds() as usize; + + let mut c = get_redis_conn()?; + + // We need to store a pointer from both the DevAddr and DevEUI to the + // passive-roaming device-session ID. This is needed: + // * Because the DevAddr is not guaranteed to be unique + // * Because the DevEUI might not be given (thus is also not guaranteed + // to be an unique identifier). + // + // But: + // * We need to be able to lookup the session using the DevAddr (potentially + // using the MIC validation). + // * We need to be able to stop a passive-roaming session given a DevEUI. + redis::pipe() + .atomic() + .cmd("SADD") + .arg(&dev_addr_key) + .arg(&sess_id.to_string()) + .ignore() + .cmd("SADD") + .arg(&dev_eui_key) + .arg(&sess_id.to_string()) + .ignore() + .cmd("PEXPIRE") + .arg(&dev_addr_key) + .arg(&ttl) + .ignore() + .cmd("PEXPIRE") + .arg(&dev_eui_key) + .arg(&ttl) + .ignore() + .cmd("PSETEX") + .arg(&sess_key) + .arg(pr_ttl) + .arg(b) + .ignore() + .query(&mut *c)?; + + Ok(()) + } + }) + .await??; + + info!(id = %sess_id, "Passive-roaming device-session saved"); + + Ok(()) +} + +pub async fn get(id: Uuid) -> Result { + task::spawn_blocking({ + move || -> Result { + let key = redis_key(format!("pr:sess:{{{}}}", id)); + let mut c = get_redis_conn()?; + let v: Vec = redis::cmd("GET") + .arg(key) + .query(&mut *c) + .context("Get passive-roaming device-session")?; + if v.is_empty() { + return Err(Error::NotFound(id.to_string())); + } + let ds = internal::PassiveRoamingDeviceSession::decode(&mut Cursor::new(v)) + .context("Decode passive-roaming device-session")?; + Ok(ds) + } + }) + .await? +} + +pub async fn delete(id: Uuid) -> Result<()> { + task::spawn_blocking({ + move || -> Result<()> { + let key = redis_key(format!("pr:sess:{{{}}}", id)); + let mut c = get_redis_conn()?; + redis::cmd("DEL").arg(&key).query(&mut *c)?; + Ok(()) + } + }) + .await??; + info!(id = %id, "Passive-roaming device-session deleted"); + Ok(()) +} + +pub async fn get_for_phy_payload( + phy: &lrwn::PhyPayload, +) -> Result, Error> { + // Clone the PhyPayload, as we will update the f_cnt to the full (32bit) frame-counter value + // for calculating the MIC. + let mut phy = phy.clone(); + + let (dev_addr, f_cnt_orig) = if let lrwn::Payload::MACPayload(v) = &phy.payload { + (v.fhdr.devaddr, v.fhdr.f_cnt) + } else { + return Err(Error::InvalidPayload("MacPayload".to_string())); + }; + + let sessions = get_sessions_for_dev_addr(dev_addr).await?; + let mut out: Vec = Vec::new(); + + for ds in sessions { + // We will not validate the MIC. + if !ds.validate_mic { + out.push(ds); + continue; + } + + let f_nwk_s_int_key = AES128Key::from_slice(&ds.f_nwk_s_int_key)?; + + // Set the full frame-counter. + if let lrwn::Payload::MACPayload(pl) = &mut phy.payload { + pl.fhdr.f_cnt = get_full_f_cnt_up(ds.f_cnt_up, f_cnt_orig); + } + + let mic_ok = if ds.lorawan_1_1 { + phy.validate_uplink_data_micf(&f_nwk_s_int_key)? + } else { + phy.validate_uplink_data_mic( + lrwn::MACVersion::LoRaWAN1_0, + 0, + 0, + 0, + &f_nwk_s_int_key, + &f_nwk_s_int_key, + )? + }; + + if mic_ok { + out.push(ds); + } + } + + Ok(out) +} + +async fn get_sessions_for_dev_addr( + dev_addr: DevAddr, +) -> Result> { + let mut out: Vec = Vec::new(); + let ids = get_session_ids_for_dev_addr(dev_addr).await?; + + for id in ids { + if let Ok(v) = get(id).await { + out.push(v); + } + } + + Ok(out) +} + +async fn get_sessions_for_dev_eui( + dev_eui: EUI64, +) -> Result> { + let mut out: Vec = Vec::new(); + let ids = get_session_ids_for_dev_eui(dev_eui).await?; + + for id in ids { + if let Ok(v) = get(id).await { + out.push(v); + } + } + + Ok(out) +} + +async fn get_session_ids_for_dev_addr(dev_addr: DevAddr) -> Result> { + task::spawn_blocking({ + move || -> Result> { + let key = redis_key(format!("pr:devaddr:{{{}}}", dev_addr)); + let mut c = get_redis_conn()?; + let v: Vec = redis::cmd("SMEMBERS").arg(key).query(&mut *c)?; + + let mut out: Vec = Vec::new(); + for id in &v { + out.push(Uuid::from_str(id)?); + } + + Ok(out) + } + }) + .await? +} + +pub async fn get_session_ids_for_dev_eui(dev_eui: EUI64) -> Result> { + task::spawn_blocking({ + move || -> Result> { + let key = redis_key(format!("pr:dev:{{{}}}", dev_eui)); + let mut c = get_redis_conn()?; + let v: Vec = redis::cmd("SMEMBERS").arg(key).query(&mut *c)?; + + let mut out: Vec = Vec::new(); + for id in &v { + out.push(Uuid::from_str(id)?); + } + + Ok(out) + } + }) + .await? +} + +fn get_full_f_cnt_up(next_expected_full_fcnt: u32, truncated_f_cnt: u32) -> u32 { + // Handle re-transmission. + if truncated_f_cnt == (((next_expected_full_fcnt % (1 << 16)) as u16).wrapping_sub(1)) as u32 { + return next_expected_full_fcnt - 1; + } + + let gap = ((truncated_f_cnt as u16).wrapping_sub((next_expected_full_fcnt % (1 << 16)) as u16)) + as u32; + + next_expected_full_fcnt.wrapping_add(gap) +} diff --git a/chirpstack/src/test/class_a_pr_test.rs b/chirpstack/src/test/class_a_pr_test.rs new file mode 100644 index 00000000..68a7c1b5 --- /dev/null +++ b/chirpstack/src/test/class_a_pr_test.rs @@ -0,0 +1,509 @@ +use std::str::FromStr; + +use bytes::Bytes; +use chrono::Utc; +use httpmock::prelude::*; +use prost::Message; +use uuid::Uuid; + +use crate::api::backend as backend_api; +use crate::backend::{joinserver, roaming}; +use crate::gateway::backend as gateway_backend; +use crate::storage::{ + application, device, device_profile, device_queue, device_session, gateway, tenant, +}; +use crate::{config, test, uplink}; +use chirpstack_api::{common, gw, internal}; +use lrwn::{AES128Key, NetID, EUI64}; + +#[tokio::test] +async fn test_fns_uplink() { + let _guard = test::prepare().await; + + let sns_mock = MockServer::start(); + + let mut conf = (*config::get()).clone(); + + // Set NetID. + conf.network.net_id = NetID::from_str("000202").unwrap(); + + // Set roaming agreement. + conf.roaming.servers.push(config::RoamingServer { + net_id: NetID::from_str("000505").unwrap(), + server: sns_mock.url("/"), + ..Default::default() + }); + + config::set(conf); + joinserver::setup().unwrap(); + roaming::setup().unwrap(); + + let t = tenant::create(tenant::Tenant { + name: "tenant".into(), + can_have_gateways: true, + ..Default::default() + }) + .await + .unwrap(); + + let gw = gateway::create(gateway::Gateway { + name: "gateway".into(), + tenant_id: t.id, + gateway_id: EUI64::from_str("0102030405060708").unwrap(), + ..Default::default() + }) + .await + .unwrap(); + + let recv_time = Utc::now(); + + let mut rx_info = gw::UplinkRxInfo { + gateway_id: gw.gateway_id.to_string(), + time: Some(recv_time.into()), + ..Default::default() + }; + rx_info.set_metadata_string("region_name", "eu868"); + rx_info.set_metadata_string("region_common_name", "EU868"); + + let mut tx_info = gw::UplinkTxInfo { + frequency: 868100000, + ..Default::default() + }; + uplink::helpers::set_uplink_modulation("eu868", &mut tx_info, 0).unwrap(); + + let data_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: { + let mut d = lrwn::DevAddr::from_be_bytes([0, 0, 0, 0]); + d.set_addr_prefix(&lrwn::NetID::from_str("000505").unwrap()); + d + }, + f_ctrl: Default::default(), + f_cnt: 1, + f_opts: lrwn::MACCommandSet::new(vec![]), + }, + f_port: None, + frm_payload: None, + }), + mic: Some([1, 2, 3, 4]), + }; + + // Setup sns mock. + let mut sns_pr_start_req_mock = sns_mock.mock(|when, then| { + when.method(POST) + .path("/") + .json_body_obj(&backend::PRStartReqPayload { + base: backend::BasePayload { + sender_id: vec![0, 2, 2], + receiver_id: vec![0, 5, 5], + message_type: backend::MessageType::PRStartReq, + transaction_id: 1234, + ..Default::default() + }, + phy_payload: data_phy.to_vec().unwrap(), + ul_meta_data: backend::ULMetaData { + ul_freq: Some(868.1), + data_rate: Some(0), + recv_time: recv_time, + rf_region: "EU868".to_string(), + gw_cnt: Some(1), + gw_info: roaming::rx_info_to_gw_info(&[rx_info.clone()]).unwrap(), + ..Default::default() + }, + }); + + then.json_body_obj(&backend::PRStartAnsPayload { + base: backend::BasePayloadResult { + base: backend::BasePayload { + receiver_id: vec![1, 2, 3], + sender_id: vec![3, 2, 1], + message_type: backend::MessageType::PRStartAns, + transaction_id: 1234, + ..Default::default() + }, + result: backend::ResultPayload { + result_code: backend::ResultCode::Success, + ..Default::default() + }, + }, + ..Default::default() + }) + .status(200); + }); + + gateway_backend::set_backend(&"eu868", Box::new(gateway_backend::mock::Backend {})).await; + + // Simulate uplink + uplink::handle_uplink( + Uuid::new_v4(), + gw::UplinkFrameSet { + phy_payload: data_phy.to_vec().unwrap(), + tx_info: Some(tx_info), + rx_info: vec![rx_info], + }, + ) + .await + .unwrap(); + + sns_pr_start_req_mock.assert(); + sns_pr_start_req_mock.delete(); + + joinserver::reset(); + roaming::reset(); +} + +#[tokio::test] +async fn test_sns_uplink() { + let _guard = test::prepare().await; + let fns_mock = MockServer::start(); + let mut conf = (*config::get()).clone(); + + // Set NetID. + conf.network.net_id = NetID::from_str("000505").unwrap(); + + // Set roaming agreement. + conf.roaming.servers.push(config::RoamingServer { + net_id: NetID::from_str("000202").unwrap(), + server: fns_mock.url("/"), + ..Default::default() + }); + + config::set(conf); + joinserver::setup().unwrap(); + roaming::setup().unwrap(); + + let t = tenant::create(tenant::Tenant { + name: "tenant".into(), + can_have_gateways: true, + ..Default::default() + }) + .await + .unwrap(); + + let app = application::create(application::Application { + name: "app".into(), + tenant_id: t.id.clone(), + ..Default::default() + }) + .await + .unwrap(); + + let dp = device_profile::create(device_profile::DeviceProfile { + name: "dp".into(), + tenant_id: t.id.clone(), + region: lrwn::region::CommonName::EU868, + mac_version: lrwn::region::MacVersion::LORAWAN_1_0_2, + reg_params_revision: lrwn::region::Revision::A, + supports_otaa: true, + ..Default::default() + }) + .await + .unwrap(); + + let dev = device::create(device::Device { + name: "device".into(), + application_id: app.id.clone(), + device_profile_id: dp.id.clone(), + dev_eui: EUI64::from_be_bytes([2, 2, 3, 4, 5, 6, 7, 8]), + enabled_class: "B".into(), + ..Default::default() + }) + .await + .unwrap(); + + device_queue::enqueue_item(device_queue::DeviceQueueItem { + dev_eui: dev.dev_eui, + f_port: 10, + data: vec![1, 2, 3, 4], + ..Default::default() + }) + .await + .unwrap(); + + let mut dev_addr = lrwn::DevAddr::from_be_bytes([0, 0, 0, 0]); + dev_addr.set_addr_prefix(&lrwn::NetID::from_str("000505").unwrap()); + + let ds = internal::DeviceSession { + dev_eui: vec![2, 2, 3, 4, 5, 6, 7, 8], + mac_version: common::MacVersion::Lorawan104.into(), + join_eui: vec![8, 7, 6, 5, 4, 3, 2, 1], + dev_addr: dev_addr.to_vec(), + f_nwk_s_int_key: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + s_nwk_s_int_key: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + nwk_s_enc_key: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + app_s_key: Some(common::KeyEnvelope { + kek_label: "".into(), + aes_key: vec![16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], + }), + f_cnt_up: 8, + n_f_cnt_down: 5, + enabled_uplink_channel_indices: vec![0, 1, 2], + rx1_delay: 1, + rx2_frequency: 869525000, + region_name: "eu868".into(), + ..Default::default() + }; + device_session::save(&ds).await.unwrap(); + + let mut data_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: dev_addr, + f_ctrl: Default::default(), + f_cnt: 8, + f_opts: lrwn::MACCommandSet::new(vec![]), + }, + f_port: None, + frm_payload: None, + }), + mic: None, + }; + data_phy + .set_uplink_data_mic( + lrwn::MACVersion::LoRaWAN1_0, + 0, + 0, + 0, + &AES128Key::from_slice(&ds.f_nwk_s_int_key).unwrap(), + &AES128Key::from_slice(&ds.s_nwk_s_int_key).unwrap(), + ) + .unwrap(); + + let recv_time = Utc::now(); + + let mut rx_info = gw::UplinkRxInfo { + gateway_id: "0302030405060708".to_string(), + time: Some(recv_time.into()), + ..Default::default() + }; + rx_info.set_metadata_string("region_name", "eu868"); + rx_info.set_metadata_string("region_common_name", "EU868"); + + let mut tx_info = gw::UplinkTxInfo { + frequency: 868100000, + ..Default::default() + }; + uplink::helpers::set_uplink_modulation("eu868", &mut tx_info, 0).unwrap(); + + let pr_start_req = backend::PRStartReqPayload { + base: backend::BasePayload { + sender_id: vec![0, 2, 2], + receiver_id: vec![0, 5, 5], + message_type: backend::MessageType::PRStartReq, + transaction_id: 1234, + ..Default::default() + }, + phy_payload: data_phy.to_vec().unwrap(), + ul_meta_data: backend::ULMetaData { + ul_freq: Some(868.1), + data_rate: Some(0), + recv_time: recv_time, + rf_region: "EU868".to_string(), + gw_cnt: Some(1), + gw_info: roaming::rx_info_to_gw_info(&[rx_info.clone()]).unwrap(), + ..Default::default() + }, + }; + + // Setup downlink xmit mock. + let mut fns_xmit_data_req_mock = fns_mock.mock(|when, then| { + when.method(POST) + .path("/") + .json_body_obj(&backend::XmitDataReqPayload { + base: backend::BasePayload { + receiver_id: vec![0, 2, 2], + sender_id: vec![0, 5, 5], + message_type: backend::MessageType::XmitDataReq, + transaction_id: 1234, + ..Default::default() + }, + phy_payload: hex::decode("600000000a8005000a54972baa8b983cd1").unwrap(), + dl_meta_data: Some(backend::DLMetaData { + dev_eui: ds.dev_eui.clone(), + dl_freq_1: Some(868.1), + dl_freq_2: Some(869.525), + rx_delay_1: Some(1), + class_mode: Some("A".to_string()), + data_rate_1: Some(0), + data_rate_2: Some(0), + gw_info: vec![backend::GWInfoElement { + ul_token: rx_info.encode_to_vec(), + ..Default::default() + }], + ..Default::default() + }), + ..Default::default() + }); + + then.json_body_obj(&backend::XmitDataAnsPayload { + base: backend::BasePayloadResult { + base: backend::BasePayload { + receiver_id: vec![0, 5, 5], + sender_id: vec![0, 2, 2], + message_type: backend::MessageType::XmitDataAns, + transaction_id: 1234, + ..Default::default() + }, + result: backend::ResultPayload { + result_code: backend::ResultCode::Success, + ..Default::default() + }, + }, + }) + .status(200); + }); + + let resp = + backend_api::handle_request(Bytes::from(serde_json::to_string(&pr_start_req).unwrap())) + .await; + let resp_b = hyper::body::to_bytes(resp.into_body()).await.unwrap(); + + let pr_start_ans: backend::PRStartAnsPayload = serde_json::from_slice(&resp_b).unwrap(); + + assert_eq!( + backend::PRStartAnsPayload { + base: backend::BasePayloadResult { + base: backend::BasePayload { + sender_id: vec![0, 5, 5], + receiver_id: vec![0, 2, 2], + message_type: backend::MessageType::PRStartAns, + transaction_id: 1234, + ..Default::default() + }, + result: backend::ResultPayload { + result_code: backend::ResultCode::Success, + ..Default::default() + }, + ..Default::default() + }, + dev_eui: ds.dev_eui.clone(), + nwk_s_key: Some(backend::KeyEnvelope { + kek_label: "".to_string(), + aes_key: ds.nwk_s_enc_key.clone(), + }), + f_cnt_up: Some(8), + ..Default::default() + }, + pr_start_ans + ); + + fns_xmit_data_req_mock.assert(); + fns_xmit_data_req_mock.delete(); +} + +#[tokio::test] +async fn test_sns_dev_not_found() { + let _guard = test::prepare().await; + let fns_mock = MockServer::start(); + + let mut conf = (*config::get()).clone(); + + // Set NetID. + conf.network.net_id = NetID::from_str("000505").unwrap(); + + // Set roaming agreement. + conf.roaming.servers.push(config::RoamingServer { + net_id: NetID::from_str("000202").unwrap(), + server: fns_mock.url("/"), + ..Default::default() + }); + + config::set(conf); + joinserver::setup().unwrap(); + roaming::setup().unwrap(); + + let mut dev_addr = lrwn::DevAddr::from_be_bytes([0, 0, 0, 0]); + dev_addr.set_addr_prefix(&lrwn::NetID::from_str("000505").unwrap()); + + let data_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: dev_addr, + f_ctrl: Default::default(), + f_cnt: 8, + f_opts: lrwn::MACCommandSet::new(vec![]), + }, + f_port: None, + frm_payload: None, + }), + mic: Some([1, 2, 3, 4]), + }; + + let recv_time = Utc::now(); + + let mut rx_info = gw::UplinkRxInfo { + gateway_id: "0302030405060708".to_string(), + time: Some(recv_time.into()), + ..Default::default() + }; + rx_info.set_metadata_string("region_name", "eu868"); + rx_info.set_metadata_string("region_common_name", "EU868"); + + let mut tx_info = gw::UplinkTxInfo { + frequency: 868100000, + ..Default::default() + }; + uplink::helpers::set_uplink_modulation("eu868", &mut tx_info, 0).unwrap(); + + let pr_start_req = backend::PRStartReqPayload { + base: backend::BasePayload { + sender_id: vec![0, 2, 2], + receiver_id: vec![0, 5, 5], + message_type: backend::MessageType::PRStartReq, + transaction_id: 1234, + ..Default::default() + }, + phy_payload: data_phy.to_vec().unwrap(), + ul_meta_data: backend::ULMetaData { + ul_freq: Some(868.1), + data_rate: Some(0), + recv_time: recv_time, + rf_region: "EU868".to_string(), + gw_cnt: Some(1), + gw_info: roaming::rx_info_to_gw_info(&[rx_info.clone()]).unwrap(), + ..Default::default() + }, + }; + + let resp = + backend_api::handle_request(Bytes::from(serde_json::to_string(&pr_start_req).unwrap())) + .await; + let resp_b = hyper::body::to_bytes(resp.into_body()).await.unwrap(); + + let pr_start_ans: backend::PRStartAnsPayload = serde_json::from_slice(&resp_b).unwrap(); + + assert_eq!( + backend::PRStartAnsPayload { + base: backend::BasePayloadResult { + base: backend::BasePayload { + sender_id: vec![0, 5, 5], + receiver_id: vec![0, 2, 2], + message_type: backend::MessageType::PRStartAns, + transaction_id: 1234, + ..Default::default() + }, + result: backend::ResultPayload { + result_code: backend::ResultCode::UnknownDevAddr, + description: format!("Object does not exist (id: {})", dev_addr), + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }, + pr_start_ans + ); +} diff --git a/chirpstack/src/test/mod.rs b/chirpstack/src/test/mod.rs index ff2027d2..89cd2d06 100644 --- a/chirpstack/src/test/mod.rs +++ b/chirpstack/src/test/mod.rs @@ -3,10 +3,12 @@ use std::sync::{Mutex, Once}; use crate::{adr, config, region, storage}; mod assert; +mod class_a_pr_test; mod class_a_test; mod class_b_test; mod class_c_test; mod multicast_test; +mod otaa_pr_test; mod otaa_test; static TRACING_INIT: Once = Once::new(); diff --git a/chirpstack/src/test/otaa_pr_test.rs b/chirpstack/src/test/otaa_pr_test.rs new file mode 100644 index 00000000..8edf6070 --- /dev/null +++ b/chirpstack/src/test/otaa_pr_test.rs @@ -0,0 +1,418 @@ +use std::str::FromStr; + +use bytes::Bytes; +use chrono::Utc; +use httpmock::prelude::*; +use prost::Message; +use uuid::Uuid; + +use super::assert; +use crate::api::backend as backend_api; +use crate::backend::{joinserver, roaming}; +use crate::gateway::backend as gateway_backend; +use crate::storage::{application, device, device_keys, device_profile, gateway, tenant}; +use crate::{config, test, uplink}; +use chirpstack_api::gw; +use lrwn::{AES128Key, NetID, EUI64}; + +#[tokio::test] +async fn test_fns() { + let _guard = test::prepare().await; + + let js_mock = MockServer::start(); + let sns_mock = MockServer::start(); + + let mut conf = (*config::get()).clone(); + + // Set NetID. + conf.network.net_id = NetID::from_str("010203").unwrap(); + + // Set Join Server. + conf.join_server.servers.push(config::JoinServerServer { + join_eui: EUI64::from_str("0102030405060708").unwrap(), + server: js_mock.url("/"), + ..Default::default() + }); + + // Set roaming agreement. + conf.roaming.servers.push(config::RoamingServer { + net_id: NetID::from_str("030201").unwrap(), + server: sns_mock.url("/"), + ..Default::default() + }); + + config::set(conf); + joinserver::setup().unwrap(); + roaming::setup().unwrap(); + + let t = tenant::create(tenant::Tenant { + name: "tenant".into(), + can_have_gateways: true, + ..Default::default() + }) + .await + .unwrap(); + + let gw = gateway::create(gateway::Gateway { + name: "gateway".into(), + tenant_id: t.id, + gateway_id: EUI64::from_str("0102030405060708").unwrap(), + ..Default::default() + }) + .await + .unwrap(); + + let recv_time = Utc::now(); + + let mut rx_info = gw::UplinkRxInfo { + gateway_id: gw.gateway_id.to_string(), + time: Some(recv_time.into()), + ..Default::default() + }; + rx_info.set_metadata_string("region_name", "eu868"); + rx_info.set_metadata_string("region_common_name", "EU868"); + + let mut tx_info = gw::UplinkTxInfo { + frequency: 868100000, + ..Default::default() + }; + uplink::helpers::set_uplink_modulation("eu868", &mut tx_info, 0).unwrap(); + + let mut jr_phy = lrwn::PhyPayload { + mhdr: lrwn::MHDR { + m_type: lrwn::MType::JoinRequest, + major: lrwn::Major::LoRaWANR1, + }, + payload: lrwn::Payload::JoinRequest(lrwn::JoinRequestPayload { + join_eui: EUI64::from_str("0102030405060708").unwrap(), + dev_eui: EUI64::from_str("0807060504030201").unwrap(), + dev_nonce: 123, + }), + mic: None, + }; + jr_phy + .set_join_request_mic(&AES128Key::from_str("01020304050607080102030405060708").unwrap()) + .unwrap(); + + // Setup JS mock (HomeNSReq). + let mut js_join_request_mock = js_mock.mock(|when, then| { + when.method(POST) + .path("/") + .json_body_obj(&backend::HomeNSReqPayload { + base: backend::BasePayload { + sender_id: vec![1, 2, 3], + receiver_id: vec![1, 2, 3, 4, 5, 6, 7, 8], + message_type: backend::MessageType::HomeNSReq, + transaction_id: 1234, + ..Default::default() + }, + dev_eui: vec![8, 7, 6, 5, 4, 3, 2, 1], + }); + + then.json_body_obj(&backend::HomeNSAnsPayload { + base: backend::BasePayloadResult { + base: backend::BasePayload { + receiver_id: vec![1, 2, 3], + sender_id: vec![1, 2, 3, 4, 5, 6, 7, 8], + message_type: backend::MessageType::HomeNSAns, + transaction_id: 1234, + ..Default::default() + }, + result: backend::ResultPayload { + result_code: backend::ResultCode::Success, + ..Default::default() + }, + }, + h_net_id: vec![3, 2, 1], + }) + .status(200); + }); + + // Setup SNS mock (PRStartReq). + let mut sns_pr_start_req_mock = sns_mock.mock(|when, then| { + when.method(POST) + .path("/") + .json_body_obj(&backend::PRStartReqPayload { + base: backend::BasePayload { + sender_id: vec![1, 2, 3], + receiver_id: vec![3, 2, 1], + message_type: backend::MessageType::PRStartReq, + transaction_id: 1234, + ..Default::default() + }, + phy_payload: jr_phy.to_vec().unwrap(), + ul_meta_data: backend::ULMetaData { + dev_eui: vec![8, 7, 6, 5, 4, 3, 2, 1], + ul_freq: Some(868.1), + data_rate: Some(0), + recv_time: recv_time, + rf_region: "EU868".to_string(), + gw_cnt: Some(1), + gw_info: roaming::rx_info_to_gw_info(&[rx_info.clone()]).unwrap(), + ..Default::default() + }, + }); + + then.json_body_obj(&backend::PRStartAnsPayload { + base: backend::BasePayloadResult { + base: backend::BasePayload { + receiver_id: vec![1, 2, 3], + sender_id: vec![3, 2, 1], + message_type: backend::MessageType::PRStartAns, + transaction_id: 1234, + ..Default::default() + }, + result: backend::ResultPayload { + result_code: backend::ResultCode::Success, + ..Default::default() + }, + }, + phy_payload: vec![1, 2, 3, 4], + dl_meta_data: Some(backend::DLMetaData { + class_mode: Some("A".to_string()), + dl_freq_1: Some(868.1), + data_rate_1: Some(0), + rx_delay_1: Some(5), + ..Default::default() + }), + ..Default::default() + }) + .status(200); + }); + + gateway_backend::set_backend(&"eu868", Box::new(gateway_backend::mock::Backend {})).await; + + // Simulate uplink + uplink::handle_uplink( + Uuid::new_v4(), + gw::UplinkFrameSet { + phy_payload: jr_phy.to_vec().unwrap(), + tx_info: Some(tx_info), + rx_info: vec![rx_info], + }, + ) + .await + .unwrap(); + + js_join_request_mock.assert(); + js_join_request_mock.delete(); + + sns_pr_start_req_mock.assert(); + sns_pr_start_req_mock.delete(); + + assert::downlink_frame(gw::DownlinkFrame { + gateway_id: "0102030405060708".into(), + items: vec![gw::DownlinkFrameItem { + phy_payload: vec![1, 2, 3, 4], + tx_info: Some(gw::DownlinkTxInfo { + frequency: 868100000, + power: 14, + modulation: Some(gw::Modulation { + parameters: Some(gw::modulation::Parameters::Lora(gw::LoraModulationInfo { + bandwidth: 125000, + spreading_factor: 12, + code_rate: gw::CodeRate::Cr45.into(), + polarization_inversion: true, + code_rate_legacy: "".to_string(), + })), + }), + board: 0, + antenna: 0, + timing: Some(gw::Timing { + parameters: Some(gw::timing::Parameters::Delay(gw::DelayTimingInfo { + delay: Some(pbjson_types::Duration { + seconds: 5, + nanos: 0, + }), + })), + }), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + })() + .await; + + joinserver::reset(); + roaming::reset(); +} + +#[tokio::test] +async fn test_sns() { + let _guard = test::prepare().await; + + let fns_mock = MockServer::start(); + + let mut conf = (*config::get()).clone(); + + // Set NetID. + conf.network.net_id = NetID::from_str("010203").unwrap(); + + // Set roaming agreement. + conf.roaming.servers.push(config::RoamingServer { + net_id: NetID::from_str("030201").unwrap(), + server: fns_mock.url("/"), + ..Default::default() + }); + + config::set(conf); + joinserver::setup().unwrap(); + roaming::setup().unwrap(); + + let t = tenant::create(tenant::Tenant { + name: "tenant".into(), + can_have_gateways: true, + ..Default::default() + }) + .await + .unwrap(); + + let app = application::create(application::Application { + name: "app".into(), + tenant_id: t.id.clone(), + ..Default::default() + }) + .await + .unwrap(); + + let dp = device_profile::create(device_profile::DeviceProfile { + name: "dp".into(), + tenant_id: t.id.clone(), + region: lrwn::region::CommonName::EU868, + mac_version: lrwn::region::MacVersion::LORAWAN_1_0_2, + reg_params_revision: lrwn::region::Revision::A, + supports_otaa: true, + ..Default::default() + }) + .await + .unwrap(); + + let dev = device::create(device::Device { + name: "device".into(), + application_id: app.id.clone(), + device_profile_id: dp.id.clone(), + dev_eui: EUI64::from_be_bytes([2, 2, 3, 4, 5, 6, 7, 8]), + enabled_class: "B".into(), + ..Default::default() + }) + .await + .unwrap(); + + let dk = device_keys::create(device_keys::DeviceKeys { + dev_eui: dev.dev_eui.clone(), + nwk_key: AES128Key::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), + dev_nonces: vec![], + ..Default::default() + }) + .await + .unwrap(); + + let mut jr_phy = lrwn::PhyPayload { + mhdr: lrwn::MHDR { + m_type: lrwn::MType::JoinRequest, + major: lrwn::Major::LoRaWANR1, + }, + payload: lrwn::Payload::JoinRequest(lrwn::JoinRequestPayload { + join_eui: EUI64::from_str("0000000000000000").unwrap(), + dev_eui: dev.dev_eui, + dev_nonce: 1, + }), + mic: None, + }; + jr_phy.set_join_request_mic(&dk.nwk_key).unwrap(); + + let recv_time = Utc::now(); + + let mut rx_info = gw::UplinkRxInfo { + gateway_id: "0302030405060708".to_string(), + time: Some(recv_time.into()), + ..Default::default() + }; + rx_info.set_metadata_string("region_name", "eu868"); + rx_info.set_metadata_string("region_common_name", "EU868"); + + let mut tx_info = gw::UplinkTxInfo { + frequency: 868100000, + ..Default::default() + }; + uplink::helpers::set_uplink_modulation("eu868", &mut tx_info, 0).unwrap(); + + let pr_start_req = backend::PRStartReqPayload { + base: backend::BasePayload { + sender_id: vec![3, 2, 1], + receiver_id: vec![1, 2, 3], + message_type: backend::MessageType::PRStartReq, + transaction_id: 1234, + ..Default::default() + }, + phy_payload: jr_phy.to_vec().unwrap(), + ul_meta_data: backend::ULMetaData { + dev_eui: dev.dev_eui.to_vec(), + ul_freq: Some(868.1), + data_rate: Some(0), + recv_time: recv_time, + rf_region: "EU868".to_string(), + gw_cnt: Some(1), + gw_info: roaming::rx_info_to_gw_info(&[rx_info.clone()]).unwrap(), + ..Default::default() + }, + }; + + let resp = + backend_api::handle_request(Bytes::from(serde_json::to_string(&pr_start_req).unwrap())) + .await; + let resp_b = hyper::body::to_bytes(resp.into_body()).await.unwrap(); + + let pr_start_ans: backend::PRStartAnsPayload = serde_json::from_slice(&resp_b).unwrap(); + + assert_eq!( + backend::PRStartAnsPayload { + base: backend::BasePayloadResult { + base: backend::BasePayload { + sender_id: vec![1, 2, 3], + receiver_id: vec![3, 2, 1], + message_type: backend::MessageType::PRStartAns, + transaction_id: 1234, + ..Default::default() + }, + result: backend::ResultPayload { + result_code: backend::ResultCode::Success, + ..Default::default() + }, + ..Default::default() + }, + phy_payload: vec![ + 32, 62, 206, 177, 148, 31, 33, 193, 200, 4, 185, 248, 156, 108, 64, 97, 1 + ], + dev_eui: vec![2, 2, 3, 4, 5, 6, 7, 8], + nwk_s_key: Some(backend::KeyEnvelope { + kek_label: "".to_string(), + aes_key: vec![ + 136, 91, 1, 94, 61, 245, 54, 151, 185, 147, 143, 76, 248, 79, 192, 28 + ], + }), + f_cnt_up: Some(0), + dl_meta_data: Some(backend::DLMetaData { + dev_eui: vec![2, 2, 3, 4, 5, 6, 7, 8], + dl_freq_1: Some(868.1), + dl_freq_2: Some(869.525), + rx_delay_1: Some(5), + class_mode: Some("A".to_string()), + data_rate_1: Some(0), + data_rate_2: Some(0), + gw_info: vec![backend::GWInfoElement { + ul_token: rx_info.encode_to_vec(), + ..Default::default() + }], + ..Default::default() + }), + dev_addr: vec![7, 2, 3, 4], + ..Default::default() + }, + pr_start_ans + ); + + joinserver::reset(); + roaming::reset(); +} diff --git a/chirpstack/src/test/otaa_test.rs b/chirpstack/src/test/otaa_test.rs index e4c06974..98ab020c 100644 --- a/chirpstack/src/test/otaa_test.rs +++ b/chirpstack/src/test/otaa_test.rs @@ -8,9 +8,9 @@ use super::assert; use crate::storage::{ application, device, device_keys, device_profile, gateway, reset_redis, tenant, }; -use crate::uplink::join::get_js_int_key; use crate::{config, gateway::backend as gateway_backend, integration, region, test, uplink}; use chirpstack_api::{common, gw, internal, meta}; +use lrwn::keys::get_js_int_key; use lrwn::{AES128Key, EUI64}; type Function = Box Pin>>>; diff --git a/chirpstack/src/uplink/data.rs b/chirpstack/src/uplink/data.rs index 7a8a7228..76326c41 100644 --- a/chirpstack/src/uplink/data.rs +++ b/chirpstack/src/uplink/data.rs @@ -2,10 +2,11 @@ use std::collections::HashMap; use anyhow::{Context, Result}; use chrono::{DateTime, Duration, Local, Utc}; -use tracing::{error, info, span, trace, warn, Instrument, Level}; +use tracing::{debug, error, info, span, trace, warn, Instrument, Level}; use super::error::Error; -use super::{filter_rx_info_by_tenant_id, helpers, UplinkFrameSet}; +use super::{data_fns, filter_rx_info_by_tenant_id, helpers, UplinkFrameSet}; +use crate::backend::roaming; use crate::storage::error::Error as StorageError; use crate::storage::{ application, device, device_gateway, device_profile, device_queue, device_session, fields, @@ -69,6 +70,7 @@ impl Data { device_gateway_rx_info: None, }; + ctx.handle_passive_roaming_device().await?; ctx.get_device_session().await?; ctx.get_device().await?; ctx.get_device_profile().await?; @@ -76,13 +78,17 @@ impl Data { ctx.get_tenant().await?; ctx.abort_on_device_is_disabled().await?; ctx.set_device_info()?; + ctx.set_device_gateway_rx_info()?; ctx.handle_retransmission_reset().await?; ctx.set_device_lock().await?; ctx.set_scheduler_run_after().await?; - ctx.filter_rx_info_by_tenant().await?; + if !ctx._is_roaming() { + // In case of roaming we do not know the gateways and therefore it must not be + // filtered. + ctx.filter_rx_info_by_tenant().await?; + } ctx.decrypt_f_opts_mac_commands()?; ctx.decrypt_frm_payload()?; - ctx.get_mac_payload()?; ctx.log_uplink_frame_set().await?; ctx.set_adr()?; ctx.set_uplink_data_rate().await?; @@ -90,7 +96,9 @@ impl Data { // ctx.send_uplink_meta_data_to_network_controller()?; ctx.handle_mac_commands().await?; - ctx.save_device_gateway_rx_info().await?; + if !ctx._is_roaming() { + ctx.save_device_gateway_rx_info().await?; + } ctx.append_meta_data_to_uplink_history()?; ctx.send_uplink_event().await?; ctx.detect_and_save_measurements().await?; @@ -104,6 +112,25 @@ impl Data { Ok(()) } + async fn handle_passive_roaming_device(&mut self) -> Result<(), Error> { + trace!("Handling passive-roaming device"); + let mac = if let lrwn::Payload::MACPayload(pl) = &self.uplink_frame_set.phy_payload.payload + { + pl + } else { + return Err(Error::AnyhowError(anyhow!("Expected MacPayload"))); + }; + + if roaming::is_roaming_dev_addr(mac.fhdr.devaddr) { + debug!(dev_addr = %mac.fhdr.devaddr, "DevAddr does not match NetID, assuming roaming device"); + data_fns::Data::handle(self.uplink_frame_set.clone(), mac.clone()).await; + + return Err(Error::Abort); + } + + Ok(()) + } + async fn get_device_session(&mut self) -> Result<(), Error> { trace!("Getting device-session for dev_addr"); @@ -209,6 +236,30 @@ impl Data { Ok(()) } + fn set_device_gateway_rx_info(&mut self) -> Result<()> { + trace!("Setting gateway rx-info for device"); + + self.device_gateway_rx_info = Some(internal::DeviceGatewayRxInfo { + dev_eui: self.device_session.as_ref().unwrap().dev_eui.clone(), + dr: self.uplink_frame_set.dr as u32, + items: self + .uplink_frame_set + .rx_info_set + .iter() + .map(|rx_info| internal::DeviceGatewayRxInfoItem { + gateway_id: hex::decode(&rx_info.gateway_id).unwrap(), + rssi: rx_info.rssi, + lora_snr: rx_info.snr, + antenna: rx_info.antenna, + board: rx_info.board, + context: rx_info.context.clone(), + }) + .collect(), + }); + + Ok(()) + } + async fn abort_on_device_is_disabled(&self) -> Result<(), Error> { let device = self.device.as_ref().unwrap(); @@ -348,6 +399,7 @@ impl Data { .context("Decrypt f_opts")?; } } + Ok(()) } @@ -376,18 +428,6 @@ impl Data { Ok(()) } - fn get_mac_payload(&mut self) -> Result<()> { - if let lrwn::Payload::MACPayload(pl) = &self.uplink_frame_set.phy_payload.payload { - self.mac_payload = Some(pl.clone()); - } - - if self.mac_payload.is_none() { - return Err(anyhow!("No MacPayload")); - } - - Ok(()) - } - async fn log_uplink_frame_set(&self) -> Result<()> { trace!("Logging uplink frame-set"); let mut ufl: api::UplinkFrameLog = (&self.uplink_frame_set).try_into()?; @@ -495,29 +535,10 @@ impl Data { async fn save_device_gateway_rx_info(&mut self) -> Result<()> { trace!("Saving gateway rx-info for device"); - let dev_gw_rx_info = internal::DeviceGatewayRxInfo { - dev_eui: self.device_session.as_ref().unwrap().dev_eui.clone(), - dr: self.uplink_frame_set.dr as u32, - items: self - .uplink_frame_set - .rx_info_set - .iter() - .map(|rx_info| internal::DeviceGatewayRxInfoItem { - gateway_id: hex::decode(&rx_info.gateway_id).unwrap(), - rssi: rx_info.rssi, - lora_snr: rx_info.snr, - antenna: rx_info.antenna, - board: rx_info.board, - context: rx_info.context.clone(), - }) - .collect(), - }; - device_gateway::save_rx_info(&dev_gw_rx_info) + device_gateway::save_rx_info(self.device_gateway_rx_info.as_ref().unwrap()) .await - .context("Save rx-info")?; - - self.device_gateway_rx_info = Some(dev_gw_rx_info); + .context("Save gatewa rx-info for device")?; Ok(()) } @@ -573,7 +594,12 @@ impl Data { let app = self.application.as_ref().unwrap(); let dp = self.device_profile.as_ref().unwrap(); let dev = self.device.as_ref().unwrap(); - let mac = self.mac_payload.as_ref().unwrap(); + let mac = if let lrwn::Payload::MACPayload(pl) = &self.uplink_frame_set.phy_payload.payload + { + pl + } else { + return Err(anyhow!("Expected MacPayload")); + }; let mut pl = integration_pb::UplinkEvent { deduplication_id: self.uplink_frame_set.uplink_set_id.to_string(), @@ -739,7 +765,12 @@ impl Data { } async fn handle_uplink_ack(&self) -> Result<()> { - let mac = self.mac_payload.as_ref().unwrap(); + let mac = if let lrwn::Payload::MACPayload(pl) = &self.uplink_frame_set.phy_payload.payload + { + pl + } else { + return Err(anyhow!("Expected MacPayload")); + }; if !mac.fhdr.f_ctrl.ack { return Ok(()); } @@ -857,4 +888,8 @@ impl Data { Ok(()) } + + fn _is_roaming(&self) -> bool { + self.uplink_frame_set.roaming_meta_data.is_some() + } } diff --git a/chirpstack/src/uplink/data_fns.rs b/chirpstack/src/uplink/data_fns.rs new file mode 100644 index 00000000..99e47f01 --- /dev/null +++ b/chirpstack/src/uplink/data_fns.rs @@ -0,0 +1,221 @@ +use anyhow::Result; +use chrono::{Duration, Utc}; +use tracing::{error, info, span, trace, Instrument, Level}; +use uuid::Uuid; + +use super::{error::Error, filter_rx_info_by_public_only, UplinkFrameSet}; +use crate::api::backend::get_async_receiver; +use crate::backend::{keywrap, roaming}; +use crate::storage::passive_roaming; +use crate::uplink::helpers; +use chirpstack_api::internal; +use lrwn::NetID; + +pub struct Data { + uplink_frame_set: UplinkFrameSet, + mac_payload: lrwn::MACPayload, + pr_device_sessions: Vec, +} + +impl Data { + pub async fn handle(ufs: UplinkFrameSet, mac_pl: lrwn::MACPayload) { + let span = span!(Level::INFO, "data_pr"); + if let Err(e) = Data::_handle(ufs, mac_pl).instrument(span).await { + match e.downcast_ref::() { + Some(Error::Abort) => { + // nothing to do + } + Some(_) | None => { + error!(error = %e, "Handle passive-roaming uplink error"); + } + } + } + } + + async fn _handle(ufs: UplinkFrameSet, mac_pl: lrwn::MACPayload) -> Result<()> { + let mut ctx = Data { + uplink_frame_set: ufs, + mac_payload: mac_pl, + pr_device_sessions: Vec::new(), + }; + + ctx.filter_rx_info_by_public_only()?; + ctx.get_pr_device_sessions().await?; + ctx.start_pr_sessions().await?; + ctx.forward_uplink_for_sessions().await?; + ctx.save_pr_device_sessions().await?; + + Ok(()) + } + + fn filter_rx_info_by_public_only(&mut self) -> Result<()> { + trace!("Filtering rx_info by public gateways only"); + filter_rx_info_by_public_only(&mut self.uplink_frame_set)?; + Ok(()) + } + + async fn get_pr_device_sessions(&mut self) -> Result<()> { + trace!("Getting passive-roaming device-sessions"); + self.pr_device_sessions = + passive_roaming::get_for_phy_payload(&self.uplink_frame_set.phy_payload).await?; + + for ds in &mut self.pr_device_sessions { + ds.f_cnt_up = self.mac_payload.fhdr.f_cnt + 1; + } + + trace!( + count = self.pr_device_sessions.len(), + "Got passive-roaming device-sessions" + ); + + Ok(()) + } + + async fn start_pr_sessions(&mut self) -> Result<()> { + // Skip this step when we already have active sessions. + if !self.pr_device_sessions.is_empty() { + return Ok(()); + } + + let net_ids = roaming::get_net_ids_for_dev_addr(self.mac_payload.fhdr.devaddr); + + trace!(net_ids = ?net_ids, "Got NetIDs"); + + for net_id in net_ids { + let ds = match self.start_pr_session(net_id).await { + Ok(v) => v, + Err(e) => { + error!(net_id = %net_id, error = %e, "Start passive-roaming error"); + continue; + } + }; + + // No need to store the device-session or call XmitDataReq when + // lifetime is not set (stateless passive-roaming). + if ds.lifetime.is_some() { + self.pr_device_sessions.push(ds); + } + } + + Ok(()) + } + + async fn forward_uplink_for_sessions(&self) -> Result<()> { + trace!("Forwarding uplink for passive-roaming sessions"); + + for ds in &self.pr_device_sessions { + let mut req = backend::XmitDataReqPayload { + phy_payload: self.uplink_frame_set.phy_payload.to_vec()?, + ul_meta_data: Some(backend::ULMetaData { + dev_addr: self.mac_payload.fhdr.devaddr.to_vec(), + data_rate: Some(self.uplink_frame_set.dr), + ul_freq: Some((self.uplink_frame_set.tx_info.frequency as f64) / 1_000_000.0), + recv_time: helpers::get_rx_timestamp_chrono(&self.uplink_frame_set.rx_info_set), + rf_region: self + .uplink_frame_set + .region_common_name + .to_string() + .replace('_', "-"), + gw_cnt: Some(self.uplink_frame_set.rx_info_set.len()), + gw_info: roaming::rx_info_to_gw_info(&self.uplink_frame_set.rx_info_set)?, + ..Default::default() + }), + ..Default::default() + }; + + let net_id = NetID::from_slice(&ds.net_id)?; + let client = roaming::get(&net_id)?; + let async_receiver = match client.is_async() { + false => None, + true => Some( + get_async_receiver(req.base.transaction_id, client.get_async_timeout()).await?, + ), + }; + + if let Err(e) = client + .xmit_data_req(backend::Role::SNS, &mut req, async_receiver) + .await + { + error!(net_id = %net_id, error = %e, "XmitDataReq failed"); + } + } + + Ok(()) + } + + async fn save_pr_device_sessions(&self) -> Result<()> { + trace!("Saving passive-roaming device-sessions"); + + for ds in &self.pr_device_sessions { + passive_roaming::save(ds).await?; + } + + Ok(()) + } + + async fn start_pr_session( + &self, + net_id: NetID, + ) -> Result { + info!(net_id = %net_id, dev_addr = %self.mac_payload.fhdr.devaddr, "Starting passive-roaming session"); + + let mut pr_req = backend::PRStartReqPayload { + phy_payload: self.uplink_frame_set.phy_payload.to_vec()?, + ul_meta_data: backend::ULMetaData { + ul_freq: Some((self.uplink_frame_set.tx_info.frequency as f64) / 1_000_000.0), + data_rate: Some(self.uplink_frame_set.dr), + recv_time: helpers::get_rx_timestamp_chrono(&self.uplink_frame_set.rx_info_set), + rf_region: self + .uplink_frame_set + .region_common_name + .to_string() + .replace('_', "-"), + gw_cnt: Some(self.uplink_frame_set.rx_info_set.len()), + gw_info: roaming::rx_info_to_gw_info(&self.uplink_frame_set.rx_info_set)?, + ..Default::default() + }, + ..Default::default() + }; + + #[cfg(test)] + { + pr_req.base.transaction_id = 1234; + } + + let client = roaming::get(&net_id)?; + let async_receiver = match client.is_async() { + false => None, + true => Some( + get_async_receiver(pr_req.base.transaction_id, client.get_async_timeout()).await?, + ), + }; + + let pr_start_ans = client + .pr_start_req(backend::Role::SNS, &mut pr_req, async_receiver) + .await?; + let sess_id = Uuid::new_v4(); + + Ok(internal::PassiveRoamingDeviceSession { + session_id: sess_id.as_bytes().to_vec(), + net_id: net_id.to_vec(), + dev_addr: self.mac_payload.fhdr.devaddr.to_vec(), + lifetime: { + let lt = pr_start_ans.lifetime.unwrap_or_default() as i64; + if lt == 0 { + None + } else { + Some((Utc::now() + Duration::seconds(lt)).into()) + } + }, + f_nwk_s_int_key: match &pr_start_ans.f_nwk_s_int_key { + Some(ke) => keywrap::unwrap(ke)?.to_vec(), + None => match &pr_start_ans.nwk_s_key { + None => Vec::new(), + Some(ke) => keywrap::unwrap(ke)?.to_vec(), + }, + }, + f_cnt_up: pr_start_ans.f_cnt_up.unwrap_or_default(), + ..Default::default() + }) + } +} diff --git a/chirpstack/src/uplink/data_sns.rs b/chirpstack/src/uplink/data_sns.rs new file mode 100644 index 00000000..56466425 --- /dev/null +++ b/chirpstack/src/uplink/data_sns.rs @@ -0,0 +1,12 @@ +use tracing::{span, Instrument, Level}; + +use super::{data, UplinkFrameSet}; + +pub struct Data {} + +impl Data { + pub async fn handle(ufs: UplinkFrameSet) { + let span = span!(Level::INFO, "data_up_sns"); + data::Data::handle(ufs).instrument(span).await + } +} diff --git a/chirpstack/src/uplink/join.rs b/chirpstack/src/uplink/join.rs index ef6483ca..85785bbe 100644 --- a/chirpstack/src/uplink/join.rs +++ b/chirpstack/src/uplink/join.rs @@ -1,21 +1,22 @@ use std::convert::TryInto; use std::sync::Arc; -use aes::cipher::{generic_array::GenericArray, BlockEncrypt, NewBlockCipher}; -use aes::{Aes128, Block}; use anyhow::{Context, Result}; use chrono::{DateTime, Local, Utc}; use rand::RngCore; -use tracing::{error, span, trace, Instrument, Level}; +use tracing::{error, info, span, trace, Instrument, Level}; use lrwn::{ - AES128Key, CFList, DLSettings, DevAddr, JoinAcceptPayload, JoinRequestPayload, JoinType, MType, - Major, NetID, Payload, PhyPayload, EUI64, MHDR, + keys, AES128Key, CFList, DLSettings, DevAddr, JoinAcceptPayload, JoinRequestPayload, JoinType, + MType, Major, Payload, PhyPayload, MHDR, }; +use super::error::Error; +use super::join_fns; use super::{filter_rx_info_by_tenant_id, helpers, UplinkFrameSet}; +use crate::api::backend::get_async_receiver; use crate::api::helpers::ToProto; -use crate::backend::{joinserver, keywrap}; +use crate::backend::{joinserver, keywrap, roaming}; use crate::storage::device_session; use crate::storage::{ application, device, device_keys, device_profile, device_queue, error::Error as StorageError, @@ -49,7 +50,14 @@ impl JoinRequest { let span = span!(Level::INFO, "join_request"); if let Err(e) = JoinRequest::_handle(ufs).instrument(span).await { - error!(error = %e, "Handle join-request error"); + match e.downcast_ref::() { + Some(Error::Abort) => { + // nothing to do + } + Some(_) | None => { + error!(error = %e, "Handle join-request error"); + } + } } } @@ -74,8 +82,8 @@ impl JoinRequest { }; ctx.get_join_request_payload()?; + ctx.get_device_or_try_pr_roaming().await?; ctx.get_js_client()?; - ctx.get_device().await?; ctx.get_application().await?; ctx.get_tenant().await?; ctx.get_device_profile().await?; @@ -130,9 +138,27 @@ impl JoinRequest { Ok(()) } - async fn get_device(&mut self) -> Result<()> { + async fn get_device_or_try_pr_roaming(&mut self) -> Result<()> { trace!("Getting device"); - self.device = Some(device::get(&self.join_request.unwrap().dev_eui).await?); + let jr = self.join_request.as_ref().unwrap(); + let dev = match device::get(&jr.dev_eui).await { + Ok(v) => v, + Err(e) => { + if let StorageError::NotFound(_) = e { + if !roaming::is_enabled() { + return Err(anyhow::Error::new(e)); + } + + info!(dev_eui = %jr.dev_eui, join_eui = %jr.join_eui, "Unknown device, trying passive-roaming activation"); + join_fns::JoinRequest::start_pr(self.uplink_frame_set.clone(), *jr).await?; + return Err(anyhow::Error::new(Error::Abort)); + } else { + return Err(anyhow::Error::new(e)); + } + } + }; + + self.device = Some(dev); Ok(()) } @@ -371,7 +397,18 @@ impl JoinRequest { ..Default::default() }; - let join_ans_pl = js_client.join_req(&mut join_req_pl).await?; + let async_receiver = match js_client.is_async() { + false => None, + true => Some( + get_async_receiver( + join_req_pl.base.transaction_id, + js_client.get_async_timeout(), + ) + .await?, + ), + }; + + let join_ans_pl = js_client.join_req(&mut join_req_pl, async_receiver).await?; if let Some(v) = &join_ans_pl.app_s_key { self.app_s_key = Some(common::KeyEnvelope { @@ -451,7 +488,7 @@ impl JoinRequest { }; if opt_neg { - let js_int_key = get_js_int_key(&join_request.dev_eui, &dk.nwk_key)?; + let js_int_key = keys::get_js_int_key(&join_request.dev_eui, &dk.nwk_key)?; phy.set_join_accept_mic( JoinType::Join, &join_request.join_eui, @@ -473,7 +510,7 @@ impl JoinRequest { trace!("Setting session-keys"); let device_keys = self.device_keys.as_ref().unwrap(); - self.f_nwk_s_int_key = Some(get_f_nwk_s_int_key( + self.f_nwk_s_int_key = Some(keys::get_f_nwk_s_int_key( opt_neg, &device_keys.nwk_key, &conf.network.net_id, @@ -483,7 +520,7 @@ impl JoinRequest { )?); self.s_nwk_s_int_key = Some(match opt_neg { - true => get_s_nwk_s_int_key( + true => keys::get_s_nwk_s_int_key( opt_neg, &device_keys.nwk_key, &conf.network.net_id, @@ -491,7 +528,7 @@ impl JoinRequest { device_keys.join_nonce as u32, join_request.dev_nonce, )?, - false => get_f_nwk_s_int_key( + false => keys::get_f_nwk_s_int_key( opt_neg, &device_keys.nwk_key, &conf.network.net_id, @@ -502,7 +539,7 @@ impl JoinRequest { }); self.nwk_s_enc_key = Some(match opt_neg { - true => get_nwk_s_enc_key( + true => keys::get_nwk_s_enc_key( opt_neg, &device_keys.nwk_key, &conf.network.net_id, @@ -510,7 +547,7 @@ impl JoinRequest { device_keys.join_nonce as u32, join_request.dev_nonce, )?, - false => get_f_nwk_s_int_key( + false => keys::get_f_nwk_s_int_key( opt_neg, &device_keys.nwk_key, &conf.network.net_id, @@ -523,7 +560,7 @@ impl JoinRequest { self.app_s_key = Some(common::KeyEnvelope { kek_label: "".to_string(), aes_key: match opt_neg { - true => get_app_s_key( + true => keys::get_app_s_key( opt_neg, &device_keys.app_key, &conf.network.net_id, @@ -531,7 +568,7 @@ impl JoinRequest { device_keys.join_nonce as u32, join_request.dev_nonce, )?, - false => get_app_s_key( + false => keys::get_app_s_key( opt_neg, &device_keys.nwk_key, &conf.network.net_id, @@ -709,258 +746,3 @@ impl JoinRequest { Ok(()) } } - -// For LoRaWAN 1.0: SNwkSIntKey = NwkSEncKey = FNwkSIntKey = NwkSKey -fn get_f_nwk_s_int_key( - opt_neg: bool, - nwk_key: &AES128Key, - net_id: &NetID, - join_eui: &EUI64, - join_nonce: u32, - dev_nonce: u16, -) -> Result { - get_s_key( - opt_neg, 0x01, nwk_key, net_id, join_eui, join_nonce, dev_nonce, - ) -} - -fn get_app_s_key( - opt_neg: bool, - nwk_key: &AES128Key, - net_id: &NetID, - join_eui: &EUI64, - join_nonce: u32, - dev_nonce: u16, -) -> Result { - get_s_key( - opt_neg, 0x02, nwk_key, net_id, join_eui, join_nonce, dev_nonce, - ) -} - -fn get_s_nwk_s_int_key( - opt_neg: bool, - nwk_key: &AES128Key, - net_id: &NetID, - join_eui: &EUI64, - join_nonce: u32, - dev_nonce: u16, -) -> Result { - get_s_key( - opt_neg, 0x03, nwk_key, net_id, join_eui, join_nonce, dev_nonce, - ) -} - -fn get_nwk_s_enc_key( - opt_neg: bool, - nwk_key: &AES128Key, - net_id: &NetID, - join_eui: &EUI64, - join_nonce: u32, - dev_nonce: u16, -) -> Result { - get_s_key( - opt_neg, 0x04, nwk_key, net_id, join_eui, join_nonce, dev_nonce, - ) -} - -fn get_js_enc_key(dev_eui: &EUI64, nwk_key: &AES128Key) -> Result { - get_js_key(0x05, dev_eui, nwk_key) -} - -pub fn get_js_int_key(dev_eui: &EUI64, nwk_key: &AES128Key) -> Result { - get_js_key(0x06, dev_eui, nwk_key) -} - -fn get_s_key( - opt_neg: bool, - typ: u8, - nwk_key: &AES128Key, - net_id: &NetID, - join_eui: &EUI64, - join_nonce: u32, - dev_nonce: u16, -) -> Result { - let key_bytes = nwk_key.to_bytes(); - let key = GenericArray::from_slice(&key_bytes); - let cipher = Aes128::new(key); - - let mut b: [u8; 16] = [0; 16]; - - b[0] = typ; - if opt_neg { - b[1..4].clone_from_slice(&join_nonce.to_le_bytes()[0..3]); - b[4..12].clone_from_slice(&join_eui.to_le_bytes()); - b[12..14].clone_from_slice(&dev_nonce.to_le_bytes()[0..2]); - } else { - b[1..4].clone_from_slice(&join_nonce.to_le_bytes()[0..3]); - b[4..7].clone_from_slice(&net_id.to_le_bytes()); - b[7..9].clone_from_slice(&dev_nonce.to_le_bytes()[0..2]); - } - - let block = Block::from_mut_slice(&mut b); - cipher.encrypt_block(block); - - Ok(AES128Key::from_slice(block)?) -} - -fn get_js_key(typ: u8, dev_eui: &EUI64, nwk_key: &AES128Key) -> Result { - let key_bytes = nwk_key.to_bytes(); - let key = GenericArray::from_slice(&key_bytes); - let cipher = Aes128::new(key); - - let mut b: [u8; 16] = [0; 16]; - b[0] = typ; - b[1..9].clone_from_slice(&dev_eui.to_le_bytes()); - - let block = Block::from_mut_slice(&mut b); - cipher.encrypt_block(block); - - Ok(AES128Key::from_slice(block)?) -} - -#[cfg(test)] -pub mod test { - use super::*; - - fn nwk_key() -> AES128Key { - AES128Key::from_bytes([ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, - 0x07, 0x08, - ]) - } - - fn app_key() -> AES128Key { - AES128Key::from_bytes([ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - ]) - } - - fn dev_eui() -> EUI64 { - EUI64::from_be_bytes([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]) - } - - fn join_eui() -> EUI64 { - EUI64::from_be_bytes([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]) - } - - fn join_nonce() -> u32 { - 65536 - } - - fn dev_nonce() -> u16 { - 258 - } - - fn net_id() -> NetID { - NetID::from_be_bytes([0x01, 0x02, 0x03]) - } - - #[test] - fn lorawan_1_0() { - let nwk_s_key = get_f_nwk_s_int_key( - false, - &nwk_key(), - &net_id(), - &join_eui(), - join_nonce(), - dev_nonce(), - ) - .unwrap(); - - let app_s_key = get_app_s_key( - false, - &nwk_key(), - &net_id(), - &join_eui(), - join_nonce(), - dev_nonce(), - ) - .unwrap(); - - assert_eq!( - AES128Key::from_bytes([ - 223, 83, 195, 95, 48, 52, 204, 206, 208, 255, 53, 76, 112, 222, 4, 223, - ]), - nwk_s_key - ); - - assert_eq!( - AES128Key::from_bytes([ - 146, 123, 156, 145, 17, 131, 207, 254, 76, 178, 255, 75, 117, 84, 95, 109 - ]), - app_s_key - ) - } - - #[test] - fn lorawan_1_1() { - let app_s_key = get_app_s_key( - true, - &app_key(), - &net_id(), - &join_eui(), - join_nonce(), - dev_nonce(), - ) - .unwrap(); - - let f_nwk_s_int_key = get_f_nwk_s_int_key( - true, - &nwk_key(), - &net_id(), - &join_eui(), - join_nonce(), - dev_nonce(), - ) - .unwrap(); - - let s_nwk_s_int_key = get_s_nwk_s_int_key( - true, - &nwk_key(), - &net_id(), - &join_eui(), - join_nonce(), - dev_nonce(), - ) - .unwrap(); - - let nwk_s_enc_key = get_nwk_s_enc_key( - true, - &nwk_key(), - &net_id(), - &join_eui(), - join_nonce(), - dev_nonce(), - ) - .unwrap(); - - assert_eq!( - AES128Key::from_bytes([ - 1, 98, 18, 21, 209, 202, 8, 254, 191, 12, 96, 44, 194, 173, 144, 250 - ]), - app_s_key, - ); - - assert_eq!( - AES128Key::from_bytes([ - 83, 127, 138, 174, 137, 108, 121, 224, 21, 209, 2, 208, 98, 134, 53, 78 - ]), - f_nwk_s_int_key, - ); - - assert_eq!( - AES128Key::from_bytes([ - 88, 148, 152, 153, 48, 146, 207, 219, 95, 210, 224, 42, 199, 81, 11, 241 - ]), - s_nwk_s_int_key, - ); - - assert_eq!( - AES128Key::from_bytes([ - 152, 152, 40, 60, 79, 102, 235, 108, 111, 213, 22, 88, 130, 4, 108, 64 - ]), - nwk_s_enc_key, - ); - } -} diff --git a/chirpstack/src/uplink/join_fns.rs b/chirpstack/src/uplink/join_fns.rs new file mode 100644 index 00000000..1fd25f10 --- /dev/null +++ b/chirpstack/src/uplink/join_fns.rs @@ -0,0 +1,184 @@ +use std::sync::Arc; + +use anyhow::{Context, Result}; +use chrono::{Duration, Utc}; +use tracing::{span, trace, Instrument, Level}; +use uuid::Uuid; + +use super::{filter_rx_info_by_public_only, UplinkFrameSet}; +use crate::api::backend::get_async_receiver; +use crate::backend::{joinserver, keywrap, roaming}; +use crate::downlink; +use crate::storage::passive_roaming; +use crate::uplink::helpers; +use backend::Client; +use chirpstack_api::internal; +use lrwn::{JoinRequestPayload, NetID}; + +pub struct JoinRequest { + uplink_frame_set: UplinkFrameSet, + join_request: JoinRequestPayload, + home_net_id: Option, + client: Option>, + pr_start_ans: Option, +} + +impl JoinRequest { + pub async fn start_pr(ufs: UplinkFrameSet, jr: JoinRequestPayload) -> Result<()> { + let span = span!(Level::INFO, "start_pr"); + JoinRequest::_start_pr(ufs, jr).instrument(span).await + } + + async fn _start_pr(ufs: UplinkFrameSet, jr: JoinRequestPayload) -> Result<()> { + let mut ctx = JoinRequest { + uplink_frame_set: ufs, + join_request: jr, + home_net_id: None, + client: None, + pr_start_ans: None, + }; + + ctx.filter_rx_info_by_public_only()?; + ctx.get_home_net_id().await?; + ctx.get_client()?; + ctx.start_roaming().await?; + ctx.save_roaming_session().await?; + + Ok(()) + } + + fn filter_rx_info_by_public_only(&mut self) -> Result<()> { + trace!("Filtering rx_info by public gateways only"); + filter_rx_info_by_public_only(&mut self.uplink_frame_set)?; + + Ok(()) + } + + async fn get_home_net_id(&mut self) -> Result<()> { + trace!("Getting home netid"); + + trace!(join_eui = %self.join_request.join_eui, "Trying to get join-server client"); + let js_client = joinserver::get(&self.join_request.join_eui)?; + + let mut home_ns_req = backend::HomeNSReqPayload { + dev_eui: self.join_request.dev_eui.to_vec(), + ..Default::default() + }; + + #[cfg(test)] + { + home_ns_req.base.transaction_id = 1234; + } + + trace!("Requesting home netid"); + let home_ns_ans = js_client.home_ns_req(&mut home_ns_req, None).await?; + self.home_net_id = Some(NetID::from_slice(&home_ns_ans.h_net_id)?); + + Ok(()) + } + + fn get_client(&mut self) -> Result<()> { + let net_id = self.home_net_id.as_ref().unwrap(); + trace!(net_id = %net_id, "Getting backend interfaces client"); + self.client = Some(roaming::get(net_id)?); + Ok(()) + } + + async fn start_roaming(&mut self) -> Result<()> { + trace!("Starting passive-roaming"); + + let mut pr_req = backend::PRStartReqPayload { + phy_payload: self.uplink_frame_set.phy_payload.to_vec()?, + ul_meta_data: backend::ULMetaData { + dev_eui: self.join_request.dev_eui.to_vec(), + ul_freq: Some((self.uplink_frame_set.tx_info.frequency as f64) / 1_000_000.0), + data_rate: Some(self.uplink_frame_set.dr), + recv_time: helpers::get_rx_timestamp_chrono(&self.uplink_frame_set.rx_info_set), + rf_region: self + .uplink_frame_set + .region_common_name + .to_string() + .replace('_', "-"), + gw_cnt: Some(self.uplink_frame_set.rx_info_set.len()), + gw_info: roaming::rx_info_to_gw_info(&self.uplink_frame_set.rx_info_set)?, + ..Default::default() + }, + ..Default::default() + }; + + #[cfg(test)] + { + pr_req.base.transaction_id = 1234; + } + + let client = self.client.as_ref().unwrap(); + let async_receiver = match client.is_async() { + false => None, + true => Some( + get_async_receiver(pr_req.base.transaction_id, client.get_async_timeout()).await?, + ), + }; + + let resp = client + .pr_start_req(backend::Role::SNS, &mut pr_req, async_receiver) + .await?; + + if let Some(dl_meta) = &resp.dl_meta_data { + downlink::roaming::PassiveRoamingDownlink::handle( + self.uplink_frame_set.clone(), + resp.phy_payload.clone(), + dl_meta.clone(), + ) + .await?; + } else { + return Err(anyhow!("DLMetaData is not set")); + } + + self.pr_start_ans = Some(resp); + Ok(()) + } + + async fn save_roaming_session(&mut self) -> Result<()> { + trace!("Saving roaming-session"); + + let pr_start_ans = self.pr_start_ans.as_ref().unwrap(); + + if pr_start_ans.dev_addr.is_empty() + || pr_start_ans.lifetime.is_none() + || pr_start_ans.lifetime.unwrap() == 0 + { + return Ok(()); + } + + let sess_id = Uuid::new_v4(); + + let sess = internal::PassiveRoamingDeviceSession { + session_id: sess_id.as_bytes().to_vec(), + net_id: self.home_net_id.unwrap().to_vec(), + dev_addr: pr_start_ans.dev_addr.clone(), + dev_eui: self.join_request.dev_eui.to_vec(), + lifetime: { + let lt = pr_start_ans.lifetime.unwrap_or_default() as i64; + if lt == 0 { + None + } else { + Some((Utc::now() + Duration::seconds(lt)).into()) + } + }, + lorawan_1_1: pr_start_ans.f_nwk_s_int_key.is_some(), + + f_nwk_s_int_key: match &pr_start_ans.f_nwk_s_int_key { + Some(ke) => keywrap::unwrap(ke)?.to_vec(), + None => match &pr_start_ans.nwk_s_key { + None => Vec::new(), + Some(ke) => keywrap::unwrap(ke)?.to_vec(), + }, + }, + ..Default::default() + }; + + passive_roaming::save(&sess) + .await + .context("Save passive-roaming device-session") + } +} diff --git a/chirpstack/src/uplink/join_sns.rs b/chirpstack/src/uplink/join_sns.rs new file mode 100644 index 00000000..ffdaba2a --- /dev/null +++ b/chirpstack/src/uplink/join_sns.rs @@ -0,0 +1,765 @@ +use std::sync::Arc; + +use anyhow::{Context, Result}; +use chrono::{DateTime, Local, Utc}; +use rand::RngCore; +use tracing::{span, trace, Instrument, Level}; + +use super::{helpers, UplinkFrameSet}; +use crate::api::helpers::ToProto; +use crate::backend::{joinserver, keywrap, roaming}; +use crate::storage::{ + application, device, device_keys, device_profile, device_queue, device_session, + error::Error as StorageError, metrics, tenant, +}; +use crate::{config, integration, metalog, region}; +use backend::{PRStartAnsPayload, PRStartReqPayload}; +use chirpstack_api::{common, integration as integration_pb, internal, meta}; +use lrwn::{keys, AES128Key, DevAddr, NetID}; + +pub struct JoinRequest { + uplink_frame_set: UplinkFrameSet, + pr_start_req: PRStartReqPayload, + pr_start_ans: Option, + + join_request: Option, + join_accept: Option, + device: Option, + device_session: Option, + js_client: Option>, + application: Option, + tenant: Option, + device_profile: Option, + device_keys: Option, + device_info: Option, + dev_addr: Option, + f_nwk_s_int_key: Option, + s_nwk_s_int_key: Option, + nwk_s_enc_key: Option, + app_s_key: Option, +} + +impl JoinRequest { + pub async fn start_pr( + ufs: UplinkFrameSet, + pr_start_req: PRStartReqPayload, + ) -> Result { + let span = span!(Level::INFO, "start_pr"); + JoinRequest::_start_pr(ufs, pr_start_req) + .instrument(span) + .await + } + + async fn _start_pr( + ufs: UplinkFrameSet, + pr_start_req: PRStartReqPayload, + ) -> Result { + let mut ctx = JoinRequest { + uplink_frame_set: ufs, + pr_start_req, + + pr_start_ans: None, + join_request: None, + join_accept: None, + device: None, + device_session: None, + js_client: None, + application: None, + tenant: None, + device_profile: None, + device_keys: None, + device_info: None, + dev_addr: None, + f_nwk_s_int_key: None, + s_nwk_s_int_key: None, + nwk_s_enc_key: None, + app_s_key: None, + }; + + ctx.get_join_request_payload()?; + ctx.get_device().await?; + ctx.get_js_client()?; + ctx.get_application().await?; + ctx.get_tenant().await?; + ctx.get_device_profile().await?; + ctx.set_device_info()?; + ctx.abort_on_device_is_disabled()?; + ctx.abort_on_otaa_is_disabled()?; + ctx.get_random_dev_addr()?; + if ctx.js_client.is_some() { + // Using join-server + ctx.get_join_accept_from_js().await?; + } else { + // Using internal keys + ctx.validate_dev_nonce_and_get_device_keys().await?; + ctx.validate_mic().await?; + ctx.construct_join_accept_and_set_keys()?; + ctx.save_device_keys().await?; + } + ctx.log_uplink_meta().await?; + ctx.create_device_session().await?; + ctx.flush_device_queue().await?; + ctx.set_device_mode().await?; + ctx.send_join_event().await?; + ctx.set_pr_start_ans_payload()?; + + ctx.pr_start_ans + .ok_or(anyhow!("PRStartAnsPayload is not set")) + } + + fn get_join_request_payload(&mut self) -> Result<()> { + trace!("Getting JoinRequestPayload"); + self.join_request = Some(match self.uplink_frame_set.phy_payload.payload { + lrwn::Payload::JoinRequest(pl) => pl, + _ => { + return Err(anyhow!("PhyPayload does not contain JoinRequest payload")); + } + }); + Ok(()) + } + + async fn get_device(&mut self) -> Result<()> { + trace!("Getting device"); + let jr = self.join_request.as_ref().unwrap(); + let dev = device::get(&jr.dev_eui).await?; + self.device = Some(dev); + Ok(()) + } + + fn get_js_client(&mut self) -> Result<()> { + let jr = self.join_request.as_ref().unwrap(); + + trace!(join_eui = %jr.join_eui, "Trying to get Join Server client"); + if let Ok(v) = joinserver::get(&jr.join_eui) { + trace!("Found Join Server client"); + self.js_client = Some(v); + } else { + trace!("Join Server client does not exist"); + } + + Ok(()) + } + + async fn get_application(&mut self) -> Result<()> { + trace!("Getting application"); + self.application = + Some(application::get(&self.device.as_ref().unwrap().application_id).await?); + Ok(()) + } + + async fn get_tenant(&mut self) -> Result<()> { + trace!("Getting tenant"); + self.tenant = Some(tenant::get(&self.application.as_ref().unwrap().tenant_id).await?); + Ok(()) + } + + async fn get_device_profile(&mut self) -> Result<()> { + trace!("Getting device-profile"); + + let dp = device_profile::get(&self.device.as_ref().unwrap().device_profile_id).await?; + if dp.region != self.uplink_frame_set.region_common_name { + return Err(anyhow!("Invalid device-profile region")); + } + + self.device_profile = Some(dp); + Ok(()) + } + + fn set_device_info(&mut self) -> Result<()> { + let tenant = self.tenant.as_ref().unwrap(); + let app = self.application.as_ref().unwrap(); + let dp = self.device_profile.as_ref().unwrap(); + let dev = self.device.as_ref().unwrap(); + + let mut tags = (&*dp.tags).clone(); + tags.clone_from(&*dev.tags); + + self.device_info = Some(integration_pb::DeviceInfo { + tenant_id: tenant.id.to_string(), + tenant_name: tenant.name.clone(), + application_id: app.id.to_string(), + application_name: app.name.to_string(), + device_profile_id: dp.id.to_string(), + device_profile_name: dp.name.clone(), + device_name: dev.name.clone(), + dev_eui: dev.dev_eui.to_string(), + tags, + }); + Ok(()) + } + + fn abort_on_device_is_disabled(&self) -> Result<()> { + if self.device.as_ref().unwrap().is_disabled { + return Err(anyhow!("Device is disabled")); + } + Ok(()) + } + + fn abort_on_otaa_is_disabled(&self) -> Result<()> { + if !self.device_profile.as_ref().unwrap().supports_otaa { + return Err(anyhow!("OTAA is disabled in device-profile")); + } + Ok(()) + } + + fn get_random_dev_addr(&mut self) -> Result<()> { + let conf = config::get(); + + let mut dev_addr: [u8; 4] = [0; 4]; + + rand::thread_rng().fill_bytes(&mut dev_addr); + + #[cfg(test)] + { + dev_addr = [1, 2, 3, 4]; + } + + let mut dev_addr = DevAddr::from_be_bytes(dev_addr); + dev_addr.set_addr_prefix(&conf.network.net_id); + self.dev_addr = Some(dev_addr); + + Ok(()) + } + + async fn get_join_accept_from_js(&mut self) -> Result<()> { + trace!("Getting join-accept from Join Server"); + + let js_client = self.js_client.as_ref().unwrap(); + let region_network = config::get_region_network(&self.uplink_frame_set.region_name)?; + let region_conf = region::get(&self.uplink_frame_set.region_name)?; + + let phy_b = self.uplink_frame_set.phy_payload.to_vec()?; + let dp = self.device_profile.as_ref().unwrap(); + let dev = self.device.as_ref().unwrap(); + + // The opt_neg flag is set for devices other than 1.0.x. + let opt_neg = !self + .device_profile + .as_ref() + .unwrap() + .mac_version + .to_string() + .starts_with("1.0"); + + let dl_settings = lrwn::DLSettings { + opt_neg, + rx2_dr: region_network.rx2_dr, + rx1_dr_offset: region_network.rx1_dr_offset, + }; + + let mut join_req_pl = backend::JoinReqPayload { + mac_version: dp.mac_version.to_string(), + phy_payload: phy_b, + dev_eui: dev.dev_eui.to_vec(), + dev_addr: self.dev_addr.unwrap().to_vec(), + dl_settings: dl_settings.to_le_bytes()?.to_vec(), + rx_delay: region_network.rx1_delay, + cf_list: match region_conf.get_cf_list(dp.mac_version) { + Some(v) => v.to_bytes()?.to_vec(), + None => Vec::new(), + }, + ..Default::default() + }; + + let join_ans_pl = js_client.join_req(&mut join_req_pl, None).await?; + + if let Some(v) = &join_ans_pl.app_s_key { + self.app_s_key = Some(common::KeyEnvelope { + kek_label: v.kek_label.clone(), + aes_key: v.aes_key.clone(), + }); + } + + if let Some(v) = &join_ans_pl.nwk_s_key { + let key = keywrap::unwrap(v).context("Unwrap nwk_s_key")?; + self.s_nwk_s_int_key = Some(key); + self.f_nwk_s_int_key = Some(key); + self.nwk_s_enc_key = Some(key); + } + + if let Some(v) = &join_ans_pl.s_nwk_s_int_key { + let key = keywrap::unwrap(v).context("Unwrap s_nwk_s_int_key")?; + self.s_nwk_s_int_key = Some(key); + } + + if let Some(v) = &join_ans_pl.f_nwk_s_int_key { + let key = keywrap::unwrap(v).context("Unwrap f_nwk_s_int_key")?; + self.f_nwk_s_int_key = Some(key); + } + + if let Some(v) = &join_ans_pl.nwk_s_enc_key { + let key = keywrap::unwrap(v).context("Unwrap nwk_s_enc_key")?; + self.nwk_s_enc_key = Some(key); + } + + self.join_accept = Some( + lrwn::PhyPayload::from_slice(&join_ans_pl.phy_payload).context("Decode PhyPayload")?, + ); + + Ok(()) + } + + async fn validate_dev_nonce_and_get_device_keys(&mut self) -> Result<()> { + trace!("Validate dev-nonce and get device-keys"); + let dev = self.device.as_ref().unwrap(); + let app = self.application.as_ref().unwrap(); + let join_request = self.join_request.as_ref().unwrap(); + + self.device_keys = Some( + match device_keys::validate_and_store_dev_nonce( + &dev.dev_eui, + join_request.dev_nonce as i32, + ) + .await + { + Ok(v) => v, + Err(v) => match v { + StorageError::InvalidDevNonce => { + integration::log_event( + &app.id, + &dev.variables, + &integration_pb::LogEvent { + deduplication_id: self.uplink_frame_set.uplink_set_id.to_string(), + time: Some(Utc::now().into()), + device_info: self.device_info.clone(), + level: integration_pb::LogLevel::Error.into(), + code: integration_pb::LogCode::Otaa.into(), + description: "DevNonce has already been used".into(), + ..Default::default() + }, + ) + .await?; + + metrics::save( + &format!("device:{}", dev.dev_eui), + &metrics::Record { + time: Local::now(), + kind: metrics::Kind::ABSOLUTE, + metrics: [("error_OTAA".into(), 1f64)].iter().cloned().collect(), + }, + ) + .await?; + + return Err(v.into()); + } + _ => { + return Err(v.into()); + } + }, + }, + ); + + Ok(()) + } + + async fn validate_mic(&self) -> Result<()> { + let device_keys = self.device_keys.as_ref().unwrap(); + if self + .uplink_frame_set + .phy_payload + .validate_join_request_mic(&device_keys.nwk_key)? + { + return Ok(()); + } + + let app = self.application.as_ref().unwrap(); + let dev = self.device.as_ref().unwrap(); + + integration::log_event( + &app.id, + &dev.variables, + &integration_pb::LogEvent { + deduplication_id: self.uplink_frame_set.uplink_set_id.to_string(), + time: Some(Utc::now().into()), + device_info: self.device_info.clone(), + level: integration_pb::LogLevel::Error.into(), + code: integration_pb::LogCode::UplinkMic.into(), + description: "MIC of join-request is invalid, make sure keys are correct".into(), + ..Default::default() + }, + ) + .await?; + + metrics::save( + &format!("device:{}", dev.dev_eui), + &metrics::Record { + time: Local::now(), + kind: metrics::Kind::ABSOLUTE, + metrics: [("error_UPLINK_MIC".into(), 1f64)] + .iter() + .cloned() + .collect(), + }, + ) + .await?; + + Err(anyhow!("Invalid MIC")) + } + + fn construct_join_accept_and_set_keys(&mut self) -> Result<()> { + trace!("Constructing JoinAccept payload"); + + let conf = config::get(); + let region_network = config::get_region_network(&self.uplink_frame_set.region_name)?; + let region_conf = region::get(&self.uplink_frame_set.region_name)?; + let join_request = self.join_request.as_ref().unwrap(); + + let dk = self.device_keys.as_mut().unwrap(); + if dk.join_nonce == (1 << 24) - 1 { + return Err(anyhow!("Join-nonce overflow")); + } + + // The opt_neg flag is set for devices other than 1.0.x. + let opt_neg = !self + .device_profile + .as_ref() + .unwrap() + .mac_version + .to_string() + .starts_with("1.0"); + + let mut phy = lrwn::PhyPayload { + mhdr: lrwn::MHDR { + m_type: lrwn::MType::JoinAccept, + major: lrwn::Major::LoRaWANR1, + }, + payload: lrwn::Payload::JoinAccept(lrwn::JoinAcceptPayload { + join_nonce: dk.join_nonce as u32, + home_netid: conf.network.net_id, + devaddr: self.dev_addr.unwrap(), + dl_settings: lrwn::DLSettings { + opt_neg, + rx2_dr: region_network.rx2_dr, + rx1_dr_offset: region_network.rx1_dr_offset, + }, + rx_delay: region_network.rx1_delay, + cflist: region_conf.get_cf_list(self.device_profile.as_ref().unwrap().mac_version), + }), + mic: None, // we need to calculate this + }; + + if opt_neg { + let js_int_key = keys::get_js_int_key(&join_request.dev_eui, &dk.nwk_key)?; + phy.set_join_accept_mic( + lrwn::JoinType::Join, + &join_request.join_eui, + join_request.dev_nonce, + &js_int_key, + )?; + } else { + phy.set_join_accept_mic( + lrwn::JoinType::Join, + &join_request.join_eui, + join_request.dev_nonce, + &dk.nwk_key, + )?; + } + + phy.encrypt_join_accept_payload(&dk.nwk_key)?; + self.join_accept = Some(phy); + + trace!("Setting session-keys"); + let device_keys = self.device_keys.as_ref().unwrap(); + + self.f_nwk_s_int_key = Some(keys::get_f_nwk_s_int_key( + opt_neg, + &device_keys.nwk_key, + &conf.network.net_id, + &join_request.join_eui, + device_keys.join_nonce as u32, + join_request.dev_nonce, + )?); + + self.s_nwk_s_int_key = Some(match opt_neg { + true => keys::get_s_nwk_s_int_key( + opt_neg, + &device_keys.nwk_key, + &conf.network.net_id, + &join_request.join_eui, + device_keys.join_nonce as u32, + join_request.dev_nonce, + )?, + false => keys::get_f_nwk_s_int_key( + opt_neg, + &device_keys.nwk_key, + &conf.network.net_id, + &join_request.join_eui, + device_keys.join_nonce as u32, + join_request.dev_nonce, + )?, + }); + + self.nwk_s_enc_key = Some(match opt_neg { + true => keys::get_nwk_s_enc_key( + opt_neg, + &device_keys.nwk_key, + &conf.network.net_id, + &join_request.join_eui, + device_keys.join_nonce as u32, + join_request.dev_nonce, + )?, + false => keys::get_f_nwk_s_int_key( + opt_neg, + &device_keys.nwk_key, + &conf.network.net_id, + &join_request.join_eui, + device_keys.join_nonce as u32, + join_request.dev_nonce, + )?, + }); + + self.app_s_key = Some(common::KeyEnvelope { + kek_label: "".to_string(), + aes_key: match opt_neg { + true => keys::get_app_s_key( + opt_neg, + &device_keys.app_key, + &conf.network.net_id, + &join_request.join_eui, + device_keys.join_nonce as u32, + join_request.dev_nonce, + )?, + false => keys::get_app_s_key( + opt_neg, + &device_keys.nwk_key, + &conf.network.net_id, + &join_request.join_eui, + device_keys.join_nonce as u32, + join_request.dev_nonce, + )?, + } + .to_vec(), + }); + + Ok(()) + } + + async fn save_device_keys(&mut self) -> Result<()> { + trace!("Updating device-keys"); + let mut dk = self.device_keys.as_mut().unwrap(); + dk.join_nonce += 1; + + *dk = device_keys::update(dk.clone()).await?; + Ok(()) + } + + async fn log_uplink_meta(&self) -> Result<()> { + trace!("Logging uplink meta"); + + let req = meta::UplinkMeta { + dev_eui: self.device.as_ref().unwrap().dev_eui.to_string(), + tx_info: Some(self.uplink_frame_set.tx_info.clone()), + rx_info: self.uplink_frame_set.rx_info_set.clone(), + message_type: common::MType::JoinRequest.into(), + phy_payload_byte_count: self.uplink_frame_set.phy_payload.to_vec()?.len() as u32, + ..Default::default() + }; + + metalog::log_uplink(&req).await?; + + Ok(()) + } + + async fn create_device_session(&mut self) -> Result<()> { + trace!("Creating device-session"); + + let region_conf = region::get(&self.uplink_frame_set.region_name)?; + let region_network = config::get_region_network(&self.uplink_frame_set.region_name)?; + + let device = self.device.as_ref().unwrap(); + let device_profile = self.device_profile.as_ref().unwrap(); + let join_request = self.join_request.as_ref().unwrap(); + + let mut ds = internal::DeviceSession { + region_name: self.uplink_frame_set.region_name.clone(), + dev_eui: device.dev_eui.to_be_bytes().to_vec(), + dev_addr: self.dev_addr.unwrap().to_be_bytes().to_vec(), + join_eui: join_request.join_eui.to_be_bytes().to_vec(), + mac_version: device_profile.mac_version.to_proto().into(), + f_nwk_s_int_key: self.f_nwk_s_int_key.as_ref().unwrap().to_vec(), + s_nwk_s_int_key: self.s_nwk_s_int_key.as_ref().unwrap().to_vec(), + nwk_s_enc_key: self.nwk_s_enc_key.as_ref().unwrap().to_vec(), + app_s_key: self.app_s_key.clone(), + f_cnt_up: 0, + n_f_cnt_down: 0, + a_f_cnt_down: 0, + conf_f_cnt: 0, + rx1_delay: region_network.rx1_delay.into(), + rx1_dr_offset: region_network.rx1_dr_offset.into(), + rx2_dr: region_network.rx2_dr.into(), + rx2_frequency: region_conf.get_defaults().rx2_frequency, + enabled_uplink_channel_indices: region_conf + .get_default_uplink_channel_indices() + .iter() + .map(|i| *i as u32) + .collect(), + class_b_ping_slot_dr: device_profile.class_b_ping_slot_dr as u32, + class_b_ping_slot_freq: device_profile.class_b_ping_slot_freq as u32, + nb_trans: 1, + skip_f_cnt_check: device.skip_fcnt_check, + ..Default::default() + }; + + if let Some(lrwn::CFList::Channels(channels)) = + region_conf.get_cf_list(device_profile.mac_version) + { + for f in channels.iter().cloned() { + if f == 0 { + continue; + } + + let i = region_conf + .get_uplink_channel_index(f, true) + .context("Unknown cf_list frequency")?; + + ds.enabled_uplink_channel_indices.push(i as u32); + + // add extra channel to extra uplink channels, so that we can + // keep track on frequency and data-rate changes + let c = region_conf + .get_uplink_channel(i) + .context("Get uplink channel error")?; + + ds.extra_uplink_channels.insert( + i as u32, + internal::DeviceSessionChannel { + frequency: c.frequency, + min_dr: c.min_dr as u32, + max_dr: c.max_dr as u32, + }, + ); + } + } + + device_session::save(&ds) + .await + .context("Saving device-session failed")?; + + self.device_session = Some(ds); + + Ok(()) + } + + async fn flush_device_queue(&self) -> Result<()> { + let dp = self.device_profile.as_ref().unwrap(); + if !dp.flush_queue_on_activate { + return Ok(()); + } + + trace!("Flushing device-queue"); + let dev = self.device.as_ref().unwrap(); + device_queue::flush_for_dev_eui(&dev.dev_eui).await?; + Ok(()) + } + + async fn set_device_mode(&mut self) -> Result<()> { + let dp = self.device_profile.as_ref().unwrap(); + let device = self.device.as_mut().unwrap(); + + // LoRaWAN 1.1 devices send a mac-command when changing to Class-C. + if dp.supports_class_c && dp.mac_version.to_string().starts_with("1.0") { + *device = device::set_enabled_class(&device.dev_eui, "C").await?; + } else { + *device = device::set_enabled_class(&device.dev_eui, "A").await?; + } + Ok(()) + } + + async fn send_join_event(&self) -> Result<()> { + trace!("Sending join event"); + + let ts: DateTime = + helpers::get_rx_timestamp(&self.uplink_frame_set.rx_info_set).into(); + + let app = self.application.as_ref().unwrap(); + let dev = self.device.as_ref().unwrap(); + + let pl = integration_pb::JoinEvent { + deduplication_id: self.uplink_frame_set.uplink_set_id.to_string(), + time: Some(ts.into()), + device_info: self.device_info.clone(), + dev_addr: self.dev_addr.as_ref().unwrap().to_string(), + }; + + integration::join_event(&app.id, &dev.variables, &pl).await?; + Ok(()) + } + + fn set_pr_start_ans_payload(&mut self) -> Result<()> { + trace!("Setting PRStartAnsPayload"); + let ds = self.device_session.as_ref().unwrap(); + let region_conf = region::get(&self.uplink_frame_set.region_name)?; + + let sender_id = NetID::from_slice(&self.pr_start_req.base.sender_id)?; + let pr_lifetime = roaming::get_passive_roaming_lifetime(sender_id)?; + let kek_label = roaming::get_passive_roaming_kek_label(sender_id)?; + + let nwk_s_key = if ds.mac_version().to_string().starts_with("1.0") { + Some(keywrap::wrap( + &kek_label, + AES128Key::from_slice(&ds.nwk_s_enc_key)?, + )?) + } else { + None + }; + + let f_nwk_s_int_key = if ds.mac_version().to_string().starts_with("1.0") { + None + } else { + Some(keywrap::wrap( + &kek_label, + AES128Key::from_slice(&ds.f_nwk_s_int_key)?, + )?) + }; + + let rx1_delay = region_conf.get_defaults().join_accept_delay1; + let rx1_dr = region_conf.get_rx1_data_rate_index(self.uplink_frame_set.dr, 0)?; + let rx1_freq = region_conf + .get_rx1_frequency_for_uplink_frequency(self.uplink_frame_set.tx_info.frequency)?; + + let rx2_dr = region_conf.get_defaults().rx2_dr; + let rx2_freq = region_conf.get_defaults().rx2_frequency; + + self.pr_start_ans = Some(PRStartAnsPayload { + base: self + .pr_start_req + .base + .to_base_payload_result(backend::ResultCode::Success, ""), + phy_payload: self.join_accept.as_ref().unwrap().to_vec()?, + dev_eui: ds.dev_eui.clone(), + dev_addr: ds.dev_addr.clone(), + lifetime: if pr_lifetime.is_zero() { + None + } else { + Some(pr_lifetime.as_secs() as usize) + }, + f_nwk_s_int_key, + nwk_s_key, + f_cnt_up: Some(0), + dl_meta_data: Some(backend::DLMetaData { + dev_eui: ds.dev_eui.clone(), + dl_freq_1: Some(rx1_freq as f64 / 1_000_000.0), + dl_freq_2: Some(rx2_freq as f64 / 1_000_000.0), + rx_delay_1: Some(rx1_delay.as_secs() as usize), + class_mode: Some("A".to_string()), + data_rate_1: Some(rx1_dr), + data_rate_2: Some(rx2_dr), + f_ns_ul_token: self.pr_start_req.ul_meta_data.f_ns_ul_token.clone(), + gw_info: self + .pr_start_req + .ul_meta_data + .gw_info + .iter() + .map(|gw| backend::GWInfoElement { + ul_token: gw.ul_token.clone(), + ..Default::default() + }) + .collect(), + ..Default::default() + }), + ..Default::default() + }); + + Ok(()) + } +} diff --git a/chirpstack/src/uplink/mod.rs b/chirpstack/src/uplink/mod.rs index df4bde10..fb19f385 100644 --- a/chirpstack/src/uplink/mod.rs +++ b/chirpstack/src/uplink/mod.rs @@ -20,9 +20,13 @@ use lrwn::region::CommonName; use lrwn::{MType, PhyPayload, EUI64}; mod data; +mod data_fns; +pub mod data_sns; mod error; pub mod helpers; pub mod join; +pub mod join_fns; +pub mod join_sns; pub mod stats; lazy_static! { @@ -41,6 +45,7 @@ pub struct UplinkFrameSet { pub gateway_tenant_id_map: HashMap, pub region_common_name: CommonName, pub region_name: String, + pub roaming_meta_data: Option, } impl TryFrom<&UplinkFrameSet> for api::UplinkFrameLog { @@ -94,6 +99,12 @@ impl TryFrom<&UplinkFrameSet> for api::UplinkFrameLog { } } +#[derive(Clone)] +pub struct RoamingMetaData { + pub base_payload: backend::BasePayload, + pub ul_meta_data: backend::ULMetaData, +} + pub fn get_deduplication_delay() -> Duration { let dur_r = DEDUPLICATION_DELAY.read().unwrap(); *dur_r @@ -272,6 +283,7 @@ pub async fn handle_uplink(deduplication_id: Uuid, uplink: gw::UplinkFrameSet) - rx_info_set: uplink.rx_info, gateway_private_map: HashMap::new(), gateway_tenant_id_map: HashMap::new(), + roaming_meta_data: None, }; uplink.dr = helpers::get_uplink_dr(&uplink.region_name, &uplink.tx_info)?; @@ -367,3 +379,25 @@ fn filter_rx_info_by_tenant_id(tenant_id: &Uuid, uplink: &mut UplinkFrameSet) -> Ok(()) } + +fn filter_rx_info_by_public_only(uplink: &mut UplinkFrameSet) -> Result<()> { + let mut rx_info_set: Vec = Vec::new(); + + for rx_info in &uplink.rx_info_set { + let gateway_id = EUI64::from_str(&rx_info.gateway_id)?; + if !(*uplink + .gateway_private_map + .get(&gateway_id) + .ok_or(anyhow!("gateway_id missing in gateway_private_map"))?) + { + rx_info_set.push(rx_info.clone()); + } + } + + uplink.rx_info_set = rx_info_set; + if uplink.rx_info_set.is_empty() { + return Err(anyhow!("rx_info_set has no items")); + } + + Ok(()) +} diff --git a/lrwn/src/devaddr.rs b/lrwn/src/devaddr.rs index f85190c2..99d05ded 100644 --- a/lrwn/src/devaddr.rs +++ b/lrwn/src/devaddr.rs @@ -52,6 +52,13 @@ impl DevAddr { self.0.to_vec() } + pub fn is_net_id(&self, net_id: NetID) -> bool { + let mut dev_addr = *self; + dev_addr.set_addr_prefix(&net_id); + + *self == dev_addr + } + pub fn netid_type(&self) -> u8 { for i in (0..=7).rev() { if self.0[0] & (1 << i) == 0 { diff --git a/lrwn/src/error.rs b/lrwn/src/error.rs index b74344c2..4a3ce77b 100644 --- a/lrwn/src/error.rs +++ b/lrwn/src/error.rs @@ -2,6 +2,9 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum Error { + #[error("NetID expects exactly 3 bytes")] + NetIdLength, + #[error("EUI64 expects exactly 8 bytes")] Eui64Length, diff --git a/lrwn/src/keys.rs b/lrwn/src/keys.rs new file mode 100644 index 00000000..255c56de --- /dev/null +++ b/lrwn/src/keys.rs @@ -0,0 +1,256 @@ +use aes::cipher::{generic_array::GenericArray, BlockEncrypt, NewBlockCipher}; +use aes::{Aes128, Block}; +use anyhow::Result; + +use crate::{AES128Key, NetID, EUI64}; + +// For LoRaWAN 1.0: SNwkSIntKey = NwkSEncKey = FNwkSIntKey = NwkSKey +pub fn get_f_nwk_s_int_key( + opt_neg: bool, + nwk_key: &AES128Key, + net_id: &NetID, + join_eui: &EUI64, + join_nonce: u32, + dev_nonce: u16, +) -> Result { + get_s_key( + opt_neg, 0x01, nwk_key, net_id, join_eui, join_nonce, dev_nonce, + ) +} + +pub fn get_app_s_key( + opt_neg: bool, + nwk_key: &AES128Key, + net_id: &NetID, + join_eui: &EUI64, + join_nonce: u32, + dev_nonce: u16, +) -> Result { + get_s_key( + opt_neg, 0x02, nwk_key, net_id, join_eui, join_nonce, dev_nonce, + ) +} + +pub fn get_s_nwk_s_int_key( + opt_neg: bool, + nwk_key: &AES128Key, + net_id: &NetID, + join_eui: &EUI64, + join_nonce: u32, + dev_nonce: u16, +) -> Result { + get_s_key( + opt_neg, 0x03, nwk_key, net_id, join_eui, join_nonce, dev_nonce, + ) +} + +pub fn get_nwk_s_enc_key( + opt_neg: bool, + nwk_key: &AES128Key, + net_id: &NetID, + join_eui: &EUI64, + join_nonce: u32, + dev_nonce: u16, +) -> Result { + get_s_key( + opt_neg, 0x04, nwk_key, net_id, join_eui, join_nonce, dev_nonce, + ) +} + +pub fn get_js_enc_key(dev_eui: &EUI64, nwk_key: &AES128Key) -> Result { + get_js_key(0x05, dev_eui, nwk_key) +} + +pub fn get_js_int_key(dev_eui: &EUI64, nwk_key: &AES128Key) -> Result { + get_js_key(0x06, dev_eui, nwk_key) +} + +fn get_s_key( + opt_neg: bool, + typ: u8, + nwk_key: &AES128Key, + net_id: &NetID, + join_eui: &EUI64, + join_nonce: u32, + dev_nonce: u16, +) -> Result { + let key_bytes = nwk_key.to_bytes(); + let key = GenericArray::from_slice(&key_bytes); + let cipher = Aes128::new(key); + + let mut b: [u8; 16] = [0; 16]; + + b[0] = typ; + if opt_neg { + b[1..4].clone_from_slice(&join_nonce.to_le_bytes()[0..3]); + b[4..12].clone_from_slice(&join_eui.to_le_bytes()); + b[12..14].clone_from_slice(&dev_nonce.to_le_bytes()[0..2]); + } else { + b[1..4].clone_from_slice(&join_nonce.to_le_bytes()[0..3]); + b[4..7].clone_from_slice(&net_id.to_le_bytes()); + b[7..9].clone_from_slice(&dev_nonce.to_le_bytes()[0..2]); + } + + let block = Block::from_mut_slice(&mut b); + cipher.encrypt_block(block); + + Ok(AES128Key::from_slice(block)?) +} + +fn get_js_key(typ: u8, dev_eui: &EUI64, nwk_key: &AES128Key) -> Result { + let key_bytes = nwk_key.to_bytes(); + let key = GenericArray::from_slice(&key_bytes); + let cipher = Aes128::new(key); + + let mut b: [u8; 16] = [0; 16]; + b[0] = typ; + b[1..9].clone_from_slice(&dev_eui.to_le_bytes()); + + let block = Block::from_mut_slice(&mut b); + cipher.encrypt_block(block); + + Ok(AES128Key::from_slice(block)?) +} + +#[cfg(test)] +pub mod test { + use super::*; + + fn nwk_key() -> AES128Key { + AES128Key::from_bytes([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, + ]) + } + + fn app_key() -> AES128Key { + AES128Key::from_bytes([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]) + } + + fn join_eui() -> EUI64 { + EUI64::from_be_bytes([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]) + } + + fn join_nonce() -> u32 { + 65536 + } + + fn dev_nonce() -> u16 { + 258 + } + + fn net_id() -> NetID { + NetID::from_be_bytes([0x01, 0x02, 0x03]) + } + + #[test] + fn lorawan_1_0() { + let nwk_s_key = get_f_nwk_s_int_key( + false, + &nwk_key(), + &net_id(), + &join_eui(), + join_nonce(), + dev_nonce(), + ) + .unwrap(); + + let app_s_key = get_app_s_key( + false, + &nwk_key(), + &net_id(), + &join_eui(), + join_nonce(), + dev_nonce(), + ) + .unwrap(); + + assert_eq!( + AES128Key::from_bytes([ + 223, 83, 195, 95, 48, 52, 204, 206, 208, 255, 53, 76, 112, 222, 4, 223, + ]), + nwk_s_key + ); + + assert_eq!( + AES128Key::from_bytes([ + 146, 123, 156, 145, 17, 131, 207, 254, 76, 178, 255, 75, 117, 84, 95, 109 + ]), + app_s_key + ) + } + + #[test] + fn lorawan_1_1() { + let app_s_key = get_app_s_key( + true, + &app_key(), + &net_id(), + &join_eui(), + join_nonce(), + dev_nonce(), + ) + .unwrap(); + + let f_nwk_s_int_key = get_f_nwk_s_int_key( + true, + &nwk_key(), + &net_id(), + &join_eui(), + join_nonce(), + dev_nonce(), + ) + .unwrap(); + + let s_nwk_s_int_key = get_s_nwk_s_int_key( + true, + &nwk_key(), + &net_id(), + &join_eui(), + join_nonce(), + dev_nonce(), + ) + .unwrap(); + + let nwk_s_enc_key = get_nwk_s_enc_key( + true, + &nwk_key(), + &net_id(), + &join_eui(), + join_nonce(), + dev_nonce(), + ) + .unwrap(); + + assert_eq!( + AES128Key::from_bytes([ + 1, 98, 18, 21, 209, 202, 8, 254, 191, 12, 96, 44, 194, 173, 144, 250 + ]), + app_s_key, + ); + + assert_eq!( + AES128Key::from_bytes([ + 83, 127, 138, 174, 137, 108, 121, 224, 21, 209, 2, 208, 98, 134, 53, 78 + ]), + f_nwk_s_int_key, + ); + + assert_eq!( + AES128Key::from_bytes([ + 88, 148, 152, 153, 48, 146, 207, 219, 95, 210, 224, 42, 199, 81, 11, 241 + ]), + s_nwk_s_int_key, + ); + + assert_eq!( + AES128Key::from_bytes([ + 152, 152, 40, 60, 79, 102, 235, 108, 111, 213, 22, 88, 130, 4, 108, 64 + ]), + nwk_s_enc_key, + ); + } +} diff --git a/lrwn/src/lib.rs b/lrwn/src/lib.rs index 2f82f4cd..522ad713 100644 --- a/lrwn/src/lib.rs +++ b/lrwn/src/lib.rs @@ -25,6 +25,7 @@ mod dl_settings; mod error; mod eui64; mod fhdr; +pub mod keys; mod maccommand; mod mhdr; mod netid; diff --git a/lrwn/src/netid.rs b/lrwn/src/netid.rs index c1e7eb5e..88f57e13 100644 --- a/lrwn/src/netid.rs +++ b/lrwn/src/netid.rs @@ -5,10 +5,23 @@ use anyhow::Result; use serde::de::{self, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -#[derive(PartialEq, Clone, Copy)] +use crate::Error; + +#[derive(PartialEq, Clone, Copy, Hash, Eq)] pub struct NetID([u8; 3]); impl NetID { + pub fn from_slice(b: &[u8]) -> Result { + if b.len() != 3 { + return Err(Error::NetIdLength); + } + + let mut bb: [u8; 3] = [0; 3]; + bb.copy_from_slice(b); + + Ok(NetID(bb)) + } + pub fn from_be_bytes(b: [u8; 3]) -> Self { NetID(b) } @@ -25,6 +38,10 @@ impl NetID { b } + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + pub fn netid_type(&self) -> u8 { self.0[0] >> 5 } @@ -59,6 +76,12 @@ impl NetID { } } +impl Default for NetID { + fn default() -> Self { + NetID([0, 0, 0]) + } +} + impl fmt::Display for NetID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", hex::encode(self.0)) @@ -72,7 +95,7 @@ impl fmt::Debug for NetID { } impl FromStr for NetID { - type Err = Box; + type Err = Error; fn from_str(s: &str) -> Result { let mut bytes: [u8; 3] = [0; 3]; diff --git a/lrwn/src/region/mod.rs b/lrwn/src/region/mod.rs index 04b93016..737bfc21 100644 --- a/lrwn/src/region/mod.rs +++ b/lrwn/src/region/mod.rs @@ -85,9 +85,9 @@ impl FromStr for CommonName { "AU915" => CommonName::AU915, "CN470" => CommonName::CN470, "AS923" => CommonName::AS923, - "AS923_2" => CommonName::AS923_2, - "AS923_3" => CommonName::AS923_3, - "AS923_4" => CommonName::AS923_4, + "AS923_2" | "AS923-2" => CommonName::AS923_2, + "AS923_3" | "AS923-3" => CommonName::AS923_3, + "AS923_4" | "AS923-4" => CommonName::AS923_4, "KR920" => CommonName::KR920, "IN865" => CommonName::IN865, "RU864" => CommonName::RU864,