diff --git a/bin/src/config.rs b/bin/src/config.rs
index a8705c1..5d3ffec 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, Clone, 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 44d4059..05d3bd0 100644
--- a/bin/src/daemon.rs
+++ b/bin/src/daemon.rs
@@ -10,13 +10,17 @@ mod qmdl_store;
mod server;
mod stats;
+use std::net::SocketAddr;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+
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, get_zip, serve_static, ServerState};
-use crate::stats::get_system_stats;
+use crate::server::{get_config, get_qmdl, get_zip, serve_static, set_config, ServerState};
+use crate::stats::{get_qmdl_manifest, get_system_stats};
use analysis::{
get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus,
@@ -31,10 +35,8 @@ 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;
@@ -56,6 +58,8 @@ 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/config", get(get_config))
+ .route("/api/config", post(set_config))
.route("/", get(|| async { Redirect::permanent("/index.html") }))
.route("/{*path}", get(serve_static))
}
@@ -65,14 +69,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)
@@ -118,46 +122,61 @@ 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>,
+ maybe_key_input_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");
+ should_restart_flag.store(false, Ordering::Relaxed);
+ }
+ res = daemon_restart_rx => {
+ if let Err(err) = res {
+ error!("Unable to listen for shutdown signal: {}", err);
}
- 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");
- }
- Err(err) => {
- 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");
+ if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx {
+ 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)
+ .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(())
})
}
@@ -167,8 +186,19 @@ 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).await?;
+ if !run_with_config(&args, config).await? {
+ return Ok(());
+ }
+ }
+}
+
+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();
@@ -181,6 +211,7 @@ async fn main() -> Result<(), RayhunterError> {
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);
@@ -206,10 +237,18 @@ async fn main() -> Result<(), RayhunterError> {
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::<()>();
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,
@@ -219,29 +258,36 @@ 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,
+ maybe_key_input_shutdown_tx,
qmdl_store_lock.clone(),
analysis_tx.clone(),
);
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;
info!("see you space cowboy...");
- Ok(())
+ Ok(should_restart_flag.load(Ordering::Relaxed))
}
#[cfg(test)]
diff --git a/bin/src/diag.rs b/bin/src/diag.rs
index bcfb501..bf35e6e 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/key_input.rs b/bin/src/key_input.rs
index 3577de4..765ace1 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);
diff --git a/bin/src/server.rs b/bin/src/server.rs
index 9d460e5..3e1c585 100644
--- a/bin/src/server.rs
+++ b/bin/src/server.rs
@@ -8,27 +8,32 @@ use axum::extract::State;
use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE};
use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
+use axum::Json;
use include_dir::{include_dir, Dir};
use log::error;
use std::sync::Arc;
+use tokio::fs::write;
use tokio::io::{copy, duplex, AsyncReadExt};
use tokio::sync::mpsc::Sender;
-use tokio::sync::RwLock;
+use tokio::sync::{oneshot, RwLock};
use tokio_util::compat::FuturesAsyncWriteCompatExt;
use tokio_util::io::ReaderStream;
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
+use crate::config::Config;
use crate::pcap::generate_pcap_data;
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>>>,
}
pub async fn get_qmdl(
@@ -41,12 +46,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,6 +92,51 @@ pub async fn serve_static(
}
}
+pub async fn get_config(
+ State(state): State>,
+) -> Result, (StatusCode, String)> {
+ Ok(Json(state.config.clone()))
+}
+
+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),
+ )
+ })?;
+
+ // 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(),
+ ))
+ }
+}
+
pub async fn get_zip(
State(state): State>,
Path(entry_name): Path,
@@ -180,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>) {
@@ -235,12 +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,
- debug_mode: true,
+ daemon_restart_tx: Arc::new(RwLock::new(None)),
})
}
diff --git a/bin/web/src/lib/components/ConfigForm.svelte b/bin/web/src/lib/components/ConfigForm.svelte
new file mode 100644
index 0000000..4c7819e
--- /dev/null
+++ b/bin/web/src/lib/components/ConfigForm.svelte
@@ -0,0 +1,194 @@
+
+
+
+
+
+ {#if showConfig}
+ {#if loading}
+
Loading config...
+ {:else if config}
+
+ {#if message}
+
+ {message}
+
+ {/if}
+ {:else}
+
+ Failed to load configuration. Please try reloading the page.
+
+ {/if}
+ {/if}
+
diff --git a/bin/web/src/lib/utils.svelte.ts b/bin/web/src/lib/utils.svelte.ts
index a720b08..7fdc0bf 100644
--- a/bin/web/src/lib/utils.svelte.ts
+++ b/bin/web/src/lib/utils.svelte.ts
@@ -1,6 +1,20 @@
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 {
+ ui_level: number;
+ 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 +35,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 f924909..2d78f6c 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}

diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs
index 54caecb..6a31bc0 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,
diff --git a/lib/src/diag_device.rs b/lib/src/diag_device.rs
index 8693233..14aec89 100644
--- a/lib/src/diag_device.rs
+++ b/lib/src/diag_device.rs
@@ -7,12 +7,14 @@ 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 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)
@@ -123,7 +171,7 @@ impl DiagDevice {
.map_err(DiagDeviceError::DeviceReadFailed)?;
}
- info!(
+ debug!(
"Parsing messages container size = {:?} [{:?}]",
bytes_read,
&self.read_buf[0..bytes_read]