Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
node: Support "transparent" Tor
Merged did:key:z6MksFqX...wzpT opened 2 years ago

This allows for configuring your node such that .onion addresses encountered are treated as regular DNS names.

8 files changed +69 -32 70d2e1a0 deeb39c5
modified radicle-cli/examples/rad-config.md
@@ -26,6 +26,7 @@ $ rad config
    },
    "connect": [],
    "externalAddresses": [],
+
    "tor": null,
    "network": "main",
    "relay": true,
    "limits": {
modified radicle-httpd/src/api/v1/profile.rs
@@ -89,6 +89,7 @@ mod routes {
                  },
                  "connect": [],
                  "externalAddresses": [],
+
                  "tor": null,
                  "network": "main",
                  "relay": true,
                  "limits": {
modified 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<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(())
}
modified radicle-node/src/runtime.rs
@@ -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 {
modified 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<Lookup, LookupError> {
        let remote = self.db.routing().get(&rid)?.iter().cloned().collect();
modified radicle-node/src/test/environment.rs
@@ -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(),
        )
modified 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<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 {
-
        // 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<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.
+
    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 {
modified 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<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,