mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-04-23 22:19:59 -07:00
client mode added (#888)
* client mode added
* Prevent OTA daemons dmclient and upgrade from running and phoning home to Verizon
* Fix workflow
* WIFI changes to support moxee. May need to rebase as delivering refactoring under other PR.
* code changes for rust based wifi client mode docs next
* Doc changes & security fixes
* Added watchdog and recover if crash occurs for wifi.
* Remove changes which were from device UI work (seperate feature which snuck into this branch)
* Add missing wifi and firewall module declarations
* cleaning up the code a bit
* Gate wpa_suplicant in installer and workflow to avoid building binary every push
* fix to check diskspace
* Improved support for subnet colisions, and attempts to rejoin network.
* Add WiFi client support and S01iptables to T-Mobile and Wingtech installers
Both installers now deploy wpa_supplicant, wpa_cli, udhcpc-hook.sh, and
the S01iptables boot-time firewall script. Config generation uses the
shared install_config/install_wifi_creds helpers instead of manual string
replacement.
* Revert "Add WiFi client support and S01iptables to T-Mobile and Wingtech installers"
This reverts commit 944b369c4f.
* Fix build: ignore unused wifi_ssid/wifi_password fields in T-Mobile and Wingtech installers
* Moved to a wifi crate
* Add host route and arp_filter to prevent subnet collisions
* add wakelock so kernel doesn't shut down wifi on battery when wifi is enabled
* Move wifi to external wifi-station crate, remove wifi from installer, extract OTA blocking
* fixed outdated info, moved udhcpc hook to wifi-station crate.
* Update to new version of wifi-station
* Address PR review feedback: replace Docker wpa build, add iw, remove OTA, revert unrelated changes
- Replace Docker-based wpa_supplicant build with shell script (scripts/build-wpa-supplicant.sh)
- Add iw cross-compilation and deployment to Orbic installer
- Skip wifi tool install if binary already exists on device
- Remove OTA daemon blocker (extracted for separate PR)
- Revert unrelated UZ801 and T-Mobile installer changes
- Remove connection.rs test scaffolding
- Rewrite S01iptables init script to read config.toml directly
- Pin url crate to 2.5.4 to fix MSRV
* Fix build script: use bash for parameter substitution
The ${VAR//pattern/replacement} syntax is a bash extension that
doesn't work in dash (Ubuntu's /bin/sh).
* Fix iw build: export PKG_CONFIG_LIBDIR as env var
Passing PKG_CONFIG_LIBDIR as a make variable doesn't export it to
$(shell pkg-config ...) calls. Set it as an environment variable
so pkg-config finds the cross-compiled libnl.
* Point wifi-station to GitHub rev 97c579a
* add comment
* Update daemon/src/config.rs
Add decorators
Co-authored-by: Andrej Walilko <walilkoa@gmail.com>
* Update daemon/src/server.rs
add utopia doc support
Co-authored-by: Andrej Walilko <walilkoa@gmail.com>
* Update daemon/src/server.rs
add utopia doc support
Co-authored-by: Andrej Walilko <walilkoa@gmail.com>
* Update to wifi-station with utoipa doc strings
* add utoipa to wifi-station
* added WPA3 support
* fix firewall port detection, update wifi-station to c267d37
fix ntfy port_or_known_default, comment out ntfy_url in config
template, update wifi-station with resolv.conf bind mount
fallback, udhcpc_bin config, and module path fix for UZ801
* show wifi UI for tmobile and wingtech, add udhcpc_bin config
both devices have wifi hardware and backend support. wingtech
verified on hardware (QCA6174 via PCIe). uz801 excluded for now
due to driver scan limitations with hostapd active.
* install wifi tools from orbic-usb installer, fix DNS default to Quad9, bump wifi-station rev
* fix Modal scroll listener leak, correct file transfer timeout math, document firewall fail-open, clarify UZ801 wifi status
* build-dev.sh: build wifi tools so install-dev works for orbic-family devices
* update Cargo.lock for wifi-station e8ec5b4
* fix setup_timeout_server crypto provider install, apply rustfmt
* Update installer/src/connection.rs
Co-authored-by: Cooper Quintin <cooperq@users.noreply.github.com>
* Update installer/src/orbic.rs
Co-authored-by: Cooper Quintin <cooperq@users.noreply.github.com>
* apply rustfmt to AdbConnection::run_command
---------
Co-authored-by: Andrej Walilko <walilkoa@gmail.com>
Co-authored-by: Cooper Quintin <cooperq@users.noreply.github.com>
This commit is contained in:
28
.github/workflows/main.yml
vendored
28
.github/workflows/main.yml
vendored
@@ -11,6 +11,9 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FILE_ROOTSHELL: ../../rootshell/rootshell
|
||||
FILE_RAYHUNTER_DAEMON: ../../rayhunter-daemon/rayhunter-daemon
|
||||
FILE_WPA_SUPPLICANT: ../../wpa-supplicant/wpa_supplicant
|
||||
FILE_WPA_CLI: ../../wpa-supplicant/wpa_cli
|
||||
FILE_IW: ../../wpa-supplicant/iw
|
||||
RUSTFLAGS: "-Dwarnings"
|
||||
|
||||
jobs:
|
||||
@@ -301,6 +304,30 @@ jobs:
|
||||
path: target/armv7-unknown-linux-musleabihf/firmware/rootshell
|
||||
if-no-files-found: error
|
||||
|
||||
build_wpa_supplicant:
|
||||
if: needs.files_changed.outputs.installer_changed == 'true'
|
||||
needs:
|
||||
- files_changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install cross-compiler
|
||||
run: sudo apt-get update && sudo apt-get install -y gcc-arm-linux-gnueabihf
|
||||
- name: Build wpa_supplicant (armv7)
|
||||
run: CC=arm-linux-gnueabihf-gcc STRIP=arm-linux-gnueabihf-strip HOST=arm-linux-gnueabihf scripts/build-wpa-supplicant.sh
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wpa-supplicant
|
||||
path: |
|
||||
tools/build-wpa-supplicant/out/wpa_supplicant
|
||||
tools/build-wpa-supplicant/out/wpa_cli
|
||||
tools/build-wpa-supplicant/out/iw
|
||||
if-no-files-found: error
|
||||
|
||||
build_rayhunter:
|
||||
if: needs.files_changed.outputs.daemon_needed == 'true'
|
||||
needs:
|
||||
@@ -347,6 +374,7 @@ jobs:
|
||||
needs:
|
||||
- build_rayhunter
|
||||
- build_rootshell
|
||||
- build_wpa_supplicant
|
||||
- files_changed
|
||||
- windows_installer_check_and_test
|
||||
strategy:
|
||||
|
||||
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -4556,6 +4556,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"telcom-parser",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"utoipa",
|
||||
@@ -4601,7 +4602,9 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"toml 0.8.22",
|
||||
"url",
|
||||
"utoipa",
|
||||
"wifi-station",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6748,6 +6751,20 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
|
||||
|
||||
[[package]]
|
||||
name = "wifi-station"
|
||||
version = "0.10.1"
|
||||
source = "git+https://github.com/BeigeBox/wifi-station?rev=e8ec5b4#e8ec5b491fa125bf5346b6aa84f13ed51de33044"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"log",
|
||||
"serde",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
||||
@@ -17,10 +17,11 @@ required-features = ["apidocs"]
|
||||
default = ["rustcrypto-tls"]
|
||||
rustcrypto-tls = ["reqwest/rustls-tls-webpki-roots-no-provider", "dep:rustls-rustcrypto"]
|
||||
pq-tls = ["reqwest/rustls-tls-webpki-roots-no-provider", "dep:rustls-post-quantum"]
|
||||
apidocs = ["dep:utoipa"]
|
||||
apidocs = ["dep:utoipa", "wifi-station/utoipa"]
|
||||
|
||||
[dependencies]
|
||||
rayhunter = { path = "../lib" }
|
||||
wifi-station = { git = "https://github.com/BeigeBox/wifi-station", rev = "e8ec5b4" }
|
||||
toml = "0.8.8"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt"] }
|
||||
@@ -44,3 +45,4 @@ rustls-rustcrypto = { version = "0.0.2-alpha", optional = true }
|
||||
rustls-post-quantum = { version = "0.2.4", optional = true }
|
||||
async-trait = "0.1.88"
|
||||
utoipa = { version = "5.4.0", optional = true }
|
||||
url = "2.5.4"
|
||||
|
||||
@@ -32,8 +32,24 @@ pub struct Config {
|
||||
pub enabled_notifications: Vec<NotificationType>,
|
||||
/// Vector containing the list of enabled analyzers
|
||||
pub analyzers: AnalyzerConfig,
|
||||
/// Minimum disk space required to start a recording
|
||||
pub min_space_to_start_recording_mb: u64,
|
||||
/// Minimum disk space required to continue a recording
|
||||
pub min_space_to_continue_recording_mb: u64,
|
||||
/// Wifi client SSID
|
||||
pub wifi_ssid: Option<String>,
|
||||
/// Wifi client password
|
||||
pub wifi_password: Option<String>,
|
||||
/// Wifi security type (wpa_psk or sae)
|
||||
pub wifi_security: Option<wifi_station::SecurityType>,
|
||||
/// Wifi client mode
|
||||
pub wifi_enabled: bool,
|
||||
/// Vector containing wifi client DNS servers
|
||||
pub dns_servers: Option<Vec<String>>,
|
||||
/// Wifi client firewall mode
|
||||
pub firewall_restrict_outbound: bool,
|
||||
/// Vector containing additional wifi client firewall ports to open
|
||||
pub firewall_allowed_ports: Option<Vec<u16>>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -51,20 +67,83 @@ 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,
|
||||
wifi_security: None,
|
||||
wifi_enabled: false,
|
||||
dns_servers: None,
|
||||
firewall_restrict_outbound: true,
|
||||
firewall_allowed_ports: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn wifi_config(&self) -> wifi_station::WifiConfig {
|
||||
let (wpa_bin, hostapd_conf, ctrl_interface) = match self.device {
|
||||
Device::Tmobile | Device::Wingtech => (
|
||||
Some("/usr/sbin/wpa_supplicant".into()),
|
||||
Some("/data/configs/hostapd.conf".into()),
|
||||
None,
|
||||
),
|
||||
Device::Uz801 => (
|
||||
Some("/system/bin/wpa_supplicant".into()),
|
||||
Some("/data/misc/wifi/hostapd.conf".into()),
|
||||
Some("/data/misc/wifi/sockets".into()),
|
||||
),
|
||||
_ => (None, None, None),
|
||||
};
|
||||
wifi_station::WifiConfig {
|
||||
wifi_enabled: self.wifi_enabled,
|
||||
dns_servers: self.dns_servers.clone(),
|
||||
wifi_ssid: self.wifi_ssid.clone(),
|
||||
wifi_password: self.wifi_password.clone(),
|
||||
security_type: self.wifi_security,
|
||||
wpa_supplicant_bin: wpa_bin.or_else(|| resolve_bin("wpa_supplicant")),
|
||||
hostapd_conf,
|
||||
ctrl_interface,
|
||||
udhcpc_hook_path: Some("/data/rayhunter/udhcpc-hook.sh".into()),
|
||||
dhcp_lease_path: Some("/data/rayhunter/dhcp_lease".into()),
|
||||
wpa_conf_path: Some("/data/rayhunter/wpa_sta.conf".into()),
|
||||
iw_bin: resolve_bin("iw"),
|
||||
udhcpc_bin: resolve_bin("udhcpc"),
|
||||
crash_log_dir: Some("/data/rayhunter/crash-logs".into()),
|
||||
wakelock_name: Some("rayhunter".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_bin(name: &str) -> Option<String> {
|
||||
let local = format!("/data/rayhunter/bin/{name}");
|
||||
if std::path::Path::new(&local).exists() {
|
||||
return Some(local);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
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 Some((ssid, security)) =
|
||||
wifi_station::read_network_from_wpa_conf("/data/rayhunter/wpa_sta.conf")
|
||||
{
|
||||
config.wifi_ssid = Some(ssid);
|
||||
config.wifi_security = Some(security);
|
||||
} else {
|
||||
config.wifi_ssid = None;
|
||||
config.wifi_security = None;
|
||||
}
|
||||
config.wifi_password = None;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub struct Args {
|
||||
|
||||
92
daemon/src/firewall.rs
Normal file
92
daemon/src/firewall.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use anyhow::{Result, bail};
|
||||
use log::{info, warn};
|
||||
use tokio::process::Command;
|
||||
|
||||
use wifi_station::detect_bridge_iface;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
async fn run_iptables(args: &[&str]) -> Result<()> {
|
||||
let out = Command::new("iptables").args(args).output().await?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"iptables {} failed: {}",
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn apply(config: &Config) {
|
||||
let _ = Command::new("iptables")
|
||||
.args(["-F", "OUTPUT"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if config.firewall_restrict_outbound {
|
||||
// Fail open on partial setup error: reachability beats restriction when recovery means physical access.
|
||||
match setup_outbound_whitelist(&config.firewall_allowed_ports, &config.ntfy_url).await {
|
||||
Ok(()) => info!("outbound firewall active: allowing DHCP, DNS, HTTPS only"),
|
||||
Err(e) => warn!("firewall setup failed: {e} (fail-open, outbound unrestricted)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup_outbound_whitelist(
|
||||
extra_ports: &Option<Vec<u16>>,
|
||||
ntfy_url: &Option<String>,
|
||||
) -> Result<()> {
|
||||
run_iptables(&["-A", "OUTPUT", "-o", "lo", "-j", "ACCEPT"]).await?;
|
||||
run_iptables(&["-A", "OUTPUT", "-o", detect_bridge_iface(), "-j", "ACCEPT"]).await?;
|
||||
run_iptables(&[
|
||||
"-A",
|
||||
"OUTPUT",
|
||||
"-m",
|
||||
"state",
|
||||
"--state",
|
||||
"ESTABLISHED,RELATED",
|
||||
"-j",
|
||||
"ACCEPT",
|
||||
])
|
||||
.await?;
|
||||
run_iptables(&[
|
||||
"-A", "OUTPUT", "-p", "udp", "--dport", "67:68", "-j", "ACCEPT",
|
||||
])
|
||||
.await?;
|
||||
run_iptables(&["-A", "OUTPUT", "-p", "udp", "--dport", "53", "-j", "ACCEPT"]).await?;
|
||||
run_iptables(&["-A", "OUTPUT", "-p", "tcp", "--dport", "53", "-j", "ACCEPT"]).await?;
|
||||
run_iptables(&[
|
||||
"-A", "OUTPUT", "-p", "tcp", "--dport", "443", "-j", "ACCEPT",
|
||||
])
|
||||
.await?;
|
||||
|
||||
if let Some(url) = ntfy_url
|
||||
&& let Ok(parsed) = url::Url::parse(url)
|
||||
&& let Some(port) = parsed.port_or_known_default()
|
||||
&& port != 443
|
||||
{
|
||||
let port_str = port.to_string();
|
||||
run_iptables(&[
|
||||
"-A", "OUTPUT", "-p", "tcp", "--dport", &port_str, "-j", "ACCEPT",
|
||||
])
|
||||
.await?;
|
||||
info!("firewall: auto-allowed port {port} for ntfy");
|
||||
}
|
||||
|
||||
if let Some(ports) = extra_ports {
|
||||
for port in ports {
|
||||
let port_str = port.to_string();
|
||||
run_iptables(&[
|
||||
"-A", "OUTPUT", "-p", "tcp", "--dport", &port_str, "-j", "ACCEPT",
|
||||
])
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
run_iptables(&["-A", "OUTPUT", "-j", "DROP"]).await?;
|
||||
|
||||
let _ = tokio::fs::write("/proc/sys/net/bridge/bridge-nf-call-iptables", "0").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod crypto_provider;
|
||||
pub mod diag;
|
||||
pub mod display;
|
||||
pub mod error;
|
||||
pub mod firewall;
|
||||
pub mod key_input;
|
||||
pub mod notifications;
|
||||
pub mod pcap;
|
||||
|
||||
@@ -5,13 +5,13 @@ mod crypto_provider;
|
||||
mod diag;
|
||||
mod display;
|
||||
mod error;
|
||||
mod firewall;
|
||||
mod key_input;
|
||||
mod notifications;
|
||||
mod pcap;
|
||||
mod qmdl_store;
|
||||
mod server;
|
||||
mod stats;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -23,10 +23,11 @@ use crate::notifications::{NotificationService, run_notification_worker};
|
||||
use crate::pcap::get_pcap;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::server::{
|
||||
ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_zip, serve_static,
|
||||
set_config, set_time_offset, test_notification,
|
||||
ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_wifi_status, get_zip,
|
||||
scan_wifi, serve_static, set_config, set_time_offset, test_notification,
|
||||
};
|
||||
use crate::stats::{get_qmdl_manifest, get_system_stats};
|
||||
use wifi_station::WifiStatus;
|
||||
|
||||
use analysis::{
|
||||
AnalysisCtrlMessage, AnalysisStatus, get_analysis_status, run_analysis_thread, start_analysis,
|
||||
@@ -71,6 +72,8 @@ fn get_router() -> AppRouter {
|
||||
.route("/api/config", get(get_config))
|
||||
.route("/api/config", post(set_config))
|
||||
.route("/api/test-notification", post(test_notification))
|
||||
.route("/api/wifi-status", get(get_wifi_status))
|
||||
.route("/api/wifi-scan", post(scan_wifi))
|
||||
.route("/api/time", get(get_time))
|
||||
.route("/api/time-offset", post(set_time_offset))
|
||||
.route("/api/debug/display-state", post(debug_set_display_state))
|
||||
@@ -236,7 +239,7 @@ async fn run_with_config(
|
||||
info!("Starting UI");
|
||||
|
||||
let update_ui = match &config.device {
|
||||
Device::Orbic => display::orbic::update_ui,
|
||||
Device::Orbic | Device::Moxee => display::orbic::update_ui,
|
||||
Device::Tplink => display::tplink::update_ui,
|
||||
Device::Tmobile => display::tmobile::update_ui,
|
||||
Device::Wingtech => display::wingtech::update_ui,
|
||||
@@ -284,6 +287,15 @@ async fn run_with_config(
|
||||
config.enabled_notifications.clone(),
|
||||
);
|
||||
|
||||
let wifi_status = Arc::new(RwLock::new(WifiStatus::default()));
|
||||
wifi_station::run_wifi_client(
|
||||
&task_tracker,
|
||||
&config.wifi_config(),
|
||||
shutdown_token.clone(),
|
||||
wifi_status.clone(),
|
||||
);
|
||||
firewall::apply(&config).await;
|
||||
|
||||
let state = Arc::new(ServerState {
|
||||
config_path: args.config_path.clone(),
|
||||
config,
|
||||
@@ -293,6 +305,8 @@ async fn run_with_config(
|
||||
analysis_sender: analysis_tx,
|
||||
daemon_restart_token: restart_token.clone(),
|
||||
ui_update_sender: Some(ui_update_tx),
|
||||
wifi_status,
|
||||
wifi_scan_lock: tokio::sync::Mutex::new(()),
|
||||
});
|
||||
run_server(&task_tracker, state, shutdown_token.clone()).await;
|
||||
|
||||
|
||||
@@ -248,10 +248,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn setup_timeout_server(timeout: u64) -> String {
|
||||
#[cfg(feature = "rustcrypto-tls")]
|
||||
{
|
||||
let _ = rustls_rustcrypto::provider().install_default();
|
||||
}
|
||||
crate::crypto_provider::install_default();
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
@@ -38,6 +38,8 @@ pub struct ServerState {
|
||||
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
pub daemon_restart_token: CancellationToken,
|
||||
pub ui_update_sender: Option<Sender<DisplayState>>,
|
||||
pub wifi_status: Arc<RwLock<wifi_station::WifiStatus>>,
|
||||
pub wifi_scan_lock: tokio::sync::Mutex<()>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
@@ -135,7 +137,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(
|
||||
@@ -158,7 +162,12 @@ 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;
|
||||
config_to_write.wifi_security = 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}"),
|
||||
@@ -172,6 +181,8 @@ pub async fn set_config(
|
||||
)
|
||||
})?;
|
||||
|
||||
wifi_station::update_wpa_conf(&config.wifi_config()).await;
|
||||
|
||||
// Trigger daemon restart after writing config
|
||||
state.daemon_restart_token.cancel();
|
||||
Ok((
|
||||
@@ -400,6 +411,55 @@ pub async fn get_zip(
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/wifi-status",
|
||||
tag = "Configuration",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success", body = wifi_station::WifiStatus)
|
||||
),
|
||||
summary = "Get wifi status",
|
||||
description = "Show the status of the wifi client."
|
||||
))]
|
||||
pub async fn get_wifi_status(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Json<wifi_station::WifiStatus> {
|
||||
let status = state.wifi_status.read().await;
|
||||
Json(status.clone())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
post,
|
||||
path = "/api/wifi-scan",
|
||||
tag = "Configuration",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Scan success", body = inline(Vec<wifi_station::WifiNetwork>), content_type = "application/json"),
|
||||
(status = StatusCode::TOO_MANY_REQUESTS, description = "Scan already in progress"),
|
||||
(status = StatusCode::INTERNAL_SERVER_ERROR, description = "Scan failed"),
|
||||
),
|
||||
summary = "Wifi SSID scan",
|
||||
description = "Poll for a list of available wifi networks. Returns an array of WifiNetwork objects."
|
||||
))]
|
||||
pub async fn scan_wifi(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<Json<Vec<wifi_station::WifiNetwork>>, (StatusCode, String)> {
|
||||
let _guard = state.wifi_scan_lock.try_lock().map_err(|_| {
|
||||
(
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
"WiFi scan already in progress".to_string(),
|
||||
)
|
||||
})?;
|
||||
let networks = wifi_station::scan_wifi_networks(wifi_station::STA_IFACE)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("WiFi scan failed: {e}"),
|
||||
)
|
||||
})?;
|
||||
Ok(Json(networks))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
post,
|
||||
path = "/api/debug/display-state",
|
||||
@@ -504,6 +564,8 @@ mod tests {
|
||||
analysis_sender: analysis_tx,
|
||||
daemon_restart_token: CancellationToken::new(),
|
||||
ui_update_sender: None,
|
||||
wifi_status: Arc::new(RwLock::new(wifi_station::WifiStatus::default())),
|
||||
wifi_scan_lock: tokio::sync::Mutex::new(()),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { get_config, set_config, test_notification, type Config } from '../utils.svelte';
|
||||
import {
|
||||
get_config,
|
||||
set_config,
|
||||
test_notification,
|
||||
get_wifi_status,
|
||||
scan_wifi_networks,
|
||||
type Config,
|
||||
type WifiStatus,
|
||||
type WifiNetwork,
|
||||
} from '../utils.svelte';
|
||||
import Modal from './Modal.svelte';
|
||||
|
||||
let { shown = $bindable() }: { shown: boolean } = $props();
|
||||
@@ -12,13 +21,20 @@
|
||||
let messageType = $state<'success' | 'error' | null>(null);
|
||||
let testMessage = $state('');
|
||||
let testMessageType = $state<'success' | 'error' | null>(null);
|
||||
let wifiStatus = $state<WifiStatus | null>(null);
|
||||
let wifiStatusTimer = $state<ReturnType<typeof setInterval> | null>(null);
|
||||
let scanning = $state(false);
|
||||
let scanResults = $state<WifiNetwork[]>([]);
|
||||
let dnsServersInput = $state('');
|
||||
|
||||
async function load_config() {
|
||||
try {
|
||||
loading = true;
|
||||
config = await get_config();
|
||||
dnsServersInput = config.dns_servers ? config.dns_servers.join(', ') : '';
|
||||
message = '';
|
||||
messageType = null;
|
||||
poll_wifi_status();
|
||||
} catch (error) {
|
||||
message = `Failed to load config: ${error}`;
|
||||
messageType = 'error';
|
||||
@@ -30,6 +46,15 @@
|
||||
async function save_config() {
|
||||
if (!config) return;
|
||||
|
||||
const trimmed = dnsServersInput.trim();
|
||||
config.dns_servers =
|
||||
trimmed.length > 0
|
||||
? trimmed
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
: null;
|
||||
|
||||
try {
|
||||
saving = true;
|
||||
await set_config(config);
|
||||
@@ -44,6 +69,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function poll_wifi_status() {
|
||||
if (wifiStatusTimer) clearInterval(wifiStatusTimer);
|
||||
try {
|
||||
wifiStatus = await get_wifi_status();
|
||||
} catch {
|
||||
wifiStatus = null;
|
||||
}
|
||||
wifiStatusTimer = setInterval(async () => {
|
||||
try {
|
||||
wifiStatus = await get_wifi_status();
|
||||
} catch {
|
||||
wifiStatus = null;
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
let scanError = $state('');
|
||||
|
||||
async function do_scan() {
|
||||
scanning = true;
|
||||
scanError = '';
|
||||
try {
|
||||
scanResults = await scan_wifi_networks();
|
||||
} catch (error) {
|
||||
scanResults = [];
|
||||
scanError = `Scan failed: ${error}`;
|
||||
} finally {
|
||||
scanning = false;
|
||||
}
|
||||
}
|
||||
|
||||
function select_network(network: WifiNetwork) {
|
||||
if (config) {
|
||||
config.wifi_ssid = network.ssid;
|
||||
config.wifi_password = '';
|
||||
config.wifi_security =
|
||||
network.security === 'WPA3' || network.security === 'WPA3 (transition)'
|
||||
? 'sae'
|
||||
: 'wpa_psk';
|
||||
scanResults = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function send_test_notification() {
|
||||
try {
|
||||
testingNotification = true;
|
||||
@@ -64,6 +132,16 @@
|
||||
if (shown && !config) {
|
||||
load_config();
|
||||
}
|
||||
if (!shown && wifiStatusTimer) {
|
||||
clearInterval(wifiStatusTimer);
|
||||
wifiStatusTimer = null;
|
||||
}
|
||||
return () => {
|
||||
if (wifiStatusTimer) {
|
||||
clearInterval(wifiStatusTimer);
|
||||
wifiStatusTimer = null;
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -267,6 +345,218 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if config.device === 'orbic' || config.device === 'moxee' || config.device === 'tmobile' || config.device === 'wingtech'}
|
||||
<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</h3>
|
||||
<p class="text-xs text-gray-500">
|
||||
Connect the device to an existing WiFi network for internet access (e.g.
|
||||
notifications, remote access). The hotspot AP stays running alongside
|
||||
WiFi client mode.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="wifi_enabled"
|
||||
type="checkbox"
|
||||
bind:checked={config.wifi_enabled}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label for="wifi_enabled" class="ml-2 block text-sm text-gray-700">
|
||||
Enable WiFi
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">
|
||||
Unchecking stops WiFi without clearing saved credentials.
|
||||
</p>
|
||||
|
||||
{#if wifiStatus && config.wifi_enabled}
|
||||
{#if wifiStatus.state === 'connected'}
|
||||
<p class="text-xs text-green-600">
|
||||
Connected to "{wifiStatus.ssid}" ({wifiStatus.ip})
|
||||
</p>
|
||||
{:else if wifiStatus.state === 'connecting'}
|
||||
<p class="text-xs text-amber-600">Connecting...</p>
|
||||
{:else if wifiStatus.state === 'recovering'}
|
||||
<p class="text-xs text-amber-600">Recovering connection...</p>
|
||||
{:else if wifiStatus.state === 'dataPathDead'}
|
||||
<p class="text-xs text-amber-600">
|
||||
Data path stalled, attempting recovery...
|
||||
</p>
|
||||
{:else if wifiStatus.state === 'failed'}
|
||||
<p class="text-xs text-red-600">
|
||||
Failed: {wifiStatus.error}
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="wifi_ssid"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
WiFi Network Name (SSID)
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
id="wifi_ssid"
|
||||
type="text"
|
||||
bind:value={config.wifi_ssid}
|
||||
placeholder="MyWiFiNetwork"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={do_scan}
|
||||
disabled={scanning}
|
||||
class="px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 disabled:opacity-50 border border-gray-300 rounded-md"
|
||||
>
|
||||
{scanning ? 'Scanning...' : 'Scan'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if scanError}
|
||||
<p class="text-xs text-red-600">{scanError}</p>
|
||||
{/if}
|
||||
|
||||
{#if scanResults.length > 0}
|
||||
<div
|
||||
class="border border-gray-200 rounded-md max-h-40 overflow-y-auto divide-y"
|
||||
>
|
||||
{#each scanResults as network}
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-left text-sm hover:bg-gray-50 flex justify-between"
|
||||
onclick={() => select_network(network)}
|
||||
>
|
||||
<span>{network.ssid}</span>
|
||||
<span class="text-gray-400"
|
||||
>{network.signal_dbm} dBm · {network.security}</span
|
||||
>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if config.wifi_ssid}
|
||||
<div>
|
||||
<label
|
||||
for="wifi_security"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Security Type
|
||||
</label>
|
||||
<select
|
||||
id="wifi_security"
|
||||
bind:value={config.wifi_security}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
>
|
||||
<option value="wpa_psk">WPA2 (WPA-PSK)</option>
|
||||
<option value="sae">WPA3 (SAE)</option>
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<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="Enter password"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Changing the network requires re-entering the password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if config.wifi_ssid}
|
||||
<div>
|
||||
<label
|
||||
for="dns_servers"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
DNS Servers
|
||||
</label>
|
||||
<input
|
||||
id="dns_servers"
|
||||
type="text"
|
||||
bind:value={dnsServersInput}
|
||||
placeholder="9.9.9.9, 149.112.112.112"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Comma-separated. Used when WiFi is active. Defaults to 9.9.9.9,
|
||||
149.112.112.112 (Quad9).
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="border-t pt-4 mt-6 space-y-3">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Device Security</h3>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="firewall_restrict_outbound"
|
||||
type="checkbox"
|
||||
bind:checked={config.firewall_restrict_outbound}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label
|
||||
for="firewall_restrict_outbound"
|
||||
class="ml-2 block text-sm text-gray-700"
|
||||
>
|
||||
Restrict outbound traffic
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">
|
||||
Only allows DNS, DHCP, and HTTPS (port 443) outbound. Blocks all other
|
||||
outbound connections on every interface (WiFi and cellular). Loopback and
|
||||
hotspot traffic are always allowed. Changes take effect immediately.
|
||||
</p>
|
||||
|
||||
{#if config.firewall_restrict_outbound}
|
||||
<div>
|
||||
<label
|
||||
for="firewall_allowed_ports"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Additional Allowed Ports
|
||||
</label>
|
||||
<input
|
||||
id="firewall_allowed_ports"
|
||||
type="text"
|
||||
value={config.firewall_allowed_ports
|
||||
? config.firewall_allowed_ports.join(', ')
|
||||
: ''}
|
||||
oninput={(e) => {
|
||||
const val = (e.target as HTMLInputElement).value.trim();
|
||||
config!.firewall_allowed_ports =
|
||||
val.length > 0
|
||||
? val
|
||||
.split(',')
|
||||
.map((s) => parseInt(s.trim()))
|
||||
.filter((n) => !isNaN(n) && n >= 1 && n <= 65535)
|
||||
: null;
|
||||
}}
|
||||
placeholder="22, 80"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Comma-separated TCP ports, e.g. 22, 80
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">
|
||||
Analyzer Heuristic Settings
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
}: { shown: boolean; title: string; children: Snippet } = $props();
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('scroll', () => {
|
||||
const handler = () => {
|
||||
document.documentElement.style.setProperty('--scroll-y', `${window.scrollY}px`);
|
||||
});
|
||||
};
|
||||
window.addEventListener('scroll', handler);
|
||||
return () => window.removeEventListener('scroll', handler);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -19,6 +19,7 @@ export enum enabled_notifications {
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
device: string;
|
||||
ui_level: number;
|
||||
colorblind_mode: boolean;
|
||||
key_input_mode: number;
|
||||
@@ -27,6 +28,34 @@ 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;
|
||||
wifi_security: 'wpa_psk' | 'sae' | null;
|
||||
wifi_enabled: boolean;
|
||||
dns_servers: string[] | null;
|
||||
firewall_restrict_outbound: boolean;
|
||||
firewall_allowed_ports: number[] | null;
|
||||
}
|
||||
|
||||
export interface WifiStatus {
|
||||
state: string;
|
||||
ssid?: string;
|
||||
ip?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface WifiNetwork {
|
||||
ssid: string;
|
||||
signal_dbm: number;
|
||||
security: string;
|
||||
}
|
||||
|
||||
export async function get_wifi_status(): Promise<WifiStatus> {
|
||||
return JSON.parse(await req('GET', '/api/wifi-status'));
|
||||
}
|
||||
|
||||
export async function scan_wifi_networks(): Promise<WifiNetwork[]> {
|
||||
return JSON.parse(await req('POST', '/api/wifi-scan'));
|
||||
}
|
||||
|
||||
export async function req(method: string, url: string, json_body?: unknown): Promise<string> {
|
||||
|
||||
23
dist/config.toml.in
vendored
23
dist/config.toml.in
vendored
@@ -24,7 +24,7 @@ ui_level = 1
|
||||
key_input_mode = 0
|
||||
|
||||
# If set, attempts to send a notification to the url when a new warning is triggered
|
||||
ntfy_url = ""
|
||||
# ntfy_url = "https://ntfy.sh/your-topic"
|
||||
# What notification types to enable. Does nothing if the above ntfy_url is not set.
|
||||
enabled_notifications = ["Warning", "LowBattery"]
|
||||
|
||||
@@ -34,6 +34,27 @@ 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
|
||||
# Toggle wifi_enabled to connect the device to an existing WiFi network.
|
||||
# Credentials are stored separately in wpa_sta.conf and managed via the web UI.
|
||||
wifi_enabled = false
|
||||
|
||||
# DNS servers to use when WiFi client mode is active.
|
||||
# Defaults to ["9.9.9.9", "149.112.112.112"] (Quad9) if not specified.
|
||||
# dns_servers = ["9.9.9.9", "149.112.112.112"]
|
||||
|
||||
# Device Security
|
||||
# Restrict outbound traffic to essential services only (DHCP, DNS,
|
||||
# HTTPS, and replies to inbound connections). Applies to all outbound
|
||||
# interfaces (WiFi and cellular). Loopback and hotspot bridge traffic
|
||||
# are always allowed. Defaults to true (recommended).
|
||||
firewall_restrict_outbound = true
|
||||
|
||||
# Additional TCP ports to allow outbound when the firewall is active.
|
||||
# DHCP (67-68), DNS (53), and HTTPS (443) are always allowed.
|
||||
# Example: allow HTTP (80) and SSH (22).
|
||||
# firewall_allowed_ports = [80, 22]
|
||||
|
||||
# Analyzer Configuration
|
||||
# Enable/disable specific IMSI catcher detection heuristics
|
||||
# See https://github.com/EFForg/rayhunter/blob/main/doc/heuristics.md for details
|
||||
|
||||
24
dist/scripts/S01iptables
vendored
Normal file
24
dist/scripts/S01iptables
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
CONFIG="/data/rayhunter/config.toml"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
if grep -q '^firewall_restrict_outbound = true' "$CONFIG" 2>/dev/null; then
|
||||
iptables -F OUTPUT
|
||||
iptables -A OUTPUT -o lo -j ACCEPT
|
||||
for br in bridge0 br0; do
|
||||
[ -d "/sys/class/net/$br" ] && iptables -A OUTPUT -o "$br" -j ACCEPT
|
||||
done
|
||||
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
iptables -A OUTPUT -p udp --dport 67:68 -j ACCEPT
|
||||
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
|
||||
iptables -A OUTPUT -j DROP
|
||||
echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptables 2>/dev/null
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
iptables -F OUTPUT
|
||||
iptables -P OUTPUT ACCEPT
|
||||
;;
|
||||
esac
|
||||
@@ -21,4 +21,38 @@ Through web UI you can set:
|
||||
- *Low Battery*, which will alert when the device's battery is low. Notifications may not be supported for all devices—you can check if your device is supported by looking at whether the battery level indicator is functioning on the System Information section of the Rayhunter UI.
|
||||
- With **Analyzer Heuristic Settings** you can switch on or off built-in [Rayhunter heuristics](heuristics.md). Some heuristics are experimental or can trigger a lot of false positive warnings in some networks (our tests have shown that some heuristics have different behavior in US or European networks). In that case you can decide whether you would like to have the heuristics that trigger a lot of false positives on or off. Please note that we are constantly improving and adding new heuristics, so a new release may reduce false positives in existing heuristics as well.
|
||||
|
||||
## WiFi Client Mode
|
||||
|
||||
On the **Orbic**, **Moxee**, **UZ801**, **TMOHS1**, and **Wingtech**, Rayhunter can connect the device to an existing WiFi network while keeping the hotspot running. This gives the device internet access for [notifications](https://docs.ntfy.sh/) and lets you reach the web UI from any device on that network.
|
||||
|
||||
- **Enable WiFi** turns WiFi client mode on or off. Disabling it does not erase saved credentials.
|
||||
- **Scan** searches for nearby networks. Select one from the dropdown, or type an SSID manually.
|
||||
- **Password** is required for WPA/WPA2 networks. The password is stored separately from `config.toml` (in `wpa_sta.conf` on the device) and is never exposed through the API.
|
||||
- **DNS Servers** lets you override the DNS servers used when connected. Defaults to `9.9.9.9` and `149.112.112.112` (Quad9) if not set.
|
||||
|
||||
After saving, the connection status will show **connecting**, **connected** (with the assigned IP address), or **failed** (with an error message). If the connection fails, check that the SSID and password are correct and that the network is in range.
|
||||
|
||||
### Crash Recovery
|
||||
|
||||
The WiFi kernel module (`wlan.ko`) can occasionally crash or unload, taking both the hotspot and client interfaces down with it. Rayhunter includes a watchdog that detects this and automatically reloads the module, restarts the hotspot, and reconnects to the configured network. During recovery the WiFi status will show **recovering**.
|
||||
|
||||
On the first detection of a crash, a diagnostic snapshot is saved to `/data/rayhunter/crash-logs/` on the device. You can pull these logs with `adb pull /data/rayhunter/crash-logs/` and inspect them to understand what went wrong. Each log contains:
|
||||
|
||||
- **dmesg** output (kernel messages). Look for backtraces, `BUG:`/`Oops:` lines, or `wlan`/`wcnss` errors. The kernel ring buffer is small and gets overwritten quickly, so crash details may already be gone if the crash happened well before detection.
|
||||
- **/proc/modules** snapshot. If `wlan` is absent, the module fully unloaded. If present but interfaces are gone, the driver is stuck.
|
||||
- **ip addr** output confirming which network interfaces existed at snapshot time.
|
||||
- **ps** output showing which WiFi-related processes (`hostapd`, `wpa_supplicant`, `wland`) were still running.
|
||||
|
||||
If recovery fails after 5 attempts, the status will change to **failed**. A reboot of the device will reset WiFi.
|
||||
|
||||
You can also configure WiFi during installation:
|
||||
|
||||
```sh
|
||||
./installer orbic --admin-password 'mypassword' --wifi-ssid 'MyNetwork' --wifi-password 'networkpass'
|
||||
```
|
||||
|
||||
## Device Security
|
||||
|
||||
- **Restrict outbound traffic** limits what the device can send over the network. When enabled, only DNS, DHCP, and HTTPS traffic is allowed; everything else is blocked. This is enabled by default and prevents the device from phoning home to the carrier over cellular. If you need to allow additional ports (for example, port 80 for HTTP or port 22 for SSH), add them to the **Additional allowed ports** list.
|
||||
|
||||
If you prefer editing `config.toml` file, you need to obtain a shell on your [Orbic](./orbic.md#obtaining-a-shell) or [TP-Link](./tplink-m7350.md#obtaining-a-shell) device and edit the file manually. You can view the [default configuration file on GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in).
|
||||
|
||||
@@ -50,6 +50,14 @@ reboot
|
||||
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
|
||||
|
||||
|
||||
### How do I connect my device to an existing WiFi network?
|
||||
|
||||
The Orbic, Moxee, UZ801, and TMOHS1 can connect to a nearby WiFi network while still running their own hotspot. This gives the device internet access for ntfy notifications and lets you reach the web UI from your home network. See [WiFi Client Mode](./configuration.md#wifi-client-mode) in the configuration guide for setup instructions.
|
||||
|
||||
### WiFi client mode is connected but I can't reach the internet
|
||||
|
||||
Check that the **DNS Servers** field in the config has valid entries (the default is `9.9.9.9` and `149.112.112.112`). If your home network and the device hotspot use the same subnet (for example, both are on `192.168.1.x`), try restarting the daemon by saving the config again from the web UI.
|
||||
|
||||
### How do I disable the WiFi hotspot on the Orbic RC400L?
|
||||
|
||||
To disable both WiFi bands, [first obtain a shell](./orbic.md#shell), then:
|
||||
|
||||
@@ -44,6 +44,9 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
||||
# Note: the arguments --admin-username 'myusername' and --admin-ip 'mydeviceip'
|
||||
# may be required if different from the default.
|
||||
|
||||
# Optionally configure WiFi client mode during install (Orbic and Moxee only):
|
||||
./installer orbic --admin-password 'mypassword' --wifi-ssid 'MyNetwork' --wifi-password 'networkpass'
|
||||
|
||||
# Or install over USB if you want ADB and a root shell (not recommended for most users)
|
||||
./installer orbic-usb
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ pay more than 30 USD for such a device (without shipping).
|
||||
| Wifi 5Ghz | a/ac/ax |
|
||||
| Wifi 6 | 🮱 |
|
||||
|
||||
## WiFi client mode
|
||||
|
||||
The Orbic's QCA6174 radio supports running the hotspot and connecting to an external WiFi network at the same time. See [WiFi Client Mode](./configuration.md#wifi-client-mode) for setup.
|
||||
|
||||
## Two kinds of installers
|
||||
|
||||
The orbic's installation routine underwent many different changes:
|
||||
|
||||
@@ -36,6 +36,10 @@ According to FCC ID 2APXW-TMOHS1 Test Report No. I20Z61602-WMD02 ([part 1](https
|
||||
| 66 | 1700 MHz (E-AWS) |
|
||||
| 71 | 600 MHz (USDD) |
|
||||
|
||||
## WiFi client mode
|
||||
|
||||
The TMOHS1 supports WiFi client mode, allowing Rayhunter to connect to an existing WiFi network while keeping the hotspot running. See [WiFi Client Mode](./configuration.md#wifi-client-mode) for setup.
|
||||
|
||||
## Installing
|
||||
Connect to the TMOHS1's network over WiFi or USB tethering.
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ You can access this UI in one of two ways:
|
||||
On the **Orbic**, you can find the WiFi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
|
||||
On the **TP-Link**, you can find the WiFi network password by going to the TP-Link's menu > Advanced > Wireless > Basic Settings.
|
||||
|
||||
If [WiFi client mode](./configuration.md#wifi-client-mode) is enabled, you can also reach the web UI from any device on that network at `http://<device-ip>:8080`.
|
||||
|
||||
* **Connect over USB (Orbic):** Connect your device to your laptop via USB. Run `adb forward tcp:8080 tcp:8080`, then visit <http://localhost:8080>.
|
||||
* For this you will need to install the Android Debug Bridge (ADB) on your computer, you can copy the version that was downloaded inside the `releases/platform-tools/` folder to somewhere else in your path or you can install it manually.
|
||||
* You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer), (don't worry about instructions for installing it on a phone/device yet).
|
||||
|
||||
@@ -36,6 +36,12 @@ With the device fully booted (i.e. beaming a WiFi network, blue LED, etc.) and p
|
||||
|
||||
Note: The default IP for UZ801 is typically `192.168.100.1`; if yours differs, use the `--admin-ip` argument to specify it.
|
||||
|
||||
## WiFi client mode
|
||||
|
||||
The UZ801's WCN36xx (PRONTO) radio supports concurrent AP+STA mode. The daemon has backend support for WiFi client mode on the UZ801, but this has not yet been successfully exercised end-to-end and the web UI currently does not expose the configuration surface on this device. Treat UZ801 WiFi client mode as not yet supported. See [WiFi Client Mode](./configuration.md#wifi-client-mode) for the intended setup on supported devices.
|
||||
|
||||
The interface creation method differs from the Orbic (which uses `iw`): the UZ801 creates a P2P_CLIENT virtual interface via nl80211 and converts it to a managed STATION interface. This is handled by the daemon when the feature is enabled.
|
||||
|
||||
## LED modes
|
||||
| Rayhunter state | LED indicator |
|
||||
| ---------------- | ------------------- |
|
||||
|
||||
@@ -28,6 +28,10 @@ Wingtechs are abundant on ebay and can also be found on Amazon:
|
||||
- <https://www.ebay.com/itm/127147132518>
|
||||
- <https://www.amazon.com/AT-Turbo-Hotspot-256-Black/dp/B09YWLXVWT>
|
||||
|
||||
## WiFi client mode
|
||||
|
||||
The Wingtech supports WiFi client mode, allowing Rayhunter to connect to an existing WiFi network while keeping the hotspot running. See [WiFi Client Mode](./configuration.md#wifi-client-mode) for setup.
|
||||
|
||||
## Installing
|
||||
Connect to the Wingtech's network over WiFi or USB tethering, then run the installer:
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ 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");
|
||||
set_binary_var(&wpa_dir, "FILE_IW", "iw");
|
||||
}
|
||||
|
||||
fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
|
||||
|
||||
@@ -43,6 +43,47 @@ pub async fn install_config<C: DeviceConnection>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install wifi tools (wpa_supplicant, wpa_cli, iw) to /data/rayhunter/bin.
|
||||
///
|
||||
/// Skips any binary that is already present on the device (e.g. provided by firmware),
|
||||
/// since those may be newer or better-integrated than the bundled versions.
|
||||
pub async fn install_wifi_tools<C: DeviceConnection>(
|
||||
conn: &mut C,
|
||||
wpa_supplicant: &[u8],
|
||||
wpa_cli: &[u8],
|
||||
iw: &[u8],
|
||||
) -> Result<()> {
|
||||
let tools: &[(&str, &str, &[u8])] = &[
|
||||
(
|
||||
"wpa_supplicant",
|
||||
"/data/rayhunter/bin/wpa_supplicant",
|
||||
wpa_supplicant,
|
||||
),
|
||||
("wpa_cli", "/data/rayhunter/bin/wpa_cli", wpa_cli),
|
||||
("iw", "/data/rayhunter/bin/iw", iw),
|
||||
];
|
||||
for &(name, dest, payload) in tools {
|
||||
if device_has_binary(conn, name).await {
|
||||
println!("{name} already on device, skipping");
|
||||
} else {
|
||||
conn.write_file(dest, payload).await?;
|
||||
conn.run_command(&format!("chmod +x {dest}")).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn device_has_binary<C: DeviceConnection>(conn: &mut C, name: &str) -> bool {
|
||||
// `command -v` is a POSIX shell builtin, so it works on minimal busybox firmware
|
||||
// even when /usr/bin/which is absent.
|
||||
conn.run_command(&format!(
|
||||
"\"command -v {name} >/dev/null 2>&1 && echo FOUND || echo MISSING\""
|
||||
))
|
||||
.await
|
||||
.map(|out| out.contains("FOUND"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if a directory exists using a DeviceConnection
|
||||
pub async fn dir_exists<C: DeviceConnection>(conn: &mut C, path: &str) -> bool {
|
||||
conn.run_command(&format!("test -d '{path}' && echo exists || echo missing"))
|
||||
@@ -172,7 +213,13 @@ impl TelnetConnection {
|
||||
|
||||
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
|
||||
crate::util::telnet_send_command_with_output(
|
||||
self.addr,
|
||||
command,
|
||||
self.wait_for_prompt,
|
||||
std::time::Duration::from_secs(10),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn write_file(&mut self, path: &str, content: &[u8]) -> Result<()> {
|
||||
|
||||
@@ -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_tools};
|
||||
use crate::output::{print, println};
|
||||
use crate::util::open_usb_device;
|
||||
|
||||
@@ -53,8 +53,15 @@ pub struct AdbConnection<'a> {
|
||||
}
|
||||
|
||||
impl DeviceConnection for AdbConnection<'_> {
|
||||
/// Runs through /bin/rootshell so commands execute as root (install_wifi_tools needs
|
||||
/// chmod on root-owned files). setup_rootshell must have succeeded before an
|
||||
/// AdbConnection is created; callers in this module (setup_rayhunter) enforce that
|
||||
/// ordering.
|
||||
async fn run_command(&mut self, command: &str) -> Result<String> {
|
||||
adb_command(self.device, &["sh", "-c", command])
|
||||
adb_command(
|
||||
self.device,
|
||||
&["/bin/rootshell", "-c", &format!("\"{command}\"")],
|
||||
)
|
||||
}
|
||||
|
||||
async fn write_file(&mut self, path: &str, content: &[u8]) -> Result<()> {
|
||||
@@ -146,7 +153,11 @@ async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
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?;
|
||||
adb_at_syscmd(
|
||||
&mut adb_device,
|
||||
"mkdir -p /data/rayhunter/scripts /data/rayhunter/bin",
|
||||
)
|
||||
.await?;
|
||||
install_file(
|
||||
&mut adb_device,
|
||||
"/data/rayhunter/rayhunter-daemon",
|
||||
@@ -159,6 +170,13 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re
|
||||
device: &mut adb_device,
|
||||
};
|
||||
install_config(&mut conn, "orbic", reset_config).await?;
|
||||
install_wifi_tools(
|
||||
&mut conn,
|
||||
include_bytes!(env!("FILE_WPA_SUPPLICANT")),
|
||||
include_bytes!(env!("FILE_WPA_CLI")),
|
||||
include_bytes!(env!("FILE_IW")),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
install_file(
|
||||
@@ -173,8 +191,15 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re
|
||||
include_bytes!("../../dist/scripts/misc-daemon"),
|
||||
)
|
||||
.await?;
|
||||
install_file(
|
||||
&mut adb_device,
|
||||
"/etc/init.d/S01iptables",
|
||||
include_bytes!("../../dist/scripts/S01iptables"),
|
||||
)
|
||||
.await?;
|
||||
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/rayhunter_daemon").await?;
|
||||
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?;
|
||||
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/S01iptables").await?;
|
||||
println!("done");
|
||||
print!("Waiting for reboot... ");
|
||||
adb_at_syscmd(&mut adb_device, "shutdown -r -t 1 now").await?;
|
||||
|
||||
@@ -8,7 +8,9 @@ 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_tools, 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};
|
||||
@@ -229,6 +231,15 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) ->
|
||||
let mut conn = TelnetConnection::new(addr, false);
|
||||
setup_data_directory(&mut conn, data_dir).await?;
|
||||
|
||||
// Ensure bin and scripts directories exist under the data dir (via symlink)
|
||||
telnet_send_command(
|
||||
addr,
|
||||
"mkdir -p /data/rayhunter/scripts /data/rayhunter/bin",
|
||||
"exit code 0",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/data/rayhunter/rayhunter-daemon",
|
||||
@@ -237,6 +248,14 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) ->
|
||||
)
|
||||
.await?;
|
||||
|
||||
install_wifi_tools(
|
||||
&mut conn,
|
||||
include_bytes!(env!("FILE_WPA_SUPPLICANT")),
|
||||
include_bytes!(env!("FILE_WPA_CLI")),
|
||||
include_bytes!(env!("FILE_IW")),
|
||||
)
|
||||
.await?;
|
||||
|
||||
install_config(&mut conn, "orbic", reset_config).await?;
|
||||
|
||||
telnet_send_file(
|
||||
@@ -254,6 +273,13 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) ->
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/etc/init.d/S01iptables",
|
||||
include_bytes!("../../dist/scripts/S01iptables"),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_command(
|
||||
addr,
|
||||
@@ -276,6 +302,13 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) ->
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
telnet_send_command(
|
||||
addr,
|
||||
"chmod 755 /etc/init.d/S01iptables",
|
||||
"exit code 0",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Installation complete. Rebooting device...");
|
||||
telnet_send_command(addr, "shutdown -r -t 1 now", "", false)
|
||||
|
||||
@@ -17,6 +17,7 @@ pub async fn telnet_send_command_with_output(
|
||||
addr: SocketAddr,
|
||||
command: &str,
|
||||
wait_for_prompt: bool,
|
||||
command_timeout: Duration,
|
||||
) -> Result<String> {
|
||||
if command.contains('\n') {
|
||||
bail!("multi-line commands are not allowed");
|
||||
@@ -41,7 +42,7 @@ pub async fn telnet_send_command_with_output(
|
||||
writer.write_all(format!("echo RAYHUNTER_'TELNET'_COMMAND_START; {command}; echo RAYHUNTER_'TELNET'_COMMAND_DONE\r\n").as_bytes()).await?;
|
||||
|
||||
let mut read_buf = Vec::new();
|
||||
timeout(Duration::from_secs(10), async {
|
||||
timeout(command_timeout, async {
|
||||
while let Ok(byte) = reader.read_u8().await {
|
||||
read_buf.push(byte);
|
||||
|
||||
@@ -57,7 +58,7 @@ pub async fn telnet_send_command_with_output(
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("command timed out after 10 seconds")?;
|
||||
.with_context(|| format!("command timed out after {}s", command_timeout.as_secs()))?;
|
||||
let string = String::from_utf8_lossy(&read_buf);
|
||||
let start = string.rfind("RAYHUNTER_TELNET_COMMAND_START");
|
||||
let end = string.rfind("RAYHUNTER_TELNET_COMMAND_DONE");
|
||||
@@ -79,7 +80,9 @@ pub async fn telnet_send_command(
|
||||
wait_for_prompt: bool,
|
||||
) -> Result<()> {
|
||||
let command = format!("{command}; echo command done, exit code $?");
|
||||
let output = telnet_send_command_with_output(addr, &command, wait_for_prompt).await?;
|
||||
let output =
|
||||
telnet_send_command_with_output(addr, &command, wait_for_prompt, Duration::from_secs(10))
|
||||
.await?;
|
||||
if !output.contains(expected_output) {
|
||||
bail!("{expected_output:?} not found in: {output}");
|
||||
}
|
||||
@@ -93,6 +96,9 @@ pub async fn telnet_send_file(
|
||||
wait_for_prompt: bool,
|
||||
) -> Result<()> {
|
||||
print!("Sending file {filename} ... ");
|
||||
// Allow 30s base + 2s per MB for the nc command to complete (covers slow WiFi links)
|
||||
let transfer_timeout =
|
||||
Duration::from_secs(30 + (payload.len() as u64 / (1024 * 1024)).max(1) * 2);
|
||||
let nc_output = {
|
||||
let filename = filename.to_owned();
|
||||
let handle = tokio::spawn(async move {
|
||||
@@ -100,6 +106,7 @@ pub async fn telnet_send_file(
|
||||
addr,
|
||||
&format!("nc -l -p 8081 2>&1 >{filename}.tmp"),
|
||||
wait_for_prompt,
|
||||
transfer_timeout,
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ pub async fn install(
|
||||
Args {
|
||||
admin_ip,
|
||||
admin_password,
|
||||
..
|
||||
}: Args,
|
||||
) -> Result<()> {
|
||||
wingtech_run_install(admin_ip, admin_password).await
|
||||
|
||||
@@ -32,3 +32,4 @@ num_enum = "0.7.4"
|
||||
utoipa = { version = "5.4.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -40,4 +40,5 @@ pub enum Device {
|
||||
Wingtech,
|
||||
Pinephone,
|
||||
Uz801,
|
||||
Moxee,
|
||||
}
|
||||
|
||||
@@ -43,6 +43,25 @@ build_frontend() {
|
||||
popd > /dev/null
|
||||
}
|
||||
|
||||
build_wifi_tools() {
|
||||
if [ -f "tools/build-wpa-supplicant/out/wpa_supplicant" ] \
|
||||
&& [ -f "tools/build-wpa-supplicant/out/wpa_cli" ] \
|
||||
&& [ -f "tools/build-wpa-supplicant/out/iw" ]; then
|
||||
echo "WiFi tools already built, skipping."
|
||||
return
|
||||
fi
|
||||
|
||||
if ! command -v arm-linux-musleabihf-gcc &> /dev/null; then
|
||||
echo "Error: arm-linux-musleabihf-gcc not found."
|
||||
echo "Install with: brew install FiloSottile/musl-cross/musl-cross"
|
||||
echo "(Required because the installer bundles wpa_supplicant, wpa_cli, and iw for orbic-family devices.)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building WiFi tools..."
|
||||
./scripts/build-wpa-supplicant.sh
|
||||
}
|
||||
|
||||
build_daemon() {
|
||||
echo "Building daemon..."
|
||||
cargo build-daemon-firmware-devel
|
||||
@@ -57,6 +76,7 @@ case "$COMMAND" in
|
||||
build)
|
||||
check_dependencies
|
||||
build_frontend
|
||||
build_wifi_tools
|
||||
build_daemon
|
||||
echo ""
|
||||
echo "Build complete! To install to a device, run:"
|
||||
|
||||
93
scripts/build-wpa-supplicant.sh
Executable file
93
scripts/build-wpa-supplicant.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
# Cross-compile wpa_supplicant, wpa_cli, and iw for ARMv7 (musl static).
|
||||
# Output: tools/build-wpa-supplicant/out/{wpa_supplicant,wpa_cli,iw}
|
||||
#
|
||||
# Requires: arm-linux-musleabihf-gcc (brew install FiloSottile/musl-cross/musl-cross)
|
||||
set -e
|
||||
|
||||
WPA_VERSION="2.11"
|
||||
WPA_URL="https://w1.fi/releases/wpa_supplicant-${WPA_VERSION}.tar.gz"
|
||||
LIBNL_VERSION="3.11.0"
|
||||
LIBNL_URL="https://github.com/thom311/libnl/releases/download/libnl${LIBNL_VERSION//\./_}/libnl-${LIBNL_VERSION}.tar.gz"
|
||||
IW_VERSION="6.9"
|
||||
IW_URL="https://www.kernel.org/pub/software/network/iw/iw-${IW_VERSION}.tar.xz"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
OUT_DIR="$SCRIPT_DIR/../tools/build-wpa-supplicant/out"
|
||||
BUILD_DIR="/tmp/wpa-supplicant-build-$$"
|
||||
|
||||
CC="${CC:-arm-linux-musleabihf-gcc}"
|
||||
STRIP="${STRIP:-arm-linux-musleabihf-strip}"
|
||||
HOST="${HOST:-arm-linux-musleabihf}"
|
||||
|
||||
if ! command -v "$CC" >/dev/null 2>&1; then
|
||||
echo "Error: $CC not found. Install with: brew install FiloSottile/musl-cross/musl-cross"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$BUILD_DIR" "$OUT_DIR"
|
||||
SYSROOT="$BUILD_DIR/sysroot"
|
||||
mkdir -p "$SYSROOT"
|
||||
|
||||
echo "Building libnl ${LIBNL_VERSION}..."
|
||||
curl -Lf "$LIBNL_URL" | tar xz -C "$BUILD_DIR"
|
||||
cd "$BUILD_DIR/libnl-${LIBNL_VERSION}"
|
||||
./configure \
|
||||
--host="$HOST" \
|
||||
CC="$CC" \
|
||||
--prefix="$SYSROOT" \
|
||||
--enable-static \
|
||||
--disable-shared \
|
||||
--disable-cli \
|
||||
--disable-debug \
|
||||
> /dev/null 2>&1
|
||||
make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)" > /dev/null 2>&1
|
||||
make install > /dev/null 2>&1
|
||||
|
||||
echo "Building wpa_supplicant ${WPA_VERSION}..."
|
||||
cd "$BUILD_DIR"
|
||||
curl -Lf "$WPA_URL" | tar xz
|
||||
cd "wpa_supplicant-${WPA_VERSION}/wpa_supplicant"
|
||||
|
||||
cat > .config <<'WPACONF'
|
||||
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
|
||||
WPACONF
|
||||
|
||||
NL_CFLAGS="-I${SYSROOT}/include/libnl3"
|
||||
NL_LIBS="-L${SYSROOT}/lib -lnl-genl-3 -lnl-3 -lpthread -lm"
|
||||
|
||||
make CC="$CC" \
|
||||
EXTRA_CFLAGS="$NL_CFLAGS" \
|
||||
LDFLAGS="-static" \
|
||||
LIBS="$NL_LIBS" \
|
||||
-j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)"
|
||||
|
||||
echo "Stripping..."
|
||||
$STRIP wpa_supplicant wpa_cli
|
||||
cp wpa_supplicant wpa_cli "$OUT_DIR/"
|
||||
|
||||
echo "Building iw ${IW_VERSION}..."
|
||||
cd "$BUILD_DIR"
|
||||
curl -Lf "$IW_URL" | tar xJ
|
||||
cd "iw-${IW_VERSION}"
|
||||
PKG_CONFIG_LIBDIR="$SYSROOT/lib/pkgconfig" \
|
||||
make CC="$CC" \
|
||||
LDFLAGS="-static" \
|
||||
-j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)"
|
||||
$STRIP iw
|
||||
cp iw "$OUT_DIR/"
|
||||
|
||||
rm -rf "$BUILD_DIR"
|
||||
|
||||
echo "Done. Binaries in $OUT_DIR:"
|
||||
ls -lh "$OUT_DIR"/{wpa_supplicant,wpa_cli,iw}
|
||||
Reference in New Issue
Block a user