diff --git a/daemon/src/battery/mod.rs b/daemon/src/battery/mod.rs index 4a68d0a..2c50eb7 100644 --- a/daemon/src/battery/mod.rs +++ b/daemon/src/battery/mod.rs @@ -1,14 +1,22 @@ -use std::path::Path; +use std::{path::Path, time::Duration}; +use log::{error, info}; use rayhunter::Device; use serde::Serialize; +use tokio::select; +use tokio_util::{sync::CancellationToken, task::TaskTracker}; -use crate::error::RayhunterError; +use crate::{ + error::RayhunterError, + notifications::{Notification, NotificationType}, +}; pub mod orbic; pub mod tmobile; pub mod wingtech; +const LOW_BATTERY_LEVEL: u8 = 10; + #[derive(Clone, Copy, PartialEq, Debug, Serialize)] pub struct BatteryState { level: u8, @@ -45,3 +53,59 @@ pub async fn get_battery_status(device: &Device) -> Result return Err(RayhunterError::FunctionNotSupportedForDeviceError), }) } + +pub fn run_battery_notification_worker( + task_tracker: &TaskTracker, + device: Device, + notification_channel: tokio::sync::mpsc::Sender, + shutdown_token: CancellationToken, +) { + task_tracker.spawn(async move { + // Don't send a notification initially if the device starts at a low battery level. + let mut triggered = match get_battery_status(&device).await { + Err(RayhunterError::FunctionNotSupportedForDeviceError) => { + info!("Battery level function not supported for device"); + false + } + Err(e) => { + error!("Failed to get battery status: {e}"); + true + } + Ok(status) => status.level < LOW_BATTERY_LEVEL, + }; + + loop { + select! { + _ = shutdown_token.cancelled() => break, + _ = tokio::time::sleep(Duration::from_secs(15)) => {} + } + + let status = match get_battery_status(&device).await { + Err(e) => { + error!("Failed to get battery status: {e}"); + continue; + } + Ok(status) => status, + }; + + // To avoid flapping, if the notification has already been triggered + // wait until the device has been plugged in and the battery level + // is high enough to re-enable notifications. + if triggered && status.is_plugged_in && status.level > LOW_BATTERY_LEVEL { + triggered = false; + continue; + } + if !triggered && !status.is_plugged_in && status.level <= LOW_BATTERY_LEVEL { + notification_channel + .send(Notification::new( + NotificationType::LowBattery, + "Rayhunter's battery is low".to_string(), + None, + )) + .await + .expect("Failed to send to notification channel"); + triggered = true; + } + } + }); +} diff --git a/daemon/src/config.rs b/daemon/src/config.rs index 039a4c8..f156099 100644 --- a/daemon/src/config.rs +++ b/daemon/src/config.rs @@ -5,6 +5,7 @@ use rayhunter::Device; use rayhunter::analysis::analyzer::AnalyzerConfig; use crate::error::RayhunterError; +use crate::notifications::NotificationType; #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(default)] @@ -17,6 +18,7 @@ pub struct Config { pub colorblind_mode: bool, pub key_input_mode: u8, pub ntfy_url: Option, + pub enabled_notifications: Vec, pub analyzers: AnalyzerConfig, } @@ -32,6 +34,7 @@ impl Default for Config { key_input_mode: 0, analyzers: AnalyzerConfig::default(), ntfy_url: None, + enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery], } } } diff --git a/daemon/src/diag.rs b/daemon/src/diag.rs index 636612f..8584c4e 100644 --- a/daemon/src/diag.rs +++ b/daemon/src/diag.rs @@ -24,7 +24,7 @@ use rayhunter::qmdl::QmdlWriter; use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter}; use crate::display; -use crate::notifications::Notification; +use crate::notifications::{Notification, NotificationType}; use crate::qmdl_store::{RecordingStore, RecordingStoreError}; use crate::server::ServerState; @@ -207,7 +207,7 @@ impl DiagTask { info!("a heuristic triggered on this run!"); self.notification_channel .send(Notification::new( - "heuristic-warning".to_string(), + NotificationType::Warning, format!("Rayhunter has detected a {:?} severity event", max_type), Some(Duration::from_secs(60 * 5)), )) diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 5195062..732c61e 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -14,6 +14,7 @@ mod stats; use std::net::SocketAddr; use std::sync::Arc; +use crate::battery::run_battery_notification_worker; use crate::config::{parse_args, parse_config}; use crate::diag::run_diag_read_thread; use crate::error::RayhunterError; @@ -260,7 +261,20 @@ async fn run_with_config( qmdl_store_lock.clone(), analysis_tx.clone(), ); - run_notification_worker(&task_tracker, notification_service); + + run_battery_notification_worker( + &task_tracker, + config.device.clone(), + notification_service.new_handler(), + shutdown_token.clone(), + ); + + run_notification_worker( + &task_tracker, + notification_service, + config.enabled_notifications.clone(), + ); + let state = Arc::new(ServerState { config_path: args.config_path.clone(), config, diff --git a/daemon/src/notifications.rs b/daemon/src/notifications.rs index 5c5b3a6..4fa0ccf 100644 --- a/daemon/src/notifications.rs +++ b/daemon/src/notifications.rs @@ -5,19 +5,30 @@ use std::{ }; use log::error; +use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::{self, error::TryRecvError}; use tokio_util::task::TaskTracker; +#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] +pub enum NotificationType { + Warning, + LowBattery, +} + pub struct Notification { - message_type: String, + notification_type: NotificationType, message: String, debounce: Option, } impl Notification { - pub fn new(message_type: String, message: String, debounce: Option) -> Self { + pub fn new( + notification_type: NotificationType, + message: String, + debounce: Option, + ) -> Self { Notification { - message_type, + notification_type, message, debounce, } @@ -52,6 +63,7 @@ impl NotificationService { pub fn run_notification_worker( task_tracker: &TaskTracker, mut notification_service: NotificationService, + enabled_notifications: Vec, ) { task_tracker.spawn(async move { if let Some(url) = notification_service.url @@ -65,8 +77,12 @@ pub fn run_notification_worker( loop { match notification_service.rx.try_recv() { Ok(notification) => { + if !enabled_notifications.contains(¬ification.notification_type) { + continue; + } + let status = notification_statuses - .entry(notification.message_type) + .entry(notification.notification_type) .or_insert_with(|| NotificationStatus { message: "".to_string(), needs_sending: true, diff --git a/daemon/web/src/lib/components/ConfigForm.svelte b/daemon/web/src/lib/components/ConfigForm.svelte index 8652fae..0297295 100644 --- a/daemon/web/src/lib/components/ConfigForm.svelte +++ b/daemon/web/src/lib/components/ConfigForm.svelte @@ -111,18 +111,6 @@ -
- - -
-
+
+

