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))