cargo: Add feature for Tor support

This commit is contained in:
Lorenz Leutgeb 2026-02-15 01:40:39 +01:00
parent 8bac24d6ab
commit 1e132685bc
No known key found for this signature in database
13 changed files with 44 additions and 8 deletions

View File

@ -13,6 +13,10 @@ rust-version.workspace = true
name = "rad"
path = "src/main.rs"
[features]
default = ["tor"]
tor = ["radicle/tor"]
[dependencies]
anyhow = "1"
chrono = { workspace = true, features = ["clock", "std"] }

View File

@ -10,9 +10,10 @@ build = "build.rs"
rust-version.workspace = true
[features]
default = ["backtrace", "systemd", "structured-logger", "socket2"]
default = ["backtrace", "systemd", "structured-logger", "socket2", "tor"]
systemd = ["dep:radicle-systemd"]
test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "radicle-protocol/test", "qcheck", "snapbox"]
tor = ["cyphernet/tor", "radicle/tor", "radicle-protocol/tor"]
[dependencies]
backtrace = { version = "0.3.75", optional = true }
@ -21,7 +22,7 @@ bytes = { workspace = true }
chrono = { workspace = true, features = ["clock"] }
colored = { workspace = true }
crossbeam-channel = { workspace = true }
cyphernet = { workspace = true, features = ["tor", "dns", "ed25519", "p2p-ed25519", "noise-framework", "noise_sha2"] }
cyphernet = { workspace = true, features = ["dns", "ed25519", "p2p-ed25519", "noise-framework", "noise_sha2"] }
fastrand = { workspace = true }
gix-packetline = { workspace = true, features = ["blocking-io"] }
lexopt = { workspace = true }

View File

