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:
Dorian
2026-03-20 11:56:20 +00:00
co-authored by Claude Opus 4.6
parent adf0aa465f
commit 6f5188ef7f
8 changed files with 278 additions and 60 deletions
+151 -4
View File
@@ -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;
}
+5 -1
View File
@@ -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)]
+12
View File
@@ -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