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}