pub struct ProviderServer<D> { /* private fields */ }Expand description
A multi-session OTA provider server: accepts inbound CASE sessions as the
responder (one per pooled credential), then dispatches server-side
InvokeRequests. Generic over the datagram transport so it runs over
TokioUdpTransport in production and InMemoryDatagram in tests.
This productionizes the responder accept-flow proven in the actor’s loopback
tests (run_loopback_device): Sigma1→Sigma2→Sigma3→SessionManager register,
then secured IM dispatch on the established session.
The credential pool is consumed one entry per accept_case call. When the
pool is exhausted, accept_case (and any caller such as serve_ota_once)
returns Error::Operational with the message
"provider server: credential pool exhausted". The pool is sized by the
caller — serve_ota mints four entries (first session + post-reboot session
- retry slack) from the persisted fabric.
Implementations§
Source§impl<D: AsyncDatagram> ProviderServer<D>
impl<D: AsyncDatagram> ProviderServer<D>
Sourcepub fn new(
io: D,
credentials: Vec<CaseCredentials>,
roots: TrustedRoots,
base_session_id: u16,
now: MatterTime,
) -> Self
pub fn new( io: D, credentials: Vec<CaseCredentials>, roots: TrustedRoots, base_session_id: u16, now: MatterTime, ) -> Self
Build a provider server bound to io, authenticating from the
credentials pool (our operational identities). roots and now are
used to validate the peer’s certificate chain on each accept.
base_session_id is the first secured session id advertised in Sigma2;
the Nth accept uses base_session_id.wrapping_add(N) so consecutive
sessions never reuse the same local id.
The pool is consumed one entry per accept. When it is empty, the next
call to Self::serve_ota_once (or any method that calls accept_case)
returns an Error::Operational containing
"provider server: credential pool exhausted".
Sourcepub fn with_record_sink(
self,
sink: Box<dyn Fn(ResumptionRecord) + Send + Sync>,
) -> Self
pub fn with_record_sink( self, sink: Box<dyn Fn(ResumptionRecord) + Send + Sync>, ) -> Self
Register a callback that is invoked once per completed accept with the
fresh ResumptionRecord the handshake produced (rotated on the resumed
path, brand-new on the full path). The caller can use this to persist the
record immediately — a future that is cancelled after accept_case
completes but before the caller stores the record would otherwise lose the
rotation. The sink is called synchronously and must not block; spawn
an async task if async work is needed.
Sourcepub fn with_resumption_records(self, records: Vec<ResumptionRecord>) -> Self
pub fn with_resumption_records(self, records: Vec<ResumptionRecord>) -> Self
Seed the server with known CASE resumption records (see the field
docs). An inbound resumption-requesting Sigma1 matching one of these
by id is accepted via Sigma2_Resume; anything else falls back to a
full handshake.
Sourcepub fn with_expected_peer(self, node_id: u64) -> Self
pub fn with_expected_peer(self, node_id: u64) -> Self
Pin the peer: an accepted session must authenticate as node_id or
the accept fails (consuming its pooled credential). Without this, any
member of the fabric could consume the serve.
Sourcepub async fn accept_and_dispatch_once<H>(
self,
handler: H,
max_invokes: usize,
) -> Result<usize, Error>
pub async fn accept_and_dispatch_once<H>( self, handler: H, max_invokes: usize, ) -> Result<usize, Error>
Accept ONE inbound CASE session, then dispatch up to max_invokes
server-side InvokeRequests through handler, replying to each on its
exchange. Returns the number of invokes dispatched.
handler maps a parsed InvokeRequest to the encoded InvokeResponse
message bytes (e.g. via matter_interaction::build_invoke_response_*).
§Errors
Returns Error::Operational on a transport, CASE-handshake, or framing
failure (including a non-NewSession Sigma1 or an unexpected opcode), or
Error::Transport / Error::InteractionModel from the session / IM
layers.
Sourcepub async fn serve_ota_once(
self,
offer: ImageOffer,
image: Vec<u8>,
max_block_size: u16,
) -> Result<(), Error>
pub async fn serve_ota_once( self, offer: ImageOffer, image: Vec<u8>, max_block_size: u16, ) -> Result<(), Error>
Accept CASE sessions in sequence, serving image to the requestor over the
full OTA flow — QueryImage → QueryImageResponse, a BDX transfer, then
ApplyUpdateRequest → ApplyUpdateResponse (Proceed) — and completing once
NotifyUpdateApplied is received on ANY session. A real requestor downloads
and applies on its first session, reboots into the new image, and sends
NotifyUpdateApplied on a fresh session; this method spans that reboot by
running an outer loop over accept_case calls.
Unsecured frames (session id 0) arriving while a secured session is being
served are recognised as new-session-establishment attempts; they are
carried into the next outer iteration as the first_frame for the next
accept_case call, so no handshake bytes are lost.
The caller owns the deadline: wrap serve_ota_once in
tokio::time::timeout (or similar) to bound a requestor that never
returns. Pool exhaustion (all credentials consumed) and a per-session step
budget are the two error paths.
The fresh ResumptionRecord the accept handshake produced is re-seeded
and forwarded to the record_sink (if set via
Self::with_record_sink) before the OTA dispatch loop begins — the
caller need not wait for the full OTA flow to persist the rotation.
offer shapes the QueryImageResponse (its ImageURI/UpdateToken);
max_block_size caps each BDX block. All replies are unreliable
(piggyback ack) — happy-path, localhost-validated. Messages route by
ProtocolId: Interaction-Model invokes go to the matter-ota handlers,
ProtocolId::BDX messages drive a matter_bdx::BlockSender.
§Errors
Error::Operational on a CASE/transport/codec failure, a BDX abort, an
unexpected OTA command, or if a session exhausts its step budget without
an unsecured carry-frame; Error::Transport / Error::InteractionModel
from the session / IM layers.