Skip to content

Commit 804f00f

Browse files
authored
Merge pull request #778 from jharveyb/2026-01-tor-support-outbound-peers
Add Tor support for outbound connections via SOCKS
2 parents 4e4425e + 37dd9e2 commit 804f00f

File tree

5 files changed

+186
-31
lines changed

5 files changed

+186
-31
lines changed

bindings/ldk_node.udl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ typedef dictionary EsploraSyncConfig;
99

1010
typedef dictionary ElectrumSyncConfig;
1111

12+
typedef dictionary TorConfig;
13+
1214
typedef interface NodeEntropy;
1315

1416
typedef enum WordCount;
@@ -53,6 +55,8 @@ interface Builder {
5355
[Throws=BuildError]
5456
void set_announcement_addresses(sequence<SocketAddress> announcement_addresses);
5557
[Throws=BuildError]
58+
void set_tor_config(TorConfig tor_config);
59+
[Throws=BuildError]
5660
void set_node_alias(string node_alias);
5761
[Throws=BuildError]
5862
void set_async_payments_role(AsyncPaymentsRole? role);

src/builder.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use vss_client::headers::VssHeaderProvider;
4545
use crate::chain::ChainSource;
4646
use crate::config::{
4747
default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole,
48-
BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig,
48+
BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, TorConfig,
4949
DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL,
5050
};
5151
use crate::connection::ConnectionManager;
@@ -165,6 +165,8 @@ pub enum BuildError {
165165
InvalidListeningAddresses,
166166
/// The given announcement addresses are invalid, e.g. too many were passed.
167167
InvalidAnnouncementAddresses,
168+
/// The given tor proxy address is invalid, e.g. an onion address was passed.
169+
InvalidTorProxyAddress,
168170
/// The provided alias is invalid.
169171
InvalidNodeAlias,
170172
/// An attempt to setup a runtime has failed.
@@ -206,6 +208,7 @@ impl fmt::Display for BuildError {
206208
Self::InvalidAnnouncementAddresses => {
207209
write!(f, "Given announcement addresses are invalid.")
208210
},
211+
Self::InvalidTorProxyAddress => write!(f, "Given Tor proxy address is invalid."),
209212
Self::RuntimeSetupFailed => write!(f, "Failed to setup a runtime."),
210213
Self::ReadFailed => write!(f, "Failed to read from store."),
211214
Self::WriteFailed => write!(f, "Failed to write to store."),
@@ -523,6 +526,23 @@ impl NodeBuilder {
523526
Ok(self)
524527
}
525528

529+
/// Configures the [`Node`] instance to use a Tor SOCKS proxy for outbound connections to peers with OnionV3 addresses.
530+
/// Connections to clearnet addresses are not affected, and are not made over Tor.
531+
/// The proxy address must not itself be an onion address.
532+
///
533+
/// **Note**: If unset, connecting to peer OnionV3 addresses will fail.
534+
pub fn set_tor_config(&mut self, tor_config: TorConfig) -> Result<&mut Self, BuildError> {
535+
match tor_config.proxy_address {
536+
SocketAddress::OnionV2 { .. } | SocketAddress::OnionV3 { .. } => {
537+
return Err(BuildError::InvalidTorProxyAddress);
538+
},
539+
_ => {},
540+
}
541+
542+
self.config.tor_config = Some(tor_config);
543+
Ok(self)
544+
}
545+
526546
/// Sets the node alias that will be used when broadcasting announcements to the gossip
527547
/// network.
528548
///
@@ -957,6 +977,15 @@ impl ArcedNodeBuilder {
957977
self.inner.write().unwrap().set_announcement_addresses(announcement_addresses).map(|_| ())
958978
}
959979

980+
/// Configures the [`Node`] instance to use a Tor SOCKS proxy for outbound connections to peers with OnionV3 addresses.
981+
/// Connections to clearnet addresses are not affected, and are not made over Tor.
982+
/// The proxy address must not itself be an onion address.
983+
///
984+
/// **Note**: If unset, connecting to peer OnionV3 addresses will fail.
985+
pub fn set_tor_config(&self, tor_config: TorConfig) -> Result<(), BuildError> {
986+
self.inner.write().unwrap().set_tor_config(tor_config).map(|_| ())
987+
}
988+
960989
/// Sets the node alias that will be used when broadcasting announcements to the gossip
961990
/// network.
962991
///
@@ -1146,6 +1175,15 @@ fn build_with_store_internal(
11461175
}
11471176
}
11481177

1178+
if let Some(tor_config) = &config.tor_config {
1179+
match tor_config.proxy_address {
1180+
SocketAddress::OnionV2 { .. } | SocketAddress::OnionV3 { .. } => {
1181+
return Err(BuildError::InvalidTorProxyAddress);
1182+
},
1183+
_ => {},
1184+
}
1185+
}
1186+
11491187
let tx_broadcaster = Arc::new(TransactionBroadcaster::new(Arc::clone(&logger)));
11501188
let fee_estimator = Arc::new(OnchainFeeEstimator::new());
11511189

@@ -1779,8 +1817,12 @@ fn build_with_store_internal(
17791817

17801818
liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::downgrade(&peer_manager)));
17811819

1782-
let connection_manager =
1783-
Arc::new(ConnectionManager::new(Arc::clone(&peer_manager), Arc::clone(&logger)));
1820+
let connection_manager = Arc::new(ConnectionManager::new(
1821+
Arc::clone(&peer_manager),
1822+
config.tor_config.clone(),
1823+
Arc::clone(&keys_manager),
1824+
Arc::clone(&logger),
1825+
));
17841826

17851827
let output_sweeper = match sweeper_bytes_res {
17861828
Ok(output_sweeper) => Arc::new(output_sweeper),

src/config.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ pub(crate) const LNURL_AUTH_TIMEOUT_SECS: u64 = 15;
131131
/// | `probing_liquidity_limit_multiplier` | 3 |
132132
/// | `log_level` | Debug |
133133
/// | `anchor_channels_config` | Some(..) |
134-
/// | `route_parameters` | None |
134+
/// | `route_parameters` | None |
135+
/// | `tor_config` | None |
135136
///
136137
/// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their
137138
/// respective default values.
@@ -196,6 +197,13 @@ pub struct Config {
196197
/// **Note:** If unset, default parameters will be used, and you will be able to override the
197198
/// parameters on a per-payment basis in the corresponding method calls.
198199
pub route_parameters: Option<RouteParametersConfig>,
200+
/// Configuration options for enabling peer connections via the Tor network.
201+
///
202+
/// Setting [`TorConfig`] enables connecting to peers with OnionV3 addresses. No other connections
203+
/// are routed via Tor. Please refer to [`TorConfig`] for further information.
204+
///
205+
/// **Note**: If unset, connecting to peer OnionV3 addresses will fail.
206+
pub tor_config: Option<TorConfig>,
199207
}
200208

201209
impl Default for Config {
@@ -208,6 +216,7 @@ impl Default for Config {
208216
trusted_peers_0conf: Vec::new(),
209217
probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER,
210218
anchor_channels_config: Some(AnchorChannelsConfig::default()),
219+
tor_config: None,
211220
route_parameters: None,
212221
node_alias: None,
213222
}
@@ -487,6 +496,16 @@ pub struct BitcoindRestClientConfig {
487496
pub rest_port: u16,
488497
}
489498

499+
/// Configuration for connecting to peers via the Tor Network.
500+
#[derive(Debug, Clone)]
501+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
502+
pub struct TorConfig {
503+
/// Tor daemon SOCKS proxy address. Only connections to OnionV3 peers will be made
504+
/// via this proxy; other connections (IPv4 peers, Electrum server) will not be
505+
/// routed over Tor.
506+
pub proxy_address: SocketAddress,
507+
}
508+
490509
/// Options which apply on a per-channel basis and may change at runtime or based on negotiation
491510
/// with our counterparty.
492511
#[derive(Copy, Clone, Debug, PartialEq, Eq)]

src/connection.rs

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ use std::time::Duration;
1414
use bitcoin::secp256k1::PublicKey;
1515
use lightning::ln::msgs::SocketAddress;
1616

17+
use crate::config::TorConfig;
1718
use crate::logger::{log_error, log_info, LdkLogger};
18-
use crate::types::PeerManager;
19+
use crate::types::{KeysManager, PeerManager};
1920
use crate::Error;
2021

2122
pub(crate) struct ConnectionManager<L: Deref + Clone + Sync + Send>
@@ -25,16 +26,22 @@ where
2526
pending_connections:
2627
Mutex<HashMap<PublicKey, Vec<tokio::sync::oneshot::Sender<Result<(), Error>>>>>,
2728
peer_manager: Arc<PeerManager>,
29+
tor_proxy_config: Option<TorConfig>,
30+
keys_manager: Arc<KeysManager>,
2831
logger: L,
2932
}
3033

3134
impl<L: Deref + Clone + Sync + Send> ConnectionManager<L>
3235
where
3336
L::Target: LdkLogger,
3437
{
35-
pub(crate) fn new(peer_manager: Arc<PeerManager>, logger: L) -> Self {
38+
pub(crate) fn new(
39+
peer_manager: Arc<PeerManager>, tor_proxy_config: Option<TorConfig>,
40+
keys_manager: Arc<KeysManager>, logger: L,
41+
) -> Self {
3642
let pending_connections = Mutex::new(HashMap::new());
37-
Self { pending_connections, peer_manager, logger }
43+
44+
Self { pending_connections, peer_manager, tor_proxy_config, keys_manager, logger }
3845
}
3946

4047
pub(crate) async fn connect_peer_if_necessary(
@@ -64,27 +71,114 @@ where
6471

6572
log_info!(self.logger, "Connecting to peer: {}@{}", node_id, addr);
6673

67-
let socket_addr = addr
68-
.to_socket_addrs()
69-
.map_err(|e| {
70-
log_error!(self.logger, "Failed to resolve network address {}: {}", addr, e);
71-
self.propagate_result_to_subscribers(&node_id, Err(Error::InvalidSocketAddress));
72-
Error::InvalidSocketAddress
73-
})?
74-
.next()
75-
.ok_or_else(|| {
76-
log_error!(self.logger, "Failed to resolve network address {}", addr);
74+
let res = match addr {
75+
SocketAddress::OnionV2(old_onion_addr) => {
76+
log_error!(
77+
self.logger,
78+
"Failed to resolve network address {:?}: Resolution of OnionV2 addresses is currently unsupported.",
79+
old_onion_addr
80+
);
7781
self.propagate_result_to_subscribers(&node_id, Err(Error::InvalidSocketAddress));
78-
Error::InvalidSocketAddress
79-
})?;
82+
return Err(Error::InvalidSocketAddress);
83+
},
84+
SocketAddress::OnionV3 { .. } => {
85+
let proxy_config = self.tor_proxy_config.as_ref().ok_or_else(|| {
86+
log_error!(
87+
self.logger,
88+
"Failed to resolve network address {:?}: Tor usage is not configured.",
89+
addr
90+
);
91+
self.propagate_result_to_subscribers(
92+
&node_id,
93+
Err(Error::InvalidSocketAddress),
94+
);
95+
Error::InvalidSocketAddress
96+
})?;
97+
let proxy_addr = proxy_config
98+
.proxy_address
99+
.to_socket_addrs()
100+
.map_err(|e| {
101+
log_error!(
102+
self.logger,
103+
"Failed to resolve Tor proxy network address {}: {}",
104+
proxy_config.proxy_address,
105+
e
106+
);
107+
self.propagate_result_to_subscribers(
108+
&node_id,
109+
Err(Error::InvalidSocketAddress),
110+
);
111+
Error::InvalidSocketAddress
112+
})?
113+
.next()
114+
.ok_or_else(|| {
115+
log_error!(
116+
self.logger,
117+
"Failed to resolve Tor proxy network address {}",
118+
proxy_config.proxy_address
119+
);
120+
self.propagate_result_to_subscribers(
121+
&node_id,
122+
Err(Error::InvalidSocketAddress),
123+
);
124+
Error::InvalidSocketAddress
125+
})?;
126+
let connection_future = lightning_net_tokio::tor_connect_outbound(
127+
Arc::clone(&self.peer_manager),
128+
node_id,
129+
addr.clone(),
130+
proxy_addr,
131+
Arc::clone(&self.keys_manager),
132+
);
133+
self.await_connection(connection_future, node_id, addr).await
134+
},
135+
_ => {
136+
let socket_addr = addr
137+
.to_socket_addrs()
138+
.map_err(|e| {
139+
log_error!(
140+
self.logger,
141+
"Failed to resolve network address {}: {}",
142+
addr,
143+
e
144+
);
145+
self.propagate_result_to_subscribers(
146+
&node_id,
147+
Err(Error::InvalidSocketAddress),
148+
);
149+
Error::InvalidSocketAddress
150+
})?
151+
.next()
152+
.ok_or_else(|| {
153+
log_error!(self.logger, "Failed to resolve network address {}", addr);
154+
self.propagate_result_to_subscribers(
155+
&node_id,
156+
Err(Error::InvalidSocketAddress),
157+
);
158+
Error::InvalidSocketAddress
159+
})?;
160+
let connection_future = lightning_net_tokio::connect_outbound(
161+
Arc::clone(&self.peer_manager),
162+
node_id,
163+
socket_addr,
164+
);
165+
self.await_connection(connection_future, node_id, addr).await
166+
},
167+
};
80168

81-
let connection_future = lightning_net_tokio::connect_outbound(
82-
Arc::clone(&self.peer_manager),
83-
node_id,
84-
socket_addr,
85-
);
169+
self.propagate_result_to_subscribers(&node_id, res);
86170

87-
let res = match connection_future.await {
171+
res
172+
}
173+
174+
async fn await_connection<F, CF>(
175+
&self, connection_future: F, node_id: PublicKey, addr: SocketAddress,
176+
) -> Result<(), Error>
177+
where
178+
F: std::future::Future<Output = Option<CF>>,
179+
CF: std::future::Future<Output = ()>,
180+
{
181+
match connection_future.await {
88182
Some(connection_closed_future) => {
89183
let mut connection_closed_future = Box::pin(connection_closed_future);
90184
loop {
@@ -106,11 +200,7 @@ where
106200
log_error!(self.logger, "Failed to connect to peer: {}@{}", node_id, addr);
107201
Err(Error::ConnectionFailed)
108202
},
109-
};
110-
111-
self.propagate_result_to_subscribers(&node_id, res);
112-
113-
res
203+
}
114204
}
115205

116206
fn register_or_subscribe_pending_connection(

src/ffi/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ impl VssClientHeaderProvider for VssHeaderProviderAdapter {
142142
}
143143

144144
use crate::builder::sanitize_alias;
145-
pub use crate::config::{default_config, ElectrumSyncConfig, EsploraSyncConfig};
145+
pub use crate::config::{default_config, ElectrumSyncConfig, EsploraSyncConfig, TorConfig};
146146
pub use crate::entropy::{generate_entropy_mnemonic, NodeEntropy, WordCount};
147147
use crate::error::Error;
148148
pub use crate::liquidity::LSPS1OrderStatus;

0 commit comments

Comments
 (0)