-
Notifications
You must be signed in to change notification settings - Fork 32
Open
Labels
enhancementNew feature or requestNew feature or request
Description
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 theArgsstructure to specify the Tor options updating theLdkUserInfostructure 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 thetokio-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.
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 trueand verify it connects via Tor. - Run the node with
--tor-active true--tor-skip-proxy-for-clearnet-targets trueand verify it can connect via both methods. - Run the node with
--tor-activefalse and verify it connects via clearnet.
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request