node: Improve Tor configuration flexibility

*Breaking change* The `tor` configuration field is no longer
supported. Users must remove that field from their config.

Allows for a mixed mode, where regular addresses bypass the Tor proxy,
while onion addresses go through it.

When `onion.mode` is set to "forward", Tor connections are fowarded to
the global proxy, and if it isn't set, to the OS's DNS resolution.

For mixed mode, a global proxy simply isn't set, so that IP/DNS is not
proxied.
This commit is contained in:
cloudhead 2024-05-16 12:53:47 +02:00
parent cb2cbf014a
commit ca7db1620c
No known key found for this signature in database
5 changed files with 47 additions and 58 deletions

View File

@ -29,7 +29,6 @@ $ rad config
"db": { "db": {
"journalMode": "rollback" "journalMode": "rollback"
}, },
"tor": null,
"network": "main", "network": "main",
"log": "INFO", "log": "INFO",
"relay": "auto", "relay": "auto",

View File

@ -90,7 +90,6 @@ mod routes {
"connect": [], "connect": [],
"externalAddresses": [], "externalAddresses": [],
"db": { "journalMode": "rollback" }, "db": { "journalMode": "rollback" },
"tor": null,
"network": "main", "network": "main",
"log": "INFO", "log": "INFO",
"relay": "auto", "relay": "auto",

View File

@ -1232,7 +1232,7 @@ where
} }
Entry::Vacant(e) => { Entry::Vacant(e) => {
if let HostName::Ip(ip) = addr.host { if let HostName::Ip(ip) = addr.host {
if !ip.is_loopback() && !self.config.is_proxy_ip(ip) { if !address::is_local(&ip) {
if let Err(e) = if let Err(e) =
self.db self.db
.addresses_mut() .addresses_mut()
@ -2335,8 +2335,8 @@ where
.map(|ka| (peer.nid, ka)) .map(|ka| (peer.nid, ka))
}) })
.filter(|(_, ka)| match AddressType::from(&ka.addr) { .filter(|(_, ka)| match AddressType::from(&ka.addr) {
// Only consider Tor addresses if Tor is configured. // Only consider onion addresses if configured.
AddressType::Onion => self.config.tor.is_some(), AddressType::Onion => self.config.onion.is_some(),
AddressType::Dns | AddressType::Ipv4 | AddressType::Ipv6 => true, AddressType::Dns | AddressType::Ipv4 | AddressType::Ipv6 => true,
}) })
.take(wanted) .take(wanted)

View File

@ -21,7 +21,7 @@ use netservices::{NetConnection, NetReader, NetWriter};
use reactor::{ResourceId, ResourceType, Timestamp}; use reactor::{ResourceId, ResourceType, Timestamp};
use radicle::collections::RandomMap; use radicle::collections::RandomMap;
use radicle::node::config::TorConfig; use radicle::node::config::AddressConfig;
use radicle::node::NodeId; use radicle::node::NodeId;
use radicle::storage::WriteStorage; use radicle::storage::WriteStorage;
@ -1071,42 +1071,37 @@ pub fn dial<G: Signer + Ecdh<Pk = NodeId>>(
signer: G, signer: G,
config: &service::Config, config: &service::Config,
) -> io::Result<WireSession<G>> { ) -> io::Result<WireSession<G>> {
let inet_addr: NetAddr<InetHost> = match config.tor { let inet_addr: NetAddr<InetHost> = match &remote_addr.host {
// In Tor proxy mode, simply specify the proxy address for all connections, HostName::Ip(ip) => NetAddr::new(InetHost::Ip(*ip), remote_addr.port),
// since we'll be routing all connections through the proxy. HostName::Dns(dns) => NetAddr::new(InetHost::Dns(dns.clone()), remote_addr.port),
Some(TorConfig::Proxy { address: proxy }) => proxy.into(), HostName::Tor(onion) => match config.onion {
// In transparent Tor mode, we treat `.onion` addresses as regular DNS names. // In Tor proxy mode, simply specify the proxy address.
Some(TorConfig::Transparent) => { Some(AddressConfig::Proxy { address }) => address.into(),
let host = match &remote_addr.host { // In "forward" mode, if a global proxy is set, we use that, otherwise
HostName::Ip(ip) => InetHost::Ip(*ip), // we treat `.onion` addresses as regular DNS names.
HostName::Dns(dns) => InetHost::Dns(dns.clone()), Some(AddressConfig::Forward) => {
HostName::Tor(onion) => InetHost::Dns(onion.to_string()), if let Some(proxy) = config.proxy {
_ => { proxy.into()
return Err(io::Error::new( } else {
io::ErrorKind::Unsupported, NetAddr::new(InetHost::Dns(onion.to_string()), remote_addr.port)
"unsupported remote address type",
))
} }
}; }
NetAddr::new(host, remote_addr.port) None => {
} return Err(io::Error::new(
// Without any Tor configuration, refuse to connect to a `.onion` address. io::ErrorKind::Unsupported,
None => { "no configuration found for .onion addresses",
let host = match &remote_addr.host { ));
HostName::Ip(ip) => InetHost::Ip(*ip), }
HostName::Dns(dns) => InetHost::Dns(dns.clone()), },
_ => { _ => {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::Unsupported, io::ErrorKind::Unsupported,
"unsupported remote address type", "unsupported remote address type",
)) ))
}
};
NetAddr::new(host, remote_addr.port)
} }
}; };
// Whether to tunnel regular connections through the proxy. // Whether to tunnel regular connections through the proxy.
let force_proxy = matches!(config.tor, Some(TorConfig::Proxy { .. })); let force_proxy = config.proxy.is_some();
// Nb. This timeout is currently not used by the underlying library due to the // Nb. This timeout is currently not used by the underlying library due to the
// `socket2` library not supporting non-blocking connect with timeout. // `socket2` library not supporting non-blocking connect with timeout.
let connection = net::TcpStream::connect_nonblocking(inet_addr, DEFAULT_DIAL_TIMEOUT)?; let connection = net::TcpStream::connect_nonblocking(inet_addr, DEFAULT_DIAL_TIMEOUT)?;

