mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-08 14:11:52 -07:00
client mode added
This commit is contained in:
@@ -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`.
|
||||
Executable
+30
@@ -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."
|
||||
Executable
+154
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Vendored
+7
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 /
|
||||
Reference in New Issue
Block a user