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:
parent
412251ac9a
commit
db4de0ac96
@ -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);
|
||||
|
||||
@ -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!({
|
||||
|
||||
@ -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 }))
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
));
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@ -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!(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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!(
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(());
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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))?;
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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")?;
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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!({
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user