umsh_mac/
forward_id.rs

1//! Routing identity of a packet as seen by a forwarder.
2//!
3//! This is the identity that duplicate suppression and forwarding
4//! confirmation both key on, and it is intentionally not the
5//! destination's logical delivery identity:
6//!
7//! - delivery identity is governed by replay windows / frame counters at
8//!   the destination
9//! - routing identity must remain stable across repeater rewrites of
10//!   dynamic routing metadata
11//! - forwarding-confirmation identity matches routing identity so a node
12//!   can recognize "the same packet, forwarded onward"
13//!
14//! It lives outside the coordinator because it is an interop surface,
15//! not private state: any forwarder that participates in the same mesh —
16//! including an [internet bridge], whose two radios must agree on which
17//! frames are the same packet — has to compute it bit-for-bit
18//! identically.
19//!
20//! [internet bridge]: https://darconeous.github.io/umsh/docs/protocol/internet-bridging.html
21
22use umsh_core::{OptionNumber, PacketHeader, PacketType, ParsedOptions};
23
24use crate::cache::DupCacheKey;
25
26/// Routing identity of an already-parsed frame.
27///
28/// `None` when the frame carries a MIC this cache cannot key on (absent,
29/// or wider than the 16 bytes the key holds), or when its options do not
30/// parse.
31pub fn forwarding_dup_key_parsed(header: &PacketHeader, frame: &[u8]) -> Option<DupCacheKey> {
32    if !header.packet_type().is_secure() {
33        let hash = normalized_routable_hash32(header, frame);
34        // Acks carry the same hash under their own variant so the cache
35        // can age them out fast enough for deliberate re-acknowledgements
36        // (spec §Duplicate Acknowledgement Window) to be carried onward.
37        return Some(if header.packet_type() == PacketType::MacAck {
38            DupCacheKey::AckHash32(hash)
39        } else {
40            DupCacheKey::Hash32(hash)
41        });
42    }
43    let options = ParsedOptions::extract(frame, header.options_range.clone()).ok()?;
44    let mic = frame.get(header.mic_range.clone())?;
45    if mic.is_empty() || mic.len() > 16 {
46        return None;
47    }
48    let mut bytes = [0u8; 16];
49    bytes[..mic.len()].copy_from_slice(mic);
50    Some(DupCacheKey::Mic {
51        bytes,
52        len: mic.len() as u8,
53        route_retry: options.route_retry,
54    })
55}
56
57/// Routing identity of a frame, parsing its header first.
58///
59/// `None` when the frame does not parse, or for the reasons
60/// [`forwarding_dup_key_parsed`] gives.
61pub fn forwarding_dup_key(frame: &[u8]) -> Option<DupCacheKey> {
62    let header = PacketHeader::parse(frame).ok()?;
63    forwarding_dup_key_parsed(&header, frame)
64}
65
66/// FNV-1a over the fields a repeater may not rewrite, for packets that
67/// carry no MIC to key on.
68fn normalized_routable_hash32(header: &PacketHeader, frame: &[u8]) -> u32 {
69    let mut hash = 0x811C_9DC5u32;
70
71    hash_u8(&mut hash, header.packet_type() as u8);
72    hash_u8(&mut hash, header.fcf.full_source() as u8);
73
74    if !header.options_range.is_empty() {
75        for entry in umsh_core::iter_options(frame, header.options_range.clone()) {
76            let Ok((number, value)) = entry else {
77                continue;
78            };
79            let option = OptionNumber::from(number);
80            if option.is_dynamic() {
81                continue;
82            }
83            hash_u16(&mut hash, number);
84            hash_u16(&mut hash, value.len() as u16);
85            hash_bytes(&mut hash, value);
86        }
87    }
88
89    match header.packet_type() {
90        PacketType::Broadcast => {
91            match header.source {
92                umsh_core::SourceAddrRef::Hint(hint) => hash_bytes(&mut hash, &hint.0),
93                umsh_core::SourceAddrRef::FullKeyAt { offset } => {
94                    if let Some(key) = frame.get(offset..offset + 32) {
95                        hash_bytes(&mut hash, key);
96                    }
97                }
98                umsh_core::SourceAddrRef::Encrypted { offset, len } => {
99                    if let Some(src) = frame.get(offset..offset + len) {
100                        hash_bytes(&mut hash, src);
101                    }
102                }
103                umsh_core::SourceAddrRef::None => {}
104            }
105            if let Some(payload) = frame.get(header.body_range.clone()) {
106                hash_bytes(&mut hash, payload);
107            }
108        }
109        PacketType::MacAck => {
110            // The ack trailer (`ack_mic || ack_tag`) uniquely identifies
111            // the acknowledged exchange; the ack carries no other
112            // distinguishing fields.
113            if let Some(trailer) = frame.get(header.mic_range.clone()) {
114                hash_bytes(&mut hash, trailer);
115            }
116        }
117        _ => {
118            if let Some(bytes) = frame.get(header.body_range.clone()) {
119                hash_bytes(&mut hash, bytes);
120            }
121        }
122    }
123    hash
124}
125
126fn hash_u8(hash: &mut u32, value: u8) {
127    *hash ^= u32::from(value);
128    *hash = hash.wrapping_mul(0x0100_0193);
129}
130
131fn hash_u16(hash: &mut u32, value: u16) {
132    hash_bytes(hash, &value.to_be_bytes());
133}
134
135fn hash_bytes(hash: &mut u32, bytes: &[u8]) {
136    for byte in bytes {
137        hash_u8(hash, *byte);
138    }
139}