umsh_node/
peer.rs

1use alloc::boxed::Box;
2
3use umsh_core::{MicSize, PublicKey};
4use umsh_mac::SendOptions;
5
6/// MIC size callers should normally ask for on a ping.
7///
8/// A ping frame is otherwise nearly half MIC: with a 2-byte nonce the whole
9/// frame is 34 bytes, 16 of them the authenticator. Dropping to 8 takes a
10/// quarter off the airtime while leaving forgery resistance at 2^-64 — far
11/// out of reach at LoRa packet rates, and the echo payload is a random nonce
12/// and filler that is worth nothing to forge.
13///
14/// Not applied inside [`PeerConnection::ping`], which honours whatever it is
15/// given: a caller measuring how 16-byte-MIC traffic fares should ping with a
16/// 16-byte MIC.
17pub const PING_MIC_SIZE: MicSize = MicSize::Mic8;
18
19use crate::node::{LocalNode, NodeError, Subscription, SubscriptionHandle};
20use crate::receive::ReceivedPacketRef;
21use crate::ticket::SendProgressTicket;
22use crate::transport::Transport;
23
24/// Relationship with one remote peer, bound to a transport context.
25///
26/// Generic over `T: Transport` — works with `LocalNode` (unicast) or
27/// `BoundChannel` (blind unicast) identically.
28#[derive(Clone)]
29pub struct PeerConnection<T: Transport> {
30    transport: T,
31    peer: PublicKey,
32}
33
34impl<T: Transport> PeerConnection<T> {
35    /// Create a new peer connection.
36    pub(crate) fn new(transport: T, peer: PublicKey) -> Self {
37        Self { transport, peer }
38    }
39
40    /// The remote peer's public key.
41    pub fn peer(&self) -> &PublicKey {
42        &self.peer
43    }
44
45    /// Send a raw payload to this peer (delegates to `transport.send()`).
46    pub async fn send(
47        &self,
48        payload: &[u8],
49        options: &SendOptions,
50    ) -> Result<SendProgressTicket, T::Error> {
51        self.transport.send(&self.peer, payload, options).await
52    }
53}
54
55/// Everything a peer relationship needs from the node behind its transport.
56///
57/// Implemented once for both carriages: a peer reached through a channel gets
58/// the same pings, subscriptions, and identity requests as one reached by
59/// plain unicast, and each frame goes out over the transport it was built on.
60impl<M, T> PeerConnection<T>
61where
62    M: crate::mac::MacBackend,
63    T: Transport<Error = NodeError<M>> + crate::transport::NodeAccess<Backend = M>,
64{
65    fn add_receive_handler<F>(&self, handler: F) -> SubscriptionHandle
66    where
67        F: FnMut(&ReceivedPacketRef<'_>) -> bool + 'static,
68    {
69        self.transport
70            .local_node()
71            .state()
72            .borrow_mut()
73            .peer_subscriptions_mut(self.peer)
74            .receive_handlers
75            .insert(Box::new(handler))
76    }
77
78    pub fn on_receive<F>(&self, handler: F) -> Subscription
79    where
80        F: FnMut(&ReceivedPacketRef<'_>) -> bool + 'static,
81    {
82        let handle = self.add_receive_handler(handler);
83        let state = self.transport.local_node().state().clone();
84        let peer = self.peer;
85        Subscription::new(move || {
86            let mut state = state.borrow_mut();
87            let Some(entry) = state.find_peer_subscriptions_mut(peer) else {
88                return false;
89            };
90            entry.receive_handlers.remove(handle)
91        })
92    }
93
94    fn add_ack_received_handler<F>(&self, handler: F) -> SubscriptionHandle
95    where
96        F: FnMut(crate::SendToken) + 'static,
97    {
98        self.transport
99            .local_node()
100            .state()
101            .borrow_mut()
102            .peer_subscriptions_mut(self.peer)
103            .ack_received_handlers
104            .insert(Box::new(handler))
105    }
106
107    pub fn on_ack_received<F>(&self, handler: F) -> Subscription
108    where
109        F: FnMut(crate::SendToken) + 'static,
110    {
111        let handle = self.add_ack_received_handler(handler);
112        let state = self.transport.local_node().state().clone();
113        let peer = self.peer;
114        Subscription::new(move || {
115            let mut state = state.borrow_mut();
116            let Some(entry) = state.find_peer_subscriptions_mut(peer) else {
117                return false;
118            };
119            entry.ack_received_handlers.remove(handle)
120        })
121    }
122
123    fn add_ack_timeout_handler<F>(&self, handler: F) -> SubscriptionHandle
124    where
125        F: FnMut(crate::SendToken) + 'static,
126    {
127        self.transport
128            .local_node()
129            .state()
130            .borrow_mut()
131            .peer_subscriptions_mut(self.peer)
132            .ack_timeout_handlers
133            .insert(Box::new(handler))
134    }
135
136    pub fn on_ack_timeout<F>(&self, handler: F) -> Subscription
137    where
138        F: FnMut(crate::SendToken) + 'static,
139    {
140        let handle = self.add_ack_timeout_handler(handler);
141        let state = self.transport.local_node().state().clone();
142        let peer = self.peer;
143        Subscription::new(move || {
144            let mut state = state.borrow_mut();
145            let Some(entry) = state.find_peer_subscriptions_mut(peer) else {
146                return false;
147            };
148            entry.ack_timeout_handlers.remove(handle)
149        })
150    }
151
152    pub fn on_pfs_established<F>(&self, handler: F) -> Subscription
153    where
154        F: FnMut() + 'static,
155    {
156        let handle = self
157            .transport
158            .local_node()
159            .state()
160            .borrow_mut()
161            .peer_subscriptions_mut(self.peer)
162            .pfs_established_handlers
163            .insert(Box::new(handler));
164        let state = self.transport.local_node().state().clone();
165        let peer = self.peer;
166        Subscription::new(move || {
167            let mut state = state.borrow_mut();
168            let Some(entry) = state.find_peer_subscriptions_mut(peer) else {
169                return false;
170            };
171            entry.pfs_established_handlers.remove(handle)
172        })
173    }
174
175    pub fn on_pfs_ended<F>(&self, handler: F) -> Subscription
176    where
177        F: FnMut() + 'static,
178    {
179        let handle = self
180            .transport
181            .local_node()
182            .state()
183            .borrow_mut()
184            .peer_subscriptions_mut(self.peer)
185            .pfs_ended_handlers
186            .insert(Box::new(handler));
187        let state = self.transport.local_node().state().clone();
188        let peer = self.peer;
189        Subscription::new(move || {
190            let mut state = state.borrow_mut();
191            let Some(entry) = state.find_peer_subscriptions_mut(peer) else {
192                return false;
193            };
194            entry.pfs_ended_handlers.remove(handle)
195        })
196    }
197
198    pub fn on_pong<F>(&self, handler: F) -> Subscription
199    where
200        F: FnMut(u64) + 'static,
201    {
202        let handle = self
203            .transport
204            .local_node()
205            .state()
206            .borrow_mut()
207            .peer_subscriptions_mut(self.peer)
208            .pong_handlers
209            .insert(Box::new(handler));
210        let state = self.transport.local_node().state().clone();
211        let peer = self.peer;
212        Subscription::new(move || {
213            let mut state = state.borrow_mut();
214            let Some(entry) = state.find_peer_subscriptions_mut(peer) else {
215                return false;
216            };
217            entry.pong_handlers.remove(handle)
218        })
219    }
220
221    pub fn on_ping_timeout<F>(&self, handler: F) -> Subscription
222    where
223        F: FnMut() + 'static,
224    {
225        let handle = self
226            .transport
227            .local_node()
228            .state()
229            .borrow_mut()
230            .peer_subscriptions_mut(self.peer)
231            .ping_timeout_handlers
232            .insert(Box::new(handler));
233        let state = self.transport.local_node().state().clone();
234        let peer = self.peer;
235        Subscription::new(move || {
236            let mut state = state.borrow_mut();
237            let Some(entry) = state.find_peer_subscriptions_mut(peer) else {
238                return false;
239            };
240            entry.ping_timeout_handlers.remove(handle)
241        })
242    }
243
244    /// Send an echo request and record it as pending until the matching echo
245    /// response arrives or `timeout_ms` elapses.
246    ///
247    /// `options` is honoured as given, so a ping travels the same way the
248    /// traffic it is measuring would: same MIC size, same encryption, same
249    /// routing and region options. Only `ack_requested` is overridden — the
250    /// echo response *is* the acknowledgement, so asking for a MAC ack as
251    /// well would put a second frame on the air for nothing.
252    ///
253    /// `extra_bytes` pads the payload beyond the 2-byte nonce. Padding
254    /// matters for the same reason the MIC size does: packet error rate rises
255    /// with frame length, so a minimal ping reports a link as usable when a
256    /// full-size message would not get through. See [`PING_MIC_SIZE`] for the
257    /// MIC size callers should normally ask for.
258    ///
259    /// The padding is bounded by what one frame can carry, not by this call:
260    /// a ping too large to build fails with the MAC's own send error rather
261    /// than being quietly shortened into a measurement of a smaller frame.
262    pub async fn ping(
263        &self,
264        extra_bytes: usize,
265        options: &SendOptions,
266        timeout_ms: u64,
267    ) -> Result<crate::ticket::SendProgressTicket, NodeError<M>> {
268        // Generate a 2-byte nonce via the MAC RNG.
269        let mut nonce_bytes = [0u8; 2];
270        self.transport
271            .local_node()
272            .fill_random(&mut nonce_bytes)
273            .await;
274        let nonce = u16::from_be_bytes(nonce_bytes);
275
276        // Build data: [nonce_hi, nonce_lo, 0xA5, 0xA5, ...].
277        let total = 2 + extra_bytes;
278        let mut data = alloc::vec![0xA5u8; total];
279        data[0] = nonce_bytes[0];
280        data[1] = nonce_bytes[1];
281
282        // Encode the outbound payload: PayloadType::MacCommand byte followed by
283        // the encoded command body. The receiver's MAC dispatches on payload[0]
284        // and the EchoRequest auto-reply in the coordinator depends on this
285        // framing.
286        let cmd = crate::mac_command::MacCommand::EchoRequest { data: &data };
287        let mut buf = alloc::vec![0u8; total + 2];
288        buf[0] = umsh_core::PayloadType::MacCommand as u8;
289        let n = crate::mac_command::encode(&cmd, &mut buf[1..])?;
290        let n = n + 1;
291
292        // Record the pending ping BEFORE sending (avoid race if response is very fast).
293        let sent_at_ms = self.transport.local_node().now_ms().await;
294        self.transport.local_node().record_ping(
295            nonce,
296            self.peer,
297            sent_at_ms,
298            sent_at_ms + timeout_ms,
299        );
300
301        // The caller's options carry through unchanged: a ping that is
302        // secured, sized or routed differently from real traffic measures a
303        // link the real traffic will not use. Only the ack request is
304        // dropped, because the EchoResponse already serves as one.
305        let opts = options.clone().with_ack_requested(false);
306        self.send(&buf[..n], &opts).await
307    }
308
309    /// Solicit this peer's current identity by sending a targeted MAC
310    /// Identity Request (command 1). Because the request is a unicast to a
311    /// specific peer, no filter options are needed — filters exist only to
312    /// narrow a broadcast solicitation. A random NONCE is included so the
313    /// peer echoes it in its identity response, matching the responder's
314    /// correlation contract.
315    ///
316    /// The response arrives asynchronously as a `PayloadType::NodeIdentity`
317    /// frame on the normal receive path, not as the return value here.
318    pub async fn request_identity(
319        &self,
320        options: &SendOptions,
321    ) -> Result<SendProgressTicket, NodeError<M>> {
322        let mut nonce_bytes = [0u8; 4];
323        self.transport
324            .local_node()
325            .fill_random(&mut nonce_bytes)
326            .await;
327        let nonce = u32::from_be_bytes(nonce_bytes);
328
329        let opts_block = crate::mac_command::IdentityRequestBuilder::new()
330            .nonce(nonce)?
331            .build();
332        let cmd = crate::mac_command::MacCommand::IdentityRequest {
333            options: &opts_block,
334        };
335        let mut buf = [0u8; 128];
336        buf[0] = umsh_core::PayloadType::MacCommand as u8;
337        let n = crate::mac_command::encode(&cmd, &mut buf[1..])? + 1;
338
339        self.send(&buf[..n], options).await
340    }
341
342    /// Ask this peer for one page of its known peer repeaters (command 10).
343    ///
344    /// `nonce` is echoed in the response, which is what tells one page's
345    /// answer from a stale copy of an earlier one. `cursor` resumes an
346    /// enumeration from where a previous response said to; pass `None` for
347    /// the first page, and expect a Total on the answer to that one.
348    ///
349    /// The response arrives asynchronously as a MAC command on the normal
350    /// receive path, not as the return value here.
351    pub async fn request_peer_repeaters(
352        &self,
353        nonce: u16,
354        cursor: Option<&[u8]>,
355        options: &SendOptions,
356    ) -> Result<SendProgressTicket, NodeError<M>> {
357        let mut builder = crate::mac_command::PeerRepeatersRequestBuilder::new().nonce(nonce)?;
358        if let Some(cursor) = cursor {
359            builder = builder.cursor(cursor)?;
360        }
361        let opts_block = builder.build();
362        let cmd = crate::mac_command::MacCommand::PeerRepeatersRequest {
363            options: &opts_block,
364        };
365        let mut buf = [0u8; 128];
366        buf[0] = umsh_core::PayloadType::MacCommand as u8;
367        let n = crate::mac_command::encode(&cmd, &mut buf[1..])? + 1;
368
369        self.send(&buf[..n], options).await
370    }
371
372    /// The route the MAC has cached for this peer, if any.
373    pub async fn route(&self) -> Option<umsh_mac::CachedRoute> {
374        self.transport.local_node().peer_route(&self.peer).await
375    }
376
377    /// Forget this peer's cached route, returning whether one was held.
378    pub async fn clear_route(&self) -> bool {
379        self.transport
380            .local_node()
381            .clear_peer_route(&self.peer)
382            .await
383    }
384
385    /// Install a route learned before this MAC existed, returning whether
386    /// it could be installed.
387    pub async fn restore_route(&self, route: umsh_mac::CachedRoute) -> bool {
388        self.transport
389            .local_node()
390            .restore_peer_route(&self.peer, route)
391            .await
392    }
393}
394
395/// Forward secrecy is negotiated between two nodes, not inside a channel: the
396/// session it establishes replaces the pairwise keys a unicast is sealed with.
397/// These stay on the plain-unicast transport.
398impl<M: crate::mac::MacBackend> PeerConnection<LocalNode<M>> {
399    #[cfg(feature = "software-crypto")]
400    pub async fn request_pfs(
401        &self,
402        duration_minutes: u16,
403        options: &SendOptions,
404    ) -> Result<SendProgressTicket, NodeError<M>> {
405        self.transport
406            .request_pfs(&self.peer, duration_minutes, options)
407            .await
408    }
409
410    #[cfg(feature = "software-crypto")]
411    pub async fn end_pfs(&self, options: &SendOptions) -> Result<(), NodeError<M>> {
412        self.transport.end_pfs(&self.peer, options).await
413    }
414
415    #[cfg(feature = "software-crypto")]
416    pub async fn pfs_status(&self) -> Result<crate::node::PfsStatus, NodeError<M>> {
417        self.transport.pfs_status(&self.peer).await
418    }
419}