Skip to main content

pamoja_lorawan/
header.rs

1//! Reading a frame far enough to route it, before any key is involved.
2//!
3//! [`Session::decode`](crate::Session::decode) needs the session a frame belongs to, but a
4//! gateway or network server holds many sessions and has to work out which one a frame is
5//! for. That answer is in the header, which travels in the clear: the device address, the
6//! frame counter, and what kind of message it is. [`FrameHeader::parse`] reads exactly that
7//! much and nothing more, so a receiver can look the session up and then decode.
8//!
9//! Nothing here is authenticated. The MIC covers the header, but checking it needs the
10//! session key, so treat everything this reports as a routing hint until
11//! [`Session::decode`](crate::Session::decode) has verified the frame.
12
13use crate::error::LorawanError;
14use crate::frame::{
15    Direction, MTYPE_CONFIRMED_DOWN, MTYPE_CONFIRMED_UP, MTYPE_JOIN_ACCEPT, MTYPE_JOIN_REQUEST,
16    MTYPE_MASK, MTYPE_UNCONFIRMED_DOWN, MTYPE_UNCONFIRMED_UP,
17};
18
19// A data frame's fixed header: MHDR, DevAddr, FCtrl, and FCnt, before any frame options.
20const FHDR_LEN: usize = 8;
21// The shortest data frame is that header plus its MIC.
22const MIN_DATA_FRAME: usize = FHDR_LEN + 4;
23
24// The FCtrl bits, as the spec lays them out.
25const FCTRL_ADR: u8 = 0x80;
26const FCTRL_ACK: u8 = 0x20;
27const FCTRL_FPENDING: u8 = 0x10;
28const FCTRL_FOPTS_LEN: u8 = 0x0F;
29
30/// What kind of message a frame is, read from its header.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum MessageType {
33    /// A device asking to join a network.
34    JoinRequest,
35    /// A network admitting a device.
36    JoinAccept,
37    /// Data from a device that does not need acknowledging.
38    UnconfirmedUp,
39    /// Data from a device that asks to be acknowledged.
40    ConfirmedUp,
41    /// Data to a device that does not need acknowledging.
42    UnconfirmedDown,
43    /// Data to a device that asks to be acknowledged.
44    ConfirmedDown,
45}
46
47impl MessageType {
48    /// Reports whether this is a data frame rather than part of a join exchange.
49    ///
50    /// # Returns
51    ///
52    /// `true` for the four data types, which are the ones carrying a device address.
53    pub fn is_data(self) -> bool {
54        !matches!(self, MessageType::JoinRequest | MessageType::JoinAccept)
55    }
56
57    /// Returns the direction this message type travels.
58    ///
59    /// # Returns
60    ///
61    /// The direction, or [`None`] for a join-accept, which the spec secures as a
62    /// downlink but which carries no data-frame direction bit.
63    pub fn direction(self) -> Option<Direction> {
64        match self {
65            MessageType::JoinRequest | MessageType::UnconfirmedUp | MessageType::ConfirmedUp => {
66                Some(Direction::Uplink)
67            }
68            MessageType::UnconfirmedDown | MessageType::ConfirmedDown => Some(Direction::Downlink),
69            MessageType::JoinAccept => None,
70        }
71    }
72}
73
74/// A frame read only as far as its unencrypted header.
75///
76/// # Examples
77///
78/// ```
79/// use pamoja_lorawan::{FrameHeader, MessageType, Session, Uplink};
80///
81/// let session = Session::new(0x2601_1BDA, [0x2B; 16], [0x99; 16]);
82/// let frame = session.encode_uplink(&Uplink::new(42, 1, b"temp=4.8"))?;
83///
84/// // A gateway reads the address out of the frame to find the right session.
85/// let header = FrameHeader::parse(frame.as_bytes())?;
86/// assert_eq!(header.message_type(), MessageType::UnconfirmedUp);
87/// assert_eq!(header.dev_addr(), Some(0x2601_1BDA));
88/// assert_eq!(header.fcnt(), Some(42));
89/// assert_eq!(header.fport(), Some(1));
90///
91/// // Only then can it verify and decrypt.
92/// let rx = session.decode(frame.as_bytes(), 42)?;
93/// assert_eq!(rx.payload(), b"temp=4.8");
94/// # Ok::<(), pamoja_lorawan::LorawanError>(())
95/// ```
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub struct FrameHeader {
98    message_type: MessageType,
99    dev_addr: Option<u32>,
100    fcnt: Option<u16>,
101    fport: Option<u8>,
102    adr: bool,
103    ack: bool,
104    fpending: bool,
105    fopts_len: usize,
106    payload_len: usize,
107}
108
109impl FrameHeader {
110    /// Reads a frame far enough to route it, without any key.
111    ///
112    /// # Arguments
113    ///
114    /// * `bytes` - the raw frame as it came off the radio.
115    ///
116    /// # Returns
117    ///
118    /// What the header says the frame is.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`LorawanError::FrameTooShort`] if the frame is empty or shorter than the
123    /// header and MIC a data frame needs, [`LorawanError::UnsupportedMType`] if the message
124    /// type is one this crate does not read, or [`LorawanError::MalformedFrame`] if the
125    /// frame options run past the end of the frame.
126    pub fn parse(bytes: &[u8]) -> Result<FrameHeader, LorawanError> {
127        if bytes.is_empty() {
128            return Err(LorawanError::FrameTooShort);
129        }
130
131        let message_type = match bytes[0] & MTYPE_MASK {
132            MTYPE_JOIN_REQUEST => MessageType::JoinRequest,
133            MTYPE_JOIN_ACCEPT => MessageType::JoinAccept,
134            MTYPE_UNCONFIRMED_UP => MessageType::UnconfirmedUp,
135            MTYPE_CONFIRMED_UP => MessageType::ConfirmedUp,
136            MTYPE_UNCONFIRMED_DOWN => MessageType::UnconfirmedDown,
137            MTYPE_CONFIRMED_DOWN => MessageType::ConfirmedDown,
138            other => return Err(LorawanError::UnsupportedMType(other)),
139        };
140
141        // A join frame is opaque without the root key, so its type is all there is to read.
142        if !message_type.is_data() {
143            return Ok(FrameHeader {
144                message_type,
145                dev_addr: None,
146                fcnt: None,
147                fport: None,
148                adr: false,
149                ack: false,
150                fpending: false,
151                fopts_len: 0,
152                payload_len: 0,
153            });
154        }
155
156        if bytes.len() < MIN_DATA_FRAME {
157            return Err(LorawanError::FrameTooShort);
158        }
159
160        let fctrl = bytes[5];
161        let fopts_len = usize::from(fctrl & FCTRL_FOPTS_LEN);
162        let after_fopts = FHDR_LEN + fopts_len;
163        if bytes.len() < after_fopts + 4 {
164            return Err(LorawanError::MalformedFrame);
165        }
166
167        // A port is present only when something follows the frame options.
168        let remaining = bytes.len() - after_fopts - 4;
169        let (fport, payload_len) = if remaining == 0 {
170            (None, 0)
171        } else {
172            (Some(bytes[after_fopts]), remaining - 1)
173        };
174
175        Ok(FrameHeader {
176            message_type,
177            dev_addr: Some(u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]])),
178            fcnt: Some(u16::from_le_bytes([bytes[6], bytes[7]])),
179            fport,
180            adr: fctrl & FCTRL_ADR != 0,
181            ack: fctrl & FCTRL_ACK != 0,
182            fpending: fctrl & FCTRL_FPENDING != 0,
183            fopts_len,
184            payload_len,
185        })
186    }
187
188    /// Returns what kind of message the frame is.
189    ///
190    /// # Returns
191    ///
192    /// The message type.
193    pub fn message_type(&self) -> MessageType {
194        self.message_type
195    }
196
197    /// Returns the direction the frame travels.
198    ///
199    /// # Returns
200    ///
201    /// The direction, or [`None`] for a join-accept.
202    pub fn direction(&self) -> Option<Direction> {
203        self.message_type.direction()
204    }
205
206    /// Returns the device address the frame carries.
207    ///
208    /// This is what a receiver looks a session up by.
209    ///
210    /// # Returns
211    ///
212    /// The address, or [`None`] for a join frame, which carries none.
213    pub fn dev_addr(&self) -> Option<u32> {
214        self.dev_addr
215    }
216
217    /// Returns the low 16 bits of the frame counter.
218    ///
219    /// # Returns
220    ///
221    /// The counter, or [`None`] for a join frame.
222    pub fn fcnt(&self) -> Option<u16> {
223        self.fcnt
224    }
225
226    /// Returns the port the frame was sent on.
227    ///
228    /// # Returns
229    ///
230    /// The port, or [`None`] for a join frame or a data frame carrying only frame
231    /// options.
232    pub fn fport(&self) -> Option<u8> {
233        self.fport
234    }
235
236    /// Reports whether the frame asks to be acknowledged.
237    ///
238    /// # Returns
239    ///
240    /// `true` for a confirmed data frame.
241    pub fn confirmed(&self) -> bool {
242        matches!(
243            self.message_type,
244            MessageType::ConfirmedUp | MessageType::ConfirmedDown
245        )
246    }
247
248    /// Reports whether the frame takes part in adaptive data rate.
249    ///
250    /// # Returns
251    ///
252    /// The ADR bit.
253    pub fn adr(&self) -> bool {
254        self.adr
255    }
256
257    /// Reports whether the frame acknowledges the last confirmed one.
258    ///
259    /// # Returns
260    ///
261    /// The ACK bit.
262    pub fn ack(&self) -> bool {
263        self.ack
264    }
265
266    /// Reports whether the network has more downlink data waiting.
267    ///
268    /// # Returns
269    ///
270    /// The frame-pending bit.
271    pub fn fpending(&self) -> bool {
272        self.fpending
273    }
274
275    /// Returns how many bytes of frame options the header carries.
276    ///
277    /// # Returns
278    ///
279    /// The length, from 0 to 15.
280    pub fn fopts_len(&self) -> usize {
281        self.fopts_len
282    }
283
284    /// Returns the length of the still-encrypted payload.
285    ///
286    /// # Returns
287    ///
288    /// The payload length in bytes, which is 0 when the frame carries only options.
289    pub fn payload_len(&self) -> usize {
290        self.payload_len
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::{Device, Downlink, JoinGrant, Session, Uplink};
298
299    const NWK_SKEY: [u8; 16] = [0x2B; 16];
300    const APP_SKEY: [u8; 16] = [0x99; 16];
301    const DEV_ADDR: u32 = 0x2601_1BDA;
302
303    fn session() -> Session {
304        Session::new(DEV_ADDR, NWK_SKEY, APP_SKEY)
305    }
306
307    #[test]
308    fn an_uplink_reports_the_address_a_receiver_routes_by() {
309        let frame = session()
310            .encode_uplink(&Uplink::new(42, 1, b"temp=4.8").confirmed().with_adr())
311            .unwrap();
312        let header = FrameHeader::parse(frame.as_bytes()).unwrap();
313
314        assert_eq!(header.message_type(), MessageType::ConfirmedUp);
315        assert_eq!(header.direction(), Some(Direction::Uplink));
316        assert_eq!(header.dev_addr(), Some(DEV_ADDR));
317        assert_eq!(header.fcnt(), Some(42));
318        assert_eq!(header.fport(), Some(1));
319        assert!(header.confirmed());
320        assert!(header.adr());
321        assert!(!header.ack());
322        assert_eq!(header.fopts_len(), 0);
323        assert_eq!(header.payload_len(), 8);
324    }
325
326    #[test]
327    fn a_downlink_reports_its_options_and_pending_flag() {
328        let fopts = [0x03u8, 0x50, 0x00];
329        let frame = session()
330            .encode_downlink(
331                &Downlink::new(7, 2, b"ack")
332                    .with_fpending()
333                    .with_fopts(&fopts),
334            )
335            .unwrap();
336        let header = FrameHeader::parse(frame.as_bytes()).unwrap();
337
338        assert_eq!(header.message_type(), MessageType::UnconfirmedDown);
339        assert_eq!(header.direction(), Some(Direction::Downlink));
340        assert!(header.fpending());
341        assert_eq!(header.fopts_len(), fopts.len());
342        assert_eq!(header.fport(), Some(2));
343        assert_eq!(header.payload_len(), 3);
344    }
345
346    #[test]
347    fn a_frame_carrying_only_options_has_no_port() {
348        let fopts = [0x02u8, 0x01];
349        let frame = session()
350            .encode_uplink(&Uplink::new(1, 0, b"").with_fopts(&fopts))
351            .unwrap();
352        let header = FrameHeader::parse(frame.as_bytes()).unwrap();
353
354        assert_eq!(header.fopts_len(), fopts.len());
355        assert_eq!(header.payload_len(), 0);
356    }
357
358    #[test]
359    fn the_join_frames_report_their_type_and_nothing_else() {
360        let device = Device::new([0x11; 8], [0x22; 8], [0xAB; 16]);
361        let request = FrameHeader::parse(device.join_request(0x1234).as_bytes()).unwrap();
362        assert_eq!(request.message_type(), MessageType::JoinRequest);
363        assert!(!request.message_type().is_data());
364        assert_eq!(request.dev_addr(), None);
365        assert_eq!(request.fcnt(), None);
366        assert_eq!(request.direction(), Some(Direction::Uplink));
367
368        let grant = JoinGrant::new(0x0003_0201, 0x0006_0504, DEV_ADDR);
369        let accept = FrameHeader::parse(grant.accept(&[0xAB; 16], 0x1234).as_bytes()).unwrap();
370        assert_eq!(accept.message_type(), MessageType::JoinAccept);
371        assert_eq!(
372            accept.direction(),
373            None,
374            "a join-accept carries no direction bit"
375        );
376    }
377
378    #[test]
379    fn the_header_a_gateway_reads_agrees_with_the_decoded_frame() {
380        let session = session();
381        let frame = session
382            .encode_uplink(&Uplink::new(9, 3, b"reading").with_ack())
383            .unwrap();
384        let header = FrameHeader::parse(frame.as_bytes()).unwrap();
385        let rx = session.decode(frame.as_bytes(), 9).unwrap();
386
387        assert_eq!(header.dev_addr(), Some(rx.dev_addr()));
388        assert_eq!(header.fcnt(), Some(rx.fcnt()));
389        assert_eq!(header.fport(), rx.fport());
390        assert_eq!(header.ack(), rx.ack());
391        assert_eq!(header.payload_len(), rx.payload().len());
392    }
393
394    #[test]
395    fn a_truncated_frame_is_refused() {
396        assert_eq!(FrameHeader::parse(&[]), Err(LorawanError::FrameTooShort));
397        assert_eq!(
398            FrameHeader::parse(&[0x40, 0x01, 0x02]),
399            Err(LorawanError::FrameTooShort)
400        );
401    }
402
403    #[test]
404    fn frame_options_running_past_the_end_are_malformed() {
405        // The FCtrl claims fifteen bytes of options a frame this short cannot hold.
406        let mut frame = [0u8; MIN_DATA_FRAME];
407        frame[0] = MTYPE_UNCONFIRMED_UP;
408        frame[5] = 0x0F;
409        assert_eq!(
410            FrameHeader::parse(&frame),
411            Err(LorawanError::MalformedFrame)
412        );
413    }
414
415    #[test]
416    fn a_message_type_this_crate_does_not_read_is_reported() {
417        // 0xC0 is the proprietary message type.
418        assert_eq!(
419            FrameHeader::parse(&[0xC0; 16]),
420            Err(LorawanError::UnsupportedMType(0xC0))
421        );
422    }
423}