From 2561f767bb93ad6ec042ccab8e6953d4e49110ac Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sat, 14 Feb 2026 17:20:45 +0100 Subject: [PATCH 1/6] Install to /cache/rayhunter-data for tplink, add --data-dir parameter This fixes several space-related issues at once. We have observed the following phenomenon on TP-Link, Orbic and Moxee: - Filling /data bricks the device (broken wifi, broken rndis, broken display) - Filling /cache does not (it only bricks rayhunter if it's installed there, and it might break firmware updates) Therefore it would make sense to store the entire rayhunter installation in /cache. This is a great idea for TP-Link and Moxee, because /cache is significantly larger than /data. However, on Orbic, /data is significantly larger than /cache! This PR refactors orbic-network and tplink to use a shared codepath for setting up the data directory. A symlink is created at /data/rayhunter, and what it points to is device-specific: - Orbic will have its data at `/data/rayhunter-data` - There is a new alias `installer moxee` that overrides this to `/cache/rayhunter-data` - TP-Link will have its data at /cache/rayhunter-data when there's no SD card, and /media/whatever when there is one. In all cases, existing data is migrated to the new location. The user can switch back and forth between two values of --data-dir and the data will be moved over every time. This PR has one huge wart, and that is that the USB installer for Orbic remains untouched. The annoying reason for this is that the DeviceConnection trait is insufficient to reflect all the different kinds of shells you can have over USB: adb with fakeroot, and serial with real root. I think it's not possible to create the right directories with 'rootshell -c'. I'm thinking of spawning a telnet server over serial, so that we can just do telnet again, but this is for another time. --- doc/moxee.md | 5 +- installer/src/connection.rs | 115 +++++++++++++++++++++++++++++++-- installer/src/lib.rs | 42 +++++++++++- installer/src/moxee.rs | 15 +++++ installer/src/orbic.rs | 8 +-- installer/src/orbic_network.rs | 20 +++--- installer/src/tplink.rs | 85 +++++++++++++----------- installer/src/util.rs | 19 ++++-- 8 files changed, 238 insertions(+), 71 deletions(-) create mode 100644 installer/src/moxee.rs diff --git a/doc/moxee.md b/doc/moxee.md index e107dc19..02f40c28 100644 --- a/doc/moxee.md +++ b/doc/moxee.md @@ -40,11 +40,14 @@ According to [FCC ID 2APQU-K779HSDL](https://fcc.report/FCC-ID/2APQU-K779HSDL), Connect to the hotspot's network using WiFi or USB tethering and run: ```sh -./installer orbic --admin-password 'mypassword' +./installer moxee --admin-password 'mypassword' ``` The password (in place of `mypassword`) is under the battery. +`./installer moxee` is almost the same as `./installer orbic`, it just comes +with slightly better defaults that will give you more space for recordings. + ## Obtaining a shell ```sh diff --git a/installer/src/connection.rs b/installer/src/connection.rs index e77538a4..d7751a46 100644 --- a/installer/src/connection.rs +++ b/installer/src/connection.rs @@ -1,9 +1,9 @@ use std::future::Future; use std::net::SocketAddr; -use anyhow::Result; +use anyhow::{Result, bail}; -use crate::output::println; +use crate::output::{print, println}; /// Abstraction for device communication (telnet or ADB) pub trait DeviceConnection { @@ -23,13 +23,14 @@ pub async fn file_exists(conn: &mut C, path: &str) -> bool .unwrap_or(false) } -/// Shared config installation logic +/// Shared config installation logic. Installs to /data/rayhunter/config.toml which resolves +/// through the symlink to the actual data directory. pub async fn install_config( conn: &mut C, - config_path: &str, device_type: &str, reset_config: bool, ) -> Result<()> { + let config_path = "/data/rayhunter/config.toml"; if reset_config || !file_exists(conn, config_path).await { let config = crate::CONFIG_TOML.replace( r#"#device = "orbic""#, @@ -42,6 +43,112 @@ pub async fn install_config( Ok(()) } +/// Check if a directory exists using a DeviceConnection +pub async fn dir_exists(conn: &mut C, path: &str) -> bool { + conn.run_command(&format!("test -d {path} && echo exists || echo missing")) + .await + .map(|output| output.contains("exists")) + .unwrap_or(false) +} + +/// Check if a path is a symlink using a DeviceConnection +pub async fn is_symlink(conn: &mut C, path: &str) -> bool { + conn.run_command(&format!("test -L {path} && echo yes || echo no")) + .await + .map(|output| output.contains("yes")) + .unwrap_or(false) +} + +/// Read the target of a symlink using a DeviceConnection +pub async fn readlink(conn: &mut C, path: &str) -> Result { + // Use a prefix marker to find the actual output line, since some shells (TP-Link) echo + // back the command and run_command appends protocol lines. + let output = conn + .run_command(&format!("echo RL:$(readlink {path})")) + .await?; + let result = output + .lines() + .find_map(|line| line.trim().strip_prefix("RL:")) + .unwrap_or("") + .to_string(); + Ok(result) +} + +/// Set up the data directory at `data_dir` and create a symlink from `/data/rayhunter` to it. +/// +/// Handles migration from old locations: +/// - If `/data/rayhunter` is a real directory, moves its contents to `data_dir` +/// - If `/data/rayhunter` is a symlink to a different location, moves from the old target +/// - If `/data/rayhunter` doesn't exist, just creates the symlink +/// - If `/data/rayhunter` is a symlink to `data_dir`, does nothing +pub async fn setup_data_directory(conn: &mut C, data_dir: &str) -> Result<()> { + if data_dir == "/data/rayhunter" { + bail!("data_dir must not be /data/rayhunter"); + } + + // Determine where old data lives, if anywhere + let old_data_source = if is_symlink(conn, "/data/rayhunter").await { + let current_target = readlink(conn, "/data/rayhunter").await?; + if current_target == data_dir { + println!("Data directory already configured at {data_dir}"); + return Ok(()); + } + conn.run_command("rm -f /data/rayhunter").await?; + // The old symlink target is where data actually lives + if dir_exists(conn, ¤t_target).await { + Some(current_target) + } else { + None + } + } else if dir_exists(conn, "/data/rayhunter").await { + if dir_exists(conn, data_dir).await { + bail!("Both /data/rayhunter and {data_dir} exist and are directories."); + } + // Real directory (pre-migration Orbic state) + Some("/data/rayhunter".to_string()) + } else { + None + }; + + // Migrate old data if present + if let Some(old_source) = &old_data_source { + // Stop rayhunter-daemon so it doesn't write during migration. + // The device will be rebooted at the end of installation anyway. + print!("Stopping rayhunter-daemon ... "); + let _ = conn + .run_command("/etc/init.d/rayhunter_daemon stop 2>/dev/null; true") + .await; + println!("ok"); + + print!("Migrating data from {old_source} to {data_dir} ... "); + + // mv old data into its place. If source and destination are on the same filesystem, + // this is an instant rename. + // XXX: DeviceConnection::run_command does not expose the exit code of the ran command. It + // probably should, or a utility for it should exist? + let mv_output = conn + .run_command(&format!("mv {old_source} {data_dir} && echo MV_OK")) + .await?; + if mv_output.contains("MV_OK") { + println!("ok"); + } else { + bail!("Failed to move data from {old_source} to {data_dir}:\n{mv_output}"); + } + } else { + // No migration needed, just ensure the target directory exists + conn.run_command(&format!("mkdir -p {data_dir}")).await?; + } + + // Create the symlink + print!("Creating symlink /data/rayhunter -> {data_dir} ... "); + conn.run_command("mkdir -p /data").await?; + conn.run_command(&format!("ln -sf {data_dir} /data/rayhunter")) + .await?; + println!("ok"); + + Ok(()) +} + /// Telnet-based connection wrapper pub struct TelnetConnection { pub addr: SocketAddr, diff --git a/installer/src/lib.rs b/installer/src/lib.rs index 3b6fee3a..70625bf4 100644 --- a/installer/src/lib.rs +++ b/installer/src/lib.rs @@ -6,6 +6,7 @@ use env_logger::Env; use anyhow::bail; mod connection; +mod moxee; #[cfg(not(target_os = "android"))] mod orbic; mod orbic_auth; @@ -40,9 +41,11 @@ enum Command { /// Install rayhunter on the Orbic RC400L using the legacy USB+ADB-based installer. #[cfg(not(target_os = "android"))] OrbicUsb(InstallOrbic), - /// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network. + /// Install rayhunter on the Orbic RC400L via network. #[clap(alias = "orbic-network")] Orbic(OrbicNetworkArgs), + /// Install rayhunter on the Moxee Hotspot via network. + Moxee(MoxeeArgs), /// Install rayhunter on the TMobile TMOHS1. Tmobile(TmobileArgs), /// Install rayhunter on the Uz801. @@ -84,6 +87,11 @@ struct InstallTpLink { /// Overwrite config.toml even if it already exists on the device. #[arg(long)] reset_config: bool, + + /// Override the data directory path. Defaults to /cache/rayhunter-data (or SD card path when + /// SD card is used). Must not be /data/rayhunter. + #[arg(long)] + data_dir: Option, } #[derive(Parser, Debug)] @@ -110,6 +118,35 @@ struct OrbicNetworkArgs { /// Overwrite config.toml even if it already exists on the device. #[arg(long)] reset_config: bool, + + /// Override the data directory path. Defaults to /data/rayhunter-data. + /// Must not be /data/rayhunter. + #[arg(long)] + data_dir: Option, +} + +#[derive(Parser, Debug)] +struct MoxeeArgs { + /// IP address for Moxee admin interface, if custom. + #[arg(long, default_value = "192.168.1.1")] + admin_ip: String, + + /// Admin username for authentication. + #[arg(long, default_value = "admin")] + admin_username: String, + + /// Admin password for authentication. + #[arg(long)] + admin_password: Option, + + /// Overwrite config.toml even if it already exists on the device. + #[arg(long)] + reset_config: bool, + + /// Override the data directory path. Defaults to /cache/rayhunter-data. + /// Must not be /data/rayhunter. + #[arg(long)] + data_dir: Option, } #[derive(Parser, Debug)] @@ -245,7 +282,8 @@ async fn run(args: Args) -> Result<(), Error> { .context("Failed to install rayhunter on the Pinephone's Quectel modem")?, #[cfg(not(target_os = "android"))] Command::OrbicUsb(args) => orbic::install(args.reset_config).await.context("\nFailed to install rayhunter on the Orbic RC400L (USB installer)")?, - Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password, args.reset_config).await.context("\nFailed to install rayhunter on the Orbic RC400L")?, + Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password, args.reset_config, args.data_dir).await.context("\nFailed to install rayhunter on the Orbic RC400L")?, + Command::Moxee(args) => moxee::install(args).await.context("\nFailed to install rayhunter on the Moxee Hotspot")?, Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?, Command::Util(subcommand) => { match subcommand.command { diff --git a/installer/src/moxee.rs b/installer/src/moxee.rs new file mode 100644 index 00000000..204948c3 --- /dev/null +++ b/installer/src/moxee.rs @@ -0,0 +1,15 @@ +use anyhow::Result; + +use crate::MoxeeArgs; + +pub async fn install(args: MoxeeArgs) -> Result<()> { + let data_dir = args.data_dir.or(Some("/cache/rayhunter-data".to_string())); + crate::orbic_network::install( + args.admin_ip, + args.admin_username, + args.admin_password, + args.reset_config, + data_dir, + ) + .await +} diff --git a/installer/src/orbic.rs b/installer/src/orbic.rs index a414e4b0..c38dbbde 100644 --- a/installer/src/orbic.rs +++ b/installer/src/orbic.rs @@ -158,13 +158,7 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re let mut conn = AdbConnection { device: &mut adb_device, }; - install_config( - &mut conn, - "/data/rayhunter/config.toml", - "orbic", - reset_config, - ) - .await?; + install_config(&mut conn, "orbic", reset_config).await?; } install_file( diff --git a/installer/src/orbic_network.rs b/installer/src/orbic_network.rs index 840c9246..177705aa 100644 --- a/installer/src/orbic_network.rs +++ b/installer/src/orbic_network.rs @@ -8,7 +8,7 @@ use serde::Deserialize; use tokio::time::sleep; use crate::RAYHUNTER_DAEMON_INIT; -use crate::connection::{TelnetConnection, install_config}; +use crate::connection::{TelnetConnection, install_config, setup_data_directory}; use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password}; use crate::output::{eprintln, print, println}; use crate::util::{interactive_shell, telnet_send_command, telnet_send_file}; @@ -147,6 +147,7 @@ pub async fn install( admin_username: String, admin_password: Option, reset_config: bool, + data_dir: Option, ) -> Result<()> { let Some(admin_password) = admin_password else { eprintln!( @@ -170,7 +171,8 @@ pub async fn install( wait_for_telnet(&admin_ip).await?; println!("done"); - setup_rayhunter(&admin_ip, reset_config).await + let data_dir = data_dir.unwrap_or_else(|| "/data/rayhunter-data".to_string()); + setup_rayhunter(&admin_ip, reset_config, &data_dir).await } async fn wait_for_telnet(admin_ip: &str) -> Result<()> { @@ -194,7 +196,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> { Ok(()) } -async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> { +async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) -> Result<()> { let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?; let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); @@ -208,7 +210,8 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> { ) .await?; - telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", false).await?; + let mut conn = TelnetConnection::new(addr, false); + setup_data_directory(&mut conn, data_dir).await?; telnet_send_file( addr, @@ -218,14 +221,7 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> { ) .await?; - let mut conn = TelnetConnection::new(addr, false); - install_config( - &mut conn, - "/data/rayhunter/config.toml", - "orbic", - reset_config, - ) - .await?; + install_config(&mut conn, "orbic", reset_config).await?; telnet_send_file( addr, diff --git a/installer/src/tplink.rs b/installer/src/tplink.rs index efbf3d49..8873dc41 100644 --- a/installer/src/tplink.rs +++ b/installer/src/tplink.rs @@ -18,7 +18,7 @@ use serde::Deserialize; use tokio::time::sleep; use crate::InstallTpLink; -use crate::connection::{TelnetConnection, install_config}; +use crate::connection::{TelnetConnection, install_config, setup_data_directory}; use crate::output::println; use crate::util::{interactive_shell, telnet_send_command, telnet_send_file}; @@ -30,10 +30,19 @@ pub async fn main_tplink( admin_ip, sdcard_path, reset_config, + data_dir, }: InstallTpLink, ) -> Result<(), Error> { let is_v3 = start_telnet(&admin_ip).await?; - tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3, reset_config).await + tplink_run_install( + skip_sdcard, + admin_ip, + sdcard_path, + is_v3, + reset_config, + data_dir, + ) + .await } #[derive(Deserialize)] @@ -114,19 +123,15 @@ async fn tplink_run_install( mut sdcard_path: String, is_v3: bool, reset_config: bool, + cli_data_dir: Option, ) -> Result<(), Error> { println!("Connecting via telnet to {admin_ip}"); let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); - if skip_sdcard { - sdcard_path = "/data/rayhunter-data".to_owned(); - telnet_send_command( - addr, - &format!("mkdir -p {sdcard_path}"), - "exit code 0", - true, - ) - .await? + let data_dir = if let Some(dir) = cli_data_dir { + dir + } else if skip_sdcard { + "/cache/rayhunter-data".to_owned() } else { if sdcard_path.is_empty() { let try_paths = [ @@ -146,9 +151,12 @@ async fn tplink_run_install( } if sdcard_path.is_empty() { + // This error message is shown when the installer cannot figure out where this + // device _would_ mount an SD card, regardless of whether the user did insert one. + // If we get here, it's likely the installer was never tested for this hardware + // version. anyhow::bail!( - "Unable to determine sdcard path. Rayhunter needs a FAT-formatted SD card to function.\n\n\ - If you already inserted a FAT formatted SD card, this is a bug. Please file an issue with your hardware version.\n\n\ + "Unable to determine sdcard path. This is a bug. Please file an issue with your hardware version.\n\n\ The installer has tried to find an empty folder to mount to on these paths: {try_paths:?}\n\ ...but none of them exist.\n\n\ At this point, you may 'telnet {admin_ip}' and poke around in the device to figure out what went wrong yourself." @@ -166,49 +174,43 @@ async fn tplink_run_install( .await .is_err() { - telnet_send_command(addr, &format!("mount /dev/mmcblk0p1 {sdcard_path}"), "exit code 0", true).await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?; + // Try to mount the SD card, and if that fails we assume the user didn't insert one. + telnet_send_command(addr, &format!("mount /dev/mmcblk0p1 {sdcard_path}"), "exit code 0", true).await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few hours. Insert one and rerun this installer, or pass --skip-sdcard")?; } else { println!("sdcard already mounted"); } - } - // there is too little space on the internal flash to store anything, but the initrd script - // expects things to be at this location - telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0", true).await?; - telnet_send_command(addr, "mkdir -p /data", "exit code 0", true).await?; - telnet_send_command( - addr, - &format!("ln -sf {sdcard_path} /data/rayhunter"), - "exit code 0", - true, - ) - .await?; + sdcard_path + }; let mut conn = TelnetConnection::new(addr, true); - let config_path = format!("{sdcard_path}/config.toml"); - install_config(&mut conn, &config_path, "tplink", reset_config).await?; + setup_data_directory(&mut conn, &data_dir).await?; + + install_config(&mut conn, "tplink", reset_config).await?; let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); telnet_send_file( addr, - &format!("{sdcard_path}/rayhunter-daemon"), + "/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin, true, ) .await?; + let init_script = get_rayhunter_daemon(if skip_sdcard { None } else { Some(&data_dir) }); + telnet_send_file( addr, "/etc/init.d/rayhunter_daemon", - get_rayhunter_daemon(&sdcard_path).as_bytes(), + init_script.as_bytes(), true, ) .await?; telnet_send_command( addr, - &format!("chmod ugo+x {sdcard_path}/rayhunter-daemon"), + "chmod ugo+x /data/rayhunter/rayhunter-daemon", "exit code 0", true, ) @@ -368,18 +370,19 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> { Ok(()) } -fn get_rayhunter_daemon(sdcard_path: &str) -> String { +fn get_rayhunter_daemon(sdcard_path: Option<&str>) -> String { // Even though TP-Link eventually auto-mounts the SD card, it sometimes does so too late. And // changing the order in which daemons are started up seems to not work reliably. // // This part of the daemon dynamically generated because we may have to eventually add logic // specific to a particular hardware revision here. - crate::RAYHUNTER_DAEMON_INIT.replace( - "#RAYHUNTER-PRESTART", - &format!( - "(mount /dev/mmcblk0p1 {sdcard_path} || true) 2>&1 | tee /tmp/rayhunter-mount.log" - ), - ) + let prestart = match sdcard_path { + Some(path) => { + format!("(mount /dev/mmcblk0p1 {path} || true) 2>&1 | tee /tmp/rayhunter-mount.log") + } + None => String::new(), + }; + crate::RAYHUNTER_DAEMON_INIT.replace("#RAYHUNTER-PRESTART", &prestart) } /// Root the TP-Link device and open an interactive shell @@ -390,6 +393,10 @@ pub async fn shell(admin_ip: &str) -> Result<(), Error> { #[test] fn test_get_rayhunter_daemon() { - let s = get_rayhunter_daemon("/media/card"); + let s = get_rayhunter_daemon(Some("/media/card")); assert!(s.contains("mount /dev/mmcblk0p1 /media/card")); + + let s = get_rayhunter_daemon(None); + assert!(!s.contains("mmcblk0p1")); + assert!(!s.contains("#RAYHUNTER-PRESTART")); } diff --git a/installer/src/util.rs b/installer/src/util.rs index 0a112238..8a5ee47a 100644 --- a/installer/src/util.rs +++ b/installer/src/util.rs @@ -102,7 +102,7 @@ pub async fn telnet_send_file( let handle = tokio::spawn(async move { telnet_send_command_with_output( addr, - &format!("nc -l -p 8081 >{filename}.tmp"), + &format!("nc -l -p 8081 2>&1 >{filename}.tmp"), wait_for_prompt, ) .await @@ -130,7 +130,7 @@ pub async fn telnet_send_file( print!("attempt {attempts}... "); } - { + let send_result: Result<()> = async { let mut stream = stream?; stream.write_all(payload).await?; @@ -143,14 +143,21 @@ pub async fn telnet_send_file( // application buffers here. sleep(Duration::from_millis(1000)).await; - // ensure that stream is dropped before we wait for nc to terminate. - drop(stream); + Ok(()) } + .await; - handle + let nc_output = handle .await .context("background nc writer failed")? - .context("background nc writer failed")? + .context("background nc writer failed")?; + + if let Err(e) = send_result { + bail!( + "Failed to send data to nc: {e}. nc output: '{}'", + nc_output.trim() + ); + } }; let checksum = md5::compute(payload); From 388ee8ca2ea42e8e7c9ba9cc9debef62df5db381 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sun, 22 Feb 2026 17:48:54 +0100 Subject: [PATCH 2/6] Update installer/src/lib.rs Co-authored-by: Will Greenberg --- installer/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/installer/src/lib.rs b/installer/src/lib.rs index 70625bf4..69859d67 100644 --- a/installer/src/lib.rs +++ b/installer/src/lib.rs @@ -89,7 +89,8 @@ struct InstallTpLink { reset_config: bool, /// Override the data directory path. Defaults to /cache/rayhunter-data (or SD card path when - /// SD card is used). Must not be /data/rayhunter. + /// SD card is used). Must not be /data/rayhunter, which lives on a storage partition that's + /// too small for normal Rayhunter operation. #[arg(long)] data_dir: Option, } From 50b595cc318c0b6418108a9092720ab330e89f6e Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sun, 22 Feb 2026 17:47:02 +0100 Subject: [PATCH 3/6] better errorhandling --- installer/src/connection.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/installer/src/connection.rs b/installer/src/connection.rs index d7751a46..0e1fffe9 100644 --- a/installer/src/connection.rs +++ b/installer/src/connection.rs @@ -66,12 +66,14 @@ pub async fn readlink(conn: &mut C, path: &str) -> Result Date: Sun, 22 Feb 2026 17:52:33 +0100 Subject: [PATCH 4/6] slightly better escaping for user input --- installer/src/connection.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/installer/src/connection.rs b/installer/src/connection.rs index 0e1fffe9..575112f5 100644 --- a/installer/src/connection.rs +++ b/installer/src/connection.rs @@ -17,7 +17,7 @@ pub trait DeviceConnection { /// Check if a file exists using a DeviceConnection pub async fn file_exists(conn: &mut C, path: &str) -> bool { - conn.run_command(&format!("test -f {path} && echo exists || echo missing")) + conn.run_command(&format!("test -f '{path}' && echo exists || echo missing")) .await .map(|output| output.contains("exists")) .unwrap_or(false) @@ -45,7 +45,7 @@ pub async fn install_config( /// Check if a directory exists using a DeviceConnection pub async fn dir_exists(conn: &mut C, path: &str) -> bool { - conn.run_command(&format!("test -d {path} && echo exists || echo missing")) + conn.run_command(&format!("test -d '{path}' && echo exists || echo missing")) .await .map(|output| output.contains("exists")) .unwrap_or(false) @@ -53,7 +53,7 @@ pub async fn dir_exists(conn: &mut C, path: &str) -> bool { /// Check if a path is a symlink using a DeviceConnection pub async fn is_symlink(conn: &mut C, path: &str) -> bool { - conn.run_command(&format!("test -L {path} && echo yes || echo no")) + conn.run_command(&format!("test -L '{path}' && echo yes || echo no")) .await .map(|output| output.contains("yes")) .unwrap_or(false) @@ -64,7 +64,7 @@ pub async fn readlink(conn: &mut C, path: &str) -> Result(conn: &mut C, data_dir: & bail!("data_dir must not be /data/rayhunter"); } + if data_dir.contains("'") { + bail!("data_dir must not contain an apostrophe (')"); + } + // Determine where old data lives, if anywhere let old_data_source = if is_symlink(conn, "/data/rayhunter").await { let current_target = readlink(conn, "/data/rayhunter").await?; @@ -129,7 +133,7 @@ pub async fn setup_data_directory(conn: &mut C, data_dir: & // XXX: DeviceConnection::run_command does not expose the exit code of the ran command. It // probably should, or a utility for it should exist? let mv_output = conn - .run_command(&format!("mv {old_source} {data_dir} && echo MV_OK")) + .run_command(&format!("mv {old_source} '{data_dir}' && echo MV_OK")) .await?; if mv_output.contains("MV_OK") { println!("ok"); @@ -138,13 +142,13 @@ pub async fn setup_data_directory(conn: &mut C, data_dir: & } } else { // No migration needed, just ensure the target directory exists - conn.run_command(&format!("mkdir -p {data_dir}")).await?; + conn.run_command(&format!("mkdir -p '{data_dir}'")).await?; } // Create the symlink print!("Creating symlink /data/rayhunter -> {data_dir} ... "); conn.run_command("mkdir -p /data").await?; - conn.run_command(&format!("ln -sf {data_dir} /data/rayhunter")) + conn.run_command(&format!("ln -sf '{data_dir}' /data/rayhunter")) .await?; println!("ok"); From ce0c01d12ba12409e213054ac72c404d592db596 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 23 Feb 2026 14:51:16 +0100 Subject: [PATCH 5/6] add one missing single quote --- installer/src/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/src/connection.rs b/installer/src/connection.rs index 575112f5..e5cd8446 100644 --- a/installer/src/connection.rs +++ b/installer/src/connection.rs @@ -133,7 +133,7 @@ pub async fn setup_data_directory(conn: &mut C, data_dir: & // XXX: DeviceConnection::run_command does not expose the exit code of the ran command. It // probably should, or a utility for it should exist? let mv_output = conn - .run_command(&format!("mv {old_source} '{data_dir}' && echo MV_OK")) + .run_command(&format!("mv '{old_source}' '{data_dir}' && echo MV_OK")) .await?; if mv_output.contains("MV_OK") { println!("ok"); From 99080a35817a9f51db74f81c393fd3c8aead7c31 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 23 Feb 2026 22:22:15 +0100 Subject: [PATCH 6/6] fix botched rebase --- installer/src/util.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/installer/src/util.rs b/installer/src/util.rs index 8a5ee47a..6b5f5e19 100644 --- a/installer/src/util.rs +++ b/installer/src/util.rs @@ -158,6 +158,8 @@ pub async fn telnet_send_file( nc_output.trim() ); } + + nc_output }; let checksum = md5::compute(payload);