diff --git a/Cargo.lock b/Cargo.lock index c3a5591..3a64a40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,9 +313,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" dependencies = [ "clap_builder", "clap_derive", @@ -323,9 +323,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -1093,6 +1093,7 @@ dependencies = [ "libc", "log", "pcap-file-tokio", + "serde", "telcom-parser", "thiserror", "tokio", @@ -1104,6 +1105,7 @@ version = "0.1.0" dependencies = [ "axum", "chrono", + "clap", "env_logger", "futures", "futures-core", @@ -1113,6 +1115,7 @@ dependencies = [ "mime_guess", "rayhunter", "serde", + "serde_json", "tempdir", "thiserror", "tokio", diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 334f2f9..c59b46f 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -5,7 +5,11 @@ edition = "2021" [[bin]] name = "rayhunter-daemon" -path = "src/main.rs" +path = "src/daemon.rs" + +[[bin]] +name = "rayhunter-check" +path = "src/check.rs" [dependencies] rayhunter = { path = "../lib" } @@ -25,3 +29,5 @@ tempdir = "0.3.7" chrono = { version = "0.4.31", features = ["serde"] } tokio-stream = "0.1.14" futures = "0.3.30" +clap = { version = "4.5.2", features = ["derive"] } +serde_json = "1.0.114" diff --git a/bin/src/check.rs b/bin/src/check.rs new file mode 100644 index 0000000..7f627e7 --- /dev/null +++ b/bin/src/check.rs @@ -0,0 +1,132 @@ +use std::{future, path::PathBuf, pin::pin}; +use chrono::{DateTime, FixedOffset}; +use rayhunter::{analysis::{analyzer::{Event, EventType, Harness}, information_element::InformationElement, lte_downgrade::LteSib6And7DowngradeAnalyzer}, diag::DataType, gsmtap_parser::GsmtapParser, qmdl::QmdlReader}; +use tokio::fs::File; +use serde::Serialize; +use clap::Parser; +use futures::TryStreamExt; + +#[derive(Parser, Debug)] +#[command(version, about)] +struct Args { + #[arg(short, long)] + qmdl_path: PathBuf, +} + +#[derive(Serialize, Debug)] +struct AnalyzerMetadata { + name: String, + description: String, +} + +#[derive(Serialize, Debug)] +struct ReportMetadata { + num_packets_analyzed: usize, + num_packets_skipped: usize, + num_warnings: usize, + first_packet_time: DateTime, + last_packet_time: DateTime, + analyzers: Vec, +} + +#[derive(Serialize, Debug)] +struct PacketAnalysis { + timestamp: DateTime, + events: Vec>, +} + +#[derive(Serialize, Debug)] +struct AnalysisReport { + metadata: ReportMetadata, + analysis: Vec, +} + +#[tokio::main] +async fn main() { + env_logger::init(); + let args = Args::parse(); + + let mut harness = Harness::new(); + harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{})); + + let mut num_packets_analyzed = 0; + let mut num_warnings = 0; + let mut first_packet_time: Option> = None; + let mut last_packet_time: Option> = None; + let mut skipped_message_reasons: Vec = Vec::new(); + let mut analysis: Vec = Vec::new(); + let mut analyzers: Vec = Vec::new(); + + let names = harness.get_names(); + let descriptions = harness.get_names(); + for (name, description) in names.iter().zip(descriptions.iter()) { + analyzers.push(AnalyzerMetadata { + name: name.to_string(), + description: description.to_string(), + }); + } + + let qmdl_file = File::open(args.qmdl_path).await.expect("failed to open QMDL file"); + let file_size = qmdl_file.metadata().await.expect("failed to get QMDL file metadata").len(); + let mut gsmtap_parser = GsmtapParser::new(); + let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize)); + let mut qmdl_stream = pin!(qmdl_reader.as_stream() + .try_filter(|container| future::ready(container.data_type == DataType::UserSpace))); + while let Some(container) = qmdl_stream.try_next().await.expect("failed getting QMDL container") { + for maybe_qmdl_message in container.into_messages() { + let qmdl_message = match maybe_qmdl_message { + Ok(msg) => msg, + Err(err) => { + skipped_message_reasons.push(format!("{:?}", err)); + continue; + } + }; + let gsmtap_message = match gsmtap_parser.parse(qmdl_message) { + Ok(msg) => msg, + Err(err) => { + skipped_message_reasons.push(format!("{:?}", err)); + continue; + } + }; + let Some((timestamp, gsmtap_msg)) = gsmtap_message else { + continue; + }; + let element = match InformationElement::try_from(&gsmtap_msg) { + Ok(element) => element, + Err(err) => { + skipped_message_reasons.push(format!("{:?}", err)); + continue; + } + }; + if first_packet_time.is_none() { + first_packet_time = Some(timestamp.to_datetime()); + } + last_packet_time = Some(timestamp.to_datetime()); + num_packets_analyzed += 1; + let analysis_result = harness.analyze_information_element(&element); + if analysis_result.iter().any(Option::is_some) { + num_warnings += analysis_result.iter() + .filter(|maybe_event| matches!(maybe_event, Some(Event { event_type: EventType::QualitativeWarning { .. }, .. }))) + .count(); + analysis.push(PacketAnalysis { + timestamp: timestamp.to_datetime(), + events: analysis_result, + }); + } + } + } + + let report = AnalysisReport { + metadata: ReportMetadata { + num_packets_analyzed, + num_packets_skipped: skipped_message_reasons.len(), + num_warnings, + first_packet_time: first_packet_time.expect("no packet times set"), + last_packet_time: last_packet_time.expect("no packet times set"), + analyzers, + }, + analysis, + }; + + println!("{}", serde_json::to_string(&report).expect("failed to serialize report")); +} diff --git a/bin/src/pcap.rs b/bin/src/pcap.rs index 5dadd24..95f655a 100644 --- a/bin/src/pcap.rs +++ b/bin/src/pcap.rs @@ -48,7 +48,7 @@ pub async fn get_pcap(State(state): State>, Path(qmdl_name): Pa for maybe_msg in container.into_messages() { match maybe_msg { Ok(msg) => { - let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg) + let maybe_gsmtap_msg = gsmtap_parser.parse(msg) .expect("error parsing gsmtap message"); if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp).await diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 88b72ac..caa6337 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -18,3 +18,4 @@ telcom-parser = { path = "../telcom-parser" } tokio = { version = "1.35.1", features = ["full"] } futures-core = "0.3.30" futures = "0.3.30" +serde = { version = "1.0.197", features = ["derive"] } diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index e7486d4..f80fdd0 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use serde::Serialize; use super::information_element::InformationElement; @@ -7,6 +8,7 @@ use super::information_element::InformationElement; /// * Low: if combined with a large number of other Warnings, user should investigate /// * Medium: if combined with a few other Warnings, user should investigate /// * High: user should investigate +#[derive(Serialize, Debug, Clone)] pub enum Severity { Low, Medium, @@ -15,14 +17,17 @@ pub enum Severity { /// [QualitativeWarning] events will always be shown to the user in some manner, /// while `Informational` ones may be hidden based on user settings. +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "type")] pub enum EventType { Informational, - QualitativeWarning(Severity), + QualitativeWarning { severity: Severity }, } /// Events are user-facing signals that can be emitted by an [Analyzer] upon a /// message being received. They can be used to signifiy an IC detection /// warning, or just to display some relevant information to the user. +#[derive(Serialize, Debug, Clone)] pub struct Event { pub event_type: EventType, pub message: String, @@ -49,3 +54,37 @@ pub trait Analyzer { /// thousands of them alongside many other [Analyzers](Analyzer). fn analyze_information_element(&mut self, ie: &InformationElement) -> Option; } + +pub struct Harness { + analyzers: Vec>, +} + +impl Harness { + pub fn new() -> Self { + Self { + analyzers: Vec::new(), + } + } + + pub fn add_analyzer(&mut self, analyzer: Box) { + self.analyzers.push(analyzer); + } + + pub fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec> { + self.analyzers.iter_mut() + .map(|analyzer| analyzer.analyze_information_element(ie)) + .collect() + } + + pub fn get_names(&self) -> Vec> { + self.analyzers.iter() + .map(|analyzer| analyzer.get_name()) + .collect() + } + + pub fn get_descriptions(&self) -> Vec> { + self.analyzers.iter() + .map(|analyzer| analyzer.get_description()) + .collect() + } +} diff --git a/lib/src/analysis/information_element.rs b/lib/src/analysis/information_element.rs index 653b221..c3d2121 100644 --- a/lib/src/analysis/information_element.rs +++ b/lib/src/analysis/information_element.rs @@ -52,31 +52,34 @@ pub enum LteInformationElement { //ScMcchNb(), } -impl TryFrom<&GsmtapMessage> for LteInformationElement { +impl TryFrom<&GsmtapMessage> for InformationElement { type Error = InformationElementError; fn try_from(gsmtap_msg: &GsmtapMessage) -> Result { - if let GsmtapType::LteRrc(lte_rrc_subtype) = gsmtap_msg.header.gsmtap_type { - use LteRrcSubtype as L; - use LteInformationElement as R; - return match lte_rrc_subtype { - L::DlCcch => Ok(R::DlCcch(decode(&gsmtap_msg.payload)?)), - L::DlDcch => Ok(R::DlDcch(decode(&gsmtap_msg.payload)?)), - L::UlCcch => Ok(R::UlCcch(decode(&gsmtap_msg.payload)?)), - L::UlDcch => Ok(R::UlDcch(decode(&gsmtap_msg.payload)?)), - L::BcchBch => Ok(R::BcchBch(decode(&gsmtap_msg.payload)?)), - L::BcchDlSch => Ok(R::BcchDlSch(decode(&gsmtap_msg.payload)?)), - L::PCCH => Ok(R::PCCH(decode(&gsmtap_msg.payload)?)), - L::MCCH => Ok(R::MCCH(decode(&gsmtap_msg.payload)?)), - L::ScMcch => Ok(R::ScMcch(decode(&gsmtap_msg.payload)?)), - L::BcchBchMbms => Ok(R::BcchBchMbms(decode(&gsmtap_msg.payload)?)), - L::BcchDlSchBr => Ok(R::BcchDlSchBr(decode(&gsmtap_msg.payload)?)), - L::BcchDlSchMbms => Ok(R::BcchDlSchMbms(decode(&gsmtap_msg.payload)?)), - L::SbcchSlBch => Ok(R::SbcchSlBch(decode(&gsmtap_msg.payload)?)), - L::SbcchSlBchV2x => Ok(R::SbcchSlBchV2x(decode(&gsmtap_msg.payload)?)), - _ => Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)), - }; + match gsmtap_msg.header.gsmtap_type { + GsmtapType::LteRrc(lte_rrc_subtype) => { + use LteRrcSubtype as L; + use LteInformationElement as R; + let lte = match lte_rrc_subtype { + L::DlCcch => R::DlCcch(decode(&gsmtap_msg.payload)?), + L::DlDcch => R::DlDcch(decode(&gsmtap_msg.payload)?), + L::UlCcch => R::UlCcch(decode(&gsmtap_msg.payload)?), + L::UlDcch => R::UlDcch(decode(&gsmtap_msg.payload)?), + L::BcchBch => R::BcchBch(decode(&gsmtap_msg.payload)?), + L::BcchDlSch => R::BcchDlSch(decode(&gsmtap_msg.payload)?), + L::PCCH => R::PCCH(decode(&gsmtap_msg.payload)?), + L::MCCH => R::MCCH(decode(&gsmtap_msg.payload)?), + L::ScMcch => R::ScMcch(decode(&gsmtap_msg.payload)?), + L::BcchBchMbms => R::BcchBchMbms(decode(&gsmtap_msg.payload)?), + L::BcchDlSchBr => R::BcchDlSchBr(decode(&gsmtap_msg.payload)?), + L::BcchDlSchMbms => R::BcchDlSchMbms(decode(&gsmtap_msg.payload)?), + L::SbcchSlBch => R::SbcchSlBch(decode(&gsmtap_msg.payload)?), + L::SbcchSlBchV2x => R::SbcchSlBchV2x(decode(&gsmtap_msg.payload)?), + _ => return Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)), + }; + Ok(InformationElement::LTE(lte)) + }, + _ => Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)), } - Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)) } } diff --git a/lib/src/analysis/lte_downgrade.rs b/lib/src/analysis/lte_downgrade.rs index da73c9d..44003fd 100644 --- a/lib/src/analysis/lte_downgrade.rs +++ b/lib/src/analysis/lte_downgrade.rs @@ -5,10 +5,10 @@ use super::information_element::{InformationElement, LteInformationElement}; use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1, CellReselectionPriority, SystemInformationBlockType7, SystemInformationCriticalExtensions, SystemInformation_r8_IEsSib_TypeAndInfo, SystemInformation_r8_IEsSib_TypeAndInfo_Entry}; /// Based on heuristic T7 from Shinjo Park's "Why We Cannot Win". -pub struct LteSib7DowngradeAnalyzer { +pub struct LteSib6And7DowngradeAnalyzer { } -impl LteSib7DowngradeAnalyzer { +impl LteSib6And7DowngradeAnalyzer { fn unpack_system_information<'a>(&self, ie: &'a InformationElement) -> Option<&'a SystemInformation_r8_IEsSib_TypeAndInfo> { if let InformationElement::LTE(LteInformationElement::BcchDlSch(bcch_dl_sch_message)) = ie { if let BCCH_DL_SCH_MessageType::C1(BCCH_DL_SCH_MessageType_c1::SystemInformation(system_information)) = &bcch_dl_sch_message.message { @@ -22,13 +22,13 @@ impl LteSib7DowngradeAnalyzer { } // TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones -impl Analyzer for LteSib7DowngradeAnalyzer { +impl Analyzer for LteSib6And7DowngradeAnalyzer { fn get_name(&self) -> Cow { - Cow::from("LTE SIB 7 Downgrade") + Cow::from("LTE SIB 6/7 Downgrade") } fn get_description(&self) -> Cow { - Cow::from("Tests for LTE cells broadcasting a SIB type 7 which include 2G/3G frequencies with higher priorities.") + Cow::from("Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.") } fn analyze_information_element(&mut self, ie: &InformationElement) -> Option { @@ -41,7 +41,7 @@ impl Analyzer for LteSib7DowngradeAnalyzer { if let Some(CellReselectionPriority(p)) = carrier_info.cell_reselection_priority { if p == 0 { return Some(Event { - event_type: EventType::QualitativeWarning(Severity::High), + event_type: EventType::QualitativeWarning { severity: Severity::High }, message: "LTE cell advertised a 3G cell for priority 0 reselection".to_string(), }); } @@ -53,7 +53,7 @@ impl Analyzer for LteSib7DowngradeAnalyzer { if let Some(CellReselectionPriority(p)) = carrier_info.cell_reselection_priority { if p == 0 { return Some(Event { - event_type: EventType::QualitativeWarning(Severity::High), + event_type: EventType::QualitativeWarning { severity: Severity::High }, message: "LTE cell advertised a 3G cell for priority 0 reselection".to_string(), }); } @@ -66,7 +66,7 @@ impl Analyzer for LteSib7DowngradeAnalyzer { if let Some(CellReselectionPriority(p)) = carrier_info.common_info.cell_reselection_priority { if p == 0 { return Some(Event { - event_type: EventType::QualitativeWarning(Severity::High), + event_type: EventType::QualitativeWarning { severity: Severity::High }, message: "LTE cell advertised a 2G cell for priority 0 reselection".to_string(), }); } diff --git a/lib/src/gsmtap_parser.rs b/lib/src/gsmtap_parser.rs index 8ca9cfb..eacfc61 100644 --- a/lib/src/gsmtap_parser.rs +++ b/lib/src/gsmtap_parser.rs @@ -26,7 +26,7 @@ impl GsmtapParser { GsmtapParser {} } - pub fn recv_message(&mut self, msg: Message) -> Result, GsmtapParserError> { + pub fn parse(&mut self, msg: Message) -> Result, GsmtapParserError> { if let Message::Log { timestamp, body, .. } = msg { match self.log_to_gsmtap(body)? { Some(msg) => Ok(Some((timestamp, msg))), diff --git a/lib/tests/test_lte_parsing.rs b/lib/tests/test_lte_parsing.rs index ed449b8..187807d 100644 --- a/lib/tests/test_lte_parsing.rs +++ b/lib/tests/test_lte_parsing.rs @@ -42,7 +42,7 @@ fn test_lte_rrc_ota() { } } }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[0x10, 0x15]); assert_eq!(gsmtap_msg.header.packet_type, 13); assert_eq!(gsmtap_msg.header.timeslot, 0); @@ -85,7 +85,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x10, 0x15, ]); @@ -132,7 +132,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29, 0x15, 0x16, 0x00, @@ -183,7 +183,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x08, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x05, 0x43, 0x68, 0xc0, 0x3b, 0xda, 0x30, 0x04, 0xa6, @@ -229,7 +229,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x28, 0x18, 0x40, 0x16, 0x08, 0x08, 0x80, 0x00, 0x00, @@ -274,7 +274,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x40, 0x0c, 0x8e, 0xc9, 0x42, 0x89, 0xe0, ]); @@ -324,7 +324,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x08, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c, 0x31, 0x68, 0x04, 0x40, 0x1a, 0x00, 0x49, 0x16, @@ -370,7 +370,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[0x2c, 0x00]); assert_eq!(gsmtap_msg.header.packet_type, 13); assert_eq!(gsmtap_msg.header.timeslot, 0); @@ -412,7 +412,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x40, 0x0b, 0x8e, 0xc1, 0xdd, 0x13, 0xb0, ]); @@ -455,7 +455,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[0x2e, 0x02]); assert_eq!(gsmtap_msg.header.packet_type, 13); assert_eq!(gsmtap_msg.header.timeslot, 0); @@ -501,7 +501,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x40, 0x49, 0x88, 0x05, 0xc0, 0x97, 0x02, 0xd3, 0xb0, 0x98, 0x1c, 0x20, 0xa0, 0x81, 0x8c, 0x43,