node: Support "transparent" Tor

This allows for configuring your node such that `.onion` addresses
encountered are treated as regular DNS names.
This commit is contained in:
cloudhead 2024-04-24 18:55:05 +02:00
parent 70d2e1a0db
commit deeb39c558
No known key found for this signature in database
8 changed files with 69 additions and 32 deletions

View File

@ -26,6 +26,7 @@ $ rad config
},
"connect": [],
"externalAddresses": [],
"tor": null,
"network": "main",
"relay": true,
"limits": {

View File

@ -89,6 +89,7 @@ mod routes {
},
"connect": [],
"externalAddresses": [],
"tor": null,
"network": "main",
"relay": true,
"limits": {

View File

@ -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<std::net::SocketAddr> = 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(())
}

View File

@ -139,7 +139,6 @@ impl Runtime {
home: Home,
config: service::Config,
listen: Vec<net::SocketAddr>,
proxy: net::SocketAddr,
signals: chan::Receiver<()>,
signer: G,
) -> Result<Runtime, Error>
@ -238,7 +237,7 @@ impl Runtime {
service.initialize(clock)?;
let (worker_send, worker_recv) = chan::unbounded::<worker::Task>();
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 {

View File

@ -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<Lookup, LookupError> {
let remote = self.db.routing().get(&rid)?.iter().cloned().collect();

View File

@ -492,13 +492,11 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {
/// Spawn a node in its own thread.
pub fn spawn(self) -> NodeHandle<G> {
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(),
)

View File

@ -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<D, S, G: Signer + Ecdh> {
listening: RandomMap<RawFd, net::SocketAddr>,
/// Peer (established) sessions.
peers: Peers,
/// SOCKS5 proxy address.
proxy: net::SocketAddr,
}
impl<D, S, G> Wire<D, S, G>
@ -328,19 +327,13 @@ where
S: WriteStorage + 'static,
G: Signer + Ecdh<Pk = NodeId>,
{
pub fn new(
service: Service<D, S, G>,
worker: chan::Sender<Task>,
signer: G,
proxy: net::SocketAddr,
) -> Self {
pub fn new(service: Service<D, S, G>, worker: chan::Sender<Task>, 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::<WireSession<G>>::with_session(session, Link::Outbound)
@ -1057,19 +1049,46 @@ pub fn dial<G: Signer + Ecdh<Pk = NodeId>>(
remote_addr: NetAddr<HostName>,
remote_id: <G as EcSk>::Pk,
signer: G,
proxy_addr: NetAddr<InetHost>,
force_proxy: bool,
config: &service::Config,
) -> io::Result<WireSession<G>> {
let connection = if force_proxy {
// Convert the remote address into an internet protocol address (IP or DNS).
let inet_addr: NetAddr<InetHost> = 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.
net::TcpStream::connect_nonblocking(proxy_addr, DEFAULT_DIAL_TIMEOUT)?
} else {
net::TcpStream::connect_nonblocking(
remote_addr.connection_addr(proxy_addr),
DEFAULT_DIAL_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<G: Signer + Ecdh<Pk = NodeId>>(
Some(remote_id),
connection,
signer,
force_proxy,
))
}
@ -1088,7 +1106,7 @@ pub fn accept<G: Signer + Ecdh<Pk = NodeId>>(
connection: net::TcpStream,
signer: G,
) -> WireSession<G> {
session::<G>(remote_addr, None, connection, signer, false)
session::<G>(remote_addr, None, connection, signer)
}
/// Create a new [`WireSession`].
@ -1097,9 +1115,8 @@ fn session<G: Signer + Ecdh<Pk = NodeId>>(
remote_id: Option<NodeId>,
connection: net::TcpStream,
signer: G,
force_proxy: bool,
) -> WireSession<G> {
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 {

View File

@ -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<Address>,
/// Tor configuration.
#[serde(default)]
pub tor: Option<TorConfig>,
/// 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,