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

1991
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -42,13 +42,19 @@ blake3 = "1"
bytes = "1" bytes = "1"
clap = { version = "4", features = ["derive", "env"] } clap = { version = "4", features = ["derive", "env"] }
directories = "5" directories = "5"
# Compatibility pins for iroh 0.95's ed25519-dalek prerelease dependency.
ed25519 = "=3.0.0-rc.1"
ed25519-dalek = { version = "2", features = ["rand_core"] } ed25519-dalek = { version = "2", features = ["rand_core"] }
pkcs8 = "=0.11.0-rc.11"
futures = "0.3" futures = "0.3"
hex = "0.4" hex = "0.4"
hexane = "=0.1.5" hexane = "=0.1.5"
iroh = { version = "0.90.0", features = ["discovery-local-network"] } iroh = { version = "0.95.1", features = ["discovery-local-network"] }
n0-watcher = "0.2" iroh-blobs = "0.97.0"
iroh-docs = "0.95.0"
iroh-gossip = "0.95.0"
postcard = { version = "1", features = ["alloc"] } postcard = { version = "1", features = ["alloc"] }
rand = "0.9"
rand_core = { version = "0.6", features = ["getrandom"] } rand_core = { version = "0.6", features = ["getrandom"] }
rusqlite = { version = "0.32", features = ["bundled"] } rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }

View file

@ -198,13 +198,12 @@ has `cas.fetch` on `resource:cas:local`, and the caller verifies that the bytes
hash to the requested BLAKE3 CAS hash before storing them locally. Successful hash to the requested BLAKE3 CAS hash before storing them locally. Successful
fetches record the serving peer as a local provider, visible with fetches record the serving peer as a local provider, visible with
`geth cas providers <hash>`. This is the bootstrap transfer path; future work `geth cas providers <hash>`. This is the bootstrap transfer path; future work
will move provider/fetch behavior to `iroh-blobs`. `geth status` currently will move provider/fetch behavior to `iroh-blobs`. `geth-iroh` is now pinned to
reports the native backend blocker for CAS, KV, and pubsub: the daemon endpoint `iroh 0.95.1` and compiles the native backend libraries `iroh-blobs 0.97.0`,
is pinned to `iroh 0.90.0`, while the Rust-1.85-compatible backend crates `iroh-docs 0.95.0`, and `iroh-gossip 0.95.0` against the same daemon-owned
resolved from crates.io are `iroh-blobs 0.97.0`, `iroh-docs 0.95.0`, and endpoint generation. `geth status` reports those backends as ready to wire; the
`iroh-gossip 0.95.0`, all of which require `iroh 0.95`. These cannot be wired module implementations still use the explicit bootstrap control path until the
to the daemon-owned endpoint until the endpoint wrapper is upgraded in one module-specific migrations replace it.
coordinated step.
Remote resource commands that accept `--bearer-secret` can also authorize with a Remote resource commands that accept `--bearer-secret` can also authorize with a
resource-scoped bearer proof generated from the private bearer token returned at resource-scoped bearer proof generated from the private bearer token returned at
creation time. The persisted auth log stores a public bearer id and token creation time. The persisted auth log stores a public bearer id and token

View file

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

View file

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

View file

