From 142f8bd5b2c5837af2fa451ae8c1b9fffa6f252a Mon Sep 17 00:00:00 2001 From: Jack Lund Date: Mon, 11 May 2026 23:17:08 -0500 Subject: [PATCH 1/8] First pass towards adding wifi scan for OUIs https://github.com/EFForg/rayhunter/issues/1000 Added ability to scan wifi networks (with a change to the wifi-station library, which I will PR once I get this working). Trying to integrate it into the analysis setup didn't work well, because that's pretty much only set up for packet analysis, so I'm instead sending the change to the display state directly from the wifi analysis function, which probably isn't really what's needed. --- Cargo.lock | 3 +-- daemon/Cargo.toml | 2 +- daemon/src/analysis.rs | 44 ++++++++++++++++++++++++++++++------ daemon/src/config.rs | 3 +++ daemon/src/main.rs | 14 +++++++++--- daemon/src/scan.rs | 42 ++++++++++++++++++++++++++++++++++ lib/src/analysis/analyzer.rs | 2 ++ 7 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 daemon/src/scan.rs diff --git a/Cargo.lock b/Cargo.lock index 9c829c39..ca80d02b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7168,8 +7168,7 @@ checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "wifi-station" version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb84ca59dc42c3818d652f7bbd0715abe3bc8f74d160831e865b3946d39fc19" +source = "git+https://github.com/jacklund/wifi-station?branch=add-bssid#26045857d891208b3c04836a91f6082b569bb5bb" dependencies = [ "anyhow", "chrono", diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index 4d51b02e..a28e6a95 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -21,7 +21,7 @@ apidocs = ["dep:utoipa", "wifi-station/utoipa"] [dependencies] rayhunter = { path = "../lib" } -wifi-station = "0.10.1" +wifi-station = { git = "https://github.com/jacklund/wifi-station", branch = "add-bssid" } toml = "0.8.8" serde = { version = "1.0.193", features = ["derive"] } serde_repr = "0.1" diff --git a/daemon/src/analysis.rs b/daemon/src/analysis.rs index 41efd959..54dacbeb 100644 --- a/daemon/src/analysis.rs +++ b/daemon/src/analysis.rs @@ -14,11 +14,14 @@ use rayhunter::qmdl::QmdlReader; use serde::Serialize; use tokio::fs::File; use tokio::io::{AsyncWriteExt, BufWriter}; -use tokio::sync::mpsc::Receiver; +use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::{RwLock, RwLockWriteGuard}; use tokio_util::task::TaskTracker; +use wifi_station::WifiNetwork; -use crate::qmdl_store::{FileKind, RecordingStore}; +use crate::display; +use crate::qmdl_store::FileKind; +use crate::qmdl_store::RecordingStore; use crate::server::ServerState; pub struct AnalysisWriter { @@ -108,6 +111,7 @@ impl AnalysisStatus { pub enum AnalysisCtrlMessage { NewFilesQueued, RecordingFinished(String), + WifiNetworksDetected(Vec), Exit, } @@ -162,11 +166,9 @@ async fn perform_analysis( .expect("failed to get QMDL file metadata") .len(); let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize)); - let mut qmdl_stream = pin::pin!( - qmdl_reader - .as_stream() - .try_filter(|container| future::ready(container.data_type == DataType::UserSpace)) - ); + let mut qmdl_stream = pin::pin!(qmdl_reader + .as_stream() + .try_filter(|container| future::ready(container.data_type == DataType::UserSpace))); info!("Starting analysis for {name}..."); while let Some(container) = qmdl_stream @@ -189,12 +191,37 @@ async fn perform_analysis( Ok(()) } +async fn analyze_wifi_networks( + wifi_ouis: &Option>, + networks: Vec, + ui_update_sender: &Sender, +) { + if let Some(ouis) = wifi_ouis { + for network in networks { + if ouis + .iter() + .find(|oui| network.bssid.starts_with(*oui)) + .is_some() + { + ui_update_sender + .send(display::DisplayState::WarningDetected { + event_type: EventType::High, + }) + .await + .expect("couldn't send ui update message: {}"); + } + } + } +} + pub fn run_analysis_thread( task_tracker: &TaskTracker, mut analysis_rx: Receiver, qmdl_store_lock: Arc>, analysis_status_lock: Arc>, analyzer_config: AnalyzerConfig, + ui_update_sender: Sender, + wifi_ouis: Option>, ) { task_tracker.spawn(async move { loop { @@ -215,6 +242,9 @@ pub fn run_analysis_thread( let mut status = analysis_status_lock.write().await; status.finished.push(name); } + Some(AnalysisCtrlMessage::WifiNetworksDetected(networks)) => { + analyze_wifi_networks(&wifi_ouis, networks, &ui_update_sender).await; + } Some(AnalysisCtrlMessage::Exit) | None => return, } } diff --git a/daemon/src/config.rs b/daemon/src/config.rs index 0aa68e6c..34d9da36 100644 --- a/daemon/src/config.rs +++ b/daemon/src/config.rs @@ -86,6 +86,8 @@ pub struct Config { pub dns_servers: Option>, /// WebDAV upload configuration. The upload worker runs whenever `webdav.url` is non-empty. pub webdav: WebdavConfig, + /// Optional WiFi OUIs for analysis + pub wifi_ouis: Option>, } /// Configuration for uploading finished QMDL recordings to a WebDAV server. @@ -148,6 +150,7 @@ impl Default for Config { wifi_enabled: false, dns_servers: None, webdav: WebdavConfig::default(), + wifi_ouis: None, } } } diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 3be95e4f..22335578 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -10,6 +10,7 @@ mod key_input; mod notifications; mod pcap; mod qmdl_store; +mod scan; mod server; mod stats; mod update; @@ -26,6 +27,7 @@ use crate::gps::{get_gps, post_gps}; use crate::notifications::{NotificationService, run_notification_worker}; use crate::pcap::get_pcap; use crate::qmdl_store::RecordingStore; +use crate::scan::run_wifi_scanner; use crate::server::{ ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_wifi_status, get_zip, scan_wifi, serve_static, set_config, set_time_offset, test_notification, @@ -281,6 +283,8 @@ async fn run_with_config( qmdl_store_lock.clone(), analysis_status_lock.clone(), config.analyzers.clone(), + ui_update_tx.clone(), + config.wifi_ouis.clone(), ); run_shutdown_thread( @@ -343,19 +347,23 @@ async fn run_with_config( let state = Arc::new(ServerState { config_path: args.config_path.clone(), - config, + config: config.clone(), qmdl_store_lock: qmdl_store_lock.clone(), diag_device_ctrl_sender: diag_tx, analysis_status_lock, analysis_sender: analysis_tx, daemon_restart_token: restart_token.clone(), - ui_update_sender: Some(ui_update_tx), + ui_update_sender: Some(ui_update_tx.clone()), wifi_status, wifi_scan_lock: tokio::sync::Mutex::new(()), gps_state: Arc::new(tokio::sync::RwLock::new(initial_gps)), update_status_lock: update_status_lock.clone(), }); - run_server(&task_tracker, state, shutdown_token.clone()).await; + run_server(&task_tracker, state.clone(), shutdown_token.clone()).await; + + if config.analyzers.wifi_oui_analyzer { + run_wifi_scanner(&task_tracker, state, shutdown_token.clone()).await; + } task_tracker.close(); task_tracker.wait().await; diff --git a/daemon/src/scan.rs b/daemon/src/scan.rs new file mode 100644 index 00000000..3bd3581f --- /dev/null +++ b/daemon/src/scan.rs @@ -0,0 +1,42 @@ +use log::{info, warn}; +use std::sync::Arc; +use std::time::Duration; +use tokio::{select, task::JoinHandle, time}; +use tokio_util::{sync::CancellationToken, task::TaskTracker}; +use wifi_station::{STA_IFACE, scan_wifi_networks}; + +use crate::{analysis::AnalysisCtrlMessage, server::ServerState}; + +pub async fn run_wifi_scanner( + task_tracker: &TaskTracker, + state: Arc, + shutdown_token: CancellationToken, +) -> JoinHandle<()> { + info!("starting wifi scanner"); + + task_tracker.spawn(async move { + loop { + select! { + _ = shutdown_token.cancelled() => break, + _ = time::sleep(Duration::from_secs(15)) => { + if state.wifi_scan_lock.try_lock().is_err() { + warn!("WiFi scan already in progress"); + continue; + } + match scan_wifi_networks(STA_IFACE).await { + Ok(networks) => { + if let Err(e) = state.analysis_sender.send( + AnalysisCtrlMessage::WifiNetworksDetected(networks) + ).await { + warn!("couldn't send analysis message: {e}"); + } + } + Err(e) => { + warn!("Error scanning wifi networks: {e}"); + } + } + } + } + } + }) +} diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index 426e9345..1e7da217 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -30,6 +30,7 @@ pub struct AnalyzerConfig { pub incomplete_sib: bool, pub test_analyzer: bool, pub imsi_requested: bool, + pub wifi_oui_analyzer: bool, } impl Default for AnalyzerConfig { @@ -43,6 +44,7 @@ impl Default for AnalyzerConfig { nas_null_cipher: true, incomplete_sib: true, test_analyzer: false, + wifi_oui_analyzer: true, } } } From e1a767d65246765c418606e599ad561fa439605b Mon Sep 17 00:00:00 2001 From: Jack Lund Date: Wed, 13 May 2026 18:22:25 -0500 Subject: [PATCH 2/8] Try to hook wifi oui analyzer into analysis harness --- daemon/src/analysis.rs | 59 +++++++++++-------- daemon/src/lib.rs | 1 + daemon/src/main.rs | 2 +- daemon/src/scan.rs | 2 +- .../web/src/lib/components/ConfigForm.svelte | 48 +++++++++++++++ daemon/web/src/lib/utils.svelte.ts | 1 + dist/config.toml.in | 1 + lib/src/analysis/analyzer.rs | 16 ++++- lib/src/analysis/information_element.rs | 1 + lib/src/analysis/mod.rs | 1 + lib/src/analysis/wifi_oui_analyzer.rs | 56 ++++++++++++++++++ 11 files changed, 160 insertions(+), 28 deletions(-) create mode 100644 lib/src/analysis/wifi_oui_analyzer.rs diff --git a/daemon/src/analysis.rs b/daemon/src/analysis.rs index 54dacbeb..c7214d1b 100644 --- a/daemon/src/analysis.rs +++ b/daemon/src/analysis.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::time::Duration; use std::{cmp, future, pin}; use axum::Json; @@ -21,6 +22,7 @@ use wifi_station::WifiNetwork; use crate::display; use crate::qmdl_store::FileKind; +use crate::notifications::{Notification, NotificationType}; use crate::qmdl_store::RecordingStore; use crate::server::ServerState; @@ -191,29 +193,6 @@ async fn perform_analysis( Ok(()) } -async fn analyze_wifi_networks( - wifi_ouis: &Option>, - networks: Vec, - ui_update_sender: &Sender, -) { - if let Some(ouis) = wifi_ouis { - for network in networks { - if ouis - .iter() - .find(|oui| network.bssid.starts_with(*oui)) - .is_some() - { - ui_update_sender - .send(display::DisplayState::WarningDetected { - event_type: EventType::High, - }) - .await - .expect("couldn't send ui update message: {}"); - } - } - } -} - pub fn run_analysis_thread( task_tracker: &TaskTracker, mut analysis_rx: Receiver, @@ -221,7 +200,7 @@ pub fn run_analysis_thread( analysis_status_lock: Arc>, analyzer_config: AnalyzerConfig, ui_update_sender: Sender, - wifi_ouis: Option>, + notification_channel: Sender, ) { task_tracker.spawn(async move { loop { @@ -243,9 +222,39 @@ pub fn run_analysis_thread( status.finished.push(name); } Some(AnalysisCtrlMessage::WifiNetworksDetected(networks)) => { - analyze_wifi_networks(&wifi_ouis, networks, &ui_update_sender).await; + if !analyzer_config.wifi_ouis.is_empty() { + let mut harness = Harness::new_with_config(&analyzer_config); + let mut events = harness + .analyze_wifi_ouis(networks.iter().map(|n| n.bssid.clone()).collect()); + if !events.is_empty() { + events.sort_by(|a, b| a.event_type.cmp(&b.event_type)); + if let Some(max_event) = events.pop() { + if max_event.event_type > EventType::Informational { + info!("a heuristic triggered on this run!"); + notification_channel + .send(Notification::new( + NotificationType::Warning, + format!( + "Rayhunter has detected a {:?} severity event", + max_event.event_type, + ), + Some(Duration::from_secs(60 * 5)), + )) + .await + .expect("Failed to send to notification channel"); + ui_update_sender + .send(display::DisplayState::WarningDetected { + event_type: max_event.event_type, + }) + .await + .expect("couldn't send ui update message: {}"); + } + } + } + } } Some(AnalysisCtrlMessage::Exit) | None => return, + } } }); diff --git a/daemon/src/lib.rs b/daemon/src/lib.rs index c415088e..dc395184 100644 --- a/daemon/src/lib.rs +++ b/daemon/src/lib.rs @@ -10,6 +10,7 @@ pub mod key_input; pub mod notifications; pub mod pcap; pub mod qmdl_store; +pub mod scan; pub mod server; pub mod stats; pub mod update; diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 22335578..9417046e 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -284,7 +284,7 @@ async fn run_with_config( analysis_status_lock.clone(), config.analyzers.clone(), ui_update_tx.clone(), - config.wifi_ouis.clone(), + notification_service.new_handler(), ); run_shutdown_thread( diff --git a/daemon/src/scan.rs b/daemon/src/scan.rs index 3bd3581f..cc372e7d 100644 --- a/daemon/src/scan.rs +++ b/daemon/src/scan.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::{select, task::JoinHandle, time}; use tokio_util::{sync::CancellationToken, task::TaskTracker}; -use wifi_station::{STA_IFACE, scan_wifi_networks}; +use wifi_station::{scan_wifi_networks, STA_IFACE}; use crate::{analysis::AnalysisCtrlMessage, server::ServerState}; diff --git a/daemon/web/src/lib/components/ConfigForm.svelte b/daemon/web/src/lib/components/ConfigForm.svelte index fd19c485..a187f1db 100644 --- a/daemon/web/src/lib/components/ConfigForm.svelte +++ b/daemon/web/src/lib/components/ConfigForm.svelte @@ -29,12 +29,14 @@ let scanning = $state(false); let scanResults = $state([]); let dnsServersInput = $state(''); + let wifiOUIsInput = $state(''); async function load_config() { try { loading = true; config = await get_config(); dnsServersInput = config.dns_servers ? config.dns_servers.join(', ') : ''; + wifiOUIsInput = config.analyzers.wifi_ouis ? config.analyzers.wifi_ouis.join(', ') : ''; message = ''; messageType = null; poll_wifi_status(); @@ -58,6 +60,15 @@ .filter((s) => s.length > 0) : null; + const trimmed_ouis = wifiOUIsInput.trim(); + config.analyzers.wifi_ouis = + trimmed_ouis.length > 0 + ? trimmed_ouis + .split(',') + .map((s) => s.trim()) + .filter((s) => s.length > 0) + : null; + try { saving = true; await set_config(config); @@ -764,6 +775,43 @@ Diagnostic Analyzer + +
+ + +
+ + {#if config.analyzers.wifi_oui_analyzer} +
+ + +

+ Comma-separated triplets of hex octets, of the form "AB:CD:EF". + Used when WiFi OUI Analyzer is active. +

+
+ {/if} diff --git a/daemon/web/src/lib/utils.svelte.ts b/daemon/web/src/lib/utils.svelte.ts index 57f81f19..144fc0a2 100644 --- a/daemon/web/src/lib/utils.svelte.ts +++ b/daemon/web/src/lib/utils.svelte.ts @@ -11,6 +11,7 @@ export interface AnalyzerConfig { incomplete_sib: boolean; test_analyzer: boolean; diagnostic_analyzer: boolean; + wifi_oui_analyzer: boolean; } export enum enabled_notifications { diff --git a/dist/config.toml.in b/dist/config.toml.in index 5d98f6d4..867d2211 100644 --- a/dist/config.toml.in +++ b/dist/config.toml.in @@ -83,3 +83,4 @@ nas_null_cipher = true incomplete_sib = true test_analyzer = false diagnostic_analyzer = true +wifi_oui_analyzer = true diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index 1e7da217..91f9e6a4 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -14,7 +14,7 @@ use super::{ imsi_requested::ImsiRequestedAnalyzer, incomplete_sib::IncompleteSibAnalyzer, information_element::InformationElement, nas_null_cipher::NasNullCipherAnalyzer, null_cipher::NullCipherAnalyzer, priority_2g_downgrade::LteSib6And7DowngradeAnalyzer, - test_analyzer::TestAnalyzer, + test_analyzer::TestAnalyzer, wifi_oui_analyzer::WifiOUIAnalyzer, }; /// A list of booleans which stores information about which analyzers are enabled @@ -31,6 +31,7 @@ pub struct AnalyzerConfig { pub test_analyzer: bool, pub imsi_requested: bool, pub wifi_oui_analyzer: bool, + pub wifi_ouis: Vec, } impl Default for AnalyzerConfig { @@ -45,6 +46,7 @@ impl Default for AnalyzerConfig { incomplete_sib: true, test_analyzer: false, wifi_oui_analyzer: true, + wifi_ouis: Vec::new(), } } } @@ -367,6 +369,10 @@ impl Harness { harness.add_analyzer(Box::new(DiagnosticAnalyzer {})); } + if analyzer_config.wifi_oui_analyzer { + harness.add_analyzer(Box::new(WifiOUIAnalyzer::new(&analyzer_config.wifi_ouis))); + } + harness } @@ -374,6 +380,14 @@ impl Harness { self.analyzers.push(analyzer); } + pub fn analyze_wifi_ouis(&mut self, bssids: Vec) -> Vec { + self + .analyze_information_element(&InformationElement::WifiBSSIDList(bssids)) + .iter() + .flat_map(|e| e.clone()) + .collect::>() + } + pub fn analyze_pcap_packet(&mut self, packet: EnhancedPacketBlock) -> AnalysisRow { self.packet_num += 1; diff --git a/lib/src/analysis/information_element.rs b/lib/src/analysis/information_element.rs index 54089bef..b68f1971 100644 --- a/lib/src/analysis/information_element.rs +++ b/lib/src/analysis/information_element.rs @@ -26,6 +26,7 @@ pub enum InformationElement { // so we box it to prevent the size of the enum (any variant) from blowing up. LTE(Box), FiveG, + WifiBSSIDList(Vec), } #[derive(Debug)] diff --git a/lib/src/analysis/mod.rs b/lib/src/analysis/mod.rs index 4d30779c..fd825bd7 100644 --- a/lib/src/analysis/mod.rs +++ b/lib/src/analysis/mod.rs @@ -9,3 +9,4 @@ pub mod null_cipher; pub mod priority_2g_downgrade; pub mod test_analyzer; pub mod util; +pub mod wifi_oui_analyzer; diff --git a/lib/src/analysis/wifi_oui_analyzer.rs b/lib/src/analysis/wifi_oui_analyzer.rs new file mode 100644 index 00000000..db7ef8ac --- /dev/null +++ b/lib/src/analysis/wifi_oui_analyzer.rs @@ -0,0 +1,56 @@ +use crate::analysis::{ + analyzer::{Analyzer, Event, EventType}, + information_element::InformationElement, +}; + +pub struct WifiOUIAnalyzer { + wifi_ouis: Vec, +} + +impl WifiOUIAnalyzer { + pub fn new(ouis: &[String]) -> Self { + Self { + wifi_ouis: ouis.to_vec(), + } + } +} + +impl Analyzer for WifiOUIAnalyzer { + fn get_name(&self) -> std::borrow::Cow<'_, str> { + "WifiOUIAnalyzer".into() + } + + fn get_description(&self) -> std::borrow::Cow<'_, str> { + "blah blah blah".into() + } + + fn get_version(&self) -> u32 { + 1 + } + + fn analyze_information_element( + &mut self, + ie: &InformationElement, + _packet_num: usize, + ) -> Option { + if let InformationElement::WifiBSSIDList(bssids) = ie { + if !self.wifi_ouis.is_empty() { + for bssid in bssids { + if self + .wifi_ouis + .iter() + .find(|oui| bssid.starts_with(*oui)) + .is_some() + { + return Some(Event { + event_type: EventType::High, + message: "Detected possible IMSI catcher wifi endpoint".to_string(), + }); + } + } + } + } + + None + } +} From 559b35ba292b62341579b8858b999abc6a67e355 Mon Sep 17 00:00:00 2001 From: Jack Lund Date: Wed, 13 May 2026 19:24:45 -0500 Subject: [PATCH 3/8] Got wifi oui analyzer working! --- daemon/src/analysis.rs | 4 +++- daemon/src/scan.rs | 4 +++- lib/src/analysis/wifi_oui_analyzer.rs | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/daemon/src/analysis.rs b/daemon/src/analysis.rs index c7214d1b..2472d200 100644 --- a/daemon/src/analysis.rs +++ b/daemon/src/analysis.rs @@ -8,7 +8,7 @@ use axum::{ http::StatusCode, }; use futures::TryStreamExt; -use log::{error, info}; +use log::{debug, error, info}; use rayhunter::analysis::analyzer::{AnalyzerConfig, EventType, Harness}; use rayhunter::diag::{DataType, MessagesContainer}; use rayhunter::qmdl::QmdlReader; @@ -222,10 +222,12 @@ pub fn run_analysis_thread( status.finished.push(name); } Some(AnalysisCtrlMessage::WifiNetworksDetected(networks)) => { + debug!("Networks detected, configured OUIs: {:?}", analyzer_config.wifi_ouis.join(",")); if !analyzer_config.wifi_ouis.is_empty() { let mut harness = Harness::new_with_config(&analyzer_config); let mut events = harness .analyze_wifi_ouis(networks.iter().map(|n| n.bssid.clone()).collect()); + debug!("Called analyze_wifi_ouis, got events: {:?}", events); if !events.is_empty() { events.sort_by(|a, b| a.event_type.cmp(&b.event_type)); if let Some(max_event) = events.pop() { diff --git a/daemon/src/scan.rs b/daemon/src/scan.rs index cc372e7d..b850998b 100644 --- a/daemon/src/scan.rs +++ b/daemon/src/scan.rs @@ -1,4 +1,4 @@ -use log::{info, warn}; +use log::{debug, info, warn}; use std::sync::Arc; use std::time::Duration; use tokio::{select, task::JoinHandle, time}; @@ -23,8 +23,10 @@ pub async fn run_wifi_scanner( warn!("WiFi scan already in progress"); continue; } + debug!("Calling scan_wifi_networks()"); match scan_wifi_networks(STA_IFACE).await { Ok(networks) => { + debug!("Found {} networks", networks.len()); if let Err(e) = state.analysis_sender.send( AnalysisCtrlMessage::WifiNetworksDetected(networks) ).await { diff --git a/lib/src/analysis/wifi_oui_analyzer.rs b/lib/src/analysis/wifi_oui_analyzer.rs index db7ef8ac..8f416b5a 100644 --- a/lib/src/analysis/wifi_oui_analyzer.rs +++ b/lib/src/analysis/wifi_oui_analyzer.rs @@ -1,3 +1,5 @@ +use log::{debug, info}; + use crate::analysis::{ analyzer::{Analyzer, Event, EventType}, information_element::InformationElement, @@ -34,14 +36,16 @@ impl Analyzer for WifiOUIAnalyzer { _packet_num: usize, ) -> Option { if let InformationElement::WifiBSSIDList(bssids) = ie { + debug!("WifiOUIAnalyzer got BSSIDs {:?}", bssids); if !self.wifi_ouis.is_empty() { for bssid in bssids { if self .wifi_ouis .iter() - .find(|oui| bssid.starts_with(*oui)) + .find(|oui| bssid.to_uppercase().starts_with(&oui.to_uppercase())) .is_some() { + debug!("Found match for bssid {bssid}"); return Some(Event { event_type: EventType::High, message: "Detected possible IMSI catcher wifi endpoint".to_string(), From 51d53dab6f5573ff914f4c12154e26f38e510969 Mon Sep 17 00:00:00 2001 From: Jack Lund Date: Sun, 17 May 2026 23:02:21 -0500 Subject: [PATCH 4/8] Clippy & fmt --- daemon/src/analysis.rs | 58 ++++++++++++++------------- daemon/src/scan.rs | 2 +- lib/src/analysis/analyzer.rs | 3 +- lib/src/analysis/wifi_oui_analyzer.rs | 2 +- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/daemon/src/analysis.rs b/daemon/src/analysis.rs index 2472d200..4b1c29c3 100644 --- a/daemon/src/analysis.rs +++ b/daemon/src/analysis.rs @@ -168,9 +168,11 @@ async fn perform_analysis( .expect("failed to get QMDL file metadata") .len(); let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize)); - let mut qmdl_stream = pin::pin!(qmdl_reader - .as_stream() - .try_filter(|container| future::ready(container.data_type == DataType::UserSpace))); + let mut qmdl_stream = pin::pin!( + qmdl_reader + .as_stream() + .try_filter(|container| future::ready(container.data_type == DataType::UserSpace)) + ); info!("Starting analysis for {name}..."); while let Some(container) = qmdl_stream @@ -222,41 +224,43 @@ pub fn run_analysis_thread( status.finished.push(name); } Some(AnalysisCtrlMessage::WifiNetworksDetected(networks)) => { - debug!("Networks detected, configured OUIs: {:?}", analyzer_config.wifi_ouis.join(",")); + debug!( + "Networks detected, configured OUIs: {:?}", + analyzer_config.wifi_ouis.join(",") + ); if !analyzer_config.wifi_ouis.is_empty() { let mut harness = Harness::new_with_config(&analyzer_config); let mut events = harness .analyze_wifi_ouis(networks.iter().map(|n| n.bssid.clone()).collect()); debug!("Called analyze_wifi_ouis, got events: {:?}", events); if !events.is_empty() { - events.sort_by(|a, b| a.event_type.cmp(&b.event_type)); - if let Some(max_event) = events.pop() { - if max_event.event_type > EventType::Informational { - info!("a heuristic triggered on this run!"); - notification_channel - .send(Notification::new( - NotificationType::Warning, - format!( - "Rayhunter has detected a {:?} severity event", - max_event.event_type, - ), - Some(Duration::from_secs(60 * 5)), - )) - .await - .expect("Failed to send to notification channel"); - ui_update_sender - .send(display::DisplayState::WarningDetected { - event_type: max_event.event_type, - }) - .await - .expect("couldn't send ui update message: {}"); - } + events.sort_by_key(|a| a.event_type); + if let Some(max_event) = events.pop() + && max_event.event_type > EventType::Informational + { + info!("a heuristic triggered on this run!"); + notification_channel + .send(Notification::new( + NotificationType::Warning, + format!( + "Rayhunter has detected a {:?} severity event", + max_event.event_type, + ), + Some(Duration::from_secs(60 * 5)), + )) + .await + .expect("Failed to send to notification channel"); + ui_update_sender + .send(display::DisplayState::WarningDetected { + event_type: max_event.event_type, + }) + .await + .expect("couldn't send ui update message: {}"); } } } } Some(AnalysisCtrlMessage::Exit) | None => return, - } } }); diff --git a/daemon/src/scan.rs b/daemon/src/scan.rs index b850998b..698f14a4 100644 --- a/daemon/src/scan.rs +++ b/daemon/src/scan.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::{select, task::JoinHandle, time}; use tokio_util::{sync::CancellationToken, task::TaskTracker}; -use wifi_station::{scan_wifi_networks, STA_IFACE}; +use wifi_station::{STA_IFACE, scan_wifi_networks}; use crate::{analysis::AnalysisCtrlMessage, server::ServerState}; diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index 91f9e6a4..880020f6 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -381,8 +381,7 @@ impl Harness { } pub fn analyze_wifi_ouis(&mut self, bssids: Vec) -> Vec { - self - .analyze_information_element(&InformationElement::WifiBSSIDList(bssids)) + self.analyze_information_element(&InformationElement::WifiBSSIDList(bssids)) .iter() .flat_map(|e| e.clone()) .collect::>() diff --git a/lib/src/analysis/wifi_oui_analyzer.rs b/lib/src/analysis/wifi_oui_analyzer.rs index 8f416b5a..78dcf8cf 100644 --- a/lib/src/analysis/wifi_oui_analyzer.rs +++ b/lib/src/analysis/wifi_oui_analyzer.rs @@ -1,4 +1,4 @@ -use log::{debug, info}; +use log::debug; use crate::analysis::{ analyzer::{Analyzer, Event, EventType}, From d5f1ba291345ed3ddc182facadef5f669654f5d8 Mon Sep 17 00:00:00 2001 From: Jack Lund Date: Sun, 17 May 2026 23:23:47 -0500 Subject: [PATCH 5/8] Add test --- lib/src/analysis/analyzer.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index 880020f6..f105b224 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -563,4 +563,17 @@ mod tests { ); assert!(row.events[2].is_none()); } + + #[test] + fn test_analyze_wifi_ouis() { + let mut analyzer_config = AnalyzerConfig::default(); + analyzer_config.wifi_oui_analyzer = true; + analyzer_config.wifi_ouis = vec!["AA:BB:CC".to_string()]; + let mut harness = Harness::new_with_config(&analyzer_config); + let events = harness.analyze_wifi_ouis(vec!["00:11:22:33:44:55".to_string()]); + assert!(events.is_empty()); + let events = harness.analyze_wifi_ouis(vec!["AA:BB:CC:33:44:55".to_string()]); + assert_eq!(1, events.len()); + assert_eq!(EventType::High, events[0].event_type); + } } From e1423f464788348d436cb9c0396c22800489bd1d Mon Sep 17 00:00:00 2001 From: Jack Lund Date: Mon, 18 May 2026 22:37:55 -0500 Subject: [PATCH 6/8] npm lint --- daemon/web/src/lib/components/ConfigForm.svelte | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/daemon/web/src/lib/components/ConfigForm.svelte b/daemon/web/src/lib/components/ConfigForm.svelte index a187f1db..a8add675 100644 --- a/daemon/web/src/lib/components/ConfigForm.svelte +++ b/daemon/web/src/lib/components/ConfigForm.svelte @@ -783,10 +783,7 @@ bind:checked={config.analyzers.wifi_oui_analyzer} class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm" /> -