mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-07-08 09:38:10 -07:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c39235d5e4 | |||
| 21e12d16e1 | |||
| 892c833344 | |||
| 3a7f512842 | |||
| 8d8099f66b | |||
| 7831c5085e | |||
| 956b719e12 | |||
| 73bcb05a0b | |||
| 6c8b6ec64f | |||
| 63ea5df6f7 | |||
| 2d63fc00c7 | |||
| 2927f791e4 | |||
| ec5b306311 | |||
| a5c86bc408 | |||
| 338d41dceb | |||
| 759b2ea4c5 | |||
| a51cafbb14 | |||
| ddc39cf516 | |||
| 66d83ea575 | |||
| 0028847b73 | |||
| 6fec428c5b | |||
| 11b6af96c7 | |||
| 76c2039556 | |||
| c424902560 | |||
| 8f03ad8f4a | |||
| 68ba2ab625 | |||
| 38f476b664 | |||
| cae056d959 |
+12
-7
@@ -2,10 +2,7 @@ use clap::Parser;
|
||||
use log::{debug, error, info, warn};
|
||||
use pcap_file_tokio::pcapng::{Block, PcapNgReader};
|
||||
use rayhunter::{
|
||||
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness},
|
||||
gsmtap_parser,
|
||||
pcap::GsmtapPcapWriter,
|
||||
qmdl::QmdlMessageReader,
|
||||
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness}, diag::Message, gsmtap::parser as gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlMessageReader,
|
||||
};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use tokio::fs::File;
|
||||
@@ -115,12 +112,20 @@ async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool) {
|
||||
.await
|
||||
.expect("failed to open QmdlReader");
|
||||
let mut report = Report::new(qmdl_path);
|
||||
while let Some(maybe_message) = qmdl_reader
|
||||
.get_next_message()
|
||||
let mut stats = HashMap::new();
|
||||
while let Some(buf) = qmdl_reader
|
||||
.get_next_buf()
|
||||
.await
|
||||
.expect("failed to get message")
|
||||
{
|
||||
report.process_row(harness.analyze_qmdl_message(maybe_message));
|
||||
let log_type = u16::from_le_bytes([buf[6], buf[7]]);
|
||||
stats.entry(log_type)
|
||||
.and_modify(|total| *total += buf.len())
|
||||
.or_insert(buf.len());
|
||||
report.process_row(harness.analyze_qmdl_message(Message::from_hdlc(&buf)));
|
||||
}
|
||||
for (id, size) in stats {
|
||||
println!("{id:04x}: {size}");
|
||||
}
|
||||
report.print_summary(show_skipped);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -5,10 +5,10 @@ use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::analysis::diagnostic::DiagnosticAnalyzer;
|
||||
use crate::diag::{DiagParsingError, Message};
|
||||
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType};
|
||||
use crate::diag::diaglog::LogBody;
|
||||
use crate::diag::{DiagParsingError, Message, MessagesContainer};
|
||||
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType, parser as gsmtap_parser};
|
||||
use crate::util::RuntimeMetadata;
|
||||
use crate::{diag::MessagesContainer, gsmtap_parser};
|
||||
|
||||
use super::{
|
||||
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
|
||||
@@ -431,17 +431,27 @@ impl Harness {
|
||||
return row;
|
||||
}
|
||||
};
|
||||
let gsmtap_message = match gsmtap_parser::parse(qmdl_message) {
|
||||
Ok(msg) => msg,
|
||||
if let Message::Log { body, .. } = &qmdl_message {
|
||||
match body {
|
||||
LogBody::LteLl1ServingCellTiming { data } => {
|
||||
if data.starting_ul_timing_advance != 0 {
|
||||
println!("ta {}", data.starting_ul_timing_advance);
|
||||
}
|
||||
}
|
||||
LogBody::LteMl1ServingCellMeasurementAndEvaluation { data } => {
|
||||
// println!("serving cell pci {}, earfcn {}", data.get_pci(), data.get_earfcn());
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
let (timestamp, gsmtap_msg) = match gsmtap_parser::parse(qmdl_message) {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => return row,
|
||||
Err(err) => {
|
||||
row.skipped_message_reason = Some(format!("{err:?}"));
|
||||
return row;
|
||||
}
|
||||
};
|
||||
|
||||
let Some((timestamp, gsmtap_msg)) = gsmtap_message else {
|
||||
return row;
|
||||
};
|
||||
row.packet_timestamp = Some(timestamp.to_datetime());
|
||||
|
||||
let element = match InformationElement::try_from(&gsmtap_msg) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use deku::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(bit_order = "lsb", ctx = "_: deku::ctx::Order")]
|
||||
pub struct ServingCellTiming {
|
||||
#[deku(assert_eq = "1")]
|
||||
pub version: u8,
|
||||
#[deku(bits = 5, assert = "*num_records <= 20")]
|
||||
pub num_records: u8,
|
||||
#[deku(bits = 4, assert = "*starting_sub_fn <= 9")]
|
||||
pub starting_sub_fn: u8,
|
||||
#[deku(bits = 10, pad_bits_after = "5", assert = "*starting_system_fn <= 1023")]
|
||||
pub starting_system_fn: u16,
|
||||
#[deku(bits = 19, pad_bits_after = "13", assert = "*starting_dl_frame_timing_offs <= 307200")]
|
||||
pub starting_dl_frame_timing_offs: u32, // in Ts units
|
||||
#[deku(bits = 19, assert = "*starting_ul_frame_timing_offs <= 307200")]
|
||||
pub starting_ul_frame_timing_offs: u32, // in Ts units
|
||||
#[deku(bits = 11, pad_bits_after = "2")]
|
||||
pub starting_ul_timing_advance: u16, // in 16 Ts units
|
||||
#[deku(count = "*num_records")]
|
||||
pub timing_adjustment: Vec<TimingAdjustment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(bit_order = "lsb", ctx = "_: deku::ctx::Order")]
|
||||
pub struct TimingAdjustment {
|
||||
#[deku(bits = 11, assert = "(-512..=511).contains(dl_frame_timing_adjustment)")]
|
||||
pub dl_frame_timing_adjustment: i16, // in Ts units
|
||||
#[deku(bits = 5, assert = "(-16..=15).contains(ul_frame_timing_adjustment)")]
|
||||
pub ul_frame_timing_adjustment: i8, // in Ts units
|
||||
#[deku(bits = 8, assert = "(-128..=127).contains(timing_advance)")]
|
||||
pub timing_advance: i8, // in 16 Ts units
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
match body {
|
||||
LogBody::LteRrcOtaMessage { .. } => true,
|
||||
LogBody::LteMacRachResponse { .. } => true,
|
||||
LogBody::LteMl1NeighborCellsMeasurements { .. } => true,
|
||||
LogBody::Nas4GMessage { .. } => true,
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "opcode: u32, subopcode: u32", id = "opcode")]
|
||||
pub enum ResponsePayload {
|
||||
#[deku(id = "115")]
|
||||
LogConfig(#[deku(ctx = "subopcode")] LogConfigResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "subopcode: u32", id = "subopcode")]
|
||||
pub enum LogConfigResponse {
|
||||
#[deku(id = "1")]
|
||||
RetrieveIdRanges { log_mask_sizes: [u32; 16] },
|
||||
|
||||
#[deku(id = "3")]
|
||||
SetMask,
|
||||
}
|
||||
|
||||
pub fn build_log_mask_request(
|
||||
log_type: u32,
|
||||
log_mask_bitsize: u32,
|
||||
accepted_log_codes: &[u32],
|
||||
) -> Request {
|
||||
let mut current_byte: u8 = 0;
|
||||
let mut num_bits_written: u8 = 0;
|
||||
let mut log_mask: Vec<u8> = vec![];
|
||||
for i in 0..log_mask_bitsize {
|
||||
let log_code: u32 = (log_type << 12) | i;
|
||||
if accepted_log_codes.contains(&log_code) {
|
||||
current_byte |= 1 << num_bits_written;
|
||||
}
|
||||
num_bits_written += 1;
|
||||
|
||||
if num_bits_written == 8 || i == log_mask_bitsize - 1 {
|
||||
log_mask.push(current_byte);
|
||||
current_byte = 0;
|
||||
num_bits_written = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Request::LogConfig(LogConfigRequest::SetMask {
|
||||
log_type,
|
||||
log_mask_bitsize,
|
||||
log_mask,
|
||||
})
|
||||
}
|
||||
+20
-12
@@ -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::*;
|
||||
|
||||
+17
-1
@@ -199,6 +199,22 @@ where
|
||||
|
||||
Ok(Some(Message::from_hdlc(&buf)))
|
||||
}
|
||||
|
||||
pub async fn get_next_buf(
|
||||
&mut self,
|
||||
) -> Result<Option<Vec<u8>>, std::io::Error> {
|
||||
let mut buf = vec![];
|
||||
if self
|
||||
.buf_reader
|
||||
.read_until(MESSAGE_TERMINATOR, &mut buf)
|
||||
.await?
|
||||
== 0
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(buf))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsyncRead for QmdlMessageReader<T>
|
||||
@@ -218,7 +234,7 @@ where
|
||||
mod test {
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::diag::{DataType, HdlcEncapsulatedMessage, test::get_test_message};
|
||||
use crate::diag::{DataType, HdlcEncapsulatedMessage, diaglog::test::get_test_message};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -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