umsh_node/
host.rs

1use alloc::rc::Rc;
2use alloc::vec::Vec;
3use core::cell::RefCell;
4
5use umsh_mac::{LocalIdentityId, SendOptions};
6
7use crate::dispatch::EventDispatcher;
8use crate::identity_responder::IdentityResponsePlan;
9use crate::mac::MacBackend;
10use crate::node::{LocalNode, LocalNodeState, NodeMembership, PfsLifecycle};
11use crate::receive::ReceivedPacketRef;
12use crate::{NodeIdentityPayload, OwnedMacCommand, mac_command};
13use umsh_core::PayloadType;
14
15/// Error returned when a [`Host`] cannot make progress.
16///
17/// Wraps the underlying MAC runtime failure.
18#[derive(Debug)]
19pub enum HostError<E> {
20    Mac(E),
21}
22
23/// Multi-identity orchestration layer for the node API.
24///
25/// `Host` owns the shared MAC run loop and routes inbound events to the correct
26/// [`LocalNode`], including ephemeral PFS identities that belong to a long-term node.
27/// Applications typically:
28///
29/// 1. construct a `Host` from a shared [`MacHandle`](umsh_mac::MacHandle)
30/// 2. register one or more [`LocalNode`]s with [`add_node`](Self::add_node)
31/// 3. attach callbacks to nodes / peers / wrappers
32/// 4. drive progress with [`run`](Self::run) or [`pump_once`](Self::pump_once)
33///
34/// `run()` is the preferred long-lived driver. `pump_once()` exists for callers that need to
35/// multiplex UMSH progress with other async work using `select!`.
36///
37/// # Callback re-entrancy
38///
39/// Receive and control callbacks (`on_receive`, `on_text`, `on_ack_received`, …) are invoked
40/// synchronously from inside [`pump_once`](Self::pump_once) while the coordinator borrow is
41/// held. A callback therefore MUST NOT call back into the MAC — it cannot `await` a `send`,
42/// and a blocking or re-entrant borrow of the coordinator would deadlock. Keep callbacks
43/// short and side-effect-free with respect to the MAC: record what you need (e.g. push to a
44/// queue or set a flag) and perform any follow-up sends after `pump_once` / `run` returns
45/// control to your own task.
46pub struct Host<M: MacBackend> {
47    mac: M,
48    dispatcher: Rc<RefCell<EventDispatcher>>,
49    nodes: Vec<(LocalIdentityId, LocalNode<M>)>,
50    pfs_control_options: SendOptions,
51}
52
53impl<M: MacBackend> Host<M> {
54    /// Create a host around a shared MAC backend.
55    pub fn new(mac: M) -> Self {
56        Self {
57            mac,
58            dispatcher: Rc::new(RefCell::new(EventDispatcher::new())),
59            nodes: Vec::new(),
60            pfs_control_options: SendOptions::default()
61                .with_ack_requested(true)
62                .with_flood_hops(5),
63        }
64    }
65
66    /// Return a clone of the underlying shared MAC backend.
67    pub fn mac(&self) -> M {
68        self.mac.clone()
69    }
70
71    /// Borrow the send options used for node-managed PFS control messages.
72    pub fn pfs_control_options(&self) -> &SendOptions {
73        &self.pfs_control_options
74    }
75
76    /// Replace the send options used for node-managed PFS control messages.
77    pub fn set_pfs_control_options(&mut self, options: SendOptions) {
78        self.pfs_control_options = options;
79    }
80
81    /// Create and register a [`LocalNode`] for an already-registered local identity.
82    ///
83    /// The returned handle is cheap to clone and becomes the application-facing entry point
84    /// for sending traffic and attaching callbacks for that identity.
85    pub fn add_node(&mut self, identity_id: LocalIdentityId) -> LocalNode<M> {
86        let membership = Rc::new(RefCell::new(NodeMembership::new()));
87        let state = Rc::new(RefCell::new(LocalNodeState::new()));
88        let node = LocalNode::new(
89            identity_id,
90            self.mac.clone(),
91            self.dispatcher.clone(),
92            membership,
93            state,
94        );
95        self.nodes.push((identity_id, node.clone()));
96        node
97    }
98
99    /// Look up a previously added node by identity id.
100    pub fn node(&self, identity_id: LocalIdentityId) -> Option<LocalNode<M>> {
101        self.nodes
102            .iter()
103            .find(|(id, _)| *id == identity_id)
104            .map(|(_, node)| node.clone())
105    }
106
107    fn route_node(&self, identity_id: LocalIdentityId) -> Option<LocalNode<M>> {
108        if let Some(node) = self.node(identity_id) {
109            return Some(node);
110        }
111
112        #[cfg(feature = "software-crypto")]
113        {
114            return self
115                .nodes
116                .iter()
117                .find(|(_, node)| node.owns_ephemeral_identity(identity_id))
118                .map(|(_, node)| node.clone());
119        }
120
121        #[cfg(not(feature = "software-crypto"))]
122        {
123            None
124        }
125    }
126
127    /// Drive the shared MAC until one wake cycle completes.
128    ///
129    /// This is a single wake-driven step, not a non-blocking poll. It waits until the MAC has
130    /// meaningful work to do (radio activity or a protocol deadline), dispatches any resulting
131    /// callbacks, services PFS command handling, and then returns.
132    ///
133    /// Use this when you need to multiplex UMSH progress with other async sources using
134    /// `select!`. If UMSH owns the task, prefer [`run`](Self::run).
135    pub async fn pump_once(&mut self) -> Result<(), HostError<M::RunError>> {
136        let now_ms = self.mac.now_ms().await;
137        let pending_pfs = Rc::new(RefCell::new(Vec::<(
138            LocalIdentityId,
139            umsh_core::PublicKey,
140            Option<umsh_core::ChannelId>,
141            OwnedMacCommand,
142        )>::new()));
143        let pending_pfs_ref = pending_pfs.clone();
144        let pending_identity = Rc::new(RefCell::new(
145            Vec::<(LocalNode<M>, IdentityResponsePlan)>::new(),
146        ));
147        let pending_identity_ref = pending_identity.clone();
148        let pending_peer_repeaters = Rc::new(RefCell::new(Vec::<(
149            LocalNode<M>,
150            umsh_core::PublicKey,
151            Option<umsh_core::ChannelId>,
152            Vec<u8>,
153        )>::new()));
154        let pending_peer_repeaters_ref = pending_peer_repeaters.clone();
155        let dispatcher = self.dispatcher.clone();
156        let nodes = self.nodes.clone();
157        self.mac
158            .next_event(move |identity_id, event| {
159                dispatcher
160                    .borrow_mut()
161                    .dispatch_ticket_state(identity_id, &event);
162                let Some(node) = route_node(&nodes, identity_id) else {
163                    return;
164                };
165                match event {
166                    umsh_mac::MacEventRef::Received(packet) => {
167                        let _ = node.dispatch_received_packet(&packet);
168                        if packet.packet_type() == umsh_core::PacketType::Broadcast
169                            && packet.payload().is_empty()
170                        {
171                            if let Some(from_hint) = packet.from_hint() {
172                                node.dispatch_beacon(from_hint, packet.from_key());
173                            }
174                        } else if let Some(from) = packet.from_key() {
175                            dispatch_payload_callbacks(
176                                &node,
177                                &packet,
178                                from,
179                                &pending_pfs_ref,
180                                &pending_identity_ref,
181                                &pending_peer_repeaters_ref,
182                                now_ms,
183                            );
184                        }
185                    }
186                    umsh_mac::MacEventRef::AckReceived { peer, receipt } => {
187                        node.dispatch_ack_received(
188                            peer,
189                            crate::SendToken::new(identity_id, receipt),
190                        );
191                    }
192                    umsh_mac::MacEventRef::AckTimeout { peer, receipt } => {
193                        node.dispatch_ack_timeout(
194                            peer,
195                            crate::SendToken::new(identity_id, receipt),
196                        );
197                    }
198                    umsh_mac::MacEventRef::Transmitted { wire_bytes, .. } => {
199                        node.dispatch_transmitted(wire_bytes);
200                    }
201                    umsh_mac::MacEventRef::Forwarded { .. } => {}
202                    // Ticket resolution (failed + finished) is handled by
203                    // dispatch_ticket_state above; there is no peer to
204                    // notify since the frame never aired.
205                    umsh_mac::MacEventRef::TxAbandoned { .. } => {}
206                }
207            })
208            .await
209            .map_err(HostError::Mac)?;
210
211        let queued: Vec<(
212            LocalIdentityId,
213            umsh_core::PublicKey,
214            Option<umsh_core::ChannelId>,
215            OwnedMacCommand,
216        )> = pending_pfs.borrow_mut().drain(..).collect();
217        for (identity_id, from, channel, command) in queued {
218            self.handle_pfs_command(identity_id, from, channel, command)
219                .await;
220        }
221
222        let identity_replies: Vec<(LocalNode<M>, IdentityResponsePlan)> =
223            pending_identity.borrow_mut().drain(..).collect();
224        for (node, plan) in identity_replies {
225            node.send_identity_response(plan).await;
226        }
227
228        let peer_repeater_requests: Vec<(
229            LocalNode<M>,
230            umsh_core::PublicKey,
231            Option<umsh_core::ChannelId>,
232            Vec<u8>,
233        )> = pending_peer_repeaters.borrow_mut().drain(..).collect();
234        for (node, from, channel, request) in peer_repeater_requests {
235            node.answer_peer_repeaters_request(from, channel, &request)
236                .await;
237        }
238
239        self.service_protocol_timeouts().await;
240
241        Ok(())
242    }
243
244    /// Service node-layer deadlines independently of MAC/radio wake events.
245    ///
246    /// A quiet radio is not a MAC wake source, so applications that multiplex
247    /// [`pump_once`](Self::pump_once) with other work must also call this from
248    /// their own timer. In particular, ping and PFS request timeouts must fire
249    /// even when the remote peer sends no response at all.
250    pub async fn service_protocol_timeouts(&mut self) {
251        let now_ms = self.mac.now_ms().await;
252        service_node_timeouts(&self.nodes, now_ms).await;
253    }
254
255    /// Detach an owned handle that services the same node-layer deadlines as
256    /// [`service_protocol_timeouts`](Self::service_protocol_timeouts).
257    ///
258    /// Use this when the host pump runs as its own long-lived future (so the
259    /// `Host` is exclusively borrowed) and a sibling timer task must still
260    /// fire ping/PFS timeouts — e.g. while the pump is parked awaiting a slow
261    /// physical transmit. The handle snapshots the current node set; nodes
262    /// added afterwards are not covered by it.
263    pub fn protocol_timeout_servicer(&self) -> ProtocolTimeoutServicer<M>
264    where
265        M: Clone,
266    {
267        ProtocolTimeoutServicer {
268            mac: self.mac.clone(),
269            nodes: self.nodes.clone(),
270        }
271    }
272
273    /// Run the shared MAC/Host loop forever.
274    ///
275    /// This is the preferred long-lived driver for node-based applications. It keeps the
276    /// runtime wake policy inside the MAC/Host stack rather than requiring callers to write
277    /// poll/sleep loops themselves.
278    pub async fn run(&mut self) -> Result<(), HostError<M::RunError>> {
279        loop {
280            self.pump_once().await?;
281        }
282    }
283
284    async fn handle_pfs_command(
285        &mut self,
286        identity_id: LocalIdentityId,
287        from: umsh_core::PublicKey,
288        channel: Option<umsh_core::ChannelId>,
289        command: OwnedMacCommand,
290    ) {
291        let Some(node) = self.route_node(identity_id) else {
292            return;
293        };
294
295        match node
296            .handle_pfs_command(&from, channel, &command, &self.pfs_control_options)
297            .await
298        {
299            Ok(Some(PfsLifecycle::Established(peer))) => node.dispatch_pfs_established(peer),
300            Ok(Some(PfsLifecycle::Ended(peer))) => node.dispatch_pfs_ended(peer),
301            Ok(None) => {}
302            // Previously the error was dropped here, so a failed negotiation
303            // (e.g. no ephemeral identity slot) left both sides silently stuck.
304            // Surface it so applications can report the failure.
305            Err(err) => node.dispatch_pfs_failed(from, err.pfs_failure()),
306        }
307    }
308}
309
310/// Owned deadline-service handle detached from a [`Host`]; see
311/// [`Host::protocol_timeout_servicer`].
312pub struct ProtocolTimeoutServicer<M: MacBackend> {
313    mac: M,
314    nodes: Vec<(LocalIdentityId, LocalNode<M>)>,
315}
316
317impl<M: MacBackend> ProtocolTimeoutServicer<M> {
318    /// Fire any elapsed ping and PFS deadlines on the snapshot's nodes.
319    pub async fn service(&self) {
320        let now_ms = self.mac.now_ms().await;
321        service_node_timeouts(&self.nodes, now_ms).await;
322    }
323}
324
325async fn service_node_timeouts<M: MacBackend>(
326    nodes: &[(LocalIdentityId, LocalNode<M>)],
327    now_ms: u64,
328) {
329    #[cfg(feature = "software-crypto")]
330    for (_, node) in nodes {
331        if let Ok(expired) = node.expire_pfs_sessions().await {
332            for peer in expired {
333                node.dispatch_pfs_ended(peer);
334            }
335        }
336        // Abandon sent PFS requests that were never answered, so the
337        // requester reports a timeout instead of sitting in `Requested`.
338        for peer in node.expire_pfs_requests(now_ms) {
339            node.dispatch_pfs_failed(peer, crate::node::PfsFailure::Timeout);
340        }
341    }
342
343    for (_, node) in nodes {
344        node.expire_pings(now_ms);
345    }
346}
347
348fn route_node<M: MacBackend>(
349    nodes: &[(LocalIdentityId, LocalNode<M>)],
350    identity_id: LocalIdentityId,
351) -> Option<LocalNode<M>> {
352    nodes
353        .iter()
354        .find(|(id, _)| *id == identity_id)
355        .map(|(_, node)| node.clone())
356        .or_else(|| {
357            nodes
358                .iter()
359                .find(|(_, node)| node.owns_ephemeral_identity(identity_id))
360                .map(|(_, node)| node.clone())
361        })
362}
363
364fn dispatch_payload_callbacks<M: MacBackend>(
365    node: &LocalNode<M>,
366    packet: &ReceivedPacketRef<'_>,
367    from: umsh_core::PublicKey,
368    pending_pfs: &Rc<
369        RefCell<
370            Vec<(
371                LocalIdentityId,
372                umsh_core::PublicKey,
373                Option<umsh_core::ChannelId>,
374                OwnedMacCommand,
375            )>,
376        >,
377    >,
378    pending_identity: &Rc<RefCell<Vec<(LocalNode<M>, IdentityResponsePlan)>>>,
379    pending_peer_repeaters: &Rc<
380        RefCell<
381            Vec<(
382                LocalNode<M>,
383                umsh_core::PublicKey,
384                Option<umsh_core::ChannelId>,
385                Vec<u8>,
386            )>,
387        >,
388    >,
389    now_ms: u64,
390) {
391    if packet.payload_type() == PayloadType::NodeIdentity {
392        if let Ok(identity) = NodeIdentityPayload::from_bytes(packet.payload()) {
393            // Recorded before the callback so an observer that asks for the
394            // peer-repeater listing from inside it sees this identity in it.
395            node.observe_peer_identity(from, &identity, now_ms);
396            node.dispatch_node_discovered(from, identity.name.as_deref());
397        }
398        return;
399    }
400
401    if packet.payload_type() == PayloadType::MacCommand {
402        if let Ok(command) = mac_command::parse(packet.payload()) {
403            // A MAC command is addressed to one node. A command that arrives by
404            // multicast or broadcast is ignored unless its own definition gives
405            // rules for that carriage (mac-commands.md), and today the Identity
406            // Request is the only one that does. Anything else is dropped here,
407            // before observers see it.
408            let addressed_to_one_node = matches!(
409                packet.packet_family(),
410                umsh_mac::PacketFamily::Unicast | umsh_mac::PacketFamily::BlindUnicast
411            );
412            if !addressed_to_one_node
413                && !matches!(command, mac_command::MacCommand::IdentityRequest { .. })
414            {
415                return;
416            }
417            // The carriage this request arrived on, which is the carriage its
418            // response owes it back. `Some` only for a blind unicast: a plain
419            // unicast is answered in kind, and the identity responder decides
420            // for itself how to answer a multicast or broadcast solicitation.
421            let reply_channel =
422                matches!(packet.packet_family(), umsh_mac::PacketFamily::BlindUnicast)
423                    .then(|| packet.channel().map(|c| c.id()))
424                    .flatten();
425            // Match borrowed variants before converting to owned.
426            if let mac_command::MacCommand::EchoResponse { data } = command {
427                node.match_pong(
428                    from,
429                    data,
430                    packet,
431                    packet.received_at_ms().unwrap_or(now_ms),
432                );
433            }
434            // Identity Request: let the built-in responder (if enabled) build a
435            // reply plan now, while the reception context is live; the async
436            // pump sends it after this synchronous dispatch returns.
437            if let mac_command::MacCommand::IdentityRequest { options } = command {
438                if let Some(plan) = node.evaluate_identity_request(
439                    packet,
440                    from,
441                    options,
442                    packet.received_at_ms().unwrap_or(now_ms),
443                ) {
444                    pending_identity.borrow_mut().push((node.clone(), plan));
445                }
446            }
447            // Peer Repeaters Request: the answer needs the MAC's transmitter
448            // observations, which only an async borrow reaches, so it is
449            // built by the pump after this synchronous dispatch returns.
450            if let mac_command::MacCommand::PeerRepeatersRequest { options } = command
451                && node.peer_repeaters_responder_enabled()
452            {
453                pending_peer_repeaters.borrow_mut().push((
454                    node.clone(),
455                    from,
456                    reply_channel,
457                    Vec::from(options),
458                ));
459            }
460            let owned = OwnedMacCommand::from(command);
461            node.dispatch_mac_command(from, &owned);
462            if matches!(
463                owned,
464                OwnedMacCommand::PfsSessionRequest { .. }
465                    | OwnedMacCommand::PfsSessionResponse { .. }
466                    | OwnedMacCommand::EndPfsSession
467            ) {
468                pending_pfs
469                    .borrow_mut()
470                    .push((node.identity_id(), from, reply_channel, owned));
471            }
472        }
473    }
474}