umsh_ulcp/
wifi.rs

1//! Wi-Fi codecs (`PROP_WIFI_*`).
2//!
3//! The structures of `docs/protocol/src/ulcp-wifi.md`: a known-network
4//! entry and its credential, a scan result, the station's link, and the
5//! access point's configuration, state, and clients.
6//!
7//! Every codec here is used by both sides, which is the point: a device
8//! that builds `PROP_WIFI_LINK` with the same writer a host parses it
9//! with cannot disagree with that host about the format.
10//!
11//! Decoding is structural and tolerant in the way
12//! `docs/protocol/src/ulcp-conformance.md` requires of receivers: it
13//! rejects what it cannot parse and accepts what it can, including
14//! trailing octets a later revision may append. Well-formedness, the
15//! rules a device answers `STATUS_INVALID_ARGUMENT` to, is
16//! [`NetworkEntry::validate`] and [`ApConfig::validate`], kept separate
17//! so that a sender can check itself before transmitting and a device
18//! can check the sender.
19//!
20//! Whole-table values concatenate items with a length prefix each; that
21//! framing is [`crate::items::prefixed_items`] and is not repeated here.
22
23/// Longest SSID, in octets. Not text: an SSID is 0 to 32 arbitrary
24/// octets, and a device compares it bytewise.
25pub const SSID_MAX_LEN: usize = 32;
26/// Length of a BSSID or a station MAC address.
27pub const MAC_LEN: usize = 6;
28/// Length of a raw pairwise master key.
29pub const PMK_LEN: usize = 32;
30/// Shortest WPA/WPA2 passphrase, per 802.11 Annex J.
31pub const PASSPHRASE_MIN_LEN: usize = 8;
32/// Longest WPA/WPA2 passphrase, per 802.11 Annex J.
33pub const PASSPHRASE_MAX_LEN: usize = 63;
34/// Longest SAE password this protocol admits. SAE itself sets no bound;
35/// this is where the more generous stacks stop.
36pub const SAE_PASSWORD_MAX_LEN: usize = 128;
37
38/// `FLAGS` bit 0: the network hides its SSID, so the device probes for
39/// it by name rather than waiting to hear it.
40pub const FLAG_HIDDEN: u8 = 1 << 0;
41/// `FLAGS` bit 1: the credential is a raw [`PMK_LEN`]-octet pairwise
42/// master key rather than a passphrase.
43pub const FLAG_RAW_KEY: u8 = 1 << 1;
44
45const FLAGS_RESERVED: u8 = !(FLAG_HIDDEN | FLAG_RAW_KEY);
46
47/// Largest encoded [`NetworkEntry`]: flags, mode, length, a full SSID,
48/// and the longest credential any mode defines.
49pub const NETWORK_ENTRY_MAX_LEN: usize = 3 + SSID_MAX_LEN + SAE_PASSWORD_MAX_LEN;
50/// Largest encoded [`ScanResult`].
51pub const SCAN_RESULT_MAX_LEN: usize = 11 + SSID_MAX_LEN;
52/// Largest encoded [`Link`]: state, reason, BSSID, and frequency.
53pub const LINK_MAX_LEN: usize = 10;
54/// Largest encoded [`ApConfig`].
55pub const AP_CONFIG_MAX_LEN: usize = 11 + SSID_MAX_LEN + SAE_PASSWORD_MAX_LEN;
56/// Encoded length of an [`ApClient`].
57pub const AP_CLIENT_LEN: usize = 11;
58
59/// Decode or well-formedness failure.
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum WifiError {
62    /// The input ended before the structure was complete, or a declared
63    /// length ran past it.
64    Truncated,
65    /// The value is structurally wrong: a reserved flag bit set, an
66    /// unknown enumeration code, a field of impossible length.
67    Malformed,
68    /// The value parses but is not something a device may be asked for:
69    /// an empty SSID, a credential of the wrong length or kind for its
70    /// mode, a mode with no credential form. This is the
71    /// `STATUS_INVALID_ARGUMENT` set.
72    NotWellFormed,
73    /// The output buffer cannot hold the encoded value.
74    BufferTooSmall,
75}
76
77/// A station's minimum security requirement, or an advertised/AP mode.
78/// Numeric codes are not a security ranking; use [`Self::permits`].
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum SecurityMode {
81    /// `WIFI_SEC_OPEN`: no security at all.
82    Open = 0,
83    /// `WIFI_SEC_OWE`: Enhanced Open. Encrypted against anyone
84    /// listening, authenticated against nobody, and the only
85    /// credential-free mode permitted on 6 GHz.
86    Owe = 1,
87    /// `WIFI_SEC_WPA2`: WPA2-Personal, covering the mixed networks that
88    /// negotiate CCMP.
89    Wpa2 = 2,
90    /// `WIFI_SEC_WPA3`: WPA3-Personal, SAE.
91    Wpa3 = 3,
92    /// `WIFI_SEC_WPA`: the TKIP-only remainder, which a device may
93    /// decline to join.
94    Wpa = 4,
95    /// `WIFI_SEC_WEP`: reportable, not joinable. No credential form is
96    /// defined.
97    Wep = 5,
98    /// `WIFI_SEC_WPA2_ENT`: reportable, not joinable here.
99    Wpa2Ent = 6,
100    /// `WIFI_SEC_WPA3_ENT`: reportable, not joinable here.
101    Wpa3Ent = 7,
102    /// `WIFI_SEC_WPA3_ENT_192`: reportable, not joinable here.
103    Wpa3Ent192 = 8,
104}
105
106impl SecurityMode {
107    /// Whether an authentication mode satisfies this station requirement
108    /// with the stored credential. A raw PMK cannot authenticate with SAE.
109    pub const fn permits(self, negotiated: Self, raw_key: bool) -> bool {
110        match (self, negotiated) {
111            (Self::Open, Self::Open | Self::Owe) | (Self::Owe, Self::Owe) => !raw_key,
112            (Self::Wpa, Self::Wpa | Self::Wpa2) | (Self::Wpa2, Self::Wpa2) => true,
113            (Self::Wpa | Self::Wpa2 | Self::Wpa3, Self::Wpa3) => !raw_key,
114            _ => false,
115        }
116    }
117
118    /// Strongest advertised mode supported by both the device and credential.
119    /// This preference order is explicit; wire codes are not a ranking.
120    pub fn strongest_compatible(
121        self,
122        advertised: u16,
123        supported: u16,
124        raw_key: bool,
125    ) -> Option<Self> {
126        [Self::Wpa3, Self::Wpa2, Self::Wpa, Self::Owe, Self::Open]
127            .into_iter()
128            .find(|mode| advertised & supported & mode.bit() != 0 && self.permits(*mode, raw_key))
129    }
130
131    /// The wire code for this mode.
132    pub const fn code(self) -> u8 {
133        self as u8
134    }
135
136    /// Strict conversion from a wire octet. Codes 9 through 15 are
137    /// reserved and 16 upward cannot be reported at all, since a scan
138    /// result carries these as a 16-bit set.
139    pub const fn from_code(code: u8) -> Option<Self> {
140        match code {
141            0 => Some(Self::Open),
142            1 => Some(Self::Owe),
143            2 => Some(Self::Wpa2),
144            3 => Some(Self::Wpa3),
145            4 => Some(Self::Wpa),
146            5 => Some(Self::Wep),
147            6 => Some(Self::Wpa2Ent),
148            7 => Some(Self::Wpa3Ent),
149            8 => Some(Self::Wpa3Ent192),
150            _ => None,
151        }
152    }
153
154    /// This mode's bit in a [`ScanResult::modes`] set.
155    pub const fn bit(self) -> u16 {
156        1 << (self as u16)
157    }
158
159    /// Whether a host can express a credential for this mode, and so
160    /// whether an entry naming it can be inserted at all. The four that
161    /// cannot are numbered so that a scan result can say what it heard.
162    pub const fn has_credential_form(self) -> bool {
163        matches!(
164            self,
165            Self::Open | Self::Owe | Self::Wpa2 | Self::Wpa3 | Self::Wpa
166        )
167    }
168
169    /// Whether this mode's credential is a passphrase from which the
170    /// device derives a pairwise master key, and so whether
171    /// [`FLAG_RAW_KEY`] may stand in for one. SAE derives its key
172    /// through the exchange rather than from the password, so a stored
173    /// key is not a substitute there.
174    pub const fn derives_key_from_passphrase(self) -> bool {
175        matches!(self, Self::Wpa2 | Self::Wpa)
176    }
177
178    /// Whether an access point may offer this mode.
179    pub const fn may_be_offered(self) -> bool {
180        matches!(self, Self::Open | Self::Owe | Self::Wpa2 | Self::Wpa3)
181    }
182
183    /// The credential length bounds for this mode, as a passphrase.
184    /// `None` when the mode takes no credential or defines no form.
185    pub const fn passphrase_len_bounds(self) -> Option<(usize, usize)> {
186        match self {
187            Self::Wpa2 | Self::Wpa => Some((PASSPHRASE_MIN_LEN, PASSPHRASE_MAX_LEN)),
188            Self::Wpa3 => Some((1, SAE_PASSWORD_MAX_LEN)),
189            _ => None,
190        }
191    }
192}
193
194/// Read a little-endian `u16` at `at`, or `None` past the end.
195const fn le16(bytes: &[u8], at: usize) -> Option<u16> {
196    if at + 2 > bytes.len() {
197        return None;
198    }
199    Some((bytes[at] as u16) | ((bytes[at + 1] as u16) << 8))
200}
201
202/// Whether these octets are a credential a host may hand over as a
203/// passphrase: UTF-8, and free of U+0000 since the wire carries no
204/// terminator and the device derives keys from exactly these octets.
205fn passphrase_is_expressible(bytes: &[u8]) -> bool {
206    !bytes.contains(&0) && core::str::from_utf8(bytes).is_ok()
207}
208
209/// Well-formedness of a credential against the mode it is offered for.
210fn credential_is_well_formed(security: SecurityMode, raw_key: bool, credential: &[u8]) -> bool {
211    if !security.has_credential_form() {
212        return false;
213    }
214    if raw_key {
215        return security.derives_key_from_passphrase() && credential.len() == PMK_LEN;
216    }
217    match security.passphrase_len_bounds() {
218        // Open and OWE carry no credential at all.
219        None => credential.is_empty(),
220        Some((min, max)) => {
221            credential.len() >= min
222                && credential.len() <= max
223                && passphrase_is_expressible(credential)
224        }
225    }
226}
227
228/// One entry of `PROP_WIFI_NETWORKS`: a network the device knows.
229///
230/// **Secret-bearing.** The reported form stops at the SSID, which is
231/// what [`Self::encode_reported`] writes and what [`Self::decode`]
232/// yields from a device's answer, with `credential` empty.
233#[derive(Clone, Copy, Debug, PartialEq, Eq)]
234pub struct NetworkEntry<'a> {
235    /// The network hides its SSID.
236    pub hidden: bool,
237    /// `credential` is a raw pairwise master key rather than a
238    /// passphrase.
239    pub raw_key: bool,
240    pub security: SecurityMode,
241    /// 1 to [`SSID_MAX_LEN`] octets. Never empty: an empty selection
242    /// means no selection, so an entry with no name could be stored and
243    /// never chosen.
244    pub ssid: &'a [u8],
245    /// Empty in the reported form, and for the modes that take none.
246    pub credential: &'a [u8],
247}
248
249impl<'a> NetworkEntry<'a> {
250    /// The flags octet for this entry.
251    pub const fn flags(&self) -> u8 {
252        let mut flags = 0;
253        if self.hidden {
254            flags |= FLAG_HIDDEN;
255        }
256        if self.raw_key {
257            flags |= FLAG_RAW_KEY;
258        }
259        flags
260    }
261
262    /// Whether this entry is one a device may be asked to store.
263    ///
264    /// Separate from encoding so that a host can check itself before
265    /// transmitting and a device can apply the same rule to what
266    /// arrives, answering `STATUS_INVALID_ARGUMENT` to anything this
267    /// rejects. A mode that is well-formed but beyond the hardware is a
268    /// different answer, `STATUS_UNIMPLEMENTED`, and is not decided
269    /// here.
270    pub fn validate(&self) -> Result<(), WifiError> {
271        if self.ssid.is_empty() || self.ssid.len() > SSID_MAX_LEN {
272            return Err(WifiError::NotWellFormed);
273        }
274        if !credential_is_well_formed(self.security, self.raw_key, self.credential) {
275            return Err(WifiError::NotWellFormed);
276        }
277        Ok(())
278    }
279
280    fn encode_inner(&self, out: &mut [u8], with_credential: bool) -> Result<usize, WifiError> {
281        let credential: &[u8] = if with_credential {
282            self.credential
283        } else {
284            &[]
285        };
286        let len = 3 + self.ssid.len() + credential.len();
287        if self.ssid.len() > SSID_MAX_LEN {
288            return Err(WifiError::NotWellFormed);
289        }
290        if out.len() < len {
291            return Err(WifiError::BufferTooSmall);
292        }
293        out[0] = self.flags();
294        out[1] = self.security.code();
295        out[2] = self.ssid.len() as u8;
296        out[3..3 + self.ssid.len()].copy_from_slice(self.ssid);
297        out[3 + self.ssid.len()..len].copy_from_slice(credential);
298        Ok(len)
299    }
300
301    /// Encode the item form, credential included.
302    pub fn encode(&self, out: &mut [u8]) -> Result<usize, WifiError> {
303        self.encode_inner(out, true)
304    }
305
306    /// Encode the reported form: the item through its SSID. No
307    /// `CMD_PROP_GET` and no notification ever carries a credential.
308    pub fn encode_reported(&self, out: &mut [u8]) -> Result<usize, WifiError> {
309        self.encode_inner(out, false)
310    }
311
312    /// Decode one item. The credential is whatever follows the SSID,
313    /// which is empty in a reported entry.
314    pub fn decode(bytes: &'a [u8]) -> Result<Self, WifiError> {
315        if bytes.len() < 3 {
316            return Err(WifiError::Truncated);
317        }
318        let flags = bytes[0];
319        if flags & FLAGS_RESERVED != 0 {
320            return Err(WifiError::Malformed);
321        }
322        let security = SecurityMode::from_code(bytes[1]).ok_or(WifiError::Malformed)?;
323        let ssid_len = bytes[2] as usize;
324        if ssid_len > SSID_MAX_LEN {
325            return Err(WifiError::Malformed);
326        }
327        if bytes.len() < 3 + ssid_len {
328            return Err(WifiError::Truncated);
329        }
330        Ok(Self {
331            hidden: flags & FLAG_HIDDEN != 0,
332            raw_key: flags & FLAG_RAW_KEY != 0,
333            security,
334            ssid: &bytes[3..3 + ssid_len],
335            credential: &bytes[3 + ssid_len..],
336        })
337    }
338}
339
340/// One entry of `PROP_WIFI_SCAN_RESULTS`: an access point the device
341/// heard.
342#[derive(Clone, Copy, Debug, PartialEq, Eq)]
343pub struct ScanResult<'a> {
344    /// The security modes this access point offers, as
345    /// [`SecurityMode::bit`] flags. **No bits set** means the device did
346    /// not determine them, which a receiver that reads beacon headers
347    /// for their addresses has no reason to do.
348    pub modes: u16,
349    /// Center frequency of the primary 20 MHz channel.
350    pub frequency_mhz: u16,
351    pub rssi_dbm: i8,
352    /// The key: one item per access point.
353    pub bssid: [u8; MAC_LEN],
354    /// Empty when no name was reported, whether hidden or unread.
355    pub ssid: &'a [u8],
356}
357
358impl<'a> ScanResult<'a> {
359    /// Whether the device determined the offered modes at all.
360    pub const fn modes_known(&self) -> bool {
361        self.modes != 0
362    }
363
364    /// Whether this access point offers `mode`.
365    pub const fn offers(&self, mode: SecurityMode) -> bool {
366        self.modes & mode.bit() != 0
367    }
368
369    pub fn encode(&self, out: &mut [u8]) -> Result<usize, WifiError> {
370        if self.ssid.len() > SSID_MAX_LEN {
371            return Err(WifiError::NotWellFormed);
372        }
373        let len = 11 + self.ssid.len();
374        if out.len() < len {
375            return Err(WifiError::BufferTooSmall);
376        }
377        out[0..2].copy_from_slice(&self.modes.to_le_bytes());
378        out[2..4].copy_from_slice(&self.frequency_mhz.to_le_bytes());
379        out[4] = self.rssi_dbm as u8;
380        out[5..11].copy_from_slice(&self.bssid);
381        out[11..len].copy_from_slice(self.ssid);
382        Ok(len)
383    }
384
385    pub fn decode(bytes: &'a [u8]) -> Result<Self, WifiError> {
386        if bytes.len() < 11 {
387            return Err(WifiError::Truncated);
388        }
389        let ssid = &bytes[11..];
390        if ssid.len() > SSID_MAX_LEN {
391            return Err(WifiError::Malformed);
392        }
393        let mut bssid = [0u8; MAC_LEN];
394        bssid.copy_from_slice(&bytes[5..11]);
395        Ok(Self {
396            modes: le16(bytes, 0).ok_or(WifiError::Truncated)?,
397            frequency_mhz: le16(bytes, 2).ok_or(WifiError::Truncated)?,
398            rssi_dbm: bytes[4] as i8,
399            bssid,
400            ssid,
401        })
402    }
403}
404
405/// `PROP_WIFI_LINK` states.
406#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
407pub enum LinkState {
408    /// `WIFI_LINK_DOWN`: not trying. The station is off, or nothing is
409    /// selected.
410    #[default]
411    Down = 0,
412    /// `WIFI_LINK_CONNECTING`: the whole of trying, including the waits
413    /// between attempts.
414    Connecting = 1,
415    /// `WIFI_LINK_UP`: authenticated and associated.
416    Up = 2,
417}
418
419impl LinkState {
420    pub const fn code(self) -> u8 {
421        self as u8
422    }
423
424    pub const fn from_code(code: u8) -> Option<Self> {
425        match code {
426            0 => Some(Self::Down),
427            1 => Some(Self::Connecting),
428            2 => Some(Self::Up),
429            _ => None,
430        }
431    }
432}
433
434/// Why the station is [`LinkState::Connecting`] rather than up.
435#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
436pub enum LinkReason {
437    /// `WIFI_REASON_NONE`: no attempt has failed yet. Also the value in
438    /// the other two states.
439    #[default]
440    None = 0,
441    /// `WIFI_REASON_NOT_FOUND`: not heard, and no answer to a probe.
442    NotFound = 1,
443    /// `WIFI_REASON_AUTH`: the network rejected the credential.
444    Auth = 2,
445    /// `WIFI_REASON_REJECTED`: the association was refused for another
446    /// reason.
447    Rejected = 3,
448    /// `WIFI_REASON_LOST`: the association was up and dropped.
449    Lost = 4,
450    /// `WIFI_REASON_OTHER`: something the device has no name for.
451    Other = 5,
452}
453
454impl LinkReason {
455    pub const fn code(self) -> u8 {
456        self as u8
457    }
458
459    pub const fn from_code(code: u8) -> Option<Self> {
460        match code {
461            0 => Some(Self::None),
462            1 => Some(Self::NotFound),
463            2 => Some(Self::Auth),
464            3 => Some(Self::Rejected),
465            4 => Some(Self::Lost),
466            5 => Some(Self::Other),
467            _ => None,
468        }
469    }
470}
471
472/// Which access point the station is on, present only while up.
473#[derive(Clone, Copy, Debug, PartialEq, Eq)]
474pub struct Association {
475    pub bssid: [u8; MAC_LEN],
476    /// Center frequency of the access point's primary channel.
477    pub frequency_mhz: u16,
478}
479
480/// `PROP_WIFI_LINK`.
481#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
482pub struct Link {
483    pub state: LinkState,
484    pub reason: LinkReason,
485    /// `Some` exactly when `state` is [`LinkState::Up`].
486    pub association: Option<Association>,
487}
488
489impl Link {
490    pub fn encode(&self, out: &mut [u8]) -> Result<usize, WifiError> {
491        let len = if self.association.is_some() { 10 } else { 2 };
492        if out.len() < len {
493            return Err(WifiError::BufferTooSmall);
494        }
495        out[0] = self.state.code();
496        out[1] = self.reason.code();
497        if let Some(association) = self.association {
498            out[2..8].copy_from_slice(&association.bssid);
499            out[8..10].copy_from_slice(&association.frequency_mhz.to_le_bytes());
500        }
501        Ok(len)
502    }
503
504    pub fn decode(bytes: &[u8]) -> Result<Self, WifiError> {
505        if bytes.len() < 2 {
506            return Err(WifiError::Truncated);
507        }
508        let state = LinkState::from_code(bytes[0]).ok_or(WifiError::Malformed)?;
509        let reason = LinkReason::from_code(bytes[1]).ok_or(WifiError::Malformed)?;
510        let association = if matches!(state, LinkState::Up) {
511            if bytes.len() < 10 {
512                return Err(WifiError::Truncated);
513            }
514            let mut bssid = [0u8; MAC_LEN];
515            bssid.copy_from_slice(&bytes[2..8]);
516            Some(Association {
517                bssid,
518                frequency_mhz: le16(bytes, 8).ok_or(WifiError::Truncated)?,
519            })
520        } else {
521            None
522        };
523        Ok(Self {
524            state,
525            reason,
526            association,
527        })
528    }
529}
530
531/// `PROP_WIFI_AP_STATE` states.
532#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
533pub enum ApState {
534    /// `WIFI_AP_DOWN`: disabled, unconfigured, or refused the radio.
535    #[default]
536    Down = 0,
537    /// `WIFI_AP_UP`: beaconing and accepting stations.
538    Up = 1,
539}
540
541impl ApState {
542    pub const fn code(self) -> u8 {
543        self as u8
544    }
545
546    pub const fn from_code(code: u8) -> Option<Self> {
547        match code {
548            0 => Some(Self::Down),
549            1 => Some(Self::Up),
550            _ => None,
551        }
552    }
553}
554
555/// `PROP_WIFI_AP_STATE`.
556#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
557pub struct ApStatus {
558    pub state: ApState,
559    /// Where the access point is actually beaconing, present exactly
560    /// when it is up. Not necessarily the configured frequency: where
561    /// one radio serves both, the access point follows the station.
562    pub frequency_mhz: Option<u16>,
563}
564
565impl ApStatus {
566    pub fn encode(&self, out: &mut [u8]) -> Result<usize, WifiError> {
567        let len = if self.frequency_mhz.is_some() { 3 } else { 1 };
568        if out.len() < len {
569            return Err(WifiError::BufferTooSmall);
570        }
571        out[0] = self.state.code();
572        if let Some(frequency) = self.frequency_mhz {
573            out[1..3].copy_from_slice(&frequency.to_le_bytes());
574        }
575        Ok(len)
576    }
577
578    pub fn decode(bytes: &[u8]) -> Result<Self, WifiError> {
579        let state = ApState::from_code(*bytes.first().ok_or(WifiError::Truncated)?)
580            .ok_or(WifiError::Malformed)?;
581        let frequency_mhz = if matches!(state, ApState::Up) {
582            Some(le16(bytes, 1).ok_or(WifiError::Truncated)?)
583        } else {
584            None
585        };
586        Ok(Self {
587            state,
588            frequency_mhz,
589        })
590    }
591}
592
593/// `PROP_WIFI_AP_CONFIG`: the network the device offers.
594///
595/// **Secret-bearing**, like [`NetworkEntry`]: the reported form stops at
596/// the SSID.
597#[derive(Clone, Copy, Debug, PartialEq, Eq)]
598pub struct ApConfig<'a> {
599    /// Beacon without the name. A courtesy to neighbors' pickers, not a
600    /// secret: the name is in every association.
601    pub hidden: bool,
602    pub security: SecurityMode,
603    /// Stations admitted at once, or 0 for the device's own limit. A
604    /// device clamps a larger value to what it can hold rather than
605    /// refusing it.
606    pub max_clients: u8,
607    /// Preferred primary channel frequency, or 0 for the device's
608    /// choice. A preference, not a promise.
609    pub frequency_mhz: u16,
610    /// The device's own address on the offered subnet, or all-zero with
611    /// `prefix` 0 for the device's default.
612    pub address: [u8; 4],
613    /// Prefix of the offered subnet, 8 to 30, or 0 with an all-zero
614    /// address.
615    pub prefix: u8,
616    pub ssid: &'a [u8],
617    pub credential: &'a [u8],
618}
619
620impl<'a> ApConfig<'a> {
621    /// The default subnet a device offers when `address` is all-zero:
622    /// what a phone joining an embedded device has come to expect.
623    pub const DEFAULT_ADDRESS: [u8; 4] = [192, 168, 4, 1];
624    /// Prefix that goes with [`Self::DEFAULT_ADDRESS`].
625    pub const DEFAULT_PREFIX: u8 = 24;
626
627    pub const fn flags(&self) -> u8 {
628        if self.hidden { FLAG_HIDDEN } else { 0 }
629    }
630
631    /// Whether the device's own address and prefix were left to it.
632    pub fn uses_default_subnet(&self) -> bool {
633        self.address == [0; 4] && self.prefix == 0
634    }
635
636    /// Whether this configuration is one a device may be asked to
637    /// offer. Excludes the modes an access point may not offer, and the
638    /// subnets that are not usable to hand addresses out on.
639    ///
640    /// Whether the frequency is one the device may transmit on is a
641    /// regulatory question this cannot answer, and belongs to the
642    /// device.
643    pub fn validate(&self) -> Result<(), WifiError> {
644        if self.ssid.is_empty() || self.ssid.len() > SSID_MAX_LEN {
645            return Err(WifiError::NotWellFormed);
646        }
647        if !self.security.may_be_offered() {
648            return Err(WifiError::NotWellFormed);
649        }
650        // An access point holds the key it hands out, so a raw key is
651        // not a form it accepts.
652        if !credential_is_well_formed(self.security, false, self.credential) {
653            return Err(WifiError::NotWellFormed);
654        }
655        if !self.uses_default_subnet() {
656            if !(8..=30).contains(&self.prefix) {
657                return Err(WifiError::NotWellFormed);
658            }
659            if !crate::ip::ipv4_is_usable_host(&self.address) {
660                return Err(WifiError::NotWellFormed);
661            }
662        }
663        Ok(())
664    }
665
666    fn encode_inner(&self, out: &mut [u8], with_credential: bool) -> Result<usize, WifiError> {
667        let credential: &[u8] = if with_credential {
668            self.credential
669        } else {
670            &[]
671        };
672        if self.ssid.len() > SSID_MAX_LEN {
673            return Err(WifiError::NotWellFormed);
674        }
675        let len = 11 + self.ssid.len() + credential.len();
676        if out.len() < len {
677            return Err(WifiError::BufferTooSmall);
678        }
679        out[0] = self.flags();
680        out[1] = self.security.code();
681        out[2] = self.max_clients;
682        out[3..5].copy_from_slice(&self.frequency_mhz.to_le_bytes());
683        out[5..9].copy_from_slice(&self.address);
684        out[9] = self.prefix;
685        out[10] = self.ssid.len() as u8;
686        out[11..11 + self.ssid.len()].copy_from_slice(self.ssid);
687        out[11 + self.ssid.len()..len].copy_from_slice(credential);
688        Ok(len)
689    }
690
691    /// Encode the value, credential included.
692    pub fn encode(&self, out: &mut [u8]) -> Result<usize, WifiError> {
693        self.encode_inner(out, true)
694    }
695
696    /// Encode the reported form: the value through its SSID.
697    pub fn encode_reported(&self, out: &mut [u8]) -> Result<usize, WifiError> {
698        self.encode_inner(out, false)
699    }
700
701    pub fn decode(bytes: &'a [u8]) -> Result<Self, WifiError> {
702        if bytes.len() < 11 {
703            return Err(WifiError::Truncated);
704        }
705        let flags = bytes[0];
706        if flags & !FLAG_HIDDEN != 0 {
707            return Err(WifiError::Malformed);
708        }
709        let security = SecurityMode::from_code(bytes[1]).ok_or(WifiError::Malformed)?;
710        let ssid_len = bytes[10] as usize;
711        if ssid_len > SSID_MAX_LEN {
712            return Err(WifiError::Malformed);
713        }
714        if bytes.len() < 11 + ssid_len {
715            return Err(WifiError::Truncated);
716        }
717        let mut address = [0u8; 4];
718        address.copy_from_slice(&bytes[5..9]);
719        Ok(Self {
720            hidden: flags & FLAG_HIDDEN != 0,
721            security,
722            max_clients: bytes[2],
723            frequency_mhz: le16(bytes, 3).ok_or(WifiError::Truncated)?,
724            address,
725            prefix: bytes[9],
726            ssid: &bytes[11..11 + ssid_len],
727            credential: &bytes[11 + ssid_len..],
728        })
729    }
730}
731
732/// One entry of `PROP_WIFI_AP_CLIENTS`: a station on the device's
733/// network.
734#[derive(Clone, Copy, Debug, PartialEq, Eq)]
735pub struct ApClient {
736    /// The key.
737    pub mac: [u8; MAC_LEN],
738    /// As the device last heard this client.
739    pub rssi_dbm: i8,
740    /// What the device's DHCP server leased it, all-zero until it has
741    /// one.
742    pub address: [u8; 4],
743}
744
745impl ApClient {
746    /// Whether the client has taken a lease yet.
747    pub fn has_address(&self) -> bool {
748        self.address != [0; 4]
749    }
750
751    pub fn encode(&self, out: &mut [u8]) -> Result<usize, WifiError> {
752        if out.len() < AP_CLIENT_LEN {
753            return Err(WifiError::BufferTooSmall);
754        }
755        out[0..6].copy_from_slice(&self.mac);
756        out[6] = self.rssi_dbm as u8;
757        out[7..11].copy_from_slice(&self.address);
758        Ok(AP_CLIENT_LEN)
759    }
760
761    pub fn decode(bytes: &[u8]) -> Result<Self, WifiError> {
762        if bytes.len() < AP_CLIENT_LEN {
763            return Err(WifiError::Truncated);
764        }
765        let mut mac = [0u8; MAC_LEN];
766        mac.copy_from_slice(&bytes[0..6]);
767        let mut address = [0u8; 4];
768        address.copy_from_slice(&bytes[7..11]);
769        Ok(Self {
770            mac,
771            rssi_dbm: bytes[6] as i8,
772            address,
773        })
774    }
775}
776
777#[cfg(test)]
778mod tests {
779    use super::*;
780
781    const SSID: &[u8] = b"umsh-test";
782
783    #[test]
784    fn minimum_security_is_a_credential_compatible_relation() {
785        use SecurityMode::*;
786        for minimum in [Open, Owe, Wpa, Wpa2, Wpa3] {
787            for offered in [
788                Open, Owe, Wpa, Wpa2, Wpa3, Wep, Wpa2Ent, Wpa3Ent, Wpa3Ent192,
789            ] {
790                let passphrase = matches!(
791                    (minimum, offered),
792                    (Open, Open | Owe)
793                        | (Owe, Owe)
794                        | (Wpa, Wpa | Wpa2 | Wpa3)
795                        | (Wpa2, Wpa2 | Wpa3)
796                        | (Wpa3, Wpa3)
797                );
798                assert_eq!(minimum.permits(offered, false), passphrase);
799                let pmk = matches!((minimum, offered), (Wpa, Wpa | Wpa2) | (Wpa2, Wpa2));
800                assert_eq!(minimum.permits(offered, true), pmk);
801            }
802        }
803    }
804
805    #[test]
806    fn strongest_selection_supports_legacy_wpa_without_lowering_stronger_profiles() {
807        use SecurityMode::*;
808        let supported = Open.bit() | Wpa.bit() | Wpa2.bit() | Wpa3.bit();
809        assert_eq!(
810            Wpa.strongest_compatible(Wpa.bit(), supported, false),
811            Some(Wpa)
812        );
813        assert_eq!(
814            Wpa.strongest_compatible(supported, supported, false),
815            Some(Wpa3)
816        );
817        assert_eq!(
818            Wpa.strongest_compatible(supported, supported, true),
819            Some(Wpa2)
820        );
821        assert_eq!(
822            Wpa.strongest_compatible(Wpa.bit(), supported, true),
823            Some(Wpa)
824        );
825        assert_eq!(Wpa2.strongest_compatible(Wpa.bit(), supported, false), None);
826        assert_eq!(
827            Wpa3.strongest_compatible(Wpa.bit() | Wpa2.bit(), supported, false),
828            None
829        );
830        assert_eq!(
831            Wpa2.strongest_compatible(Wpa3.bit(), supported, false),
832            Some(Wpa3)
833        );
834        assert_eq!(Wpa2.strongest_compatible(Wpa3.bit(), supported, true), None);
835    }
836
837    #[test]
838    fn security_codes_round_trip_strictly() {
839        for code in 0..=8u8 {
840            let mode = SecurityMode::from_code(code).expect("allocated");
841            assert_eq!(mode.code(), code);
842        }
843        // 9 through 15 are reserved; SAE-PK is the likeliest occupant.
844        assert_eq!(SecurityMode::from_code(9), None);
845        assert_eq!(SecurityMode::from_code(15), None);
846        assert_eq!(SecurityMode::from_code(16), None);
847    }
848
849    #[test]
850    fn every_mode_fits_the_sixteen_bit_scan_set() {
851        for code in 0..=8u8 {
852            let mode = SecurityMode::from_code(code).expect("allocated");
853            assert_ne!(mode.bit(), 0, "mode {code} has no bit in the set");
854        }
855    }
856
857    #[test]
858    fn only_four_modes_may_be_offered() {
859        assert!(SecurityMode::Open.may_be_offered());
860        assert!(SecurityMode::Owe.may_be_offered());
861        assert!(SecurityMode::Wpa2.may_be_offered());
862        assert!(SecurityMode::Wpa3.may_be_offered());
863        // Joinable but not offerable: no credential form for offering
864        // TKIP, and the rest have no credential form at all.
865        assert!(!SecurityMode::Wpa.may_be_offered());
866        assert!(!SecurityMode::Wep.may_be_offered());
867        assert!(!SecurityMode::Wpa2Ent.may_be_offered());
868    }
869
870    #[test]
871    fn a_network_entry_round_trips_with_its_passphrase() {
872        let entry = NetworkEntry {
873            hidden: true,
874            raw_key: false,
875            security: SecurityMode::Wpa2,
876            ssid: SSID,
877            credential: b"correct horse",
878        };
879        entry.validate().expect("well formed");
880        let mut buf = [0u8; NETWORK_ENTRY_MAX_LEN];
881        let len = entry.encode(&mut buf).expect("encodes");
882        assert_eq!(len, 3 + SSID.len() + 13);
883        assert_eq!(buf[0], FLAG_HIDDEN);
884        assert_eq!(buf[1], SecurityMode::Wpa2.code());
885        assert_eq!(buf[2], SSID.len() as u8);
886        assert_eq!(NetworkEntry::decode(&buf[..len]), Ok(entry));
887    }
888
889    #[test]
890    fn the_reported_form_carries_no_credential() {
891        let entry = NetworkEntry {
892            hidden: false,
893            raw_key: false,
894            security: SecurityMode::Wpa3,
895            ssid: SSID,
896            credential: b"a password nobody should read back",
897        };
898        let mut buf = [0u8; NETWORK_ENTRY_MAX_LEN];
899        let len = entry.encode_reported(&mut buf).expect("encodes");
900        assert_eq!(len, 3 + SSID.len());
901        let decoded = NetworkEntry::decode(&buf[..len]).expect("decodes");
902        assert!(decoded.credential.is_empty());
903        assert_eq!(decoded.ssid, SSID);
904        assert_eq!(decoded.security, SecurityMode::Wpa3);
905        // The secret is nowhere in the octets, not merely unparsed.
906        assert!(!buf[..len].windows(8).any(|w| w == b"a passwo"));
907    }
908
909    #[test]
910    fn credential_bounds_follow_the_mode() {
911        let entry = |security, raw_key, credential| NetworkEntry {
912            hidden: false,
913            raw_key,
914            security,
915            ssid: SSID,
916            credential,
917        };
918        // WPA2 takes 8 to 63 octets, per 802.11 Annex J.
919        assert!(
920            entry(SecurityMode::Wpa2, false, b"1234567")
921                .validate()
922                .is_err()
923        );
924        assert!(
925            entry(SecurityMode::Wpa2, false, b"12345678")
926                .validate()
927                .is_ok()
928        );
929        assert!(
930            entry(SecurityMode::Wpa2, false, &[b'x'; 63])
931                .validate()
932                .is_ok()
933        );
934        assert!(
935            entry(SecurityMode::Wpa2, false, &[b'x'; 64])
936                .validate()
937                .is_err()
938        );
939        // SAE puts no bound on its password, so the 8 to 63 rule does
940        // not apply and one octet is legal.
941        assert!(entry(SecurityMode::Wpa3, false, b"x").validate().is_ok());
942        assert!(
943            entry(SecurityMode::Wpa3, false, &[b'x'; 128])
944                .validate()
945                .is_ok()
946        );
947        assert!(
948            entry(SecurityMode::Wpa3, false, &[b'x'; 129])
949                .validate()
950                .is_err()
951        );
952        // Open and OWE carry no credential at all.
953        assert!(entry(SecurityMode::Open, false, b"").validate().is_ok());
954        assert!(
955            entry(SecurityMode::Open, false, b"anything")
956                .validate()
957                .is_err()
958        );
959        assert!(entry(SecurityMode::Owe, false, b"").validate().is_ok());
960        // The four reportable modes cannot be joined.
961        for mode in [
962            SecurityMode::Wep,
963            SecurityMode::Wpa2Ent,
964            SecurityMode::Wpa3Ent,
965            SecurityMode::Wpa3Ent192,
966        ] {
967            assert!(entry(mode, false, b"").validate().is_err());
968            assert!(entry(mode, false, b"secret12").validate().is_err());
969        }
970    }
971
972    #[test]
973    fn a_raw_key_stands_in_only_where_a_passphrase_is_derived() {
974        let entry = |security, credential| NetworkEntry {
975            hidden: false,
976            raw_key: true,
977            security,
978            ssid: SSID,
979            credential,
980        };
981        assert!(
982            entry(SecurityMode::Wpa2, &[0xAB; PMK_LEN])
983                .validate()
984                .is_ok()
985        );
986        assert!(
987            entry(SecurityMode::Wpa, &[0xAB; PMK_LEN])
988                .validate()
989                .is_ok()
990        );
991        assert!(entry(SecurityMode::Wpa2, &[0xAB; 31]).validate().is_err());
992        // SAE derives its key through the exchange, not from the
993        // password, so a stored key is not a substitute.
994        assert!(
995            entry(SecurityMode::Wpa3, &[0xAB; PMK_LEN])
996                .validate()
997                .is_err()
998        );
999        assert!(
1000            entry(SecurityMode::Open, &[0xAB; PMK_LEN])
1001                .validate()
1002                .is_err()
1003        );
1004    }
1005
1006    #[test]
1007    fn a_passphrase_is_utf8_without_a_terminator() {
1008        let entry = |credential| NetworkEntry {
1009            hidden: false,
1010            raw_key: false,
1011            security: SecurityMode::Wpa2,
1012            ssid: SSID,
1013            credential,
1014        };
1015        assert!(entry("passwörter".as_bytes()).validate().is_ok());
1016        assert!(entry(b"with\0a\0nul").validate().is_err());
1017        assert!(
1018            entry(&[0xFF, 0xFE, b'a', b'b', b'c', b'd', b'e', b'f'])
1019                .validate()
1020                .is_err()
1021        );
1022    }
1023
1024    #[test]
1025    fn an_empty_ssid_is_never_storable() {
1026        let entry = NetworkEntry {
1027            hidden: false,
1028            raw_key: false,
1029            security: SecurityMode::Open,
1030            ssid: b"",
1031            credential: b"",
1032        };
1033        assert_eq!(entry.validate(), Err(WifiError::NotWellFormed));
1034        let long = NetworkEntry {
1035            ssid: &[b'x'; 33],
1036            ..entry
1037        };
1038        assert_eq!(long.validate(), Err(WifiError::NotWellFormed));
1039    }
1040
1041    #[test]
1042    fn a_reserved_flag_bit_is_malformed() {
1043        let mut buf = [0u8; 8];
1044        buf[0] = 1 << 2;
1045        buf[1] = SecurityMode::Open.code();
1046        buf[2] = 1;
1047        buf[3] = b'x';
1048        assert_eq!(NetworkEntry::decode(&buf[..4]), Err(WifiError::Malformed));
1049    }
1050
1051    #[test]
1052    fn a_truncated_entry_is_not_a_malformed_one() {
1053        assert_eq!(NetworkEntry::decode(&[]), Err(WifiError::Truncated));
1054        assert_eq!(NetworkEntry::decode(&[0, 0]), Err(WifiError::Truncated));
1055        // Declares a 9-octet SSID and carries four.
1056        assert_eq!(
1057            NetworkEntry::decode(&[0, 0, 9, b'a', b'b', b'c', b'd']),
1058            Err(WifiError::Truncated)
1059        );
1060    }
1061
1062    #[test]
1063    fn a_scan_result_round_trips() {
1064        let result = ScanResult {
1065            modes: SecurityMode::Wpa2.bit() | SecurityMode::Wpa3.bit(),
1066            frequency_mhz: 2437,
1067            rssi_dbm: -67,
1068            bssid: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01],
1069            ssid: SSID,
1070        };
1071        let mut buf = [0u8; SCAN_RESULT_MAX_LEN];
1072        let len = result.encode(&mut buf).expect("encodes");
1073        assert_eq!(len, 11 + SSID.len());
1074        assert_eq!(ScanResult::decode(&buf[..len]), Ok(result));
1075        // A transition network offers both, and the host picks.
1076        assert!(result.offers(SecurityMode::Wpa2));
1077        assert!(result.offers(SecurityMode::Wpa3));
1078        assert!(!result.offers(SecurityMode::Open));
1079        assert!(result.modes_known());
1080    }
1081
1082    #[test]
1083    fn a_nameless_result_is_still_an_access_point() {
1084        let result = ScanResult {
1085            modes: 0,
1086            frequency_mhz: 5180,
1087            rssi_dbm: -80,
1088            bssid: [1, 2, 3, 4, 5, 6],
1089            ssid: b"",
1090        };
1091        let mut buf = [0u8; SCAN_RESULT_MAX_LEN];
1092        let len = result.encode(&mut buf).expect("encodes");
1093        assert_eq!(len, 11);
1094        let decoded = ScanResult::decode(&buf[..len]).expect("decodes");
1095        assert_eq!(decoded, result);
1096        // No bits set means the device did not determine the modes,
1097        // which is not the same as an access point offering nothing.
1098        assert!(!decoded.modes_known());
1099    }
1100
1101    #[test]
1102    fn a_link_carries_its_association_only_when_up() {
1103        let down = Link {
1104            state: LinkState::Connecting,
1105            reason: LinkReason::Auth,
1106            association: None,
1107        };
1108        let mut buf = [0u8; LINK_MAX_LEN];
1109        let len = down.encode(&mut buf).expect("encodes");
1110        assert_eq!(len, 2);
1111        assert_eq!(Link::decode(&buf[..len]), Ok(down));
1112
1113        let up = Link {
1114            state: LinkState::Up,
1115            reason: LinkReason::None,
1116            association: Some(Association {
1117                bssid: [0xA, 0xB, 0xC, 0xD, 0xE, 0xF],
1118                frequency_mhz: 5745,
1119            }),
1120        };
1121        let len = up.encode(&mut buf).expect("encodes");
1122        assert_eq!(len, 10);
1123        assert_eq!(Link::decode(&buf[..len]), Ok(up));
1124    }
1125
1126    #[test]
1127    fn an_up_link_without_an_association_is_truncated() {
1128        assert_eq!(
1129            Link::decode(&[LinkState::Up.code(), 0]),
1130            Err(WifiError::Truncated)
1131        );
1132        assert_eq!(Link::decode(&[9, 0]), Err(WifiError::Malformed));
1133        assert_eq!(Link::decode(&[0, 9]), Err(WifiError::Malformed));
1134    }
1135
1136    #[test]
1137    fn the_access_point_state_reports_where_it_landed() {
1138        let mut buf = [0u8; 3];
1139        let down = ApStatus {
1140            state: ApState::Down,
1141            frequency_mhz: None,
1142        };
1143        assert_eq!(down.encode(&mut buf), Ok(1));
1144        assert_eq!(ApStatus::decode(&buf[..1]), Ok(down));
1145
1146        let up = ApStatus {
1147            state: ApState::Up,
1148            frequency_mhz: Some(2412),
1149        };
1150        assert_eq!(up.encode(&mut buf), Ok(3));
1151        assert_eq!(ApStatus::decode(&buf[..3]), Ok(up));
1152    }
1153
1154    #[test]
1155    fn an_access_point_config_round_trips_and_redacts() {
1156        let config = ApConfig {
1157            hidden: false,
1158            security: SecurityMode::Wpa2,
1159            max_clients: 4,
1160            frequency_mhz: 2437,
1161            address: [192, 168, 4, 1],
1162            prefix: 24,
1163            ssid: SSID,
1164            credential: b"sharedsecret",
1165        };
1166        config.validate().expect("well formed");
1167        let mut buf = [0u8; AP_CONFIG_MAX_LEN];
1168        let len = config.encode(&mut buf).expect("encodes");
1169        assert_eq!(ApConfig::decode(&buf[..len]), Ok(config));
1170
1171        let len = config.encode_reported(&mut buf).expect("encodes");
1172        let reported = ApConfig::decode(&buf[..len]).expect("decodes");
1173        assert!(reported.credential.is_empty());
1174        assert_eq!(reported.ssid, SSID);
1175        assert_eq!(reported.address, [192, 168, 4, 1]);
1176    }
1177
1178    #[test]
1179    fn an_access_point_refuses_what_it_cannot_offer() {
1180        let base = ApConfig {
1181            hidden: false,
1182            security: SecurityMode::Wpa2,
1183            max_clients: 0,
1184            frequency_mhz: 0,
1185            address: [0; 4],
1186            prefix: 0,
1187            ssid: SSID,
1188            credential: b"sharedsecret",
1189        };
1190        // The default subnet is the all-zero address with prefix 0.
1191        assert!(base.validate().is_ok());
1192        assert!(base.uses_default_subnet());
1193        // TKIP and the reportable-only modes are not offerable.
1194        assert!(
1195            ApConfig {
1196                security: SecurityMode::Wpa,
1197                ..base
1198            }
1199            .validate()
1200            .is_err()
1201        );
1202        // A subnet the device cannot hand addresses out on.
1203        assert!(
1204            ApConfig {
1205                address: [169, 254, 1, 1],
1206                prefix: 24,
1207                ..base
1208            }
1209            .validate()
1210            .is_err()
1211        );
1212        assert!(
1213            ApConfig {
1214                address: [192, 168, 4, 1],
1215                prefix: 31,
1216                ..base
1217            }
1218            .validate()
1219            .is_err()
1220        );
1221        assert!(
1222            ApConfig {
1223                address: [192, 168, 4, 1],
1224                prefix: 24,
1225                ..base
1226            }
1227            .validate()
1228            .is_ok()
1229        );
1230    }
1231
1232    #[test]
1233    fn an_access_point_client_round_trips() {
1234        let client = ApClient {
1235            mac: [2, 4, 6, 8, 10, 12],
1236            rssi_dbm: -45,
1237            address: [192, 168, 4, 37],
1238        };
1239        let mut buf = [0u8; AP_CLIENT_LEN];
1240        assert_eq!(client.encode(&mut buf), Ok(AP_CLIENT_LEN));
1241        assert_eq!(ApClient::decode(&buf), Ok(client));
1242        assert!(client.has_address());
1243
1244        let unleased = ApClient {
1245            address: [0; 4],
1246            ..client
1247        };
1248        assert!(!unleased.has_address());
1249    }
1250
1251    #[test]
1252    fn encoders_report_a_buffer_too_small_rather_than_truncating() {
1253        let entry = NetworkEntry {
1254            hidden: false,
1255            raw_key: false,
1256            security: SecurityMode::Open,
1257            ssid: SSID,
1258            credential: b"",
1259        };
1260        let mut small = [0u8; 4];
1261        assert_eq!(entry.encode(&mut small), Err(WifiError::BufferTooSmall));
1262        let mut client = [0u8; 10];
1263        assert_eq!(
1264            ApClient {
1265                mac: [0; 6],
1266                rssi_dbm: 0,
1267                address: [0; 4]
1268            }
1269            .encode(&mut client),
1270            Err(WifiError::BufferTooSmall)
1271        );
1272    }
1273
1274    #[test]
1275    fn a_decoder_tolerates_octets_a_later_revision_appends() {
1276        // A scan result's SSID is the remainder, so it cannot grow; the
1277        // link and the client can, and a receiver keeps what it knows.
1278        let mut buf = [0u8; 16];
1279        buf[0] = LinkState::Up.code();
1280        buf[10] = 0xFF;
1281        buf[11] = 0xFF;
1282        let decoded = Link::decode(&buf[..12]).expect("decodes");
1283        assert_eq!(decoded.state, LinkState::Up);
1284        assert!(decoded.association.is_some());
1285    }
1286}