diff --git a/Cargo.lock b/Cargo.lock index 3cf40df..8dcd347 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1927,6 +1927,28 @@ dependencies = [ "libm", ] +[[package]] +name = "num_enum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -2356,6 +2378,7 @@ dependencies = [ "libc", "log", "nix", + "num_enum", "pcap-file-tokio", "pycrate-rs", "serde", @@ -2371,6 +2394,7 @@ dependencies = [ "clap", "futures", "log", + "pcap-file-tokio", "rayhunter", "simple_logger", "tokio", diff --git a/check/Cargo.toml b/check/Cargo.toml index 095261c..2f57f45 100644 --- a/check/Cargo.toml +++ b/check/Cargo.toml @@ -8,6 +8,7 @@ rayhunter = { path = "../lib" } futures = { version = "0.3.30", default-features = false } log = "0.4.20" tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] } +pcap-file-tokio = "0.1.0" clap = { version = "4.5.2", features = ["derive"] } simple_logger = "5.0.0" walkdir = "2.5.0" diff --git a/check/src/main.rs b/check/src/main.rs index f6dcbcc..1185992 100644 --- a/check/src/main.rs +++ b/check/src/main.rs @@ -1,12 +1,9 @@ use clap::Parser; use futures::TryStreamExt; -use log::{error, info, warn}; +use log::{error, info, warn, debug}; +use pcap_file_tokio::pcapng::{Block, PcapNgReader}; use rayhunter::{ - analysis::analyzer::{AnalyzerConfig, EventType, Harness}, - diag::DataType, - gsmtap_parser, - pcap::GsmtapPcapWriter, - qmdl::QmdlReader, + analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness}, diag::DataType, gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlReader }; use std::{collections::HashMap, future, path::PathBuf, pin::pin}; use tokio::fs::File; @@ -28,7 +25,90 @@ struct Args { verbose: bool, } -async fn analyze_file(qmdl_path: &str, show_skipped: bool) { +#[derive(Default)] +struct Report { + skipped_reasons: HashMap, + total_messages: u32, + warnings: u32, + skipped: u32, + file_path: String, +} + +impl Report { + fn new(file_path: &str) -> Self { + let mut report = Report::default(); + report.file_path = file_path.to_string(); + report + } + + fn process_row(&mut self, row: AnalysisRow) { + self.total_messages += 1; + if let Some(reason) = row.skipped_message_reason { + *self.skipped_reasons.entry(reason).or_insert(0) += 1; + self.skipped += 1; + return; + } + for maybe_event in row.events { + let Some(event) = maybe_event else { continue }; + let Some(timestamp) = row.packet_timestamp else { continue }; + match event.event_type { + EventType::Informational => { + info!( + "{}: INFO - {} {}", + self.file_path, timestamp, event.message, + ); + } + EventType::QualitativeWarning { severity } => { + warn!( + "{}: WARNING (Severity: {:?}) - {} {}", + self.file_path, severity, timestamp, event.message, + ); + self.warnings += 1; + } + } + } + } + + fn print_summary(&self, show_skipped: bool) { + if show_skipped && self.skipped > 0 { + info!("{}: messages skipped:", self.file_path); + for (reason, count) in self.skipped_reasons.iter() { + info!(" - {count}: \"{reason}\""); + } + } + info!( + "{}: {} messages analyzed, {} warnings, {} messages skipped", + self.file_path, + self.total_messages, + self.warnings, + self.skipped + ); + } +} + +async fn analyze_pcap(pcap_path: &str, show_skipped: bool) { + let mut harness = Harness::new_with_config(&AnalyzerConfig::default()); + let pcap_file = &mut File::open(&pcap_path) + .await + .expect("failed to open file"); + let mut pcap_reader = PcapNgReader::new(pcap_file) + .await + .expect("failed to read PCAP file"); + let mut report = Report::new(pcap_path); + while let Some(Ok(block)) = pcap_reader.next_block().await { + let row = match block { + Block::EnhancedPacket(packet) => harness.analyze_pcap_packet(packet), + other => { + debug!("{pcap_path}: skipping pcap packet {other:?}"); + continue; + }, + }; + report.process_row(row); + } + report.print_summary(show_skipped); +} + +async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool) { let mut harness = Harness::new_with_config(&AnalyzerConfig::default()); let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file"); let file_size = qmdl_file @@ -42,52 +122,17 @@ async fn analyze_file(qmdl_path: &str, show_skipped: bool) { .as_stream() .try_filter(|container| future::ready(container.data_type == DataType::UserSpace)) ); - let mut skipped_reasons: HashMap = HashMap::new(); - let mut total_messages = 0; - let mut warnings = 0; - let mut skipped = 0; + let mut report = Report::new(qmdl_path); while let Some(container) = qmdl_stream .try_next() .await .expect("failed getting QMDL container") { for row in harness.analyze_qmdl_messages(container) { - total_messages += 1; - if let Some(reason) = row.skipped_message_reason { - *skipped_reasons.entry(reason).or_insert(0) += 1; - skipped += 1; - continue; - } - for maybe_event in row.events { - let Some(event) = maybe_event else { continue }; - let Some(timestamp) = row.packet_timestamp else { continue }; - match event.event_type { - EventType::Informational => { - info!( - "{}: INFO - {} {}", - qmdl_path, timestamp, event.message, - ); - } - EventType::QualitativeWarning { severity } => { - warn!( - "{}: WARNING (Severity: {:?}) - {} {}", - qmdl_path, severity, timestamp, event.message, - ); - warnings += 1; - } - } - } + report.process_row(row); } } - if show_skipped && skipped > 0 { - info!("{qmdl_path}: messages skipped:"); - for (reason, count) in skipped_reasons.iter() { - info!(" - {count}: \"{reason}\""); - } - } - info!( - "{qmdl_path}: {total_messages} messages analyzed, {warnings} warnings, {skipped} messages skipped" - ); + report.print_summary(show_skipped); } async fn pcapify(qmdl_path: &PathBuf) { @@ -136,11 +181,11 @@ async fn main() { .with_module_level("asn1_codecs", log::LevelFilter::Error) .init() .unwrap(); - info!("Analyzers:"); let harness = Harness::new_with_config(&AnalyzerConfig::default()); + info!("Analyzers:"); for analyzer in harness.get_metadata().analyzers { - info!(" - {}: {} (v{})", analyzer.name, analyzer.description, analyzer.version); + info!(" - {} (v{}): {}", analyzer.name, analyzer.description, analyzer.version); } for maybe_entry in WalkDir::new(&args.path) { @@ -150,15 +195,18 @@ async fn main() { }; let name = entry.file_name(); let name_str = name.to_str().unwrap(); + let path = entry.path(); + let path_str = path.to_str().unwrap(); // instead of relying on the QMDL extension, can we check if a file is // QMDL by inspecting the contents? if name_str.ends_with(".qmdl") { - let path = entry.path(); - let path_str = path.to_str().unwrap(); - analyze_file(path_str, args.show_skipped).await; + analyze_qmdl(path_str, args.show_skipped).await; if args.pcapify { pcapify(&path.to_path_buf()).await; } + } else if name_str.ends_with(".pcap") { + // TODO: if we've already analyzed a QMDL, skip its corresponding pcap + analyze_pcap(path_str, args.show_skipped).await; } } } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index d333cd5..496cdb7 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -17,7 +17,7 @@ wingtech = [] [dependencies] bytes = "1.5.0" -chrono = "0.4.31" +chrono = { version = "0.4.31", features = ["serde"] } crc = "3.0.1" deku = { version = "0.18.0", features = ["logging"] } libc = "0.2.150" @@ -27,6 +27,7 @@ pcap-file-tokio = "0.1.0" pycrate-rs = { git = "https://github.com/wgreenberg/pycrate-rs" } thiserror = "1.0.50" telcom-parser = { path = "../telcom-parser" } -tokio = { version = "1.44.2", default-features = false } +tokio = { version = "1.44.2", default-features = false, features = ["time", "rt", "macros"] } futures = { version = "0.3.30", default-features = false } serde = { version = "1.0.197", features = ["derive"] } +num_enum = "0.7.4" diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index 508fcf9..36f86db 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -1,7 +1,9 @@ use chrono::{DateTime, FixedOffset}; +use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock; use serde::{Deserialize, Serialize}; use std::borrow::Cow; +use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType}; use crate::util::RuntimeMetadata; use crate::{diag::MessagesContainer, gsmtap_parser}; @@ -167,6 +169,39 @@ impl Harness { self.analyzers.push(analyzer); } + pub fn analyze_pcap_packet(&mut self, packet: EnhancedPacketBlock) -> AnalysisRow { + let epoch = DateTime::parse_from_rfc3339("1980-01-06T00:00:00-00:00").unwrap(); + let mut row = AnalysisRow { + packet_timestamp: Some(epoch + packet.timestamp), + skipped_message_reason: None, + events: Vec::new(), + }; + let gsmtap_offset = 20 + 8; + let gsmtap_data = &packet.data[gsmtap_offset..]; + // the type and subtype are at byte offsets 3 and 13, respectively + let gsmtap_header = match GsmtapType::new(gsmtap_data[2], gsmtap_data[12]) { + Ok(gsmtap_type) => GsmtapHeader::new(gsmtap_type), + Err(err) => { + row.skipped_message_reason = Some(format!("failed to read GsmtapHeader: {err:?}")); + return row; + }, + }; + let packet_offset = gsmtap_offset + 16; + let packet_data = &packet.data[packet_offset..]; + let gsmtap_message = GsmtapMessage { + header: gsmtap_header, + payload: packet_data.to_vec(), + }; + row.events = match InformationElement::try_from(&gsmtap_message) { + Ok(element) => self.analyze_information_element(&element), + Err(err) => { + row.skipped_message_reason = Some(format!("failed to convert gsmtap message to IE: {err:?}")); + return row; + }, + }; + return row; + } + pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> Vec { let mut rows = Vec::new(); for maybe_qmdl_message in container.into_messages() { @@ -211,7 +246,7 @@ impl Harness { rows } - fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec> { + pub fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec> { self.analyzers .iter_mut() .map(|analyzer| analyzer.analyze_information_element(ie)) diff --git a/lib/src/diag_device.rs b/lib/src/diag_device.rs index 67686b3..727dd34 100644 --- a/lib/src/diag_device.rs +++ b/lib/src/diag_device.rs @@ -293,7 +293,7 @@ impl DiagDevice { // TPLINK M7350 v5 source code can be downloaded at https://www.tp-link.com/de/support/gpl-code/?app=omada #[repr(C)] #[derive(Debug, Clone, Copy)] -struct diag_logging_mode_param_t { +struct DiagLoggingModeParam { req_mode: u32, peripheral_mask: u32, mode_param: u8, @@ -303,16 +303,16 @@ struct diag_logging_mode_param_t { fn enable_frame_readwrite(fd: i32, mode: u32) -> DiagResult<()> { unsafe { if libc::ioctl(fd, DIAG_IOCTL_SWITCH_LOGGING, mode, 0, 0, 0) < 0 { - let try_params: &[diag_logging_mode_param_t] = &[ + let try_params: &[DiagLoggingModeParam] = &[ // tplink M7350 HW revision 3-8 need this mode #[cfg(feature = "tplink")] - diag_logging_mode_param_t { + DiagLoggingModeParam { req_mode: mode, peripheral_mask: 0, mode_param: 1, }, // tplink M7350 HW revision v9 requires the same parameters as orbic - diag_logging_mode_param_t { + DiagLoggingModeParam { req_mode: mode, peripheral_mask: u32::MAX, mode_param: 0, @@ -326,8 +326,8 @@ fn enable_frame_readwrite(fd: i32, mode: u32) -> DiagResult<()> { ret = libc::ioctl( fd, DIAG_IOCTL_SWITCH_LOGGING, - &mut params as *mut diag_logging_mode_param_t, - std::mem::size_of::(), + &mut params as *mut DiagLoggingModeParam, + std::mem::size_of::(), 0, 0, 0, diff --git a/lib/src/gsmtap.rs b/lib/src/gsmtap.rs index 15ea2a7..84daa29 100644 --- a/lib/src/gsmtap.rs +++ b/lib/src/gsmtap.rs @@ -1,6 +1,7 @@ //! The spec for GSMTAP is here: https://github.com/osmocom/libosmocore/blob/master/include/osmocom/core/gsmtap.h use deku::prelude::*; +use num_enum::TryFromPrimitive; #[derive(Debug, Copy, Clone, PartialEq)] pub enum GsmtapType { @@ -28,14 +29,14 @@ pub enum GsmtapType { // based on https://github.com/fgsect/scat/blob/97442580e628de414c9f7c2a185f4e28d0ee7523/src/scat/parsers/qualcomm/diagltelogparser.py#L1337 #[repr(u8)] -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, TryFromPrimitive)] pub enum LteNasSubtype { Plain = 0, Secure = 1, } #[repr(u8)] -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, TryFromPrimitive)] pub enum UmSubtype { Unknown = 0x00, Bcch = 0x01, @@ -56,7 +57,7 @@ pub enum UmSubtype { } #[repr(u8)] -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, TryFromPrimitive)] pub enum UmtsRrcSubtype { DlDcch = 0, UlDcch = 1, @@ -123,7 +124,7 @@ pub enum UmtsRrcSubtype { } #[repr(u8)] -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, TryFromPrimitive)] pub enum LteRrcSubtype { DlCcch = 0, DlDcch = 1, @@ -150,7 +151,54 @@ pub enum LteRrcSubtype { ScMcchNb = 22, } +#[derive(Debug)] +pub enum GsmtapTypeError { + InvalidTypeSubtypeCombo(u8, u8), +} + impl GsmtapType { + pub fn new(gsmtap_type: u8, gsmtap_subtype: u8) -> Result { + let maybe_result = match gsmtap_type { + 0x01 => match UmSubtype::try_from(gsmtap_subtype) { + Ok(subtype) => Some(GsmtapType::Um(subtype)), + _ => None, + }, + 0x02 => Some(GsmtapType::Abis), + 0x03 => Some(GsmtapType::UmBurst), + 0x04 => Some(GsmtapType::SIM), + 0x05 => Some(GsmtapType::TetraI1), + 0x06 => Some(GsmtapType::TetraI1Burst), + 0x07 => Some(GsmtapType::WmxBurst), + 0x08 => Some(GsmtapType::GbLlc), + 0x09 => Some(GsmtapType::GbSndcp), + 0x0a => Some(GsmtapType::Gmr1Um), + 0x0b => Some(GsmtapType::UmtsRlcMac), + 0x0c => match UmtsRrcSubtype::try_from(gsmtap_subtype) { + Ok(subtype) => Some(GsmtapType::UmtsRrc(subtype)), + _ => None, + }, + 0x0d => match LteRrcSubtype::try_from(gsmtap_subtype) { + Ok(subtype) => Some(GsmtapType::LteRrc(subtype)), + _ => None, + }, + 0x0e => Some(GsmtapType::LteMac), + 0x0f => Some(GsmtapType::LteMacFramed), + 0x10 => Some(GsmtapType::OsmocoreLog), + 0x11 => Some(GsmtapType::QcDiag), + 0x12 => match LteNasSubtype::try_from(gsmtap_subtype) { + Ok(subtype) => Some(GsmtapType::LteNas(subtype)), + _ => None, + }, + 0x13 => Some(GsmtapType::E1T1), + 0x14 => Some(GsmtapType::GsmRlp), + _ => None, + }; + match maybe_result { + Some(result) => Ok(result), + None => Err(GsmtapTypeError::InvalidTypeSubtypeCombo(gsmtap_type, gsmtap_subtype)), + } + } + pub fn get_type(&self) -> u8 { match self { GsmtapType::Um(_) => 0x01,