#![allow(unreachable_pub)]
#![cfg_attr(not(feature = "ota"), allow(dead_code, unused_imports))]
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::time::Instant;
use matter_cert::{MatterTime, TrustedRoots};
use matter_commissioning::driver::{decode_unsecured, encode_unsecured_reply, AsyncDatagram};
use matter_crypto::{CaseCredentials, CaseResponder, ResumptionRecord, Sigma1Outcome};
use matter_interaction::{
build_invoke_response_command, build_invoke_response_status, parse_invoke_request, CommandPath,
ImStatus,
};
#[cfg(any(feature = "unstable-provider", test))]
use matter_interaction::ParsedInvokeRequest;
use matter_transport::{
DecodeInboundOutput, MatterService, MrpFlags, ProtocolId, ServiceKind, SessionId,
SessionManager, SessionRole,
};
use crate::error::Error;
const OP_SIGMA1: u8 = 0x30;
const OP_SIGMA2: u8 = 0x31;
const OP_SIGMA3: u8 = 0x32;
const OP_SIGMA2_RESUME: u8 = 0x33;
const OP_STATUS_REPORT: u8 = 0x40;
const OP_MRP_STANDALONE_ACK: u8 = 0x10;
const OP_INVOKE_REQUEST: u8 = 0x08;
const MAX_AWAIT_SIGMA1_DISCARDS: usize = 64;
const OP_INVOKE_RESPONSE: u8 = 0x09;
const STATUS_GENERAL_FAILURE: u16 = 0x0001;
fn encode_status_report_body(general: u16, proto: ProtocolId, protocol_status: u16) -> Vec<u8> {
let proto_id: u32 = (u32::from(proto.vendor) << 16) | u32::from(proto.protocol);
let mut body = Vec::with_capacity(8);
body.extend_from_slice(&general.to_le_bytes());
body.extend_from_slice(&proto_id.to_le_bytes());
body.extend_from_slice(&protocol_status.to_le_bytes());
body
}
fn parse_status_report_body(payload: &[u8]) -> Option<(u16, u32, u16)> {
let b: &[u8; 8] = payload.get(..8)?.try_into().ok()?;
Some((
u16::from_le_bytes([b[0], b[1]]),
u32::from_le_bytes([b[2], b[3], b[4], b[5]]),
u16::from_le_bytes([b[6], b[7]]),
))
}
const OTA_PROVIDER_CLUSTER: u32 = 0x0029;
const CMD_QUERY_IMAGE: u32 = 0x00;
const CMD_QUERY_IMAGE_RESPONSE: u32 = 0x01;
const CMD_APPLY_UPDATE_REQUEST: u32 = 0x02;
const CMD_APPLY_UPDATE_RESPONSE: u32 = 0x03;
const CMD_NOTIFY_UPDATE_APPLIED: u32 = 0x04;
fn is_unsecured_frame(frame: &[u8]) -> bool {
frame.len() >= 3 && frame[1] == 0 && frame[2] == 0
}
type CarriedFrame = (Vec<u8>, SocketAddr);
#[must_use]
pub fn build_operational_service(
compressed_fabric_id: [u8; 8],
node_id: u64,
addresses: Vec<IpAddr>,
port: u16,
) -> MatterService {
let instance_name =
matter_commissioning::driver::operational_instance_name(compressed_fabric_id, node_id);
MatterService::new(
instance_name,
ServiceKind::Operational,
addresses,
port,
std::collections::HashMap::new(),
)
}
pub struct ProviderServer<D> {
io: D,
credentials: Vec<CaseCredentials>,
roots: TrustedRoots,
base_session_id: u16,
accepts: u16,
now: MatterTime,
handshake_counter: u32,
expected_peer: Option<u64>,
resumption_records: Vec<ResumptionRecord>,
record_sink: Option<Box<dyn Fn(ResumptionRecord) + Send + Sync>>,
}
impl<D: AsyncDatagram> ProviderServer<D> {
#[must_use]
pub fn new(
io: D,
credentials: Vec<CaseCredentials>,
roots: TrustedRoots,
base_session_id: u16,
now: MatterTime,
) -> Self {
Self {
io,
credentials,
roots,
base_session_id,
accepts: 0,
now,
handshake_counter: 1,
expected_peer: None,
resumption_records: Vec::new(),
record_sink: None,
}
}
#[must_use]
pub fn with_record_sink(mut self, sink: Box<dyn Fn(ResumptionRecord) + Send + Sync>) -> Self {
self.record_sink = Some(sink);
self
}
#[must_use]
pub fn with_resumption_records(mut self, records: Vec<ResumptionRecord>) -> Self {
self.resumption_records = records;
self
}
#[must_use]
pub fn with_expected_peer(mut self, node_id: u64) -> Self {
self.expected_peer = Some(node_id);
self
}
fn next_handshake_counter(&mut self) -> u32 {
let c = self.handshake_counter;
self.handshake_counter = self.handshake_counter.wrapping_add(1);
c
}
async fn recv(&self) -> Result<(Vec<u8>, SocketAddr), Error> {
self.io
.recv_from()
.await
.map_err(|e| Error::Operational(format!("provider recv: {e}")))
}
async fn send(&self, bytes: &[u8], peer: SocketAddr) -> Result<(), Error> {
self.io
.send_to(bytes, peer)
.await
.map_err(|e| Error::Operational(format!("provider send: {e}")))
}
async fn recv_secured(
&self,
sessions: &mut SessionManager,
peer: SocketAddr,
) -> Result<(Vec<u8>, SocketAddr), Error> {
use matter_transport::MrpEvent;
loop {
let Some(deadline) = sessions.poll_timeout() else {
return self.recv().await;
};
let wait = deadline.saturating_duration_since(Instant::now());
match tokio::time::timeout(wait, self.recv()).await {
Ok(result) => return result,
Err(_deadline_hit) => {
for event in sessions.handle_timeout(Instant::now()) {
match event {
MrpEvent::Retransmit { packet, .. }
| MrpEvent::SendStandaloneAck { packet, .. } => {
self.send(&packet, peer).await?;
}
_ => {}
}
}
}
}
}
}
async fn accept_case(
&mut self,
first_frame: Option<CarriedFrame>,
) -> Result<(SessionManager, SessionId, SocketAddr, Option<CarriedFrame>), Error> {
if self.credentials.is_empty() {
return Err(Error::Operational(
"provider server: credential pool exhausted".into(),
));
}
let mut carried = first_frame;
let mut discarded = 0usize;
let (m1, peer) = loop {
let (bytes, from) = match carried.take() {
Some(f) => f,
None => self.recv().await?,
};
match decode_unsecured(&bytes) {
Ok(m) if m.opcode == OP_SIGMA1 => break (m, from),
_ => {
discarded += 1;
if discarded >= MAX_AWAIT_SIGMA1_DISCARDS {
return Err(Error::Operational(format!(
"no Sigma1 within {MAX_AWAIT_SIGMA1_DISCARDS} frames"
)));
}
}
}
};
let credentials = self.credentials.remove(0);
let responder_session_id = self.base_session_id.wrapping_add(self.accepts);
self.accepts = self.accepts.wrapping_add(1);
let mut responder = CaseResponder::new(
credentials,
self.roots.clone(),
responder_session_id,
self.now,
)
.map_err(|e| Error::Operational(format!("CASE responder init: {e}")))?;
let outcome = responder
.handle_sigma1(&m1.payload)
.map_err(|e| Error::Operational(format!("handle_sigma1: {e}")))?;
let resumed = match outcome {
Sigma1Outcome::NewSession => false,
Sigma1Outcome::ResumptionRequested { id } => {
if let Some(pos) = self.resumption_records.iter().position(|r| r.id == id) {
let record = self.resumption_records.swap_remove(pos);
responder
.accept_resumption(record)
.map_err(|e| Error::Operational(format!("accept_resumption: {e}")))?;
true
} else {
responder
.reject_resumption()
.map_err(|e| Error::Operational(format!("reject_resumption: {e}")))?;
false
}
}
};
let carry = if resumed {
self.complete_resumed(&mut responder, &m1, peer).await?;
None
} else {
self.complete_full(&mut responder, &m1, peer).await?
};
let output = responder
.finish()
.map_err(|e| Error::Operational(format!("CASE finish: {e}")))?;
if let Some(expected) = self.expected_peer {
if output.peer.node_id != expected {
return Err(Error::Operational(format!(
"provider server: accepted peer node {:#x} is not the expected {expected:#x}",
output.peer.node_id
)));
}
}
if let Some(record) = output.resumption_record.clone() {
self.resumption_records.push(record.clone());
if let Some(sink) = &self.record_sink {
sink(record);
}
}
let mut sessions = SessionManager::new();
let sid = sessions.register_case(&output, SessionRole::Responder);
Ok((sessions, sid, peer, carry))
}
async fn complete_resumed(
&mut self,
responder: &mut CaseResponder,
m1: &matter_commissioning::driver::UnsecuredMessage,
peer: SocketAddr,
) -> Result<(), Error> {
let sigma2_resume = responder
.next_message()
.map_err(|e| Error::Operational(format!("sigma2_resume: {e}")))?;
let c = self.next_handshake_counter();
let wire = encode_unsecured_reply(
c,
m1.exchange_id,
OP_SIGMA2_RESUME,
ProtocolId::SECURE_CHANNEL,
true,
Some(m1.message_counter),
m1.source_node_id,
&sigma2_resume,
);
self.send(&wire, peer).await?;
for _ in 0..8 {
let (bytes, _) = self.recv().await?;
let m = decode_unsecured(&bytes)
.map_err(|e| Error::Operational(format!("post-resume frame: {e}")))?;
match m.opcode {
OP_STATUS_REPORT => {
let general_code = m
.payload
.get(0..2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
.ok_or_else(|| {
Error::Operational("truncated resumption StatusReport".into())
})?;
if general_code != 0 {
return Err(Error::Operational(format!(
"initiator rejected resumption: StatusReport general code {general_code}"
)));
}
let c = self.next_handshake_counter();
let ack = encode_unsecured_reply(
c,
m.exchange_id,
OP_MRP_STANDALONE_ACK,
ProtocolId::SECURE_CHANNEL,
false,
Some(m.message_counter),
m.source_node_id.or(m1.source_node_id),
&[],
);
self.send(&ack, peer).await?;
return Ok(());
}
OP_SIGMA1 => {
let c = self.next_handshake_counter();
let wire = encode_unsecured_reply(
c,
m.exchange_id,
OP_SIGMA2_RESUME,
ProtocolId::SECURE_CHANNEL,
true,
Some(m.message_counter),
m.source_node_id.or(m1.source_node_id),
&sigma2_resume,
);
self.send(&wire, peer).await?;
}
OP_MRP_STANDALONE_ACK => {}
other => {
return Err(Error::Operational(format!(
"expected resumption StatusReport (0x40), got {other:#04x}"
)))
}
}
}
Err(Error::Operational(
"no StatusReport after Sigma2_Resume within frame budget".into(),
))
}
async fn complete_full(
&mut self,
responder: &mut CaseResponder,
m1: &matter_commissioning::driver::UnsecuredMessage,
peer: SocketAddr,
) -> Result<Option<CarriedFrame>, Error> {
let sigma2 = responder
.next_message()
.map_err(|e| Error::Operational(format!("sigma2: {e}")))?;
let c = self.next_handshake_counter();
let wire = encode_unsecured_reply(
c,
m1.exchange_id,
OP_SIGMA2,
ProtocolId::SECURE_CHANNEL,
true,
Some(m1.message_counter),
m1.source_node_id,
&sigma2,
);
self.send(&wire, peer).await?;
let (s3, _) = self.recv().await?;
let m3 = decode_unsecured(&s3).map_err(|e| Error::Operational(format!("sigma3: {e}")))?;
if m3.opcode != OP_SIGMA3 {
return Err(Error::Operational(format!(
"expected Sigma3 (0x32), got {:#04x}",
m3.opcode
)));
}
responder
.handle_sigma3(&m3.payload)
.map_err(|e| Error::Operational(format!("handle_sigma3: {e}")))?;
let mut body = Vec::with_capacity(8);
body.extend_from_slice(&0u16.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u16.to_le_bytes()); let c = self.next_handshake_counter();
let report = encode_unsecured_reply(
c,
m3.exchange_id,
OP_STATUS_REPORT,
ProtocolId::SECURE_CHANNEL,
true,
Some(m3.message_counter),
m3.source_node_id.or(m1.source_node_id),
&body,
);
self.send(&report, peer).await?;
let (bytes, from) = self.recv().await?;
if let Ok(m) = decode_unsecured(&bytes) {
if m.opcode == OP_SIGMA1 && m.exchange_id != m1.exchange_id {
return Ok(Some((bytes, from)));
}
}
Ok(None)
}
#[cfg(any(feature = "unstable-provider", test))]
pub async fn accept_and_dispatch_once<H>(
mut self,
mut handler: H,
max_invokes: usize,
) -> Result<usize, Error>
where
H: FnMut(&ParsedInvokeRequest) -> Vec<u8>,
{
let (mut sessions, sid, peer, _fast_sigma1) = self.accept_case(None).await?;
let mut dispatched = 0usize;
while dispatched < max_invokes {
let (wire, _) = self.recv_secured(&mut sessions, peer).await?;
if let DecodeInboundOutput::AppMessage {
exchange_id,
opcode,
payload,
..
} = sessions.decode_inbound(&wire, Instant::now())?
{
if opcode != OP_INVOKE_REQUEST {
continue;
}
let parsed = parse_invoke_request(&payload)?;
let response = handler(&parsed);
let out = sessions.encode_outbound(
sid,
Some(exchange_id),
OP_INVOKE_RESPONSE,
ProtocolId::INTERACTION_MODEL,
&response,
MrpFlags { reliable: false },
Instant::now(),
)?;
self.send(&out.wire_bytes, peer).await?;
dispatched += 1;
}
}
Ok(dispatched)
}
#[cfg(feature = "ota")]
#[allow(clippy::too_many_lines)] pub async fn serve_ota_once(
mut self,
offer: matter_ota::ImageOffer,
image: Vec<u8>,
max_block_size: u16,
) -> Result<(), Error> {
use matter_bdx::{BdxMessage, BlockSender, MessageType, SenderOutcome};
let image: Arc<[u8]> = Arc::from(image);
let mut bdx: Option<BlockSender> = None;
let mut carried: Option<(Vec<u8>, SocketAddr)> = None;
loop {
let (mut sessions, sid, peer, fast_sigma1) =
match self.accept_case(carried.take()).await {
Ok(accepted) => accepted,
Err(e) => {
if self.credentials.is_empty() {
return Err(e); }
continue; }
};
if let Some(frame) = fast_sigma1 {
carried = Some(frame);
continue;
}
let max_progress = image.len() / usize::from(max_block_size.max(1)) + 64;
let max_iterations = max_progress.saturating_mul(8).max(1024);
let mut progress = 0usize;
let mut iterations = 0usize;
while progress < max_progress && iterations < max_iterations {
iterations += 1;
let (wire, from) = self.recv_secured(&mut sessions, peer).await?;
if is_unsecured_frame(&wire) {
carried = Some((wire, from));
break;
}
let Ok(decoded) = sessions.decode_inbound(&wire, Instant::now()) else {
continue;
};
let DecodeInboundOutput::AppMessage {
exchange_id,
protocol_id,
opcode,
payload,
..
} = decoded
else {
if let DecodeInboundOutput::DuplicateReliableAckResent { ack_packet, .. } =
decoded
{
self.send(&ack_packet, peer).await?;
}
continue;
};
let advanced = (protocol_id == ProtocolId::INTERACTION_MODEL
&& opcode == OP_INVOKE_REQUEST)
|| protocol_id == ProtocolId::BDX;
if protocol_id == ProtocolId::INTERACTION_MODEL && opcode == OP_INVOKE_REQUEST {
let parsed = parse_invoke_request(&payload)?;
let cmd = parsed
.commands
.first()
.ok_or_else(|| Error::Operational("OTA invoke had no command".into()))?;
let response = if cmd.path.command == CMD_QUERY_IMAGE {
bdx = Some(BlockSender::from_shared(Arc::clone(&image), max_block_size));
let fields = matter_ota::handle_query_image(&cmd.fields_tlv, Some(&offer))
.map_err(|e| Error::Operational(format!("QueryImage: {e}")))?;
build_invoke_response_command(
CommandPath {
endpoint: 0,
cluster: OTA_PROVIDER_CLUSTER,
command: CMD_QUERY_IMAGE_RESPONSE,
},
&fields,
)
} else if cmd.path.command == CMD_APPLY_UPDATE_REQUEST {
let fields = matter_ota::handle_apply_update_request(&cmd.fields_tlv)
.map_err(|e| Error::Operational(format!("ApplyUpdateRequest: {e}")))?;
build_invoke_response_command(
CommandPath {
endpoint: 0,
cluster: OTA_PROVIDER_CLUSTER,
command: CMD_APPLY_UPDATE_RESPONSE,
},
&fields,
)
} else if cmd.path.command == CMD_NOTIFY_UPDATE_APPLIED {
matter_ota::parse_notify_update_applied(&cmd.fields_tlv)
.map_err(|e| Error::Operational(format!("NotifyUpdateApplied: {e}")))?;
let r = build_invoke_response_status(
CommandPath {
endpoint: 0,
cluster: OTA_PROVIDER_CLUSTER,
command: CMD_NOTIFY_UPDATE_APPLIED,
},
ImStatus::Success,
);
let out = sessions.encode_outbound(
sid,
Some(exchange_id),
OP_INVOKE_RESPONSE,
ProtocolId::INTERACTION_MODEL,
&r,
MrpFlags { reliable: false },
Instant::now(),
)?;
self.send(&out.wire_bytes, peer).await?;
return Ok(());
} else {
return Err(Error::Operational(format!(
"unexpected OTA command {:#04x}",
cmd.path.command
)));
};
let out = sessions.encode_outbound(
sid,
Some(exchange_id),
OP_INVOKE_RESPONSE,
ProtocolId::INTERACTION_MODEL,
&response,
MrpFlags { reliable: false },
Instant::now(),
)?;
self.send(&out.wire_bytes, peer).await?;
} else if protocol_id == ProtocolId::SECURE_CHANNEL && opcode == OP_STATUS_REPORT {
let (general, proto, code) =
parse_status_report_body(&payload).ok_or_else(|| {
Error::Operational("BDX StatusReport body truncated".into())
})?;
return Err(Error::Operational(format!(
"BDX transfer aborted by peer: StatusReport general={general:#06x} \
protocol={proto:#010x} status={code:#06x}"
)));
} else if protocol_id == ProtocolId::BDX {
let mt = MessageType::from_u8(opcode).ok_or_else(|| {
Error::Operational(format!("unknown BDX opcode {opcode:#04x}"))
})?;
let msg = BdxMessage::decode(mt, &payload)
.map_err(|e| Error::Operational(format!("BDX decode: {e}")))?;
if matches!(msg, BdxMessage::ReceiveInit(_)) && bdx.is_some() {
bdx = Some(BlockSender::from_shared(Arc::clone(&image), max_block_size));
}
let sender = bdx.as_mut().ok_or_else(|| {
Error::Operational("BDX message before QueryImage".into())
})?;
let outcome = match msg {
BdxMessage::ReceiveInit(init) => sender.accept_receive_init(&init),
BdxMessage::BlockQuery(q) => sender.handle_block_query(&q),
BdxMessage::BlockAckEof(a) => sender.handle_block_ack_eof(&a),
_ => {
return Err(Error::Operational("unexpected inbound BDX message".into()))
}
};
match outcome {
SenderOutcome::Send(out) => {
let w = sessions.encode_outbound(
sid,
Some(exchange_id),
out.message_type.to_u8(),
ProtocolId::BDX,
&out.payload,
MrpFlags { reliable: true },
Instant::now(),
)?;
self.send(&w.wire_bytes, peer).await?;
}
SenderOutcome::Done => {}
SenderOutcome::Abort(code) => {
let body = encode_status_report_body(
STATUS_GENERAL_FAILURE,
ProtocolId::BDX,
code.to_u16(),
);
if let Ok(w) = sessions.encode_outbound(
sid,
Some(exchange_id),
OP_STATUS_REPORT,
ProtocolId::SECURE_CHANNEL,
&body,
MrpFlags { reliable: true },
Instant::now(),
) {
let _ = self.send(&w.wire_bytes, peer).await;
}
return Err(Error::Operational(format!(
"BDX transfer aborted: status {:#06x}",
code.to_u16()
)));
}
}
}
if advanced {
progress += 1;
}
}
if carried.is_none() {
return Err(Error::Operational(format!(
"OTA session ended without completing: served {progress}/{max_progress} \
transfer-advancing messages in {iterations}/{max_iterations} iterations"
)));
}
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)] use super::*;
use std::net::Ipv6Addr;
#[test]
fn operational_service_has_expected_name_kind_and_port() {
let compressed = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE];
let node_id = 0x0000_0000_0000_0001;
let addr = IpAddr::V6(Ipv6Addr::LOCALHOST);
let svc = build_operational_service(compressed, node_id, vec![addr], 5540);
assert_eq!(svc.kind, ServiceKind::Operational);
assert_eq!(svc.port, 5540);
assert_eq!(svc.instance_name, "DEADBEEFCAFEBABE-0000000000000001");
assert_eq!(svc.addresses, vec![addr]);
}
#[cfg(feature = "ota")]
#[test]
fn status_report_body_byte_layout_and_roundtrip() {
let code = matter_bdx::BdxStatusCode::BadBlockCounter.to_u16(); let body = encode_status_report_body(STATUS_GENERAL_FAILURE, ProtocolId::BDX, code);
assert_eq!(
body,
vec![0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x17, 0x00],
"GeneralCode(LE) || ProtocolId(LE u32) || ProtocolStatus(LE)"
);
assert_eq!(
parse_status_report_body(&body),
Some((STATUS_GENERAL_FAILURE, 0x0000_0002, code))
);
}
#[test]
fn status_report_body_rejects_truncated() {
assert_eq!(parse_status_report_body(&[0x01, 0x00, 0x02]), None);
assert_eq!(parse_status_report_body(&[]), None);
}
#[test]
fn is_unsecured_frame_classifies_correctly() {
let unsecured = encode_unsecured_reply(
1,
1,
0x30,
ProtocolId::SECURE_CHANNEL,
false,
None,
None,
&[],
);
assert!(
is_unsecured_frame(&unsecured),
"unsecured reply must have session id 0"
);
let secured = vec![0x00u8, 0x34, 0x12, 0x00, 0x00, 0x00];
assert!(
!is_unsecured_frame(&secured),
"non-zero session id must not be classified as unsecured"
);
assert!(
!is_unsecured_frame(&[0x00u8, 0x00]),
"2-byte slice must return false"
);
}
#[cfg(feature = "ota")]
#[tokio::test]
async fn empty_credential_pool_errors_before_any_io() {
let (io, _peer) = matter_commissioning::driver::InMemoryDatagram::pair();
let server = ProviderServer::new(
io,
Vec::new(),
TrustedRoots::new(),
0x10,
MatterTime::from_unix_secs(2_000_000_000),
);
let offer = matter_ota::ImageOffer {
software_version: 2,
software_version_string: "2.0".into(),
image_uri: "bdx://0/fw.ota".into(),
update_token: vec![0xAB; 16],
};
let err = server
.serve_ota_once(offer, vec![0u8; 16], 960)
.await
.expect_err("empty pool must fail fast");
assert!(
err.to_string().contains("credential pool exhausted"),
"unexpected error: {err}"
);
}
}