Add Iroh peer ping
This commit is contained in:
parent
8ab3ef004e
commit
679eeb48a3
15 changed files with 619 additions and 18 deletions
|
|
@ -105,9 +105,9 @@ 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, manual signed peer-card export/import/list commands, untrusted
|
types, manual signed peer-card export/import/list commands, `geth peer ping`
|
||||||
discovery-backend trait, custom relay-map config, and Iroh local-network
|
over Iroh, untrusted discovery-backend trait, custom relay-map config, and
|
||||||
discovery toggle exist.
|
Iroh local-network discovery toggle exist.
|
||||||
- Canonical signed-operation envelopes exist for keychain/auth signature
|
- Canonical signed-operation envelopes exist for keychain/auth signature
|
||||||
payloads. The keychain reducer builds an active identity view for admin keys,
|
payloads. The keychain reducer builds an active identity view for admin keys,
|
||||||
users, devices, nodes, agents, and endpoint bindings.
|
users, devices, nodes, agents, and endpoint bindings.
|
||||||
|
|
|
||||||
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -1052,6 +1052,7 @@ dependencies = [
|
||||||
"geth-store",
|
"geth-store",
|
||||||
"geth-types",
|
"geth-types",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
@ -1194,6 +1195,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hex",
|
"hex",
|
||||||
"iroh",
|
"iroh",
|
||||||
|
"n0-watcher",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|
@ -1243,7 +1245,9 @@ dependencies = [
|
||||||
"geth-ssh-identity",
|
"geth-ssh-identity",
|
||||||
"geth-store",
|
"geth-store",
|
||||||
"geth-types",
|
"geth-types",
|
||||||
|
"iroh",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
iroh = { version = "0.90.0", features = ["discovery-local-network"] }
|
iroh = { version = "0.90.0", features = ["discovery-local-network"] }
|
||||||
|
n0-watcher = "0.2"
|
||||||
postcard = { version = "1", features = ["alloc"] }
|
postcard = { version = "1", features = ["alloc"] }
|
||||||
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"] }
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ The bootstrap implementation provides:
|
||||||
- `geth peer export [--out <path>]`
|
- `geth peer export [--out <path>]`
|
||||||
- `geth peer import <path>`
|
- `geth peer import <path>`
|
||||||
- `geth peer list`
|
- `geth peer list`
|
||||||
|
- `geth peer ping <node-id>`
|
||||||
- `geth resource list`
|
- `geth resource list`
|
||||||
- `geth resource create <kind> <name>`
|
- `geth resource create <kind> <name>`
|
||||||
- `geth keychain init [--admin-key <path>]`
|
- `geth keychain init [--admin-key <path>]`
|
||||||
|
|
@ -116,9 +117,11 @@ The bootstrap implementation provides:
|
||||||
- `geth ssh revocation import <path> [--format jsonl|openssh-krl-spec]`
|
- `geth ssh revocation import <path> [--format jsonl|openssh-krl-spec]`
|
||||||
- local pipe registry commands: `geth pipe listen/connect`
|
- local pipe registry commands: `geth pipe listen/connect`
|
||||||
|
|
||||||
`geth peer export/import/list` is for untrusted peer-card exchange while live
|
`geth peer export/import/list` is for untrusted peer-card exchange. Peer cards
|
||||||
LAN discovery and authenticated Iroh dialing are still being built. Importing a
|
include the Iroh EndpointID plus currently known relay/direct addresses.
|
||||||
peer card never grants capabilities by itself.
|
`geth peer ping <node-id>` uses the local daemon's Iroh endpoint to dial an
|
||||||
|
imported peer card and exchange a signed candidate-only peer-card ping.
|
||||||
|
Importing or pinging a peer card never grants capabilities by itself.
|
||||||
|
|
||||||
Other command groups exist as explicit stubs: `ssh proxy`.
|
Other command groups exist as explicit stubs: `ssh proxy`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,9 @@ pub enum PeerCommand {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
List,
|
List,
|
||||||
|
Ping {
|
||||||
|
node: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
|
|
@ -460,6 +463,7 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
||||||
PeerCommand::Export { out } => ControlRequest::PeerCardExport { out },
|
PeerCommand::Export { out } => ControlRequest::PeerCardExport { out },
|
||||||
PeerCommand::Import { path } => ControlRequest::PeerCardImport { path },
|
PeerCommand::Import { path } => ControlRequest::PeerCardImport { path },
|
||||||
PeerCommand::List => ControlRequest::PeerCardList,
|
PeerCommand::List => ControlRequest::PeerCardList,
|
||||||
|
PeerCommand::Ping { node } => ControlRequest::PeerPing { node },
|
||||||
},
|
},
|
||||||
Command::Resource {
|
Command::Resource {
|
||||||
command: ResourceCommand::List,
|
command: ResourceCommand::List,
|
||||||
|
|
@ -802,6 +806,19 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||||
}
|
}
|
||||||
println!("note: {note}");
|
println!("note: {note}");
|
||||||
}
|
}
|
||||||
|
ControlResponse::PeerPinged {
|
||||||
|
peer_node_id,
|
||||||
|
peer_agent_id,
|
||||||
|
endpoint_id,
|
||||||
|
alpn,
|
||||||
|
note,
|
||||||
|
} => {
|
||||||
|
println!("peer pong: {peer_node_id}");
|
||||||
|
println!("agent: {peer_agent_id}");
|
||||||
|
println!("endpoint: {endpoint_id}");
|
||||||
|
println!("alpn: {alpn}");
|
||||||
|
println!("note: {note}");
|
||||||
|
}
|
||||||
ControlResponse::ResourceList { resources } => {
|
ControlResponse::ResourceList { resources } => {
|
||||||
if resources.is_empty() {
|
if resources.is_empty() {
|
||||||
println!("no resources");
|
println!("no resources");
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ pub enum ControlRequest {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
PeerCardList,
|
PeerCardList,
|
||||||
|
PeerPing {
|
||||||
|
node: String,
|
||||||
|
},
|
||||||
ResourceList,
|
ResourceList,
|
||||||
ResourceCreate {
|
ResourceCreate {
|
||||||
kind: String,
|
kind: String,
|
||||||
|
|
@ -228,6 +231,13 @@ pub enum ControlResponse {
|
||||||
peers: Vec<DiscoveredPeer>,
|
peers: Vec<DiscoveredPeer>,
|
||||||
note: String,
|
note: String,
|
||||||
},
|
},
|
||||||
|
PeerPinged {
|
||||||
|
peer_node_id: String,
|
||||||
|
peer_agent_id: String,
|
||||||
|
endpoint_id: String,
|
||||||
|
alpn: String,
|
||||||
|
note: String,
|
||||||
|
},
|
||||||
ResourceList {
|
ResourceList {
|
||||||
resources: Vec<ResourceDescriptor>,
|
resources: Vec<ResourceDescriptor>,
|
||||||
},
|
},
|
||||||
|
|
@ -428,6 +438,29 @@ pub struct CasBlob {
|
||||||
pub pinned: bool,
|
pub pinned: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||||
|
pub enum PeerControlRequest {
|
||||||
|
Ping { peer_card: PeerCard, nonce: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||||
|
pub enum PeerControlResponse {
|
||||||
|
Pong {
|
||||||
|
node_id: String,
|
||||||
|
agent_id: String,
|
||||||
|
endpoint_id: String,
|
||||||
|
remote_endpoint_id: String,
|
||||||
|
alpn: String,
|
||||||
|
nonce: String,
|
||||||
|
note: String,
|
||||||
|
},
|
||||||
|
Error {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ControlError {
|
pub enum ControlError {
|
||||||
#[error("json error: {0}")]
|
#[error("json error: {0}")]
|
||||||
|
|
@ -454,6 +487,26 @@ pub fn decode_response(line: &str) -> Result<ControlResponse, ControlError> {
|
||||||
serde_json::from_str(line).map_err(ControlError::from)
|
serde_json::from_str(line).map_err(ControlError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn encode_peer_request(request: &PeerControlRequest) -> Result<String, ControlError> {
|
||||||
|
let mut line = serde_json::to_string(request)?;
|
||||||
|
line.push('\n');
|
||||||
|
Ok(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_peer_request(line: &str) -> Result<PeerControlRequest, ControlError> {
|
||||||
|
serde_json::from_str(line).map_err(ControlError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_peer_response(response: &PeerControlResponse) -> Result<String, ControlError> {
|
||||||
|
let mut line = serde_json::to_string(response)?;
|
||||||
|
line.push('\n');
|
||||||
|
Ok(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_peer_response(line: &str) -> Result<PeerControlResponse, ControlError> {
|
||||||
|
serde_json::from_str(line).map_err(ControlError::from)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -557,5 +610,28 @@ mod tests {
|
||||||
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
|
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
|
||||||
request
|
request
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let request = ControlRequest::PeerPing {
|
||||||
|
node: "node:peer".to_owned(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
|
||||||
|
request
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = PeerControlResponse::Pong {
|
||||||
|
node_id: "node:peer".to_owned(),
|
||||||
|
agent_id: "agent:peer".to_owned(),
|
||||||
|
endpoint_id: "endpoint:peer".to_owned(),
|
||||||
|
remote_endpoint_id: "endpoint:caller".to_owned(),
|
||||||
|
alpn: "/geth/control/1".to_owned(),
|
||||||
|
nonce: "nonce".to_owned(),
|
||||||
|
note: "candidate only".to_owned(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
decode_peer_response(&encode_peer_response(&response).expect("encode"))
|
||||||
|
.expect("decode"),
|
||||||
|
response
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,8 @@ struct PeerCardSigningPayload {
|
||||||
pub struct EndpointCandidate {
|
pub struct EndpointCandidate {
|
||||||
pub endpoint_id: String,
|
pub endpoint_id: String,
|
||||||
pub relay_url: Option<String>,
|
pub relay_url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub direct_addresses: Vec<String>,
|
||||||
pub source: DiscoverySource,
|
pub source: DiscoverySource,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,6 +200,7 @@ mod tests {
|
||||||
vec![EndpointCandidate {
|
vec![EndpointCandidate {
|
||||||
endpoint_id: "endpoint:iroh".to_owned(),
|
endpoint_id: "endpoint:iroh".to_owned(),
|
||||||
relay_url: None,
|
relay_url: None,
|
||||||
|
direct_addresses: vec!["127.0.0.1:12345".to_owned()],
|
||||||
source: DiscoverySource::Manual,
|
source: DiscoverySource::Manual,
|
||||||
}],
|
}],
|
||||||
UnixMillis(1),
|
UnixMillis(1),
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ license.workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
iroh.workspace = true
|
iroh.workspace = true
|
||||||
|
n0-watcher.workspace = true
|
||||||
rand_core.workspace = true
|
rand_core.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ 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";
|
||||||
|
|
@ -203,6 +204,7 @@ impl GethRelayMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct GethIrohEndpoint {
|
pub struct GethIrohEndpoint {
|
||||||
endpoint: iroh::Endpoint,
|
endpoint: iroh::Endpoint,
|
||||||
status: EndpointStatus,
|
status: EndpointStatus,
|
||||||
|
|
@ -219,11 +221,42 @@ impl GethIrohEndpoint {
|
||||||
self.endpoint.node_id().to_string()
|
self.endpoint.node_id().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn endpoint(&self) -> iroh::Endpoint {
|
||||||
|
self.endpoint.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()))?;
|
||||||
|
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()
|
||||||
|
.map(|addr| addr.to_string())
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn shutdown(&self) {
|
pub async fn shutdown(&self) {
|
||||||
self.endpoint.close().await;
|
self.endpoint.close().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct GethNodeAddr {
|
||||||
|
pub endpoint_id: String,
|
||||||
|
pub relay_url: Option<String>,
|
||||||
|
pub direct_addresses: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start_endpoint(config: &GethIrohConfig) -> Result<GethIrohEndpoint, IrohError> {
|
pub async fn start_endpoint(config: &GethIrohConfig) -> Result<GethIrohEndpoint, IrohError> {
|
||||||
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()
|
||||||
|
|
@ -319,6 +352,10 @@ pub enum IrohError {
|
||||||
InvalidRelayUrl { url: String, message: String },
|
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>),
|
||||||
|
#[error("timed out waiting for iroh node address")]
|
||||||
|
NodeAddrTimeout,
|
||||||
|
#[error("iroh node address watcher is unavailable: {0}")]
|
||||||
|
NodeAddrUnavailable(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|
@ -437,6 +474,9 @@ mod tests {
|
||||||
assert_eq!(status.endpoint_id, Some(endpoint.node_id()));
|
assert_eq!(status.endpoint_id, Some(endpoint.node_id()));
|
||||||
assert_eq!(status.relay_mode, "disabled");
|
assert_eq!(status.relay_mode, "disabled");
|
||||||
assert!(!status.local_discovery);
|
assert!(!status.local_discovery);
|
||||||
|
let node_addr = endpoint.node_addr_snapshot().await.expect("node addr");
|
||||||
|
assert_eq!(node_addr.endpoint_id, endpoint.node_id());
|
||||||
|
assert!(!node_addr.direct_addresses.is_empty());
|
||||||
endpoint.shutdown().await;
|
endpoint.shutdown().await;
|
||||||
}
|
}
|
||||||
Err(IrohError::Bind(error)) => {
|
Err(IrohError::Bind(error)) => {
|
||||||
|
|
|
||||||
|
|
@ -28,3 +28,7 @@ geth-secrets = { path = "../geth-secrets" }
|
||||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||||
geth-store = { path = "../geth-store" }
|
geth-store = { path = "../geth-store" }
|
||||||
geth-types = { path = "../geth-types" }
|
geth-types = { path = "../geth-types" }
|
||||||
|
iroh.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use geth_cas::{
|
||||||
use geth_config::{GethConfig, GethPaths, RelayMode};
|
use geth_config::{GethConfig, GethPaths, RelayMode};
|
||||||
use geth_control::{
|
use geth_control::{
|
||||||
CasBlob, ControlRequest, ControlResponse, KeychainStatusResponse, NodeIdResponse,
|
CasBlob, ControlRequest, ControlResponse, KeychainStatusResponse, NodeIdResponse,
|
||||||
StatusResponse,
|
PeerControlRequest, PeerControlResponse, StatusResponse,
|
||||||
};
|
};
|
||||||
use geth_crypto::AgentKey;
|
use geth_crypto::AgentKey;
|
||||||
use geth_db::DbResource;
|
use geth_db::DbResource;
|
||||||
|
|
@ -112,6 +112,10 @@ pub enum NodeError {
|
||||||
Discovery(#[from] geth_discovery::DiscoveryError),
|
Discovery(#[from] geth_discovery::DiscoveryError),
|
||||||
#[error("cannot export peer card before the daemon has an Iroh EndpointID")]
|
#[error("cannot export peer card before the daemon has an Iroh EndpointID")]
|
||||||
IrohEndpointUnavailable,
|
IrohEndpointUnavailable,
|
||||||
|
#[error("peer candidate not found: {0}")]
|
||||||
|
PeerNotFound(String),
|
||||||
|
#[error("iroh peer error: {0}")]
|
||||||
|
IrohPeer(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -120,6 +124,7 @@ pub struct LocalNode {
|
||||||
pub agent_id: String,
|
pub agent_id: String,
|
||||||
pub node_id: String,
|
pub node_id: String,
|
||||||
pub iroh_status: EndpointStatus,
|
pub iroh_status: EndpointStatus,
|
||||||
|
iroh_endpoint: Arc<Mutex<Option<GethIrohEndpoint>>>,
|
||||||
runtime: Arc<NodeRuntime>,
|
runtime: Arc<NodeRuntime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,6 +170,7 @@ pub fn init_node(paths: &GethPaths) -> Result<LocalNode, NodeError> {
|
||||||
agent_id,
|
agent_id,
|
||||||
node_id,
|
node_id,
|
||||||
iroh_status: EndpointStatus::scaffolded(),
|
iroh_status: EndpointStatus::scaffolded(),
|
||||||
|
iroh_endpoint: Arc::new(Mutex::new(None)),
|
||||||
runtime: Arc::new(NodeRuntime {
|
runtime: Arc::new(NodeRuntime {
|
||||||
pubsub: Mutex::new(PubsubRuntime::default()),
|
pubsub: Mutex::new(PubsubRuntime::default()),
|
||||||
pipes: Mutex::new(PipeRuntime::default()),
|
pipes: Mutex::new(PipeRuntime::default()),
|
||||||
|
|
@ -179,6 +185,14 @@ pub fn open_node(paths: &GethPaths) -> Result<LocalNode, NodeError> {
|
||||||
pub async fn run_daemon(paths: GethPaths) -> Result<(), NodeError> {
|
pub async fn run_daemon(paths: GethPaths) -> Result<(), NodeError> {
|
||||||
let mut node = init_node(&paths)?;
|
let mut node = init_node(&paths)?;
|
||||||
let _iroh_endpoint = start_daemon_iroh_endpoint(&mut node).await?;
|
let _iroh_endpoint = start_daemon_iroh_endpoint(&mut node).await?;
|
||||||
|
if let Some(endpoint) = node
|
||||||
|
.iroh_endpoint
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| NodeError::RuntimeLockPoisoned)?
|
||||||
|
.clone()
|
||||||
|
{
|
||||||
|
spawn_iroh_control_accept_loop(node.clone(), endpoint);
|
||||||
|
}
|
||||||
if Path::new(&paths.socket_path()).exists() {
|
if Path::new(&paths.socket_path()).exists() {
|
||||||
std::fs::remove_file(paths.socket_path())?;
|
std::fs::remove_file(paths.socket_path())?;
|
||||||
}
|
}
|
||||||
|
|
@ -211,12 +225,23 @@ pub async fn send_control(
|
||||||
Ok(geth_control::decode_response(&line)?)
|
Ok(geth_control::decode_response(&line)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_request_async(
|
||||||
|
node: &LocalNode,
|
||||||
|
request: ControlRequest,
|
||||||
|
) -> Result<ControlResponse, NodeError> {
|
||||||
|
match request {
|
||||||
|
ControlRequest::PeerCardExport { out } => export_peer_card(node, out, true).await,
|
||||||
|
ControlRequest::PeerPing { node: peer_node } => peer_ping(node, &peer_node).await,
|
||||||
|
other => handle_request(node, other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_stream(node: LocalNode, stream: UnixStream) -> Result<(), NodeError> {
|
async fn handle_stream(node: LocalNode, stream: UnixStream) -> Result<(), NodeError> {
|
||||||
let mut reader = BufReader::new(stream);
|
let mut reader = BufReader::new(stream);
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
reader.read_line(&mut line).await?;
|
reader.read_line(&mut line).await?;
|
||||||
let request = geth_control::decode_request(&line)?;
|
let request = geth_control::decode_request(&line)?;
|
||||||
let response = match handle_request(&node, request) {
|
let response = match handle_request_async(&node, request).await {
|
||||||
Ok(response) => response,
|
Ok(response) => response,
|
||||||
Err(error) => ControlResponse::Error {
|
Err(error) => ControlResponse::Error {
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
|
|
@ -229,6 +254,243 @@ async fn handle_stream(node: LocalNode, stream: UnixStream) -> Result<(), NodeEr
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn export_peer_card(
|
||||||
|
node: &LocalNode,
|
||||||
|
out: Option<std::path::PathBuf>,
|
||||||
|
include_node_addr: bool,
|
||||||
|
) -> Result<ControlResponse, NodeError> {
|
||||||
|
let card = local_peer_card(node, DiscoverySource::Manual, include_node_addr).await?;
|
||||||
|
if let Some(path) = &out {
|
||||||
|
std::fs::write(path, serde_json::to_string_pretty(&card)?)?;
|
||||||
|
}
|
||||||
|
Ok(ControlResponse::PeerCardExported {
|
||||||
|
card,
|
||||||
|
out,
|
||||||
|
note: discovery_is_untrusted_note().to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn local_peer_card(
|
||||||
|
node: &LocalNode,
|
||||||
|
source: DiscoverySource,
|
||||||
|
include_node_addr: bool,
|
||||||
|
) -> Result<PeerCard, NodeError> {
|
||||||
|
let endpoint_id = node
|
||||||
|
.iroh_status
|
||||||
|
.endpoint_id
|
||||||
|
.clone()
|
||||||
|
.ok_or(NodeError::IrohEndpointUnavailable)?;
|
||||||
|
let endpoint = node
|
||||||
|
.iroh_endpoint
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| NodeError::RuntimeLockPoisoned)?
|
||||||
|
.clone();
|
||||||
|
let node_addr = if include_node_addr {
|
||||||
|
match endpoint {
|
||||||
|
Some(endpoint) => endpoint.node_addr_snapshot().await.ok(),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let candidate = EndpointCandidate {
|
||||||
|
endpoint_id,
|
||||||
|
relay_url: node_addr.as_ref().and_then(|addr| addr.relay_url.clone()),
|
||||||
|
direct_addresses: node_addr
|
||||||
|
.map(|addr| addr.direct_addresses)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
let key = AgentKey::load(&node.paths.agent_key())?;
|
||||||
|
PeerCard::signed(
|
||||||
|
NodeId::new(node.node_id.clone()),
|
||||||
|
&key,
|
||||||
|
vec![candidate],
|
||||||
|
UnixMillis(geth_store::now_ms()),
|
||||||
|
)
|
||||||
|
.map_err(NodeError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn peer_ping(node: &LocalNode, peer_node: &str) -> Result<ControlResponse, NodeError> {
|
||||||
|
let store = Store::open(&node.paths.metadata_db())?;
|
||||||
|
let stored = store
|
||||||
|
.get_peer_card(peer_node)?
|
||||||
|
.ok_or_else(|| NodeError::PeerNotFound(peer_node.to_owned()))?;
|
||||||
|
let peer_card: PeerCard = serde_json::from_str(&stored.card_json)?;
|
||||||
|
peer_card.validate_candidate()?;
|
||||||
|
let candidate = peer_card
|
||||||
|
.endpoints
|
||||||
|
.first()
|
||||||
|
.ok_or(geth_discovery::DiscoveryError::MissingEndpoint)?;
|
||||||
|
let node_addr = iroh_node_addr_from_candidate(candidate)?;
|
||||||
|
let endpoint = node
|
||||||
|
.iroh_endpoint
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| NodeError::RuntimeLockPoisoned)?
|
||||||
|
.clone()
|
||||||
|
.ok_or(NodeError::IrohEndpointUnavailable)?;
|
||||||
|
let self_card = local_peer_card(node, DiscoverySource::PeerExchange, true).await?;
|
||||||
|
let nonce = geth_crypto::blake3_hex(
|
||||||
|
format!("{}\0{}\0{}", node.node_id, peer_node, geth_store::now_ms()).as_bytes(),
|
||||||
|
);
|
||||||
|
let request = PeerControlRequest::Ping {
|
||||||
|
peer_card: self_card,
|
||||||
|
nonce: nonce.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conn = endpoint
|
||||||
|
.endpoint()
|
||||||
|
.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 (mut send, mut recv) = conn
|
||||||
|
.open_bi()
|
||||||
|
.await
|
||||||
|
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
send.write_all(geth_control::encode_peer_request(&request)?.as_bytes())
|
||||||
|
.await
|
||||||
|
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
send.finish()
|
||||||
|
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
let bytes = recv
|
||||||
|
.read_to_end(64 * 1024)
|
||||||
|
.await
|
||||||
|
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
let text =
|
||||||
|
std::str::from_utf8(&bytes).map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
match geth_control::decode_peer_response(text)? {
|
||||||
|
PeerControlResponse::Pong {
|
||||||
|
node_id,
|
||||||
|
agent_id,
|
||||||
|
endpoint_id,
|
||||||
|
alpn: remote_alpn,
|
||||||
|
nonce: response_nonce,
|
||||||
|
note,
|
||||||
|
..
|
||||||
|
} if response_nonce == nonce => Ok(ControlResponse::PeerPinged {
|
||||||
|
peer_node_id: node_id,
|
||||||
|
peer_agent_id: agent_id,
|
||||||
|
endpoint_id,
|
||||||
|
alpn: if remote_alpn == "unknown" {
|
||||||
|
alpn
|
||||||
|
} else {
|
||||||
|
remote_alpn
|
||||||
|
},
|
||||||
|
note,
|
||||||
|
}),
|
||||||
|
PeerControlResponse::Pong { .. } => Err(NodeError::IrohPeer(
|
||||||
|
"peer ping response nonce did not match request".to_owned(),
|
||||||
|
)),
|
||||||
|
PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_iroh_control_accept_loop(node: LocalNode, endpoint: GethIrohEndpoint) {
|
||||||
|
let raw_endpoint = endpoint.endpoint();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(incoming) = raw_endpoint.accept().await {
|
||||||
|
let node = node.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(error) = handle_iroh_control_connection(node, incoming).await {
|
||||||
|
tracing::warn!(%error, "iroh control request failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_iroh_control_connection(
|
||||||
|
node: LocalNode,
|
||||||
|
incoming: iroh::endpoint::Incoming,
|
||||||
|
) -> Result<(), NodeError> {
|
||||||
|
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 (mut send, mut recv) = conn
|
||||||
|
.accept_bi()
|
||||||
|
.await
|
||||||
|
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
let bytes = recv
|
||||||
|
.read_to_end(64 * 1024)
|
||||||
|
.await
|
||||||
|
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
let text =
|
||||||
|
std::str::from_utf8(&bytes).map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
let response = match geth_control::decode_peer_request(text)? {
|
||||||
|
PeerControlRequest::Ping { peer_card, nonce } => {
|
||||||
|
peer_card.validate_candidate()?;
|
||||||
|
let discovered = DiscoveredPeer::candidate(
|
||||||
|
peer_card.clone(),
|
||||||
|
UnixMillis(geth_store::now_ms()),
|
||||||
|
DiscoverySource::PeerExchange,
|
||||||
|
)?;
|
||||||
|
let store = Store::open(&node.paths.metadata_db())?;
|
||||||
|
store.upsert_peer_card(&StoredPeerCard {
|
||||||
|
peer_id: peer_card.node_id.to_string(),
|
||||||
|
card_json: serde_json::to_string(&peer_card)?,
|
||||||
|
updated_at_ms: discovered.discovered_at.0,
|
||||||
|
})?;
|
||||||
|
PeerControlResponse::Pong {
|
||||||
|
node_id: node.node_id.clone(),
|
||||||
|
agent_id: node.agent_id.clone(),
|
||||||
|
endpoint_id: node.iroh_status.endpoint_id.clone().unwrap_or_default(),
|
||||||
|
remote_endpoint_id,
|
||||||
|
alpn,
|
||||||
|
nonce,
|
||||||
|
note: "peer endpoint authenticated by Iroh and peer-card signature; candidate status does not grant resource capabilities".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
send.write_all(geth_control::encode_peer_response(&response)?.as_bytes())
|
||||||
|
.await
|
||||||
|
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
send.finish()
|
||||||
|
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iroh_node_addr_from_candidate(
|
||||||
|
candidate: &EndpointCandidate,
|
||||||
|
) -> Result<iroh::NodeAddr, NodeError> {
|
||||||
|
let node_id = candidate
|
||||||
|
.endpoint_id
|
||||||
|
.parse::<iroh::NodeId>()
|
||||||
|
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
let direct_addresses = candidate
|
||||||
|
.direct_addresses
|
||||||
|
.iter()
|
||||||
|
.map(|addr| {
|
||||||
|
addr.parse::<std::net::SocketAddr>()
|
||||||
|
.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);
|
||||||
|
if let Some(relay_url) = &candidate.relay_url {
|
||||||
|
node_addr = node_addr.with_relay_url(
|
||||||
|
relay_url
|
||||||
|
.parse::<iroh::RelayUrl>()
|
||||||
|
.map_err(|error| NodeError::IrohPeer(error.to_string()))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(node_addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_alpn(alpn: Vec<u8>) -> String {
|
||||||
|
String::from_utf8_lossy(&alpn).into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_request(
|
pub fn handle_request(
|
||||||
node: &LocalNode,
|
node: &LocalNode,
|
||||||
request: ControlRequest,
|
request: ControlRequest,
|
||||||
|
|
@ -264,6 +526,7 @@ pub fn handle_request(
|
||||||
vec![EndpointCandidate {
|
vec![EndpointCandidate {
|
||||||
endpoint_id,
|
endpoint_id,
|
||||||
relay_url: None,
|
relay_url: None,
|
||||||
|
direct_addresses: Vec::new(),
|
||||||
source: DiscoverySource::Manual,
|
source: DiscoverySource::Manual,
|
||||||
}],
|
}],
|
||||||
UnixMillis(geth_store::now_ms()),
|
UnixMillis(geth_store::now_ms()),
|
||||||
|
|
@ -307,6 +570,7 @@ pub fn handle_request(
|
||||||
note: discovery_is_untrusted_note().to_owned(),
|
note: discovery_is_untrusted_note().to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
ControlRequest::PeerPing { .. } => Err(NodeError::IrohEndpointUnavailable),
|
||||||
ControlRequest::ResourceList => Ok(ControlResponse::ResourceList {
|
ControlRequest::ResourceList => Ok(ControlResponse::ResourceList {
|
||||||
resources: store
|
resources: store
|
||||||
.list_resources()?
|
.list_resources()?
|
||||||
|
|
@ -1564,6 +1828,10 @@ async fn start_daemon_iroh_endpoint(
|
||||||
store.upsert_node_endpoint(endpoint_id, &node.node_id, &node.agent_id, "iroh")?;
|
store.upsert_node_endpoint(endpoint_id, &node.node_id, &node.agent_id, "iroh")?;
|
||||||
}
|
}
|
||||||
node.iroh_status = status;
|
node.iroh_status = status;
|
||||||
|
*node
|
||||||
|
.iroh_endpoint
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| NodeError::RuntimeLockPoisoned)? = Some(endpoint.clone());
|
||||||
Ok(Some(endpoint))
|
Ok(Some(endpoint))
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
@ -1695,3 +1963,90 @@ fn ssh_revocation_from_stored(
|
||||||
published: stored.published,
|
published: stored.published,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn write_offline_iroh_config(paths: &GethPaths) {
|
||||||
|
std::fs::write(
|
||||||
|
paths.config_file(),
|
||||||
|
"[iroh]\nrelay_mode = \"disabled\"\nlocal_discovery = false\n",
|
||||||
|
)
|
||||||
|
.expect("write config");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn peer_ping_uses_signed_peer_card_over_iroh() {
|
||||||
|
let left_home = tempfile::tempdir().expect("left home");
|
||||||
|
let right_home = tempfile::tempdir().expect("right home");
|
||||||
|
let left_paths = GethPaths::from_home(left_home.path());
|
||||||
|
let right_paths = GethPaths::from_home(right_home.path());
|
||||||
|
let mut left = init_node(&left_paths).expect("init left");
|
||||||
|
let mut right = init_node(&right_paths).expect("init right");
|
||||||
|
write_offline_iroh_config(&left_paths);
|
||||||
|
write_offline_iroh_config(&right_paths);
|
||||||
|
|
||||||
|
let Some(left_endpoint) = start_daemon_iroh_endpoint(&mut left)
|
||||||
|
.await
|
||||||
|
.expect("left iroh")
|
||||||
|
else {
|
||||||
|
eprintln!("skipping peer ping assertion; left Iroh endpoint unavailable");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(right_endpoint) = start_daemon_iroh_endpoint(&mut right)
|
||||||
|
.await
|
||||||
|
.expect("right iroh")
|
||||||
|
else {
|
||||||
|
eprintln!("skipping peer ping assertion; right Iroh endpoint unavailable");
|
||||||
|
left_endpoint.shutdown().await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
spawn_iroh_control_accept_loop(right.clone(), right_endpoint.clone());
|
||||||
|
|
||||||
|
let exported = handle_request_async(&right, ControlRequest::PeerCardExport { out: None })
|
||||||
|
.await
|
||||||
|
.expect("export right peer card");
|
||||||
|
let right_card = match exported {
|
||||||
|
ControlResponse::PeerCardExported { card, .. } => card,
|
||||||
|
other => panic!("unexpected export response: {other:?}"),
|
||||||
|
};
|
||||||
|
assert!(!right_card.endpoints[0].direct_addresses.is_empty());
|
||||||
|
Store::open(&left_paths.metadata_db())
|
||||||
|
.expect("open left store")
|
||||||
|
.upsert_peer_card(&StoredPeerCard {
|
||||||
|
peer_id: right_card.node_id.to_string(),
|
||||||
|
card_json: serde_json::to_string(&right_card).expect("card json"),
|
||||||
|
updated_at_ms: geth_store::now_ms(),
|
||||||
|
})
|
||||||
|
.expect("insert right peer");
|
||||||
|
|
||||||
|
let ping = handle_request_async(
|
||||||
|
&left,
|
||||||
|
ControlRequest::PeerPing {
|
||||||
|
node: right_card.node_id.to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("peer ping");
|
||||||
|
|
||||||
|
match ping {
|
||||||
|
ControlResponse::PeerPinged {
|
||||||
|
peer_node_id,
|
||||||
|
peer_agent_id,
|
||||||
|
alpn,
|
||||||
|
note,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(peer_node_id, right.node_id);
|
||||||
|
assert_eq!(peer_agent_id, right.agent_id);
|
||||||
|
assert_eq!(alpn, "/geth/control/1");
|
||||||
|
assert!(note.contains("does not grant resource capabilities"));
|
||||||
|
}
|
||||||
|
other => panic!("unexpected ping response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
left_endpoint.shutdown().await;
|
||||||
|
right_endpoint.shutdown().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,5 @@ geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||||
geth-store = { path = "../geth-store" }
|
geth-store = { path = "../geth-store" }
|
||||||
geth-types = { path = "../geth-types" }
|
geth-types = { path = "../geth-types" }
|
||||||
rusqlite.workspace = true
|
rusqlite.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,87 @@ fn geth_status_against_running_daemon() {
|
||||||
assert!(stdout.contains("iroh discovery: local-network disabled"));
|
assert!(stdout.contains("iroh discovery: local-network disabled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn peer_ping_uses_daemon_owned_iroh_endpoint() {
|
||||||
|
let left_home = tempfile::tempdir().expect("left tempdir");
|
||||||
|
let right_home = tempfile::tempdir().expect("right tempdir");
|
||||||
|
if !unix_sockets_available(left_home.path()) || !unix_sockets_available(right_home.path()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert!(run_geth(left_home.path(), &["init"]).status.success());
|
||||||
|
assert!(run_geth(right_home.path(), &["init"]).status.success());
|
||||||
|
for home in [left_home.path(), right_home.path()] {
|
||||||
|
std::fs::write(
|
||||||
|
home.join("config.toml"),
|
||||||
|
"[iroh]\nrelay_mode = \"disabled\"\nlocal_discovery = false\n",
|
||||||
|
)
|
||||||
|
.expect("write config");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut left_daemon = spawn_daemon(left_home.path());
|
||||||
|
let mut right_daemon = spawn_daemon(right_home.path());
|
||||||
|
wait_for_socket(&left_home.path().join("run/geth.sock"));
|
||||||
|
wait_for_socket(&right_home.path().join("run/geth.sock"));
|
||||||
|
|
||||||
|
let left_card = left_home.path().join("left-peer-card.json");
|
||||||
|
let right_card = right_home.path().join("right-peer-card.json");
|
||||||
|
let left_export = run_geth(
|
||||||
|
left_home.path(),
|
||||||
|
&["peer", "export", "--out", left_card.to_str().unwrap()],
|
||||||
|
);
|
||||||
|
let right_export = run_geth(
|
||||||
|
right_home.path(),
|
||||||
|
&["peer", "export", "--out", right_card.to_str().unwrap()],
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
left_export.status.success(),
|
||||||
|
"left export stderr: {}",
|
||||||
|
String::from_utf8_lossy(&left_export.stderr)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
right_export.status.success(),
|
||||||
|
"right export stderr: {}",
|
||||||
|
String::from_utf8_lossy(&right_export.stderr)
|
||||||
|
);
|
||||||
|
let import = run_geth(
|
||||||
|
left_home.path(),
|
||||||
|
&["peer", "import", right_card.to_str().unwrap()],
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
import.status.success(),
|
||||||
|
"import stderr: {}",
|
||||||
|
String::from_utf8_lossy(&import.stderr)
|
||||||
|
);
|
||||||
|
let right_card_json =
|
||||||
|
std::fs::read_to_string(&right_card).expect("read right exported peer card");
|
||||||
|
let right_card: geth_discovery::PeerCard =
|
||||||
|
serde_json::from_str(&right_card_json).expect("decode right peer card");
|
||||||
|
assert!(
|
||||||
|
right_card.endpoints[0]
|
||||||
|
.direct_addresses
|
||||||
|
.iter()
|
||||||
|
.any(|addr| addr.contains("127.0.0.1") || addr.contains("[::1]"))
|
||||||
|
);
|
||||||
|
|
||||||
|
let ping = run_geth(
|
||||||
|
left_home.path(),
|
||||||
|
&["peer", "ping", right_card.node_id.as_str()],
|
||||||
|
);
|
||||||
|
let _ = left_daemon.kill();
|
||||||
|
let _ = right_daemon.kill();
|
||||||
|
let _ = left_daemon.wait();
|
||||||
|
let _ = right_daemon.wait();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
ping.status.success(),
|
||||||
|
"ping stderr: {}",
|
||||||
|
String::from_utf8_lossy(&ping.stderr)
|
||||||
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&ping.stdout);
|
||||||
|
assert!(stdout.contains("peer pong:"));
|
||||||
|
assert!(stdout.contains("candidate status does not grant resource capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn peer_card_export_import_and_list_are_candidate_only() {
|
fn peer_card_export_import_and_list_are_candidate_only() {
|
||||||
let source_home = tempfile::tempdir().expect("source tempdir");
|
let source_home = tempfile::tempdir().expect("source tempdir");
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,19 @@ authorization state, or make EndpointID knowledge sufficient for access.
|
||||||
The current daemon can enable Iroh's local-network discovery service through
|
The current daemon can enable Iroh's local-network discovery service through
|
||||||
`[iroh].local_discovery = true`, which is the default. This publishes and
|
`[iroh].local_discovery = true`, which is the default. This publishes and
|
||||||
discovers Iroh node addressing. `geth peer export/import/list` supports manual
|
discovers Iroh node addressing. `geth peer export/import/list` supports manual
|
||||||
exchange of signed peer cards as untrusted candidates. Automatic signed
|
exchange of signed peer cards as untrusted candidates. Peer cards include the
|
||||||
peer-card advertisement over LAN discovery remains separate future work.
|
Iroh EndpointID plus relay/direct address candidates when the daemon can observe
|
||||||
|
them. `geth peer ping <node-id>` dials an imported peer card over Iroh and
|
||||||
|
exchanges signed peer-card metadata. Automatic signed peer-card advertisement
|
||||||
|
over LAN discovery remains separate future work.
|
||||||
|
|
||||||
Peer cards are the discovery payload. A peer card carries node ID, agent ID,
|
Peer cards are the discovery payload. A peer card carries node ID, agent ID,
|
||||||
endpoint candidates, timestamp, signing public key, and an Ed25519 signature
|
endpoint candidates, timestamp, signing public key, and an Ed25519 signature
|
||||||
over a canonical payload. Imported peer cards are stored as untrusted metadata
|
over a canonical payload. Imported and ping-discovered peer cards are stored as
|
||||||
in `peer_cards`; trust reduction is future work. `auth explain` reports when a
|
untrusted metadata in `peer_cards`; trust reduction is future work. `auth
|
||||||
subject is only a discovered peer candidate and denies access.
|
explain` reports when a subject is only a discovered peer candidate and denies
|
||||||
|
access. The peer ping path authenticates the Iroh endpoint and peer-card
|
||||||
|
signature, but it does not authorize any resource module.
|
||||||
|
|
||||||
The daemon starts this endpoint during `geth daemon run` and keeps it alive for
|
The daemon starts this endpoint during `geth daemon run` and keeps it alive for
|
||||||
the daemon lifetime. When endpoint startup succeeds, the Iroh EndpointID is
|
the daemon lifetime. When endpoint startup succeeds, the Iroh EndpointID is
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,8 @@ geth-to-geth connections without granting trust from discovery alone.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `[x]` Manual `geth peer export/import/list` can exchange signed peer cards
|
- `[x]` Manual `geth peer export/import/list` can exchange signed peer cards
|
||||||
and store them as untrusted candidates.
|
and store them as untrusted candidates.
|
||||||
|
- `[x]` Exported daemon peer cards include Iroh EndpointID plus available
|
||||||
|
relay/direct address candidates.
|
||||||
- `[ ]` The daemon can advertise and discover signed geth peer cards over LAN
|
- `[ ]` The daemon can advertise and discover signed geth peer cards over LAN
|
||||||
discovery.
|
discovery.
|
||||||
- `[x]` Imported peer cards are stored only as untrusted peer candidates.
|
- `[x]` Imported peer cards are stored only as untrusted peer candidates.
|
||||||
|
|
@ -125,11 +127,18 @@ geth-to-geth connections without granting trust from discovery alone.
|
||||||
- No discovery result grants capabilities or trust.
|
- No discovery result grants capabilities or trust.
|
||||||
- `auth explain` can distinguish "discovered" from "trusted".
|
- `auth explain` can distinguish "discovered" from "trusted".
|
||||||
|
|
||||||
- `[ ]` Basic authenticated peer connection.
|
- `[~]` Basic authenticated peer connection.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- A node can dial another node over Iroh using an EndpointID from a peer card.
|
- `[x]` `geth peer ping <node-id>` dials another node over Iroh using an
|
||||||
- The remote side proves an agent/node binding before module access.
|
imported signed peer card.
|
||||||
- Knowing only an EndpointID is insufficient to access a protected module.
|
- `[x]` The remote side validates the caller's signed peer card and stores it
|
||||||
|
as a candidate only.
|
||||||
|
- `[x]` The ping response records negotiated ALPN and remote endpoint
|
||||||
|
identity.
|
||||||
|
- `[ ]` The remote side proves an agent/node binding before protected module
|
||||||
|
access.
|
||||||
|
- `[ ]` Protected module handlers reject requests that only know an
|
||||||
|
EndpointID and lack resource capabilities.
|
||||||
|
|
||||||
## Phase 2: Trust And Authorization
|
## Phase 2: Trust And Authorization
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue