mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-07-12 03:28:09 -07:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19d52ca788 | |||
| 5bc452f4cd | |||
| ed6d2dc078 | |||
| e3535af670 | |||
| 2b70c83220 | |||
| 44b4fb67db | |||
| f1fddf0c93 | |||
| 650a30782c | |||
| b21cfed0e3 | |||
| 5227939b1e | |||
| 99ba6147a2 | |||
| e7c578b229 | |||
| cd7322e4e8 | |||
| c51ce797f0 | |||
| aa37f3530b | |||
| 8f8b035b24 | |||
| 7e4a7a2589 | |||
| d80654a84a | |||
| ea93088999 | |||
| 3cac12f990 | |||
| 8691ef38df | |||
| b203300558 | |||
| 250e14afc0 | |||
| f7b0ef665a | |||
| 1ab99acd76 | |||
| a3cf7ac1ee | |||
| e8d4a489de | |||
| 97e7e7c3d0 | |||
| 39c183d67f | |||
| cd28354bec | |||
| 1e4b812273 | |||
| 04cf2cbd67 |
+1
-1
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user