umsh_node/
node.rs

1use alloc::boxed::Box;
2use alloc::rc::Rc;
3use alloc::vec::Vec;
4use core::cell::RefCell;
5use core::num::NonZeroU32;
6
7use umsh_core::{ChannelId, NodeHint, PublicKey, RouterHint};
8use umsh_mac::{CachedRoute, LocalIdentityId, SendOptions};
9
10#[cfg(feature = "software-crypto")]
11use crate::channel::Channel;
12use crate::dispatch::EventDispatcher;
13use crate::identity_responder::{
14    IdentityRequestContext, IdentityResponder, IdentityResponsePlan, NodeIdentityProfile,
15    RespondDecision, default_respond_policy,
16};
17use crate::mac::MacBackend;
18use crate::peer::PeerConnection;
19use crate::peer_repeaters::{self, PeerRepeaterTable};
20#[cfg(feature = "software-crypto")]
21use crate::pfs::{PfsSessionManager, PfsState};
22use crate::receive::ReceivedPacketRef;
23use crate::ticket::{SendProgressTicket, SendToken};
24use crate::transport::Transport;
25use crate::{AppEncodeError, OwnedMacCommand};
26
27/// Octets one Peer Repeaters Response body may occupy.
28///
29/// Sized against the same 192-octet reply buffer the identity responder
30/// works to, less the payload-type and command bytes and the MAC's own
31/// per-frame overhead. A list that does not fit is paged, so this is a
32/// packing budget rather than a limit on what the node can report.
33const PEER_REPEATERS_RESPONSE_BUDGET: usize = 160;
34
35/// A cursor is the listing generation and the index to resume at.
36///
37/// The generation moves when an identity is recorded, which is what makes a
38/// stale cursor recognizable. It does not move when the MAC hears a new
39/// transmitter, so a neighbor first heard part way through a walk can shift
40/// the entries after it by one — a listing describes a neighborhood at a
41/// moment, and the spec lets a responder page imperfectly rather than freeze
42/// a snapshot per requester.
43const PEER_REPEATERS_CURSOR_LEN: usize = 3;
44
45/// Per-node shared membership state. All cloned `LocalNode` handles and
46/// their `BoundChannel`s share the same instance via `Rc<RefCell<...>>`.
47pub(crate) struct NodeMembership {
48    #[cfg(feature = "software-crypto")]
49    pub channels: Vec<ChannelMembershipEntry>,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
53pub(crate) struct SubscriptionHandle(NonZeroU32);
54
55/// Owned subscription guard.
56///
57/// Dropping the value automatically unregisters the callback.
58pub struct Subscription {
59    cancel: Rc<RefCell<Option<Box<dyn FnMut() -> bool>>>>,
60}
61
62impl Subscription {
63    pub(crate) fn new(cancel: impl FnMut() -> bool + 'static) -> Self {
64        Self {
65            cancel: Rc::new(RefCell::new(Some(Box::new(cancel)))),
66        }
67    }
68
69    /// Unregister immediately instead of waiting for drop.
70    pub fn unsubscribe(self) -> bool {
71        Self::run_cancel(&self.cancel)
72    }
73
74    fn run_cancel(cancel: &Rc<RefCell<Option<Box<dyn FnMut() -> bool>>>>) -> bool {
75        let Some(mut cancel) = cancel.borrow_mut().take() else {
76            return false;
77        };
78        cancel()
79    }
80}
81
82impl Drop for Subscription {
83    fn drop(&mut self) {
84        let _ = Self::run_cancel(&self.cancel);
85    }
86}
87
88pub(crate) struct HandlerTable<T> {
89    slots: Vec<Option<T>>,
90}
91
92impl<T> Default for HandlerTable<T> {
93    fn default() -> Self {
94        Self { slots: Vec::new() }
95    }
96}
97
98impl<T> HandlerTable<T> {
99    pub(crate) fn insert(&mut self, handler: T) -> SubscriptionHandle {
100        if let Some((index, slot)) = self
101            .slots
102            .iter_mut()
103            .enumerate()
104            .find(|(_, slot)| slot.is_none())
105        {
106            *slot = Some(handler);
107            return SubscriptionHandle(NonZeroU32::new((index + 1) as u32).unwrap());
108        }
109        self.slots.push(Some(handler));
110        SubscriptionHandle(NonZeroU32::new(self.slots.len() as u32).unwrap())
111    }
112
113    pub(crate) fn remove(&mut self, handle: SubscriptionHandle) -> bool {
114        let index = handle.0.get() as usize - 1;
115        let Some(slot) = self.slots.get_mut(index) else {
116            return false;
117        };
118        slot.take().is_some()
119    }
120
121    fn any_mut(&mut self, mut f: impl FnMut(&mut T) -> bool) -> bool {
122        for slot in &mut self.slots {
123            let Some(handler) = slot.as_mut() else {
124                continue;
125            };
126            if f(handler) {
127                return true;
128            }
129        }
130        false
131    }
132
133    fn for_each_mut(&mut self, mut f: impl FnMut(&mut T)) {
134        for slot in &mut self.slots {
135            let Some(handler) = slot.as_mut() else {
136                continue;
137            };
138            f(handler);
139        }
140    }
141}
142
143pub(crate) struct PendingPing {
144    pub nonce: u16,
145    pub peer: PublicKey,
146    pub sent_at_ms: u64,
147    pub deadline_ms: u64,
148}
149
150/// Measurements attached to an authenticated echo response.
151///
152/// RSSI, SNR, and LQI describe the final radio hop into this node. Route
153/// hints are the authenticated trace-route entries accumulated by repeaters;
154/// endpoints are not included in that list.
155#[derive(Clone, Debug, PartialEq, Eq)]
156pub struct PongMetadata {
157    pub round_trip_ms: u64,
158    /// Radio links the response crossed, counting the final one into this
159    /// node: a direct reply is one hop. `None` when the reply was
160    /// source-routed without a trace route, which crosses hops nobody
161    /// recorded. See
162    /// [`ReceivedPacketRef::hop_count`](umsh_mac::ReceivedPacketRef::hop_count).
163    pub hop_count: Option<u8>,
164    pub route_hints: Vec<RouterHint>,
165    pub rssi_dbm: Option<i16>,
166    pub snr_centibels: Option<i16>,
167    pub lqi: Option<u8>,
168}
169
170pub(crate) struct PeerSubscriptions {
171    peer: PublicKey,
172    pub(crate) receive_handlers: HandlerTable<Box<dyn FnMut(&ReceivedPacketRef<'_>) -> bool>>,
173    pub(crate) ack_received_handlers: HandlerTable<Box<dyn FnMut(SendToken)>>,
174    pub(crate) ack_timeout_handlers: HandlerTable<Box<dyn FnMut(SendToken)>>,
175    pub(crate) pfs_established_handlers: HandlerTable<Box<dyn FnMut()>>,
176    pub(crate) pfs_ended_handlers: HandlerTable<Box<dyn FnMut()>>,
177    pub(crate) pong_handlers: HandlerTable<Box<dyn FnMut(u64)>>,
178    pub(crate) ping_timeout_handlers: HandlerTable<Box<dyn FnMut()>>,
179}
180
181impl PeerSubscriptions {
182    fn new(peer: PublicKey) -> Self {
183        Self {
184            peer,
185            receive_handlers: HandlerTable::default(),
186            ack_received_handlers: HandlerTable::default(),
187            ack_timeout_handlers: HandlerTable::default(),
188            pfs_established_handlers: HandlerTable::default(),
189            pfs_ended_handlers: HandlerTable::default(),
190            pong_handlers: HandlerTable::default(),
191            ping_timeout_handlers: HandlerTable::default(),
192        }
193    }
194}
195
196pub(crate) struct LocalNodeState {
197    receive_handlers: HandlerTable<Box<dyn FnMut(&ReceivedPacketRef<'_>) -> bool>>,
198    node_discovered_handlers: HandlerTable<Box<dyn FnMut(PublicKey, Option<&str>)>>,
199    beacon_handlers: HandlerTable<Box<dyn FnMut(NodeHint, Option<PublicKey>)>>,
200    mac_command_handlers: HandlerTable<Box<dyn FnMut(PublicKey, &OwnedMacCommand)>>,
201    transmitted_handlers: HandlerTable<Box<dyn FnMut(&[u8])>>,
202    ack_received_handlers: HandlerTable<Box<dyn FnMut(PublicKey, SendToken)>>,
203    ack_timeout_handlers: HandlerTable<Box<dyn FnMut(PublicKey, SendToken)>>,
204    pfs_established_handlers: HandlerTable<Box<dyn FnMut(PublicKey)>>,
205    pfs_ended_handlers: HandlerTable<Box<dyn FnMut(PublicKey)>>,
206    pfs_failed_handlers: HandlerTable<Box<dyn FnMut(PublicKey, PfsFailure)>>,
207    pong_handlers: HandlerTable<Box<dyn FnMut(PublicKey, u64)>>,
208    pong_metadata_handlers: HandlerTable<Box<dyn FnMut(PublicKey, &PongMetadata)>>,
209    ping_timeout_handlers: HandlerTable<Box<dyn FnMut(PublicKey)>>,
210    pending_pings: Vec<PendingPing>,
211    peer_subscriptions: Vec<PeerSubscriptions>,
212    identity_responder: Option<IdentityResponder>,
213    /// The repeaters whose identities this node has seen, and whether it
214    /// answers questions about them.
215    peer_repeaters: PeerRepeaterTable,
216    peer_repeaters_responder: bool,
217    #[cfg(feature = "software-crypto")]
218    pfs: PfsSessionManager,
219}
220
221impl LocalNodeState {
222    pub(crate) fn new() -> Self {
223        Self {
224            receive_handlers: HandlerTable::default(),
225            node_discovered_handlers: HandlerTable::default(),
226            beacon_handlers: HandlerTable::default(),
227            mac_command_handlers: HandlerTable::default(),
228            transmitted_handlers: HandlerTable::default(),
229            ack_received_handlers: HandlerTable::default(),
230            ack_timeout_handlers: HandlerTable::default(),
231            pfs_established_handlers: HandlerTable::default(),
232            pfs_ended_handlers: HandlerTable::default(),
233            pfs_failed_handlers: HandlerTable::default(),
234            pong_handlers: HandlerTable::default(),
235            pong_metadata_handlers: HandlerTable::default(),
236            ping_timeout_handlers: HandlerTable::default(),
237            pending_pings: Vec::new(),
238            peer_subscriptions: Vec::new(),
239            identity_responder: None,
240            peer_repeaters: PeerRepeaterTable::new(),
241            peer_repeaters_responder: false,
242            #[cfg(feature = "software-crypto")]
243            pfs: PfsSessionManager::new(),
244        }
245    }
246
247    pub(crate) fn peer_subscriptions_mut(&mut self, peer: PublicKey) -> &mut PeerSubscriptions {
248        if let Some(index) = self
249            .peer_subscriptions
250            .iter()
251            .position(|entry| entry.peer == peer)
252        {
253            return &mut self.peer_subscriptions[index];
254        }
255        self.peer_subscriptions.push(PeerSubscriptions::new(peer));
256        self.peer_subscriptions
257            .last_mut()
258            .expect("peer subscriptions just inserted")
259    }
260
261    pub(crate) fn find_peer_subscriptions_mut(
262        &mut self,
263        peer: PublicKey,
264    ) -> Option<&mut PeerSubscriptions> {
265        self.peer_subscriptions
266            .iter_mut()
267            .find(|entry| entry.peer == peer)
268    }
269}
270
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272#[cfg(feature = "software-crypto")]
273pub enum PfsStatus {
274    Inactive,
275    Requested,
276    Active {
277        local_ephemeral_id: LocalIdentityId,
278        peer_ephemeral: PublicKey,
279        expires_ms: u64,
280    },
281}
282
283#[derive(Clone, Copy, Debug, PartialEq, Eq)]
284pub(crate) enum PfsLifecycle {
285    Established(PublicKey),
286    Ended(PublicKey),
287}
288
289/// Why a local PFS negotiation step failed. Surfaced to applications via
290/// [`LocalNode::on_pfs_failed`] so a failed/stalled negotiation reports a
291/// reason instead of silently doing nothing.
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
293pub enum PfsFailure {
294    /// No free identity or peer slot to activate the ephemeral session —
295    /// e.g. the MAC `IDENTITIES`/peer table is exhausted, or more concurrent
296    /// PFS sessions were requested than there are ephemeral identity slots.
297    Capacity,
298    /// A PFS response or teardown referenced a session that does not exist.
299    SessionMissing,
300    /// Crypto failure while deriving the ephemeral session keys.
301    Crypto,
302    /// Failed to transmit a PFS control frame to the peer.
303    Send,
304    /// A sent PFS request was not answered before its deadline (the peer never
305    /// responded, or its response was lost).
306    Timeout,
307    /// Any other node-layer failure during PFS processing.
308    Other,
309}
310
311#[cfg(feature = "software-crypto")]
312pub(crate) struct ChannelMembershipEntry {
313    pub channel: Channel,
314    /// Monotonically increasing per this (node, channel) pair.
315    /// Bumped on leave; BoundChannel snapshots this at creation.
316    pub generation: u64,
317    /// False after leave(); entry kept until re-joined or GC'd.
318    pub active: bool,
319}
320
321impl NodeMembership {
322    pub fn new() -> Self {
323        Self {
324            #[cfg(feature = "software-crypto")]
325            channels: Vec::new(),
326        }
327    }
328}
329
330/// Errors produced by node-layer operations.
331#[derive(Clone, PartialEq, Eq)]
332pub enum NodeError<M: MacBackend> {
333    /// Underlying MAC-layer failure.
334    Mac(crate::mac::MacBackendError<M::SendError, M::CapacityError>),
335    /// The node has left this channel since the handle was created.
336    ChannelLeft,
337    /// The peer is not registered.
338    PeerMissing,
339    /// Control-payload encode failure.
340    AppEncode(AppEncodeError),
341    /// The referenced PFS session was missing.
342    #[cfg(feature = "software-crypto")]
343    PfsSessionMissing,
344    /// The PFS session table is full.
345    #[cfg(feature = "software-crypto")]
346    PfsSessionTableFull,
347    /// Crypto failure during PFS processing.
348    #[cfg(feature = "software-crypto")]
349    Crypto(umsh_crypto::CryptoError),
350}
351
352impl<M: MacBackend> NodeError<M> {
353    /// Coarse classification of this error for surfacing PFS failures to
354    /// applications (the concrete error type is generic over `M`, so callers
355    /// that just want to report a failure use this instead).
356    pub(crate) fn pfs_failure(&self) -> PfsFailure {
357        use crate::mac::MacBackendError;
358        match self {
359            NodeError::Mac(MacBackendError::Capacity(_)) => PfsFailure::Capacity,
360            NodeError::Mac(MacBackendError::Send(_)) => PfsFailure::Send,
361            #[cfg(feature = "software-crypto")]
362            NodeError::PfsSessionTableFull => PfsFailure::Capacity,
363            #[cfg(feature = "software-crypto")]
364            NodeError::PfsSessionMissing => PfsFailure::SessionMissing,
365            #[cfg(feature = "software-crypto")]
366            NodeError::Crypto(_) => PfsFailure::Crypto,
367            _ => PfsFailure::Other,
368        }
369    }
370}
371
372impl<M> core::fmt::Debug for NodeError<M>
373where
374    M: MacBackend,
375    M::SendError: core::fmt::Debug,
376    M::CapacityError: core::fmt::Debug,
377{
378    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
379        match self {
380            Self::Mac(e) => f.debug_tuple("Mac").field(e).finish(),
381            Self::ChannelLeft => f.write_str("ChannelLeft"),
382            Self::PeerMissing => f.write_str("PeerMissing"),
383            Self::AppEncode(e) => f.debug_tuple("AppEncode").field(e).finish(),
384            #[cfg(feature = "software-crypto")]
385            Self::PfsSessionMissing => f.write_str("PfsSessionMissing"),
386            #[cfg(feature = "software-crypto")]
387            Self::PfsSessionTableFull => f.write_str("PfsSessionTableFull"),
388            #[cfg(feature = "software-crypto")]
389            Self::Crypto(e) => f.debug_tuple("Crypto").field(e).finish(),
390        }
391    }
392}
393
394impl<M: MacBackend> From<crate::mac::MacBackendError<M::SendError, M::CapacityError>>
395    for NodeError<M>
396{
397    fn from(e: crate::mac::MacBackendError<M::SendError, M::CapacityError>) -> Self {
398        Self::Mac(e)
399    }
400}
401
402impl<M: MacBackend> From<AppEncodeError> for NodeError<M> {
403    fn from(e: AppEncodeError) -> Self {
404        Self::AppEncode(e)
405    }
406}
407
408#[cfg(feature = "software-crypto")]
409impl<M: MacBackend> From<umsh_crypto::CryptoError> for NodeError<M> {
410    fn from(e: umsh_crypto::CryptoError) -> Self {
411        Self::Crypto(e)
412    }
413}
414
415/// Per-identity application handle.
416///
417/// `LocalNode` owns its channel membership set via shared interior state.
418/// Different `LocalNode` instances (for different identities) may join
419/// different channel sets.
420#[derive(Clone)]
421pub struct LocalNode<M: MacBackend> {
422    identity_id: LocalIdentityId,
423    mac: M,
424    dispatcher: Rc<RefCell<EventDispatcher>>,
425    #[allow(dead_code)] // Used by channel methods (software-crypto feature)
426    membership: Rc<RefCell<NodeMembership>>,
427    state: Rc<RefCell<LocalNodeState>>,
428}
429
430impl<M: MacBackend> LocalNode<M> {
431    /// Create a new local node.
432    pub(crate) fn new(
433        identity_id: LocalIdentityId,
434        mac: M,
435        dispatcher: Rc<RefCell<EventDispatcher>>,
436        membership: Rc<RefCell<NodeMembership>>,
437        state: Rc<RefCell<LocalNodeState>>,
438    ) -> Self {
439        Self {
440            identity_id,
441            mac,
442            dispatcher,
443            membership,
444            state,
445        }
446    }
447
448    /// The identity slot this node operates on.
449    pub fn identity_id(&self) -> LocalIdentityId {
450        self.identity_id
451    }
452
453    /// Create a peer connection (registers peer in MAC if new).
454    pub async fn peer(&self, key: PublicKey) -> Result<PeerConnection<Self>, NodeError<M>> {
455        self.mac.add_peer(key).await?;
456        Ok(PeerConnection::new(self.clone(), key))
457    }
458
459    /// Remove a peer from the MAC and drop this node's per-peer bookkeeping:
460    /// outstanding pings and per-peer subscription tables. Returns whether
461    /// the peer was registered in the MAC. Idempotent — removing an unknown
462    /// peer still clears any node-layer residue and reports `false`.
463    pub async fn remove_peer(&self, key: &PublicKey) -> bool {
464        let removed = self.mac.remove_peer(key).await;
465        let mut state = self.state.borrow_mut();
466        state.pending_pings.retain(|ping| ping.peer != *key);
467        state.peer_subscriptions.retain(|entry| entry.peer != *key);
468        removed
469    }
470
471    /// Return the live TX frame counter for this node's identity, if available.
472    pub async fn frame_counter(&self) -> Option<u32> {
473        self.mac.frame_counter(self.identity_id).await
474    }
475
476    /// Return the persisted TX frame-counter boundary for this node's identity.
477    pub async fn persisted_frame_counter(&self) -> Option<u32> {
478        self.mac.persisted_frame_counter(self.identity_id).await
479    }
480
481    /// Invoke `f` for every peer currently registered in the MAC-layer peer registry.
482    ///
483    /// Covers all known peers, not just those with an active crypto session.
484    pub async fn for_each_peer(&self, f: &mut dyn FnMut(PublicKey)) {
485        self.mac.for_each_peer(f).await
486    }
487
488    /// Invoke `f` for each peer with established crypto state, passing the
489    /// peer's public key, last-accepted RX counter, and persisted RX boundary.
490    pub async fn for_each_peer_counter(&self, f: &mut dyn FnMut(PublicKey, u32, u32)) {
491        self.mac.for_each_peer_counter(self.identity_id, f).await
492    }
493
494    /// Return the route the MAC has cached for `peer`, if any.
495    ///
496    /// A cached route is what the next send to this peer will use, so this is
497    /// the state to inspect when traffic takes an unexpected path.
498    pub async fn peer_route(&self, peer: &PublicKey) -> Option<CachedRoute> {
499        self.mac.peer_route(peer).await
500    }
501
502    /// Forget the route cached for `peer`, returning whether one was held.
503    ///
504    /// Sends fall back to the default delivery mode until an inbound packet
505    /// teaches a route again; the peer and its crypto state are untouched.
506    pub async fn clear_peer_route(&self, peer: &PublicKey) -> bool {
507        self.mac.clear_peer_route(peer).await
508    }
509
510    /// Install a route learned before this MAC existed, returning whether
511    /// `peer` was registered to receive it.
512    ///
513    /// For a host that remembers routes across the lifetime of the stack
514    /// underneath it; see [`umsh_mac::MacHandle::restore_peer_route`].
515    pub async fn restore_peer_route(&self, peer: &PublicKey, route: CachedRoute) -> bool {
516        self.mac.restore_peer_route(peer, route).await
517    }
518
519    fn add_receive_handler<F>(&self, handler: F) -> SubscriptionHandle
520    where
521        F: FnMut(&ReceivedPacketRef<'_>) -> bool + 'static,
522    {
523        self.state
524            .borrow_mut()
525            .receive_handlers
526            .insert(Box::new(handler))
527    }
528
529    pub fn on_receive<F>(&self, handler: F) -> Subscription
530    where
531        F: FnMut(&ReceivedPacketRef<'_>) -> bool + 'static,
532    {
533        let handle = self.add_receive_handler(handler);
534        let state = self.state.clone();
535        Subscription::new(move || state.borrow_mut().receive_handlers.remove(handle))
536    }
537
538    fn add_node_discovered_handler<F>(&self, handler: F) -> SubscriptionHandle
539    where
540        F: FnMut(PublicKey, Option<&str>) + 'static,
541    {
542        self.state
543            .borrow_mut()
544            .node_discovered_handlers
545            .insert(Box::new(handler))
546    }
547
548    pub fn on_node_discovered<F>(&self, handler: F) -> Subscription
549    where
550        F: FnMut(PublicKey, Option<&str>) + 'static,
551    {
552        let handle = self.add_node_discovered_handler(handler);
553        let state = self.state.clone();
554        Subscription::new(move || state.borrow_mut().node_discovered_handlers.remove(handle))
555    }
556
557    fn add_beacon_handler<F>(&self, handler: F) -> SubscriptionHandle
558    where
559        F: FnMut(NodeHint, Option<PublicKey>) + 'static,
560    {
561        self.state
562            .borrow_mut()
563            .beacon_handlers
564            .insert(Box::new(handler))
565    }
566
567    pub fn on_beacon<F>(&self, handler: F) -> Subscription
568    where
569        F: FnMut(NodeHint, Option<PublicKey>) + 'static,
570    {
571        let handle = self.add_beacon_handler(handler);
572        let state = self.state.clone();
573        Subscription::new(move || state.borrow_mut().beacon_handlers.remove(handle))
574    }
575
576    fn add_mac_command_handler<F>(&self, handler: F) -> SubscriptionHandle
577    where
578        F: FnMut(PublicKey, &OwnedMacCommand) + 'static,
579    {
580        self.state
581            .borrow_mut()
582            .mac_command_handlers
583            .insert(Box::new(handler))
584    }
585
586    pub fn on_mac_command<F>(&self, handler: F) -> Subscription
587    where
588        F: FnMut(PublicKey, &OwnedMacCommand) + 'static,
589    {
590        let handle = self.add_mac_command_handler(handler);
591        let state = self.state.clone();
592        Subscription::new(move || state.borrow_mut().mac_command_handlers.remove(handle))
593    }
594
595    /// Enable the built-in Identity Request responder with a custom respond
596    /// policy.
597    ///
598    /// The node will answer a matching [Identity Request](crate::mac_command)
599    /// with an authenticated unicast identity response (see
600    /// [`identity_responder`](crate::identity_responder)). `policy` is the
601    /// registerable discriminator invoked for each selected request; return
602    /// [`RespondDecision::Ignore`] to decline. Replaces any previously enabled
603    /// responder.
604    pub fn enable_identity_responder<P>(&self, profile: NodeIdentityProfile, policy: P)
605    where
606        P: FnMut(&IdentityRequestContext<'_>) -> RespondDecision + 'static,
607    {
608        self.state.borrow_mut().identity_responder =
609            Some(IdentityResponder::new(profile, Box::new(policy)));
610    }
611
612    /// Enable the Identity Request responder with the
613    /// [`default_respond_policy`](crate::identity_responder::default_respond_policy).
614    pub fn enable_identity_responder_default(&self, profile: NodeIdentityProfile) {
615        self.enable_identity_responder(profile, default_respond_policy);
616    }
617
618    /// Disable the Identity Request responder, if enabled.
619    pub fn disable_identity_responder(&self) {
620        self.state.borrow_mut().identity_responder = None;
621    }
622
623    /// Update the live fields of the enabled responder's identity profile
624    /// (e.g. refresh `location`/`altitude_m` from a GPS fix). No-op if the
625    /// responder is not enabled.
626    pub fn update_identity_profile(&self, f: impl FnOnce(&mut NodeIdentityProfile)) {
627        if let Some(responder) = self.state.borrow_mut().identity_responder.as_mut() {
628            f(&mut responder.profile);
629        }
630    }
631
632    /// Read the enabled responder's identity profile, returning `None`
633    /// if the responder is not enabled.
634    ///
635    /// The point of reading it rather than reconstructing it is that
636    /// there is one statement of what this node is, and every framing of
637    /// it — the Identity Request reply, a standalone signed
638    /// advertisement, a local-control read — has to be that same
639    /// statement.
640    pub fn with_identity_profile<R>(&self, f: impl FnOnce(&NodeIdentityProfile) -> R) -> Option<R> {
641        self.state
642            .borrow()
643            .identity_responder
644            .as_ref()
645            .map(|responder| f(&responder.profile))
646    }
647
648    /// Evaluate an incoming Identity Request against the enabled responder,
649    /// producing a reply plan the async pump can execute. Returns `None` when
650    /// no responder is enabled, the request does not select this node, or the
651    /// policy declines.
652    pub(crate) fn evaluate_identity_request(
653        &self,
654        packet: &ReceivedPacketRef<'_>,
655        from: PublicKey,
656        options: &[u8],
657        now_ms: u64,
658    ) -> Option<IdentityResponsePlan> {
659        // Flood management for solicitations that can reach many nodes: a
660        // broadcast (or multicast) Identity Request must not carry a routing
661        // constraint (a Route option is fine only when empty) — a steered
662        // request is still on its way to the neighborhood it meant to ask.
663        //
664        // A request that no FILTER_NODE_HINT narrows may additionally be
665        // answered by every node it reaches, so it must also arrive
666        // unrepeated, FHOPS absent or fully zero; flood routing such a request
667        // multiplies replies across the mesh. A hint-filtered request names
668        // one answering node and so may travel as far as it likes.
669        let filters = crate::mac_command::IdentityRequestFilters::new(options);
670        if matches!(
671            packet.packet_family(),
672            crate::PacketFamily::Broadcast | crate::PacketFamily::Multicast
673        ) {
674            if packet.source_route().is_some_and(|route| !route.is_empty()) {
675                return None;
676            }
677            if !filters.hint_filtered() && packet.flood_hops().is_some_and(|hops| hops.0 != 0) {
678                return None;
679            }
680        }
681        let ctx = IdentityRequestContext {
682            from_key: from,
683            from_hint: packet.from_hint(),
684            source_authenticated: packet.source_authenticated(),
685            has_full_source: packet.has_full_source(),
686            channel: packet.channel().map(|c| c.id()),
687            family: packet.packet_family(),
688            filters,
689            rssi: packet.rssi(),
690            snr: packet.snr(),
691            trace_route: packet.trace_route().unwrap_or(&[]),
692        };
693        self.state
694            .borrow_mut()
695            .identity_responder
696            .as_mut()?
697            .evaluate(&ctx, now_ms)
698    }
699
700    /// Send a response over the carriage its request arrived on.
701    ///
702    /// A blind unicast conceals both endpoints from anyone without the channel
703    /// key, so an answer sent off the channel would name the pair the request
704    /// took care to hide. `channel` is `Some` only when the request was itself
705    /// a blind unicast—take it from the received packet's
706    /// [`channel`](crate::ReceivedPacketRef::channel).
707    pub async fn send_response(
708        &self,
709        to: &PublicKey,
710        channel: Option<ChannelId>,
711        payload: &[u8],
712        options: &SendOptions,
713    ) -> Result<Option<umsh_mac::SendReceipt>, crate::MacBackendError<M::SendError, M::CapacityError>>
714    {
715        match channel {
716            Some(channel) => {
717                self.mac
718                    .send_blind_unicast(self.identity_id, to, &channel, payload, options)
719                    .await
720            }
721            None => {
722                self.mac
723                    .send_unicast(self.identity_id, to, payload, options)
724                    .await
725            }
726        }
727    }
728
729    /// Send a resolved Identity Request reply as an authenticated unicast.
730    ///
731    /// Uses the node's long-term identity (not a PFS ephemeral). Relies on the
732    /// crypto state the MAC already resolved for the requester — permanent or
733    /// transient — and never promotes/pins the peer. Failures are dropped: the
734    /// requester can always ask again.
735    pub(crate) async fn send_identity_response(&self, plan: IdentityResponsePlan) {
736        let unrouted = || {
737            let options = SendOptions::default();
738            if plan.full_source {
739                options.with_full_source()
740            } else {
741                options
742            }
743        };
744        // Steer the reply back down the trace the request arrived by. An
745        // explicit route beats whatever the peer registry cached, which for a
746        // broadcast solicitation is nothing at all. A route too long to
747        // express falls back to no route rather than to a truncated one,
748        // which would strand the reply mid-path.
749        //
750        // Routed before the no_flood block deliberately: try_with_source_route
751        // back-fills a flood budget from the route length, and the reply to an
752        // unhinted solicitation must carry no FHOPS field at all.
753        let mut options = if plan.route.is_empty() {
754            unrouted()
755        } else {
756            unrouted()
757                .try_with_source_route(&plan.route)
758                .unwrap_or_else(|_| unrouted())
759        };
760        if plan.no_flood {
761            // The solicitation named no single node, so it was allowed one hop
762            // and no more; the reply carries no FHOPS field so no repeater can
763            // flood it back across the mesh.
764            options = options.no_flood();
765        }
766        if plan.delayed {
767            // Every node the solicitation selected is answering the same
768            // frame; a random hold spreads the replies across the window.
769            // Channel-activity failures then follow the MAC's normal bounded
770            // CCA backoff-and-retry.
771            let mut jitter = [0u8; 2];
772            self.mac.fill_random(&mut jitter).await;
773            let delay = u16::from_be_bytes(jitter)
774                % (crate::identity_responder::RESPONSE_MAX_DELAY_MS
775                    - crate::identity_responder::RESPONSE_MIN_DELAY_MS
776                    + 1)
777                + crate::identity_responder::RESPONSE_MIN_DELAY_MS;
778            options = options.with_tx_delay_ms(delay);
779        }
780        // A broadcast solicitation's source is not auto-registered on
781        // receive, so the requester may be a complete stranger. Take a
782        // transient slot for them — never a pinned one — so the unicast
783        // below has somewhere to go.
784        let _ = self.mac.ensure_transient_peer(&plan.to).await;
785        let _ = self
786            .send_response(&plan.to, plan.channel, &plan.framed, &options)
787            .await;
788    }
789
790    /// Answer Peer Repeaters Requests with what this node knows about the
791    /// repeaters around it.
792    ///
793    /// Off by default. The table itself fills regardless — an identity is
794    /// recorded when it arrives, whether or not anyone will ever ask — so
795    /// switching this on does not start from an empty neighborhood.
796    pub fn enable_peer_repeaters_responder(&self) {
797        self.state.borrow_mut().peer_repeaters_responder = true;
798    }
799
800    pub fn disable_peer_repeaters_responder(&self) {
801        self.state.borrow_mut().peer_repeaters_responder = false;
802    }
803
804    pub fn peer_repeaters_responder_enabled(&self) -> bool {
805        self.state.borrow().peer_repeaters_responder
806    }
807
808    /// Record what an arriving identity said, if it came from a repeater.
809    pub(crate) fn observe_peer_identity(
810        &self,
811        from: PublicKey,
812        identity: &crate::NodeIdentityPayload,
813        now_ms: u64,
814    ) {
815        self.state
816            .borrow_mut()
817            .peer_repeaters
818            .observe_identity(&from, identity, now_ms);
819    }
820
821    /// The peer repeaters this node knows of, merged with what the radio has
822    /// heard from each.
823    pub async fn peer_repeaters(&self) -> Vec<peer_repeaters::MergedPeerRepeater> {
824        let now_ms = self.mac.now_ms().await;
825        let mut observations = Vec::new();
826        self.mac
827            .for_each_transmitter_observation(&mut |observation| observations.push(observation))
828            .await;
829        // The borrow ends before the merge so a handler holding the state
830        // across an await cannot deadlock against it.
831        let identities = self.state.borrow().peer_repeaters.clone();
832        peer_repeaters::merge(&identities, observations.iter(), now_ms)
833    }
834
835    /// Build and send one page of this node's peer-repeater list.
836    ///
837    /// Evaluated here rather than in the synchronous receive dispatch because
838    /// the answer needs the MAC's observations, which only an async borrow
839    /// reaches.
840    pub(crate) async fn answer_peer_repeaters_request(
841        &self,
842        to: PublicKey,
843        channel: Option<ChannelId>,
844        request: &[u8],
845    ) {
846        if !self.peer_repeaters_responder_enabled() {
847            return;
848        }
849        let view = crate::mac_command::PeerRepeatersRequestView::new(request);
850        let generation = self.state.borrow().peer_repeaters.generation();
851        let peers = self.peer_repeaters().await;
852
853        // A cursor from a listing that has since changed names a place in a
854        // list that no longer exists, so the enumeration restarts rather than
855        // resuming into a different neighborhood.
856        let start = match view.cursor() {
857            Some([high, low, index]) if u16::from_be_bytes([*high, *low]) == generation => {
858                usize::from(*index)
859            }
860            _ => 0,
861        };
862        let total = u8::try_from(peers.len()).unwrap_or(u8::MAX);
863
864        let mut builder =
865            crate::mac_command::PeerRepeatersResponseBuilder::new(PEER_REPEATERS_RESPONSE_BUDGET)
866                .total(total)
867                .reserve_cursor(PEER_REPEATERS_CURSOR_LEN);
868        if let Some(nonce) = view.nonce() {
869            builder = builder.nonce(nonce);
870        }
871
872        let mut next = start;
873        for peer in peers.iter().skip(start) {
874            let regions: Vec<u8> = peer.regions.iter().flatten().copied().collect();
875            let entry = crate::mac_command::PeerRepeaterEntry {
876                hint: &peer.hint,
877                name: peer.name.as_deref(),
878                rssi_snr: match (peer.rssi_dbm, peer.snr) {
879                    (Some(rssi), Some(snr)) => Some((rssi, snr)),
880                    _ => None,
881                },
882                last_heard_min: peer.last_heard_min,
883                location: peer.location,
884                regions: &regions,
885            };
886            // A single entry too large for a whole page would stall the
887            // enumeration on it forever, so it is skipped rather than retried.
888            match builder.try_push(&entry) {
889                Ok(true) => next += 1,
890                Ok(false) => break,
891                Err(_) => next += 1,
892            }
893        }
894        if next < peers.len() {
895            let [high, low] = generation.to_be_bytes();
896            builder = builder.cursor(&[high, low, u8::try_from(next).unwrap_or(u8::MAX)]);
897        }
898        let Ok(body) = builder.build() else {
899            return;
900        };
901
902        // The payload-type byte, then the command — the framing every MAC
903        // command travels in.
904        let mut framed = [0u8; PEER_REPEATERS_RESPONSE_BUDGET + 8];
905        framed[0] = umsh_core::PayloadType::MacCommand as u8;
906        let Ok(len) = crate::mac_command::encode(
907            &crate::mac_command::MacCommand::PeerRepeatersResponse { body: &body },
908            &mut framed[1..],
909        ) else {
910            return;
911        };
912        let _ = self.mac.ensure_transient_peer(&to).await;
913        let _ = self
914            .send_response(&to, channel, &framed[..len + 1], &SendOptions::default())
915            .await;
916    }
917
918    fn add_transmitted_handler<F>(&self, handler: F) -> SubscriptionHandle
919    where
920        F: FnMut(&[u8]) + 'static,
921    {
922        self.state
923            .borrow_mut()
924            .transmitted_handlers
925            .insert(Box::new(handler))
926    }
927
928    /// Subscribe to raw on-wire bytes of every frame successfully handed to the radio.
929    pub fn on_transmitted<F>(&self, handler: F) -> Subscription
930    where
931        F: FnMut(&[u8]) + 'static,
932    {
933        let handle = self.add_transmitted_handler(handler);
934        let state = self.state.clone();
935        Subscription::new(move || state.borrow_mut().transmitted_handlers.remove(handle))
936    }
937
938    fn add_ack_received_handler<F>(&self, handler: F) -> SubscriptionHandle
939    where
940        F: FnMut(PublicKey, SendToken) + 'static,
941    {
942        self.state
943            .borrow_mut()
944            .ack_received_handlers
945            .insert(Box::new(handler))
946    }
947
948    pub fn on_ack_received<F>(&self, handler: F) -> Subscription
949    where
950        F: FnMut(PublicKey, SendToken) + 'static,
951    {
952        let handle = self.add_ack_received_handler(handler);
953        let state = self.state.clone();
954        Subscription::new(move || state.borrow_mut().ack_received_handlers.remove(handle))
955    }
956
957    fn add_ack_timeout_handler<F>(&self, handler: F) -> SubscriptionHandle
958    where
959        F: FnMut(PublicKey, SendToken) + 'static,
960    {
961        self.state
962            .borrow_mut()
963            .ack_timeout_handlers
964            .insert(Box::new(handler))
965    }
966
967    pub fn on_ack_timeout<F>(&self, handler: F) -> Subscription
968    where
969        F: FnMut(PublicKey, SendToken) + 'static,
970    {
971        let handle = self.add_ack_timeout_handler(handler);
972        let state = self.state.clone();
973        Subscription::new(move || state.borrow_mut().ack_timeout_handlers.remove(handle))
974    }
975
976    fn add_pfs_established_handler<F>(&self, handler: F) -> SubscriptionHandle
977    where
978        F: FnMut(PublicKey) + 'static,
979    {
980        self.state
981            .borrow_mut()
982            .pfs_established_handlers
983            .insert(Box::new(handler))
984    }
985
986    pub fn on_pfs_established<F>(&self, handler: F) -> Subscription
987    where
988        F: FnMut(PublicKey) + 'static,
989    {
990        let handle = self.add_pfs_established_handler(handler);
991        let state = self.state.clone();
992        Subscription::new(move || state.borrow_mut().pfs_established_handlers.remove(handle))
993    }
994
995    fn add_pfs_ended_handler<F>(&self, handler: F) -> SubscriptionHandle
996    where
997        F: FnMut(PublicKey) + 'static,
998    {
999        self.state
1000            .borrow_mut()
1001            .pfs_ended_handlers
1002            .insert(Box::new(handler))
1003    }
1004
1005    pub fn on_pfs_ended<F>(&self, handler: F) -> Subscription
1006    where
1007        F: FnMut(PublicKey) + 'static,
1008    {
1009        let handle = self.add_pfs_ended_handler(handler);
1010        let state = self.state.clone();
1011        Subscription::new(move || state.borrow_mut().pfs_ended_handlers.remove(handle))
1012    }
1013
1014    fn add_pfs_failed_handler<F>(&self, handler: F) -> SubscriptionHandle
1015    where
1016        F: FnMut(PublicKey, PfsFailure) + 'static,
1017    {
1018        self.state
1019            .borrow_mut()
1020            .pfs_failed_handlers
1021            .insert(Box::new(handler))
1022    }
1023
1024    /// Subscribe to PFS negotiation failures for any peer. The handler is
1025    /// invoked with the peer's long-term key and a coarse [`PfsFailure`]
1026    /// reason whenever a local PFS step (accepting a request, completing a
1027    /// response, or tearing down) fails — so a stalled negotiation reports a
1028    /// reason instead of silently doing nothing.
1029    pub fn on_pfs_failed<F>(&self, handler: F) -> Subscription
1030    where
1031        F: FnMut(PublicKey, PfsFailure) + 'static,
1032    {
1033        let handle = self.add_pfs_failed_handler(handler);
1034        let state = self.state.clone();
1035        Subscription::new(move || state.borrow_mut().pfs_failed_handlers.remove(handle))
1036    }
1037
1038    fn add_pong_handler<F>(&self, handler: F) -> SubscriptionHandle
1039    where
1040        F: FnMut(PublicKey, u64) + 'static,
1041    {
1042        self.state
1043            .borrow_mut()
1044            .pong_handlers
1045            .insert(Box::new(handler))
1046    }
1047
1048    pub fn on_pong<F>(&self, handler: F) -> Subscription
1049    where
1050        F: FnMut(PublicKey, u64) + 'static,
1051    {
1052        let handle = self.add_pong_handler(handler);
1053        let state = self.state.clone();
1054        Subscription::new(move || state.borrow_mut().pong_handlers.remove(handle))
1055    }
1056
1057    pub fn on_pong_with_metadata<F>(&self, handler: F) -> Subscription
1058    where
1059        F: FnMut(PublicKey, &PongMetadata) + 'static,
1060    {
1061        let handle = self
1062            .state
1063            .borrow_mut()
1064            .pong_metadata_handlers
1065            .insert(Box::new(handler));
1066        let state = self.state.clone();
1067        Subscription::new(move || state.borrow_mut().pong_metadata_handlers.remove(handle))
1068    }
1069
1070    fn add_ping_timeout_handler<F>(&self, handler: F) -> SubscriptionHandle
1071    where
1072        F: FnMut(PublicKey) + 'static,
1073    {
1074        self.state
1075            .borrow_mut()
1076            .ping_timeout_handlers
1077            .insert(Box::new(handler))
1078    }
1079
1080    pub fn on_ping_timeout<F>(&self, handler: F) -> Subscription
1081    where
1082        F: FnMut(PublicKey) + 'static,
1083    {
1084        let handle = self.add_ping_timeout_handler(handler);
1085        let state = self.state.clone();
1086        Subscription::new(move || state.borrow_mut().ping_timeout_handlers.remove(handle))
1087    }
1088
1089    pub(crate) fn record_ping(
1090        &self,
1091        nonce: u16,
1092        peer: PublicKey,
1093        sent_at_ms: u64,
1094        deadline_ms: u64,
1095    ) {
1096        self.state.borrow_mut().pending_pings.push(PendingPing {
1097            nonce,
1098            peer,
1099            sent_at_ms,
1100            deadline_ms,
1101        });
1102    }
1103
1104    pub(crate) async fn now_ms(&self) -> u64 {
1105        self.mac.now_ms().await
1106    }
1107
1108    pub(crate) async fn fill_random(&self, dest: &mut [u8]) {
1109        self.mac.fill_random(dest).await
1110    }
1111
1112    /// Called when an EchoResponse arrives. Matches against pending pings and fires pong handlers.
1113    pub(crate) fn match_pong(
1114        &self,
1115        from: PublicKey,
1116        data: &[u8],
1117        packet: &ReceivedPacketRef<'_>,
1118        now_ms: u64,
1119    ) {
1120        if data.len() < 2 {
1121            return;
1122        }
1123        let nonce = u16::from_be_bytes([data[0], data[1]]);
1124        let mut state = self.state.borrow_mut();
1125        let idx = state
1126            .pending_pings
1127            .iter()
1128            .position(|p| p.nonce == nonce && p.peer == from);
1129        if let Some(idx) = idx {
1130            let ping = state.pending_pings.swap_remove(idx);
1131            let rtt_ms = now_ms.saturating_sub(ping.sent_at_ms);
1132            let route_hints = packet.trace_route_hops().collect::<Vec<_>>();
1133            let metadata = PongMetadata {
1134                round_trip_ms: rtt_ms,
1135                hop_count: packet.hop_count(),
1136                route_hints,
1137                rssi_dbm: packet.rssi(),
1138                snr_centibels: packet.snr().map(|snr| snr.as_centibels()),
1139                lqi: packet.lqi().map(core::num::NonZeroU8::get),
1140            };
1141            if let Some(entry) = state.peer_subscriptions.iter_mut().find(|e| e.peer == from) {
1142                entry.pong_handlers.for_each_mut(|h| h(rtt_ms));
1143            }
1144            state.pong_handlers.for_each_mut(|h| h(from, rtt_ms));
1145            state
1146                .pong_metadata_handlers
1147                .for_each_mut(|handler| handler(from, &metadata));
1148        }
1149    }
1150
1151    /// Called periodically by the host timeout service. Fires timeout handlers for expired pings.
1152    pub(crate) fn expire_pings(&self, now_ms: u64) {
1153        let mut state = self.state.borrow_mut();
1154        let mut i = 0;
1155        while i < state.pending_pings.len() {
1156            if now_ms >= state.pending_pings[i].deadline_ms {
1157                let ping = state.pending_pings.swap_remove(i);
1158                if let Some(entry) = state
1159                    .peer_subscriptions
1160                    .iter_mut()
1161                    .find(|e| e.peer == ping.peer)
1162                {
1163                    entry.ping_timeout_handlers.for_each_mut(|h| h());
1164                }
1165                state.ping_timeout_handlers.for_each_mut(|h| h(ping.peer));
1166            } else {
1167                i += 1;
1168            }
1169        }
1170    }
1171
1172    #[cfg(feature = "software-crypto")]
1173    pub async fn request_pfs(
1174        &self,
1175        peer: &PublicKey,
1176        duration_minutes: u16,
1177        options: &SendOptions,
1178    ) -> Result<SendProgressTicket, NodeError<M>> {
1179        let receipt = self
1180            .state
1181            .borrow_mut()
1182            .pfs
1183            .request_session(&self.mac, self.identity_id, peer, duration_minutes, options)
1184            .await?;
1185        Ok(self.register_ack_send(self.identity_id, receipt))
1186    }
1187
1188    #[cfg(feature = "software-crypto")]
1189    pub async fn end_pfs(
1190        &self,
1191        peer: &PublicKey,
1192        options: &SendOptions,
1193    ) -> Result<(), NodeError<M>> {
1194        let _ = self
1195            .state
1196            .borrow_mut()
1197            .pfs
1198            .end_session(&self.mac, self.identity_id, peer, true, options)
1199            .await?;
1200        Ok(())
1201    }
1202
1203    #[cfg(feature = "software-crypto")]
1204    pub async fn pfs_status(&self, peer: &PublicKey) -> Result<PfsStatus, NodeError<M>> {
1205        let now_ms = self.mac.now_ms().await;
1206        let state = self.state.borrow();
1207        if let Some(session) = state
1208            .pfs
1209            .sessions()
1210            .iter()
1211            .find(|session| session.peer_long_term == *peer)
1212        {
1213            return Ok(match session.state {
1214                PfsState::Requested => PfsStatus::Requested,
1215                PfsState::Active => PfsStatus::Active {
1216                    local_ephemeral_id: session.local_ephemeral_id,
1217                    peer_ephemeral: session.peer_ephemeral,
1218                    expires_ms: session.expires_ms,
1219                },
1220            });
1221        }
1222        if state.pfs.active_route(peer, now_ms).is_some() {
1223            // Defensive fallback for any future session bookkeeping changes.
1224            return Ok(PfsStatus::Requested);
1225        }
1226        Ok(PfsStatus::Inactive)
1227    }
1228
1229    /// Join a channel. Registers the channel key in the MAC if this is
1230    /// the first node to join it. Returns the bound channel handle.
1231    #[cfg(feature = "software-crypto")]
1232    pub async fn join(&self, channel: &Channel) -> Result<BoundChannel<M>, NodeError<M>> {
1233        let membership = self.membership.borrow_mut();
1234
1235        // Check if already joined.
1236        if let Some(entry) = membership.channels.iter().find(|e| e.channel == *channel) {
1237            if entry.active {
1238                return Ok(BoundChannel {
1239                    node: self.clone(),
1240                    channel: entry.channel.clone(),
1241                    join_generation: entry.generation,
1242                });
1243            }
1244        }
1245
1246        // Either new or re-joining after a leave. Both register with the MAC:
1247        // `leave` unregisters the key, so a dormant entry has no MAC state to
1248        // reuse.
1249        drop(membership);
1250        self.mac.add_private_channel(channel.key().clone()).await?;
1251        let mut membership = self.membership.borrow_mut();
1252
1253        if let Some(entry) = membership
1254            .channels
1255            .iter_mut()
1256            .find(|e| e.channel == *channel)
1257        {
1258            entry.active = true;
1259            entry.generation = entry.generation.wrapping_add(1);
1260            return Ok(BoundChannel {
1261                node: self.clone(),
1262                channel: entry.channel.clone(),
1263                join_generation: entry.generation,
1264            });
1265        }
1266
1267        let generation = 0;
1268        membership.channels.push(ChannelMembershipEntry {
1269            channel: channel.clone(),
1270            generation,
1271            active: true,
1272        });
1273
1274        Ok(BoundChannel {
1275            node: self.clone(),
1276            channel: channel.clone(),
1277            join_generation: generation,
1278        })
1279    }
1280
1281    /// Leave a channel. Marks the membership entry inactive, bumps that
1282    /// entry's generation counter, and unregisters the channel key from the
1283    /// MAC so inbound traffic on it is no longer decrypted.
1284    #[cfg(feature = "software-crypto")]
1285    pub async fn leave(&self, channel: &Channel) -> Result<(), NodeError<M>> {
1286        {
1287            let mut membership = self.membership.borrow_mut();
1288            let Some(entry) = membership
1289                .channels
1290                .iter_mut()
1291                .find(|e| e.channel == *channel && e.active)
1292            else {
1293                return Ok(());
1294            };
1295            entry.active = false;
1296            entry.generation = entry.generation.wrapping_add(1);
1297        }
1298        self.mac.remove_channel(channel.key()).await;
1299        Ok(())
1300    }
1301
1302    /// Get a handle to an already-joined channel.
1303    #[cfg(feature = "software-crypto")]
1304    pub fn bound_channel(&self, channel: &Channel) -> Option<BoundChannel<M>> {
1305        let membership = self.membership.borrow();
1306        membership
1307            .channels
1308            .iter()
1309            .find(|e| e.channel == *channel && e.active)
1310            .map(|entry| BoundChannel {
1311                node: self.clone(),
1312                channel: entry.channel.clone(),
1313                join_generation: entry.generation,
1314            })
1315    }
1316
1317    /// List all joined channels.
1318    #[cfg(feature = "software-crypto")]
1319    pub fn bound_channels(&self) -> Vec<BoundChannel<M>> {
1320        let membership = self.membership.borrow();
1321        membership
1322            .channels
1323            .iter()
1324            .filter(|e| e.active)
1325            .map(|entry| BoundChannel {
1326                node: self.clone(),
1327                channel: entry.channel.clone(),
1328                join_generation: entry.generation,
1329            })
1330            .collect()
1331    }
1332
1333    /// Register an ACK-tracked send with the dispatcher and return a progress ticket.
1334    fn register_ack_send(
1335        &self,
1336        send_identity_id: LocalIdentityId,
1337        receipt: Option<umsh_mac::SendReceipt>,
1338    ) -> SendProgressTicket {
1339        match receipt {
1340            Some(receipt) => {
1341                let token = SendToken::new(send_identity_id, receipt);
1342                let state = self.dispatcher.borrow_mut().register_ticket(token, false);
1343                SendProgressTicket::new(token, state)
1344            }
1345            // Unicast/blind-unicast without ACK requested — no tracking.
1346            None => SendProgressTicket::fire_and_forget(),
1347        }
1348    }
1349
1350    /// Register a non-ACK send (broadcast/multicast) with the dispatcher.
1351    ///
1352    /// The ticket starts unfinished. The dispatcher marks it transmitted and
1353    /// finished when the MAC fires the `Transmitted` event with this receipt.
1354    fn register_non_ack_send(
1355        &self,
1356        send_identity_id: LocalIdentityId,
1357        receipt: umsh_mac::SendReceipt,
1358    ) -> SendProgressTicket {
1359        let token = SendToken::new(send_identity_id, receipt);
1360        let state = self.dispatcher.borrow_mut().register_ticket(token, true);
1361        SendProgressTicket::new(token, state)
1362    }
1363
1364    pub(crate) fn state(&self) -> &Rc<RefCell<LocalNodeState>> {
1365        &self.state
1366    }
1367
1368    #[cfg(feature = "software-crypto")]
1369    pub(crate) fn owns_ephemeral_identity(&self, identity_id: LocalIdentityId) -> bool {
1370        self.state
1371            .borrow()
1372            .pfs
1373            .sessions()
1374            .iter()
1375            .any(|session| session.local_ephemeral_id == identity_id)
1376    }
1377
1378    #[cfg(feature = "software-crypto")]
1379    pub(crate) async fn handle_pfs_command(
1380        &self,
1381        from: &PublicKey,
1382        channel: Option<ChannelId>,
1383        command: &OwnedMacCommand,
1384        options: &SendOptions,
1385    ) -> Result<Option<PfsLifecycle>, NodeError<M>> {
1386        match *command {
1387            OwnedMacCommand::PfsSessionRequest {
1388                ephemeral_key,
1389                duration_minutes,
1390            } => {
1391                self.state
1392                    .borrow_mut()
1393                    .pfs
1394                    .accept_request(
1395                        &self.mac,
1396                        self.identity_id,
1397                        *from,
1398                        ephemeral_key,
1399                        duration_minutes,
1400                        channel,
1401                        options,
1402                    )
1403                    .await?;
1404                Ok(Some(PfsLifecycle::Established(*from)))
1405            }
1406            OwnedMacCommand::PfsSessionResponse {
1407                ephemeral_key,
1408                duration_minutes,
1409            } => {
1410                if self
1411                    .state
1412                    .borrow_mut()
1413                    .pfs
1414                    .accept_response(
1415                        &self.mac,
1416                        self.identity_id,
1417                        *from,
1418                        ephemeral_key,
1419                        duration_minutes,
1420                    )
1421                    .await?
1422                {
1423                    Ok(Some(PfsLifecycle::Established(*from)))
1424                } else {
1425                    Ok(None)
1426                }
1427            }
1428            OwnedMacCommand::EndPfsSession => {
1429                let _ = self
1430                    .state
1431                    .borrow_mut()
1432                    .pfs
1433                    .end_session(&self.mac, self.identity_id, from, false, options)
1434                    .await?;
1435                Ok(Some(PfsLifecycle::Ended(*from)))
1436            }
1437            _ => Ok(None),
1438        }
1439    }
1440
1441    pub(crate) fn dispatch_received_packet(&self, packet: &ReceivedPacketRef<'_>) -> bool {
1442        let peer = packet.from_key();
1443        let mut state = self.state.borrow_mut();
1444
1445        if let Some(peer) = peer.map(|peer| canonical_peer(&state, peer)) {
1446            if let Some(entry) = state
1447                .peer_subscriptions
1448                .iter_mut()
1449                .find(|entry| entry.peer == peer)
1450            {
1451                if entry.receive_handlers.any_mut(|handler| handler(packet)) {
1452                    return true;
1453                }
1454            }
1455        }
1456
1457        state.receive_handlers.any_mut(|handler| handler(packet))
1458    }
1459
1460    pub(crate) fn dispatch_node_discovered(&self, key: PublicKey, name: Option<&str>) {
1461        self.state
1462            .borrow_mut()
1463            .node_discovered_handlers
1464            .for_each_mut(|handler| handler(key, name));
1465    }
1466
1467    pub(crate) fn dispatch_beacon(&self, from_hint: NodeHint, from_key: Option<PublicKey>) {
1468        self.state
1469            .borrow_mut()
1470            .beacon_handlers
1471            .for_each_mut(|handler| handler(from_hint, from_key));
1472    }
1473
1474    pub(crate) fn dispatch_mac_command(&self, from: PublicKey, command: &OwnedMacCommand) {
1475        self.state
1476            .borrow_mut()
1477            .mac_command_handlers
1478            .for_each_mut(|handler| handler(from, command));
1479    }
1480
1481    pub(crate) fn dispatch_transmitted(&self, wire_bytes: &[u8]) {
1482        self.state
1483            .borrow_mut()
1484            .transmitted_handlers
1485            .for_each_mut(|handler| handler(wire_bytes));
1486    }
1487
1488    pub(crate) fn dispatch_ack_received(&self, peer: PublicKey, token: SendToken) {
1489        let mut state = self.state.borrow_mut();
1490        let peer = canonical_peer(&state, peer);
1491        if let Some(entry) = state
1492            .peer_subscriptions
1493            .iter_mut()
1494            .find(|entry| entry.peer == peer)
1495        {
1496            entry
1497                .ack_received_handlers
1498                .for_each_mut(|handler| handler(token));
1499        }
1500        state
1501            .ack_received_handlers
1502            .for_each_mut(|handler| handler(peer, token));
1503    }
1504
1505    pub(crate) fn dispatch_ack_timeout(&self, peer: PublicKey, token: SendToken) {
1506        let mut state = self.state.borrow_mut();
1507        let peer = canonical_peer(&state, peer);
1508        if let Some(entry) = state
1509            .peer_subscriptions
1510            .iter_mut()
1511            .find(|entry| entry.peer == peer)
1512        {
1513            entry
1514                .ack_timeout_handlers
1515                .for_each_mut(|handler| handler(token));
1516        }
1517        state
1518            .ack_timeout_handlers
1519            .for_each_mut(|handler| handler(peer, token));
1520    }
1521
1522    pub(crate) fn dispatch_pfs_established(&self, peer: PublicKey) {
1523        let mut state = self.state.borrow_mut();
1524        let peer = canonical_peer(&state, peer);
1525        if let Some(entry) = state
1526            .peer_subscriptions
1527            .iter_mut()
1528            .find(|entry| entry.peer == peer)
1529        {
1530            entry
1531                .pfs_established_handlers
1532                .for_each_mut(|handler| handler());
1533        }
1534        state
1535            .pfs_established_handlers
1536            .for_each_mut(|handler| handler(peer));
1537    }
1538
1539    pub(crate) fn dispatch_pfs_ended(&self, peer: PublicKey) {
1540        let mut state = self.state.borrow_mut();
1541        let peer = canonical_peer(&state, peer);
1542        if let Some(entry) = state
1543            .peer_subscriptions
1544            .iter_mut()
1545            .find(|entry| entry.peer == peer)
1546        {
1547            entry.pfs_ended_handlers.for_each_mut(|handler| handler());
1548        }
1549        state
1550            .pfs_ended_handlers
1551            .for_each_mut(|handler| handler(peer));
1552    }
1553
1554    pub(crate) fn dispatch_pfs_failed(&self, peer: PublicKey, reason: PfsFailure) {
1555        let mut state = self.state.borrow_mut();
1556        let peer = canonical_peer(&state, peer);
1557        state
1558            .pfs_failed_handlers
1559            .for_each_mut(|handler| handler(peer, reason));
1560    }
1561
1562    pub(crate) async fn expire_pfs_sessions(&self) -> Result<Vec<PublicKey>, NodeError<M>> {
1563        #[cfg(feature = "software-crypto")]
1564        {
1565            let now_ms = self.mac.now_ms().await;
1566            return self
1567                .state
1568                .borrow_mut()
1569                .pfs
1570                .expire_sessions(&self.mac, now_ms)
1571                .await;
1572        }
1573        #[cfg(not(feature = "software-crypto"))]
1574        {
1575            Ok(Vec::new())
1576        }
1577    }
1578
1579    /// Drop any sent PFS requests that were not answered before their deadline,
1580    /// returning the peers so the caller can report a [`PfsFailure::Timeout`].
1581    #[cfg(feature = "software-crypto")]
1582    pub(crate) fn expire_pfs_requests(&self, now_ms: u64) -> Vec<PublicKey> {
1583        self.state.borrow_mut().pfs.expire_requests(now_ms)
1584    }
1585}
1586
1587fn canonical_peer(state: &LocalNodeState, peer: PublicKey) -> PublicKey {
1588    #[cfg(feature = "software-crypto")]
1589    {
1590        if let Some(session) = state
1591            .pfs
1592            .sessions()
1593            .iter()
1594            .find(|session| session.state == PfsState::Active && session.peer_ephemeral == peer)
1595        {
1596            return session.peer_long_term;
1597        }
1598    }
1599    peer
1600}
1601
1602impl<M: MacBackend> crate::transport::sealed::Sealed for LocalNode<M> {}
1603
1604impl<M: MacBackend> crate::transport::NodeAccess for LocalNode<M> {
1605    type Backend = M;
1606
1607    fn local_node(&self) -> &LocalNode<M> {
1608        self
1609    }
1610}
1611
1612impl<M: MacBackend> Transport for LocalNode<M> {
1613    type Error = NodeError<M>;
1614
1615    async fn send(
1616        &self,
1617        to: &PublicKey,
1618        payload: &[u8],
1619        options: &SendOptions,
1620    ) -> Result<SendProgressTicket, Self::Error> {
1621        #[cfg(feature = "software-crypto")]
1622        let (send_identity_id, receipt) = {
1623            let now_ms = self.mac.now_ms().await;
1624            if let Some((local_id, peer_ephemeral)) =
1625                self.state.borrow().pfs.active_route(to, now_ms)
1626            {
1627                let receipt = self
1628                    .mac
1629                    .send_unicast(local_id, &peer_ephemeral, payload, options)
1630                    .await?;
1631                (local_id, receipt)
1632            } else {
1633                let receipt = self
1634                    .mac
1635                    .send_unicast(self.identity_id, to, payload, options)
1636                    .await?;
1637                (self.identity_id, receipt)
1638            }
1639        };
1640        #[cfg(not(feature = "software-crypto"))]
1641        let (send_identity_id, receipt) = (
1642            self.identity_id,
1643            self.mac
1644                .send_unicast(self.identity_id, to, payload, options)
1645                .await?,
1646        );
1647        Ok(self.register_ack_send(send_identity_id, receipt))
1648    }
1649
1650    async fn send_all(
1651        &self,
1652        payload: &[u8],
1653        options: &SendOptions,
1654    ) -> Result<SendProgressTicket, Self::Error> {
1655        let receipt = self
1656            .mac
1657            .send_broadcast(self.identity_id, payload, options)
1658            .await?;
1659        Ok(self.register_non_ack_send(self.identity_id, receipt))
1660    }
1661}
1662
1663/// A channel bound to a specific `LocalNode`. Implements `Transport`.
1664///
1665/// Holds a snapshot of the per-channel membership generation at creation
1666/// time. If the node leaves this channel, operations return
1667/// `NodeError::ChannelLeft`.
1668#[cfg(feature = "software-crypto")]
1669#[derive(Clone)]
1670pub struct BoundChannel<M: MacBackend> {
1671    node: LocalNode<M>,
1672    channel: Channel,
1673    join_generation: u64,
1674}
1675
1676#[cfg(feature = "software-crypto")]
1677impl<M: MacBackend> BoundChannel<M> {
1678    /// The underlying channel descriptor.
1679    pub fn channel(&self) -> &Channel {
1680        &self.channel
1681    }
1682
1683    /// True if the node is still a member of this channel.
1684    pub fn is_active(&self) -> bool {
1685        let membership = self.node.membership.borrow();
1686        membership
1687            .channels
1688            .iter()
1689            .any(|e| e.channel == self.channel && e.active && e.generation == self.join_generation)
1690    }
1691
1692    /// Create a peer connection through this channel.
1693    pub fn peer(&self, key: PublicKey) -> PeerConnection<Self> {
1694        PeerConnection::new(self.clone(), key)
1695    }
1696
1697    /// Check membership is still valid.
1698    fn check_active(&self) -> Result<(), NodeError<M>> {
1699        if self.is_active() {
1700            Ok(())
1701        } else {
1702            Err(NodeError::ChannelLeft)
1703        }
1704    }
1705
1706    /// Return the owning local node for this bound channel.
1707    pub fn node(&self) -> &LocalNode<M> {
1708        &self.node
1709    }
1710}
1711
1712#[cfg(feature = "software-crypto")]
1713impl<M: MacBackend> crate::transport::sealed::Sealed for BoundChannel<M> {}
1714
1715#[cfg(feature = "software-crypto")]
1716impl<M: MacBackend> crate::transport::NodeAccess for BoundChannel<M> {
1717    type Backend = M;
1718
1719    fn local_node(&self) -> &LocalNode<M> {
1720        &self.node
1721    }
1722}
1723
1724#[cfg(feature = "software-crypto")]
1725impl<M: MacBackend> Transport for BoundChannel<M> {
1726    type Error = NodeError<M>;
1727
1728    async fn send(
1729        &self,
1730        to: &PublicKey,
1731        payload: &[u8],
1732        options: &SendOptions,
1733    ) -> Result<SendProgressTicket, Self::Error> {
1734        self.check_active()?;
1735        let receipt = self
1736            .node
1737            .mac
1738            .send_blind_unicast(
1739                self.node.identity_id,
1740                to,
1741                self.channel.channel_id(),
1742                payload,
1743                options,
1744            )
1745            .await?;
1746        Ok(self.node.register_ack_send(self.node.identity_id, receipt))
1747    }
1748
1749    async fn send_all(
1750        &self,
1751        payload: &[u8],
1752        options: &SendOptions,
1753    ) -> Result<SendProgressTicket, Self::Error> {
1754        self.check_active()?;
1755        let receipt = self
1756            .node
1757            .mac
1758            .send_multicast(
1759                self.node.identity_id,
1760                self.channel.channel_id(),
1761                payload,
1762                options,
1763            )
1764            .await?;
1765        Ok(self
1766            .node
1767            .register_non_ack_send(self.node.identity_id, receipt))
1768    }
1769}