Add Iroh relay mode config

This commit is contained in:
Eric Wendland 2026-05-16 03:17:45 +02:00
commit 0f2ad755d9
13 changed files with 210 additions and 21 deletions

View file

@ -507,6 +507,7 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
"endpoint: {}",
status.endpoint_id.as_deref().unwrap_or("not started")
);
println!("iroh relay: {}", status.iroh_relay_mode);
println!("iroh: {}", status.iroh);
}
ControlResponse::NodeId(node) => {

View file

@ -9,3 +9,4 @@ license.workspace = true
directories.workspace = true
serde.workspace = true
thiserror.workspace = true
toml_edit.workspace = true

View file

@ -1,5 +1,14 @@
use std::path::{Path, PathBuf};
pub const DEFAULT_CONFIG_TOML: &str = r#"# geth local node config
# Remote node-to-node communication is Iroh-only.
#
# relay_mode controls Iroh relay use for practical internet connectivity.
# Values: "default", "staging", "disabled".
[iroh]
relay_mode = "default"
"#;
#[derive(Clone, Debug)]
pub struct GethPaths {
home: PathBuf,
@ -83,4 +92,106 @@ pub enum ConfigError {
NoDataDirectory,
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("toml parse error: {0}")]
TomlParse(#[from] toml_edit::TomlError),
#[error("invalid iroh relay_mode: {0}")]
InvalidRelayMode(String),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct GethConfig {
pub iroh: IrohConfig,
}
impl GethConfig {
#[must_use]
pub fn default_toml() -> &'static str {
DEFAULT_CONFIG_TOML
}
pub fn load(path: &Path) -> Result<Self, ConfigError> {
if !path.exists() {
return Ok(Self::default());
}
Self::parse(&std::fs::read_to_string(path)?)
}
pub fn parse(text: &str) -> Result<Self, ConfigError> {
let document = text.parse::<toml_edit::DocumentMut>()?;
let relay_mode = match document.get("iroh").and_then(|iroh| iroh.get("relay_mode")) {
Some(item) => {
let value = item
.as_str()
.ok_or_else(|| ConfigError::InvalidRelayMode(item.to_string()))?;
RelayMode::parse(value)?
}
None => RelayMode::default(),
};
Ok(Self {
iroh: IrohConfig { relay_mode },
})
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct IrohConfig {
pub relay_mode: RelayMode,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum RelayMode {
Disabled,
#[default]
Default,
Staging,
}
impl RelayMode {
pub fn parse(value: &str) -> Result<Self, ConfigError> {
match value {
"disabled" => Ok(Self::Disabled),
"default" => Ok(Self::Default),
"staging" => Ok(Self::Staging),
other => Err(ConfigError::InvalidRelayMode(other.to_owned())),
}
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Disabled => "disabled",
Self::Default => "default",
Self::Staging => "staging",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_uses_iroh_relays() {
let config = GethConfig::parse(GethConfig::default_toml()).expect("parse config");
assert_eq!(config.iroh.relay_mode, RelayMode::Default);
}
#[test]
fn config_parses_disabled_relay_mode() {
let config = GethConfig::parse("[iroh]\nrelay_mode = \"disabled\"\n").expect("parse");
assert_eq!(config.iroh.relay_mode, RelayMode::Disabled);
}
#[test]
fn config_rejects_unknown_relay_mode() {
let error = GethConfig::parse("[iroh]\nrelay_mode = \"magic\"\n").expect_err("error");
assert!(matches!(error, ConfigError::InvalidRelayMode(value) if value == "magic"));
}
#[test]
fn config_rejects_non_string_relay_mode() {
let error = GethConfig::parse("[iroh]\nrelay_mode = true\n").expect_err("error");
assert!(matches!(error, ConfigError::InvalidRelayMode(_)));
}
}

View file

@ -148,6 +148,7 @@ pub struct StatusResponse {
pub node_id: String,
pub iroh_enabled: bool,
pub endpoint_id: Option<String>,
pub iroh_relay_mode: String,
pub iroh: String,
}

View file

@ -23,9 +23,17 @@ pub struct GethIrohConfig {
impl GethIrohConfig {
#[must_use]
pub fn local(secret_key_path: impl Into<PathBuf>) -> Self {
Self::local_with_relay(secret_key_path, GethRelayMode::Default)
}
#[must_use]
pub fn local_with_relay(
secret_key_path: impl Into<PathBuf>,
relay_mode: GethRelayMode,
) -> Self {
Self {
secret_key_path: secret_key_path.into(),
relay_mode: GethRelayMode::Disabled,
relay_mode,
bind_ipv4: None,
bind_ipv6: None,
alpns: all_alpns(),
@ -49,6 +57,15 @@ impl GethRelayMode {
Self::Staging => iroh::RelayMode::Staging,
}
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Disabled => "disabled",
Self::Default => "default",
Self::Staging => "staging",
}
}
}
pub struct GethIrohEndpoint {
@ -90,6 +107,7 @@ pub async fn start_endpoint(config: &GethIrohConfig) -> Result<GethIrohEndpoint,
let status = EndpointStatus {
enabled: true,
endpoint_id: Some(endpoint.node_id().to_string()),
relay_mode: config.relay_mode.as_str().to_owned(),
note: format!(
"Iroh endpoint started with iroh 0.90.0; relay mode: {:?}",
config.relay_mode
@ -143,6 +161,7 @@ pub fn all_alpns() -> Vec<Vec<u8>> {
pub struct EndpointStatus {
pub enabled: bool,
pub endpoint_id: Option<String>,
pub relay_mode: String,
pub note: String,
}
@ -152,6 +171,7 @@ impl EndpointStatus {
Self {
enabled: false,
endpoint_id: None,
relay_mode: "not-started".to_owned(),
note: "Iroh endpoint is not running outside daemon mode".to_owned(),
}
}
@ -197,15 +217,29 @@ mod tests {
assert_eq!(first.public(), second.public());
}
#[test]
fn local_config_selects_relay_mode() {
let config = GethIrohConfig::local_with_relay("iroh.ed25519", GethRelayMode::Staging);
assert_eq!(config.relay_mode, GethRelayMode::Staging);
assert!(matches!(
config.relay_mode.to_iroh(),
iroh::RelayMode::Staging
));
}
#[tokio::test]
async fn endpoint_status_tracks_node_id_when_bind_is_available() {
let dir = tempfile::tempdir().expect("tempdir");
let config = GethIrohConfig::local(dir.path().join("iroh.ed25519"));
let config = GethIrohConfig::local_with_relay(
dir.path().join("iroh.ed25519"),
GethRelayMode::Disabled,
);
match start_endpoint(&config).await {
Ok(endpoint) => {
let status = endpoint.status();
assert!(status.enabled);
assert_eq!(status.endpoint_id, Some(endpoint.node_id()));
assert_eq!(status.relay_mode, "disabled");
endpoint.shutdown().await;
}
Err(IrohError::Bind(error)) => {

View file

@ -2,13 +2,13 @@ pub mod service;
use geth_auth::AuthExplanation;
use geth_cas::{LocalCas, hash_path};
use geth_config::GethPaths;
use geth_config::{GethConfig, GethPaths, RelayMode};
use geth_control::{
CasBlob, ControlRequest, ControlResponse, KeychainStatusResponse, NodeIdResponse,
StatusResponse,
};
use geth_crypto::AgentKey;
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint};
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
use geth_resource::ResourceDescriptor;
use geth_ssh_identity::{
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
@ -68,10 +68,7 @@ pub struct LocalNode {
pub fn init_node(paths: &GethPaths) -> Result<LocalNode, NodeError> {
paths.ensure_base_dirs()?;
if !paths.config_file().exists() {
std::fs::write(
paths.config_file(),
"# geth local node config\n# Remote node-to-node communication is Iroh-only.\n",
)?;
std::fs::write(paths.config_file(), GethConfig::default_toml())?;
}
let key = AgentKey::load_or_create(&paths.agent_key())?;
let agent_id = key.agent_id().to_string();
@ -163,6 +160,7 @@ pub fn handle_request(
node_id: node.node_id.clone(),
iroh_enabled: node.iroh_status.enabled,
endpoint_id: node.iroh_status.endpoint_id.clone(),
iroh_relay_mode: node.iroh_status.relay_mode.clone(),
iroh: node.iroh_status.note.clone(),
})),
ControlRequest::NodeId => Ok(ControlResponse::NodeId(NodeIdResponse {
@ -447,7 +445,10 @@ fn stable_node_id(agent_id: &str) -> String {
async fn start_daemon_iroh_endpoint(
node: &mut LocalNode,
) -> Result<Option<GethIrohEndpoint>, NodeError> {
let config = GethIrohConfig::local(node.paths.iroh_key());
let node_config = GethConfig::load(&node.paths.config_file())?;
let relay_mode = node_config.iroh.relay_mode;
let iroh_relay_mode = config_relay_mode_to_iroh(relay_mode);
let config = GethIrohConfig::local_with_relay(node.paths.iroh_key(), iroh_relay_mode);
match geth_iroh::start_endpoint(&config).await {
Ok(endpoint) => {
let status = endpoint.status();
@ -462,6 +463,7 @@ async fn start_daemon_iroh_endpoint(
node.iroh_status = EndpointStatus {
enabled: false,
endpoint_id: None,
relay_mode: relay_mode.as_str().to_owned(),
note: format!("Iroh endpoint failed to start: {error}"),
};
Ok(None)
@ -469,6 +471,14 @@ async fn start_daemon_iroh_endpoint(
}
}
fn config_relay_mode_to_iroh(mode: RelayMode) -> GethRelayMode {
match mode {
RelayMode::Disabled => GethRelayMode::Disabled,
RelayMode::Default => GethRelayMode::Default,
RelayMode::Staging => GethRelayMode::Staging,
}
}
fn expected_openssh_cert_path(public_key_path: &Path) -> String {
let text = public_key_path.display().to_string();
if let Some(prefix) = text.strip_suffix(".pub") {

View file

@ -59,6 +59,11 @@ fn geth_init_in_temp_home() {
assert!(home.path().join("geth.sqlite").exists());
assert!(home.path().join("identity/agent.ed25519").exists());
assert!(home.path().join("config.toml").exists());
assert!(
std::fs::read_to_string(home.path().join("config.toml"))
.expect("read config")
.contains("relay_mode = \"default\"")
);
}
#[test]
@ -68,6 +73,11 @@ fn geth_status_against_running_daemon() {
return;
}
assert!(run_geth(home.path(), &["init"]).status.success());
std::fs::write(
home.path().join("config.toml"),
"[iroh]\nrelay_mode = \"disabled\"\n",
)
.expect("write config");
let mut daemon = spawn_daemon(home.path());
wait_for_socket(&home.path().join("run/geth.sock"));
@ -84,6 +94,7 @@ fn geth_status_against_running_daemon() {
assert!(stdout.contains("geth daemon: running"));
assert!(stdout.contains("agent:"));
assert!(stdout.contains("endpoint:"));
assert!(stdout.contains("iroh relay: disabled"));
}
#[test]