diff --git a/radicle-cli/examples/rad-config.md b/radicle-cli/examples/rad-config.md index 9a323433..15797643 100644 --- a/radicle-cli/examples/rad-config.md +++ b/radicle-cli/examples/rad-config.md @@ -26,6 +26,7 @@ $ rad config }, "connect": [], "externalAddresses": [], + "tor": null, "network": "main", "relay": true, "limits": { diff --git a/radicle-httpd/src/api/v1/profile.rs b/radicle-httpd/src/api/v1/profile.rs index 58917fa4..57d702fe 100644 --- a/radicle-httpd/src/api/v1/profile.rs +++ b/radicle-httpd/src/api/v1/profile.rs @@ -89,6 +89,7 @@ mod routes { }, "connect": [], "externalAddresses": [], + "tor": null, "network": "main", "relay": true, "limits": { diff --git a/radicle-node/src/main.rs b/radicle-node/src/main.rs index 25e90a55..5ca29ab5 100644 --- a/radicle-node/src/main.rs +++ b/radicle-node/src/main.rs @@ -115,7 +115,6 @@ fn execute() -> anyhow::Result<()> { // Add the preferred seeds as persistent peers so that we reconnect to them automatically. config.node.connect.extend(config.preferred_seeds); - let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050); let listen: Vec = if !options.listen.is_empty() { options.listen.clone() } else { @@ -133,7 +132,7 @@ fn execute() -> anyhow::Result<()> { log::debug!(target: "node", "Removing existing control socket.."); fs::remove_file(home.socket()).ok(); } - Runtime::init(home, config.node, listen, proxy, signals, signer)?.run()?; + Runtime::init(home, config.node, listen, signals, signer)?.run()?; Ok(()) } diff --git a/radicle-node/src/runtime.rs b/radicle-node/src/runtime.rs index 6177a643..5c653ea2 100644 --- a/radicle-node/src/runtime.rs +++ b/radicle-node/src/runtime.rs @@ -139,7 +139,6 @@ impl Runtime { home: Home, config: service::Config, listen: Vec, - proxy: net::SocketAddr, signals: chan::Receiver<()>, signer: G, ) -> Result @@ -238,7 +237,7 @@ impl Runtime { service.initialize(clock)?; let (worker_send, worker_recv) = chan::unbounded::(); - let mut wire = Wire::new(service, worker_send, signer.clone(), proxy); + let mut wire = Wire::new(service, worker_send, signer.clone()); let mut local_addrs = Vec::new(); for addr in listen { diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index 7c4dc14f..01e39654 100644 --- a/radicle-node/src/service.rs +++ b/radicle-node/src/service.rs @@ -552,6 +552,11 @@ where &mut self.outbox } + /// Get configuration. + pub fn config(&self) -> &Config { + &self.config + } + /// Lookup a repository, both locally and in the routing table. pub fn lookup(&self, rid: RepoId) -> Result { let remote = self.db.routing().get(&rid)?.iter().cloned().collect(); diff --git a/radicle-node/src/test/environment.rs b/radicle-node/src/test/environment.rs index 205c9b36..869d75da 100644 --- a/radicle-node/src/test/environment.rs +++ b/radicle-node/src/test/environment.rs @@ -492,13 +492,11 @@ impl + Signer + Clone> Node { /// Spawn a node in its own thread. pub fn spawn(self) -> NodeHandle { let listen = vec![([0, 0, 0, 0], 0).into()]; - let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050); let (_, signals) = chan::bounded(1); let rt = Runtime::init( self.home.clone(), self.config, listen, - proxy, signals, self.signer.clone(), ) diff --git a/radicle-node/src/wire/protocol.rs b/radicle-node/src/wire/protocol.rs index 7917c398..67de456a 100644 --- a/radicle-node/src/wire/protocol.rs +++ b/radicle-node/src/wire/protocol.rs @@ -21,6 +21,7 @@ use netservices::{NetConnection, NetProtocol, NetReader, NetWriter}; use reactor::{ResourceId, ResourceType, Timestamp}; use radicle::collections::RandomMap; +use radicle::node::config::TorConfig; use radicle::node::NodeId; use radicle::storage::WriteStorage; @@ -318,8 +319,6 @@ pub struct Wire { listening: RandomMap, /// Peer (established) sessions. peers: Peers, - /// SOCKS5 proxy address. - proxy: net::SocketAddr, } impl Wire @@ -328,19 +327,13 @@ where S: WriteStorage + 'static, G: Signer + Ecdh, { - pub fn new( - service: Service, - worker: chan::Sender, - signer: G, - proxy: net::SocketAddr, - ) -> Self { + pub fn new(service: Service, worker: chan::Sender, signer: G) -> Self { assert!(service.started().is_some(), "Service must be initialized"); Self { service, worker, signer, - proxy, actions: VecDeque::new(), inbound: RandomMap::default(), outbound: RandomMap::default(), @@ -954,8 +947,7 @@ where addr.to_inner(), node_id, self.signer.clone(), - self.proxy.into(), - false, + self.service.config(), ) .and_then(|session| { NetTransport::>::with_session(session, Link::Outbound) @@ -1057,19 +1049,46 @@ pub fn dial>( remote_addr: NetAddr, remote_id: ::Pk, signer: G, - proxy_addr: NetAddr, - force_proxy: bool, + config: &service::Config, ) -> io::Result> { - let connection = if force_proxy { - // Nb. This timeout is currently not used by the underlying library due to the - // `socket2` library not supporting non-blocking connect with timeout. - net::TcpStream::connect_nonblocking(proxy_addr, DEFAULT_DIAL_TIMEOUT)? - } else { - net::TcpStream::connect_nonblocking( - remote_addr.connection_addr(proxy_addr), - DEFAULT_DIAL_TIMEOUT, - )? + // Convert the remote address into an internet protocol address (IP or DNS). + let inet_addr: NetAddr = match config.tor { + // In Tor proxy mode, simply specify a different connection address. + Some(TorConfig::Proxy { address: proxy }) => remote_addr.connection_addr(proxy.into()), + // In transparent Tor mode, we treat `.onion` addresses as regular DNS names. + Some(TorConfig::Transparent) => { + let host = match &remote_addr.host { + HostName::Ip(ip) => InetHost::Ip(*ip), + HostName::Dns(dns) => InetHost::Dns(dns.clone()), + HostName::Tor(onion) => InetHost::Dns(onion.to_string()), + _ => { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "unsupported remote address type", + )) + } + }; + NetAddr::new(host, remote_addr.port) + } + // Without any Tor configuration, refuse to connect to a `.onion` address. + None => { + let host = match &remote_addr.host { + HostName::Ip(ip) => InetHost::Ip(*ip), + HostName::Dns(dns) => InetHost::Dns(dns.clone()), + _ => { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "unsupported remote address type", + )) + } + }; + NetAddr::new(host, remote_addr.port) + } }; + // Nb. This timeout is currently not used by the underlying library due to the + // `socket2` library not supporting non-blocking connect with timeout. + let connection = net::TcpStream::connect_nonblocking(inet_addr, DEFAULT_DIAL_TIMEOUT)?; + connection.set_read_timeout(Some(DEFAULT_CONNECTION_TIMEOUT))?; connection.set_write_timeout(Some(DEFAULT_CONNECTION_TIMEOUT))?; @@ -1078,7 +1097,6 @@ pub fn dial>( Some(remote_id), connection, signer, - force_proxy, )) } @@ -1088,7 +1106,7 @@ pub fn accept>( connection: net::TcpStream, signer: G, ) -> WireSession { - session::(remote_addr, None, connection, signer, false) + session::(remote_addr, None, connection, signer) } /// Create a new [`WireSession`]. @@ -1097,9 +1115,8 @@ fn session>( remote_id: Option, connection: net::TcpStream, signer: G, - force_proxy: bool, ) -> WireSession { - let socks5 = socks5::Socks5::with(remote_addr, force_proxy); + let socks5 = socks5::Socks5::with(remote_addr, false); let proxy = Socks5Session::with(connection, socks5); let pair = G::generate_keypair(); let keyset = Keyset { diff --git a/radicle/src/node/config.rs b/radicle/src/node/config.rs index a84f475b..d95d4a3b 100644 --- a/radicle/src/node/config.rs +++ b/radicle/src/node/config.rs @@ -234,6 +234,19 @@ impl Default for PeerConfig { } } +/// Tor configuration. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase", tag = "mode")] +pub enum TorConfig { + /// Connect via SOCKS5 proxy. + Proxy { + /// Tor proxy address. + address: net::SocketAddr, + }, + /// Treat Tor onion addresses as DNS names. + Transparent, +} + /// Service configuration. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] @@ -253,6 +266,9 @@ pub struct Config { /// Specify the node's public addresses #[serde(default)] pub external_addresses: Vec
, + /// Tor configuration. + #[serde(default)] + pub tor: Option, /// Peer-to-peer network. #[serde(default)] pub network: Network, @@ -289,6 +305,7 @@ impl Config { connect: HashSet::default(), external_addresses: vec![], network: Network::default(), + tor: None, relay: true, limits: Limits::default(), workers: DEFAULT_WORKERS,