cleaning up the code a bit

This commit is contained in:
Ember
2026-02-25 20:19:42 -08:00
parent 96ab600a5a
commit 8e09ca42aa
5 changed files with 135 additions and 117 deletions
-2
View File
@@ -11,8 +11,6 @@ env:
CARGO_TERM_COLOR: always
FILE_ROOTSHELL: ../../rootshell/rootshell
FILE_RAYHUNTER_DAEMON: ../../rayhunter-daemon/rayhunter-daemon
FILE_WPA_SUPPLICANT: ../../rayhunter-daemon/rayhunter-daemon
FILE_WPA_CLI: ../../rayhunter-daemon/rayhunter-daemon
RUSTFLAGS: "-Dwarnings"
jobs:
+59 -77
View File
@@ -1,3 +1,4 @@
use anyhow::{Result, bail};
use log::{info, warn};
use tokio::process::Command;
@@ -5,6 +6,18 @@ use crate::config::Config;
const FIREWALL_FLAG: &str = "/data/rayhunter/firewall-enabled";
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) {
if config.block_ota_daemons {
block_ota_daemons().await;
@@ -16,8 +29,13 @@ pub async fn apply(config: &Config) {
.await;
if config.firewall_restrict_outbound {
setup_outbound_whitelist(&config.firewall_allowed_ports, &config.ntfy_url).await;
let _ = tokio::fs::write(FIREWALL_FLAG, "").await;
match setup_outbound_whitelist(&config.firewall_allowed_ports, &config.ntfy_url).await {
Ok(()) => {
info!("outbound firewall active: allowing DHCP, DNS, HTTPS only");
let _ = tokio::fs::write(FIREWALL_FLAG, "").await;
}
Err(e) => warn!("firewall setup failed: {e}"),
}
} else {
let _ = tokio::fs::remove_file(FIREWALL_FLAG).await;
}
@@ -44,96 +62,60 @@ async fn block_ota_daemons() {
}
}
async fn setup_outbound_whitelist(extra_ports: &Option<Vec<u16>>, ntfy_url: &Option<String>) {
let _ = Command::new("iptables")
.args(["-A", "OUTPUT", "-o", "lo", "-j", "ACCEPT"])
.output()
.await;
let _ = Command::new("iptables")
.args(["-A", "OUTPUT", "-o", "bridge0", "-j", "ACCEPT"])
.output()
.await;
let _ = Command::new("iptables")
.args([
"-A",
"OUTPUT",
"-m",
"state",
"--state",
"ESTABLISHED,RELATED",
"-j",
"ACCEPT",
])
.output()
.await;
let _ = Command::new("iptables")
.args([
"-A", "OUTPUT", "-p", "udp", "--dport", "67:68", "-j", "ACCEPT",
])
.output()
.await;
let _ = Command::new("iptables")
.args(["-A", "OUTPUT", "-p", "udp", "--dport", "53", "-j", "ACCEPT"])
.output()
.await;
let _ = Command::new("iptables")
.args(["-A", "OUTPUT", "-p", "tcp", "--dport", "53", "-j", "ACCEPT"])
.output()
.await;
let _ = Command::new("iptables")
.args([
"-A", "OUTPUT", "-p", "tcp", "--dport", "443", "-j", "ACCEPT",
])
.output()
.await;
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", "bridge0", "-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()
&& port != 443
{
let _ = Command::new("iptables")
.args([
"-A",
"OUTPUT",
"-p",
"tcp",
"--dport",
&port.to_string(),
"-j",
"ACCEPT",
])
.output()
.await;
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 _ = Command::new("iptables")
.args([
"-A",
"OUTPUT",
"-p",
"tcp",
"--dport",
&port.to_string(),
"-j",
"ACCEPT",
])
.output()
.await;
let port_str = port.to_string();
run_iptables(&[
"-A", "OUTPUT", "-p", "tcp", "--dport", &port_str, "-j", "ACCEPT",
])
.await?;
}
}
let _ = Command::new("iptables")
.args(["-A", "OUTPUT", "-j", "DROP"])
.output()
.await;
run_iptables(&["-A", "OUTPUT", "-j", "DROP"]).await?;
let _ = tokio::fs::write("/proc/sys/net/bridge/bridge-nf-call-iptables", "0").await;
info!("outbound firewall active: allowing DHCP, DNS, HTTPS only");
Ok(())
}
+1
View File
@@ -312,6 +312,7 @@ async fn run_with_config(
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;
+10 -2
View File
@@ -38,6 +38,7 @@ pub struct ServerState {
pub daemon_restart_token: CancellationToken,
pub ui_update_sender: Option<Sender<DisplayState>>,
pub wifi_status: Arc<RwLock<crate::wifi::WifiStatus>>,
pub wifi_scan_lock: tokio::sync::Mutex<()>,
}
#[cfg_attr(feature = "apidocs", utoipa::path(
@@ -411,9 +412,15 @@ pub async fn get_wifi_status(
}
pub async fn scan_wifi(
State(_state): State<Arc<ServerState>>,
State(state): State<Arc<ServerState>>,
) -> Result<Json<Vec<crate::wifi::WifiNetwork>>, (StatusCode, String)> {
let networks = crate::wifi::scan_wifi_networks("wlan1")
let _guard = state.wifi_scan_lock.try_lock().map_err(|_| {
(
StatusCode::TOO_MANY_REQUESTS,
"WiFi scan already in progress".to_string(),
)
})?;
let networks = crate::wifi::scan_wifi_networks(crate::wifi::STA_IFACE)
.await
.map_err(|e| {
(
@@ -529,6 +536,7 @@ mod tests {
daemon_restart_token: CancellationToken::new(),
ui_update_sender: None,
wifi_status: Arc::new(RwLock::new(crate::wifi::WifiStatus::default())),
wifi_scan_lock: tokio::sync::Mutex::new(()),
})
}
+65 -36
View File
@@ -17,17 +17,29 @@ use crate::config::Config;
pub const WPA_CONF_PATH: &str = "/data/rayhunter/wpa_sta.conf";
const WPA_BIN: &str = "/data/rayhunter/bin/wpa_supplicant";
const DEFAULT_DNS: &[&str] = &["8.8.8.8", "1.1.1.1"];
const DEFAULT_DNS: &[&str] = &["9.9.9.9", "149.112.112.112"];
const CRASH_LOG_DIR: &str = "/data/rayhunter/crash-logs";
const MAX_RECOVERY_ATTEMPTS: u32 = 5;
const BASE_BACKOFF_SECS: u64 = 30;
const HOSTAPD_CONF: &str = "/data/misc/wifi/hostapd.conf";
const AP_IFACE: &str = "wlan0";
const BRIDGE_IFACE: &str = "bridge0";
pub const STA_IFACE: &str = "wlan1";
#[derive(Clone, Copy, PartialEq, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum WifiState {
#[default]
Disabled,
Connecting,
Connected,
Failed,
Recovering,
}
#[derive(Clone, Serialize, Default)]
pub struct WifiStatus {
pub state: String,
pub state: WifiState,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -49,7 +61,7 @@ struct WifiClient {
impl WifiClient {
fn new(dns_servers: Vec<String>) -> Self {
WifiClient {
iface: "wlan1".to_string(),
iface: STA_IFACE.to_string(),
wpa_child: None,
dhcp_child: None,
rt_table: 100,
@@ -321,7 +333,9 @@ impl WifiClient {
// inferring the gateway as .1 from the kernel subnet route.
let ip = self.get_interface_ip().await?;
if let Some(last_dot) = ip.rfind('.') {
return Ok(format!("{}.1", &ip[..last_dot]));
let gw = format!("{}.1", &ip[..last_dot]);
warn!("no explicit gateway for {}, assuming {gw}", self.iface);
return Ok(gw);
}
bail!("no default gateway for interface")
@@ -495,7 +509,7 @@ async fn reload_wifi_module() -> Result<()> {
AP_IFACE,
"interface",
"add",
"wlan1",
STA_IFACE,
"type",
"managed",
])
@@ -503,7 +517,7 @@ async fn reload_wifi_module() -> Result<()> {
.await?;
if !add_sta.status.success() {
bail!(
"failed to create wlan1: {}",
"failed to create {STA_IFACE}: {}",
String::from_utf8_lossy(&add_sta.stderr).trim()
);
}
@@ -533,7 +547,7 @@ pub fn run_wifi_client(
task_tracker.spawn(async move {
{
let mut status = wifi_status.write().await;
status.state = "connecting".to_string();
status.state = WifiState::Connecting;
status.ssid = ssid.clone();
}
@@ -542,7 +556,7 @@ pub fn run_wifi_client(
Ok(()) => {
let ip = client.get_interface_ip().await.ok();
let mut status = wifi_status.write().await;
status.state = "connected".to_string();
status.state = WifiState::Connected;
status.ssid = ssid.clone();
status.ip = ip;
status.error = None;
@@ -551,7 +565,7 @@ pub fn run_wifi_client(
Err(e) => {
client.stop().await;
let mut status = wifi_status.write().await;
status.state = "failed".to_string();
status.state = WifiState::Failed;
status.error = Some(format!("{e}"));
error!("WiFi client failed to start: {e}");
return;
@@ -566,7 +580,7 @@ pub fn run_wifi_client(
_ = shutdown_token.cancelled() => {
client.stop().await;
let mut status = wifi_status.write().await;
status.state = "disabled".to_string();
status.state = WifiState::Disabled;
status.ip = None;
status.error = None;
info!("WiFi client stopped");
@@ -580,7 +594,7 @@ pub fn run_wifi_client(
);
client.stop().await;
let mut status = wifi_status.write().await;
status.state = "failed".to_string();
status.state = WifiState::Failed;
status.error = Some(format!(
"module crash recovery failed after {MAX_RECOVERY_ATTEMPTS} attempts"
));
@@ -589,12 +603,12 @@ pub fn run_wifi_client(
recovery_attempts += 1;
warn!(
"wlan1 interface disappeared, attempting recovery ({recovery_attempts}/{MAX_RECOVERY_ATTEMPTS})"
"{STA_IFACE} interface disappeared, attempting recovery ({recovery_attempts}/{MAX_RECOVERY_ATTEMPTS})"
);
{
let mut status = wifi_status.write().await;
status.state = "recovering".to_string();
status.state = WifiState::Recovering;
status.ip = None;
status.error = None;
}
@@ -610,7 +624,7 @@ pub fn run_wifi_client(
if let Err(e) = reload_wifi_module().await {
error!("module reload failed: {e}");
let mut status = wifi_status.write().await;
status.state = "recovering".to_string();
status.state = WifiState::Recovering;
status.error = Some(format!("{e}"));
backoff_secs = (backoff_secs * 2).min(240);
continue;
@@ -620,7 +634,7 @@ pub fn run_wifi_client(
Ok(()) => {
let ip = client.get_interface_ip().await.ok();
let mut status = wifi_status.write().await;
status.state = "connected".to_string();
status.state = WifiState::Connected;
status.ip = ip;
status.error = None;
info!(
@@ -633,7 +647,7 @@ pub fn run_wifi_client(
error!("WiFi client restart after recovery failed: {e}");
client.stop().await;
let mut status = wifi_status.write().await;
status.state = "recovering".to_string();
status.state = WifiState::Recovering;
status.error = Some(format!("{e}"));
backoff_secs = (backoff_secs * 2).min(240);
}
@@ -675,6 +689,10 @@ pub fn run_wifi_client(
}
pub async fn update_wpa_conf(config: &Config) {
update_wpa_conf_at(config, WPA_CONF_PATH).await;
}
async fn update_wpa_conf_at(config: &Config, path: &str) {
let has_ssid = config
.wifi_ssid
.as_ref()
@@ -689,18 +707,18 @@ pub async fn update_wpa_conf(config: &Config) {
config.wifi_ssid.as_ref().unwrap(),
config.wifi_password.as_ref().unwrap(),
);
if let Err(e) = tokio::fs::write(WPA_CONF_PATH, conf).await {
if let Err(e) = tokio::fs::write(path, conf).await {
warn!("failed to write wpa_supplicant config: {e}");
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ =
tokio::fs::set_permissions(WPA_CONF_PATH, std::fs::Permissions::from_mode(0o600))
.await;
let _ = tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).await;
}
} else if !has_ssid {
let _ = tokio::fs::remove_file(WPA_CONF_PATH).await;
let _ = tokio::fs::remove_file(path).await;
} else {
warn!("wifi_ssid set without wifi_password, skipping wpa_supplicant config");
}
}
@@ -738,10 +756,10 @@ pub async fn scan_wifi_networks(iface: &str) -> Result<Vec<WifiNetwork>> {
.args(["dev", iface, "scan"])
.output()
.await?;
parse_iw_scan(&String::from_utf8_lossy(&out.stdout))
Ok(parse_iw_scan(&String::from_utf8_lossy(&out.stdout)))
}
fn parse_iw_scan(output: &str) -> Result<Vec<WifiNetwork>> {
fn parse_iw_scan(output: &str) -> Vec<WifiNetwork> {
let mut networks: Vec<WifiNetwork> = Vec::new();
let mut current_ssid: Option<String> = None;
let mut current_signal: i32 = -100;
@@ -777,7 +795,7 @@ fn parse_iw_scan(output: &str) -> Result<Vec<WifiNetwork>> {
}
networks.sort_by(|a, b| b.signal_dbm.cmp(&a.signal_dbm));
Ok(networks)
networks
}
fn push_or_update(networks: &mut Vec<WifiNetwork>, ssid: String, signal: i32, security: &str) {
@@ -816,7 +834,7 @@ BSS 11:22:33:44:55:66(on wlan1)
\tSSID: OtherNet
\tWPA:\t * Version: 1
";
let networks = parse_iw_scan(output).unwrap();
let networks = parse_iw_scan(output);
assert_eq!(networks.len(), 2);
assert_eq!(networks[0].ssid, "MyNetwork");
assert_eq!(networks[0].signal_dbm, -45);
@@ -838,7 +856,7 @@ BSS 11:22:33:44:55:66(on wlan1)
\tSSID: DupNet
\tRSN:\t * Version: 1
";
let networks = parse_iw_scan(output).unwrap();
let networks = parse_iw_scan(output);
assert_eq!(networks.len(), 1);
assert_eq!(networks[0].ssid, "DupNet");
assert_eq!(networks[0].signal_dbm, -50);
@@ -851,7 +869,7 @@ BSS aa:bb:cc:dd:ee:ff(on wlan1)
\tsignal: -45.00 dBm
\tSSID:
";
let networks = parse_iw_scan(output).unwrap();
let networks = parse_iw_scan(output);
assert_eq!(networks.len(), 0);
}
@@ -862,7 +880,7 @@ BSS aa:bb:cc:dd:ee:ff(on wlan1)
\tsignal: -60.00 dBm
\tSSID: OpenCafe
";
let networks = parse_iw_scan(output).unwrap();
let networks = parse_iw_scan(output);
assert_eq!(networks.len(), 1);
assert_eq!(networks[0].security, "Open");
}
@@ -871,24 +889,35 @@ BSS aa:bb:cc:dd:ee:ff(on wlan1)
async fn test_update_wpa_conf_writes_and_removes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wpa_sta.conf");
let path_str = path.to_str().unwrap();
let mut config = Config::default();
config.wifi_ssid = Some("TestNet".to_string());
config.wifi_password = Some("pass123".to_string());
tokio::fs::write(&path, "").await.unwrap();
let conf = rayhunter::format_wpa_conf(
config.wifi_ssid.as_ref().unwrap(),
config.wifi_password.as_ref().unwrap(),
);
tokio::fs::write(&path, &conf).await.unwrap();
update_wpa_conf_at(&config, path_str).await;
let content = tokio::fs::read_to_string(&path).await.unwrap();
assert!(content.contains("ssid=\"TestNet\""));
assert!(content.contains("psk=\"pass123\""));
tokio::fs::remove_file(&path).await.unwrap();
config.wifi_ssid = None;
config.wifi_password = None;
update_wpa_conf_at(&config, path_str).await;
assert!(!path.exists());
}
#[tokio::test]
async fn test_update_wpa_conf_ssid_without_password_is_noop() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wpa_sta.conf");
let path_str = path.to_str().unwrap();
let mut config = Config::default();
config.wifi_ssid = Some("TestNet".to_string());
config.wifi_password = None;
update_wpa_conf_at(&config, path_str).await;
assert!(!path.exists());
}
}