From 10a82958ca9e6702da0197674b73c1b3dbfe51f2 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Sun, 15 Feb 2026 13:13:04 +0100 Subject: [PATCH] node: Explicit default for `AddressConfig` The intent to drop outgoing connections is modeled as `Option::None` which is brittle and easy to miss. Extend `enum AddressConfig` with a variant that is more explicit. --- crates/radicle-cli/examples/rad-config.md | 22 ++++++++----- crates/radicle-node/src/wire.rs | 6 ++-- crates/radicle-protocol/src/service.rs | 4 ++- crates/radicle/src/node/config.rs | 38 ++++++++++++++++++++--- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/crates/radicle-cli/examples/rad-config.md b/crates/radicle-cli/examples/rad-config.md index bc984af4..358aa511 100644 --- a/crates/radicle-cli/examples/rad-config.md +++ b/crates/radicle-cli/examples/rad-config.md @@ -247,14 +247,7 @@ $ rad config schema }, "onion": { "description": "Onion address config.", - "anyOf": [ - { - "$ref": "#/$defs/AddressConfig" - }, - { - "type": "null" - } - ] + "$ref": "#/$defs/AddressConfig" }, "network": { "description": "Peer-to-peer network.", @@ -410,6 +403,19 @@ $ rad config schema "required": [ "mode" ] + }, + { + "description": "Drop connections to this address type.", + "type": "object", + "properties": { + "mode": { + "type": "string", + "const": "drop" + } + }, + "required": [ + "mode" + ] } ] }, diff --git a/crates/radicle-node/src/wire.rs b/crates/radicle-node/src/wire.rs index 383df8bf..329656ac 100644 --- a/crates/radicle-node/src/wire.rs +++ b/crates/radicle-node/src/wire.rs @@ -1096,10 +1096,10 @@ pub fn dial>( (HostName::Tor(onion), proxy) => match config.onion { // In onion proxy mode, simply use the configured proxy address. // This takes precedence over any global proxy. - Some(AddressConfig::Proxy { address }) => address.into(), + AddressConfig::Proxy { address } => address.into(), // In "forward" mode, if a global proxy is set, we use that, otherwise // we treat `.onion` addresses as regular DNS names. - Some(AddressConfig::Forward) => { + AddressConfig::Forward => { if let Some(proxy) = proxy { proxy.into() } else { @@ -1107,7 +1107,7 @@ pub fn dial>( } } // If onion address support isn't configured, refuse to connect. - None => { + AddressConfig::Drop => { return Err(io::Error::new( io::ErrorKind::Unsupported, "no configuration found for .onion addresses", diff --git a/crates/radicle-protocol/src/service.rs b/crates/radicle-protocol/src/service.rs index dee16ea7..4bade888 100644 --- a/crates/radicle-protocol/src/service.rs +++ b/crates/radicle-protocol/src/service.rs @@ -30,6 +30,8 @@ use radicle::node; use radicle::node::address; use radicle::node::address::Store as _; use radicle::node::address::{AddressBook, AddressType, KnownAddress}; +#[cfg(feature = "tor")] +use radicle::node::config::AddressConfig; use radicle::node::config::{PeerConfig, RateLimit}; use radicle::node::device::Device; use radicle::node::refs::Store as _; @@ -2656,7 +2658,7 @@ where match AddressType::from(address) { // Only consider onion addresses if configured. #[cfg(feature = "tor")] - AddressType::Onion => self.config.onion.is_some(), + AddressType::Onion => self.config.onion != AddressConfig::Drop, AddressType::Dns | AddressType::Ipv4 | AddressType::Ipv6 => true, } } diff --git a/crates/radicle/src/node/config.rs b/crates/radicle/src/node/config.rs index 600151e0..16c46486 100644 --- a/crates/radicle/src/node/config.rs +++ b/crates/radicle/src/node/config.rs @@ -355,7 +355,7 @@ pub enum Relay { } /// Proxy configuration. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "mode")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg(feature = "tor")] @@ -368,6 +368,9 @@ pub enum AddressConfig { /// Forward address to the next layer. Either this is the global proxy, /// or the operating system, via DNS. Forward, + /// Drop connections to this address type. + #[default] + Drop, } /// Default seeding policy. Applies when no repository policies for the given repo are found. @@ -546,8 +549,12 @@ pub struct Config { pub proxy: Option, /// Onion address config. #[cfg(feature = "tor")] - #[serde(default, skip_serializing_if = "Option::is_none")] - pub onion: Option, + #[serde( + default, + skip_serializing_if = "crate::serde_ext::is_default", + deserialize_with = "crate::serde_ext::null_to_default" + )] + pub onion: AddressConfig, /// Peer-to-peer network. #[serde(default)] pub network: Network, @@ -603,7 +610,7 @@ impl Config { network: Network::default(), proxy: None, #[cfg(feature = "tor")] - onion: None, + onion: AddressConfig::Drop, relay: Relay::default(), limits: Limits::default(), workers: Workers::default(), @@ -944,4 +951,27 @@ mod test { crate::storage::refs::FeatureLevel::Parent ); } + + #[cfg(feature = "tor")] + #[test] + fn onion_absent() { + let actual: super::Config = serde_json::from_value(json!({ + "alias": "radicle", + })) + .unwrap(); + assert_eq!(super::AddressConfig::Drop, actual.onion); + } + + #[cfg(feature = "tor")] + #[test] + fn onion_null() { + // Backwards compatibility: Prior versions allowed to set `onion` to `null`, + // which should be treated the same as the default, i.e. `Drop`. + let actual: super::Config = serde_json::from_value(json!({ + "alias": "radicle", + "onion": null, + })) + .unwrap(); + assert_eq!(super::AddressConfig::Drop, actual.onion); + } }