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.
This commit is contained in:
Will Greenberg
2026-06-18 13:45:59 -07:00
parent 759b2ea4c5
commit 338d41dceb
11 changed files with 266 additions and 33 deletions
+2 -3
View File
@@ -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,
+69 -20
View File
@@ -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<AdditionalInfo>,
@@ -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<Packet> {
// 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<Msg1>,
msg2: Option<Msg2>,
msg3: Option<Msg3>,
additional_info: Option<AdditionalInfo>,
) {
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],
}),
+1 -1
View File
@@ -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};
+10 -6
View File
@@ -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<u8>,
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,
+3 -1
View File
@@ -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;
+160
View File
@@ -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<Option<GsmtapMessage>, 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"),
);
}
}
+1
View File
@@ -4,6 +4,7 @@ use deku::prelude::*;
use num_enum::TryFromPrimitive;
pub mod parser;
mod mac;
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum GsmtapType {
+16 -2
View File
@@ -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<Option<(Timestamp, GsmtapMessage)>, GsmtapParserError> {
@@ -165,7 +168,18 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, 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)
+2
View File
@@ -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")]
+2
View File
@@ -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;