From c697773244ad88100a53e0cc9fd6549248ed90dc Mon Sep 17 00:00:00 2001 From: Andrej Date: Sat, 2 Aug 2025 20:58:19 -0400 Subject: [PATCH 01/26] uz801: Add initial (experimental) support --- daemon/src/display/mod.rs | 1 + daemon/src/display/uz801.rs | 81 +++++++++++++++ daemon/src/main.rs | 1 + daemon/src/stats.rs | 119 ++++++++++++++++------ installer/src/main.rs | 18 ++++ installer/src/uz801.rs | 197 ++++++++++++++++++++++++++++++++++++ lib/src/lib.rs | 1 + 7 files changed, 388 insertions(+), 30 deletions(-) create mode 100644 daemon/src/display/uz801.rs create mode 100644 installer/src/uz801.rs diff --git a/daemon/src/display/mod.rs b/daemon/src/display/mod.rs index 6f19ca5..ce06324 100644 --- a/daemon/src/display/mod.rs +++ b/daemon/src/display/mod.rs @@ -6,6 +6,7 @@ pub mod tmobile; pub mod tplink; pub mod tplink_framebuffer; pub mod tplink_onebit; +pub mod uz801; pub mod wingtech; #[derive(Clone, Copy, PartialEq)] diff --git a/daemon/src/display/uz801.rs b/daemon/src/display/uz801.rs new file mode 100644 index 0000000..1810029 --- /dev/null +++ b/daemon/src/display/uz801.rs @@ -0,0 +1,81 @@ +/// Display module for Uz801, light LEDs on the front of the device. +/// DisplayState::Recording => Signal LED is solid blue (wifi LED). +/// DisplayState::Paused => Green LED is solid. +/// DisplayState::WarningDetected => Signal LED is solid red. +use log::{error, info}; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tokio_util::task::TaskTracker; + +use std::time::Duration; + +use crate::config; +use crate::display::DisplayState; + +macro_rules! led { + ($l:expr) => {{ format!("/sys/class/leds/{}/brightness", $l) }}; +} + +async fn led_on(path: String) { + tokio::fs::write(&path, "1").await.ok(); +} + +async fn led_off(path: String) { + tokio::fs::write(&path, "0").await.ok(); +} + +pub fn update_ui( + task_tracker: &TaskTracker, + config: &config::Config, + mut ui_shutdown_rx: oneshot::Receiver<()>, + mut ui_update_rx: mpsc::Receiver, +) { + let mut invisible: bool = false; + if config.ui_level == 0 { + info!("Invisible mode, not spawning UI."); + invisible = true; + } + task_tracker.spawn(async move { + let mut state = DisplayState::Recording; + let mut last_state = DisplayState::Paused; + + loop { + match ui_shutdown_rx.try_recv() { + Ok(_) => { + info!("received UI shutdown"); + break; + } + Err(oneshot::error::TryRecvError::Empty) => {} + Err(e) => panic!("error receiving shutdown message: {e}"), + } + match ui_update_rx.try_recv() { + Ok(new_state) => state = new_state, + Err(mpsc::error::TryRecvError::Empty) => {} + Err(e) => error!("error receiving ui update message: {e}"), + }; + if invisible || state == last_state { + tokio::time::sleep(Duration::from_secs(1)).await; + continue; + } + match state { + DisplayState::Paused => { + led_off(led!("red")).await; + led_off(led!("green")).await; + led_on(led!("wifi")).await; + } + DisplayState::Recording => { + led_off(led!("red")).await; + led_off(led!("wifi")).await; + led_on(led!("green")).await; + } + DisplayState::WarningDetected => { + led_off(led!("green")).await; + led_off(led!("wifi")).await; + led_on(led!("red")).await; + } + } + last_state = state; + tokio::time::sleep(Duration::from_secs(1)).await; + } + }); +} diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 31c4ec5..f1f17be 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -243,6 +243,7 @@ async fn run_with_config( Device::Tmobile => display::tmobile::update_ui, Device::Wingtech => display::wingtech::update_ui, Device::Pinephone => display::headless::update_ui, + Device::Uz801 => display::uz801::update_ui }; update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); diff --git a/daemon/src/stats.rs b/daemon/src/stats.rs index 6fdcfb4..c645420 100644 --- a/daemon/src/stats.rs +++ b/daemon/src/stats.rs @@ -7,7 +7,7 @@ use axum::Json; use axum::extract::State; use axum::http::StatusCode; use log::error; -use rayhunter::util::RuntimeMetadata; +use rayhunter::{Device, util::RuntimeMetadata}; use serde::Serialize; use tokio::process::Command; @@ -19,10 +19,10 @@ pub struct SystemStats { } impl SystemStats { - pub async fn new(qmdl_path: &str) -> Result { + pub async fn new(qmdl_path: &str, device: &Device) -> Result { Ok(Self { - disk_stats: DiskStats::new(qmdl_path).await?, - memory_stats: MemoryStats::new().await?, + disk_stats: DiskStats::new(qmdl_path, device).await?, + memory_stats: MemoryStats::new(device).await?, runtime_metadata: RuntimeMetadata::new(), }) } @@ -40,21 +40,46 @@ pub struct DiskStats { impl DiskStats { // runs "df -h " to get storage statistics for the partition containing - // the QMDL file - pub async fn new(qmdl_path: &str) -> Result { + // the QMDL file. The Uz801 device doesn't support the -h flag, so we skip it for that device. + pub async fn new(qmdl_path: &str, device: &Device) -> Result { let mut df_cmd = Command::new("df"); - df_cmd.arg("-h"); + // Only add -h flag for devices other than Uz801, as Uz801's df doesn't support it + if !matches!(device, Device::Uz801) { + df_cmd.arg("-h"); + } df_cmd.arg(qmdl_path); let stdout = get_cmd_output(df_cmd).await?; - let mut parts = stdout.split_whitespace().skip(7).to_owned(); - Ok(Self { - partition: parts.next().ok_or("error parsing df output")?.to_string(), - total_size: parts.next().ok_or("error parsing df output")?.to_string(), - used_size: parts.next().ok_or("error parsing df output")?.to_string(), - available_size: parts.next().ok_or("error parsing df output")?.to_string(), - used_percent: parts.next().ok_or("error parsing df output")?.to_string(), - mounted_on: parts.next().ok_or("error parsing df output")?.to_string(), - }) + + if matches!(device, Device::Uz801) { + // Handle Uz801 format: + // Filesystem Size Used Free Blksize + // /data/rayhunter/ 774.9M 68.0M 706.9M 4096 + let lines: Vec<&str> = stdout.lines().collect(); + if lines.len() < 2 { + return Err("error parsing df output: insufficient lines".to_string()); + } + let data_line = lines[1]; + let mut parts = data_line.split_whitespace(); + Ok(Self { + partition: parts.next().ok_or("error parsing df output: missing filesystem")?.to_string(), + total_size: parts.next().ok_or("error parsing df output: missing size")?.to_string(), + used_size: parts.next().ok_or("error parsing df output: missing used")?.to_string(), + available_size: parts.next().ok_or("error parsing df output: missing free")?.to_string(), + used_percent: "N/A".to_string(), // Uz801 df doesn't provide percentage + mounted_on: qmdl_path.to_string(), // Use the path we queried + }) + } else { + // Handle standard df -h format + let mut parts = stdout.split_whitespace().skip(7); + Ok(Self { + partition: parts.next().ok_or("error parsing df output")?.to_string(), + total_size: parts.next().ok_or("error parsing df output")?.to_string(), + used_size: parts.next().ok_or("error parsing df output")?.to_string(), + available_size: parts.next().ok_or("error parsing df output")?.to_string(), + used_percent: parts.next().ok_or("error parsing df output")?.to_string(), + mounted_on: parts.next().ok_or("error parsing df output")?.to_string(), + }) + } } } @@ -83,19 +108,53 @@ async fn get_cmd_output(mut cmd: Command) -> Result { } impl MemoryStats { - // runs "free -k" and parses the output to retrieve memory stats - pub async fn new() -> Result { - let mut free_cmd = Command::new("free"); - free_cmd.arg("-k"); - let stdout = get_cmd_output(free_cmd).await?; - let mut numbers = stdout - .split_whitespace() - .flat_map(|part| part.parse::()); - Ok(Self { - total: humanize_kb(numbers.next().ok_or("error parsing free output")?), - used: humanize_kb(numbers.next().ok_or("error parsing free output")?), - free: humanize_kb(numbers.next().ok_or("error parsing free output")?), - }) + // runs "free -k" and parses the output to retrieve memory stats for most devices, + // or reads /proc/meminfo for Uz801 which doesn't have the free command + pub async fn new(device: &Device) -> Result { + if matches!(device, Device::Uz801) { + // Read /proc/meminfo for Uz801 + let meminfo_content = tokio::fs::read_to_string("/proc/meminfo") + .await + .map_err(|e| format!("error reading /proc/meminfo: {}", e))?; + + let mut mem_total_kb = None; + let mut mem_free_kb = None; + + for line in meminfo_content.lines() { + if let Some(value_str) = line.strip_prefix("MemTotal:") { + if let Some(kb_str) = value_str.trim().strip_suffix(" kB") { + mem_total_kb = kb_str.trim().parse::().ok(); + } + } else if let Some(value_str) = line.strip_prefix("MemFree:") { + if let Some(kb_str) = value_str.trim().strip_suffix(" kB") { + mem_free_kb = kb_str.trim().parse::().ok(); + } + } + } + + let total = mem_total_kb.ok_or("error parsing MemTotal from /proc/meminfo")?; + let free = mem_free_kb.ok_or("error parsing MemFree from /proc/meminfo")?; + let used = total - free; + + Ok(Self { + total: humanize_kb(total), + used: humanize_kb(used), + free: humanize_kb(free), + }) + } else { + // Use free command for other devices + let mut free_cmd = Command::new("free"); + free_cmd.arg("-k"); + let stdout = get_cmd_output(free_cmd).await?; + let mut numbers = stdout + .split_whitespace() + .flat_map(|part| part.parse::()); + Ok(Self { + total: humanize_kb(numbers.next().ok_or("error parsing free output")?), + used: humanize_kb(numbers.next().ok_or("error parsing free output")?), + free: humanize_kb(numbers.next().ok_or("error parsing free output")?), + }) + } } } @@ -111,7 +170,7 @@ pub async fn get_system_stats( State(state): State>, ) -> Result, (StatusCode, String)> { let qmdl_store = state.qmdl_store_lock.read().await; - match SystemStats::new(qmdl_store.path.to_str().unwrap()).await { + match SystemStats::new(qmdl_store.path.to_str().unwrap(), &state.config.device).await { Ok(stats) => Ok(Json(stats)), Err(err) => { error!("error getting system stats: {err}"); diff --git a/installer/src/main.rs b/installer/src/main.rs index 620c2ee..b4fb351 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -6,6 +6,7 @@ mod orbic; mod pinephone; mod tmobile; mod tplink; +mod uz801; mod util; mod wingtech; @@ -27,6 +28,8 @@ enum Command { Orbic(InstallOrbic), /// Install rayhunter on the TMobile TMOHS1. Tmobile(TmobileArgs), + /// Install rayhunter on the Uz801. + Uz801(Uz801Args), /// Install rayhunter on a PinePhone's Quectel modem. Pinephone(InstallPinephone), /// Install rayhunter on the TP-Link M7350. @@ -82,6 +85,8 @@ enum UtilSubCommand { TmobileStartAdb(TmobileArgs), /// Root the Tmobile and launch telnetd. TmobileStartTelnet(TmobileArgs), + /// Root the Uz801 and launch adb. + Uz801StartAdb(Uz801Args), /// Root the tplink and launch telnetd. TplinkStartTelnet(TplinkStartTelnet), /// Root the Wingtech and launch telnetd. @@ -115,6 +120,17 @@ struct TmobileArgs { admin_password: String, } +#[derive(Parser, Debug)] +struct Uz801Args { + /// IP address for Uz801 admin interface, if custom. + #[arg(long, default_value = "192.168.0.1")] + admin_ip: String, + + /// Web portal admin password. + #[arg(long)] + admin_password: String, +} + #[derive(Parser, Debug)] struct TplinkStartTelnet { /// IP address for TP-Link admin interface, if custom. @@ -168,6 +184,7 @@ async fn run() -> Result<(), Error> { match command { Command::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, + Command::Uz801(args) => uz801::install(args).await.context("Failed to install rayhunter on the Uz801. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, Command::Pinephone(_) => pinephone::install().await .context("Failed to install rayhunter on the Pinephone's Quectel modem")?, @@ -195,6 +212,7 @@ async fn run() -> Result<(), Error> { UtilSubCommand::Shell => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?, UtilSubCommand::TmobileStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Tmobile TMOHS1")?, UtilSubCommand::TmobileStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Tmobile TMOHS1")?, + UtilSubCommand::Uz801StartAdb(args) => uz801::activate_usb_debug(&args.admin_ip).await.context("\nFailed to activate USB debug on the Uz801")?, UtilSubCommand::TplinkStartTelnet(options) => { tplink::start_telnet(&options.admin_ip).await?; } diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs new file mode 100644 index 0000000..3a2458d --- /dev/null +++ b/installer/src/uz801.rs @@ -0,0 +1,197 @@ +/// Installer for the Uz801 hotspot. +/// +/// Installation process: +/// 1. Use curl to activate USB debugging backdoor +/// 2. Wait for device reboot and ADB availability +/// 3. Use ADB to install rayhunter files +/// 4. Modify startup script to launch rayhunter on boot +use std::time::Duration; + +use anyhow::Result; +use tokio::time::sleep; +use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError}; +use std::io::ErrorKind; + +use crate::Uz801Args as Args; +use crate::util::echo; + +pub async fn install( + Args { + admin_ip, + admin_password: _, // Not used for Uz801 + }: Args, +) -> Result<()> { + run_install(admin_ip).await +} + +async fn run_install(admin_ip: String) -> Result<()> { + echo!("Activating USB debugging backdoor... "); + activate_usb_debug(&admin_ip).await?; + println!("ok"); + + echo!("Waiting for device reboot and ADB connection... "); + let mut adb_device = wait_for_adb().await?; + println!("ok"); + + echo!("Installing rayhunter files... "); + install_rayhunter_files(&mut adb_device).await?; + println!("ok"); + + echo!("Modifying startup script... "); + modify_startup_script(&mut adb_device).await?; + println!("ok"); + + echo!("Starting rayhunter daemon... "); + start_rayhunter(&mut adb_device).await?; + println!("ok"); + + echo!("Testing rayhunter... "); + test_rayhunter(&admin_ip).await?; + println!("ok"); + println!("rayhunter is running at http://{admin_ip}:8080"); + + Ok(()) +} + +pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> { + let url = format!("http://{}/usbdebug.html", admin_ip); + let client = reqwest::Client::new(); + let response = client.get(&url).send().await?; + + if !response.status().is_success() { + anyhow::bail!("Failed to activate USB debug: HTTP {}", response.status()); + } + + Ok(()) +} + +async fn wait_for_adb() -> Result { + const MAX_ATTEMPTS: u32 = 30; // 30 seconds + let mut attempts = 0; + + // Wait a bit for the reboot to start + sleep(Duration::from_secs(5)).await; + + loop { + if attempts >= MAX_ATTEMPTS { + anyhow::bail!("Timeout waiting for ADB connection after USB debug activation"); + } + + match ADBUSBDevice::new(0x05c6, 0x9025) { // Common Qualcomm ADB VID/PID + Ok(mut device) => { + // Test ADB connection + if test_adb_connection(&mut device).await.is_ok() { + return Ok(device); + } + } + Err(RustADBError::DeviceNotFound(_)) => { + // Device not ready yet, continue waiting + } + Err(e) => { + anyhow::bail!("ADB connection error: {}", e); + } + } + + sleep(Duration::from_secs(1)).await; + attempts += 1; + } +} + +async fn test_adb_connection(adb_device: &mut ADBUSBDevice) -> Result<()> { + let mut buf = Vec::::new(); + adb_device.shell_command(&["echo", "test"], &mut buf)?; + let output = String::from_utf8_lossy(&buf); + if output.contains("test") { + Ok(()) + } else { + anyhow::bail!("ADB connection test failed") + } +} + +async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> { + // Create rayhunter directory + let mut buf = Vec::::new(); + adb_device.shell_command(&["mkdir", "-p", "/data/rayhunter"], &mut buf)?; + + // Install rayhunter daemon binary + let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); + let mut daemon_data = rayhunter_daemon_bin.as_slice(); + adb_device.push(&mut daemon_data, "/data/rayhunter/rayhunter-daemon")?; + + // Install config file + let config_content = crate::CONFIG_TOML + .replace("#device = \"orbic\"", "device = \"uz801\""); + let mut config_data = config_content.as_bytes(); + adb_device.push(&mut config_data, "/data/rayhunter/config.toml")?; + + // Make daemon executable + let mut buf = Vec::::new(); + adb_device.shell_command(&["chmod", "755", "/data/rayhunter/rayhunter-daemon"], &mut buf)?; + + Ok(()) +} + +async fn modify_startup_script(adb_device: &mut ADBUSBDevice) -> Result<()> { + // Pull the existing startup script + let mut script_content = Vec::::new(); + adb_device.pull("/system/bin/initmifiservice.sh", &mut script_content)?; + + // Convert to string and add our line + let mut script_str = String::from_utf8_lossy(&script_content).into_owned(); + + // Add rayhunter startup line if not already present + let rayhunter_line = "/data/rayhunter/rayhunter-daemon /data/rayhunter/config.toml &\n"; + if !script_str.contains("/data/rayhunter/rayhunter-daemon") { + script_str.push_str(rayhunter_line); + } + + // Push the modified script back + let mut modified_script = script_str.as_bytes(); + adb_device.push(&mut modified_script, "/system/bin/initmifiservice.sh")?; + + // Make sure it's executable + let mut buf = Vec::::new(); + adb_device.shell_command(&["chmod", "755", "/system/bin/initmifiservice.sh"], &mut buf)?; + + Ok(()) +} + +async fn start_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> { + let mut buf = Vec::::new(); + adb_device.shell_command(&[ + "/data/rayhunter/rayhunter-daemon", + "/data/rayhunter/config.toml", + "&" + ], &mut buf)?; + + // Give it a moment to start + sleep(Duration::from_secs(3)).await; + + Ok(()) +} + +async fn test_rayhunter(admin_ip: &str) -> Result<()> { + const MAX_FAILURES: u32 = 10; + let mut failures = 0; + + let client = reqwest::Client::new(); + + while failures < MAX_FAILURES { + let url = format!("http://{}:8080/index.html", admin_ip); + + if let Ok(response) = client.get(&url).send().await { + if response.status().is_success() { + if let Ok(body) = response.text().await { + if body.contains("html") { + return Ok(()); + } + } + } + } + + failures += 1; + sleep(Duration::from_secs(3)).await; + } + + anyhow::bail!("timeout reached! failed to reach rayhunter, something went wrong :(") +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c53f741..58d9556 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -25,4 +25,5 @@ pub enum Device { Tmobile, Wingtech, Pinephone, + Uz801 } From 6473c05e3ed908d33a95cdb9cb37b57a2f2ccb7c Mon Sep 17 00:00:00 2001 From: Andrej Date: Sat, 2 Aug 2025 20:59:09 -0400 Subject: [PATCH 02/26] uz801: Refactor strings, since `&&'static str` can be coerced into `&dyn AsRef`: `&` --- installer/src/uz801.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 3a2458d..f8134ac 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -116,13 +116,13 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> { // Install rayhunter daemon binary let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); let mut daemon_data = rayhunter_daemon_bin.as_slice(); - adb_device.push(&mut daemon_data, "/data/rayhunter/rayhunter-daemon")?; + adb_device.push(&mut daemon_data, &"/data/rayhunter/rayhunter-daemon")?; // Install config file let config_content = crate::CONFIG_TOML .replace("#device = \"orbic\"", "device = \"uz801\""); let mut config_data = config_content.as_bytes(); - adb_device.push(&mut config_data, "/data/rayhunter/config.toml")?; + adb_device.push(&mut config_data, &"/data/rayhunter/config.toml")?; // Make daemon executable let mut buf = Vec::::new(); @@ -134,7 +134,7 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> { async fn modify_startup_script(adb_device: &mut ADBUSBDevice) -> Result<()> { // Pull the existing startup script let mut script_content = Vec::::new(); - adb_device.pull("/system/bin/initmifiservice.sh", &mut script_content)?; + adb_device.pull(&"/system/bin/initmifiservice.sh", &mut script_content)?; // Convert to string and add our line let mut script_str = String::from_utf8_lossy(&script_content).into_owned(); @@ -147,7 +147,7 @@ async fn modify_startup_script(adb_device: &mut ADBUSBDevice) -> Result<()> { // Push the modified script back let mut modified_script = script_str.as_bytes(); - adb_device.push(&mut modified_script, "/system/bin/initmifiservice.sh")?; + adb_device.push(&mut modified_script, &"/system/bin/initmifiservice.sh")?; // Make sure it's executable let mut buf = Vec::::new(); From 7a053a4f89f1eebbd60965e9f7142d597fa67dcb Mon Sep 17 00:00:00 2001 From: Andrej Date: Sat, 2 Aug 2025 21:03:28 -0400 Subject: [PATCH 03/26] uz801: cargo fmt run --- daemon/src/main.rs | 2 +- daemon/src/stats.rs | 30 +++++++++++----- installer/src/main.rs | 2 +- installer/src/uz801.rs | 77 +++++++++++++++++++++++------------------- lib/src/lib.rs | 2 +- 5 files changed, 67 insertions(+), 46 deletions(-) diff --git a/daemon/src/main.rs b/daemon/src/main.rs index f1f17be..e53cd46 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -243,7 +243,7 @@ async fn run_with_config( Device::Tmobile => display::tmobile::update_ui, Device::Wingtech => display::wingtech::update_ui, Device::Pinephone => display::headless::update_ui, - Device::Uz801 => display::uz801::update_ui + Device::Uz801 => display::uz801::update_ui, }; update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); diff --git a/daemon/src/stats.rs b/daemon/src/stats.rs index c645420..2b0eac0 100644 --- a/daemon/src/stats.rs +++ b/daemon/src/stats.rs @@ -49,7 +49,7 @@ impl DiskStats { } df_cmd.arg(qmdl_path); let stdout = get_cmd_output(df_cmd).await?; - + if matches!(device, Device::Uz801) { // Handle Uz801 format: // Filesystem Size Used Free Blksize @@ -61,10 +61,22 @@ impl DiskStats { let data_line = lines[1]; let mut parts = data_line.split_whitespace(); Ok(Self { - partition: parts.next().ok_or("error parsing df output: missing filesystem")?.to_string(), - total_size: parts.next().ok_or("error parsing df output: missing size")?.to_string(), - used_size: parts.next().ok_or("error parsing df output: missing used")?.to_string(), - available_size: parts.next().ok_or("error parsing df output: missing free")?.to_string(), + partition: parts + .next() + .ok_or("error parsing df output: missing filesystem")? + .to_string(), + total_size: parts + .next() + .ok_or("error parsing df output: missing size")? + .to_string(), + used_size: parts + .next() + .ok_or("error parsing df output: missing used")? + .to_string(), + available_size: parts + .next() + .ok_or("error parsing df output: missing free")? + .to_string(), used_percent: "N/A".to_string(), // Uz801 df doesn't provide percentage mounted_on: qmdl_path.to_string(), // Use the path we queried }) @@ -116,10 +128,10 @@ impl MemoryStats { let meminfo_content = tokio::fs::read_to_string("/proc/meminfo") .await .map_err(|e| format!("error reading /proc/meminfo: {}", e))?; - + let mut mem_total_kb = None; let mut mem_free_kb = None; - + for line in meminfo_content.lines() { if let Some(value_str) = line.strip_prefix("MemTotal:") { if let Some(kb_str) = value_str.trim().strip_suffix(" kB") { @@ -131,11 +143,11 @@ impl MemoryStats { } } } - + let total = mem_total_kb.ok_or("error parsing MemTotal from /proc/meminfo")?; let free = mem_free_kb.ok_or("error parsing MemFree from /proc/meminfo")?; let used = total - free; - + Ok(Self { total: humanize_kb(total), used: humanize_kb(used), diff --git a/installer/src/main.rs b/installer/src/main.rs index b4fb351..cad01e3 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -6,8 +6,8 @@ mod orbic; mod pinephone; mod tmobile; mod tplink; -mod uz801; mod util; +mod uz801; mod wingtech; pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.in"); diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index f8134ac..e43cdf5 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -7,10 +7,10 @@ /// 4. Modify startup script to launch rayhunter on boot use std::time::Duration; -use anyhow::Result; -use tokio::time::sleep; use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError}; +use anyhow::Result; use std::io::ErrorKind; +use tokio::time::sleep; use crate::Uz801Args as Args; use crate::util::echo; @@ -57,27 +57,28 @@ pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> { let url = format!("http://{}/usbdebug.html", admin_ip); let client = reqwest::Client::new(); let response = client.get(&url).send().await?; - + if !response.status().is_success() { anyhow::bail!("Failed to activate USB debug: HTTP {}", response.status()); } - + Ok(()) } async fn wait_for_adb() -> Result { const MAX_ATTEMPTS: u32 = 30; // 30 seconds let mut attempts = 0; - + // Wait a bit for the reboot to start sleep(Duration::from_secs(5)).await; - + loop { if attempts >= MAX_ATTEMPTS { anyhow::bail!("Timeout waiting for ADB connection after USB debug activation"); } - - match ADBUSBDevice::new(0x05c6, 0x9025) { // Common Qualcomm ADB VID/PID + + match ADBUSBDevice::new(0x05c6, 0x9025) { + // Common Qualcomm ADB VID/PID Ok(mut device) => { // Test ADB connection if test_adb_connection(&mut device).await.is_ok() { @@ -91,7 +92,7 @@ async fn wait_for_adb() -> Result { anyhow::bail!("ADB connection error: {}", e); } } - + sleep(Duration::from_secs(1)).await; attempts += 1; } @@ -112,22 +113,24 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> { // Create rayhunter directory let mut buf = Vec::::new(); adb_device.shell_command(&["mkdir", "-p", "/data/rayhunter"], &mut buf)?; - + // Install rayhunter daemon binary let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); let mut daemon_data = rayhunter_daemon_bin.as_slice(); adb_device.push(&mut daemon_data, &"/data/rayhunter/rayhunter-daemon")?; - + // Install config file - let config_content = crate::CONFIG_TOML - .replace("#device = \"orbic\"", "device = \"uz801\""); + let config_content = crate::CONFIG_TOML.replace("#device = \"orbic\"", "device = \"uz801\""); let mut config_data = config_content.as_bytes(); adb_device.push(&mut config_data, &"/data/rayhunter/config.toml")?; - + // Make daemon executable let mut buf = Vec::::new(); - adb_device.shell_command(&["chmod", "755", "/data/rayhunter/rayhunter-daemon"], &mut buf)?; - + adb_device.shell_command( + &["chmod", "755", "/data/rayhunter/rayhunter-daemon"], + &mut buf, + )?; + Ok(()) } @@ -135,50 +138,56 @@ async fn modify_startup_script(adb_device: &mut ADBUSBDevice) -> Result<()> { // Pull the existing startup script let mut script_content = Vec::::new(); adb_device.pull(&"/system/bin/initmifiservice.sh", &mut script_content)?; - + // Convert to string and add our line let mut script_str = String::from_utf8_lossy(&script_content).into_owned(); - + // Add rayhunter startup line if not already present let rayhunter_line = "/data/rayhunter/rayhunter-daemon /data/rayhunter/config.toml &\n"; if !script_str.contains("/data/rayhunter/rayhunter-daemon") { script_str.push_str(rayhunter_line); } - + // Push the modified script back let mut modified_script = script_str.as_bytes(); adb_device.push(&mut modified_script, &"/system/bin/initmifiservice.sh")?; - + // Make sure it's executable let mut buf = Vec::::new(); - adb_device.shell_command(&["chmod", "755", "/system/bin/initmifiservice.sh"], &mut buf)?; - + adb_device.shell_command( + &["chmod", "755", "/system/bin/initmifiservice.sh"], + &mut buf, + )?; + Ok(()) } async fn start_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> { let mut buf = Vec::::new(); - adb_device.shell_command(&[ - "/data/rayhunter/rayhunter-daemon", - "/data/rayhunter/config.toml", - "&" - ], &mut buf)?; - + adb_device.shell_command( + &[ + "/data/rayhunter/rayhunter-daemon", + "/data/rayhunter/config.toml", + "&", + ], + &mut buf, + )?; + // Give it a moment to start sleep(Duration::from_secs(3)).await; - + Ok(()) } async fn test_rayhunter(admin_ip: &str) -> Result<()> { const MAX_FAILURES: u32 = 10; let mut failures = 0; - + let client = reqwest::Client::new(); - + while failures < MAX_FAILURES { let url = format!("http://{}:8080/index.html", admin_ip); - + if let Ok(response) = client.get(&url).send().await { if response.status().is_success() { if let Ok(body) = response.text().await { @@ -188,10 +197,10 @@ async fn test_rayhunter(admin_ip: &str) -> Result<()> { } } } - + failures += 1; sleep(Duration::from_secs(3)).await; } - + anyhow::bail!("timeout reached! failed to reach rayhunter, something went wrong :(") } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 58d9556..a1afa73 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -25,5 +25,5 @@ pub enum Device { Tmobile, Wingtech, Pinephone, - Uz801 + Uz801, } From 6141087f9dbd21c44250071dd49f4f322c1c4355 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 00:41:40 -0400 Subject: [PATCH 04/26] uz801: Added docs --- doc/SUMMARY.md | 1 + doc/uz801.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 doc/uz801.md diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index ddd5114..b62d88d 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -16,6 +16,7 @@ - [TP-Link M7350](./tplink-m7350.md) - [TP-Link M7310](./tplink-m7310.md) - [Tmobile TMOHS1](./tmobile-tmohs1.md) + - [UZ801](./uz801.md) - [Wingtech CT2MHS01](./wingtech-ct2mhs01.md) - [PinePhone and PinePhone Pro](./pinephone.md) - [Support, feedback, and community](./support-feedback-community.md) diff --git a/doc/uz801.md b/doc/uz801.md new file mode 100644 index 0000000..5564dcc --- /dev/null +++ b/doc/uz801.md @@ -0,0 +1,54 @@ +# UZ801 + +The UZ801 is a 4G/LTE USB modem which is built on top of a Qualcomm Snapdragon 410 (MSM8916, with MDM8916 modem.) It does not have a screen, but it does have LEDs which can be used to signal the same status as the green/red bar on the Orbic hotspot. It uses a custom Android-based firmware with limited coreutils. More information about this device can be found [here](https://github.com/AlienWolfX/UZ801-USB_MODEM/wiki/Overview) + +It is worth noting that the CPU has only 2 of the cores enabled on the stock Android-based firmware, probably to avoid overheating as they did not exactly engineer any cooling solution. There is 384MB of RAM on the SoC, and 4GB of eMMC in the form of an SK Hynix NAND flash chip located external to the SoC. + +Rayhunter has been tested on UZ801 devices with firmware supporting USB debugging backdoor access. It is not certain whether all of the sticks that use this board will be compatible with the automated installer, or even with any alternative manual installation method. Please consider sharing your device's firmware version and hardware information [here](https://github.com/EFForg/rayhunter/discussions/479) to help improve compatibility. + +## Supported bands + +The UZ801 supports various LTE bands depending on the specific hardware revision and carrier customization. Check your device specifications for the exact band support. + +The most frequent bands found on these devices are LTE bands 1/3/5/8/20. Research on whether Qualcomm diagnostic tools can be used to write new band support into the NVRAM is pending. + +## Installing +Connect to the UZ801's network over WiFi or USB tethering. + +The UZ801 uses a unique installation process that activates a hidden USB debugging backdoor: + +```sh +./installer uz801 --admin-ip 192.168.100.1 +``` + +Note: The default admin IP for UZ801 is typically `192.168.100.1` instead of `192.168.0.1`. + +The installation process works as follows: +1. Activates the USB debugging backdoor via HTTP request to `/usbdebug.html` +2. Waits for device reboot and ADB availability +3. Uses ADB to install rayhunter files and modify the startup script +4. Launches rayhunter daemon automatically + +## LED modes +| Rayhunter state | LED indicator | +| ---------------- | ------------------- | +| Recording | WiFi (blue) LED solid on| +| Paused | Green LED solid on | +| Warning Detected | Red LED solid on | + +Note: Unlike the TMOHS1, the UZ801 uses solid LED indicators instead of blinking patterns. + +## Obtaining a shell +The UZ801 supports ADB access after the USB debugging backdoor is activated. + +```sh +adb shell +``` + +## Device-specific notes + +- The UZ801 has a limited set of coreutils, so some standard Linux commands may not be available +- The `df` command doesn't support the `-h` flag for human-readable output +- The `free` command is not available; memory information is read from `/proc/meminfo` +- USB debugging must be activated via the web backdoor before ADB access is possible (this is required only once) +- The device uses `/system/bin/initmifiservice.sh` as the main startup script From 28a0c060175d168f2b84fa00d797f336335130c0 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 00:51:35 -0400 Subject: [PATCH 05/26] uz801: Add purchase links to documentation --- doc/uz801.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/doc/uz801.md b/doc/uz801.md index 5564dcc..e891fe5 100644 --- a/doc/uz801.md +++ b/doc/uz801.md @@ -2,15 +2,29 @@ The UZ801 is a 4G/LTE USB modem which is built on top of a Qualcomm Snapdragon 410 (MSM8916, with MDM8916 modem.) It does not have a screen, but it does have LEDs which can be used to signal the same status as the green/red bar on the Orbic hotspot. It uses a custom Android-based firmware with limited coreutils. More information about this device can be found [here](https://github.com/AlienWolfX/UZ801-USB_MODEM/wiki/Overview) -It is worth noting that the CPU has only 2 of the cores enabled on the stock Android-based firmware, probably to avoid overheating as they did not exactly engineer any cooling solution. There is 384MB of RAM on the SoC, and 4GB of eMMC in the form of an SK Hynix NAND flash chip located external to the SoC. +It is worth noting that even though the Snapdragon 410 is a quad-core SoC, the CPU has only 2 of the cores enabled on the stock Android-based firmware, probably to avoid overheating as they did not exactly engineer any cooling solution. Regardless, even with 2 disabled cores there is plenty of compute overhead. There are 384MB of RAM on the SoC, and 4GB of eMMC in the form of an SK Hynix NAND flash chip located external to the SoC. Rayhunter has been tested on UZ801 devices with firmware supporting USB debugging backdoor access. It is not certain whether all of the sticks that use this board will be compatible with the automated installer, or even with any alternative manual installation method. Please consider sharing your device's firmware version and hardware information [here](https://github.com/EFForg/rayhunter/discussions/479) to help improve compatibility. +## Where to purchase + +There are several option to purchase this device: +1. AliExpress: +- [1](https://www.aliexpress.us/item/3256808999940005.html) +- [2](https://www.aliexpress.us/item/3256809191207903.html) +- [3](https://www.aliexpress.us/item/3256809191207903.html) +2. eBay: +- [1](https://www.ebay.com/itm/394512588226) +- [2](https://www.ebay.com/itm/195655408253) +- [3](https://www.ebay.com/itm/116678550086) +3. Amazon: +- [1](https://www.amazon.com/150Mbps-Adapter-Network-Lightweight-Portable/dp/B0DQC64ZFS) +- [2](https://www.amazon.com/Heayzoki-Network-Adapter-Wireless-Connection/dp/B0CG4W31M4) ## Supported bands The UZ801 supports various LTE bands depending on the specific hardware revision and carrier customization. Check your device specifications for the exact band support. -The most frequent bands found on these devices are LTE bands 1/3/5/8/20. Research on whether Qualcomm diagnostic tools can be used to write new band support into the NVRAM is pending. +The most frequent bands found on these devices are LTE bands 1/3/5/8/20. In the US, this means that Verizon's band 5 towers are the only towers that this device could communicate with in its normal usage as an LTE modem. Research on whether Qualcomm diagnostic tools can be used to write new band support into the NVRAM is pending. ## Installing Connect to the UZ801's network over WiFi or USB tethering. From 0540504eea4b28eef93b10aff7423145535a8715 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 10:25:56 -0400 Subject: [PATCH 06/26] uz801: Correct LED definition comment --- daemon/src/display/uz801.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/src/display/uz801.rs b/daemon/src/display/uz801.rs index 1810029..8f6422c 100644 --- a/daemon/src/display/uz801.rs +++ b/daemon/src/display/uz801.rs @@ -1,6 +1,6 @@ /// Display module for Uz801, light LEDs on the front of the device. -/// DisplayState::Recording => Signal LED is solid blue (wifi LED). -/// DisplayState::Paused => Green LED is solid. +/// DisplayState::Recording => Green LED is solid. +/// DisplayState::Paused => Signal LED is solid blue (wifi LED). /// DisplayState::WarningDetected => Signal LED is solid red. use log::{error, info}; use tokio::sync::mpsc; From 38a408757ab8642ef95c1faff345a265b7a686b1 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 10:28:43 -0400 Subject: [PATCH 07/26] Add uz801 to supported devices list --- doc/supported-devices.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/supported-devices.md b/doc/supported-devices.md index db6751e..b41f034 100644 --- a/doc/supported-devices.md +++ b/doc/supported-devices.md @@ -24,6 +24,7 @@ Rayhunter is confirmed to work on these devices. | [Tmobile TMOHS1](./tmobile-tmohs1.md) | Americas | | [TP-Link M7310](./tplink-m7310.md) | Africa, Europe, Middle East | | [PinePhone and PinePhone Pro](./pinephone.md) | Global | +| [FY UZ801](./uz801.md) | Asia, Europe | ## Adding new devices Rayhunter was built and tested primarily on the Orbic RC400L mobile hotspot, but the community has been working hard at adding support for other devices. Theoretically, if a device runs a Qualcomm modem and exposes a `/dev/diag` interface, Rayhunter may work on it. From f57fc611c2135ae2cb3bd248b0d86f5bc7659445 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 10:41:33 -0400 Subject: [PATCH 08/26] uz801: Use busybox coreutils (they were hiding all along) --- daemon/src/stats.rs | 123 ++++++++++++-------------------------------- 1 file changed, 34 insertions(+), 89 deletions(-) diff --git a/daemon/src/stats.rs b/daemon/src/stats.rs index 2b0eac0..0b5e90d 100644 --- a/daemon/src/stats.rs +++ b/daemon/src/stats.rs @@ -40,58 +40,30 @@ pub struct DiskStats { impl DiskStats { // runs "df -h " to get storage statistics for the partition containing - // the QMDL file. The Uz801 device doesn't support the -h flag, so we skip it for that device. + // the QMDL file. pub async fn new(qmdl_path: &str, device: &Device) -> Result { - let mut df_cmd = Command::new("df"); - // Only add -h flag for devices other than Uz801, as Uz801's df doesn't support it - if !matches!(device, Device::Uz801) { - df_cmd.arg("-h"); + // Uz801 needs to be told to use the busybox df specifically + let mut df_cmd: Command; + if matches!(device, Device::Uz801) { + df_cmd = Command::new("busybox"); + df_cmd.arg("df"); + } else { + df_cmd = Command::new("df"); } + df_cmd.arg("-h"); df_cmd.arg(qmdl_path); let stdout = get_cmd_output(df_cmd).await?; - if matches!(device, Device::Uz801) { - // Handle Uz801 format: - // Filesystem Size Used Free Blksize - // /data/rayhunter/ 774.9M 68.0M 706.9M 4096 - let lines: Vec<&str> = stdout.lines().collect(); - if lines.len() < 2 { - return Err("error parsing df output: insufficient lines".to_string()); - } - let data_line = lines[1]; - let mut parts = data_line.split_whitespace(); - Ok(Self { - partition: parts - .next() - .ok_or("error parsing df output: missing filesystem")? - .to_string(), - total_size: parts - .next() - .ok_or("error parsing df output: missing size")? - .to_string(), - used_size: parts - .next() - .ok_or("error parsing df output: missing used")? - .to_string(), - available_size: parts - .next() - .ok_or("error parsing df output: missing free")? - .to_string(), - used_percent: "N/A".to_string(), // Uz801 df doesn't provide percentage - mounted_on: qmdl_path.to_string(), // Use the path we queried - }) - } else { - // Handle standard df -h format - let mut parts = stdout.split_whitespace().skip(7); - Ok(Self { - partition: parts.next().ok_or("error parsing df output")?.to_string(), - total_size: parts.next().ok_or("error parsing df output")?.to_string(), - used_size: parts.next().ok_or("error parsing df output")?.to_string(), - available_size: parts.next().ok_or("error parsing df output")?.to_string(), - used_percent: parts.next().ok_or("error parsing df output")?.to_string(), - mounted_on: parts.next().ok_or("error parsing df output")?.to_string(), - }) - } + // Handle standard df -h format + let mut parts = stdout.split_whitespace().skip(7); + Ok(Self { + partition: parts.next().ok_or("error parsing df output")?.to_string(), + total_size: parts.next().ok_or("error parsing df output")?.to_string(), + used_size: parts.next().ok_or("error parsing df output")?.to_string(), + available_size: parts.next().ok_or("error parsing df output")?.to_string(), + used_percent: parts.next().ok_or("error parsing df output")?.to_string(), + mounted_on: parts.next().ok_or("error parsing df output")?.to_string(), + }) } } @@ -121,52 +93,25 @@ async fn get_cmd_output(mut cmd: Command) -> Result { impl MemoryStats { // runs "free -k" and parses the output to retrieve memory stats for most devices, - // or reads /proc/meminfo for Uz801 which doesn't have the free command pub async fn new(device: &Device) -> Result { + // Use busybox for Uz801 + let mut free_cmd: Command; if matches!(device, Device::Uz801) { - // Read /proc/meminfo for Uz801 - let meminfo_content = tokio::fs::read_to_string("/proc/meminfo") - .await - .map_err(|e| format!("error reading /proc/meminfo: {}", e))?; - - let mut mem_total_kb = None; - let mut mem_free_kb = None; - - for line in meminfo_content.lines() { - if let Some(value_str) = line.strip_prefix("MemTotal:") { - if let Some(kb_str) = value_str.trim().strip_suffix(" kB") { - mem_total_kb = kb_str.trim().parse::().ok(); - } - } else if let Some(value_str) = line.strip_prefix("MemFree:") { - if let Some(kb_str) = value_str.trim().strip_suffix(" kB") { - mem_free_kb = kb_str.trim().parse::().ok(); - } - } - } - - let total = mem_total_kb.ok_or("error parsing MemTotal from /proc/meminfo")?; - let free = mem_free_kb.ok_or("error parsing MemFree from /proc/meminfo")?; - let used = total - free; - - Ok(Self { - total: humanize_kb(total), - used: humanize_kb(used), - free: humanize_kb(free), - }) + free_cmd = Command::new("busybox"); + free_cmd.arg("free"); } else { - // Use free command for other devices - let mut free_cmd = Command::new("free"); - free_cmd.arg("-k"); - let stdout = get_cmd_output(free_cmd).await?; - let mut numbers = stdout - .split_whitespace() - .flat_map(|part| part.parse::()); - Ok(Self { - total: humanize_kb(numbers.next().ok_or("error parsing free output")?), - used: humanize_kb(numbers.next().ok_or("error parsing free output")?), - free: humanize_kb(numbers.next().ok_or("error parsing free output")?), - }) + free_cmd = Command::new("free"); } + free_cmd.arg("-k"); + let stdout = get_cmd_output(free_cmd).await?; + let mut numbers = stdout + .split_whitespace() + .flat_map(|part| part.parse::()); + Ok(Self { + total: humanize_kb(numbers.next().ok_or("error parsing free output")?), + used: humanize_kb(numbers.next().ok_or("error parsing free output")?), + free: humanize_kb(numbers.next().ok_or("error parsing free output")?), + }) } } From c26ad29ffb48f1dd4aa1490f20924db660b459b3 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 11:05:50 -0400 Subject: [PATCH 09/26] uz801: Fix installer defaults, fix docs for admin IP --- doc/uz801.md | 4 ++-- installer/src/main.rs | 8 ++------ installer/src/uz801.rs | 1 - 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/doc/uz801.md b/doc/uz801.md index e891fe5..25a011a 100644 --- a/doc/uz801.md +++ b/doc/uz801.md @@ -32,10 +32,10 @@ Connect to the UZ801's network over WiFi or USB tethering. The UZ801 uses a unique installation process that activates a hidden USB debugging backdoor: ```sh -./installer uz801 --admin-ip 192.168.100.1 +./installer uz801 ``` -Note: The default admin IP for UZ801 is typically `192.168.100.1` instead of `192.168.0.1`. +Note: The default IP for UZ801 is typically `192.168.100.1`; if yours differs, use the `--admin-ip` argument to specify it. The installation process works as follows: 1. Activates the USB debugging backdoor via HTTP request to `/usbdebug.html` diff --git a/installer/src/main.rs b/installer/src/main.rs index cad01e3..2b418cc 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -123,12 +123,8 @@ struct TmobileArgs { #[derive(Parser, Debug)] struct Uz801Args { /// IP address for Uz801 admin interface, if custom. - #[arg(long, default_value = "192.168.0.1")] - admin_ip: String, - - /// Web portal admin password. - #[arg(long)] - admin_password: String, + #[arg(long, default_value = "192.168.100.1")] + admin_ip: String } #[derive(Parser, Debug)] diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index e43cdf5..f612cca 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -18,7 +18,6 @@ use crate::util::echo; pub async fn install( Args { admin_ip, - admin_password: _, // Not used for Uz801 }: Args, ) -> Result<()> { run_install(admin_ip).await From 4d2d49326afa255cc61d819a6d446fd4c9a9dc22 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 11:12:46 -0400 Subject: [PATCH 10/26] uz801: Update installer usb backdoor trigger code --- installer/src/uz801.rs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index f612cca..58888fc 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -53,13 +53,30 @@ async fn run_install(admin_ip: String) -> Result<()> { } pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> { - let url = format!("http://{}/usbdebug.html", admin_ip); - let client = reqwest::Client::new(); - let response = client.get(&url).send().await?; - - if !response.status().is_success() { - anyhow::bail!("Failed to activate USB debug: HTTP {}", response.status()); - } + let url = format!("http://{}/ajax", admin_ip); + let referer = format!("http://{}/usbdebug.html", admin_ip); + let origin = format!("http://{}", admin_ip); + + let _handle = tokio::spawn(async move { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .unwrap(); + + let _response = client + .post(&url) + .header("Accept", "application/json, text/javascript, */*; q=0.01") + .header("Accept-Encoding", "gzip, deflate") + .header("Referer", &referer) + .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + .header("X-Requested-With", "XMLHttpRequest") + .header("Origin", &origin) + .header("Connection", "keep-alive") + .body(r#"{"funcNo":2001}"#) + .send() + .await; + // Ignore any errors - the device will reboot and connection will be lost + }); Ok(()) } From 412ad3d8bfed70ebf6989f6ed07b70c5b64e9341 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 16:29:59 -0400 Subject: [PATCH 11/26] cargo fmt run --- installer/src/main.rs | 2 +- installer/src/uz801.rs | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/installer/src/main.rs b/installer/src/main.rs index 2b418cc..30a343a 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -124,7 +124,7 @@ struct TmobileArgs { struct Uz801Args { /// IP address for Uz801 admin interface, if custom. #[arg(long, default_value = "192.168.100.1")] - admin_ip: String + admin_ip: String, } #[derive(Parser, Debug)] diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 58888fc..449b14d 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -15,11 +15,7 @@ use tokio::time::sleep; use crate::Uz801Args as Args; use crate::util::echo; -pub async fn install( - Args { - admin_ip, - }: Args, -) -> Result<()> { +pub async fn install(Args { admin_ip }: Args) -> Result<()> { run_install(admin_ip).await } @@ -56,19 +52,22 @@ pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> { let url = format!("http://{}/ajax", admin_ip); let referer = format!("http://{}/usbdebug.html", admin_ip); let origin = format!("http://{}", admin_ip); - + let _handle = tokio::spawn(async move { let client = reqwest::Client::builder() .timeout(Duration::from_secs(5)) .build() .unwrap(); - + let _response = client .post(&url) .header("Accept", "application/json, text/javascript, */*; q=0.01") .header("Accept-Encoding", "gzip, deflate") .header("Referer", &referer) - .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + .header( + "Content-Type", + "application/x-www-form-urlencoded; charset=UTF-8", + ) .header("X-Requested-With", "XMLHttpRequest") .header("Origin", &origin) .header("Connection", "keep-alive") From 9f661ab3983deb556659f760ed9c6442eed7115b Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 16:51:36 -0400 Subject: [PATCH 12/26] uz801: Update documentation --- doc/uz801.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/doc/uz801.md b/doc/uz801.md index 25a011a..fcfd902 100644 --- a/doc/uz801.md +++ b/doc/uz801.md @@ -38,7 +38,7 @@ The UZ801 uses a unique installation process that activates a hidden USB debuggi Note: The default IP for UZ801 is typically `192.168.100.1`; if yours differs, use the `--admin-ip` argument to specify it. The installation process works as follows: -1. Activates the USB debugging backdoor via HTTP request to `/usbdebug.html` +1. Activates the USB debugging backdoor via HTTP AJAX request 2. Waits for device reboot and ADB availability 3. Uses ADB to install rayhunter files and modify the startup script 4. Launches rayhunter daemon automatically @@ -46,8 +46,8 @@ The installation process works as follows: ## LED modes | Rayhunter state | LED indicator | | ---------------- | ------------------- | -| Recording | WiFi (blue) LED solid on| -| Paused | Green LED solid on | +| Recording | Green LED solid on | +| Paused | WiFi (blue) LED solid on | | Warning Detected | Red LED solid on | Note: Unlike the TMOHS1, the UZ801 uses solid LED indicators instead of blinking patterns. @@ -61,8 +61,6 @@ adb shell ## Device-specific notes -- The UZ801 has a limited set of coreutils, so some standard Linux commands may not be available -- The `df` command doesn't support the `-h` flag for human-readable output -- The `free` command is not available; memory information is read from `/proc/meminfo` -- USB debugging must be activated via the web backdoor before ADB access is possible (this is required only once) -- The device uses `/system/bin/initmifiservice.sh` as the main startup script +- The UZ801 does not symlink busybox for some core system utils, for some reason. Please use `busybox `, e.g. `busybox df -h`. +- USB debugging must be activated via the web backdoor before ADB access is possible (this is required only once.) +- The device uses `/system/bin/initmifiservice.sh` as the main startup script. From a3db5029ad65d0ae9353d3e10a32fd825b36b8d7 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 16:51:46 -0400 Subject: [PATCH 13/26] uz801: Update installer --- installer/src/uz801.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 449b14d..8680e83 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -70,7 +70,6 @@ pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> { ) .header("X-Requested-With", "XMLHttpRequest") .header("Origin", &origin) - .header("Connection", "keep-alive") .body(r#"{"funcNo":2001}"#) .send() .await; @@ -92,8 +91,9 @@ async fn wait_for_adb() -> Result { anyhow::bail!("Timeout waiting for ADB connection after USB debug activation"); } + // UZ801 USB vendor and product IDs. + // TODO: Research if other variants use different IDs. match ADBUSBDevice::new(0x05c6, 0x9025) { - // Common Qualcomm ADB VID/PID Ok(mut device) => { // Test ADB connection if test_adb_connection(&mut device).await.is_ok() { From cb22e179d61554bba6a27203e8d7bcf6d6133485 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 17:30:15 -0400 Subject: [PATCH 14/26] uz801: installer: Add missing dependency --- installer/src/uz801.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 8680e83..3771052 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -6,10 +6,10 @@ /// 3. Use ADB to install rayhunter files /// 4. Modify startup script to launch rayhunter on boot use std::time::Duration; +use std::io::Write; use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError}; use anyhow::Result; -use std::io::ErrorKind; use tokio::time::sleep; use crate::Uz801Args as Args; From 7184ccd5c15495af03a3a5358c14214abcebdce2 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 18:06:13 -0400 Subject: [PATCH 15/26] uz801: installer: Update VID/PID, add remount --- installer/src/main.rs | 2 +- installer/src/uz801.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/installer/src/main.rs b/installer/src/main.rs index 30a343a..5095d02 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -180,7 +180,7 @@ async fn run() -> Result<(), Error> { match command { Command::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, - Command::Uz801(args) => uz801::install(args).await.context("Failed to install rayhunter on the Uz801. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, + Command::Uz801(args) => uz801::install(args).await.context("Failed to install rayhunter on the Uz801. Make sure your computer is connected to the hotspot using USB.")?, Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, Command::Pinephone(_) => pinephone::install().await .context("Failed to install rayhunter on the Pinephone's Quectel modem")?, diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 3771052..5852ccb 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -93,7 +93,7 @@ async fn wait_for_adb() -> Result { // UZ801 USB vendor and product IDs. // TODO: Research if other variants use different IDs. - match ADBUSBDevice::new(0x05c6, 0x9025) { + match ADBUSBDevice::new(0x05c6, 0x90b6) { Ok(mut device) => { // Test ADB connection if test_adb_connection(&mut device).await.is_ok() { @@ -129,6 +129,9 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> { let mut buf = Vec::::new(); adb_device.shell_command(&["mkdir", "-p", "/data/rayhunter"], &mut buf)?; + // Remount system as writable + adb_device.shell_command(&["mount", "-o", "remount,rw", "/system"], &mut buf)?; + // Install rayhunter daemon binary let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); let mut daemon_data = rayhunter_daemon_bin.as_slice(); From 1f19bc880f29630f6c3315df0821cbfa37c55bca Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 18:35:54 -0400 Subject: [PATCH 16/26] uz801: Force LED updates every 5s to beat MifiService --- daemon/src/display/uz801.rs | 48 +++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/daemon/src/display/uz801.rs b/daemon/src/display/uz801.rs index 8f6422c..b4b30c9 100644 --- a/daemon/src/display/uz801.rs +++ b/daemon/src/display/uz801.rs @@ -38,6 +38,7 @@ pub fn update_ui( task_tracker.spawn(async move { let mut state = DisplayState::Recording; let mut last_state = DisplayState::Paused; + let mut last_update = std::time::Instant::now(); loop { match ui_shutdown_rx.try_recv() { @@ -53,28 +54,33 @@ pub fn update_ui( Err(mpsc::error::TryRecvError::Empty) => {} Err(e) => error!("error receiving ui update message: {e}"), }; - if invisible || state == last_state { - tokio::time::sleep(Duration::from_secs(1)).await; - continue; + + // Update LEDs if state changed or if 5 seconds have passed since last update + let now = std::time::Instant::now(); + let should_update = !invisible && (state != last_state || now.duration_since(last_update) >= Duration::from_secs(5)); + + if should_update { + match state { + DisplayState::Paused => { + led_off(led!("red")).await; + led_off(led!("green")).await; + led_on(led!("wifi")).await; + } + DisplayState::Recording => { + led_off(led!("red")).await; + led_off(led!("wifi")).await; + led_on(led!("green")).await; + } + DisplayState::WarningDetected => { + led_off(led!("green")).await; + led_off(led!("wifi")).await; + led_on(led!("red")).await; + } + } + last_state = state; + last_update = now; } - match state { - DisplayState::Paused => { - led_off(led!("red")).await; - led_off(led!("green")).await; - led_on(led!("wifi")).await; - } - DisplayState::Recording => { - led_off(led!("red")).await; - led_off(led!("wifi")).await; - led_on(led!("green")).await; - } - DisplayState::WarningDetected => { - led_off(led!("green")).await; - led_off(led!("wifi")).await; - led_on(led!("red")).await; - } - } - last_state = state; + tokio::time::sleep(Duration::from_secs(1)).await; } }); From 574e897610889ae4d10b136ad8b46c7e6a8c063b Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 18:49:38 -0400 Subject: [PATCH 17/26] cargo fmt pass --- daemon/src/display/uz801.rs | 10 ++++++---- installer/src/uz801.rs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/daemon/src/display/uz801.rs b/daemon/src/display/uz801.rs index b4b30c9..ea0fd7e 100644 --- a/daemon/src/display/uz801.rs +++ b/daemon/src/display/uz801.rs @@ -54,11 +54,13 @@ pub fn update_ui( Err(mpsc::error::TryRecvError::Empty) => {} Err(e) => error!("error receiving ui update message: {e}"), }; - + // Update LEDs if state changed or if 5 seconds have passed since last update let now = std::time::Instant::now(); - let should_update = !invisible && (state != last_state || now.duration_since(last_update) >= Duration::from_secs(5)); - + let should_update = !invisible + && (state != last_state + || now.duration_since(last_update) >= Duration::from_secs(5)); + if should_update { match state { DisplayState::Paused => { @@ -80,7 +82,7 @@ pub fn update_ui( last_state = state; last_update = now; } - + tokio::time::sleep(Duration::from_secs(1)).await; } }); diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 5852ccb..f594071 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -1,3 +1,4 @@ +use std::io::Write; /// Installer for the Uz801 hotspot. /// /// Installation process: @@ -6,7 +7,6 @@ /// 3. Use ADB to install rayhunter files /// 4. Modify startup script to launch rayhunter on boot use std::time::Duration; -use std::io::Write; use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError}; use anyhow::Result; From e36b490d15b23cb5e07dbedfee7f0b41caf32af2 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 19:27:25 -0400 Subject: [PATCH 18/26] uz801: installer: Add file transfer verify, other fixes --- installer/src/uz801.rs | 85 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index f594071..5c74571 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -84,7 +84,7 @@ async fn wait_for_adb() -> Result { let mut attempts = 0; // Wait a bit for the reboot to start - sleep(Duration::from_secs(5)).await; + sleep(Duration::from_secs(10)).await; loop { if attempts >= MAX_ATTEMPTS { @@ -132,10 +132,14 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> { // Remount system as writable adb_device.shell_command(&["mount", "-o", "remount,rw", "/system"], &mut buf)?; - // Install rayhunter daemon binary + // Install rayhunter daemon binary with verification let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); - let mut daemon_data = rayhunter_daemon_bin.as_slice(); - adb_device.push(&mut daemon_data, &"/data/rayhunter/rayhunter-daemon")?; + install_file_with_verification( + adb_device, + "/data/rayhunter/rayhunter-daemon", + rayhunter_daemon_bin, + ) + .await?; // Install config file let config_content = crate::CONFIG_TOML.replace("#device = \"orbic\"", "device = \"uz801\""); @@ -152,6 +156,75 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> { Ok(()) } +async fn install_file_with_verification( + adb_device: &mut ADBUSBDevice, + dest_path: &str, + file_data: &[u8], +) -> Result<()> { + const MAX_RETRIES: u32 = 5; + let expected_size = file_data.len(); + + println!("Installing {} ({} bytes)...", dest_path, expected_size); + + for attempt in 1..=MAX_RETRIES { + // Push the file + let mut data_copy = file_data; + match adb_device.push(&mut data_copy, &dest_path) { + Ok(_) => { + // Verify the file size + let mut buf = Vec::::new(); + if let Ok(_) = adb_device.shell_command(&["ls", "-l", dest_path], &mut buf) { + let output = String::from_utf8_lossy(&buf); + + // Parse the file size from ls output + if let Some(size_str) = output.split_whitespace().nth(3) { + if let Ok(actual_size) = size_str.parse::() { + if actual_size == expected_size { + println!( + "Successfully installed {} (verified {} bytes)", + dest_path, actual_size + ); + return Ok(()); + } else { + println!( + "Size mismatch on attempt {}: expected {}, got {}", + attempt, expected_size, actual_size + ); + + // Remove the incomplete file before retry + let mut buf = Vec::::new(); + adb_device + .shell_command(&["rm", "-f", dest_path], &mut buf) + .ok(); + + if attempt < MAX_RETRIES { + println!("Retrying file transfer..."); + sleep(Duration::from_secs(1)).await; + continue; + } + } + } + } + } + } + Err(e) => { + println!("Push failed on attempt {}: {}", attempt, e); + if attempt < MAX_RETRIES { + sleep(Duration::from_secs(1)).await; + continue; + } + return Err(e.into()); + } + } + } + + anyhow::bail!( + "Failed to install {} after {} attempts - size verification failed", + dest_path, + MAX_RETRIES + ) +} + async fn modify_startup_script(adb_device: &mut ADBUSBDevice) -> Result<()> { // Pull the existing startup script let mut script_content = Vec::::new(); @@ -217,7 +290,9 @@ async fn test_rayhunter(admin_ip: &str) -> Result<()> { } failures += 1; - sleep(Duration::from_secs(3)).await; + // modified from 3 because MifiService has to set up the + // network routing and it takes a bit longer + sleep(Duration::from_secs(5)).await; } anyhow::bail!("timeout reached! failed to reach rayhunter, something went wrong :(") From 55794cbdd5c974246816982ade342091086c5327 Mon Sep 17 00:00:00 2001 From: Andrej Date: Sun, 3 Aug 2025 20:03:01 -0400 Subject: [PATCH 19/26] uz801: Fix clippy warnings --- installer/src/uz801.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 5c74571..2ae3b2e 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -49,9 +49,9 @@ async fn run_install(admin_ip: String) -> Result<()> { } pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> { - let url = format!("http://{}/ajax", admin_ip); - let referer = format!("http://{}/usbdebug.html", admin_ip); - let origin = format!("http://{}", admin_ip); + let url = format!("http://{admin_ip}/ajax"); + let referer = format!("http://{admin_ip}/usbdebug.html"); + let origin = format!("http://{admin_ip}"); let _handle = tokio::spawn(async move { let client = reqwest::Client::builder() @@ -277,7 +277,7 @@ async fn test_rayhunter(admin_ip: &str) -> Result<()> { let client = reqwest::Client::new(); while failures < MAX_FAILURES { - let url = format!("http://{}:8080/index.html", admin_ip); + let url = format!("http://{admin_ip}:8080/index.html"); if let Ok(response) = client.get(&url).send().await { if response.status().is_success() { From da4a86be130a78f29680922686048e02f4b1744a Mon Sep 17 00:00:00 2001 From: Andrej Date: Mon, 4 Aug 2025 11:03:42 -0400 Subject: [PATCH 20/26] uz801: Installer improvements --- installer/src/uz801.rs | 169 +++++++++++++---------------------------- 1 file changed, 52 insertions(+), 117 deletions(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 2ae3b2e..7d5b7db 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -1,4 +1,5 @@ use std::io::Write; +use std::path::Path; /// Installer for the Uz801 hotspot. /// /// Installation process: @@ -9,7 +10,8 @@ use std::io::Write; use std::time::Duration; use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError}; -use anyhow::Result; +use anyhow::{Result, anyhow}; +use md5::compute as md5_compute; use tokio::time::sleep; use crate::Uz801Args as Args; @@ -36,14 +38,13 @@ async fn run_install(admin_ip: String) -> Result<()> { modify_startup_script(&mut adb_device).await?; println!("ok"); - echo!("Starting rayhunter daemon... "); - start_rayhunter(&mut adb_device).await?; + echo!("Rebooting the device... "); + let _ = adb_device.reboot(adb_client::RebootType::System); println!("ok"); - echo!("Testing rayhunter... "); - test_rayhunter(&admin_ip).await?; - println!("ok"); - println!("rayhunter is running at http://{admin_ip}:8080"); + println!("Installation complete!"); + println!("Please wait for the device to reboot (light will turn green)"); + println!("Then access rayhunter at: http://{admin_ip}:8080"); Ok(()) } @@ -134,12 +135,11 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> { // Install rayhunter daemon binary with verification let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); - install_file_with_verification( + install_file( adb_device, "/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin, - ) - .await?; + )?; // Install config file let config_content = crate::CONFIG_TOML.replace("#device = \"orbic\"", "device = \"uz801\""); @@ -156,73 +156,53 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> { Ok(()) } -async fn install_file_with_verification( - adb_device: &mut ADBUSBDevice, - dest_path: &str, - file_data: &[u8], -) -> Result<()> { - const MAX_RETRIES: u32 = 5; - let expected_size = file_data.len(); - - println!("Installing {} ({} bytes)...", dest_path, expected_size); - +/// Transfer a file to the device's filesystem with adb push. +/// Validates the file sends successfully to /data/local/tmp +/// before overwriting the destination. +fn install_file(adb_device: &mut ADBUSBDevice, dest: &str, payload: &[u8]) -> Result<()> { + const MAX_RETRIES: u32 = 3; + + let file_name = Path::new(dest) + .file_name() + .ok_or_else(|| anyhow!("{dest} does not have a file name"))? + .to_str() + .ok_or_else(|| anyhow!("{dest}'s file name is not UTF8"))? + .to_owned(); + let push_tmp_path = format!("/data/local/tmp/{file_name}"); + let file_hash = md5_compute(payload); + for attempt in 1..=MAX_RETRIES { // Push the file - let mut data_copy = file_data; - match adb_device.push(&mut data_copy, &dest_path) { - Ok(_) => { - // Verify the file size - let mut buf = Vec::::new(); - if let Ok(_) = adb_device.shell_command(&["ls", "-l", dest_path], &mut buf) { - let output = String::from_utf8_lossy(&buf); - - // Parse the file size from ls output - if let Some(size_str) = output.split_whitespace().nth(3) { - if let Ok(actual_size) = size_str.parse::() { - if actual_size == expected_size { - println!( - "Successfully installed {} (verified {} bytes)", - dest_path, actual_size - ); - return Ok(()); - } else { - println!( - "Size mismatch on attempt {}: expected {}, got {}", - attempt, expected_size, actual_size - ); - - // Remove the incomplete file before retry - let mut buf = Vec::::new(); - adb_device - .shell_command(&["rm", "-f", dest_path], &mut buf) - .ok(); - - if attempt < MAX_RETRIES { - println!("Retrying file transfer..."); - sleep(Duration::from_secs(1)).await; - continue; - } - } - } - } - } - } - Err(e) => { - println!("Push failed on attempt {}: {}", attempt, e); - if attempt < MAX_RETRIES { - sleep(Duration::from_secs(1)).await; - continue; - } + let mut payload_copy = payload; + if let Err(e) = adb_device.push(&mut payload_copy, &push_tmp_path) { + if attempt == MAX_RETRIES { return Err(e.into()); } + continue; + } + + // Verify with md5sum + let mut buf = Vec::::new(); + if let Ok(_) = adb_device.shell_command(&["busybox", "md5sum", &push_tmp_path], &mut buf) { + let output = String::from_utf8_lossy(&buf); + if output.contains(&format!("{file_hash:x}")) { + // Verification successful, move to final destination + let mut buf = Vec::::new(); + adb_device.shell_command(&["mv", &push_tmp_path, dest], &mut buf)?; + println!("ok"); + return Ok(()); + } + } + + // Verification failed, clean up and retry + if attempt < MAX_RETRIES { + println!("MD5 verification failed on attempt {}, retrying...", attempt); + let mut buf = Vec::::new(); + adb_device.shell_command(&["rm", "-f", &push_tmp_path], &mut buf).ok(); } } - - anyhow::bail!( - "Failed to install {} after {} attempts - size verification failed", - dest_path, - MAX_RETRIES - ) + + anyhow::bail!("MD5 verification failed for {dest} after {MAX_RETRIES} attempts") } async fn modify_startup_script(adb_device: &mut ADBUSBDevice) -> Result<()> { @@ -251,49 +231,4 @@ async fn modify_startup_script(adb_device: &mut ADBUSBDevice) -> Result<()> { )?; Ok(()) -} - -async fn start_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> { - let mut buf = Vec::::new(); - adb_device.shell_command( - &[ - "/data/rayhunter/rayhunter-daemon", - "/data/rayhunter/config.toml", - "&", - ], - &mut buf, - )?; - - // Give it a moment to start - sleep(Duration::from_secs(3)).await; - - Ok(()) -} - -async fn test_rayhunter(admin_ip: &str) -> Result<()> { - const MAX_FAILURES: u32 = 10; - let mut failures = 0; - - let client = reqwest::Client::new(); - - while failures < MAX_FAILURES { - let url = format!("http://{admin_ip}:8080/index.html"); - - if let Ok(response) = client.get(&url).send().await { - if response.status().is_success() { - if let Ok(body) = response.text().await { - if body.contains("html") { - return Ok(()); - } - } - } - } - - failures += 1; - // modified from 3 because MifiService has to set up the - // network routing and it takes a bit longer - sleep(Duration::from_secs(5)).await; - } - - anyhow::bail!("timeout reached! failed to reach rayhunter, something went wrong :(") -} +} \ No newline at end of file From 2e6343c343b46b805e2d8d7995d6965896df3c5e Mon Sep 17 00:00:00 2001 From: Andrej Date: Mon, 4 Aug 2025 11:35:23 -0400 Subject: [PATCH 21/26] uz801: clippy fixes --- installer/src/uz801.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 7d5b7db..607b024 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -183,7 +183,7 @@ fn install_file(adb_device: &mut ADBUSBDevice, dest: &str, payload: &[u8]) -> Re // Verify with md5sum let mut buf = Vec::::new(); - if let Ok(_) = adb_device.shell_command(&["busybox", "md5sum", &push_tmp_path], &mut buf) { + if adb_device.shell_command(&["busybox", "md5sum", &push_tmp_path], &mut buf).is_ok() { let output = String::from_utf8_lossy(&buf); if output.contains(&format!("{file_hash:x}")) { // Verification successful, move to final destination From c893f8e2a9fb204b4c9ee1171c4c402128cd021e Mon Sep 17 00:00:00 2001 From: Andrej Date: Mon, 4 Aug 2025 13:16:46 -0400 Subject: [PATCH 22/26] uz801: update docs --- doc/uz801.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/uz801.md b/doc/uz801.md index fcfd902..ed1d5ed 100644 --- a/doc/uz801.md +++ b/doc/uz801.md @@ -27,9 +27,8 @@ The UZ801 supports various LTE bands depending on the specific hardware revision The most frequent bands found on these devices are LTE bands 1/3/5/8/20. In the US, this means that Verizon's band 5 towers are the only towers that this device could communicate with in its normal usage as an LTE modem. Research on whether Qualcomm diagnostic tools can be used to write new band support into the NVRAM is pending. ## Installing -Connect to the UZ801's network over WiFi or USB tethering. -The UZ801 uses a unique installation process that activates a hidden USB debugging backdoor: +With the device fully booted (i.e. beaming a wifi network, blue LED, etc.) and plugged into the computer that is performing the installation, run: ```sh ./installer uz801 @@ -37,12 +36,6 @@ The UZ801 uses a unique installation process that activates a hidden USB debuggi Note: The default IP for UZ801 is typically `192.168.100.1`; if yours differs, use the `--admin-ip` argument to specify it. -The installation process works as follows: -1. Activates the USB debugging backdoor via HTTP AJAX request -2. Waits for device reboot and ADB availability -3. Uses ADB to install rayhunter files and modify the startup script -4. Launches rayhunter daemon automatically - ## LED modes | Rayhunter state | LED indicator | | ---------------- | ------------------- | @@ -61,6 +54,14 @@ adb shell ## Device-specific notes +The UZ801 uses a unique installation process that activates a hidden USB debugging backdoor. + +The installation process works as follows: +1. Activates the USB debugging backdoor via HTTP AJAX request +2. Waits for device reboot and ADB availability +3. Uses ADB to install rayhunter files and modify the startup script +4. Launches rayhunter daemon automatically + - The UZ801 does not symlink busybox for some core system utils, for some reason. Please use `busybox `, e.g. `busybox df -h`. -- USB debugging must be activated via the web backdoor before ADB access is possible (this is required only once.) +- USB debugging must be activated via the web backdoor before ADB access is possible (this is required only once.) The installer does this already. - The device uses `/system/bin/initmifiservice.sh` as the main startup script. From 5184c6138d053b2cf81db25a56de2122f8ee785c Mon Sep 17 00:00:00 2001 From: Andrej Date: Mon, 4 Aug 2025 13:23:14 -0400 Subject: [PATCH 23/26] uz801: Add uninstalling instructions --- doc/uninstalling.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/uninstalling.md b/doc/uninstalling.md index 4649999..6b83ece 100644 --- a/doc/uninstalling.md +++ b/doc/uninstalling.md @@ -22,3 +22,18 @@ Your device is now Rayhunter-free, and should no longer be in a rooted ADB-enabl 4. `update-rc.d rayhunter_daemon remove` 5. (hardware revision v4.0+ only) In `Settings > NAT Settings > Port Triggers` in TP-Link's admin UI, remove any leftover port triggers. +## UZ801 + +0. (Optional): Back up the qmdl folder with all of the captures: +`adb pull /data/rayhunter/qmdl .` +1. Run `adb shell` to get a root shell on the device +2. Delete the /data/rayhunter folder: `rm -rf /data/rayhunter` +3. Modify the initmifiservice.sh script to remove the rayhunter +startup line: +```sh +mount -o remount,rw /system +busybox vi /system/bin/initmifiservice.sh +``` +Then type 999G (shift+g), then type dd. Then press the colon key (:) and type wq. Finally, press Enter. +4. Lastly, run `setprop persist.sys.usb.config rndis`. +5. Type `reboot` to reboot the device. \ No newline at end of file From f4522dbe3d1d2fc9cb135dd66da170e5a597e87b Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 5 Aug 2025 14:21:19 -0400 Subject: [PATCH 24/26] cargo fmt run --- installer/src/uz801.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 607b024..0475ec7 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -161,7 +161,7 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> { /// before overwriting the destination. fn install_file(adb_device: &mut ADBUSBDevice, dest: &str, payload: &[u8]) -> Result<()> { const MAX_RETRIES: u32 = 3; - + let file_name = Path::new(dest) .file_name() .ok_or_else(|| anyhow!("{dest} does not have a file name"))? @@ -170,7 +170,7 @@ fn install_file(adb_device: &mut ADBUSBDevice, dest: &str, payload: &[u8]) -> Re .to_owned(); let push_tmp_path = format!("/data/local/tmp/{file_name}"); let file_hash = md5_compute(payload); - + for attempt in 1..=MAX_RETRIES { // Push the file let mut payload_copy = payload; @@ -183,7 +183,10 @@ fn install_file(adb_device: &mut ADBUSBDevice, dest: &str, payload: &[u8]) -> Re // Verify with md5sum let mut buf = Vec::::new(); - if adb_device.shell_command(&["busybox", "md5sum", &push_tmp_path], &mut buf).is_ok() { + if adb_device + .shell_command(&["busybox", "md5sum", &push_tmp_path], &mut buf) + .is_ok() + { let output = String::from_utf8_lossy(&buf); if output.contains(&format!("{file_hash:x}")) { // Verification successful, move to final destination @@ -193,15 +196,20 @@ fn install_file(adb_device: &mut ADBUSBDevice, dest: &str, payload: &[u8]) -> Re return Ok(()); } } - + // Verification failed, clean up and retry if attempt < MAX_RETRIES { - println!("MD5 verification failed on attempt {}, retrying...", attempt); + println!( + "MD5 verification failed on attempt {}, retrying...", + attempt + ); let mut buf = Vec::::new(); - adb_device.shell_command(&["rm", "-f", &push_tmp_path], &mut buf).ok(); + adb_device + .shell_command(&["rm", "-f", &push_tmp_path], &mut buf) + .ok(); } } - + anyhow::bail!("MD5 verification failed for {dest} after {MAX_RETRIES} attempts") } @@ -231,4 +239,4 @@ async fn modify_startup_script(adb_device: &mut ADBUSBDevice) -> Result<()> { )?; Ok(()) -} \ No newline at end of file +} From 13877f7209a2f331f8006d774447ae49f3bece6b Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 5 Aug 2025 21:05:31 +0200 Subject: [PATCH 25/26] cargo clippy --- installer/src/uz801.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 0475ec7..4475911 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -200,8 +200,7 @@ fn install_file(adb_device: &mut ADBUSBDevice, dest: &str, payload: &[u8]) -> Re // Verification failed, clean up and retry if attempt < MAX_RETRIES { println!( - "MD5 verification failed on attempt {}, retrying...", - attempt + "MD5 verification failed on attempt {attempt}, retrying..." ); let mut buf = Vec::::new(); adb_device From 31bd60dea18df8129c75bec978071c536675aee1 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 5 Aug 2025 21:11:17 +0200 Subject: [PATCH 26/26] cargo fmt --- installer/src/uz801.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index 4475911..2d66375 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -199,9 +199,7 @@ fn install_file(adb_device: &mut ADBUSBDevice, dest: &str, payload: &[u8]) -> Re // Verification failed, clean up and retry if attempt < MAX_RETRIES { - println!( - "MD5 verification failed on attempt {attempt}, retrying..." - ); + println!("MD5 verification failed on attempt {attempt}, retrying..."); let mut buf = Vec::::new(); adb_device .shell_command(&["rm", "-f", &push_tmp_path], &mut buf)