From 3c932f0ce99f3026a381e7c8dd306a2833ecb4c3 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Tue, 30 Apr 2024 14:43:38 -0700 Subject: [PATCH] daemon: run analysis in realtime Currently we just show the results of analysis as a
 tagged
JSON blob, but eventually we can make some actual UI
---
 bin/src/analysis.rs           |  56 +++++++++
 bin/src/check.rs              | 108 +---------------
 bin/src/daemon.rs             |  23 +++-
 bin/src/diag.rs               |  22 +++-
 bin/src/pcap.rs               |   5 +-
 bin/src/server.rs             |   2 +
 bin/static/index.html         |   4 +
 bin/static/js/main.js         |   8 ++
 lib/src/analysis/analyzer.rs  | 130 ++++++++++++++++++-
 lib/src/gsmtap_parser.rs      | 229 ++++++++++++++++------------------
 lib/tests/test_lte_parsing.rs |  35 +++---
 11 files changed, 361 insertions(+), 261 deletions(-)
 create mode 100644 bin/src/analysis.rs

diff --git a/bin/src/analysis.rs b/bin/src/analysis.rs
new file mode 100644
index 0000000..a9d23c5
--- /dev/null
+++ b/bin/src/analysis.rs
@@ -0,0 +1,56 @@
+use std::sync::Arc;
+
+use axum::{extract::State, http::StatusCode, Json};
+use log::error;
+use rayhunter::{analysis::analyzer::{AnalysisReport, Harness}, diag::MessagesContainer};
+use tokio::sync;
+use tokio_util::task::TaskTracker;
+
+use crate::server::ServerState;
+
+#[derive(Debug)]
+pub enum AnalysisMessage {
+    Reset,
+    GetReport(sync::oneshot::Sender),
+    AnalyzeContainer(MessagesContainer),
+    StopThread,
+}
+
+pub fn run_analysis_thread(task_tracker: &TaskTracker) -> sync::mpsc::Sender {
+    let (tx, mut rx) = sync::mpsc::channel(5);
+
+    task_tracker.spawn(async move {
+        let mut harness = Harness::new_with_all_analyzers();
+        loop {
+            match rx.recv().await {
+                Some(AnalysisMessage::GetReport(sender)) => {
+                    // this might fail if the client closes their connection
+                    // before we're done building the report
+                    if let Err(e) = sender.send(harness.build_analysis_report()) {
+                        error!("failed to send analysis report: {:?}", e);
+                    }
+                },
+                Some(AnalysisMessage::Reset) => harness = Harness::new_with_all_analyzers(),
+                Some(AnalysisMessage::AnalyzeContainer(container)) => harness.analyze_qmdl_messages(container),
+                Some(AnalysisMessage::StopThread) | None => break,
+            }
+        }
+    });
+
+    tx
+}
+
+pub async fn get_analysis_report(State(state): State>) -> Result, (StatusCode, String)> {
+    if state.readonly_mode {
+        return Err((StatusCode::FORBIDDEN, "server is in readonly mode".to_string()));
+    }
+    let analysis_tx = state.maybe_analysis_tx.as_ref().unwrap();
+    let (report_tx, report_rx) = tokio::sync::oneshot::channel();
+    if let Err(e) = analysis_tx.send(AnalysisMessage::GetReport(report_tx)).await {
+        return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("error reaching analysis thread: {:?}", e)));
+    }
+    match report_rx.await {
+        Ok(report) => Ok(Json(report)),
+        Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, format!("error fetching analysis report: {:?}", e)))
+    }
+}
diff --git a/bin/src/check.rs b/bin/src/check.rs
index 7f627e7..0383c50 100644
--- a/bin/src/check.rs
+++ b/bin/src/check.rs
@@ -1,8 +1,6 @@
 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 rayhunter::{analysis::analyzer::Harness, diag::DataType, qmdl::QmdlReader};
 use tokio::fs::File;
-use serde::Serialize;
 use clap::Parser;
 use futures::TryStreamExt;
 
@@ -13,120 +11,22 @@ struct Args {
     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 mut harness = Harness::new_with_all_analyzers();
 
     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,
-                });
-            }
-        }
+        harness.analyze_qmdl_messages(container)
     }
 
-    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,
-    };
-
+    let report = harness.build_analysis_report();
     println!("{}", serde_json::to_string(&report).expect("failed to serialize report"));
 }