@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::net::{SocketAddrV4, SocketAddrV6}; use std::net::{SocketAddrV4, SocketAddrV6};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration;
pub const ALPN_CONTROL: &[u8] = b"/geth/control/1"; pub const ALPN_CONTROL: &[u8] = b"/geth/control/1";
pub const ALPN_KV: &[u8] = b"/geth/kv/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_DOCUMENT: &[u8] = b"/geth/document/1";
pub const ALPN_SSH_PROXY: &[u8] = b"/geth/ssh-proxy/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)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GethIrohConfig { pub struct GethIrohConfig {
pub secret_key_path: PathBuf, 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)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum GethRelayMode { pub enum GethRelayMode {
@ -218,7 +254,7 @@ impl GethIrohEndpoint {
#[must_use] #[must_use]
pub fn node_id(&self) -> String { pub fn node_id(&self) -> String {
self.endpoint.node_id().to_string() self.endpoint.id().to_string()
} }
#[must_use] #[must_use]
@ -227,19 +263,12 @@ impl GethIrohEndpoint {
} }
pub async fn node_addr_snapshot(&self) -> Result<GethNodeAddr, IrohError> { pub async fn node_addr_snapshot(&self) -> Result<GethNodeAddr, IrohError> {
use n0_watcher::Watcher; let endpoint_addr = self.endpoint.addr();
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()))?;
Ok(GethNodeAddr { Ok(GethNodeAddr {
endpoint_id: node_addr.node_id.to_string(), endpoint_id: endpoint_addr.id.to_string(),
relay_url: node_addr.relay_url.map(|url| url.to_string()), relay_url: endpoint_addr.relay_urls().next().map(ToString::to_string),
direct_addresses: node_addr direct_addresses: endpoint_addr
.direct_addresses .ip_addrs()
.into_iter()
.map(|addr| addr.to_string()) .map(|addr| addr.to_string())
.collect(), .collect(),
}) })
@ -265,7 +294,7 @@ pub async fn start_endpoint(config: &GethIrohConfig) -> Result<GethIrohEndpoint,
.alpns(config.alpns.clone()); .alpns(config.alpns.clone());
if config.local_discovery { 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 { 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 endpoint = builder.bind().await.map_err(IrohError::from)?;
let status = EndpointStatus { let status = EndpointStatus {
enabled: true, enabled: true,
endpoint_id: Some(endpoint.node_id().to_string()), endpoint_id: Some(endpoint.id().to_string()),
relay_mode: config.relay_mode.label(), relay_mode: config.relay_mode.label(),
local_discovery: config.local_discovery, local_discovery: config.local_discovery,
note: format!( note: format!(
"Iroh endpoint started with iroh 0.90.0; relay mode: {:?}; local discovery: {}", "Iroh endpoint started with iroh {}; relay mode: {:?}; local discovery: {}; native libraries: iroh-blobs {}, iroh-docs {}, iroh-gossip {}",
config.relay_mode, config.local_discovery IROH_VERSION,
config.relay_mode,
config.local_discovery,
IROH_BLOBS_VERSION,
IROH_DOCS_VERSION,
IROH_GOSSIP_VERSION
), ),
}; };
Ok(GethIrohEndpoint { endpoint, status }) 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() { if let Some(parent) = path.parent() {
std::fs::create_dir_all(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"); let tmp = path.with_extension("tmp");
std::fs::write(&tmp, hex::encode(secret_key.to_bytes()))?; std::fs::write(&tmp, hex::encode(secret_key.to_bytes()))?;
std::fs::rename(tmp, path)?; std::fs::rename(tmp, path)?;
@ -389,6 +423,30 @@ mod tests {
assert_eq!(alpns.len(), 8); 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] #[test]
fn default_router_registers_all_protocols() { fn default_router_registers_all_protocols() {
let router = default_protocol_router(); 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) .connect(node_addr, geth_iroh::ALPN_CONTROL)
.await .await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?; .map_err(|error| NodeError::IrohPeer(error.to_string()))?;
let alpn = conn let alpn = display_alpn(conn.alpn());
.alpn()
.map(display_alpn)
.unwrap_or_else(|| "unknown".to_owned());
let (mut send, mut recv) = conn let (mut send, mut recv) = conn
.open_bi() .open_bi()
.await .await
@ -2178,14 +2175,16 @@ async fn handle_local_pipe_tcp_stream(
.map_err(|error| NodeError::IrohPeer(error.to_string()))?; .map_err(|error| NodeError::IrohPeer(error.to_string()))?;
remote_send remote_send
.finish() .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 mut remote_recv = remote_recv;
let download = async { let download = async {
tokio::io::copy(&mut remote_recv, &mut local_write) tokio::io::copy(&mut remote_recv, &mut local_write)
.await .await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?; .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)?; tokio::try_join!(upload, download)?;
Ok(()) Ok(())
@ -2301,14 +2300,16 @@ async fn handle_local_pipe_unix_stream(
.map_err(|error| NodeError::IrohPeer(error.to_string()))?; .map_err(|error| NodeError::IrohPeer(error.to_string()))?;
remote_send remote_send
.finish() .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 mut remote_recv = remote_recv;
let download = async { let download = async {
tokio::io::copy(&mut remote_recv, &mut local_write) tokio::io::copy(&mut remote_recv, &mut local_write)
.await .await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?; .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)?; tokio::try_join!(upload, download)?;
Ok(()) Ok(())
@ -2511,14 +2512,16 @@ async fn handle_local_ssh_proxy_stream(
.map_err(|error| NodeError::IrohPeer(error.to_string()))?; .map_err(|error| NodeError::IrohPeer(error.to_string()))?;
remote_send remote_send
.finish() .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 mut remote_recv = remote_recv;
let download = async { let download = async {
tokio::io::copy(&mut remote_recv, &mut local_write) tokio::io::copy(&mut remote_recv, &mut local_write)
.await .await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?; .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)?; tokio::try_join!(upload, download)?;
Ok(()) Ok(())
@ -4437,22 +4440,16 @@ async fn handle_iroh_control_connection(
let conn = incoming let conn = incoming
.await .await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?; .map_err(|error| NodeError::IrohPeer(error.to_string()))?;
let remote_endpoint_id = conn let remote_endpoint_id = conn.remote_id().to_string();
.remote_node_id() let alpn = display_alpn(conn.alpn());
.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 (mut send, mut recv) = conn let (mut send, mut recv) = conn
.accept_bi() .accept_bi()
.await .await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?; .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; 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; return handle_pipe_wire_connection(node, remote_endpoint_id, send, recv).await;
} }
let bytes = recv let bytes = recv
@ -5946,10 +5943,10 @@ async fn handle_pipe_unix_wire_connection(
fn iroh_node_addr_from_candidate( fn iroh_node_addr_from_candidate(
candidate: &EndpointCandidate, candidate: &EndpointCandidate,
) -> Result<iroh::NodeAddr, NodeError> { ) -> Result<iroh::EndpointAddr, NodeError> {
let node_id = candidate let endpoint_id = candidate
.endpoint_id .endpoint_id
.parse::<iroh::NodeId>() .parse::<iroh::EndpointId>()
.map_err(|error| NodeError::IrohPeer(error.to_string()))?; .map_err(|error| NodeError::IrohPeer(error.to_string()))?;
let direct_addresses = candidate let direct_addresses = candidate
.direct_addresses .direct_addresses
@ -5959,7 +5956,10 @@ fn iroh_node_addr_from_candidate(
.map_err(|error| NodeError::IrohPeer(error.to_string())) .map_err(|error| NodeError::IrohPeer(error.to_string()))
}) })
.collect::<Result<Vec<_>, _>>()?; .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 { if let Some(relay_url) = &candidate.relay_url {
node_addr = node_addr.with_relay_url( node_addr = node_addr.with_relay_url(
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 { fn display_alpn(alpn: &[u8]) -> String {
String::from_utf8_lossy(&alpn).into_owned() String::from_utf8_lossy(alpn).into_owned()
} }
pub fn handle_request( pub fn handle_request(
@ -7661,33 +7661,21 @@ pub fn handle_request(
} }
fn native_backend_statuses() -> Vec<NativeBackendStatus> { 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(); geth_iroh::native_iroh_libraries()
vec![ .into_iter()
NativeBackendStatus { .map(|library| NativeBackendStatus {
module: "cas".to_owned(), module: library.module,
current_backend: "iroh-control-alpn-bootstrap".to_owned(), current_backend: "shared-iroh-endpoint-ready".to_owned(),
target_crate: "iroh-blobs".to_owned(), target_crate: library.crate_name,
target_version: "0.97.0".to_owned(), target_version: library.crate_version,
status: "blocked".to_owned(), status: "ready-to-wire".to_owned(),
blocker: blocker.clone(), blocker: format!(
}, "none; native ALPN {} is compiled against the daemon-owned iroh {} endpoint, but the module still needs its bootstrap control-path implementation replaced",
NativeBackendStatus { library.alpn,
module: "kv".to_owned(), geth_iroh::IROH_VERSION
current_backend: "iroh-control-alpn-bootstrap".to_owned(), ),
target_crate: "iroh-docs".to_owned(), })
target_version: "0.95.0".to_owned(), .collect()
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,
},
]
} }
fn stored_resource_to_descriptor(stored: StoredResource) -> Result<ResourceDescriptor, NodeError> { 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 relay: disabled"));
assert!(stdout.contains("iroh discovery: local-network disabled")); assert!(stdout.contains("iroh discovery: local-network disabled"));
assert!(stdout.contains( 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( 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( 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)"
)); ));
} }

View file

@ -12,4 +12,8 @@ prefix-scoped capabilities.
## Consequences ## Consequences
The MVP exposes CLI shape and types while deferring iroh-docs API pinning. The prototype exposes CLI shape and durable local KV state. `iroh-docs 0.95.0`
is pinned and compiles against the daemon-owned `iroh 0.95.1` endpoint
generation, so the remaining work is replacing the bootstrap control-path KV
sync with an Iroh Documents namespace implementation and resource-scoped
authorization checks around namespace access.

View file

@ -18,9 +18,7 @@ Remote geth node-to-node communication is Iroh-only. The daemon will own one
shared Iroh endpoint and register module protocols on ALPNs such as shared Iroh endpoint and register module protocols on ALPNs such as
`/geth/cas/1`, `/geth/kv/1`, `/geth/pipe/1`, and `/geth/ssh-proxy/1`. `/geth/cas/1`, `/geth/kv/1`, `/geth/pipe/1`, and `/geth/ssh-proxy/1`.
The first pinned Iroh integration uses `iroh = 0.90.0`, because newer The pinned Iroh integration uses `iroh = 0.95.1`. `geth-iroh` wraps
Rust-1.85-compatible candidates in the 0.93-0.95 range failed to compile through
a transitive `ed25519-dalek` prerelease dependency. `geth-iroh` wraps
`iroh::Endpoint::builder()`, configures geth ALPNs with `Builder::alpns`, uses `iroh::Endpoint::builder()`, configures geth ALPNs with `Builder::alpns`, uses
`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
@ -30,22 +28,20 @@ uses Iroh's default relay policy; local-only/offline development can set
`relay_map = "<name>"`, validated at config load, and reported in status as `relay_map = "<name>"`, validated at config load, and reported in status as
`custom:<name>` without exposing relay URLs. `custom:<name>` without exposing relay URLs.
The native module-backend crates currently available for the intended CAS, KV, The native module-backend crates for the intended CAS, KV, and pubsub
and pubsub replacements are not wired in yet because they require a coordinated replacements now compile against the same endpoint generation:
endpoint upgrade. Crates.io metadata checked during this prototype pass resolved `iroh-blobs 0.97.0`, `iroh-docs 0.95.0`, and `iroh-gossip 0.95.0`. `geth-iroh`
`iroh-blobs 0.97.0`, `iroh-docs 0.95.0`, and `iroh-gossip 0.95.0` as exposes their native ALPNs so module migrations can register handlers without
Rust-1.85-compatible candidates; those crates depend on `iroh 0.95` and cannot creating a second daemon endpoint. `geth status` reports these backends as
share the daemon-owned `iroh 0.90.0` endpoint. Pulling them in beside the ready to wire. The module implementations still use explicit bootstrap
current endpoint would create parallel Iroh stacks and violate the one-endpoint control-ALPN paths until each module is migrated to its native protocol.
daemon invariant. Until the endpoint wrapper upgrades as a unit, `geth status`
reports CAS, KV, and pubsub native backends as blocked and the bootstrap
control-ALPN paths remain explicit.
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
registrations, and returns explicit unknown-ALPN errors. It does not yet accept registrations, and returns explicit unknown-ALPN errors. The current daemon
or dispatch remote streams; peer authentication and module handlers are later accept loop dispatches geth control, pipe, and SSH-proxy streams directly; the
Phase 1 work. next backend migrations should attach iroh-blobs, iroh-docs, and iroh-gossip
handlers to the same endpoint instead of creating parallel endpoints.
The target product should use Iroh relay support for practical internet The target product should use Iroh relay support for practical internet
connectivity and mDNS/LAN discovery for local networks. These are connectivity connectivity and mDNS/LAN discovery for local networks. These are connectivity

View file

@ -102,7 +102,7 @@ Implementation order:
equivalent. equivalent.
- `[x]` Fallback/stub behavior remains clearly marked where APIs are not yet - `[x]` Fallback/stub behavior remains clearly marked where APIs are not yet
pinned. pinned.
- `[ ]` Upgrade `geth-iroh` from `iroh 0.90.0` to an endpoint version - `[x]` Upgrade `geth-iroh` from `iroh 0.90.0` to an endpoint version
compatible with `iroh-blobs`, `iroh-docs`, and `iroh-gossip` without compatible with `iroh-blobs`, `iroh-docs`, and `iroh-gossip` without
introducing a second daemon endpoint. introducing a second daemon endpoint.
@ -459,8 +459,8 @@ authorization and durable-state boundaries clear.
- `[x]` `geth cas providers <hash>` lists locally known providers. - `[x]` `geth cas providers <hash>` lists locally known providers.
- `[x]` Tests cover local provider metadata storage. - `[x]` Tests cover local provider metadata storage.
- `[ ]` Replace the bootstrap control-ALPN transfer with `iroh-blobs` - `[ ]` Replace the bootstrap control-ALPN transfer with `iroh-blobs`
provider/fetch behavior after the daemon endpoint upgrades to an Iroh provider/fetch behavior using the daemon-owned `iroh 0.95.1` endpoint and
version compatible with `iroh-blobs 0.97.0` or a newer pinned equivalent. pinned `iroh-blobs 0.97.0`.
- `[x]` CAS pin and cache policy. - `[x]` CAS pin and cache policy.
Acceptance criteria: Acceptance criteria: