umsh_text/node_adapter/
inbound.rs

1//! Received frames to engine sources, with the channel display rules.
2
3use alloc::vec::Vec;
4
5use umsh_core::{ChannelKey, NodeHint, PayloadType, PublicKey, RouterHint};
6use umsh_node::{PacketFamily, ReceivedPacketRef};
7
8use crate::chat::{ChannelRegistry, TextSource};
9
10/// The protocol's two well-known channels, which carry rules of their own
11/// (`multicast-channels.md` §Default Channels).
12///
13/// Held by key, so a received frame is recognized by the key that
14/// authenticated it, with no derivation per frame.
15#[derive(Clone, Copy)]
16pub struct WellKnownChannels {
17    pub public: ChannelKey,
18    pub emergency: ChannelKey,
19}
20
21impl WellKnownChannels {
22    /// The canonical name of the `public` channel.
23    pub const PUBLIC_NAME: &str = "public";
24    /// The canonical name of the `EMERGENCY` channel, after case folding.
25    pub const EMERGENCY_NAME: &str = "emergency";
26
27    /// Derive both keys from their canonical names, rather than writing them
28    /// down as literals that could drift from the derivation every other
29    /// named channel goes through.
30    #[cfg(feature = "software-crypto")]
31    pub fn derive() -> Self {
32        let key = |name| {
33            *umsh_node::Channel::named(name)
34                .expect("well-known channel names are short ASCII")
35                .key()
36        };
37        Self {
38            public: key(Self::PUBLIC_NAME),
39            emergency: key(Self::EMERGENCY_NAME),
40        }
41    }
42
43    pub fn is_public(&self, key: &ChannelKey) -> bool {
44        key.0 == self.public.0
45    }
46
47    pub fn is_emergency(&self, key: &ChannelKey) -> bool {
48        key.0 == self.emergency.0
49    }
50
51    /// Whether `key` is one of the two.
52    pub fn contains(&self, key: &ChannelKey) -> bool {
53        self.is_public(key) || self.is_emergency(key)
54    }
55}
56
57/// Radio observations for a received frame.
58#[derive(Clone, Debug, Default, PartialEq, Eq)]
59pub struct RxSummary {
60    pub rssi_dbm: Option<i16>,
61    pub snr_centibels: Option<i16>,
62    pub lqi: Option<u8>,
63    /// Radio links the frame crossed to get here, counting the final one
64    /// into this device. Absent for a frame source-routed without a trace
65    /// route, whose hops nobody recorded.
66    pub hop_count: Option<u8>,
67    /// Intermediate-router hints in trace-route order: nearest us first,
68    /// which is usable directly as a source route back.
69    pub route_hints: Vec<RouterHint>,
70    pub source_authenticated: bool,
71    /// Seconds the frame spent queued on the radio before delivery; zero
72    /// for a live reception.
73    pub buffered_age_s: u32,
74}
75
76impl RxSummary {
77    pub fn from_packet(packet: &ReceivedPacketRef<'_>) -> Self {
78        Self {
79            rssi_dbm: packet.rssi(),
80            snr_centibels: packet.snr().map(|snr| snr.as_centibels()),
81            lqi: packet.lqi().map(|lqi| lqi.get()),
82            hop_count: packet.hop_count(),
83            route_hints: packet.trace_route_hints().collect(),
84            source_authenticated: packet.source_authenticated(),
85            buffered_age_s: packet.rx().buffered_age_s(),
86        }
87    }
88}
89
90/// A text frame copied out of a receive callback.
91///
92/// The callback runs inside the node's pump, where the chat facade is not
93/// at hand, so the channel is held by the key that authenticated the frame
94/// until [`InboundText::source`] resolves it against the facade's channels.
95#[derive(Clone)]
96pub struct InboundText {
97    origin: Origin,
98    /// The text payload, without the payload-type byte.
99    pub payload: Vec<u8>,
100    pub received_at_ms: Option<u64>,
101    pub rx: RxSummary,
102}
103
104#[derive(Clone, Copy)]
105enum Origin {
106    Direct {
107        peer: PublicKey,
108    },
109    ChannelGroup {
110        channel: ChannelKey,
111        hint: NodeHint,
112        full_key: Option<PublicKey>,
113    },
114    ChannelDirect {
115        channel: ChannelKey,
116        peer: PublicKey,
117    },
118}
119
120impl InboundText {
121    /// The key of the channel it came on, for a channel frame: one the node
122    /// holds, since the key authenticated the frame.
123    pub fn channel_key(&self) -> Option<ChannelKey> {
124        match self.origin {
125            Origin::Direct { .. } => None,
126            Origin::ChannelGroup { channel, .. } | Origin::ChannelDirect { channel, .. } => {
127                Some(channel)
128            }
129        }
130    }
131
132    /// Who sent it, with its channel named by tag. `None` when `channels`
133    /// does not hold the channel—one the session has left.
134    pub fn source(&self, channels: &ChannelRegistry) -> Option<TextSource> {
135        Some(match self.origin {
136            Origin::Direct { peer } => TextSource::Direct { peer },
137            Origin::ChannelGroup {
138                channel,
139                hint,
140                full_key,
141            } => TextSource::ChannelGroup {
142                channel: channels.tag_for(&channel)?,
143                hint,
144                full_key,
145            },
146            Origin::ChannelDirect { channel, peer } => TextSource::ChannelDirect {
147                channel: channels.tag_for(&channel)?,
148                peer,
149            },
150        })
151    }
152}
153
154/// What a received frame is to a chat session.
155#[derive(Clone)]
156pub enum Inbound {
157    /// A text message to feed the facade.
158    Text(InboundText),
159    /// Our own multicast, relayed back to us. It is the message we already
160    /// have, not a second one, but it is evidence: something received our
161    /// frame and forwarded it, the only reachability signal a multicast ever
162    /// produces.
163    Echo { hop_count: Option<u8> },
164    /// A text frame the channel rules say must not be shown.
165    Refused(Refusal),
166}
167
168/// Why a text frame was refused (`multicast-channels.md` §Default Channels).
169#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170pub enum Refusal {
171    /// `EMERGENCY` chat must be readable by every node in range.
172    EncryptedEmergency,
173    /// `EMERGENCY` chat must name its sender by full key.
174    AnonymousEmergency,
175    /// `public` chat is shown only with its sender's full key.
176    AnonymousPublic,
177}
178
179/// Classify a received frame for a chat session whose identity is
180/// `local_key`.
181///
182/// `None` for anything that is not a text frame a chat session holds: another
183/// payload type, a unicast or blind unicast without its sender's key, a
184/// multicast without a claimed hint, or another packet family.
185///
186/// The checks on the well-known channels live here, at the chat layer, rather
187/// than under the MAC: the requirements are about chat messages, and the MAC
188/// is deliberately incurious about what it carries. A frame that fails one is
189/// refused rather than shown unmarked—a message the reader would act on in an
190/// emergency must not arrive with its origin or its reach in question.
191/// `EMERGENCY` chat must also be signed, but the text-message format has no
192/// signature yet, so that requirement is neither sent nor checked.
193pub fn inbound_text(
194    packet: &ReceivedPacketRef<'_>,
195    local_key: &PublicKey,
196    well_known: &WellKnownChannels,
197) -> Option<Inbound> {
198    if packet.payload_type() != PayloadType::TextMessage {
199        return None;
200    }
201    let family = packet.packet_family();
202    // Every group send carries our full source address, so a repeater's copy
203    // arrives naming us. Scoped to multicast, because a directed frame naming
204    // us is not an echo: a unicast reaches a receive handler only when it also
205    // named a local identity as its destination, which makes it a message we
206    // addressed to ourselves. That belongs in the transcript like any other.
207    if family == PacketFamily::Multicast && packet.from_key() == Some(*local_key) {
208        return Some(Inbound::Echo {
209            hop_count: packet.hop_count(),
210        });
211    }
212    let channel = packet.channel().map(|channel| *channel.key());
213    if let Some(key) = &channel {
214        if well_known.is_emergency(key) {
215            if packet.encrypted() {
216                return Some(Inbound::Refused(Refusal::EncryptedEmergency));
217            }
218            if !packet.has_full_source() || packet.from_key().is_none() {
219                return Some(Inbound::Refused(Refusal::AnonymousEmergency));
220            }
221        } else if well_known.is_public(key)
222            && family == PacketFamily::Multicast
223            && !packet.has_full_source()
224        {
225            return Some(Inbound::Refused(Refusal::AnonymousPublic));
226        }
227    }
228    let origin = match (family, channel) {
229        (PacketFamily::Unicast, _) => Origin::Direct {
230            peer: packet.from_key()?,
231        },
232        // Membership is what the channel MIC authenticates; the hint is the
233        // only sender identity a multicast frame must carry.
234        (PacketFamily::Multicast, Some(channel)) => Origin::ChannelGroup {
235            channel,
236            hint: packet.from_hint()?,
237            full_key: packet.from_key(),
238        },
239        // Without a full key there is nobody to attribute the message to, and
240        // nobody to answer a repair request to.
241        (PacketFamily::BlindUnicast, Some(channel)) => Origin::ChannelDirect {
242            channel,
243            peer: packet.from_key()?,
244        },
245        _ => return None,
246    };
247    Some(Inbound::Text(InboundText {
248        origin,
249        payload: packet.payload().to_vec(),
250        received_at_ms: packet.received_at_ms(),
251        rx: RxSummary::from_packet(packet),
252    }))
253}