client mode added

This commit is contained in:
Ember
2026-02-14 12:55:48 -08:00
parent a7d38730f5
commit d9e269a825
15 changed files with 573 additions and 18 deletions
+86
View File
@@ -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`.
+30
View File
@@ -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."
+154
View File
@@ -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" <<WPAEOF
ctrl_interface=/var/run/wpa_supplicant
network={
ssid="$SSID"
psk="$PSK"
key_mgmt=WPA-PSK
}
WPAEOF
echo "Starting wpa_supplicant"
"$WPA_BIN" -i "$IFACE" -Dnl80211 -c "$WPA_CONF" -B -P "$WPA_PID"
sleep 5
echo "wpa_supplicant status:"
iw dev "$IFACE" link
echo "Starting DHCP"
udhcpc -i "$IFACE" -s /etc/udhcpc.d/50default -p "$DHCP_PID" -t 10 -A 3 -b
sleep 3
WLAN1_IP=$(ip addr show "$IFACE" | grep 'inet ' | awk '{print $2}' | cut -d/ -f1)
WLAN1_CIDR=$(ip addr show "$IFACE" | grep 'inet ' | awk '{print $2}')
WLAN1_SUBNET=$(ip route show dev "$IFACE" | grep 'proto kernel' | awk '{print $1}')
WLAN1_GW=$(ip route show dev "$IFACE" | grep 'proto kernel' | awk '{print $1}' | cut -d/ -f1)
WLAN1_GW="${WLAN1_GW%.*}.1"
if [ -z "$WLAN1_IP" ]; then
echo "Failed to get IP on $IFACE"
exit 1
fi
echo "IP: $WLAN1_IP Subnet: $WLAN1_SUBNET CIDR: $WLAN1_CIDR Gateway: $WLAN1_GW"
# Fix default route: ensure it goes through wlan1, not bridge0
GATEWAY=$(ip route show default | grep "dev bridge0" | awk '{print $3}')
if [ -n "$GATEWAY" ]; then
echo "Fixing default route: bridge0 -> 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
+19 -3
View File
@@ -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";
/// The structure of a valid rayhunter configuration
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
@@ -34,6 +36,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<String>,
pub wifi_password: Option<String>,
}
impl Default for Config {
@@ -51,6 +55,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,
}
}
}
@@ -59,12 +65,22 @@ pub async fn parse_config<P>(path: P) -> Result<Config, RayhunterError>
where
P: AsRef<std::path::Path>,
{
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 {
+54 -2
View File
@@ -134,7 +134,9 @@ pub async fn serve_static(
pub async fn get_config(
State(state): State<Arc<ServerState>>,
) -> Result<Json<Config>, (StatusCode, String)> {
Ok(Json(state.config.clone()))
let mut config = state.config.clone();
config.wifi_password = None;
Ok(Json(config))
}
#[cfg_attr(feature = "apidocs", utoipa::path(
@@ -157,7 +159,11 @@ pub async fn set_config(
State(state): State<Arc<ServerState>>,
Json(config): Json<Config>,
) -> 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}"),
@@ -171,6 +177,8 @@ pub async fn set_config(
)
})?;
update_wifi_creds(&config).await;
// Trigger daemon restart after writing config
state.daemon_restart_token.cancel();
Ok((
@@ -179,6 +187,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}");
}
}
}
}
#[cfg_attr(feature = "apidocs", utoipa::path(
post,
path = "/api/test-notification",
@@ -283,6 +283,48 @@
</div>
</div>
<div class="border-t pt-4 mt-6 space-y-3">
<h3 class="text-lg font-semibold text-gray-800 mb-4">
WiFi Client Mode (Orbic only)
</h3>
<p class="text-xs text-gray-500">
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.
</p>
<div>
<label for="wifi_ssid" class="block text-sm font-medium text-gray-700 mb-1">
WiFi Network Name (SSID)
</label>
<input
id="wifi_ssid"
type="text"
bind:value={config.wifi_ssid}
placeholder="MyWiFiNetwork"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
/>
</div>
<div>
<label
for="wifi_password"
class="block text-sm font-medium text-gray-700 mb-1"
>
WiFi Password
</label>
<input
id="wifi_password"
type="password"
bind:value={config.wifi_password}
placeholder={config.wifi_ssid
? 'Leave blank to keep current password'
: 'password'}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
/>
</div>
</div>
<div class="border-t pt-4 mt-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">
Analyzer Heuristic Settings
+2
View File
@@ -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<string> {
+7
View File
@@ -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
+4
View File
@@ -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) {
+22
View File
@@ -155,6 +155,28 @@ pub async fn setup_data_directory<C: DeviceConnection>(conn: &mut C, data_dir: &
Ok(())
}
const WIFI_CREDS_PATH: &str = "/data/rayhunter/wifi-creds.conf";
pub async fn install_wifi_creds<C: DeviceConnection>(
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,
+26 -2
View File
@@ -100,6 +100,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<String>,
/// WiFi password for the network specified by --wifi-ssid.
#[arg(long)]
wifi_password: Option<String>,
}
#[derive(Parser, Debug)]
@@ -124,6 +132,14 @@ struct OrbicNetworkArgs {
/// Must not be /data/rayhunter.
#[arg(long)]
data_dir: Option<String>,
/// WiFi network name to connect to (enables WiFi client mode).
#[arg(long)]
wifi_ssid: Option<String>,
/// WiFi password for the network specified by --wifi-ssid.
#[arg(long)]
wifi_password: Option<String>,
}
#[derive(Parser, Debug)]
@@ -148,6 +164,14 @@ struct MoxeeArgs {
/// Must not be /data/rayhunter.
#[arg(long)]
data_dir: Option<String>,
/// WiFi network name to connect to (enables WiFi client mode).
#[arg(long)]
wifi_ssid: Option<String>,
/// WiFi password for the network specified by --wifi-ssid.
#[arg(long)]
wifi_password: Option<String>,
}
#[derive(Parser, Debug)]
@@ -282,8 +306,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, args.data_dir).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.data_dir, args.wifi_ssid.as_deref(), args.wifi_password.as_deref()).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) => {
+2
View File
@@ -10,6 +10,8 @@ pub async fn install(args: MoxeeArgs) -> Result<()> {
args.admin_password,
args.reset_config,
data_dir,
args.wifi_ssid.as_deref(),
args.wifi_password.as_deref(),
)
.await
}
+46 -6
View File
@@ -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<bool> {
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,28 +148,63 @@ async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
Ok(())
}
async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Result<ADBUSBDevice> {
async fn setup_rayhunter(
mut adb_device: ADBUSBDevice,
reset_config: bool,
wifi_ssid: Option<&str>,
wifi_password: Option<&str>,
) -> Result<ADBUSBDevice> {
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 {
device: &mut adb_device,
};
install_config(&mut conn, "orbic", 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 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(
+35 -5
View File
@@ -8,7 +8,7 @@ use serde::Deserialize;
use tokio::time::sleep;
use crate::RAYHUNTER_DAEMON_INIT;
use crate::connection::{TelnetConnection, install_config, setup_data_directory};
use crate::connection::{TelnetConnection, install_config, install_wifi_creds, 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};
@@ -148,6 +148,8 @@ pub async fn install(
admin_password: Option<String>,
reset_config: bool,
data_dir: Option<String>,
wifi_ssid: Option<&str>,
wifi_password: Option<&str>,
) -> Result<()> {
let Some(admin_password) = admin_password else {
eprintln!(
@@ -172,7 +174,7 @@ pub async fn install(
println!("done");
let data_dir = data_dir.unwrap_or_else(|| "/data/rayhunter-data".to_string());
setup_rayhunter(&admin_ip, reset_config, &data_dir).await
setup_rayhunter(&admin_ip, reset_config, &data_dir, wifi_ssid, wifi_password).await
}
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
@@ -196,9 +198,17 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
Ok(())
}
async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) -> Result<()> {
async fn setup_rayhunter(
admin_ip: &str,
reset_config: bool,
data_dir: &str,
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
@@ -220,13 +230,33 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) ->
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?;
install_config(&mut conn, "orbic", 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 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?;
@@ -241,7 +271,7 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) ->
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,
)
+44
View File
@@ -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 /