mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-07-27 09:38:09 -07:00
daemon: run analysis in realtime
Currently we just show the results of analysis as a <pre> tagged JSON blob, but eventually we can make some actual UI
This commit is contained in:
@@ -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<AnalysisReport>),
|
||||||
|
AnalyzeContainer(MessagesContainer),
|
||||||
|
StopThread,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_analysis_thread(task_tracker: &TaskTracker) -> sync::mpsc::Sender<AnalysisMessage> {
|
||||||
|
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<Arc<ServerState>>) -> Result<Json<AnalysisReport>, (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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-104
@@ -1,8 +1,6 @@
|
|||||||
use std::{future, path::PathBuf, pin::pin};
|
use std::{future, path::PathBuf, pin::pin};
|
||||||
use chrono::{DateTime, FixedOffset};
|
use rayhunter::{analysis::analyzer::Harness, diag::DataType, qmdl::QmdlReader};
|
||||||
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 tokio::fs::File;
|
||||||
use serde::Serialize;
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
|
||||||
@@ -13,120 +11,22 @@ struct Args {
|
|||||||
qmdl_path: PathBuf,
|
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<FixedOffset>,
|
|
||||||
last_packet_time: DateTime<FixedOffset>,
|
|
||||||
analyzers: Vec<AnalyzerMetadata>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
struct PacketAnalysis {
|
|
||||||
timestamp: DateTime<FixedOffset>,
|
|
||||||
events: Vec<Option<Event>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
struct AnalysisReport {
|
|
||||||
metadata: ReportMetadata,
|
|
||||||
analysis: Vec<PacketAnalysis>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
let mut harness = Harness::new();
|
let mut harness = Harness::new_with_all_analyzers();
|
||||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
|
|
||||||
|
|
||||||
let mut num_packets_analyzed = 0;
|
|
||||||
let mut num_warnings = 0;
|
|
||||||
let mut first_packet_time: Option<DateTime<FixedOffset>> = None;
|
|
||||||
let mut last_packet_time: Option<DateTime<FixedOffset>> = None;
|
|
||||||
let mut skipped_message_reasons: Vec<String> = Vec::new();
|
|
||||||
let mut analysis: Vec<PacketAnalysis> = Vec::new();
|
|
||||||
let mut analyzers: Vec<AnalyzerMetadata> = 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 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 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_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||||
let mut qmdl_stream = pin!(qmdl_reader.as_stream()
|
let mut qmdl_stream = pin!(qmdl_reader.as_stream()
|
||||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||||
while let Some(container) = qmdl_stream.try_next().await.expect("failed getting QMDL container") {
|
while let Some(container) = qmdl_stream.try_next().await.expect("failed getting QMDL container") {
|
||||||
for maybe_qmdl_message in container.into_messages() {
|
harness.analyze_qmdl_messages(container)
|
||||||
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 {
|
let report = harness.build_analysis_report();
|
||||||
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"));
|
println!("{}", serde_json::to_string(&report).expect("failed to serialize report"));
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-5
@@ -1,3 +1,4 @@
|
|||||||
|
mod analysis;
|
||||||
mod config;
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod pcap;
|
mod pcap;
|
||||||
@@ -14,6 +15,7 @@ use crate::pcap::get_pcap;
|
|||||||
use crate::stats::get_system_stats;
|
use crate::stats::get_system_stats;
|
||||||
use crate::error::RayhunterError;
|
use crate::error::RayhunterError;
|
||||||
|
|
||||||
|
use analysis::{get_analysis_report, run_analysis_thread, AnalysisMessage};
|
||||||
use axum::response::Redirect;
|
use axum::response::Redirect;
|
||||||
use diag::{DiagDeviceCtrlMessage, start_recording, stop_recording};
|
use diag::{DiagDeviceCtrlMessage, start_recording, stop_recording};
|
||||||
use log::{info, error};
|
use log::{info, error};
|
||||||
@@ -37,12 +39,14 @@ async fn run_server(
|
|||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
qmdl_store_lock: Arc<RwLock<QmdlStore>>,
|
qmdl_store_lock: Arc<RwLock<QmdlStore>>,
|
||||||
server_shutdown_rx: oneshot::Receiver<()>,
|
server_shutdown_rx: oneshot::Receiver<()>,
|
||||||
diag_device_sender: Sender<DiagDeviceCtrlMessage>
|
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
|
maybe_analysis_tx: Option<Sender<AnalysisMessage>>
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
let state = Arc::new(ServerState {
|
let state = Arc::new(ServerState {
|
||||||
qmdl_store_lock,
|
qmdl_store_lock,
|
||||||
diag_device_ctrl_sender: diag_device_sender,
|
diag_device_ctrl_sender: diag_device_sender,
|
||||||
readonly_mode: config.readonly_mode,
|
readonly_mode: config.readonly_mode,
|
||||||
|
maybe_analysis_tx,
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -52,6 +56,7 @@ async fn run_server(
|
|||||||
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
|
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
|
||||||
.route("/api/start-recording", post(start_recording))
|
.route("/api/start-recording", post(start_recording))
|
||||||
.route("/api/stop-recording", post(stop_recording))
|
.route("/api/stop-recording", post(stop_recording))
|
||||||
|
.route("/api/analysis-report", get(get_analysis_report))
|
||||||
.route("/", get(|| async { Redirect::permanent("/index.html") }))
|
.route("/", get(|| async { Redirect::permanent("/index.html") }))
|
||||||
.route("/*path", get(serve_static))
|
.route("/*path", get(serve_static))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
@@ -87,7 +92,8 @@ fn run_ctrl_c_thread(
|
|||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
server_shutdown_tx: oneshot::Sender<()>,
|
server_shutdown_tx: oneshot::Sender<()>,
|
||||||
qmdl_store_lock: Arc<RwLock<QmdlStore>>
|
qmdl_store_lock: Arc<RwLock<QmdlStore>>,
|
||||||
|
maybe_analysis_tx: Option<Sender<AnalysisMessage>>
|
||||||
) -> JoinHandle<Result<(), RayhunterError>> {
|
) -> JoinHandle<Result<(), RayhunterError>> {
|
||||||
task_tracker.spawn(async move {
|
task_tracker.spawn(async move {
|
||||||
match tokio::signal::ctrl_c().await {
|
match tokio::signal::ctrl_c().await {
|
||||||
@@ -103,6 +109,10 @@ fn run_ctrl_c_thread(
|
|||||||
.expect("couldn't send server shutdown signal");
|
.expect("couldn't send server shutdown signal");
|
||||||
diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
|
diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
|
||||||
.expect("couldn't send Exit message to diag thread");
|
.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) => {
|
Err(err) => {
|
||||||
error!("Unable to listen for shutdown signal: {}", 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 qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?));
|
||||||
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
||||||
|
let mut maybe_analysis_tx = None;
|
||||||
if !config.readonly_mode {
|
if !config.readonly_mode {
|
||||||
let mut dev = DiagDevice::new().await
|
let mut dev = DiagDevice::new().await
|
||||||
.map_err(RayhunterError::DiagInitError)?;
|
.map_err(RayhunterError::DiagInitError)?;
|
||||||
dev.config_logs().await
|
dev.config_logs().await
|
||||||
.map_err(RayhunterError::DiagInitError)?;
|
.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::<()>();
|
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_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).await;
|
run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, tx, maybe_analysis_tx).await;
|
||||||
|
|
||||||
task_tracker.close();
|
task_tracker.close();
|
||||||
task_tracker.wait().await;
|
task_tracker.wait().await;
|
||||||
|
|||||||
+19
-3
@@ -6,13 +6,14 @@ use axum::http::StatusCode;
|
|||||||
use rayhunter::diag::DataType;
|
use rayhunter::diag::DataType;
|
||||||
use rayhunter::diag_device::DiagDevice;
|
use rayhunter::diag_device::DiagDevice;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use rayhunter::qmdl::QmdlWriter;
|
use rayhunter::qmdl::QmdlWriter;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
use futures::{StreamExt, TryStreamExt};
|
use futures::{StreamExt, TryStreamExt};
|
||||||
|
|
||||||
|
use crate::analysis::AnalysisMessage;
|
||||||
use crate::qmdl_store::QmdlStore;
|
use crate::qmdl_store::QmdlStore;
|
||||||
use crate::server::ServerState;
|
use crate::server::ServerState;
|
||||||
|
|
||||||
@@ -22,7 +23,13 @@ pub enum DiagDeviceCtrlMessage {
|
|||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_diag_read_thread(task_tracker: &TaskTracker, mut dev: DiagDevice, mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>, qmdl_store_lock: Arc<RwLock<QmdlStore>>) {
|
pub fn run_diag_read_thread(
|
||||||
|
task_tracker: &TaskTracker,
|
||||||
|
mut dev: DiagDevice,
|
||||||
|
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
||||||
|
qmdl_store_lock: Arc<RwLock<QmdlStore>>,
|
||||||
|
analysis_tx: Sender<AnalysisMessage>
|
||||||
|
) {
|
||||||
task_tracker.spawn(async move {
|
task_tracker.spawn(async move {
|
||||||
let initial_file = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
|
let initial_file = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
|
||||||
let mut qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_file));
|
let mut qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_file));
|
||||||
@@ -33,8 +40,14 @@ pub fn run_diag_read_thread(task_tracker: &TaskTracker, mut dev: DiagDevice, mut
|
|||||||
match msg {
|
match msg {
|
||||||
Some(DiagDeviceCtrlMessage::StartRecording(new_writer)) => {
|
Some(DiagDeviceCtrlMessage::StartRecording(new_writer)) => {
|
||||||
qmdl_writer = Some(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
|
// None means all the Senders have been dropped, so it's
|
||||||
// time to go
|
// time to go
|
||||||
Some(DiagDeviceCtrlMessage::Exit) | None => {
|
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???");
|
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
|
qmdl_store.update_entry(index, writer.total_written).await
|
||||||
.expect("failed to update qmdl file size");
|
.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!");
|
debug!("done!");
|
||||||
} else {
|
} else {
|
||||||
debug!("no qmdl_writer set, continuing...");
|
debug!("no qmdl_writer set, continuing...");
|
||||||
|
|||||||
+2
-3
@@ -1,7 +1,7 @@
|
|||||||
use crate::ServerState;
|
use crate::ServerState;
|
||||||
|
|
||||||
use rayhunter::diag::DataType;
|
use rayhunter::diag::DataType;
|
||||||
use rayhunter::gsmtap_parser::GsmtapParser;
|
use rayhunter::gsmtap_parser;
|
||||||
use rayhunter::pcap::GsmtapPcapWriter;
|
use rayhunter::pcap::GsmtapPcapWriter;
|
||||||
use rayhunter::qmdl::QmdlReader;
|
use rayhunter::qmdl::QmdlReader;
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
@@ -34,7 +34,6 @@ pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
|
|||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
||||||
// the QMDL reader should stop at the last successfully written data chunk
|
// the QMDL reader should stop at the last successfully written data chunk
|
||||||
// (entry.size_bytes)
|
// (entry.size_bytes)
|
||||||
let mut gsmtap_parser = GsmtapParser::new();
|
|
||||||
let (reader, writer) = duplex(1024);
|
let (reader, writer) = duplex(1024);
|
||||||
let mut pcap_writer = GsmtapPcapWriter::new(writer).await.unwrap();
|
let mut pcap_writer = GsmtapPcapWriter::new(writer).await.unwrap();
|
||||||
pcap_writer.write_iface_header().await.unwrap();
|
pcap_writer.write_iface_header().await.unwrap();
|
||||||
@@ -48,7 +47,7 @@ pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
|
|||||||
for maybe_msg in container.into_messages() {
|
for maybe_msg in container.into_messages() {
|
||||||
match maybe_msg {
|
match maybe_msg {
|
||||||
Ok(msg) => {
|
Ok(msg) => {
|
||||||
let maybe_gsmtap_msg = gsmtap_parser.parse(msg)
|
let maybe_gsmtap_msg = gsmtap_parser::parse(msg)
|
||||||
.expect("error parsing gsmtap message");
|
.expect("error parsing gsmtap message");
|
||||||
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
|
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
|
||||||
pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp).await
|
pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp).await
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use tokio::sync::RwLock;
|
|||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use include_dir::{include_dir, Dir};
|
use include_dir::{include_dir, Dir};
|
||||||
|
|
||||||
|
use crate::analysis::AnalysisMessage;
|
||||||
use crate::DiagDeviceCtrlMessage;
|
use crate::DiagDeviceCtrlMessage;
|
||||||
use crate::qmdl_store::QmdlStore;
|
use crate::qmdl_store::QmdlStore;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ pub struct ServerState {
|
|||||||
pub qmdl_store_lock: Arc<RwLock<QmdlStore>>,
|
pub qmdl_store_lock: Arc<RwLock<QmdlStore>>,
|
||||||
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
pub readonly_mode: bool,
|
pub readonly_mode: bool,
|
||||||
|
pub maybe_analysis_tx: Option<Sender<AnalysisMessage>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
|
pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
|
||||||
|
|||||||
@@ -34,5 +34,9 @@
|
|||||||
<h3>System stats</h3>
|
<h3>System stats</h3>
|
||||||
<pre id="system-stats">Loading...</pre>
|
<pre id="system-stats">Loading...</pre>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Analysis Report</h3>
|
||||||
|
<pre id="analysis-report">Loading...</pre>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ async function populateDivs() {
|
|||||||
const systemStatsDiv = document.getElementById('system-stats');
|
const systemStatsDiv = document.getElementById('system-stats');
|
||||||
systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2);
|
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();
|
const qmdlManifest = await getQmdlManifest();
|
||||||
updateQmdlManifestTable(qmdlManifest);
|
updateQmdlManifestTable(qmdlManifest);
|
||||||
}
|
}
|
||||||
@@ -49,6 +53,10 @@ function createEntryRow(entry) {
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAnalysisReport() {
|
||||||
|
return JSON.parse(await req('GET', '/api/analysis-report'));
|
||||||
|
}
|
||||||
|
|
||||||
async function getSystemStats() {
|
async function getSystemStats() {
|
||||||
return JSON.parse(await req('GET', '/api/system-stats'));
|
return JSON.parse(await req('GET', '/api/system-stats'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use chrono::{DateTime, FixedOffset};
|
||||||
use serde::Serialize;
|
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.
|
/// Qualitative measure of how severe a Warning event type is.
|
||||||
/// The levels should break down like this:
|
/// The levels should break down like this:
|
||||||
@@ -55,22 +58,117 @@ pub trait Analyzer {
|
|||||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event>;
|
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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<DateTime<FixedOffset>>,
|
||||||
|
last_packet_time: Option<DateTime<FixedOffset>>,
|
||||||
|
analyzers: Vec<AnalyzerMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
pub struct PacketAnalysis {
|
||||||
|
timestamp: DateTime<FixedOffset>,
|
||||||
|
events: Vec<Option<Event>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
pub struct AnalysisReport {
|
||||||
|
metadata: ReportMetadata,
|
||||||
|
analysis: Vec<PacketAnalysis>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Harness {
|
pub struct Harness {
|
||||||
analyzers: Vec<Box<dyn Analyzer>>,
|
analyzers: Vec<Box<dyn Analyzer + Send>>,
|
||||||
|
pub num_packets_analyzed: usize,
|
||||||
|
pub num_warnings: usize,
|
||||||
|
pub skipped_message_reasons: Vec<String>,
|
||||||
|
pub first_packet_time: Option<DateTime<FixedOffset>>,
|
||||||
|
pub last_packet_time: Option<DateTime<FixedOffset>>,
|
||||||
|
pub analysis: Vec<PacketAnalysis>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Harness {
|
impl Harness {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
analyzers: Vec::new(),
|
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<dyn Analyzer>) {
|
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<dyn Analyzer + Send>) {
|
||||||
self.analyzers.push(analyzer);
|
self.analyzers.push(analyzer);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
|
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<Option<Event>> {
|
||||||
self.analyzers.iter_mut()
|
self.analyzers.iter_mut()
|
||||||
.map(|analyzer| analyzer.analyze_information_element(ie))
|
.map(|analyzer| analyzer.analyze_information_element(ie))
|
||||||
.collect()
|
.collect()
|
||||||
@@ -87,4 +185,28 @@ impl Harness {
|
|||||||
.map(|analyzer| analyzer.get_description())
|
.map(|analyzer| analyzer.get_description())
|
||||||
.collect()
|
.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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-122
@@ -4,15 +4,6 @@ use crate::gsmtap::*;
|
|||||||
use log::error;
|
use log::error;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub struct GsmtapParser {
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for GsmtapParser {
|
|
||||||
fn default() -> Self {
|
|
||||||
GsmtapParser::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum GsmtapParserError {
|
pub enum GsmtapParserError {
|
||||||
#[error("Invalid LteRrcOtaMessage ext header version {0}")]
|
#[error("Invalid LteRrcOtaMessage ext header version {0}")]
|
||||||
@@ -21,119 +12,113 @@ pub enum GsmtapParserError {
|
|||||||
InvalidLteRrcOtaHeaderPduNum(u8, u8),
|
InvalidLteRrcOtaHeaderPduNum(u8, u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GsmtapParser {
|
pub fn parse(msg: Message) -> Result<Option<(Timestamp, GsmtapMessage)>, GsmtapParserError> {
|
||||||
pub fn new() -> Self {
|
if let Message::Log { timestamp, body, .. } = msg {
|
||||||
GsmtapParser {}
|
match log_to_gsmtap(body)? {
|
||||||
}
|
Some(msg) => Ok(Some((timestamp, msg))),
|
||||||
|
None => Ok(None),
|
||||||
pub fn parse(&mut self, msg: Message) -> Result<Option<(Timestamp, GsmtapMessage)>, 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<Option<GsmtapMessage>, 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)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, 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)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
use rayhunter::diag::{
|
use rayhunter::{diag::{
|
||||||
Message,
|
LogBody, LteRrcOtaPacket, Message, Timestamp
|
||||||
LogBody,
|
}, gsmtap_parser};
|
||||||
LteRrcOtaPacket,
|
|
||||||
Timestamp,
|
|
||||||
};
|
|
||||||
use rayhunter::gsmtap_parser::GsmtapParser;
|
|
||||||
use deku::prelude::*;
|
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]
|
#[test]
|
||||||
fn test_lte_rrc_ota() {
|
fn test_lte_rrc_ota() {
|
||||||
let mut parser = GsmtapParser::new();
|
|
||||||
let v26_binary = &[
|
let v26_binary = &[
|
||||||
0x10, 0x0, 0x23, 0x0, 0x23, 0x0, 0xc0, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
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,
|
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.payload, &[0x10, 0x15]);
|
||||||
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
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, &[
|
assert_eq!(&gsmtap_msg.payload, &[
|
||||||
0x10, 0x15,
|
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, &[
|
assert_eq!(&gsmtap_msg.payload, &[
|
||||||
0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50,
|
0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50,
|
||||||
0xdc, 0x29, 0x15, 0x16, 0x00,
|
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, &[
|
assert_eq!(&gsmtap_msg.payload, &[
|
||||||
0x08, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x05,
|
0x08, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x05,
|
||||||
0x43, 0x68, 0xc0, 0x3b, 0xda, 0x30, 0x04, 0xa6,
|
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, &[
|
assert_eq!(&gsmtap_msg.payload, &[
|
||||||
0x28, 0x18, 0x40, 0x16, 0x08, 0x08, 0x80, 0x00,
|
0x28, 0x18, 0x40, 0x16, 0x08, 0x08, 0x80, 0x00,
|
||||||
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, &[
|
assert_eq!(&gsmtap_msg.payload, &[
|
||||||
0x40, 0x0c, 0x8e, 0xc9, 0x42, 0x89, 0xe0,
|
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, &[
|
assert_eq!(&gsmtap_msg.payload, &[
|
||||||
0x08, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c,
|
0x08, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c,
|
||||||
0x31, 0x68, 0x04, 0x40, 0x1a, 0x00, 0x49, 0x16,
|
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.payload, &[0x2c, 0x00]);
|
||||||
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
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, &[
|
assert_eq!(&gsmtap_msg.payload, &[
|
||||||
0x40, 0x0b, 0x8e, 0xc1, 0xdd, 0x13, 0xb0,
|
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.payload, &[0x2e, 0x02]);
|
||||||
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
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, &[
|
assert_eq!(&gsmtap_msg.payload, &[
|
||||||
0x40, 0x49, 0x88, 0x05, 0xc0, 0x97, 0x02, 0xd3,
|
0x40, 0x49, 0x88, 0x05, 0xc0, 0x97, 0x02, 0xd3,
|
||||||
0xb0, 0x98, 0x1c, 0x20, 0xa0, 0x81, 0x8c, 0x43,
|
0xb0, 0x98, 0x1c, 0x20, 0xa0, 0x81, 0x8c, 0x43,
|
||||||
|
|||||||
Reference in New Issue
Block a user