From 46b2f60f1a79eef8fe2f8b2295cc1d216fd28b05 Mon Sep 17 00:00:00 2001 From: JHB <16675200+jharveyb@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:38:18 -0500 Subject: [PATCH] Add Tor support for outbound connections via SOCKS --- bindings/ldk_node.udl | 9 ++ src/builder.rs | 40 ++++++++- src/config.rs | 18 ++++ src/connection.rs | 185 ++++++++++++++++++++++++++++++++++++------ src/lib.rs | 2 + 5 files changed, 226 insertions(+), 28 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d40f72f4a..9921bd2a0 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -13,6 +13,7 @@ dictionary Config { u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; RouteParametersConfig? route_parameters; + TorConfig? tor_config; }; dictionary AnchorChannelsConfig { @@ -57,6 +58,11 @@ dictionary LSPS2ServiceConfig { boolean client_trusts_lsp; }; +dictionary TorConfig { + SocketAddress proxy_address; + boolean proxy_all_outbound; +}; + interface NodeEntropy { [Name=from_bip39_mnemonic] constructor(Mnemonic mnemonic, string? passphrase); @@ -126,6 +132,8 @@ interface Builder { [Throws=BuildError] void set_announcement_addresses(sequence announcement_addresses); [Throws=BuildError] + void set_tor_config(TorConfig tor_config); + [Throws=BuildError] void set_node_alias(string node_alias); [Throws=BuildError] void set_async_payments_role(AsyncPaymentsRole? role); @@ -388,6 +396,7 @@ enum BuildError { "InvalidChannelMonitor", "InvalidListeningAddresses", "InvalidAnnouncementAddresses", + "InvalidTorProxyAddress", "InvalidNodeAlias", "RuntimeSetupFailed", "ReadFailed", diff --git a/src/builder.rs b/src/builder.rs index a2ea9aea7..5a198707d 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -45,7 +45,7 @@ use vss_client::headers::VssHeaderProvider; use crate::chain::ChainSource; use crate::config::{ default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, - BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, + BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, TorConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, }; use crate::connection::ConnectionManager; @@ -163,6 +163,8 @@ pub enum BuildError { InvalidListeningAddresses, /// The given announcement addresses are invalid, e.g. too many were passed. InvalidAnnouncementAddresses, + /// The given tor proxy address is invalid, e.g. an onion address was passed. + InvalidTorProxyAddress, /// The provided alias is invalid. InvalidNodeAlias, /// An attempt to setup a runtime has failed. @@ -204,6 +206,7 @@ impl fmt::Display for BuildError { Self::InvalidAnnouncementAddresses => { write!(f, "Given announcement addresses are invalid.") }, + Self::InvalidTorProxyAddress => write!(f, "Given Tor proxy address is invalid."), Self::RuntimeSetupFailed => write!(f, "Failed to setup a runtime."), Self::ReadFailed => write!(f, "Failed to read from store."), Self::WriteFailed => write!(f, "Failed to write to store."), @@ -521,6 +524,23 @@ impl NodeBuilder { Ok(self) } + /// Configures the [`Node`] instance to use a Tor SOCKS proxy for some (or all) outbound connections. + /// The proxy address must not itself be an onion address. + /// + /// + /// **Note**: If unset, connecting to peer OnionV3 addresses will fail. + pub fn set_tor_config(&mut self, tor_config: TorConfig) -> Result<&mut Self, BuildError> { + match tor_config.proxy_address { + SocketAddress::OnionV2 { .. } | SocketAddress::OnionV3 { .. } => { + return Err(BuildError::InvalidTorProxyAddress); + }, + _ => {}, + } + + self.config.tor_config = Some(tor_config); + Ok(self) + } + /// Sets the node alias that will be used when broadcasting announcements to the gossip /// network. /// @@ -918,6 +938,14 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_announcement_addresses(announcement_addresses).map(|_| ()) } + /// Configures the [`Node`] instance to use a Tor SOCKS proxy for some (or all) outbound connections. + /// The proxy address must not itself be an onion address. + /// + /// **Note**: If unset, connecting to peer OnionV3 addresses will fail. + pub fn set_tor_config(&self, tor_config: TorConfig) -> Result<(), BuildError> { + self.inner.write().unwrap().set_tor_config(tor_config).map(|_| ()) + } + /// Sets the node alias that will be used when broadcasting announcements to the gossip /// network. /// @@ -1711,8 +1739,14 @@ fn build_with_store_internal( liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::downgrade(&peer_manager))); - let connection_manager = - Arc::new(ConnectionManager::new(Arc::clone(&peer_manager), Arc::clone(&logger))); + // Use a different RNG seed for the ConnectionManager + let ephemeral_bytes: [u8; 32] = keys_manager.get_secure_random_bytes(); + let connection_manager = Arc::new(ConnectionManager::new( + Arc::clone(&peer_manager), + config.tor_config.clone(), + ephemeral_bytes, + Arc::clone(&logger), + )); let output_sweeper = match sweeper_bytes_res { Ok(output_sweeper) => Arc::new(output_sweeper), diff --git a/src/config.rs b/src/config.rs index 1dfa66176..931df6004 100644 --- a/src/config.rs +++ b/src/config.rs @@ -192,6 +192,13 @@ pub struct Config { /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. pub route_parameters: Option, + /// Configuration options for enabling peer connections via the Tor network. + /// + /// Setting [`TorConfig`] enables connecting to Tor-only peers. Please refer to [`TorConfig`] + /// for further information. + /// + /// **Note**: If unset, connecting to peer OnionV3 addresses will fail. + pub tor_config: Option, } impl Default for Config { @@ -204,6 +211,7 @@ impl Default for Config { trusted_peers_0conf: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, anchor_channels_config: Some(AnchorChannelsConfig::default()), + tor_config: None, route_parameters: None, node_alias: None, } @@ -478,6 +486,16 @@ pub struct BitcoindRestClientConfig { pub rest_port: u16, } +/// Configuration for connecting to peers via the Tor Network. +#[derive(Debug, Clone)] +pub struct TorConfig { + /// Tor daemon SOCKS proxy address. + pub proxy_address: SocketAddress, + + /// If set, all outbound peer connections will be made via the Tor SOCKS proxy. + pub proxy_all_outbound: bool, +} + /// Options which apply on a per-channel basis and may change at runtime or based on negotiation /// with our counterparty. #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/src/connection.rs b/src/connection.rs index e3a25f357..57a658655 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -13,7 +13,9 @@ use std::time::Duration; use bitcoin::secp256k1::PublicKey; use lightning::ln::msgs::SocketAddress; +use lightning::sign::RandomBytes; +use crate::config::TorConfig; use crate::logger::{log_error, log_info, LdkLogger}; use crate::types::PeerManager; use crate::Error; @@ -25,6 +27,8 @@ where pending_connections: Mutex>>>>, peer_manager: Arc, + tor_proxy_config: Option, + tor_proxy_rng: Arc, logger: L, } @@ -32,9 +36,14 @@ impl ConnectionManager where L::Target: LdkLogger, { - pub(crate) fn new(peer_manager: Arc, logger: L) -> Self { + pub(crate) fn new( + peer_manager: Arc, tor_proxy_config: Option, + ephemeral_random_data: [u8; 32], logger: L, + ) -> Self { let pending_connections = Mutex::new(HashMap::new()); - Self { pending_connections, peer_manager, logger } + let tor_proxy_rng = Arc::new(RandomBytes::new(ephemeral_random_data)); + + Self { pending_connections, peer_manager, tor_proxy_config, tor_proxy_rng, logger } } pub(crate) async fn connect_peer_if_necessary( @@ -64,27 +73,157 @@ where log_info!(self.logger, "Connecting to peer: {}@{}", node_id, addr); - let socket_addr = addr - .to_socket_addrs() - .map_err(|e| { - log_error!(self.logger, "Failed to resolve network address {}: {}", addr, e); - self.propagate_result_to_subscribers(&node_id, Err(Error::InvalidSocketAddress)); - Error::InvalidSocketAddress - })? - .next() - .ok_or_else(|| { - log_error!(self.logger, "Failed to resolve network address {}", addr); + let res = match addr { + SocketAddress::OnionV2(old_onion_addr) => { + log_error!( + self.logger, + "Failed to resolve network address {:?}: Resolution of OnionV2 addresses is currently unsupported.", + old_onion_addr + ); self.propagate_result_to_subscribers(&node_id, Err(Error::InvalidSocketAddress)); - Error::InvalidSocketAddress - })?; + return Err(Error::InvalidSocketAddress); + }, + SocketAddress::OnionV3 { .. } => { + let proxy_config = self.tor_proxy_config.as_ref().ok_or_else(|| { + log_error!( + self.logger, + "Failed to resolve network address {:?}: Tor usage is not configured.", + addr + ); + self.propagate_result_to_subscribers( + &node_id, + Err(Error::InvalidSocketAddress), + ); + Error::InvalidSocketAddress + })?; + let proxy_addr = proxy_config + .proxy_address + .to_socket_addrs() + .map_err(|e| { + log_error!( + self.logger, + "Failed to resolve Tor proxy network address {}: {}", + proxy_config.proxy_address, + e + ); + self.propagate_result_to_subscribers( + &node_id, + Err(Error::InvalidSocketAddress), + ); + Error::InvalidSocketAddress + })? + .next() + .ok_or_else(|| { + log_error!( + self.logger, + "Failed to resolve Tor proxy network address {}", + proxy_config.proxy_address + ); + self.propagate_result_to_subscribers( + &node_id, + Err(Error::InvalidSocketAddress), + ); + Error::InvalidSocketAddress + })?; + let connection_future = lightning_net_tokio::tor_connect_outbound( + Arc::clone(&self.peer_manager), + node_id, + addr.clone(), + proxy_addr, + Arc::clone(&self.tor_proxy_rng), + ); + self.await_connection(connection_future, node_id, addr).await + }, + _ => { + let socket_addr = addr + .to_socket_addrs() + .map_err(|e| { + log_error!( + self.logger, + "Failed to resolve network address {}: {}", + addr, + e + ); + self.propagate_result_to_subscribers( + &node_id, + Err(Error::InvalidSocketAddress), + ); + Error::InvalidSocketAddress + })? + .next() + .ok_or_else(|| { + log_error!(self.logger, "Failed to resolve network address {}", addr); + self.propagate_result_to_subscribers( + &node_id, + Err(Error::InvalidSocketAddress), + ); + Error::InvalidSocketAddress + })?; + match &self.tor_proxy_config { + None | Some(TorConfig { proxy_all_outbound: false, .. }) => { + let connection_future = lightning_net_tokio::connect_outbound( + Arc::clone(&self.peer_manager), + node_id, + socket_addr, + ); + self.await_connection(connection_future, node_id, addr).await + }, + Some(proxy_config) => { + let proxy_addr = proxy_config + .proxy_address + .to_socket_addrs() + .map_err(|e| { + log_error!( + self.logger, + "Failed to resolve Tor proxy network address {}: {}", + proxy_config.proxy_address, + e + ); + self.propagate_result_to_subscribers( + &node_id, + Err(Error::InvalidSocketAddress), + ); + Error::InvalidSocketAddress + })? + .next() + .ok_or_else(|| { + log_error!( + self.logger, + "Failed to resolve Tor proxy network address {}", + proxy_config.proxy_address + ); + self.propagate_result_to_subscribers( + &node_id, + Err(Error::InvalidSocketAddress), + ); + Error::InvalidSocketAddress + })?; + let connection_future = lightning_net_tokio::tor_connect_outbound( + Arc::clone(&self.peer_manager), + node_id, + addr.clone(), + proxy_addr, + Arc::clone(&self.tor_proxy_rng), + ); + self.await_connection(connection_future, node_id, addr).await + }, + } + }, + }; + + self.propagate_result_to_subscribers(&node_id, res); - let connection_future = lightning_net_tokio::connect_outbound( - Arc::clone(&self.peer_manager), - node_id, - socket_addr, - ); + res + } - let res = match connection_future.await { + async fn await_connection( + &self, connection_future: F, node_id: PublicKey, addr: SocketAddress, + ) -> Result<(), Error> + where + F: std::future::Future>, + CF: std::future::Future, + { + match connection_future.await { Some(connection_closed_future) => { let mut connection_closed_future = Box::pin(connection_closed_future); loop { @@ -106,11 +245,7 @@ where log_error!(self.logger, "Failed to connect to peer: {}@{}", node_id, addr); Err(Error::ConnectionFailed) }, - }; - - self.propagate_result_to_subscribers(&node_id, res); - - res + } } fn register_or_subscribe_pending_connection( diff --git a/src/lib.rs b/src/lib.rs index 1b93cb6e9..808359ffa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,6 +122,8 @@ pub use builder::BuildError; #[cfg(not(feature = "uniffi"))] pub use builder::NodeBuilder as Builder; use chain::ChainSource; +#[cfg(feature = "uniffi")] +use config::TorConfig; use config::{ default_user_config, may_announce_channel, AsyncPaymentsRole, ChannelConfig, Config, NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL,