fix: harden OTA updates, AIUI desktop gap, LND no-proxy
- update.rs: post-OTA probe falls back to http://127.0.0.1/ on connect error (nginx binds :80, not :443) so good updates are no longer rolled back; recover stuck update_in_progress; avoid ETXTBSY on running binary - LND: REST client bypasses proxy, GET newaddress p2wkh, wallet readiness/unlock after restart - Dashboard.vue: chat route back to plain h-full (desktop bottom-gap fix) - vite.config.ts: dev-only /aiui proxy - tests/release/run.sh: release gate harness (static+frontend+backend) - CHANGELOG: v1.7.89-alpha notes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
495b90782a
commit
c49e8fcacd
Generated
+1
-1
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.88-alpha"
|
||||
version = "1.7.89-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.88-alpha"
|
||||
version = "1.7.89-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -38,6 +38,7 @@ impl RpcHandler {
|
||||
let macaroon_hex = hex::encode(&macaroon_bytes);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
@@ -180,6 +181,7 @@ impl RpcHandler {
|
||||
let macaroon_hex = hex::encode(&macaroon_bytes);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
|
||||
@@ -63,6 +63,7 @@ impl RpcHandler {
|
||||
let macaroon_bytes = read_lnd_admin_macaroon().await?;
|
||||
let macaroon_hex = hex::encode(&macaroon_bytes);
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
|
||||
@@ -530,6 +530,7 @@ impl RpcHandler {
|
||||
// Call LND REST API to initialize wallet with derived entropy.
|
||||
// LND must be running but NOT yet initialized (no existing wallet).
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
|
||||
@@ -76,7 +76,7 @@ pub async fn ensure_wallet_initialized() -> Result<()> {
|
||||
let admin_macaroon = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon";
|
||||
let wallet_db = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/wallet.db";
|
||||
if file_exists_as_root(wallet_db).await {
|
||||
if file_exists_as_root(admin_macaroon).await {
|
||||
if file_exists_as_root(admin_macaroon).await && lnd_getinfo_ready(admin_macaroon).await {
|
||||
return Ok(());
|
||||
}
|
||||
unlock_existing_wallet().await?;
|
||||
@@ -127,6 +127,7 @@ async fn unlock_existing_wallet() -> Result<()> {
|
||||
|
||||
async fn unlock_existing_wallet_via_rest() -> Result<()> {
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(20))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
@@ -204,6 +205,7 @@ struct InitWalletRequest {
|
||||
|
||||
async fn init_wallet_via_rest() -> Result<()> {
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(20))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
@@ -305,12 +307,12 @@ async fn decode_lnd_unlocker_response<T: for<'de> Deserialize<'de>>(
|
||||
anyhow::bail!("LND REST {path} returned {status}: {text}")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn lnd_getinfo_ready(admin_macaroon: &str) -> bool {
|
||||
let Ok(macaroon) = read_file_as_root(admin_macaroon).await else {
|
||||
return false;
|
||||
};
|
||||
let Ok(client) = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
|
||||
@@ -3220,11 +3220,11 @@ app:
|
||||
- /data/.filebrowser.json
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /tmp/filebrowser-srv
|
||||
source: /var/lib/archipelago/filebrowser-srv
|
||||
target: /srv
|
||||
options: [rw]
|
||||
- type: bind
|
||||
source: /tmp/filebrowser-data
|
||||
source: /var/lib/archipelago/filebrowser-data
|
||||
target: /data
|
||||
options: [rw]
|
||||
"#;
|
||||
@@ -3244,7 +3244,7 @@ app:
|
||||
secret_file: bitcoin-rpc-password
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /tmp/lnd
|
||||
source: /var/lib/archipelago/lnd
|
||||
target: /root/.lnd
|
||||
"#;
|
||||
AppManifest::parse(yaml).unwrap()
|
||||
|
||||
@@ -367,12 +367,20 @@ async fn probe_frontend_once() -> Result<()> {
|
||||
.context("build probe client")?;
|
||||
// Prefer HTTPS since that's the failure mode we're catching (nginx
|
||||
// 500 on the PWA). HTTP usually redirects to HTTPS and would mask
|
||||
// the bug.
|
||||
let resp = client
|
||||
.get("https://127.0.0.1/")
|
||||
.send()
|
||||
.await
|
||||
.context("probe GET https://127.0.0.1/")?;
|
||||
// the bug. BUT not every node binds 443 on loopback (.116 serves
|
||||
// plain HTTP; 443 there belongs to tailscale) — on a *connect*
|
||||
// error, fall back to HTTP so a healthy node isn't "verified" into
|
||||
// a rollback. An HTTP error status stays fatal on whichever scheme
|
||||
// answered.
|
||||
let resp = match client.get("https://127.0.0.1/").send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) if e.is_connect() => client
|
||||
.get("http://127.0.0.1/")
|
||||
.send()
|
||||
.await
|
||||
.context("probe GET http://127.0.0.1/ (https not bound on loopback)")?,
|
||||
Err(e) => return Err(e).context("probe GET https://127.0.0.1/"),
|
||||
};
|
||||
let status = resp.status();
|
||||
if status.is_success() || status.is_redirection() {
|
||||
return Ok(());
|
||||
@@ -1078,6 +1086,17 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
||||
let current_binary = Path::new("/usr/local/bin/archipelago");
|
||||
if current_binary.exists() {
|
||||
let backup_path = backup_dir.join("archipelago");
|
||||
// A leftover backup from an earlier rollback can be root-owned
|
||||
// (rollback used to chown it in place), and fs::copy O_TRUNCs the
|
||||
// existing file — EACCES as the service user, wedging every apply
|
||||
// (seen on .116, v1.7.86 OTA). Unlink first; the dir is
|
||||
// service-owned so unlink works even when the file isn't ours.
|
||||
if backup_path.exists() {
|
||||
if let Err(e) = fs::remove_file(&backup_path).await {
|
||||
tracing::warn!(error = %e, "unlink of stale binary backup failed, retrying via host_sudo");
|
||||
let _ = host_sudo(&["rm", "-f", &backup_path.to_string_lossy()]).await;
|
||||
}
|
||||
}
|
||||
fs::copy(current_binary, &backup_path)
|
||||
.await
|
||||
.context("Failed to backup current binary")?;
|
||||
@@ -1415,21 +1434,38 @@ pub async fn rollback_update(data_dir: &Path) -> Result<()> {
|
||||
|
||||
let backup_binary = backup_dir.join("archipelago");
|
||||
if backup_binary.exists() {
|
||||
// Use host_sudo + mv so we escape the archipelago service's
|
||||
// ProtectSystem=strict mount namespace. A plain fs::copy or
|
||||
// `sudo cp` from inside the service hits EROFS on /usr/local/bin,
|
||||
// which would silently orphan the rollback — exactly the
|
||||
// opposite of what auto-rollback is for. Pattern matches
|
||||
// apply_update()'s binary swap above.
|
||||
// Same two namespace gotchas as apply_update()'s binary swap:
|
||||
// `cp` straight onto the running binary is O_TRUNC and fails
|
||||
// ETXTBSY (exit 1 — exactly what broke the .116 rollback), and
|
||||
// plain sudo inherits ProtectSystem=strict, so everything goes
|
||||
// through host_sudo. Copy to a temp name on the same filesystem,
|
||||
// fix ownership on the TEMP file (never the stored backup — an
|
||||
// in-place chown is what later wedged apply_update), then mv,
|
||||
// which is an atomic rename and tolerates a busy destination.
|
||||
let backup_str = backup_binary.to_string_lossy().to_string();
|
||||
let _ = host_sudo(&["chmod", "0755", &backup_str]).await;
|
||||
let _ = host_sudo(&["chown", "root:root", &backup_str]).await;
|
||||
let status = host_sudo(&["cp", &backup_str, "/usr/local/bin/archipelago"])
|
||||
let tmp = format!(
|
||||
"/usr/local/bin/.archipelago.rollback.{}",
|
||||
chrono::Utc::now().timestamp_millis()
|
||||
);
|
||||
let copy = host_sudo(&["cp", &backup_str, &tmp])
|
||||
.await
|
||||
.context("Failed to stage backup binary via host_sudo")?;
|
||||
if !copy.success() {
|
||||
anyhow::bail!(
|
||||
"cp backup binary to {} failed (exit {:?})",
|
||||
tmp,
|
||||
copy.code()
|
||||
);
|
||||
}
|
||||
let _ = host_sudo(&["chmod", "0755", &tmp]).await;
|
||||
let _ = host_sudo(&["chown", "root:root", &tmp]).await;
|
||||
let status = host_sudo(&["mv", &tmp, "/usr/local/bin/archipelago"])
|
||||
.await
|
||||
.context("Failed to restore backup binary via host_sudo")?;
|
||||
if !status.success() {
|
||||
let _ = host_sudo(&["rm", "-f", &tmp]).await;
|
||||
anyhow::bail!(
|
||||
"cp backup binary into /usr/local/bin failed (exit {:?})",
|
||||
"mv backup binary into /usr/local/bin failed (exit {:?})",
|
||||
status.code()
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user