From 30b31b36700b5a7be9a42b9497ed1b69a9ff4dda Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 23 Apr 2026 04:15:44 -0400 Subject: [PATCH] fix(lnd): read admin macaroon via sudo fallback LND's admin.macaroon is owned by a rootless-podman subordinate UID (typically 100000) with mode 640. The archipelago server runs as UID 1000 and cannot read the file directly, which caused every dashboard LND RPC (getinfo, connect-info, export-channel-backup) and lnd_client to fail with "Failed to read LND admin macaroon". Add a read_lnd_admin_macaroon() helper that first tries a direct read (for operators who have relaxed permissions) then falls back to `sudo -n cat`, mirroring the pattern already used for Tor hidden service hostnames in handle_lnd_connect_info. Centralise the canonical macaroon path as LND_ADMIN_MACAROON_PATH and route all four callers through the helper. Verified on .228: GET /lnd-connect-info now returns 200 with cert, macaroon, and tor_onion fields. Dashboard QR/connect-string UI unblocked. --- core/archipelago/src/api/rpc/lnd/info.rs | 18 +++------- core/archipelago/src/api/rpc/lnd/mod.rs | 44 +++++++++++++++++++++--- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/core/archipelago/src/api/rpc/lnd/info.rs b/core/archipelago/src/api/rpc/lnd/info.rs index 97f86d4a..fb5e9ec6 100644 --- a/core/archipelago/src/api/rpc/lnd/info.rs +++ b/core/archipelago/src/api/rpc/lnd/info.rs @@ -3,7 +3,7 @@ use anyhow::{Context, Result}; use base64::Engine; use serde::{Deserialize, Serialize}; -use super::{LndAmount, LndBalanceResponse}; +use super::{LndAmount, LndBalanceResponse, read_lnd_admin_macaroon}; #[derive(Debug, Serialize)] struct LndInfo { @@ -34,11 +34,7 @@ struct LndChannelBalanceResponse { impl RpcHandler { pub(in crate::api::rpc) async fn handle_lnd_getinfo(&self) -> Result { - let macaroon_path = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon"; - - let macaroon_bytes = tokio::fs::read(macaroon_path) - .await - .context("Failed to read LND admin macaroon — is LND installed?")?; + let macaroon_bytes = read_lnd_admin_macaroon().await?; let macaroon_hex = hex::encode(&macaroon_bytes); let client = reqwest::Client::builder() @@ -114,7 +110,6 @@ impl RpcHandler { /// for building lndconnect:// URIs in the frontend. pub(crate) async fn handle_lnd_connect_info(&self) -> Result { let cert_path = "/var/lib/archipelago/lnd/tls.cert"; - let macaroon_path = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon"; // Read and encode TLS cert (PEM -> DER -> base64url) let cert_pem = tokio::fs::read_to_string(cert_path) @@ -130,9 +125,7 @@ impl RpcHandler { let cert_b64url = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cert_der); // Read and encode macaroon (binary -> base64url) - let macaroon_bytes = tokio::fs::read(macaroon_path) - .await - .context("Failed to read LND admin macaroon")?; + let macaroon_bytes = read_lnd_admin_macaroon().await?; let macaroon_b64url = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&macaroon_bytes); @@ -183,10 +176,7 @@ impl RpcHandler { pub(in crate::api::rpc) async fn handle_lnd_export_channel_backup( &self, ) -> Result { - let macaroon_path = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon"; - let macaroon_bytes = tokio::fs::read(macaroon_path) - .await - .context("Failed to read LND admin macaroon")?; + let macaroon_bytes = read_lnd_admin_macaroon().await?; let macaroon_hex = hex::encode(&macaroon_bytes); let client = reqwest::Client::builder() diff --git a/core/archipelago/src/api/rpc/lnd/mod.rs b/core/archipelago/src/api/rpc/lnd/mod.rs index 90cc4626..1e29b08e 100644 --- a/core/archipelago/src/api/rpc/lnd/mod.rs +++ b/core/archipelago/src/api/rpc/lnd/mod.rs @@ -4,7 +4,11 @@ mod payments; mod wallet; use crate::api::rpc::RpcHandler; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, anyhow}; + +/// Canonical on-host path for LND's admin macaroon. +pub(crate) const LND_ADMIN_MACAROON_PATH: &str = + "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon"; // Shared LND response types used by multiple submodules #[derive(Debug, serde::Deserialize)] @@ -17,15 +21,45 @@ pub(super) struct LndAmount { pub sat: Option, } +/// Read LND's admin macaroon from disk. +/// +/// The macaroon lives inside LND's container data dir and is owned by a +/// rootless-podman subordinate UID (typically 100000), mode 640. The +/// archipelago server runs as UID 1000 and therefore cannot read it +/// directly. We first try a plain read (works if an operator has relaxed +/// permissions), then fall back to `sudo cat` — mirroring the pattern +/// already used for Tor hidden-service hostnames. +pub(crate) async fn read_lnd_admin_macaroon() -> Result> { + match tokio::fs::read(LND_ADMIN_MACAROON_PATH).await { + Ok(bytes) => Ok(bytes), + Err(direct_err) => { + let output = tokio::process::Command::new("sudo") + .args(["-n", "cat", LND_ADMIN_MACAROON_PATH]) + .output() + .await + .with_context(|| { + format!( + "Failed to read LND admin macaroon (direct: {direct_err}); sudo fallback also failed" + ) + })?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "Failed to read LND admin macaroon — is LND installed? (direct: {direct_err}; sudo: {})", + stderr.trim() + )); + } + Ok(output.stdout) + } + } +} + impl RpcHandler { /// Helper: create an authenticated LND REST client. /// Returns an HTTP client configured for LND's self-signed TLS and the /// hex-encoded admin macaroon for request headers. pub(crate) async fn lnd_client(&self) -> Result<(reqwest::Client, String)> { - let macaroon_path = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon"; - let macaroon_bytes = tokio::fs::read(macaroon_path) - .await - .context("Failed to read LND admin macaroon — is LND installed?")?; + let macaroon_bytes = read_lnd_admin_macaroon().await?; let macaroon_hex = hex::encode(&macaroon_bytes); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(15))