From 9b759e6b4216bb2d1ab392cd3a2ea663f594b8dc Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 2 Jun 2025 21:11:55 +0200 Subject: [PATCH 01/11] add basic restart endpoint --- bin/src/daemon.rs | 110 +++++++++++++++++++++++++++-------------- bin/src/server.rs | 25 +++++++++- lib/src/diag_device.rs | 4 +- 3 files changed, 100 insertions(+), 39 deletions(-) diff --git a/bin/src/daemon.rs b/bin/src/daemon.rs index 54df4361..f0754ac3 100644 --- a/bin/src/daemon.rs +++ b/bin/src/daemon.rs @@ -10,13 +10,18 @@ mod qmdl_store; mod server; mod stats; +use std::net::SocketAddr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + use crate::config::{parse_args, parse_config}; use crate::diag::run_diag_read_thread; use crate::error::RayhunterError; use crate::pcap::get_pcap; use crate::qmdl_store::RecordingStore; -use crate::server::{get_qmdl, serve_static, ServerState}; -use crate::stats::get_system_stats; +use crate::server::{get_qmdl, restart_daemon, serve_static, ServerState}; +use crate::stats::{get_qmdl_manifest, get_system_stats}; use analysis::{ get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus, @@ -31,13 +36,12 @@ use diag::{ use log::{error, info}; use qmdl_store::RecordingStoreError; use rayhunter::diag_device::DiagDevice; -use stats::get_qmdl_manifest; -use std::net::SocketAddr; -use std::sync::Arc; use tokio::net::TcpListener; +use tokio::select; use tokio::sync::mpsc::{self, Sender}; use tokio::sync::{oneshot, RwLock}; use tokio::task::JoinHandle; +use tokio::time::sleep; use tokio_util::task::TaskTracker; type AppRouter = Router>; @@ -55,6 +59,7 @@ fn get_router() -> AppRouter { .route("/api/analysis-report/{name}", get(get_analysis_report)) .route("/api/analysis", get(get_analysis_status)) .route("/api/analysis/{name}", post(start_analysis)) + .route("/api/restart-daemon", post(restart_daemon)) .route("/", get(|| async { Redirect::permanent("/index.html") })) .route("/{*path}", get(serve_static)) } @@ -117,46 +122,60 @@ async fn init_qmdl_store(config: &config::Config) -> Result, + daemon_restart_rx: oneshot::Receiver<()>, + should_restart_flag: Arc, server_shutdown_tx: oneshot::Sender<()>, maybe_ui_shutdown_tx: Option>, qmdl_store_lock: Arc>, analysis_tx: Sender, ) -> JoinHandle> { + info!("create shutdown thread"); + task_tracker.spawn(async move { - match tokio::signal::ctrl_c().await { - Ok(()) => { - let mut qmdl_store = qmdl_store_lock.write().await; - if qmdl_store.current_entry.is_some() { - info!("Closing current QMDL entry..."); - qmdl_store.close_current_entry().await?; - info!("Done!"); + select! { + res = tokio::signal::ctrl_c() => { + if let Err(err) = res { + error!("Unable to listen for shutdown signal: {}", err); } - server_shutdown_tx - .send(()) - .expect("couldn't send server shutdown signal"); - info!("sending UI shutdown"); - if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx { - ui_shutdown_tx - .send(()) - .expect("couldn't send ui shutdown signal"); - } - diag_device_sender - .send(DiagDeviceCtrlMessage::Exit) - .await - .expect("couldn't send Exit message to diag thread"); - analysis_tx - .send(AnalysisCtrlMessage::Exit) - .await - .expect("couldn't send Exit message to analysis thread"); + should_restart_flag.store(false, Ordering::Relaxed); } - Err(err) => { - error!("Unable to listen for shutdown signal: {}", err); + res = daemon_restart_rx => { + if let Err(err) = res { + error!("Unable to listen for shutdown signal: {}", err); + } + + should_restart_flag.store(true, Ordering::Relaxed); } + }; + + let mut qmdl_store = qmdl_store_lock.write().await; + if qmdl_store.current_entry.is_some() { + info!("Closing current QMDL entry..."); + qmdl_store.close_current_entry().await?; + info!("Done!"); } + + server_shutdown_tx + .send(()) + .expect("couldn't send server shutdown signal"); + info!("sending UI shutdown"); + if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx { + ui_shutdown_tx + .send(()) + .expect("couldn't send ui shutdown signal"); + } + diag_device_sender + .send(DiagDeviceCtrlMessage::Exit) + .await + .expect("couldn't send Exit message to diag thread"); + analysis_tx + .send(AnalysisCtrlMessage::Exit) + .await + .expect("couldn't send Exit message to analysis thread"); Ok(()) }) } @@ -166,8 +185,21 @@ async fn main() -> Result<(), RayhunterError> { env_logger::init(); let args = parse_args(); - let config = parse_config(&args.config_path)?; + loop { + let config = parse_config(&args.config_path)?; + if !run_with_config(&config).await? { + return Ok(()); + } + + // For some reason the diag device needs a very long time to become available again within + // the same process, on TP-Link M7350 v3. While process restart would reset it faster. + println!("Restarting Rayhunter. Waiting for 5 seconds..."); + sleep(Duration::from_secs(5)).await; + } +} + +async fn run_with_config(config: &config::Config) -> Result { // TaskTrackers give us an interface to spawn tokio threads, and then // eventually await all of them ending let task_tracker = TaskTracker::new(); @@ -207,8 +239,9 @@ async fn main() -> Result<(), RayhunterError> { info!("Starting Key Input service"); key_input::run_key_input_thread(&task_tracker, &config, diag_tx.clone()); } + + let (daemon_restart_tx, daemon_restart_rx) = oneshot::channel::<()>(); let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>(); - info!("create shutdown thread"); let analysis_status_lock = Arc::new(RwLock::new(analysis_status)); run_analysis_thread( &task_tracker, @@ -218,9 +251,13 @@ async fn main() -> Result<(), RayhunterError> { config.enable_dummy_analyzer, config.analyzers.clone(), ); - run_ctrl_c_thread( + let should_restart_flag = Arc::new(AtomicBool::new(false)); + + run_shutdown_thread( &task_tracker, diag_tx.clone(), + daemon_restart_rx, + should_restart_flag.clone(), server_shutdown_tx, maybe_ui_shutdown_tx, qmdl_store_lock.clone(), @@ -233,6 +270,7 @@ async fn main() -> Result<(), RayhunterError> { debug_mode: config.debug_mode, analysis_status_lock, analysis_sender: analysis_tx, + daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))), }); run_server(&task_tracker, &config, state, server_shutdown_rx).await; @@ -240,7 +278,7 @@ async fn main() -> Result<(), RayhunterError> { task_tracker.wait().await; info!("see you space cowboy..."); - Ok(()) + Ok(should_restart_flag.load(Ordering::Relaxed)) } #[cfg(test)] diff --git a/bin/src/server.rs b/bin/src/server.rs index 9065733e..19d990b9 100644 --- a/bin/src/server.rs +++ b/bin/src/server.rs @@ -8,7 +8,7 @@ use include_dir::{include_dir, Dir}; use std::sync::Arc; use tokio::io::AsyncReadExt; use tokio::sync::mpsc::Sender; -use tokio::sync::RwLock; +use tokio::sync::{oneshot, RwLock}; use tokio_util::io::ReaderStream; use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus}; @@ -22,6 +22,7 @@ pub struct ServerState { pub analysis_status_lock: Arc>, pub analysis_sender: Sender, pub debug_mode: bool, + pub daemon_restart_tx: Arc>>>, } pub async fn get_qmdl( @@ -76,3 +77,25 @@ pub async fn serve_static( .unwrap(), } } + +pub async fn restart_daemon( + State(state): State>, +) -> Result<(StatusCode, String), (StatusCode, String)> { + let mut restart_tx = state.daemon_restart_tx.write().await; + + if let Some(sender) = restart_tx.take() { + sender.send(()).map_err(|()| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "couldn't send restart signal".to_string(), + ) + })?; + + Ok((StatusCode::ACCEPTED, "restart signal sent".to_string())) + } else { + Ok(( + StatusCode::ACCEPTED, + "restart already triggered".to_string(), + )) + } +} diff --git a/lib/src/diag_device.rs b/lib/src/diag_device.rs index 86932333..f364e1fa 100644 --- a/lib/src/diag_device.rs +++ b/lib/src/diag_device.rs @@ -7,7 +7,7 @@ use crate::log_codes; use deku::prelude::*; use futures::TryStream; -use log::{error, info}; +use log::{debug, error, info}; use std::io::ErrorKind; use std::os::fd::AsRawFd; use thiserror::Error; @@ -123,7 +123,7 @@ impl DiagDevice { .map_err(DiagDeviceError::DeviceReadFailed)?; } - info!( + debug!( "Parsing messages container size = {:?} [{:?}]", bytes_read, &self.read_buf[0..bytes_read] From d166dfc13dc8b458021f0945c774da1ad43fb74b Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 3 Jun 2025 01:01:43 +0200 Subject: [PATCH 02/11] get config and set config --- bin/src/config.rs | 8 ++--- bin/src/daemon.rs | 14 ++++++--- bin/src/server.rs | 57 +++++++++++++++++++++++++++++++----- lib/src/analysis/analyzer.rs | 2 +- 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/bin/src/config.rs b/bin/src/config.rs index a8705c1f..045ea001 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs @@ -1,10 +1,10 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use rayhunter::analysis::analyzer::AnalyzerConfig; use crate::error::RayhunterError; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(default)] pub struct Config { pub qmdl_store_path: String, @@ -32,11 +32,11 @@ impl Default for Config { } } -pub fn parse_config

(path: P) -> Result +pub async fn parse_config

(path: P) -> Result where P: AsRef, { - if let Ok(config_file) = std::fs::read_to_string(&path) { + if let Ok(config_file) = tokio::fs::read_to_string(&path).await { Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?) } else { Ok(Config::default()) diff --git a/bin/src/daemon.rs b/bin/src/daemon.rs index f0754ac3..6749f212 100644 --- a/bin/src/daemon.rs +++ b/bin/src/daemon.rs @@ -20,7 +20,7 @@ use crate::diag::run_diag_read_thread; use crate::error::RayhunterError; use crate::pcap::get_pcap; use crate::qmdl_store::RecordingStore; -use crate::server::{get_qmdl, restart_daemon, serve_static, ServerState}; +use crate::server::{get_config, get_qmdl, restart_daemon, serve_static, set_config, ServerState}; use crate::stats::{get_qmdl_manifest, get_system_stats}; use analysis::{ @@ -60,6 +60,8 @@ fn get_router() -> AppRouter { .route("/api/analysis", get(get_analysis_status)) .route("/api/analysis/{name}", post(start_analysis)) .route("/api/restart-daemon", post(restart_daemon)) + .route("/api/config", get(get_config)) + .route("/api/config", post(set_config)) .route("/", get(|| async { Redirect::permanent("/index.html") })) .route("/{*path}", get(serve_static)) } @@ -187,8 +189,8 @@ async fn main() -> Result<(), RayhunterError> { let args = parse_args(); loop { - let config = parse_config(&args.config_path)?; - if !run_with_config(&config).await? { + let config = parse_config(&args.config_path).await?; + if !run_with_config(&args, &config).await? { return Ok(()); } @@ -199,7 +201,10 @@ async fn main() -> Result<(), RayhunterError> { } } -async fn run_with_config(config: &config::Config) -> Result { +async fn run_with_config( + args: &config::Args, + config: &config::Config, +) -> Result { // TaskTrackers give us an interface to spawn tokio threads, and then // eventually await all of them ending let task_tracker = TaskTracker::new(); @@ -264,6 +269,7 @@ async fn run_with_config(config: &config::Config) -> Result>, pub diag_device_ctrl_sender: Sender, pub ui_update_sender: Sender, @@ -35,12 +40,15 @@ pub async fn get_qmdl( StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_idx), ))?; - let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await.map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("error opening QMDL file: {}", e), - ) - })?; + let qmdl_file = qmdl_store + .open_entry_qmdl(entry_index) + .await + .map_err(|err| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("error opening QMDL file: {}", err), + ) + })?; let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64); let qmdl_stream = ReaderStream::new(limited_qmdl_file); @@ -84,13 +92,12 @@ pub async fn restart_daemon( let mut restart_tx = state.daemon_restart_tx.write().await; if let Some(sender) = restart_tx.take() { - sender.send(()).map_err(|()| { + sender.send(()).map_err(|_| { ( StatusCode::INTERNAL_SERVER_ERROR, "couldn't send restart signal".to_string(), ) })?; - Ok((StatusCode::ACCEPTED, "restart signal sent".to_string())) } else { Ok(( @@ -99,3 +106,37 @@ pub async fn restart_daemon( )) } } + +pub async fn get_config( + State(state): State>, +) -> Result, (StatusCode, String)> { + let config = parse_config(&state.config_path).await.map_err(|err| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("failed to read config file: {}", err), + ) + })?; + + Ok(Json(config)) +} + +pub async fn set_config( + State(state): State>, + Json(config): Json, +) -> Result<(StatusCode, String), (StatusCode, String)> { + let config_str = toml::to_string_pretty(&config).map_err(|err| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("failed to serialize config as TOML: {}", err), + ) + })?; + + write(&state.config_path, config_str).await.map_err(|err| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("failed to write config file: {}", err), + ) + })?; + + Ok((StatusCode::ACCEPTED, "wrote config".to_string())) +} diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index 54caecb3..6a31bc0e 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -11,7 +11,7 @@ use super::{ null_cipher::NullCipherAnalyzer, priority_2g_downgrade::LteSib6And7DowngradeAnalyzer, }; -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(default)] pub struct AnalyzerConfig { pub imsi_requested: bool, From 9904b74d21e9376b6d0d25d269f39624ab31f2fc Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 19 Jun 2025 00:41:46 +0200 Subject: [PATCH 03/11] basic ui --- bin/web/src/lib/components/ConfigForm.svelte | 279 +++++++++++++++++++ bin/web/src/lib/utils.svelte.ts | 37 +++ bin/web/src/routes/+page.svelte | 2 + 3 files changed, 318 insertions(+) create mode 100644 bin/web/src/lib/components/ConfigForm.svelte diff --git a/bin/web/src/lib/components/ConfigForm.svelte b/bin/web/src/lib/components/ConfigForm.svelte new file mode 100644 index 00000000..d8b8c591 --- /dev/null +++ b/bin/web/src/lib/components/ConfigForm.svelte @@ -0,0 +1,279 @@ + + +

+ + + {#if showConfig} + {#if loading} +
Loading config...
+ {:else if config} +
{ e.preventDefault(); saveConfig(); }}> +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+

Analyzer Settings

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ + + +
+
+ + {#if message} +
+ {message} +
+ {/if} + {:else} +
+ Failed to load configuration. Please try reloading the page. +
+ {/if} + {/if} +
\ No newline at end of file diff --git a/bin/web/src/lib/utils.svelte.ts b/bin/web/src/lib/utils.svelte.ts index a720b084..30a379a0 100644 --- a/bin/web/src/lib/utils.svelte.ts +++ b/bin/web/src/lib/utils.svelte.ts @@ -1,6 +1,24 @@ import { Manifest } from "./manifest.svelte"; import type { SystemStats } from "./systemStats"; +export interface AnalyzerConfig { + imsi_requested: boolean; + connection_redirect_2g_downgrade: boolean; + lte_sib6_and_7_downgrade: boolean; + null_cipher: boolean; +} + +export interface Config { + qmdl_store_path: string; + port: number; + debug_mode: boolean; + ui_level: number; + enable_dummy_analyzer: boolean; + colorblind_mode: boolean; + key_input_mode: number; + analyzers: AnalyzerConfig; +} + export async function req(method: string, url: string): Promise { const response = await fetch(url, { method: method, @@ -21,3 +39,22 @@ export async function get_manifest(): Promise { export async function get_system_stats(): Promise { return JSON.parse(await req('GET', '/api/system-stats')); } + +export async function get_config(): Promise { + return JSON.parse(await req('GET', '/api/config')); +} + +export async function set_config(config: Config): Promise { + const response = await fetch('/api/config', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(config) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(error); + } +} diff --git a/bin/web/src/routes/+page.svelte b/bin/web/src/routes/+page.svelte index f9249095..2d78f6cb 100644 --- a/bin/web/src/routes/+page.svelte +++ b/bin/web/src/routes/+page.svelte @@ -8,6 +8,7 @@ import SystemStatsTable from "$lib/components/SystemStatsTable.svelte"; import DeleteAllButton from "$lib/components/DeleteAllButton.svelte"; import RecordingControls from "$lib/components//RecordingControls.svelte"; + import ConfigForm from "$lib/components/ConfigForm.svelte"; let manager: AnalysisManager = new AnalysisManager(); let loaded = $state(false); @@ -75,6 +76,7 @@ + {:else}
From 5e5514a11f541b19ff6b6070e6a59c38504e45e4 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 19 Jun 2025 19:22:07 +0200 Subject: [PATCH 04/11] Fix key_input blocking shutdown Discovered in #351 where restart would hang forever. key_input.rs never properly implemented shutdown because it didn't have to do anything interesting on shutdown. Wire up oneshot channels so that it falls in line with other services. I do wonder though if there's a more clever way of handling this. For example I could just not use the task_tracker, use tokio::spawn and let the task get cancelled by tokio. --- bin/src/daemon.rs | 20 +++++++++++++++----- bin/src/key_input.rs | 18 ++++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/bin/src/daemon.rs b/bin/src/daemon.rs index 6749f212..9b6cce90 100644 --- a/bin/src/daemon.rs +++ b/bin/src/daemon.rs @@ -131,6 +131,7 @@ fn run_shutdown_thread( should_restart_flag: Arc, server_shutdown_tx: oneshot::Sender<()>, maybe_ui_shutdown_tx: Option>, + maybe_key_input_shutdown_tx: Option>, qmdl_store_lock: Arc>, analysis_tx: Sender, ) -> JoinHandle> { @@ -164,11 +165,11 @@ fn run_shutdown_thread( server_shutdown_tx .send(()) .expect("couldn't send server shutdown signal"); - info!("sending UI shutdown"); if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx { - ui_shutdown_tx - .send(()) - .expect("couldn't send ui shutdown signal"); + let _ = ui_shutdown_tx.send(()); + } + if let Some(key_input_shutdown_tx) = maybe_key_input_shutdown_tx { + let _ = key_input_shutdown_tx.send(()); } diag_device_sender .send(DiagDeviceCtrlMessage::Exit) @@ -217,6 +218,7 @@ async fn run_with_config( let (ui_update_tx, ui_update_rx) = mpsc::channel::(1); let (analysis_tx, analysis_rx) = mpsc::channel::(5); let mut maybe_ui_shutdown_tx = None; + let mut maybe_key_input_shutdown_tx = None; if !config.debug_mode { let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel(); maybe_ui_shutdown_tx = Some(ui_shutdown_tx); @@ -242,7 +244,14 @@ async fn run_with_config( display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); info!("Starting Key Input service"); - key_input::run_key_input_thread(&task_tracker, &config, diag_tx.clone()); + let (key_input_shutdown_tx, key_input_shutdown_rx) = oneshot::channel(); + maybe_key_input_shutdown_tx = Some(key_input_shutdown_tx); + key_input::run_key_input_thread( + &task_tracker, + &config, + diag_tx.clone(), + key_input_shutdown_rx, + ); } let (daemon_restart_tx, daemon_restart_rx) = oneshot::channel::<()>(); @@ -265,6 +274,7 @@ async fn run_with_config( should_restart_flag.clone(), server_shutdown_tx, maybe_ui_shutdown_tx, + maybe_key_input_shutdown_tx, qmdl_store_lock.clone(), analysis_tx.clone(), ); diff --git a/bin/src/key_input.rs b/bin/src/key_input.rs index 3577de43..765ace11 100644 --- a/bin/src/key_input.rs +++ b/bin/src/key_input.rs @@ -1,8 +1,9 @@ -use log::error; +use log::{error, info}; use std::time::{Duration, Instant}; use tokio::fs::File; use tokio::io::AsyncReadExt; use tokio::sync::mpsc::Sender; +use tokio::sync::oneshot; use tokio_util::task::TaskTracker; use crate::config; @@ -20,6 +21,7 @@ pub fn run_key_input_thread( task_tracker: &TaskTracker, config: &config::Config, diag_tx: Sender, + mut ui_shutdown_rx: oneshot::Receiver<()>, ) { if config.key_input_mode == 0 { return; @@ -40,9 +42,17 @@ pub fn run_key_input_thread( let mut last_event_time: Option = None; loop { - if let Err(e) = file.read_exact(&mut buffer).await { - error!("failed to read key input: {}", e); - return; + tokio::select! { + _ = &mut ui_shutdown_rx => { + info!("received key input shutdown"); + return; + } + result = file.read_exact(&mut buffer) => { + if let Err(e) = result { + error!("failed to read key input: {}", e); + return; + } + } } let event = parse_event(buffer); From b825174a07ec9e718b38aa75610de8675f7de590 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 23 Jun 2025 21:03:01 +0200 Subject: [PATCH 05/11] Apply suggestions from code review Co-authored-by: Will Greenberg --- bin/web/src/lib/components/ConfigForm.svelte | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bin/web/src/lib/components/ConfigForm.svelte b/bin/web/src/lib/components/ConfigForm.svelte index d8b8c591..6697f3d6 100644 --- a/bin/web/src/lib/components/ConfigForm.svelte +++ b/bin/web/src/lib/components/ConfigForm.svelte @@ -80,7 +80,7 @@ {#if showConfig} {#if loading}
Loading config...
- {:else if config} + {:else if config}
{ e.preventDefault(); saveConfig(); }}>
@@ -249,7 +249,7 @@ +
+ - -
-
- - {#if message} -
- {message} -
- {/if} - {:else} + +
+ + {#if message} +
+ {message} +
+ {/if} + {:else}
Failed to load configuration. Please try reloading the page.
{/if} {/if} - \ No newline at end of file + From 21142069091ab95a44e530ec679653c0d5ed424b Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 23 Jun 2025 21:24:04 +0200 Subject: [PATCH 07/11] Remove advanced options --- bin/web/src/lib/components/ConfigForm.svelte | 50 -------------------- bin/web/src/lib/utils.svelte.ts | 4 -- 2 files changed, 54 deletions(-) diff --git a/bin/web/src/lib/components/ConfigForm.svelte b/bin/web/src/lib/components/ConfigForm.svelte index cd334e5d..a489aa57 100644 --- a/bin/web/src/lib/components/ConfigForm.svelte +++ b/bin/web/src/lib/components/ConfigForm.svelte @@ -82,32 +82,6 @@
Loading config...
{:else if config}
{ e.preventDefault(); saveConfig(); }}> -
- - -
- -
- - -
-
-
- - -
- -
- - -
-
Date: Mon, 23 Jun 2025 22:47:34 +0200 Subject: [PATCH 08/11] Add exponential backoff --- bin/src/daemon.rs | 7 ------ lib/src/diag_device.rs | 48 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/bin/src/daemon.rs b/bin/src/daemon.rs index 9b6cce90..54358867 100644 --- a/bin/src/daemon.rs +++ b/bin/src/daemon.rs @@ -13,7 +13,6 @@ mod stats; use std::net::SocketAddr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use std::time::Duration; use crate::config::{parse_args, parse_config}; use crate::diag::run_diag_read_thread; @@ -41,7 +40,6 @@ use tokio::select; use tokio::sync::mpsc::{self, Sender}; use tokio::sync::{oneshot, RwLock}; use tokio::task::JoinHandle; -use tokio::time::sleep; use tokio_util::task::TaskTracker; type AppRouter = Router>; @@ -194,11 +192,6 @@ async fn main() -> Result<(), RayhunterError> { if !run_with_config(&args, &config).await? { return Ok(()); } - - // For some reason the diag device needs a very long time to become available again within - // the same process, on TP-Link M7350 v3. While process restart would reset it faster. - println!("Restarting Rayhunter. Waiting for 5 seconds..."); - sleep(Duration::from_secs(5)).await; } } diff --git a/lib/src/diag_device.rs b/lib/src/diag_device.rs index f364e1fa..14aec89e 100644 --- a/lib/src/diag_device.rs +++ b/lib/src/diag_device.rs @@ -10,9 +10,11 @@ use futures::TryStream; use log::{debug, error, info}; use std::io::ErrorKind; use std::os::fd::AsRawFd; +use std::time::Duration; use thiserror::Error; use tokio::fs::File; use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::time::sleep; pub type DiagResult = Result; @@ -85,6 +87,52 @@ pub struct DiagDevice { impl DiagDevice { pub async fn new() -> DiagResult { + Self::new_with_retries(Duration::from_secs(30)).await + } + + pub async fn new_with_retries(max_duration: Duration) -> DiagResult { + // For some reason the diag device needs a very long time to become available again with in + // the same process, on TP-Link M7350 v3. While process restart would reset it faster. + + let start_time = std::time::Instant::now(); + let max_delay = Duration::from_secs(5); + + let mut delay = Duration::from_millis(100); + let mut num_retries = 0; + + loop { + match Self::try_new().await { + Ok(device) => { + info!( + "Diag device initialization succeeded after {} retries", + num_retries + ); + return Ok(device); + } + Err(e) => { + num_retries += 1; + if start_time.elapsed() >= max_duration { + error!( + "Failed to initialize diag device after {:?}: {}", + max_duration, e + ); + return Err(e); + } + + info!( + "Diag device initialization failed {} times, retrying in {:?}: {}", + num_retries, delay, e + ); + sleep(delay).await; + + // Exponential backoff + delay = std::cmp::min(delay * 2, max_delay); + } + } + } + } + + async fn try_new() -> DiagResult { let diag_file = File::options() .read(true) .write(true) From a21c9af354083ea6cb6bd916b401baaedb892a5f Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 24 Jun 2025 01:11:50 +0200 Subject: [PATCH 09/11] Restart when config is set --- bin/src/config.rs | 2 +- bin/src/daemon.rs | 17 +++---- bin/src/diag.rs | 8 +-- bin/src/server.rs | 53 ++++++++------------ bin/web/src/lib/components/ConfigForm.svelte | 38 +------------- 5 files changed, 36 insertions(+), 82 deletions(-) diff --git a/bin/src/config.rs b/bin/src/config.rs index 045ea001..5d3ffece 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs @@ -4,7 +4,7 @@ use rayhunter::analysis::analyzer::AnalyzerConfig; use crate::error::RayhunterError; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(default)] pub struct Config { pub qmdl_store_path: String, diff --git a/bin/src/daemon.rs b/bin/src/daemon.rs index 54358867..4a9817da 100644 --- a/bin/src/daemon.rs +++ b/bin/src/daemon.rs @@ -19,7 +19,7 @@ use crate::diag::run_diag_read_thread; use crate::error::RayhunterError; use crate::pcap::get_pcap; use crate::qmdl_store::RecordingStore; -use crate::server::{get_config, get_qmdl, restart_daemon, serve_static, set_config, ServerState}; +use crate::server::{get_config, get_qmdl, serve_static, set_config, ServerState}; use crate::stats::{get_qmdl_manifest, get_system_stats}; use analysis::{ @@ -57,7 +57,6 @@ fn get_router() -> AppRouter { .route("/api/analysis-report/{name}", get(get_analysis_report)) .route("/api/analysis", get(get_analysis_status)) .route("/api/analysis/{name}", post(start_analysis)) - .route("/api/restart-daemon", post(restart_daemon)) .route("/api/config", get(get_config)) .route("/api/config", post(set_config)) .route("/", get(|| async { Redirect::permanent("/index.html") })) @@ -69,14 +68,14 @@ fn get_router() -> AppRouter { // (i.e. user hit ctrl+c) async fn run_server( task_tracker: &TaskTracker, - config: &config::Config, state: Arc, server_shutdown_rx: oneshot::Receiver<()>, ) -> JoinHandle<()> { info!("spinning up server"); - let app = get_router().with_state(state); - let addr = SocketAddr::from(([0, 0, 0, 0], config.port)); + let addr = SocketAddr::from(([0, 0, 0, 0], state.config.port)); let listener = TcpListener::bind(&addr).await.unwrap(); + let app = get_router().with_state(state); + task_tracker.spawn(async move { info!("The orca is hunting for stingrays..."); axum::serve(listener, app) @@ -189,7 +188,7 @@ async fn main() -> Result<(), RayhunterError> { loop { let config = parse_config(&args.config_path).await?; - if !run_with_config(&args, &config).await? { + if !run_with_config(&args, config).await? { return Ok(()); } } @@ -197,7 +196,7 @@ async fn main() -> Result<(), RayhunterError> { async fn run_with_config( args: &config::Args, - config: &config::Config, + config: config::Config, ) -> Result { // TaskTrackers give us an interface to spawn tokio threads, and then // eventually await all of them ending @@ -273,15 +272,15 @@ async fn run_with_config( ); let state = Arc::new(ServerState { config_path: args.config_path.clone(), + config, qmdl_store_lock: qmdl_store_lock.clone(), diag_device_ctrl_sender: diag_tx, ui_update_sender: ui_update_tx, - debug_mode: config.debug_mode, analysis_status_lock, analysis_sender: analysis_tx, daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))), }); - run_server(&task_tracker, &config, state, server_shutdown_rx).await; + run_server(&task_tracker, state, server_shutdown_rx).await; task_tracker.close(); task_tracker.wait().await; diff --git a/bin/src/diag.rs b/bin/src/diag.rs index bcfb5019..bf35e6e6 100644 --- a/bin/src/diag.rs +++ b/bin/src/diag.rs @@ -158,7 +158,7 @@ pub fn run_diag_read_thread( pub async fn start_recording( State(state): State>, ) -> Result<(StatusCode, String), (StatusCode, String)> { - if state.debug_mode { + if state.config.debug_mode { return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); } @@ -179,7 +179,7 @@ pub async fn start_recording( pub async fn stop_recording( State(state): State>, ) -> Result<(StatusCode, String), (StatusCode, String)> { - if state.debug_mode { + if state.config.debug_mode { return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); } state @@ -199,7 +199,7 @@ pub async fn delete_recording( State(state): State>, Path(qmdl_name): Path, ) -> Result<(StatusCode, String), (StatusCode, String)> { - if state.debug_mode { + if state.config.debug_mode { return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); } let mut qmdl_store = state.qmdl_store_lock.write().await; @@ -244,7 +244,7 @@ pub async fn delete_recording( pub async fn delete_all_recordings( State(state): State>, ) -> Result<(StatusCode, String), (StatusCode, String)> { - if state.debug_mode { + if state.config.debug_mode { return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); } state diff --git a/bin/src/server.rs b/bin/src/server.rs index 71199993..e90c89cd 100644 --- a/bin/src/server.rs +++ b/bin/src/server.rs @@ -14,19 +14,18 @@ use tokio::sync::{oneshot, RwLock}; use tokio_util::io::ReaderStream; use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus}; -use crate::config::parse_config; use crate::config::Config; use crate::qmdl_store::RecordingStore; use crate::{display, DiagDeviceCtrlMessage}; pub struct ServerState { pub config_path: String, + pub config: Config, pub qmdl_store_lock: Arc>, pub diag_device_ctrl_sender: Sender, pub ui_update_sender: Sender, pub analysis_status_lock: Arc>, pub analysis_sender: Sender, - pub debug_mode: bool, pub daemon_restart_tx: Arc>>>, } @@ -86,38 +85,10 @@ pub async fn serve_static( } } -pub async fn restart_daemon( - State(state): State>, -) -> Result<(StatusCode, String), (StatusCode, String)> { - let mut restart_tx = state.daemon_restart_tx.write().await; - - if let Some(sender) = restart_tx.take() { - sender.send(()).map_err(|_| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "couldn't send restart signal".to_string(), - ) - })?; - Ok((StatusCode::ACCEPTED, "restart signal sent".to_string())) - } else { - Ok(( - StatusCode::ACCEPTED, - "restart already triggered".to_string(), - )) - } -} - pub async fn get_config( State(state): State>, ) -> Result, (StatusCode, String)> { - let config = parse_config(&state.config_path).await.map_err(|err| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("failed to read config file: {}", err), - ) - })?; - - Ok(Json(config)) + Ok(Json(state.config.clone())) } pub async fn set_config( @@ -138,5 +109,23 @@ pub async fn set_config( ) })?; - Ok((StatusCode::ACCEPTED, "wrote config".to_string())) + // Trigger daemon restart after writing config + let mut restart_tx = state.daemon_restart_tx.write().await; + if let Some(sender) = restart_tx.take() { + sender.send(()).map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "couldn't send restart signal".to_string(), + ) + })?; + Ok(( + StatusCode::ACCEPTED, + "wrote config and triggered restart".to_string(), + )) + } else { + Ok(( + StatusCode::ACCEPTED, + "wrote config but restart already triggered".to_string(), + )) + } } diff --git a/bin/web/src/lib/components/ConfigForm.svelte b/bin/web/src/lib/components/ConfigForm.svelte index a489aa57..4c7819e3 100644 --- a/bin/web/src/lib/components/ConfigForm.svelte +++ b/bin/web/src/lib/components/ConfigForm.svelte @@ -5,7 +5,6 @@ let loading = $state(false); let saving = $state(false); - let restarting = $state(false); let message = $state(""); let messageType = $state<"success" | "error" | null>(null); let showConfig = $state(false); @@ -30,7 +29,7 @@ try { saving = true; await set_config(config); - message = "Config saved successfully!"; + message = "Config saved successfully! Rayhunter is restarting now. Reload the page in a few seconds."; messageType = "success"; } catch (error) { message = `Failed to save config: ${error}`; @@ -40,23 +39,6 @@ } } - async function restartRayhunter() { - if (!window.confirm('Are you sure you want to restart Rayhunter? This will temporarily stop all monitoring.')) { - return; - } - - try { - restarting = true; - await fetch('/api/restart-daemon', { method: 'POST' }); - message = "Rayhunter restart initiated!"; - messageType = "success"; - } catch (error) { - message = `Failed to restart Rayhunter: ${error}`; - messageType = "error"; - } finally { - restarting = false; - } - } // Load config when first shown $effect(() => { @@ -192,26 +174,10 @@ - Save Config + Apply and restart {/if} -
{#if message} From 2cba26a4ccdf9b1432588bc4b48837ebfff29c81 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 24 Jun 2025 01:25:01 +0200 Subject: [PATCH 10/11] Remove unused import --- bin/src/server.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/src/server.rs b/bin/src/server.rs index 11cbee1b..bf58b6d8 100644 --- a/bin/src/server.rs +++ b/bin/src/server.rs @@ -233,7 +233,6 @@ mod tests { use super::*; use async_zip::base::read::mem::ZipFileReader; use axum::extract::{Path, State}; - use std::io::Cursor; use tempfile::TempDir; async fn create_test_qmdl_store() -> (TempDir, Arc>) { From 2ececf9c581c6c18170f2ebed9462b2d24b32f7c Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 24 Jun 2025 02:09:22 +0200 Subject: [PATCH 11/11] Fix tests --- bin/src/server.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/src/server.rs b/bin/src/server.rs index bf58b6d8..3e1c5851 100644 --- a/bin/src/server.rs +++ b/bin/src/server.rs @@ -287,11 +287,14 @@ mod tests { }; Arc::new(ServerState { + config_path: "/tmp/test_config.toml".to_string(), + config: Config::default(), qmdl_store_lock: store_lock, diag_device_ctrl_sender: tx, ui_update_sender: ui_tx, analysis_status_lock: Arc::new(RwLock::new(analysis_status)), analysis_sender: analysis_tx, + daemon_restart_tx: Arc::new(RwLock::new(None)), }) }