mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-07-21 07:08:10 -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:
+3
-1
@@ -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"
|
||||
|
||||
+82
-3
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+18
-4
@@ -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();
|
||||
|
||||
+64
-2
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user