From 5c79174b36f7ce077b3d1b9feafe88008696fc31 Mon Sep 17 00:00:00 2001 From: tv Date: Wed, 28 Oct 2026 17:40:07 +0100 Subject: introduce relay only mode --- src/config/mod.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 6 deletions(-) (limited to 'src/config/mod.rs') diff --git a/src/config/mod.rs b/src/config/mod.rs index 0890d3b..9210922 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -100,7 +100,7 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ Kind::Text, "PATH", None, - "Users of the HTTP API (htpasswd file with Argon2 hashes). Required.", + "Users of the HTTP API (htpasswd file with Argon2 hashes). Required, but with relay-only (then without it, no HTTP API).", ), option( "protocol-writers", @@ -191,7 +191,7 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ Kind::Text, "PATH", None, - "This node's key (Ed25519, 32 raw bytes): who the node is, whatever transport reaches it. Keep it with the node: a new key is a new node. Required.", + "This node's key (Ed25519, 32 raw bytes): who the node is, whatever transport reaches it. Keep it with the node: a new key is a new node. Required, but with relay-only (then not allowed).", ), option( "create-node-key", @@ -305,6 +305,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ Some("9662"), "With the tor transport: the port of this node's onion service (published with its onion address).", ), + option( + "relay-only", + 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.", + ), option( "libp2p-key-file", Kind::Text, @@ -377,7 +384,8 @@ pub(crate) fn spec(name: &str) -> Option<&'static OptionSpec> { pub(crate) struct Settings { #[serde(default = "default_bind_addr")] pub(crate) bind_addr: SocketAddr, - pub(crate) htpasswd: String, + /// Required (`check`), but with `relay_only`. + pub(crate) htpasswd: Option, #[serde(default)] pub(crate) protocol_writers: Vec, #[serde(default)] @@ -404,7 +412,8 @@ pub(crate) struct Settings { pub(crate) remake: Option, #[serde(default)] pub(crate) force: Vec, - pub(crate) node_key_file: PathBuf, + /// Required (`check`), but not allowed with `relay_only`. + pub(crate) node_key_file: Option, #[serde(default)] pub(crate) create_node_key: bool, pub(crate) swarm_key_file: Option, @@ -448,6 +457,8 @@ pub(crate) struct Settings { #[serde(default = "default_tor_port")] #[cfg_attr(not(feature = "tor"), allow(dead_code))] pub(crate) tor_port: u16, + #[serde(default)] + pub(crate) relay_only: bool, pub(crate) mycelium_rest_url: Option, pub(crate) mycelium_key_file: Option, pub(crate) mycelium_network_key_file: Option, @@ -552,6 +563,18 @@ impl Settings { if self.genesis && self.recover_from.is_some() { bail!("recover-from starts the swarm from a backup; it can't be combined with genesis"); } + if self.relay_only { + self.check_relay_only()?; + } else { + if self.htpasswd.is_none() { + bail!("htpasswd is required: the users of the HTTP API"); + } + if self.node_key_file.is_none() { + bail!( + "node-key-file is required: who this node is (create one with create-node-key)" + ); + } + } if self.transports.is_empty() { bail!("transports: a node needs at least one"); } @@ -604,6 +627,53 @@ impl Settings { } } +impl Settings { + /// 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<()> { + if !cfg!(feature = "libp2p") { + 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"); + } + if self.libp2p_listen.is_empty() { + bail!("relay-only needs libp2p-listen: where nodes reach the relay"); + } + if self.libp2p_key_file.is_none() { + bail!( + "relay-only needs libp2p-key-file: without it, the relay's peer ID changes with every start and nodes lose it" + ); + } + let swarm_options = [ + ("node-key-file", self.node_key_file.is_some()), + ("create-node-key", self.create_node_key), + ("seeds", !self.seeds.is_empty()), + ("genesis", self.genesis), + ("state-file", self.state_file.is_some()), + ("document-file", self.document_file.is_some()), + ("recover-from", self.recover_from.is_some()), + ("remake", self.remake.is_some()), + ("swarm-key-file", self.swarm_key_file.is_some()), + ("protocol-writers", !self.protocol_writers.is_empty()), + ("alias", self.alias.is_some()), + ("debug-api", self.debug_api), + ]; + let set: Vec<&str> = swarm_options + .iter() + .filter(|(_, set)| *set) + .map(|(name, _)| *name) + .collect(); + if !set.is_empty() { + bail!( + "relay-only is no node of a swarm, so these don't apply: {}", + set.join(", ") + ); + } + Ok(()) + } +} + /// Up to 64 characters, no control characters, not blank. fn check_alias(alias: &str) -> Result<()> { if alias.trim().is_empty() { @@ -949,8 +1019,6 @@ fn culprit(merged: &Layer) -> Option<(&String, &Entry)> { return false; } let mut alone = serde_json::Map::new(); - alone.insert("htpasswd".to_owned(), JsonValue::String(String::new())); - alone.insert("node-key-file".to_owned(), JsonValue::String(String::new())); alone.insert((*name).clone(), entry.value.clone()); serde_json::from_value::(JsonValue::Object(alone)).is_err() }) @@ -1506,6 +1574,30 @@ mod tests { ) }; assert!(transports(json!([])).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-listen": ["/ip6/::/tcp/9661"], + "libp2p-key-file": "libp2p.key", + }); + settings + .as_object_mut() + .unwrap() + .extend(extra.as_object().unwrap().clone()); + resolve(layer(settings), Vec::new()) + }; + assert_eq!(relay(json!({})).is_ok(), cfg!(feature = "libp2p")); + assert_eq!( + relay(json!({ "htpasswd": "u" })).is_ok(), + cfg!(feature = "libp2p") + ); + 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!({ "libp2p-listen": [] })).is_err()); assert!(transports(json!(["pigeon"])).is_err()); assert!(transports(json!(["mycelium", "mycelium"])).is_err()); assert!(transports(json!(["mycelium"])).is_ok()); -- cgit v1.3.1