Do not overwrite configs by default

On tplink and orbic, do not overwrite config files by default. There is
a new flag `installer orbic --reset-config` that one can use to restore
the old behavior. This fixes #778, a long-standing issue existent since
0.3.0.

The businesslogic for config file overrides is shared to some degree.
The Install trait from pinephone.rs has been moved out and renamed to
DeviceConnection for that purpose, so that `install_config` can be
shared across installers, which in turn can delegate to the trait for
running commands and copying files. This also works towards #542.

However, the pinephone and other installers have not been adapted to
support --reset-config out of fear of regressions. A future refactor by
somebody with ability to test on pinephone should probably also consider
using the same DeviceConnection impl as orbic, if possible.
This commit is contained in:
Markus Unterwaditzer
2026-01-14 21:49:17 +01:00
committed by Will Greenberg
parent 9e08e662ff
commit d607c63cc8
6 changed files with 185 additions and 67 deletions

View File

@@ -0,0 +1,68 @@
use std::future::Future;
use std::net::SocketAddr;
use anyhow::Result;
use crate::output::println;
/// Abstraction for device communication (telnet or ADB)
pub trait DeviceConnection {
/// Run a shell command and return its output
fn run_command(&mut self, command: &str) -> impl Future<Output = Result<String>> + Send;
/// Write a file to the device
fn write_file(&mut self, path: &str, content: &[u8])
-> impl Future<Output = Result<()>> + Send;
}
/// Check if a file exists using a DeviceConnection
pub async fn file_exists<C: DeviceConnection>(conn: &mut C, path: &str) -> bool {
conn.run_command(&format!("test -f {path} && echo exists || echo missing"))
.await
.map(|output| output.contains("exists"))
.unwrap_or(false)
}
/// Shared config installation logic
pub async fn install_config<C: DeviceConnection>(
conn: &mut C,
config_path: &str,
device_type: &str,
reset_config: bool,
) -> Result<()> {
if reset_config || !file_exists(conn, config_path).await {
let config = crate::CONFIG_TOML.replace(
r#"#device = "orbic""#,
&format!(r#"device = "{device_type}""#),
);
conn.write_file(config_path, config.as_bytes()).await?;
} else {
println!("Config file already exists, skipping (use --reset-config to overwrite)");
}
Ok(())
}
/// Telnet-based connection wrapper
pub struct TelnetConnection {
pub addr: SocketAddr,
pub wait_for_prompt: bool,
}
impl TelnetConnection {
pub fn new(addr: SocketAddr, wait_for_prompt: bool) -> Self {
Self {
addr,
wait_for_prompt,
}
}
}
impl DeviceConnection for TelnetConnection {
async fn run_command(&mut self, command: &str) -> Result<String> {
crate::util::telnet_send_command_with_output(self.addr, command, self.wait_for_prompt).await
}
async fn write_file(&mut self, path: &str, content: &[u8]) -> Result<()> {
crate::util::telnet_send_file(self.addr, path, content, self.wait_for_prompt).await
}
}

View File

@@ -5,6 +5,7 @@ use env_logger::Env;
#[cfg(not(target_os = "android"))]
use anyhow::bail;
mod connection;
#[cfg(not(target_os = "android"))]
mod orbic;
mod orbic_auth;
@@ -79,10 +80,18 @@ struct InstallTpLink {
/// your custom path may conflict with the builtin storage functionality.
#[arg(long, default_value = "")]
sdcard_path: String,
/// Overwrite config.toml even if it already exists on the device.
#[arg(long)]
reset_config: bool,
}
#[derive(Parser, Debug)]
struct InstallOrbic {}
struct InstallOrbic {
/// Overwrite config.toml even if it already exists on the device.
#[arg(long)]
reset_config: bool,
}
#[derive(Parser, Debug)]
struct OrbicNetworkArgs {
@@ -97,6 +106,10 @@ struct OrbicNetworkArgs {
/// Admin password for authentication.
#[arg(long)]
admin_password: Option<String>,
/// Overwrite config.toml even if it already exists on the device.
#[arg(long)]
reset_config: bool,
}
#[derive(Parser, Debug)]
@@ -231,8 +244,8 @@ async fn run(args: Args) -> Result<(), Error> {
Command::Pinephone(_) => pinephone::install().await
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
#[cfg(not(target_os = "android"))]
Command::OrbicUsb(_) => orbic::install().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).await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
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::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
Command::Util(subcommand) => {
match subcommand.command {

View File

@@ -12,9 +12,10 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
use sha2::{Digest, Sha256};
use tokio::time::sleep;
use crate::RAYHUNTER_DAEMON_INIT;
use crate::connection::{DeviceConnection, install_config};
use crate::output::{print, println};
use crate::util::open_usb_device;
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
Make sure your device is plugged in and turned on.
@@ -46,6 +47,21 @@ const PRODUCT_ID: u16 = 0xf601;
const INTERFACE: u8 = 1;
/// ADB-based connection wrapper for DeviceConnection trait
pub struct AdbConnection<'a> {
device: &'a mut ADBUSBDevice,
}
impl DeviceConnection for AdbConnection<'_> {
async fn run_command(&mut self, command: &str) -> Result<String> {
adb_command(self.device, &["sh", "-c", command])
}
async fn write_file(&mut self, path: &str, content: &[u8]) -> Result<()> {
install_file(self.device, path, content).await
}
}
#[cfg(target_os = "windows")]
const RNDIS_INTERFACE: u8 = 0;
@@ -61,7 +77,7 @@ async fn confirm() -> Result<bool> {
Ok(input.trim() == "yes")
}
pub async fn install() -> Result<()> {
pub async fn install(reset_config: bool) -> Result<()> {
println!(
"WARNING: The orbic USB installer is not recommended for most usecases. Consider using ./installer orbic instead, unless you want ADB access for other purposes."
);
@@ -80,7 +96,7 @@ pub async fn install() -> Result<()> {
setup_rootshell(&mut adb_device).await?;
println!("done");
print!("Installing rayhunter... ");
let mut adb_device = setup_rayhunter(adb_device).await?;
let mut adb_device = setup_rayhunter(adb_device, reset_config).await?;
println!("done");
print!("Testing rayhunter... ");
test_rayhunter(&mut adb_device).await?;
@@ -127,7 +143,7 @@ async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
Ok(())
}
async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Result<ADBUSBDevice> {
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
adb_at_syscmd(&mut adb_device, "mkdir -p /data/rayhunter").await?;
@@ -137,14 +153,20 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
rayhunter_daemon_bin,
)
.await?;
install_file(
&mut adb_device,
"/data/rayhunter/config.toml",
CONFIG_TOML
.replace("#device = \"orbic\"", "device = \"orbic\"")
.as_bytes(),
)
.await?;
{
let mut conn = AdbConnection {
device: &mut adb_device,
};
install_config(
&mut conn,
"/data/rayhunter/config.toml",
"orbic",
reset_config,
)
.await?;
}
install_file(
&mut adb_device,
"/etc/init.d/rayhunter_daemon",

View File

@@ -7,10 +7,11 @@ use reqwest::Client;
use serde::Deserialize;
use tokio::time::sleep;
use crate::RAYHUNTER_DAEMON_INIT;
use crate::connection::{TelnetConnection, install_config};
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};
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
// Some kajeet devices have password protected telnetd on port 23, so we use port 24 just in case
const TELNET_PORT: u16 = 24;
@@ -142,6 +143,7 @@ pub async fn install(
admin_ip: String,
admin_username: String,
admin_password: Option<String>,
reset_config: bool,
) -> Result<()> {
let Some(admin_password) = admin_password else {
eprintln!(
@@ -165,7 +167,7 @@ pub async fn install(
wait_for_telnet(&admin_ip).await?;
println!("done");
setup_rayhunter(&admin_ip).await
setup_rayhunter(&admin_ip, reset_config).await
}
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
@@ -189,7 +191,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
Ok(())
}
async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
@@ -213,13 +215,12 @@ async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
)
.await?;
telnet_send_file(
addr,
let mut conn = TelnetConnection::new(addr, false);
install_config(
&mut conn,
"/data/rayhunter/config.toml",
CONFIG_TOML
.replace(r#"#device = "orbic""#, r#"device = "orbic""#)
.as_bytes(),
false,
"orbic",
reset_config,
)
.await?;

View File

@@ -9,6 +9,7 @@ use nusb::Interface;
use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
use tokio::time::sleep;
use crate::connection::DeviceConnection;
use crate::orbic::test_rayhunter;
use crate::output::{print, println};
use crate::util::open_usb_device;
@@ -25,33 +26,39 @@ pub async fn install() -> Result<()> {
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
println!("ok");
adb.run_command(&["mount", "-o", "remount,rw", "/"], "exit code 0")?;
adb.run_command(&["mkdir", "-p", "/data/rayhunter"], "exit code 0")?;
run_command_expect(&mut adb, "mount -o remount,rw /", "exit code 0").await?;
run_command_expect(&mut adb, "mkdir -p /data/rayhunter", "exit code 0").await?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
adb.install_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)?;
adb.install_file(
adb.write_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)
.await?;
adb.write_file(
"/data/rayhunter/config.toml",
CONFIG_TOML
.replace("#device = \"orbic\"", "device = \"pinephone\"")
.as_bytes(),
)?;
adb.install_file(
)
.await?;
adb.write_file(
"/etc/init.d/rayhunter_daemon",
RAYHUNTER_DAEMON_INIT.as_bytes(),
)?;
adb.install_file(
)
.await?;
adb.write_file(
"/etc/init.d/misc-daemon",
include_bytes!("../../dist/scripts/misc-daemon"),
)?;
adb.run_command(
&["chmod", "755", "/etc/init.d/rayhunter_daemon"],
)
.await?;
run_command_expect(
&mut adb,
"chmod 755 /etc/init.d/rayhunter_daemon",
"exit code 0",
)?;
adb.run_command(&["chmod", "755", "/etc/init.d/misc-daemon"], "exit code 0")?;
)
.await?;
run_command_expect(&mut adb, "chmod 755 /etc/init.d/misc-daemon", "exit code 0").await?;
println!("Rebooting device and waiting 30 seconds for it to start up.");
adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?;
run_command_expect(&mut adb, "shutdown -r -t 1 now", "exit code 0").await?;
sleep(Duration::from_secs(30)).await;
print!("Unlocking modem ... ");
@@ -68,6 +75,19 @@ pub async fn install() -> Result<()> {
Ok(())
}
/// Helper to run a command and check for expected output
async fn run_command_expect(
adb: &mut ADBUSBDevice,
command: &str,
expected_output: &str,
) -> Result<()> {
let output = adb.run_command(command).await?;
if !output.contains(expected_output) {
bail!("{expected_output:?} not found in: {output}");
}
Ok(())
}
struct Qusbcfg {
vendor_id: u16,
product_id: u16,
@@ -175,29 +195,18 @@ pub async fn stop_adb() -> Result<()> {
Ok(())
}
trait Install {
fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()>;
fn install_file(&mut self, dest: &str, payload: &[u8]) -> Result<()>;
}
impl Install for ADBUSBDevice {
/// Run an adb shell command, append '; echo exit code $?' to the command and verify its output.
fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()> {
impl DeviceConnection for ADBUSBDevice {
/// Run an adb shell command, append '; echo exit code $?' to the command and return output.
async fn run_command(&mut self, command: &str) -> Result<String> {
let mut buf = Vec::<u8>::new();
let mut cmd = Vec::<&str>::new();
cmd.extend_from_slice(command);
cmd.extend_from_slice(&[";", "echo", "exit code $?"]);
let cmd = ["sh", "-c", &format!("{command}; echo exit code $?")];
self.shell_command(&cmd, &mut buf)?;
let output = String::from_utf8_lossy(&buf);
if !output.contains(expected_output) {
bail!("{expected_output:?} not found in: {output}");
}
Ok(())
Ok(String::from_utf8_lossy(&buf).into_owned())
}
/// Transfer a file to the modem's filesystem with adb push.
/// Validates the file sends successfully to /tmp before overwriting the destination.
fn install_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
async fn write_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
print!("Sending file {dest} ... ");
let file_name = Path::new(dest)
.file_name()
@@ -208,8 +217,16 @@ impl Install for ADBUSBDevice {
let push_tmp_path = format!("/tmp/{file_name}");
let file_hash = md5_compute(payload);
self.push(&mut payload, &push_tmp_path)?;
self.run_command(&["md5sum", &push_tmp_path], &format!("{file_hash:x}"))?;
self.run_command(&["mv", &push_tmp_path, dest], "exit code 0")?;
let output = self.run_command(&format!("md5sum {push_tmp_path}")).await?;
if !output.contains(&format!("{file_hash:x}")) {
bail!("{:x} not found in: {output}", file_hash);
}
let output = self
.run_command(&format!("mv {push_tmp_path} {dest}"))
.await?;
if !output.contains("exit code 0") {
bail!("exit code 0 not found in: {output}");
}
println!("ok");
Ok(())
}

View File

@@ -18,6 +18,7 @@ use serde::Deserialize;
use tokio::time::sleep;
use crate::InstallTpLink;
use crate::connection::{TelnetConnection, install_config};
use crate::output::println;
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
@@ -28,10 +29,11 @@ pub async fn main_tplink(
skip_sdcard,
admin_ip,
sdcard_path,
reset_config,
}: InstallTpLink,
) -> Result<(), Error> {
let is_v3 = start_telnet(&admin_ip).await?;
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3).await
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3, reset_config).await
}
#[derive(Deserialize)]
@@ -111,6 +113,7 @@ async fn tplink_run_install(
admin_ip: String,
mut sdcard_path: String,
is_v3: bool,
reset_config: bool,
) -> Result<(), Error> {
println!("Connecting via telnet to {admin_ip}");
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
@@ -181,15 +184,9 @@ async fn tplink_run_install(
)
.await?;
telnet_send_file(
addr,
&format!("{sdcard_path}/config.toml"),
crate::CONFIG_TOML
.replace("#device = \"orbic\"", "device = \"tplink\"")
.as_bytes(),
true,
)
.await?;
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?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));