diff options
| author | tv <tv@krebsco.de> | 2026-11-01 06:13:02 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-01 06:13:02 +0100 |
| commit | b4aa16f2a3c3f9dc0d4eb6e7399bd7ffe269d669 (patch) | |
| tree | dc8cdb00fee7c98e58adf447eb2a1a80d83588d0 /src | |
| parent | 6b36e91036824e38a1f558579823ea648e188ac8 (diff) | |
config: use individual transport enable flags
Diffstat (limited to 'src')
| -rw-r--r-- | src/config/mod.rs | 204 | ||||
| -rw-r--r-- | src/main.rs | 3 | ||||
| -rw-r--r-- | src/transport/libp2p/mod.rs | 2 | ||||
| -rw-r--r-- | src/transport/mod.rs | 2 | ||||
| -rw-r--r-- | src/transport/paths.rs | 2 | ||||
| -rw-r--r-- | src/transport/tor.rs | 2 |
6 files changed, 146 insertions, 69 deletions
diff --git a/src/config/mod.rs b/src/config/mod.rs index 25b39ea..418c8e3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -271,15 +271,32 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ "Chunk size when serving large transfers to other nodes (e.g. an epoch's snapshot to a rejoining node), 1KiB to 4MiB.", ), option( - "transports", + "mycelium-enable", + Kind::Flag, + "BOOL", + Some("false"), + "Use the Mycelium transport (in builds with the mycelium feature). A node uses every transport enabled, and needs one at least (none is on by default); nodes on several carry the traffic between nodes that share none.", + ), + option( + "libp2p-enable", + Kind::Flag, + "BOOL", + Some("false"), + "Use the libp2p transport (in builds with the libp2p feature).", + ), + option( + "tor-enable", + Kind::Flag, + "BOOL", + Some("false"), + "Use the Tor transport (in builds with the tor feature).", + ), + option( + "transport-order", Kind::List, "NAME,[NAME...]", - if cfg!(feature = "mycelium") { - Some("mycelium") - } else { - None - }, - "The transports this node uses, in order of preference: mycelium, libp2p and tor (each in builds with its cargo feature; mycelium is the default where built). A peer is reached on the first one both have, until the paths are measured (then on the fastest); nodes on several carry the traffic between nodes that share none.", + Some("mycelium,libp2p,tor"), + "The transports' order of preference, whether enabled or not: a peer is reached on the first enabled one both have, until the paths are measured (then on the fastest). Transports left out come after, in the default order.", ), option( "libp2p-listen", @@ -370,7 +387,7 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ Kind::Flag, "BOOL", Some("false"), - "Only relay for nodes of swarms (libp2p circuit relay), without being a node: no document, no swarm, no node key. Needs transports = libp2p, libp2p-listen and libp2p-key-file; options about a swarm or a document are errors with it. List it for a swarm in the document (_continuum/relays), or configure it (libp2p-relays). The HTTP API (with htpasswd) has /health, /admin/stats and /admin/log only.", + "Only relay for nodes of swarms (libp2p circuit relay), without being a node: no document, no swarm, no node key. Needs libp2p alone (libp2p-enable, no other transport), libp2p-listen and libp2p-key-file; options about a swarm or a document are errors with it. List it for a swarm in the document (_continuum/relays), or configure it (libp2p-relays). The HTTP API (with htpasswd) has /health, /admin/stats and /admin/log only.", ), option( "libp2p-key-file", @@ -519,7 +536,17 @@ pub(crate) struct Settings { /// In bytes. #[serde(default = "default_blob_chunk", with = "crate::units::size_option")] pub(crate) blob_chunk: u64, - #[serde(default = "default_transports")] + #[serde(default)] + pub(crate) mycelium_enable: bool, + #[serde(default)] + pub(crate) libp2p_enable: bool, + #[serde(default)] + pub(crate) tor_enable: bool, + #[serde(default = "default_transport_order")] + pub(crate) transport_order: Vec<String>, + /// The transports this node uses, in order of preference: those enabled, in + /// `transport_order` (`resolve`). + #[serde(skip)] pub(crate) transports: Vec<String>, #[serde(default)] #[cfg_attr(not(feature = "libp2p"), allow(dead_code))] @@ -606,15 +633,13 @@ fn default_clock_skew_warn() -> Duration { fn default_blob_chunk() -> u64 { 64 * 1024 } -/// Mycelium where built; else none, so the transports must be named. -fn default_transports() -> Vec<String> { - if cfg!(feature = "mycelium") { - vec!["mycelium".to_owned()] - } else { - Vec::new() - } +fn default_transport_order() -> Vec<String> { + TRANSPORTS + .iter() + .map(|(name, _)| (*name).to_owned()) + .collect() } -/// The transports `transports` may name, and whether this build has each. +/// The transports there are, in the default order, and whether this build has each. pub(crate) const TRANSPORTS: &[(&str, bool)] = &[ ("mycelium", cfg!(feature = "mycelium")), ("libp2p", cfg!(feature = "libp2p")), @@ -671,9 +696,33 @@ impl Settings { ); } } + for (index, name) in self.transport_order.iter().enumerate() { + if !TRANSPORTS.iter().any(|(known, _)| known == name) { + bail!( + "transport-order: unknown transport {name:?} (known: {})", + TRANSPORTS + .iter() + .map(|(known, _)| *known) + .collect::<Vec<_>>() + .join(", ") + ); + } + if self.transport_order[..index].contains(name) { + bail!("transport-order: {name} is named twice"); + } + } + for (name, built) in TRANSPORTS { + if self.enabled(name) && !built { + bail!("{name}-enable: this build has no {name} (built without the {name} feature)"); + } + } if self.transports.is_empty() { bail!( - "transports: a node needs at least one (this build has: {})", + "no transport enabled: a node needs at least one ({}-enable; this build has: {})", + TRANSPORTS + .iter() + .find(|(_, built)| *built) + .map_or("libp2p", |(name, _)| *name), TRANSPORTS .iter() .filter(|(_, built)| *built) @@ -682,25 +731,6 @@ impl Settings { .join(", ") ); } - for (index, name) in self.transports.iter().enumerate() { - match TRANSPORTS.iter().find(|(known, _)| known == name) { - None => bail!( - "transports: unknown transport {name:?} (known: {})", - TRANSPORTS - .iter() - .map(|(known, _)| *known) - .collect::<Vec<_>>() - .join(", ") - ), - Some((_, false)) => { - bail!("transports: this build has no {name} (built without the {name} feature)") - } - Some(_) if self.transports[..index].contains(name) => { - bail!("transports: {name} is named twice") - } - Some(_) => {} - } - } // Each transport's own options, now that the names are good. crate::transport::check(self)?; if self.debug_epoch.is_some() && !self.debug_api { @@ -729,6 +759,32 @@ impl Settings { } impl Settings { + /// Whether transport `name` is enabled (`<name>-enable`). + fn enabled(&self, name: &str) -> bool { + match name { + "mycelium" => self.mycelium_enable, + "libp2p" => self.libp2p_enable, + "tor" => self.tor_enable, + _ => false, + } + } + + /// The transports used, in order: the enabled ones, as `transport_order` puts + /// them, then those it leaves out, in the default order. + fn ordered_transports(&self) -> Vec<String> { + self.transport_order + .iter() + .map(String::as_str) + .chain(TRANSPORTS.iter().map(|(name, _)| *name)) + .filter(|name| self.enabled(name)) + .fold(Vec::new(), |mut used, name| { + if !used.iter().any(|known: &String| known == name) { + used.push(name.to_owned()); + } + used + }) + } + /// A relay that is no node: libp2p alone, dialable, with a key kept, and nothing /// about a swarm or a document. fn check_relay_only(&self) -> Result<()> { @@ -736,7 +792,7 @@ impl Settings { bail!("relay-only relays over libp2p; this build has no libp2p"); } if self.transports != ["libp2p"] { - bail!("relay-only relays over libp2p alone: set transports to libp2p"); + bail!("relay-only relays over libp2p alone: set libp2p-enable, and no other transport"); } // Its listen address and key: libp2p's checks (`transport::libp2p::check`). let swarm_options = [ @@ -1244,10 +1300,11 @@ pub(crate) struct Loaded { } impl Loaded { - /// The options given for transports this node doesn't use (`transports`), which it - /// ignores: per transport, whether this build has it, and the options. A shared - /// configuration may well carry them; still worth a warning, as it may as well be a - /// mistake (a build without that transport, or a transport left out of the list). + /// The options given for transports this node doesn't use (not enabled), which it + /// ignores: per transport, whether this build has it, and the options (but its + /// `-enable`). A shared configuration may well carry them; still worth a warning, + /// as it may as well be a mistake (a build without that transport, or a transport + /// not enabled). pub(crate) fn unused_transport_options(&self) -> Vec<(&'static str, bool, Vec<String>)> { TRANSPORTS .iter() @@ -1257,7 +1314,11 @@ impl Loaded { let options: Vec<String> = self .origins .iter() - .filter(|(option, origin)| option.starts_with(&prefix) && !origin.unset) + .filter(|(option, origin)| { + option.starts_with(&prefix) + && *option != &format!("{name}-enable") + && !origin.unset + }) .map(|(option, _)| option.clone()) .collect(); (!options.is_empty()).then_some((*name, *built, options)) @@ -1421,6 +1482,7 @@ fn resolve(merged: Layer, files: Vec<PathBuf>) -> Result<Loaded> { } })?; settings.alias = settings.alias.map(|alias| alias.trim().to_owned()); + settings.transports = settings.ordered_transports(); settings.check().context("invalid configuration")?; Ok(Loaded { settings, @@ -1474,13 +1536,13 @@ mod tests { layer } - /// Unless `layer` names transports: this build's first, with what it needs to start + /// Unless `layer` enables transports: this build's first, with what it needs to start /// (Tor its state directory). fn with_built_transport(layer: &mut Layer) { - if layer.contains_key("transports") { + if layer.keys().any(|name| name.ends_with("-enable")) { return; } - layer.insert("transports".to_owned(), entry(json!([built_transport()]))); + layer.insert(format!("{}-enable", built_transport()), entry(json!(true))); if built_transport() == "tor" { layer .entry("tor-state-dir".to_owned()) @@ -1527,8 +1589,10 @@ mod tests { options.sort_unstable(); assert_eq!(names, options); - // Help defaults match the real ones. - for spec in OPTIONS { + // Help defaults match the real ones (but for what `layer` sets to start a + // transport: `with_built_transport`). + let set = format!("{}-enable", built_transport()); + for spec in OPTIONS.iter().filter(|spec| spec.name != set) { if let Some(default) = spec.default { assert_eq!( from_text(spec.kind, default).unwrap(), @@ -1766,19 +1830,24 @@ mod tests { ) .is_err() ); - let transports = |names: JsonValue| { - resolve( - layer(json!({ "htpasswd": "u", "node-key-file": "node.key", "transports": names })), - Vec::new(), - ) + // The transports enabled (`names`), the others not, in `order` if given. + let transports = |names: &[&str], order: Option<JsonValue>| { + let mut settings = json!({ "htpasswd": "u", "node-key-file": "node.key" }); + for (name, _) in TRANSPORTS { + settings[format!("{name}-enable")] = json!(names.contains(name)); + } + if let Some(order) = order { + settings["transport-order"] = order; + } + resolve(layer(settings), Vec::new()) }; - assert!(transports(json!([])).is_err()); + assert!(transports(&[], None).is_err()); // Without the node key: an error, but for a relay. assert!(resolve(layer(json!({ "htpasswd": "u" })), Vec::new()).is_err()); let relay = |extra: JsonValue| { let mut settings = json!({ "relay-only": true, - "transports": ["libp2p"], + "libp2p-enable": true, "libp2p-listen": ["/ip6/::/tcp/9661"], "libp2p-key-file": "libp2p.key", }); @@ -1795,19 +1864,26 @@ mod tests { ); assert!(relay(json!({ "node-key-file": "node.key" })).is_err()); assert!(relay(json!({ "seeds": ["a"] })).is_err()); - assert!(relay(json!({ "transports": ["mycelium"] })).is_err()); - assert!(relay(json!({ "transports": ["tor"] })).is_err()); + assert!(relay(json!({ "mycelium-enable": true })).is_err()); + assert!(relay(json!({ "tor-enable": true })).is_err()); assert!(relay(json!({ "libp2p-listen": [] })).is_err()); - assert!(transports(json!(["pigeon"])).is_err()); - assert!(transports(json!(["mycelium", "mycelium"])).is_err()); + assert!(transports(&["mycelium"], Some(json!(["pigeon"]))).is_err()); + assert!(transports(&["mycelium"], Some(json!(["mycelium", "mycelium"]))).is_err()); assert_eq!( - transports(json!(["mycelium"])).is_ok(), + transports(&["mycelium"], None).is_ok(), cfg!(feature = "mycelium") ); - assert_eq!( - transports(json!(["libp2p", "mycelium"])).is_ok(), - cfg!(feature = "libp2p") && cfg!(feature = "mycelium") - ); + if cfg!(feature = "libp2p") && cfg!(feature = "mycelium") { + // In the order given, the ones left out after; whether enabled or not. + let used = |order| { + transports(&["libp2p", "mycelium"], order) + .unwrap() + .settings + .transports + }; + assert_eq!(used(None), ["mycelium", "libp2p"]); + assert_eq!(used(Some(json!(["tor", "libp2p"]))), ["libp2p", "mycelium"]); + } } #[test] diff --git a/src/main.rs b/src/main.rs index a6f2937..40f0167 100644 --- a/src/main.rs +++ b/src/main.rs @@ -169,7 +169,8 @@ async fn main() -> Result<()> { }; debug!(epoch = %initial_epoch, "Local epoch"); - // In the order of preference (`--transports`, checked when loading). + // The enabled, in the order of preference (`--transport-order`, checked when + // loading). let mut transports: Vec<(&'static str, Arc<dyn Transport>)> = Vec::new(); for name in &args.transports { let Some(transport) = transport::start(name, args, &node_key, stop.clone()).await? else { diff --git a/src/transport/libp2p/mod.rs b/src/transport/libp2p/mod.rs index ace84c6..6032891 100644 --- a/src/transport/libp2p/mod.rs +++ b/src/transport/libp2p/mod.rs @@ -1,4 +1,4 @@ -//! libp2p as a transport (`--transports libp2p`, in builds with the `libp2p` feature): +//! libp2p as a transport (`--libp2p-enable`, in builds with the `libp2p` feature): //! direct connections over TCP (Noise, Yamux) and QUIC, to addresses this node knows. //! //! A node's transport identity is its libp2p peer ID; where it is, its listen addresses diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 705e473..5ad2427 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -35,7 +35,7 @@ compile_error!( ); /// The options of the transports this build has, each checked by its transport when the -/// configuration is loaded (`config::Settings::check`, after the names in `transports`): +/// configuration is loaded (`config::Settings::check`, after the transports enabled): /// Mycelium's always (its conflicts are errors wherever they come from, as before); /// libp2p's and Tor's if the node uses them. Options of transports not built aren't /// checked: they're ignored, and said so (`config::Loaded::unused_transport_options`). diff --git a/src/transport/paths.rs b/src/transport/paths.rs index 7ebde70..658f681 100644 --- a/src/transport/paths.rs +++ b/src/transport/paths.rs @@ -8,7 +8,7 @@ //! transport of its address, hands every transport's messages to one handler under the //! sender's address, and puts together what the transports say about themselves //! (addresses, seeds they found, figures). The transports are in order of preference, -//! as configured (`--transports`). +//! as configured (`--transport-order`). //! //! A message to a node with several addresses goes to the first of them that takes it //! (`send_first`), in the order `rank` gives. A send that fails is tried at once at the diff --git a/src/transport/tor.rs b/src/transport/tor.rs index 001309b..b091abb 100644 --- a/src/transport/tor.rs +++ b/src/transport/tor.rs @@ -1,4 +1,4 @@ -//! Tor as a transport (`--transports tor`, in builds with the `tor` feature), for +//! Tor as a transport (`--tor-enable`, in builds with the `tor` feature), for //! reachability: every node runs an onion service (arti), reachable from anywhere //! without a forwarded port or a relay. Not for anonymity: a node's other addresses are //! in the member list next to its onion address. |
