diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 559951fb..df23708a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,6 +11,8 @@ env: CARGO_TERM_COLOR: always FILE_ROOTSHELL: ../../rootshell/rootshell FILE_RAYHUNTER_DAEMON: ../../rayhunter-daemon/rayhunter-daemon + FILE_WPA_SUPPLICANT: ../../rayhunter-daemon/rayhunter-daemon + FILE_WPA_CLI: ../../rayhunter-daemon/rayhunter-daemon RUSTFLAGS: "-Dwarnings" jobs: diff --git a/client-mode/README.md b/client-mode/README.md new file mode 100644 index 00000000..5c34f499 --- /dev/null +++ b/client-mode/README.md @@ -0,0 +1,86 @@ +# WiFi Client Mode for Rayhunter (Orbic RC400L) + +Connect the Orbic to an existing WiFi network while keeping its AP running. +This enables internet access (for ntfy notifications, etc.) and allows +accessing the Rayhunter web UI from any device on your network. + +## How It Works + +The Orbic's QCA6174 supports concurrent AP + station mode. `wlan0` runs +the AP (via hostapd/QCMAP), and `wlan1` is configured as a station using +a cross-compiled `wpa_supplicant`. + +## Quick Start + +1. Build wpa_supplicant (one-time): + ``` + cd tools/build-wpa-supplicant + docker build --platform linux/amd64 --target export --output type=local,dest=./out . + ``` + +2. Push files to device: + ``` + sh client-mode/scripts/setup-device.sh + ``` + +3. Set credentials via the Rayhunter web UI (Settings > WiFi Client Mode), + or via the installer: + ``` + ./installer orbic --admin-password YOUR_PASS --wifi-ssid MyNetwork --wifi-password MyPass + ``` + +4. Reboot. WiFi client starts automatically. Check the log: + ``` + adb shell cat /tmp/wifi-client.log + ``` + +## File Layout on Device + +``` +/data/rayhunter/ + bin/wpa_supplicant # Static ARMv7 binary + bin/wpa_cli # Static ARMv7 binary + scripts/wifi-client.sh # Main script (start/stop/status) + wifi-creds.conf # Credentials (ssid=X / password=Y) +``` + +## What the Script Does + +1. Waits for wlan1 to appear (up to 30s) +2. Sets wlan1 to managed mode, starts wpa_supplicant +3. Obtains IP via DHCP +4. Fixes routing: replaces bridge0 default route, adds policy routing + (table 100) so replies from wlan1's IP always exit via wlan1 +5. Sets DNS to 8.8.8.8 +6. Configures iptables: allows inbound on wlan1, blocks outbound except + ESTABLISHED/RELATED, DHCP, DNS, and HTTPS (port 443 for ntfy) + +## AT+SYSCMD + +Commands needing `CAP_NET_ADMIN` (iw, iptables, ip rule) cannot run through +rootshell -- ADB's capability bounding set is too restrictive. The init +script triggers wifi-client.sh which runs with full capabilities. + +Key constraint: AT+SYSCMD via `/dev/smd8` is **one-shot per boot**. The +installer uses USB bulk transfers and can send multiple commands. + +## Disabling + +Delete or rename the credentials file, then reboot: +``` +adb shell "mv /data/rayhunter/wifi-creds.conf /data/rayhunter/wifi-creds.conf.disabled" +``` + +All network changes are runtime-only -- a reboot always restores defaults. + +## Troubleshooting + +Check the log first: `adb shell cat /tmp/wifi-client.log` + +- **No log file**: wifi-client.sh didn't run. Check that wifi-creds.conf + exists and the init script has the PRESTART replacement. +- **wpa_supplicant connects but no IP**: Check udhcpc uses + `-s /etc/udhcpc.d/50default`. +- **Can't reach device from LAN**: Likely a policy routing issue. The + script handles this, but if bridge0 and wlan1 share a subnet + (both 192.168.1.0/24), check `ip rule show` and `ip route show table 100`. diff --git a/client-mode/scripts/setup-device.sh b/client-mode/scripts/setup-device.sh new file mode 100755 index 00000000..847053dc --- /dev/null +++ b/client-mode/scripts/setup-device.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# Pushes all client-mode files to the Orbic device via ADB. +# Run from the rayhunter repo root. +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WPA_DIR="$SCRIPT_DIR/../../tools/build-wpa-supplicant/out" + +if ! adb devices | grep -q device$; then + echo "No ADB device found" + exit 1 +fi + +echo "Pushing scripts..." +adb shell "mkdir -p /data/rayhunter/scripts /data/rayhunter/bin" +adb push "$SCRIPT_DIR/wifi-client.sh" /data/rayhunter/scripts/wifi-client.sh + +if [ -f "$WPA_DIR/wpa_supplicant" ]; then + echo "Pushing wpa_supplicant binaries..." + adb push "$WPA_DIR/wpa_supplicant" /data/rayhunter/bin/wpa_supplicant + adb push "$WPA_DIR/wpa_cli" /data/rayhunter/bin/wpa_cli +else + echo "wpa_supplicant binaries not found at $WPA_DIR" + echo "Build them first: see tools/build-wpa-supplicant/Dockerfile" + exit 1 +fi + +echo "" +echo "Files pushed. Set WiFi credentials via the web UI or installer," +echo "then reboot. WiFi client starts automatically on boot." diff --git a/client-mode/scripts/wifi-client.sh b/client-mode/scripts/wifi-client.sh new file mode 100755 index 00000000..65141c99 --- /dev/null +++ b/client-mode/scripts/wifi-client.sh @@ -0,0 +1,154 @@ +#!/bin/sh +# WiFi client mode for Rayhunter - connects wlan1 to an existing network +# Reads credentials from /data/rayhunter/wifi-creds.conf +# Format: +# ssid=YourNetworkName +# password=YourPassword + +LOG="/tmp/wifi-client.log" +exec > "$LOG" 2>&1 + +CRED_FILE="/data/rayhunter/wifi-creds.conf" +WPA_BIN="/data/rayhunter/bin/wpa_supplicant" +WPA_CONF="/tmp/wpa_sta.conf" +WPA_PID="/tmp/wpa_sta.pid" +DHCP_PID="/tmp/udhcpc_wlan1.pid" +IFACE="wlan1" +RT_TABLE=100 + +stop() { + [ -f "$WPA_PID" ] && kill "$(cat "$WPA_PID")" 2>/dev/null && rm -f "$WPA_PID" + [ -f "$DHCP_PID" ] && kill "$(cat "$DHCP_PID")" 2>/dev/null && rm -f "$DHCP_PID" + ip link set "$IFACE" down 2>/dev/null +} + +start() { + if [ ! -f "$CRED_FILE" ]; then + echo "No credentials file at $CRED_FILE" + exit 1 + fi + + SSID=$(grep '^ssid=' "$CRED_FILE" | cut -d= -f2-) + PSK=$(grep '^password=' "$CRED_FILE" | cut -d= -f2-) + + if [ -z "$SSID" ] || [ -z "$PSK" ]; then + echo "Missing ssid or password in $CRED_FILE" + exit 1 + fi + + # Wait for the wireless interface to appear (created asynchronously by QCMAP/hostapd) + for i in $(seq 1 30); do + [ -d "/sys/class/net/$IFACE" ] && break + [ "$i" = "1" ] && echo "Waiting for $IFACE..." + sleep 1 + done + if [ ! -d "/sys/class/net/$IFACE" ]; then + echo "$IFACE not found after 30s, giving up" + exit 1 + fi + + stop 2>/dev/null + sleep 1 + + echo "Configuring $IFACE for station mode" + iw dev "$IFACE" set type managed + ip link set "$IFACE" up + + cat > "$WPA_CONF" < wlan1" + ip route del default dev bridge0 2>/dev/null + fi + ip route replace default via "$WLAN1_GW" dev "$IFACE" metric 10 + + # Policy routing: force traffic from our DHCP IP out wlan1 + # (needed because bridge0 shares the same subnet) + ip rule del from "$WLAN1_IP" table $RT_TABLE 2>/dev/null + ip route flush table $RT_TABLE 2>/dev/null + ip rule add from "$WLAN1_IP" table $RT_TABLE + ip route add "$WLAN1_SUBNET" dev "$IFACE" src "$WLAN1_IP" table $RT_TABLE + ip route add default via "$WLAN1_GW" dev "$IFACE" table $RT_TABLE + + echo "nameserver 8.8.8.8" > /etc/resolv.conf + + # Allow inbound traffic on wlan1 + iptables -I INPUT -i "$IFACE" -j ACCEPT + iptables -I FORWARD -i "$IFACE" -j ACCEPT + + # Block stock Orbic daemons from phoning home (dmclient, upgrade, etc.) + # Allow only: replies to incoming connections, DHCP renewal, DNS, and HTTPS + # (needed for ntfy notifications). + iptables -A OUTPUT -o "$IFACE" -m state --state ESTABLISHED,RELATED -j ACCEPT + iptables -A OUTPUT -o "$IFACE" -p udp --dport 67:68 -j ACCEPT + iptables -A OUTPUT -o "$IFACE" -p udp --dport 53 -j ACCEPT + iptables -A OUTPUT -o "$IFACE" -p tcp --dport 53 -j ACCEPT + iptables -A OUTPUT -o "$IFACE" -p tcp --dport 443 -j ACCEPT + iptables -A OUTPUT -o "$IFACE" -j DROP + + echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptables + + echo "=== iptables OUTPUT ===" + iptables -L OUTPUT -v -n 2>&1 + + echo "=== policy routing ===" + ip rule show + echo "--- table $RT_TABLE ---" + ip route show table $RT_TABLE + + echo "=== network state ===" + ip addr show "$IFACE" | grep 'inet ' + ip route show + + echo "Internet test:" + wget -q -O /dev/null http://detectportal.firefox.com/success.txt && echo "OK" || echo "FAILED" +} + +status() { + if [ -f "$WPA_PID" ] && kill -0 "$(cat "$WPA_PID")" 2>/dev/null; then + ip addr show "$IFACE" | grep 'inet ' | awk '{print $2}' + else + echo "disconnected" + return 1 + fi +} + +case "$1" in + start) start ;; + stop) stop ;; + status) status ;; + *) echo "Usage: $0 {start|stop|status}" >&2; exit 1 ;; +esac diff --git a/daemon/src/config.rs b/daemon/src/config.rs index 4157a343..73e37071 100644 --- a/daemon/src/config.rs +++ b/daemon/src/config.rs @@ -7,6 +7,8 @@ use rayhunter::analysis::analyzer::AnalyzerConfig; use crate::error::RayhunterError; use crate::notifications::NotificationType; +pub const WIFI_CREDS_PATH: &str = "/data/rayhunter/wifi-creds.conf"; + #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(default)] pub struct Config { @@ -22,6 +24,8 @@ pub struct Config { pub analyzers: AnalyzerConfig, pub min_space_to_start_recording_mb: u64, pub min_space_to_continue_recording_mb: u64, + pub wifi_ssid: Option, + pub wifi_password: Option, } impl Default for Config { @@ -39,6 +43,8 @@ impl Default for Config { enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery], min_space_to_start_recording_mb: 1, min_space_to_continue_recording_mb: 1, + wifi_ssid: None, + wifi_password: None, } } } @@ -47,12 +53,22 @@ pub async fn parse_config

