diff --git a/binaries/cuprated/src/config/p2p.rs b/binaries/cuprated/src/config/p2p.rs index 98ce9adad..cbbdede7b 100644 --- a/binaries/cuprated/src/config/p2p.rs +++ b/binaries/cuprated/src/config/p2p.rs @@ -18,7 +18,7 @@ use cuprate_p2p_core::{ transports::{Tcp, TcpServerConfig}, ClearNet, NetworkZone, Tor, Transport, }; -use cuprate_p2p_transport::{Arti, ArtiClientConfig, ArtiServerConfig}; +use cuprate_p2p_transport::{Arti, ArtiClientConfig, ArtiServerConfig, Socks, SocksClientConfig}; use cuprate_wire::OnionAddr; use crate::{p2p::ProxySettings, tor::TorMode}; @@ -289,6 +289,14 @@ impl ClearNetConfig { server_config, } } + + /// Gets the transport config for [`ClearNet`] over [`Socks`]. + pub fn socks_transport_config(&self) -> TransportConfig { + TransportConfig { + client_config: self.proxy.clone().try_into().unwrap(), + server_config: None, + } + } } impl Default for ClearNetConfig { diff --git a/binaries/cuprated/src/p2p.rs b/binaries/cuprated/src/p2p.rs index 9aab66c97..76bb1dbf6 100644 --- a/binaries/cuprated/src/p2p.rs +++ b/binaries/cuprated/src/p2p.rs @@ -2,8 +2,9 @@ //! //! Will handle initiating the P2P and contains a protocol request handler. -use std::convert::From; +use std::{convert::From, str::FromStr}; +use anyhow::anyhow; use arti_client::TorClient; use futures::{FutureExt, TryFutureExt}; use serde::{Deserialize, Serialize}; @@ -17,7 +18,7 @@ use cuprate_p2p::{config::TransportConfig, NetworkInterface, P2PConfig}; use cuprate_p2p_core::{ client::InternalPeerID, transports::Tcp, ClearNet, NetworkZone, Tor, Transport, }; -use cuprate_p2p_transport::{Arti, ArtiClientConfig, Daemon}; +use cuprate_p2p_transport::{Arti, ArtiClientConfig, Daemon, Socks, SocksClientConfig}; use cuprate_txpool::service::{TxpoolReadHandle, TxpoolWriteHandle}; use cuprate_types::blockchain::BlockchainWriteRequest; @@ -26,8 +27,8 @@ use crate::{ config::Config, constants::PANIC_CRITICAL_SERVICE_ERROR, tor::{ - transport_arti_config, transport_clearnet_arti_config, transport_daemon_config, TorContext, - TorMode, + transport_arti_config, transport_clearnet_arti_config, transport_clearnet_daemon_config, + transport_daemon_config, TorContext, TorMode, }, txpool::{self, IncomingTxHandler}, }; @@ -39,13 +40,43 @@ pub mod request_handler; pub use network_address::CrossNetworkInternalPeerId; /// A simple parsing enum for the `p2p.clear_net.proxy` field -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] pub enum ProxySettings { Tor, #[serde(untagged)] Socks(String), } +impl TryFrom for SocksClientConfig { + type Error = anyhow::Error; + + fn try_from(value: ProxySettings) -> Result { + let ProxySettings::Socks(url) = value else { + panic!("Tor proxy setting should not be parsed!") + }; + + let Some((_, url)) = url.split_once("socks5://") else { + return Err(anyhow!("Invalid proxy url header.")); + }; + + let (authentication, addr) = url + .split_once('@') + .map(|(up, ad)| { + ( + up.split_once(':') + .map(|(a, b)| (a.to_string(), b.to_string())), + ad, + ) + }) + .unwrap_or((None, url)); + + Ok(Self { + proxy: addr.parse()?, + authentication, + }) + } +} + /// This struct collect all supported and optional network zone interfaces. pub struct NetworkInterfaces { /// Mandatory clearnet network interface @@ -90,30 +121,42 @@ pub async fn initialize_zones_p2p( .await .unwrap() } - TorMode::Daemon => { - tracing::error!("Anonymizing clearnet connections through the Tor daemon is not yet supported."); - std::process::exit(0); - } + TorMode::Daemon => start_zone_p2p::( + blockchain_read_handle.clone(), + context_svc.clone(), + txpool_read_handle.clone(), + config.clearnet_p2p_config(), + transport_clearnet_daemon_config(config), + ) + .await + .unwrap(), TorMode::Off => { tracing::error!("Clearnet proxy set to \"tor\" but Tor is actually off. Please be sure to set a mode in the configuration or command line"); std::process::exit(0); } }, ProxySettings::Socks(ref s) => { - if !s.is_empty() { - tracing::error!("Socks proxy is not yet supported."); - std::process::exit(0); + if s.is_empty() { + start_zone_p2p::( + blockchain_read_handle.clone(), + context_svc.clone(), + txpool_read_handle.clone(), + config.clearnet_p2p_config(), + config.p2p.clear_net.tcp_transport_config(config.network), + ) + .await + .unwrap() + } else { + start_zone_p2p::( + blockchain_read_handle.clone(), + context_svc.clone(), + txpool_read_handle.clone(), + config.clearnet_p2p_config(), + config.p2p.clear_net.socks_transport_config(), + ) + .await + .unwrap() } - - start_zone_p2p::( - blockchain_read_handle.clone(), - context_svc.clone(), - txpool_read_handle.clone(), - config.clearnet_p2p_config(), - config.p2p.clear_net.tcp_transport_config(config.network), - ) - .await - .unwrap() } } }; diff --git a/binaries/cuprated/src/tor.rs b/binaries/cuprated/src/tor.rs index 343c02f1f..c6da628f0 100644 --- a/binaries/cuprated/src/tor.rs +++ b/binaries/cuprated/src/tor.rs @@ -22,6 +22,7 @@ use cuprate_p2p::TransportConfig; use cuprate_p2p_core::{ClearNet, Tor}; use cuprate_p2p_transport::{ Arti, ArtiClientConfig, ArtiServerConfig, Daemon, DaemonClientConfig, DaemonServerConfig, + Socks, SocksClientConfig, }; use cuprate_wire::OnionAddr; @@ -197,3 +198,14 @@ pub fn transport_daemon_config(config: &Config) -> TransportConfig ), } } + +/// Gets the transport config for [`ClearNet`] over [`Socks`]. +pub const fn transport_clearnet_daemon_config(config: &Config) -> TransportConfig { + TransportConfig { + client_config: SocksClientConfig { + proxy: config.tor.daemon.address, + authentication: None, + }, + server_config: None, + } +} diff --git a/p2p/p2p-transport/src/lib.rs b/p2p/p2p-transport/src/lib.rs index b6abb8508..f8e14b226 100644 --- a/p2p/p2p-transport/src/lib.rs +++ b/p2p/p2p-transport/src/lib.rs @@ -10,6 +10,10 @@ pub use arti::{Arti, ArtiClientConfig, ArtiServerConfig}; mod tor; pub use tor::{Daemon, DaemonClientConfig, DaemonServerConfig}; +/// SOCKS5 implementation +mod socks; +pub use socks::{Socks, SocksClientConfig}; + /// Disabled listener mod disabled; pub(crate) use disabled::DisabledListener; diff --git a/p2p/p2p-transport/src/socks.rs b/p2p/p2p-transport/src/socks.rs new file mode 100644 index 000000000..4684778b2 --- /dev/null +++ b/p2p/p2p-transport/src/socks.rs @@ -0,0 +1,75 @@ +//! Socks Transport +//! +//! This module defines a transport method for the `ClearNet` network zone using a generic SOCKS5 proxy. +//! + +//---------------------------------------------------------------------------------------------------- Imports + +use std::{ + io::{self, ErrorKind}, + net::SocketAddr, +}; + +use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; +use tokio_socks::tcp::Socks5Stream; +use tokio_util::codec::{FramedRead, FramedWrite}; + +use cuprate_p2p_core::{ClearNet, NetworkZone, Transport}; +use cuprate_wire::MoneroWireCodec; + +use crate::DisabledListener; + +//---------------------------------------------------------------------------------------------------- Configuration + +/// Socks5 proxied TCP transport. +#[derive(Debug, Clone, Copy, Default)] +pub struct Socks; + +#[derive(Clone)] +pub struct SocksClientConfig { + /// Proxy address + pub proxy: SocketAddr, + + /// According to RFC 1929, if authentication is enabled, both username and password fields MUST NOT be empty. + pub authentication: Option<(String, String)>, +} + +//---------------------------------------------------------------------------------------------------- Transport + +#[async_trait::async_trait] +impl Transport for Socks { + type ClientConfig = SocksClientConfig; + type ServerConfig = (); + + type Stream = FramedRead; + type Sink = FramedWrite; + type Listener = DisabledListener; + + async fn connect_to_peer( + addr: ::Addr, + config: &Self::ClientConfig, + ) -> Result<(Self::Stream, Self::Sink), io::Error> { + // Optional authentication + let proxy = if let Some((username, password)) = config.authentication.as_ref() { + Socks5Stream::connect_with_password(config.proxy, addr, username, password).await + } else { + Socks5Stream::connect(config.proxy, addr.to_string()).await + }; + + proxy + .map_err(|e| io::Error::new(ErrorKind::ConnectionAborted, e.to_string())) + .map(|stream| { + let (stream, sink) = stream.into_inner().into_split(); + ( + FramedRead::new(stream, MoneroWireCodec::default()), + FramedWrite::new(sink, MoneroWireCodec::default()), + ) + }) + } + + async fn incoming_connection_listener( + _config: Self::ServerConfig, + ) -> Result { + panic!("In proxy mode, inbound is disabled!"); + } +}