mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-04-26 15:39:59 -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:
@@ -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<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 {
|
||||
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 {
|
||||
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<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);
|
||||
}
|
||||
|
||||
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()
|
||||
.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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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)
|
||||
},
|
||||
pub fn parse(msg: Message) -> Result<Option<(Timestamp, GsmtapMessage)>, 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<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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user