Compare commits

...

32 Commits

Author SHA1 Message Date
Will Greenberg 19d52ca788 appease clippy 2026-07-08 17:47:27 -07:00
Will Greenberg 5bc452f4cd run cargo fmt 2026-07-08 17:37:12 -07:00
Will Greenberg ed6d2dc078 Add LL1 Serving Cell Timing messages
This log should provide constant feedback about the serving cell's LTE
timing advance value.
2026-07-08 17:23:28 -07:00
Will Greenberg e3535af670 run cargo fmt 2026-07-08 17:23:28 -07:00
Will Greenberg 2b70c83220 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-07-08 17:23:28 -07:00
Will Greenberg 44b4fb67db 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-07-08 17:23:28 -07:00
Will Greenberg f1fddf0c93 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-07-08 17:23:28 -07:00
Will Greenberg 650a30782c rm unimplemented diag message format 2026-07-08 17:23:28 -07:00
Will Greenberg b21cfed0e3 run cargo fmt 2026-07-08 17:23:27 -07:00
Will Greenberg 5227939b1e appease clippy 2026-07-08 17:23:27 -07:00
Will Greenberg 99ba6147a2 add more comments 2026-07-08 17:23:27 -07:00
Will Greenberg e7c578b229 cleanups from rebase 2026-07-08 17:23:27 -07:00
Will Greenberg cd7322e4e8 refactors/tweaks 2026-07-08 17:23:27 -07:00
Will Greenberg c51ce797f0 run cargo fmt 2026-07-08 17:23:27 -07:00
Will Greenberg aa37f3530b 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-07-08 17:23:27 -07:00
Will Greenberg 8f8b035b24 run cargo fmt 2026-07-08 17:23:27 -07:00
Will Greenberg 7e4a7a2589 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-07-08 17:23:27 -07:00
Will Greenberg d80654a84a Update diag log mask for new messages 2026-07-08 17:23:27 -07:00
Will Greenberg ea93088999 trim test case
These measurement packets are of a fixed length, and the SCAT test case
had excess data
2026-07-08 17:23:27 -07:00
Will Greenberg 3cac12f990 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-07-08 17:23:27 -07:00
Will Greenberg 8691ef38df lib/diag: add ML1 Neighbor cell measurement
This adds support for Neighboring Cells Measurements, and makes some
minor changes to Serving Cell Measurements.
2026-07-08 17:23:27 -07:00
Carlos Guerra b203300558 format fixes for linters to be happy 2026-07-08 17:23:27 -07:00
Carlos Guerra 250e14afc0 addressing review comments: minor refactor for optimization, and correction of rrc_rel size 2026-07-08 17:23:27 -07:00
Carlos Guerra f7b0ef665a linting and polishing for PR 2026-07-08 17:23:27 -07:00
Carlos Guerra 1ab99acd76 Collect signal strength and timing advances. LTE serving cell measurements (0xb17f) and RACH Timing Advance (0xb062) 2026-07-08 17:23:27 -07:00
Brad Warren a3cf7ac1ee add note about powershell vs cmd 2026-07-08 11:18:21 -07:00
sawft99 e8d4a489de Update faq.md 2026-07-08 11:18:21 -07:00
David Brungardt 97e7e7c3d0 Change Wording Slightly 2026-07-06 11:13:55 -07:00
David Brungardt 39c183d67f Update FAQ w/ Question About SIM Cards Outside US 2026-07-06 11:13:55 -07:00
Will Greenberg cd28354bec run cargo fmt 2026-06-25 15:45:59 -07:00
Will Greenberg 1e4b812273 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-25 15:45:59 -07:00
Will Greenberg 04cf2cbd67 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-25 15:45:59 -07:00
21 changed files with 1614 additions and 432 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ use log::{debug, error, info, warn};
use pcap_file_tokio::pcapng::{Block, PcapNgReader};
use rayhunter::{
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness},
gsmtap_parser,
gsmtap::parser as gsmtap_parser,
pcap::GsmtapPcapWriter,
qmdl::QmdlMessageReader,
};
+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;
+6 -4
View File
@@ -4,20 +4,24 @@
**It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but that sim card does not have to be actively registered with a service plan. If you want to use the device as a hotspot in addition to a research device, or get [notifications](./configuration.md), an active plan would of course be necessary.
### If I am located outside of the United States, should I use the SIM card that came with my device?
We recommend using a SIM card from a carrier from whichever country you are located in.
### How can I test that my device is working?
You can enable the `Test Heuristic` under `Analyzer Heuristic Settings` in the config section on your web dashboard. This will cause an alert to trigger every time your device sees a cell tower, you might need to reboot your device or move around a bit to get this one to trigger, but it will be very noisy once it does. People have also tested it by building IMSI catchers at home, but we don't recommend that, since it violates FCC regulations and will probably upset your neighbors.
<a name="red"></a>
### Help, Rayhunter's line is red/orange/yellow/dotted/dashed! What should I do?
Unfortunately, the circumstances that might lead to a positive cell site simulator (CSS) signal are quite varied, so we don't have a universal recommendation for how to deal with the a positive signal. Depending on your circumstances and threat model, you may want to turn off your phone until you are out of the area and tell your friends to do the same!
Unfortunately, the circumstances that might lead to a positive cell site simulator (CSS) signal are quite varied, so we don't have a universal recommendation for how to deal with a positive signal. Depending on your circumstances and threat model, you may want to turn off your phone until you are out of the area and tell your friends to do the same!
If you've received a Rayhunter warning and would like to help us with our research, please send your Rayhunter data captures (Zip file downloaded from the web interface) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version. If you're unfamiliar with Signal, feel free to check out our [Security Self Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
Please note that this file may contain sensitive information such as your IMSI and the unique IDs of cell towers you were near which could be used to ascertain your location at the time.
### Should I get a locked or unlocked orbic device? What is the difference?
If you want to use a non-Verizon SIM card you will probably need an unlocked device. But it's not clear which devices are locked nor how to unlock them, we welcome any experimentation and information regarding the use of unlocked devices. So far most verizon branded orbic devices we have encountered are actually unlocked.
@@ -36,7 +40,6 @@ relevant as it does not use or touch USB.
[First obtain a shell](./orbic.md#shell), then:
```sh
# inside of Orbic's shell:
echo 9 > /usrdata/mode.cfg
@@ -55,7 +58,6 @@ reboot
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
### How do I connect my device to an existing WiFi network?
The Orbic, Moxee, UZ801, and TMOHS1 can connect to a nearby WiFi network while still running their own hotspot. This gives the device internet access for ntfy notifications and lets you reach the web UI from your home network. See [WiFi Client Mode](./configuration.md#wifi-client-mode) in the configuration guide for setup instructions.
+5 -2
View File
@@ -42,8 +42,11 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
# For Orbic:
./installer orbic --admin-password 'mypassword'
# Note: the arguments --admin-username 'myusername' and --admin-ip 'mydeviceip'
# may be required if different from the default.
# Important notes:
# - the arguments --admin-username 'myusername' and --admin-ip 'mydeviceip'
# may be required if different from the default.
# - on Windows this command should be run in PowerShell and not the Command
# Prompt (CMD) where characters like quotes are treated differently.
# For TP-Link:
./installer tplink
+5 -9
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};
use super::{
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
@@ -431,17 +430,14 @@ impl Harness {
return row;
}
};
let gsmtap_message = match gsmtap_parser::parse(qmdl_message) {
Ok(msg) => msg,
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) {
+44
View File
@@ -0,0 +1,44 @@
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;
};
matches!(
body,
LogBody::LteRrcOtaMessage { .. }
| LogBody::LteMacRachResponse { .. }
| LogBody::LteMl1NeighborCellsMeasurements { .. }
| LogBody::Nas4GMessage { .. }
)
}
}
#[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::*;
+1 -1
View File
@@ -218,7 +218,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