release(v1.7.35-alpha): rootless-netns self-heal + app update button + bitcoin-core 28.4 + Node DID unification
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
- core/archipelago/src/bootstrap.rs (NEW): embed scripts/container-doctor.sh
and image-recipe/configs/archipelago-doctor.{service,timer} via
include_str! and sync to disk + enable the timer on every archipelago
startup. Idempotent (content-hash compare), dev-box symlink guard keeps
the git checkout untouched, best-effort (warn-only on failure) so
bootstrap never blocks server readiness. Wired in main.rs as a
background tokio task.
- scripts/container-doctor.sh: add fix_rootless_netns_egress(). Detects
when the rootless-netns has lost its pasta tap (container-to-container
still works but outbound DNS/TCP fails) via an nsenter probe into
aardvark-dns; with a two-probe 10s debounce to rule out transients and
a host-precheck that bails out if the host itself is offline. When the
rootless-netns is truly broken, does a graceful podman stop --all /
start --all so pasta + aardvark-dns rebuild the netns from scratch.
Bitcoin-knots and every other outbound container recover in one cycle.
- core/archipelago/src/update.rs: host_sudo → pub(crate) so bootstrap.rs
can reuse the existing systemd-run escape hatch.
- apps/bitcoin-core/manifest.yml: bump app version 24.0.0 → 28.4.0 and
image bitcoin/bitcoin:24.0 → bitcoin/bitcoin:28.4. Resources aligned
with the real container-specs.sh large-disk tune (4 GiB memory cap,
cpu_limit: 0 so bitcoind can run -par=auto across every core).
- neode-ui/src/views/apps/AppCard.vue + Apps.vue: add an Update button
+ Updating spinner to every app card that has available-update set.
Wires through serverStore.updatePackage(id) — the same RPC the detail
view already calls. common.update / common.updating i18n keys added in
en.json and es.json.
- core/archipelago/src/identity_manager.rs: add create_from_signing_key()
that mirrors an existing Ed25519 key as a manager-level identity with
a deterministic id (`node-<pubkey16>`). Idempotent across restarts,
gets the hex-SVG master avatar.
- core/archipelago/src/server.rs: the auto-create path on first boot now
mirrors the node's own signing_key (seed-derived on onboarded installs)
as a "Node" identity instead of generating a random "Default" keypair.
Once this ships, the DID on the Web5 DID Status card (via node.did
RPC), the Node entry on the Identities page (via identity.list), and
the DID used for peer-to-peer connects (via server_info.pubkey) all
resolve to the same seed-derived pubkey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
app:
|
||||
id: bitcoin-core
|
||||
name: Bitcoin Core
|
||||
version: 24.0.0
|
||||
version: 28.4.0
|
||||
description: Full Bitcoin node implementation. The reference implementation of the Bitcoin protocol.
|
||||
|
||||
|
||||
container:
|
||||
image: bitcoin/bitcoin:24.0
|
||||
image: bitcoin/bitcoin:28.4
|
||||
image_signature: cosign://...
|
||||
pull_policy: verify-signature
|
||||
|
||||
@@ -13,8 +13,8 @@ app:
|
||||
- storage: 500Gi # Minimum disk space for mainnet
|
||||
|
||||
resources:
|
||||
cpu_limit: 2
|
||||
memory_limit: 2Gi
|
||||
cpu_limit: 0 # 0 = unlimited; bitcoind uses -par=auto across all cores
|
||||
memory_limit: 4Gi # matches container-specs.sh bitcoin-knots large-disk dbcache=4096
|
||||
disk_limit: 500Gi
|
||||
|
||||
security:
|
||||
|
||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.34-alpha"
|
||||
version = "1.7.35-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.34-alpha"
|
||||
version = "1.7.35-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
152
core/archipelago/src/bootstrap.rs
Normal file
152
core/archipelago/src/bootstrap.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
//! Bootstrap host-side doctor artifacts on every archipelago startup.
|
||||
//!
|
||||
//! The update pipeline swaps the archipelago binary but does not touch
|
||||
//! scripts or systemd units — those are installed once by the ISO builder.
|
||||
//! Without this module, changes to `container-doctor.sh` or the doctor
|
||||
//! service/timer never reach boxes installed before the change.
|
||||
//!
|
||||
//! On startup we compare three embedded files against their on-disk
|
||||
//! copies and rewrite any that differ, then enable the doctor timer if
|
||||
//! it isn't already. Idempotent: no-ops on boxes that match the
|
||||
//! embedded version. All work is best-effort — failures are logged but
|
||||
//! never abort the backend.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
use tokio::fs;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::update::host_sudo;
|
||||
|
||||
const DOCTOR_SH: &str = include_str!("../../../scripts/container-doctor.sh");
|
||||
const DOCTOR_SERVICE: &str =
|
||||
include_str!("../../../image-recipe/configs/archipelago-doctor.service");
|
||||
const DOCTOR_TIMER: &str =
|
||||
include_str!("../../../image-recipe/configs/archipelago-doctor.timer");
|
||||
|
||||
const DOCTOR_SH_PATH: &str = "/home/archipelago/archy/scripts/container-doctor.sh";
|
||||
const DOCTOR_SERVICE_PATH: &str = "/etc/systemd/system/archipelago-doctor.service";
|
||||
const DOCTOR_TIMER_PATH: &str = "/etc/systemd/system/archipelago-doctor.timer";
|
||||
|
||||
/// Entry point called from main startup. Never returns an error to the caller —
|
||||
/// failing to bootstrap the doctor must not prevent the backend from serving.
|
||||
pub async fn ensure_doctor_installed() {
|
||||
match run().await {
|
||||
Ok(changed) if changed => info!("Doctor artifacts synchronized with binary"),
|
||||
Ok(_) => debug!("Doctor artifacts already in sync"),
|
||||
Err(e) => warn!("Doctor bootstrap failed (non-fatal): {:#}", e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run() -> Result<bool> {
|
||||
// Dev-box guard: on contributors' laptops `/home/archipelago/archy` is
|
||||
// typically a symlink into the git checkout, and writing through it
|
||||
// would clobber the working tree with whatever the binary happens to
|
||||
// have been compiled from. Production ISO installs materialize a real
|
||||
// directory.
|
||||
let home_archy = Path::new("/home/archipelago/archy");
|
||||
if fs::symlink_metadata(home_archy)
|
||||
.await
|
||||
.map(|m| m.file_type().is_symlink())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
debug!("/home/archipelago/archy is a symlink — skipping doctor bootstrap (dev box)");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Skip entirely on machines without the canonical scripts directory —
|
||||
// writing orphan files there just causes confusion.
|
||||
let scripts_dir = Path::new(DOCTOR_SH_PATH)
|
||||
.parent()
|
||||
.context("doctor script path has no parent")?;
|
||||
if !scripts_dir.exists() {
|
||||
debug!(
|
||||
"Scripts dir {} missing — skipping doctor bootstrap",
|
||||
scripts_dir.display()
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
// 1. Script — lives in archipelago's home dir, user-writable.
|
||||
if needs_write(DOCTOR_SH_PATH, DOCTOR_SH).await {
|
||||
fs::write(DOCTOR_SH_PATH, DOCTOR_SH)
|
||||
.await
|
||||
.with_context(|| format!("write {}", DOCTOR_SH_PATH))?;
|
||||
let _ = tokio::process::Command::new("chmod")
|
||||
.args(["+x", DOCTOR_SH_PATH])
|
||||
.status()
|
||||
.await;
|
||||
info!("Updated {}", DOCTOR_SH_PATH);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 2. Systemd unit files — /etc is restricted; route through host_sudo.
|
||||
let service_changed = write_root_if_needed(DOCTOR_SERVICE_PATH, DOCTOR_SERVICE).await?;
|
||||
let timer_changed = write_root_if_needed(DOCTOR_TIMER_PATH, DOCTOR_TIMER).await?;
|
||||
changed = changed || service_changed || timer_changed;
|
||||
|
||||
// 3. Reload + enable. Only when we actually touched units, or when the
|
||||
// timer isn't enabled yet (catches fresh upgrades of boxes that predate
|
||||
// the doctor entirely).
|
||||
let timer_enabled = is_timer_enabled().await;
|
||||
if service_changed || timer_changed || !timer_enabled {
|
||||
if let Err(e) = host_sudo(&["systemctl", "daemon-reload"]).await {
|
||||
warn!("daemon-reload failed: {:#}", e);
|
||||
}
|
||||
if let Err(e) = host_sudo(&["systemctl", "enable", "--now", "archipelago-doctor.timer"])
|
||||
.await
|
||||
{
|
||||
warn!("enable archipelago-doctor.timer failed: {:#}", e);
|
||||
} else if !timer_enabled {
|
||||
info!("Enabled archipelago-doctor.timer");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
async fn needs_write(path: &str, expected: &str) -> bool {
|
||||
match fs::read_to_string(path).await {
|
||||
Ok(current) => current != expected,
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write content to a root-owned path via `sudo mv` of a user-owned tmp file.
|
||||
/// Returns true if a write happened.
|
||||
async fn write_root_if_needed(path: &str, content: &str) -> Result<bool> {
|
||||
if !needs_write(path, content).await {
|
||||
return Ok(false);
|
||||
}
|
||||
let tmp = format!(
|
||||
"/tmp/archipelago-bootstrap-{}-{}.tmp",
|
||||
std::process::id(),
|
||||
Path::new(path)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("unit")
|
||||
);
|
||||
fs::write(&tmp, content)
|
||||
.await
|
||||
.with_context(|| format!("write tmp {}", tmp))?;
|
||||
let status = host_sudo(&["mv", &tmp, path])
|
||||
.await
|
||||
.with_context(|| format!("sudo mv {} -> {}", tmp, path))?;
|
||||
if !status.success() {
|
||||
let _ = fs::remove_file(&tmp).await;
|
||||
anyhow::bail!("sudo mv to {} exited with {}", path, status);
|
||||
}
|
||||
info!("Updated {}", path);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn is_timer_enabled() -> bool {
|
||||
tokio::process::Command::new("systemctl")
|
||||
.args(["is-enabled", "--quiet", "archipelago-doctor.timer"])
|
||||
.status()
|
||||
.await
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -216,6 +216,80 @@ impl IdentityManager {
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// Mirror an existing Ed25519 signing key as a manager-level identity.
|
||||
///
|
||||
/// Used at boot to expose the node's own seed-derived key (the one that
|
||||
/// backs `server_info.pubkey` and peer-to-peer connections) as an
|
||||
/// entry in the Identities page, so all three surfaces — DID Status,
|
||||
/// "Node" entry on Identities, and peer-connect DID — resolve to the
|
||||
/// same DID. The id is deterministic (`node-<pubkey16>`), so repeated
|
||||
/// calls on the same key are idempotent: if the file already exists
|
||||
/// we return the existing record untouched.
|
||||
pub async fn create_from_signing_key(
|
||||
&self,
|
||||
name: String,
|
||||
purpose: IdentityPurpose,
|
||||
signing_key: SigningKey,
|
||||
) -> Result<IdentityRecord> {
|
||||
let pubkey_hex = hex::encode(signing_key.verifying_key().as_bytes());
|
||||
let did = did_key_from_pubkey_hex(&pubkey_hex)?;
|
||||
let id = format!("node-{}", &pubkey_hex[..16]);
|
||||
|
||||
// Idempotent: if we already mirrored this key, just return it.
|
||||
let file_path = self.identities_dir.join(format!("{}.json", id));
|
||||
if file_path.exists() {
|
||||
return self.get(&id).await;
|
||||
}
|
||||
|
||||
let created_at = chrono::Utc::now().to_rfc3339();
|
||||
// Mark as the node (master) identity so it gets the hex SVG.
|
||||
let default_profile = IdentityProfile {
|
||||
picture: Some(crate::avatar::default_picture(&pubkey_hex, true)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let identity_file = IdentityFile {
|
||||
id: id.clone(),
|
||||
name: name.clone(),
|
||||
purpose: purpose.clone(),
|
||||
secret_key: signing_key.to_bytes().to_vec(),
|
||||
pubkey_hex: pubkey_hex.clone(),
|
||||
did: did.clone(),
|
||||
created_at,
|
||||
nostr_secret_hex: None,
|
||||
nostr_pubkey_hex: None,
|
||||
profile: Some(default_profile),
|
||||
derivation_index: Some(0),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&identity_file)
|
||||
.context("Failed to serialize identity")?;
|
||||
fs::write(&file_path, json.as_bytes())
|
||||
.await
|
||||
.context("Failed to write identity file")?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&file_path, std::fs::Permissions::from_mode(0o600))
|
||||
.await
|
||||
.context("Failed to set identity file permissions")?;
|
||||
}
|
||||
|
||||
// First identity becomes the default.
|
||||
let (existing, _) = self.list().await?;
|
||||
if existing.len() <= 1 {
|
||||
self.set_default(&id).await?;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Mirrored node signing key as Node identity '{}' ({})",
|
||||
name,
|
||||
purpose
|
||||
);
|
||||
|
||||
self.get(&id).await
|
||||
}
|
||||
|
||||
/// Create a new identity with keys derived from a BIP-39 master seed.
|
||||
/// The derivation index is auto-incremented and persisted.
|
||||
pub async fn create_from_seed(
|
||||
|
||||
@@ -28,6 +28,7 @@ mod avatar;
|
||||
mod backup;
|
||||
mod bitcoin_rpc;
|
||||
mod blobs;
|
||||
mod bootstrap;
|
||||
mod config;
|
||||
mod constants;
|
||||
mod container;
|
||||
@@ -171,6 +172,11 @@ async fn main() -> Result<()> {
|
||||
update::run_update_scheduler(update_data_dir).await;
|
||||
});
|
||||
|
||||
// Synchronize host-side doctor artifacts (script + systemd units) with
|
||||
// what's embedded in this binary. Runs in the background so it never
|
||||
// delays server readiness; best-effort, warnings only.
|
||||
tokio::spawn(bootstrap::ensure_doctor_installed());
|
||||
|
||||
// Spawn periodic container snapshot (for crash recovery)
|
||||
crash_recovery::spawn_snapshot_task(config.data_dir.clone());
|
||||
|
||||
|
||||
@@ -89,22 +89,32 @@ impl Server {
|
||||
// Load persisted messages (Archipelago channel)
|
||||
node_message::init(&config.data_dir).await;
|
||||
|
||||
// Auto-create default identity if none exist (fresh boot or factory reset)
|
||||
// Auto-create the Node identity on fresh boot, mirroring the node's
|
||||
// own signing key (seed-derived when onboarded, random otherwise).
|
||||
// This keeps the DID shown on the Identities page, the DID Status
|
||||
// card, and the DID used for peer-to-peer connects all aligned on
|
||||
// one value — the seed-derived node DID. Idempotent: if the entry
|
||||
// already exists from a prior boot, create_from_signing_key returns
|
||||
// the existing record unchanged.
|
||||
{
|
||||
let im = crate::identity_manager::IdentityManager::new(&config.data_dir).await;
|
||||
if let Ok(mgr) = im {
|
||||
if let Ok((list, _)) = mgr.list().await {
|
||||
if list.is_empty() {
|
||||
let signing_key = ed25519_dalek::SigningKey::from_bytes(
|
||||
&identity.signing_key().to_bytes(),
|
||||
);
|
||||
match mgr
|
||||
.create(
|
||||
"Default".to_string(),
|
||||
.create_from_signing_key(
|
||||
"Node".to_string(),
|
||||
crate::identity_manager::IdentityPurpose::Personal,
|
||||
signing_key,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(record) => {
|
||||
let _ = mgr.create_nostr_key(&record.id).await;
|
||||
tracing::info!(did = %record.did, "Auto-created default identity with Nostr key");
|
||||
tracing::info!(did = %record.did, "Auto-created Node identity mirroring node key");
|
||||
}
|
||||
Err(e) => tracing::debug!("Auto-identity creation (non-fatal): {}", e),
|
||||
}
|
||||
|
||||
@@ -792,7 +792,7 @@ pub async fn cancel_download(data_dir: &Path) -> Result<()> {
|
||||
/// though sudo itself is root. `systemd-run --wait` spawns a transient
|
||||
/// service unit that inherits systemd's default protections (i.e. none
|
||||
/// of ours), escaping the namespace.
|
||||
async fn host_sudo(args: &[&str]) -> Result<std::process::ExitStatus> {
|
||||
pub(crate) async fn host_sudo(args: &[&str]) -> Result<std::process::ExitStatus> {
|
||||
let mut full: Vec<&str> = vec![
|
||||
"systemd-run",
|
||||
"--wait",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.6.0-alpha",
|
||||
"version": "1.7.35-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"launch": "Launch",
|
||||
"starting": "Starting...",
|
||||
"stopping": "Stopping...",
|
||||
"update": "Update",
|
||||
"updating": "Updating...",
|
||||
"send": "Send",
|
||||
"sending": "Sending...",
|
||||
"back": "Back",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"launch": "Abrir",
|
||||
"starting": "Iniciando...",
|
||||
"stopping": "Deteniendo...",
|
||||
"update": "Actualizar",
|
||||
"updating": "Actualizando...",
|
||||
"send": "Enviar",
|
||||
"sending": "Enviando...",
|
||||
"back": "Volver",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
@start="actions.startApp"
|
||||
@stop="actions.stopApp"
|
||||
@restart="actions.restartApp"
|
||||
@update="updateApp"
|
||||
@show-uninstall="showUninstallModal"
|
||||
/>
|
||||
</div>
|
||||
@@ -296,4 +297,12 @@ function goToApp(id: string) {
|
||||
function launchApp(id: string) {
|
||||
useAppLauncherStore().openSession(id)
|
||||
}
|
||||
|
||||
async function updateApp(id: string) {
|
||||
try {
|
||||
await serverStore.updatePackage(id)
|
||||
} catch (err) {
|
||||
actions.actionError.value = `Failed to update ${id}: ${err instanceof Error ? err.message : 'Unknown error'}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -114,6 +114,29 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-4 flex gap-2">
|
||||
<!-- Update available -->
|
||||
<button
|
||||
v-if="pkg['available-update'] && pkg.state !== 'updating'"
|
||||
@click.stop="$emit('update', id)"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium flex items-center justify-center gap-1.5 bg-orange-500/20 border border-orange-500/40 text-orange-200 hover:bg-orange-500/30 transition-colors"
|
||||
:title="`Update to v${pkg['available-update']}`"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{{ t('common.update') }}
|
||||
</button>
|
||||
<!-- Updating in progress -->
|
||||
<span
|
||||
v-if="pkg.state === 'updating'"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium flex items-center justify-center gap-1.5 bg-orange-500/20 border border-orange-500/40 text-orange-200"
|
||||
>
|
||||
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ t('common.updating') }}
|
||||
</span>
|
||||
<!-- Launch -->
|
||||
<button
|
||||
v-if="canLaunch(pkg)"
|
||||
@@ -211,6 +234,7 @@ const emit = defineEmits<{
|
||||
start: [id: string]
|
||||
stop: [id: string]
|
||||
restart: [id: string]
|
||||
update: [id: string]
|
||||
showUninstall: [id: string, pkg: PackageDataEntry]
|
||||
}>()
|
||||
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
{
|
||||
"version": "1.7.34-alpha",
|
||||
"version": "1.7.35-alpha",
|
||||
"release_date": "2026-04-22",
|
||||
"changelog": [
|
||||
"Clearing browser data on an already-onboarded node no longer sends you back through the onboarding wizard. The page now asks the node on every visit and re-caches the answer locally, so clearing site data is harmless.",
|
||||
"Login screen background now rotates each time you land on it — cycles through the six intro images so the lock screen doesn't look identical on every logout.",
|
||||
"Removed the zoom-in effect from subsequent logins. Only the first-ever login (right after onboarding) plays the full zoom + glitch reveal; every re-login now just lands on the dashboard with the welcome typing, no animation."
|
||||
"Apps that lose internet after a network hiccup now self-heal within 30 minutes. No restart, no reinstall — your node spots the broken egress and cycles its own containers to bring connectivity back. Rolls out automatically on the next update, no action needed.",
|
||||
"One-click Update button on every app card. When a new version of an app is available, the orange badge you already saw now has a matching button right next to it — no need to open the app details page to trigger the upgrade.",
|
||||
"Bitcoin Core 28.4 is now available in the App Store (upgraded from 24.0). This is a separate app from Bitcoin Knots, which remains the default Bitcoin node.",
|
||||
"Your node's identity is now unified across every screen. The DID shown on the Web5 page, the identity labeled 'Node' on the Identities page, and the DID your peers see when they connect to you are guaranteed to match your seed-derived Node DID. The 'Default' entry on Identities is now labeled 'Node' so it's clear which one represents your node itself."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.33-alpha",
|
||||
"new_version": "1.7.34-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.34-alpha/archipelago",
|
||||
"sha256": "ba0c6cb531f48a8d350775ab61972291c5c92a298551a7c0543d42e1fe3b18e1",
|
||||
"size_bytes": 40791976
|
||||
"current_version": "1.7.34-alpha",
|
||||
"new_version": "1.7.35-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.35-alpha/archipelago",
|
||||
"sha256": "61a78a445a9b9e232460a4180d7765ad615b9bd0a2c6e99f2d610c9e909ede0c",
|
||||
"size_bytes": 41101648
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.34-alpha.tar.gz",
|
||||
"current_version": "1.7.33-alpha",
|
||||
"new_version": "1.7.34-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.34-alpha/archipelago-frontend-1.7.34-alpha.tar.gz",
|
||||
"sha256": "8700c1ce6534567f008670a810682333966481e0c2e5d36164013219fb2ed3d9",
|
||||
"size_bytes": 77007484
|
||||
"name": "archipelago-frontend-1.7.35-alpha.tar.gz",
|
||||
"current_version": "1.7.34-alpha",
|
||||
"new_version": "1.7.35-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.35-alpha/archipelago-frontend-1.7.35-alpha.tar.gz",
|
||||
"sha256": "8866c1bb79fbcd56d230ea3fd307c9b77f556602b6b2703dff2793fd8b014534",
|
||||
"size_bytes": 77005896
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
releases/v1.7.35-alpha/archipelago
Executable file
BIN
releases/v1.7.35-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.35-alpha/archipelago-frontend-1.7.35-alpha.tar.gz
Normal file
BIN
releases/v1.7.35-alpha/archipelago-frontend-1.7.35-alpha.tar.gz
Normal file
Binary file not shown.
@@ -367,7 +367,72 @@ print(' '.join(['\"' + a + '\"' if ' ' in a else a for a in args[2:]]))
|
||||
[ ${#fixed_names[@]} -gt 0 ] && return 0 || return 1
|
||||
}
|
||||
|
||||
# ── Fix 8: Restart stopped core containers ──────────────────
|
||||
# ── Fix 8: Rootless netns egress lost ────────────────────────
|
||||
# Rootless podman uses pasta to give containers internet egress. If pasta's
|
||||
# tap vanishes (host link flap, mount churn), the rootless-netns keeps inter-
|
||||
# container traffic working but silently loses outbound. Bitcoin IBD stalls
|
||||
# at 0 peers; package pulls fail. The only reliable repair is a stop-all/
|
||||
# start-all cycle so pasta + aardvark-dns rebuild the netns from scratch.
|
||||
fix_rootless_netns_egress() {
|
||||
local archi_uid
|
||||
archi_uid=$(id -u archipelago 2>/dev/null) || return 1
|
||||
|
||||
# Locate the rootless-netns via aardvark-dns (it lives inside it).
|
||||
local aardvark_pid
|
||||
aardvark_pid=$(pgrep -U "$archi_uid" -f '^/usr/lib/podman/aardvark-dns' 2>/dev/null | head -1)
|
||||
[ -z "$aardvark_pid" ] && return 1 # no rootless network active
|
||||
|
||||
# Host precheck: if the host itself can't reach the internet, no point
|
||||
# cycling containers — this is an upstream problem.
|
||||
if ! timeout 3 bash -c '</dev/tcp/1.1.1.1/443' 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Probe egress from inside the rootless-netns. One probe is noisy;
|
||||
# require two consecutive failures 10s apart to rule out transients.
|
||||
if timeout 3 nsenter -t "$aardvark_pid" -n bash -c '</dev/tcp/1.1.1.1/443' 2>/dev/null; then
|
||||
return 1 # first probe succeeded
|
||||
fi
|
||||
sleep 10
|
||||
aardvark_pid=$(pgrep -U "$archi_uid" -f '^/usr/lib/podman/aardvark-dns' 2>/dev/null | head -1)
|
||||
[ -z "$aardvark_pid" ] && return 1
|
||||
if timeout 3 nsenter -t "$aardvark_pid" -n bash -c '</dev/tcp/1.1.1.1/443' 2>/dev/null; then
|
||||
return 1 # recovered on its own
|
||||
fi
|
||||
|
||||
log "Rootless-netns egress is broken (host online, container netns unreachable) — cycling"
|
||||
|
||||
local PODMANCMD="sudo -u archipelago XDG_RUNTIME_DIR=/run/user/$archi_uid podman"
|
||||
local running
|
||||
running=$($PODMANCMD ps --format '{{.Names}}' 2>/dev/null)
|
||||
if [ -z "$running" ]; then
|
||||
log " No running containers to cycle — skipping"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local count
|
||||
count=$(echo "$running" | wc -l)
|
||||
log " Stopping $count running containers (graceful, 30s)..."
|
||||
$PODMANCMD stop --all --time 30 >/dev/null 2>&1
|
||||
sleep 5
|
||||
|
||||
log " Starting containers back up..."
|
||||
for c in $running; do
|
||||
$PODMANCMD start "$c" >/dev/null 2>&1 &
|
||||
done
|
||||
wait
|
||||
sleep 5
|
||||
|
||||
aardvark_pid=$(pgrep -U "$archi_uid" -f '^/usr/lib/podman/aardvark-dns' 2>/dev/null | head -1)
|
||||
if [ -n "$aardvark_pid" ] && timeout 3 nsenter -t "$aardvark_pid" -n bash -c '</dev/tcp/1.1.1.1/443' 2>/dev/null; then
|
||||
log " Rootless-netns egress restored ($count containers cycled)"
|
||||
else
|
||||
log " WARN: egress still broken after cycle — may need manual intervention"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Fix 9: Restart stopped core containers ──────────────────
|
||||
# Rootless Podman 4.x restart policies don't auto-restart on crash.
|
||||
# This check restarts any exited core containers (tiers 0-2).
|
||||
fix_stopped_core_containers() {
|
||||
@@ -414,6 +479,7 @@ run_fix "tor-permissions" fix_tor_permissions
|
||||
run_fix "searxng" fix_searxng
|
||||
run_fix "bitcoin-txindex" fix_bitcoin_txindex
|
||||
run_fix "exit-127" fix_exit_127
|
||||
run_fix "netns-egress" fix_rootless_netns_egress
|
||||
run_fix "stopped-core" fix_stopped_core_containers
|
||||
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user