fix(vpn,reconcile): restore WG peers on boot + filebrowser spec drift

Follow-up to 8b7cb002 (no version bump — same v1.7.0-alpha manifest):

* WireGuard peer persistence. Kernel peer state is ephemeral; the add-peer
  RPC wrote each peer to data_dir/nostr-vpn/peers/*.json but nothing
  re-pushed them on reboot. Result on .198: wg0 came up listening with zero
  peers after last night's reboot. Added vpn::restore_wg_peers() — reads
  the peers dir, waits up to 30s for wg0 to exist, then replays each via
  `archipelago-wg add-peer`. Spawned from main.rs alongside the other
  startup tasks.
* Reconcile + filebrowser drift. scripts/container-specs.sh load_spec_
  filebrowser now declares SPEC_NETWORK="archy-net" (to match what
  first-boot-containers.sh creates) and pins the filebrowser-data volume
  + wget-style healthcheck so the reconciler stops reporting network
  drift. Without this, reconcile would kill the healthy first-boot
  filebrowser container and recreate it on bridge, breaking the archy-net
  DNS name the backend proxies to.

Manifest binary sha/size refreshed:
  6c178a76…3582cc, 40361912 bytes.
Rebuilt ISO at image-recipe/results/archipelago-installer-unbundled-x86_64.iso
(Apr 20 07:10) carries both fixes baked in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-20 07:10:49 -04:00
parent 6a4d48b49f
commit 0399f45fb2
3 changed files with 94 additions and 2 deletions

View File

@@ -176,6 +176,14 @@ async fn main() -> Result<()> {
// Spawn disk space monitor (warns at 85%, auto-cleans at 90%)
disk_monitor::spawn_disk_monitor(config.data_dir.clone());
// Restore WireGuard peers into wg0 (kernel loses them on every reboot).
{
let data_dir = config.data_dir.clone();
tokio::spawn(async move {
vpn::restore_wg_peers(&data_dir).await;
});
}
// Spawn ElectrumX status cache (refreshes every 15s, serves cached data to avoid race conditions)
electrs_status::spawn_status_cache();

View File

@@ -708,6 +708,89 @@ pub async fn configure_wireguard(
Ok(wg_config)
}
/// Restore WireGuard peers from `data_dir/nostr-vpn/peers/*.json` into the
/// kernel after a reboot.
///
/// Kernel peer state is ephemeral. The add-peer RPC persists each peer to
/// a JSON file but only pushes it into wg0 at creation time — without this
/// restore step, every reboot drops all peers and the user has to re-add
/// them via QR.
pub async fn restore_wg_peers(data_dir: &Path) {
let peers_dir = data_dir.join("nostr-vpn/peers");
let mut entries = match fs::read_dir(&peers_dir).await {
Ok(e) => e,
Err(_) => return,
};
// archipelago-wg.service may race us on boot; wait up to 30s for wg0.
for _ in 0..30 {
let up = tokio::process::Command::new("ip")
.args(["link", "show", "wg0"])
.output()
.await
.map(|o| o.status.success())
.unwrap_or(false);
if up {
break;
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
let mut restored = 0usize;
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
let content = match fs::read_to_string(&path).await {
Ok(c) => c,
Err(e) => {
tracing::warn!("VPN restore: read {:?} failed: {}", path, e);
continue;
}
};
let v: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(e) => {
tracing::warn!("VPN restore: parse {:?} failed: {}", path, e);
continue;
}
};
let pubkey = v.get("public_key").and_then(|s| s.as_str());
let ip = v.get("ip").and_then(|s| s.as_str());
let (pubkey, ip) = match (pubkey, ip) {
(Some(p), Some(i)) => (p, i),
_ => {
tracing::warn!("VPN restore: {:?} missing public_key or ip", path);
continue;
}
};
let out = tokio::process::Command::new("sudo")
.args(["archipelago-wg", "add-peer", pubkey, ip])
.output()
.await;
match out {
Ok(o) if o.status.success() => restored += 1,
Ok(o) => tracing::warn!(
"VPN restore: add-peer failed for {}: {}",
pubkey,
String::from_utf8_lossy(&o.stderr).trim()
),
Err(e) => tracing::warn!("VPN restore: add-peer spawn failed: {}", e),
}
}
if restored > 0 {
tracing::info!(
"🔐 VPN: restored {} WireGuard peer(s) from {}",
restored,
peers_dir.display()
);
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -453,10 +453,11 @@ load_spec_filebrowser() {
reset_spec
SPEC_NAME="filebrowser"
SPEC_IMAGE="${FILEBROWSER_IMAGE}"
SPEC_NETWORK="archy-net"
SPEC_PORTS="8083:80"
SPEC_VOLUMES="/var/lib/archipelago/filebrowser:/srv"
SPEC_VOLUMES="/var/lib/archipelago/filebrowser:/srv /var/lib/archipelago/filebrowser-data:/data"
SPEC_MEMORY="$(mem_limit filebrowser)"
SPEC_HEALTH_CMD="curl -sf http://localhost:80/ || exit 1"
SPEC_HEALTH_CMD="wget -q --spider http://localhost:80/health || exit 1"
SPEC_TIER="3"
SPEC_DATA_DIR="/var/lib/archipelago/filebrowser"
SPEC_CAPS=""