Upgrade Iroh native backend foundation

This commit is contained in:
Eric Wendland 2026-05-22 15:28:52 +02:00
commit 9d46dd4d0d
11 changed files with 1588 additions and 712 deletions

View file

@ -1534,10 +1534,12 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
backend.target_version,
backend.status
);
println!(
"native backend {} blocker: {}",
backend.module, backend.blocker
);
if !backend.blocker.is_empty() {
println!(
"native backend {} note: {}",
backend.module, backend.blocker
);
}
}
}
ControlResponse::NodeId(node) => {

View file

@ -7,9 +7,13 @@ license.workspace = true
[dependencies]
hex.workspace = true
ed25519.workspace = true
pkcs8.workspace = true
iroh.workspace = true
n0-watcher.workspace = true
rand_core.workspace = true
iroh-blobs.workspace = true
iroh-docs.workspace = true
iroh-gossip.workspace = true
rand.workspace = true
serde.workspace = true
thiserror.workspace = true
tokio.workspace = true

View file

@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::net::{SocketAddrV4, SocketAddrV6};
use std::path::{Path, PathBuf};
use std::time::Duration;
pub const ALPN_CONTROL: &[u8] = b"/geth/control/1";
pub const ALPN_KV: &[u8] = b"/geth/kv/1";
@ -13,6 +12,11 @@ pub const ALPN_DB: &[u8] = b"/geth/db/1";
pub const ALPN_DOCUMENT: &[u8] = b"/geth/document/1";
pub const ALPN_SSH_PROXY: &[u8] = b"/geth/ssh-proxy/1";
pub const IROH_VERSION: &str = "0.95.1";
pub const IROH_BLOBS_VERSION: &str = "0.97.0";
pub const IROH_DOCS_VERSION: &str = "0.95.0";
pub const IROH_GOSSIP_VERSION: &str = "0.95.0";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GethIrohConfig {
pub secret_key_path: PathBuf,
@ -157,6 +161,38 @@ pub fn default_protocol_descriptors() -> Vec<ProtocolDescriptor> {
]
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NativeIrohLibrary {
pub module: String,
pub crate_name: String,
pub crate_version: String,
pub alpn: String,
}
#[must_use]
pub fn native_iroh_libraries() -> Vec<NativeIrohLibrary> {
vec![
NativeIrohLibrary {
module: "cas".to_owned(),
crate_name: "iroh-blobs".to_owned(),
crate_version: IROH_BLOBS_VERSION.to_owned(),
alpn: display_alpn(iroh_blobs::ALPN),
},
NativeIrohLibrary {
module: "kv".to_owned(),
crate_name: "iroh-docs".to_owned(),
crate_version: IROH_DOCS_VERSION.to_owned(),
alpn: display_alpn(iroh_docs::ALPN),
},
NativeIrohLibrary {
module: "pubsub".to_owned(),
crate_name: "iroh-gossip".to_owned(),
crate_version: IROH_GOSSIP_VERSION.to_owned(),
alpn: display_alpn(iroh_gossip::ALPN),
},
]
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum GethRelayMode {
@ -218,7 +254,7 @@ impl GethIrohEndpoint {
#[must_use]
pub fn node_id(&self) -> String {
self.endpoint.node_id().to_string()
self.endpoint.id().to_string()
}
#[must_use]
@ -227,19 +263,12 @@ impl GethIrohEndpoint {
}
pub async fn node_addr_snapshot(&self) -> Result<GethNodeAddr, IrohError> {
use n0_watcher::Watcher;
let mut watcher = self.endpoint.node_addr();
let node_addr = tokio::time::timeout(Duration::from_secs(2), watcher.initialized())
.await
.map_err(|_| IrohError::NodeAddrTimeout)?
.map_err(|error| IrohError::NodeAddrUnavailable(error.to_string()))?;
let endpoint_addr = self.endpoint.addr();
Ok(GethNodeAddr {
endpoint_id: node_addr.node_id.to_string(),
relay_url: node_addr.relay_url.map(|url| url.to_string()),
direct_addresses: node_addr
.direct_addresses
.into_iter()
endpoint_id: endpoint_addr.id.to_string(),
relay_url: endpoint_addr.relay_urls().next().map(ToString::to_string),
direct_addresses: endpoint_addr
.ip_addrs()
.map(|addr| addr.to_string())
.collect(),
})
@ -265,7 +294,7 @@ pub async fn start_endpoint(config: &GethIrohConfig) -> Result<GethIrohEndpoint,
.alpns(config.alpns.clone());
if config.local_discovery {
builder = builder.discovery_local_network();
builder = builder.discovery(iroh::discovery::mdns::MdnsDiscovery::builder());
}
if let Some(bind_ipv4) = config.bind_ipv4 {
@ -278,12 +307,17 @@ pub async fn start_endpoint(config: &GethIrohConfig) -> Result<GethIrohEndpoint,
let endpoint = builder.bind().await.map_err(IrohError::from)?;
let status = EndpointStatus {
enabled: true,
endpoint_id: Some(endpoint.node_id().to_string()),
endpoint_id: Some(endpoint.id().to_string()),
relay_mode: config.relay_mode.label(),
local_discovery: config.local_discovery,
note: format!(
"Iroh endpoint started with iroh 0.90.0; relay mode: {:?}; local discovery: {}",
config.relay_mode, config.local_discovery
"Iroh endpoint started with iroh {}; relay mode: {:?}; local discovery: {}; native libraries: iroh-blobs {}, iroh-docs {}, iroh-gossip {}",
IROH_VERSION,
config.relay_mode,
config.local_discovery,
IROH_BLOBS_VERSION,
IROH_DOCS_VERSION,
IROH_GOSSIP_VERSION
),
};
Ok(GethIrohEndpoint { endpoint, status })
@ -297,7 +331,7 @@ pub fn load_or_create_secret_key(path: &Path) -> Result<iroh::SecretKey, IrohErr
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let secret_key = iroh::SecretKey::generate(rand_core::OsRng);
let secret_key = iroh::SecretKey::generate(&mut rand::rng());
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, hex::encode(secret_key.to_bytes()))?;
std::fs::rename(tmp, path)?;
@ -389,6 +423,30 @@ mod tests {
assert_eq!(alpns.len(), 8);
}
#[test]
fn native_iroh_libraries_are_pinned_and_expose_alpns() {
let libraries = native_iroh_libraries();
assert_eq!(libraries.len(), 3);
assert!(libraries.iter().any(|library| {
library.module == "cas"
&& library.crate_name == "iroh-blobs"
&& library.crate_version == IROH_BLOBS_VERSION
&& library.alpn == "/iroh-bytes/4"
}));
assert!(libraries.iter().any(|library| {
library.module == "kv"
&& library.crate_name == "iroh-docs"
&& library.crate_version == IROH_DOCS_VERSION
&& library.alpn == "/iroh-sync/1"
}));
assert!(libraries.iter().any(|library| {
library.module == "pubsub"
&& library.crate_name == "iroh-gossip"
&& library.crate_version == IROH_GOSSIP_VERSION
&& library.alpn == "/iroh-gossip/1"
}));
}
#[test]
fn default_router_registers_all_protocols() {
let router = default_protocol_router();

View file

@ -1045,10 +1045,7 @@ async fn peer_ping(node: &LocalNode, peer_node: &str) -> Result<ControlResponse,
.connect(node_addr, geth_iroh::ALPN_CONTROL)
.await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
let alpn = conn
.alpn()
.map(display_alpn)
.unwrap_or_else(|| "unknown".to_owned());
let alpn = display_alpn(conn.alpn());
let (mut send, mut recv) = conn
.open_bi()
.await
@ -2178,14 +2175,16 @@ async fn handle_local_pipe_tcp_stream(
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
remote_send
.finish()
.map_err(|error| NodeError::IrohPeer(error.to_string()))
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
Ok::<(), NodeError>(())
};
let mut remote_recv = remote_recv;
let download = async {
tokio::io::copy(&mut remote_recv, &mut local_write)
.await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
local_write.shutdown().await.map_err(NodeError::from)
local_write.shutdown().await.map_err(NodeError::from)?;
Ok::<(), NodeError>(())
};
tokio::try_join!(upload, download)?;
Ok(())
@ -2301,14 +2300,16 @@ async fn handle_local_pipe_unix_stream(
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
remote_send
.finish()
.map_err(|error| NodeError::IrohPeer(error.to_string()))
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
Ok::<(), NodeError>(())
};
let mut remote_recv = remote_recv;
let download = async {
tokio::io::copy(&mut remote_recv, &mut local_write)
.await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
local_write.shutdown().await.map_err(NodeError::from)
local_write.shutdown().await.map_err(NodeError::from)?;
Ok::<(), NodeError>(())
};
tokio::try_join!(upload, download)?;
Ok(())
@ -2511,14 +2512,16 @@ async fn handle_local_ssh_proxy_stream(
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
remote_send
.finish()
.map_err(|error| NodeError::IrohPeer(error.to_string()))
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
Ok::<(), NodeError>(())
};
let mut remote_recv = remote_recv;
let download = async {
tokio::io::copy(&mut remote_recv, &mut local_write)
.await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
local_write.shutdown().await.map_err(NodeError::from)
local_write.shutdown().await.map_err(NodeError::from)?;
Ok::<(), NodeError>(())
};
tokio::try_join!(upload, download)?;
Ok(())
@ -4437,22 +4440,16 @@ async fn handle_iroh_control_connection(
let conn = incoming
.await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
let remote_endpoint_id = conn
.remote_node_id()
.map(|node_id| node_id.to_string())
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
let alpn = conn
.alpn()
.map(display_alpn)
.unwrap_or_else(|| "unknown".to_owned());
let remote_endpoint_id = conn.remote_id().to_string();
let alpn = display_alpn(conn.alpn());
let (mut send, mut recv) = conn
.accept_bi()
.await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
if alpn == display_alpn(geth_iroh::ALPN_SSH_PROXY.to_vec()) {
if alpn == display_alpn(geth_iroh::ALPN_SSH_PROXY) {
return handle_ssh_proxy_wire_connection(node, remote_endpoint_id, send, recv).await;
}
if alpn == display_alpn(geth_iroh::ALPN_PIPE.to_vec()) {
if alpn == display_alpn(geth_iroh::ALPN_PIPE) {
return handle_pipe_wire_connection(node, remote_endpoint_id, send, recv).await;
}
let bytes = recv
@ -5946,10 +5943,10 @@ async fn handle_pipe_unix_wire_connection(
fn iroh_node_addr_from_candidate(
candidate: &EndpointCandidate,
) -> Result<iroh::NodeAddr, NodeError> {
let node_id = candidate
) -> Result<iroh::EndpointAddr, NodeError> {
let endpoint_id = candidate
.endpoint_id
.parse::<iroh::NodeId>()
.parse::<iroh::EndpointId>()
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
let direct_addresses = candidate
.direct_addresses
@ -5959,7 +5956,10 @@ fn iroh_node_addr_from_candidate(
.map_err(|error| NodeError::IrohPeer(error.to_string()))
})
.collect::<Result<Vec<_>, _>>()?;
let mut node_addr = iroh::NodeAddr::new(node_id).with_direct_addresses(direct_addresses);
let mut node_addr = iroh::EndpointAddr::new(endpoint_id);
for address in direct_addresses {
node_addr = node_addr.with_ip_addr(address);
}
if let Some(relay_url) = &candidate.relay_url {
node_addr = node_addr.with_relay_url(
relay_url
@ -6005,8 +6005,8 @@ fn restricted_admin_shell_output(node: &LocalNode, command: &str) -> Result<Stri
}
}
fn display_alpn(alpn: Vec<u8>) -> String {
String::from_utf8_lossy(&alpn).into_owned()
fn display_alpn(alpn: &[u8]) -> String {
String::from_utf8_lossy(alpn).into_owned()
}
pub fn handle_request(
@ -7661,33 +7661,21 @@ pub fn handle_request(
}
fn native_backend_statuses() -> Vec<NativeBackendStatus> {
let blocker = "pinned blocker: current daemon endpoint uses iroh 0.90.0; the Rust-1.85-compatible backend crates resolved from crates.io require iroh 0.95, so they cannot share the daemon-owned endpoint until geth performs a coordinated Iroh endpoint upgrade".to_owned();
vec![
NativeBackendStatus {
module: "cas".to_owned(),
current_backend: "iroh-control-alpn-bootstrap".to_owned(),
target_crate: "iroh-blobs".to_owned(),
target_version: "0.97.0".to_owned(),
status: "blocked".to_owned(),
blocker: blocker.clone(),
},
NativeBackendStatus {
module: "kv".to_owned(),
current_backend: "iroh-control-alpn-bootstrap".to_owned(),
target_crate: "iroh-docs".to_owned(),
target_version: "0.95.0".to_owned(),
status: "blocked".to_owned(),
blocker: blocker.clone(),
},
NativeBackendStatus {
module: "pubsub".to_owned(),
current_backend: "iroh-control-alpn-bootstrap".to_owned(),
target_crate: "iroh-gossip".to_owned(),
target_version: "0.95.0".to_owned(),
status: "blocked".to_owned(),
blocker,
},
]
geth_iroh::native_iroh_libraries()
.into_iter()
.map(|library| NativeBackendStatus {
module: library.module,
current_backend: "shared-iroh-endpoint-ready".to_owned(),
target_crate: library.crate_name,
target_version: library.crate_version,
status: "ready-to-wire".to_owned(),
blocker: format!(
"none; native ALPN {} is compiled against the daemon-owned iroh {} endpoint, but the module still needs its bootstrap control-path implementation replaced",
library.alpn,
geth_iroh::IROH_VERSION
),
})
.collect()
}
fn stored_resource_to_descriptor(stored: StoredResource) -> Result<ResourceDescriptor, NodeError> {

View file

@ -155,13 +155,13 @@ fn geth_status_against_running_daemon() {
assert!(stdout.contains("iroh relay: disabled"));
assert!(stdout.contains("iroh discovery: local-network disabled"));
assert!(stdout.contains(
"native backend cas: iroh-control-alpn-bootstrap target iroh-blobs 0.97.0 (blocked)"
"native backend cas: shared-iroh-endpoint-ready target iroh-blobs 0.97.0 (ready-to-wire)"
));
assert!(stdout.contains(
"native backend kv: iroh-control-alpn-bootstrap target iroh-docs 0.95.0 (blocked)"
"native backend kv: shared-iroh-endpoint-ready target iroh-docs 0.95.0 (ready-to-wire)"
));
assert!(stdout.contains(
"native backend pubsub: iroh-control-alpn-bootstrap target iroh-gossip 0.95.0 (blocked)"
"native backend pubsub: shared-iroh-endpoint-ready target iroh-gossip 0.95.0 (ready-to-wire)"
));
}