Compare commits

...

28 Commits

Author SHA1 Message Date
Will Greenberg c39235d5e4 wip 2026-06-26 14:57:51 -07:00
Will Greenberg 21e12d16e1 wip 2026-06-26 14:54:03 -07:00
Will Greenberg 892c833344 Add LL1 Serving Cell Timing messages
This log should provide constant feedback about the serving cell's LTE
timing advance value.
2026-06-26 11:49:38 -07:00
Will Greenberg 3a7f512842 run cargo fmt 2026-06-25 07:39:36 -07:00
Will Greenberg 8d8099f66b Remove Mac UL/DL message support
These weren't parsing correctly despite the parser seemingly matching
SCATs, so disable these for now. They weren't being used for anything
yet, anyway.
2026-06-25 07:38:37 -07:00
Will Greenberg 7831c5085e If a Message isn't convertable to GSMTAP, don't try
This also removes the pseudo GSMTAP packets added for measurement
results, since they add a ton of noise to PCAPs. Eventually, we should
keep track of the latest signal for a given PCI, and annotate actual
packets with that value.
2026-06-25 07:36:23 -07:00
Will Greenberg 956b719e12 hardcode build_log_mask_request() unit test input
Instead of having to change the expected value every time we add a log
type to our list, let's just hardcode it to a known value.
2026-06-24 16:17:31 -07:00
Will Greenberg 73bcb05a0b rm unimplemented diag message format 2026-06-24 15:52:08 -07:00
Will Greenberg 6c8b6ec64f run cargo fmt 2026-06-24 12:43:29 -07:00
Will Greenberg 63ea5df6f7 appease clippy 2026-06-24 12:42:58 -07:00
Will Greenberg 2d63fc00c7 add more comments 2026-06-24 12:42:58 -07:00
Will Greenberg 2927f791e4 cleanups from rebase 2026-06-24 12:42:58 -07:00
Will Greenberg ec5b306311 refactors/tweaks 2026-06-24 12:42:58 -07:00
Will Greenberg a5c86bc408 run cargo fmt 2026-06-24 12:42:58 -07:00
Will Greenberg 338d41dceb 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.
2026-06-24 12:42:55 -07:00
Will Greenberg 759b2ea4c5 run cargo fmt 2026-06-24 12:40:03 -07:00
Will Greenberg a51cafbb14 lib/diag/diaglog: add MAC parsing for RACH attempts
This adds a deku parser for MAC RACH packets, along with some unit tests
adapted from SCAT's parser.
2026-06-24 12:39:11 -07:00
Will Greenberg ddc39cf516 Update diag log mask for new messages 2026-06-24 12:39:11 -07:00
Will Greenberg 66d83ea575 trim test case
These measurement packets are of a fixed length, and the SCAT test case
had excess data
2026-06-24 12:39:11 -07:00
Will Greenberg 0028847b73 lib/gsmtap_parser: downgrade unsupported log to debug msg
Previously this was an error message to help underscore when a device
was sending unexpected messages, but now that we're receiving
measurement logs which have no place in GSMTAP frames, it's expected to
skip some log messages.
2026-06-24 12:39:11 -07:00
Will Greenberg 6fec428c5b lib/diag: add ML1 Neighbor cell measurement
This adds support for Neighboring Cells Measurements, and makes some
minor changes to Serving Cell Measurements.
2026-06-24 12:39:10 -07:00
Carlos Guerra 11b6af96c7 format fixes for linters to be happy 2026-06-24 12:37:38 -07:00
Carlos Guerra 76c2039556 addressing review comments: minor refactor for optimization, and correction of rrc_rel size 2026-06-24 12:37:37 -07:00
Carlos Guerra c424902560 linting and polishing for PR 2026-06-24 12:35:29 -07:00
Carlos Guerra 8f03ad8f4a Collect signal strength and timing advances. LTE serving cell measurements (0xb17f) and RACH Timing Advance (0xb062) 2026-06-24 12:32:11 -07:00
Will Greenberg 68ba2ab625 run cargo fmt 2026-06-24 12:25:56 -07:00
Will Greenberg 38f476b664 lib: refactor gsmtap/gsmtap_parser into a single module
This'll allow us to break out more specific GSMTAP parsing into
submodules more easily.
2026-06-24 12:24:01 -07:00
Will Greenberg cae056d959 lib/diag.rs refactor
This splits diag.rs, which was growing way too big for my taste, into a
number of submodules. This should help us compartmentalize tests better,
as well as use mod namespaces to shorten our struct/enum names.
2026-06-24 12:19:11 -07:00
19 changed files with 1633 additions and 432 deletions
+12 -7
View File
@@ -2,10 +2,7 @@ use clap::Parser;
use log::{debug, error, info, warn};
use pcap_file_tokio::pcapng::{Block, PcapNgReader};
use rayhunter::{
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness},
gsmtap_parser,
pcap::GsmtapPcapWriter,
qmdl::QmdlMessageReader,
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness}, diag::Message, gsmtap::parser as gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlMessageReader,
};
use std::{collections::HashMap, path::PathBuf};
use tokio::fs::File;
@@ -115,12 +112,20 @@ async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool) {
.await
.expect("failed to open QmdlReader");
let mut report = Report::new(qmdl_path);
while let Some(maybe_message) = qmdl_reader
.get_next_message()
let mut stats = HashMap::new();
while let Some(buf) = qmdl_reader
.get_next_buf()
.await
.expect("failed to get message")
{
report.process_row(harness.analyze_qmdl_message(maybe_message));
let log_type = u16::from_le_bytes([buf[6], buf[7]]);
stats.entry(log_type)
.and_modify(|total| *total += buf.len())
.or_insert(buf.len());
report.process_row(harness.analyze_qmdl_message(Message::from_hdlc(&buf)));
}
for (id, size) in stats {
println!("{id:04x}: {size}");
}
report.print_summary(show_skipped);
}
+1 -1
View File
@@ -10,7 +10,7 @@ use axum::http::StatusCode;
use axum::http::header::CONTENT_TYPE;
use axum::response::{IntoResponse, Response};
use log::error;
use rayhunter::gsmtap_parser;
use rayhunter::gsmtap::parser as gsmtap_parser;
use rayhunter::pcap::{GpsPoint, GsmtapPcapWriter};
use rayhunter::qmdl::QmdlMessageReader;
use std::sync::Arc;
+19 -9
View File
@@ -5,10 +5,10 @@ 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::diaglog::LogBody;
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};
use super::{
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
@@ -431,17 +431,27 @@ impl Harness {
return row;
}
};
let gsmtap_message = match gsmtap_parser::parse(qmdl_message) {
Ok(msg) => msg,
if let Message::Log { body, .. } = &qmdl_message {
match body {
LogBody::LteLl1ServingCellTiming { data } => {
if data.starting_ul_timing_advance != 0 {
println!("ta {}", data.starting_ul_timing_advance);
}
}
LogBody::LteMl1ServingCellMeasurementAndEvaluation { data } => {
// println!("serving cell pci {}, earfcn {}", data.get_pci(), data.get_earfcn());
}
_ => {},
}
}
let (timestamp, gsmtap_msg) = match gsmtap_parser::parse(qmdl_message) {
Ok(Some(msg)) => msg,
Ok(None) => return row,
Err(err) => {
row.skipped_message_reason = Some(format!("{err:?}"));
return row;
}
};
let Some((timestamp, gsmtap_msg)) = gsmtap_message else {
return row;
};
row.packet_timestamp = Some(timestamp.to_datetime());
let element = match InformationElement::try_from(&gsmtap_msg) {
+33
View File
@@ -0,0 +1,33 @@
use deku::prelude::*;
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(bit_order = "lsb", ctx = "_: deku::ctx::Order")]
pub struct ServingCellTiming {
#[deku(assert_eq = "1")]
pub version: u8,
#[deku(bits = 5, assert = "*num_records <= 20")]
pub num_records: u8,
#[deku(bits = 4, assert = "*starting_sub_fn <= 9")]
pub starting_sub_fn: u8,
#[deku(bits = 10, pad_bits_after = "5", assert = "*starting_system_fn <= 1023")]
pub starting_system_fn: u16,
#[deku(bits = 19, pad_bits_after = "13", assert = "*starting_dl_frame_timing_offs <= 307200")]
pub starting_dl_frame_timing_offs: u32, // in Ts units
#[deku(bits = 19, assert = "*starting_ul_frame_timing_offs <= 307200")]
pub starting_ul_frame_timing_offs: u32, // in Ts units
#[deku(bits = 11, pad_bits_after = "2")]
pub starting_ul_timing_advance: u16, // in 16 Ts units
#[deku(count = "*num_records")]
pub timing_adjustment: Vec<TimingAdjustment>,
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(bit_order = "lsb", ctx = "_: deku::ctx::Order")]
pub struct TimingAdjustment {
#[deku(bits = 11, assert = "(-512..=511).contains(dl_frame_timing_adjustment)")]
pub dl_frame_timing_adjustment: i16, // in Ts units
#[deku(bits = 5, assert = "(-16..=15).contains(ul_frame_timing_adjustment)")]
pub ul_frame_timing_adjustment: i8, // in Ts units
#[deku(bits = 8, assert = "(-128..=127).contains(timing_advance)")]
pub timing_advance: i8, // in 16 Ts units
}
+480
View File
@@ -0,0 +1,480 @@
//! Diag MAC RACH serialization/deserialization. As with most of our diag
//! parsers, these structs were derived SCAT:
//! https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/src/scat/parsers/qualcomm/diagltelogparser.py#L853
use deku::prelude::*;
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
pub struct Packet {
#[deku(assert_eq = "1")]
pub version: u8,
pub num_subpackets: u8,
#[deku(pad_bytes_before = "2", count = "*num_subpackets")]
pub subpackets: Vec<Subpacket>,
}
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
pub struct Subpacket {
pub id: u8,
pub version: u8,
pub size: u16,
// size includes the header length, so subtract that
#[deku(ctx = "*id, *version, *size - 4")]
pub body: SubpacketBody,
}
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
#[deku(ctx = "id: u8, version: u8, size: u16", id = "id")]
pub enum SubpacketBody {
#[deku(id = 0x06)]
RachAttempt(#[deku(ctx = "version")] rach::Attempt),
#[deku(id_pat = "_")]
Other {
#[deku(count = "size")]
data: Vec<u8>,
},
}
pub mod rach {
//! Derived from https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/src/scat/parsers/qualcomm/diagltelogparser.py#L496
use super::*;
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
#[deku(ctx = "version: u8")]
pub struct Attempt {
#[deku(ctx = "version")]
pub header: AttemptHeader,
#[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>,
}
impl Attempt {
pub fn get_msg1(&self) -> Option<&Msg1> {
if self.header.has_msg1() {
Some(&self.msg1)
} else {
None
}
}
pub fn get_msg2(&self) -> Option<&Msg2> {
if self.header.has_msg2() {
Some(&self.msg2)
} else {
None
}
}
pub fn get_msg3(&self) -> Option<&Msg3> {
if self.header.has_msg3() {
Some(&self.msg3)
} else {
None
}
}
}
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
pub struct AdditionalInfo {
pub ul_earfcn: u32,
pub p_max: u8,
pub scell_id: u8,
pub unk1: u32,
pub unk2: u32,
}
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
#[deku(ctx = "version: u8", id = "version")]
pub enum Msg1 {
#[deku(id = "0x02")]
V2 {
preamble_index: u8,
preamble_index_mask: u8,
preamble_power_offset: i16,
},
#[deku(id_pat = "0x03 | 0x31")]
V3Or31 {
preamble_index: u8,
preamble_index_mask: u8,
preamble_power_offset: i16,
},
#[deku(id = "0x32")]
V32 {
preamble_index: u8,
preamble_index_mask: u8,
preamble_power_offset: i16,
unk1: u16,
group: i8,
},
}
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,
pub result: u8,
pub tc_rnti: u16,
pub ta: u16,
}
#[derive(DekuRead, DekuWrite, Debug, Clone, PartialEq)]
#[deku(ctx = "version: u8")]
pub struct Msg3 {
#[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 {
#[deku(id = 0x02)]
V2 {
num_attempt: u8,
rach_result: u8,
contention: u8,
msg_bitmask: u8,
},
#[deku(id_pat = "0x03 | 0x31 | 0x32")]
V3 {
sub_id: u8,
cell_id: u8,
num_attempt: u8,
rach_result: u8,
contention: u8,
msg_bitmask: u8,
},
}
impl AttemptHeader {
fn get_bitmask(&self) -> u8 {
match self {
AttemptHeader::V2 { msg_bitmask, .. } => *msg_bitmask,
AttemptHeader::V3 { msg_bitmask, .. } => *msg_bitmask,
}
}
pub fn has_msg1(&self) -> bool {
self.get_bitmask() & 0x01 > 0
}
pub fn has_msg2(&self) -> bool {
self.get_bitmask() & 0x02 > 0
}
pub fn has_msg3(&self) -> bool {
self.get_bitmask() & 0x04 > 0
}
}
}
#[cfg(test)]
pub(crate) mod test {
//! These tests were adapted from SCAT's MAC RACH parser's unit tests,
//! and the values were produced by modifying the tests to output the
//! entire parsed struct rather than the hexlified gsmtap packets. See
//! the changes in this commit for more info:
//! https://github.com/wgreenberg/scat/commit/adb21575832b4f3b30c8f2aaca9ee843ef74f38b
use super::*;
use crate::diag::diaglog::mac::rach::{AdditionalInfo, AttemptHeader, Msg1, Msg2, Msg3};
use crate::{diag::diaglog::mac::rach::Msg3Grant, test_util::unhexlify};
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();
let leftover_bits = reader.rest().len();
let leftover_bytes = total_size - reader.stream_position().unwrap() as usize;
assert_eq!(leftover_bytes, 0);
assert_eq!(leftover_bits, 0);
packet
}
fn assert_rach_subpacket(
packet: &Packet,
header: AttemptHeader,
msg1: Option<Msg1>,
msg2: Option<Msg2>,
msg3: Option<Msg3>,
additional_info: Option<AdditionalInfo>,
) {
assert_eq!(packet.version, 0x01);
assert_eq!(packet.num_subpackets, 1);
assert_eq!(packet.subpackets.len(), 1);
if let SubpacketBody::RachAttempt(attempt) = &packet.subpackets[0].body {
assert_eq!(attempt.header, header);
assert_eq!(attempt.get_msg1(), msg1.as_ref());
assert_eq!(attempt.get_msg2(), msg2.as_ref());
assert_eq!(attempt.get_msg3(), msg3.as_ref());
assert_eq!(attempt.additional_info, additional_info);
} else {
panic!("not rach attempt {:?}", packet.subpackets[0].body);
}
}
#[test]
fn test_rach_attempt_parsing() {
let test_packets = mac_rach_test_packets_from_scat();
assert_rach_subpacket(
&test_packets[0],
rach::AttemptHeader::V2 {
num_attempt: 1,
rach_result: 0,
contention: 1,
msg_bitmask: 7,
},
Some(Msg1::V2 {
preamble_index: 27,
preamble_index_mask: 255,
preamble_power_offset: -104,
}),
Some(Msg2 {
backoff: 0,
result: 1,
tc_rnti: 6691,
ta: 4,
}),
Some(Msg3 {
grant: Msg3Grant::V1 { grant: 72728 },
unk_grant: 7,
harq_id: 6,
mac_pdu: [0x00, 0x46, 0x5c, 0x80, 0xbd, 0x06, 0x48, 0x00, 0x00, 0x00],
}),
None,
);
assert_rach_subpacket(
&test_packets[1],
rach::AttemptHeader::V3 {
sub_id: 1,
cell_id: 0,
num_attempt: 1,
rach_result: 0,
contention: 1,
msg_bitmask: 7,
},
Some(Msg1::V3Or31 {
preamble_index: 24,
preamble_index_mask: 255,
preamble_power_offset: -92,
}),
Some(Msg2 {
backoff: 0,
result: 1,
tc_rnti: 25030,
ta: 11,
}),
Some(Msg3 {
grant: Msg3Grant::V1 { grant: 41652 },
unk_grant: 18,
harq_id: 1,
mac_pdu: [0x20, 0x06, 0x1f, 0x42, 0x3f, 0x8d, 0x95, 0x07, 0x58, 0x00],
}),
None,
);
assert_rach_subpacket(
&test_packets[2],
rach::AttemptHeader::V3 {
sub_id: 1,
cell_id: 0,
num_attempt: 1,
rach_result: 0,
contention: 0,
msg_bitmask: 3,
},
Some(Msg1::V3Or31 {
preamble_index: 63,
preamble_index_mask: 0,
preamble_power_offset: -104,
}),
Some(Msg2 {
backoff: 0,
result: 1,
tc_rnti: 27452,
ta: 7,
}),
None,
Some(AdditionalInfo {
ul_earfcn: 19300,
p_max: 24,
scell_id: 0,
unk1: 1,
unk2: 1237,
}),
);
assert_rach_subpacket(
&test_packets[3],
AttemptHeader::V3 {
sub_id: 1,
cell_id: 0,
num_attempt: 1,
rach_result: 0,
contention: 1,
msg_bitmask: 7,
},
Some(Msg1::V3Or31 {
preamble_index: 10,
preamble_index_mask: 255,
preamble_power_offset: -104,
}),
Some(Msg2 {
backoff: 0,
result: 1,
tc_rnti: 18460,
ta: 7,
}),
Some(Msg3 {
grant: Msg3Grant::V1 { grant: 57880 },
unk_grant: 7,
harq_id: 0,
mac_pdu: [0x00, 0x52, 0x3b, 0x7d, 0xfd, 0x69, 0xb6, 0x00, 0x00, 0x00],
}),
Some(AdditionalInfo {
ul_earfcn: 21749,
p_max: 255,
scell_id: 0,
unk1: 1,
unk2: 1238,
}),
);
assert_rach_subpacket(
&test_packets[4],
AttemptHeader::V3 {
sub_id: 1,
cell_id: 0,
num_attempt: 1,
rach_result: 0,
contention: 0,
msg_bitmask: 3,
},
Some(Msg1::V32 {
preamble_index: 41,
preamble_index_mask: 0,
preamble_power_offset: -92,
unk1: 235,
group: 0,
}),
Some(Msg2 {
backoff: 0,
result: 1,
tc_rnti: 46741,
ta: 3,
}),
None,
Some(AdditionalInfo {
ul_earfcn: 24200,
p_max: 23,
scell_id: 0,
unk1: 1,
unk2: 1381,
}),
);
assert_rach_subpacket(
&test_packets[5],
AttemptHeader::V3 {
sub_id: 1,
cell_id: 0,
num_attempt: 1,
rach_result: 0,
contention: 1,
msg_bitmask: 7,
},
Some(Msg1::V32 {
preamble_index: 19,
preamble_index_mask: 255,
preamble_power_offset: -96,
unk1: 235,
group: 0,
}),
Some(Msg2 {
backoff: 0,
result: 1,
tc_rnti: 23213,
ta: 5,
}),
Some(Msg3 {
grant: Msg3Grant::V32 { grant: 83636 },
unk_grant: 18,
harq_id: 4,
mac_pdu: [0x20, 0x06, 0x1f, 0x42, 0x5d, 0xc9, 0xbe, 0x41, 0xb4, 0x00],
}),
Some(AdditionalInfo {
ul_earfcn: 21350,
p_max: 24,
scell_id: 0,
unk1: 1,
unk2: 1306,
}),
);
}
}
+351
View File
@@ -0,0 +1,351 @@
//! Diag ML1 measurement log serialization/deserialization. As with most of our
//! diag parsers, these structs were derived SCAT:
//! Neighbor cell measurements: https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/src/scat/parsers/qualcomm/diagltelogparser.py#L192
//! Serving cell measurements: https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/src/scat/parsers/qualcomm/diagltelogparser.py#L114
use deku::ctx::Order;
use deku::prelude::*;
fn decode_rsrp(rsrp: u16) -> f32 {
rsrp as f32 / 16.0 - 180.0
}
fn decode_rssi(rssi: u16) -> f32 {
rssi as f32 / 16.0 - 110.0
}
fn decode_rsrq(rsrq: u16) -> f32 {
rsrq as f32 / 16.0 - 30.0
}
pub mod serving_cell {
use super::*;
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(bit_order = "lsb")]
pub struct MeasurementAndEvaluation {
pub header: MeasurementAndEvaluationHeader,
#[deku(bits = 12, pad_bits_after = "20")]
meas_rsrp: u16,
avg_rsrp: u32,
#[deku(bits = 10, pad_bits_after = "22")]
meas_rsrq: u16,
#[deku(pad_bits_before = "10", bits = 11, pad_bits_after = "11")]
meas_rssi: u16,
rxlev: u32,
s_search: u32,
#[deku(cond = "header.get_rrc_rel() == 0x01")]
r9_data: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "_: Order", id_type = "u8", bit_order = "lsb")]
pub enum MeasurementAndEvaluationHeader {
#[deku(id = "4")]
V4 {
rrc_rel: u8,
_reserved: u16,
earfcn: u16,
#[deku(bits = 9)]
pci: u16,
#[deku(bits = 7)]
serv_layer_priority: u8,
},
#[deku(id = "5")]
V5 {
rrc_rel: u8,
_reserved: u16,
earfcn: u32,
#[deku(bits = 9)]
pci: u16,
#[deku(bits = 7, pad_bytes_after = "2")]
serv_layer_priority: u8,
},
}
impl MeasurementAndEvaluationHeader {
fn get_rrc_rel(&self) -> u8 {
match self {
MeasurementAndEvaluationHeader::V4 { rrc_rel, .. } => *rrc_rel,
MeasurementAndEvaluationHeader::V5 { rrc_rel, .. } => *rrc_rel,
}
}
}
impl MeasurementAndEvaluation {
pub fn get_pci(&self) -> u16 {
match &self.header {
MeasurementAndEvaluationHeader::V4 { pci, .. } => *pci,
MeasurementAndEvaluationHeader::V5 { pci, .. } => *pci,
}
}
pub fn get_earfcn(&self) -> u32 {
match &self.header {
MeasurementAndEvaluationHeader::V4 { earfcn, .. } => *earfcn as u32,
MeasurementAndEvaluationHeader::V5 { earfcn, .. } => *earfcn,
}
}
pub fn get_meas_rsrp(&self) -> f32 {
decode_rsrp(self.meas_rsrp)
}
pub fn get_meas_rssi(&self) -> f32 {
decode_rssi(self.meas_rssi)
}
pub fn get_meas_rsrq(&self) -> f32 {
decode_rsrq(self.meas_rsrq)
}
}
}
pub mod neighbor_cells {
use super::*;
#[derive(Clone, Debug, DekuRead, DekuWrite, PartialEq)]
#[deku(id_type = "u8", bit_order = "lsb")]
pub enum MeasurementsHeader {
#[deku(id = "4")]
V4 {
rrc_rel: u8,
_reserved1: u16,
earfcn: u16,
#[deku(bits = 6)]
q_rxlevmin: u8,
#[deku(bits = 10)]
n_cells: u16,
},
#[deku(id = "5")]
V5 {
rrc_rel: u8,
_reserved1: u16,
earfcn: u32,
#[deku(bits = 6)]
q_rxlevmin: u8,
#[deku(bits = 26)]
n_cells: u32,
},
}
impl MeasurementsHeader {
fn get_n_cells(&self) -> usize {
match self {
MeasurementsHeader::V4 { n_cells, .. } => *n_cells as usize,
MeasurementsHeader::V5 { n_cells, .. } => *n_cells as usize,
}
}
}
#[derive(Clone, Debug, DekuRead, DekuWrite, PartialEq)]
pub struct Measurements {
pub header: MeasurementsHeader,
#[deku(count = "header.get_n_cells()")]
pub cells: Vec<MeasurementsCell>,
}
impl Measurements {
pub fn get_earfcn(&self) -> u32 {
match &self.header {
MeasurementsHeader::V4 { earfcn, .. } => *earfcn as u32,
MeasurementsHeader::V5 { earfcn, .. } => *earfcn,
}
}
}
#[derive(Clone, Debug, DekuRead, DekuWrite, PartialEq)]
#[deku(bit_order = "lsb")]
pub struct MeasurementsCell {
#[deku(bits = 9)]
pub pci: u16,
#[deku(bits = 11)]
meas_rssi: u16,
#[deku(bits = 12)]
meas_rsrp: u16,
#[deku(pad_bits_before = "12", bits = 12, pad_bits_after = "8")]
avg_rsrp: u16,
#[deku(pad_bits_before = "12", bits = 10, pad_bits_after = "10")]
meas_rsrq: u16,
#[deku(bits = 10, pad_bits_after = "10")]
avg_rsrq: u16,
#[deku(bits = 6, pad_bits_after = "6")]
s_rxlev: u16,
n_freq_offset: u16,
val5: u16,
ant0_offset: u32,
ant1_offset: u32,
unk1: u32,
}
impl MeasurementsCell {
pub fn get_meas_rsrp(&self) -> f32 {
decode_rsrp(self.meas_rsrp)
}
pub fn get_meas_rssi(&self) -> f32 {
decode_rssi(self.meas_rssi)
}
pub fn get_meas_rsrq(&self) -> f32 {
decode_rsrq(self.meas_rsrq)
}
}
}
#[cfg(test)]
mod test {
//! The tests for serving cell/neighbor cell measurements were adapted from
//! SCAT's tests. The expected values were collected by modifying SCAT to
//! print out the full-precision expected values. See:
//! https://github.com/wgreenberg/scat/commit/e53d657861e8a66b52d635ff9518ac896c23ab06
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};
use crate::test_util::unhexlify;
use std::io::Seek;
fn parse_ncell_measurements(hexlified_bytes: &str) -> (u8, neighbor_cells::Measurements) {
let (total_size, mut reader) = unhexlify(hexlified_bytes);
match LogBody::from_reader_with_ctx(&mut reader, (LOG_LTE_ML1_NEIGHBOR_MEAS as u16, 0)) {
Ok(LogBody::LteMl1NeighborCellsMeasurements { data }) => {
if !reader.end() {
let leftover_bits = reader.rest();
let leftover_bytes = total_size - reader.stream_position().unwrap() as usize;
panic!(
"failed to read entire buffer ({} bytes, {} bits left)",
leftover_bytes,
leftover_bits.len()
);
}
let pkt_version = match data.header {
neighbor_cells::MeasurementsHeader::V4 { .. } => 4,
neighbor_cells::MeasurementsHeader::V5 { .. } => 5,
};
(pkt_version, data)
}
Ok(x) => panic!("expected MeasurementAndEvaluation, but parsed {:?}", x),
Err(x) => panic!("failed to parse MeasurementAndEvaluation {:?}", x),
}
}
fn parse_meas_eval(hexlified_bytes: &str) -> (u8, serving_cell::MeasurementAndEvaluation) {
let (total_size, mut reader) = unhexlify(hexlified_bytes);
match LogBody::from_reader_with_ctx(
&mut reader,
(LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C as u16, 0),
) {
Ok(LogBody::LteMl1ServingCellMeasurementAndEvaluation { data }) => {
if !reader.end() {
let leftover_bits = reader.rest();
let leftover_bytes = total_size - reader.stream_position().unwrap() as usize;
panic!(
"failed to read entire buffer ({} bytes, {} bits left)",
leftover_bytes,
leftover_bits.len()
);
}
let pkt_version = match data.header {
serving_cell::MeasurementAndEvaluationHeader::V4 { .. } => 4,
serving_cell::MeasurementAndEvaluationHeader::V5 { .. } => 5,
};
(pkt_version, data)
}
Ok(x) => panic!("expected MeasurementAndEvaluation, but parsed {:?}", x),
Err(x) => panic!("failed to parse MeasurementAndEvaluation {:?}", x),
}
}
fn scell_meas_and_eval_case(
hexlified_bytes: &str,
pkt_version: u8,
pci: u16,
earfcn: u32,
rsrp: f32,
rsrq: f32,
rssi: f32,
) {
let (parsed_pkt_version, data) = parse_meas_eval(hexlified_bytes);
assert_eq!(parsed_pkt_version, pkt_version);
assert_eq!(data.get_pci(), pci, "incorrect pci");
assert_eq!(data.get_earfcn(), earfcn, "incorrect earfcn");
assert_eq!(data.get_meas_rsrp(), rsrp, "incorrect rsrp");
assert_eq!(data.get_meas_rsrq(), rsrq, "incorrect rsrq");
assert_eq!(data.get_meas_rssi(), rssi, "incorrect rssi");
}
#[test]
fn test_scell_meas() {
scell_meas_and_eval_case(
"040100009C18D60AECC44E00E2244E00FFFCE30FFED80A0047AD56021D310100A2624100",
4,
214,
6300,
-101.25,
-14.0625,
-66.625,
);
scell_meas_and_eval_case(
"05010000160d0000d40e00004bb444005444450039e514133149070048adfe019f310100a23f0000",
5,
212,
3350,
-111.3125,
-10.4375,
-80.875,
);
scell_meas_and_eval_case(
"05010000f424000a4d43434d4e434d41524b45527c307c3236327c317c34323330333233347c7c4d",
5,
333,
167781620,
-127.125,
-22.25,
2.75,
);
scell_meas_and_eval_case(
"0501000000190000a90d0000d9944d00d9944d006081d5d55d2568bc48ad3e027f314fe0891900e0",
5,
425,
6400,
-102.4375,
-8.0,
-77.4375,
);
}
fn ncell_meas_case(
hexlified_bytes: &str,
pkt_version: u8,
earfcn: u32,
cells: Vec<(u16, f32, f32, f32)>,
) {
let (parsed_pkt_version, data) = parse_ncell_measurements(hexlified_bytes);
assert_eq!(parsed_pkt_version, pkt_version, "incorrect pkt_version");
assert_eq!(data.cells.len(), cells.len(), "incorrect number of cells");
assert_eq!(data.get_earfcn(), earfcn, "incorrect earfcn");
for (parsed, (pci, rsrp, rssi, rsrq)) in data.cells.iter().zip(cells) {
assert_eq!(parsed.pci, pci, "incorrect pci");
assert_eq!(parsed.get_meas_rsrp(), rsrp, "incorrect rsrp");
assert_eq!(parsed.get_meas_rssi(), rssi, "incorrect rssi");
assert_eq!(parsed.get_meas_rsrq(), rsrq, "incorrect rsrq");
}
}
#[test]
fn test_ncell_meas() {
ncell_meas_case(
"040100009C1847008348E44DDEA44C00CAB4CC32B6D8420300000000FF773301FF77330122020100",
4,
6300,
vec![(131, -102.125, -75.75, -17.3125)],
);
ncell_meas_case(
"05010000160d0000480000006cea413bb4433b00b4f3cc33cf3c130200000000ffefc00fffefc00f45081600",
5,
3350,
vec![(108, -120.75, -94.6875, -17.0625)],
);
}
}
+121 -394
View File
@@ -1,159 +1,12 @@
//! Diag protocol serialization/deserialization
//! Diag LogBody serialization/deserialization
use chrono::{DateTime, FixedOffset};
use crc::{Algorithm, Crc};
use deku::prelude::*;
use crate::hdlc::{self, hdlc_decapsulate};
use log::warn;
use thiserror::Error;
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
pub const MESSAGE_ESCAPE_CHAR: u8 = 0x7d;
pub const ESCAPED_MESSAGE_TERMINATOR: u8 = 0x5e;
pub const ESCAPED_MESSAGE_ESCAPE_CHAR: u8 = 0x5d;
#[derive(Debug, Clone, DekuWrite)]
pub struct RequestContainer {
pub data_type: DataType,
#[deku(skip)]
pub use_mdm: bool,
#[deku(skip, cond = "!*use_mdm")]
pub mdm_field: i32,
pub hdlc_encapsulated_request: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, DekuWrite)]
#[deku(id_type = "u32")]
pub enum Request {
#[deku(id = "115")]
LogConfig(LogConfigRequest),
}
#[derive(Debug, Clone, PartialEq, DekuWrite)]
#[deku(id_type = "u32", endian = "little")]
pub enum LogConfigRequest {
#[deku(id = "1")]
RetrieveIdRanges,
#[deku(id = "3")]
SetMask {
log_type: u32,
log_mask_bitsize: u32,
log_mask: Vec<u8>,
},
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(id_type = "u32", endian = "little")]
pub enum DataType {
#[deku(id = "32")]
UserSpace,
#[deku(id_pat = "_")]
Other(u32),
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum DiagParsingError {
#[error("Failed to parse Message: {0}, data: {1:?}")]
MessageParsingError(deku::DekuError, Vec<u8>),
#[error("HDLC decapsulation of message failed: {0}, data: {1:?}")]
HdlcDecapsulationError(hdlc::HdlcError, Vec<u8>),
}
// this is sorta based on the params qcsuper uses, plus what seems to be used in
// https://github.com/fgsect/scat/blob/f1538b397721df3ab8ba12acd26716abcf21f78b/util.py#L47
pub const CRC_CCITT_ALG: Algorithm<u16> = Algorithm {
poly: 0x1021,
init: 0xffff,
refin: true,
refout: true,
width: 16,
xorout: 0xffff,
check: 0x2189,
residue: 0x0000,
};
pub const CRC_CCITT: Crc<u16> = Crc::<u16>::new(&CRC_CCITT_ALG);
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
pub struct MessagesContainer {
pub data_type: DataType,
pub num_messages: u32,
#[deku(count = "num_messages")]
pub messages: Vec<HdlcEncapsulatedMessage>,
}
impl MessagesContainer {
pub fn messages(&self) -> Vec<Result<Message, DiagParsingError>> {
let mut result = Vec::new();
for msg in &self.messages {
for sub_msg in msg.data.split_inclusive(|&b| b == MESSAGE_TERMINATOR) {
result.push(Message::from_hdlc(sub_msg));
}
}
result
}
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
pub struct HdlcEncapsulatedMessage {
pub len: u32,
#[deku(count = "len")]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(id_type = "u8")]
pub enum Message {
#[deku(id = "16")]
Log {
pending_msgs: u8,
outer_length: u16,
inner_length: u16,
log_type: u16,
timestamp: Timestamp,
// pass the log type and log length (inner_length - (sizeof(log_type) + sizeof(timestamp)))
#[deku(ctx = "*log_type, inner_length.saturating_sub(12)")]
body: LogBody,
},
// kinda unpleasant deku hackery here. deku expects an enum's variant to be
// right before its data, but in this case, a status value comes between the
// variants and the data. so we need to use deku's context (ctx) feature to
// pass those opcodes down to their respective parsers.
#[deku(id_pat = "_")]
Response {
opcode1: u8, // the "id" (from deku's POV) gets parsed into this field
opcode2: u8,
opcode3: u8,
opcode4: u8,
subopcode: u32,
status: u32,
#[deku(ctx = "u32::from_le_bytes([*opcode1, *opcode2, *opcode3, *opcode4]), *subopcode")]
payload: ResponsePayload,
},
}
impl Message {
pub fn from_hdlc(data: &[u8]) -> Result<Message, DiagParsingError> {
match hdlc_decapsulate(data, &CRC_CCITT) {
Ok(data) => match Message::from_bytes((&data, 0)) {
Ok(((leftover_bytes, _), res)) => {
if !leftover_bytes.is_empty() {
warn!(
"warning: {} leftover bytes when parsing Message",
leftover_bytes.len()
);
}
Ok(res)
}
Err(e) => Err(DiagParsingError::MessageParsingError(e, data)),
},
Err(err) => Err(DiagParsingError::HdlcDecapsulationError(err, data.to_vec())),
}
}
}
pub mod ll1;
pub mod mac;
pub mod ml1;
pub mod rrc;
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "log_type: u16, hdr_len: u16", id = "log_type")]
@@ -186,7 +39,7 @@ pub enum LogBody {
LteRrcOtaMessage {
ext_header_version: u8,
#[deku(ctx = "*ext_header_version")]
packet: LteRrcOtaPacket,
packet: rrc::LteRrcOtaPacket,
},
// the four NAS command opcodes refer to:
// * 0xb0e2: plain ESM NAS message (incoming)
@@ -225,6 +78,25 @@ pub enum LogBody {
#[deku(count = "hdr_len")]
msg: Vec<u8>,
},
#[deku(id = "0xb17f")]
LteMl1ServingCellMeasurementAndEvaluation {
data: ml1::serving_cell::MeasurementAndEvaluation,
},
#[deku(id = "0xb180")]
LteMl1NeighborCellsMeasurements {
data: ml1::neighbor_cells::Measurements,
},
#[deku(id = "0xb062")]
LteMacRachResponse { packet: mac::Packet },
#[deku(id = "0xb063")]
LteMacDl { packet: mac::Packet },
#[deku(id = "0xb064")]
LteMacUl { packet: mac::Packet },
#[deku(id = "0xb114")]
LteLl1ServingCellTiming {
#[deku(ctx = "deku::ctx::Order::Lsb0")]
data: ll1::ServingCellTiming,
}
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
@@ -240,113 +112,6 @@ pub enum Nas4GMessageDirection {
Uplink,
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
pub enum LteRrcOtaPacket {
#[deku(id_pat = "0..=4")]
V0 {
rrc_rel_maj: u8,
rrc_rel_min: u8,
bearer_id: u8,
phy_cell_id: u16,
earfcn: u16,
sfn_subfn: u16,
pdu_num: u8,
len: u16,
#[deku(count = "len")]
packet: Vec<u8>,
},
#[deku(id_pat = "5..=7")]
V5 {
rrc_rel_maj: u8,
rrc_rel_min: u8,
bearer_id: u8,
phy_cell_id: u16,
earfcn: u16,
sfn_subfn: u16,
pdu_num: u8,
sib_mask: u32,
len: u16,
#[deku(count = "len")]
packet: Vec<u8>,
},
#[deku(id_pat = "8..=24")]
V8 {
rrc_rel_maj: u8,
rrc_rel_min: u8,
bearer_id: u8,
phy_cell_id: u16,
earfcn: u32,
sfn_subfn: u16,
pdu_num: u8,
sib_mask: u32,
len: u16,
#[deku(count = "len")]
packet: Vec<u8>,
},
#[deku(id_pat = "25..")]
V25 {
rrc_rel_maj: u8,
rrc_rel_min: u8,
nr_rrc_rel_maj: u8,
nr_rrc_rel_min: u8,
bearer_id: u8,
phy_cell_id: u16,
earfcn: u32,
sfn_subfn: u16,
pdu_num: u8,
sib_mask: u32,
len: u16,
#[deku(count = "len")]
packet: Vec<u8>,
},
}
impl LteRrcOtaPacket {
fn get_sfn_subfn(&self) -> u16 {
match self {
LteRrcOtaPacket::V0 { sfn_subfn, .. } => *sfn_subfn,
LteRrcOtaPacket::V5 { sfn_subfn, .. } => *sfn_subfn,
LteRrcOtaPacket::V8 { sfn_subfn, .. } => *sfn_subfn,
LteRrcOtaPacket::V25 { sfn_subfn, .. } => *sfn_subfn,
}
}
pub fn get_sfn(&self) -> u32 {
self.get_sfn_subfn() as u32 >> 4
}
pub fn get_subfn(&self) -> u8 {
(self.get_sfn_subfn() & 0xf) as u8
}
pub fn get_pdu_num(&self) -> u8 {
match self {
LteRrcOtaPacket::V0 { pdu_num, .. } => *pdu_num,
LteRrcOtaPacket::V5 { pdu_num, .. } => *pdu_num,
LteRrcOtaPacket::V8 { pdu_num, .. } => *pdu_num,
LteRrcOtaPacket::V25 { pdu_num, .. } => *pdu_num,
}
}
pub fn get_earfcn(&self) -> u32 {
match self {
LteRrcOtaPacket::V0 { earfcn, .. } => *earfcn as u32,
LteRrcOtaPacket::V5 { earfcn, .. } => *earfcn as u32,
LteRrcOtaPacket::V8 { earfcn, .. } => *earfcn,
LteRrcOtaPacket::V25 { earfcn, .. } => *earfcn,
}
}
pub fn take_payload(self) -> Vec<u8> {
match self {
LteRrcOtaPacket::V0 { packet, .. } => packet,
LteRrcOtaPacket::V5 { packet, .. } => packet,
LteRrcOtaPacket::V8 { packet, .. } => packet,
LteRrcOtaPacket::V25 { packet, .. } => packet,
}
}
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(endian = "little")]
pub struct Timestamp {
@@ -367,58 +132,92 @@ impl Timestamp {
}
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "opcode: u32, subopcode: u32", id = "opcode")]
pub enum ResponsePayload {
#[deku(id = "115")]
LogConfig(#[deku(ctx = "subopcode")] LogConfigResponse),
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "subopcode: u32", id = "subopcode")]
pub enum LogConfigResponse {
#[deku(id = "1")]
RetrieveIdRanges { log_mask_sizes: [u32; 16] },
#[deku(id = "3")]
SetMask,
}
pub fn build_log_mask_request(
log_type: u32,
log_mask_bitsize: u32,
accepted_log_codes: &[u32],
) -> Request {
let mut current_byte: u8 = 0;
let mut num_bits_written: u8 = 0;
let mut log_mask: Vec<u8> = vec![];
for i in 0..log_mask_bitsize {
let log_code: u32 = (log_type << 12) | i;
if accepted_log_codes.contains(&log_code) {
current_byte |= 1 << num_bits_written;
}
num_bits_written += 1;
if num_bits_written == 8 || i == log_mask_bitsize - 1 {
log_mask.push(current_byte);
current_byte = 0;
num_bits_written = 0;
}
}
Request::LogConfig(LogConfigRequest::SetMask {
log_type,
log_mask_bitsize,
log_mask,
})
}
#[cfg(test)]
pub(crate) mod test {
use super::*;
use crate::{diag::*, hdlc, log_codes};
#[test]
fn test_logs() {
let data = vec![
16, 0, 38, 0, 38, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20, 14, 48, 0, 160, 0,
2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 7, 0, 64, 1, 238, 173, 213, 77, 208,
];
let msg = Message::from_bytes((&data, 0)).unwrap().1;
assert_eq!(
msg,
Message::Log {
pending_msgs: 0,
outer_length: 38,
inner_length: 38,
log_type: 0xb0c0,
timestamp: Timestamp {
ts: 72659535985485082
},
body: LogBody::LteRrcOtaMessage {
ext_header_version: 20,
packet: rrc::LteRrcOtaPacket::V8 {
rrc_rel_maj: 14,
rrc_rel_min: 48,
bearer_id: 0,
phy_cell_id: 160,
earfcn: 2050,
sfn_subfn: 4057,
pdu_num: 5,
sib_mask: 0,
len: 7,
packet: vec![0x40, 0x1, 0xee, 0xad, 0xd5, 0x4d, 0xd0],
},
},
}
);
}
#[test]
fn test_fuzz_crash_inner_length_underflow() {
// Regression test: inner_length < 12 previously caused panic.
// Fixed by using saturating_sub in Message::Log body length calculation.
let fuzz_data = b"\x10\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
let _ = Message::from_bytes((fuzz_data, 0));
}
#[test]
fn test_fuzz_crash_nas_hdr_len_underflow() {
// Regression test for two things:
// - hdr_len < 4 previously caused panic in Nas4GMessage.
// - Upgrading to deku 0.20 caused incorrect parsing behavior (double-read of discriminant)
let nas_msg =
b"\x10\x00\x14\x00\x02\x00\xe2\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00";
let ((rest, _), msg) = Message::from_bytes((nas_msg, 0)).unwrap();
assert_eq!(rest.len(), 0);
assert!(
matches!(
msg,
Message::Log {
log_type: 0xb0e2,
body: LogBody::Nas4GMessage {
direction: Nas4GMessageDirection::Downlink,
..
},
..
}
),
"Unexpected message: {:?}",
msg
);
}
#[test]
fn test_fuzz_crash_ip_traffic_hdr_len_underflow() {
// Regression test: hdr_len < 8 previously caused panic in IpTraffic.
// Fixed by using saturating_sub for msg length calculation.
let ip_msg = b"\x10\x00\x14\x00\x02\x00\xeb\x11\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00";
let _ = Message::from_bytes((ip_msg, 0));
}
// Just about all of these test cases from manually parsing diag packets w/ QCSuper
#[test]
fn test_request_serialization() {
let req = Request::LogConfig(LogConfigRequest::RetrieveIdRanges);
@@ -442,7 +241,17 @@ pub(crate) mod test {
let req = build_log_mask_request(
log_type,
bitsize,
&crate::diag_device::LOG_CODES_FOR_RAW_PACKET_LOGGING,
&[
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C,
log_codes::WCDMA_SIGNALLING_MESSAGE,
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C,
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C,
log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C,
log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C,
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C,
log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C,
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C,
],
);
assert_eq!(
req,
@@ -450,11 +259,9 @@ 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, 0x0, 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,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
0, 0, 12, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
],
})
);
@@ -481,42 +288,6 @@ pub(crate) mod test {
);
}
#[test]
fn test_logs() {
let data = vec![
16, 0, 38, 0, 38, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20, 14, 48, 0, 160, 0,
2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 7, 0, 64, 1, 238, 173, 213, 77, 208,
];
let msg = Message::from_bytes((&data, 0)).unwrap().1;
assert_eq!(
msg,
Message::Log {
pending_msgs: 0,
outer_length: 38,
inner_length: 38,
log_type: 0xb0c0,
timestamp: Timestamp {
ts: 72659535985485082
},
body: LogBody::LteRrcOtaMessage {
ext_header_version: 20,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 14,
rrc_rel_min: 48,
bearer_id: 0,
phy_cell_id: 160,
earfcn: 2050,
sfn_subfn: 4057,
pdu_num: 5,
sib_mask: 0,
len: 7,
packet: vec![0x40, 0x1, 0xee, 0xad, 0xd5, 0x4d, 0xd0],
},
},
}
);
}
fn make_container(data_type: DataType, message: HdlcEncapsulatedMessage) -> MessagesContainer {
MessagesContainer {
data_type,
@@ -540,7 +311,7 @@ pub(crate) mod test {
},
body: LogBody::LteRrcOtaMessage {
ext_header_version: 20,
packet: LteRrcOtaPacket::V8 {
packet: diaglog::rrc::LteRrcOtaPacket::V8 {
rrc_rel_maj: 14,
rrc_rel_min: 48,
bearer_id: 0,
@@ -624,50 +395,6 @@ pub(crate) mod test {
));
}
#[test]
fn test_fuzz_crash_inner_length_underflow() {
// Regression test: inner_length < 12 previously caused panic.
// Fixed by using saturating_sub in Message::Log body length calculation.
let fuzz_data = b"\x10\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
let _ = Message::from_bytes((fuzz_data, 0));
}
#[test]
fn test_fuzz_crash_nas_hdr_len_underflow() {
// Regression test for two things:
// - hdr_len < 4 previously caused panic in Nas4GMessage.
// - Upgrading to deku 0.20 caused incorrect parsing behavior (double-read of discriminant)
let nas_msg =
b"\x10\x00\x14\x00\x02\x00\xe2\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00";
let ((rest, _), msg) = Message::from_bytes((nas_msg, 0)).unwrap();
assert_eq!(rest.len(), 0);
assert!(
matches!(
msg,
Message::Log {
log_type: 0xb0e2,
body: LogBody::Nas4GMessage {
direction: Nas4GMessageDirection::Downlink,
..
},
..
}
),
"Unexpected message: {:?}",
msg
);
}
#[test]
fn test_fuzz_crash_ip_traffic_hdr_len_underflow() {
// Regression test: hdr_len < 8 previously caused panic in IpTraffic.
// Fixed by using saturating_sub for msg length calculation.
let ip_msg = b"\x10\x00\x14\x00\x02\x00\xeb\x11\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00";
let _ = Message::from_bytes((ip_msg, 0));
}
#[test]
fn test_fuzz_crash_response_opcode_parsing() {
// Regression test: Upgrading to deku 0.20 caused incorrect parsing of Response messages.
+110
View File
@@ -0,0 +1,110 @@
//! Diag LTE RRC serialization/deserialization
use deku::prelude::*;
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
pub enum LteRrcOtaPacket {
#[deku(id_pat = "0..=4")]
V0 {
rrc_rel_maj: u8,
rrc_rel_min: u8,
bearer_id: u8,
phy_cell_id: u16,
earfcn: u16,
sfn_subfn: u16,
pdu_num: u8,
len: u16,
#[deku(count = "len")]
packet: Vec<u8>,
},
#[deku(id_pat = "5..=7")]
V5 {
rrc_rel_maj: u8,
rrc_rel_min: u8,
bearer_id: u8,
phy_cell_id: u16,
earfcn: u16,
sfn_subfn: u16,
pdu_num: u8,
sib_mask: u32,
len: u16,
#[deku(count = "len")]
packet: Vec<u8>,
},
#[deku(id_pat = "8..=24")]
V8 {
rrc_rel_maj: u8,
rrc_rel_min: u8,
bearer_id: u8,
phy_cell_id: u16,
earfcn: u32,
sfn_subfn: u16,
pdu_num: u8,
sib_mask: u32,
len: u16,
#[deku(count = "len")]
packet: Vec<u8>,
},
#[deku(id_pat = "25..")]
V25 {
rrc_rel_maj: u8,
rrc_rel_min: u8,
nr_rrc_rel_maj: u8,
nr_rrc_rel_min: u8,
bearer_id: u8,
phy_cell_id: u16,
earfcn: u32,
sfn_subfn: u16,
pdu_num: u8,
sib_mask: u32,
len: u16,
#[deku(count = "len")]
packet: Vec<u8>,
},
}
impl LteRrcOtaPacket {
fn get_sfn_subfn(&self) -> u16 {
match self {
LteRrcOtaPacket::V0 { sfn_subfn, .. } => *sfn_subfn,
LteRrcOtaPacket::V5 { sfn_subfn, .. } => *sfn_subfn,
LteRrcOtaPacket::V8 { sfn_subfn, .. } => *sfn_subfn,
LteRrcOtaPacket::V25 { sfn_subfn, .. } => *sfn_subfn,
}
}
pub fn get_sfn(&self) -> u32 {
self.get_sfn_subfn() as u32 >> 4
}
pub fn get_subfn(&self) -> u8 {
(self.get_sfn_subfn() & 0xf) as u8
}
pub fn get_pdu_num(&self) -> u8 {
match self {
LteRrcOtaPacket::V0 { pdu_num, .. } => *pdu_num,
LteRrcOtaPacket::V5 { pdu_num, .. } => *pdu_num,
LteRrcOtaPacket::V8 { pdu_num, .. } => *pdu_num,
LteRrcOtaPacket::V25 { pdu_num, .. } => *pdu_num,
}
}
pub fn get_earfcn(&self) -> u32 {
match self {
LteRrcOtaPacket::V0 { earfcn, .. } => *earfcn as u32,
LteRrcOtaPacket::V5 { earfcn, .. } => *earfcn as u32,
LteRrcOtaPacket::V8 { earfcn, .. } => *earfcn,
LteRrcOtaPacket::V25 { earfcn, .. } => *earfcn,
}
}
pub fn take_payload(self) -> Vec<u8> {
match self {
LteRrcOtaPacket::V0 { packet, .. } => packet,
LteRrcOtaPacket::V5 { packet, .. } => packet,
LteRrcOtaPacket::V8 { packet, .. } => packet,
LteRrcOtaPacket::V25 { packet, .. } => packet,
}
}
}
+220
View File
@@ -0,0 +1,220 @@
//! Diag protocol serialization/deserialization
use crc::{Algorithm, Crc};
use deku::prelude::*;
use crate::hdlc::{self, hdlc_decapsulate};
use log::warn;
use thiserror::Error;
pub mod diaglog;
use diaglog::{LogBody, Timestamp};
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
pub const MESSAGE_ESCAPE_CHAR: u8 = 0x7d;
pub const ESCAPED_MESSAGE_TERMINATOR: u8 = 0x5e;
pub const ESCAPED_MESSAGE_ESCAPE_CHAR: u8 = 0x5d;
#[derive(Debug, Clone, DekuWrite)]
pub struct RequestContainer {
pub data_type: DataType,
#[deku(skip)]
pub use_mdm: bool,
#[deku(skip, cond = "!*use_mdm")]
pub mdm_field: i32,
pub hdlc_encapsulated_request: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, DekuWrite)]
#[deku(id_type = "u32")]
pub enum Request {
#[deku(id = "115")]
LogConfig(LogConfigRequest),
}
#[derive(Debug, Clone, PartialEq, DekuWrite)]
#[deku(id_type = "u32", endian = "little")]
pub enum LogConfigRequest {
#[deku(id = "1")]
RetrieveIdRanges,
#[deku(id = "3")]
SetMask {
log_type: u32,
log_mask_bitsize: u32,
log_mask: Vec<u8>,
},
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(id_type = "u32", endian = "little")]
pub enum DataType {
#[deku(id = "32")]
UserSpace,
#[deku(id_pat = "_")]
Other(u32),
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum DiagParsingError {
#[error("Failed to parse Message: {0}, data: {1:?}")]
MessageParsingError(deku::DekuError, Vec<u8>),
#[error("HDLC decapsulation of message failed: {0}, data: {1:?}")]
HdlcDecapsulationError(hdlc::HdlcError, Vec<u8>),
}
// this is sorta based on the params qcsuper uses, plus what seems to be used in
// https://github.com/fgsect/scat/blob/f1538b397721df3ab8ba12acd26716abcf21f78b/util.py#L47
pub const CRC_CCITT_ALG: Algorithm<u16> = Algorithm {
poly: 0x1021,
init: 0xffff,
refin: true,
refout: true,
width: 16,
xorout: 0xffff,
check: 0x2189,
residue: 0x0000,
};
pub const CRC_CCITT: Crc<u16> = Crc::<u16>::new(&CRC_CCITT_ALG);
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
pub struct MessagesContainer {
pub data_type: DataType,
pub num_messages: u32,
#[deku(count = "num_messages")]
pub messages: Vec<HdlcEncapsulatedMessage>,
}
impl MessagesContainer {
pub fn messages(&self) -> Vec<Result<Message, DiagParsingError>> {
let mut result = Vec::new();
for msg in &self.messages {
for sub_msg in msg.data.split_inclusive(|&b| b == MESSAGE_TERMINATOR) {
result.push(Message::from_hdlc(sub_msg));
}
}
result
}
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
pub struct HdlcEncapsulatedMessage {
pub len: u32,
#[deku(count = "len")]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(id_type = "u8")]
pub enum Message {
#[deku(id = "16")]
Log {
pending_msgs: u8,
outer_length: u16,
inner_length: u16,
log_type: u16,
timestamp: Timestamp,
// pass the log type and log length (inner_length - (sizeof(log_type) + sizeof(timestamp)))
#[deku(ctx = "*log_type, inner_length.saturating_sub(12)")]
body: LogBody,
},
// kinda unpleasant deku hackery here. deku expects an enum's variant to be
// right before its data, but in this case, a status value comes between the
// variants and the data. so we need to use deku's context (ctx) feature to
// pass those opcodes down to their respective parsers.
#[deku(id_pat = "_")]
Response {
opcode1: u8, // the "id" (from deku's POV) gets parsed into this field
opcode2: u8,
opcode3: u8,
opcode4: u8,
subopcode: u32,
status: u32,
#[deku(ctx = "u32::from_le_bytes([*opcode1, *opcode2, *opcode3, *opcode4]), *subopcode")]
payload: ResponsePayload,
},
}
impl Message {
pub fn from_hdlc(data: &[u8]) -> Result<Message, DiagParsingError> {
match hdlc_decapsulate(data, &CRC_CCITT) {
Ok(data) => match Message::from_bytes((&data, 0)) {
Ok(((leftover_bytes, _), res)) => {
if !leftover_bytes.is_empty() {
warn!(
"warning: {} leftover bytes when parsing Message",
leftover_bytes.len()
);
}
Ok(res)
}
Err(e) => Err(DiagParsingError::MessageParsingError(e, data)),
},
Err(err) => Err(DiagParsingError::HdlcDecapsulationError(err, data.to_vec())),
}
}
/// Returns whether this message should be parsed into a GSMTAP packet for
/// display in pcap files
pub fn is_gsmtap_message(&self) -> bool {
let Message::Log { body, .. } = self else {
return false;
};
match body {
LogBody::LteRrcOtaMessage { .. } => true,
LogBody::LteMacRachResponse { .. } => true,
LogBody::LteMl1NeighborCellsMeasurements { .. } => true,
LogBody::Nas4GMessage { .. } => true,
_ => false
}
}
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "opcode: u32, subopcode: u32", id = "opcode")]
pub enum ResponsePayload {
#[deku(id = "115")]
LogConfig(#[deku(ctx = "subopcode")] LogConfigResponse),
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "subopcode: u32", id = "subopcode")]
pub enum LogConfigResponse {
#[deku(id = "1")]
RetrieveIdRanges { log_mask_sizes: [u32; 16] },
#[deku(id = "3")]
SetMask,
}
pub fn build_log_mask_request(
log_type: u32,
log_mask_bitsize: u32,
accepted_log_codes: &[u32],
) -> Request {
let mut current_byte: u8 = 0;
let mut num_bits_written: u8 = 0;
let mut log_mask: Vec<u8> = vec![];
for i in 0..log_mask_bitsize {
let log_code: u32 = (log_type << 12) | i;
if accepted_log_codes.contains(&log_code) {
current_byte |= 1 << num_bits_written;
}
num_bits_written += 1;
if num_bits_written == 8 || i == log_mask_bitsize - 1 {
log_mask.push(current_byte);
current_byte = 0;
num_bits_written = 0;
}
}
Request::LogConfig(LogConfigRequest::SetMask {
log_type,
log_mask_bitsize,
log_mask,
})
}
+20 -12
View File
@@ -40,22 +40,30 @@ pub enum DiagDeviceError {
ParseMessagesContainerError(deku::DekuError),
}
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 17] = [
// Layer 2:
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C,
// Layer 3:
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C, // 0x512f
log_codes::WCDMA_SIGNALLING_MESSAGE, // 0x412f
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C, // 0xb0c0
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C, // 0xb821
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C,
log_codes::WCDMA_SIGNALLING_MESSAGE,
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C,
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C,
// NAS:
log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C, // 0x713a
log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C, // 0xb0e2
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C, // 0xb0e3
log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C, // 0xb0ec
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed
log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C,
log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C,
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C,
log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C,
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C,
// User IP traffic:
log_codes::LOG_DATA_PROTOCOL_LOGGING_C, // 0x11eb
log_codes::LOG_DATA_PROTOCOL_LOGGING_C,
// LTE physical layer serving cell measurements: RSRP, RSRQ, RSSI
log_codes::LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C,
log_codes::LOG_LTE_ML1_NEIGHBOR_MEAS,
// LTE MAC Random Access Channel response: contains Timing Advance
log_codes::LOG_LTE_MAC_RACH_RESPONSE_C,
log_codes::LOG_LTE_MAC_DL,
log_codes::LOG_LTE_MAC_UL,
0xb114, // maybe timing advance related?
];
const BUFFER_LEN: usize = 1024 * 1024 * 10;
+187
View File
@@ -0,0 +1,187 @@
//! The structs/enum values defined here are derived from a number of sources:
//! * SCAT's construction of MAC GSMTAP packets: https://github.com/fgsect/scat/blob/9763cb5b1dcd5ee980f5b0ead9a8d520c8c51a51/src/scat/parsers/qualcomm/diagltelogparser.py#L562-L640
//! * https://www.sharetechnote.com/html/MAC_LTE.html#MAC_PDU_Structure_RAR
//! * 3GPP's TS 36.321, mostly sections 6.1.4, 6.1.5, and 6.1.6
use deku::prelude::*;
use crate::{
diag::diaglog::mac::SubpacketBody,
gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType},
};
use deku::{DekuContainerWrite, DekuError};
#[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")]
Ri,
#[deku(id = "5")]
Sps,
#[deku(id = "6")]
M,
#[deku(id = "7")]
Sl,
#[deku(id = "9")]
Sc,
#[deku(id = "10")]
G,
}
#[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 and just compare the payloads
let expected_payload = &data.into_inner().into_inner()[34..];
assert_eq!(&msg.payload, expected_payload);
}
(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",
),
);
}
}
@@ -3,6 +3,9 @@
use deku::prelude::*;
use num_enum::TryFromPrimitive;
mod mac;
pub mod parser;
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum GsmtapType {
Um(UmSubtype),
@@ -1,7 +1,9 @@
use crate::diag::*;
use crate::gsmtap::*;
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::error;
use log::{debug, warn};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -10,9 +12,14 @@ 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> {
if !msg.is_gsmtap_message() {
return Ok(None);
}
if let Message::Log {
timestamp, body, ..
} = msg
@@ -27,6 +34,8 @@ pub fn parse(msg: Message) -> Result<Option<(Timestamp, GsmtapMessage)>, GsmtapP
}
fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserError> {
// Note: if support for another LogBody variant is added here, it should
// also be added to Message::is_gsmtap_message
match value {
LogBody::LteRrcOtaMessage {
ext_header_version,
@@ -152,8 +161,26 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
payload: msg,
}))
}
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.first() else {
return Err(GsmtapParserError::InvalidLteMacRachResponse(
"no subpackets".to_string(),
));
};
mac_subpacket_to_gsmtap(&subpacket.body).map_err(|err| {
GsmtapParserError::InvalidLteMacRachResponse(format!(
"unable to serialize GSMTAP payload: {err:?}"
))
})
}
_ => {
error!("gsmtap_sink: ignoring unhandled log type: {value:?}");
debug!("gsmtap_sink: ignoring unhandled log type: {value:?}");
Ok(None)
}
}
@@ -162,6 +189,7 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
#[cfg(test)]
mod tests {
use super::*;
use crate::gsmtap::GsmtapType;
use deku::DekuContainerWrite;
#[test]
+2 -1
View File
@@ -15,11 +15,12 @@ pub mod analysis;
pub mod clock;
pub mod diag;
pub mod gsmtap;
pub mod gsmtap_parser;
pub mod hdlc;
pub mod log_codes;
pub mod pcap;
pub mod qmdl;
#[cfg(test)]
mod test_util;
pub mod util;
// bin/check.rs may target windows and does not use this mod
+10
View File
@@ -31,6 +31,16 @@ pub const LOG_NR_RRC_OTA_MSG_LOG_C: u32 = 0xb821;
// These are 4G-related log types.
pub const LOG_LTE_RRC_OTA_MSG_LOG_C: u32 = 0xb0c0;
// Qualcomm ML1 (physical layer) serving cell measurement report: RSRP, RSRQ, RSSI
pub const LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C: u32 = 0xb17f;
pub const LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE: u32 = 0xb193;
pub const LOG_LTE_ML1_NEIGHBOR_MEAS: u32 = 0xb180;
pub const LOG_LTE_ML1_MAC_RAR_MSG1_REPORT: u32 = 0xb167;
pub const LOG_LTE_ML1_MAC_RAR_MSG2_REPORT: u32 = 0xb168;
// 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;
+1 -1
View File
@@ -1,6 +1,6 @@
//! Parse QMDL files and create a pcap file.
//! Creates a plausible IP header and [GSMtap](https://osmocom.org/projects/baseband/wiki/GSMTAP) header and then puts the rest of the data under that for wireshark to parse.
use crate::diag::Timestamp;
use crate::diag::diaglog::Timestamp;
use crate::gsmtap::GsmtapMessage;
use chrono::prelude::*;
+17 -1
View File
@@ -199,6 +199,22 @@ where
Ok(Some(Message::from_hdlc(&buf)))
}
pub async fn get_next_buf(
&mut self,
) -> Result<Option<Vec<u8>>, std::io::Error> {
let mut buf = vec![];
if self
.buf_reader
.read_until(MESSAGE_TERMINATOR, &mut buf)
.await?
== 0
{
return Ok(None);
}
Ok(Some(buf))
}
}
impl<T> AsyncRead for QmdlMessageReader<T>
@@ -218,7 +234,7 @@ where
mod test {
use std::io::Cursor;
use crate::diag::{DataType, HdlcEncapsulatedMessage, test::get_test_message};
use crate::diag::{DataType, HdlcEncapsulatedMessage, diaglog::test::get_test_message};
use super::*;
+11
View File
@@ -0,0 +1,11 @@
use deku::reader::Reader;
use std::io::Cursor;
pub fn unhexlify(hexlified_bytes: &str) -> (usize, Reader<Cursor<Vec<u8>>>) {
let byte_len = hexlified_bytes.len() / 2;
let bytes = (0..hexlified_bytes.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hexlified_bytes[i..i + 2], 16).unwrap())
.collect();
(byte_len, Reader::new(Cursor::new(bytes)))
}
+3 -2
View File
@@ -1,7 +1,8 @@
use deku::prelude::*;
use rayhunter::{
diag::{LogBody, LteRrcOtaPacket, Message, Timestamp},
gsmtap_parser,
diag::Message,
diag::diaglog::{LogBody, Timestamp, rrc::LteRrcOtaPacket},
gsmtap::parser as gsmtap_parser,
};
// Tests here are based on https://github.com/fgsect/scat/blob/97442580e628de414c9f7c2a185f4e28d0ee7523/tests/test_diagltelogparser.py