fix: rpcauth credentials, reboot survival, system Tor for all containers
- Bitcoin RPC: switch to rpcauth (salted hash in bitcoin.conf, no plaintext in config or CLI). Password stable across reboots/restarts/deploys. - Remove daily-reboot-test.sh cron on both servers - Enable podman-restart.service for container auto-start after reboot - System Tor: SocksPort 0.0.0.0:9050 with SocksPolicy for container access - LND: tor.socks=host.containers.internal:9050 (system Tor, not container) - Bitcoin: -proxy=host.containers.internal:9050 for Tor outbound - bitcoin_rpc.rs: reads from secrets file, cached, stable credentials - package.rs: dynamic rpc_user/rpc_pass, rpcauth hash generation - network.rs: fix missing send_to_peer args (mesh encryption update) - first-boot-containers.sh: rpcauth generation, system Tor config - deploy-to-target.sh: rpcauth credentials, LND config migration - Mesh: encrypted channel message support (ChaCha20-Poly1305 updates) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b4d204d1d6
commit
b31148a8b7
@ -121,6 +121,8 @@ impl RpcHandler {
|
||||
to_onion,
|
||||
my_pubkey,
|
||||
&req_msg.to_string(),
|
||||
None,
|
||||
None,
|
||||
).await?;
|
||||
|
||||
// Also add them as a pending peer locally
|
||||
|
||||
@ -193,15 +193,15 @@ impl RpcHandler {
|
||||
"--restart=unless-stopped", // Auto-restart policy
|
||||
];
|
||||
|
||||
// Read Bitcoin RPC password from secrets for container configs
|
||||
let rpc_pass = crate::bitcoin_rpc::bitcoin_rpc_password().await;
|
||||
// Read Bitcoin RPC credentials from cookie file for container configs
|
||||
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||
|
||||
// App-specific configuration (should come from manifest)
|
||||
let (mut ports, mut volumes, env_vars, custom_command, mut custom_args) = {
|
||||
let mut allocator = self.port_allocator.lock().map_err(|e| {
|
||||
anyhow::anyhow!("Port allocator lock poisoned: {}", e)
|
||||
})?;
|
||||
get_app_config(package_id, &self.config.host_ip, &mut allocator, &rpc_pass)
|
||||
get_app_config(package_id, &self.config.host_ip, &mut allocator, &rpc_user, &rpc_pass)
|
||||
};
|
||||
|
||||
// Fedimint Gateway: auto-detect LND and switch to lnd mode
|
||||
@ -224,7 +224,7 @@ impl RpcHandler {
|
||||
"$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC".to_string(),
|
||||
"--network".to_string(), "bitcoin".to_string(),
|
||||
"--bitcoind-url".to_string(), format!("http://{}:8332", self.config.host_ip),
|
||||
"--bitcoind-username".to_string(), "archipelago".to_string(),
|
||||
"--bitcoind-username".to_string(), rpc_user.clone(),
|
||||
"--bitcoind-password".to_string(), rpc_pass.clone(),
|
||||
"lnd".to_string(),
|
||||
"--lnd-rpc-host".to_string(), format!("{}:10009", self.config.host_ip),
|
||||
@ -304,24 +304,34 @@ impl RpcHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-install: Create bitcoin.conf for Bitcoin nodes with RPC + txindex
|
||||
// Pre-install: Create bitcoin.conf with rpcauth (salted hash, no plaintext)
|
||||
if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") {
|
||||
let bitcoin_dir = "/var/lib/archipelago/bitcoin";
|
||||
let conf_path = format!("{}/bitcoin.conf", bitcoin_dir);
|
||||
// Generate rpcauth hash: HMAC-SHA256(salt, password)
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
let salt_bytes: [u8; 16] = rand::random();
|
||||
let salt_hex = hex::encode(salt_bytes);
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(salt_hex.as_bytes())
|
||||
.expect("HMAC accepts any key length");
|
||||
mac.update(rpc_pass.as_bytes());
|
||||
let hash_hex = hex::encode(mac.finalize().into_bytes());
|
||||
let rpcauth_line = format!("rpcauth={}:{}${}", rpc_user, salt_hex, hash_hex);
|
||||
|
||||
let bitcoin_conf = format!("\
|
||||
# rpcauth: salted hash only — no plaintext password in config or CLI\n\
|
||||
{}\n\
|
||||
server=1\n\
|
||||
prune=550\n\
|
||||
rpcuser=archipelago\n\
|
||||
rpcpassword={}\n\
|
||||
rpcbind=0.0.0.0\n\
|
||||
rpcallowip=127.0.0.1/32\n\
|
||||
rpcallowip=10.88.0.0/16\n\
|
||||
rpcallowip=0.0.0.0/0\n\
|
||||
rpcport=8332\n\
|
||||
listen=1\n\
|
||||
printtoconsole=1\n", rpc_pass);
|
||||
printtoconsole=1\n", rpcauth_line);
|
||||
let _ = tokio::fs::create_dir_all(bitcoin_dir).await;
|
||||
let _ = tokio::fs::write(&conf_path, bitcoin_conf).await;
|
||||
info!("Created bitcoin.conf at {} with RPC + txindex enabled", conf_path);
|
||||
info!("Created bitcoin.conf with rpcauth (no plaintext credentials)");
|
||||
}
|
||||
|
||||
// Add port mappings (skip if host network mode like Tailscale)
|
||||
@ -1481,6 +1491,7 @@ fn get_app_config(
|
||||
app_id: &str,
|
||||
host_ip: &str,
|
||||
allocator: &mut PortAllocator,
|
||||
rpc_user: &str,
|
||||
rpc_pass: &str,
|
||||
) -> (Vec<String>, Vec<String>, Vec<String>, Option<String>, Option<Vec<String>>) {
|
||||
match app_id {
|
||||
@ -1514,7 +1525,7 @@ fn get_app_config(
|
||||
format!("BTCPAY_HOST={}:23000", host_ip),
|
||||
"BTCPAY_CHAINS=btc".to_string(),
|
||||
format!("BTCPAY_BTCRPCURL=http://{}:8332", host_ip),
|
||||
"BTCPAY_BTCRPCUSER=archipelago".to_string(),
|
||||
format!("BTCPAY_BTCRPCUSER={}", rpc_user),
|
||||
format!("BTCPAY_BTCRPCPASSWORD={}", rpc_pass),
|
||||
"BTCPAY_POSTGRES=User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true".to_string(),
|
||||
],
|
||||
@ -1538,7 +1549,7 @@ fn get_app_config(
|
||||
"ELECTRUM_TLS_ENABLED=false".to_string(),
|
||||
format!("CORE_RPC_HOST={}", host_ip),
|
||||
"CORE_RPC_PORT=8332".to_string(),
|
||||
"CORE_RPC_USERNAME=archipelago".to_string(),
|
||||
format!("CORE_RPC_USERNAME={}", rpc_user),
|
||||
format!("CORE_RPC_PASSWORD={}", rpc_pass),
|
||||
"DATABASE_ENABLED=true".to_string(),
|
||||
"DATABASE_HOST=archy-mempool-db".to_string(),
|
||||
@ -1556,7 +1567,7 @@ fn get_app_config(
|
||||
vec!["50001:50001".to_string()],
|
||||
vec!["/var/lib/archipelago/electrumx:/data".to_string()],
|
||||
vec![
|
||||
format!("DAEMON_URL=http://archipelago:{}@{}:8332/", rpc_pass, bitcoin_host),
|
||||
format!("DAEMON_URL=http://{}:{}@{}:8332/", rpc_user, rpc_pass, bitcoin_host),
|
||||
"COIN=Bitcoin".to_string(),
|
||||
"DB_DIRECTORY=/data".to_string(),
|
||||
"SERVICES=tcp://:50001,rpc://0.0.0.0:8000".to_string(),
|
||||
@ -1720,7 +1731,7 @@ fn get_app_config(
|
||||
vec!["/var/lib/archipelago/fedimint:/data".to_string()],
|
||||
vec![
|
||||
"FM_DATA_DIR=/data".to_string(),
|
||||
"FM_BITCOIND_USERNAME=archipelago".to_string(),
|
||||
format!("FM_BITCOIND_USERNAME={}", rpc_user),
|
||||
format!("FM_BITCOIND_PASSWORD={}", rpc_pass),
|
||||
"FM_BITCOIN_NETWORK=bitcoin".to_string(),
|
||||
"FM_BIND_P2P=0.0.0.0:8173".to_string(),
|
||||
@ -1746,7 +1757,7 @@ fn get_app_config(
|
||||
"$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC".to_string(),
|
||||
"--network".to_string(), "bitcoin".to_string(),
|
||||
"--bitcoind-url".to_string(), format!("http://{}:8332", host_ip),
|
||||
"--bitcoind-username".to_string(), "archipelago".to_string(),
|
||||
"--bitcoind-username".to_string(), rpc_user.to_string(),
|
||||
"--bitcoind-password".to_string(), rpc_pass.to_string(),
|
||||
"ldk".to_string(),
|
||||
"--ldk-lightning-port".to_string(), "9737".to_string(),
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
//! Shared Bitcoin RPC credential management.
|
||||
//! Reads credentials from the per-installation secrets file, falling back to
|
||||
//! environment variables, then a dev-only default.
|
||||
//! Bitcoin RPC credential management.
|
||||
//!
|
||||
//! Uses `rpcauth` in bitcoin.conf (salted hash — no plaintext in config or CLI).
|
||||
//! The actual password is stored in `/var/lib/archipelago/secrets/bitcoin-rpc-password`
|
||||
//! and stays stable across reboots, restarts, and deploys.
|
||||
|
||||
use tokio::sync::OnceCell;
|
||||
use tracing::debug;
|
||||
|
||||
const SECRETS_PATH: &str = "/var/lib/archipelago/secrets/bitcoin-rpc-password";
|
||||
const DEFAULT_USER: &str = "archipelago";
|
||||
const RPC_USER: &str = "archipelago";
|
||||
|
||||
static CACHED_PASSWORD: OnceCell<String> = OnceCell::const_new();
|
||||
|
||||
/// Read the Bitcoin RPC password from the secrets file, env var, or dev fallback.
|
||||
/// Read the Bitcoin RPC password from the secrets file.
|
||||
/// Falls back to env var (dev), then generates and persists a random password.
|
||||
async fn read_password() -> String {
|
||||
// 1. Try secrets file (production path)
|
||||
// 1. Secrets file (production)
|
||||
if let Ok(pass) = tokio::fs::read_to_string(SECRETS_PATH).await {
|
||||
let pass = pass.trim().to_string();
|
||||
if !pass.is_empty() {
|
||||
@ -21,7 +24,7 @@ async fn read_password() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try environment variable
|
||||
// 2. Environment variable (dev)
|
||||
if let Ok(pass) = std::env::var("BITCOIN_RPC_PASSWORD") {
|
||||
if !pass.is_empty() {
|
||||
debug!("Bitcoin RPC password loaded from env var");
|
||||
@ -29,29 +32,31 @@ async fn read_password() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Generate a random password and persist it (first-boot provisioning)
|
||||
// 3. Generate and persist (first boot)
|
||||
let random_pass = generate_random_password();
|
||||
if let Some(parent) = std::path::Path::new(SECRETS_PATH).parent() {
|
||||
let _ = tokio::fs::create_dir_all(parent).await;
|
||||
}
|
||||
match tokio::fs::write(SECRETS_PATH, &random_pass).await {
|
||||
Ok(_) => {
|
||||
// Restrict permissions to owner-only
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(SECRETS_PATH, std::fs::Permissions::from_mode(0o600));
|
||||
let _ = std::fs::set_permissions(
|
||||
SECRETS_PATH,
|
||||
std::fs::Permissions::from_mode(0o600),
|
||||
);
|
||||
}
|
||||
debug!("Bitcoin RPC password: generated and saved to secrets file");
|
||||
debug!("Bitcoin RPC password generated and saved");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to save generated Bitcoin RPC password: {} — using ephemeral", e);
|
||||
tracing::warn!("Failed to save Bitcoin RPC password: {}", e);
|
||||
}
|
||||
}
|
||||
random_pass
|
||||
}
|
||||
|
||||
/// Generate a cryptographically random password for Bitcoin RPC (32 hex chars).
|
||||
/// Generate a cryptographically random password (32 hex chars).
|
||||
fn generate_random_password() -> String {
|
||||
let bytes: [u8; 16] = rand::random();
|
||||
hex::encode(bytes)
|
||||
@ -62,11 +67,16 @@ pub async fn bitcoin_rpc_credentials() -> (String, String) {
|
||||
let pass = CACHED_PASSWORD
|
||||
.get_or_init(|| async { read_password().await })
|
||||
.await;
|
||||
(DEFAULT_USER.to_string(), pass.clone())
|
||||
(RPC_USER.to_string(), pass.clone())
|
||||
}
|
||||
|
||||
/// Get the Bitcoin RPC password as a plain string (for config generation).
|
||||
/// Get the Bitcoin RPC password (for container config generation).
|
||||
pub async fn bitcoin_rpc_password() -> String {
|
||||
let (_, pass) = bitcoin_rpc_credentials().await;
|
||||
pass
|
||||
}
|
||||
|
||||
/// Get the Bitcoin RPC username.
|
||||
pub async fn bitcoin_rpc_username() -> String {
|
||||
RPC_USER.to_string()
|
||||
}
|
||||
|
||||
@ -66,6 +66,10 @@ pub struct MeshState {
|
||||
pub stego_mode: super::steganography::SteganographyMode,
|
||||
/// Chunk reassembly buffer for multi-frame messages.
|
||||
chunk_buffer: RwLock<HashMap<(u32, u8), ChunkAssembly>>,
|
||||
/// Double Ratchet session manager for forward-secret encryption.
|
||||
pub session_manager: Arc<super::session::SessionManager>,
|
||||
/// Whether to encrypt directed relay messages (config toggle for rollback).
|
||||
pub encrypt_relay: bool,
|
||||
}
|
||||
|
||||
/// In-progress chunk reassembly for a multi-frame message.
|
||||
@ -81,6 +85,8 @@ impl MeshState {
|
||||
block_header_cache: Arc<super::bitcoin_relay::BlockHeaderCache>,
|
||||
relay_tracker: Option<Arc<super::bitcoin_relay::RelayTracker>>,
|
||||
stego_mode: super::steganography::SteganographyMode,
|
||||
encrypt_relay: bool,
|
||||
session_manager: Arc<super::session::SessionManager>,
|
||||
) -> (Arc<Self>, broadcast::Receiver<MeshEvent>, mpsc::Receiver<MeshCommand>) {
|
||||
let (tx, rx) = broadcast::channel(64);
|
||||
let (cmd_tx, cmd_rx) = mpsc::channel(32);
|
||||
@ -108,6 +114,8 @@ impl MeshState {
|
||||
relay_tracker,
|
||||
stego_mode,
|
||||
chunk_buffer: RwLock::new(HashMap::new()),
|
||||
session_manager,
|
||||
encrypt_relay,
|
||||
});
|
||||
(state, rx, cmd_rx)
|
||||
}
|
||||
@ -496,6 +504,8 @@ async fn handle_frame(
|
||||
handle_typed_message(&payload, contact_id, &name, state).await;
|
||||
} else if let Some(decoded) = try_base64_typed(&payload) {
|
||||
handle_typed_message(&decoded, contact_id, &name, state).await;
|
||||
} else if let Some(decoded) = try_decrypt_ratchet_base64(&payload, contact_id, state).await {
|
||||
handle_typed_message(&decoded, contact_id, &name, state).await;
|
||||
} else if let Some(decoded) = try_decrypt_base64(&payload, contact_id, state).await {
|
||||
handle_typed_message(&decoded, contact_id, &name, state).await;
|
||||
} else if let Some(decoded) = try_chunk_reassemble(&payload, contact_id, state).await {
|
||||
@ -521,6 +531,8 @@ async fn handle_frame(
|
||||
handle_typed_message(&payload, contact_id, &name, state).await;
|
||||
} else if let Some(decoded) = try_base64_typed(&payload) {
|
||||
handle_typed_message(&decoded, contact_id, &name, state).await;
|
||||
} else if let Some(decoded) = try_decrypt_ratchet_base64(&payload, contact_id, state).await {
|
||||
handle_typed_message(&decoded, contact_id, &name, state).await;
|
||||
} else if let Some(decoded) = try_decrypt_base64(&payload, contact_id, state).await {
|
||||
handle_typed_message(&decoded, contact_id, &name, state).await;
|
||||
} else if let Some(decoded) = try_chunk_reassemble(&payload, contact_id, state).await {
|
||||
@ -853,6 +865,66 @@ async fn try_decrypt_base64(
|
||||
try_decrypt_typed(&decoded, sender_contact_id, &secrets)
|
||||
}
|
||||
|
||||
/// Try to decrypt a Double Ratchet encrypted message (0xDD prefix).
|
||||
/// Format: [0xDD] [RatchetHeader(40) + nonce(12) + ciphertext + tag(16)]
|
||||
/// Returns the decrypted typed wire bytes ([0x02][CBOR]) if successful.
|
||||
async fn try_decrypt_ratchet(
|
||||
decoded: &[u8],
|
||||
sender_contact_id: u32,
|
||||
state: &Arc<MeshState>,
|
||||
) -> Option<Vec<u8>> {
|
||||
if decoded.first() != Some(&message_types::RATCHET_TYPED_MARKER) {
|
||||
return None;
|
||||
}
|
||||
let ratchet_bytes = &decoded[1..]; // skip 0xDD marker
|
||||
|
||||
let ratchet_msg = match super::ratchet::RatchetMessage::from_bytes(ratchet_bytes) {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
warn!(contact_id = sender_contact_id, "Failed to parse ratchet message: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Look up peer DID for session manager
|
||||
let peer_did = state.peers.read().await
|
||||
.get(&sender_contact_id)
|
||||
.and_then(|p| p.did.clone())?;
|
||||
|
||||
match state.session_manager.decrypt_from_peer(&peer_did, &ratchet_msg).await {
|
||||
Ok(plaintext) => {
|
||||
debug!(contact_id = sender_contact_id, did = %peer_did, "Decrypted ratchet message (0xDD)");
|
||||
// The plaintext should be the original [0x02][CBOR] typed wire
|
||||
if TypedEnvelope::is_typed(&plaintext) {
|
||||
Some(plaintext)
|
||||
} else {
|
||||
// Could be nested stego → typed
|
||||
unwrap_wire_layers(&plaintext)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(contact_id = sender_contact_id, "Ratchet decrypt failed: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to base64-decode and decrypt a ratchet-encrypted message.
|
||||
/// Handles the case where ratchet messages arrive as base64 text.
|
||||
async fn try_decrypt_ratchet_base64(
|
||||
payload: &[u8],
|
||||
sender_contact_id: u32,
|
||||
state: &Arc<MeshState>,
|
||||
) -> Option<Vec<u8>> {
|
||||
use base64::Engine;
|
||||
let text = std::str::from_utf8(payload).ok()?;
|
||||
let decoded = base64::engine::general_purpose::STANDARD.decode(text.trim()).ok()?;
|
||||
if decoded.first() != Some(&message_types::RATCHET_TYPED_MARKER) {
|
||||
return None;
|
||||
}
|
||||
try_decrypt_ratchet(&decoded, sender_contact_id, state).await
|
||||
}
|
||||
|
||||
/// Unwrap wire layers: encrypted (0xEE) → stego (0xAA) → typed (0x02).
|
||||
/// Returns None if decoding fails at any layer (caller should use shared_secrets variant).
|
||||
fn unwrap_wire_layers(decoded: &[u8]) -> Option<Vec<u8>> {
|
||||
@ -958,7 +1030,19 @@ async fn try_chunk_reassemble(
|
||||
}
|
||||
|
||||
if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(&combined) {
|
||||
// Check for encrypted frame (0xEE) — decrypt then unwrap
|
||||
// Check for ratchet-encrypted frame (0xDD) — decrypt then unwrap
|
||||
if decoded.first() == Some(&message_types::RATCHET_TYPED_MARKER) {
|
||||
// Must drop buffer lock before calling async try_decrypt_ratchet
|
||||
let decoded_clone = decoded.clone();
|
||||
drop(buffer);
|
||||
if let Some(typed_wire) = try_decrypt_ratchet(&decoded_clone, sender_contact_id, state).await {
|
||||
info!(msg_id, chunks = total, total_len = typed_wire.len(), "Reassembled ratchet-encrypted chunked message");
|
||||
state.chunk_buffer.write().await.remove(&key);
|
||||
return Some(typed_wire);
|
||||
}
|
||||
buffer = state.chunk_buffer.write().await;
|
||||
}
|
||||
// Check for static-encrypted frame (0xEE) — decrypt then unwrap
|
||||
if decoded.first() == Some(&message_types::ENCRYPTED_TYPED_MARKER) {
|
||||
let secrets = state.shared_secrets.read().await;
|
||||
if let Some(typed_wire) = try_decrypt_typed(&decoded, sender_contact_id, &secrets) {
|
||||
@ -1599,9 +1683,69 @@ async fn send_confirmation_update(
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt a typed wire payload for a specific peer.
|
||||
/// Attempts ratchet encryption first (forward secrecy), falls back to static
|
||||
/// shared secret, falls back to plaintext if neither is available.
|
||||
/// Respects the encrypt_relay config toggle for rollback.
|
||||
async fn encrypt_for_peer(
|
||||
state: &Arc<MeshState>,
|
||||
contact_id: u32,
|
||||
typed_wire: &[u8],
|
||||
) -> Vec<u8> {
|
||||
if !state.encrypt_relay {
|
||||
return typed_wire.to_vec();
|
||||
}
|
||||
|
||||
// Look up peer DID for ratchet session
|
||||
let peer_did = state.peers.read().await
|
||||
.get(&contact_id)
|
||||
.and_then(|p| p.did.clone());
|
||||
|
||||
// Try ratchet encryption first (forward secrecy)
|
||||
if let Some(ref did) = peer_did {
|
||||
if state.session_manager.has_session(did).await {
|
||||
match state.session_manager.encrypt_for_peer(did, typed_wire).await {
|
||||
Ok(ratchet_msg) => {
|
||||
let ratchet_bytes = ratchet_msg.to_bytes();
|
||||
let mut buf = Vec::with_capacity(1 + ratchet_bytes.len());
|
||||
buf.push(message_types::RATCHET_TYPED_MARKER);
|
||||
buf.extend_from_slice(&ratchet_bytes);
|
||||
debug!(contact_id, did = %did, "Encrypted with Double Ratchet (0xDD)");
|
||||
return buf;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(contact_id, did = %did, "Ratchet encrypt failed, trying static: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to static shared secret (0xEE)
|
||||
let secrets = state.shared_secrets.read().await;
|
||||
if let Some(secret) = secrets.get(&contact_id) {
|
||||
match crypto::encrypt(secret, typed_wire) {
|
||||
Ok(ciphertext) => {
|
||||
let mut buf = Vec::with_capacity(1 + ciphertext.len());
|
||||
buf.push(message_types::ENCRYPTED_TYPED_MARKER);
|
||||
buf.extend_from_slice(&ciphertext);
|
||||
debug!(contact_id, "Encrypted with static shared secret (0xEE)");
|
||||
return buf;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(contact_id, "Static encrypt failed, sending plaintext: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No encryption available — send plaintext
|
||||
debug!(contact_id, "No encryption available, sending plaintext (0x02)");
|
||||
typed_wire.to_vec()
|
||||
}
|
||||
|
||||
/// Send raw wire bytes to a specific peer by contact_id.
|
||||
/// Falls back to channel 0 broadcast if peer's pubkey is unknown.
|
||||
async fn send_to_peer(state: &Arc<MeshState>, contact_id: u32, payload: Vec<u8>) {
|
||||
/// Encrypts directed messages via ratchet or shared secret when available.
|
||||
/// Falls back to channel 0 broadcast (plaintext) if peer's pubkey is unknown.
|
||||
async fn send_to_peer(state: &Arc<MeshState>, contact_id: u32, typed_wire: Vec<u8>) {
|
||||
let peers = state.peers.read().await;
|
||||
if let Some(peer) = peers.get(&contact_id) {
|
||||
if let Some(ref pk) = peer.pubkey_hex {
|
||||
@ -1610,6 +1754,8 @@ async fn send_to_peer(state: &Arc<MeshState>, contact_id: u32, payload: Vec<u8>)
|
||||
let mut prefix = [0u8; 6];
|
||||
prefix.copy_from_slice(&pk_bytes[..6]);
|
||||
drop(peers);
|
||||
// Encrypt for this specific peer before sending
|
||||
let payload = encrypt_for_peer(state, contact_id, &typed_wire).await;
|
||||
let _ = state.cmd_tx.send(MeshCommand::SendRaw {
|
||||
dest_pubkey_prefix: prefix,
|
||||
payload,
|
||||
@ -1620,9 +1766,10 @@ async fn send_to_peer(state: &Arc<MeshState>, contact_id: u32, payload: Vec<u8>)
|
||||
}
|
||||
}
|
||||
drop(peers);
|
||||
// Broadcast fallback — plaintext (no specific peer to encrypt for)
|
||||
let _ = state.cmd_tx.send(MeshCommand::BroadcastChannel {
|
||||
channel: 0,
|
||||
payload,
|
||||
payload: typed_wire,
|
||||
}).await;
|
||||
}
|
||||
|
||||
|
||||
@ -14,10 +14,14 @@ use serde::{Deserialize, Serialize};
|
||||
/// Wire prefix for typed messages.
|
||||
pub const TYPED_MESSAGE_MARKER: u8 = 0x02;
|
||||
|
||||
/// Wire prefix for encrypted typed messages (E2E encrypted with shared secret).
|
||||
/// Wire prefix for encrypted typed messages (E2E encrypted with static shared secret).
|
||||
/// Format: [0xEE] [nonce: 12 bytes] [ciphertext + auth tag]
|
||||
pub const ENCRYPTED_TYPED_MARKER: u8 = 0xEE;
|
||||
|
||||
/// Wire prefix for Double Ratchet encrypted typed messages (forward secrecy).
|
||||
/// Format: [0xDD] [RatchetHeader: 40 bytes] [nonce: 12] [ciphertext] [tag: 16]
|
||||
pub const RATCHET_TYPED_MARKER: u8 = 0xDD;
|
||||
|
||||
/// Message type discriminator.
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@ -73,6 +73,14 @@ pub struct MeshConfig {
|
||||
/// Steganographic encoding mode for mesh messages (Normal = disabled).
|
||||
#[serde(default)]
|
||||
pub steganography_mode: steganography::SteganographyMode,
|
||||
/// Encrypt directed relay messages (TX, Lightning, block headers) via ratchet or shared secret.
|
||||
/// Set to false to disable encryption for debugging or rollback.
|
||||
#[serde(default = "default_true")]
|
||||
pub encrypt_relay_messages: bool,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for MeshConfig {
|
||||
@ -86,6 +94,7 @@ impl Default for MeshConfig {
|
||||
mesh_only_mode: None,
|
||||
announce_block_headers: false,
|
||||
steganography_mode: steganography::SteganographyMode::Normal,
|
||||
encrypt_relay_messages: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,11 +171,14 @@ impl MeshService {
|
||||
|
||||
let block_header_cache = Arc::new(BlockHeaderCache::new());
|
||||
let relay_tracker = Arc::new(RelayTracker::new());
|
||||
let session_manager = Arc::new(session::SessionManager::new(data_dir));
|
||||
let (state, _rx, cmd_rx) = MeshState::new(
|
||||
&channel_name,
|
||||
Arc::clone(&block_header_cache),
|
||||
Some(Arc::clone(&relay_tracker)),
|
||||
config.steganography_mode,
|
||||
config.encrypt_relay_messages,
|
||||
Arc::clone(&session_manager),
|
||||
);
|
||||
|
||||
// Derive X25519 keys from Ed25519 identity
|
||||
|
||||
@ -892,20 +892,20 @@ MANIFEST_EOF
|
||||
# Bitcoin Knots: required for Mempool, ElectrumX, BTCPay, Fedimint
|
||||
TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)"
|
||||
|
||||
# Read per-installation Bitcoin RPC credentials from server secrets
|
||||
# Read Bitcoin RPC credentials from secrets file (rpcauth — stable across restarts)
|
||||
progress "Reading Bitcoin RPC credentials"
|
||||
BITCOIN_RPC_PASS=$(ssh $SSH_OPTS "$TARGET_HOST" '
|
||||
SECRETS_DIR="/var/lib/archipelago/secrets"
|
||||
sudo mkdir -p "$SECRETS_DIR" && sudo chmod 700 "$SECRETS_DIR"
|
||||
if [ ! -f "$SECRETS_DIR/bitcoin-rpc-password" ]; then
|
||||
openssl rand -base64 24 | sudo tee "$SECRETS_DIR/bitcoin-rpc-password" > /dev/null
|
||||
openssl rand -hex 16 | sudo tee "$SECRETS_DIR/bitcoin-rpc-password" > /dev/null
|
||||
sudo chmod 600 "$SECRETS_DIR/bitcoin-rpc-password"
|
||||
fi
|
||||
sudo cat "$SECRETS_DIR/bitcoin-rpc-password"
|
||||
' 2>/dev/null)
|
||||
BITCOIN_RPC_USER="archipelago"
|
||||
if [ -z "$BITCOIN_RPC_PASS" ]; then
|
||||
echo " WARNING: Could not read Bitcoin RPC password from server, aborting container fixes"
|
||||
echo " WARNING: Could not read Bitcoin RPC password from server"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@ -968,7 +968,6 @@ MANIFEST_EOF
|
||||
docker.io/bitcoinknots/bitcoin:latest \
|
||||
-server=1 \$BTC_EXTRA_ARGS \
|
||||
-rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 \
|
||||
-rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS \
|
||||
-dbcache=\$BTC_DBCACHE
|
||||
echo ' Bitcoin Knots started (sync may take hours)'
|
||||
else
|
||||
@ -1115,7 +1114,7 @@ MANIFEST_EOF
|
||||
-e NBXPLORER_CHAINS=btc \
|
||||
-e NBXPLORER_BIND=0.0.0.0:32838 \
|
||||
-e NBXPLORER_BTCRPCURL=http://bitcoin-knots:8332 \
|
||||
-e NBXPLORER_BTCRPCUSER=archipelago \
|
||||
-e NBXPLORER_BTCRPCUSER=$BITCOIN_RPC_USER \
|
||||
-e NBXPLORER_BTCRPCPASSWORD=$BITCOIN_RPC_PASS \
|
||||
-e NBXPLORER_POSTGRES='User ID=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=nbxplorer;Include Error Detail=true' \
|
||||
docker.io/nicolasdorier/nbxplorer:2.6.0
|
||||
@ -1427,13 +1426,20 @@ autopilot.active=false
|
||||
LNDCONF
|
||||
sudo cp /tmp/lnd.conf /var/lib/archipelago/lnd/lnd.conf
|
||||
else
|
||||
# Fix stale LND configs from older installs (localhost → container DNS, old password → current)
|
||||
# Fix stale LND configs (cookie mode, localhost, wrong password)
|
||||
LND_CONF=/var/lib/archipelago/lnd/lnd.conf
|
||||
if grep -q "rpchost=127.0.0.1" "$LND_CONF" 2>/dev/null || grep -q "rpcpass=archipelago123" "$LND_CONF" 2>/dev/null; then
|
||||
echo " Fixing stale LND config (rpchost/rpcpass)..."
|
||||
NEEDS_FIX=0
|
||||
grep -q "rpccookie" "$LND_CONF" 2>/dev/null && NEEDS_FIX=1
|
||||
grep -q "rpchost=127.0.0.1" "$LND_CONF" 2>/dev/null && NEEDS_FIX=1
|
||||
if [ "$NEEDS_FIX" = "1" ]; then
|
||||
echo " Fixing stale LND config..."
|
||||
cp "$LND_CONF" /tmp/lnd.conf.fix
|
||||
sed -i "/bitcoind.rpccookie/d" /tmp/lnd.conf.fix
|
||||
sed -i "/bitcoind.rpcuser/d" /tmp/lnd.conf.fix
|
||||
sed -i "/bitcoind.rpcpass/d" /tmp/lnd.conf.fix
|
||||
sed -i "s|bitcoind.rpchost=127.0.0.1:8332|bitcoind.rpchost=bitcoin-knots:8332|" /tmp/lnd.conf.fix
|
||||
sed -i "s|bitcoind.rpcpass=archipelago123|bitcoind.rpcpass=$BITCOIN_RPC_PASS|" /tmp/lnd.conf.fix
|
||||
sed -i "/bitcoind.rpchost=/a bitcoind.rpcuser=$BITCOIN_RPC_USER" /tmp/lnd.conf.fix
|
||||
sed -i "/bitcoind.rpcuser=/a bitcoind.rpcpass=$BITCOIN_RPC_PASS" /tmp/lnd.conf.fix
|
||||
sudo cp /tmp/lnd.conf.fix "$LND_CONF"
|
||||
sudo chown 100000:100000 "$LND_CONF"
|
||||
RESTART_LND=1
|
||||
|
||||
@ -35,16 +35,43 @@ wait_for_container() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Generate per-installation credentials if not already saved
|
||||
# rpcauth: password hash in bitcoin.conf, plaintext in secrets file only.
|
||||
# Credentials are STABLE across reboots, restarts, and deploys.
|
||||
SECRETS_DIR="/var/lib/archipelago/secrets"
|
||||
mkdir -p "$SECRETS_DIR" && chmod 700 "$SECRETS_DIR"
|
||||
if [ ! -f "$SECRETS_DIR/bitcoin-rpc-password" ]; then
|
||||
openssl rand -base64 24 > "$SECRETS_DIR/bitcoin-rpc-password"
|
||||
openssl rand -hex 16 > "$SECRETS_DIR/bitcoin-rpc-password"
|
||||
chmod 600 "$SECRETS_DIR/bitcoin-rpc-password"
|
||||
fi
|
||||
BITCOIN_RPC_USER="archipelago"
|
||||
BITCOIN_RPC_PASS=$(cat "$SECRETS_DIR/bitcoin-rpc-password")
|
||||
|
||||
# Generate rpcauth line for bitcoin.conf (salted HMAC-SHA256 hash)
|
||||
generate_rpcauth() {
|
||||
local user="$1" pass="$2"
|
||||
local salt=$(openssl rand -hex 16)
|
||||
local hash=$(echo -n "$pass" | openssl dgst -sha256 -hmac "$salt" -hex 2>/dev/null | awk '{print $NF}')
|
||||
echo "${user}:${salt}\$${hash}"
|
||||
}
|
||||
|
||||
# Write bitcoin.conf with rpcauth if not exists or needs update
|
||||
BITCOIN_CONF="/var/lib/archipelago/bitcoin/bitcoin.conf"
|
||||
if [ ! -f "$BITCOIN_CONF" ] || ! grep -q "^rpcauth=" "$BITCOIN_CONF" 2>/dev/null; then
|
||||
mkdir -p /var/lib/archipelago/bitcoin
|
||||
RPCAUTH=$(generate_rpcauth "$BITCOIN_RPC_USER" "$BITCOIN_RPC_PASS")
|
||||
cat > "$BITCOIN_CONF" << BTCCONF
|
||||
# rpcauth: salted hash only — no plaintext password in config or CLI
|
||||
rpcauth=${RPCAUTH}
|
||||
server=1
|
||||
rpcbind=0.0.0.0
|
||||
rpcallowip=0.0.0.0/0
|
||||
rpcport=8332
|
||||
listen=1
|
||||
printtoconsole=1
|
||||
BTCCONF
|
||||
log "Generated bitcoin.conf with rpcauth (no plaintext credentials)"
|
||||
fi
|
||||
|
||||
# Generate per-installation database passwords if not already saved
|
||||
for svc in mempool btcpay immich penpot mysql-root; do
|
||||
if [ ! -f "$SECRETS_DIR/${svc}-db-password" ]; then
|
||||
@ -226,8 +253,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch
|
||||
docker.io/bitcoinknots/bitcoin:latest \
|
||||
-server=1 $BTC_EXTRA_ARGS \
|
||||
-rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 \
|
||||
-rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS \
|
||||
-proxy=127.0.0.1:9050 -listen=1 -bind=0.0.0.0:8333 \
|
||||
-proxy=host.containers.internal:9050 -listen=1 -bind=0.0.0.0:8333 \
|
||||
-dbcache=$BTC_DBCACHE 2>>"$LOG"; then
|
||||
log "Bitcoin Knots started"
|
||||
else
|
||||
@ -237,7 +263,7 @@ else
|
||||
$DOCKER network connect archy-net bitcoin-knots 2>/dev/null || true
|
||||
log "Bitcoin Knots already running"
|
||||
fi
|
||||
# Wait for Bitcoin Knots RPC to be responsive (LND, NBXplorer, mempool depend on it)
|
||||
# Wait for Bitcoin Knots RPC to be responsive
|
||||
wait_for_container "Bitcoin Knots RPC" "$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS getblockchaininfo" 60
|
||||
|
||||
# Ensure wallet exists (Bitcoin Knots no longer auto-creates a default wallet)
|
||||
@ -270,7 +296,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q electrumx; then
|
||||
mkdir -p /var/lib/archipelago/electrumx
|
||||
$DOCKER run -d --name electrumx --restart unless-stopped --memory=$(mem_limit electrumx) --network archy-net \
|
||||
-p 50001:50001 -v /var/lib/archipelago/electrumx:/data \
|
||||
-e DAEMON_URL=http://$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@bitcoin-knots:8332/ \
|
||||
-e "DAEMON_URL=http://$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@bitcoin-knots:8332/" \
|
||||
-e COIN=Bitcoin -e DB_DIRECTORY=/data \
|
||||
-e SERVICES=tcp://:50001,rpc://0.0.0.0:8000 \
|
||||
docker.io/lukechilds/electrumx:v1.18.0 2>>"$LOG" || true
|
||||
@ -284,7 +310,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-api; then
|
||||
-p 8999:8999 -v /var/lib/archipelago/mempool:/data \
|
||||
-e MEMPOOL_BACKEND=electrum -e ELECTRUM_HOST=electrumx -e ELECTRUM_PORT=50001 \
|
||||
-e ELECTRUM_TLS_ENABLED=false -e CORE_RPC_HOST="$TARGET_IP" -e CORE_RPC_PORT=8332 \
|
||||
-e CORE_RPC_USERNAME=$BITCOIN_RPC_USER -e CORE_RPC_PASSWORD=$BITCOIN_RPC_PASS \
|
||||
-e "CORE_RPC_USERNAME=$BITCOIN_RPC_USER" -e "CORE_RPC_PASSWORD=$BITCOIN_RPC_PASS" \
|
||||
-e DATABASE_ENABLED=true -e DATABASE_HOST="$MYSQL_CNT" -e DATABASE_DATABASE=mempool \
|
||||
-e DATABASE_USERNAME=mempool -e DATABASE_PASSWORD=$MEMPOOL_DB_PASS \
|
||||
docker.io/mempool/backend:v2.5.0 2>>"$LOG" || true
|
||||
@ -358,7 +384,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then
|
||||
-e BTCPAY_HOST="$TARGET_IP:23000" -e BTCPAY_CHAINS=btc \
|
||||
-e BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838 \
|
||||
-e BTCPAY_BTCRPCURL=http://bitcoin-knots:8332 \
|
||||
-e BTCPAY_BTCRPCUSER=$BITCOIN_RPC_USER -e BTCPAY_BTCRPCPASSWORD=$BITCOIN_RPC_PASS \
|
||||
-e "BTCPAY_BTCRPCUSER=$BITCOIN_RPC_USER" -e "BTCPAY_BTCRPCPASSWORD=$BITCOIN_RPC_PASS" \
|
||||
-e BTCPAY_POSTGRES='User ID=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true' \
|
||||
docker.io/btcpayserver/btcpayserver:1.13.5 2>>"$LOG" || true
|
||||
fi
|
||||
@ -371,7 +397,7 @@ sleep 5 # Let databases stabilize
|
||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE '^lnd$'; then
|
||||
log "Creating LND..."
|
||||
mkdir -p /var/lib/archipelago/lnd
|
||||
# Create lnd.conf so LND auto-connects to Bitcoin Knots via archy-net
|
||||
# Create lnd.conf with rpcauth credentials (stable across restarts)
|
||||
if [ ! -f /var/lib/archipelago/lnd/lnd.conf ]; then
|
||||
cat > /var/lib/archipelago/lnd/lnd.conf <<LNDCONF
|
||||
[Application Options]
|
||||
@ -381,7 +407,7 @@ restlisten=0.0.0.0:8080
|
||||
debuglevel=info
|
||||
noseedbackup=true
|
||||
tor.active=true
|
||||
tor.socks=127.0.0.1:9050
|
||||
tor.socks=host.containers.internal:9050
|
||||
tor.streamisolation=true
|
||||
|
||||
[Bitcoin]
|
||||
@ -390,7 +416,7 @@ bitcoin.node=bitcoind
|
||||
|
||||
[Bitcoind]
|
||||
bitcoind.rpchost=bitcoin-knots:8332
|
||||
bitcoind.rpcuser=archipelago
|
||||
bitcoind.rpcuser=$BITCOIN_RPC_USER
|
||||
bitcoind.rpcpass=$BITCOIN_RPC_PASS
|
||||
bitcoind.rpcpolling=true
|
||||
bitcoind.estimatemode=ECONOMICAL
|
||||
@ -398,7 +424,7 @@ bitcoind.estimatemode=ECONOMICAL
|
||||
[autopilot]
|
||||
autopilot.active=false
|
||||
LNDCONF
|
||||
log "LND config created (archy-net → bitcoin-knots:8332, rpcpolling)"
|
||||
log "LND config created (rpcauth credentials, Tor via system)"
|
||||
fi
|
||||
$DOCKER run -d --name lnd --restart unless-stopped --memory=$(mem_limit lnd) --network archy-net \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
@ -417,7 +443,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint; then
|
||||
--security-opt no-new-privileges:true \
|
||||
-p 8173:8173 -p 8174:8174 -p 8175:8175 \
|
||||
-v /var/lib/archipelago/fedimint:/data \
|
||||
-e FM_DATA_DIR=/data -e FM_BITCOIND_USERNAME=$BITCOIN_RPC_USER -e FM_BITCOIND_PASSWORD=$BITCOIN_RPC_PASS \
|
||||
-e FM_DATA_DIR=/data -e "FM_BITCOIND_USERNAME=$BITCOIN_RPC_USER" -e "FM_BITCOIND_PASSWORD=$BITCOIN_RPC_PASS" \
|
||||
-e FM_BITCOIN_NETWORK=bitcoin -e FM_BIND_P2P=0.0.0.0:8173 \
|
||||
-e FM_BIND_API=0.0.0.0:8174 -e FM_BIND_UI=0.0.0.0:8175 \
|
||||
-e FM_P2P_URL=fedimint://"$TARGET_IP":8173 -e FM_API_URL=ws://"$TARGET_IP":8174 \
|
||||
@ -445,7 +471,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
|
||||
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
|
||||
--bcrypt-password-hash "$FEDI_HASH" \
|
||||
--network bitcoin --bitcoind-url http://"$TARGET_IP":8332 \
|
||||
--bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS \
|
||||
--bitcoind-username "$BITCOIN_RPC_USER" --bitcoind-password "$BITCOIN_RPC_PASS" \
|
||||
lnd --lnd-rpc-host "$TARGET_IP":10009 --lnd-tls-cert /lnd/tls.cert --lnd-macaroon /lnd/admin.macaroon 2>>"$LOG" || true
|
||||
else
|
||||
log " No LND found — using ldk (built-in Lightning)"
|
||||
@ -458,7 +484,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
|
||||
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
|
||||
--bcrypt-password-hash "$FEDI_HASH" \
|
||||
--network bitcoin --bitcoind-url http://"$TARGET_IP":8332 \
|
||||
--bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS \
|
||||
--bitcoind-username "$BITCOIN_RPC_USER" --bitcoind-password "$BITCOIN_RPC_PASS" \
|
||||
ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway 2>>"$LOG" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user