diff --git a/Cargo.lock b/Cargo.lock index 4d7d4192dc8..0b762e89510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4245,6 +4245,10 @@ dependencies = [ "toyos-pci", ] +[[package]] +name = "toyos-ieee80211" +version = "0.1.0" + [[package]] name = "toyos-input" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 266e2e06747..76a7d0c9bbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ members = [ "lib/fat32/check", "lib/gpt", "lib/hda", + "lib/ieee80211", "lib/input", "lib/inspect", "lib/logkeeper-api", diff --git a/NOTICE b/NOTICE index 0b3bdebef66..5da18d669ea 100644 --- a/NOTICE +++ b/NOTICE @@ -411,6 +411,41 @@ a work of theirs. Two things the tools wrote are not: of them are input a reader has to skip. +lib/ieee80211/tests/fixtures/tcpdump/ — 802.11 captures from tcpdump's tests +----------------------------------------------------------------------------- + + ieee802.11_exthdr.pcap 4,499 bytes + sha256 5d1179c7045f3fe6a4a6621b758ee25c7a8ec1eece9d3d7be707969aa96a5236 + ieee802.11_htc.pcap 466 bytes + sha256 a762951573320132a595a91833a1bc6c39c2bfd7f526c43f1402c307438fbb13 + ieee802.11_meshhdr-oobr.pcap 126 bytes + sha256 cf4cff158b93f4e983abdf4d2fd97a6996bbfe36d82795b610fb128674794038 + ieee802.11_meshid.pcap 823 bytes + sha256 9c64693b3f9d72365c198574ec0f4443c91c3d6dfa7f7a7d7ec420b14eb0cdbf + ieee802.11_parse_elements_oobr.pcap 295 bytes + sha256 5b89c6377bc6ada50619abce34ec0cbb3031790a79fffccaf44da610b9e91748 + ieee802.11_rates_oobr.pcap 111 bytes + sha256 dff15ca82eb9814a34ea0d875ea5f63c0f1a5d178bf390ddc919bec7b3421c85 + ieee802.11_rx-stbc.pcap 541 bytes + sha256 04322b0ee0cf314941e7e30c41378fbe96618a2b3952458dd04b9e8fbd581d75 + ieee802.11_tim_ie_oobr.pcap 335 bytes + sha256 fbb8b20efb233ec789214ecf96d5365cc8a34c275dae2685061ac72486a20bda + Copyright: The Tcpdump Group and the captures' contributors + Licence text: licenses/BSD-3-Clause-tcpdump.txt (tcpdump's `LICENSE`, + sha256 8a54594d257e14a5260ac770f1633516cb51e3fc28c40136ce2697014eda7afd) + SPDX-License-Identifier: BSD-3-Clause + +`toyos-ieee80211`'s host tests read these frames: bytes 802.11 radios and +other implementations wrote, from outside the Wi-Fi track's references. Each +is the file of that name under `tests/` in tcpdump's repository at commit +57768afc4cffbaa17faad7808f106bc325ada417 +(https://github.com/the-tcpdump-group/tcpdump), unmodified. Four hold traffic: a +station scanning and joining, a mesh point's beacon and probes, and data frames, +protected and with HT Control. The four named `oobr` are hostile inputs tcpdump +recorded for its own over-reads. Nothing in this repository fetches +them, and no test reads tcpdump's dissection of them. + + build/microcode/intel-ucode/* — Intel microcode, redistributable unmodified --------------------------------------------------------------------------- diff --git a/build/licence.rs b/build/licence.rs index b03111974da..5db0bd4d143 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -459,6 +459,55 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "NOTICE", Terms::Spdx("(MIT OR Apache-2.0) AND BSD-2-Clause"), ), + // Captures from tcpdump's test suite, read hermetically by `lib/ieee80211/tests/corpus.rs`. + ( + "lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_exthdr.pcap", + "5d1179c7045f3fe6a4a6621b758ee25c7a8ec1eece9d3d7be707969aa96a5236", + "NOTICE", + Terms::Spdx("BSD-3-Clause"), + ), + ( + "lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_htc.pcap", + "a762951573320132a595a91833a1bc6c39c2bfd7f526c43f1402c307438fbb13", + "NOTICE", + Terms::Spdx("BSD-3-Clause"), + ), + ( + "lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshhdr-oobr.pcap", + "cf4cff158b93f4e983abdf4d2fd97a6996bbfe36d82795b610fb128674794038", + "NOTICE", + Terms::Spdx("BSD-3-Clause"), + ), + ( + "lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshid.pcap", + "9c64693b3f9d72365c198574ec0f4443c91c3d6dfa7f7a7d7ec420b14eb0cdbf", + "NOTICE", + Terms::Spdx("BSD-3-Clause"), + ), + ( + "lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_parse_elements_oobr.pcap", + "5b89c6377bc6ada50619abce34ec0cbb3031790a79fffccaf44da610b9e91748", + "NOTICE", + Terms::Spdx("BSD-3-Clause"), + ), + ( + "lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rates_oobr.pcap", + "dff15ca82eb9814a34ea0d875ea5f63c0f1a5d178bf390ddc919bec7b3421c85", + "NOTICE", + Terms::Spdx("BSD-3-Clause"), + ), + ( + "lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rx-stbc.pcap", + "04322b0ee0cf314941e7e30c41378fbe96618a2b3952458dd04b9e8fbd581d75", + "NOTICE", + Terms::Spdx("BSD-3-Clause"), + ), + ( + "lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_tim_ie_oobr.pcap", + "fbb8b20efb233ec789214ecf96d5365cc8a34c275dae2685061ac72486a20bda", + "NOTICE", + Terms::Spdx("BSD-3-Clause"), + ), ( "assets/DOOM1.WAD", "1d7d43be501e67d927e415e0b8f3e29c3bf33075e859721816f652a526cac771", diff --git a/issues/a-fragmented-wifi-frame-is-dropped.md b/issues/a-fragmented-wifi-frame-is-dropped.md new file mode 100644 index 00000000000..a2c670d4b14 --- /dev/null +++ b/issues/a-fragmented-wifi-frame-is-dropped.md @@ -0,0 +1,34 @@ +--- +status: open +kind: defect +opened: 2026-10-14 +--- + +# A fragmented Wi-Fi frame is dropped + +`toyos-ieee80211` refuses every fragment by name (`Refused::Fragmented`, +`lib/ieee80211/src/frame.rs`): a frame whose More Fragments bit is set or whose +Fragment Number is not 0. It does not reassemble the MSDU or MMPDU. + +**The refusal is the right default under the trust model.** Frames are +untrusted input, and reassembly is where Vanhoef's fragment-and-forge attacks +live ("Fragment and Forge: Breaking Wi-Fi Through Frame Aggregation and +Fragmentation", USENIX Security 2021): fragments of different keys, of a frame +sent before a rekey, or a plaintext fragment mixed into an encrypted frame. A +reassembly buffer that does not exist cannot mix them. + +**The cost is real.** Every MSDU that an access point or a peer fragments is +lost. 802.11 lets a transmitter fragment any unicast MSDU or MMPDU above its +fragmentation threshold (IEEE 802.11-2020). An AP configured with a +threshold below its MTU loses every MSDU above it silently: the radio +acknowledges each fragment before the parser sees it, so the sender never +retransmits, and every higher-layer retransmission of that MSDU is lost the +same way. + +- **Owner:** the orchestrator, for the Wi-Fi track (`issues/there-is-no-wifi.md`). +- **Evidence:** the refusal and its test, `every_refusal_has_a_hostile_input_that_produces_it` in `lib/ieee80211/tests/refusals.rs`. +- **Exit:** reassembly that holds the fragment-and-forge defences, each refusal tested by host tests with the paper's attack frames: + - fragments reassembled only under one key and one packet-number run, with consecutive packet numbers; + - the buffer cleared on a rekey or reassociation; + - a plaintext fragment never joined to protected ones; + - a bounded buffer per sender and TID. diff --git a/issues/there-is-no-wifi.md b/issues/there-is-no-wifi.md index 69ede7aa4a2..7873c9da973 100644 --- a/issues/there-is-no-wifi.md +++ b/issues/there-is-no-wifi.md @@ -73,8 +73,8 @@ and owns each one this list gives no other owner. - **Writers.** Every W stage's owner is the orchestrator, who briefs a writer for it: an implementer told never to read the references or the - demo. The stage's design round is that writer's. The first W stage's pull - request, briefed to, adds that sentence to `.claude/agents/implementer.md`. + demo. The stage's design round is that writer's. Root `CLAUDE.md`'s + clean-room rule is that sentence; no prompt carries another. - **Reviews.** The orchestrator's brief for each W stage's review and design acceptance says that they quote no reference. A defect found through a reference reaches the writer only as a reader's revision of the @@ -230,6 +230,48 @@ Depends on: nothing; W6 waits on it. `build/ssh/tests/fuzz.rs`; and a third-party capture corpus from outside the references, beside the crate, with provenance, terms and `NOTICE` rows, pinned by hash. +- **Owed: `lib/ieee80211`'s readings checked against the text.** W1 was + written without IEEE 802.11-2020, 802.11ax-2021 or the WFA WMM + Specification v1.2 at hand; each reading below rests on the writer's + knowledge, and on the captures where `lib/ieee80211/tests/corpus.rs` reads + them. Owner: the orchestrator. + - **Exit:** every reading below checked against the text; each one the text + contradicts fixed, and each leniency the text rules out turned into a + refusal by name. + - **Frame structure:** the protocol version, type 3 as Extension, the + reserved and contention-free subtypes; non-QoS Order as strictly + ordered; the four-address layout; Fragment Number and More Fragments; + `ManagementWithDs`; `GroupTransmitter` for a management SA and a data TA. + - **Control:** the BAR and BA Type values and TID_INFO; a BlockAckReq's + Fragment Number being 0; the HE bitmap sizes by Fragment Number bits 2 + and 1, bit 0 a fragment level, bit 3 refused; the bandwidth-signaling TA. + - **Management bodies:** a beacon interval of 0 refused; the AID's two high + bits and its range of 1 to 2007. + - **Elements:** the Element ID Extension octet; each typed element allowed + once, WMM's included; the SSID's 32 octets; 1 to 8 Supported Rates and + Extended Supported Rates of length 0 refused; the selectors 127, 126, 123 + and 122; the TIM's offset as N1/2, its bound of 251 octets and its minimum + of one bitmap octet; Country's string, triplets, operating triplet from + 201 and channel bound of 200; MFPR as bit 6, MFPC as bit 7 and MFPR + without MFPC refused; the RSNXE's Field Length and SAE-H2E as bit 5; the + HT and VHT field splits and reserved values; Extended Capabilities' bit + numbering; HE Capabilities' 6, 11 and 4 octet fields, the 160 and 80+80 + MCS sets under PHY Capabilities B3 and B4, PPE Thresholds Present as + B55, and the PPE length of 7 + 6 × (NSTS + 1) × RU count bits; HE + Operation's bits 14, 15 and 17 and its five-octet 6 GHz field; WMM's + layouts, version 1, AIFSN of at least 2 and each ACI once, and its exact + length. + - **Data and the window:** the A-MSDU subframe header and its padding; the + bridge-tunnel types 0x80F3 and 0x8137; CCMP's Ext IV bit; the §10.25.6 + rules with the half-space boundary of 2048; the HE buffer of 256. + - **Each refusal's class**, `Malformed` or `Unsupported`. + - **The leniencies**, each to become a refusal if the text rules it out: + octets past the RSNE's and the RSNXE's last field ignored; octets past + the fields of HT and VHT Capabilities and Operation and of HE + Capabilities and Operation ignored (fewer refused); a Country element + accepted without its pad octet, and with one where none is needed. The + strict side, padding after an A-MSDU's last subframe refused, is checked + too. **W2, the handshakes, in `wlan`**, sans-IO: the 4-way and group-key handshakes, SAE by hash-to-element, PMF's keys, and the Transition Disable diff --git a/lib/ieee80211/Cargo.toml b/lib/ieee80211/Cargo.toml new file mode 100644 index 00000000000..07273ea2701 --- /dev/null +++ b/lib/ieee80211/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "toyos-ieee80211" +description = "IEEE 802.11 frames a station receives and sends, parsed in place and refused by name: management, block-ack control and data frames, their elements, A-MSDU, 802.11-to-Ethernet translation and the block-ack reorder window (pure, no_std with alloc, forbid(unsafe_code))." +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +publish = false diff --git a/lib/ieee80211/src/control.rs b/lib/ieee80211/src/control.rs new file mode 100644 index 00000000000..5351164df6a --- /dev/null +++ b/lib/ieee80211/src/control.rs @@ -0,0 +1,105 @@ +//! BlockAckReq (802.11-2020 §9.3.1.7) and BlockAck (§9.3.1.8) of the Compressed variant, the one an +//! HT, VHT or HE station's immediate block-ack agreement uses; their bodies follow Frame Control +//! and Duration. +//! +//! An HE BlockAck's bitmap is 32, 64, 128 or 256 bits long, as its Fragment Number subfield says +//! (802.11ax-2021 §9.3.1.8.2). Acknowledging fragments, the subfield's bit 0, is refused, because a +//! fragment is (`Refused::Fragmented`). + +use crate::read::Reader; +use crate::{Address, Refused, Seq}; + +/// The BAR and BA Type value of the Compressed variant (Table 9-24, Table 9-28). +const COMPRESSED: u16 = 2; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BlockAckReq { + pub ra: Address, + pub ta: Address, + pub tid: u8, + /// The Starting Sequence Number: everything before it is no longer to be waited for. + pub ssn: Seq, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BlockAck { + pub ra: Address, + pub ta: Address, + pub tid: u8, + pub ssn: Seq, + /// Bit n acknowledges sequence number SSN + n, for n below `bits`. + pub bitmap: [u64; 4], + pub bits: u16, +} + +impl BlockAck { + /// Whether `seq` is inside the bitmap and acknowledged by it. + pub fn acked(&self, seq: Seq) -> bool { + let offset = seq.since(self.ssn); + offset < self.bits + && self + .bitmap + .get(usize::from(offset / 64)) + .is_some_and(|word| word >> (offset % 64) & 1 != 0) + } +} + +/// The receiver and transmitter, the control field's TID, and the Starting Sequence Control's +/// Fragment Number and sequence number. +fn compressed(r: &mut Reader<'_>) -> Result<(Address, Address, u8, u8, Seq), Refused> { + let ra = r.address()?; + // A VHT transmitter may set the TA's group bit as a bandwidth signaling TA (§9.3.1.1). + let Address([first, rest @ ..]) = r.address()?; + let [b1, b2, b3, b4, b5] = rest; + let ta = Address([first & !1, b1, b2, b3, b4, b5]); + let control = r.u16()?; + if (control >> 1) & 0x0F != COMPRESSED { + return Err(Refused::BlockAckVariant); + } + let [_, high] = control.to_le_bytes(); + let [low, seq_high] = r.u16()?.to_le_bytes(); + let seq = Seq::new(u16::from_le_bytes([low, seq_high]) >> 4); + Ok((ra, ta, high >> 4, low & 0x0F, seq)) +} + +impl BlockAckReq { + pub(crate) fn parse(after_duration: &[u8]) -> Result { + let mut r = Reader::new(after_duration); + let (ra, ta, tid, fragment, ssn) = compressed(&mut r)?; + if fragment != 0 { + return Err(Refused::BitmapSize); + } + if !r.is_empty() { + return Err(Refused::ControlLength); + } + Ok(Self { ra, ta, tid, ssn }) + } +} + +impl BlockAck { + pub(crate) fn parse(after_duration: &[u8]) -> Result { + let mut r = Reader::new(after_duration); + let (ra, ta, tid, fragment, ssn) = compressed(&mut r)?; + // Bits 2 and 1 of the Fragment Number give the bitmap's length, bit 0 a fragment level. + let bits: u16 = match fragment { + 0b0000 => 64, + 0b0010 => 256, + 0b0100 => 128, + 0b0110 => 32, + _ => return Err(Refused::BitmapSize), + }; + let mut bitmap = [0u64; 4]; + let mut octets = r.take(usize::from(bits / 8))?.iter(); + for word in &mut bitmap { + let mut value = [0u8; 8]; + for (slot, &octet) in value.iter_mut().zip(octets.by_ref()) { + *slot = octet; + } + *word = u64::from_le_bytes(value); + } + if !r.is_empty() { + return Err(Refused::ControlLength); + } + Ok(Self { ra, ta, tid, ssn, bitmap, bits }) + } +} diff --git a/lib/ieee80211/src/data.rs b/lib/ieee80211/src/data.rs new file mode 100644 index 00000000000..6dfc8f02514 --- /dev/null +++ b/lib/ieee80211/src/data.rs @@ -0,0 +1,180 @@ +//! Data frame bodies: the CCMP header (§12.5.3.2), A-MSDU (§9.3.2.2), and translation between an +//! MSDU and an Ethernet II frame by IEEE 802.1H: an RFC 1042 LLC/SNAP header, or the bridge-tunnel +//! one for the two EtherTypes 802.1H sends that way. + +use crate::frame::Data; +use crate::read::Reader; +use crate::{Address, Refused, Seq}; + +/// The CCMP header that stays in front of a frame body its receiver decrypted. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CcmpHeader { + /// The 48-bit packet number. + pub pn: u64, + pub key_id: u8, +} + +impl CcmpHeader { + pub const LEN: usize = 8; + + /// The header, and the body after it. + pub fn split(body: &[u8]) -> Result<(Self, &[u8]), Refused> { + let mut r = Reader::new(body); + let [pn0, pn1, _, key, pn2, pn3, pn4, pn5] = r.array()?; + if key & 1 << 5 == 0 { + return Err(Refused::ExtendedIvClear); + } + let pn = u64::from_le_bytes([pn0, pn1, pn2, pn3, pn4, pn5, 0, 0]); + Ok((Self { pn, key_id: key >> 6 }, r.rest())) + } +} + +/// One MSDU and the addresses it travels between. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Msdu<'a> { + pub da: Address, + pub sa: Address, + pub payload: &'a [u8], +} + +/// The MSDUs `frame` carries in `body`, its plaintext: one, or an A-MSDU's subframes in order. +/// +/// An A-MSDU subframe header is an IEEE 802.3 header, its Length big-endian, and every subframe +/// but the last is padded to a multiple of four octets. +pub fn msdus<'a>(frame: &Data<'_>, body: &'a [u8]) -> Result, Refused> { + if frame.null { + return Ok(Msdus(Inner::One(None))); + } + if !frame.qos.is_some_and(|q| q.amsdu()) { + return Ok(Msdus(Inner::One(Some(Msdu { da: frame.da(), sa: frame.sa(), payload: body })))); + } + // The A-MSDU Present bit is outside CCMP's protection unless both peers negotiated SPP + // A-MSDU, so a first subframe whose destination is an LLC/SNAP header is a frame an attacker + // flipped the bit on (Vanhoef, "Fragment and Forge", USENIX Security 2021, §3.2). + if body.starts_with(&SNAP) { + return Err(Refused::SnapHeaderAsAddress); + } + Ok(Msdus(Inner::Subframes(Reader::new(body)))) +} + +pub struct Msdus<'a>(Inner<'a>); + +enum Inner<'a> { + One(Option>), + Subframes(Reader<'a>), +} + +impl<'a> Iterator for Msdus<'a> { + type Item = Result, Refused>; + + fn next(&mut self) -> Option { + match &mut self.0 { + Inner::One(one) => one.take().map(Ok), + Inner::Subframes(r) if r.is_empty() => None, + Inner::Subframes(r) => { + let next = subframe(r); + if next.is_err() { + *r = Reader::new(&[]); + } + Some(next) + } + } + } +} + +fn subframe<'a>(r: &mut Reader<'a>) -> Result, Refused> { + let da = r.address()?; + let sa = r.address()?; + let len = u16::from_be_bytes(r.array()?); + let payload = r.take(usize::from(len)).map_err(|_| Refused::SubframeLength)?; + if !r.is_empty() { + let pad = usize::from(len.wrapping_add(14).wrapping_neg() & 3); + r.take(pad).map_err(|_| Refused::SubframePadding)?; + if r.is_empty() { + return Err(Refused::SubframePadding); + } + } + Ok(Msdu { da, sa, payload }) +} + +/// An Ethernet II frame's header fields, and its payload. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Ethernet<'a> { + pub dst: Address, + pub src: Address, + pub ethertype: u16, + pub payload: &'a [u8], +} + +const SNAP: [u8; 6] = [0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00]; +const BRIDGE_TUNNEL: [u8; 6] = [0xAA, 0xAA, 0x03, 0x00, 0x00, 0xF8]; +/// The EtherTypes 802.1H carries under the bridge-tunnel OUI: AppleTalk AARP and IPX. +const TUNNELED: [u16; 2] = [0x80F3, 0x8137]; +/// The least EtherType: below it the field is an IEEE 802.3 length (IEEE 802.3 §3.2.6). +const MIN_ETHERTYPE: u16 = 0x0600; + +pub const LLC_SNAP_LEN: usize = 8; + +impl<'a> Ethernet<'a> { + pub fn from_msdu(msdu: Msdu<'a>) -> Result { + let mut r = Reader::new(msdu.payload); + let header: [u8; 6] = r.array()?; + let ethertype = u16::from_be_bytes(r.array()?); + let [llc0, llc1, llc2, ..] = header; + if [llc0, llc1, llc2] != [0xAA, 0xAA, 0x03] { + return Err(Refused::NotSnap); + } + if header == SNAP && TUNNELED.contains(ðertype) { + return Err(Refused::TranslatedType); + } + if header != SNAP && header != BRIDGE_TUNNEL { + return Err(Refused::UnknownOui); + } + if ethertype < MIN_ETHERTYPE { + return Err(Refused::LengthField); + } + Ok(Self { dst: msdu.da, src: msdu.sa, ethertype, payload: r.rest() }) + } +} + +/// The LLC/SNAP header an MSDU carrying `ethertype` starts with. +pub fn llc_snap(ethertype: u16) -> Result<[u8; LLC_SNAP_LEN], Refused> { + if ethertype < MIN_ETHERTYPE { + return Err(Refused::LengthField); + } + let [oui0, oui1, oui2, oui3, oui4, oui5] = if TUNNELED.contains(ðertype) { BRIDGE_TUNNEL } else { SNAP }; + let [t0, t1] = ethertype.to_be_bytes(); + Ok([oui0, oui1, oui2, oui3, oui4, oui5, t0, t1]) +} + +/// The header of a QoS Data frame a station sends its access point: To DS, Address 1 the BSSID, +/// Address 2 the source and Address 3 the destination (Table 9-30). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ToDs { + pub bssid: Address, + pub sa: Address, + pub da: Address, + pub seq: Seq, + pub tid: u8, + pub protected: bool, +} + +impl ToDs { + pub const LEN: usize = 26; + + pub fn header(&self) -> [u8; Self::LEN] { + let mut out = [0; Self::LEN]; + let protected = if self.protected { 0x40 } else { 0 }; + let fields = [0x88, 0x01 | protected, 0, 0] + .into_iter() + .chain(self.bssid.0) + .chain(self.sa.0) + .chain(self.da.0) + .chain((self.seq.get() << 4).to_le_bytes()) + .chain(u16::from(self.tid & 0x0F).to_le_bytes()); + for (slot, byte) in out.iter_mut().zip(fields) { + *slot = byte; + } + out + } +} diff --git a/lib/ieee80211/src/element/country.rs b/lib/ieee80211/src/element/country.rs new file mode 100644 index 00000000000..5a82f846a97 --- /dev/null +++ b/lib/ieee80211/src/element/country.rs @@ -0,0 +1,71 @@ +//! The Country element (§9.4.2.8): a country string, then triplets, then a pad octet of 0 where +//! one makes the length even. + +use crate::Refused; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Country<'a> { + /// Two upper-case ISO 3166-1 letters, then the environment or table octet. + pub string: [u8; 3], + triplets: &'a [u8], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Triplet { + /// Channels `first_channel` onward, `channels` of them, at no more than `max_power_dbm`. + Subband { first_channel: u8, channels: u8, max_power_dbm: i8 }, + /// The operating class the subband triplets after it are in. + Operating { extension_id: u8, class: u8, coverage_class: u8 }, +} + +/// A first octet of at least this opens an Operating triplet. +const OPERATING_EXTENSION: u8 = 201; +const LAST_CHANNEL: u16 = 200; + +impl<'a> Country<'a> { + pub(super) fn parse(body: &'a [u8]) -> Result { + let Some((&[a, b, environment], rest)) = body.split_first_chunk::<3>() else { + return Err(Refused::ElementLength); + }; + if !a.is_ascii_uppercase() || !b.is_ascii_uppercase() { + return Err(Refused::CountryString); + } + let triplets = match rest.len() % 3 { + 0 => rest, + 1 => match rest.split_last() { + Some((0, triplets)) => triplets, + _ => return Err(Refused::CountryPad), + }, + _ => return Err(Refused::CountryTriplet), + }; + if triplets.is_empty() { + return Err(Refused::ElementLength); + } + for &[first, channels, _] in triplets.as_chunks::<3>().0 { + let past_last = u16::from(first).saturating_add(u16::from(channels)) > LAST_CHANNEL + 1; + if first < OPERATING_EXTENSION && (first == 0 || channels == 0 || past_last) { + return Err(Refused::CountryChannels); + } + } + Ok(Self { string: [a, b, environment], triplets }) + } + + pub fn triplets(&self) -> Triplets<'a> { + Triplets(self.triplets.as_chunks::<3>().0.iter()) + } +} + +pub struct Triplets<'a>(core::slice::Iter<'a, [u8; 3]>); + +impl Iterator for Triplets<'_> { + type Item = Triplet; + + fn next(&mut self) -> Option { + let &[a, b, c] = self.0.next()?; + Some(if a >= OPERATING_EXTENSION { + Triplet::Operating { extension_id: a, class: b, coverage_class: c } + } else { + Triplet::Subband { first_channel: a, channels: b, max_power_dbm: i8::from_le_bytes([c]) } + }) + } +} diff --git a/lib/ieee80211/src/element/he.rs b/lib/ieee80211/src/element/he.rs new file mode 100644 index 00000000000..11aa0f184a7 --- /dev/null +++ b/lib/ieee80211/src/element/he.rs @@ -0,0 +1,94 @@ +//! HE Capabilities (802.11ax-2021 §9.4.2.248) and HE Operation (§9.4.2.249), Element ID Extension +//! 35 and 36. Their lengths follow from bits inside them; a shorter element is refused, and octets +//! past those fields are ignored, so a field a later amendment appends costs no beacon. + +use crate::read::Reader; +use crate::Refused; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HeCapabilities<'a> { + pub mac: [u8; 6], + pub phy: [u8; 11], + /// Rx and Tx HE-MCS maps for 80 MHz and narrower. + pub mcs_80: [u8; 4], + /// For 160 MHz, present when PHY Capabilities B3 (Channel Width Set B2) is set. + pub mcs_160: Option<[u8; 4]>, + /// For 80+80 MHz, present when PHY Capabilities B4 (Channel Width Set B3) is set. + pub mcs_80p80: Option<[u8; 4]>, + /// Present when PHY Capabilities B55, PPE Thresholds Present, is set. + pub ppe_thresholds: Option<&'a [u8]>, +} + +impl<'a> HeCapabilities<'a> { + pub(super) fn parse(body: &'a [u8]) -> Result { + let mut r = Reader::new(body); + let length = |_| Refused::ElementLength; + let mac = r.array().map_err(length)?; + let phy: [u8; 11] = r.array().map_err(length)?; + let [phy0, _, _, _, _, _, phy6, ..] = phy; + let mcs_80 = r.array().map_err(length)?; + let mcs_160 = if phy0 & 1 << 3 != 0 { Some(r.array().map_err(length)?) } else { None }; + let mcs_80p80 = if phy0 & 1 << 4 != 0 { Some(r.array().map_err(length)?) } else { None }; + let ppe_thresholds = if phy6 & 1 << 7 != 0 { Some(ppe(&mut r)?) } else { None }; + Ok(Self { mac, phy, mcs_80, mcs_160, mcs_80p80, ppe_thresholds }) + } +} + +/// The PPE Thresholds field: NSTS and an RU index bitmask, then a pair of 3-bit thresholds per +/// stream and RU size, padded to whole octets (§9.4.2.248.5). +fn ppe<'a>(r: &mut Reader<'a>) -> Result<&'a [u8], Refused> { + let &first = r.rest().first().ok_or(Refused::PpeThresholds)?; + let streams = u32::from(first & 0b111).saturating_add(1); + let sizes = (first >> 3 & 0x0F).count_ones(); + let bits = streams.saturating_mul(sizes).saturating_mul(6).saturating_add(7); + let octets = usize::try_from(bits.div_ceil(8)).map_err(|_| Refused::PpeThresholds)?; + r.take(octets).map_err(|_| Refused::PpeThresholds) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HeOperation { + /// The HE Operation Parameters field, 24 bits. + pub parameters: u32, + /// BSS Color Information: the color in bits 0 to 5, Partial bit 6, Disabled bit 7. + pub bss_color: u8, + pub basic_mcs: u16, + /// Channel Width and two center frequency segments, as VHT Operation's. + pub vht: Option<[u8; 3]>, + pub max_cohosted_bssid: Option, + pub six_ghz: Option, +} + +/// The 6 GHz Operation Information field. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SixGhz { + pub primary_channel: u8, + /// Channel Width in bits 0 and 1, Duplicate Beacon bit 2, Regulatory Info bits 3 to 5. + pub control: u8, + pub center_segment0: u8, + pub center_segment1: u8, + pub minimum_rate: u8, +} + +impl HeOperation { + const VHT_PRESENT: u32 = 1 << 14; + const COHOSTED: u32 = 1 << 15; + const SIX_GHZ_PRESENT: u32 = 1 << 17; + + pub(super) fn parse(body: &[u8]) -> Result { + let mut r = Reader::new(body); + let length = |_| Refused::ElementLength; + let [p0, p1, p2] = r.array().map_err(length)?; + let parameters = u32::from_le_bytes([p0, p1, p2, 0]); + let bss_color = r.u8().map_err(length)?; + let basic_mcs = r.u16().map_err(length)?; + let vht = if parameters & Self::VHT_PRESENT != 0 { Some(r.array().map_err(length)?) } else { None }; + let max_cohosted_bssid = if parameters & Self::COHOSTED != 0 { Some(r.u8().map_err(length)?) } else { None }; + let six_ghz = if parameters & Self::SIX_GHZ_PRESENT != 0 { + let [primary_channel, control, center_segment0, center_segment1, minimum_rate] = r.array().map_err(length)?; + Some(SixGhz { primary_channel, control, center_segment0, center_segment1, minimum_rate }) + } else { + None + }; + Ok(Self { parameters, bss_color, basic_mcs, vht, max_cohosted_bssid, six_ghz }) + } +} diff --git a/lib/ieee80211/src/element/ht.rs b/lib/ieee80211/src/element/ht.rs new file mode 100644 index 00000000000..e3f26ce5c83 --- /dev/null +++ b/lib/ieee80211/src/element/ht.rs @@ -0,0 +1,133 @@ +//! HT Capabilities (§9.4.2.55), HT Operation (§9.4.2.56), VHT Capabilities (§9.4.2.157) and VHT +//! Operation (§9.4.2.158): fixed-length elements, refused when shorter or where a field holds a +//! value the standard reserves. Octets past the fixed fields are ignored, so a field a later +//! amendment appends costs no beacon. + +use crate::read::Reader; +use crate::Refused; + +fn fields(body: &[u8]) -> Result<[u8; N], Refused> { + body.first_chunk().copied().ok_or(Refused::ElementLength) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HtCapabilities { + pub info: u16, + pub ampdu_parameters: u8, + /// The Supported MCS Set field; its first 77 bits are the Rx MCS Bitmask. + pub mcs: [u8; 16], + pub extended: u16, + pub beamforming: u32, + pub antenna_selection: u8, +} + +impl HtCapabilities { + pub(super) fn parse(body: &[u8]) -> Result { + let bytes: [u8; 26] = fields(body)?; + let mut r = Reader::new(&bytes); + Ok(Self { + info: r.u16()?, + ampdu_parameters: r.u8()?, + mcs: r.array()?, + extended: r.u16()?, + beamforming: r.u32()?, + antenna_selection: r.u8()?, + }) + } +} + +/// Secondary Channel Offset (§9.4.2.56). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SecondaryChannel { + None, + Above, + Below, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HtOperation { + pub primary_channel: u8, + pub secondary: SecondaryChannel, + /// STA Channel Width: any channel width in the HT Capabilities' set. + pub any_width: bool, + pub info: [u8; 5], + pub basic_mcs: [u8; 16], +} + +impl HtOperation { + pub(super) fn parse(body: &[u8]) -> Result { + let bytes: [u8; 22] = fields(body)?; + let mut r = Reader::new(&bytes); + let primary_channel = r.u8()?; + let info: [u8; 5] = r.array()?; + let [first, ..] = info; + let secondary = match first & 0b11 { + 0 => SecondaryChannel::None, + 1 => SecondaryChannel::Above, + 3 => SecondaryChannel::Below, + _ => return Err(Refused::SecondaryChannelOffset), + }; + Ok(Self { primary_channel, secondary, any_width: first & 1 << 2 != 0, info, basic_mcs: r.array()? }) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct VhtCapabilities { + pub info: u32, + pub rx_mcs_map: u16, + pub rx_highest: u16, + pub tx_mcs_map: u16, + pub tx_highest: u16, +} + +impl VhtCapabilities { + pub(super) fn parse(body: &[u8]) -> Result { + let bytes: [u8; 12] = fields(body)?; + let mut r = Reader::new(&bytes); + let caps = Self { + info: r.u32()?, + rx_mcs_map: r.u16()?, + rx_highest: r.u16()?, + tx_mcs_map: r.u16()?, + tx_highest: r.u16()?, + }; + if caps.info & 0b11 == 0b11 || (caps.info >> 2) & 0b11 == 0b11 { + return Err(Refused::VhtReserved); + } + Ok(caps) + } +} + +/// The VHT Operation Information's Channel Width (Table 9-274). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum VhtChannelWidth { + TwentyOrForty, + /// 80, 160 or 80+80 MHz, told apart by the center frequency segments. + Eighty, + /// 160 MHz, in the deprecated encoding. + OneSixty, + /// Non-contiguous 80+80 MHz, in the deprecated encoding. + EightyPlusEighty, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct VhtOperation { + pub width: VhtChannelWidth, + pub center_segment0: u8, + pub center_segment1: u8, + pub basic_mcs_map: u16, +} + +impl VhtOperation { + pub(super) fn parse(body: &[u8]) -> Result { + let [width, center_segment0, center_segment1, m0, m1] = fields(body)?; + let width = match width { + 0 => VhtChannelWidth::TwentyOrForty, + 1 => VhtChannelWidth::Eighty, + 2 => VhtChannelWidth::OneSixty, + 3 => VhtChannelWidth::EightyPlusEighty, + _ => return Err(Refused::VhtReserved), + }; + Ok(Self { width, center_segment0, center_segment1, basic_mcs_map: u16::from_le_bytes([m0, m1]) }) + } +} diff --git a/lib/ieee80211/src/element/mod.rs b/lib/ieee80211/src/element/mod.rs new file mode 100644 index 00000000000..483bcf62cbc --- /dev/null +++ b/lib/ieee80211/src/element/mod.rs @@ -0,0 +1,208 @@ +//! Elements (§9.4.2): the TLV walk of a body's element list, and the elements a station decides +//! by, each checked against its definition and allowed once. +//! +//! A vendor-specific element other than WMM's, and any other element this module does not type, is +//! skipped, as §10.28.9 has a receiver ignore what it does not know; one it types that breaks its +//! definition refuses the whole list. + +mod country; +mod he; +mod ht; +mod rsn; +mod tim; +mod wmm; + +pub use country::{Country, Triplet, Triplets}; +pub use he::{HeCapabilities, HeOperation, SixGhz}; +pub use ht::{HtCapabilities, HtOperation, SecondaryChannel, VhtCapabilities, VhtChannelWidth, VhtOperation}; +pub use rsn::{Rsn, Rsnx, Suite, Suites}; +pub use tim::Tim; +pub use wmm::{AcParameters, Wmm}; + +use crate::read::Reader; +use crate::Refused; + +/// An element's identifier: an Element ID, or 255 and its Element ID Extension (§9.4.2.1). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Id { + Base(u8), + Extension(u8), +} + +impl Id { + pub const SSID: Self = Self::Base(0); + pub const SUPPORTED_RATES: Self = Self::Base(1); + pub const DSSS_PARAMETER_SET: Self = Self::Base(3); + pub const TIM: Self = Self::Base(5); + pub const COUNTRY: Self = Self::Base(7); + pub const HT_CAPABILITIES: Self = Self::Base(45); + pub const RSN: Self = Self::Base(48); + pub const EXTENDED_SUPPORTED_RATES: Self = Self::Base(50); + pub const HT_OPERATION: Self = Self::Base(61); + pub const EXTENDED_CAPABILITIES: Self = Self::Base(127); + pub const VHT_CAPABILITIES: Self = Self::Base(191); + pub const VHT_OPERATION: Self = Self::Base(192); + pub const VENDOR_SPECIFIC: Self = Self::Base(221); + pub const RSNX: Self = Self::Base(244); + pub const HE_CAPABILITIES: Self = Self::Extension(35); + pub const HE_OPERATION: Self = Self::Extension(36); + const EXTENSION: u8 = 255; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Element<'a> { + pub id: Id, + /// The information, after an extension's Element ID Extension octet. + pub body: &'a [u8], +} + +/// The elements of a list in order; the first malformed one ends it. +pub struct Iter<'a> { + r: Reader<'a>, +} + +pub const fn iter(elements: &[u8]) -> Iter<'_> { + Iter { r: Reader::new(elements) } +} + +impl<'a> Iterator for Iter<'a> { + type Item = Result, Refused>; + + fn next(&mut self) -> Option { + if self.r.is_empty() { + return None; + } + let next = self.read(); + if next.is_err() { + self.r = Reader::new(&[]); + } + Some(next) + } +} + +impl<'a> Iter<'a> { + fn read(&mut self) -> Result, Refused> { + let [id, len] = self.r.array().map_err(|_| Refused::ElementPastBody)?; + let body = self.r.take(usize::from(len)).map_err(|_| Refused::ElementPastBody)?; + if id != Id::EXTENSION { + return Ok(Element { id: Id::Base(id), body }); + } + let (&ext, body) = body.split_first().ok_or(Refused::ExtensionWithoutId)?; + Ok(Element { id: Id::Extension(ext), body }) + } +} + +/// A rate in units of 500 kb/s, or a BSS membership selector (§9.4.2.3). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rate { + /// In the BSSBasicRateSet, or for a selector, required of a member. + pub basic: bool, + pub value: u8, +} + +impl Rate { + /// BSS membership selectors (Table 9-78). + pub const HT_PHY: u8 = 127; + pub const VHT_PHY: u8 = 126; + pub const SAE_H2E_ONLY: u8 = 123; + /// 802.11ax-2021 Table 9-78. + pub const HE_PHY: u8 = 122; +} + +/// The Supported Rates and BSS Membership Selectors element and its Extended continuation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rates<'a> { + supported: &'a [u8], + extended: &'a [u8], +} + +impl Rates<'_> { + pub fn iter(&self) -> impl Iterator + '_ { + self.supported.iter().chain(self.extended).map(|&b| Rate { basic: b & 0x80 != 0, value: b & 0x7F }) + } +} + +/// The elements of one list a station decides by. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Elements<'a> { + pub ssid: Option<&'a [u8]>, + pub rates: Option>, + pub dsss_channel: Option, + pub tim: Option>, + pub country: Option>, + pub rsn: Option>, + pub rsnx: Option>, + pub ht_capabilities: Option, + pub ht_operation: Option, + pub vht_capabilities: Option, + pub vht_operation: Option, + pub extended_capabilities: Option>, + pub he_capabilities: Option>, + pub he_operation: Option, + pub wmm: Option, +} + +/// The Extended Capabilities element: bit n of its field, absent past its length, is 0 (§9.4.2.26). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ExtendedCapabilities<'a>(pub &'a [u8]); + +impl ExtendedCapabilities<'_> { + pub fn bit(&self, n: usize) -> bool { + self.0.get(n / 8).is_some_and(|octet| octet >> (n % 8) & 1 != 0) + } +} + +fn once(slot: &mut Option, value: T) -> Result<(), Refused> { + if slot.is_some() { + return Err(Refused::DuplicateElement); + } + *slot = Some(value); + Ok(()) +} + +impl<'a> Elements<'a> { + pub fn parse(list: &'a [u8]) -> Result { + let mut e = Self::default(); + let mut supported: Option<&'a [u8]> = None; + let mut extended: Option<&'a [u8]> = None; + for element in iter(list) { + let Element { id, body } = element?; + match id { + Id::SSID if body.len() > 32 => return Err(Refused::SsidLength), + Id::SSID => once(&mut e.ssid, body)?, + Id::SUPPORTED_RATES if body.is_empty() || body.len() > 8 => return Err(Refused::RateCount), + Id::SUPPORTED_RATES => once(&mut supported, body)?, + Id::EXTENDED_SUPPORTED_RATES if body.is_empty() => return Err(Refused::ElementLength), + Id::EXTENDED_SUPPORTED_RATES => once(&mut extended, body)?, + Id::DSSS_PARAMETER_SET => match body { + &[channel] => once(&mut e.dsss_channel, channel)?, + _ => return Err(Refused::ElementLength), + }, + Id::TIM => once(&mut e.tim, Tim::parse(body)?)?, + Id::COUNTRY => once(&mut e.country, Country::parse(body)?)?, + Id::RSN => once(&mut e.rsn, Rsn::parse(body)?)?, + Id::RSNX => once(&mut e.rsnx, Rsnx::parse(body)?)?, + Id::HT_CAPABILITIES => once(&mut e.ht_capabilities, HtCapabilities::parse(body)?)?, + Id::HT_OPERATION => once(&mut e.ht_operation, HtOperation::parse(body)?)?, + Id::VHT_CAPABILITIES => once(&mut e.vht_capabilities, VhtCapabilities::parse(body)?)?, + Id::VHT_OPERATION => once(&mut e.vht_operation, VhtOperation::parse(body)?)?, + Id::EXTENDED_CAPABILITIES => once(&mut e.extended_capabilities, ExtendedCapabilities(body))?, + Id::HE_CAPABILITIES => once(&mut e.he_capabilities, HeCapabilities::parse(body)?)?, + Id::HE_OPERATION => once(&mut e.he_operation, HeOperation::parse(body)?)?, + Id::VENDOR_SPECIFIC => { + if let Some(rest) = body.strip_prefix(&wmm::HEADER) + && let Some(w) = Wmm::parse(rest)? + { + once(&mut e.wmm, w)?; + } + } + Id::Base(_) | Id::Extension(_) => {} + } + } + e.rates = match (supported, extended) { + (None, None) => None, + (supported, extended) => Some(Rates { supported: supported.unwrap_or(&[]), extended: extended.unwrap_or(&[]) }), + }; + Ok(e) + } +} diff --git a/lib/ieee80211/src/element/rsn.rs b/lib/ieee80211/src/element/rsn.rs new file mode 100644 index 00000000000..680ee23d7a7 --- /dev/null +++ b/lib/ieee80211/src/element/rsn.rs @@ -0,0 +1,154 @@ +//! The RSN element (§9.4.2.24) and the RSN Extension element (§9.4.2.241). +//! +//! An RSNE's fields after its version are each optional, but only from the end: one present +//! needs every one before it. What follows the last field is ignored, as §10.28.9 has a receiver +//! do with an extensible element. + +use crate::read::Reader; +use crate::Refused; + +/// A cipher or AKM suite selector: an OUI or CID and a type (§9.4.2.24.2, §9.4.2.24.3). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Suite(pub [u8; 4]); + +impl Suite { + const fn ieee(kind: u8) -> Self { + Self([0x00, 0x0F, 0xAC, kind]) + } + + // Cipher suites, Table 9-149. + pub const TKIP: Self = Self::ieee(2); + pub const CCMP_128: Self = Self::ieee(4); + pub const BIP_CMAC_128: Self = Self::ieee(6); + pub const GROUP_ADDRESSED_TRAFFIC_NOT_ALLOWED: Self = Self::ieee(7); + pub const GCMP_128: Self = Self::ieee(8); + pub const GCMP_256: Self = Self::ieee(9); + pub const CCMP_256: Self = Self::ieee(10); + pub const BIP_GMAC_128: Self = Self::ieee(11); + pub const BIP_GMAC_256: Self = Self::ieee(12); + pub const BIP_CMAC_256: Self = Self::ieee(13); + + // AKM suites, Table 9-151. + pub const AKM_8021X: Self = Self::ieee(1); + pub const AKM_PSK: Self = Self::ieee(2); + pub const AKM_FT_PSK: Self = Self::ieee(4); + pub const AKM_PSK_SHA256: Self = Self::ieee(6); + pub const AKM_SAE: Self = Self::ieee(8); + pub const AKM_FT_SAE: Self = Self::ieee(9); +} + +/// A list of suite selectors, four octets each. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Suites<'a>(&'a [u8]); + +impl Suites<'_> { + pub fn iter(&self) -> impl Iterator + '_ { + self.0.as_chunks::<4>().0.iter().map(|&s| Suite(s)) + } + + pub fn contains(&self, suite: Suite) -> bool { + self.iter().any(|s| s == suite) + } + + pub const fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rsn<'a> { + pub group_data: Option, + pub pairwise: Option>, + pub akm: Option>, + pub capabilities: Option, + /// The PMKIDs, 16 octets each. + pub pmkids: Option<&'a [u8]>, + pub group_management: Option, +} + +const MFPR: u16 = 1 << 6; +const MFPC: u16 = 1 << 7; + +impl<'a> Rsn<'a> { + pub(super) fn parse(body: &'a [u8]) -> Result { + let mut r = Reader::new(body); + if r.u16().map_err(|_| Refused::ElementLength)? != 1 { + return Err(Refused::RsnVersion); + } + let mut rsn = Self { + group_data: None, + pairwise: None, + akm: None, + capabilities: None, + pmkids: None, + group_management: None, + }; + if r.is_empty() { + return Ok(rsn); + } + rsn.group_data = Some(Suite(r.array()?)); + if r.is_empty() { + return Ok(rsn); + } + rsn.pairwise = Some(Suites(counted(&mut r, 4)?)); + if r.is_empty() { + return Ok(rsn); + } + rsn.akm = Some(Suites(counted(&mut r, 4)?)); + if r.is_empty() { + return Ok(rsn); + } + let capabilities = r.u16()?; + if capabilities & MFPR != 0 && capabilities & MFPC == 0 { + return Err(Refused::MfpRequiredNotCapable); + } + rsn.capabilities = Some(capabilities); + if r.is_empty() { + return Ok(rsn); + } + rsn.pmkids = Some(counted(&mut r, 16)?); + if r.is_empty() { + return Ok(rsn); + } + rsn.group_management = Some(Suite(r.array()?)); + Ok(rsn) + } + + /// Management Frame Protection Capable, bit 7 of the RSN Capabilities field. + pub fn mfp_capable(&self) -> bool { + self.capabilities.is_some_and(|c| c & MFPC != 0) + } + + /// Management Frame Protection Required, bit 6. + pub fn mfp_required(&self) -> bool { + self.capabilities.is_some_and(|c| c & MFPR != 0) + } +} + +/// A two-octet count, then that many items of `size` octets. +fn counted<'a>(r: &mut Reader<'a>, size: usize) -> Result<&'a [u8], Refused> { + let count = usize::from(r.u16()?); + let len = count.checked_mul(size).ok_or(Refused::RsnCount)?; + r.take(len).map_err(|_| Refused::RsnCount) +} + +/// The RSNXE: its Extended RSN Capabilities field, whose low four bits give its length less one. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rsnx<'a>(&'a [u8]); + +impl<'a> Rsnx<'a> { + pub(super) fn parse(body: &'a [u8]) -> Result { + let &first = body.first().ok_or(Refused::ElementLength)?; + let len = usize::from(first & 0x0F).saturating_add(1); + body.get(..len).map(Self).ok_or(Refused::ExtendedRsnLength) + } + + pub fn bit(&self, n: usize) -> bool { + self.0.get(n / 8).is_some_and(|octet| octet >> (n % 8) & 1 != 0) + } + + /// SAE Hash-to-Element, bit 5 (Table 9-321). + pub fn sae_h2e(&self) -> bool { + self.bit(5) + } +} diff --git a/lib/ieee80211/src/element/tim.rs b/lib/ieee80211/src/element/tim.rs new file mode 100644 index 00000000000..b1f1cac3f17 --- /dev/null +++ b/lib/ieee80211/src/element/tim.rs @@ -0,0 +1,54 @@ +//! The TIM element (§9.4.2.5). + +use crate::read::Reader; +use crate::Refused; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Tim<'a> { + pub dtim_count: u8, + /// Never 0. + pub dtim_period: u8, + control: u8, + bitmap: &'a [u8], +} + +/// The partial virtual bitmap covers AIDs 0 to 2007: octets 0 to 250. +const BITMAP_OCTETS: usize = 251; + +impl<'a> Tim<'a> { + pub(super) fn parse(body: &'a [u8]) -> Result { + let mut r = Reader::new(body); + let [dtim_count, dtim_period, control] = r.array().map_err(|_| Refused::ElementLength)?; + let bitmap = r.rest(); + if bitmap.is_empty() { + return Err(Refused::ElementLength); + } + if dtim_period == 0 { + return Err(Refused::DtimPeriod); + } + let tim = Self { dtim_count, dtim_period, control, bitmap }; + if tim.offset().saturating_add(bitmap.len()) > BITMAP_OCTETS { + return Err(Refused::TimBitmap); + } + Ok(tim) + } + + /// N1, the first octet the partial bitmap holds: the Bitmap Offset subfield times two. + fn offset(&self) -> usize { + usize::from(self.control & 0xFE) + } + + /// Whether group-addressed frames are buffered: bit 0 of the Bitmap Control field. + pub const fn group_buffered(&self) -> bool { + self.control & 1 != 0 + } + + /// Whether frames are buffered for the station with association ID `aid`. + pub fn buffered(&self, aid: u16) -> bool { + let aid = usize::from(aid); + (aid / 8) + .checked_sub(self.offset()) + .and_then(|octet| self.bitmap.get(octet)) + .is_some_and(|octet| octet >> (aid % 8) & 1 != 0) + } +} diff --git a/lib/ieee80211/src/element/wmm.rs b/lib/ieee80211/src/element/wmm.rs new file mode 100644 index 00000000000..4ffbd3929e2 --- /dev/null +++ b/lib/ieee80211/src/element/wmm.rs @@ -0,0 +1,80 @@ +//! The WMM Information and Parameter elements (Wi-Fi Alliance, WMM Specification v1.2, §2.2.1 and +//! §2.2.2): vendor-specific elements under OUI 00-50-F2, OUI type 2, version 1. + +use crate::read::Reader; +use crate::Refused; + +/// The OUI and OUI type a WMM element's body starts with. +pub(super) const HEADER: [u8; 4] = [0x00, 0x50, 0xF2, 0x02]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Wmm { + Information { qos_info: u8 }, + Parameter { qos_info: u8, access_categories: [AcParameters; 4] }, +} + +/// One AC Parameter Record, indexed in [`Wmm::Parameter`] by its ACI: best effort, background, +/// video, voice. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AcParameters { + /// 2 to 15 slots. + pub aifsn: u8, + pub admission_control: bool, + /// The exponents of CWmin and CWmax: CW is 2^ECW − 1. + pub ecw_min: u8, + pub ecw_max: u8, + /// In units of 32 µs. + pub txop_limit: u16, +} + +const INFORMATION: u8 = 0; +const PARAMETER: u8 = 1; + +impl Wmm { + /// The element after its OUI and OUI type, or `None` for another WMM subtype (a TSPEC). + pub(super) fn parse(after_header: &[u8]) -> Result, Refused> { + let mut r = Reader::new(after_header); + let length = |_| Refused::ElementLength; + let subtype = r.u8().map_err(length)?; + if subtype != INFORMATION && subtype != PARAMETER { + return Ok(None); + } + if r.u8().map_err(length)? != 1 { + return Err(Refused::WmmVersion); + } + let qos_info = r.u8().map_err(length)?; + let wmm = if subtype == INFORMATION { + Self::Information { qos_info } + } else { + r.u8().map_err(length)?; + let mut access_categories = [None; 4]; + for _ in 0..4 { + let [aci_aifsn, ecw] = r.array().map_err(length)?; + let txop_limit = r.u16().map_err(length)?; + let aifsn = aci_aifsn & 0x0F; + if aifsn < 2 { + return Err(Refused::WmmAifsn); + } + let record = AcParameters { + aifsn, + admission_control: aci_aifsn & 1 << 4 != 0, + ecw_min: ecw & 0x0F, + ecw_max: ecw >> 4, + txop_limit, + }; + if let Some(slot) = access_categories.get_mut(usize::from(aci_aifsn >> 5 & 0b11)) { + *slot = Some(record); + } + } + // Four records fill all four categories only if no ACI repeats. + let [Some(be), Some(bk), Some(vi), Some(vo)] = access_categories else { + return Err(Refused::WmmAccessCategory); + }; + Self::Parameter { qos_info, access_categories: [be, bk, vi, vo] } + }; + if !r.is_empty() { + return Err(Refused::ElementLength); + } + Ok(Some(wmm)) + } +} diff --git a/lib/ieee80211/src/frame.rs b/lib/ieee80211/src/frame.rs new file mode 100644 index 00000000000..9b4f3486bae --- /dev/null +++ b/lib/ieee80211/src/frame.rs @@ -0,0 +1,261 @@ +//! The MAC header (§9.2.3, §9.2.4) of the frames a station handles: every management subtype it +//! takes part in, BlockAckReq and BlockAck, and Data, Null, QoS Data and QoS Null. +//! +//! The input is one MPDU without its FCS. A body is handed back undecoded, because a protected +//! one is ciphertext until the caller removes its protection: management bodies decode in +//! [`crate::mgmt`], data bodies in [`crate::data`]. + +use crate::control::{BlockAck, BlockAckReq}; +use crate::read::Reader; +use crate::{Address, Refused, Seq}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Frame<'a> { + Management(Management<'a>), + BlockAckReq(BlockAckReq), + BlockAck(BlockAck), + Data(Data<'a>), +} + +/// The Frame Control flags a receiver acts on (§9.2.4.1). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Flags { + pub retry: bool, + pub power_management: bool, + pub more_data: bool, + pub protected: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ManagementSubtype { + AssociationRequest, + AssociationResponse, + ReassociationRequest, + ReassociationResponse, + ProbeRequest, + ProbeResponse, + Beacon, + Disassociation, + Authentication, + Deauthentication, + Action, + ActionNoAck, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Management<'a> { + pub flags: Flags, + pub subtype: ManagementSubtype, + pub duration: u16, + pub da: Address, + pub sa: Address, + pub bssid: Address, + pub seq: Seq, + pub ht_control: Option, + pub body: &'a [u8], +} + +/// To DS and From DS (Table 9-4). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Ds { + /// Station to station within one BSS. + Neither, + /// Station to access point. + ToDs, + /// Access point to station. + FromDs, + /// The four-address format, with its Address 4. + Both(Address), +} + +/// The QoS Control field (§9.2.4.5). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct QosControl(pub u16); + +impl QosControl { + pub const fn tid(self) -> u8 { + let [low, _] = self.0.to_le_bytes(); + low & 0x0F + } + + /// The Ack Policy subfield, bits 5 and 6 (Table 9-11). + pub const fn ack_policy(self) -> u8 { + let [low, _] = self.0.to_le_bytes(); + (low >> 5) & 0b11 + } + + /// The A-MSDU Present subfield, bit 7. + pub const fn amsdu(self) -> bool { + self.0 & 0x0080 != 0 + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Data<'a> { + pub flags: Flags, + pub ds: Ds, + pub duration: u16, + pub addr1: Address, + pub addr2: Address, + pub addr3: Address, + pub seq: Seq, + pub qos: Option, + pub ht_control: Option, + /// A Null or QoS Null frame, which carries no MSDU. + pub null: bool, + pub body: &'a [u8], +} + +impl Data<'_> { + /// The receiver, which Address 1 always is. + pub const fn ra(&self) -> Address { + self.addr1 + } + + /// The transmitter, which Address 2 always is. + pub const fn ta(&self) -> Address { + self.addr2 + } + + /// The MSDU's destination (Table 9-30). + pub const fn da(&self) -> Address { + match self.ds { + Ds::Neither | Ds::FromDs => self.addr1, + Ds::ToDs | Ds::Both(_) => self.addr3, + } + } + + /// The MSDU's source (Table 9-30). + pub const fn sa(&self) -> Address { + match self.ds { + Ds::Neither | Ds::ToDs => self.addr2, + Ds::FromDs => self.addr3, + Ds::Both(addr4) => addr4, + } + } +} + +impl<'a> Frame<'a> { + pub fn parse(bytes: &'a [u8]) -> Result { + let mut r = Reader::new(bytes); + let [fc0, fc1] = r.array()?; + if fc0 & 0b11 != 0 { + return Err(Refused::ProtocolVersion); + } + let subtype = fc0 >> 4; + let flags = Flags { + retry: fc1 & 0x08 != 0, + power_management: fc1 & 0x10 != 0, + more_data: fc1 & 0x20 != 0, + protected: fc1 & 0x40 != 0, + }; + let ds_bits = fc1 & 0b11; + let more_fragments = fc1 & 0x04 != 0; + let order = fc1 & 0x80 != 0; + let duration = r.u16()?; + match (fc0 >> 2) & 0b11 { + 0 => { + let subtype = management_subtype(subtype)?; + if ds_bits != 0 { + return Err(Refused::ManagementWithDs); + } + let da = r.address()?; + let sa = r.address()?; + let bssid = r.address()?; + let seq = sequence_control(r.u16()?, more_fragments)?; + if sa.is_group() { + return Err(Refused::GroupTransmitter); + } + let ht_control = if order { Some(r.u32()?) } else { None }; + Ok(Self::Management(Management { + flags, + subtype, + duration, + da, + sa, + bssid, + seq, + ht_control, + body: r.rest(), + })) + } + 1 => match subtype { + 8 => BlockAckReq::parse(r.rest()).map(Self::BlockAckReq), + 9 => BlockAck::parse(r.rest()).map(Self::BlockAck), + _ => Err(Refused::UnhandledControl), + }, + 2 => { + let (qos, null) = match subtype { + 0 => (false, false), + 4 => (false, true), + 8 => (true, false), + 12 => (true, true), + 13 => return Err(Refused::ReservedSubtype), + _ => return Err(Refused::ContentionFreeData), + }; + if order && !qos { + return Err(Refused::StrictlyOrdered); + } + let addr1 = r.address()?; + let addr2 = r.address()?; + let addr3 = r.address()?; + let seq = sequence_control(r.u16()?, more_fragments)?; + let ds = match ds_bits { + 0 => Ds::Neither, + 1 => Ds::ToDs, + 2 => Ds::FromDs, + _ => Ds::Both(r.address()?), + }; + if addr2.is_group() { + return Err(Refused::GroupTransmitter); + } + let qos = if qos { Some(QosControl(r.u16()?)) } else { None }; + let ht_control = if order { Some(r.u32()?) } else { None }; + Ok(Self::Data(Data { + flags, + ds, + duration, + addr1, + addr2, + addr3, + seq, + qos, + ht_control, + null, + body: r.rest(), + })) + } + _ => Err(Refused::ExtensionFrame), + } + } +} + +/// Table 9-1. Timing Advertisement and ATIM are no station's business in an infrastructure BSS. +fn management_subtype(subtype: u8) -> Result { + use ManagementSubtype::*; + Ok(match subtype { + 0 => AssociationRequest, + 1 => AssociationResponse, + 2 => ReassociationRequest, + 3 => ReassociationResponse, + 4 => ProbeRequest, + 5 => ProbeResponse, + 8 => Beacon, + 10 => Disassociation, + 11 => Authentication, + 12 => Deauthentication, + 13 => Action, + 14 => ActionNoAck, + 7 | 15 => return Err(Refused::ReservedSubtype), + _ => return Err(Refused::UnhandledManagement), + }) +} + +/// A fragment is refused rather than reassembled: no reassembly buffer exists to mix fragments +/// of different keys or frames in (Vanhoef, "Fragment and Forge", USENIX Security 2021, §5). +fn sequence_control(field: u16, more_fragments: bool) -> Result { + if more_fragments || field & 0x000F != 0 { + return Err(Refused::Fragmented); + } + Ok(Seq::new(field >> 4)) +} diff --git a/lib/ieee80211/src/lib.rs b/lib/ieee80211/src/lib.rs new file mode 100644 index 00000000000..57aa8172565 --- /dev/null +++ b/lib/ieee80211/src/lib.rs @@ -0,0 +1,172 @@ +//! IEEE Std 802.11-2020 frames as a station meets them, with 802.11ax-2021's HE elements and +//! BlockAck bitmaps and the Wi-Fi Alliance's WMM element: parsed in place from bytes a radio or a +//! peer wrote, and refused by the first rule they break, by name. +//! +//! [`frame::Frame::parse`] takes one MPDU without its FCS and yields a typed management, control +//! or data frame whose body still borrows the input. Elements are read by [`element`], A-MSDU +//! and the translation to and from Ethernet by [`data`], and the receive reordering buffer is +//! [`reorder::Window`]. +//! +//! A [`Refused`] is either [`Class::Malformed`], input no correct sender produces, or +//! [`Class::Unsupported`], a well-formed frame this station does not handle. + +#![no_std] +#![forbid(unsafe_code)] +#![cfg_attr( + not(test), + forbid( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::as_conversions + ) +)] + +extern crate alloc; + +pub mod control; +pub mod data; +pub mod element; +pub mod frame; +pub mod mgmt; +pub mod reorder; + +mod read; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Class { + /// No correct sender produces these bytes. + Malformed, + /// Well formed, and not something this station handles. + Unsupported, +} + +macro_rules! refusals { + ($($variant:ident = $class:ident, $why:literal;)*) => { + /// Why a frame or element was not accepted, or a reordering buffer not made. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub enum Refused { + $(#[doc = $why] $variant,)* + } + + impl Refused { + pub const ALL: &'static [Self] = &[$(Self::$variant,)*]; + + pub const fn class(self) -> Class { + match self { + $(Self::$variant => Class::$class,)* + } + } + + pub const fn reason(self) -> &'static str { + match self { + $(Self::$variant => $why,)* + } + } + } + }; +} + +refusals! { + // The MAC header, 9.2. + Truncated = Malformed, "the bytes end inside a field"; + ProtocolVersion = Unsupported, "the protocol version is not 0 (§9.2.4.1.2)"; + ExtensionFrame = Unsupported, "a frame of type Extension (DMG and S1G only)"; + ReservedSubtype = Malformed, "a subtype the standard reserves"; + ManagementWithDs = Malformed, "a management frame with To DS or From DS set"; + UnhandledManagement = Unsupported, "a management subtype a station does not handle"; + UnhandledControl = Unsupported, "a control frame other than BlockAckReq or BlockAck"; + ContentionFreeData = Unsupported, "a data subtype of the point coordination function"; + StrictlyOrdered = Unsupported, "a non-QoS data frame with the Order bit set"; + Fragmented = Unsupported, "a fragment of an MSDU or MMPDU"; + GroupTransmitter = Malformed, "a transmitter address with the group bit set"; + // Management bodies, §9.3.3. + ZeroBeaconInterval = Malformed, "a beacon interval of 0 time units"; + AssociationId = Malformed, "an association ID outside 1 to 2007 in a successful response"; + // Elements, §9.4.2. + ElementPastBody = Malformed, "an element's length runs past the frame body"; + ExtensionWithoutId = Malformed, "an Element ID Extension element of length 0"; + DuplicateElement = Malformed, "an element that may appear once appears twice"; + ElementLength = Malformed, "an element whose length its definition does not allow"; + SsidLength = Malformed, "an SSID longer than 32 octets"; + RateCount = Malformed, "a Supported Rates element of 0 or more than 8 rates"; + DtimPeriod = Malformed, "a TIM element with a DTIM period of 0"; + TimBitmap = Malformed, "a TIM partial virtual bitmap past AID 2007"; + CountryString = Malformed, "a country string whose first two octets are not upper-case letters"; + CountryPad = Malformed, "a Country element whose pad octet is not 0"; + CountryTriplet = Malformed, "a Country element whose triplets do not tile it"; + CountryChannels = Malformed, "a subband triplet of 0 channels, or running past channel 200"; + RsnVersion = Unsupported, "an RSN element of a version other than 1"; + RsnCount = Malformed, "an RSN suite count running past the element"; + MfpRequiredNotCapable = Malformed, "management frame protection required but not capable"; + ExtendedRsnLength = Malformed, "an RSNXE whose field length runs past the element"; + SecondaryChannelOffset = Malformed, "an HT Operation secondary channel offset of 2"; + VhtReserved = Malformed, "a VHT capability or operation field of a reserved value"; + PpeThresholds = Malformed, "HE PPE Thresholds whose length its own NSTS and RU mask do not give"; + WmmVersion = Unsupported, "a WMM element of a version other than 1"; + WmmAifsn = Malformed, "a WMM AC Parameter Record with an AIFSN below 2"; + WmmAccessCategory = Malformed, "a WMM Parameter element that does not give each access category once"; + // Control frames, §9.3.1. + BlockAckVariant = Unsupported, "a block-ack variant other than Compressed"; + BitmapSize = Unsupported, "a block-ack Fragment Number this station does not handle: a fragment level, or a bitmap other than 32, 64, 128 or 256 bits"; + ControlLength = Malformed, "a block-ack frame whose length its variant does not give"; + // Data, §9.3.2 and §12.5.3. + SubframeLength = Malformed, "an A-MSDU subframe whose length runs past the MPDU"; + SubframePadding = Malformed, "an A-MSDU subframe whose padding runs past the MPDU"; + SnapHeaderAsAddress = Malformed, "an A-MSDU whose first destination is an LLC/SNAP header"; + NotSnap = Unsupported, "an MSDU that is not LLC/SNAP"; + UnknownOui = Unsupported, "an LLC/SNAP header with an OUI other than RFC 1042's or bridge-tunnel's"; + TranslatedType = Unsupported, "an RFC 1042 header carrying a type 802.1H sends by bridge-tunnel"; + LengthField = Unsupported, "an EtherType below 0x0600, which is an IEEE 802.3 length"; + ExtendedIvClear = Malformed, "a CCMP header without its Ext IV bit"; + // The reordering buffer, §10.25.6. + WindowSize = Malformed, "a reordering buffer of 0 or more than 256 MPDUs"; +} + +impl core::fmt::Display for Refused { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.reason()) + } +} + +/// A 48-bit MAC address (IEEE 802-2014 §8). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Address(pub [u8; 6]); + +impl Address { + pub const BROADCAST: Self = Self([0xFF; 6]); + + /// The Individual/Group bit, the least significant bit of the first octet. + pub const fn is_group(self) -> bool { + let [first, ..] = self.0; + first & 1 != 0 + } +} + +/// A 12-bit sequence number, compared modulo 4096 (§9.2.4.4.2, §10.3.2.14). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Seq(u16); + +impl Seq { + pub const MODULUS: u16 = 4096; + + /// The low 12 bits of `value`. + pub const fn new(value: u16) -> Self { + Self(value & 0x0FFF) + } + + pub const fn get(self) -> u16 { + self.0 + } + + pub const fn add(self, n: u16) -> Self { + Self::new(self.0.wrapping_add(n)) + } + + /// How far `self` is ahead of `base`, modulo 4096. + pub const fn since(self, base: Self) -> u16 { + self.0.wrapping_sub(base.0) & 0x0FFF + } +} diff --git a/lib/ieee80211/src/mgmt.rs b/lib/ieee80211/src/mgmt.rs new file mode 100644 index 00000000000..66898038c01 --- /dev/null +++ b/lib/ieee80211/src/mgmt.rs @@ -0,0 +1,128 @@ +//! Management frame bodies (§9.3.3): the fixed fields of each subtype, and its elements still as +//! bytes for [`crate::element::Elements::parse`]. +//! +//! An Authentication frame's fields past its status, an SAE exchange's among them, and an Action +//! frame's past its category are its algorithm's or category's to read. + +use crate::frame::ManagementSubtype; +use crate::read::Reader; +use crate::{Address, Refused}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Body<'a> { + Beacon(Beacon<'a>), + ProbeResponse(Beacon<'a>), + ProbeRequest { elements: &'a [u8] }, + Authentication(Authentication<'a>), + Deauthentication { reason: u16, rest: &'a [u8] }, + Disassociation { reason: u16, rest: &'a [u8] }, + AssociationRequest(AssociationRequest<'a>), + ReassociationRequest(AssociationRequest<'a>), + AssociationResponse(AssociationResponse<'a>), + ReassociationResponse(AssociationResponse<'a>), + Action { category: u8, rest: &'a [u8] }, + ActionNoAck { category: u8, rest: &'a [u8] }, +} + +/// A Beacon or Probe Response (§9.3.3.2, §9.3.3.10). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Beacon<'a> { + pub timestamp: u64, + /// In time units of 1,024 µs, never 0. + pub interval: u16, + pub capability: u16, + pub elements: &'a [u8], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Authentication<'a> { + pub algorithm: u16, + pub transaction: u16, + pub status: u16, + pub rest: &'a [u8], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AssociationRequest<'a> { + pub capability: u16, + pub listen_interval: u16, + /// A reassociation's Current AP Address. + pub current_ap: Option
, + pub elements: &'a [u8], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AssociationResponse<'a> { + pub capability: u16, + pub status: u16, + /// 1 to 2007 when the status is success, and absent otherwise. + pub aid: Option, + pub elements: &'a [u8], +} + +/// The Authentication Algorithm Number of SAE (Table 9-43). +pub const ALGORITHM_SAE: u16 = 3; +pub const ALGORITHM_OPEN: u16 = 0; +pub const STATUS_SUCCESS: u16 = 0; + +impl<'a> Body<'a> { + pub fn parse(subtype: ManagementSubtype, body: &'a [u8]) -> Result { + use ManagementSubtype as S; + let mut r = Reader::new(body); + Ok(match subtype { + S::Beacon => Self::Beacon(Beacon::read(&mut r)?), + S::ProbeResponse => Self::ProbeResponse(Beacon::read(&mut r)?), + S::ProbeRequest => Self::ProbeRequest { elements: body }, + S::Authentication => Self::Authentication(Authentication { + algorithm: r.u16()?, + transaction: r.u16()?, + status: r.u16()?, + rest: r.rest(), + }), + S::Deauthentication => Self::Deauthentication { reason: r.u16()?, rest: r.rest() }, + S::Disassociation => Self::Disassociation { reason: r.u16()?, rest: r.rest() }, + S::AssociationRequest => Self::AssociationRequest(AssociationRequest::read(&mut r, false)?), + S::ReassociationRequest => Self::ReassociationRequest(AssociationRequest::read(&mut r, true)?), + S::AssociationResponse => Self::AssociationResponse(AssociationResponse::read(&mut r)?), + S::ReassociationResponse => Self::ReassociationResponse(AssociationResponse::read(&mut r)?), + S::Action => Self::Action { category: r.u8()?, rest: r.rest() }, + S::ActionNoAck => Self::ActionNoAck { category: r.u8()?, rest: r.rest() }, + }) + } +} + +impl<'a> Beacon<'a> { + fn read(r: &mut Reader<'a>) -> Result { + let timestamp = r.u64()?; + let interval = r.u16()?; + let capability = r.u16()?; + if interval == 0 { + return Err(Refused::ZeroBeaconInterval); + } + Ok(Self { timestamp, interval, capability, elements: r.rest() }) + } +} + +impl<'a> AssociationRequest<'a> { + fn read(r: &mut Reader<'a>, reassociation: bool) -> Result { + let capability = r.u16()?; + let listen_interval = r.u16()?; + let current_ap = if reassociation { Some(r.address()?) } else { None }; + Ok(Self { capability, listen_interval, current_ap, elements: r.rest() }) + } +} + +impl<'a> AssociationResponse<'a> { + fn read(r: &mut Reader<'a>) -> Result { + let capability = r.u16()?; + let status = r.u16()?; + // The two most significant bits of the AID field are set (§9.4.1.8). + let aid = r.u16()? & 0x3FFF; + let aid = match status { + STATUS_SUCCESS if (1..=2007).contains(&aid) => Some(aid), + STATUS_SUCCESS => return Err(Refused::AssociationId), + _ => None, + }; + Ok(Self { capability, status, aid, elements: r.rest() }) + } +} diff --git a/lib/ieee80211/src/read.rs b/lib/ieee80211/src/read.rs new file mode 100644 index 00000000000..d7152178178 --- /dev/null +++ b/lib/ieee80211/src/read.rs @@ -0,0 +1,54 @@ +//! A cursor over untrusted bytes: every read either yields the field or [`Refused::Truncated`]. + +use crate::{Address, Refused}; + +pub(crate) struct Reader<'a> { + rest: &'a [u8], +} + +impl<'a> Reader<'a> { + pub(crate) const fn new(bytes: &'a [u8]) -> Self { + Self { rest: bytes } + } + + pub(crate) const fn rest(&self) -> &'a [u8] { + self.rest + } + + pub(crate) const fn is_empty(&self) -> bool { + self.rest.is_empty() + } + + pub(crate) fn take(&mut self, n: usize) -> Result<&'a [u8], Refused> { + let (head, tail) = self.rest.split_at_checked(n).ok_or(Refused::Truncated)?; + self.rest = tail; + Ok(head) + } + + pub(crate) fn array(&mut self) -> Result<[u8; N], Refused> { + let (head, tail) = self.rest.split_first_chunk::().ok_or(Refused::Truncated)?; + self.rest = tail; + Ok(*head) + } + + pub(crate) fn u8(&mut self) -> Result { + let [b] = self.array()?; + Ok(b) + } + + pub(crate) fn u16(&mut self) -> Result { + self.array().map(u16::from_le_bytes) + } + + pub(crate) fn u32(&mut self) -> Result { + self.array().map(u32::from_le_bytes) + } + + pub(crate) fn u64(&mut self) -> Result { + self.array().map(u64::from_le_bytes) + } + + pub(crate) fn address(&mut self) -> Result { + self.array().map(Address) + } +} diff --git a/lib/ieee80211/src/reorder.rs b/lib/ieee80211/src/reorder.rs new file mode 100644 index 00000000000..6e90c4959ea --- /dev/null +++ b/lib/ieee80211/src/reorder.rs @@ -0,0 +1,128 @@ +//! The receive reordering buffer of one block-ack agreement (§10.25.6): MPDUs held by sequence +//! number and passed up in order. +//! +//! A frame in the window is held, and the run from the window's start passes up. A frame past +//! the window's end, within half the sequence space, moves the window to end at it, passing up +//! everything held before the new start. One further back than that is old, and one already held +//! is a duplicate: both are dropped, the ordinary fate of a retransmission. A +//! BlockAckReq's Starting Sequence Number moves the start forward as a frame would, and so does +//! a device's release or a timeout, through [`Window::release_to`]. + +use alloc::vec::Vec; +use core::num::{NonZeroU16, NonZeroUsize}; + +use crate::{Refused, Seq}; + +/// The largest buffer an HE agreement negotiates (802.11ax-2021 §9.4.1.14); HT and VHT stop at 64. +pub const MAX_SIZE: u16 = 256; +const HALF: u16 = Seq::MODULUS / 2; + +/// What became of a received frame. +#[must_use] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Received { + /// Held, or passed up with the run it completed. + Taken, + /// Dropped: before the window. + Old, + /// Dropped: its sequence number is already held. + Duplicate, +} + +pub struct Window { + start: Seq, + /// Slot `(head + n) % size` holds sequence number `start + n`. + slots: Vec>, + size: NonZeroU16, + head: usize, +} + +impl Window { + /// A window of `size` MPDUs starting at the agreement's Starting Sequence Number. + pub fn new(ssn: Seq, size: u16) -> Result { + let Some(size) = NonZeroU16::new(size).filter(|s| s.get() <= MAX_SIZE) else { + return Err(Refused::WindowSize); + }; + let mut slots = Vec::new(); + slots.resize_with(usize::from(size.get()), || None); + Ok(Self { start: ssn, slots, size, head: 0 }) + } + + pub const fn start(&self) -> Seq { + self.start + } + + /// Take `item`, received with sequence number `seq`, and pass up into `out` every item it + /// puts in order. + pub fn receive(&mut self, seq: Seq, item: T, out: &mut Vec) -> Received { + let ahead = seq.since(self.start); + if ahead >= HALF { + return Received::Old; + } + // A frame past the window's end becomes its end: the start moves to `size - 1` before it. + if let Some(past) = ahead.checked_sub(self.size.get().saturating_sub(1)).filter(|&p| p > 0) { + self.advance(past, out); + } + let offset = usize::from(seq.since(self.start)); + let slot = self.slot(offset); + if slot.is_some() { + return Received::Duplicate; + } + *slot = Some(item); + self.pass_run(out); + Received::Taken + } + + /// Move the window's start forward to `ssn`, passing up every held item before it, then the + /// run from it. A `ssn` at or behind the start changes nothing. + pub fn release_to(&mut self, ssn: Seq, out: &mut Vec) { + let ahead = ssn.since(self.start); + if ahead == 0 || ahead >= HALF { + return; + } + self.advance(ahead, out); + self.pass_run(out); + } + + /// The sequence number of the first item held, which a timeout releases to. + pub fn first_held(&self) -> Option { + (0..self.slots.len()) + .find(|&n| self.peek(n)) + .and_then(|n| u16::try_from(n).ok()) + .map(|n| self.start.add(n)) + } + + fn index(&self, offset: usize) -> usize { + self.head.wrapping_add(offset) % NonZeroUsize::from(self.size) + } + + fn peek(&self, offset: usize) -> bool { + self.slots.get(self.index(offset)).is_some_and(Option::is_some) + } + + fn slot(&mut self, offset: usize) -> &mut Option { + let index = self.index(offset); + match self.slots.get_mut(index) { + Some(slot) => slot, + None => unreachable!("an index modulo the slot count is a slot"), + } + } + + /// Move the start `n` forward, passing up what the slots it leaves hold. + fn advance(&mut self, n: u16, out: &mut Vec) { + for _ in 0..usize::from(n).min(self.slots.len()) { + out.extend(self.slot(0).take()); + self.head = self.index(1); + } + self.start = self.start.add(n); + } + + /// Pass up the run of held items from the start. + fn pass_run(&mut self, out: &mut Vec) { + while let Some(item) = self.slot(0).take() { + out.push(item); + self.head = self.index(1); + self.start = self.start.add(1); + } + } +} diff --git a/lib/ieee80211/tests/common/mod.rs b/lib/ieee80211/tests/common/mod.rs new file mode 100644 index 00000000000..5af704c4f5f --- /dev/null +++ b/lib/ieee80211/tests/common/mod.rs @@ -0,0 +1,150 @@ +#![allow(dead_code)] + +use toyos_ieee80211::Address; + +/// SplitMix64: a seeded stream, so a fuzz red names its iteration. +pub struct Rng(pub u64); + +impl Rng { + pub fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + pub fn below(&mut self, n: usize) -> usize { + (self.next() % n as u64) as usize + } + + pub fn chance(&mut self, percent: usize) -> bool { + self.below(100) < percent + } + + pub fn bytes(&mut self, max: usize) -> Vec { + let len = self.below(max + 1); + (0..len).map(|_| self.next() as u8).collect() + } + + pub fn address(&mut self) -> Address { + let mut a = [0; 6]; + a.iter_mut().for_each(|b| *b = self.next() as u8); + a[0] &= !1; + Address(a) + } +} + +pub const AP: Address = Address([0x02, 0x11, 0x22, 0x33, 0x44, 0x55]); +pub const STA: Address = Address([0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE]); +pub const PEER: Address = Address([0x02, 0x01, 0x02, 0x03, 0x04, 0x05]); + +/// A management frame's header: Frame Control's subtype, then DA, SA and BSSID. +pub fn management(subtype: u8, body: &[u8]) -> Vec { + let mut f = vec![subtype << 4, 0, 0, 0]; + f.extend(STA.0); + f.extend(AP.0); + f.extend(AP.0); + f.extend([0x10, 0x00]); + f.extend(body); + f +} + +/// An element: its ID, its length, its body. +pub fn element(id: u8, body: &[u8]) -> Vec { + let mut e = vec![id, body.len() as u8]; + e.extend(body); + e +} + +/// A Beacon body around `elements`. +pub fn beacon(elements: &[u8]) -> Vec { + let mut b = vec![0; 8]; + b.extend(100u16.to_le_bytes()); + b.extend(0x0431u16.to_le_bytes()); + b.extend(elements); + b +} + +/// A QoS Data frame from the access point, its A-MSDU Present bit as given. +pub fn qos_from_ds(amsdu: bool, body: &[u8]) -> Vec { + let mut f = vec![0x88, 0x02, 0, 0]; + f.extend(STA.0); + f.extend(AP.0); + f.extend(PEER.0); + f.extend([0x20, 0x00]); + f.extend([if amsdu { 0x80 } else { 0 }, 0]); + f.extend(body); + f +} + +/// An A-MSDU subframe, padded to four octets unless it is the last. +pub fn subframe(da: Address, sa: Address, msdu: &[u8], last: bool) -> Vec { + let mut s = Vec::new(); + s.extend(da.0); + s.extend(sa.0); + s.extend((msdu.len() as u16).to_be_bytes()); + s.extend(msdu); + while !last && s.len() % 4 != 0 { + s.push(0); + } + s +} + +/// One record of a pcap file: its link type and the bytes captured. +pub struct Record { + pub linktype: u32, + pub bytes: Vec, +} + +/// The records of a classic pcap file (draft-ietf-opsawg-pcap), either byte order. +pub fn pcap(file: &[u8]) -> Vec { + let magic = u32::from_le_bytes(file[..4].try_into().unwrap()); + let le = match magic { + 0xa1b2_c3d4 | 0xa1b2_3c4d => true, + 0xd4c3_b2a1 | 0x4d3c_b2a1 => false, + _ => panic!("not a pcap file: magic {magic:#x}"), + }; + let word = |at: usize| { + let b: [u8; 4] = file[at..at + 4].try_into().unwrap(); + if le { u32::from_le_bytes(b) } else { u32::from_be_bytes(b) } + }; + let linktype = word(20) & 0x0FFF_FFFF; + let mut out = Vec::new(); + let mut at = 24; + while at < file.len() { + let len = word(at + 8) as usize; + out.push(Record { linktype, bytes: file[at + 16..at + 16 + len].to_vec() }); + at += 16 + len; + } + out +} + +/// The 802.11 frame a record holds, without a radiotap header (radiotap.org) and without an FCS: +/// a trailing four octets that are the CRC-32 of what precedes them are the FCS. +pub fn mpdu(record: &Record) -> Vec { + let frame = match record.linktype & 0xFFFF { + 105 => &record.bytes[..], + 127 => { + let len = u16::from_le_bytes([record.bytes[2], record.bytes[3]]) as usize; + &record.bytes[len.min(record.bytes.len())..] + } + other => panic!("link type {other} is not 802.11"), + }; + match frame.split_last_chunk::<4>() { + Some((body, fcs)) if crc32(body) == u32::from_le_bytes(*fcs) => body.to_vec(), + _ => frame.to_vec(), + } +} + +/// CRC-32 of IEEE 802.3 §3.2.9, which 802.11's FCS is (§9.2.4.8). +pub fn crc32(bytes: &[u8]) -> u32 { + let mut crc = !0u32; + for &b in bytes { + crc ^= u32::from(b); + for _ in 0..8 { + crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 }; + } + } + !crc +} diff --git a/lib/ieee80211/tests/corpus.rs b/lib/ieee80211/tests/corpus.rs new file mode 100644 index 00000000000..940544cc63a --- /dev/null +++ b/lib/ieee80211/tests/corpus.rs @@ -0,0 +1,148 @@ +//! Frames other implementations wrote, from tcpdump's test captures (`fixtures/tcpdump/`, pinned in +//! `NOTICE`), each read as the standard says its bytes read. + +mod common; + +use toyos_ieee80211::data::{msdus, CcmpHeader, Ethernet}; +use toyos_ieee80211::element::{Elements, SecondaryChannel, Suite, VhtChannelWidth}; +use toyos_ieee80211::frame::{Data, Frame, ManagementSubtype}; +use toyos_ieee80211::mgmt::Body; +use toyos_ieee80211::Address; + +fn frames(name: &str) -> Vec> { + let path = format!("{}/tests/fixtures/tcpdump/{name}.pcap", env!("CARGO_MANIFEST_DIR")); + let file = std::fs::read(&path).unwrap_or_else(|e| panic!("{path}: {e}")); + common::pcap(&file).iter().map(common::mpdu).collect() +} + +/// A frame's outcome in a word: its kind, or its own or its body's or elements' refusal. +fn outcome(mpdu: &[u8]) -> String { + match Frame::parse(mpdu) { + Err(e) => format!("{e:?}"), + Ok(Frame::Data(d)) => format!("{}Data", if d.null { "Null" } else { "" }), + Ok(Frame::BlockAck(_) | Frame::BlockAckReq(_)) => "BlockAck".into(), + Ok(Frame::Management(m)) => { + let elements = match Body::parse(m.subtype, m.body) { + Err(e) => return format!("{:?}:{e:?}", m.subtype), + Ok(Body::Beacon(b) | Body::ProbeResponse(b)) => Some(b.elements), + Ok(Body::ProbeRequest { elements }) => Some(elements), + Ok(Body::AssociationRequest(a) | Body::ReassociationRequest(a)) => Some(a.elements), + Ok(Body::AssociationResponse(a) | Body::ReassociationResponse(a)) => Some(a.elements), + Ok(_) => None, + }; + match elements.map(Elements::parse) { + Some(Err(e)) => format!("{:?}:{e:?}", m.subtype), + _ => format!("{:?}", m.subtype), + } + } + } +} + +fn data(mpdu: &[u8]) -> Data<'_> { + match Frame::parse(mpdu) { + Ok(Frame::Data(d)) => d, + other => panic!("not a data frame: {other:?}"), + } +} + +#[test] +fn every_capture_frame_reads_as_its_bytes_say_or_is_refused_by_name() { + let mut seen = Vec::new(); + for name in [ + "ieee802.11_exthdr", + "ieee802.11_htc", + "ieee802.11_meshid", + "ieee802.11_rx-stbc", + "ieee802.11_parse_elements_oobr", + "ieee802.11_rates_oobr", + "ieee802.11_tim_ie_oobr", + "ieee802.11_meshhdr-oobr", + ] { + seen.push(format!("{name}: {}", frames(name).iter().map(|f| outcome(f)).collect::>().join(" "))); + } + let exthdr_scan = "ProbeRequest UnhandledControl ProbeResponse ".repeat(6); + let expected = [ + format!( + "ieee802.11_exthdr: {exthdr_scan}Authentication UnhandledControl Authentication AssociationRequest \ + UnhandledControl AssociationResponse NullData NullData" + ), + "ieee802.11_htc: Data".into(), + "ieee802.11_meshid: Beacon ProbeRequest ProbeResponse".into(), + "ieee802.11_rx-stbc: Data Data Data".into(), + "ieee802.11_parse_elements_oobr: Beacon:RsnVersion".into(), + "ieee802.11_rates_oobr: ProtocolVersion".into(), + "ieee802.11_tim_ie_oobr: ReassociationResponse:RsnVersion ReassociationResponse:ElementPastBody \ + Truncated ReassociationResponse:ElementPastBody" + .into(), + "ieee802.11_meshhdr-oobr: ProtocolVersion".into(), + ]; + assert_eq!(seen, expected); +} + +#[test] +fn a_captured_sae_beacon_reads_its_rsn_ht_and_vht_operation() { + let beacon = &frames("ieee802.11_meshid")[0]; + let Ok(Frame::Management(m)) = Frame::parse(beacon) else { panic!("not management") }; + assert_eq!(m.subtype, ManagementSubtype::Beacon); + let Ok(Body::Beacon(b)) = Body::parse(m.subtype, m.body) else { panic!("not a beacon body") }; + let e = Elements::parse(b.elements).unwrap(); + + assert_eq!(e.dsss_channel, Some(149)); + let rsn = e.rsn.unwrap(); + assert_eq!(rsn.group_data, Some(Suite::CCMP_128)); + assert!(rsn.pairwise.unwrap().contains(Suite::CCMP_128)); + assert!(rsn.akm.unwrap().contains(Suite::AKM_SAE)); + assert!(!rsn.mfp_capable()); + let ht = e.ht_operation.unwrap(); + assert_eq!((ht.primary_channel, ht.secondary), (149, SecondaryChannel::Above)); + let vht = e.vht_operation.unwrap(); + // Channels 149 to 161 are one 80 MHz channel, centred on 155. + assert_eq!((vht.width, vht.center_segment0, vht.center_segment1), (VhtChannelWidth::Eighty, 155, 0)); + let tim = e.tim.unwrap(); + assert_eq!((tim.dtim_count, tim.dtim_period, tim.group_buffered()), (1, 2, false)); +} + +#[test] +fn a_captured_association_response_carries_the_wmm_default_parameters() { + use toyos_ieee80211::element::{AcParameters, Wmm}; + let response = &frames("ieee802.11_exthdr")[23]; + let Ok(Frame::Management(m)) = Frame::parse(response) else { panic!("not management") }; + let Ok(Body::AssociationResponse(r)) = Body::parse(m.subtype, m.body) else { panic!("not a response") }; + assert_eq!(r.aid, Some(1)); + let Some(Wmm::Parameter { access_categories: [be, bk, vi, vo], .. }) = Elements::parse(r.elements).unwrap().wmm else { + panic!("no WMM Parameter element") + }; + let ac = |aifsn, ecw_min, ecw_max, txop_limit| AcParameters { aifsn, admission_control: false, ecw_min, ecw_max, txop_limit }; + // AC_VI's TXOP is 3.008 ms and AC_VO's 1.504 ms: 94 and 47 units of 32 µs. + assert_eq!([be, bk, vi, vo], [ac(3, 4, 10, 0), ac(7, 4, 10, 0), ac(2, 3, 4, 94), ac(2, 2, 3, 47)]); +} + +#[test] +fn a_captured_ccmp_frame_keeps_its_header_and_carries_an_ipv4_msdu() { + let frame = &frames("ieee802.11_rx-stbc")[0]; + let d = data(frame); + assert!(d.flags.protected); + let (ccmp, plaintext) = CcmpHeader::split(d.body).unwrap(); + assert_eq!((ccmp.pn, ccmp.key_id), (17, 0)); + + let msdu = msdus(&d, plaintext).unwrap().next().unwrap().unwrap(); + let eth = Ethernet::from_msdu(msdu).unwrap(); + // From DS: Address 1 is the destination and Address 3 the source. + assert_eq!((eth.dst, eth.src), (d.addr1, d.addr3)); + assert_eq!(eth.ethertype, 0x0800); + assert_eq!(eth.payload[..2], [0x45, 0x00]); +} + +#[test] +fn a_captured_to_ds_frame_with_ht_control_reaches_ethernet_as_a_broadcast_dhcp_request() { + let frame = &frames("ieee802.11_htc")[0]; + let d = data(frame); + assert!(d.ht_control.is_some()); + let msdu = msdus(&d, d.body).unwrap().next().unwrap().unwrap(); + let eth = Ethernet::from_msdu(msdu).unwrap(); + // To DS: Address 3 is the destination and Address 2 the source. + assert_eq!((eth.dst, eth.src), (Address::BROADCAST, d.addr2)); + assert_eq!(eth.ethertype, 0x0800); + let udp = ð.payload[20..24]; + assert_eq!(udp, [0, 68, 0, 67]); +} diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_exthdr.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_exthdr.pcap new file mode 100644 index 00000000000..613a4437da5 Binary files /dev/null and b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_exthdr.pcap differ diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_htc.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_htc.pcap new file mode 100644 index 00000000000..40fce117995 Binary files /dev/null and b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_htc.pcap differ diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshhdr-oobr.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshhdr-oobr.pcap new file mode 100644 index 00000000000..7d7cf895197 Binary files /dev/null and b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshhdr-oobr.pcap differ diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshid.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshid.pcap new file mode 100644 index 00000000000..7d149a78146 Binary files /dev/null and b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_meshid.pcap differ diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_parse_elements_oobr.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_parse_elements_oobr.pcap new file mode 100644 index 00000000000..a4acaf6cc53 Binary files /dev/null and b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_parse_elements_oobr.pcap differ diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rates_oobr.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rates_oobr.pcap new file mode 100644 index 00000000000..8ebff6199dc Binary files /dev/null and b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rates_oobr.pcap differ diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rx-stbc.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rx-stbc.pcap new file mode 100644 index 00000000000..b825a0a3f3b Binary files /dev/null and b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_rx-stbc.pcap differ diff --git a/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_tim_ie_oobr.pcap b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_tim_ie_oobr.pcap new file mode 100644 index 00000000000..4634bc22997 Binary files /dev/null and b/lib/ieee80211/tests/fixtures/tcpdump/ieee802.11_tim_ie_oobr.pcap differ diff --git a/lib/ieee80211/tests/fuzz.rs b/lib/ieee80211/tests/fuzz.rs new file mode 100644 index 00000000000..f1052043d96 --- /dev/null +++ b/lib/ieee80211/tests/fuzz.rs @@ -0,0 +1,290 @@ +//! **Structured fuzzing of the boundary**, a fixed count of seeded inputs, so a red names its +//! iteration. +//! +//! Frames are drawn from the grammar a radio hands over — beacons and the other management bodies +//! with lists of typed and unknown elements, BlockAckReq and BlockAck, data frames carrying LLC/SNAP +//! MSDUs or A-MSDUs, protected or not — and then sometimes truncated, bent, lengthened or given a +//! huge length. Every one is read through every layer and ends in a value or a named refusal. One +//! left unbent reads back as what was drawn. The reorder window is driven with sequence numbers +//! around a moving base and passes every accepted frame up once, in order. + +mod common; + +use std::collections::BTreeMap; + +use common::{element, Rng, AP, STA}; +use toyos_ieee80211::data::{llc_snap, msdus, CcmpHeader, Ethernet}; +use toyos_ieee80211::element::Elements; +use toyos_ieee80211::frame::Frame; +use toyos_ieee80211::mgmt::Body; +use toyos_ieee80211::reorder::{Received, Window}; +use toyos_ieee80211::{Address, Refused, Seq}; + +const FUZZ_ITERATIONS: u64 = 20_000; + +/// The Ethernet frames a data frame carries: destination, source, EtherType and payload. +type Sent = Vec<(Address, Address, u16, Vec)>; + +/// One element: often typed and well formed, sometimes bent, sometimes unknown. +fn draw_element(rng: &mut Rng) -> Vec { + let suite = |rng: &mut Rng| [0x00, 0x0F, 0xAC, rng.below(14) as u8]; + match rng.below(18) { + 0 => element(0, &rng.bytes(34)), + 1 => element(1, &rng.bytes(9)), + 2 => element(50, &rng.bytes(12)), + 3 => element(3, &rng.bytes(2)), + 4 => { + let mut body = vec![rng.below(4) as u8, rng.below(4) as u8, rng.next() as u8]; + body.extend(rng.bytes(8)); + element(5, &body) + } + 5 => { + let mut body = [b"US ", b"DE\x04", b"jp ", b"GB "][rng.below(4)].to_vec(); + for _ in 0..rng.below(5) { + body.extend([[1, 13, 20], [36, 4, 23], [190, 12, 30], [201, 81, 0], [0, 1, 1]][rng.below(5)]); + } + if rng.chance(30) { + body.push(rng.below(2) as u8); + } + element(7, &body) + } + 6 => { + let mut body = vec![1, 0]; + body.extend(suite(rng)); + for _ in 0..2 { + let n = rng.below(3); + body.extend((n as u16).to_le_bytes()); + (0..n).for_each(|_| body.extend(suite(rng))); + } + body.extend([[0x00, 0x00], [0xC0, 0x00], [0x80, 0x00], [0x40, 0x00]][rng.below(4)]); + body.truncate(rng.below(body.len() + 1).max(2)); + element(48, &body) + } + 7 => element(244, &[rng.below(4) as u8 | 0x20, rng.next() as u8][..rng.below(2) + 1]), + 8 => element(45, &rng.bytes(27)), + 9 => { + let mut body = rng.bytes(22); + body.resize(22, 0); + element(61, &body) + } + 10 => element(191, &rng.bytes(13)), + 11 => element(192, &[rng.below(5) as u8, 42, 0, 0xFC, 0xFF]), + 12 => element(127, &rng.bytes(10)), + 13 => { + let phy0 = [0, 0x08, 0x18, 0x10][rng.below(4)]; + let ppe = rng.chance(40); + let mut body = vec![35]; + body.extend((0..6).map(|_| rng.next() as u8)); + // PHY Capabilities: B3 and B4 in octet 0, PPE Thresholds Present (B55) in octet 6. + body.extend([phy0, 0, rng.next() as u8, 0, 0, 0, if ppe { 0x80 } else { 0 }, 0, 0, 0, 0]); + body.extend(vec![0xFA; 4 + 4 * (phy0.count_ones() as usize)]); + if ppe { + let first = rng.next() as u8 & 0x7F; + let bits = 7 + 6 * ((first & 7) as usize + 1) * (first >> 3).count_ones() as usize; + body.push(first); + body.extend(vec![0; bits.div_ceil(8) - 1]); + } + element(255, &body) + } + 14 => { + let present = [0u32, 1 << 14, 1 << 15, 1 << 17, 1 << 14 | 1 << 15 | 1 << 17][rng.below(5)]; + let p = present.to_le_bytes(); + let mut body = vec![36, p[0], p[1], p[2], rng.next() as u8, 0xFC, 0xFF]; + let extra = 3 * usize::from(present & 1 << 14 != 0) + usize::from(present & 1 << 15 != 0) + 5 * usize::from(present & 1 << 17 != 0); + body.extend((0..extra).map(|_| rng.next() as u8)); + element(255, &body) + } + 15 | 16 => { + let mut body = vec![0x00, 0x50, 0xF2, 0x02, rng.below(3) as u8, 1, rng.next() as u8]; + if body[4] == 1 { + body.push(0); + let mut acis = [0u8, 1, 2, 3]; + if rng.chance(10) { + acis[3] = acis[rng.below(3)]; + } + for aci in acis { + body.extend([aci << 5 | (2 + rng.below(14)) as u8, rng.next() as u8, rng.next() as u8, 0]); + } + } + element(221, &body) + } + _ => { + let id = rng.next() as u8; + if id == 255 { element(255, &rng.bytes(4)) } else { element(id, &rng.bytes(20)) } + } + } +} + +/// A management frame: Frame Control's subtype, a body of its fixed fields, and maybe elements. +fn draw_management(rng: &mut Rng) -> Vec { + let subtype = [8, 5, 4, 11, 12, 10, 0, 1, 2, 3, 13, 14][rng.below(12)]; + let mut f = common::management(subtype, &[]); + f.extend((0..12).map(|_| rng.next() as u8)); + if subtype == 8 || subtype == 5 { + f[32..34].copy_from_slice(&100u16.to_le_bytes()); + } + for _ in 0..rng.below(8) { + f.extend(draw_element(rng)); + } + f +} + +fn draw_block_ack(rng: &mut Rng) -> Vec { + let bar = rng.chance(50); + let mut f = vec![if bar { 0x84 } else { 0x94 }, 0, 0, 0]; + f.extend(STA.0); + f.extend(AP.0); + f.extend([if rng.chance(80) { 0x04 } else { rng.next() as u8 }, (rng.below(16) << 4) as u8]); + let (fragment, octets) = if bar { (0, 0) } else { [(0, 8), (2, 32), (4, 16), (6, 4)][rng.below(4)] }; + f.extend(((rng.below(4096) << 4) as u16 | fragment).to_le_bytes()); + f.extend((0..octets).map(|_| rng.next() as u8)); + f +} + +/// A data frame from the access point and the Ethernet frames it was built from. +fn draw_data(rng: &mut Rng) -> (Vec, Sent) { + let ethertypes = [0x0800, 0x0806, 0x86DD, 0x888E, 0x8137, 0x80F3]; + let mut sent = Vec::new(); + let amsdu = rng.chance(40); + let protected = rng.chance(30); + let mut body = Vec::new(); + if protected { + let pn = rng.next() & 0xFFFF_FFFF_FFFF; + let p = pn.to_le_bytes(); + body.extend([p[0], p[1], 0, 0x20 | (rng.below(4) as u8) << 6, p[2], p[3], p[4], p[5]]); + } + let count = if amsdu { 1 + rng.below(4) } else { 1 }; + for i in 0..count { + let (da, sa) = if amsdu { (rng.address(), rng.address()) } else { (STA, common::PEER) }; + let ethertype = ethertypes[rng.below(ethertypes.len())]; + let payload = rng.bytes(60); + let msdu = [&llc_snap(ethertype).unwrap()[..], &payload].concat(); + if amsdu { + body.extend(common::subframe(da, sa, &msdu, i + 1 == count)); + } else { + body.extend(&msdu); + } + sent.push((da, sa, ethertype, payload)); + } + let mut f = common::qos_from_ds(amsdu, &body); + if protected { + f[1] |= 0x40; + } + (f, sent) +} + +/// Bend `frame` one way, or not at all; whether it changed. +fn mutate(rng: &mut Rng, frame: &mut Vec) -> bool { + if !rng.chance(40) || frame.is_empty() { + return false; + } + match rng.below(4) { + 0 => frame.truncate(rng.below(frame.len())), + 1 => { + let at = rng.below(frame.len()); + frame[at] ^= 1 + rng.below(255) as u8; + } + 2 => frame.extend(rng.bytes(8).into_iter().chain([rng.next() as u8])), + _ => { + let at = rng.below(frame.len()); + frame[at] = 0xFF; + } + } + true +} + +/// Read `frame` through every layer; the first refusal, or the Ethernet frames it carries. +fn read(frame: &[u8]) -> Result { + match Frame::parse(frame)? { + Frame::Management(m) => { + let elements = match Body::parse(m.subtype, m.body)? { + Body::Beacon(b) | Body::ProbeResponse(b) => b.elements, + Body::ProbeRequest { elements } => elements, + Body::AssociationRequest(a) | Body::ReassociationRequest(a) => a.elements, + Body::AssociationResponse(a) | Body::ReassociationResponse(a) => a.elements, + _ => return Ok(Vec::new()), + }; + let e = Elements::parse(elements)?; + if let Some(tim) = e.tim { + (0..=2008).for_each(|aid| _ = tim.buffered(aid)); + } + if let Some(country) = e.country { + country.triplets().for_each(drop); + } + Ok(Vec::new()) + } + Frame::BlockAck(ba) => { + (0..4096).for_each(|s| _ = ba.acked(Seq::new(s))); + Ok(Vec::new()) + } + Frame::BlockAckReq(_) => Ok(Vec::new()), + Frame::Data(d) => { + let plaintext = if d.flags.protected { CcmpHeader::split(d.body)?.1 } else { d.body }; + let mut out = Vec::new(); + for msdu in msdus(&d, plaintext)? { + let eth = Ethernet::from_msdu(msdu?)?; + out.push((eth.dst, eth.src, eth.ethertype, eth.payload.to_vec())); + } + Ok(out) + } + } +} + +#[test] +fn every_drawn_frame_is_read_or_refused_by_name_and_an_unbent_one_reads_back() { + let mut outcomes: BTreeMap = BTreeMap::new(); + let mut read_back = 0; + for iteration in 0..FUZZ_ITERATIONS { + let mut rng = Rng(iteration); + let (mut frame, sent) = match rng.below(3) { + 0 => (draw_management(&mut rng), None), + 1 => (draw_block_ack(&mut rng), None), + _ => { + let (f, sent) = draw_data(&mut rng); + (f, Some(sent)) + } + }; + let bent = mutate(&mut rng, &mut frame); + let result = read(&frame); + let key = match &result { + Ok(_) => "read".to_string(), + Err(e) => format!("{e:?}"), + }; + *outcomes.entry(key).or_default() += 1; + if let (Some(sent), false) = (sent, bent) { + assert_eq!(result.as_ref().ok(), Some(&sent), "iteration {iteration}: an unbent data frame"); + read_back += 1; + } + } + println!("{FUZZ_ITERATIONS} frames, {read_back} read back: {outcomes:#?}"); + assert!(read_back > FUZZ_ITERATIONS as usize / 5, "the fuzz reads data frames back"); + assert!(outcomes.len() > 32, "the fuzz reaches most refusals: {}", outcomes.len()); +} + +#[test] +fn the_reorder_window_passes_every_accepted_frame_up_once_and_in_order() { + for iteration in 0..FUZZ_ITERATIONS / 10 { + let mut rng = Rng(1 << 40 | iteration); + let size = 1 + rng.below(256) as u16; + let first = rng.below(4096) as u32; + let mut w = Window::new(Seq::new(first as u16), size).unwrap(); + let mut base = first; + let mut accepted = 0; + let mut out: Vec = Vec::new(); + for _ in 0..rng.below(300) { + if rng.chance(5) { + base += rng.below(size as usize * 2) as u32; + w.release_to(Seq::new(base as u16), &mut out); + continue; + } + let position = (base + rng.below(size as usize * 2) as u32).saturating_sub(rng.below(size as usize) as u32); + if w.receive(Seq::new(position as u16), position, &mut out) == Received::Taken { + accepted += 1; + } + base = base.max(position.saturating_sub(size as u32 - 1)); + } + w.release_to(w.start().add(2047), &mut out); + assert!(out.windows(2).all(|p| p[0] < p[1]), "iteration {iteration}: out of order: {out:?}"); + assert_eq!(out.len(), accepted, "iteration {iteration}: a frame lost or passed up twice"); + } +} diff --git a/lib/ieee80211/tests/refusals.rs b/lib/ieee80211/tests/refusals.rs new file mode 100644 index 00000000000..d417d2f6507 --- /dev/null +++ b/lib/ieee80211/tests/refusals.rs @@ -0,0 +1,357 @@ +//! Every refusal, reached by a hostile input of its own, and the boundaries beside them accepted. + +mod common; + +use std::collections::BTreeSet; + +use common::{beacon, element, management, qos_from_ds, subframe, AP, PEER, STA}; +use toyos_ieee80211::data::{llc_snap, msdus, CcmpHeader, Ethernet, Msdu}; +use toyos_ieee80211::element::Elements; +use toyos_ieee80211::frame::{Frame, ManagementSubtype}; +use toyos_ieee80211::mgmt::Body; +use toyos_ieee80211::reorder::Window; +use toyos_ieee80211::{Address, Refused, Seq}; + +fn refusal(result: Result) -> Refused { + match result { + Ok(_) => panic!("accepted"), + Err(e) => e, + } +} + +fn frame(bytes: &[u8]) -> Refused { + refusal(Frame::parse(bytes)) +} + +fn elements(list: &[u8]) -> Refused { + refusal(Elements::parse(list)) +} + +fn country(body: &[u8]) -> Refused { + elements(&element(7, body)) +} + +/// Frame Control `fc`, then 22 zero octets: Duration, three addresses, Sequence Control. +fn header(fc: [u8; 2]) -> Vec { + let mut f = fc.to_vec(); + f.resize(24, 0); + f +} + +fn ethernet(payload: &[u8]) -> Refused { + refusal(Ethernet::from_msdu(Msdu { da: STA, sa: PEER, payload })) +} + +fn amsdu(body: &[u8]) -> Refused { + let f = qos_from_ds(true, body); + let Ok(Frame::Data(d)) = Frame::parse(&f) else { panic!("not data") }; + match msdus(&d, d.body) { + Err(e) => e, + Ok(mut m) => refusal(m.try_fold((), |(), r| r.map(drop))), + } +} + +/// A BlockAckReq (subtype 8) or BlockAck (9) after its RA and TA. +fn block_ack(subtype: u8, rest: &[u8]) -> Refused { + let mut f = vec![subtype << 4 | 0x04, 0, 0, 0]; + f.extend(AP.0); + f.extend(STA.0); + f.extend(rest); + frame(&f) +} + +/// An HE Capabilities element whose PHY Capabilities octet 0 is `phy0`, with PPE Thresholds +/// Present (B55) or not, and then `rest`. +fn he_capabilities(phy0: u8, ppe: bool, rest: &[u8]) -> Vec { + let mut phy = [0u8; 11]; + phy[0] = phy0; + phy[6] = if ppe { 0x80 } else { 0 }; + element(255, &[&[35][..], &[0; 6], &phy, &[0xFA, 0xFF, 0xFA, 0xFF], rest].concat()) +} + +/// A vendor-specific element under WMM's OUI and type. +fn wmm(after_type: &[u8]) -> Vec { + element(221, &[&[0x00, 0x50, 0xF2, 0x02][..], after_type].concat()) +} + +/// One hostile input per refusal. +fn cases() -> Vec<(Refused, Refused)> { + use Refused::*; + let mut fragmented = management(8, &[]); + fragmented[22] = 0x01; + let mut group_sa = management(8, &[]); + group_sa[10] |= 1; + let rsn_v1 = |rest: &[u8]| element(48, &[&[1, 0][..], rest].concat()); + let ht_operation = |first: u8| element(61, &[&[6, first][..], &[0; 20]].concat()); + vec![ + (Truncated, frame(&[0x80, 0, 0])), + (ProtocolVersion, frame(&header([0x81, 0]))), + (ExtensionFrame, frame(&header([0x0C, 0]))), + (ReservedSubtype, frame(&header([0x70, 0]))), + (ManagementWithDs, frame(&header([0x80, 0x01]))), + (UnhandledManagement, frame(&header([0x90, 0]))), + (UnhandledControl, frame(&[0xD4, 0, 0, 0, 2, 0, 0, 0, 0, 1])), + (ContentionFreeData, frame(&header([0x18, 0]))), + (StrictlyOrdered, frame(&header([0x08, 0x80]))), + (Fragmented, frame(&fragmented)), + (GroupTransmitter, frame(&group_sa)), + (ZeroBeaconInterval, refusal(Body::parse(ManagementSubtype::Beacon, &[0; 12]))), + (AssociationId, refusal(Body::parse(ManagementSubtype::AssociationResponse, &[0, 0, 0, 0, 0, 0xC0]))), + (ElementPastBody, elements(&[0, 5, b'a'])), + (ExtensionWithoutId, elements(&[255, 0])), + (DuplicateElement, elements(&[element(0, b"a"), element(0, b"b")].concat())), + (ElementLength, elements(&element(3, &[1, 1]))), + (SsidLength, elements(&element(0, &[b'a'; 33]))), + (RateCount, elements(&element(1, &[2; 9]))), + (DtimPeriod, elements(&element(5, &[0, 0, 0, 0]))), + (TimBitmap, elements(&element(5, &[0, 1, 250, 0, 0]))), + (CountryString, country(b"us \x01\x0b\x14")), + (CountryPad, country(b"US \x01\x0b\x14\x01")), + (CountryTriplet, country(b"US \x01\x0b")), + (CountryChannels, country(b"US \x24\x00\x17")), + (RsnVersion, elements(&element(48, &[2, 0]))), + (RsnCount, elements(&rsn_v1(&[0x00, 0x0F, 0xAC, 4, 2, 0, 0x00, 0x0F, 0xAC, 4]))), + ( + MfpRequiredNotCapable, + elements(&rsn_v1(&[0, 0x0F, 0xAC, 4, 1, 0, 0, 0x0F, 0xAC, 4, 1, 0, 0, 0x0F, 0xAC, 8, 0x40, 0])), + ), + (ExtendedRsnLength, elements(&element(244, &[0x02, 0x20]))), + (SecondaryChannelOffset, elements(&ht_operation(2))), + (VhtReserved, elements(&element(192, &[4, 42, 0, 0xFF, 0xFF]))), + (PpeThresholds, elements(&he_capabilities(0, true, &[0b0001_1001, 0, 0]))), + (WmmVersion, elements(&wmm(&[0, 2, 0]))), + (WmmAifsn, elements(&wmm(&[[1, 1, 0, 0].as_slice(), &[0x01, 0xA4, 0, 0], &[0x27, 0xA4, 0, 0], &[0x42, 0x43, 94, 0], &[0x62, 0x32, 47, 0]].concat()))), + (WmmAccessCategory, elements(&wmm(&[[1, 1, 0, 0].as_slice(), &[0x03, 0xA4, 0, 0], &[0x27, 0xA4, 0, 0], &[0x42, 0x43, 94, 0], &[0x42, 0x32, 47, 0]].concat()))), + (BlockAckVariant, block_ack(8, &[0x00, 0x00, 0x00, 0x00])), + (BitmapSize, block_ack(9, &[[0x04, 0x00, 0x01, 0x00].as_slice(), &[0xFF; 8]].concat())), + (ControlLength, block_ack(8, &[0x04, 0x00, 0x00, 0x00, 0x00])), + (SubframeLength, amsdu(&[STA.0.as_slice(), &PEER.0, &[0, 100], &[0; 10]].concat())), + (SubframePadding, amsdu(&subframe(STA, PEER, b"x", false))), + (SnapHeaderAsAddress, amsdu(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x08, 0x00, 0x45, 0, 0, 0, 0, 0])), + (NotSnap, ethernet(&[0xF0, 0xF0, 0x03, 0, 0, 0, 0x08, 0x00])), + (UnknownOui, ethernet(&[0xAA, 0xAA, 0x03, 0x00, 0x10, 0x18, 0x08, 0x00])), + (TranslatedType, ethernet(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x81, 0x37])), + (LengthField, ethernet(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x05, 0xFF])), + (ExtendedIvClear, refusal(CcmpHeader::split(&[1, 0, 0, 0, 0, 0, 0, 0]))), + (WindowSize, refusal(Window::::new(Seq::new(0), 257))), + ] +} + +#[test] +fn every_refusal_has_a_hostile_input_that_produces_it() { + let cases = cases(); + for (wanted, got) in &cases { + assert_eq!(got, wanted); + } + let named: BTreeSet = cases.iter().map(|(wanted, _)| *wanted).collect(); + let all: BTreeSet = Refused::ALL.iter().copied().collect(); + assert_eq!(all.difference(&named).collect::>(), Vec::<&Refused>::new()); +} + +#[test] +fn an_he_capabilities_element_is_at_least_as_long_as_its_widths_and_ppe_thresholds_say() { + let parse = |list: Vec| Elements::parse(&list).map(|e| e.he_capabilities.map(|h| (h.mcs_160.is_some(), h.mcs_80p80.is_some(), h.ppe_thresholds.map(<[u8]>::len)))); + assert_eq!(parse(he_capabilities(0, false, &[])), Ok(Some((false, false, None)))); + assert_eq!(parse(he_capabilities(0x18, false, &[0; 8])), Ok(Some((true, true, None)))); + assert_eq!(parse(he_capabilities(0x08, false, &[0; 3])), Err(Refused::ElementLength)); + assert_eq!(parse(he_capabilities(0x18, false, &[0; 7])), Err(Refused::ElementLength)); + // Two streams and two RU sizes: 7 + 2 × 2 × 6 = 31 bits, four octets; the fifth is ignored. + assert_eq!(parse(he_capabilities(0, true, &[0b0001_1001, 0, 0, 0])), Ok(Some((false, false, Some(4))))); + assert_eq!(parse(he_capabilities(0, true, &[0b0001_1001, 0, 0, 0, 0])), Ok(Some((false, false, Some(4))))); + assert_eq!(parse(he_capabilities(0x08, false, &[0; 8])), Ok(Some((true, false, None)))); +} + +#[test] +fn ppe_thresholds_are_present_under_phy_capabilities_b55_and_not_b23() { + // Four octets that would read as PPE Thresholds, after the MCS set. + let mut b23 = he_capabilities(0, false, &[0xFF; 4]); + // Element ID, length, extension ID and 6 MAC octets: PHY octet 2 is at 11. + b23[11] = 0x80; + assert_eq!(Elements::parse(&b23).unwrap().he_capabilities.unwrap().ppe_thresholds, None); + assert_eq!(elements(&he_capabilities(0, true, &[])), Refused::PpeThresholds); +} + +#[test] +fn an_he_operation_element_is_at_least_as_long_as_its_present_bits_say() { + let op = |parameters: u32, rest: &[u8]| { + let p = parameters.to_le_bytes(); + element(255, &[&[36, p[0], p[1], p[2], 0x05, 0xFC, 0xFF][..], rest].concat()) + }; + let list = op(1 << 17, &[37, 0x02, 39, 0, 6]); + let six = Elements::parse(&list).unwrap().he_operation.unwrap(); + assert_eq!(six.six_ghz.map(|s| (s.primary_channel, s.center_segment0)), Some((37, 39))); + assert_eq!(six.bss_color & 0x3F, 5); + let list = op(1 << 14 | 1 << 15 | 1 << 17, &[1, 42, 0, 3, 37, 2, 39, 0, 6]); + assert_eq!(Elements::parse(&list).unwrap().he_operation.unwrap().vht, Some([1, 42, 0])); + assert_eq!(elements(&op(1 << 14, &[1, 42])), Refused::ElementLength); + assert_eq!(Elements::parse(&op(0, &[0])).map(|e| e.he_operation.map(|o| o.vht)), Ok(Some(None))); + assert_eq!(elements(&element(255, &[36, 0, 0, 0, 0x05, 0xFC])), Refused::ElementLength); +} + +#[test] +fn ht_and_vht_elements_ignore_octets_past_their_fields_and_refuse_fewer() { + let ht_operation = |len: usize| element(61, &vec![0; len]); + assert!(Elements::parse(&ht_operation(23)).unwrap().ht_operation.is_some()); + assert_eq!(elements(&ht_operation(21)), Refused::ElementLength); + let vht_operation = |len: usize| element(192, &[&[1, 42, 0, 0xFC, 0xFF][..], &[0; 2]].concat()[..len]); + assert!(Elements::parse(&vht_operation(7)).unwrap().vht_operation.is_some()); + assert_eq!(elements(&vht_operation(4)), Refused::ElementLength); +} + +#[test] +fn a_wmm_information_element_and_another_wmm_subtype_are_read_and_skipped() { + use toyos_ieee80211::element::Wmm; + let wmm_of = |list: Vec| Elements::parse(&list).unwrap().wmm; + assert_eq!(wmm_of(wmm(&[0, 1, 0x80])), Some(Wmm::Information { qos_info: 0x80 })); + assert_eq!(wmm_of(wmm(&[2, 1, 0xFF])), None); + assert_eq!(elements(&wmm(&[0, 1])), Refused::ElementLength); + assert_eq!(elements(&[wmm(&[0, 1, 0]), wmm(&[0, 1, 0])].concat()), Refused::DuplicateElement); +} + +#[test] +fn an_he_block_ack_bitmap_is_as_long_as_its_fragment_number_says() { + let ba = |fragment: u8, bitmap: &[u8]| { + let mut f = vec![0x94, 0, 0, 0]; + f.extend(STA.0); + f.extend(AP.0); + f.extend([0x04, 0x00, fragment | 0x10, 0x00]); + f.extend(bitmap); + match Frame::parse(&f)? { + Frame::BlockAck(b) => Ok(b), + other => panic!("not a BlockAck: {other:?}"), + } + }; + for (fragment, octets) in [(0b0000, 8), (0b0010, 32), (0b0100, 16), (0b0110, 4)] { + let mut bitmap = vec![0; octets]; + bitmap[octets - 1] = 0x80; + let b = ba(fragment, &bitmap).unwrap(); + let last = Seq::new(1).add(u16::try_from(octets * 8 - 1).unwrap()); + assert!(b.acked(last) && !b.acked(last.add(1)) && !b.acked(Seq::new(1)), "{octets} octets"); + assert_eq!(ba(fragment, &bitmap[1..]).err(), Some(Refused::Truncated)); + } + assert_eq!(ba(0b1000, &[0; 8]).err(), Some(Refused::BitmapSize)); + assert_eq!(ba(0b0001, &[0; 8]).err(), Some(Refused::BitmapSize)); +} + +#[test] +fn every_prefix_of_a_frame_is_refused_as_truncated_short_of_its_header() { + let f = management(8, &beacon(&element(0, b"net"))); + for len in 0..24 { + assert_eq!(frame(&f[..len]), Refused::Truncated, "{len} octets"); + } + for len in 24..36 { + let Ok(Frame::Management(m)) = Frame::parse(&f[..len]) else { panic!("{len} octets") }; + assert_eq!(refusal(Body::parse(m.subtype, m.body)), Refused::Truncated, "{len} octets"); + } +} + +#[test] +fn an_element_whose_length_runs_one_past_the_body_is_refused() { + let mut list = element(0, b"net"); + list[1] = 4; + assert_eq!(elements(&list), Refused::ElementPastBody); + assert_eq!(elements(&[0]), Refused::ElementPastBody); +} + +#[test] +fn a_malformed_country_element_is_refused_whichever_rule_it_breaks() { + assert_eq!(country(b"US"), Refused::ElementLength); + assert_eq!(country(b"US "), Refused::ElementLength); + assert_eq!(country(b"US \x01"), Refused::CountryPad); + assert_eq!(country(b"U1 \x01\x0b\x14"), Refused::CountryString); + assert_eq!(country(b"US \x00\x0b\x14"), Refused::CountryChannels); + assert_eq!(country(b"US \xbe\x0c\x14"), Refused::CountryChannels); + assert_eq!(country(b"US \x01\x0b\x14\x24\x04"), Refused::CountryTriplet); +} + +#[test] +fn a_country_element_at_its_bounds_is_accepted() { + use toyos_ieee80211::element::Triplet; + // Channel 190 onward, eleven of them, ends at channel 200; a pad of 0 evens the length. + let list = element(7, b"DE \x01\x0d\x14\xbe\x0b\xf6\xc9\x51\x00\x00"); + let c = Elements::parse(&list).unwrap().country.unwrap(); + assert_eq!(&c.string, b"DE "); + assert_eq!( + c.triplets().collect::>(), + [ + Triplet::Subband { first_channel: 1, channels: 13, max_power_dbm: 20 }, + Triplet::Subband { first_channel: 190, channels: 11, max_power_dbm: -10 }, + Triplet::Operating { extension_id: 201, class: 81, coverage_class: 0 }, + ] + ); +} + +#[test] +fn an_ssid_of_32_octets_and_a_tim_reaching_aid_2007_are_accepted() { + let tim_last_octet = [&[0, 1, 250][..], &[0x80]].concat(); + let list = [element(0, &[b'a'; 32]), element(5, &tim_last_octet)].concat(); + let e = Elements::parse(&list).unwrap(); + assert_eq!(e.ssid.unwrap().len(), 32); + let tim = e.tim.unwrap(); + assert!(tim.buffered(2007)); + assert!(!tim.buffered(2006)); + assert!(!tim.buffered(7)); +} + +#[test] +fn an_association_response_takes_aid_1_to_2007_with_its_two_high_bits_set() { + let response = |aid: u16| { + let body = [&0u16.to_le_bytes()[..], &0u16.to_le_bytes(), &(aid | 0xC000).to_le_bytes()].concat(); + match Body::parse(ManagementSubtype::AssociationResponse, &body) { + Ok(Body::AssociationResponse(r)) => Ok(r.aid), + Ok(_) => panic!("another body"), + Err(e) => Err(e), + } + }; + assert_eq!(response(1), Ok(Some(1))); + assert_eq!(response(2007), Ok(Some(2007))); + assert_eq!(response(2008), Err(Refused::AssociationId)); +} + +#[test] +fn an_rsn_element_may_end_after_any_whole_field_and_not_inside_one() { + let full = [ + &[1, 0][..], + &[0x00, 0x0F, 0xAC, 4], + &[1, 0, 0x00, 0x0F, 0xAC, 4], + &[1, 0, 0x00, 0x0F, 0xAC, 8], + &[0xC0, 0x00], + &[0, 0], + &[0x00, 0x0F, 0xAC, 6], + ] + .concat(); + let field_ends = [2, 6, 12, 18, 20, 22, 26]; + for len in 2..=full.len() { + let list = element(48, &full[..len]); + let result = Elements::parse(&list); + assert_eq!(result.is_ok(), field_ends.contains(&len), "{len} octets: {:?}", result.err()); + } + let list = element(48, &full); + let rsn = Elements::parse(&list).unwrap().rsn.unwrap(); + assert!(rsn.mfp_required() && rsn.mfp_capable()); +} + +#[test] +fn llc_snap_refuses_a_length_and_tunnels_aarp_and_ipx() { + assert_eq!(llc_snap(0x05FF), Err(Refused::LengthField)); + assert_eq!(llc_snap(0x8137).unwrap()[..6], [0xAA, 0xAA, 0x03, 0x00, 0x00, 0xF8]); + assert_eq!(llc_snap(0x80F3).unwrap()[..6], [0xAA, 0xAA, 0x03, 0x00, 0x00, 0xF8]); + assert_eq!(llc_snap(0x0800).unwrap(), [0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00]); +} + +#[test] +fn a_bridge_tunnel_msdu_is_its_ethertype_on_ethernet() { + let eth = Ethernet::from_msdu(Msdu { da: STA, sa: PEER, payload: &[0xAA, 0xAA, 0x03, 0, 0, 0xF8, 0x81, 0x37, 9] }).unwrap(); + assert_eq!((eth.ethertype, eth.payload), (0x8137, &[9][..])); +} + +#[test] +fn a_block_ack_transmitter_with_its_bandwidth_signaling_bit_is_the_individual_address() { + let mut f = vec![0x84, 0, 0, 0]; + f.extend(STA.0); + f.extend([AP.0[0] | 1, AP.0[1], AP.0[2], AP.0[3], AP.0[4], AP.0[5]]); + f.extend([0x04, 0x30, 0x10, 0x00]); + let Ok(Frame::BlockAckReq(bar)) = Frame::parse(&f) else { panic!("not a BlockAckReq") }; + assert_eq!((bar.ta, bar.tid, bar.ssn), (AP, 3, Seq::new(1))); + assert_ne!(bar.ta, Address::BROADCAST); +} diff --git a/lib/ieee80211/tests/reorder.rs b/lib/ieee80211/tests/reorder.rs new file mode 100644 index 00000000000..20d079ad348 --- /dev/null +++ b/lib/ieee80211/tests/reorder.rs @@ -0,0 +1,130 @@ +//! The receive reordering buffer against §10.25.6's rules, one rule per test. + +use toyos_ieee80211::reorder::{Received, Window}; +use toyos_ieee80211::{Refused, Seq}; + +fn window(start: u16, size: u16) -> Window { + Window::new(Seq::new(start), size).unwrap() +} + +fn receive(w: &mut Window, seq: u16) -> Vec { + let mut out = Vec::new(); + assert_eq!(w.receive(Seq::new(seq), seq, &mut out), Received::Taken); + out +} + +#[test] +fn frames_in_order_pass_up_as_they_arrive() { + let mut w = window(10, 8); + assert_eq!(receive(&mut w, 10), [10]); + assert_eq!(receive(&mut w, 11), [11]); + assert_eq!(w.start(), Seq::new(12)); +} + +#[test] +fn a_hole_holds_the_frames_after_it_until_it_fills() { + let mut w = window(10, 8); + assert_eq!(receive(&mut w, 12), []); + assert_eq!(receive(&mut w, 11), []); + assert_eq!(w.first_held(), Some(Seq::new(11))); + assert_eq!(receive(&mut w, 10), [10, 11, 12]); + assert_eq!(w.first_held(), None); +} + +#[test] +fn a_frame_past_the_end_moves_the_window_to_end_at_it() { + let mut w = window(10, 8); + assert_eq!(receive(&mut w, 12), []); + assert_eq!(receive(&mut w, 14), []); + // 20 ends the window at 20, so it starts at 13: 12 passes up, then the run 13 lacks. + assert_eq!(receive(&mut w, 20), [12]); + assert_eq!(w.start(), Seq::new(13)); + assert_eq!(receive(&mut w, 13), [13, 14]); +} + +#[test] +fn the_last_slot_of_the_window_moves_nothing() { + let mut w = window(10, 8); + assert_eq!(receive(&mut w, 17), []); + assert_eq!(w.start(), Seq::new(10)); +} + +#[test] +fn a_frame_a_whole_window_past_passes_up_everything_held() { + let mut w = window(10, 4); + receive(&mut w, 11); + receive(&mut w, 13); + assert_eq!(receive(&mut w, 2000), [11, 13]); + assert_eq!(w.start(), Seq::new(1997)); +} + +#[test] +fn a_frame_half_the_sequence_space_behind_is_dropped_as_old() { + let mut w = window(10, 8); + let mut out = Vec::new(); + assert_eq!(w.receive(Seq::new(9), 9, &mut out), Received::Old); + assert_eq!(w.receive(Seq::new(10 + 2048), 0, &mut out), Received::Old); + assert_eq!(receive(&mut w, 10 + 2047), []); +} + +#[test] +fn a_frame_already_held_is_dropped_as_a_duplicate() { + let mut w = window(10, 8); + receive(&mut w, 12); + let mut out = Vec::new(); + assert_eq!(w.receive(Seq::new(12), 99, &mut out), Received::Duplicate); + assert_eq!(receive(&mut w, 10), [10]); + assert_eq!(receive(&mut w, 11), [11, 12]); +} + +#[test] +fn a_release_passes_up_what_is_held_before_it_and_the_run_from_it() { + let mut w = window(10, 8); + receive(&mut w, 11); + receive(&mut w, 13); + receive(&mut w, 14); + let mut out = Vec::new(); + w.release_to(Seq::new(13), &mut out); + assert_eq!(out, [11, 13, 14]); + assert_eq!(w.start(), Seq::new(15)); +} + +#[test] +fn a_release_at_or_behind_the_start_changes_nothing() { + let mut w = window(10, 8); + receive(&mut w, 12); + let mut out = Vec::new(); + w.release_to(Seq::new(10), &mut out); + w.release_to(Seq::new(5), &mut out); + assert_eq!((out, w.start()), (vec![], Seq::new(10))); +} + +#[test] +fn the_window_runs_across_the_end_of_the_sequence_space() { + let mut w = window(4094, 8); + assert_eq!(receive(&mut w, 1), []); + assert_eq!(receive(&mut w, 4095), []); + assert_eq!(receive(&mut w, 0), []); + assert_eq!(receive(&mut w, 4094), [4094, 4095, 0, 1]); + assert_eq!(w.start(), Seq::new(2)); +} + +#[test] +fn a_window_of_one_to_two_hundred_fifty_six_is_made_and_no_other() { + assert!(Window::::new(Seq::new(0), 1).is_ok()); + assert!(Window::::new(Seq::new(0), 256).is_ok()); + assert_eq!(Window::::new(Seq::new(0), 0).err(), Some(Refused::WindowSize)); +} + +#[test] +fn an_he_window_holds_two_hundred_fifty_six_frames_behind_a_hole() { + let mut w = window(4000, 256); + for seq in 4001..4096 { + assert_eq!(receive(&mut w, seq), []); + } + for seq in 0..160 { + assert_eq!(receive(&mut w, seq), []); + } + let all: Vec = (4000..4096).chain(0..160).collect(); + assert_eq!(receive(&mut w, 4000), all); +} diff --git a/lib/ieee80211/tests/translate.rs b/lib/ieee80211/tests/translate.rs new file mode 100644 index 00000000000..cb50467dcd8 --- /dev/null +++ b/lib/ieee80211/tests/translate.rs @@ -0,0 +1,53 @@ +//! MSDUs out of data frames, and Ethernet frames into them and back. + +mod common; + +use common::{qos_from_ds, subframe, AP, PEER, STA}; +use toyos_ieee80211::data::{llc_snap, msdus, Ethernet, Msdu, ToDs}; +use toyos_ieee80211::frame::{Ds, Frame}; +use toyos_ieee80211::{Address, Seq}; + +#[test] +fn an_amsdu_yields_each_subframe_with_its_own_addresses() { + let other = Address([0x02, 9, 9, 9, 9, 9]); + let body = [subframe(STA, PEER, b"abc", false), subframe(STA, other, b"defgh", true)].concat(); + let f = qos_from_ds(true, &body); + let Ok(Frame::Data(d)) = Frame::parse(&f) else { panic!("not data") }; + let got: Vec = msdus(&d, d.body).unwrap().map(Result::unwrap).collect(); + assert_eq!( + got, + [Msdu { da: STA, sa: PEER, payload: b"abc" }, Msdu { da: STA, sa: other, payload: b"defgh" }] + ); +} + +#[test] +fn a_frame_without_the_amsdu_bit_is_one_msdu_between_its_header_addresses() { + let f = qos_from_ds(false, b"payload"); + let Ok(Frame::Data(d)) = Frame::parse(&f) else { panic!("not data") }; + let got: Vec = msdus(&d, d.body).unwrap().map(Result::unwrap).collect(); + // From DS: Address 1 is the destination, Address 3 the source. + assert_eq!(got, [Msdu { da: STA, sa: PEER, payload: b"payload" }]); +} + +#[test] +fn a_null_frame_carries_no_msdu() { + let mut f = qos_from_ds(false, &[]); + f[0] = 0xC8; + let Ok(Frame::Data(d)) = Frame::parse(&f) else { panic!("not data") }; + assert!(d.null); + assert_eq!(msdus(&d, d.body).unwrap().count(), 0); +} + +#[test] +fn an_ethernet_frame_sent_to_the_ap_reads_back_as_itself() { + let header = ToDs { bssid: AP, sa: STA, da: PEER, seq: Seq::new(4095), tid: 6, protected: false }; + let payload = b"\x45\x00 an ipv4 datagram"; + let f = [&header.header()[..], &llc_snap(0x0800).unwrap(), payload].concat(); + let Ok(Frame::Data(d)) = Frame::parse(&f) else { panic!("not data") }; + assert_eq!((d.ds, d.ra(), d.seq, d.qos.unwrap().tid()), (Ds::ToDs, AP, Seq::new(4095), 6)); + let msdu = msdus(&d, d.body).unwrap().next().unwrap().unwrap(); + assert_eq!( + Ethernet::from_msdu(msdu).unwrap(), + Ethernet { dst: PEER, src: STA, ethertype: 0x0800, payload } + ); +} diff --git a/licenses/BSD-3-Clause-tcpdump.txt b/licenses/BSD-3-Clause-tcpdump.txt new file mode 100644 index 00000000000..a10474d54ae --- /dev/null +++ b/licenses/BSD-3-Clause-tcpdump.txt @@ -0,0 +1,19 @@ +License: BSD + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. The names of the authors may not be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.