RGB-Tools / rgb-lightning-node

MIT License
17 stars 19 forks source link

Integrate Tor Connection Options #36

Open bitcynic opened 1 month ago

bitcynic commented 1 month ago

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.
zoedberg commented 1 month ago

First of all thanks for the suggestion and the effort put in this issue.

The feature is very interesting but I have some doubts about the implementation side. You mentioned the tor-control library, do you mean this tor_control library? If so I have some concerns about it, since it seems just an initial implementation (10 commits) and unmaintained (last update is from 2016).

To better frame this proposal, is the proposed code a suggestion or have you tried to run it as well?

bitcynic commented 1 month ago

Hi Zoe, first of all, thank you for the feedback.

You are right, the correct library name is with an underscore and not a hyphen, it is my typo. You are also right about your concerns about its use. The alternative for creating Onion Services would be the other library mentioned: arti which is maintained by the Tor project itself. However, it is little more than experimental at the moment.

In conclusion, these are only suggestions. If there are no approaches that you evaluate better, I can try to implement and run it.

zoedberg commented 1 month ago

It seems the Rust ecosystem is not completely ready for Tor. arti for sure seems very promising. Considering the status of RLN and RGB I would wait a little bit more for this feature (assuming it's not a blocker for you), hoping that, when we'll re-evaluate this, arti will be ready.

bitcynic commented 1 month ago

Hi Zoe, after discussing it with the team I would say we can put Tor integration on standby for the time being, i.e. it is not blocking. Definitely using arti is the most promising one, but as you rightly pointed out in general there are no mature libraries in Rust to interact with Tor. Let's wait for the developments of arti and RGB. Thank you.