(path: P) -> Result where P: AsRef, { - if let Ok(config_file) = tokio::fs::read_to_string(&path).await { - Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?) + let mut config = if let Ok(config_file) = tokio::fs::read_to_string(&path).await { + toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)? } else { warn!("unable to read config file, using default config"); - Ok(Config::default()) + Config::default() + }; + + if let Ok(creds) = tokio::fs::read_to_string(WIFI_CREDS_PATH).await { + config.wifi_ssid = creds + .lines() + .find_map(|line| line.strip_prefix("ssid=")) + .map(|s| s.to_string()); } + config.wifi_password = None; + + Ok(config) } pub struct Args { diff --git a/daemon/src/server.rs b/daemon/src/server.rs index 6d454da7..1f176126 100644 --- a/daemon/src/server.rs +++ b/daemon/src/server.rs @@ -109,14 +109,20 @@ pub async fn serve_static( pub async fn get_config( State(state): State>, ) -> Result, (StatusCode, String)> { - Ok(Json(state.config.clone())) + let mut config = state.config.clone(); + config.wifi_password = None; + Ok(Json(config)) } pub async fn set_config( State(state): State>, Json(config): Json, ) -> Result<(StatusCode, String), (StatusCode, String)> { - let config_str = toml::to_string_pretty(&config).map_err(|err| { + let mut config_to_write = config.clone(); + config_to_write.wifi_ssid = None; + config_to_write.wifi_password = None; + + let config_str = toml::to_string_pretty(&config_to_write).map_err(|err| { ( StatusCode::INTERNAL_SERVER_ERROR, format!("failed to serialize config as TOML: {err}"), @@ -130,6 +136,8 @@ pub async fn set_config( ) })?; + update_wifi_creds(&config).await; + // Trigger daemon restart after writing config state.daemon_restart_token.cancel(); Ok(( @@ -138,6 +146,50 @@ pub async fn set_config( )) } +async fn update_wifi_creds(config: &Config) { + let has_ssid = config + .wifi_ssid + .as_ref() + .is_some_and(|s| !s.trim().is_empty()); + let has_password = config + .wifi_password + .as_ref() + .is_some_and(|s| !s.trim().is_empty()); + + let creds_path = crate::config::WIFI_CREDS_PATH; + + if !has_ssid { + if tokio::fs::metadata(creds_path).await.is_ok() + && let Err(e) = tokio::fs::remove_file(creds_path).await + { + warn!("failed to remove wifi credentials: {e}"); + } + } else if has_password { + let contents = format!( + "ssid={}\npassword={}\n", + config.wifi_ssid.as_ref().unwrap(), + config.wifi_password.as_ref().unwrap() + ); + if let Err(e) = write(creds_path, contents).await { + warn!("failed to write wifi credentials: {e}"); + } + } else if let Ok(existing) = tokio::fs::read_to_string(creds_path).await { + let existing_password = existing + .lines() + .find_map(|line| line.strip_prefix("password=")); + if let Some(password) = existing_password { + let contents = format!( + "ssid={}\npassword={}\n", + config.wifi_ssid.as_ref().unwrap(), + password + ); + if let Err(e) = write(creds_path, contents).await { + warn!("failed to write wifi credentials: {e}"); + } + } + } +} + pub async fn test_notification( State(state): State>, ) -> Result<(StatusCode, String), (StatusCode, String)> { diff --git a/daemon/web/src/lib/components/ConfigForm.svelte b/daemon/web/src/lib/components/ConfigForm.svelte index 6f100146..f9c07694 100644 --- a/daemon/web/src/lib/components/ConfigForm.svelte +++ b/daemon/web/src/lib/components/ConfigForm.svelte @@ -283,6 +283,48 @@ +

+

+ WiFi Client Mode (Orbic only) +

+

+ Connect the device to an existing WiFi network for internet access (e.g. + notifications). The hotspot AP stays running. Leave both fields empty to + disable. +

+ +
+ + +
+ +
+ + +
+
+

Analyzer Heuristic Settings diff --git a/daemon/web/src/lib/utils.svelte.ts b/daemon/web/src/lib/utils.svelte.ts index d8b24066..739c0cc1 100644 --- a/daemon/web/src/lib/utils.svelte.ts +++ b/daemon/web/src/lib/utils.svelte.ts @@ -27,6 +27,8 @@ export interface Config { analyzers: AnalyzerConfig; min_space_to_start_recording_mb: number; min_space_to_continue_recording_mb: number; + wifi_ssid: string | null; + wifi_password: string | null; } export async function req(method: string, url: string, json_body?: unknown): Promise { diff --git a/dist/config.toml.in b/dist/config.toml.in index fb64a329..adbcc1a5 100644 --- a/dist/config.toml.in +++ b/dist/config.toml.in @@ -34,6 +34,13 @@ min_space_to_start_recording_mb = 1 # Minimum free space (MB) to continue recording (stops if below this) min_space_to_continue_recording_mb = 1 +# WiFi Client Mode (Orbic only) +# Set both wifi_ssid and wifi_password to connect the device to an existing WiFi network. +# This enables internet access for notifications while keeping the hotspot AP running. +# Leave unset or empty to disable. +#wifi_ssid = "" +#wifi_password = "" + # Analyzer Configuration # Enable/disable specific IMSI catcher detection heuristics # See https://github.com/EFForg/rayhunter/blob/main/doc/heuristics.md for details diff --git a/installer/build.rs b/installer/build.rs index 8b2e41b7..bbc028e2 100644 --- a/installer/build.rs +++ b/installer/build.rs @@ -17,6 +17,10 @@ fn main() { .join(&profile); set_binary_var(&include_dir, "FILE_ROOTSHELL", "rootshell"); set_binary_var(&include_dir, "FILE_RAYHUNTER_DAEMON", "rayhunter-daemon"); + + let wpa_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tools/build-wpa-supplicant/out"); + set_binary_var(&wpa_dir, "FILE_WPA_SUPPLICANT", "wpa_supplicant"); + set_binary_var(&wpa_dir, "FILE_WPA_CLI", "wpa_cli"); } fn set_binary_var(include_dir: &Path, var: &str, file: &str) { diff --git a/installer/src/connection.rs b/installer/src/connection.rs index e77538a4..a717dedc 100644 --- a/installer/src/connection.rs +++ b/installer/src/connection.rs @@ -42,6 +42,28 @@ pub async fn install_config( Ok(()) } +const WIFI_CREDS_PATH: &str = "/data/rayhunter/wifi-creds.conf"; + +pub async fn install_wifi_creds( + conn: &mut C, + wifi_ssid: Option<&str>, + wifi_password: Option<&str>, +) -> Result<()> { + match (wifi_ssid, wifi_password) { + (Some(ssid), Some(password)) if !ssid.is_empty() && !password.is_empty() => { + let contents = format!("ssid={ssid}\npassword={password}\n"); + conn.write_file(WIFI_CREDS_PATH, contents.as_bytes()) + .await?; + println!("WiFi client mode credentials written"); + } + (Some(_), None) | (None, Some(_)) => { + println!("Both --wifi-ssid and --wifi-password are required, skipping WiFi setup"); + } + _ => {} + } + 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..57198403 100644 --- a/installer/src/lib.rs +++ b/installer/src/lib.rs @@ -91,6 +91,14 @@ struct InstallOrbic { /// Overwrite config.toml even if it already exists on the device. #[arg(long)] reset_config: bool, + + /// WiFi network name to connect to (enables WiFi client mode). + #[arg(long)] + wifi_ssid: Option, + + /// WiFi password for the network specified by --wifi-ssid. + #[arg(long)] + wifi_password: Option, } #[derive(Parser, Debug)] @@ -110,6 +118,14 @@ struct OrbicNetworkArgs { /// Overwrite config.toml even if it already exists on the device. #[arg(long)] reset_config: bool, + + /// WiFi network name to connect to (enables WiFi client mode). + #[arg(long)] + wifi_ssid: Option, + + /// WiFi password for the network specified by --wifi-ssid. + #[arg(long)] + wifi_password: Option, } #[derive(Parser, Debug)] @@ -244,8 +260,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(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::OrbicUsb(args) => orbic::install(args.reset_config, args.wifi_ssid.as_deref(), args.wifi_password.as_deref()).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, args.wifi_ssid.as_deref(), args.wifi_password.as_deref()).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 { diff --git a/installer/src/orbic.rs b/installer/src/orbic.rs index a414e4b0..117407c8 100644 --- a/installer/src/orbic.rs +++ b/installer/src/orbic.rs @@ -13,7 +13,7 @@ use sha2::{Digest, Sha256}; use tokio::time::sleep; use crate::RAYHUNTER_DAEMON_INIT; -use crate::connection::{DeviceConnection, install_config}; +use crate::connection::{DeviceConnection, install_config, install_wifi_creds}; use crate::output::{print, println}; use crate::util::open_usb_device; @@ -77,7 +77,11 @@ async fn confirm() -> Result { Ok(input.trim() == "yes") } -pub async fn install(reset_config: bool) -> Result<()> { +pub async fn install( + reset_config: bool, + wifi_ssid: Option<&str>, + wifi_password: Option<&str>, +) -> 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." ); @@ -96,7 +100,8 @@ pub async fn install(reset_config: bool) -> Result<()> { setup_rootshell(&mut adb_device).await?; println!("done"); print!("Installing rayhunter... "); - let mut adb_device = setup_rayhunter(adb_device, reset_config).await?; + let mut adb_device = + setup_rayhunter(adb_device, reset_config, wifi_ssid, wifi_password).await?; println!("done"); print!("Testing rayhunter... "); test_rayhunter(&mut adb_device).await?; @@ -143,16 +148,46 @@ async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> { Ok(()) } -async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Result { +async fn setup_rayhunter( + mut adb_device: ADBUSBDevice, + reset_config: bool, + wifi_ssid: Option<&str>, + wifi_password: Option<&str>, +) -> Result { let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); - adb_at_syscmd(&mut adb_device, "mkdir -p /data/rayhunter").await?; + let wpa_supplicant_bin = include_bytes!(env!("FILE_WPA_SUPPLICANT")); + let wpa_cli_bin = include_bytes!(env!("FILE_WPA_CLI")); + + adb_at_syscmd( + &mut adb_device, + "mkdir -p /data/rayhunter/scripts /data/rayhunter/bin", + ) + .await?; install_file( &mut adb_device, "/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin, ) .await?; + install_file( + &mut adb_device, + "/data/rayhunter/scripts/wifi-client.sh", + include_bytes!("../../client-mode/scripts/wifi-client.sh"), + ) + .await?; + install_file( + &mut adb_device, + "/data/rayhunter/bin/wpa_supplicant", + wpa_supplicant_bin, + ) + .await?; + install_file(&mut adb_device, "/data/rayhunter/bin/wpa_cli", wpa_cli_bin).await?; + adb_at_syscmd( + &mut adb_device, + "chmod +x /data/rayhunter/bin/wpa_supplicant /data/rayhunter/bin/wpa_cli", + ) + .await?; { let mut conn = AdbConnection { @@ -165,12 +200,17 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re reset_config, ) .await?; + install_wifi_creds(&mut conn, wifi_ssid, wifi_password).await?; } + let rayhunter_daemon_init = RAYHUNTER_DAEMON_INIT.replace( + "#RAYHUNTER-PRESTART", + "pkill -f start_qt_daemon 2>/dev/null || true; sleep 1; pkill -f qt_daemon 2>/dev/null || true\n printf '#!/bin/sh\\nwhile true; do sleep 3600; done\\n' > /tmp/daemon-stub\n chmod 755 /tmp/daemon-stub\n mount --bind /tmp/daemon-stub /usr/bin/dmclient 2>/dev/null || true\n mount --bind /tmp/daemon-stub /usr/bin/upgrade 2>/dev/null || true\n kill -9 $(pidof dmclient) 2>/dev/null || true\n kill -9 $(pidof upgrade) 2>/dev/null || true\n sh /data/rayhunter/scripts/wifi-client.sh start 2>/dev/null &", + ); install_file( &mut adb_device, "/etc/init.d/rayhunter_daemon", - RAYHUNTER_DAEMON_INIT.as_bytes(), + rayhunter_daemon_init.as_bytes(), ) .await?; install_file( diff --git a/installer/src/orbic_network.rs b/installer/src/orbic_network.rs index 840c9246..299a9c37 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, install_wifi_creds}; 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,8 @@ pub async fn install( admin_username: String, admin_password: Option, reset_config: bool, + wifi_ssid: Option<&str>, + wifi_password: Option<&str>, ) -> Result<()> { let Some(admin_password) = admin_password else { eprintln!( @@ -170,7 +172,7 @@ pub async fn install( wait_for_telnet(&admin_ip).await?; println!("done"); - setup_rayhunter(&admin_ip, reset_config).await + setup_rayhunter(&admin_ip, reset_config, wifi_ssid, wifi_password).await } async fn wait_for_telnet(admin_ip: &str) -> Result<()> { @@ -194,9 +196,16 @@ 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, + wifi_ssid: Option<&str>, + wifi_password: Option<&str>, +) -> Result<()> { let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?; let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); + let wpa_supplicant_bin = include_bytes!(env!("FILE_WPA_SUPPLICANT")); + let wpa_cli_bin = include_bytes!(env!("FILE_WPA_CLI")); // Remount filesystem as read-write to allow modifications // This is really only necessary for the Moxee Hotspot @@ -208,7 +217,13 @@ 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?; + telnet_send_command( + addr, + "mkdir -p /data/rayhunter/scripts /data/rayhunter/bin", + "exit code 0", + false, + ) + .await?; telnet_send_file( addr, @@ -217,6 +232,21 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> { false, ) .await?; + telnet_send_file( + addr, + "/data/rayhunter/scripts/wifi-client.sh", + include_bytes!("../../client-mode/scripts/wifi-client.sh"), + false, + ) + .await?; + telnet_send_file( + addr, + "/data/rayhunter/bin/wpa_supplicant", + wpa_supplicant_bin, + false, + ) + .await?; + telnet_send_file(addr, "/data/rayhunter/bin/wpa_cli", wpa_cli_bin, false).await?; let mut conn = TelnetConnection::new(addr, false); install_config( @@ -226,11 +256,16 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> { reset_config, ) .await?; + install_wifi_creds(&mut conn, wifi_ssid, wifi_password).await?; + let rayhunter_daemon_init = RAYHUNTER_DAEMON_INIT.replace( + "#RAYHUNTER-PRESTART", + "pkill -f start_qt_daemon 2>/dev/null || true; sleep 1; pkill -f qt_daemon 2>/dev/null || true\n printf '#!/bin/sh\\nwhile true; do sleep 3600; done\\n' > /tmp/daemon-stub\n chmod 755 /tmp/daemon-stub\n mount --bind /tmp/daemon-stub /usr/bin/dmclient 2>/dev/null || true\n mount --bind /tmp/daemon-stub /usr/bin/upgrade 2>/dev/null || true\n kill -9 $(pidof dmclient) 2>/dev/null || true\n kill -9 $(pidof upgrade) 2>/dev/null || true\n sh /data/rayhunter/scripts/wifi-client.sh start 2>/dev/null &", + ); telnet_send_file( addr, "/etc/init.d/rayhunter_daemon", - RAYHUNTER_DAEMON_INIT.as_bytes(), + rayhunter_daemon_init.as_bytes(), false, ) .await?; @@ -245,7 +280,7 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> { telnet_send_command( addr, - "chmod +x /data/rayhunter/rayhunter-daemon", + "chmod +x /data/rayhunter/rayhunter-daemon /data/rayhunter/bin/wpa_supplicant /data/rayhunter/bin/wpa_cli", "exit code 0", false, ) diff --git a/tools/build-wpa-supplicant/Dockerfile b/tools/build-wpa-supplicant/Dockerfile new file mode 100644 index 00000000..9b716ff4 --- /dev/null +++ b/tools/build-wpa-supplicant/Dockerfile @@ -0,0 +1,44 @@ +FROM ubuntu:20.04 AS builder +ENV DEBIAN_FRONTEND=noninteractive + +RUN dpkg --add-architecture armhf && \ + echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu focal main universe" > /etc/apt/sources.list && \ + echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu focal-updates main universe" >> /etc/apt/sources.list && \ + echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu focal-security main universe" >> /etc/apt/sources.list && \ + echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports focal main universe" >> /etc/apt/sources.list && \ + echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports focal-updates main universe" >> /etc/apt/sources.list && \ + echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports focal-security main universe" >> /etc/apt/sources.list && \ + apt-get update && apt-get install -y \ + build-essential gcc-arm-linux-gnueabihf pkg-config wget \ + libnl-3-dev:armhf libnl-genl-3-dev:armhf \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build +RUN wget https://w1.fi/releases/wpa_supplicant-2.11.tar.gz && \ + tar -xf wpa_supplicant-2.11.tar.gz + +WORKDIR /build/wpa_supplicant-2.11/wpa_supplicant + +RUN printf '%s\n' \ + "CONFIG_DRIVER_NL80211=y" \ + "CONFIG_LIBNL32=y" \ + "CONFIG_CRYPTO=internal" \ + "CONFIG_TLS=internal" \ + "CONFIG_INTERNAL_LIBTOMMATH=y" \ + "CONFIG_INTERNAL_LIBTOMMATH_FAST=y" \ + "CONFIG_CTRL_IFACE=y" \ + "CONFIG_BACKEND=file" \ + "CONFIG_NO_CONFIG_WRITE=y" \ + "CONFIG_NO_RANDOM_POOL=y" \ + "CONFIG_GETRANDOM=y" \ + > .config + +RUN make CC=arm-linux-gnueabihf-gcc \ + EXTRA_CFLAGS="$(arm-linux-gnueabihf-pkg-config --cflags libnl-3.0 libnl-genl-3.0)" \ + LDFLAGS="-static" \ + LIBS="$(arm-linux-gnueabihf-pkg-config --libs --static libnl-3.0 libnl-genl-3.0) -lm -lpthread" \ + -j$(nproc) + +FROM scratch AS export +COPY --from=builder /build/wpa_supplicant-2.11/wpa_supplicant/wpa_supplicant / +COPY --from=builder /build/wpa_supplicant-2.11/wpa_supplicant/wpa_cli /