Re-implement passive-roaming.

This commit is contained in:
Orne Brocaar 2022-06-30 11:11:21 +01:00
parent 3fd821ebdf
commit f27b8da38d
46 changed files with 5117 additions and 461 deletions

12
Cargo.lock generated
View File

@ -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",
]

View File

@ -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;
}

2
api/rust/Cargo.lock generated vendored
View File

@ -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",

View File

@ -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;
}

View File

@ -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"

File diff suppressed because it is too large Load Diff

View File

@ -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]

View File

@ -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<hyper::Body> {
let mut b: Vec<u8> = 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<hyper::Body> {
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::<StorageError>() {
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<backend::Client>,
bp: backend::BasePayload,
b: &[u8],
) -> http::Response<hyper::Body> {
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<backend::PRStartAnsPayload> {
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<backend::PRStartAnsPayload> {
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(&region_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<backend::PRStartAnsPayload> {
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(&region_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<backend::Client>,
bp: backend::BasePayload,
b: &[u8],
) -> http::Response<hyper::Body> {
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<backend::PRStopAnsPayload> {
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<backend::Client>,
bp: backend::BasePayload,
b: &[u8],
) -> http::Response<hyper::Body> {
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<backend::XmitDataAnsPayload> {
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(&region_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<http::Response<hyper::Body>> {
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<oneshot::Receiver<Vec<u8>>> {
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);
}
}

View File

@ -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(())
}

View File

@ -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<Arc<Client>> {
})?
.clone())
}
#[cfg(test)]
pub fn reset() {
let mut clients_w = CLIENTS.write().unwrap();
*clients_w = HashMap::new();
}

View File

@ -23,3 +23,18 @@ pub fn unwrap(ke: &KeyEnvelope) -> Result<AES128Key> {
Err(anyhow!("KEK label {} does not exist", ke.kek_label))
}
pub fn wrap(label: &str, key: AES128Key) -> Result<KeyEnvelope> {
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))
}

View File

@ -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(())
}

View File

@ -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<HashMap<NetID, Arc<Client>>> = 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<Arc<Client>> {
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<std::time::Duration> {
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<String> {
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<NetID> {
let mut out: Vec<NetID> = 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<Vec<GWInfoElement>> {
let mut out: Vec<GWInfoElement> = 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<Vec<gw::UplinkRxInfo>> {
let mut out: Vec<gw::UplinkRxInfo> = 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<gw::UplinkTxInfo> {
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<Vec<gw::UplinkRxInfo>> {
let mut out: Vec<gw::UplinkRxInfo> = 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();
}

View File

@ -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?;

View File

@ -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<Kek>,
pub regions: Vec<Region>,
}
@ -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<RoamingServer>,
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 {

View File

@ -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<usize, lrwn::region::Channel> = HashMap::new();

View File

@ -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<gw::UplinkRxInfo>,
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(&region_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(())
}
}

View File

@ -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()
});

View File

@ -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;

View File

@ -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<u8>,
dl_meta_data: DLMetaData,
network_conf: config::RegionNetwork,
region_conf: Arc<Box<dyn lrwn::region::Region + Sync + Send>>,
downlink_frame: gw::DownlinkFrame,
downlink_gateway: Option<internal::DeviceGatewayRxInfoItem>,
}
impl PassiveRoamingDownlink {
pub async fn handle(ufs: UplinkFrameSet, phy: Vec<u8>, 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<u8>, 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(())
}
}

View File

@ -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 {

View File

@ -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();

View File

@ -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 {

View File

@ -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();

View File

@ -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();

View File

@ -75,3 +75,20 @@ pub fn get(region_name: &str) -> Result<Arc<Box<dyn region::Region + Sync + Send
.ok_or_else(|| anyhow!("region_name {} does not exist in REGIONS", region_name))?
.clone())
}
/// This returns the (first) region-name, based on the given common-name.
/// This function is used for roaming, as within the context of roaming, only
/// the common-name is given by the other party.
pub fn get_region_name(common_name: region::CommonName) -> Result<String> {
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
))
}

View File

@ -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<internal::DeviceSession, 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();
// 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<Vec<EUI64>> {
task::spawn_blocking({
let dev_addr = dev_addr;

View File

@ -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;

View File

@ -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<Utc> = 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<internal::PassiveRoamingDeviceSession, Error> {
task::spawn_blocking({
move || -> Result<internal::PassiveRoamingDeviceSession, Error> {
let key = redis_key(format!("pr:sess:{{{}}}", id));
let mut c = get_redis_conn()?;
let v: Vec<u8> = 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<Vec<internal::PassiveRoamingDeviceSession>, 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<internal::PassiveRoamingDeviceSession> = 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<Vec<internal::PassiveRoamingDeviceSession>> {
let mut out: Vec<internal::PassiveRoamingDeviceSession> = 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<Vec<internal::PassiveRoamingDeviceSession>> {
let mut out: Vec<internal::PassiveRoamingDeviceSession> = 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<Vec<Uuid>> {
task::spawn_blocking({
move || -> Result<Vec<Uuid>> {
let key = redis_key(format!("pr:devaddr:{{{}}}", dev_addr));
let mut c = get_redis_conn()?;
let v: Vec<String> = redis::cmd("SMEMBERS").arg(key).query(&mut *c)?;
let mut out: Vec<Uuid> = 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<Vec<Uuid>> {
task::spawn_blocking({
move || -> Result<Vec<Uuid>> {
let key = redis_key(format!("pr:dev:{{{}}}", dev_eui));
let mut c = get_redis_conn()?;
let v: Vec<String> = redis::cmd("SMEMBERS").arg(key).query(&mut *c)?;
let mut out: Vec<Uuid> = 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)
}

View File

@ -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
);
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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<dyn Fn() -> Pin<Box<dyn Future<Output = ()>>>>;

View File

@ -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()
}
}

View File

@ -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<internal::PassiveRoamingDeviceSession>,
}
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::<Error>() {
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<internal::PassiveRoamingDeviceSession> {
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()
})
}
}

View File

@ -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
}
}

View File

@ -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::<Error>() {
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<AES128Key> {
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<AES128Key> {
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<AES128Key> {
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<AES128Key> {
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<AES128Key> {
get_js_key(0x05, dev_eui, nwk_key)
}
pub fn get_js_int_key(dev_eui: &EUI64, nwk_key: &AES128Key) -> Result<AES128Key> {
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<AES128Key> {
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<AES128Key> {
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,
);
}
}

View File

@ -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<NetID>,
client: Option<Arc<Client>>,
pr_start_ans: Option<backend::PRStartAnsPayload>,
}
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")
}
}

View File

@ -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<PRStartAnsPayload>,
join_request: Option<lrwn::JoinRequestPayload>,
join_accept: Option<lrwn::PhyPayload>,
device: Option<device::Device>,
device_session: Option<internal::DeviceSession>,
js_client: Option<Arc<backend::Client>>,
application: Option<application::Application>,
tenant: Option<tenant::Tenant>,
device_profile: Option<device_profile::DeviceProfile>,
device_keys: Option<device_keys::DeviceKeys>,
device_info: Option<integration_pb::DeviceInfo>,
dev_addr: Option<DevAddr>,
f_nwk_s_int_key: Option<AES128Key>,
s_nwk_s_int_key: Option<AES128Key>,
nwk_s_enc_key: Option<AES128Key>,
app_s_key: Option<common::KeyEnvelope>,
}
impl JoinRequest {
pub async fn start_pr(
ufs: UplinkFrameSet,
pr_start_req: PRStartReqPayload,
) -> Result<PRStartAnsPayload> {
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<PRStartAnsPayload> {
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<Utc> =
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(())
}
}

View File

@ -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<EUI64, Uuid>,
pub region_common_name: CommonName,
pub region_name: String,
pub roaming_meta_data: Option<RoamingMetaData>,
}
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<gw::UplinkRxInfo> = 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(())
}

View File

@ -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 {

View File

@ -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,

256
lrwn/src/keys.rs Normal file
View File

@ -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<AES128Key> {
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<AES128Key> {
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<AES128Key> {
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<AES128Key> {
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<AES128Key> {
get_js_key(0x05, dev_eui, nwk_key)
}
pub fn get_js_int_key(dev_eui: &EUI64, nwk_key: &AES128Key) -> Result<AES128Key> {
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<AES128Key> {
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<AES128Key> {
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,
);
}
}

View File

@ -25,6 +25,7 @@ mod dl_settings;
mod error;
mod eui64;
mod fhdr;
pub mod keys;
mod maccommand;
mod mhdr;
mod netid;

View File

@ -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<Self, Error> {
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<u8> {
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<dyn std::error::Error>;
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes: [u8; 3] = [0; 3];

View File

@ -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,