Add custom Iroh relay maps
This commit is contained in:
parent
81149605bc
commit
280472cecb
10 changed files with 241 additions and 32 deletions
10
AGENTS.md
10
AGENTS.md
|
|
@ -105,8 +105,8 @@ Roadmap items should be actionable and checkable:
|
||||||
daemon-owned Iroh endpoint startup, built-in relay-mode config, SSH
|
daemon-owned Iroh endpoint startup, built-in relay-mode config, SSH
|
||||||
certificate metadata, revocation metadata, user service definitions, and a
|
certificate metadata, revocation metadata, user service definitions, and a
|
||||||
pinned `geth-iroh` endpoint wrapper with protocol-router scaffold, peer-card
|
pinned `geth-iroh` endpoint wrapper with protocol-router scaffold, peer-card
|
||||||
types, and untrusted discovery-backend trait exist.
|
types, untrusted discovery-backend trait, and custom relay-map config exist.
|
||||||
- Custom relay maps, mDNS discovery transport, peer auth over Iroh, cr-sqlite,
|
- mDNS discovery transport, peer auth over Iroh, cr-sqlite, iroh-docs,
|
||||||
iroh-docs, iroh-gossip, iroh-blobs, Automerge sync, real auth enforcement,
|
iroh-gossip, iroh-blobs, Automerge sync, real auth enforcement, OpenSSH KRL
|
||||||
OpenSSH KRL generation, and Keyhive/BeeKEM-style authorization are future
|
generation, and Keyhive/BeeKEM-style authorization are future roadmap items
|
||||||
roadmap items unless implemented later.
|
unless implemented later.
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1092,6 +1092,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"toml_edit",
|
"toml_edit",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "rt-multi
|
||||||
toml_edit = "0.25.11"
|
toml_edit = "0.25.11"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
|
url = "2"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unsafe_code = "forbid"
|
unsafe_code = "forbid"
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ All remote node-to-node geth communication is designed to happen over Iroh only.
|
||||||
SSH is not a geth transport backend, and there is no SSH fallback transport.
|
SSH is not a geth transport backend, and there is no SSH fallback transport.
|
||||||
The default node config uses Iroh's default relay policy for practical
|
The default node config uses Iroh's default relay policy for practical
|
||||||
connectivity; set `[iroh].relay_mode = "disabled"` for local-only/offline
|
connectivity; set `[iroh].relay_mode = "disabled"` for local-only/offline
|
||||||
development.
|
development. Named custom relay maps can be selected with
|
||||||
|
`relay_mode = "custom"` and `relay_map = "<name>"`.
|
||||||
|
|
||||||
SSH keys are used as admin trust anchors and ecosystem integration points.
|
SSH keys are used as admin trust anchors and ecosystem integration points.
|
||||||
OpenSSH, FIDO, and YubiKey-backed keys can sign geth trust objects through
|
OpenSSH, FIDO, and YubiKey-backed keys can sign geth trust objects through
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,4 @@ directories.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
toml_edit.workspace = true
|
toml_edit.workspace = true
|
||||||
|
url.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub const DEFAULT_CONFIG_TOML: &str = r#"# geth local node config
|
pub const DEFAULT_CONFIG_TOML: &str = r#"# geth local node config
|
||||||
# Remote node-to-node communication is Iroh-only.
|
# Remote node-to-node communication is Iroh-only.
|
||||||
#
|
#
|
||||||
# relay_mode controls Iroh relay use for practical internet connectivity.
|
# relay_mode controls Iroh relay use for practical internet connectivity.
|
||||||
# Values: "default", "staging", "disabled".
|
# Values: "default", "staging", "disabled", "custom".
|
||||||
[iroh]
|
[iroh]
|
||||||
relay_mode = "default"
|
relay_mode = "default"
|
||||||
|
# relay_map = "home"
|
||||||
|
#
|
||||||
|
# [iroh.relay_maps.home]
|
||||||
|
# urls = ["https://relay.example.com"]
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -96,6 +101,18 @@ pub enum ConfigError {
|
||||||
TomlParse(#[from] toml_edit::TomlError),
|
TomlParse(#[from] toml_edit::TomlError),
|
||||||
#[error("invalid iroh relay_mode: {0}")]
|
#[error("invalid iroh relay_mode: {0}")]
|
||||||
InvalidRelayMode(String),
|
InvalidRelayMode(String),
|
||||||
|
#[error("custom iroh relay_mode requires iroh.relay_map")]
|
||||||
|
MissingRelayMapSelection,
|
||||||
|
#[error("selected iroh relay map does not exist: {0}")]
|
||||||
|
UnknownRelayMap(String),
|
||||||
|
#[error("invalid iroh relay_maps table")]
|
||||||
|
InvalidRelayMapsTable,
|
||||||
|
#[error("invalid iroh relay map `{0}`")]
|
||||||
|
InvalidRelayMap(String),
|
||||||
|
#[error("iroh relay map `{0}` must include at least one URL")]
|
||||||
|
EmptyRelayMap(String),
|
||||||
|
#[error("invalid iroh relay URL in map `{map}`: {url}")]
|
||||||
|
InvalidRelayUrl { map: String, url: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
|
@ -118,18 +135,22 @@ impl GethConfig {
|
||||||
|
|
||||||
pub fn parse(text: &str) -> Result<Self, ConfigError> {
|
pub fn parse(text: &str) -> Result<Self, ConfigError> {
|
||||||
let document = text.parse::<toml_edit::DocumentMut>()?;
|
let document = text.parse::<toml_edit::DocumentMut>()?;
|
||||||
|
let relay_maps = parse_relay_maps(&document)?;
|
||||||
let relay_mode = match document.get("iroh").and_then(|iroh| iroh.get("relay_mode")) {
|
let relay_mode = match document.get("iroh").and_then(|iroh| iroh.get("relay_mode")) {
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
let value = item
|
let value = item
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| ConfigError::InvalidRelayMode(item.to_string()))?;
|
.ok_or_else(|| ConfigError::InvalidRelayMode(item.to_string()))?;
|
||||||
RelayMode::parse(value)?
|
RelayMode::parse(value, &document, &relay_maps)?
|
||||||
}
|
}
|
||||||
None => RelayMode::default(),
|
None => RelayMode::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
iroh: IrohConfig { relay_mode },
|
iroh: IrohConfig {
|
||||||
|
relay_mode,
|
||||||
|
relay_maps,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,36 +158,117 @@ impl GethConfig {
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
pub struct IrohConfig {
|
pub struct IrohConfig {
|
||||||
pub relay_mode: RelayMode,
|
pub relay_mode: RelayMode,
|
||||||
|
pub relay_maps: BTreeMap<String, RelayMapConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RelayMapConfig {
|
||||||
|
pub urls: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
pub enum RelayMode {
|
pub enum RelayMode {
|
||||||
Disabled,
|
Disabled,
|
||||||
#[default]
|
#[default]
|
||||||
Default,
|
Default,
|
||||||
Staging,
|
Staging,
|
||||||
|
Custom {
|
||||||
|
map: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RelayMode {
|
impl RelayMode {
|
||||||
pub fn parse(value: &str) -> Result<Self, ConfigError> {
|
fn parse(
|
||||||
|
value: &str,
|
||||||
|
document: &toml_edit::DocumentMut,
|
||||||
|
relay_maps: &BTreeMap<String, RelayMapConfig>,
|
||||||
|
) -> Result<Self, ConfigError> {
|
||||||
match value {
|
match value {
|
||||||
"disabled" => Ok(Self::Disabled),
|
"disabled" => Ok(Self::Disabled),
|
||||||
"default" => Ok(Self::Default),
|
"default" => Ok(Self::Default),
|
||||||
"staging" => Ok(Self::Staging),
|
"staging" => Ok(Self::Staging),
|
||||||
|
"custom" => {
|
||||||
|
let map = document
|
||||||
|
.get("iroh")
|
||||||
|
.and_then(|iroh| iroh.get("relay_map"))
|
||||||
|
.and_then(toml_edit::Item::as_str)
|
||||||
|
.ok_or(ConfigError::MissingRelayMapSelection)?
|
||||||
|
.to_owned();
|
||||||
|
if !relay_maps.contains_key(&map) {
|
||||||
|
return Err(ConfigError::UnknownRelayMap(map));
|
||||||
|
}
|
||||||
|
Ok(Self::Custom { map })
|
||||||
|
}
|
||||||
other => Err(ConfigError::InvalidRelayMode(other.to_owned())),
|
other => Err(ConfigError::InvalidRelayMode(other.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn as_str(self) -> &'static str {
|
pub fn label(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::Disabled => "disabled",
|
Self::Disabled => "disabled".to_owned(),
|
||||||
Self::Default => "default",
|
Self::Default => "default".to_owned(),
|
||||||
Self::Staging => "staging",
|
Self::Staging => "staging".to_owned(),
|
||||||
|
Self::Custom { map } => format!("custom:{map}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_relay_maps(
|
||||||
|
document: &toml_edit::DocumentMut,
|
||||||
|
) -> Result<BTreeMap<String, RelayMapConfig>, ConfigError> {
|
||||||
|
let Some(relay_maps_item) = document.get("iroh").and_then(|iroh| iroh.get("relay_maps")) else {
|
||||||
|
return Ok(BTreeMap::new());
|
||||||
|
};
|
||||||
|
let relay_maps_table = relay_maps_item
|
||||||
|
.as_table_like()
|
||||||
|
.ok_or(ConfigError::InvalidRelayMapsTable)?;
|
||||||
|
let mut relay_maps = BTreeMap::new();
|
||||||
|
for (name, item) in relay_maps_table.iter() {
|
||||||
|
let urls = parse_relay_map_urls(name, item)?;
|
||||||
|
relay_maps.insert(name.to_owned(), RelayMapConfig { urls });
|
||||||
|
}
|
||||||
|
Ok(relay_maps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_relay_map_urls(name: &str, item: &toml_edit::Item) -> Result<Vec<String>, ConfigError> {
|
||||||
|
let table = item
|
||||||
|
.as_table_like()
|
||||||
|
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
||||||
|
let urls_item = table
|
||||||
|
.get("urls")
|
||||||
|
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
||||||
|
let urls_array = urls_item
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
||||||
|
let mut urls = Vec::new();
|
||||||
|
for value in urls_array.iter() {
|
||||||
|
let url = value
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
||||||
|
validate_relay_url(name, url)?;
|
||||||
|
urls.push(url.to_owned());
|
||||||
|
}
|
||||||
|
if urls.is_empty() {
|
||||||
|
return Err(ConfigError::EmptyRelayMap(name.to_owned()));
|
||||||
|
}
|
||||||
|
Ok(urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_relay_url(map: &str, relay_url: &str) -> Result<(), ConfigError> {
|
||||||
|
let url = url::Url::parse(relay_url).map_err(|_| ConfigError::InvalidRelayUrl {
|
||||||
|
map: map.to_owned(),
|
||||||
|
url: relay_url.to_owned(),
|
||||||
|
})?;
|
||||||
|
match url.scheme() {
|
||||||
|
"http" | "https" => Ok(()),
|
||||||
|
_ => Err(ConfigError::InvalidRelayUrl {
|
||||||
|
map: map.to_owned(),
|
||||||
|
url: relay_url.to_owned(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -183,6 +285,32 @@ mod tests {
|
||||||
assert_eq!(config.iroh.relay_mode, RelayMode::Disabled);
|
assert_eq!(config.iroh.relay_mode, RelayMode::Disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_parses_custom_relay_map() {
|
||||||
|
let config = GethConfig::parse(
|
||||||
|
r#"
|
||||||
|
[iroh]
|
||||||
|
relay_mode = "custom"
|
||||||
|
relay_map = "home"
|
||||||
|
|
||||||
|
[iroh.relay_maps.home]
|
||||||
|
urls = ["https://relay.example.com"]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("parse");
|
||||||
|
assert_eq!(
|
||||||
|
config.iroh.relay_mode,
|
||||||
|
RelayMode::Custom {
|
||||||
|
map: "home".to_owned()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
config.iroh.relay_maps["home"].urls,
|
||||||
|
vec!["https://relay.example.com".to_owned()]
|
||||||
|
);
|
||||||
|
assert_eq!(config.iroh.relay_mode.label(), "custom:home");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_rejects_unknown_relay_mode() {
|
fn config_rejects_unknown_relay_mode() {
|
||||||
let error = GethConfig::parse("[iroh]\nrelay_mode = \"magic\"\n").expect_err("error");
|
let error = GethConfig::parse("[iroh]\nrelay_mode = \"magic\"\n").expect_err("error");
|
||||||
|
|
@ -194,4 +322,27 @@ mod tests {
|
||||||
let error = GethConfig::parse("[iroh]\nrelay_mode = true\n").expect_err("error");
|
let error = GethConfig::parse("[iroh]\nrelay_mode = true\n").expect_err("error");
|
||||||
assert!(matches!(error, ConfigError::InvalidRelayMode(_)));
|
assert!(matches!(error, ConfigError::InvalidRelayMode(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_rejects_invalid_custom_relay_url() {
|
||||||
|
let error = GethConfig::parse(
|
||||||
|
r#"
|
||||||
|
[iroh]
|
||||||
|
relay_mode = "custom"
|
||||||
|
relay_map = "home"
|
||||||
|
|
||||||
|
[iroh.relay_maps.home]
|
||||||
|
urls = ["not a url"]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect_err("error");
|
||||||
|
assert!(matches!(error, ConfigError::InvalidRelayUrl { map, .. } if map == "home"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_rejects_missing_custom_relay_map() {
|
||||||
|
let error = GethConfig::parse("[iroh]\nrelay_mode = \"custom\"\nrelay_map = \"home\"\n")
|
||||||
|
.expect_err("error");
|
||||||
|
assert!(matches!(error, ConfigError::UnknownRelayMap(map) if map == "home"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,23 +160,43 @@ pub enum GethRelayMode {
|
||||||
Disabled,
|
Disabled,
|
||||||
Default,
|
Default,
|
||||||
Staging,
|
Staging,
|
||||||
|
Custom {
|
||||||
|
name: String,
|
||||||
|
relay_urls: Vec<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GethRelayMode {
|
impl GethRelayMode {
|
||||||
fn to_iroh(&self) -> iroh::RelayMode {
|
fn to_iroh(&self) -> Result<iroh::RelayMode, IrohError> {
|
||||||
match self {
|
match self {
|
||||||
Self::Disabled => iroh::RelayMode::Disabled,
|
Self::Disabled => Ok(iroh::RelayMode::Disabled),
|
||||||
Self::Default => iroh::RelayMode::Default,
|
Self::Default => Ok(iroh::RelayMode::Default),
|
||||||
Self::Staging => iroh::RelayMode::Staging,
|
Self::Staging => Ok(iroh::RelayMode::Staging),
|
||||||
|
Self::Custom { relay_urls, .. } => {
|
||||||
|
let relay_urls = relay_urls
|
||||||
|
.iter()
|
||||||
|
.map(|url| {
|
||||||
|
url.parse::<iroh::RelayUrl>()
|
||||||
|
.map_err(|error| IrohError::InvalidRelayUrl {
|
||||||
|
url: url.clone(),
|
||||||
|
message: error.to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
Ok(iroh::RelayMode::Custom(iroh::RelayMap::from_iter(
|
||||||
|
relay_urls,
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub fn label(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::Disabled => "disabled",
|
Self::Disabled => "disabled".to_owned(),
|
||||||
Self::Default => "default",
|
Self::Default => "default".to_owned(),
|
||||||
Self::Staging => "staging",
|
Self::Staging => "staging".to_owned(),
|
||||||
|
Self::Custom { name, .. } => format!("custom:{name}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +226,7 @@ pub async fn start_endpoint(config: &GethIrohConfig) -> Result<GethIrohEndpoint,
|
||||||
let secret_key = load_or_create_secret_key(&config.secret_key_path)?;
|
let secret_key = load_or_create_secret_key(&config.secret_key_path)?;
|
||||||
let mut builder = iroh::Endpoint::builder()
|
let mut builder = iroh::Endpoint::builder()
|
||||||
.secret_key(secret_key)
|
.secret_key(secret_key)
|
||||||
.relay_mode(config.relay_mode.to_iroh())
|
.relay_mode(config.relay_mode.to_iroh()?)
|
||||||
.alpns(config.alpns.clone());
|
.alpns(config.alpns.clone());
|
||||||
|
|
||||||
if let Some(bind_ipv4) = config.bind_ipv4 {
|
if let Some(bind_ipv4) = config.bind_ipv4 {
|
||||||
|
|
@ -220,7 +240,7 @@ pub async fn start_endpoint(config: &GethIrohConfig) -> Result<GethIrohEndpoint,
|
||||||
let status = EndpointStatus {
|
let status = EndpointStatus {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
endpoint_id: Some(endpoint.node_id().to_string()),
|
endpoint_id: Some(endpoint.node_id().to_string()),
|
||||||
relay_mode: config.relay_mode.as_str().to_owned(),
|
relay_mode: config.relay_mode.label(),
|
||||||
note: format!(
|
note: format!(
|
||||||
"Iroh endpoint started with iroh 0.90.0; relay mode: {:?}",
|
"Iroh endpoint started with iroh 0.90.0; relay mode: {:?}",
|
||||||
config.relay_mode
|
config.relay_mode
|
||||||
|
|
@ -286,6 +306,8 @@ pub enum IrohError {
|
||||||
Hex(#[from] hex::FromHexError),
|
Hex(#[from] hex::FromHexError),
|
||||||
#[error("invalid iroh secret key length")]
|
#[error("invalid iroh secret key length")]
|
||||||
InvalidSecretKeyLength,
|
InvalidSecretKeyLength,
|
||||||
|
#[error("invalid iroh relay URL `{url}`: {message}")]
|
||||||
|
InvalidRelayUrl { url: String, message: String },
|
||||||
#[error("failed to bind iroh endpoint: {0}")]
|
#[error("failed to bind iroh endpoint: {0}")]
|
||||||
Bind(Box<iroh::endpoint::BindError>),
|
Bind(Box<iroh::endpoint::BindError>),
|
||||||
}
|
}
|
||||||
|
|
@ -370,11 +392,27 @@ mod tests {
|
||||||
let config = GethIrohConfig::local_with_relay("iroh.ed25519", GethRelayMode::Staging);
|
let config = GethIrohConfig::local_with_relay("iroh.ed25519", GethRelayMode::Staging);
|
||||||
assert_eq!(config.relay_mode, GethRelayMode::Staging);
|
assert_eq!(config.relay_mode, GethRelayMode::Staging);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
config.relay_mode.to_iroh(),
|
config.relay_mode.to_iroh().expect("iroh relay mode"),
|
||||||
iroh::RelayMode::Staging
|
iroh::RelayMode::Staging
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_relay_mode_builds_iroh_relay_map() {
|
||||||
|
let mode = GethRelayMode::Custom {
|
||||||
|
name: "home".to_owned(),
|
||||||
|
relay_urls: vec!["https://relay.example.com".to_owned()],
|
||||||
|
};
|
||||||
|
let relay_mode = mode.to_iroh().expect("custom relay mode");
|
||||||
|
match relay_mode {
|
||||||
|
iroh::RelayMode::Custom(map) => {
|
||||||
|
assert_eq!(map.len(), 1);
|
||||||
|
assert_eq!(mode.label(), "custom:home");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected relay mode: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn endpoint_status_tracks_node_id_when_bind_is_available() {
|
async fn endpoint_status_tracks_node_id_when_bind_is_available() {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
|
||||||
|
|
@ -454,8 +454,9 @@ async fn start_daemon_iroh_endpoint(
|
||||||
node: &mut LocalNode,
|
node: &mut LocalNode,
|
||||||
) -> Result<Option<GethIrohEndpoint>, NodeError> {
|
) -> Result<Option<GethIrohEndpoint>, NodeError> {
|
||||||
let node_config = GethConfig::load(&node.paths.config_file())?;
|
let node_config = GethConfig::load(&node.paths.config_file())?;
|
||||||
let relay_mode = node_config.iroh.relay_mode;
|
let relay_mode = node_config.iroh.relay_mode.clone();
|
||||||
let iroh_relay_mode = config_relay_mode_to_iroh(relay_mode);
|
let relay_mode_label = relay_mode.label();
|
||||||
|
let iroh_relay_mode = config_relay_mode_to_iroh(&relay_mode, &node_config.iroh.relay_maps);
|
||||||
let config = GethIrohConfig::local_with_relay(node.paths.iroh_key(), iroh_relay_mode);
|
let config = GethIrohConfig::local_with_relay(node.paths.iroh_key(), iroh_relay_mode);
|
||||||
match geth_iroh::start_endpoint(&config).await {
|
match geth_iroh::start_endpoint(&config).await {
|
||||||
Ok(endpoint) => {
|
Ok(endpoint) => {
|
||||||
|
|
@ -471,7 +472,7 @@ async fn start_daemon_iroh_endpoint(
|
||||||
node.iroh_status = EndpointStatus {
|
node.iroh_status = EndpointStatus {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
endpoint_id: None,
|
endpoint_id: None,
|
||||||
relay_mode: relay_mode.as_str().to_owned(),
|
relay_mode: relay_mode_label,
|
||||||
note: format!("Iroh endpoint failed to start: {error}"),
|
note: format!("Iroh endpoint failed to start: {error}"),
|
||||||
};
|
};
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|
@ -479,11 +480,23 @@ async fn start_daemon_iroh_endpoint(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config_relay_mode_to_iroh(mode: RelayMode) -> GethRelayMode {
|
fn config_relay_mode_to_iroh(
|
||||||
|
mode: &RelayMode,
|
||||||
|
relay_maps: &std::collections::BTreeMap<String, geth_config::RelayMapConfig>,
|
||||||
|
) -> GethRelayMode {
|
||||||
match mode {
|
match mode {
|
||||||
RelayMode::Disabled => GethRelayMode::Disabled,
|
RelayMode::Disabled => GethRelayMode::Disabled,
|
||||||
RelayMode::Default => GethRelayMode::Default,
|
RelayMode::Default => GethRelayMode::Default,
|
||||||
RelayMode::Staging => GethRelayMode::Staging,
|
RelayMode::Staging => GethRelayMode::Staging,
|
||||||
|
RelayMode::Custom { map } => {
|
||||||
|
let relay_map = relay_maps
|
||||||
|
.get(map)
|
||||||
|
.expect("custom relay map was validated during config load");
|
||||||
|
GethRelayMode::Custom {
|
||||||
|
name: map.clone(),
|
||||||
|
relay_urls: relay_map.urls.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,10 @@ a transitive `ed25519-dalek` prerelease dependency. `geth-iroh` wraps
|
||||||
`Builder::relay_mode`, persists an `iroh::SecretKey` as hex-encoded 32-byte key
|
`Builder::relay_mode`, persists an `iroh::SecretKey` as hex-encoded 32-byte key
|
||||||
material, and shuts down through `Endpoint::close().await`. The default config
|
material, and shuts down through `Endpoint::close().await`. The default config
|
||||||
uses Iroh's default relay policy; local-only/offline development can set
|
uses Iroh's default relay policy; local-only/offline development can set
|
||||||
`[iroh].relay_mode = "disabled"`.
|
`[iroh].relay_mode = "disabled"`. Named custom relay maps are configured under
|
||||||
|
`[iroh.relay_maps.<name>]`, selected with `relay_mode = "custom"` plus
|
||||||
|
`relay_map = "<name>"`, validated at config load, and reported in status as
|
||||||
|
`custom:<name>` without exposing relay URLs.
|
||||||
|
|
||||||
Module ALPNs are registered through `geth-iroh`'s protocol router scaffold. The
|
Module ALPNs are registered through `geth-iroh`'s protocol router scaffold. The
|
||||||
router owns the default protocol descriptors, rejects duplicate ALPN
|
router owns the default protocol descriptors, rejects duplicate ALPN
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ geth-to-geth connections without granting trust from discovery alone.
|
||||||
- `geth status --json` reports the selected relay mode.
|
- `geth status --json` reports the selected relay mode.
|
||||||
- Tests cover config parsing and endpoint builder relay-mode selection.
|
- Tests cover config parsing and endpoint builder relay-mode selection.
|
||||||
|
|
||||||
- `[ ]` Custom relay maps.
|
- `[x]` Custom relay maps.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- Config can define and select named custom relay maps.
|
- Config can define and select named custom relay maps.
|
||||||
- Invalid relay URLs fail config validation with clear errors.
|
- Invalid relay URLs fail config validation with clear errors.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue