diff --git a/daemon/src/analysis.rs b/daemon/src/analysis.rs index 226e73f..db34ac7 100644 --- a/daemon/src/analysis.rs +++ b/daemon/src/analysis.rs @@ -18,7 +18,6 @@ use tokio::sync::mpsc::Receiver; use tokio::sync::{RwLock, RwLockWriteGuard}; use tokio_util::task::TaskTracker; -use crate::dummy_analyzer::TestAnalyzer; use crate::qmdl_store::RecordingStore; use crate::server::ServerState; @@ -37,13 +36,10 @@ pub struct AnalysisWriter { impl AnalysisWriter { pub async fn new( file: File, - enable_dummy_analyzer: bool, analyzer_config: &AnalyzerConfig, ) -> Result { - let mut harness = Harness::new_with_config(analyzer_config); - if enable_dummy_analyzer { - harness.add_analyzer(Box::new(TestAnalyzer { count: 0 })); - } + let harness = Harness::new_with_config(analyzer_config); + let mut result = Self { writer: BufWriter::new(file), @@ -56,16 +52,20 @@ impl AnalysisWriter { } // Runs the analysis harness on the given container, serializing the results - // to the analysis file and returning the file's new length. + // to the analysis file, returning the file's new length, and whether any + // warnings were detected pub async fn analyze( &mut self, container: MessagesContainer, ) -> Result<(usize, bool), std::io::Error> { - let row = self.harness.analyze_qmdl_messages(container); - if !row.is_empty() { - self.write(&row).await?; + let mut warning_detected = false; + for row in self.harness.analyze_qmdl_messages(container) { + if !row.is_empty() { + self.write(&row).await?; + } + warning_detected |= row.contains_warnings(); } - Ok((self.bytes_written, row.contains_warnings())) + Ok((self.bytes_written, warning_detected)) } async fn write(&mut self, value: &T) -> Result<(), std::io::Error> { @@ -134,7 +134,6 @@ async fn finish_running_analysis(analysis_status_lock: Arc>, - enable_dummy_analyzer: bool, analyzer_config: &AnalyzerConfig, ) -> Result<(), String> { info!("Opening QMDL and analysis file for {name}..."); @@ -156,7 +155,7 @@ async fn perform_analysis( }; let mut analysis_writer = - AnalysisWriter::new(analysis_file, enable_dummy_analyzer, analyzer_config) + AnalysisWriter::new(analysis_file, analyzer_config) .await .map_err(|e| format!("{e:?}"))?; let file_size = qmdl_file @@ -203,7 +202,6 @@ pub fn run_analysis_thread( mut analysis_rx: Receiver, qmdl_store_lock: Arc>, analysis_status_lock: Arc>, - enable_dummy_analyzer: bool, analyzer_config: AnalyzerConfig, ) { task_tracker.spawn(async move { @@ -216,7 +214,6 @@ pub fn run_analysis_thread( if let Err(err) = perform_analysis( &name, qmdl_store_lock.clone(), - enable_dummy_analyzer, &analyzer_config, ) .await diff --git a/daemon/src/config.rs b/daemon/src/config.rs index 5d3ffec..8d1c4f5 100644 --- a/daemon/src/config.rs +++ b/daemon/src/config.rs @@ -11,10 +11,12 @@ pub struct Config { pub port: u16, pub debug_mode: bool, pub ui_level: u8, - pub enable_dummy_analyzer: bool, pub colorblind_mode: bool, pub key_input_mode: u8, pub analyzers: AnalyzerConfig, + + // deprecated + pub enable_dummy_analyzer: bool, } impl Default for Config { @@ -24,10 +26,12 @@ impl Default for Config { port: 8080, debug_mode: false, ui_level: 1, - enable_dummy_analyzer: false, colorblind_mode: false, key_input_mode: 0, analyzers: AnalyzerConfig::default(), + + // deprecated + enable_dummy_analyzer: false, } } } diff --git a/daemon/src/diag.rs b/daemon/src/diag.rs index 0454a27..f5158cc 100644 --- a/daemon/src/diag.rs +++ b/daemon/src/diag.rs @@ -37,14 +37,13 @@ pub fn run_diag_read_thread( ui_update_sender: Sender, qmdl_store_lock: Arc>, analysis_sender: Sender, - enable_dummy_analyzer: bool, analyzer_config: AnalyzerConfig, ) { task_tracker.spawn(async move { let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry"); let mut maybe_qmdl_writer: Option> = Some(QmdlWriter::new(initial_qmdl_file)); let mut diag_stream = pin!(dev.as_stream().into_stream()); - let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer, &analyzer_config).await + let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, &analyzer_config).await .expect("failed to create analysis writer")); loop { tokio::select! { @@ -66,7 +65,7 @@ pub fn run_diag_read_thread( analysis_writer.close().await.expect("failed to close analysis writer"); } - maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer, &analyzer_config).await + maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, &analyzer_config).await .expect("failed to write to analysis file")); if let Err(e) = ui_update_sender.send(display::DisplayState::Recording).await { diff --git a/daemon/src/dummy_analyzer.rs b/daemon/src/dummy_analyzer.rs deleted file mode 100644 index 8b4ddc8..0000000 --- a/daemon/src/dummy_analyzer.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::borrow::Cow; - -use rayhunter::telcom_parser::lte_rrc::{PCCH_MessageType, PCCH_MessageType_c1, PagingUE_Identity}; - -use rayhunter::analysis::analyzer::{Analyzer, Event, EventType, Severity}; -use rayhunter::analysis::information_element::{InformationElement, LteInformationElement}; - -pub struct TestAnalyzer { - pub count: i32, -} - -impl Analyzer for TestAnalyzer { - fn get_name(&self) -> Cow { - Cow::from("Example Analyzer") - } - - fn get_description(&self) -> Cow { - Cow::from( - "Always returns true, if you are seeing this you are either a developer or you are about to have problems.", - ) - } - - fn analyze_information_element(&mut self, ie: &InformationElement) -> Option { - self.count += 1; - if self.count % 100 == 0 { - return Some(Event { - event_type: EventType::Informational, - message: "multiple of 100 events processed".to_string(), - }); - } - let pcch_msg = match ie { - InformationElement::LTE(lte_ie) => match &**lte_ie { - LteInformationElement::PCCH(pcch_msg) => pcch_msg, - _ => return None, - }, - _ => return None, - }; - let PCCH_MessageType::C1(PCCH_MessageType_c1::Paging(paging)) = &pcch_msg.message else { - return None; - }; - for record in &paging.paging_record_list.as_ref()?.0 { - if let PagingUE_Identity::S_TMSI(_) = record.ue_identity { - return Some(Event { - event_type: EventType::QualitativeWarning { - severity: Severity::Low, - }, - message: "TMSI was provided to cell".to_string(), - }); - } - } - None - } -} diff --git a/daemon/src/main.rs b/daemon/src/main.rs index ac5aa8c..0ef81bb 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -2,7 +2,6 @@ mod analysis; mod config; mod diag; mod display; -mod dummy_analyzer; mod error; mod key_input; mod pcap; @@ -231,7 +230,6 @@ async fn run_with_config( ui_update_tx.clone(), qmdl_store_lock.clone(), analysis_tx.clone(), - config.enable_dummy_analyzer, config.analyzers.clone(), ); info!("Starting UI"); @@ -256,7 +254,6 @@ async fn run_with_config( analysis_rx, qmdl_store_lock.clone(), analysis_status_lock.clone(), - config.enable_dummy_analyzer, config.analyzers.clone(), ); let should_restart_flag = Arc::new(AtomicBool::new(false)); diff --git a/dist/config.toml.example b/dist/config.toml.example index b63a76a..f4d3bb3 100644 --- a/dist/config.toml.example +++ b/dist/config.toml.example @@ -2,7 +2,6 @@ qmdl_store_path = "/data/rayhunter/qmdl" port = 8080 debug_mode = false -enable_dummy_analyzer = false colorblind_mode = false # UI Levels: # @@ -28,4 +27,4 @@ key_input_mode = 0 imsi_requested = true connection_redirect_2g_downgrade = true lte_sib6_and_7_downgrade = true -null_cipher = true +null_cipher = true diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index 8f6d45e..a7b31f1 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -31,6 +31,8 @@ impl Default for AnalyzerConfig { } } +pub const REPORT_VERSION: u32 = 1; + /// Qualitative measure of how severe a Warning event type is. /// The levels should break down like this: /// * Low: if combined with a large number of other Warnings, user should investigate @@ -81,44 +83,44 @@ pub trait Analyzer { /// [Analyzer] updates per message, since it may be run over hundreds or /// thousands of them alongside many other [Analyzers](Analyzer). fn analyze_information_element(&mut self, ie: &InformationElement) -> Option; + + /// Returns a version number for this Analyzer. This should only ever + /// increase in value, and do so whenever substantial changes are made to + /// the Analyzer's heuristic. + fn get_version(&self) -> u32; } #[derive(Serialize, Debug)] pub struct AnalyzerMetadata { pub name: String, pub description: String, + pub version: u32, } #[derive(Serialize, Debug)] pub struct ReportMetadata { pub analyzers: Vec, pub rayhunter: RuntimeMetadata, -} - -#[derive(Serialize, Debug, Clone)] -pub struct PacketAnalysis { - pub timestamp: DateTime, - pub events: Vec>, + // anytime the format of the report changes, bump this by 1 + pub report_version: u32, } #[derive(Serialize, Debug)] pub struct AnalysisRow { - pub timestamp: DateTime, - pub skipped_message_reasons: Vec, - pub analysis: Vec, + pub packet_timestamp: Option>, + pub skipped_message_reason: Option, + pub events: Vec>, } impl AnalysisRow { pub fn is_empty(&self) -> bool { - self.skipped_message_reasons.is_empty() && self.analysis.is_empty() + self.skipped_message_reason.is_none() && !self.contains_warnings() } pub fn contains_warnings(&self) -> bool { - for analysis in &self.analysis { - for event in analysis.events.iter().flatten() { - if matches!(event.event_type, EventType::QualitativeWarning { .. }) { - return true; - } + for event in self.events.iter().flatten() { + if matches!(event.event_type, EventType::QualitativeWarning { .. }) { + return true; } } false @@ -165,17 +167,20 @@ impl Harness { self.analyzers.push(analyzer); } - pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> AnalysisRow { - let mut row = AnalysisRow { - timestamp: chrono::Local::now().fixed_offset(), - skipped_message_reasons: Vec::new(), - analysis: Vec::new(), - }; + pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> Vec { + let mut rows = Vec::new(); for maybe_qmdl_message in container.into_messages() { + rows.push(AnalysisRow { + packet_timestamp: None, + skipped_message_reason: None, + events: Vec::new(), + }); + // unwrap is safe here since we just pushed a value + let row = rows.last_mut().unwrap(); let qmdl_message = match maybe_qmdl_message { Ok(msg) => msg, Err(err) => { - row.skipped_message_reasons.push(format!("{err:?}")); + row.skipped_message_reason = Some(format!("{err:?}")); continue; } }; @@ -183,7 +188,7 @@ impl Harness { let gsmtap_message = match gsmtap_parser::parse(qmdl_message) { Ok(msg) => msg, Err(err) => { - row.skipped_message_reasons.push(format!("{err:?}")); + row.skipped_message_reason = Some(format!("{err:?}")); continue; } }; @@ -191,24 +196,19 @@ impl Harness { let Some((timestamp, gsmtap_msg)) = gsmtap_message else { continue; }; + row.packet_timestamp = Some(timestamp.to_datetime()); let element = match InformationElement::try_from(&gsmtap_msg) { Ok(element) => element, Err(err) => { - row.skipped_message_reasons.push(format!("{err:?}")); + row.skipped_message_reason = Some(format!("{err:?}")); continue; } }; - let analysis_result = self.analyze_information_element(&element); - if analysis_result.iter().any(Option::is_some) { - row.analysis.push(PacketAnalysis { - timestamp: timestamp.to_datetime(), - events: analysis_result, - }); - } + row.events = self.analyze_information_element(&element); } - row + rows } fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec> { @@ -218,28 +218,13 @@ impl Harness { .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() - } - pub fn get_metadata(&self) -> ReportMetadata { - let names = self.get_names(); - let descriptions = self.get_descriptions(); let mut analyzers = Vec::new(); - for (name, description) in names.iter().zip(descriptions.iter()) { + for analyzer in &self.analyzers { analyzers.push(AnalyzerMetadata { - name: name.to_string(), - description: description.to_string(), + name: analyzer.get_name().to_string(), + description: analyzer.get_description().to_string(), + version: analyzer.get_version(), }); } @@ -248,6 +233,7 @@ impl Harness { ReportMetadata { analyzers, rayhunter, + report_version: REPORT_VERSION, } } } diff --git a/lib/src/analysis/connection_redirect_downgrade.rs b/lib/src/analysis/connection_redirect_downgrade.rs index 2042621..0f40fa5 100644 --- a/lib/src/analysis/connection_redirect_downgrade.rs +++ b/lib/src/analysis/connection_redirect_downgrade.rs @@ -22,6 +22,10 @@ impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer { Cow::from("Tests if a cell releases our connection and redirects us to a 2G cell.") } + fn get_version(&self) -> u32 { + 1 + } + fn analyze_information_element(&mut self, ie: &InformationElement) -> Option { unpack!(InformationElement::LTE(lte_ie) = ie); let message = match &**lte_ie { diff --git a/lib/src/analysis/imsi_provided.rs b/lib/src/analysis/imsi_provided.rs index 05723ab..df1516d 100644 --- a/lib/src/analysis/imsi_provided.rs +++ b/lib/src/analysis/imsi_provided.rs @@ -16,6 +16,10 @@ impl Analyzer for ImsiProvidedAnalyzer { Cow::from("Tests whether the UE's IMSI was ever provided to the cell") } + fn get_version(&self) -> u32 { + 1 + } + fn analyze_information_element(&mut self, ie: &InformationElement) -> Option { let pcch_msg = match ie { InformationElement::LTE(lte_ie) => match &**lte_ie { diff --git a/lib/src/analysis/imsi_requested.rs b/lib/src/analysis/imsi_requested.rs index 3861687..24b759e 100644 --- a/lib/src/analysis/imsi_requested.rs +++ b/lib/src/analysis/imsi_requested.rs @@ -34,6 +34,10 @@ impl Analyzer for ImsiRequestedAnalyzer { Cow::from("Tests whether the ME sends an IMSI Identity Request NAS message") } + fn get_version(&self) -> u32 { + 1 + } + fn analyze_information_element(&mut self, ie: &InformationElement) -> Option { self.packet_num += 1; let payload = match ie { diff --git a/lib/src/analysis/null_cipher.rs b/lib/src/analysis/null_cipher.rs index d778cdf..22642fc 100644 --- a/lib/src/analysis/null_cipher.rs +++ b/lib/src/analysis/null_cipher.rs @@ -127,6 +127,10 @@ impl Analyzer for NullCipherAnalyzer { Cow::from("Tests whether the cell suggests using a null cipher (EEA0)") } + fn get_version(&self) -> u32 { + 1 + } + fn analyze_information_element(&mut self, ie: &InformationElement) -> Option { let dcch_msg = match ie { InformationElement::LTE(lte_ie) => match &**lte_ie { diff --git a/lib/src/analysis/priority_2g_downgrade.rs b/lib/src/analysis/priority_2g_downgrade.rs index 2c36525..64a9150 100644 --- a/lib/src/analysis/priority_2g_downgrade.rs +++ b/lib/src/analysis/priority_2g_downgrade.rs @@ -46,6 +46,10 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer { ) } + fn get_version(&self) -> u32 { + 1 + } + fn analyze_information_element( &mut self, ie: &InformationElement,