Enhancement: proposal to introduce Tor connection options and hybrid mode
I would like to suggest an enhancement to support connectivity via Tor. This feature will provide users the ability to connect to peers via clearnet, Tor, or a hybrid system (both) as in ^lnd 0.14.0.
Proposed Changes
- Add new arguments for Tor configuration
Introduce new arguments in the Args structure to specify the Tor options updating the LdkUserInfo structure to include the Tor configuration options.
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
// Other existing arguments
// Flag to activate/deactivate Tor, default is false
#[arg(long, default_value_t = false)]
tor_active: bool,
// Address and port of the Tor SOCKS5 proxy, default is localhost:9050
#[arg(long, default_value = "localhost:9050")]
tor_socks: String,
// Flag to enable Tor stream isolation, default is false
#[arg(long, default_value_t = false)]
tor_stream_isolation: bool,
// Address and port for Tor control connections, default is localhost:9051
#[arg(long, default_value = "localhost:9051")]
tor_control: String,
// Flag to skip the Tor proxy for clearnet targets, default is false
#[arg(long, default_value_t = false)]
tor_skip_proxy_for_clearnet_targets: bool,
// Option for the password used for authentication on the Tor control port
#[arg(long)]
tor_password: Option<String>,
// Default path for the Tor onion service private key
#[arg(long, default_value_t = default_tor_private_key_path())]
tor_private_key_path: String,
}
// Function to get the default path for the Tor private key
fn default_tor_private_key_path() -> String {
env::current_dir().unwrap().to_str().unwrap().to_string()
}
pub(crate) struct LdkUserInfo {
// Other existing fields
pub(crate) tor_active: bool,
pub(crate) tor_socks: String,
pub(crate) tor_stream_isolation: bool,
pub(crate) tor_control: String,
pub(crate) tor_skip_proxy_for_clearnet_targets: bool,
pub(crate) tor_password: Option<String>,
pub(crate) tor_private_key_path: String,
}
pub(crate) fn parse_startup_args() -> Result<LdkUserInfo, AppError> {
let args = Args::parse(); // Parse the CLI arguments using clap
Ok(LdkUserInfo {
// ...
tor_active: args.tor_active,
tor_socks: args.tor_socks,
tor_stream_isolation: args.tor_stream_isolation,
tor_control: args.tor_control,
tor_skip_proxy_for_clearnet_targets: args.tor_skip_proxy_for_clearnet_targets,
tor_password: args.tor_password,
tor_private_key_path: args.tor_private_key_path,
})
}
- Enhance
ldk.rs
Update the PeerManager initialization to handle connections via both clearnet and Tor, based on the provided Tor arguments. We will integrate the tokio-socks library to support Tor connections via an external Tor proxy and the tor-control library to use the Tor Control Protocol (TorCP) to create an Onion Service for inbound connections again via the external Tor proxy. I do not consider it feasible at this time to use the arti library for integrating a Tor proxy directly into the application because it is still an unstable library and not comparable in security to the C implementation of the Tor daemon. Also, arti is not yet able to give full support to Onion Service, so we would be limited in handling inbound connections.
use tokio_socks::tcp::Socks5Stream;
use tor_control::{TorClient, TorAuthMethod};
use std::fs;
use std::path::Path;
use std::net::SocketAddr;
pub(crate) async fn start_ldk(
app_state: Arc<AppState>,
mnemonic: Mnemonic,
user_info: LdkUserInfo, // Add user_info as a parameter
) -> Result<(LdkBackgroundServices, Arc<UnlockedAppState>), APIError> {
let static_state = &app_state.static_state;
// Initialize the FeeEstimator, BroadcasterInterface, and KeysManager here...
let tor_proxy: Option<SocketAddr> = if user_info.tor_active {
Some(user_info.tor_socks.parse().expect("Invalid SOCKS proxy address"))
} else {
None
};
let tor_client = if user_info.tor_active {
// Configure Tor client
let mut tor_client_builder = TorClient::builder().control_port(user_info.tor_control);
if let Some(password) = &user_info.tor_password {
tor_client_builder = tor_client_builder.auth_password(password);
} else {
tor_client_builder = tor_client_builder.auth_none();
}
let tor_client = tor_client_builder
.build()
.await
.expect("Failed to connect to Tor control port");
// Verify the authentication method
if let Some(password) = &user_info.tor_password {
match tor_client.is_authenticated() {
Ok(true) => (),
_ => panic!("Tor password authentication failed"),
}
}
// Create or use an existing Onion Service
let onion_service = if Path::new(&user_info.tor_private_key_path).exists() {
let key_data = fs::read_to_string(&user_info.tor_private_key_path).expect("Unable to read private key file");
tor_client.add_onion_v3(key_data, 80, 80, None).await.expect("Failed to create Onion Service")
} else {
let onion_service = tor_client.create_onion_v3(80, None).await.expect("Failed to create Onion Service");
fs::write(&user_info.tor_private_key_path, &onion_service.private_key).expect("Unable to write private key file");
onion_service
};
println!("Onion Service Address: {}", onion_service.address);
Some(tor_client)
} else {
None
};
if user_info.tor_active {
// Listener for Tor connections
let peer_manager_connection_handler = peer_manager.clone();
let stop_processing = Arc::new(AtomicBool::new(false));
let stop_listen = Arc::clone(&stop_processing);
tokio::spawn(async move {
loop {
let peer_mgr = peer_manager_connection_handler.clone();
if user_info.tor_stream_isolation {
tor_client.as_ref().unwrap().signal_newnym().await.expect("Failed to create a new Tor circuit");
}
match Socks5Stream::connect(tor_proxy.unwrap(), onion_service.address).await {
Ok(stream) => {
if stop_listen.load(Ordering::Acquire) {
return;
}
tokio::spawn(async move {
lightning_net_tokio::setup_inbound(
peer_mgr.clone(),
stream.into_std().unwrap(),
)
.await;
});
}
Err(e) => {
eprintln!("Failed to connect via Tor: {:?}", e);
tokio::time::sleep(Duration::from_secs(5)).await; // Retry delay
}
}
}
});
}
if !user_info.tor_active || user_info.tor_skip_proxy_for_clearnet_targets {
// Listener for clearnet connections
let peer_manager_connection_handler = peer_manager.clone();
let listening_port = static_state.ldk_peer_listening_port;
let stop_processing = Arc::new(AtomicBool::new(false));
let stop_listen = Arc::clone(&stop_processing);
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("[::]:{}", listening_port))
.await
.expect("Failed to bind to listen port - is something else already listening on it?");
loop {
let peer_mgr = peer_manager_connection_handler.clone();
let tcp_stream = listener.accept().await.unwrap().0;
if stop_listen.load(Ordering::Acquire) {
return;
}
tokio::spawn(async move {
lightning_net_tokio::setup_inbound(
peer_mgr.clone(),
tcp_stream.into_std().unwrap(),
)
.await;
});
}
});
}
// Function to handle LDK events
async fn handle_ldk_events(
event: Event,
unlocked_state: Arc<UnlockedAppState>,
static_state: Arc<StaticState>,
tor_proxy: Option<SocketAddr>, // Added optional tor_proxy parameter
) {
match event {
Event::ConnectionNeeded { node_id, addresses } => {
tokio::spawn(async move {
for address in addresses {
if let Ok(sockaddrs) = address.to_socket_addrs() {
for addr in sockaddrs {
let pm = Arc::clone(&unlocked_state.peer_manager);
if let Some(tor_proxy) = tor_proxy {
// Handle connections via Tor
match Socks5Stream::connect(tor_proxy, addr).await {
Ok(stream) => {
if connect_peer_if_necessary(node_id, stream.into_std().unwrap(), pm).await.is_ok() {
return;
}
}
Err(e) => {
eprintln!("Failed to connect via Tor: {:?}", e);
}
}
} else {
// Handle clearnet connections
if connect_peer_if_necessary(node_id, addr, pm).await.is_ok() {
return;
}
}
}
}
}
});
}
_ => {}
}
}
// Start handling LDK events
let tor_proxy_option = if user_info.tor_active && !user_info.tor_skip_proxy_for_clearnet_targets {
Some(user_info.tor_socks.parse().expect("Invalid SOCKS proxy address"))
} else {
None
};
handle_ldk_events(event, unlocked_state, static_state, tor_proxy_option).await;
// Remaining LDK setup and startup
// (Code omitted for brevity)
Ok((
LdkBackgroundServices {
stop_processing,
peer_manager: peer_manager.clone(),
bp_exit,
background_processor: Some(background_processor),
},
unlocked_state,
))
}
Explanation of changes in start_ldk
-
Tor Configuration:
- Tor Client Setup: Configure the Tor client using tor_control::TorClient with appropriate authentication (password or none).
- Onion Service Management: Check if an onion service key exists. If not, create a new onion service and save the private key. If it exists, use the existing key to set up the service.
- Error Handling: Validate Tor authentication and handle errors if the password is incorrect or authentication fails.
- Tor Stream Isolation: If tor_stream_isolation is set to true, signal the Tor client to create a new circuit for each new connection using tor_client.signal_newnym().
-
Proxy Handling:
- SOCKS Proxy: Set up the SOCKS proxy using tokio_socks::tcp::Socks5Stream.
- Conditional Proxy Use: Only use the SOCKS proxy if tor_active is true and tor_skip_proxy_for_clearnet_targets is false. Otherwise, handle clearnet connections.
-
Listeners:
- Tor Listener: Set up a listener for connections via Tor if tor_active is true. Include logic to create a new Tor circuit if tor_stream_isolation is enabled.
- Clearnet Listener: Set up a listener for clearnet connections if tor_active is false or if tor_skip_proxy_for_clearnet_targets is true.
-
Event Handling:
- handle_ldk_events: Adjusted to optionally use the SOCKS proxy based on configuration.
Testing
To ensure the proper functioning of a minimal implementation:
- Run the node with
--tor-active true and verify it connects via Tor.
- Run the node with
--tor-active true --tor-skip-proxy-for-clearnet-targets true and verify it can connect via both methods.
- Run the node with
--tor-active false and verify it connects via clearnet.
Enhancement: proposal to introduce Tor connection options and hybrid mode
I would like to suggest an enhancement to support connectivity via Tor. This feature will provide users the ability to connect to peers via clearnet, Tor, or a hybrid system (both) as in
^lnd 0.14.0.Proposed Changes
Introduce new arguments in the
Argsstructure to specify the Tor options updating theLdkUserInfostructure to include the Tor configuration options.ldk.rsUpdate the PeerManager initialization to handle connections via both clearnet and Tor, based on the provided Tor arguments. We will integrate the
tokio-sockslibrary to support Tor connections via an external Tor proxy and thetor-controllibrary to use the Tor Control Protocol (TorCP) to create an Onion Service for inbound connections again via the external Tor proxy. I do not consider it feasible at this time to use theartilibrary for integrating a Tor proxy directly into the application because it is still an unstable library and not comparable in security to the C implementation of the Tor daemon. Also,artiis not yet able to give full support to Onion Service, so we would be limited in handling inbound connections.Explanation of changes in
start_ldkTor Configuration:
Proxy Handling:
Listeners:
Event Handling:
Testing
To ensure the proper functioning of a minimal implementation:
--tor-active trueand verify it connects via Tor.--tor-active true--tor-skip-proxy-for-clearnet-targets trueand verify it can connect via both methods.--tor-activefalse and verify it connects via clearnet.