View File

@ -7,7 +7,7 @@ use localtime::LocalDuration;
use crate::node; use crate::node;
use crate::node::policy::{Policy, Scope}; use crate::node::policy::{Policy, Scope};
use crate::node::{address, db, Address, Alias, NodeId}; use crate::node::{db, Address, Alias, NodeId};
/// Target number of peers to maintain connections to. /// Target number of peers to maintain connections to.
pub const TARGET_OUTBOUND_PEERS: usize = 8; pub const TARGET_OUTBOUND_PEERS: usize = 8;
@ -247,17 +247,18 @@ pub enum Relay {
Auto, Auto,
} }
/// Tor configuration. /// Proxy configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase", tag = "mode")] #[serde(rename_all = "camelCase", tag = "mode")]
pub enum TorConfig { pub enum AddressConfig {
/// Connect via SOCKS5 proxy. /// Proxy connections to this address type.
Proxy { Proxy {
/// Tor proxy address. /// Proxy address.
address: net::SocketAddr, address: net::SocketAddr,
}, },
/// Treat Tor onion addresses as DNS names. /// Forward address to the next layer. Either this is the global proxy,
Transparent, /// or the operating system, via DNS.
Forward,
} }
/// Database configuration. /// Database configuration.
@ -290,9 +291,12 @@ pub struct Config {
/// Database config. /// Database config.
#[serde(default)] #[serde(default)]
pub db: DbConfig, pub db: DbConfig,
/// Tor configuration. /// Global proxy.
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub tor: Option<TorConfig>, pub proxy: Option<net::SocketAddr>,
/// Onion address config.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub onion: Option<AddressConfig>,
/// Peer-to-peer network. /// Peer-to-peer network.
#[serde(default)] #[serde(default)]
pub network: Network, pub network: Network,
@ -334,7 +338,8 @@ impl Config {
external_addresses: vec![], external_addresses: vec![],
db: DbConfig::default(), db: DbConfig::default(),
network: Network::default(), network: Network::default(),
tor: None, proxy: None,
onion: None,
relay: Relay::default(), relay: Relay::default(),
limits: Limits::default(), limits: Limits::default(),
workers: DEFAULT_WORKERS, workers: DEFAULT_WORKERS,
@ -359,15 +364,6 @@ impl Config {
self.peer(id).is_some() self.peer(id).is_some()
} }
/// Check if the given IP address is our proxy.
pub fn is_proxy_ip(&self, ip: net::IpAddr) -> bool {
match self.tor {
None => false,
Some(TorConfig::Proxy { address }) => address.ip() == ip,
Some(TorConfig::Transparent) => address::is_local(&ip),
}
}
/// Are we a relay node? This determines what we do with gossip messages from other peers. /// Are we a relay node? This determines what we do with gossip messages from other peers.
pub fn is_relay(&self) -> bool { pub fn is_relay(&self) -> bool {
match self.relay { match self.relay {