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