mirror of
https://github.com/chirpstack/chirpstack.git
synced 2025-03-22 03:55:33 +00:00
Re-implement passive-roaming.
This commit is contained in:
parent
3fd821ebdf
commit
f27b8da38d
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
|
31
api/proto/internal/internal.proto
vendored
31
api/proto/internal/internal.proto
vendored
@ -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
2
api/rust/Cargo.lock
generated
vendored
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
@ -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]
|
||||
|
552
chirpstack/src/api/backend/mod.rs
Normal file
552
chirpstack/src/api/backend/mod.rs
Normal 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(®ion_name, tx_info.frequency, dr)?,
|
||||
phy_payload: phy,
|
||||
tx_info,
|
||||
rx_info_set: rx_info,
|
||||
gateway_private_map: HashMap::new(),
|
||||
gateway_tenant_id_map: HashMap::new(),
|
||||
region_common_name,
|
||||
region_name,
|
||||
roaming_meta_data: Some(RoamingMetaData {
|
||||
base_payload: pl.base.clone(),
|
||||
ul_meta_data: pl.ul_meta_data.clone(),
|
||||
}),
|
||||
};
|
||||
|
||||
join_sns::JoinRequest::start_pr(ufs, pl).await
|
||||
}
|
||||
|
||||
async fn _handle_pr_start_req_data(
|
||||
pl: backend::PRStartReqPayload,
|
||||
phy: lrwn::PhyPayload,
|
||||
) -> Result<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(®ion_name, tx_info.frequency, dr)?,
|
||||
phy_payload: phy,
|
||||
tx_info,
|
||||
rx_info_set: rx_info,
|
||||
gateway_private_map: HashMap::new(),
|
||||
gateway_tenant_id_map: HashMap::new(),
|
||||
region_common_name,
|
||||
region_name,
|
||||
roaming_meta_data: Some(RoamingMetaData {
|
||||
base_payload: pl.base.clone(),
|
||||
ul_meta_data: pl.ul_meta_data.clone(),
|
||||
}),
|
||||
};
|
||||
|
||||
// get device-session
|
||||
let ds = device_session::get_for_phypayload(&ufs.phy_payload, ufs.dr, ufs.ch as u8).await?;
|
||||
let pr_lifetime = roaming::get_passive_roaming_lifetime(sender_id)?;
|
||||
let kek_label = roaming::get_passive_roaming_kek_label(sender_id)?;
|
||||
|
||||
let nwk_s_key = if ds.mac_version().to_string().starts_with("1.0") {
|
||||
Some(keywrap::wrap(
|
||||
&kek_label,
|
||||
AES128Key::from_slice(&ds.nwk_s_enc_key)?,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let f_nwk_s_int_key = if ds.mac_version().to_string().starts_with("1.0") {
|
||||
None
|
||||
} else {
|
||||
Some(keywrap::wrap(
|
||||
&kek_label,
|
||||
AES128Key::from_slice(&ds.f_nwk_s_int_key)?,
|
||||
)?)
|
||||
};
|
||||
|
||||
// In case of stateless, the payload is directly handled
|
||||
if pr_lifetime.is_zero() {
|
||||
data_sns::Data::handle(ufs).await;
|
||||
}
|
||||
|
||||
Ok(backend::PRStartAnsPayload {
|
||||
base: pl
|
||||
.base
|
||||
.to_base_payload_result(backend::ResultCode::Success, ""),
|
||||
dev_eui: ds.dev_eui.clone(),
|
||||
lifetime: if pr_lifetime.is_zero() {
|
||||
None
|
||||
} else {
|
||||
Some(pr_lifetime.as_secs() as usize)
|
||||
},
|
||||
f_nwk_s_int_key,
|
||||
nwk_s_key,
|
||||
f_cnt_up: Some(ds.f_cnt_up),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_pr_stop_req(
|
||||
sender_client: Arc<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(®ion_name, tx_info.frequency, dr)?,
|
||||
phy_payload: phy,
|
||||
tx_info,
|
||||
rx_info_set: rx_info,
|
||||
gateway_private_map: HashMap::new(),
|
||||
gateway_tenant_id_map: HashMap::new(),
|
||||
region_common_name,
|
||||
region_name,
|
||||
roaming_meta_data: Some(RoamingMetaData {
|
||||
base_payload: pl.base.clone(),
|
||||
ul_meta_data: ul_meta_data.clone(),
|
||||
}),
|
||||
};
|
||||
|
||||
data_sns::Data::handle(ufs).await;
|
||||
}
|
||||
|
||||
if let Some(dl_meta_data) = &pl.dl_meta_data {
|
||||
data_fns::Data::handle(pl.clone(), dl_meta_data.clone()).await?;
|
||||
}
|
||||
|
||||
Ok(backend::XmitDataAnsPayload {
|
||||
base: pl
|
||||
.base
|
||||
.to_base_payload_result(backend::ResultCode::Success, ""),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_async_ans(bp: &BasePayload, b: &[u8]) -> Result<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);
|
||||
}
|
||||
}
|
@ -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(())
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
320
chirpstack/src/backend/roaming.rs
Normal file
320
chirpstack/src/backend/roaming.rs
Normal 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();
|
||||
}
|
@ -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?;
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
174
chirpstack/src/downlink/data_fns.rs
Normal file
174
chirpstack/src/downlink/data_fns.rs
Normal 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(®ion_common_name)?;
|
||||
|
||||
let mut ctx = Data {
|
||||
region_name,
|
||||
region_common_name,
|
||||
uplink_rx_info,
|
||||
xmit_data_req: pl,
|
||||
dl_meta_data: dl_meta,
|
||||
downlink_frame: gw::DownlinkFrame {
|
||||
downlink_id: rand::thread_rng().gen(),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
ctx.set_downlink_frame()?;
|
||||
ctx.save_downlink_frame().await?;
|
||||
ctx.send_downlink_frame().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_downlink_frame(&mut self) -> Result<()> {
|
||||
trace!("Setting DownlinkFrame parameters");
|
||||
let region_conf = region::get(&self.region_name)?;
|
||||
|
||||
let rx_info = self
|
||||
.uplink_rx_info
|
||||
.first()
|
||||
.cloned()
|
||||
.ok_or(anyhow!("rx_info is empty"))?;
|
||||
|
||||
self.downlink_frame.gateway_id = rx_info.gateway_id.clone();
|
||||
if self.dl_meta_data.dl_freq_1.is_some()
|
||||
&& self.dl_meta_data.data_rate_1.is_some()
|
||||
&& self.dl_meta_data.rx_delay_1.is_some()
|
||||
{
|
||||
let mut tx_info = gw::DownlinkTxInfo {
|
||||
frequency: (self.dl_meta_data.dl_freq_1.unwrap() * 1_000_000.0) as u32,
|
||||
board: rx_info.board,
|
||||
antenna: rx_info.antenna,
|
||||
context: rx_info.context.clone(),
|
||||
timing: Some(gw::Timing {
|
||||
parameters: Some(gw::timing::Parameters::Delay(gw::DelayTimingInfo {
|
||||
delay: Some(pbjson_types::Duration {
|
||||
seconds: self.dl_meta_data.rx_delay_1.unwrap() as i64,
|
||||
nanos: 0,
|
||||
}),
|
||||
})),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
tx_info.power = region_conf.get_downlink_tx_power(tx_info.frequency) as i32;
|
||||
|
||||
let rx1_dr = region_conf.get_data_rate(self.dl_meta_data.data_rate_1.unwrap())?;
|
||||
helpers::set_tx_info_data_rate(&mut tx_info, &rx1_dr)?;
|
||||
|
||||
self.downlink_frame.items.push(gw::DownlinkFrameItem {
|
||||
phy_payload: self.xmit_data_req.phy_payload.clone(),
|
||||
tx_info: Some(tx_info),
|
||||
tx_info_legacy: None,
|
||||
});
|
||||
}
|
||||
|
||||
if self.dl_meta_data.dl_freq_2.is_some()
|
||||
&& self.dl_meta_data.data_rate_2.is_some()
|
||||
&& self.dl_meta_data.rx_delay_1.is_some()
|
||||
{
|
||||
let mut tx_info = gw::DownlinkTxInfo {
|
||||
frequency: (self.dl_meta_data.dl_freq_2.unwrap() * 1_000_000.0) as u32,
|
||||
board: rx_info.board,
|
||||
antenna: rx_info.antenna,
|
||||
context: rx_info.context,
|
||||
timing: Some(gw::Timing {
|
||||
parameters: Some(gw::timing::Parameters::Delay(gw::DelayTimingInfo {
|
||||
delay: Some(pbjson_types::Duration {
|
||||
seconds: self.dl_meta_data.rx_delay_1.unwrap() as i64 + 1,
|
||||
nanos: 0,
|
||||
}),
|
||||
})),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
tx_info.power = region_conf.get_downlink_tx_power(tx_info.frequency) as i32;
|
||||
|
||||
let rx2_dr = region_conf.get_data_rate(self.dl_meta_data.data_rate_2.unwrap())?;
|
||||
helpers::set_tx_info_data_rate(&mut tx_info, &rx2_dr)?;
|
||||
|
||||
self.downlink_frame.items.push(gw::DownlinkFrameItem {
|
||||
phy_payload: self.xmit_data_req.phy_payload.clone(),
|
||||
tx_info: Some(tx_info),
|
||||
tx_info_legacy: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_downlink_frame(&self) -> Result<()> {
|
||||
trace!("Saving downlink frame");
|
||||
|
||||
downlink_frame::save(&internal::DownlinkFrame {
|
||||
downlink_id: self.downlink_frame.downlink_id,
|
||||
downlink_frame: Some(self.downlink_frame.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.context("Save downlink frame")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_downlink_frame(&self) -> Result<()> {
|
||||
trace!("Sending downlink frame");
|
||||
|
||||
gateway::backend::send_downlink(&self.region_name, &self.downlink_frame)
|
||||
.await
|
||||
.context("Send downlink frame")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -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()
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
|
215
chirpstack/src/downlink/roaming.rs
Normal file
215
chirpstack/src/downlink/roaming.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
260
chirpstack/src/storage/passive_roaming.rs
Normal file
260
chirpstack/src/storage/passive_roaming.rs
Normal 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)
|
||||
}
|
509
chirpstack/src/test/class_a_pr_test.rs
Normal file
509
chirpstack/src/test/class_a_pr_test.rs
Normal 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
|
||||
);
|
||||
}
|
@ -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();
|
||||
|
418
chirpstack/src/test/otaa_pr_test.rs
Normal file
418
chirpstack/src/test/otaa_pr_test.rs
Normal 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();
|
||||
}
|
@ -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 = ()>>>>;
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
221
chirpstack/src/uplink/data_fns.rs
Normal file
221
chirpstack/src/uplink/data_fns.rs
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
12
chirpstack/src/uplink/data_sns.rs
Normal file
12
chirpstack/src/uplink/data_sns.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
184
chirpstack/src/uplink/join_fns.rs
Normal file
184
chirpstack/src/uplink/join_fns.rs
Normal 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")
|
||||
}
|
||||
}
|
765
chirpstack/src/uplink/join_sns.rs
Normal file
765
chirpstack/src/uplink/join_sns.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -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(())
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
256
lrwn/src/keys.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ mod dl_settings;
|
||||
mod error;
|
||||
mod eui64;
|
||||
mod fhdr;
|
||||
pub mod keys;
|
||||
mod maccommand;
|
||||
mod mhdr;
|
||||
mod netid;
|
||||
|
@ -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];
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user