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.
This commit is contained in:
parent
4b8ef0a098
commit
be96002372
@ -3,7 +3,7 @@ use anyhow::{Context, Result};
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{LndAmount, LndBalanceResponse};
|
use super::{LndAmount, LndBalanceResponse, read_lnd_admin_macaroon};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct LndInfo {
|
struct LndInfo {
|
||||||
@ -34,11 +34,7 @@ struct LndChannelBalanceResponse {
|
|||||||
|
|
||||||
impl RpcHandler {
|
impl RpcHandler {
|
||||||
pub(in crate::api::rpc) async fn handle_lnd_getinfo(&self) -> Result<serde_json::Value> {
|
pub(in crate::api::rpc) async fn handle_lnd_getinfo(&self) -> Result<serde_json::Value> {
|
||||||
let macaroon_path = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon";
|
let macaroon_bytes = read_lnd_admin_macaroon().await?;
|
||||||
|
|
||||||
let macaroon_bytes = tokio::fs::read(macaroon_path)
|
|
||||||
.await
|
|
||||||
.context("Failed to read LND admin macaroon — is LND installed?")?;
|
|
||||||
let macaroon_hex = hex::encode(&macaroon_bytes);
|
let macaroon_hex = hex::encode(&macaroon_bytes);
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
@ -114,7 +110,6 @@ impl RpcHandler {
|
|||||||
/// for building lndconnect:// URIs in the frontend.
|
/// for building lndconnect:// URIs in the frontend.
|
||||||
pub(crate) async fn handle_lnd_connect_info(&self) -> Result<serde_json::Value> {
|
pub(crate) async fn handle_lnd_connect_info(&self) -> Result<serde_json::Value> {
|
||||||
let cert_path = "/var/lib/archipelago/lnd/tls.cert";
|
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)
|
// Read and encode TLS cert (PEM -> DER -> base64url)
|
||||||
let cert_pem = tokio::fs::read_to_string(cert_path)
|
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);
|
let cert_b64url = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cert_der);
|
||||||
|
|
||||||
// Read and encode macaroon (binary -> base64url)
|
// Read and encode macaroon (binary -> base64url)
|
||||||
let macaroon_bytes = tokio::fs::read(macaroon_path)
|
let macaroon_bytes = read_lnd_admin_macaroon().await?;
|
||||||
.await
|
|
||||||
.context("Failed to read LND admin macaroon")?;
|
|
||||||
let macaroon_b64url =
|
let macaroon_b64url =
|
||||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&macaroon_bytes);
|
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(
|
pub(in crate::api::rpc) async fn handle_lnd_export_channel_backup(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let macaroon_path = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon";
|
let macaroon_bytes = read_lnd_admin_macaroon().await?;
|
||||||
let macaroon_bytes = tokio::fs::read(macaroon_path)
|
|
||||||
.await
|
|
||||||
.context("Failed to read LND admin macaroon")?;
|
|
||||||
let macaroon_hex = hex::encode(&macaroon_bytes);
|
let macaroon_hex = hex::encode(&macaroon_bytes);
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
|
|||||||
@ -4,7 +4,11 @@ mod payments;
|
|||||||
mod wallet;
|
mod wallet;
|
||||||
|
|
||||||
use crate::api::rpc::RpcHandler;
|
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
|
// Shared LND response types used by multiple submodules
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
@ -17,15 +21,45 @@ pub(super) struct LndAmount {
|
|||||||
pub sat: Option<String>,
|
pub sat: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<Vec<u8>> {
|
||||||
|
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 {
|
impl RpcHandler {
|
||||||
/// Helper: create an authenticated LND REST client.
|
/// Helper: create an authenticated LND REST client.
|
||||||
/// Returns an HTTP client configured for LND's self-signed TLS and the
|
/// Returns an HTTP client configured for LND's self-signed TLS and the
|
||||||
/// hex-encoded admin macaroon for request headers.
|
/// hex-encoded admin macaroon for request headers.
|
||||||
pub(crate) async fn lnd_client(&self) -> Result<(reqwest::Client, String)> {
|
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 = read_lnd_admin_macaroon().await?;
|
||||||
let macaroon_bytes = tokio::fs::read(macaroon_path)
|
|
||||||
.await
|
|
||||||
.context("Failed to read LND admin macaroon — is LND installed?")?;
|
|
||||||
let macaroon_hex = hex::encode(&macaroon_bytes);
|
let macaroon_hex = hex::encode(&macaroon_bytes);
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(15))
|
.timeout(std::time::Duration::from_secs(15))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user