Skip to content

Integrate Tor Connection Options #36

@bitcynic

Description

@bitcynic

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

  1. 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,
    })
}
  1. 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

  1. 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().
  2. 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.
  3. 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.
  4. 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:

  1. Run the node with --tor-active true and verify it connects via Tor.
  2. Run the node with --tor-active true --tor-skip-proxy-for-clearnet-targets true and verify it can connect via both methods.
  3. Run the node with --tor-active false and verify it connects via clearnet.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions