fix(wifi,fips): self-heal polkit rule + fast-retry anchors after daemon restart
Two fleet fixes that OTA can deliver (unlike the ISO-only polkit rule): - WiFi (issue #99): nodes drive NetworkManager from a system-level service with no logind seat, so the stock polkit rule denies them and Wi-Fi setup fails with 'Insufficient privileges'. Fresh ISOs since 2026-05 ship the scoped rule, but OTA'd nodes never got it (OTA replaces binary + web UI, not host system config). Add a startup self-heal that installs the rule if missing and best-effort ensures polkitd — both wrapped so an offline/locked apt can never fail startup; the rule is written regardless so it activates once polkitd lands. Idempotent on the rule's unique subject.user marker. - FIPS: regenerating fips.yaml (this build does, once, on first boot after the OTA) restarts the fips daemon; for a few seconds /run/fips/control.sock is gone and every seed-anchor dial fails, islanding the node until the next 5-min tick. Detect that exact failure (all dials fail on control.sock) and retry in 15s instead — bounded to 8 fast retries/episode so a node with no fips daemon falls back to the steady cadence rather than busy-looping. FIPS unit suite: 33/33 pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6a848c38cc
commit
cb301b1aef
@ -137,6 +137,13 @@ pub async fn ensure_doctor_installed() {
|
||||
Ok(false) => debug!("/opt/archipelago/apps already populated (or no installer copy)"),
|
||||
Err(e) => warn!("Apps dir repair failed (non-fatal): {:#}", e),
|
||||
}
|
||||
match run_polkit_networkmanager_repair().await {
|
||||
Ok(true) => info!(
|
||||
"Installed NetworkManager polkit rule for the archipelago user — Wi-Fi setup enabled"
|
||||
),
|
||||
Ok(false) => debug!("NetworkManager polkit rule already present"),
|
||||
Err(e) => warn!("polkit NetworkManager repair failed (non-fatal): {:#}", e),
|
||||
}
|
||||
match run_journald_dropin().await {
|
||||
Ok(true) => info!("Installed journald log-volume policy drop-in"),
|
||||
Ok(false) => debug!("journald log-volume policy already in place"),
|
||||
@ -442,6 +449,68 @@ exit 2
|
||||
}
|
||||
}
|
||||
|
||||
/// Self-heal Wi-Fi setup on nodes that predate the polkit fix (issue #99).
|
||||
///
|
||||
/// Archipelago drives NetworkManager from a system-level systemd service
|
||||
/// (`User=archipelago`, no logind seat), so the stock NM polkit rule — which
|
||||
/// only authorizes `subject.local && subject.active` sessions — denies it, and
|
||||
/// "connect to Wi-Fi" fails with "Insufficient privileges". Fresh ISO installs
|
||||
/// since 2026-05 ship the rule below, but nodes that reached this build over
|
||||
/// OTA never got it (OTA replaces the binary + web UI, not host system config).
|
||||
///
|
||||
/// Install the scoped rule if it is missing, and best-effort ensure `polkitd`
|
||||
/// itself is present (without the daemon the rule is inert). Both are wrapped
|
||||
/// so an offline/locked apt or a missing package can never fail startup — the
|
||||
/// rule is still written so it takes effect once polkitd arrives (e.g. after an
|
||||
/// ISO reflash). Idempotent: keyed on the rule's unique `subject.user` marker.
|
||||
async fn run_polkit_networkmanager_repair() -> Result<bool> {
|
||||
let script = r#"
|
||||
set -u
|
||||
RULE=/etc/polkit-1/rules.d/49-archipelago-networkmanager.rules
|
||||
MARKER='subject.user == "archipelago"'
|
||||
# Rule already installed — nothing to do.
|
||||
if grep -qF "$MARKER" "$RULE" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
# The rule is inert without the polkit daemon. Older nodes (the ones that hit
|
||||
# issue #99) shipped without it. Try to install it, but never let apt failure
|
||||
# (offline node, locked dpkg, package unavailable) abort the heal — the rule is
|
||||
# written regardless so it activates whenever polkitd lands.
|
||||
if [ ! -d /usr/share/polkit-1 ] && ! command -v pkaction >/dev/null 2>&1; then
|
||||
timeout 240 apt-get install -y --no-install-recommends polkitd >/dev/null 2>&1 \
|
||||
|| timeout 240 sh -c 'apt-get update >/dev/null 2>&1 && apt-get install -y --no-install-recommends polkitd >/dev/null 2>&1' \
|
||||
|| true
|
||||
fi
|
||||
mkdir -p /etc/polkit-1/rules.d
|
||||
cat > "$RULE" <<'RULEEOF'
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (subject.user == "archipelago" && action.id.indexOf("org.freedesktop.NetworkManager.") == 0) {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
RULEEOF
|
||||
chmod 644 "$RULE"
|
||||
# Pick up the new rule. polkitd re-reads rules.d on reload; restart as a
|
||||
# fallback. Non-fatal if the unit name differs or the daemon is absent.
|
||||
systemctl reload polkit 2>/dev/null \
|
||||
|| systemctl restart polkit 2>/dev/null \
|
||||
|| systemctl restart polkit.service 2>/dev/null \
|
||||
|| true
|
||||
exit 2
|
||||
"#;
|
||||
let status = host_sudo(&["sh", "-lc", script])
|
||||
.await
|
||||
.context("install NetworkManager polkit rule")?;
|
||||
match status.code() {
|
||||
Some(0) => Ok(false),
|
||||
Some(2) => Ok(true),
|
||||
_ => {
|
||||
warn!("polkit NetworkManager repair helper exited with {}", status);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_bitcoin_rpc_repair() -> Result<bool> {
|
||||
// Older installs can have a container-owned bitcoin.conf with only rpcauth
|
||||
// and printtoconsole. Repair it at startup so OTA fixes existing nodes
|
||||
|
||||
@ -688,12 +688,27 @@ impl Server {
|
||||
let fips_peer_registry = fips_peer_registry.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(300));
|
||||
// Steady cadence, but retry fast right after a daemon restart:
|
||||
// regenerating fips.yaml (this build does, once, on first boot
|
||||
// after the OTA) restarts the fips daemon, and for a few seconds
|
||||
// `/run/fips/control.sock` is gone so every `fipsctl connect`
|
||||
// fails and the node islands until the next tick. Detect that
|
||||
// exact failure and retry in 15s instead of 5 min — bounded, so a
|
||||
// node with no fips daemon falls back to the steady cadence
|
||||
// rather than busy-looping.
|
||||
const STEADY: Duration = Duration::from_secs(300);
|
||||
const FAST: Duration = Duration::from_secs(15);
|
||||
const MAX_FAST_RETRIES: u32 = 8; // ≤2 min of fast retries/episode
|
||||
let mut fast_retries: u32 = 0;
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let mut daemon_restarting = false;
|
||||
match crate::fips::anchors::load(&data_dir).await {
|
||||
Ok(list) if !list.is_empty() => {
|
||||
let _ = crate::fips::anchors::apply(&list).await;
|
||||
let results = crate::fips::anchors::apply(&list).await;
|
||||
daemon_restarting = !results.is_empty()
|
||||
&& results
|
||||
.iter()
|
||||
.all(|r| !r.ok && r.message.contains("control.sock"));
|
||||
}
|
||||
Ok(_) => { /* no seed anchors configured yet */ }
|
||||
Err(e) => {
|
||||
@ -717,6 +732,15 @@ impl Server {
|
||||
let _ = crate::fips::anchors::apply(&direct).await;
|
||||
}
|
||||
}
|
||||
|
||||
let next = if daemon_restarting && fast_retries < MAX_FAST_RETRIES {
|
||||
fast_retries += 1;
|
||||
FAST
|
||||
} else {
|
||||
fast_retries = 0;
|
||||
STEADY
|
||||
};
|
||||
tokio::time::sleep(next).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user