diff --git a/bin/src/daemon.rs b/bin/src/daemon.rs
index 4d65521..9aad6c5 100644
--- a/bin/src/daemon.rs
+++ b/bin/src/daemon.rs
@@ -1,3 +1,4 @@
+mod analysis;
 mod config;
 mod error;
 mod pcap;
@@ -14,6 +15,7 @@ use crate::pcap::get_pcap;
 use crate::stats::get_system_stats;
 use crate::error::RayhunterError;
 
+use analysis::{get_analysis_report, run_analysis_thread, AnalysisMessage};
 use axum::response::Redirect;
 use diag::{DiagDeviceCtrlMessage, start_recording, stop_recording};
 use log::{info, error};
@@ -37,12 +39,14 @@ async fn run_server(
     config: &config::Config,
     qmdl_store_lock: Arc>,
     server_shutdown_rx: oneshot::Receiver<()>,
-    diag_device_sender: Sender
+    diag_device_sender: Sender,
+    maybe_analysis_tx: Option>
 ) -> JoinHandle<()> {
     let state = Arc::new(ServerState {
         qmdl_store_lock,
         diag_device_ctrl_sender: diag_device_sender,
         readonly_mode: config.readonly_mode,
+        maybe_analysis_tx,
     });
 
     let app = Router::new()
@@ -52,6 +56,7 @@ async fn run_server(
         .route("/api/qmdl-manifest", get(get_qmdl_manifest))
         .route("/api/start-recording", post(start_recording))
         .route("/api/stop-recording", post(stop_recording))
+        .route("/api/analysis-report", get(get_analysis_report))
         .route("/", get(|| async { Redirect::permanent("/index.html") }))
         .route("/*path", get(serve_static))
         .with_state(state);
@@ -87,7 +92,8 @@ fn run_ctrl_c_thread(
     task_tracker: &TaskTracker,
     diag_device_sender: Sender,
     server_shutdown_tx: oneshot::Sender<()>,
-    qmdl_store_lock: Arc>
+    qmdl_store_lock: Arc>,
+    maybe_analysis_tx: Option>
 ) -> JoinHandle> {
     task_tracker.spawn(async move {
         match tokio::signal::ctrl_c().await {
@@ -103,6 +109,10 @@ fn run_ctrl_c_thread(
                     .expect("couldn't send server shutdown signal");
                 diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
                     .expect("couldn't send Exit message to diag thread");
+                if let Some(analysis_tx) = maybe_analysis_tx {
+                    analysis_tx.send(AnalysisMessage::StopThread).await
+                        .expect("couldn't send Exit message to analysis thread")
+                }
             },
             Err(err) => {
                 error!("Unable to listen for shutdown signal: {}", err);
@@ -125,18 +135,21 @@ async fn main() -> Result<(), RayhunterError> {
 
     let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?));
     let (tx, rx) = mpsc::channel::(1);
+    let mut maybe_analysis_tx = None;
     if !config.readonly_mode {
         let mut dev = DiagDevice::new().await
             .map_err(RayhunterError::DiagInitError)?;
         dev.config_logs().await
             .map_err(RayhunterError::DiagInitError)?;
 
-        run_diag_read_thread(&task_tracker, dev, rx, qmdl_store_lock.clone());
+        let analysis_tx = run_analysis_thread(&task_tracker);
+        run_diag_read_thread(&task_tracker, dev, rx, qmdl_store_lock.clone(), analysis_tx.clone());
+        maybe_analysis_tx = Some(analysis_tx);
     }
 
     let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
-    run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, qmdl_store_lock.clone());
-    run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, tx).await;
+    run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, qmdl_store_lock.clone(), maybe_analysis_tx.clone());
+    run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, tx, maybe_analysis_tx).await;
 
     task_tracker.close();
     task_tracker.wait().await;
diff --git a/bin/src/diag.rs b/bin/src/diag.rs
index 440f8be..e9746eb 100644
--- a/bin/src/diag.rs
+++ b/bin/src/diag.rs
@@ -6,13 +6,14 @@ use axum::http::StatusCode;
 use rayhunter::diag::DataType;
 use rayhunter::diag_device::DiagDevice;
 use tokio::sync::RwLock;
-use tokio::sync::mpsc::Receiver;
+use tokio::sync::mpsc::{Receiver, Sender};
 use rayhunter::qmdl::QmdlWriter;
 use log::{debug, error, info};
 use tokio::fs::File;
 use tokio_util::task::TaskTracker;
 use futures::{StreamExt, TryStreamExt};
 
+use crate::analysis::AnalysisMessage;
 use crate::qmdl_store::QmdlStore;
 use crate::server::ServerState;
 
@@ -22,7 +23,13 @@ pub enum DiagDeviceCtrlMessage {
     Exit,
 }
 
-pub fn run_diag_read_thread(task_tracker: &TaskTracker, mut dev: DiagDevice, mut qmdl_file_rx: Receiver, qmdl_store_lock: Arc>) {
+pub fn run_diag_read_thread(
+    task_tracker: &TaskTracker,
+    mut dev: DiagDevice,
+    mut qmdl_file_rx: Receiver,
+    qmdl_store_lock: Arc>,
+    analysis_tx: Sender
+) {
     task_tracker.spawn(async move {
         let initial_file = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
         let mut qmdl_writer: Option> = Some(QmdlWriter::new(initial_file));
@@ -33,8 +40,14 @@ pub fn run_diag_read_thread(task_tracker: &TaskTracker, mut dev: DiagDevice, mut
                     match msg {
                         Some(DiagDeviceCtrlMessage::StartRecording(new_writer)) => {
                             qmdl_writer = Some(new_writer);
+                            analysis_tx.send(AnalysisMessage::Reset).await
+                                .expect("failed to send message to analysis thread");
+                        },
+                        Some(DiagDeviceCtrlMessage::StopRecording) => {
+                            qmdl_writer = None;
+                            analysis_tx.send(AnalysisMessage::Reset).await
+                                .expect("failed to send message to analysis thread");
                         },
-                        Some(DiagDeviceCtrlMessage::StopRecording) => qmdl_writer = None,
                         // None means all the Senders have been dropped, so it's
                         // time to go
                         Some(DiagDeviceCtrlMessage::Exit) | None => {
@@ -59,6 +72,9 @@ pub fn run_diag_read_thread(task_tracker: &TaskTracker, mut dev: DiagDevice, mut
                                 let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
                                 qmdl_store.update_entry(index, writer.total_written).await
                                     .expect("failed to update qmdl file size");
+                                debug!("sending container to analysis thread...");
+                                analysis_tx.send(AnalysisMessage::AnalyzeContainer(container)).await
+                                    .expect("failed sending messages container to analysis thread");
                                 debug!("done!");
                             } else {
                                 debug!("no qmdl_writer set, continuing...");
diff --git a/bin/src/pcap.rs b/bin/src/pcap.rs
index 95f655a..78a5802 100644
--- a/bin/src/pcap.rs
+++ b/bin/src/pcap.rs
@@ -1,7 +1,7 @@
 use crate::ServerState;
 
 use rayhunter::diag::DataType;
-use rayhunter::gsmtap_parser::GsmtapParser;
+use rayhunter::gsmtap_parser;
 use rayhunter::pcap::GsmtapPcapWriter;
 use rayhunter::qmdl::QmdlReader;
 use axum::body::Body;
@@ -34,7 +34,6 @@ pub async fn get_pcap(State(state): State>, Path(qmdl_name): Pa
         .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
     // the QMDL reader should stop at the last successfully written data chunk
     // (entry.size_bytes)
-    let mut gsmtap_parser = GsmtapParser::new();
     let (reader, writer) = duplex(1024);
     let mut pcap_writer = GsmtapPcapWriter::new(writer).await.unwrap();
     pcap_writer.write_iface_header().await.unwrap();
@@ -48,7 +47,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.parse(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/bin/src/server.rs b/bin/src/server.rs
index 783130d..290bd4b 100644
--- a/bin/src/server.rs
+++ b/bin/src/server.rs
@@ -11,6 +11,7 @@ use tokio::sync::RwLock;
 use tokio_util::io::ReaderStream;
 use include_dir::{include_dir, Dir};
 
+use crate::analysis::AnalysisMessage;
 use crate::DiagDeviceCtrlMessage;
 use crate::qmdl_store::QmdlStore;
 
@@ -18,6 +19,7 @@ pub struct ServerState {
     pub qmdl_store_lock: Arc>,
     pub diag_device_ctrl_sender: Sender,
     pub readonly_mode: bool,
+    pub maybe_analysis_tx: Option>,
 }
 
 pub async fn get_qmdl(State(state): State>, Path(qmdl_name): Path) -> Result {
diff --git a/bin/static/index.html b/bin/static/index.html
index 4b1e308..878f79d 100644
--- a/bin/static/index.html
+++ b/bin/static/index.html
@@ -34,5 +34,9 @@
         

System stats

Loading...
+
+

Analysis Report

+
Loading...
+
diff --git a/bin/static/js/main.js b/bin/static/js/main.js index a6e1323..6cfa738 100644 --- a/bin/static/js/main.js +++ b/bin/static/js/main.js @@ -3,6 +3,10 @@ async function populateDivs() { const systemStatsDiv = document.getElementById('system-stats'); systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2); + const analysisReport = await getAnalysisReport(); + const analysisReportDiv = document.getElementById('analysis-report'); + analysisReportDiv.innerHTML = JSON.stringify(analysisReport, null, 2); + const qmdlManifest = await getQmdlManifest(); updateQmdlManifestTable(qmdlManifest); } @@ -49,6 +53,10 @@ function createEntryRow(entry) { return row; } +async function getAnalysisReport() { + return JSON.parse(await req('GET', '/api/analysis-report')); +} + async function getSystemStats() { return JSON.parse(await req('GET', '/api/system-stats')); } diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index f80fdd0..5501de9 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -1,7 +1,10 @@ use std::borrow::Cow; +use chrono::{DateTime, FixedOffset}; use serde::Serialize; -use super::information_element::InformationElement; +use crate::{diag::MessagesContainer, gsmtap_parser}; + +use super::{information_element::InformationElement, lte_downgrade::LteSib6And7DowngradeAnalyzer}; /// Qualitative measure of how severe a Warning event type is. /// The levels should break down like this: @@ -55,22 +58,117 @@ pub trait Analyzer { fn analyze_information_element(&mut self, ie: &InformationElement) -> Option; } +#[derive(Serialize, Debug)] +pub struct AnalyzerMetadata { + name: String, + description: String, +} + +#[derive(Serialize, Debug)] +pub struct ReportMetadata { + num_packets_analyzed: usize, + num_packets_skipped: usize, + num_warnings: usize, + first_packet_time: Option>, + last_packet_time: Option>, + analyzers: Vec, +} + +#[derive(Serialize, Debug, Clone)] +pub struct PacketAnalysis { + timestamp: DateTime, + events: Vec>, +} + +#[derive(Serialize, Debug)] +pub struct AnalysisReport { + metadata: ReportMetadata, + analysis: Vec, +} + pub struct Harness { - analyzers: Vec>, + analyzers: Vec>, + pub num_packets_analyzed: usize, + pub num_warnings: usize, + pub skipped_message_reasons: Vec, + pub first_packet_time: Option>, + pub last_packet_time: Option>, + pub analysis: Vec, } impl Harness { pub fn new() -> Self { Self { analyzers: Vec::new(), + num_packets_analyzed: 0, + skipped_message_reasons: Vec::new(), + num_warnings: 0, + first_packet_time: None, + last_packet_time: None, + analysis: Vec::new(), } } - pub fn add_analyzer(&mut self, analyzer: Box) { + pub fn new_with_all_analyzers() -> Self { + let mut harness = Harness::new(); + harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{})); + harness + } + + pub fn add_analyzer(&mut self, analyzer: Box) { self.analyzers.push(analyzer); } - pub fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec> { + pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) { + for maybe_qmdl_message in container.into_messages() { + let qmdl_message = match maybe_qmdl_message { + Ok(msg) => msg, + Err(err) => { + self.skipped_message_reasons.push(format!("{:?}", err)); + continue; + } + }; + + let gsmtap_message = match gsmtap_parser::parse(qmdl_message) { + Ok(msg) => msg, + Err(err) => { + self.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) => { + self.skipped_message_reasons.push(format!("{:?}", err)); + continue; + } + }; + + if self.first_packet_time.is_none() { + self.first_packet_time = Some(timestamp.to_datetime()); + } + + self.last_packet_time = Some(timestamp.to_datetime()); + self.num_packets_analyzed += 1; + let analysis_result = self.analyze_information_element(&element); + if analysis_result.iter().any(Option::is_some) { + self.num_warnings += analysis_result.iter() + .filter(|maybe_event| matches!(maybe_event, Some(Event { event_type: EventType::QualitativeWarning { .. }, .. }))) + .count(); + self.analysis.push(PacketAnalysis { + timestamp: timestamp.to_datetime(), + events: analysis_result, + }); + } + } + } + + fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec> { self.analyzers.iter_mut() .map(|analyzer| analyzer.analyze_information_element(ie)) .collect() @@ -87,4 +185,28 @@ impl Harness { .map(|analyzer| analyzer.get_description()) .collect() } + + pub fn build_analysis_report(&self) -> AnalysisReport { + let names = self.get_names(); + let descriptions = self.get_names(); + let mut analyzers = Vec::new(); + for (name, description) in names.iter().zip(descriptions.iter()) { + analyzers.push(AnalyzerMetadata { + name: name.to_string(), + description: description.to_string(), + }); + } + + AnalysisReport { + metadata: ReportMetadata { + num_packets_analyzed: self.num_packets_analyzed, + num_packets_skipped: self.skipped_message_reasons.len(), + num_warnings: self.num_warnings, + first_packet_time: self.first_packet_time, + last_packet_time: self.last_packet_time, + analyzers, + }, + analysis: self.analysis.clone(), + } + } } diff --git a/lib/src/gsmtap_parser.rs b/lib/src/gsmtap_parser.rs index eacfc61..3460e9c 100644 --- a/lib/src/gsmtap_parser.rs +++ b/lib/src/gsmtap_parser.rs @@ -4,15 +4,6 @@ use crate::gsmtap::*; use log::error; use thiserror::Error; -pub struct GsmtapParser { -} - -impl Default for GsmtapParser { - fn default() -> Self { - GsmtapParser::new() - } -} - #[derive(Debug, Error)] pub enum GsmtapParserError { #[error("Invalid LteRrcOtaMessage ext header version {0}")] @@ -21,119 +12,113 @@ pub enum GsmtapParserError { InvalidLteRrcOtaHeaderPduNum(u8, u8), } -impl GsmtapParser { - pub fn new() -> Self { - GsmtapParser {} - } - - 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))), - None => Ok(None), - } - } else { - Ok(None) - } - } - - fn log_to_gsmtap(&self, value: LogBody) -> Result, GsmtapParserError> { - match value { - LogBody::LteRrcOtaMessage { ext_header_version, packet } => { - let gsmtap_type = match ext_header_version { - 0x02 | 0x03 | 0x04 | 0x06 | 0x07 | 0x08 | 0x0d | 0x16 => match packet.get_pdu_num() { - 1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), - 2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), - 3 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), - 4 => GsmtapType::LteRrc(LteRrcSubtype::PCCH), - 5 => GsmtapType::LteRrc(LteRrcSubtype::DlCcch), - 6 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch), - 7 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch), - 8 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), - pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), - }, - 0x09 | 0x0c => match packet.get_pdu_num() { - 8 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), - 9 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), - 10 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), - 11 => GsmtapType::LteRrc(LteRrcSubtype::PCCH), - 12 => GsmtapType::LteRrc(LteRrcSubtype::DlCcch), - 13 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch), - 14 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch), - 15 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), - pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), - }, - 0x0e..=0x10 => match packet.get_pdu_num() { - 1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), - 2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), - 4 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), - 5 => GsmtapType::LteRrc(LteRrcSubtype::PCCH), - 6 => GsmtapType::LteRrc(LteRrcSubtype::DlCcch), - 7 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch), - 8 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch), - 9 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), - pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), - }, - 0x13 | 0x1a | 0x1b => match packet.get_pdu_num() { - 1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), - 3 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), - 6 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), - 7 => GsmtapType::LteRrc(LteRrcSubtype::PCCH), - 8 => GsmtapType::LteRrc(LteRrcSubtype::DlCcch), - 9 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch), - 10 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch), - 11 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), - 45 => GsmtapType::LteRrc(LteRrcSubtype::BcchBchNb), - 46 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSchNb), - 47 => GsmtapType::LteRrc(LteRrcSubtype::PcchNb), - 48 => GsmtapType::LteRrc(LteRrcSubtype::DlCcchNb), - 49 => GsmtapType::LteRrc(LteRrcSubtype::DlDcchNb), - 50 => GsmtapType::LteRrc(LteRrcSubtype::UlCcchNb), - 52 => GsmtapType::LteRrc(LteRrcSubtype::UlDcchNb), - pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), - } - 0x14 | 0x18 | 0x19 => match packet.get_pdu_num() { - 1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), - 2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), - 4 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), - 5 => GsmtapType::LteRrc(LteRrcSubtype::PCCH), - 6 => GsmtapType::LteRrc(LteRrcSubtype::DlCcch), - 7 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch), - 8 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch), - 9 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), - 54 => GsmtapType::LteRrc(LteRrcSubtype::BcchBchNb), - 55 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSchNb), - 56 => GsmtapType::LteRrc(LteRrcSubtype::PcchNb), - 57 => GsmtapType::LteRrc(LteRrcSubtype::DlCcchNb), - 58 => GsmtapType::LteRrc(LteRrcSubtype::DlDcchNb), - 59 => GsmtapType::LteRrc(LteRrcSubtype::UlCcchNb), - 61 => GsmtapType::LteRrc(LteRrcSubtype::UlDcchNb), - pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), - }, - _ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)), - }; - let mut header = GsmtapHeader::new(gsmtap_type); - // Wireshark GSMTAP only accepts 14 bits of ARFCN - header.arfcn = packet.get_earfcn().try_into().unwrap_or(0); - header.frame_number = packet.get_sfn(); - header.subslot = packet.get_subfn(); - Ok(Some(GsmtapMessage { - header, - payload: packet.take_payload(), - })) - }, - LogBody::Nas4GMessage { msg, .. } => { - // currently we only handle "plain" (i.e. non-secure) NAS messages - let header = GsmtapHeader::new(GsmtapType::LteNas(LteNasSubtype::Plain)); - Ok(Some(GsmtapMessage { - header, - payload: msg, - })) - }, - _ => { - error!("gsmtap_sink: ignoring unhandled log type: {:?}", value); - Ok(None) - }, +pub fn parse(msg: Message) -> Result, GsmtapParserError> { + if let Message::Log { timestamp, body, .. } = msg { + match log_to_gsmtap(body)? { + Some(msg) => Ok(Some((timestamp, msg))), + None => Ok(None), } + } else { + Ok(None) + } +} + +fn log_to_gsmtap(value: LogBody) -> Result, GsmtapParserError> { + match value { + LogBody::LteRrcOtaMessage { ext_header_version, packet } => { + let gsmtap_type = match ext_header_version { + 0x02 | 0x03 | 0x04 | 0x06 | 0x07 | 0x08 | 0x0d | 0x16 => match packet.get_pdu_num() { + 1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), + 2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), + 3 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), + 4 => GsmtapType::LteRrc(LteRrcSubtype::PCCH), + 5 => GsmtapType::LteRrc(LteRrcSubtype::DlCcch), + 6 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch), + 7 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch), + 8 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), + pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), + }, + 0x09 | 0x0c => match packet.get_pdu_num() { + 8 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), + 9 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), + 10 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), + 11 => GsmtapType::LteRrc(LteRrcSubtype::PCCH), + 12 => GsmtapType::LteRrc(LteRrcSubtype::DlCcch), + 13 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch), + 14 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch), + 15 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), + pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), + }, + 0x0e..=0x10 => match packet.get_pdu_num() { + 1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), + 2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), + 4 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), + 5 => GsmtapType::LteRrc(LteRrcSubtype::PCCH), + 6 => GsmtapType::LteRrc(LteRrcSubtype::DlCcch), + 7 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch), + 8 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch), + 9 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), + pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), + }, + 0x13 | 0x1a | 0x1b => match packet.get_pdu_num() { + 1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), + 3 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), + 6 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), + 7 => GsmtapType::LteRrc(LteRrcSubtype::PCCH), + 8 => GsmtapType::LteRrc(LteRrcSubtype::DlCcch), + 9 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch), + 10 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch), + 11 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), + 45 => GsmtapType::LteRrc(LteRrcSubtype::BcchBchNb), + 46 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSchNb), + 47 => GsmtapType::LteRrc(LteRrcSubtype::PcchNb), + 48 => GsmtapType::LteRrc(LteRrcSubtype::DlCcchNb), + 49 => GsmtapType::LteRrc(LteRrcSubtype::DlDcchNb), + 50 => GsmtapType::LteRrc(LteRrcSubtype::UlCcchNb), + 52 => GsmtapType::LteRrc(LteRrcSubtype::UlDcchNb), + pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), + } + 0x14 | 0x18 | 0x19 => match packet.get_pdu_num() { + 1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), + 2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), + 4 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), + 5 => GsmtapType::LteRrc(LteRrcSubtype::PCCH), + 6 => GsmtapType::LteRrc(LteRrcSubtype::DlCcch), + 7 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch), + 8 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch), + 9 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), + 54 => GsmtapType::LteRrc(LteRrcSubtype::BcchBchNb), + 55 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSchNb), + 56 => GsmtapType::LteRrc(LteRrcSubtype::PcchNb), + 57 => GsmtapType::LteRrc(LteRrcSubtype::DlCcchNb), + 58 => GsmtapType::LteRrc(LteRrcSubtype::DlDcchNb), + 59 => GsmtapType::LteRrc(LteRrcSubtype::UlCcchNb), + 61 => GsmtapType::LteRrc(LteRrcSubtype::UlDcchNb), + pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), + }, + _ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)), + }; + let mut header = GsmtapHeader::new(gsmtap_type); + // Wireshark GSMTAP only accepts 14 bits of ARFCN + header.arfcn = packet.get_earfcn().try_into().unwrap_or(0); + header.frame_number = packet.get_sfn(); + header.subslot = packet.get_subfn(); + Ok(Some(GsmtapMessage { + header, + payload: packet.take_payload(), + })) + }, + LogBody::Nas4GMessage { msg, .. } => { + // currently we only handle "plain" (i.e. non-secure) NAS messages + let header = GsmtapHeader::new(GsmtapType::LteNas(LteNasSubtype::Plain)); + Ok(Some(GsmtapMessage { + header, + payload: msg, + })) + }, + _ => { + error!("gsmtap_sink: ignoring unhandled log type: {:?}", value); + Ok(None) + }, } } diff --git a/lib/tests/test_lte_parsing.rs b/lib/tests/test_lte_parsing.rs index 187807d..afc3829 100644 --- a/lib/tests/test_lte_parsing.rs +++ b/lib/tests/test_lte_parsing.rs @@ -1,17 +1,12 @@ -use rayhunter::diag::{ - Message, - LogBody, - LteRrcOtaPacket, - Timestamp, -}; -use rayhunter::gsmtap_parser::GsmtapParser; +use rayhunter::{diag::{ + LogBody, LteRrcOtaPacket, Message, Timestamp +}, gsmtap_parser}; use deku::prelude::*; -// Tests here are based on https://github.com/fgsect/scat/blob/97442580e628de414c9f7c2a185f4e28d0ee7523/tests/test_diagltelogparser.py +// Tests here are based on https://github.com/fgsect/scat/blob/97442580e628de414c9f7c2a185f4e28d0ee7523/tests/test_diaglteloggsmtap_parser::py #[test] fn test_lte_rrc_ota() { - let mut parser = GsmtapParser::new(); let v26_binary = &[ 0x10, 0x0, 0x23, 0x0, 0x23, 0x0, 0xc0, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0xf, 0x40, 0xf, 0x40, 0x1, 0xe, 0x1, 0x13, 0x7, @@ -42,7 +37,7 @@ fn test_lte_rrc_ota() { } } }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_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 +80,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x10, 0x15, ]); @@ -132,7 +127,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_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 +178,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_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 +224,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x28, 0x18, 0x40, 0x16, 0x08, 0x08, 0x80, 0x00, 0x00, @@ -274,7 +269,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x40, 0x0c, 0x8e, 0xc9, 0x42, 0x89, 0xe0, ]); @@ -324,7 +319,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_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 +365,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_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 +407,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x40, 0x0b, 0x8e, 0xc1, 0xdd, 0x13, 0xb0, ]); @@ -455,7 +450,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_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 +496,7 @@ fn test_lte_rrc_ota() { }, }, }); - let (_, gsmtap_msg) = parser.parse(parsed).unwrap().unwrap(); + let (_, gsmtap_msg) = gsmtap_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,