RGB-Tools / RGB-Tools/rgb-lightning-node

Integrate Tor Connection Options

Open
#36 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Rust
Stars
50
Forks
48
Avg merge
8h 52m
Merged PRs (30d)
2

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

  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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the Args and LdkUserInfo definitions, then inspect ldk.rs and the start_ldk entry point to understand current peer connection setup. Trace how inbound listeners and ConnectionNeeded events are handled. Done should provide the proposed clearnet, Tor, and hybrid behavior with configurable Tor settings and preserved onion-service state, including appropriate error handling.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
backend, networking
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.