Skip to main content

openipc_core/
radiotap.rs

1pub const FRAME_TYPE_DATA: u8 = 0x08;
2pub const FRAME_TYPE_RTS: u8 = 0xb4;
3
4const IEEE80211_RADIOTAP_MCS_HAVE_BW: u8 = 0x01;
5const IEEE80211_RADIOTAP_MCS_HAVE_MCS: u8 = 0x02;
6const IEEE80211_RADIOTAP_MCS_HAVE_GI: u8 = 0x04;
7const IEEE80211_RADIOTAP_MCS_HAVE_FEC: u8 = 0x10;
8const IEEE80211_RADIOTAP_MCS_HAVE_STBC: u8 = 0x20;
9const IEEE80211_RADIOTAP_MCS_SGI: u8 = 0x04;
10const IEEE80211_RADIOTAP_MCS_FEC_LDPC: u8 = 0x10;
11const IEEE80211_RADIOTAP_MCS_STBC_SHIFT: u8 = 5;
12
13const IEEE80211_RADIOTAP_VHT_FLAG_STBC: u8 = 0x01;
14const IEEE80211_RADIOTAP_VHT_FLAG_SGI: u8 = 0x04;
15const IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0: u8 = 0x01;
16
17const RADIOTAP_PRESENT_RATE: u32 = 1 << 2;
18const RADIOTAP_PRESENT_TX_FLAGS: u32 = 1 << 15;
19const RADIOTAP_PRESENT_MCS: u32 = 1 << 19;
20const RADIOTAP_PRESENT_VHT: u32 = 1 << 21;
21const RADIOTAP_TX_FLAGS_NO_ACK: u16 = 0x0008;
22
23pub const RADIOTAP_LEGACY_LEN: usize = 13;
24pub const RADIOTAP_HT_LEN: usize = 13;
25pub const RADIOTAP_VHT_LEN: usize = 22;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ChannelBandwidth {
29    Mhz20,
30    Mhz40,
31    Mhz80,
32    Mhz160,
33}
34
35impl ChannelBandwidth {
36    pub const fn realtek_desc_bits(self) -> u8 {
37        match self {
38            Self::Mhz20 => 0,
39            Self::Mhz40 => 1,
40            Self::Mhz80 | Self::Mhz160 => 2,
41        }
42    }
43
44    const fn ht_mcs_bits(self) -> u8 {
45        match self {
46            Self::Mhz20 => 0,
47            Self::Mhz40 | Self::Mhz80 | Self::Mhz160 => 1,
48        }
49    }
50
51    const fn vht_bits(self) -> u8 {
52        match self {
53            Self::Mhz20 => 0x00,
54            Self::Mhz40 => 0x01,
55            Self::Mhz80 => 0x04,
56            Self::Mhz160 => 0x0b,
57        }
58    }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum TxModeKind {
63    Legacy,
64    Ht,
65    Vht,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct TxMode {
70    pub kind: TxModeKind,
71    pub legacy_rate_500kbps: u8,
72    pub ht_mcs: u8,
73    pub vht_mcs: u8,
74    pub vht_nss: u8,
75    pub bandwidth: ChannelBandwidth,
76    pub short_gi: bool,
77    pub ldpc: bool,
78    pub stbc: bool,
79}
80
81impl TxMode {
82    pub const fn legacy(rate_500kbps: u8) -> Self {
83        Self {
84            kind: TxModeKind::Legacy,
85            legacy_rate_500kbps: rate_500kbps,
86            ht_mcs: 0,
87            vht_mcs: 0,
88            vht_nss: 1,
89            bandwidth: ChannelBandwidth::Mhz20,
90            short_gi: false,
91            ldpc: false,
92            stbc: false,
93        }
94    }
95
96    pub const fn legacy_6m() -> Self {
97        Self::legacy(12)
98    }
99
100    pub const fn legacy_1m() -> Self {
101        Self::legacy(2)
102    }
103
104    pub const fn ht(mcs: u8) -> Self {
105        Self {
106            kind: TxModeKind::Ht,
107            legacy_rate_500kbps: 12,
108            ht_mcs: mcs,
109            vht_mcs: 0,
110            vht_nss: 1,
111            bandwidth: ChannelBandwidth::Mhz20,
112            short_gi: false,
113            ldpc: false,
114            stbc: false,
115        }
116    }
117
118    pub const fn vht(nss: u8, mcs: u8) -> Self {
119        Self {
120            kind: TxModeKind::Vht,
121            legacy_rate_500kbps: 12,
122            ht_mcs: 0,
123            vht_mcs: mcs,
124            vht_nss: nss,
125            bandwidth: ChannelBandwidth::Mhz20,
126            short_gi: false,
127            ldpc: false,
128            stbc: false,
129        }
130    }
131}
132
133impl Default for TxMode {
134    fn default() -> Self {
135        Self::legacy_6m()
136    }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct TxRadioParams {
141    pub mcs_index: u8,
142    pub nss: u8,
143    pub bandwidth: ChannelBandwidth,
144    pub short_gi: bool,
145    pub stbc: u8,
146    pub ldpc: bool,
147    pub vht: bool,
148    pub frame_type: u8,
149}
150
151impl TxRadioParams {
152    pub const fn openipc_uplink_default() -> Self {
153        Self {
154            mcs_index: 0,
155            nss: 1,
156            bandwidth: ChannelBandwidth::Mhz20,
157            short_gi: false,
158            stbc: 1,
159            ldpc: true,
160            vht: false,
161            frame_type: FRAME_TYPE_RTS,
162        }
163    }
164}
165
166impl Default for TxRadioParams {
167    fn default() -> Self {
168        Self::openipc_uplink_default()
169    }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct RadiotapTxInfo {
174    pub vht: bool,
175    pub mcs_index: u8,
176    pub nss: u8,
177    pub bandwidth: ChannelBandwidth,
178    pub short_gi: bool,
179    pub stbc: u8,
180    pub ldpc: bool,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum RadiotapError {
185    TooShort,
186    InvalidLength,
187    UnsupportedHeader,
188}
189
190impl std::fmt::Display for RadiotapError {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        match self {
193            Self::TooShort => write!(f, "radiotap packet is too short"),
194            Self::InvalidLength => write!(f, "radiotap length is invalid"),
195            Self::UnsupportedHeader => write!(f, "unsupported radiotap TX header"),
196        }
197    }
198}
199
200impl std::error::Error for RadiotapError {}
201
202pub fn build_radiotap_header(params: TxRadioParams) -> Vec<u8> {
203    if params.vht {
204        build_vht_radiotap_header(params)
205    } else {
206        build_ht_radiotap_header(params)
207    }
208}
209
210pub fn build_stream_radiotap(mode: TxMode) -> Vec<u8> {
211    match mode.kind {
212        TxModeKind::Legacy => build_legacy_radiotap_header(mode),
213        TxModeKind::Ht => build_ht_radiotap_header(TxRadioParams {
214            mcs_index: mode.ht_mcs,
215            bandwidth: mode.bandwidth,
216            short_gi: mode.short_gi,
217            stbc: u8::from(mode.stbc),
218            ldpc: mode.ldpc,
219            vht: false,
220            ..TxRadioParams::default()
221        }),
222        TxModeKind::Vht => build_vht_radiotap_header(TxRadioParams {
223            mcs_index: mode.vht_mcs,
224            nss: mode.vht_nss,
225            bandwidth: mode.bandwidth,
226            short_gi: mode.short_gi,
227            stbc: u8::from(mode.stbc),
228            ldpc: mode.ldpc,
229            vht: true,
230            ..TxRadioParams::default()
231        }),
232    }
233}
234
235pub fn build_legacy_radiotap_header(mode: TxMode) -> Vec<u8> {
236    vec![
237        0x00,
238        0x00,
239        RADIOTAP_LEGACY_LEN as u8,
240        0x00,
241        (RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_TX_FLAGS) as u8,
242        ((RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_TX_FLAGS) >> 8) as u8,
243        0x00,
244        0x00,
245        mode.legacy_rate_500kbps,
246        0x00,
247        (RADIOTAP_TX_FLAGS_NO_ACK & 0xff) as u8,
248        (RADIOTAP_TX_FLAGS_NO_ACK >> 8) as u8,
249        0x00,
250    ]
251}
252
253pub fn build_ht_radiotap_header(params: TxRadioParams) -> Vec<u8> {
254    let known = IEEE80211_RADIOTAP_MCS_HAVE_MCS
255        | IEEE80211_RADIOTAP_MCS_HAVE_BW
256        | IEEE80211_RADIOTAP_MCS_HAVE_GI
257        | IEEE80211_RADIOTAP_MCS_HAVE_STBC
258        | IEEE80211_RADIOTAP_MCS_HAVE_FEC;
259    let mut flags = params.bandwidth.ht_mcs_bits();
260    if params.short_gi {
261        flags |= IEEE80211_RADIOTAP_MCS_SGI;
262    }
263    if params.ldpc {
264        flags |= IEEE80211_RADIOTAP_MCS_FEC_LDPC;
265    }
266    flags |= (params.stbc & 0x03) << IEEE80211_RADIOTAP_MCS_STBC_SHIFT;
267
268    vec![
269        0x00,
270        0x00,
271        RADIOTAP_HT_LEN as u8,
272        0x00,
273        0x00,
274        0x80,
275        0x08,
276        0x00,
277        0x08,
278        0x00,
279        known,
280        flags,
281        params.mcs_index.min(31),
282    ]
283}
284
285pub fn build_vht_radiotap_header(params: TxRadioParams) -> Vec<u8> {
286    let mut flags = 0u8;
287    if params.stbc != 0 {
288        flags |= IEEE80211_RADIOTAP_VHT_FLAG_STBC;
289    }
290    if params.short_gi {
291        flags |= IEEE80211_RADIOTAP_VHT_FLAG_SGI;
292    }
293    let nss = params.nss.clamp(1, 4);
294    let mcs = params.mcs_index.min(9);
295    let mcs_nss0 = (mcs << 4) | nss;
296    let coding = if params.ldpc {
297        IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0
298    } else {
299        0
300    };
301
302    vec![
303        0x00,
304        0x00,
305        RADIOTAP_VHT_LEN as u8,
306        0x00,
307        0x00,
308        0x80,
309        0x20,
310        0x00,
311        0x08,
312        0x00,
313        0x45,
314        0x00,
315        flags,
316        params.bandwidth.vht_bits(),
317        mcs_nss0,
318        0x00,
319        0x00,
320        0x00,
321        coding,
322        0x00,
323        0x00,
324        0x00,
325    ]
326}
327
328pub fn radiotap_len(packet: &[u8]) -> Result<usize, RadiotapError> {
329    if packet.len() < 4 {
330        return Err(RadiotapError::TooShort);
331    }
332    let len = u16::from_le_bytes([packet[2], packet[3]]) as usize;
333    if len == 0 || len >= packet.len() {
334        return Err(RadiotapError::InvalidLength);
335    }
336    Ok(len)
337}
338
339pub fn parse_tx_mode_str(spec: &str) -> TxMode {
340    let trimmed = spec
341        .chars()
342        .filter(|ch| !ch.is_whitespace())
343        .flat_map(char::to_uppercase)
344        .collect::<String>();
345    if trimmed.is_empty() {
346        return TxMode::default();
347    }
348
349    let mut tokens = trimmed.split('/');
350    let Some(rate_token) = tokens.next() else {
351        return TxMode::default();
352    };
353    let Some(mut mode) = parse_tx_rate_token(rate_token) else {
354        return TxMode::default();
355    };
356
357    for token in tokens {
358        match token {
359            "SGI" => mode.short_gi = true,
360            "LDPC" => mode.ldpc = true,
361            "STBC" => mode.stbc = true,
362            "20" => mode.bandwidth = ChannelBandwidth::Mhz20,
363            "40" => mode.bandwidth = ChannelBandwidth::Mhz40,
364            "80" => mode.bandwidth = ChannelBandwidth::Mhz80,
365            "160" => mode.bandwidth = ChannelBandwidth::Mhz160,
366            _ => {}
367        }
368    }
369    mode
370}
371
372pub fn parse_radiotap_tx_mode(packet: &[u8]) -> Result<Option<TxMode>, RadiotapError> {
373    let len = radiotap_len(packet)?;
374    if len < 8 || packet.len() < len {
375        return Err(RadiotapError::InvalidLength);
376    }
377    let present = u32::from_le_bytes(packet[4..8].try_into().expect("present word is in range"));
378    if present & (1 << 31) != 0 {
379        return Err(RadiotapError::UnsupportedHeader);
380    }
381
382    let mut offset = 8usize;
383    let mut mode = None;
384
385    if present & RADIOTAP_PRESENT_RATE != 0 {
386        require_field(packet, len, offset, 1)?;
387        mode = Some(TxMode::legacy(packet[offset]));
388        offset += 1;
389    }
390
391    if present & RADIOTAP_PRESENT_TX_FLAGS != 0 {
392        offset = align(offset, 2);
393        require_field(packet, len, offset, 2)?;
394        offset += 2;
395    }
396
397    if present & RADIOTAP_PRESENT_MCS != 0 {
398        require_field(packet, len, offset, 3)?;
399        let known = packet[offset];
400        let flags = packet[offset + 1];
401        let mcs = packet[offset + 2];
402        let mut ht = TxMode::ht(if known & IEEE80211_RADIOTAP_MCS_HAVE_MCS != 0 {
403            mcs.min(31)
404        } else {
405            0
406        });
407        ht.bandwidth = if known & IEEE80211_RADIOTAP_MCS_HAVE_BW != 0 && flags & 0x03 == 1 {
408            ChannelBandwidth::Mhz40
409        } else {
410            ChannelBandwidth::Mhz20
411        };
412        ht.short_gi =
413            known & IEEE80211_RADIOTAP_MCS_HAVE_GI != 0 && flags & IEEE80211_RADIOTAP_MCS_SGI != 0;
414        ht.ldpc = known & IEEE80211_RADIOTAP_MCS_HAVE_FEC != 0
415            && flags & IEEE80211_RADIOTAP_MCS_FEC_LDPC != 0;
416        ht.stbc = known & IEEE80211_RADIOTAP_MCS_HAVE_STBC != 0
417            && ((flags >> IEEE80211_RADIOTAP_MCS_STBC_SHIFT) & 0x03) != 0;
418        mode = Some(ht);
419        offset += 3;
420    }
421
422    if present & RADIOTAP_PRESENT_VHT != 0 {
423        offset = align(offset, 2);
424        require_field(packet, len, offset, 12)?;
425        let known = u16::from_le_bytes([packet[offset], packet[offset + 1]]);
426        let flags = packet[offset + 2];
427        let bandwidth = match packet[offset + 3] & 0x1f {
428            1..=3 => ChannelBandwidth::Mhz40,
429            4..=10 => ChannelBandwidth::Mhz80,
430            11..=31 => ChannelBandwidth::Mhz160,
431            _ => ChannelBandwidth::Mhz20,
432        };
433        let mcs_nss = packet[offset + 4];
434        let mut vht = TxMode::vht((mcs_nss & 0x0f).clamp(1, 4), ((mcs_nss >> 4) & 0x0f).min(9));
435        if known & (1 << 6) != 0 {
436            vht.bandwidth = bandwidth;
437        }
438        vht.short_gi = known & (1 << 2) != 0 && flags & IEEE80211_RADIOTAP_VHT_FLAG_SGI != 0;
439        vht.stbc = known & 1 != 0 && flags & IEEE80211_RADIOTAP_VHT_FLAG_STBC != 0;
440        vht.ldpc = packet[offset + 8] & IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0 != 0;
441        mode = Some(vht);
442    }
443
444    Ok(mode)
445}
446
447pub fn parse_radiotap_tx_info(packet: &[u8]) -> Result<RadiotapTxInfo, RadiotapError> {
448    match parse_radiotap_tx_mode(packet)? {
449        Some(mode) => Ok(RadiotapTxInfo {
450            vht: mode.kind == TxModeKind::Vht,
451            mcs_index: match mode.kind {
452                TxModeKind::Legacy | TxModeKind::Ht => mode.ht_mcs,
453                TxModeKind::Vht => mode.vht_mcs,
454            },
455            nss: mode.vht_nss,
456            bandwidth: mode.bandwidth,
457            short_gi: mode.short_gi,
458            stbc: u8::from(mode.stbc),
459            ldpc: mode.ldpc,
460        }),
461        None => Err(RadiotapError::UnsupportedHeader),
462    }
463}
464
465fn parse_tx_rate_token(token: &str) -> Option<TxMode> {
466    match token {
467        "1M" => Some(TxMode::legacy(2)),
468        "2M" => Some(TxMode::legacy(4)),
469        "5.5M" | "5M" => Some(TxMode::legacy(11)),
470        "6M" => Some(TxMode::legacy(12)),
471        "9M" => Some(TxMode::legacy(18)),
472        "11M" => Some(TxMode::legacy(22)),
473        "12M" => Some(TxMode::legacy(24)),
474        "18M" => Some(TxMode::legacy(36)),
475        "24M" => Some(TxMode::legacy(48)),
476        "36M" => Some(TxMode::legacy(72)),
477        "48M" => Some(TxMode::legacy(96)),
478        "54M" => Some(TxMode::legacy(108)),
479        _ => {
480            if let Some(raw) = token.strip_prefix("MCS") {
481                return raw
482                    .parse::<u8>()
483                    .ok()
484                    .filter(|mcs| *mcs <= 31)
485                    .map(TxMode::ht);
486            }
487            if let Some(raw) = token.strip_prefix("VHT") {
488                let (nss_raw, mcs_raw) = raw.split_once("SS_MCS")?;
489                let nss = nss_raw.parse::<u8>().ok()?;
490                let mcs = mcs_raw.parse::<u8>().ok()?;
491                if (1..=4).contains(&nss) && mcs <= 9 {
492                    return Some(TxMode::vht(nss, mcs));
493                }
494            }
495            None
496        }
497    }
498}
499
500const fn align(offset: usize, alignment: usize) -> usize {
501    (offset + alignment - 1) & !(alignment - 1)
502}
503
504fn require_field(
505    packet: &[u8],
506    radiotap_len: usize,
507    offset: usize,
508    len: usize,
509) -> Result<(), RadiotapError> {
510    if offset + len <= radiotap_len && offset + len <= packet.len() {
511        Ok(())
512    } else {
513        Err(RadiotapError::InvalidLength)
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    #[test]
522    fn ht_header_roundtrips_tx_info() {
523        let params = TxRadioParams {
524            mcs_index: 3,
525            bandwidth: ChannelBandwidth::Mhz40,
526            short_gi: true,
527            stbc: 1,
528            ldpc: true,
529            ..TxRadioParams::default()
530        };
531        let mut packet = build_radiotap_header(params);
532        packet.extend_from_slice(&[0u8; 24]);
533        let parsed = parse_radiotap_tx_info(&packet).unwrap();
534        assert_eq!(parsed.mcs_index, 3);
535        assert_eq!(parsed.bandwidth, ChannelBandwidth::Mhz40);
536        assert!(parsed.short_gi);
537        assert!(parsed.ldpc);
538        assert_eq!(parsed.stbc, 1);
539    }
540
541    #[test]
542    fn parses_devourer_tx_mode_strings() {
543        let mode = parse_tx_mode_str("vht2ss_mcs5/80/sgi/ldpc/stbc");
544        assert_eq!(mode.kind, TxModeKind::Vht);
545        assert_eq!(mode.vht_nss, 2);
546        assert_eq!(mode.vht_mcs, 5);
547        assert_eq!(mode.bandwidth, ChannelBandwidth::Mhz80);
548        assert!(mode.short_gi);
549        assert!(mode.ldpc);
550        assert!(mode.stbc);
551    }
552
553    #[test]
554    fn legacy_stream_radiotap_carries_rate_and_tx_flags() {
555        let mode = TxMode::legacy(12);
556        let mut packet = build_stream_radiotap(mode);
557        packet.extend_from_slice(&[0u8; 24]);
558        let parsed = parse_radiotap_tx_mode(&packet).unwrap().unwrap();
559        assert_eq!(parsed.kind, TxModeKind::Legacy);
560        assert_eq!(parsed.legacy_rate_500kbps, 12);
561    }
562}