style: cargo fmt the workspace (release-gate cargo-fmt was red)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-14 15:38:32 -04:00
parent 412251ac9a
commit db4de0ac96
45 changed files with 641 additions and 337 deletions

View File

@ -64,8 +64,9 @@ impl RpcHandler {
.and_then(|p| p.get("trust_level"))
.and_then(|v| v.as_str())
.map(|s| {
TrustLevel::parse(s)
.ok_or_else(|| anyhow::anyhow!("Invalid trust_level: {s} (expected trusted|observer)"))
TrustLevel::parse(s).ok_or_else(|| {
anyhow::anyhow!("Invalid trust_level: {s} (expected trusted|observer)")
})
})
.transpose()?
.unwrap_or(TrustLevel::Trusted);

View File

@ -292,9 +292,7 @@ impl RpcHandler {
let r_hash_hex = inv
.get("r_hash")
.and_then(|v| v.as_str())
.and_then(|b64| {
base64::engine::general_purpose::STANDARD.decode(b64).ok()
})
.and_then(|b64| base64::engine::general_purpose::STANDARD.decode(b64).ok())
.map(hex::encode)
.unwrap_or_default();
transactions.push(serde_json::json!({

View File

@ -56,9 +56,8 @@ impl RpcHandler {
let words =
crate::seed::load_lnd_aezeed_encrypted(&self.config.data_dir, &node_secret).await;
node_secret.zeroize();
let words = words.map_err(|_| {
anyhow::anyhow!("Could not decrypt the saved Lightning seed backup")
})?;
let words = words
.map_err(|_| anyhow::anyhow!("Could not decrypt the saved Lightning seed backup"))?;
let word_count = words.len();
Ok(serde_json::json!({ "words": words, "word_count": word_count }))

View File

@ -478,7 +478,8 @@ impl RpcHandler {
bytes,
};
let payload = message_types::encode_payload(&content)?;
let envelope = TypedEnvelope::new(MeshMessageType::ContentInline, payload).with_seq(seq);
let envelope =
TypedEnvelope::new(MeshMessageType::ContentInline, payload).with_seq(seq);
let wire = envelope.to_wire()?;
if use_resource_transfer {
svc.send_content_resource(
@ -583,8 +584,7 @@ impl RpcHandler {
.map(|d| nodes.iter().any(|n| &n.did == d))
.unwrap_or(false);
let est_seconds =
(size.saturating_add(lora_bytes_per_sec - 1) / lora_bytes_per_sec).max(1);
let est_seconds = (size.saturating_add(lora_bytes_per_sec - 1) / lora_bytes_per_sec).max(1);
let is_reticulum = device_type == crate::mesh::types::DeviceType::Reticulum;
let (tier, reason) = if size <= MESH_AUTO_MAX {
@ -596,7 +596,10 @@ impl RpcHandler {
("auto-mesh", "No Tor path — sending inline over mesh")
}
} else if is_reticulum && size <= RETICULUM_RESOURCE_MAX {
("resource-mesh", "Sending directly over LoRa via a Reticulum resource transfer")
(
"resource-mesh",
"Sending directly over LoRa via a Reticulum resource transfer",
)
} else if size <= TOR_LARGE_WARN {
if has_tor {
("tor-only", "Too large for mesh — Tor only")

View File

@ -214,8 +214,9 @@ pub(super) fn extract_client_ip(parts: &hyper::http::request::Parts) -> IpAddr {
Some(ip) => ip,
// No socket info recorded (shouldn't happen in the server path);
// fall back to the pre-extension behavior.
None => forwarded_client_ip(&parts.headers)
.unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)),
None => {
forwarded_client_ip(&parts.headers).unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
}
}
}
@ -234,10 +235,7 @@ mod client_ip_tests {
use super::*;
use std::net::SocketAddr;
fn parts_with(
peer: Option<&str>,
real_ip: Option<&str>,
) -> hyper::http::request::Parts {
fn parts_with(peer: Option<&str>, real_ip: Option<&str>) -> hyper::http::request::Parts {
let mut builder = hyper::Request::builder().uri("/rpc/v1");
if let Some(ip) = real_ip {
builder = builder.header("x-real-ip", ip);

View File

@ -54,11 +54,11 @@ use hyper::{Request, Response, StatusCode};
use std::sync::Arc;
use tracing::{debug, error};
pub use middleware::PeerAddr;
use middleware::{
derive_csrf_token, extract_client_ip, extract_cookie, sanitize_error_message,
CACHEABLE_METHODS, UNAUTHENTICATED_METHODS,
};
pub use middleware::PeerAddr;
use response::{cookie_header, json_response, ResponseCache, RpcError, RpcRequest, RpcResponse};
/// Default dev password when no user is set up (matches mock-backend).

View File

@ -1,13 +1,12 @@
use super::RpcHandler;
use crate::network::router as net_router;
use anyhow::Result;
use archipelago_openwrt::{
detect,
router::Router,
tollgate::{self, TollGateConfig},
wan,
wifi_scan,
wan, wifi_scan,
};
use crate::network::router as net_router;
/// Default port for the local Cashu mint (nutshell / cashu-mint app).
const LOCAL_MINT_PORT: u16 = 3338;
@ -23,7 +22,9 @@ impl RpcHandler {
) -> Result<serde_json::Value> {
let p = params.unwrap_or_default();
let subnet: [u8; 4] = parse_ipv4(
p.get("subnet").and_then(|v| v.as_str()).unwrap_or("192.168.1.0"),
p.get("subnet")
.and_then(|v| v.as_str())
.unwrap_or("192.168.1.0"),
)?;
let prefix = p.get("prefix").and_then(|v| v.as_u64()).unwrap_or(24) as u8;
let ssh_user = p
@ -59,8 +60,18 @@ impl RpcHandler {
.get("host")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| if saved.configured { Some(saved.address.clone()) } else { None })
.ok_or_else(|| anyhow::anyhow!("No router configured — provide host or call router.configure first"))?;
.or_else(|| {
if saved.configured {
Some(saved.address.clone())
} else {
None
}
})
.ok_or_else(|| {
anyhow::anyhow!(
"No router configured — provide host or call router.configure first"
)
})?;
let ssh_user = p
.get("ssh_user")
@ -92,11 +103,14 @@ impl RpcHandler {
None,
Some(&ssh_user),
Some(&ssh_password),
).await;
)
.await;
}
// System info
let release = router.run_ok("cat /etc/openwrt_release").unwrap_or_default();
let release = router
.run_ok("cat /etc/openwrt_release")
.unwrap_or_default();
let hostname = router
.uci_get("system.@system[0].hostname")
.unwrap_or_else(|_| "unknown".into());
@ -170,8 +184,18 @@ impl RpcHandler {
.get("host")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| if saved.configured { Some(saved.address.clone()) } else { None })
.ok_or_else(|| anyhow::anyhow!("No router configured — provide host or call router.configure first"))?;
.or_else(|| {
if saved.configured {
Some(saved.address.clone())
} else {
None
}
})
.ok_or_else(|| {
anyhow::anyhow!(
"No router configured — provide host or call router.configure first"
)
})?;
let ssh_user = p
.get("ssh_user")
.and_then(|v| v.as_str())
@ -200,10 +224,7 @@ impl RpcHandler {
.get("step_size_ms")
.and_then(|v| v.as_u64())
.unwrap_or(60_000),
min_steps: p
.get("min_steps")
.and_then(|v| v.as_u64())
.unwrap_or(1) as u32,
min_steps: p.get("min_steps").and_then(|v| v.as_u64()).unwrap_or(1) as u32,
enabled: p.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true),
};
@ -229,13 +250,34 @@ impl RpcHandler {
let saved = net_router::load_router_config(&self.config.data_dir).await?;
let p = params.unwrap_or_default();
let host = p.get("host").and_then(|v| v.as_str()).map(|s| s.to_string())
.or_else(|| if saved.configured { Some(saved.address.clone()) } else { None })
.ok_or_else(|| anyhow::anyhow!("No router configured — provide host or call router.configure first"))?;
let ssh_user = p.get("ssh_user").and_then(|v| v.as_str()).map(|s| s.to_string())
.or_else(|| saved.username.clone()).unwrap_or_else(|| "root".to_string());
let ssh_password = p.get("ssh_password").and_then(|v| v.as_str()).map(|s| s.to_string())
.or_else(|| saved.password.clone()).unwrap_or_default();
let host = p
.get("host")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| {
if saved.configured {
Some(saved.address.clone())
} else {
None
}
})
.ok_or_else(|| {
anyhow::anyhow!(
"No router configured — provide host or call router.configure first"
)
})?;
let ssh_user = p
.get("ssh_user")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| saved.username.clone())
.unwrap_or_else(|| "root".to_string());
let ssh_password = p
.get("ssh_password")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| saved.password.clone())
.unwrap_or_default();
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
router.verify_openwrt()?;
@ -243,13 +285,15 @@ impl RpcHandler {
let networks = wifi_scan::scan_networks(&router)?;
let result: Vec<serde_json::Value> = networks
.iter()
.map(|n| serde_json::json!({
"ssid": n.ssid,
"bssid": n.bssid,
"signal": n.signal,
"channel": n.channel,
"encryption": n.encryption,
}))
.map(|n| {
serde_json::json!({
"ssid": n.ssid,
"bssid": n.bssid,
"signal": n.signal,
"channel": n.channel,
"encryption": n.encryption,
})
})
.collect();
Ok(serde_json::json!({ "networks": result }))
@ -265,18 +309,50 @@ impl RpcHandler {
let saved = net_router::load_router_config(&self.config.data_dir).await?;
let p = params.unwrap_or_default();
let host = p.get("host").and_then(|v| v.as_str()).map(|s| s.to_string())
.or_else(|| if saved.configured { Some(saved.address.clone()) } else { None })
.ok_or_else(|| anyhow::anyhow!("No router configured — provide host or call router.configure first"))?;
let ssh_user = p.get("ssh_user").and_then(|v| v.as_str()).map(|s| s.to_string())
.or_else(|| saved.username.clone()).unwrap_or_else(|| "root".to_string());
let ssh_password = p.get("ssh_password").and_then(|v| v.as_str()).map(|s| s.to_string())
.or_else(|| saved.password.clone()).unwrap_or_default();
let host = p
.get("host")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| {
if saved.configured {
Some(saved.address.clone())
} else {
None
}
})
.ok_or_else(|| {
anyhow::anyhow!(
"No router configured — provide host or call router.configure first"
)
})?;
let ssh_user = p
.get("ssh_user")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| saved.username.clone())
.unwrap_or_else(|| "root".to_string());
let ssh_password = p
.get("ssh_password")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| saved.password.clone())
.unwrap_or_default();
let ssid = p.get("ssid").and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required field: ssid"))?.to_string();
let password = p.get("password").and_then(|v| v.as_str()).unwrap_or("").to_string();
let encryption = p.get("encryption").and_then(|v| v.as_str()).unwrap_or("psk2").to_string();
let ssid = p
.get("ssid")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required field: ssid"))?
.to_string();
let password = p
.get("password")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let encryption = p
.get("encryption")
.and_then(|v| v.as_str())
.unwrap_or("psk2")
.to_string();
let dhcp_start = p.get("dhcp_start").and_then(|v| v.as_u64()).unwrap_or(100) as u32;
let dhcp_limit = p.get("dhcp_limit").and_then(|v| v.as_u64()).unwrap_or(150) as u32;
let masq = p.get("masq").and_then(|v| v.as_bool()).unwrap_or(true);
@ -284,7 +360,14 @@ impl RpcHandler {
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
router.verify_openwrt()?;
let config = wan::WispConfig { ssid: ssid.clone(), password, encryption, dhcp_start, dhcp_limit, masq };
let config = wan::WispConfig {
ssid: ssid.clone(),
password,
encryption,
dhcp_start,
dhcp_limit,
masq,
};
wan::configure_wisp(&router, &config)?;
Ok(serde_json::json!({ "ok": true, "host": host, "ssid": ssid }))
@ -325,14 +408,16 @@ fn parse_wifi_interfaces(raw: &str) -> Vec<serde_json::Value> {
let mut ifaces: Vec<serde_json::Value> = sections
.into_iter()
.filter(|(_, f)| f.get("mode").map(|m| m == "ap").unwrap_or(false))
.map(|(name, f)| serde_json::json!({
"section": name,
"ssid": f.get("ssid").cloned().unwrap_or_default(),
"device": f.get("device").cloned().unwrap_or_default(),
"encryption": f.get("encryption").cloned().unwrap_or_else(|| "none".into()),
"network": f.get("network").cloned().unwrap_or_default(),
"disabled": f.get("disabled").map(|v| v == "1").unwrap_or(false),
}))
.map(|(name, f)| {
serde_json::json!({
"section": name,
"ssid": f.get("ssid").cloned().unwrap_or_default(),
"device": f.get("device").cloned().unwrap_or_default(),
"encryption": f.get("encryption").cloned().unwrap_or_else(|| "none".into()),
"network": f.get("network").cloned().unwrap_or_default(),
"disabled": f.get("disabled").map(|v| v == "1").unwrap_or(false),
})
})
.collect();
ifaces.sort_by_key(|v| v["section"].as_str().unwrap_or("").to_string());

View File

@ -378,9 +378,11 @@ where
// Fail fast if any missing dependency has no installed container
// under any name variant — waiting cannot satisfy it.
let some_dep_not_installed = missing
.iter()
.any(|dep| !dep.containers.iter().any(|c| existing.iter().any(|e| e == c)));
let some_dep_not_installed = missing.iter().any(|dep| {
!dep.containers
.iter()
.any(|c| existing.iter().any(|e| e == c))
});
if some_dep_not_installed {
let msg = match check_install_deps(package_id, &running) {
Err(e) => e.to_string(),
@ -879,8 +881,10 @@ mod tests {
}
/// Collects "Waiting for X to start…" labels emitted during the wait.
fn label_sink() -> (Arc<Mutex<Vec<String>>>, impl FnMut(String) -> std::future::Ready<()>)
{
fn label_sink() -> (
Arc<Mutex<Vec<String>>>,
impl FnMut(String) -> std::future::Ready<()>,
) {
let labels = Arc::new(Mutex::new(Vec::new()));
let sink = {
let labels = Arc::clone(&labels);
@ -930,7 +934,8 @@ mod tests {
// so async_lifecycle removes the optimistic Installing entry.
assert!(err.downcast_ref::<DependencyGateError>().is_some());
assert!(
err.to_string().contains("LND requires a running Bitcoin node"),
err.to_string()
.contains("LND requires a running Bitcoin node"),
"unexpected message: {err}"
);
}

View File

@ -63,7 +63,9 @@ impl RpcHandler {
let to_start = if self.orchestrator.is_some() && uses_single_orchestrator_app(package_id) {
vec![orchestrator_app_id(package_id).to_string()]
} else if let Some(members) = orchestrator_stack_members(self.orchestrator.is_some(), package_id) {
} else if let Some(members) =
orchestrator_stack_members(self.orchestrator.is_some(), package_id)
{
members
} else {
ordered_containers_for_start(package_id).await?
@ -170,11 +172,12 @@ impl RpcHandler {
// fallback to a raw `podman stop` that races systemd over the unit
// (immich, gate 2026-07-09).
let to_stop_ids = if !single_orchestrator_app {
orchestrator_stack_members(self.orchestrator.is_some(), package_id)
.map(|mut members| {
orchestrator_stack_members(self.orchestrator.is_some(), package_id).map(
|mut members| {
members.reverse();
members
})
},
)
} else {
None
};
@ -280,7 +283,9 @@ impl RpcHandler {
let companion_app_id = package_id_owned.clone();
let to_restart = if single_orchestrator_app {
vec![orchestrator_app_id(package_id).to_string()]
} else if let Some(members) = orchestrator_stack_members(self.orchestrator.is_some(), package_id) {
} else if let Some(members) =
orchestrator_stack_members(self.orchestrator.is_some(), package_id)
{
// Restart stacks via member APP ids: restarting by live container
// name podman-stops the quadlet container (systemd --rm removes
// it) and the start half then finds no such container — a 5-min
@ -2160,7 +2165,9 @@ mod tests {
assert!(is_missing_container_error(
"Error: no such object: \"mempool\""
));
assert!(is_missing_container_error("Error: no such container mempool"));
assert!(is_missing_container_error(
"Error: no such container mempool"
));
assert!(is_missing_container_error(
"Error: no container with name or id \"x\" found"
));

View File

@ -100,7 +100,8 @@ impl RpcHandler {
}
let location_file = self.config.data_dir.join("server-location.json");
let payload = serde_json::json!({ "lat": lat, "lon": lon, "share_location": share_location });
let payload =
serde_json::json!({ "lat": lat, "lon": lon, "share_location": share_location });
tokio::fs::write(&location_file, serde_json::to_vec(&payload)?)
.await
.context("Failed to write server location")?;
@ -130,7 +131,9 @@ impl RpcHandler {
/// resolves to on the LAN (avahi-daemon advertises `<hostname>.local`).
/// Lets Settings show users where to reach this node over HTTPS for
/// features (mic/camera access) that require a secure context.
pub(in crate::api::rpc) async fn handle_system_get_hostname(&self) -> Result<serde_json::Value> {
pub(in crate::api::rpc) async fn handle_system_get_hostname(
&self,
) -> Result<serde_json::Value> {
let hostname = tokio::fs::read_to_string("/etc/hostname")
.await
.map(|s| s.trim().to_string())
@ -401,7 +404,8 @@ async fn set_system_hostname(hostname: &str) -> Result<()> {
/// top once a node has been renamed away from the install-time default.
async fn regenerate_tls_cert(hostname: &str) -> Result<()> {
let subj = format!("/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN={hostname}");
let san = format!("subjectAltName=DNS:{hostname},DNS:{hostname}.local,DNS:localhost,IP:127.0.0.1");
let san =
format!("subjectAltName=DNS:{hostname},DNS:{hostname}.local,DNS:localhost,IP:127.0.0.1");
let output = tokio::process::Command::new("/usr/bin/sudo")
.args([
"-n",

View File

@ -53,9 +53,7 @@ pub fn stack_member_app_ids(package_id: &str) -> &'static [&'static str] {
// The legacy umbrella id maps to the split stack (the orchestrator's
// umbrella alias handles this too; listing it here keeps the RPC
// layer's fan-out explicit).
"mempool" | "mempool-web" => {
&["archy-mempool-db", "mempool-api", "archy-mempool-web"]
}
"mempool" | "mempool-web" => &["archy-mempool-db", "mempool-api", "archy-mempool-web"],
_ => &[],
}
}

View File

@ -131,7 +131,9 @@ pub async fn ensure_doctor_installed() {
Err(e) => warn!("Bitcoin RPC repair failed (non-fatal): {:#}", e),
}
match run_apps_dir_repair().await {
Ok(true) => info!("Populated /opt/archipelago/apps from installer copy at /etc/archipelago/apps"),
Ok(true) => {
info!("Populated /opt/archipelago/apps from installer copy at /etc/archipelago/apps")
}
Ok(false) => debug!("/opt/archipelago/apps already populated (or no installer copy)"),
Err(e) => warn!("Apps dir repair failed (non-fatal): {:#}", e),
}

View File

@ -400,10 +400,7 @@ pub async fn refresh_catalog(data_dir: &Path) -> anyhow::Result<CatalogRefresh>
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("no catalog mirrors reachable")))
}
async fn fetch_one(
client: &reqwest::Client,
url: &str,
) -> anyhow::Result<(AppCatalog, String)> {
async fn fetch_one(client: &reqwest::Client, url: &str) -> anyhow::Result<(AppCatalog, String)> {
let resp = client.get(url).send().await?;
if !resp.status().is_success() {
anyhow::bail!("HTTP {}", resp.status());
@ -510,7 +507,10 @@ mod tests {
// on the apps HashMap's nondeterministic key order (seen live on .228).
let dir = tempfile::tempdir().unwrap();
let body = r#"{"schema":1,"apps":{"demo":{"version":"1.0.0"}}}"#;
assert!(write_cache(dir.path(), body).unwrap(), "first write is a change");
assert!(
write_cache(dir.path(), body).unwrap(),
"first write is a change"
);
assert!(
!write_cache(dir.path(), body).unwrap(),
"identical rewrite is not a change"

View File

@ -7,12 +7,8 @@
/// Registries images may be pulled from with an explicit host part.
/// (git.tx1138.com was removed 2026-07-10: the host is retired and must
/// never be pulled through again.)
pub const TRUSTED_REGISTRIES: &[&str] = &[
"docker.io",
"ghcr.io",
"localhost",
"146.59.87.168:3000",
];
pub const TRUSTED_REGISTRIES: &[&str] =
&["docker.io", "ghcr.io", "localhost", "146.59.87.168:3000"];
/// Validate a container image reference.
///

View File

@ -727,14 +727,12 @@ pub async fn ensure_btcpay_lnd_connection_secret(secrets_dir: &std::path::Path)
Ok(s) => s,
Err(_) => return Ok(()), // LND not installed/provisioned yet
};
let thumbprint =
cert_sha256_thumbprint(&pem).context("computing LND tls.cert thumbprint")?;
let thumbprint = cert_sha256_thumbprint(&pem).context("computing LND tls.cert thumbprint")?;
let target = secrets_dir.join(BTCPAY_LND_CONNECTION_SECRET);
// Fast path (no sudo): existing secret already pins the current cert.
if let Ok(existing) = fs::read_to_string(&target).await {
if !existing.trim().is_empty()
&& existing.contains(&format!("certthumbprint={thumbprint}"))
if !existing.trim().is_empty() && existing.contains(&format!("certthumbprint={thumbprint}"))
{
return Ok(());
}
@ -782,7 +780,9 @@ mod tests {
conf_path: tmp.path().join("lnd/lnd.conf"),
};
let out = ensure_config(&paths, "secret", "bitcoin-knots").await.unwrap();
let out = ensure_config(&paths, "secret", "bitcoin-knots")
.await
.unwrap();
assert_eq!(out, EnsureOutcome::Written);
let conf = fs::read_to_string(&paths.conf_path).await.unwrap();
assert!(conf.contains("bitcoin.active=true"));
@ -801,11 +801,15 @@ mod tests {
};
assert_eq!(
ensure_config(&paths, "first", "bitcoin-knots").await.unwrap(),
ensure_config(&paths, "first", "bitcoin-knots")
.await
.unwrap(),
EnsureOutcome::Written
);
assert_eq!(
ensure_config(&paths, "second", "bitcoin-knots").await.unwrap(),
ensure_config(&paths, "second", "bitcoin-knots")
.await
.unwrap(),
EnsureOutcome::Written
);
let conf = fs::read_to_string(&paths.conf_path).await.unwrap();
@ -854,7 +858,9 @@ mod tests {
.unwrap();
assert_eq!(
ensure_config(&paths, "repaired", "bitcoin-knots").await.unwrap(),
ensure_config(&paths, "repaired", "bitcoin-knots")
.await
.unwrap(),
EnsureOutcome::Written
);
let conf = fs::read_to_string(&paths.conf_path).await.unwrap();

View File

@ -1468,9 +1468,7 @@ impl ProdContainerOrchestrator {
);
}
}
for (port, a, b) in
host_port_collisions(state.manifests.values().map(|lm| &lm.manifest))
{
for (port, a, b) in host_port_collisions(state.manifests.values().map(|lm| &lm.manifest)) {
tracing::error!(
port,
app_a = %a,
@ -1666,7 +1664,10 @@ impl ProdContainerOrchestrator {
// and left the unit down for minutes (.228 mempool frontend, gate
// 2026-07-09). Skip this cycle; the worker owns the outcome.
if crate::app_ops::lifecycle_op_in_flight(&app_id) {
report.record(&app_id, ReconcileAction::Left("lifecycle-op-in-flight".into()));
report.record(
&app_id,
ReconcileAction::Left("lifecycle-op-in-flight".into()),
);
crate::crash_recovery::pending_boot_start_done(&app_id);
crate::crash_recovery::pending_boot_start_done(&container_name);
continue;
@ -1773,10 +1774,9 @@ impl ProdContainerOrchestrator {
{
let state = self.state.read().await;
for (app, dep) in degraded_running_apps(
&report,
state.manifests.values().map(|lm| &lm.manifest),
) {
for (app, dep) in
degraded_running_apps(&report, state.manifests.values().map(|lm| &lm.manifest))
{
tracing::error!(
app_id = %app,
dependency = %dep,
@ -3184,8 +3184,7 @@ impl ProdContainerOrchestrator {
// `optional` secret_env — btcpay must still start when LND is
// absent or the derivation fails, so log-and-continue.
if let Err(e) =
crate::container::lnd::ensure_btcpay_lnd_connection_secret(&self.secrets_dir)
.await
crate::container::lnd::ensure_btcpay_lnd_connection_secret(&self.secrets_dir).await
{
tracing::warn!(error = %e, "btcpay-lnd-connection secret not generated; btcpay will run without the internal LND node");
}
@ -3260,20 +3259,17 @@ impl ProdContainerOrchestrator {
manifest.app.container.secret_env_refs = Vec::new();
manifest.app.container.secret_env_hash = None;
} else {
let hash =
archipelago_container::manifest::secret_env_content_hash(&secret_bearing);
let hash = archipelago_container::manifest::secret_env_content_hash(&secret_bearing);
let app_id = manifest.app.id.clone();
manifest.app.container.secret_env_refs = secret_bearing
.into_iter()
.map(|(key, value)| archipelago_container::manifest::SecretEnvRef {
secret_name: format!(
"archy-env-{}-{}",
app_id,
key.to_ascii_lowercase()
),
env_key: key,
value,
})
.map(
|(key, value)| archipelago_container::manifest::SecretEnvRef {
secret_name: format!("archy-env-{}-{}", app_id, key.to_ascii_lowercase()),
env_key: key,
value,
},
)
.collect();
manifest.app.container.secret_env_hash = Some(hash.clone());
@ -3281,12 +3277,7 @@ impl ProdContainerOrchestrator {
// the steady-state reconcile free: podman is only consulted when
// the resolved content actually changed (or on first touch after
// boot). Mock runtimes no-op via the trait default.
let cached = self
.env_secret_cache
.lock()
.await
.get(&app_id)
.cloned();
let cached = self.env_secret_cache.lock().await.get(&app_id).cloned();
if cached.as_deref() != Some(hash.as_str()) {
self.runtime
.ensure_env_secrets(&manifest.app.container.secret_env_refs)
@ -3926,7 +3917,10 @@ impl ContainerOrchestrator for ProdContainerOrchestrator {
async fn start(&self, app_id: &str) -> Result<()> {
if let Some(members) = self.mempool_umbrella_members(app_id).await {
tracing::info!(app_id, "starting legacy umbrella id via split-stack members");
tracing::info!(
app_id,
"starting legacy umbrella id via split-stack members"
);
for (i, member) in members.iter().enumerate() {
if i > 0 {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
@ -3969,7 +3963,10 @@ impl ContainerOrchestrator for ProdContainerOrchestrator {
async fn stop(&self, app_id: &str) -> Result<()> {
if let Some(members) = self.mempool_umbrella_members(app_id).await {
tracing::info!(app_id, "stopping legacy umbrella id via split-stack members");
tracing::info!(
app_id,
"stopping legacy umbrella id via split-stack members"
);
for member in members.iter().rev() {
Box::pin(self.stop(member))
.await
@ -4036,7 +4033,10 @@ impl ContainerOrchestrator for ProdContainerOrchestrator {
async fn restart(&self, app_id: &str) -> Result<()> {
if let Some(members) = self.mempool_umbrella_members(app_id).await {
tracing::info!(app_id, "restarting legacy umbrella id via split-stack members");
tracing::info!(
app_id,
"restarting legacy umbrella id via split-stack members"
);
for (i, member) in members.iter().enumerate() {
if i > 0 {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
@ -4239,9 +4239,8 @@ fn command_argv_drifted(
// therefore Config.Cmd) has them as spaces. Normalize both sides so the
// same script never reads as drift over line breaks alone (bitcoin-knots
// and fedimint-gateway on .228, 2026-07-08).
let norm = |v: &[String]| -> Vec<String> {
v.iter().map(|s| s.replace(['\r', '\n'], " ")).collect()
};
let norm =
|v: &[String]| -> Vec<String> { v.iter().map(|s| s.replace(['\r', '\n'], " ")).collect() };
let current_cmd = norm(current_cmd);
let expected_args = norm(expected_args);
let Some(expected_entry) = expected_entry else {
@ -5508,14 +5507,20 @@ app:
("bitcoin-knots", ReconcileAction::Installed),
("lnd", ReconcileAction::NoOp),
]);
assert_eq!(cascade_pairs_for_report(&r, &none), vec![("bitcoin-knots", "lnd")]);
assert_eq!(
cascade_pairs_for_report(&r, &none),
vec![("bitcoin-knots", "lnd")]
);
// Backend merely started from stopped also moves the IP → cascade.
let r = report(vec![
("bitcoin-core", ReconcileAction::Started),
("lnd", ReconcileAction::NoOp),
]);
assert_eq!(cascade_pairs_for_report(&r, &none), vec![("bitcoin-core", "lnd")]);
assert_eq!(
cascade_pairs_for_report(&r, &none),
vec![("bitcoin-core", "lnd")]
);
// Backend untouched → no cascade.
let r = report(vec![
@ -6016,7 +6021,9 @@ app:
let calls = rt.calls();
for name in ["archy-mempool-db", "mempool-api", "archy-mempool-web"] {
assert!(
calls.iter().any(|c| c == &format!("start_container:{name}")),
calls
.iter()
.any(|c| c == &format!("start_container:{name}")),
"{name} not started: {calls:?}"
);
}

View File

@ -1258,7 +1258,10 @@ app:
assert!(u.read_only_root);
assert!(u.no_new_privileges);
assert_eq!(u.cap_add, vec!["NET_BIND_SERVICE"]);
assert_eq!(u.ports, vec![(8332, 8332, "tcp".to_string(), String::new())]);
assert_eq!(
u.ports,
vec![(8332, 8332, "tcp".to_string(), String::new())]
);
assert_eq!(u.environment, vec!["BITCOIN_NETWORK=mainnet"]);
assert_eq!(u.bind_mounts.len(), 1);
assert_eq!(

View File

@ -607,14 +607,9 @@ mod prune_missing_content_tests {
availability: Availability::AllPeers,
added_at: "2026-01-01T00:00:00Z".to_string(),
};
save_catalog(
data_dir,
&ContentCatalog {
items: vec![item],
},
)
.await
.unwrap();
save_catalog(data_dir, &ContentCatalog { items: vec![item] })
.await
.unwrap();
// File was never written to disk under content/files/ or filebrowser/.
let result = serve_content(data_dir, "missing-item", None, None, None, None)

View File

@ -60,8 +60,9 @@ pub fn is_recovery_complete() -> bool {
// the outcome — a container that truly failed goes back to showing its
// real state on the next scan.
static PENDING_BOOT_STARTS: std::sync::LazyLock<std::sync::RwLock<std::collections::HashSet<String>>> =
std::sync::LazyLock::new(|| std::sync::RwLock::new(std::collections::HashSet::new()));
static PENDING_BOOT_STARTS: std::sync::LazyLock<
std::sync::RwLock<std::collections::HashSet<String>>,
> = std::sync::LazyLock::new(|| std::sync::RwLock::new(std::collections::HashSet::new()));
/// Register container/app names an active recovery or reconcile pass
/// intends to start.
@ -182,7 +183,10 @@ pub async fn load_user_uninstalled(data_dir: &Path) -> std::collections::HashSet
}
/// Save the set of user-uninstalled app/container names to disk.
pub async fn save_user_uninstalled(data_dir: &Path, uninstalled: &std::collections::HashSet<String>) {
pub async fn save_user_uninstalled(
data_dir: &Path,
uninstalled: &std::collections::HashSet<String>,
) {
let path = data_dir.join(USER_UNINSTALLED_FILE);
if let Ok(json) = serde_json::to_string_pretty(uninstalled) {
let _ = fs::write(&path, json).await;
@ -244,10 +248,7 @@ pub async fn check_for_crash(data_dir: &Path) -> Result<Option<Vec<RunningContai
// that is not us and whose cmdline looks like the archipelago binary.
if !old_pid.is_empty() {
if let Ok(pid) = old_pid.parse::<u32>() {
if pid != std::process::id()
&& is_process_running(pid)
&& process_is_archipelago(pid)
{
if pid != std::process::id() && is_process_running(pid) && process_is_archipelago(pid) {
warn!(
"Previous process (PID {}) is still running — not a crash, skipping recovery",
pid

View File

@ -360,8 +360,8 @@ mod tests {
None,
TrustLevel::Trusted,
)
.await
.unwrap();
.await
.unwrap();
assert!(code.starts_with("fed1:"));
let parsed = parse_invite(&code).unwrap();
@ -384,8 +384,8 @@ mod tests {
Some(fips),
TrustLevel::Trusted,
)
.await
.unwrap();
.await
.unwrap();
let parsed = parse_invite(&code).unwrap();
assert_eq!(parsed.fips_npub.as_deref(), Some(fips));
}

View File

@ -403,7 +403,21 @@ mod tests {
last_transport_at: None,
},
];
let state = build_local_state(vec![], 0.0, 0, 0, 0, 0, 0, true, None, None, None, &peers, None);
let state = build_local_state(
vec![],
0.0,
0,
0,
0,
0,
0,
true,
None,
None,
None,
&peers,
None,
);
assert_eq!(state.federated_peers.len(), 1);
assert_eq!(state.federated_peers[0].did, "did:key:zTrusted");
assert_eq!(

View File

@ -1486,7 +1486,10 @@ mod tests {
#[tokio::test]
async fn lifecycle_guard_covers_name_app_id_and_legacy_underscores() {
assert!(!lifecycle_op_covers_container("hm-guard-app", "hm-guard-app"));
assert!(!lifecycle_op_covers_container(
"hm-guard-app",
"hm-guard-app"
));
// Held package lock covers the container directly and via stack
// membership; the '_'→'-' probe covers legacy underscore names.

View File

@ -107,8 +107,7 @@ async fn main() -> Result<()> {
// RUST_LOG=archipelago=debug) to get debug logs back when debugging.
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
)
.init();

View File

@ -162,7 +162,10 @@ async fn status_line() -> String {
} else {
format!("electrum {:.0}%", e.progress_pct)
};
format!("Archipelago OS v{}: {btc}, {elec}.", env!("CARGO_PKG_VERSION"))
format!(
"Archipelago OS v{}: {btc}, {elec}.",
env!("CARGO_PKG_VERSION")
)
}
#[cfg(test)]

View File

@ -141,7 +141,9 @@ impl MeshRadioDevice {
anyhow::bail!("Native image send is Reticulum-only")
}
Self::Reticulum(device) => {
device.send_native_image(dest_pubkey_prefix, mime, bytes, caption).await
device
.send_native_image(dest_pubkey_prefix, mime, bytes, caption)
.await
}
}
}
@ -297,7 +299,9 @@ async fn auto_detect_and_open(
info!(path = %path, "Found Reticulum (RNode) device via auto-detect");
return Ok((path.clone(), MeshRadioDevice::Reticulum(dev), info));
}
Err(e) => debug!(path = %path, error = %e, "Reticulum daemon failed to initialize"),
Err(e) => {
debug!(path = %path, error = %e, "Reticulum daemon failed to initialize")
}
},
Err(e) => debug!(path = %path, error = %e, "Not a Reticulum RNode"),
}
@ -323,7 +327,9 @@ async fn auto_detect_and_open(
}
Err(e) => debug!(path = %path, error = %e, "Not a Meshtastic device"),
},
Err(e) => debug!(path = %path, error = %e, "Could not open serial port for Meshtastic"),
Err(e) => {
debug!(path = %path, error = %e, "Could not open serial port for Meshtastic")
}
}
}
}
@ -399,7 +405,9 @@ async fn open_preferred_path(
{
Ok(mut dev) => match dev.initialize().await {
Ok(info) => return Ok((MeshRadioDevice::Reticulum(dev), info)),
Err(e) => debug!(path = %path, error = %e, "Preferred path is not a working Reticulum RNode"),
Err(e) => {
debug!(path = %path, error = %e, "Preferred path is not a working Reticulum RNode")
}
},
Err(e) => debug!(path = %path, error = %e, "Could not open preferred path as Reticulum"),
}
@ -431,26 +439,22 @@ async fn open_reticulum_tcp(
our_x25519_pubkey_hex: &str,
) -> Result<(String, MeshRadioDevice, DeviceInfo)> {
let mut dev = match cfg {
ReticulumTcpConfig::Server { bind } => {
ReticulumLink::open_tcp_server(
bind,
data_dir,
Some(our_ed_pubkey_hex),
Some(our_x25519_pubkey_hex),
)
.await
.context("Could not open Reticulum TCP server interface")?
}
ReticulumTcpConfig::Client { connect } => {
ReticulumLink::open_tcp_client(
connect,
data_dir,
Some(our_ed_pubkey_hex),
Some(our_x25519_pubkey_hex),
)
.await
.context("Could not open Reticulum TCP client interface")?
}
ReticulumTcpConfig::Server { bind } => ReticulumLink::open_tcp_server(
bind,
data_dir,
Some(our_ed_pubkey_hex),
Some(our_x25519_pubkey_hex),
)
.await
.context("Could not open Reticulum TCP server interface")?,
ReticulumTcpConfig::Client { connect } => ReticulumLink::open_tcp_client(
connect,
data_dir,
Some(our_ed_pubkey_hex),
Some(our_x25519_pubkey_hex),
)
.await
.context("Could not open Reticulum TCP client interface")?,
};
let info = dev
.initialize()
@ -790,7 +794,9 @@ fn meshtastic_contact_id(public_key_hex: &str) -> Option<u32> {
fn reticulum_contact_id(public_key_hex: &str) -> Option<u32> {
let bytes = hex::decode(public_key_hex).ok()?;
let hash: [u8; 16] = bytes.try_into().ok()?;
Some(super::super::reticulum::reticulum_contact_id_from_hash(&hash))
Some(super::super::reticulum::reticulum_contact_id_from_hash(
&hash,
))
}
/// Drain any queued messages from the device.
@ -873,12 +879,23 @@ pub(super) async fn run_mesh_session(
"Preferred path {} probe failed: {} — trying auto-detect",
path, e
);
auto_detect_and_open(data_dir, our_ed_pubkey_hex, our_x25519_pubkey_hex, device_kind)
.await?
auto_detect_and_open(
data_dir,
our_ed_pubkey_hex,
our_x25519_pubkey_hex,
device_kind,
)
.await?
}
}
} else {
auto_detect_and_open(data_dir, our_ed_pubkey_hex, our_x25519_pubkey_hex, device_kind).await?
auto_detect_and_open(
data_dir,
our_ed_pubkey_hex,
our_x25519_pubkey_hex,
device_kind,
)
.await?
};
// Update status

View File

@ -414,7 +414,11 @@ impl MeshtasticDevice {
// tx_power defaults to max, which is what we want for a stock mesh.
let mut lora = Vec::new();
encode_varint_field_into(LORA_USE_PRESET_FIELD, 1, &mut lora);
encode_varint_field_into(LORA_MODEM_PRESET_FIELD, LORA_MODEM_PRESET_LONG_FAST, &mut lora);
encode_varint_field_into(
LORA_MODEM_PRESET_FIELD,
LORA_MODEM_PRESET_LONG_FAST,
&mut lora,
);
encode_varint_field_into(LORA_REGION_FIELD, region_code as u64, &mut lora);
encode_varint_field_into(LORA_HOP_LIMIT_FIELD, 3, &mut lora);
encode_varint_field_into(LORA_TX_ENABLED_FIELD, 1, &mut lora);
@ -772,7 +776,9 @@ impl MeshtasticDevice {
if let Err(e) = self.send_to_radio(&encode_want_config()).await {
warn!("Failed to re-request config after radio reboot: {}", e);
} else {
info!("Re-requested Meshtastic config after reboot — packet stream resubscribed");
info!(
"Re-requested Meshtastic config after reboot — packet stream resubscribed"
);
}
}
if let Some(inbound) = inbound {
@ -890,7 +896,10 @@ impl MeshtasticDevice {
if let Some((region, modem_preset)) = parse_config_lora_region(value) {
self.current_region = Some(region);
self.current_modem_preset = Some(modem_preset);
debug!(region, modem_preset, "Meshtastic LoRa region/preset from device config");
debug!(
region,
modem_preset, "Meshtastic LoRa region/preset from device config"
);
}
None
}
@ -1066,7 +1075,10 @@ fn packet_to_inbound_frame(
if packet.portnum == POSITION_APP {
if let Some((lat, lon)) = parse_position_lat_lon(&packet.payload) {
debug!(from = format!("!{:08x}", from), lat, lon, "Meshtastic position update");
debug!(
from = format!("!{:08x}", from),
lat, lon, "Meshtastic position update"
);
contact.lat = Some(lat);
contact.lon = Some(lon);
}
@ -1811,10 +1823,10 @@ mod tests {
encode_fixed32_field(1, 0x1111_2222, &mut packet); // from
encode_len_field(4, &decoded, &mut packet); // decoded
encode_fixed32_field(8, (-7.5f32).to_bits(), &mut packet); // rx_snr
// int32 rx_rssi=-92 dBm, protobuf varint-encodes a negative int32 as
// the 10-byte two's-complement-extended varint; truncating back to
// i32 after decode recovers the original value (see parse_mesh_packet's
// `v as i32` cast — confirmed by this roundtrip, not just asserted).
// int32 rx_rssi=-92 dBm, protobuf varint-encodes a negative int32 as
// the 10-byte two's-complement-extended varint; truncating back to
// i32 after decode recovers the original value (see parse_mesh_packet's
// `v as i32` cast — confirmed by this roundtrip, not just asserted).
encode_varint_field_into(12, (-92i32) as u32 as u64, &mut packet);
let parsed = parse_mesh_packet(&packet).expect("packet should parse");
@ -1901,7 +1913,10 @@ mod tests {
// must still update the contact's signal/position bookkeeping.
let frame =
packet_to_inbound_frame(&packet, Some(0x1111_1111), &mut contacts, &mut peer_pubkeys);
assert!(frame.is_none(), "POSITION_APP must not surface as a chat frame");
assert!(
frame.is_none(),
"POSITION_APP must not surface as a chat frame"
);
let contact = contacts.get(&from).expect("contact should be tracked");
assert_eq!(contact.snr, Some(-6.0));
assert_eq!(contact.rssi, Some(-80));
@ -1930,7 +1945,10 @@ mod tests {
// A `to == BROADCAST_NUM` text is a channel broadcast (3ccc on public
// LongFast), so it routes to the channel thread, carrying its sender.
assert_eq!(frame.code, protocol::RESP_MESHTASTIC_CHANNEL_TEXT);
assert_eq!(frame.data[0], 0, "no channel field set => primary/public (0)");
assert_eq!(
frame.data[0], 0,
"no channel field set => primary/public (0)"
);
assert_eq!(&frame.data[1..7], &[0xcc, 0x3c, 0x00, 0x00, 0x6d, 0x65]);
assert_eq!(&frame.data[7..], b"hello from 3ccc");
assert!(contacts.contains_key(&from));
@ -1954,9 +1972,8 @@ mod tests {
encode_len_field(4, &decoded, &mut packet);
encode_fixed32_field(7, 12_345, &mut packet);
let frame =
packet_to_inbound_frame(&packet, Some(me), &mut contacts, &mut peer_pubkeys)
.expect("directed DM must surface");
let frame = packet_to_inbound_frame(&packet, Some(me), &mut contacts, &mut peer_pubkeys)
.expect("directed DM must surface");
assert_eq!(frame.code, protocol::RESP_CONTACT_MSG_V3);
let (sender_prefix, payload, _snr) =
protocol::parse_contact_msg_v3_raw(&frame.data).unwrap();

View File

@ -213,7 +213,11 @@ pub struct TypedEnvelope {
/// Unix timestamp (seconds since epoch).
pub ts: u32,
/// Optional Ed25519 signature of (t || v || ts_bytes) — for signed messages.
#[serde(default, skip_serializing_if = "Option::is_none", with = "compact_bytes_opt")]
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "compact_bytes_opt"
)]
pub sig: Option<Vec<u8>>,
/// Message sequence number (per-sender, monotonically increasing).
#[serde(default)]
@ -564,7 +568,11 @@ pub struct ContentRefPayload {
pub mime: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "base64_opt_bytes")]
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "base64_opt_bytes"
)]
pub thumb_bytes: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub caption: Option<String>,
@ -860,8 +868,8 @@ mod tests {
use rand::rngs::OsRng;
let key = SigningKey::generate(&mut OsRng);
let envelope = TypedEnvelope::new(MeshMessageType::Alert, b"test".to_vec())
.signed_with_seq(42, &key);
let envelope =
TypedEnvelope::new(MeshMessageType::Alert, b"test".to_vec()).signed_with_seq(42, &key);
assert!(envelope.verify_signature(&key.verifying_key()).unwrap());
// v2 binds seq: replaying the signed envelope under a different
@ -880,8 +888,8 @@ mod tests {
// preimage, no seq) and allocate seq afterwards; verify must still
// accept that.
let key = SigningKey::generate(&mut OsRng);
let envelope = TypedEnvelope::new_signed(MeshMessageType::Alert, b"test".to_vec(), &key)
.with_seq(7);
let envelope =
TypedEnvelope::new_signed(MeshMessageType::Alert, b"test".to_vec(), &key).with_seq(7);
assert!(envelope.verify_signature(&key.verifying_key()).unwrap());
}

View File

@ -2327,7 +2327,10 @@ mod tests {
// Force the dev fallback even if a packaged binary happens to exist
// on this machine's PATH convention — this test wants exactly the
// freshly-built venv daemon under test.
std::env::set_var("ARCHY_RETICULUM_DAEMON_BIN", "/nonexistent-force-dev-fallback");
std::env::set_var(
"ARCHY_RETICULUM_DAEMON_BIN",
"/nonexistent-force-dev-fallback",
);
let data_dir = root.path().join("node");
std::fs::create_dir_all(data_dir.join("identity")).unwrap();

View File

@ -270,7 +270,9 @@ impl ReticulumLink {
our_ed_pubkey_hex: Option<&str>,
our_x25519_pubkey_hex: Option<&str>,
) -> Result<Self> {
probe_rnode(path).await.context("RNode KISS detect failed")?;
probe_rnode(path)
.await
.context("RNode KISS detect failed")?;
Self::spawn(
ReticulumInterface::Serial(path),
data_dir,
@ -342,8 +344,9 @@ impl ReticulumLink {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = tokio::fs::set_permissions(&runtime_dir, std::fs::Permissions::from_mode(0o700))
.await;
let _ =
tokio::fs::set_permissions(&runtime_dir, std::fs::Permissions::from_mode(0o700))
.await;
}
let label = iface.label();
let iface_key = label.replace(['/', ' ', ':', ','], "_");
@ -553,7 +556,11 @@ impl ReticulumLink {
Ok(())
}
pub async fn send_text_msg(&mut self, dest_pubkey_prefix: &[u8; 6], payload: &[u8]) -> Result<()> {
pub async fn send_text_msg(
&mut self,
dest_pubkey_prefix: &[u8; 6],
payload: &[u8],
) -> Result<()> {
let dest_hash = self
.prefix_to_hash
.get(dest_pubkey_prefix)
@ -738,11 +745,9 @@ impl ReticulumLink {
async fn drain_events(&mut self) {
loop {
let mut line = String::new();
let read = tokio::time::timeout(
Duration::from_millis(20),
self.reader.read_line(&mut line),
)
.await;
let read =
tokio::time::timeout(Duration::from_millis(20), self.reader.read_line(&mut line))
.await;
let n = match read {
Ok(Ok(n)) => n,
_ => break, // timeout (no data) or read error — stop draining
@ -840,7 +845,8 @@ impl ReticulumLink {
// to survive a restart — give it a placeholder name (the real
// one, if any, arrives via a later "announce" and overwrites
// this) so its routing entry alone doesn't get lost.
if let std::collections::hash_map::Entry::Vacant(e) = self.peers.entry(source_hash) {
if let std::collections::hash_map::Entry::Vacant(e) = self.peers.entry(source_hash)
{
e.insert(ReticulumPeer {
dest_hash: source_hash,
display_name: format!("Reticulum {}", hex::encode(&source_hash[..4])),
@ -858,13 +864,22 @@ impl ReticulumLink {
// UI (dispatch.rs's existing ContentInline handling, zero new
// frontend code) instead of the plain text bytes below.
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
let caption = ev.get("content").and_then(Value::as_str).filter(|s| !s.trim().is_empty());
let caption = ev
.get("content")
.and_then(Value::as_str)
.filter(|s| !s.trim().is_empty());
if let (Some(fmt), Some(b64)) = (
ev.get("image_format").and_then(Value::as_str),
ev.get("image_b64").and_then(Value::as_str),
) {
if let Ok(bytes) = B64.decode(b64) {
match build_content_inline_frame(&prefix, image_format_to_mime(fmt), None, caption, bytes) {
match build_content_inline_frame(
&prefix,
image_format_to_mime(fmt),
None,
caption,
bytes,
) {
Ok(frame) => {
self.inbound.push_back(frame);
return;
@ -905,7 +920,8 @@ impl ReticulumLink {
},
None => content_str.as_bytes().to_vec(),
};
self.inbound.push_back(build_synthetic_frame(&prefix, &content));
self.inbound
.push_back(build_synthetic_frame(&prefix, &content));
}
Some("resource_recv") => {
let Some(source_hex) = ev.get("source_hash").and_then(Value::as_str) else {
@ -916,7 +932,8 @@ impl ReticulumLink {
};
let prefix: [u8; 6] = source_hash[..6].try_into().unwrap();
self.prefix_to_hash.insert(prefix, source_hash);
if let std::collections::hash_map::Entry::Vacant(e) = self.peers.entry(source_hash) {
if let std::collections::hash_map::Entry::Vacant(e) = self.peers.entry(source_hash)
{
e.insert(ReticulumPeer {
dest_hash: source_hash,
display_name: format!("Reticulum {}", hex::encode(&source_hash[..4])),
@ -939,7 +956,8 @@ impl ReticulumLink {
// Resources are already a binary-safe whole-blob transfer),
// so this is the same payload shape `decode.rs` already
// accepts for a single-frame (non-chunked) typed envelope.
self.inbound.push_back(build_synthetic_frame(&prefix, &data));
self.inbound
.push_back(build_synthetic_frame(&prefix, &data));
}
Some("resource_progress") => {
debug!(
@ -1138,7 +1156,14 @@ mod tests {
#[test]
fn detect_resp_found_in_kiss_stream() {
let stream = [0x10, 0x20, KISS_FEND, KISS_CMD_DETECT, KISS_DETECT_RESP, 0x99];
let stream = [
0x10,
0x20,
KISS_FEND,
KISS_CMD_DETECT,
KISS_DETECT_RESP,
0x99,
];
assert!(contains_detect_resp(&stream));
}
@ -1154,7 +1179,9 @@ mod tests {
async fn probe_rnode_detects_real_hardware() {
let port = std::env::var("ARCHY_RNODE_TEST_PORT")
.expect("set ARCHY_RNODE_TEST_PORT to the RNode's serial path");
probe_rnode(&port).await.expect("KISS detect probe failed against real hardware");
probe_rnode(&port)
.await
.expect("KISS detect probe failed against real hardware");
}
#[test]
@ -1181,7 +1208,8 @@ mod tests {
arch_pubkey_hex: Some("abcdef".to_string()),
}];
let dir = std::env::temp_dir().join(format!("archy-reticulum-peers-test-{}", std::process::id()));
let dir =
std::env::temp_dir().join(format!("archy-reticulum-peers-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("peers.json");
std::fs::write(&path, serde_json::to_vec(&persisted).unwrap()).unwrap();
@ -1207,7 +1235,10 @@ mod tests {
h
};
let id = reticulum_contact_id_from_hash(&hash_high_bit);
assert!(id < 0x8000_0000, "must not collide with federation-synthetic space");
assert!(
id < 0x8000_0000,
"must not collide with federation-synthetic space"
);
assert_ne!(id, 0);
let zero_hash = [0u8; 16];

View File

@ -614,7 +614,9 @@ mod tests {
#[tokio::test]
async fn test_lnd_aezeed_empty_rejected() {
let dir = tempfile::tempdir().unwrap();
assert!(save_lnd_aezeed_encrypted(dir.path(), &[], "x").await.is_err());
assert!(save_lnd_aezeed_encrypted(dir.path(), &[], "x")
.await
.is_err());
}
#[tokio::test]

View File

@ -89,8 +89,10 @@ impl Server {
if let Ok(loc) = serde_json::from_slice::<serde_json::Value>(&bytes) {
data.server_info.lat = loc.get("lat").and_then(|v| v.as_f64());
data.server_info.lon = loc.get("lon").and_then(|v| v.as_f64());
data.server_info.share_location =
loc.get("share_location").and_then(|v| v.as_bool()).unwrap_or(false);
data.server_info.share_location = loc
.get("share_location")
.and_then(|v| v.as_bool())
.unwrap_or(false);
}
}
data.server_info.tor_address = docker_packages::read_tor_address("archipelago").await;
@ -1265,8 +1267,9 @@ fn merge_preserving_transitional(
/// scan is the owner: once podman reports a settled state and the id is no
/// longer queued for a boot start, the fresh state wins immediately instead
/// of being preserved for the transitional-stuck timeout.
static SCANNER_RESTARTING: std::sync::LazyLock<std::sync::Mutex<std::collections::HashSet<String>>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashSet::new()));
static SCANNER_RESTARTING: std::sync::LazyLock<
std::sync::Mutex<std::collections::HashSet<String>>,
> = std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashSet::new()));
fn take_scanner_restarting(id: &str) -> bool {
SCANNER_RESTARTING

View File

@ -67,7 +67,10 @@ pub async fn sweep_once(data_dir: &Path) -> Result<u64> {
match ecash::receive_token(data_dir, token).await {
Ok(amount) => {
received_total += amount;
info!(amount_sats = amount, "swept TollGate ecash into local wallet");
info!(
amount_sats = amount,
"swept TollGate ecash into local wallet"
);
}
Err(e) => {
// The token is still in this log line if this happens — not

View File

@ -355,8 +355,7 @@ fn parse_and_verify_manifest(raw: serde_json::Value) -> Result<(UpdateManifest,
crate::trust::SignatureStatus::Verified { anchored, .. } => anchored,
crate::trust::SignatureStatus::Unsigned => false,
};
let manifest: UpdateManifest =
serde_json::from_value(raw).context("parse update manifest")?;
let manifest: UpdateManifest = serde_json::from_value(raw).context("parse update manifest")?;
Ok((manifest, signed))
}
@ -625,8 +624,8 @@ pub async fn verify_pending_update(data_dir: &Path) {
// .116 during the v1.7.40 rollout recovery.
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
let deadline = std::time::Instant::now()
+ std::time::Duration::from_secs(PENDING_VERIFY_WINDOW_SECS);
let deadline =
std::time::Instant::now() + std::time::Duration::from_secs(PENDING_VERIFY_WINDOW_SECS);
while std::time::Instant::now() < deadline {
attempt += 1;
@ -2087,7 +2086,10 @@ mod tests {
key
}
fn sign_value(key: &ed25519_dalek::SigningKey, mut doc: serde_json::Value) -> serde_json::Value {
fn sign_value(
key: &ed25519_dalek::SigningKey,
mut doc: serde_json::Value,
) -> serde_json::Value {
let (sig, did) = crate::trust::signed_doc::sign_detached(key, &doc).unwrap();
let obj = doc.as_object_mut().unwrap();
obj.insert("signed_by".into(), serde_json::json!(did));

View File

@ -171,19 +171,18 @@ impl CashuToken {
let v4: TokenV4 =
ciborium::from_reader(decoded.as_slice()).context("Invalid CBOR in cashuB token")?;
let proofs = v4
.t
.into_iter()
.flat_map(|entry| {
let keyset_id = hex::encode(&entry.i);
entry.p.into_iter().map(move |p| Proof {
amount: p.a,
id: keyset_id.clone(),
secret: p.s,
c: hex::encode(&p.c),
let proofs =
v4.t.into_iter()
.flat_map(|entry| {
let keyset_id = hex::encode(&entry.i);
entry.p.into_iter().map(move |p| Proof {
amount: p.a,
id: keyset_id.clone(),
secret: p.s,
c: hex::encode(&p.c),
})
})
})
.collect();
.collect();
let token = CashuToken {
token: vec![TokenEntry { mint: v4.m, proofs }],
@ -407,10 +406,8 @@ mod tests {
use ciborium::value::Value;
let keyset_id = vec![0x00u8, 0x9a, 0x1f, 0x29, 0x32, 0x53, 0xe4, 0x1e];
let sig = hex::decode(
"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d94ec4da0e7f6c2b4e24",
)
.unwrap();
let sig = hex::decode("02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d94ec4da0e7f6c2b4e24")
.unwrap();
let proof = Value::Map(vec![
(Value::from("a"), Value::from(8u64)),

View File

@ -58,7 +58,13 @@ pub async fn verify_declared_signature(
sig_ref: &str,
allow_insecure_registry: bool,
) -> Result<()> {
verify_with_key_path(image, sig_ref, &pinned_pubkey_path(), allow_insecure_registry).await
verify_with_key_path(
image,
sig_ref,
&pinned_pubkey_path(),
allow_insecure_registry,
)
.await
}
async fn verify_with_key_path(
@ -192,7 +198,9 @@ mod tests {
)
.await
.unwrap_err();
assert!(err.to_string().contains("pinned cosign public key is missing"));
assert!(err
.to_string()
.contains("pinned cosign public key is missing"));
}
#[tokio::test]

View File

@ -158,9 +158,7 @@ impl ContainerRuntime for PodmanRuntime {
.podman_cli(&["secret", "inspect", &r.secret_name, "--format", &fmt])
.await
{
if out.status.success()
&& String::from_utf8_lossy(&out.stdout).trim() == hash
{
if out.status.success() && String::from_utf8_lossy(&out.stdout).trim() == hash {
continue;
}
}

View File

@ -32,7 +32,11 @@ pub async fn scan_subnet(
}
}
info!("{} hosts with TCP/22 open in /{}", candidates.len(), prefix_len);
info!(
"{} hosts with TCP/22 open in /{}",
candidates.len(),
prefix_len
);
let mut routers = Vec::new();
for ip in candidates {

View File

@ -48,10 +48,17 @@ impl Router {
return Ok(PkgManager::Opkg);
}
if add_out.contains("no such package") || add_out.contains("unable to select") {
info!("[{}] opkg not in apk repos — staying in apk-native mode", self.host);
info!(
"[{}] opkg not in apk repos — staying in apk-native mode",
self.host
);
return Ok(PkgManager::ApkNative);
}
anyhow::bail!("apk add opkg failed (exit {}): {}", add_code, add_out.trim());
anyhow::bail!(
"apk add opkg failed (exit {}): {}",
add_code,
add_out.trim()
);
}
anyhow::bail!(
@ -70,7 +77,10 @@ impl Router {
/// Install a package, skipping if already installed.
pub fn opkg_install(&self, package: &str) -> Result<()> {
// Check if already installed to avoid unnecessary network traffic.
let (_, code) = self.run(&format!("/usr/bin/opkg list-installed | grep -q '^{} '", package))?;
let (_, code) = self.run(&format!(
"/usr/bin/opkg list-installed | grep -q '^{} '",
package
))?;
if code == 0 {
info!("[{}] {} already installed", self.host, package);
return Ok(());

View File

@ -16,8 +16,7 @@ impl Router {
/// Connect to an OpenWrt router via SSH using a private key.
pub fn connect(host: &str, port: u16, user: &str, key_path: &Path) -> Result<Self> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect(&addr)
.with_context(|| format!("TCP connect to {}", addr))?;
let tcp = TcpStream::connect(&addr).with_context(|| format!("TCP connect to {}", addr))?;
let mut session = Session::new().context("create SSH session")?;
session.set_tcp_stream(tcp);
@ -36,8 +35,7 @@ impl Router {
/// Connect using a password (fallback for routers not yet provisioned with a key).
pub fn connect_password(host: &str, port: u16, user: &str, password: &str) -> Result<Self> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect(&addr)
.with_context(|| format!("TCP connect to {}", addr))?;
let tcp = TcpStream::connect(&addr).with_context(|| format!("TCP connect to {}", addr))?;
let mut session = Session::new().context("create SSH session")?;
session.set_tcp_stream(tcp);
@ -58,7 +56,9 @@ impl Router {
debug!("ssh [{}] $ {}", self.host, cmd);
let mut channel = self.session.channel_session().context("open channel")?;
channel.exec(cmd).with_context(|| format!("exec: {}", cmd))?;
channel
.exec(cmd)
.with_context(|| format!("exec: {}", cmd))?;
let mut stdout = String::new();
channel.read_to_string(&mut stdout).context("read stdout")?;
@ -72,7 +72,12 @@ impl Router {
pub fn run_ok(&self, cmd: &str) -> Result<String> {
let (out, code) = self.run(cmd)?;
if code != 0 {
anyhow::bail!("command `{}` exited with code {}: {}", cmd, code, out.trim());
anyhow::bail!(
"command `{}` exited with code {}: {}",
cmd,
code,
out.trim()
);
}
Ok(out)
}

View File

@ -46,8 +46,14 @@ pub fn install_tollgate(router: &Router) -> Result<()> {
)
})?;
info!("[{}] Downloading TollGate for {} from GitHub releases", router.host, arch);
router.run_ok(&format!("wget --no-check-certificate -O /tmp/tollgate.ipk '{}' 2>&1", url))?;
info!(
"[{}] Downloading TollGate for {} from GitHub releases",
router.host, arch
);
router.run_ok(&format!(
"wget --no-check-certificate -O /tmp/tollgate.ipk '{}' 2>&1",
url
))?;
install_ipk(router, "/tmp/tollgate.ipk")
}
@ -56,7 +62,10 @@ pub fn install_tollgate(router: &Router) -> Result<()> {
/// Downloads the .ipk from GitHub releases and extracts it manually using
/// BusyBox `ar` and `tar` (both present on all OpenWrt images).
pub fn install_tollgate_apk_native(router: &Router) -> Result<()> {
info!("[{}] Installing {} (apk-native mode)", router.host, TOLLGATE_PACKAGE);
info!(
"[{}] Installing {} (apk-native mode)",
router.host, TOLLGATE_PACKAGE
);
// Already installed? The service binary is /usr/bin/tollgate-wrt (per its
// init.d script) — TOLLGATE_PACKAGE is only the opkg/apk package name,
@ -80,14 +89,14 @@ pub fn install_tollgate_apk_native(router: &Router) -> Result<()> {
&& a=\"${DISTRIB_ARCH:-${OPENWRT_ARCH:-}}\" \
&& [ -n \"$a\" ] && echo \"$a\" \
|| /usr/bin/apk --print-arch 2>/dev/null \
|| uname -m"
|| uname -m",
)?;
// Normalise: uname -m returns bare "mipsel"/"mips"; map to 24kc variant
// which is the standard for home-router MIPS builds.
let arch = match arch_raw.trim() {
"mipsel" => "mipsel_24kc",
"mips" => "mips_24kc",
other => other,
"mips" => "mips_24kc",
other => other,
};
info!("[{}] detected arch: {:?}", router.host, arch);
if arch.is_empty() {
@ -102,11 +111,15 @@ pub fn install_tollgate_apk_native(router: &Router) -> Result<()> {
)
})?;
info!("[{}] Downloading TollGate for {} from GitHub releases", router.host, arch);
info!(
"[{}] Downloading TollGate for {} from GitHub releases",
router.host, arch
);
// --no-check-certificate: fresh OpenWrt 25.x images ship without a CA bundle;
// GitHub serves releases over HTTPS so wget would otherwise reject the cert.
let (dl_out, dl_code) = router.run(&format!(
"wget --no-check-certificate -O /tmp/tollgate.ipk '{}' 2>&1", url
"wget --no-check-certificate -O /tmp/tollgate.ipk '{}' 2>&1",
url
))?;
if dl_code != 0 {
anyhow::bail!("TollGate download failed: {}", dl_out.trim());
@ -149,9 +162,8 @@ fn install_ipk(router: &Router, ipk_path: &str) -> Result<()> {
let (_, ar_found) = router.run("command -v ar >/dev/null 2>&1")?;
if ar_found != 0 {
info!("[{}] ar not found, installing binutils", router.host);
let (pkg_out, pkg_code) = router.run(
"apk add binutils 2>&1 || opkg install binutils 2>&1"
)?;
let (pkg_out, pkg_code) =
router.run("apk add binutils 2>&1 || opkg install binutils 2>&1")?;
if pkg_code != 0 {
anyhow::bail!(
"TollGate installation failed: ar not available and binutils install failed: {}",
@ -161,9 +173,8 @@ fn install_ipk(router: &Router, ipk_path: &str) -> Result<()> {
}
// Try standard opkg ar format first (ar archive → data.tar.gz inside).
let (ar_out, ar_code) = router.run(&format!(
"cd /tmp/_tg_install && ar x {} 2>&1", ipk_path
))?;
let (ar_out, ar_code) =
router.run(&format!("cd /tmp/_tg_install && ar x {} 2>&1", ipk_path))?;
if ar_code != 0 {
// Fallback: some builds produce the .ipk as a gzip tarball rather than
@ -173,17 +184,21 @@ fn install_ipk(router: &Router, ipk_path: &str) -> Result<()> {
// flat tarball of the real package files with no ipk structure at
// all. Extract to the scratch dir and check which shape it is before
// deciding how to install it.
info!("[{}] ar failed ({}), trying tar -xzf", router.host, ar_out.trim());
info!(
"[{}] ar failed ({}), trying tar -xzf",
router.host,
ar_out.trim()
);
// List contents first — validates format without writing anything.
let (list_out, list_code) = router.run(&format!(
"tar -tzf {} 2>&1 | head -30", ipk_path
))?;
let (list_out, list_code) =
router.run(&format!("tar -tzf {} 2>&1 | head -30", ipk_path))?;
if list_code != 0 {
anyhow::bail!(
"TollGate installation failed: file is not an ar archive or gzip tar.\n\
ar: {}\ntar -t: {}",
ar_out.trim(), list_out.trim()
ar_out.trim(),
list_out.trim()
);
}
info!("[{}] ipk contents:\n{}", router.host, list_out.trim());
@ -206,14 +221,17 @@ fn install_ipk(router: &Router, ipk_path: &str) -> Result<()> {
}
let (cp_out, cp_code) = router.run("cp -a /tmp/_tg_install/. / 2>&1")?;
if cp_code != 0 {
anyhow::bail!("TollGate installation failed: file copy failed: {}", cp_out.trim());
anyhow::bail!(
"TollGate installation failed: file copy failed: {}",
cp_out.trim()
);
}
// No package-manager postinst ran for these files either — see
// the uci-defaults note below.
router.run_ok(
"for f in /etc/uci-defaults/*; do \
[ -f \"$f\" ] && ( cd \"$(dirname \"$f\")\" && . \"$f\" ) && rm -f \"$f\"; \
done; uci commit 2>/dev/null; true"
done; uci commit 2>/dev/null; true",
)?;
router.run_ok(&format!("rm -rf /tmp/_tg_install {}", ipk_path))?;
return Ok(());
@ -223,18 +241,19 @@ fn install_ipk(router: &Router, ipk_path: &str) -> Result<()> {
// Unpack data.tar.gz (the real payload) from either the `ar`-extracted or
// gzip-tar-extracted scratch dir, then run control.tar.gz's postinst.
let (tar_out, tar_code) = router.run(
"tar -xzf /tmp/_tg_install/data.tar.gz -C / 2>&1"
)?;
let (tar_out, tar_code) = router.run("tar -xzf /tmp/_tg_install/data.tar.gz -C / 2>&1")?;
if tar_code != 0 {
anyhow::bail!("TollGate installation failed: data extract failed: {}", tar_out.trim());
anyhow::bail!(
"TollGate installation failed: data extract failed: {}",
tar_out.trim()
);
}
// Run postinst if present (optional — failures are non-fatal).
router.run_ok(
"if tar -xzf /tmp/_tg_install/control.tar.gz -C /tmp/_tg_install 2>/dev/null; then \
chmod +x /tmp/_tg_install/postinst 2>/dev/null; \
/tmp/_tg_install/postinst configure 2>/dev/null || true; \
fi"
fi",
)?;
// `default_postinst` (what most packages' postinst calls, including
// this one) only runs pending /etc/uci-defaults/* scripts for packages
@ -246,7 +265,7 @@ fn install_ipk(router: &Router, ipk_path: &str) -> Result<()> {
router.run_ok(
"for f in /etc/uci-defaults/*; do \
[ -f \"$f\" ] && ( cd \"$(dirname \"$f\")\" && . \"$f\" ) && rm -f \"$f\"; \
done; uci commit 2>/dev/null; true"
done; uci commit 2>/dev/null; true",
)?;
router.run_ok(&format!("rm -rf /tmp/_tg_install {}", ipk_path))?;

View File

@ -84,9 +84,7 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> {
fn restart_services(router: &Router, enabled: bool) -> Result<()> {
if enabled {
router.run_ok("/etc/init.d/tollgate-wrt enable")?;
router.run_ok(
"/etc/init.d/tollgate-wrt restart || /etc/init.d/tollgate-wrt start"
)?;
router.run_ok("/etc/init.d/tollgate-wrt restart || /etc/init.d/tollgate-wrt start")?;
} else {
router.run_ok("/etc/init.d/tollgate-wrt stop || true")?;
router.run_ok("/etc/init.d/tollgate-wrt disable || true")?;
@ -115,7 +113,7 @@ fn restart_services(router: &Router, enabled: bool) -> Result<()> {
echo \"br-tollgate has no kernel-level IPv4 address after cycle $i, retrying\"; \
done; \
ip -4 addr show br-tollgate 2>/dev/null | grep -q 'inet ' || \
{ echo 'br-tollgate never got a kernel-level IPv4 address after 5 cycles'; exit 1; }"
{ echo 'br-tollgate never got a kernel-level IPv4 address after 5 cycles'; exit 1; }",
)?;
Ok(())
}

View File

@ -63,7 +63,7 @@ pub fn install_captive_portal_symlink(router: &Router) -> Result<()> {
fi; \
rm -rf /etc/nodogsplash/htdocs; \
ln -sf /etc/tollgate/tollgate-captive-portal-site /etc/nodogsplash/htdocs; \
fi"
fi",
)?;
Ok(())
}
@ -91,7 +91,10 @@ pub fn configure(router: &Router, cfg: &TollGateConfig) -> Result<()> {
router.uci_set("nodogsplash.main", "nodogsplash")?;
router.uci_set("nodogsplash.main.enabled", "1")?;
router.uci_set("nodogsplash.main.gatewayinterface", "br-tollgate")?;
router.uci_set("nodogsplash.main.gatewayname", &format!("{} Portal", cfg.ssid))?;
router.uci_set(
"nodogsplash.main.gatewayname",
&format!("{} Portal", cfg.ssid),
)?;
router.uci_set("nodogsplash.main.gatewaydomainname", "TollGate.lan")?;
router.uci_set("nodogsplash.main.gatewayport", "2050")?;

View File

@ -27,7 +27,10 @@ pub fn provision_ssid(router: &Router, cfg: &TollGateConfig) -> Result<()> {
("wireless.tollgate.ieee80211r", "0"),
// Stop broadcasting entirely when disabled, rather than leaving an
// open SSID up that leads nowhere once the backend is stopped.
("wireless.tollgate.disabled", if cfg.enabled { "0" } else { "1" }),
(
"wireless.tollgate.disabled",
if cfg.enabled { "0" } else { "1" },
),
],
)?;
@ -117,13 +120,9 @@ fn provision_firewall(router: &Router) -> Result<()> {
/// Return the first available wireless radio device name (e.g. "radio0").
fn detect_radio(router: &Router) -> Result<String> {
let out = router.run_ok("uci show wireless | grep -o 'wireless\\.radio[0-9]*\\.type' | head -1")?;
let out =
router.run_ok("uci show wireless | grep -o 'wireless\\.radio[0-9]*\\.type' | head -1")?;
// Extract "radioN" from "wireless.radioN.type"
let radio = out
.trim()
.split('.')
.nth(1)
.unwrap_or("radio0")
.to_string();
let radio = out.trim().split('.').nth(1).unwrap_or("radio0").to_string();
Ok(radio)
}

View File

@ -1,14 +1,14 @@
use crate::Router;
use anyhow::Result;
use tracing::info;
use crate::Router;
pub struct WispConfig {
pub ssid: String,
pub password: String,
pub encryption: String, // psk2 | psk | sae | none
pub dhcp_start: u32, // first address in DHCP pool (default 100 → .100)
pub dhcp_limit: u32, // pool size (default 150 → .100.249)
pub masq: bool, // enable NAT on WAN zone (almost always true)
pub encryption: String, // psk2 | psk | sae | none
pub dhcp_start: u32, // first address in DHCP pool (default 100 → .100)
pub dhcp_limit: u32, // pool size (default 150 → .100.249)
pub masq: bool, // enable NAT on WAN zone (almost always true)
}
pub fn configure_wisp(router: &Router, config: &WispConfig) -> Result<()> {
@ -54,11 +54,21 @@ pub fn configure_wisp(router: &Router, config: &WispConfig) -> Result<()> {
// "wifi reload" is not enough on some drivers — it keeps stale state.
let (down_out, down_code) = router.run("wifi down 2>&1")?;
if down_code != 0 {
info!("[{}] wifi down failed ({}): {}", router.host, down_code, down_out.trim());
info!(
"[{}] wifi down failed ({}): {}",
router.host,
down_code,
down_out.trim()
);
}
let (up_out, up_code) = router.run("wifi up 2>&1")?;
if up_code != 0 {
info!("[{}] wifi up failed ({}): {} — falling back to network restart", router.host, up_code, up_out.trim());
info!(
"[{}] wifi up failed ({}): {} — falling back to network restart",
router.host,
up_code,
up_out.trim()
);
router.run_ok("/etc/init.d/network restart 2>&1")?;
}
@ -72,7 +82,9 @@ pub fn get_wan_status(router: &Router) -> serde_json::Value {
.unwrap_or(false);
let ssid = router.uci_get("wireless.wwan.ssid").unwrap_or_default();
let encryption = router.uci_get("wireless.wwan.encryption").unwrap_or_default();
let encryption = router
.uci_get("wireless.wwan.encryption")
.unwrap_or_default();
let radio0_disabled = router
.uci_get("wireless.radio0.disabled")
.map(|v| v == "1")
@ -85,7 +97,10 @@ pub fn get_wan_status(router: &Router) -> serde_json::Value {
// Interface operstate (up / down / absent)
let sta_state = if !sta_iface.is_empty() {
router
.run_ok(&format!("cat /sys/class/net/{}/operstate 2>/dev/null", sta_iface))
.run_ok(&format!(
"cat /sys/class/net/{}/operstate 2>/dev/null",
sta_iface
))
.unwrap_or_else(|_| "unknown".into())
.trim()
.to_string()
@ -108,10 +123,18 @@ pub fn get_wan_status(router: &Router) -> serde_json::Value {
.to_string();
// LAN info for the DHCP setup display
let lan_ip = router.uci_get("network.lan.ipaddr").unwrap_or_else(|_| "192.168.1.1".into());
let lan_netmask = router.uci_get("network.lan.netmask").unwrap_or_else(|_| "255.255.255.0".into());
let dhcp_start = router.uci_get("dhcp.lan.start").unwrap_or_else(|_| "100".into());
let dhcp_limit = router.uci_get("dhcp.lan.limit").unwrap_or_else(|_| "150".into());
let lan_ip = router
.uci_get("network.lan.ipaddr")
.unwrap_or_else(|_| "192.168.1.1".into());
let lan_netmask = router
.uci_get("network.lan.netmask")
.unwrap_or_else(|_| "255.255.255.0".into());
let dhcp_start = router
.uci_get("dhcp.lan.start")
.unwrap_or_else(|_| "100".into());
let dhcp_limit = router
.uci_get("dhcp.lan.limit")
.unwrap_or_else(|_| "150".into());
// Masquerade: check WAN zone
let masq = {
@ -126,7 +149,11 @@ pub fn get_wan_status(router: &Router) -> serde_json::Value {
info!("[{}] WAN status: configured={} ssid={:?} assoc={:?} sta_iface={:?} sta_state={:?} ip={:?} lan={} masq={}",
router.host, configured, ssid, assoc_ssid, sta_iface, sta_state, ip, lan_ip, masq);
if !wifi_log.is_empty() {
info!("[{}] wifi_log: {}", router.host, wifi_log.replace('\n', " | "));
info!(
"[{}] wifi_log: {}",
router.host,
wifi_log.replace('\n', " | ")
);
}
serde_json::json!({

View File

@ -1,5 +1,5 @@
use anyhow::Result;
use crate::Router;
use anyhow::Result;
pub struct ScannedNetwork {
pub ssid: String,
@ -42,7 +42,12 @@ fn scan_via_mtk_site_survey(router: &Router, iface: &str) -> Result<Vec<ScannedN
fn parse_mtk_site_survey(output: &str) -> Result<Vec<ScannedNetwork>> {
let mut networks = Vec::new();
for line in output.lines() {
if !line.trim_start().as_bytes().first().is_some_and(u8::is_ascii_digit) {
if !line
.trim_start()
.as_bytes()
.first()
.is_some_and(u8::is_ascii_digit)
{
continue; // skip header/summary lines; data rows start with an index
}
let ssid = line.get(8..41).unwrap_or("").trim().to_string();
@ -51,8 +56,14 @@ fn parse_mtk_site_survey(output: &str) -> Result<Vec<ScannedNetwork>> {
}
let bssid = line.get(41..61).unwrap_or("").trim().to_string();
let security = line.get(61..84).unwrap_or("");
let channel: u8 = line.get(4..8).and_then(|s| s.trim().parse().ok()).unwrap_or(0);
let signal: i32 = line.get(84..92).and_then(|s| s.trim().parse().ok()).unwrap_or(-100);
let channel: u8 = line
.get(4..8)
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
let signal: i32 = line
.get(84..92)
.and_then(|s| s.trim().parse().ok())
.unwrap_or(-100);
networks.push(ScannedNetwork {
ssid,
bssid,
@ -96,7 +107,11 @@ fn find_wireless_iface(router: &Router) -> Result<(String, bool)> {
// Create a temporary managed interface directly on the PHY. This bypasses
// netifd entirely so it works even when there are no wifi-iface sections in
// UCI (common on a freshly-flashed device).
tracing::info!("[{}] Creating temporary scan interface on {}", router.host, phy);
tracing::info!(
"[{}] Creating temporary scan interface on {}",
router.host,
phy
);
// Remove any stale scan0 from a previous attempt, then add fresh
let _ = router.run("iw dev scan0 del 2>/dev/null");
router.run_ok(&format!(
@ -119,7 +134,12 @@ fn parse_iwinfo_scan(output: &str) -> Result<Vec<ScannedNetwork>> {
networks.push(n);
}
}
let bssid = line.split("Address:").nth(1).unwrap_or("").trim().to_string();
let bssid = line
.split("Address:")
.nth(1)
.unwrap_or("")
.trim()
.to_string();
current = Some(ScannedNetwork {
ssid: String::new(),
bssid,
@ -132,7 +152,10 @@ fn parse_iwinfo_scan(output: &str) -> Result<Vec<ScannedNetwork>> {
n.ssid = rest.trim().trim_matches('"').to_string();
} else if line.contains("Channel:") && !line.starts_with("Encryption") {
if let Some(ch_part) = line.split("Channel:").nth(1) {
n.channel = ch_part.trim().split_whitespace().next()
n.channel = ch_part
.trim()
.split_whitespace()
.next()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
}