umsh_mac/lib.rs
1#![cfg_attr(not(feature = "std"), no_std)]
2
3//! UMSH MAC-layer coordinator and supporting state types.
4//!
5//! > Note: This reference implementation is a work in progress and was developed
6//! > with the assistance of an LLM. It should be considered experimental.
7//!
8//! This crate is the central runtime for the UMSH mesh protocol. It owns every piece of
9//! radio-facing state and drives the full MAC lifecycle: receiving and authenticating
10//! inbound frames, forwarding eligible frames as a repeater, issuing and verifying transport
11//! ACKs, retransmitting unacknowledged sends, suppressing duplicates, enforcing replay
12//! windows, managing frame-counter persistence, and servicing the outbound transmit queue.
13//!
14//! The crate is `no_std` compatible. All data structures are backed by
15//! [`heapless`](https://docs.rs/heapless) fixed-capacity collections; capacity is controlled
16//! by const-generic parameters on [`Mac`] so the compiler enforces sizing at build time with
17//! zero heap allocation.
18//!
19//! # Architecture overview
20//!
21//! ```text
22//! ┌────────────────────────────────────────────────────────────────┐
23//! │ Application / upper layers │
24//! │ queue_broadcast / queue_unicast / queue_multicast / … │
25//! └──────────────────────────┬─────────────────────────────────────┘
26//! │ SendOptions → SendReceipt
27//! ▼
28//! ┌────────────────────────────────────────────────────────────────┐
29//! │ Mac<P> (coordinator.rs) │
30//! │ │
31//! │ ┌─────────────────┐ ┌────────────────┐ ┌────────────────┐ │
32//! │ │ IdentitySlot[N] │ │ PeerRegistry │ │ ChannelTable │ │
33//! │ │ frame counters │ │ public keys │ │ channel keys │ │
34//! │ │ pending ACKs │ │ cached routes │ │ derived keys │ │
35//! │ │ pairwise keys │ └────────────────┘ └────────────────┘ │
36//! │ └─────────────────┘ │
37//! │ ┌──────────────────────────────────────────────────────────┐ │
38//! │ │ TxQueue (priority-ordered outbound frame buffer) │ │
39//! │ └──────────────────────────────────────────────────────────┘ │
40//! │ ┌──────────────────────────────────────────────────────────┐ │
41//! │ │ DuplicateCache │ ReplayWindow (per peer, per identity) │ │
42//! │ └──────────────────────────────────────────────────────────┘ │
43//! └──────────────────────────┬─────────────────────────────────────┘
44//! │ async next_event()
45//! ▼
46//! ┌────────────────────────────────────────────────────────────────┐
47//! │ Platform (umsh-hal + umsh-crypto) │
48//! │ Radio · Clock · Rng · Aes/Sha · CounterStore · KeyValueStore │
49//! └────────────────────────────────────────────────────────────────┘
50//! ```
51//!
52//! # Modules and key types
53//!
54//! ## [`coordinator`] — the top-level state machine
55//!
56//! [`Mac<P>`] is the single top-level type. Create one with [`Mac::new`], register
57//! identities and peers, then drive it with [`Mac::run`], [`Mac::run_quiet`], or
58//! [`Mac::next_event`] depending on whether you want a long-lived driver loop or manual
59//! multiplexing with other async work. Everything else in this crate exists to support
60//! `Mac`.
61//!
62//! Supporting types in this module:
63//!
64//! - [`LocalIdentityId`] — opaque slot index returned when registering a local keypair.
65//! - [`LocalIdentity`] — either a long-term platform identity or an ephemeral software
66//! identity for PFS sessions.
67//! - [`IdentitySlot`] — per-identity runtime state: keys, frame counter, pending ACKs.
68//! - [`OperatingPolicy`] — transmission-time rules for the local node (amateur-radio mode,
69//! operator callsign, per-channel overrides).
70//! - [`RepeaterConfig`] — controls whether and how inbound frames are forwarded.
71//! - [`AmateurRadioMode`] — shared enum governing encryption and identification requirements
72//! under ham-radio law.
73//! - [`ChannelPolicy`] — per-channel overrides within an [`OperatingPolicy`].
74//! - [`SendError`], [`MacError`], [`CounterPersistenceError`] — error types for queuing,
75//! runtime event processing, and frame-counter store operations respectively.
76//!
77//! ## [`send`] — outbound transmission types
78//!
79//! - [`SendOptions`] — high-level parameters for a single send: MIC size, encryption,
80//! flood hops, ACK request, source route, salt, etc.
81//! - [`SendReceipt`] — opaque token returned for ACK-requested sends; matched against
82//! inbound MAC ACKs to confirm delivery.
83//! - [`TxQueue`] — priority-ordered, fixed-capacity queue of sealed frames waiting for
84//! radio transmission.
85//! - [`QueuedTx`] — one entry in the transmit queue; includes frame bytes, priority,
86//! not-before timestamp, and CAD retry count.
87//! - [`TxPriority`] — priority classes from highest (`ImmediateAck`) to lowest
88//! (`Application`).
89//! - [`AckState`] — ACK lifecycle state machine covering queued sends, forwarding
90//! confirmation, retry scheduling, and final destination ACK waiting.
91//! - [`PendingAck`] — full tracking record for one in-flight ACK-requested send, stored
92//! in the identity slot until delivery is confirmed or the deadline expires.
93//! - [`ResendRecord`] — verbatim sealed frame bytes retained for retransmission without
94//! re-sealing.
95//!
96//! ## [`cache`] — duplicate suppression and replay protection
97//!
98//! - [`DuplicateCache`] — a fixed-size FIFO ring that records recently-seen
99//! [`DupCacheKey`] values. Before forwarding or delivering any received frame, the
100//! coordinator checks this cache; matching entries are silently dropped. Prevents
101//! re-delivery of frames that echoed back via multiple repeater paths. Entries
102//! leave after [`DUP_CACHE_TTL_MS`] or when the ring recycles, whichever is first;
103//! a hashed key repeats for the life of the sender, so age is what releases it.
104//! - [`DupCacheKey`] — keyed on the truncated MIC for authenticated packets (unforgeable
105//! and compact) or a 32-bit hash of the frame body for unauthenticated ones (broadcast).
106//! - [`ReplayWindow`] — per-peer, per-identity sliding window over frame counters. Rejects
107//! exact counter replays and frames older than the backtrack window, while tolerating
108//! a small amount of out-of-order delivery. Backed by a [`RecentMic`] ring for
109//! backward-window disambiguation.
110//! - [`ReplayVerdict`] — outcome of a replay check: `Accept`, `Duplicate`, or `Replay`.
111//!
112//! ## [`peers`] — remote peer and channel registries
113//!
114//! - [`PeerRegistry`] — a flat list of [`PeerInfo`] records (public key + last-seen time +
115//! cached route). Looked up by hint or full key when matching inbound packets and routing
116//! outbound sends.
117//! - [`PeerId`] — opaque index into the peer registry.
118//! - [`CachedRoute`] — a direct link, an explicit source route, or a flood-distance
119//! estimate, learned from successfully received packets and used to route future sends
120//! without unnecessary flooding.
121//! - [`PeerCryptoMap`] — per-identity map from [`PeerId`] to [`PeerCryptoState`]
122//! (established pairwise keys + replay window). One map per [`IdentitySlot`].
123//! - [`ChannelTable`] — flat list of registered multicast channels. Each entry stores the
124//! raw channel key, the derived `k_enc`/`k_mic` keys (precomputed at registration time),
125//! and the 2-byte channel ID (also precomputed). Looked up by channel ID when
126//! authenticating inbound multicast and blind-unicast frames.
127//!
128//! ## [`handle`] — shared-ownership coordinator access
129//!
130//! - [`MacHandle`] — a `Copy`-able, lifetime-bounded reference to a `RefCell<Mac<P>>`.
131//! Designed for multi-task environments (e.g., `tokio` or RTOS task pairs) where one
132//! task runs the `next_event` loop while another enqueues sends or updates configuration
133//! without holding a long-lived mutable borrow.
134//!
135//! # Platform trait
136//!
137//! [`Platform`] is the single integration point. Implement it once per deployment target to
138//! supply concrete driver types for all hardware abstractions:
139//!
140//! ```rust,ignore
141//! struct MyPlatform;
142//!
143//! impl umsh_mac::Platform for MyPlatform {
144//! type Identity = MyHsmIdentity;
145//! type Aes = MyAesDriver;
146//! type Sha = MyShaDriver;
147//! type Radio = MySx1262Driver;
148//! type Delay = MyDelay;
149//! type Clock = MyMonotonicClock;
150//! type Rng = MyTrng;
151//! type CounterStore = MyFlashStore;
152//! type KeyValueStore = MyNvmStore;
153//! }
154//! ```
155//!
156//! The `umsh` workspace crate provides a `std`/`tokio`-backed implementation
157//! (`tokio_support::StdPlatform`) suitable for desktop development and testing.
158//!
159//! # Frame-counter persistence
160//!
161//! UMSH uses a monotonic frame counter (not a timestamp) for replay protection. Because the
162//! counter must never reuse a value, it must be committed to non-volatile storage before the
163//! corresponding value is used on-air, or after a power cycle the counter could reset to a
164//! previously-seen value, allowing old ciphertexts to replay. The coordinator manages this
165//! automatically:
166//!
167//! 1. On startup, call [`Mac::load_persisted_counter`] for each long-term identity to read
168//! the last-committed boundary from the [`umsh_hal::CounterStore`] and set the live
169//! counter to the next safe starting point.
170//! 2. At runtime, the coordinator schedules a persist whenever the live counter crosses a
171//! block boundary (every [`COUNTER_PERSIST_BLOCK_SIZE`] frames, default 128). While a
172//! persist is pending, sends will eventually block with [`SendError::CounterPersistenceLag`]
173//! if the store is not flushed in time.
174//! 3. The application calls [`Mac::service_counter_persistence`] (typically from the
175//! `next_event` callback or a background task) to drain the pending write queue.
176//!
177//! # `no_std` usage
178//!
179//! Enable `default-features = false` in `Cargo.toml`. The crate compiles without the
180//! standard library. All capacity limits are compile-time const generics. The `std` feature
181//! enables [`test_support`], which provides software-backed driver stubs for unit testing.
182
183use embedded_hal_async::delay::DelayNs;
184
185#[cfg(test)]
186pub(crate) use umsh_crypto::replay::{RECENT_MIC_CAPACITY, REPLAY_STALE_MS};
187pub(crate) const MAX_SOURCE_ROUTE_HOPS: usize = 15;
188pub(crate) const MAX_RESEND_FRAME_LEN: usize = 256;
189pub(crate) const DEFAULT_DUP_CACHE_SIZE: usize = 64;
190pub(crate) const MAX_FORWARD_RETRIES: u8 = 3;
191
192/// CAD attempts a frame gets before it is dropped, from
193/// [Channel Access § Backoff Procedure][spec]: one initial attempt and
194/// four retries. Public because every UMSH transmitter shares the
195/// procedure — a host relaying frames onto a segment contends with the
196/// same neighbours as the MAC does, and a second opinion about how long
197/// to persist would just be a second, wrong answer.
198///
199/// [spec]: https://darconeous.github.io/umsh/docs/protocol/channel-access.html#backoff-procedure
200pub const MAX_CAD_ATTEMPTS: u8 = 5;
201
202/// Default identity-slot capacity for the common `Mac<P>` configuration.
203pub const DEFAULT_IDENTITIES: usize = 4;
204/// Default remote-peer capacity for the common `Mac<P>` configuration.
205pub const DEFAULT_PEERS: usize = 16;
206/// Default shared-channel capacity for the common `Mac<P>` configuration.
207pub const DEFAULT_CHANNELS: usize = 8;
208/// Default pending-ACK capacity for the common `Mac<P>` configuration.
209pub const DEFAULT_ACKS: usize = 16;
210/// Default transmit-queue depth for the common `Mac<P>` configuration.
211pub const DEFAULT_TX: usize = 16;
212/// Default frame-buffer capacity for the common `Mac<P>` configuration.
213pub const DEFAULT_FRAME: usize = MAX_RESEND_FRAME_LEN;
214/// Default duplicate-cache capacity for the common `Mac<P>` configuration.
215pub const DEFAULT_DUP: usize = DEFAULT_DUP_CACHE_SIZE;
216/// Default per-channel full-key replay-window capacity (senders tracked
217/// per channel). Replay windows are ~330 bytes each, so these two
218/// capacities dominate the MAC's channel-table footprint; small targets
219/// shrink them. A full map fail-closes: frames from additional senders
220/// are dropped, never accepted unchecked.
221pub const DEFAULT_CHANNEL_REPLAY: usize = 8;
222/// Default per-channel hint-only replay-window capacity.
223pub const DEFAULT_CHANNEL_HINT_REPLAY: usize = 8;
224
225/// Largest value the `FHOPS_REM` nibble can carry.
226///
227/// A larger budget cannot be encoded, so requests above it are clamped rather
228/// than truncated — [`PacketBuilder::flood_hops`](umsh_core::PacketBuilder)
229/// drops the whole field for an out-of-range value, which would turn "flood
230/// further" into "do not flood at all".
231pub const MAX_FLOOD_HOPS: u8 = 15;
232
233/// Flood-hop slack granted on top of what an established route to a peer
234/// already needs.
235///
236/// A route learned from inbound traffic tells the sender how far away the peer
237/// was, so a unicast that follows it does not need the wide flood budget of a
238/// first-contact packet. Sending with exactly the route's own cost would make
239/// every stale route fail outright, so sends keep this much extra budget: a
240/// path that has grown one hop longer — a repeater moved, a link that now needs
241/// one more relay — still gets through and re-teaches the correct route.
242pub const ESTABLISHED_ROUTE_EXTRA_HOPS: u8 = 1;
243
244/// Error returned when a fixed-capacity MAC data structure is full.
245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
246pub struct CapacityError;
247
248/// Error returned when adding a peer to the MAC peer registry.
249#[derive(Clone, Copy, Debug, PartialEq, Eq)]
250pub enum AddPeerError {
251 /// The peer-registry table is full.
252 Capacity,
253 /// The public-key bytes do not decode to a valid Ed25519 point on the curve.
254 InvalidPublicKey,
255}
256
257impl From<CapacityError> for AddPeerError {
258 fn from(_: CapacityError) -> Self {
259 AddPeerError::Capacity
260 }
261}
262
263/// Error returned when adding a named channel to the MAC channel table.
264#[derive(Clone, Copy, Debug, PartialEq, Eq)]
265pub enum AddChannelError {
266 /// The channel table is full.
267 Capacity,
268 /// The channel name failed canonicalization (non-ASCII or too long).
269 InvalidName(umsh_crypto::ChannelNameError),
270}
271
272impl From<CapacityError> for AddChannelError {
273 fn from(_: CapacityError) -> Self {
274 AddChannelError::Capacity
275 }
276}
277
278/// Bundle of platform-specific associated types used by the higher layers.
279pub trait Platform {
280 /// Local identity implementation.
281 type Identity: umsh_crypto::NodeIdentity;
282 /// AES provider implementation.
283 type Aes: umsh_crypto::AesProvider;
284 /// SHA/HMAC provider implementation.
285 type Sha: umsh_crypto::Sha256Provider;
286 /// Radio implementation.
287 type Radio: umsh_hal::Radio;
288 /// Async delay implementation.
289 type Delay: DelayNs;
290 /// Monotonic clock implementation.
291 type Clock: umsh_hal::Clock;
292 /// Random-number generator implementation.
293 type Rng: rand::CryptoRng;
294 /// Persistent frame-counter store implementation.
295 type CounterStore: umsh_hal::CounterStore;
296 /// General-purpose persistent key-value store implementation.
297 type KeyValueStore: umsh_hal::KeyValueStore;
298}
299
300mod cache;
301mod coordinator;
302pub mod forward_id;
303mod handle;
304mod peers;
305mod send;
306
307pub use cache::{
308 DUP_CACHE_TTL_MS, DupCacheKey, DuplicateCache, RecentMic, ReplayVerdict, ReplayWindow,
309};
310pub use coordinator::{
311 AmateurRadioMode, ChannelPolicy, CounterPersistenceError, IdentitySlot, LocalIdentity,
312 LocalIdentityId, Mac, MacCounters, MacError, OperatingPolicy, RepeaterConfig, SendError,
313 WakeReason,
314};
315pub use handle::MacHandle;
316pub use peers::{
317 CachedRoute, ChannelState, ChannelTable, HintReplayState, PeerCryptoMap, PeerCryptoState,
318 PeerId, PeerInfo, PeerRegistry, PeerRemoval,
319};
320pub use send::{
321 AckState, ChannelInfoRef, CompletionSignal, MacEventRef, PacketFamily, PendingAck,
322 PendingAckError, QueuedTx, ReceivedPacketRef, ResendRecord, RouteHops, RxMetadata, SendOptions,
323 SendReceipt, TxPriority, TxQueue,
324};
325pub use umsh_hal::Snr;
326
327#[cfg(feature = "std")]
328pub mod test_support;
329
330#[cfg(test)]
331mod tests;