@ -21,6 +21,7 @@ use radicle::collections::{RandomMap, RandomSet};
use radicle::crypto;
use radicle::node::Link;
use radicle::node::NodeId;
#[cfg(feature = "tor")]
use radicle::node::config::AddressConfig;
use radicle::storage::WriteStorage;
use radicle_protocol::deserializer::Deserializer;
@ -1091,6 +1092,7 @@ pub fn dial<G: Ecdh<Pk = NodeId>>(
(HostName::Dns(_), Some(proxy)) => proxy.into(),
(HostName::Dns(dns), None) => NetAddr::new(InetHost::Dns(dns.clone()), remote_addr.port),
// For onion addresses, handle with care.
#[cfg(feature = "tor")]
(HostName::Tor(onion), proxy) => match config.onion {
// In onion proxy mode, simply use the configured proxy address.
// This takes precedence over any global proxy.

View File

@ -10,12 +10,13 @@ rust-version.workspace = true
[features]
test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "qcheck"]
tor = ["cypheraddr/tor", "radicle/tor"]
[dependencies]
bloomy = "1.2"
bytes = { workspace = true }
crossbeam-channel = { workspace = true }
cypheraddr = { workspace = true, features = ["serde", "tor"] }
cypheraddr = { workspace = true, features = ["serde"] }
fastrand = { workspace = true }
log = { workspace = true, features = ["std"] }
nonempty = { workspace = true, features = ["serialize"] }

View File

@ -2655,6 +2655,7 @@ where
fn is_supported_address(&self, address: &Address) -> bool {
match AddressType::from(address) {
// Only consider onion addresses if configured.
#[cfg(feature = "tor")]
AddressType::Onion => self.config.onion.is_some(),
AddressType::Dns | AddressType::Ipv4 | AddressType::Ipv6 => true,
}

View File

@ -14,6 +14,7 @@ use std::string::FromUtf8Error;
use bytes::{Buf, BufMut};
#[cfg(feature = "tor")]
use cypheraddr::tor;
use radicle::crypto::{PublicKey, Signature};
@ -56,6 +57,7 @@ pub enum Invalid {
Alias(#[from] node::AliasError),
#[error("invalid user agent string: {err}")]
InvalidUserAgent { err: String },
#[cfg(feature = "tor")]
#[error("invalid onion address: {0}")]
OnionAddr(#[from] tor::OnionAddrDecodeError),
#[error("invalid timestamp: {actual_millis} millis")]
@ -257,6 +259,7 @@ impl Encode for Refs {
}
}
#[cfg(feature = "tor")]
impl Encode for cypheraddr::tor::OnionAddrV3 {
fn encode(&self, buf: &mut impl BufMut) {
self.into_raw_bytes().encode(buf)
@ -518,6 +521,7 @@ impl Decode for node::Features {
}
}
#[cfg(feature = "tor")]
impl Decode for tor::OnionAddrV3 {
fn decode(buf: &mut impl Buf) -> Result<Self, Error> {
let bytes: [u8; tor::ONION_V3_RAW_LEN] = Decode::decode(buf)?;

View File

@ -2,7 +2,9 @@ use std::{mem, net};
use bytes::Buf;
use bytes::BufMut;
use cypheraddr::{HostName, NetAddr, tor};
#[cfg(feature = "tor")]
use cypheraddr::tor;
use cypheraddr::{HostName, NetAddr};
use radicle::crypto::Signature;
use radicle::git::Oid;
use radicle::identity::RepoId;
@ -79,6 +81,7 @@ pub enum AddressType {
Ipv4 = 1,
Ipv6 = 2,
Dns = 3,
#[cfg(feature = "tor")]
Onion = 4,
}
@ -94,6 +97,7 @@ impl From<&Address> for AddressType {
HostName::Ip(net::IpAddr::V4(_)) => AddressType::Ipv4,
HostName::Ip(net::IpAddr::V6(_)) => AddressType::Ipv6,
HostName::Dns(_) => AddressType::Dns,
#[cfg(feature = "tor")]
HostName::Tor(_) => AddressType::Onion,
_ => todo!(), // FIXME(cloudhead): Maxim will remove `non-exhaustive`
}
@ -108,6 +112,7 @@ impl TryFrom<u8> for AddressType {
1 => Ok(AddressType::Ipv4),
2 => Ok(AddressType::Ipv6),
3 => Ok(AddressType::Dns),
#[cfg(feature = "tor")]
4 => Ok(AddressType::Onion),
_ => Err(other),
}
@ -356,6 +361,7 @@ impl wire::Encode for Address {
u8::from(AddressType::Dns).encode(buf);
dns.encode(buf);
}
#[cfg(feature = "tor")]
HostName::Tor(addr) => {
u8::from(AddressType::Onion).encode(buf);
addr.encode(buf);
@ -393,6 +399,7 @@ impl wire::Decode for Address {
HostName::Dns(dns)
}
#[cfg(feature = "tor")]
Ok(AddressType::Onion) => {
let onion: tor::OnionAddrV3 = wire::Decode::decode(buf)?;

View File

@ -24,6 +24,7 @@ schemars = [
"radicle-localtime/schemars",
"dep:schemars"
]
tor = ["cyphernet/tor"]
[dependencies]
amplify = { workspace = true, features = ["std"] }
@ -32,7 +33,7 @@ bytesize = { version = "2", features = ["serde"] }
chrono = { workspace = true, features = ["clock"], optional = true }
colored = { workspace = true, optional = true }
crossbeam-channel = { workspace = true }
cyphernet = { workspace = true, features = ["tor", "dns", "p2p-ed25519"] }
cyphernet = { workspace = true, features = ["dns", "p2p-ed25519"] }
dunce = { workspace = true }
fast-glob = { version = "0.3.2" }
fastrand = { workspace = true, features = ["std"] }

View File

@ -476,6 +476,7 @@ impl Address {
}
/// Returns `true` if the [`HostName`] is a Tor onion address.
#[cfg(feature = "tor")]
pub fn is_onion(&self) -> bool {
match self.0.host {
HostName::Tor(_) => true,
@ -493,6 +494,7 @@ impl Address {
HostName::Ip(IpAddr::V4(ip)) => ip.to_string(),
HostName::Ip(IpAddr::V6(ip)) => format!("[{ip}]"),
HostName::Dns(dns) => dns.clone(),
#[cfg(feature = "tor")]
HostName::Tor(onion) => {
let onion = onion.to_string();
let start = onion.chars().take(8).collect::<String>();

View File

@ -201,6 +201,7 @@ pub enum AddressType {
Ipv4 = 1,
Ipv6 = 2,
Dns = 3,
#[cfg(feature = "tor")]
Onion = 4,
}
@ -216,6 +217,7 @@ impl From<&Address> for AddressType {
HostName::Ip(net::IpAddr::V4(_)) => AddressType::Ipv4,
HostName::Ip(net::IpAddr::V6(_)) => AddressType::Ipv6,
HostName::Dns(_) => AddressType::Dns,
#[cfg(feature = "tor")]
HostName::Tor(_) => AddressType::Onion,
_ => todo!(), // FIXME(cloudhead): Maxim will remove `non-exhaustive`
}
@ -230,6 +232,7 @@ impl TryFrom<u8> for AddressType {
1 => Ok(AddressType::Ipv4),
2 => Ok(AddressType::Ipv6),
3 => Ok(AddressType::Dns),
#[cfg(feature = "tor")]
4 => Ok(AddressType::Onion),
_ => Err(other),
}

View File

@ -535,6 +535,7 @@ impl TryFrom<&sql::Value> for AddressType {
"ipv4" => Ok(AddressType::Ipv4),
"ipv6" => Ok(AddressType::Ipv6),
"dns" => Ok(AddressType::Dns),
#[cfg(feature = "tor")]
"onion" => Ok(AddressType::Onion),
_ => Err(err),
},
@ -549,6 +550,7 @@ impl sql::BindableWithIndex for AddressType {
Self::Ipv4 => "ipv4".bind(stmt, i),
Self::Ipv6 => "ipv6".bind(stmt, i),
Self::Dns => "dns".bind(stmt, i),
#[cfg(feature = "tor")]
Self::Onion => "onion".bind(stmt, i),
}
}

View File

@ -22,7 +22,9 @@ pub type ProtocolVersion = u8;
pub mod seeds {
use std::{str::FromStr, sync::LazyLock};
use cyphernet::addr::{HostName, NetAddr, tor::OnionAddrV3};
#[cfg(feature = "tor")]
use cyphernet::addr::tor::OnionAddrV3;
use cyphernet::addr::{HostName, NetAddr};
use super::{ConnectAddress, NodeId, PeerAddr};
@ -41,6 +43,7 @@ pub mod seeds {
NodeId::from_str("z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7").unwrap(),
vec![
HostName::Dns("iris.radicle.xyz".to_owned()),
#[cfg(feature = "tor")]
#[allow(clippy::unwrap_used)] // Value is manually verified.
OnionAddrV3::from_str(
"irisradizskwweumpydlj4oammoshkxxjur3ztcmo7cou5emc6s5lfid.onion",
@ -58,6 +61,7 @@ pub mod seeds {
NodeId::from_str("z6Mkmqogy2qEM2ummccUthFEaaHvyYmYBYh3dbe9W4ebScxo").unwrap(),
vec![
HostName::Dns("rosa.radicle.xyz".to_owned()),
#[cfg(feature = "tor")]
#[allow(clippy::unwrap_used)] // Value is manually verified.
OnionAddrV3::from_str(
"rosarad5bxgdlgjnzzjygnsxrwxmoaj4vn7xinlstwglxvyt64jlnhyd.onion",
@ -354,6 +358,7 @@ pub enum Relay {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "mode")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg(feature = "tor")]
pub enum AddressConfig {
/// Proxy connections to this address type.
Proxy {
@ -540,6 +545,7 @@ pub struct Config {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proxy: Option<net::SocketAddr>,
/// Onion address config.
#[cfg(feature = "tor")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub onion: Option<AddressConfig>,
/// Peer-to-peer network.
@ -596,6 +602,7 @@ impl Config {
external_addresses: vec![],
network: Network::default(),
proxy: None,
#[cfg(feature = "tor")]
onion: None,
relay: Relay::default(),
limits: Limits::default(),

View File

@ -5,8 +5,8 @@ use std::str::FromStr;
use std::{iter, net};
use crypto::PublicKey;
use cyphernet::EcPk;
use cyphernet::addr::tor::OnionAddrV3;
#[cfg(feature = "tor")]
use cyphernet::{EcPk, addr::tor::OnionAddrV3};
use qcheck::Arbitrary;
use crate::identity::doc::Visibility;
@ -229,6 +229,7 @@ impl Arbitrary for Address {
.unwrap()
.to_string(),
),
#[cfg(feature = "tor")]
AddressType::Onion => {
let pk = PublicKey::arbitrary(g);
let addr = OnionAddrV3::from(