umsh_ulcp_device/
net.rs

1//! What the session holds about the device's Wi-Fi station and its IP
2//! stack, and what it leaves to the platform.
3//!
4//! The split is the point. A station's *configuration* is protocol state:
5//! which network to join, how to get an address, which resolvers to use.
6//! It is written by a host, saved with the rest of the snapshot, and
7//! restored on the next boot, so the session owns it. A station's
8//! *behavior* is not: the credential the driver holds, the access points
9//! it can hear, the lease it was handed. Those live where the driver
10//! lives, and the session reaches them through effects.
11//!
12//! With the `wifi` feature, the session also owns the credential table
13//! and saves it in the same transaction as selection and enablement.
14//! Platforms without that feature retain the deferred table hooks.
15
16use umsh_ulcp::{ip, wifi};
17
18/// The Wi-Fi hardware a device has.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct WifiConfig {
21    /// Whether the device can join a network, or only hear them.
22    ///
23    /// A tracker that scans access points to place itself has a receiver
24    /// and no station: it advertises `CAP_WIFI_SCAN` alone, answers the
25    /// scan properties, and refuses the rest. Every device that can join
26    /// can also scan, which is why there is no separate switch for the
27    /// scan.
28    pub station: bool,
29    /// Post-reset value of `PROP_WIFI_ENABLED`.
30    ///
31    /// Off on anything battery powered, where a station is a larger
32    /// continuous load than the mesh radio it sits beside. A mains-powered
33    /// bridge whose whole job is the connection says otherwise.
34    pub default_enabled: bool,
35    /// Supported station requirements, as SecurityMode bits.
36    pub supported_modes: u16,
37    /// Maximum SAE password length this platform accepts.
38    pub max_sae_password: usize,
39    /// Whether the driver accepts a raw WPA/WPA2 PMK.
40    pub raw_keys: bool,
41    /// Whether arbitrary SSID bytes are supported, including embedded NULs.
42    pub binary_ssids: bool,
43}
44
45impl WifiConfig {
46    pub fn validate_network(
47        &self,
48        entry: &wifi::NetworkEntry<'_>,
49    ) -> Result<(), umsh_ulcp::Status> {
50        entry
51            .validate()
52            .map_err(|_| umsh_ulcp::Status::INVALID_ARGUMENT)?;
53        if self.supported_modes & entry.security.bit() == 0
54            || (entry.raw_key && !self.raw_keys)
55            || (!self.binary_ssids
56                && (entry.ssid.contains(&0) || core::str::from_utf8(entry.ssid).is_err()))
57            || (entry.security == wifi::SecurityMode::Wpa3
58                && entry.credential.len() > self.max_sae_password)
59        {
60            return Err(umsh_ulcp::Status::UNIMPLEMENTED);
61        }
62        Ok(())
63    }
64
65    /// A station that stays off until asked.
66    pub const STATION: Self = Self {
67        station: true,
68        default_enabled: false,
69        supported_modes: 0x1f,
70        max_sae_password: 128,
71        raw_keys: true,
72        binary_ssids: true,
73    };
74
75    /// A station that comes up on its own.
76    pub const ALWAYS_ON: Self = Self {
77        station: true,
78        default_enabled: true,
79        supported_modes: 0x1f,
80        max_sae_password: 128,
81        raw_keys: true,
82        binary_ssids: true,
83    };
84
85    /// A receiver that can hear networks and join none of them.
86    pub const SCAN_ONLY: Self = Self {
87        station: false,
88        default_enabled: false,
89        supported_modes: 0x1f,
90        max_sae_password: 128,
91        raw_keys: true,
92        binary_ssids: true,
93    };
94}
95
96/// The IP families a device runs.
97///
98/// Separate from [`WifiConfig`] because a stack is not a link: a device
99/// reached over Ethernet, or over a cellular modem, has these and no
100/// Wi-Fi at all. Nothing here says which link the addresses are on.
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub struct IpConfig {
103    pub v4: bool,
104    pub v6: bool,
105    /// Whether configured resolver overrides can actually be applied.
106    pub manual_dns: bool,
107}
108
109impl IpConfig {
110    /// Both families, which is what a stack written this decade runs.
111    pub const DUAL: Self = Self {
112        v4: true,
113        v6: true,
114        manual_dns: true,
115    };
116    pub const V4_ONLY: Self = Self {
117        v4: true,
118        v6: false,
119        manual_dns: true,
120    };
121}
122
123/// Longest `PROP_IP_DNS` this device accepts.
124///
125/// Three is what a network hands out and more than any operator
126/// configures by hand. The receiver rule makes this a drop-with-status
127/// rather than a truncation: a host that sends four is told `NOMEM`
128/// rather than left believing the fourth is in use.
129pub const MAX_RESOLVERS: usize = 3;
130
131/// The device's own choice of resolvers (`PROP_IP_DNS`).
132///
133/// Held as encoded items rather than as parsed addresses because the two
134/// families are different widths and a list may mix them, and because
135/// what a host wrote is exactly what it reads back.
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub struct Resolvers {
138    entries: [[u8; 16]; MAX_RESOLVERS],
139    widths: [u8; MAX_RESOLVERS],
140    len: usize,
141}
142
143impl Default for Resolvers {
144    fn default() -> Self {
145        Self {
146            entries: [[0; 16]; MAX_RESOLVERS],
147            widths: [0; MAX_RESOLVERS],
148            len: 0,
149        }
150    }
151}
152
153impl Resolvers {
154    pub fn is_empty(&self) -> bool {
155        self.len == 0
156    }
157
158    /// Each resolver, in the order it was written.
159    pub fn iter(&self) -> impl Iterator<Item = &[u8]> {
160        (0..self.len).map(|index| &self.entries[index][..self.widths[index] as usize])
161    }
162
163    /// Append one resolver.
164    ///
165    /// Used on its own by the snapshot decoder, which sees the list an
166    /// option at a time rather than as one value.
167    pub fn push(&mut self, resolver: &[u8]) -> Result<(), ResolverError> {
168        if !ip::resolver_is_valid(resolver) {
169            return Err(ResolverError::NotWellFormed);
170        }
171        if self.len == MAX_RESOLVERS {
172            return Err(ResolverError::TooMany);
173        }
174        self.entries[self.len][..resolver.len()].copy_from_slice(resolver);
175        self.widths[self.len] = resolver.len() as u8;
176        self.len += 1;
177        Ok(())
178    }
179
180    /// Replace the whole list, or refuse it whole.
181    ///
182    /// Whole-list because that is how a small set of resolvers is edited:
183    /// an operator adds one and removes another in the same breath, and a
184    /// device left holding half of that would be resolving through a
185    /// server nobody meant to keep. A candidate is built first so a
186    /// refusal leaves the old list exactly as it was.
187    pub fn replace(&mut self, items: &[u8]) -> Result<(), ResolverError> {
188        let mut next = Self::default();
189        for item in umsh_ulcp::items::prefixed_items(items) {
190            let item = item.map_err(|_| ResolverError::Malformed)?;
191            next.push(item)?;
192        }
193        *self = next;
194        Ok(())
195    }
196
197    /// The list as it goes on the wire, length-prefixed.
198    pub fn encode(&self, out: &mut [u8]) -> Option<usize> {
199        let mut len = 0;
200        for resolver in self.iter() {
201            len += umsh_ulcp::items::encode_prefixed_item(resolver, out.get_mut(len..)?).ok()?;
202        }
203        Some(len)
204    }
205}
206
207/// Why a resolver list was refused.
208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
209pub enum ResolverError {
210    /// The items do not parse.
211    Malformed,
212    /// An entry is not four or sixteen octets, or is an address nothing
213    /// answers on.
214    NotWellFormed,
215    /// More entries than this device holds.
216    TooMany,
217}
218
219/// The name of the stored network the station should use
220/// (`PROP_WIFI_NETWORK`).
221///
222/// Empty is a value and not an absence: it is a host saying "join
223/// nothing", which is different from a host that has said nothing at all.
224#[derive(Clone, Copy, Debug, PartialEq, Eq)]
225pub struct SelectedNetwork {
226    ssid: [u8; wifi::SSID_MAX_LEN],
227    len: usize,
228}
229
230impl Default for SelectedNetwork {
231    fn default() -> Self {
232        Self {
233            ssid: [0; wifi::SSID_MAX_LEN],
234            len: 0,
235        }
236    }
237}
238
239impl SelectedNetwork {
240    pub fn as_bytes(&self) -> &[u8] {
241        &self.ssid[..self.len]
242    }
243
244    pub fn is_empty(&self) -> bool {
245        self.len == 0
246    }
247
248    /// Take a host's choice, or say it is not an SSID.
249    pub fn set(&mut self, ssid: &[u8]) -> Result<(), ()> {
250        if ssid.len() > wifi::SSID_MAX_LEN {
251            return Err(());
252        }
253        self.ssid[..ssid.len()].copy_from_slice(ssid);
254        self.len = ssid.len();
255        Ok(())
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn limited_driver_rejects_unsupported_profiles_without_rejecting_unicode() {
265        let config = WifiConfig {
266            raw_keys: false,
267            binary_ssids: false,
268            max_sae_password: 63,
269            ..WifiConfig::STATION
270        };
271        let mut entry = wifi::NetworkEntry {
272            hidden: false,
273            raw_key: false,
274            security: wifi::SecurityMode::Wpa2,
275            ssid: "Café".as_bytes(),
276            credential: b"password",
277        };
278        assert_eq!(config.validate_network(&entry), Ok(()));
279        for ssid in [b"\xff".as_slice(), b"a\0b"] {
280            entry.ssid = ssid;
281            assert_eq!(
282                config.validate_network(&entry),
283                Err(umsh_ulcp::Status::UNIMPLEMENTED)
284            );
285        }
286        entry.ssid = b"network";
287        entry.raw_key = true;
288        entry.credential = &[0x42; 32];
289        assert_eq!(
290            config.validate_network(&entry),
291            Err(umsh_ulcp::Status::UNIMPLEMENTED)
292        );
293        entry.raw_key = false;
294        entry.security = wifi::SecurityMode::Wpa3;
295        entry.credential = &[b'x'; 64];
296        assert_eq!(
297            config.validate_network(&entry),
298            Err(umsh_ulcp::Status::UNIMPLEMENTED)
299        );
300    }
301
302    fn item(bytes: &[u8]) -> Vec<u8> {
303        let mut out = Vec::new();
304        out.push(bytes.len() as u8);
305        out.extend_from_slice(bytes);
306        out
307    }
308
309    #[test]
310    fn a_resolver_list_takes_both_families_and_refuses_a_fourth() {
311        let mut resolvers = Resolvers::default();
312        let mut items = Vec::new();
313        items.extend_from_slice(&item(&[192, 168, 1, 1]));
314        let mut v6 = [0u8; 16];
315        v6[0] = 0x20;
316        v6[1] = 0x01;
317        v6[15] = 1;
318        items.extend_from_slice(&item(&v6));
319        assert_eq!(resolvers.replace(&items), Ok(()));
320        assert_eq!(resolvers.iter().count(), 2);
321
322        let mut too_many = items.clone();
323        too_many.extend_from_slice(&item(&[9, 9, 9, 9]));
324        too_many.extend_from_slice(&item(&[1, 1, 1, 1]));
325        assert_eq!(resolvers.replace(&too_many), Err(ResolverError::TooMany));
326        // The refusal left the list it refused to replace alone.
327        assert_eq!(resolvers.iter().count(), 2);
328    }
329
330    #[test]
331    fn a_resolver_nothing_answers_on_is_refused() {
332        let mut resolvers = Resolvers::default();
333        assert_eq!(
334            resolvers.replace(&item(&[0, 0, 0, 0])),
335            Err(ResolverError::NotWellFormed)
336        );
337        // An IPv4 link-local address is a host that failed to get one,
338        // not a resolver.
339        assert_eq!(
340            resolvers.replace(&item(&[169, 254, 1, 1])),
341            Err(ResolverError::NotWellFormed)
342        );
343        assert_eq!(
344            resolvers.replace(&item(&[1, 2, 3])),
345            Err(ResolverError::NotWellFormed)
346        );
347    }
348
349    #[test]
350    fn an_empty_list_clears_and_round_trips() {
351        let mut resolvers = Resolvers::default();
352        assert_eq!(resolvers.replace(&item(&[9, 9, 9, 9])), Ok(()));
353        assert_eq!(resolvers.replace(&[]), Ok(()));
354        assert!(resolvers.is_empty());
355        let mut out = [0u8; 64];
356        assert_eq!(resolvers.encode(&mut out), Some(0));
357    }
358
359    #[test]
360    fn the_selected_network_holds_arbitrary_octets_and_the_empty_choice() {
361        let mut selected = SelectedNetwork::default();
362        assert!(selected.is_empty());
363        assert_eq!(selected.set(&[0xFF, 0x00, 0x80]), Ok(()));
364        assert_eq!(selected.as_bytes(), [0xFF, 0x00, 0x80]);
365        assert_eq!(selected.set(&[]), Ok(()));
366        assert!(selected.is_empty());
367        assert_eq!(selected.set(&[0x41; 33]), Err(()));
368    }
369}