From 338d41dcebf67ba1ee6a17e031d81d8251c1a222 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Thu, 18 Jun 2026 13:45:59 -0700 Subject: [PATCH] lib: serialize MAC RACH attempts to GSMTAP This also refactors the gsmtap code into a neater module, and adds MAC UL & DL logs to our diag capture. --- lib/src/analysis/analyzer.rs | 5 +- lib/src/diag/diaglog/mac.rs | 89 ++++++++++--- lib/src/diag/diaglog/measurement.rs | 2 +- lib/src/diag/diaglog/mod.rs | 16 ++- lib/src/diag_device.rs | 4 +- lib/src/gsmtap/mac.rs | 160 ++++++++++++++++++++++++ lib/src/gsmtap/mod.rs | 1 + lib/src/gsmtap/parser.rs | 18 ++- lib/src/lib.rs | 2 + lib/src/log_codes.rs | 2 + lib/src/{diag/diaglog => }/test_util.rs | 0 11 files changed, 266 insertions(+), 33 deletions(-) create mode 100644 lib/src/gsmtap/mac.rs rename lib/src/{diag/diaglog => }/test_util.rs (100%) diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index 52e97ac..f088b58 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -5,10 +5,9 @@ use serde::{Deserialize, Serialize}; use std::borrow::Cow; use crate::analysis::diagnostic::DiagnosticAnalyzer; -use crate::diag::{DiagParsingError, Message}; -use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType}; +use crate::diag::{DiagParsingError, Message, MessagesContainer}; +use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType, parser as gsmtap_parser}; use crate::util::RuntimeMetadata; -use crate::{diag::MessagesContainer, gsmtap::parser as gsmtap_parser}; use super::{ connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer, diff --git a/lib/src/diag/diaglog/mac.rs b/lib/src/diag/diaglog/mac.rs index 6609773..c99bcd6 100644 --- a/lib/src/diag/diaglog/mac.rs +++ b/lib/src/diag/diaglog/mac.rs @@ -41,6 +41,7 @@ pub mod rach { #[deku(ctx = "version")] pub msg1: Msg1, pub msg2: Msg2, + #[deku(ctx = "version")] pub msg3: Msg3, #[deku(cond = "version == 0x31 || version == 0x32")] pub additional_info: Option, @@ -106,6 +107,16 @@ pub mod rach { }, } + impl Msg1 { + pub fn get_preamble_index(&self) -> u8 { + match self { + Msg1::V2 { preamble_index, .. } => *preamble_index, + Msg1::V3Or31 { preamble_index, .. } => *preamble_index, + Msg1::V32 { preamble_index, .. } => *preamble_index, + } + } + } + #[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)] pub struct Msg2 { pub backoff: u16, @@ -115,13 +126,39 @@ pub mod rach { } #[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)] + #[deku(ctx = "version: u8")] pub struct Msg3 { - pub grant_raw: u32, - pub grant: u16, + #[deku(ctx = "version")] + pub grant: Msg3Grant, + pub unk_grant: u16, pub harq_id: u8, pub mac_pdu: [u8; 10], } + impl Msg3 { + pub fn get_grant(&self) -> u32 { + match &self.grant { + Msg3Grant::V1 { grant } => *grant & 0xfffff, + Msg3Grant::V32 { grant } => *grant & 0xfffff, + } + } + } + + #[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)] + #[deku(ctx = "version: u8", id = "version")] + pub enum Msg3Grant { + #[deku(id_pat = "0..0x32")] + V1 { + #[deku(endian = "little")] + grant: u32, + }, + #[deku(id_pat = "0x32..")] + V32 { + #[deku(endian = "big")] + grant: u32, + } + } + #[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)] #[deku(ctx = "version: u8", id = "version")] pub enum AttemptHeader { @@ -166,13 +203,25 @@ pub mod rach { } #[cfg(test)] -mod test { - use super::super::test_util::unhexlify; +pub(crate) mod test { + use crate::{diag::diaglog::mac::rach::Msg3Grant, test_util::unhexlify}; use super::*; use crate::diag::diaglog::mac::rach::{AdditionalInfo, AttemptHeader, Msg1, Msg2, Msg3}; use std::io::Seek; + pub fn mac_rach_test_packets_from_scat() -> Vec { + // test data from SCAT unit tests: https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/tests/test_diagltelogparser.py#L129 + vec![ + parse_rach_packet("0101a06906022400010001071BFF98FF000001231A0400181C010007000600465C80BD0648000000"), + parse_rach_packet("0101a0690603280001000100010718ffa4ff000001c6610b00b4a2000012000120061f423f8d95075800"), + parse_rach_packet("0101739e063134000100010000033f0098ff0000013c6b070058ac010007000000468f47e2d446000000644b0000180001000000d5040000"), + parse_rach_packet("01010000063134000100010001070aff98ff0000011c48070018e2000007000000523b7dfd69b6000000f5540000ff0001000000d6040000"), + parse_rach_packet("01010000063238000100010000032900a4ffeb000000000195b603000000a0b412000420061f425dc9be41b800885e000017000100000065050000"), + parse_rach_packet("010100000632380001000100010713ffa0ffeb0000000001ad5a0500000146b412000420061f425dc9be41b400665300001800010000001a050000"), + ] + } + fn parse_rach_packet(bytes_str: &str) -> Packet { let (total_size, mut reader) = unhexlify(bytes_str); let packet = Packet::from_reader_with_ctx(&mut reader, ()).unwrap(); @@ -184,14 +233,13 @@ mod test { } fn assert_rach_subpacket( - hexstring: &str, + packet: &Packet, header: AttemptHeader, msg1: Option, msg2: Option, msg3: Option, additional_info: Option, ) { - let packet = parse_rach_packet(hexstring); assert_eq!(packet.version, 0x01); assert_eq!(packet.num_subpackets, 1); assert_eq!(packet.subpackets.len(), 1); @@ -215,8 +263,9 @@ mod test { * the changes in this commit for more info: * https://github.com/wgreenberg/scat/commit/adb21575832b4f3b30c8f2aaca9ee843ef74f38b */ + let test_packets = mac_rach_test_packets_from_scat(); assert_rach_subpacket( - "0101a06906022400010001071BFF98FF000001231A0400181C010007000600465C80BD0648000000", + &test_packets[0], rach::AttemptHeader::V2 { num_attempt: 1, rach_result: 0, @@ -235,8 +284,8 @@ mod test { ta: 4, }), Some(Msg3 { - grant_raw: 72728, - grant: 7, + grant: Msg3Grant::V1 { grant: 72728 }, + unk_grant: 7, harq_id: 6, mac_pdu: [0x00, 0x46, 0x5c, 0x80, 0xbd, 0x06, 0x48, 0x00, 0x00, 0x00], }), @@ -244,7 +293,7 @@ mod test { ); assert_rach_subpacket( - "0101a0690603280001000100010718ffa4ff000001c6610b00b4a2000012000120061f423f8d95075800", + &test_packets[1], rach::AttemptHeader::V3 { sub_id: 1, cell_id: 0, @@ -265,8 +314,8 @@ mod test { ta: 11, }), Some(Msg3 { - grant_raw: 41652, - grant: 18, + grant: Msg3Grant::V1 { grant: 41652 }, + unk_grant: 18, harq_id: 1, mac_pdu: [0x20, 0x06, 0x1f, 0x42, 0x3f, 0x8d, 0x95, 0x07, 0x58, 0x00], }), @@ -274,7 +323,7 @@ mod test { ); assert_rach_subpacket( - "0101739e063134000100010000033f0098ff0000013c6b070058ac010007000000468f47e2d446000000644b0000180001000000d5040000", + &test_packets[2], rach::AttemptHeader::V3 { sub_id: 1, cell_id: 0, @@ -305,7 +354,7 @@ mod test { ); assert_rach_subpacket( - "01010000063134000100010001070aff98ff0000011c48070018e2000007000000523b7dfd69b6000000f5540000ff0001000000d6040000", + &test_packets[3], AttemptHeader::V3 { sub_id: 1, cell_id: 0, @@ -326,8 +375,8 @@ mod test { ta: 7, }), Some(Msg3 { - grant_raw: 57880, - grant: 7, + grant: Msg3Grant::V1 { grant: 57880 }, + unk_grant: 7, harq_id: 0, mac_pdu: [0x00, 0x52, 0x3b, 0x7d, 0xfd, 0x69, 0xb6, 0x00, 0x00, 0x00], }), @@ -341,7 +390,7 @@ mod test { ); assert_rach_subpacket( - "01010000063238000100010000032900a4ffeb000000000195b603000000a0b412000420061f425dc9be41b800885e000017000100000065050000", + &test_packets[4], AttemptHeader::V3 { sub_id: 1, cell_id: 0, @@ -374,7 +423,7 @@ mod test { ); assert_rach_subpacket( - "010100000632380001000100010713ffa0ffeb0000000001ad5a0500000146b412000420061f425dc9be41b400665300001800010000001a050000", + &test_packets[5], AttemptHeader::V3 { sub_id: 1, cell_id: 0, @@ -397,8 +446,8 @@ mod test { ta: 5, }), Some(Msg3 { - grant_raw: 3024486656, - grant: 18, + grant: Msg3Grant::V32 { grant: 83636 }, + unk_grant: 18, harq_id: 4, mac_pdu: [0x20, 0x06, 0x1f, 0x42, 0x5d, 0xc9, 0xbe, 0x41, 0xb4, 0x00], }), diff --git a/lib/src/diag/diaglog/measurement.rs b/lib/src/diag/diaglog/measurement.rs index 5fb96ec..03cff8d 100644 --- a/lib/src/diag/diaglog/measurement.rs +++ b/lib/src/diag/diaglog/measurement.rs @@ -194,7 +194,7 @@ pub mod neighbor_cells { #[cfg(test)] mod test { - use super::super::test_util::unhexlify; + use crate::test_util::unhexlify; use super::*; use crate::diag::diaglog::LogBody; use crate::log_codes::{LOG_LTE_ML1_NEIGHBOR_MEAS, LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C}; diff --git a/lib/src/diag/diaglog/mod.rs b/lib/src/diag/diaglog/mod.rs index f97ef5c..2dc832a 100644 --- a/lib/src/diag/diaglog/mod.rs +++ b/lib/src/diag/diaglog/mod.rs @@ -6,8 +6,6 @@ use deku::prelude::*; pub mod mac; pub mod measurement; pub mod rrc; -#[cfg(test)] -mod test_util; #[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[deku(ctx = "log_type: u16, hdr_len: u16", id = "log_type")] @@ -87,11 +85,17 @@ pub enum LogBody { LteMl1NeighborCellsMeasurements { data: measurement::neighbor_cells::Measurements, }, - // Raw bytes; subpacket parsing happens in gsmtap_parser to extract Timing Advance #[deku(id = "0xb062")] LteMacRachResponse { - #[deku(count = "hdr_len")] - payload: Vec, + packet: mac::Packet, + }, + #[deku(id = "0xb063")] + LteMacDl { + packet: mac::Packet, + }, + #[deku(id = "0xb064")] + LteMacUl { + packet: mac::Packet, }, } @@ -245,7 +249,7 @@ pub(crate) mod test { log_type, log_mask_bitsize: bitsize, log_mask: vec![ - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xc, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x1, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, diff --git a/lib/src/diag_device.rs b/lib/src/diag_device.rs index ab2952f..36676a6 100644 --- a/lib/src/diag_device.rs +++ b/lib/src/diag_device.rs @@ -40,7 +40,7 @@ pub enum DiagDeviceError { ParseMessagesContainerError(deku::DekuError), } -pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 15] = [ +pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 17] = [ // Layer 2: log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226 // Layer 3: @@ -62,6 +62,8 @@ pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 15] = [ log_codes::LOG_LTE_ML1_NEIGHBOR_MEAS, // 0xb180 // LTE MAC Random Access Channel response: contains Timing Advance log_codes::LOG_LTE_MAC_RACH_RESPONSE_C, // 0xb062 + log_codes::LOG_LTE_MAC_DL, // 0xb063 + log_codes::LOG_LTE_MAC_UL, // 0xb064 ]; const BUFFER_LEN: usize = 1024 * 1024 * 10; diff --git a/lib/src/gsmtap/mac.rs b/lib/src/gsmtap/mac.rs new file mode 100644 index 0000000..047e3bc --- /dev/null +++ b/lib/src/gsmtap/mac.rs @@ -0,0 +1,160 @@ +use deku::prelude::*; + +use crate::{diag::diaglog::mac::SubpacketBody, gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType}}; +use deku::{DekuContainerWrite, DekuError}; + +// based primarily off of SCAT's gsmtap responses and https://www.sharetechnote.com/html/MAC_LTE.html#MAC_PDU_Structure_RAR +#[derive(DekuRead, DekuWrite)] +pub struct Header { + pub radio_type: RadioType, + pub direction: Direction, + pub rnti_type: RntiType, +} + +#[derive(DekuRead, DekuWrite)] +#[deku(id_type = "u8")] +pub enum RadioType { + #[deku(id = "1")] + Fdd, + #[deku(id = "2")] + Tdd, +} + +#[derive(DekuRead, DekuWrite)] +#[deku(id_type = "u8")] +pub enum Direction { + #[deku(id = "0")] + Uplink, + #[deku(id = "1")] + Downlink, +} + +#[derive(DekuRead, DekuWrite)] +#[deku(id_type = "u8")] +pub enum RntiType { + #[deku(id = "0")] + NO, + #[deku(id = "1")] + P, + #[deku(id = "2")] + RA, + #[deku(id = "3")] + C, + #[deku(id = "4")] + SI, + #[deku(id = "5")] + SPS, + #[deku(id = "6")] + M, + #[deku(id = "7")] + SL, + #[deku(id = "9")] + SC, + #[deku(id = "10")] + G, +} + +// defined in 6.5.1 of 3GPP TS 36.321 +#[derive(DekuRead, DekuWrite)] +#[deku(endian = "big")] +pub struct ETRAPIDSubheader { + #[deku(bits = 1)] + pub extended: bool, + #[deku(bits = 1)] + pub type_field: bool, + #[deku(bits = 6)] + pub rapid: u8, +} + +#[derive(DekuRead, DekuWrite)] +#[deku(endian = "big")] +pub struct RACHResponse { + #[deku(pad_bits_before = "1", bits = 11)] + pub tac: u16, + #[deku(bits = 20)] + pub ul_grant: u32, + pub tc_rnti: u16, +} + +pub fn mac_subpacket_to_gsmtap(subpacket: &SubpacketBody) -> Result, DekuError> { + match subpacket { + SubpacketBody::RachAttempt(attempt) => { + let (Some(msg1), Some(msg2), Some(msg3)) = (attempt.get_msg1(), attempt.get_msg2(), attempt.get_msg3()) else { + return Ok(None); + }; + let mut payload = Vec::new(); + payload.extend(Header { + radio_type: RadioType::Fdd, + direction: Direction::Downlink, + rnti_type: RntiType::RA, + }.to_bytes()?); + payload.push(0x01); // MAC Payload Tag + payload.extend(ETRAPIDSubheader { + extended: false, + type_field: true, + rapid: msg1.get_preamble_index(), + }.to_bytes()?); + payload.extend(RACHResponse { + tac: msg2.ta, + ul_grant: msg3.get_grant(), + tc_rnti: msg2.tc_rnti, + }.to_bytes()?); + Ok(Some(GsmtapMessage { + header: GsmtapHeader::new(GsmtapType::LteMacFramed), + payload, + })) + }, + _ => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use crate::diag::diaglog::mac::Packet; + use crate::diag::diaglog::mac::test::mac_rach_test_packets_from_scat; + use crate::test_util::unhexlify; + + use super::*; + + fn assert_mac_gsmtap(packet: &Packet, expected_hexstr: Option<&str>) { + assert_eq!(packet.subpackets.len(), 1); + let subpacket = &packet.subpackets[0]; + let result = mac_subpacket_to_gsmtap(&subpacket.body).unwrap(); + match (result, expected_hexstr) { + (Some(msg), Some(hexstr)) => { + let (_, data) = unhexlify(hexstr); + // SCAT's test cases use GSMTAP v3, but we're on V2, so skip + // their GSMTAP header + let expected_bytes = &data.into_inner().into_inner()[34..]; + assert_eq!(&msg.payload, expected_bytes); + }, + (Some(msg), None) => panic!("expected no GSMTAP message, got {msg:?}"), + (None, Some(_)) => panic!("expected GSMTAP message, got None"), + _ => {}, + } + } + + #[test] + fn test_mac_rach() { + // test data from SCAT unit tests: https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/tests/test_diagltelogparser.py#L129 + let test_packets = mac_rach_test_packets_from_scat(); + assert_mac_gsmtap( + &test_packets[0], + Some("03000009040000000000000c0000000012d53d80000000000002000400000000fffe010102015b00411c181a23"), + ); + assert_mac_gsmtap( + &test_packets[1], + Some("03000009040000000000000c0000000012d53d80000000000002000400000000fffe010102015800b0a2b461c6"), + ); + assert_mac_gsmtap(&test_packets[2], None); + assert_mac_gsmtap( + &test_packets[3], + Some("03000009040000000000000c0000000012d53d80000000000002000400000ea5fffe010102014a0070e218481c"), + ); + assert_mac_gsmtap(&test_packets[4], None); + assert_mac_gsmtap( + &test_packets[5], + Some("03000009040000000000000c0000000012d53d80000000000002000400000d16fffe0101020153005146b45aad"), + ); + } +} diff --git a/lib/src/gsmtap/mod.rs b/lib/src/gsmtap/mod.rs index d40c82d..2cd65c5 100644 --- a/lib/src/gsmtap/mod.rs +++ b/lib/src/gsmtap/mod.rs @@ -4,6 +4,7 @@ use deku::prelude::*; use num_enum::TryFromPrimitive; pub mod parser; +mod mac; #[derive(Debug, Copy, Clone, PartialEq)] pub enum GsmtapType { diff --git a/lib/src/gsmtap/parser.rs b/lib/src/gsmtap/parser.rs index 33bbb26..ac2a476 100644 --- a/lib/src/gsmtap/parser.rs +++ b/lib/src/gsmtap/parser.rs @@ -1,8 +1,9 @@ use crate::diag::Message; use crate::diag::diaglog::{LogBody, Nas4GMessageDirection, Timestamp}; +use crate::gsmtap::mac::mac_subpacket_to_gsmtap; use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype}; -use log::debug; +use log::{debug, warn}; use thiserror::Error; #[derive(Debug, Error)] @@ -11,6 +12,8 @@ pub enum GsmtapParserError { InvalidLteRrcOtaExtHeaderVersion(u8), #[error("Invalid LteRrcOtaMessage header/PDU number combination: {0}/{1}")] InvalidLteRrcOtaHeaderPduNum(u8, u8), + #[error("Invalid LteMacRachResponse packet: {0}")] + InvalidLteMacRachResponse(String), } pub fn parse(msg: Message) -> Result, GsmtapParserError> { @@ -165,7 +168,18 @@ fn log_to_gsmtap(value: LogBody) -> Result, GsmtapParserEr payload: vec![], })) } - LogBody::LteMacRachResponse { payload } => Ok(parse_rach_response(&payload)), + LogBody::LteMacRachResponse { packet } => { + if packet.subpackets.len() > 1 { + warn!("expected 1 MAC subpacket for LogBody::LteMacRachResponse, but got {}! ignoring all but the first", packet.subpackets.len()); + } + let Some(subpacket) = packet.subpackets.get(0) else { + return Err(GsmtapParserError::InvalidLteMacRachResponse(format!("no subpackets"))); + }; + mac_subpacket_to_gsmtap(&subpacket.body) + .map_err(|err| { + GsmtapParserError::InvalidLteMacRachResponse(format!("unable to serialize GSMTAP payload: {err:?}")) + }) + }, _ => { debug!("gsmtap_sink: ignoring unhandled log type: {value:?}"); Ok(None) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a649960..73ef58a 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -20,6 +20,8 @@ pub mod log_codes; pub mod pcap; pub mod qmdl; pub mod util; +#[cfg(test)] +mod test_util; // bin/check.rs may target windows and does not use this mod #[cfg(target_family = "unix")] diff --git a/lib/src/log_codes.rs b/lib/src/log_codes.rs index ecbaa80..e0bcc34 100644 --- a/lib/src/log_codes.rs +++ b/lib/src/log_codes.rs @@ -37,6 +37,8 @@ pub const LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE: u32 = 0xb193; pub const LOG_LTE_ML1_NEIGHBOR_MEAS: u32 = 0xb180; // Qualcomm MAC layer RACH response log: contains Timing Advance from Random Access Response pub const LOG_LTE_MAC_RACH_RESPONSE_C: u32 = 0xb062; +pub const LOG_LTE_MAC_DL: u32 = 0xb063; +pub const LOG_LTE_MAC_UL: u32 = 0xb064; pub const LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C: u32 = 0xb0e2; pub const LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C: u32 = 0xb0e3; pub const LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C: u32 = 0xb0ec; diff --git a/lib/src/diag/diaglog/test_util.rs b/lib/src/test_util.rs similarity index 100% rename from lib/src/diag/diaglog/test_util.rs rename to lib/src/test_util.rs