Notification Settings

+
+ + +
+ +
+
+ Enabled Notification Types +
+
+ + +
+
+ + +
+
+
+

Analyzer Heuristic Settings diff --git a/daemon/web/src/lib/utils.svelte.ts b/daemon/web/src/lib/utils.svelte.ts index 6480d35..921f5a5 100644 --- a/daemon/web/src/lib/utils.svelte.ts +++ b/daemon/web/src/lib/utils.svelte.ts @@ -12,11 +12,17 @@ export interface AnalyzerConfig { test_analyzer: boolean; } +export enum enabled_notifications { + Warning = 'Warning', + LowBattery = 'LowBattery', +} + export interface Config { ui_level: number; colorblind_mode: boolean; key_input_mode: number; ntfy_url: string; + enabled_notifications: enabled_notifications[]; analyzers: AnalyzerConfig; } diff --git a/dist/config.toml.in b/dist/config.toml.in index 4c421be..627a36c 100644 --- a/dist/config.toml.in +++ b/dist/config.toml.in @@ -23,7 +23,9 @@ ui_level = 1 key_input_mode = 0 # If set, attempts to send a notification to the url when a new warning is triggered -# ntfy_url = +ntfy_url = "" +# What notification types to enable. Does nothing if the above ntfy_url is not set. +enabled_notifications = ["Warning", "LowBattery"] # Analyzer Configuration # Enable/disable specific IMSI catcher detection heuristics @@ -35,4 +37,4 @@ lte_sib6_and_7_downgrade = true null_cipher = true nas_null_cipher = true incomplete_sib = true -test_analyzer = false \ No newline at end of file +test_analyzer = false diff --git a/doc/configuration.md b/doc/configuration.md index 9b6386a..ff3fe80 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -13,8 +13,11 @@ Through web UI you can set: - **Device Input Mode**, which defines behaviour of built-in power button of the device. *Device Input Mode* could be: - *Disable button control*: built-in power button of the device is not used by Rayhunter; - *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristichs is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button. -- **ntfy URL for Sending Notifications**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/). - **Colorblind Mode** enables color blind mode (blue line is shown instead of green line, red line remains red). Please note that this does not cover all types of color blindness, but switching green to blue should be about enough to differentiate the color change for most types of color blindness. +- **ntfy URL**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/). +- **Enabled Notification Types** allows enabling or disabling the following types of notifications: + - *Warnings*, which will alert when a heuristic is triggered. Alerts will be sent at most once every five minutes. + - *Low Battery*, which will alert when the device's battery is low. Notifications may not be supported for all devices—you can check if your device is supported by looking at whether the battery level indicator is functioning on the System Information section of the Rayhunter UI. - With **Analyzer Heuristic Settings** you can switch on or off built-in [Rayhunter heuristics](heuristics.md). Some heuristics are experimental or can trigger a lot of false positive warnings in some networks (our tests have shown that some heuristics have different behaviour in US or European networks). In that case you can decide whether you would like to have the heuristics that trigger a lot of false positives on or off. Please note that we are constantly improving and adding new heuristics, so new release may reduce false positives in existing heuristics as well. If you prefer editing `config.toml` file, you need to obtain a shell on your [Orbic](./orbic.md#obtaining-a-shell) or [TP-Link](./tplink-m7350.md#obtaining-a-shell) device and edit the file manually. You can view the [default configuration file on a GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in). diff --git a/doc/rayhunter_config.png b/doc/rayhunter_config.png index 34c6037..ae5f37a 100644 Binary files a/doc/rayhunter_config.png and b/doc/rayhunter_config.png differ