Add daemon-owned Iroh endpoint
This commit is contained in:
parent
b892dc6165
commit
ebc40ed208
14 changed files with 3930 additions and 431 deletions
|
|
@ -102,8 +102,9 @@ Roadmap items should be actionable and checkable:
|
||||||
## Current Feature Boundaries
|
## Current Feature Boundaries
|
||||||
|
|
||||||
- Local daemon, local control socket, local identity, local store, local CAS,
|
- Local daemon, local control socket, local identity, local store, local CAS,
|
||||||
SSH certificate metadata, revocation metadata, and user service definitions
|
daemon-owned Iroh endpoint startup, SSH certificate metadata, revocation
|
||||||
|
metadata, user service definitions, and a pinned `geth-iroh` endpoint wrapper
|
||||||
exist.
|
exist.
|
||||||
- Iroh, cr-sqlite, iroh-docs, iroh-gossip, iroh-blobs, Automerge sync, real auth
|
- Peer auth over Iroh, cr-sqlite, iroh-docs, iroh-gossip, iroh-blobs, Automerge
|
||||||
enforcement, OpenSSH KRL generation, and Keyhive/BeeKEM-style authorization are
|
sync, real auth enforcement, OpenSSH KRL generation, and Keyhive/BeeKEM-style
|
||||||
future roadmap items unless implemented later.
|
authorization are future roadmap items unless implemented later.
|
||||||
|
|
|
||||||
3259
Cargo.lock
generated
3259
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -44,6 +44,7 @@ directories = "5"
|
||||||
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
iroh = "0.90.0"
|
||||||
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"] }
|
||||||
|
|
|
||||||
|
|
@ -503,6 +503,10 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||||
println!("socket: {}", status.socket.display());
|
println!("socket: {}", status.socket.display());
|
||||||
println!("agent: {}", status.agent_id);
|
println!("agent: {}", status.agent_id);
|
||||||
println!("node: {}", status.node_id);
|
println!("node: {}", status.node_id);
|
||||||
|
println!(
|
||||||
|
"endpoint: {}",
|
||||||
|
status.endpoint_id.as_deref().unwrap_or("not started")
|
||||||
|
);
|
||||||
println!("iroh: {}", status.iroh);
|
println!("iroh: {}", status.iroh);
|
||||||
}
|
}
|
||||||
ControlResponse::NodeId(node) => {
|
ControlResponse::NodeId(node) => {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,11 @@ impl GethPaths {
|
||||||
self.identity_dir().join("agent.ed25519")
|
self.identity_dir().join("agent.ed25519")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn iroh_key(&self) -> PathBuf {
|
||||||
|
self.identity_dir().join("iroh.ed25519")
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn cas_dir(&self) -> PathBuf {
|
pub fn cas_dir(&self) -> PathBuf {
|
||||||
self.home.join("cas")
|
self.home.join("cas")
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,8 @@ pub struct StatusResponse {
|
||||||
pub socket: PathBuf,
|
pub socket: PathBuf,
|
||||||
pub agent_id: String,
|
pub agent_id: String,
|
||||||
pub node_id: String,
|
pub node_id: String,
|
||||||
|
pub iroh_enabled: bool,
|
||||||
|
pub endpoint_id: Option<String>,
|
||||||
pub iroh: String,
|
pub iroh: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,12 @@ rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
hex.workspace = true
|
||||||
|
iroh.workspace = true
|
||||||
|
rand_core.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::net::{SocketAddrV4, SocketAddrV6};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
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";
|
||||||
|
|
@ -9,6 +11,134 @@ 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";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct GethIrohConfig {
|
||||||
|
pub secret_key_path: PathBuf,
|
||||||
|
pub relay_mode: GethRelayMode,
|
||||||
|
pub bind_ipv4: Option<SocketAddrV4>,
|
||||||
|
pub bind_ipv6: Option<SocketAddrV6>,
|
||||||
|
pub alpns: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GethIrohConfig {
|
||||||
|
#[must_use]
|
||||||
|
pub fn local(secret_key_path: impl Into<PathBuf>) -> Self {
|
||||||
|
Self {
|
||||||
|
secret_key_path: secret_key_path.into(),
|
||||||
|
relay_mode: GethRelayMode::Disabled,
|
||||||
|
bind_ipv4: None,
|
||||||
|
bind_ipv6: None,
|
||||||
|
alpns: all_alpns(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum GethRelayMode {
|
||||||
|
Disabled,
|
||||||
|
Default,
|
||||||
|
Staging,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GethRelayMode {
|
||||||
|
fn to_iroh(&self) -> iroh::RelayMode {
|
||||||
|
match self {
|
||||||
|
Self::Disabled => iroh::RelayMode::Disabled,
|
||||||
|
Self::Default => iroh::RelayMode::Default,
|
||||||
|
Self::Staging => iroh::RelayMode::Staging,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GethIrohEndpoint {
|
||||||
|
endpoint: iroh::Endpoint,
|
||||||
|
status: EndpointStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GethIrohEndpoint {
|
||||||
|
#[must_use]
|
||||||
|
pub fn status(&self) -> EndpointStatus {
|
||||||
|
self.status.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn node_id(&self) -> String {
|
||||||
|
self.endpoint.node_id().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn shutdown(&self) {
|
||||||
|
self.endpoint.close().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_endpoint(config: &GethIrohConfig) -> Result<GethIrohEndpoint, IrohError> {
|
||||||
|
let secret_key = load_or_create_secret_key(&config.secret_key_path)?;
|
||||||
|
let mut builder = iroh::Endpoint::builder()
|
||||||
|
.secret_key(secret_key)
|
||||||
|
.relay_mode(config.relay_mode.to_iroh())
|
||||||
|
.alpns(config.alpns.clone());
|
||||||
|
|
||||||
|
if let Some(bind_ipv4) = config.bind_ipv4 {
|
||||||
|
builder = builder.bind_addr_v4(bind_ipv4);
|
||||||
|
}
|
||||||
|
if let Some(bind_ipv6) = config.bind_ipv6 {
|
||||||
|
builder = builder.bind_addr_v6(bind_ipv6);
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint = builder.bind().await.map_err(IrohError::from)?;
|
||||||
|
let status = EndpointStatus {
|
||||||
|
enabled: true,
|
||||||
|
endpoint_id: Some(endpoint.node_id().to_string()),
|
||||||
|
note: format!(
|
||||||
|
"Iroh endpoint started with iroh 0.90.0; relay mode: {:?}",
|
||||||
|
config.relay_mode
|
||||||
|
),
|
||||||
|
};
|
||||||
|
Ok(GethIrohEndpoint { endpoint, status })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_or_create_secret_key(path: &Path) -> Result<iroh::SecretKey, IrohError> {
|
||||||
|
if path.exists() {
|
||||||
|
return load_secret_key(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let secret_key = iroh::SecretKey::generate(rand_core::OsRng);
|
||||||
|
let tmp = path.with_extension("tmp");
|
||||||
|
std::fs::write(&tmp, hex::encode(secret_key.to_bytes()))?;
|
||||||
|
std::fs::rename(tmp, path)?;
|
||||||
|
Ok(secret_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_secret_key(path: &Path) -> Result<iroh::SecretKey, IrohError> {
|
||||||
|
let encoded = std::fs::read_to_string(path)?;
|
||||||
|
let bytes = hex::decode(encoded.trim())?;
|
||||||
|
let bytes: [u8; 32] = bytes
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| IrohError::InvalidSecretKeyLength)?;
|
||||||
|
Ok(iroh::SecretKey::from_bytes(&bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn all_alpns() -> Vec<Vec<u8>> {
|
||||||
|
[
|
||||||
|
ALPN_CONTROL,
|
||||||
|
ALPN_KV,
|
||||||
|
ALPN_CAS,
|
||||||
|
ALPN_PUBSUB,
|
||||||
|
ALPN_PIPE,
|
||||||
|
ALPN_DB,
|
||||||
|
ALPN_DOCUMENT,
|
||||||
|
ALPN_SSH_PROXY,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(<[u8]>::to_vec)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct EndpointStatus {
|
pub struct EndpointStatus {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
|
@ -22,7 +152,66 @@ impl EndpointStatus {
|
||||||
Self {
|
Self {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
endpoint_id: None,
|
endpoint_id: None,
|
||||||
note: "Iroh endpoint integration is scaffolded for a later pinned API pass".to_owned(),
|
note: "Iroh endpoint is not running outside daemon mode".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum IrohError {
|
||||||
|
#[error("io error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("invalid hex iroh secret key: {0}")]
|
||||||
|
Hex(#[from] hex::FromHexError),
|
||||||
|
#[error("invalid iroh secret key length")]
|
||||||
|
InvalidSecretKeyLength,
|
||||||
|
#[error("failed to bind iroh endpoint: {0}")]
|
||||||
|
Bind(Box<iroh::endpoint::BindError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<iroh::endpoint::BindError> for IrohError {
|
||||||
|
fn from(error: iroh::endpoint::BindError) -> Self {
|
||||||
|
Self::Bind(Box::new(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_alpns_contains_expected_protocols() {
|
||||||
|
let alpns = all_alpns();
|
||||||
|
assert!(alpns.contains(&ALPN_CONTROL.to_vec()));
|
||||||
|
assert!(alpns.contains(&ALPN_CAS.to_vec()));
|
||||||
|
assert!(alpns.contains(&ALPN_SSH_PROXY.to_vec()));
|
||||||
|
assert_eq!(alpns.len(), 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iroh_secret_key_persists() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let path = dir.path().join("iroh.ed25519");
|
||||||
|
let first = load_or_create_secret_key(&path).expect("create key");
|
||||||
|
let second = load_or_create_secret_key(&path).expect("load key");
|
||||||
|
assert_eq!(first.public(), second.public());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn endpoint_status_tracks_node_id_when_bind_is_available() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let config = GethIrohConfig::local(dir.path().join("iroh.ed25519"));
|
||||||
|
match start_endpoint(&config).await {
|
||||||
|
Ok(endpoint) => {
|
||||||
|
let status = endpoint.status();
|
||||||
|
assert!(status.enabled);
|
||||||
|
assert_eq!(status.endpoint_id, Some(endpoint.node_id()));
|
||||||
|
endpoint.shutdown().await;
|
||||||
|
}
|
||||||
|
Err(IrohError::Bind(error)) => {
|
||||||
|
eprintln!("skipping endpoint bind assertion; UDP unavailable: {error}");
|
||||||
|
}
|
||||||
|
Err(error) => panic!("unexpected iroh startup error: {error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ geth-cas = { path = "../geth-cas" }
|
||||||
geth-config = { path = "../geth-config" }
|
geth-config = { path = "../geth-config" }
|
||||||
geth-control = { path = "../geth-control" }
|
geth-control = { path = "../geth-control" }
|
||||||
geth-crypto = { path = "../geth-crypto" }
|
geth-crypto = { path = "../geth-crypto" }
|
||||||
|
geth-iroh = { path = "../geth-iroh" }
|
||||||
geth-resource = { path = "../geth-resource" }
|
geth-resource = { path = "../geth-resource" }
|
||||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||||
geth-store = { path = "../geth-store" }
|
geth-store = { path = "../geth-store" }
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use geth_control::{
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
};
|
};
|
||||||
use geth_crypto::AgentKey;
|
use geth_crypto::AgentKey;
|
||||||
|
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint};
|
||||||
use geth_resource::ResourceDescriptor;
|
use geth_resource::ResourceDescriptor;
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
||||||
|
|
@ -61,6 +62,7 @@ pub struct LocalNode {
|
||||||
pub paths: GethPaths,
|
pub paths: GethPaths,
|
||||||
pub agent_id: String,
|
pub agent_id: String,
|
||||||
pub node_id: String,
|
pub node_id: String,
|
||||||
|
pub iroh_status: EndpointStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_node(paths: &GethPaths) -> Result<LocalNode, NodeError> {
|
pub fn init_node(paths: &GethPaths) -> Result<LocalNode, NodeError> {
|
||||||
|
|
@ -87,6 +89,7 @@ pub fn init_node(paths: &GethPaths) -> Result<LocalNode, NodeError> {
|
||||||
paths: paths.clone(),
|
paths: paths.clone(),
|
||||||
agent_id,
|
agent_id,
|
||||||
node_id,
|
node_id,
|
||||||
|
iroh_status: EndpointStatus::scaffolded(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,7 +98,8 @@ 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 node = init_node(&paths)?;
|
let mut node = init_node(&paths)?;
|
||||||
|
let _iroh_endpoint = start_daemon_iroh_endpoint(&mut node).await?;
|
||||||
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())?;
|
||||||
}
|
}
|
||||||
|
|
@ -157,12 +161,14 @@ pub fn handle_request(
|
||||||
socket: node.paths.socket_path(),
|
socket: node.paths.socket_path(),
|
||||||
agent_id: node.agent_id.clone(),
|
agent_id: node.agent_id.clone(),
|
||||||
node_id: node.node_id.clone(),
|
node_id: node.node_id.clone(),
|
||||||
iroh: "scaffolded; no remote endpoint is started in bootstrap".to_owned(),
|
iroh_enabled: node.iroh_status.enabled,
|
||||||
|
endpoint_id: node.iroh_status.endpoint_id.clone(),
|
||||||
|
iroh: node.iroh_status.note.clone(),
|
||||||
})),
|
})),
|
||||||
ControlRequest::NodeId => Ok(ControlResponse::NodeId(NodeIdResponse {
|
ControlRequest::NodeId => Ok(ControlResponse::NodeId(NodeIdResponse {
|
||||||
agent_id: node.agent_id.clone(),
|
agent_id: node.agent_id.clone(),
|
||||||
node_id: node.node_id.clone(),
|
node_id: node.node_id.clone(),
|
||||||
endpoint_id: None,
|
endpoint_id: node.iroh_status.endpoint_id.clone(),
|
||||||
})),
|
})),
|
||||||
ControlRequest::ResourceList => Ok(ControlResponse::ResourceList {
|
ControlRequest::ResourceList => Ok(ControlResponse::ResourceList {
|
||||||
resources: store
|
resources: store
|
||||||
|
|
@ -438,6 +444,31 @@ fn stable_node_id(agent_id: &str) -> String {
|
||||||
format!("node:{agent_id}")
|
format!("node:{agent_id}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn start_daemon_iroh_endpoint(
|
||||||
|
node: &mut LocalNode,
|
||||||
|
) -> Result<Option<GethIrohEndpoint>, NodeError> {
|
||||||
|
let config = GethIrohConfig::local(node.paths.iroh_key());
|
||||||
|
match geth_iroh::start_endpoint(&config).await {
|
||||||
|
Ok(endpoint) => {
|
||||||
|
let status = endpoint.status();
|
||||||
|
if let Some(endpoint_id) = &status.endpoint_id {
|
||||||
|
let store = Store::open(&node.paths.metadata_db())?;
|
||||||
|
store.upsert_node_endpoint(endpoint_id, &node.node_id, &node.agent_id, "iroh")?;
|
||||||
|
}
|
||||||
|
node.iroh_status = status;
|
||||||
|
Ok(Some(endpoint))
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
node.iroh_status = EndpointStatus {
|
||||||
|
enabled: false,
|
||||||
|
endpoint_id: None,
|
||||||
|
note: format!("Iroh endpoint failed to start: {error}"),
|
||||||
|
};
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn expected_openssh_cert_path(public_key_path: &Path) -> String {
|
fn expected_openssh_cert_path(public_key_path: &Path) -> String {
|
||||||
let text = public_key_path.display().to_string();
|
let text = public_key_path.display().to_string();
|
||||||
if let Some(prefix) = text.strip_suffix(".pub") {
|
if let Some(prefix) = text.strip_suffix(".pub") {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,13 @@ impl Store {
|
||||||
agent_id TEXT,
|
agent_id TEXT,
|
||||||
created_at_ms INTEGER NOT NULL
|
created_at_ms INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS node_endpoints (
|
||||||
|
endpoint_id TEXT PRIMARY KEY,
|
||||||
|
node_id TEXT NOT NULL,
|
||||||
|
agent_id TEXT NOT NULL,
|
||||||
|
transport TEXT NOT NULL,
|
||||||
|
created_at_ms INTEGER NOT NULL
|
||||||
|
);
|
||||||
CREATE TABLE IF NOT EXISTS resources (
|
CREATE TABLE IF NOT EXISTS resources (
|
||||||
resource_id TEXT PRIMARY KEY,
|
resource_id TEXT PRIMARY KEY,
|
||||||
kind TEXT NOT NULL,
|
kind TEXT NOT NULL,
|
||||||
|
|
@ -168,6 +175,20 @@ impl Store {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn upsert_node_endpoint(
|
||||||
|
&self,
|
||||||
|
endpoint_id: &str,
|
||||||
|
node_id: &str,
|
||||||
|
agent_id: &str,
|
||||||
|
transport: &str,
|
||||||
|
) -> Result<(), StoreError> {
|
||||||
|
self.conn.execute(
|
||||||
|
"INSERT OR IGNORE INTO node_endpoints(endpoint_id, node_id, agent_id, transport, created_at_ms) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
|
params![endpoint_id, node_id, agent_id, transport, now_ms()],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list_resources(&self) -> Result<Vec<StoredResource>, StoreError> {
|
pub fn list_resources(&self) -> Result<Vec<StoredResource>, StoreError> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
"SELECT resource_id, kind, name, status FROM resources ORDER BY kind, name, resource_id",
|
"SELECT resource_id, kind, name, status FROM resources ORDER BY kind, name, resource_id",
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ fn geth_status_against_running_daemon() {
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
assert!(stdout.contains("geth daemon: running"));
|
assert!(stdout.contains("geth daemon: running"));
|
||||||
assert!(stdout.contains("agent:"));
|
assert!(stdout.contains("agent:"));
|
||||||
|
assert!(stdout.contains("endpoint:"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,20 @@ 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
|
||||||
|
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
|
||||||
|
`Builder::relay_mode`, persists an `iroh::SecretKey` as hex-encoded 32-byte key
|
||||||
|
material, and shuts down through `Endpoint::close().await`. Relay mode defaults
|
||||||
|
to disabled until daemon policy and discovery are implemented.
|
||||||
|
|
||||||
|
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
|
||||||
|
recorded as a transport binding for the stable geth node identity. If local UDP
|
||||||
|
binding is unavailable, the daemon keeps local control running and reports the
|
||||||
|
Iroh startup error through status output.
|
||||||
|
|
||||||
SSH keys are not transport keys. They are admin trust anchors and signing
|
SSH keys are not transport keys. They are admin trust anchors and signing
|
||||||
identities for keychain and authorization operations. SSH proxying, when added,
|
identities for keychain and authorization operations. SSH proxying, when added,
|
||||||
will carry SSH bytes over an authorized Iroh stream and will not make SSH a geth
|
will carry SSH bytes over an authorized Iroh stream and will not make SSH a geth
|
||||||
|
|
|
||||||
|
|
@ -60,14 +60,14 @@ control, local CAS, service installation, and written architecture decisions.
|
||||||
Goal: make the daemon own a real Iroh endpoint and establish authenticated
|
Goal: make the daemon own a real Iroh endpoint and establish authenticated
|
||||||
geth-to-geth connections without granting trust from discovery alone.
|
geth-to-geth connections without granting trust from discovery alone.
|
||||||
|
|
||||||
- `[ ]` Pin and compile Iroh dependencies.
|
- `[x]` Pin and compile Iroh dependencies.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `iroh` is added only after APIs are pinned and `cargo check --workspace`
|
- `iroh` is added only after APIs are pinned and `cargo check --workspace`
|
||||||
passes.
|
passes.
|
||||||
- `geth-iroh` exposes endpoint startup/shutdown wrappers.
|
- `geth-iroh` exposes endpoint startup/shutdown wrappers.
|
||||||
- Docs note exact crate versions and any API assumptions.
|
- Docs note exact crate versions and any API assumptions.
|
||||||
|
|
||||||
- `[ ]` Daemon-owned Iroh endpoint.
|
- `[x]` Daemon-owned Iroh endpoint.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- The daemon creates one shared Iroh endpoint on startup.
|
- The daemon creates one shared Iroh endpoint on startup.
|
||||||
- `geth status --json` reports endpoint status and EndpointID.
|
- `geth status --json` reports endpoint status and EndpointID.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue