pub mod service; use base64::Engine; use futures::StreamExt; use geth_auth::{AUTH_SIGNATURE_NAMESPACE, AuthExplanation, AuthOp, AuthOpKind, AuthOpSignature}; use geth_cas::{ BlobInfoSummary, CasTreeEntryKind, CasTreeObject, FileConflict, FileConflictKind, FileConflictResolution, FileConflictStatus, FileRoot, FileRootScan, LocalCas, hash_path, }; use geth_config::{GethConfig, GethPaths, RelayMode}; use geth_control::{ CasBlob, CasProvider, ControlRequest, ControlResponse, KeychainStatusResponse, NativeBackendStatus, NodeIdResponse, PeerControlRequest, PeerControlResponse, PipeWireRequest, PipeWireResponse, StatusResponse, SyncPeerRun, SyncPeerStatus, SyncStreamRun, SyncStreamStatus, SyncWatermark, }; use geth_crypto::AgentKey; use geth_db::DbResource; use geth_discovery::{ DiscoveredPeer, DiscoverySource, EndpointCandidate, PEER_CARD_LAN_DISCOVERY_SERVICE, PeerCard, discovery_is_untrusted_note, peer_card_from_txt_attributes, peer_card_txt_attributes, }; use geth_document::{DocumentResource, DocumentState}; use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode}; use geth_keychain::{ KeychainOp, KeychainOpKind, KeychainOpSignature, NODE_ENROLLMENT_REQUEST_NAMESPACE, NodeEnrollmentCapability, NodeEnrollmentProvenance, NodeEnrollmentRequest, NodeEnrollmentStatus, node_enrollment_request_signing_payload, }; use geth_kv::{KvEntry, KvResource, KvSyncEntry}; use geth_pipe::{PipeConnection, PipeListener, PipeMessage}; use geth_pubsub::PubsubMessage; use geth_resource::ResourceDescriptor; use geth_secrets::{BearerAccess, BearerChallenge, BearerProof, ResourceMasterSecret}; use geth_ssh_identity::{ SSH_CERT_ISSUANCE_NAMESPACE, SSH_CERT_REQUEST_NAMESPACE, SSH_REVOCATION_LIST_NAMESPACE, SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord, SshDistributionLogEntry, SshRecordProvenance, SshRevocationEntry, SshRevocationExportFormat, SshRevocationKind, build_ssh_cert_sign_command, cert_request_id, certificate_id, openssh_krl_spec, parse_openssh_krl_spec, revocation_id, ssh_cert_request_signing_payload, ssh_certificate_signing_payload, ssh_public_key_fingerprint, ssh_revocation_signing_payload, write_openssh_krl, }; use geth_ssh_proxy::SshProxyConnection; use geth_store::{ Store, StoredAuthOp, StoredAuthSignature, StoredDbResource, StoredDocumentResource, StoredFileConflict, StoredFileRoot, StoredKeychainOp, StoredKeychainSignature, StoredKvEntry, StoredKvStore, StoredModuleState, StoredNodeEnrollmentRequest, StoredPeerCard, StoredResource, StoredResourceSecret, StoredSshCertRequest, StoredSshCertificate, StoredSshRevocation, }; use geth_types::{ AuthOpId, BlobHash, Capability, DeviceId, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind, ResourceName, SshCertId, SshCertRequestId, UnixMillis, UserId, }; use iroh::protocol::ProtocolHandler; use iroh_docs::api::protocol::{AddrInfoOptions, ShareMode}; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::path::{Component, Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{TcpListener, TcpStream, UnixListener, UnixStream}; #[derive(Debug, thiserror::Error)] pub enum NodeError { #[error("config error: {0}")] Config(#[from] geth_config::ConfigError), #[error("crypto error: {0}")] Crypto(#[from] geth_crypto::CryptoError), #[error("store error: {0}")] Store(#[from] geth_store::StoreError), #[error("cas error: {0}")] Cas(#[from] geth_cas::CasError), #[error("db error: {0}")] Db(#[from] geth_db::DbError), #[error("overlay error: {0}")] Overlay(#[from] geth_overlay::OverlayError), #[error("control error: {0}")] Control(#[from] geth_control::ControlError), #[error("codec error: {0}")] Codec(#[from] geth_codec::CodecError), #[error("json error: {0}")] Json(#[from] serde_json::Error), #[error("io error: {0}")] Io(#[from] std::io::Error), #[error("invalid resource kind: {0}")] InvalidResourceKind(String), #[error("owner init requires --admin-key so the owner trust anchor is recorded")] OwnerInitRequiresAdminKey, #[error("owner init requires --signing-key so the owner statement is signed")] OwnerInitRequiresSigningKey, #[error( "{0} requires --signing-key so the keychain operation can replicate as a verified admin statement" )] SigningKeyRequired(String), #[error("invalid init capability grant, expected =: {0}")] InvalidInitGrant(String), #[error("invalid enrollment capability, expected =: {0}")] InvalidEnrollmentCapability(String), #[error("node enrollment request not found: {0}")] NodeEnrollmentRequestNotFound(String), #[error("node enrollment request has invalid provenance: {0}")] InvalidNodeEnrollment(String), #[error("invalid db resource name: {0}")] InvalidDbName(String), #[error("db path does not exist or is not a file: {0}")] InvalidDbPath(String), #[error("db resource not found: {0}")] DbNotFound(String), #[error("invalid kv store name: {0}")] InvalidKvName(String), #[error("invalid kv key: {0}")] InvalidKvKey(String), #[error("kv store not found: {0}")] KvNotFound(String), #[error("invalid document name: {0}")] InvalidDocumentName(String), #[error("document not found: {0}")] DocumentNotFound(String), #[error("document error: {0}")] Document(#[from] geth_document::DocumentError), #[error("resource not found: {0}")] ResourceNotFound(String), #[error("unauthorized: {0}")] Unauthorized(String), #[error("secrets error: {0}")] Secrets(#[from] geth_secrets::SecretsError), #[error("pubsub error: {0}")] Pubsub(#[from] geth_pubsub::PubsubError), #[error("pipe error: {0}")] Pipe(#[from] geth_pipe::PipeError), #[error("runtime state lock poisoned")] RuntimeLockPoisoned, #[error("invalid ssh certificate kind: {0}")] InvalidSshCertKind(String), #[error("invalid ssh certificate request status: {0}")] InvalidSshCertStatus(String), #[error("invalid ssh revocation kind: {0}")] InvalidSshRevocationKind(String), #[error("ssh certificate request not found: {0}")] SshCertRequestNotFound(String), #[error("ssh certificate request must include at least one principal")] MissingSshCertPrincipal, #[error("ssh certificate flow error: {0}")] SshCertFlow(#[from] geth_ssh_identity::SshCertFlowError), #[error("ssh identity error: {0}")] SshIdentity(#[from] geth_ssh_identity::SshIdentityError), #[error("discovery error: {0}")] Discovery(#[from] geth_discovery::DiscoveryError), #[error("cannot export peer card before the daemon has an Iroh EndpointID")] IrohEndpointUnavailable, #[error("peer candidate not found: {0}")] PeerNotFound(String), #[error("iroh peer error: {0}")] IrohPeer(String), } #[derive(Clone, Debug)] pub struct LocalNode { pub paths: GethPaths, pub agent_id: String, pub node_id: String, pub iroh_status: EndpointStatus, iroh_endpoint: Arc>>, iroh_blob_store: Arc>>, iroh_docs: Arc>>, iroh_gossip: Arc>>, runtime: Arc, } #[derive(Debug)] struct NodeRuntime { pubsub: Mutex, pipes: Mutex, } #[derive(Debug, Default)] struct PubsubRuntime { messages: VecDeque, gossip_topics: BTreeMap, } #[derive(Debug, Clone)] struct PubsubGossipTopicRuntime { sender: iroh_gossip::api::GossipSender, } #[derive(Debug, Default)] struct PipeRuntime { listeners: BTreeMap, connections: VecDeque, messages: VecDeque, } const PUBSUB_RING_LIMIT: usize = 256; const PIPE_CONNECTION_RING_LIMIT: usize = 256; const PIPE_MESSAGE_RING_LIMIT: usize = 1024; const LIVE_SYNC_STALE_AFTER_MS: i64 = 120_000; #[derive(Debug, serde::Deserialize, serde::Serialize)] struct LiveSyncCursor { cursor_ms: i64, } #[derive(Debug, serde::Deserialize, serde::Serialize)] struct LiveSyncHealth { last_attempt_ms: i64, last_success_ms: Option, last_error: Option, last_imported: usize, last_rejected: usize, } #[derive(Debug, serde::Deserialize, serde::Serialize)] struct KvDocsState { name: String, namespace_id: String, read_ticket: String, updated_at_ms: i64, } struct PipeTcpConnectWire { peer_card: PeerCard, target_addr: String, nonce: String, bearer_proof: Option, } struct PipeUnixConnectWire { peer_card: PeerCard, target_path: PathBuf, nonce: String, bearer_proof: Option, } #[derive(Clone, Debug)] pub struct InitOwnerOptions { pub admin_key_path: Option, pub signing_key_path: Option, pub owner_name: String, pub node_name: String, pub capabilities: Vec, } impl Default for InitOwnerOptions { fn default() -> Self { Self { admin_key_path: None, signing_key_path: None, owner_name: "owner".to_owned(), node_name: "local".to_owned(), capabilities: Vec::new(), } } } impl InitOwnerOptions { fn requests_owner_keychain(&self) -> bool { self.admin_key_path.is_some() || self.signing_key_path.is_some() || self.node_name != "local" || self.owner_name != "owner" || !self.capabilities.is_empty() } } pub fn init_node(paths: &GethPaths) -> Result { paths.ensure_base_dirs()?; if !paths.config_file().exists() { std::fs::write(paths.config_file(), GethConfig::default_toml())?; } let key = AgentKey::load_or_create(&paths.agent_key())?; let agent_id = key.agent_id().to_string(); let node_id = stable_node_id(&agent_id); let store = Store::open(&paths.metadata_db())?; store.upsert_agent(&agent_id, &key.public_key_hex())?; store.upsert_node(&node_id, "local", &agent_id)?; store.insert_resource(&StoredResource { resource_id: "resource:cas:local".to_owned(), kind: ResourceKind::Cas.to_string(), name: "local-cas".to_owned(), status: "active".to_owned(), })?; Ok(LocalNode { paths: paths.clone(), agent_id, node_id, iroh_status: EndpointStatus::scaffolded(), iroh_endpoint: Arc::new(Mutex::new(None)), iroh_blob_store: Arc::new(Mutex::new(None)), iroh_docs: Arc::new(Mutex::new(None)), iroh_gossip: Arc::new(Mutex::new(None)), runtime: Arc::new(NodeRuntime { pubsub: Mutex::new(PubsubRuntime::default()), pipes: Mutex::new(PipeRuntime::default()), }), }) } pub fn init_owned_node( paths: &GethPaths, options: InitOwnerOptions, ) -> Result { let initialize_owner = options.requests_owner_keychain(); if initialize_owner && options.admin_key_path.is_none() { return Err(NodeError::OwnerInitRequiresAdminKey); } if initialize_owner && options.signing_key_path.is_none() { return Err(NodeError::OwnerInitRequiresSigningKey); } let node = init_node(paths)?; if initialize_owner { initialize_owner_keychain(&node, options)?; } Ok(node) } pub fn open_node(paths: &GethPaths) -> Result { init_node(paths) } fn initialize_owner_keychain( node: &LocalNode, options: InitOwnerOptions, ) -> Result, NodeError> { let store = Store::open(&node.paths.metadata_db())?; let owner = UserId::new(format!("user:{}", stable_slug(&options.owner_name))); let device = DeviceId::new(format!("device:{}", node.node_id)); let node_id = NodeId::new(node.node_id.clone()); let agent = node.agent_id.clone().into(); let now = UnixMillis(geth_store::now_ms()); let mut ops = vec![ KeychainOp { id: generated_keychain_op_id("keychain-init", "owner", now), created_at: now, kind: KeychainOpKind::KeychainInit, }, KeychainOp { id: generated_keychain_op_id("user-add", owner.as_str(), now), created_at: now, kind: KeychainOpKind::UserAdd { user: owner.clone(), name: options.owner_name.clone(), }, }, KeychainOp { id: generated_keychain_op_id("device-add", device.as_str(), now), created_at: now, kind: KeychainOpKind::DeviceAdd { device: device.clone(), user: owner.clone(), }, }, KeychainOp { id: generated_keychain_op_id("node-add", node_id.as_str(), now), created_at: now, kind: KeychainOpKind::NodeAdd { node: node_id.clone(), device: device.clone(), name: options.node_name.clone(), }, }, KeychainOp { id: generated_keychain_op_id("agent-bind", &node.agent_id, now), created_at: now, kind: KeychainOpKind::AgentBind { agent, node: node_id.clone(), }, }, ]; if let Some(admin_key_path) = options.admin_key_path.as_ref() { let public_key = std::fs::read_to_string(admin_key_path)?; let admin_key = KeyId::new(ssh_public_key_fingerprint(&public_key)); ops.push(KeychainOp { id: generated_keychain_op_id("admin-key-add", admin_key.as_str(), now), created_at: now, kind: KeychainOpKind::AdminKeyAdd { key: admin_key }, }); } let signatures = store_and_sign_keychain_ops( &store, node, &ops, options.signing_key_path.as_deref(), options.admin_key_path.as_deref(), )?; tracing::debug!( ops = ops.len(), signatures = signatures.len(), "initialized owner keychain" ); for grant in options.capabilities { let (resource, capability) = grant .split_once('=') .ok_or_else(|| NodeError::InvalidInitGrant(grant.clone()))?; record_node_grant( &store, node_id.as_str(), resource, capability, Some(format!( "grant:init:{}:{}", stable_slug(node_id.as_str()), stable_slug(capability) )), )?; } Ok(ops) } pub async fn run_daemon(paths: GethPaths) -> Result<(), NodeError> { let mut node = init_node(&paths)?; 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); let config = GethConfig::load(&node.paths.config_file())?; if config.sync.live_sync_enabled { spawn_background_live_sync( node.clone(), Duration::from_millis(config.sync.live_sync_interval_ms), ); } } let _peer_card_lan_discovery = start_peer_card_lan_discovery(&node).await; if Path::new(&paths.socket_path()).exists() { std::fs::remove_file(paths.socket_path())?; } let listener = UnixListener::bind(paths.socket_path())?; tracing::info!(socket = %paths.socket_path().display(), "geth daemon listening"); loop { let (stream, _) = listener.accept().await?; let node = node.clone(); tokio::spawn(async move { if let Err(error) = handle_stream(node, stream).await { tracing::warn!(%error, "control request failed"); } }); } } pub async fn send_control( paths: &GethPaths, request: ControlRequest, ) -> Result { let mut stream = UnixStream::connect(paths.socket_path()).await?; stream .write_all(geth_control::encode_request(&request)?.as_bytes()) .await?; stream.shutdown().await?; let mut reader = BufReader::new(stream); let mut line = String::new(); reader.read_line(&mut line).await?; Ok(geth_control::decode_response(&line)?) } pub async fn stream_ssh_proxy( paths: &GethPaths, peer_node: String, bearer_secret: Option, ) -> Result<(), NodeError> { let mut stream = UnixStream::connect(paths.socket_path()).await?; stream .write_all( geth_control::encode_request(&ControlRequest::SshProxyStream { node: peer_node, bearer_secret, })? .as_bytes(), ) .await?; let mut reader = BufReader::new(stream); let mut line = String::new(); reader.read_line(&mut line).await?; match geth_control::decode_response(&line)? { ControlResponse::SshProxyConnected { allowed: true, .. } => { let stream = reader.into_inner(); let (mut local_read, mut local_write) = stream.into_split(); let mut stdin = tokio::io::stdin(); let mut stdout = tokio::io::stdout(); let upload = async { tokio::io::copy(&mut stdin, &mut local_write).await?; local_write.shutdown().await }; let download = async { tokio::io::copy(&mut local_read, &mut stdout).await?; stdout.flush().await }; tokio::try_join!(upload, download)?; Ok(()) } ControlResponse::SshProxyConnected { allowed: false, reason, .. } => Err(NodeError::Unauthorized(reason)), ControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), other => Err(NodeError::IrohPeer(format!( "daemon returned unexpected SSH proxy stream response: {other:?}" ))), } } pub async fn run_tcp_forward( paths: &GethPaths, listen_addr: String, peer_node: String, target_addr: String, bearer_secret: Option, ) -> Result<(), NodeError> { let listen_addr = geth_pipe::validate_tcp_forward_listen_addr(&listen_addr)?; geth_pipe::validate_tcp_forward_target_addr(&target_addr)?; let listener = TcpListener::bind(listen_addr).await?; tracing::info!( listen = %listen_addr, peer = %peer_node, target = %target_addr, "tcp forward listening" ); loop { let (client, client_addr) = listener.accept().await?; let paths = paths.clone(); let peer_node = peer_node.clone(); let target_addr = target_addr.clone(); let bearer_secret = bearer_secret.clone(); tokio::spawn(async move { if let Err(error) = stream_pipe_tcp(&paths, peer_node, target_addr, bearer_secret, client).await { tracing::warn!(%error, %client_addr, "tcp forward connection failed"); } }); } } async fn stream_pipe_tcp( paths: &GethPaths, peer_node: String, target_addr: String, bearer_secret: Option, client: TcpStream, ) -> Result<(), NodeError> { let mut stream = UnixStream::connect(paths.socket_path()).await?; stream .write_all( geth_control::encode_request(&ControlRequest::PipeTcpStream { node: peer_node, target_addr, bearer_secret, })? .as_bytes(), ) .await?; let mut reader = BufReader::new(stream); let mut line = String::new(); reader.read_line(&mut line).await?; match geth_control::decode_response(&line)? { ControlResponse::PipeRemoteConnected { allowed: true, .. } => { let daemon_stream = reader.into_inner(); let (mut daemon_read, mut daemon_write) = daemon_stream.into_split(); let (mut client_read, mut client_write) = client.into_split(); let upload = async { tokio::io::copy(&mut client_read, &mut daemon_write).await?; daemon_write.shutdown().await }; let download = async { tokio::io::copy(&mut daemon_read, &mut client_write).await?; client_write.shutdown().await }; tokio::try_join!(upload, download)?; Ok(()) } ControlResponse::PipeRemoteConnected { allowed: false, reason, .. } => Err(NodeError::Unauthorized(reason)), ControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), other => Err(NodeError::IrohPeer(format!( "daemon returned unexpected pipe TCP stream response: {other:?}" ))), } } pub async fn run_unix_forward( paths: &GethPaths, listen_path: PathBuf, peer_node: String, target_path: PathBuf, bearer_secret: Option, ) -> Result<(), NodeError> { let listen_path = geth_pipe::validate_unix_forward_path(&listen_path)?; geth_pipe::validate_unix_forward_path(&target_path)?; let listener = UnixListener::bind(&listen_path)?; tracing::info!( listen = %listen_path.display(), peer = %peer_node, target = %target_path.display(), "unix socket forward listening" ); loop { let (client, _) = listener.accept().await?; let paths = paths.clone(); let peer_node = peer_node.clone(); let target_path = target_path.clone(); let bearer_secret = bearer_secret.clone(); tokio::spawn(async move { if let Err(error) = stream_pipe_unix(&paths, peer_node, target_path, bearer_secret, client).await { tracing::warn!(%error, "unix socket forward connection failed"); } }); } } async fn stream_pipe_unix( paths: &GethPaths, peer_node: String, target_path: PathBuf, bearer_secret: Option, client: UnixStream, ) -> Result<(), NodeError> { let mut stream = UnixStream::connect(paths.socket_path()).await?; stream .write_all( geth_control::encode_request(&ControlRequest::PipeUnixStream { node: peer_node, target_path, bearer_secret, })? .as_bytes(), ) .await?; let mut reader = BufReader::new(stream); let mut line = String::new(); reader.read_line(&mut line).await?; match geth_control::decode_response(&line)? { ControlResponse::PipeRemoteConnected { allowed: true, .. } => { let daemon_stream = reader.into_inner(); let (mut daemon_read, mut daemon_write) = daemon_stream.into_split(); let (mut client_read, mut client_write) = client.into_split(); let upload = async { tokio::io::copy(&mut client_read, &mut daemon_write).await?; daemon_write.shutdown().await }; let download = async { tokio::io::copy(&mut daemon_read, &mut client_write).await?; client_write.shutdown().await }; tokio::try_join!(upload, download)?; Ok(()) } ControlResponse::PipeRemoteConnected { allowed: false, reason, .. } => Err(NodeError::Unauthorized(reason)), ControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), other => Err(NodeError::IrohPeer(format!( "daemon returned unexpected pipe Unix stream response: {other:?}" ))), } } pub async fn handle_request_async( node: &LocalNode, request: ControlRequest, ) -> Result { match request { ControlRequest::PeerCardExport { out } => export_peer_card(node, out, true).await, ControlRequest::PeerPing { node: peer_node } => peer_ping(node, &peer_node).await, ControlRequest::PeerAuthCheck { node: peer_node, resource, capability, } => peer_auth_check(node, &peer_node, resource, capability).await, ControlRequest::KeychainSync { node: peer_node } => { keychain_sync_from_peer(node, &peer_node).await } ControlRequest::AuthSync { node: peer_node } => auth_sync_from_peer(node, &peer_node).await, ControlRequest::SyncNow { node: peer_node } => sync_now(node, peer_node).await, ControlRequest::NodeEnrollSubmit { owner_node, request_id, path, } => node_enrollment_submit_to_peer(node, &owner_node, request_id, path).await, ControlRequest::NodeEnrollSync { owner_node } => { node_enrollment_sync_from_owner(node, &owner_node).await } ControlRequest::CasFetch { node: peer_node, hash, bearer_secret, } => cas_fetch_from_peer(node, &peer_node, hash, bearer_secret).await, ControlRequest::CasRootSync { node: peer_node, name, bearer_secret, } => cas_root_sync_from_peer(node, &peer_node, &name, bearer_secret).await, ControlRequest::SshCertSync { node: peer_node, bearer_secret, } => ssh_cert_sync_from_peer(node, &peer_node, bearer_secret).await, ControlRequest::SshRevocationSync { node: peer_node, bearer_secret, } => ssh_revocation_sync_from_peer(node, &peer_node, bearer_secret).await, ControlRequest::KvSync { node: peer_node, name, bearer_secret, } => kv_sync_from_peer(node, &peer_node, &name, bearer_secret).await, ControlRequest::DbSync { node: peer_node, name, limit, bearer_secret, } => db_sync_from_peer(node, &peer_node, &name, limit, bearer_secret).await, ControlRequest::PubsubPub { topic, message, node: Some(peer_node), bearer_secret, } => pubsub_publish_to_peer(node, &peer_node, topic, message, bearer_secret).await, ControlRequest::PubsubSub { topic, node: Some(peer_node), bearer_secret, } => pubsub_subscribe_from_peer(node, &peer_node, topic, bearer_secret).await, ControlRequest::PipeListen { name, node: Some(peer_node), bearer_secret, } => pipe_listen_on_peer(node, &peer_node, name, bearer_secret).await, ControlRequest::PipeConnect { target, node: Some(peer_node), bearer_secret, } => pipe_connect_to_peer(node, &peer_node, target, bearer_secret).await, ControlRequest::PipeSend { target, data_base64, node: Some(peer_node), bearer_secret, } => pipe_send_to_peer(node, &peer_node, target, data_base64, bearer_secret).await, ControlRequest::PipeTcpForward { .. } | ControlRequest::PipeTcpStream { .. } | ControlRequest::PipeUnixForward { .. } | ControlRequest::PipeUnixStream { .. } => Err(NodeError::IrohPeer( "pipe forwarding is a streaming control command; run without --json/--jsonl".to_owned(), )), ControlRequest::SshProxyConnect { node: peer_node, bearer_secret, } => ssh_proxy_connect_to_peer(node, &peer_node, bearer_secret).await, ControlRequest::SshAdminShell { node: peer_node, command, bearer_secret, } => ssh_admin_shell_to_peer(node, &peer_node, command, bearer_secret).await, ControlRequest::DocumentSync { node: peer_node, name, bearer_secret, } => document_sync_from_peer(node, &peer_node, &name, bearer_secret).await, other => { let response = handle_request(node, other)?; match &response { ControlResponse::CasAdded { hash, .. } => { mirror_local_blob_to_iroh_blobs(node, hash).await?; } ControlResponse::CasPrivateAdded { encrypted_hash, .. } => { mirror_local_blob_to_iroh_blobs(node, encrypted_hash).await?; } ControlResponse::KvCreated { kv } => { mirror_kv_store_to_iroh_docs(node, kv.name.as_str()).await?; } ControlResponse::KvSet { entry } => { let name = entry .store .as_str() .strip_prefix("kv:") .unwrap_or(entry.store.as_str()); mirror_kv_store_to_iroh_docs(node, name).await?; } ControlResponse::PubsubPublished { message } => { broadcast_pubsub_gossip( node, message.topic.as_str(), message.message.as_str(), Vec::new(), ) .await?; } _ => {} } Ok(response) } } } async fn handle_stream(node: LocalNode, stream: UnixStream) -> Result<(), NodeError> { let mut reader = BufReader::new(stream); let mut line = String::new(); reader.read_line(&mut line).await?; let request = geth_control::decode_request(&line)?; if let ControlRequest::SshProxyStream { node: peer_node, bearer_secret, } = request { let stream = reader.into_inner(); return handle_local_ssh_proxy_stream(node, &peer_node, bearer_secret, stream).await; } if let ControlRequest::PipeTcpStream { node: peer_node, target_addr, bearer_secret, } = request { let stream = reader.into_inner(); return handle_local_pipe_tcp_stream(node, &peer_node, target_addr, bearer_secret, stream) .await; } if let ControlRequest::PipeUnixStream { node: peer_node, target_path, bearer_secret, } = request { let stream = reader.into_inner(); return handle_local_pipe_unix_stream(node, &peer_node, target_path, bearer_secret, stream) .await; } let response = match handle_request_async(&node, request).await { Ok(response) => response, Err(error) => ControlResponse::Error { message: operator_error_message(&error), }, }; let mut stream = reader.into_inner(); stream .write_all(geth_control::encode_response(&response)?.as_bytes()) .await?; Ok(()) } fn operator_error_message(error: &NodeError) -> String { let mut message = error.to_string(); if let Some(next) = operator_recovery_hint(error) { message.push_str("\nnext: "); message.push_str(&next); } message } fn operator_recovery_hint(error: &NodeError) -> Option { match error { NodeError::PeerNotFound(peer) => Some(format!( "import a signed peer card for {peer} with `geth peer import `, then verify it with `geth peer list`" )), NodeError::IrohEndpointUnavailable => Some( "run `geth status` to inspect Iroh endpoint state; restart with `geth daemon run` if the daemon was started without an endpoint".to_owned(), ), NodeError::Unauthorized(_) => Some( "run `geth auth explain ` and then grant the missing capability with `geth auth grant ... --signing-key `".to_owned(), ), NodeError::ResourceNotFound(resource) => Some(format!( "run `geth resource list` to inspect resources or create {resource} with the matching module command" )), NodeError::DbNotFound(name) => Some(format!( "register the database with `geth db add {name} ` or inspect existing DB resources with `geth resource list`" )), NodeError::KvNotFound(name) => Some(format!( "create the KV store with `geth kv create {name}` or inspect existing resources with `geth resource list`" )), NodeError::DocumentNotFound(name) => Some(format!( "create the document with `geth document create {name}` or inspect existing resources with `geth resource list`" )), NodeError::SshCertRequestNotFound(_) => Some( "run `geth ssh cert requests` on the signing machine and sync metadata with `geth ssh cert sync ` if needed".to_owned(), ), NodeError::InvalidDbPath(_) => Some( "create the SQLite file first, then run `geth db add ` again".to_owned(), ), _ => None, } } async fn start_peer_card_lan_discovery(node: &LocalNode) -> Option { if !node.iroh_status.local_discovery || !node.iroh_status.enabled { return None; } let card = match local_peer_card(node, DiscoverySource::Mdns, true).await { Ok(card) => card, Err(error) => { tracing::warn!(%error, "signed peer-card LAN discovery disabled"); return None; } }; let attributes = match peer_card_txt_attributes(&card) { Ok(attributes) => attributes, Err(error) => { tracing::warn!(%error, "could not encode peer-card LAN TXT payload"); return None; } }; let Some((port, addrs)) = lan_discovery_addrs(&card) else { tracing::warn!( "signed peer-card LAN discovery disabled because no direct Iroh address is known" ); return None; }; let paths = node.paths.clone(); let self_node_id = node.node_id.clone(); let discoverer = match swarm_discovery::Discoverer::new_interactive( PEER_CARD_LAN_DISCOVERY_SERVICE.to_owned(), lan_discovery_peer_id(&node.agent_id), ) .with_addrs(port, addrs) .with_txt_attributes(attributes) { Ok(discoverer) => discoverer, Err(error) => { tracing::warn!(%error, "could not build peer-card LAN discovery"); return None; } } .with_callback(move |_peer_id, peer| { if peer.is_expiry() { return; } let card = match peer_card_from_txt_attributes(peer.txt_attributes()) { Ok(card) => card, Err(error) => { tracing::debug!(%error, "ignoring invalid peer-card LAN discovery payload"); return; } }; if card.node_id.as_str() == self_node_id { return; } let discovered = match DiscoveredPeer::candidate( card.clone(), UnixMillis(geth_store::now_ms()), DiscoverySource::Mdns, ) { Ok(discovered) => discovered, Err(error) => { tracing::debug!(%error, "ignoring invalid LAN peer-card candidate"); return; } }; match Store::open(&paths.metadata_db()).and_then(|store| { store.upsert_peer_card(&StoredPeerCard { peer_id: card.node_id.to_string(), card_json: serde_json::to_string(&card).map_err(geth_store::StoreError::from)?, updated_at_ms: discovered.discovered_at.0, }) }) { Ok(()) => tracing::debug!(node = %card.node_id, "stored LAN peer-card candidate"), Err(error) => tracing::warn!(%error, "could not store LAN peer-card candidate"), } }); match discoverer.spawn(&tokio::runtime::Handle::current()) { Ok(guard) => { tracing::info!( service = PEER_CARD_LAN_DISCOVERY_SERVICE, "signed peer-card LAN discovery running" ); Some(guard) } Err(error) => { tracing::warn!(%error, "could not start signed peer-card LAN discovery"); None } } } fn lan_discovery_peer_id(agent_id: &str) -> String { format!("geth-{}", geth_crypto::blake3_hex(agent_id.as_bytes())) } fn lan_discovery_addrs(card: &PeerCard) -> Option<(u16, Vec)> { let mut parsed = card .endpoints .iter() .flat_map(|endpoint| endpoint.direct_addresses.iter()) .filter_map(|addr| addr.parse::().ok()) .collect::>(); parsed.sort_unstable(); parsed.dedup(); let port = parsed.first()?.port(); let addrs = parsed .into_iter() .filter(|addr| addr.port() == port) .map(|addr| addr.ip()) .collect::>(); (!addrs.is_empty()).then_some((port, addrs)) } async fn export_peer_card( node: &LocalNode, out: Option, include_node_addr: bool, ) -> Result { 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 { 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 { 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)?; ensure_peer_card_matches_endpoint(&peer_card, &candidate.endpoint_id)?; 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 = display_alpn(conn.alpn()); 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)), PeerControlResponse::AuthChecked { .. } => Err(NodeError::IrohPeer( "peer returned auth-check response to ping request".to_owned(), )), PeerControlResponse::SyncStatus { .. } | PeerControlResponse::KeychainSynced { .. } | PeerControlResponse::AuthSynced { .. } | PeerControlResponse::NodeEnrollmentSubmitted { .. } | PeerControlResponse::CasFetched { .. } | PeerControlResponse::CasRootSynced { .. } | PeerControlResponse::SshCertSynced { .. } | PeerControlResponse::SshRevocationSynced { .. } | PeerControlResponse::KvSynced { .. } | PeerControlResponse::PubsubPublished { .. } | PeerControlResponse::PubsubSubscribed { .. } | PeerControlResponse::PipeConnected { .. } | PeerControlResponse::PipeListening { .. } | PeerControlResponse::SshProxyConnected { .. } | PeerControlResponse::SshAdminShellOutput { .. } | PeerControlResponse::DocumentSynced { .. } | PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer( "peer returned wrong response type to ping request".to_owned(), )), } } async fn peer_auth_check( node: &LocalNode, peer_node: &str, resource: String, capability: String, ) -> Result { 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)?; ensure_peer_card_matches_endpoint(&peer_card, &candidate.endpoint_id)?; 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{}\0{}\0{}", node.node_id, peer_node, resource, capability, geth_store::now_ms() ) .as_bytes(), ); let request = PeerControlRequest::AuthCheck { peer_card: self_card, resource: resource.clone(), capability: capability.clone(), nonce: nonce.clone(), }; let conn = endpoint .endpoint() .connect(node_addr, geth_iroh::ALPN_CONTROL) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; 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::AuthChecked { node_id, agent_id, endpoint_id, resource: response_resource, capability: response_capability, allowed, reason, evaluated_ops, nonce: response_nonce, note, .. } if response_nonce == nonce && response_resource == resource && response_capability == capability => { Ok(ControlResponse::PeerAuthChecked { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, resource: response_resource, capability: response_capability, allowed, reason, evaluated_ops, note, }) } PeerControlResponse::AuthChecked { .. } => Err(NodeError::IrohPeer( "peer auth-check response did not match request".to_owned(), )), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), PeerControlResponse::Pong { .. } => Err(NodeError::IrohPeer( "peer returned pong to auth-check request".to_owned(), )), PeerControlResponse::SyncStatus { .. } | PeerControlResponse::KeychainSynced { .. } | PeerControlResponse::AuthSynced { .. } | PeerControlResponse::NodeEnrollmentSubmitted { .. } | PeerControlResponse::CasFetched { .. } | PeerControlResponse::CasRootSynced { .. } | PeerControlResponse::SshCertSynced { .. } | PeerControlResponse::SshRevocationSynced { .. } | PeerControlResponse::KvSynced { .. } | PeerControlResponse::PubsubPublished { .. } | PeerControlResponse::PubsubSubscribed { .. } | PeerControlResponse::PipeConnected { .. } | PeerControlResponse::PipeListening { .. } | PeerControlResponse::SshProxyConnected { .. } | PeerControlResponse::SshAdminShellOutput { .. } | PeerControlResponse::DocumentSynced { .. } | PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer( "peer returned wrong response type to auth-check request".to_owned(), )), } } async fn cas_fetch_from_peer( node: &LocalNode, peer_node: &str, hash: BlobHash, bearer_secret: Option, ) -> Result { 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)?; ensure_peer_card_matches_endpoint(&peer_card, &candidate.endpoint_id)?; 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{}\0{}", node.node_id, peer_node, hash.as_str(), geth_store::now_ms() ) .as_bytes(), ); let request = PeerControlRequest::CasFetch { peer_card: self_card, hash: hash.clone(), nonce: nonce.clone(), bearer_proof: bearer_proof(bearer_secret, "resource:cas:local", "cas.fetch", &nonce), }; let conn = endpoint .endpoint() .connect(node_addr.clone(), geth_iroh::ALPN_CONTROL) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; 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 * 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::CasFetched { node_id, agent_id, endpoint_id, hash: response_hash, size_bytes, content_base64: _, allowed, reason, nonce: response_nonce, note, .. } if response_nonce == nonce && response_hash == hash => { if !allowed { return Ok(ControlResponse::CasFetched { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, hash: response_hash, size_bytes: 0, allowed, reason, note, }); } let info = fetch_blob_via_iroh_blobs(node, node_addr, response_hash.clone()).await?; if info.hash != response_hash { return Err(NodeError::IrohPeer(format!( "peer returned content hash {} for requested {}", info.hash, response_hash ))); } if info.size_bytes != size_bytes { return Err(NodeError::IrohPeer(format!( "peer authorized {size_bytes} CAS bytes but iroh-blobs fetched {} bytes", info.size_bytes ))); } store.record_cas_object( info.hash.as_str(), info.size_bytes, &info.path.to_string_lossy(), )?; store.record_cas_provider(info.hash.as_str(), &node_id, &endpoint_id)?; Ok(ControlResponse::CasFetched { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, hash: info.hash, size_bytes: info.size_bytes, allowed, reason, note, }) } PeerControlResponse::CasFetched { .. } => Err(NodeError::IrohPeer( "peer CAS fetch response did not match request".to_owned(), )), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), PeerControlResponse::Pong { .. } | PeerControlResponse::AuthChecked { .. } | PeerControlResponse::SyncStatus { .. } | PeerControlResponse::KeychainSynced { .. } | PeerControlResponse::AuthSynced { .. } | PeerControlResponse::NodeEnrollmentSubmitted { .. } | PeerControlResponse::CasRootSynced { .. } | PeerControlResponse::SshCertSynced { .. } | PeerControlResponse::SshRevocationSynced { .. } | PeerControlResponse::KvSynced { .. } | PeerControlResponse::PubsubPublished { .. } | PeerControlResponse::PubsubSubscribed { .. } | PeerControlResponse::PipeConnected { .. } | PeerControlResponse::PipeListening { .. } | PeerControlResponse::SshProxyConnected { .. } | PeerControlResponse::SshAdminShellOutput { .. } | PeerControlResponse::DocumentSynced { .. } | PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer( "peer returned wrong response type to CAS fetch".to_owned(), )), } } async fn fetch_blob_via_iroh_blobs( node: &LocalNode, node_addr: iroh::EndpointAddr, hash: BlobHash, ) -> Result { let endpoint = node .iroh_endpoint .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)? .clone() .ok_or(NodeError::IrohEndpointUnavailable)?; let blob_store = iroh_blob_store(node)?.ok_or_else(|| { NodeError::IrohPeer( "iroh-blobs store is unavailable; restart the daemon with native Iroh enabled" .to_owned(), ) })?; let iroh_hash = iroh_blob_hash(&hash)?; let conn = endpoint .endpoint() .connect(node_addr, iroh_blobs::ALPN) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-blobs connect failed: {error}")))?; blob_store .remote() .fetch(conn, iroh_blobs::HashAndFormat::raw(iroh_hash)) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-blobs fetch failed: {error}")))?; let bytes = blob_store .get_bytes(iroh_hash) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-blobs export failed: {error}")))?; let cas = LocalCas::new(node.paths.cas_dir()); let info = cas.add_bytes(bytes.as_ref())?; mirror_local_blob_to_iroh_blobs(node, &info.hash).await?; Ok(info) } fn iroh_blob_store(node: &LocalNode) -> Result, NodeError> { node.iroh_blob_store .lock() .map_err(|_| NodeError::RuntimeLockPoisoned) .map(|store| store.clone()) } fn iroh_blob_hash(hash: &BlobHash) -> Result { let bytes = hex::decode(hash.as_str()).map_err(|error| { NodeError::IrohPeer(format!("invalid CAS hash for iroh-blobs: {error}")) })?; let bytes: [u8; 32] = bytes .try_into() .map_err(|_| NodeError::IrohPeer("invalid CAS hash length for iroh-blobs".to_owned()))?; Ok(iroh_blobs::Hash::from_bytes(bytes)) } async fn mirror_local_blob_to_iroh_blobs( node: &LocalNode, hash: &BlobHash, ) -> Result<(), NodeError> { let Some(blob_store) = iroh_blob_store(node)? else { return Ok(()); }; let content = LocalCas::new(node.paths.cas_dir()).read_bytes(hash)?; let tag = blob_store .add_slice(&content) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-blobs import failed: {error}")))?; if tag.hash.to_string() != hash.as_str() { return Err(NodeError::IrohPeer(format!( "iroh-blobs imported hash {} but local CAS hash is {}", tag.hash, hash ))); } Ok(()) } async fn mirror_local_cas_to_iroh_blobs( paths: &GethPaths, blob_store: &iroh_blobs::store::fs::FsStore, ) -> Result { let cas = LocalCas::new(paths.cas_dir()); let mut count = 0; for blob in cas.list()? { let content = cas.read_bytes(&blob.hash)?; let tag = blob_store .add_slice(&content) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-blobs import failed: {error}")))?; if tag.hash.to_string() != blob.hash.as_str() { return Err(NodeError::IrohPeer(format!( "iroh-blobs imported hash {} but local CAS hash is {}", tag.hash, blob.hash ))); } count += 1; } Ok(count) } async fn cas_root_sync_from_peer( node: &LocalNode, peer_node: &str, name: &str, bearer_secret: Option, ) -> Result { geth_cas::validate_file_root_name(name)?; let response = request_peer_control(node, peer_node, "cas-root-sync", |peer_card, nonce| { PeerControlRequest::CasRootSync { peer_card, name: name.to_owned(), nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, &format!("resource:cas-tree:{name}"), "cas.fetch", &nonce, ), } }) .await?; match response { PeerControlResponse::CasRootSynced { node_id, agent_id, endpoint_id, name: response_name, root, tree_content_base64, allowed, reason, note, .. } if response_name == name => { if !allowed { return Ok(ControlResponse::CasRootSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, name: response_name, root: None, tree_bytes_imported: false, sync_conflicts: Vec::new(), allowed, reason, note, }); } let store = Store::open(&node.paths.metadata_db())?; let mut tree_bytes_imported = false; let mut imported_high_water = None; let mut sync_conflicts = Vec::new(); let root = root .map(|remote_root| { let stored_name = remote_file_root_name(&node_id, &remote_root.name); let stored_path = format!("remote:{node_id}:{}", remote_root.name); let existing_remote_root = store.get_file_root_by_name(&stored_name)?; if let Some(existing) = &existing_remote_root { if existing.path != stored_path { return Err(NodeError::IrohPeer(format!( "refusing to overwrite existing file root {} at {}", existing.name, existing.path ))); } } let base_tree = existing_remote_root .as_ref() .and_then(|existing| existing.latest_tree_json.as_deref()) .map(serde_json::from_str) .transpose()?; let mut remote_tree_json = None; if let (Some(hash), Some(content)) = ( remote_root.latest_tree.clone(), tree_content_base64.as_ref(), ) { let decoded = base64::engine::general_purpose::STANDARD .decode(content) .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let info = LocalCas::new(node.paths.cas_dir()).add_bytes(&decoded)?; if info.hash != hash { return Err(NodeError::IrohPeer(format!( "peer returned file-root tree bytes hash {} for announced {}", info.hash, hash ))); } store.record_cas_object( info.hash.as_str(), info.size_bytes, &info.path.to_string_lossy(), )?; tree_bytes_imported = true; let remote_tree: CasTreeObject = geth_codec::decode_canonical(&decoded)?; remote_tree_json = Some(serde_json::to_string(&remote_tree)?); if let (Some(base_tree), Some(local_root)) = (&base_tree, store.get_file_root_by_name(&remote_root.name)?) { if local_root.latest_tree_json.is_some() { sync_conflicts.extend(record_sync_tree_conflicts( &store, &local_root, existing_remote_root .as_ref() .and_then(|existing| existing.latest_tree_hash.as_deref()), base_tree, local_root.latest_tree_hash.as_deref(), remote_root.latest_tree.as_ref(), &remote_tree, )?); } } } let stored = StoredFileRoot { root_id: format!("file-root:remote:{node_id}:{}", remote_root.name), resource_id: remote_root.resource.clone(), name: stored_name, path: stored_path, latest_tree_hash: remote_root.latest_tree.as_ref().map(ToString::to_string), latest_tree_json: remote_tree_json, updated_at_ms: remote_root.updated_at_ms, }; store.upsert_file_root(&stored)?; imported_high_water = Some(remote_root.updated_at_ms); Ok(file_root_from_stored(&stored)) }) .transpose()?; if let Some(high_water_ms) = imported_high_water { store_live_sync_cursor( &store, peer_node, &format!("cas-tree:{response_name}"), high_water_ms, )?; } Ok(ControlResponse::CasRootSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, name: response_name, root, tree_bytes_imported, sync_conflicts, allowed, reason, note, }) } PeerControlResponse::CasRootSynced { .. } => Err(NodeError::IrohPeer( "peer file-root sync response did not match request".to_owned(), )), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to file-root sync".to_owned(), )), } } fn remote_file_root_name(peer_node_id: &str, remote_name: &str) -> String { let peer = peer_node_id .chars() .map(|ch| { if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-') { ch } else { '-' } }) .collect::(); format!("remote-{peer}-{remote_name}") } async fn ssh_cert_sync_from_peer( node: &LocalNode, peer_node: &str, bearer_secret: Option, ) -> Result { let since_ms = load_live_sync_cursor( &Store::open(&node.paths.metadata_db())?, peer_node, "ssh-certs", )?; let response = request_peer_control(node, peer_node, "ssh-cert-sync", |peer_card, nonce| { PeerControlRequest::SshCertSync { peer_card, since_ms, nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, "resource:ssh:certs", "ssh_cert.sync", &nonce, ), } }) .await?; match response { PeerControlResponse::SshCertSynced { node_id, agent_id, endpoint_id, requests, certificates, log_entries, high_water_ms, allowed, reason, note, .. } => { if !allowed { return Ok(ControlResponse::SshCertSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, requests_imported: 0, certificates_imported: 0, allowed, reason, note, }); } let store = Store::open(&node.paths.metadata_db())?; let entries = if log_entries.is_empty() { ssh_cert_distribution_log_entries(requests, certificates) } else { log_entries }; let (requests_imported, certificates_imported, _) = import_ssh_distribution_log_entries(&store, &entries)?; store_live_sync_cursor(&store, peer_node, "ssh-certs", high_water_ms)?; Ok(ControlResponse::SshCertSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, requests_imported, certificates_imported, allowed, reason, note, }) } PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to SSH cert sync".to_owned(), )), } } async fn ssh_revocation_sync_from_peer( node: &LocalNode, peer_node: &str, bearer_secret: Option, ) -> Result { let since_ms = load_live_sync_cursor( &Store::open(&node.paths.metadata_db())?, peer_node, "ssh-revocations", )?; let response = request_peer_control( node, peer_node, "ssh-revocation-sync", |peer_card, nonce| PeerControlRequest::SshRevocationSync { peer_card, since_ms, nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, "resource:ssh:revocations", "ssh_revocation.sync", &nonce, ), }, ) .await?; match response { PeerControlResponse::SshRevocationSynced { node_id, agent_id, endpoint_id, revocations, log_entries, high_water_ms, allowed, reason, note, .. } => { if !allowed { return Ok(ControlResponse::SshRevocationSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, revocations_imported: 0, allowed, reason, note, }); } let store = Store::open(&node.paths.metadata_db())?; let entries = if log_entries.is_empty() { ssh_revocation_distribution_log_entries(revocations) } else { log_entries }; let (_, _, revocations_imported) = import_ssh_distribution_log_entries(&store, &entries)?; store_live_sync_cursor(&store, peer_node, "ssh-revocations", high_water_ms)?; Ok(ControlResponse::SshRevocationSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, revocations_imported, allowed, reason, note, }) } PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to SSH revocation sync".to_owned(), )), } } async fn kv_sync_from_peer( node: &LocalNode, peer_node: &str, name: &str, bearer_secret: Option, ) -> Result { geth_kv::validate_kv_name(name).map_err(|_| NodeError::InvalidKvName(name.to_owned()))?; let stream = format!("kv:{name}"); let since_ms = load_live_sync_cursor(&Store::open(&node.paths.metadata_db())?, peer_node, &stream)?; let response = request_peer_control(node, peer_node, "kv-sync", |peer_card, nonce| { PeerControlRequest::KvSync { peer_card, name: name.to_owned(), since_ms, nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, &format!("resource:kv:{name}"), "kv.read", &nonce, ), } }) .await?; match response { PeerControlResponse::KvSynced { node_id, agent_id, endpoint_id, name: response_name, entries, high_water_ms, docs_ticket, allowed, reason, note, .. } if response_name == name => { if !allowed { return Ok(ControlResponse::KvSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, name: response_name, entries_imported: 0, allowed, reason, note, }); } let mut entries_imported = 0; if let Some(docs_ticket) = docs_ticket { entries_imported += import_kv_entries_from_iroh_docs(node, name, &docs_ticket).await?; } let store = Store::open(&node.paths.metadata_db())?; let kv = ensure_local_kv_store(&store, name)?; for entry in entries { geth_kv::validate_kv_key(&entry.key) .map_err(|_| NodeError::InvalidKvKey(entry.key.clone()))?; let should_import = store .get_kv_entry(&kv.kv_id, &entry.key)? .is_none_or(|local| entry.updated_at_ms >= local.updated_at_ms); if should_import { store.set_kv_entry(&StoredKvEntry { kv_id: kv.kv_id.clone(), key: entry.key, value: entry.value, updated_at_ms: entry.updated_at_ms, })?; entries_imported += 1; } } store_live_sync_cursor(&store, peer_node, &stream, high_water_ms)?; drop(store); mirror_kv_store_to_iroh_docs(node, name).await?; Ok(ControlResponse::KvSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, name: response_name, entries_imported, allowed, reason, note, }) } PeerControlResponse::KvSynced { .. } => Err(NodeError::IrohPeer( "peer KV sync response did not match request".to_owned(), )), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to KV sync".to_owned(), )), } } fn iroh_docs(node: &LocalNode) -> Result, NodeError> { node.iroh_docs .lock() .map_err(|_| NodeError::RuntimeLockPoisoned) .map(|docs| docs.clone()) } fn iroh_gossip(node: &LocalNode) -> Result, NodeError> { node.iroh_gossip .lock() .map_err(|_| NodeError::RuntimeLockPoisoned) .map(|gossip| gossip.clone()) } fn kv_docs_state_key(name: &str) -> String { format!("kv-doc:{name}") } fn kv_docs_meta_key() -> &'static str { "__geth/kv/name" } async fn ensure_kv_docs_state( node: &LocalNode, name: &str, ) -> Result, NodeError> { let Some(docs) = iroh_docs(node)? else { return Ok(None); }; let stored_state = Store::open(&node.paths.metadata_db())? .get_module_state(&kv_docs_state_key(name))? .map(|stored| serde_json::from_str::(&stored.state_json)) .transpose()?; if let Some(state) = stored_state { let namespace = state .namespace_id .parse::() .map_err(|error| NodeError::IrohPeer(format!("invalid KV docs namespace: {error}")))?; if let Some(doc) = docs .open(namespace) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-docs open failed: {error}")))? { return Ok(Some((doc, state))); } } let doc = docs .create() .await .map_err(|error| NodeError::IrohPeer(format!("iroh-docs create failed: {error}")))?; let ticket = doc .share(ShareMode::Read, AddrInfoOptions::RelayAndAddresses) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-docs share failed: {error}")))?; let state = KvDocsState { name: name.to_owned(), namespace_id: doc.id().to_string(), read_ticket: ticket.to_string(), updated_at_ms: geth_store::now_ms(), }; let store = Store::open(&node.paths.metadata_db())?; store.put_module_state(&StoredModuleState { module: kv_docs_state_key(name), state_json: serde_json::to_string(&state)?, updated_at_ms: state.updated_at_ms, })?; Ok(Some((doc, state))) } async fn kv_docs_read_ticket(node: &LocalNode, name: &str) -> Result, NodeError> { Ok(ensure_kv_docs_state(node, name) .await? .map(|(_, state)| state.read_ticket)) } async fn mirror_kv_store_to_iroh_docs(node: &LocalNode, name: &str) -> Result<(), NodeError> { let Some(docs) = iroh_docs(node)? else { return Ok(()); }; let entries = { let store = Store::open(&node.paths.metadata_db())?; let kv = ensure_local_kv_store(&store, name)?; store.list_kv_entries_since(&kv.kv_id, 0)? }; let Some((doc, _state)) = ensure_kv_docs_state(node, name).await? else { return Ok(()); }; let author = docs .author_default() .await .map_err(|error| NodeError::IrohPeer(format!("iroh-docs author failed: {error}")))?; doc.set_bytes( author, kv_docs_meta_key().as_bytes().to_vec(), name.as_bytes().to_vec(), ) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-docs set metadata failed: {error}")))?; for entry in entries { doc.set_bytes(author, entry.key.into_bytes(), entry.value.into_bytes()) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-docs set failed: {error}")))?; } Ok(()) } async fn mirror_all_kv_stores_to_iroh_docs(node: &LocalNode) -> Result { if iroh_docs(node)?.is_none() { return Ok(0); } let store = Store::open(&node.paths.metadata_db())?; let mut mirrored = 0; for kv in store.list_kv_stores()? { mirror_kv_store_to_iroh_docs(node, &kv.name).await?; mirrored += 1; } Ok(mirrored) } async fn import_kv_entries_from_iroh_docs( node: &LocalNode, name: &str, ticket: &str, ) -> Result { let Some(docs) = iroh_docs(node)? else { return Ok(0); }; let Some(blob_store) = iroh_blob_store(node)? else { return Ok(0); }; let ticket = ticket .parse::() .map_err(|error| NodeError::IrohPeer(format!("invalid iroh-docs ticket: {error}")))?; let doc = docs .import(ticket.clone()) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-docs import failed: {error}")))?; let state = KvDocsState { name: name.to_owned(), namespace_id: doc.id().to_string(), read_ticket: ticket.to_string(), updated_at_ms: geth_store::now_ms(), }; let store = Store::open(&node.paths.metadata_db())?; store.put_module_state(&StoredModuleState { module: kv_docs_state_key(name), state_json: serde_json::to_string(&state)?, updated_at_ms: state.updated_at_ms, })?; let mut imported = 0; for _ in 0..5 { imported += import_kv_doc_entries_once(node, name, &blob_store, &doc).await?; if imported > 0 { break; } tokio::time::sleep(Duration::from_millis(100)).await; } Ok(imported) } async fn import_kv_doc_entries_once( node: &LocalNode, name: &str, blob_store: &iroh_blobs::store::fs::FsStore, doc: &iroh_docs::api::Doc, ) -> Result { let mut imported_entries = Vec::new(); let entries = doc .get_many(iroh_docs::store::Query::single_latest_per_key()) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-docs query failed: {error}")))?; futures::pin_mut!(entries); while let Some(entry) = entries.next().await { let entry = entry .map_err(|error| NodeError::IrohPeer(format!("iroh-docs entry failed: {error}")))?; let key = String::from_utf8(entry.key().to_vec()) .map_err(|error| NodeError::IrohPeer(format!("invalid KV docs key: {error}")))?; if key == kv_docs_meta_key() { continue; } geth_kv::validate_kv_key(&key).map_err(|_| NodeError::InvalidKvKey(key.clone()))?; if entry.content_len() > 1024 * 1024 { return Err(NodeError::IrohPeer(format!( "KV docs value for {key} is too large: {} bytes", entry.content_len() ))); } let value = blob_store .get_bytes(entry.content_hash()) .await .map_err(|error| { NodeError::IrohPeer(format!("iroh-docs blob export failed: {error}")) })?; let value = String::from_utf8(value.to_vec()) .map_err(|error| NodeError::IrohPeer(format!("invalid KV docs value: {error}")))?; let updated_at_ms = (entry.timestamp() / 1000) as i64; imported_entries.push((key, value, updated_at_ms)); } let store = Store::open(&node.paths.metadata_db())?; let kv = ensure_local_kv_store(&store, name)?; let mut imported = 0; for (key, value, updated_at_ms) in imported_entries { let should_import = store .get_kv_entry(&kv.kv_id, &key)? .is_none_or(|local| updated_at_ms >= local.updated_at_ms); if should_import { store.set_kv_entry(&StoredKvEntry { kv_id: kv.kv_id.clone(), key, value, updated_at_ms, })?; imported += 1; } } Ok(imported) } async fn pubsub_publish_to_peer( node: &LocalNode, peer_node: &str, topic: String, message: String, bearer_secret: Option, ) -> Result { geth_pubsub::validate_topic(&topic)?; geth_pubsub::validate_message(&message)?; let bootstrap_peer = peer_gossip_endpoint_id(node, peer_node)?; ensure_pubsub_gossip_topic(node, topic.as_str(), vec![bootstrap_peer]).await?; let response = request_peer_control(node, peer_node, "pubsub-publish", |peer_card, nonce| { PeerControlRequest::PubsubPublish { peer_card, topic: topic.clone(), message: message.clone(), nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, &format!("resource:pubsub:{topic}"), "pubsub.publish", &nonce, ), } }) .await?; match response { PeerControlResponse::PubsubPublished { node_id, agent_id, endpoint_id, message, allowed, reason, note, .. } => { let message = message.unwrap_or_else(|| PubsubMessage { topic: topic.into(), message: String::new(), published_at: UnixMillis(0), }); Ok(ControlResponse::PubsubRemotePublished { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, message, allowed, reason, note, }) } PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to pubsub publish".to_owned(), )), } } async fn pubsub_subscribe_from_peer( node: &LocalNode, peer_node: &str, topic: String, bearer_secret: Option, ) -> Result { geth_pubsub::validate_topic(&topic)?; let bootstrap_peer = peer_gossip_endpoint_id(node, peer_node)?; ensure_pubsub_gossip_topic(node, topic.as_str(), vec![bootstrap_peer]).await?; let response = request_peer_control(node, peer_node, "pubsub-subscribe", |peer_card, nonce| { PeerControlRequest::PubsubSubscribe { peer_card, topic: topic.clone(), nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, &format!("resource:pubsub:{topic}"), "pubsub.subscribe", &nonce, ), } }) .await?; match response { PeerControlResponse::PubsubSubscribed { node_id, agent_id, endpoint_id, topic: response_topic, messages, allowed, reason, note, .. } if response_topic == topic => Ok(ControlResponse::PubsubRemoteMessages { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, topic: response_topic, messages, allowed, reason, note, }), PeerControlResponse::PubsubSubscribed { .. } => Err(NodeError::IrohPeer( "peer pubsub subscribe response did not match request".to_owned(), )), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to pubsub subscribe".to_owned(), )), } } async fn pipe_connect_to_peer( node: &LocalNode, peer_node: &str, target: String, bearer_secret: Option, ) -> Result { geth_pipe::validate_pipe_name(&target)?; let response = request_peer_control(node, peer_node, "pipe-connect", |peer_card, nonce| { PeerControlRequest::PipeConnect { peer_card, target: target.clone(), nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, &format!("resource:pipe:{target}"), "pipe.connect", &nonce, ), } }) .await?; match response { PeerControlResponse::PipeConnected { node_id, agent_id, endpoint_id, connection, allowed, reason, note, .. } => { let connection = connection.unwrap_or_else(|| PipeConnection { target, connected_at: UnixMillis(0), local_listener_found: false, note: geth_pipe::pipe_roadmap().to_owned(), }); Ok(ControlResponse::PipeRemoteConnected { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, connection, allowed, reason, note, }) } PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to pipe connect".to_owned(), )), } } async fn pipe_listen_on_peer( node: &LocalNode, peer_node: &str, name: String, bearer_secret: Option, ) -> Result { geth_pipe::validate_pipe_name(&name)?; let response = request_peer_control(node, peer_node, "pipe-listen", |peer_card, nonce| { PeerControlRequest::PipeListen { peer_card, name: name.clone(), nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, &format!("resource:pipe:{name}"), "pipe.listen", &nonce, ), } }) .await?; match response { PeerControlResponse::PipeListening { node_id, agent_id, endpoint_id, listener, allowed, reason, note, .. } => Ok(ControlResponse::PipeRemoteListening { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, listener, allowed, reason, note, }), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to pipe listen".to_owned(), )), } } async fn pipe_send_to_peer( node: &LocalNode, peer_node: &str, target: String, data_base64: String, bearer_secret: Option, ) -> Result { geth_pipe::validate_pipe_name(&target)?; base64::engine::general_purpose::STANDARD .decode(&data_base64) .map_err(|error| NodeError::IrohPeer(format!("invalid pipe base64 payload: {error}")))?; let response = request_pipe_wire(node, peer_node, "pipe-send", |peer_card, nonce| { PipeWireRequest::Send { peer_card, target: target.clone(), data_base64: data_base64.clone(), nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, &format!("resource:pipe:{target}"), "pipe.connect", &nonce, ), } }) .await?; match response { PipeWireResponse::Sent { node_id, agent_id, endpoint_id, message, listener_found, allowed, reason, note, .. } => Ok(ControlResponse::PipeRemoteSent { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, message: *message, listener_found, allowed, reason, note, }), PipeWireResponse::Error { message } => Err(NodeError::IrohPeer(message)), PipeWireResponse::Connected { .. } => Err(NodeError::IrohPeer( "peer returned TCP stream response to pipe send".to_owned(), )), } } async fn handle_local_pipe_tcp_stream( node: LocalNode, peer_node: &str, target_addr: String, bearer_secret: Option, local_stream: UnixStream, ) -> Result<(), NodeError> { geth_pipe::validate_tcp_forward_target_addr(&target_addr)?; 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)?; ensure_peer_card_matches_endpoint(&peer_card, &candidate.endpoint_id)?; 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{}\0pipe-tcp-stream\0{}\0{}", node.node_id, peer_node, target_addr, geth_store::now_ms() ) .as_bytes(), ); let resource = format!("resource:pipe-tcp:{target_addr}"); let request = PipeWireRequest::TcpConnect { peer_card: self_card, target_addr: target_addr.clone(), nonce: nonce.clone(), bearer_proof: bearer_proof(bearer_secret, &resource, "pipe.forward", &nonce), }; let conn = endpoint .endpoint() .connect(node_addr, geth_iroh::ALPN_PIPE) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let (mut remote_send, remote_recv) = conn .open_bi() .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; remote_send .write_all(geth_control::encode_pipe_wire_request(&request)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let mut remote_reader = BufReader::new(remote_recv); let mut line = String::new(); remote_reader .read_line(&mut line) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let response = geth_control::decode_pipe_wire_response(&line)?; let local_response = match response { PipeWireResponse::Connected { node_id, agent_id, endpoint_id, connection, allowed, reason, note, nonce: response_nonce, .. } if response_nonce == nonce => ControlResponse::PipeRemoteConnected { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, connection: connection.unwrap_or_else(|| PipeConnection { target: target_addr.clone(), connected_at: UnixMillis(0), local_listener_found: false, note: "pipe TCP forward was denied before connecting".to_owned(), }), allowed, reason, note, }, PipeWireResponse::Error { message } => ControlResponse::Error { message }, _ => ControlResponse::Error { message: "peer returned wrong response type to pipe TCP stream".to_owned(), }, }; let mut local_stream = local_stream; local_stream .write_all(geth_control::encode_response(&local_response)?.as_bytes()) .await?; let ControlResponse::PipeRemoteConnected { allowed: true, .. } = local_response else { return Ok(()); }; let remote_recv = remote_reader.into_inner(); let (mut local_read, mut local_write) = local_stream.into_split(); let upload = async { tokio::io::copy(&mut local_read, &mut remote_send) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; remote_send .finish() .map_err(|error| NodeError::IrohPeer(error.to_string()))?; Ok::<(), NodeError>(()) }; let mut remote_recv = remote_recv; let download = async { tokio::io::copy(&mut remote_recv, &mut local_write) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; local_write.shutdown().await.map_err(NodeError::from)?; Ok::<(), NodeError>(()) }; tokio::try_join!(upload, download)?; Ok(()) } async fn handle_local_pipe_unix_stream( node: LocalNode, peer_node: &str, target_path: PathBuf, bearer_secret: Option, local_stream: UnixStream, ) -> Result<(), NodeError> { let target_path = geth_pipe::validate_unix_forward_path(&target_path)?; let target_display = target_path.display().to_string(); 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)?; ensure_peer_card_matches_endpoint(&peer_card, &candidate.endpoint_id)?; 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{}\0pipe-unix-stream\0{}\0{}", node.node_id, peer_node, target_display, geth_store::now_ms() ) .as_bytes(), ); let resource = format!("resource:pipe-unix:{target_display}"); let request = PipeWireRequest::UnixConnect { peer_card: self_card, target_path: target_path.clone(), nonce: nonce.clone(), bearer_proof: bearer_proof(bearer_secret, &resource, "pipe.forward", &nonce), }; let conn = endpoint .endpoint() .connect(node_addr, geth_iroh::ALPN_PIPE) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let (mut remote_send, remote_recv) = conn .open_bi() .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; remote_send .write_all(geth_control::encode_pipe_wire_request(&request)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let mut remote_reader = BufReader::new(remote_recv); let mut line = String::new(); remote_reader .read_line(&mut line) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let response = geth_control::decode_pipe_wire_response(&line)?; let local_response = match response { PipeWireResponse::Connected { node_id, agent_id, endpoint_id, connection, allowed, reason, note, nonce: response_nonce, .. } if response_nonce == nonce => ControlResponse::PipeRemoteConnected { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, connection: connection.unwrap_or_else(|| PipeConnection { target: target_display.clone(), connected_at: UnixMillis(0), local_listener_found: false, note: "pipe Unix forward was denied before connecting".to_owned(), }), allowed, reason, note, }, PipeWireResponse::Error { message } => ControlResponse::Error { message }, _ => ControlResponse::Error { message: "peer returned wrong response type to pipe Unix stream".to_owned(), }, }; let mut local_stream = local_stream; local_stream .write_all(geth_control::encode_response(&local_response)?.as_bytes()) .await?; let ControlResponse::PipeRemoteConnected { allowed: true, .. } = local_response else { return Ok(()); }; let remote_recv = remote_reader.into_inner(); let (mut local_read, mut local_write) = local_stream.into_split(); let upload = async { tokio::io::copy(&mut local_read, &mut remote_send) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; remote_send .finish() .map_err(|error| NodeError::IrohPeer(error.to_string()))?; Ok::<(), NodeError>(()) }; let mut remote_recv = remote_recv; let download = async { tokio::io::copy(&mut remote_recv, &mut local_write) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; local_write.shutdown().await.map_err(NodeError::from)?; Ok::<(), NodeError>(()) }; tokio::try_join!(upload, download)?; Ok(()) } async fn ssh_proxy_connect_to_peer( node: &LocalNode, peer_node: &str, bearer_secret: Option, ) -> Result { let response = request_peer_control(node, peer_node, "ssh-proxy-connect", |peer_card, nonce| { PeerControlRequest::SshProxyConnect { peer_card, nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, "resource:ssh-proxy:local", "ssh_proxy.connect", &nonce, ), } }) .await?; match response { PeerControlResponse::SshProxyConnected { node_id, agent_id, endpoint_id, connection, allowed, reason, note, .. } => Ok(ControlResponse::SshProxyConnected { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, connection, allowed, reason, note, }), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to SSH proxy connect".to_owned(), )), } } async fn ssh_admin_shell_to_peer( node: &LocalNode, peer_node: &str, command: String, bearer_secret: Option, ) -> Result { let response = request_peer_control(node, peer_node, "ssh-admin-shell", |peer_card, nonce| { PeerControlRequest::SshAdminShell { peer_card, command: command.clone(), nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, "resource:ssh-proxy:local", "ssh_proxy.admin_shell", &nonce, ), } }) .await?; match response { PeerControlResponse::SshAdminShellOutput { node_id, agent_id, endpoint_id, command, output, allowed, reason, note, .. } => Ok(ControlResponse::SshAdminShellOutput { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, command, output, allowed, reason, note, }), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to SSH admin shell".to_owned(), )), } } async fn handle_local_ssh_proxy_stream( node: LocalNode, peer_node: &str, bearer_secret: Option, local_stream: UnixStream, ) -> Result<(), 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)?; ensure_peer_card_matches_endpoint(&peer_card, &candidate.endpoint_id)?; 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{}\0ssh-proxy-stream\0{}", node.node_id, peer_node, geth_store::now_ms() ) .as_bytes(), ); let request = PeerControlRequest::SshProxyConnect { peer_card: self_card, nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, "resource:ssh-proxy:local", "ssh_proxy.connect", &nonce, ), }; let conn = endpoint .endpoint() .connect(node_addr, geth_iroh::ALPN_SSH_PROXY) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let (mut remote_send, remote_recv) = conn .open_bi() .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; remote_send .write_all(geth_control::encode_peer_request(&request)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let mut remote_reader = BufReader::new(remote_recv); let mut line = String::new(); remote_reader .read_line(&mut line) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let response = geth_control::decode_peer_response(&line)?; let local_response = match response { PeerControlResponse::SshProxyConnected { node_id, agent_id, endpoint_id, connection, allowed, reason, note, nonce: response_nonce, .. } if response_nonce == nonce => ControlResponse::SshProxyConnected { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, connection, allowed, reason, note, }, PeerControlResponse::Error { message } => ControlResponse::Error { message }, _ => ControlResponse::Error { message: "peer returned wrong response type to SSH proxy stream".to_owned(), }, }; let mut local_stream = local_stream; local_stream .write_all(geth_control::encode_response(&local_response)?.as_bytes()) .await?; let ControlResponse::SshProxyConnected { allowed: true, .. } = local_response else { return Ok(()); }; let remote_recv = remote_reader.into_inner(); let (mut local_read, mut local_write) = local_stream.into_split(); let upload = async { tokio::io::copy(&mut local_read, &mut remote_send) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; remote_send .finish() .map_err(|error| NodeError::IrohPeer(error.to_string()))?; Ok::<(), NodeError>(()) }; let mut remote_recv = remote_recv; let download = async { tokio::io::copy(&mut remote_recv, &mut local_write) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; local_write.shutdown().await.map_err(NodeError::from)?; Ok::<(), NodeError>(()) }; tokio::try_join!(upload, download)?; Ok(()) } async fn document_sync_from_peer( node: &LocalNode, peer_node: &str, name: &str, bearer_secret: Option, ) -> Result { geth_document::validate_document_name(name) .map_err(|_| NodeError::InvalidDocumentName(name.to_owned()))?; let stream = format!("document:{name}"); let since_ms = load_live_sync_cursor(&Store::open(&node.paths.metadata_db())?, peer_node, &stream)?; let response = request_peer_control(node, peer_node, "document-sync", |peer_card, nonce| { PeerControlRequest::DocumentSync { peer_card, name: name.to_owned(), since_ms, nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, &format!("resource:document:{name}"), "document.read", &nonce, ), } }) .await?; match response { PeerControlResponse::DocumentSynced { node_id, agent_id, endpoint_id, name: response_name, state, high_water_ms, allowed, reason, note, .. } if response_name == name => { if !allowed { return Ok(ControlResponse::DocumentSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, name: response_name, updated: false, allowed, reason, note, }); } let store = Store::open(&node.paths.metadata_db())?; let mut updated = false; if let Some(state) = state { let local = ensure_local_document(&store, name)?; if state.updated_at.0 >= local.updated_at_ms { let merged = geth_document::merge_automerge_states( &local.state_json, &state.state_json, )?; store.insert_document_resource(&StoredDocumentResource { document_id: local.document_id, resource_id: local.resource_id, name: local.name, state_json: merged, updated_at_ms: state.updated_at.0, })?; updated = true; } } store_live_sync_cursor(&store, peer_node, &stream, high_water_ms)?; Ok(ControlResponse::DocumentSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, name: response_name, updated, allowed, reason, note, }) } PeerControlResponse::DocumentSynced { .. } => Err(NodeError::IrohPeer( "peer document sync response did not match request".to_owned(), )), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to document sync".to_owned(), )), } } async fn sync_status_from_peer( node: &LocalNode, peer_node: &str, ) -> Result, NodeError> { let response = request_peer_control(node, peer_node, "sync-status", |peer_card, nonce| { PeerControlRequest::SyncStatus { peer_card, nonce } }) .await?; match response { PeerControlResponse::SyncStatus { watermarks, note, .. } => { tracing::debug!(peer = %peer_node, %note, streams = watermarks.len(), "peer sync status received"); Ok(watermarks) } PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to sync status".to_owned(), )), } } async fn keychain_sync_from_peer( node: &LocalNode, peer_node: &str, ) -> Result { keychain_sync_from_peer_since(node, peer_node, 0).await } async fn keychain_sync_from_peer_since( node: &LocalNode, peer_node: &str, since_ms: i64, ) -> Result { let response = request_peer_control(node, peer_node, "keychain-sync", |peer_card, nonce| { PeerControlRequest::KeychainSync { peer_card, since_ms, nonce, } }) .await?; match response { PeerControlResponse::KeychainSynced { node_id, agent_id, endpoint_id, ops, signatures, high_water_ms, note, .. } => { let store = Store::open(&node.paths.metadata_db())?; let mut local_ops = load_keychain_ops(&store)?; let mut trusted_admins = geth_keychain::reduce_keychain_ops(&local_ops).admin_keys; let mut known_signatures = store .list_keychain_signatures()? .into_iter() .map(|signature| (signature.op_id, signature.signer, signature.namespace)) .collect::>(); let mut ops_imported = 0; let mut signatures_imported = 0; let mut invalid_ops_rejected = 0; for op in ops { let op_signatures = signatures .iter() .filter(|signature| signature.op_id == op.id) .cloned() .collect::>(); if op_signatures.is_empty() { invalid_ops_rejected += 1; continue; } let valid_signatures = op_signatures .iter() .filter(|signature| { if !trusted_admins.contains(&signature.signer) || !keychain_signature_uses_claimed_key(signature) { return false; } let stored = stored_keychain_signature_from_signature(signature); verify_keychain_signature_with_ssh(node, &op, &stored).unwrap_or(false) }) .cloned() .collect::>(); if valid_signatures.is_empty() { invalid_ops_rejected += 1; continue; } let existing = load_keychain_ops(&store)? .into_iter() .find(|existing| existing.id == op.id); if existing.as_ref().is_some_and(|existing| existing != &op) { invalid_ops_rejected += 1; continue; } store_keychain_op(&store, &op)?; if existing.is_none() { local_ops.push(op.clone()); trusted_admins = geth_keychain::reduce_keychain_ops(&local_ops).admin_keys; ops_imported += 1; } for signature in valid_signatures { let signature_key = ( signature.op_id.to_string(), signature.signer.to_string(), signature.namespace.clone(), ); store.insert_keychain_signature(&StoredKeychainSignature { op_id: signature.op_id.to_string(), signer: signature.signer.to_string(), signer_public_key: signature.signer_public_key.clone(), namespace: signature.namespace.clone(), signature: signature.signature.clone(), created_at_ms: signature.created_at.0, })?; signatures_imported += usize::from(known_signatures.insert(signature_key)); } } store_live_sync_cursor(&store, peer_node, "keychain", high_water_ms)?; Ok(ControlResponse::KeychainSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, ops_imported, signatures_imported, invalid_ops_rejected, high_water_ms, note: format!( "{note}; imported only keychain ops signed by currently trusted admin SSH keys" ), }) } PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to keychain sync".to_owned(), )), } } async fn auth_sync_from_peer( node: &LocalNode, peer_node: &str, ) -> Result { auth_sync_from_peer_since(node, peer_node, 0).await } async fn auth_sync_from_peer_since( node: &LocalNode, peer_node: &str, since_ms: i64, ) -> Result { let response = request_peer_control(node, peer_node, "auth-sync", |peer_card, nonce| { PeerControlRequest::AuthSync { peer_card, since_ms, nonce, } }) .await?; match response { PeerControlResponse::AuthSynced { node_id, agent_id, endpoint_id, ops, signatures, high_water_ms, note, .. } => { let store = Store::open(&node.paths.metadata_db())?; let trusted_admins = geth_keychain::reduce_keychain_ops(&load_keychain_ops(&store)?).admin_keys; let mut known_signatures = store .list_auth_signatures()? .into_iter() .map(|signature| (signature.op_id, signature.signer, signature.namespace)) .collect::>(); let mut ops_imported = 0; let mut signatures_imported = 0; let mut invalid_ops_rejected = 0; for op in ops { let op_signatures = signatures .iter() .filter(|signature| signature.op_id == op.id) .cloned() .collect::>(); if op_signatures.is_empty() { invalid_ops_rejected += 1; continue; } let valid_signatures = op_signatures .iter() .filter(|signature| { if !trusted_admins.contains(&signature.signer) || !auth_signature_uses_claimed_key(signature) { return false; } let stored = stored_auth_signature_from_signature(signature); verify_auth_signature_with_ssh(node, &op, &stored).unwrap_or(false) }) .cloned() .collect::>(); if valid_signatures.is_empty() { invalid_ops_rejected += 1; continue; } let existing = load_auth_ops(&store)? .into_iter() .find(|existing| existing.id == op.id); if existing.as_ref().is_some_and(|existing| existing != &op) { invalid_ops_rejected += 1; continue; } store_auth_op(&store, &op)?; ops_imported += usize::from(existing.is_none()); for signature in valid_signatures { let signature_key = ( signature.op_id.to_string(), signature.signer.to_string(), signature.namespace.clone(), ); store.insert_auth_signature(&StoredAuthSignature { op_id: signature.op_id.to_string(), signer: signature.signer.to_string(), signer_public_key: signature.signer_public_key.clone(), namespace: signature.namespace.clone(), signature: signature.signature.clone(), created_at_ms: signature.created_at.0, })?; signatures_imported += usize::from(known_signatures.insert(signature_key)); } } store_live_sync_cursor(&store, peer_node, "auth", high_water_ms)?; Ok(ControlResponse::AuthSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, ops_imported, signatures_imported, invalid_ops_rejected, high_water_ms, note: format!("{note}; imported only auth ops signed by trusted admin SSH keys"), }) } PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to auth sync".to_owned(), )), } } async fn node_enrollment_submit_to_peer( node: &LocalNode, owner_node: &str, request_id: Option, path: Option, ) -> Result { let store = Store::open(&node.paths.metadata_db())?; let request = if let Some(path) = path { let request = read_node_enrollment_request_file(&path)?; insert_node_enrollment_request_if_not_conflicting(&store, &request)?; request } else { let request_id = request_id .or_else(|| { latest_pending_node_enrollment_request(&store).map(|request| request.id.to_string()) }) .ok_or_else(|| NodeError::NodeEnrollmentRequestNotFound("latest-pending".to_owned()))?; load_node_enrollment_request(&store, &request_id)? }; let response = request_peer_control( node, owner_node, "node-enrollment-submit", |peer_card, nonce| PeerControlRequest::NodeEnrollmentSubmit { peer_card, request: request.clone(), nonce, }, ) .await?; match response { PeerControlResponse::NodeEnrollmentSubmitted { node_id, accepted, note, .. } => Ok(ControlResponse::NodeEnrollmentSubmitted { request_id: request.id.to_string(), owner_node_id: node_id, accepted, note, }), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to node enrollment submit".to_owned(), )), } } async fn node_enrollment_sync_from_owner( node: &LocalNode, owner_node: &str, ) -> Result { let keychain = keychain_sync_from_peer(node, owner_node).await?; let auth = auth_sync_from_peer(node, owner_node).await?; let (keychain_ops_imported, keychain_signatures_imported, mut invalid_ops_rejected) = match keychain { ControlResponse::KeychainSynced { ops_imported, signatures_imported, invalid_ops_rejected, .. } => (ops_imported, signatures_imported, invalid_ops_rejected), other => { return Err(NodeError::IrohPeer(format!( "unexpected keychain sync response during enrollment sync: {other:?}" ))); } }; let (auth_ops_imported, auth_signatures_imported, auth_invalid) = match auth { ControlResponse::AuthSynced { ops_imported, signatures_imported, invalid_ops_rejected, .. } => (ops_imported, signatures_imported, invalid_ops_rejected), other => { return Err(NodeError::IrohPeer(format!( "unexpected auth sync response during enrollment sync: {other:?}" ))); } }; invalid_ops_rejected += auth_invalid; Ok(ControlResponse::NodeEnrollmentSynced { owner_node: owner_node.to_owned(), keychain_ops_imported, keychain_signatures_imported, auth_ops_imported, auth_signatures_imported, invalid_ops_rejected, note: "enrollment sync pulled signed keychain and signed auth operations from the owner node" .to_owned(), }) } async fn db_sync_from_peer( node: &LocalNode, peer_node: &str, name: &str, limit: u32, bearer_secret: Option, ) -> Result { geth_db::validate_db_name(name).map_err(|_| NodeError::InvalidDbName(name.to_owned()))?; let store = Store::open(&node.paths.metadata_db())?; let local = store .get_db_resource_by_name(name)? .ok_or_else(|| NodeError::DbNotFound(name.to_owned()))?; let stream = format!("db:{name}"); let cursor = load_live_sync_cursor(&store, peer_node, &stream)?; let after_db_version = (cursor > 0).then_some(cursor); let response = request_peer_control(node, peer_node, "db-sync", |peer_card, nonce| { PeerControlRequest::DbSync { peer_card, name: name.to_owned(), after_db_version, limit, nonce: nonce.clone(), bearer_proof: bearer_proof( bearer_secret, &format!("resource:db:{name}"), "db.sync", &nonce, ), } }) .await?; match response { PeerControlResponse::DbSynced { node_id, agent_id, endpoint_id, name: response_name, batch, high_water_db_version, allowed, reason, note, .. } if response_name == name => { if !allowed { return Ok(ControlResponse::DbSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, name: response_name, changes_received: 0, changes_applied: 0, max_db_version: None, schema_match: false, allowed, reason, note, }); } let local_schema = geth_db::schema_metadata(Path::new(&local.path))?; let (changes_received, changes_applied, max_db_version, schema_match) = if let Some(batch) = batch { let schema_match = batch.schema_metadata == local_schema; let changes_received = batch.changes.len(); let max_db_version = batch.max_db_version; let changes_applied = if schema_match { geth_db::apply_crsqlite_changes(Path::new(&local.path), &batch)? } else { 0 }; if schema_match { if let Some(next_cursor) = high_water_db_version.or(max_db_version) { store_live_sync_cursor(&store, peer_node, &stream, next_cursor)?; } } ( changes_received, changes_applied, max_db_version, schema_match, ) } else { (0, 0, high_water_db_version, false) }; Ok(ControlResponse::DbSynced { peer_node_id: node_id, peer_agent_id: agent_id, endpoint_id, name: response_name, changes_received, changes_applied, max_db_version, schema_match, allowed, reason, note, }) } PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer( "peer DB sync response did not match request".to_owned(), )), PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)), _ => Err(NodeError::IrohPeer( "peer returned wrong response type to DB sync".to_owned(), )), } } fn live_sync_cursor_key(peer_node: &str, stream: &str) -> String { format!("live-sync:{peer_node}:{stream}") } fn live_sync_status_prefix(peer_node: &str) -> String { format!("live-sync-status:{peer_node}:") } fn live_sync_status_key(peer_node: &str, stream: &str) -> String { format!("{}{}", live_sync_status_prefix(peer_node), stream) } fn load_live_sync_cursor(store: &Store, peer_node: &str, stream: &str) -> Result { let key = live_sync_cursor_key(peer_node, stream); let Some(state) = store.get_module_state(&key)? else { return Ok(0); }; let cursor: LiveSyncCursor = serde_json::from_str(&state.state_json)?; Ok(cursor.cursor_ms) } fn store_live_sync_cursor( store: &Store, peer_node: &str, stream: &str, cursor_ms: i64, ) -> Result<(), NodeError> { store.put_module_state(&StoredModuleState { module: live_sync_cursor_key(peer_node, stream), state_json: serde_json::to_string(&LiveSyncCursor { cursor_ms })?, updated_at_ms: geth_store::now_ms(), })?; Ok(()) } fn record_live_sync_success( store: &Store, peer_node: &str, stream: &str, imported: usize, rejected: usize, cursor_ms: Option, ) -> Result { let now = geth_store::now_ms(); if let Some(cursor_ms) = cursor_ms { store_live_sync_cursor(store, peer_node, stream, cursor_ms)?; } store.put_module_state(&StoredModuleState { module: live_sync_status_key(peer_node, stream), state_json: serde_json::to_string(&LiveSyncHealth { last_attempt_ms: now, last_success_ms: Some(now), last_error: None, last_imported: imported, last_rejected: rejected, })?, updated_at_ms: now, })?; load_live_sync_cursor(store, peer_node, stream) } fn record_live_sync_failure( store: &Store, peer_node: &str, stream: &str, error: &NodeError, ) -> Result { let now = geth_store::now_ms(); let previous = store .get_module_state(&live_sync_status_key(peer_node, stream))? .and_then(|state| serde_json::from_str::(&state.state_json).ok()); store.put_module_state(&StoredModuleState { module: live_sync_status_key(peer_node, stream), state_json: serde_json::to_string(&LiveSyncHealth { last_attempt_ms: now, last_success_ms: previous.and_then(|health| health.last_success_ms), last_error: Some(error.to_string()), last_imported: 0, last_rejected: 0, })?, updated_at_ms: now, })?; load_live_sync_cursor(store, peer_node, stream) } fn sync_status_local(store: &Store) -> Result { let peers = store .list_peer_cards()? .into_iter() .map(|peer| { let prefix = live_sync_status_prefix(&peer.peer_id); let now_ms = geth_store::now_ms(); let mut streams = store .list_module_states_with_prefix(&prefix)? .into_iter() .map(|state| { let stream = state.module.trim_start_matches(&prefix).to_owned(); let health: LiveSyncHealth = serde_json::from_str(&state.state_json)?; let cursor_ms = load_live_sync_cursor(store, &peer.peer_id, &stream)?; let stale = health.last_success_ms.is_some_and(|last_success_ms| { now_ms.saturating_sub(last_success_ms) > LIVE_SYNC_STALE_AFTER_MS }); let status_state = sync_stream_state(&health, stale).to_owned(); let next_action = sync_stream_next_action(&peer.peer_id, &stream, &health, stale); Ok(SyncStreamStatus { stream, cursor_ms, state: status_state, stale, stale_after_ms: LIVE_SYNC_STALE_AFTER_MS, last_attempt_ms: Some(health.last_attempt_ms), last_success_ms: health.last_success_ms, last_error: health.last_error, last_imported: health.last_imported, last_rejected: health.last_rejected, next_action, }) }) .collect::, NodeError>>()?; streams.sort_by(|left, right| left.stream.cmp(&right.stream)); Ok(SyncPeerStatus { peer_node_id: peer.peer_id, streams, }) }) .collect::, NodeError>>()?; Ok(ControlResponse::SyncStatus { peers, note: "sync status is local daemon health for best-effort live sync; signed logs remain the durable source of truth".to_owned(), }) } fn sync_stream_state(health: &LiveSyncHealth, stale: bool) -> &'static str { if health.last_error.is_some() { "failed" } else if health.last_success_ms.is_none() { "never-succeeded" } else if stale { "stale" } else { "ok" } } fn sync_stream_next_action( peer_node: &str, stream: &str, health: &LiveSyncHealth, stale: bool, ) -> String { if health.last_error.is_some() { format!( "run `geth sync now {peer_node}`; if it still fails, run `geth auth explain ` for stream {stream}" ) } else if health.last_success_ms.is_none() { format!( "run `geth sync now {peer_node}` to establish the first successful sync for {stream}" ) } else if stale { format!( "run `geth sync now {peer_node}` and check `geth peer ping {peer_node}` if the stream remains stale" ) } else { "none".to_owned() } } fn should_live_sync_stream( store: &Store, peer_node: &str, stream: &str, remote_watermarks: Option<&BTreeMap>, ) -> Result { let Some(remote_watermarks) = remote_watermarks else { return Ok(true); }; let Some(remote_high_water) = remote_watermarks.get(stream) else { return Ok(false); }; if *remote_high_water == 0 { return Ok(false); } let local_cursor = load_live_sync_cursor(store, peer_node, stream)?; if stream.starts_with("db:") { Ok(*remote_high_water > local_cursor) } else { Ok(*remote_high_water >= local_cursor) } } fn sync_watermarks_for_peer( store: &Store, peer_node: &str, ) -> Result, NodeError> { let mut watermarks = Vec::new(); let keychain_high = load_keychain_ops(store)? .into_iter() .map(|op| op.created_at.0) .chain( load_keychain_signatures(store)? .into_iter() .map(|signature| signature.created_at.0), ) .max() .unwrap_or(0); watermarks.push(SyncWatermark { stream: "keychain".to_owned(), high_water: keychain_high, }); let auth_high = load_auth_ops(store)? .into_iter() .map(|op| op.created_at.0) .chain( load_auth_signatures(store)? .into_iter() .map(|signature| signature.created_at.0), ) .max() .unwrap_or(0); watermarks.push(SyncWatermark { stream: "auth".to_owned(), high_water: auth_high, }); let peer = PrincipalId::new(peer_node.to_owned()); if can_sync_resource(store, &peer, "resource:ssh:certs", "ssh_cert.sync")? { let cert_high = store .list_ssh_cert_requests()? .into_iter() .map(|request| request.created_at_ms) .chain( store .list_ssh_certificates()? .into_iter() .map(|certificate| certificate.imported_at_ms), ) .max() .unwrap_or(0); watermarks.push(SyncWatermark { stream: "ssh-certs".to_owned(), high_water: cert_high, }); } if can_sync_resource( store, &peer, "resource:ssh:revocations", "ssh_revocation.sync", )? { let revocation_high = store .list_ssh_revocations()? .into_iter() .map(|revocation| revocation.created_at_ms) .max() .unwrap_or(0); watermarks.push(SyncWatermark { stream: "ssh-revocations".to_owned(), high_water: revocation_high, }); } for kv in store.list_kv_stores()? { if can_sync_resource(store, &peer, &kv.resource_id, "kv.read")? { let high_water = store .list_kv_entries_since(&kv.kv_id, 0)? .into_iter() .map(|entry| entry.updated_at_ms) .max() .unwrap_or(0); watermarks.push(SyncWatermark { stream: format!("kv:{}", kv.name), high_water, }); } } for document in store.list_document_resources()? { if can_sync_resource(store, &peer, &document.resource_id, "document.read")? { watermarks.push(SyncWatermark { stream: format!("document:{}", document.name), high_water: document.updated_at_ms, }); } } for db in store.list_db_resources()? { if can_sync_resource(store, &peer, &db.resource_id, "db.sync")? { if let Ok(metadata) = geth_db::crsqlite_change_metadata(Path::new(&db.path)) { watermarks.push(SyncWatermark { stream: format!("db:{}", db.name), high_water: metadata.max_db_version.unwrap_or(0), }); } } } for root in store.list_file_roots()? { if root.path.starts_with("remote:") { continue; } let resource = format!("resource:cas-tree:{}", root.name); if can_sync_resource(store, &peer, &resource, "cas.fetch")? { watermarks.push(SyncWatermark { stream: format!("cas-tree:{}", root.name), high_water: root.updated_at_ms, }); } } watermarks.sort_by(|left, right| left.stream.cmp(&right.stream)); Ok(watermarks) } fn can_sync_resource( store: &Store, peer: &PrincipalId, resource: &str, capability: &str, ) -> Result { Ok(geth_auth::explain_auth_ops( &load_auth_ops_for_resource(store, resource)?, peer.clone(), ResourceId::new(resource.to_owned()), Capability::new(capability.to_owned()), ) .allowed) } fn explain_auth_for_operator( store: &Store, subject: &str, resource: &str, capability: &str, ) -> Result { let ops = load_auth_ops_for_resource(store, resource)?; let mut explanation = geth_auth::explain_auth_ops( &ops, PrincipalId::new(subject.to_owned()), ResourceId::new(resource.to_owned()), Capability::new(capability.to_owned()), ); if subject.starts_with("bearer:") { return Ok(explanation); } let peer_card = store.get_peer_card(subject)?; let keychain_view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(store)?); let subject_node = NodeId::new(subject.to_owned()); let trusted_node = keychain_view.nodes.get(&subject_node); match (trusted_node, peer_card) { (None, Some(_)) => { explanation.add_diagnostic("subject:discovered-only"); explanation.add_diagnostic("trust:missing"); if !explanation .reason .contains("discovery does not grant trust or authorization") { explanation.reason = format!( "subject is a discovered peer candidate only; discovery does not grant trust or authorization; {}", explanation.reason ); } } (None, None) => { explanation.add_diagnostic("subject:unknown"); explanation.add_diagnostic("trust:missing"); if !explanation.allowed { explanation.reason = format!( "subject is not present in the keychain and has no discovered peer card; {}", explanation.reason ); } } (Some(node), None) => { explanation.add_diagnostic("subject:trusted-node"); if node.endpoints.is_empty() { explanation.add_diagnostic("endpoint-binding:missing"); } else { explanation.add_diagnostic("endpoint-binding:present"); explanation.add_diagnostic("discovery:missing-peer-card"); } } (Some(node), Some(stored_card)) => { explanation.add_diagnostic("subject:trusted-node"); let card = serde_json::from_str::(&stored_card.card_json).ok(); let matching_endpoint = card.as_ref().and_then(|card| { card.endpoints .iter() .map(|endpoint| endpoint.endpoint_id.as_str()) .find(|endpoint| { keychain_view .endpoints .get(*endpoint) .is_some_and(|bound_node| bound_node == &node.id) }) .map(ToOwned::to_owned) }); if let Some(endpoint) = matching_endpoint { explanation.add_diagnostic(format!("endpoint-binding:matched:{endpoint}")); } else if node.endpoints.is_empty() { explanation.add_diagnostic("endpoint-binding:missing"); if !explanation.allowed { explanation.reason = format!( "trusted node has no active endpoint binding; {}", explanation.reason ); } } else { explanation.add_diagnostic("endpoint-binding:missing-for-peer-card"); if !explanation.allowed { explanation.reason = format!( "discovered peer card has no endpoint candidate bound to the trusted node; {}", explanation.reason ); } } } } Ok(explanation) } fn bearer_proof( bearer_secret: Option, resource: &str, capability: &str, nonce: &str, ) -> Option { bearer_secret.map(|token| { let token = geth_types::SecretId::new(token); let secret = geth_secrets::bearer_id_for_token(&token); let resource = ResourceId::new(resource.to_owned()); let capabilities = vec![Capability::new(capability.to_owned())]; let response = geth_secrets::bearer_response(&token, &resource, &capabilities, nonce); BearerProof { secret, resource, capabilities, nonce: nonce.to_owned(), response, } }) } fn explain_peer_or_bearer( store: &Store, peer_node: &str, resource: &str, capability: &str, nonce: &str, bearer_proof: Option<&BearerProof>, ) -> Result { let ops = load_auth_ops_for_resource(store, resource)?; let resource_id = ResourceId::new(resource.to_owned()); let capability_id = Capability::new(capability.to_owned()); let peer_explanation = geth_auth::explain_auth_ops( &ops, PrincipalId::new(peer_node.to_owned()), resource_id.clone(), capability_id.clone(), ); if peer_explanation.allowed { return Ok(peer_explanation); } let Some(proof) = bearer_proof else { return Ok(peer_explanation); }; let bearer_subject = format!("bearer:{}", proof.secret); let denied = |reason: String| AuthExplanation { subject: bearer_subject.clone(), resource: resource.to_owned(), capability: capability.to_owned(), allowed: false, reason, evaluated_ops: peer_explanation.evaluated_ops, diagnostics: vec!["subject:bearer-secret".to_owned()], }; if proof.resource != resource_id { return Ok(denied(format!( "bearer proof resource {} does not match requested resource {resource}", proof.resource ))); } if proof.nonce != nonce { return Ok(denied( "bearer proof nonce does not match request".to_owned(), )); } if !proof .capabilities .iter() .any(|granted| geth_auth::capability_allows(granted, &capability_id)) { return Ok(denied( "bearer proof does not include the requested capability".to_owned(), )); } let now = UnixMillis(geth_store::now_ms()); let Some(access) = load_bearer_access(store)?.into_iter().find(|access| { access.secret == proof.secret && access.resource == proof.resource && access.expires_at.is_none_or(|expires| expires.0 > now.0) }) else { return Ok(denied( "bearer access is not active for requested resource".to_owned(), )); }; let Some(token_hash) = access.token_hash.as_deref() else { return Ok(denied( "bearer access does not have a token verifier".to_owned(), )); }; if !geth_secrets::verify_bearer_response_with_token_hash( token_hash, &proof.resource, &proof.capabilities, nonce, &proof.response, ) { return Ok(denied("bearer proof response is invalid".to_owned())); } if !access .capabilities .iter() .any(|granted| geth_auth::capability_allows(granted, &capability_id)) { return Ok(denied( "bearer access lacks requested capability".to_owned(), )); } Ok(AuthExplanation { subject: bearer_subject, resource: resource.to_owned(), capability: capability.to_owned(), allowed: true, reason: "bearer proof allows this resource-scoped capability without granting node identity" .to_owned(), evaluated_ops: peer_explanation.evaluated_ops, diagnostics: vec![ "subject:bearer-secret".to_owned(), format!("bearer:active:{}", access.secret), "capability:matched".to_owned(), ], }) } async fn request_peer_control( node: &LocalNode, peer_node: &str, operation: &str, build_request: impl FnOnce(PeerCard, String) -> PeerControlRequest, ) -> Result { let store = Store::open(&node.paths.metadata_db())?; let peer_node = resolve_peer_node_for_control(&store, peer_node); let stored = store .get_peer_card(&peer_node)? .ok_or_else(|| NodeError::PeerNotFound(peer_node.clone()))?; 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)?; ensure_peer_card_matches_endpoint(&peer_card, &candidate.endpoint_id)?; 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{}\0{}", node.node_id, peer_node, operation, geth_store::now_ms() ) .as_bytes(), ); let request = build_request(self_card, nonce.clone()); let conn = endpoint .endpoint() .connect(node_addr, geth_iroh::ALPN_CONTROL) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; 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(16 * 1024 * 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 = geth_control::decode_peer_response(text)?; match &response { PeerControlResponse::SshCertSynced { nonce: response_nonce, .. } | PeerControlResponse::KeychainSynced { nonce: response_nonce, .. } | PeerControlResponse::AuthSynced { nonce: response_nonce, .. } | PeerControlResponse::NodeEnrollmentSubmitted { nonce: response_nonce, .. } | PeerControlResponse::SshRevocationSynced { nonce: response_nonce, .. } | PeerControlResponse::KvSynced { nonce: response_nonce, .. } | PeerControlResponse::PubsubPublished { nonce: response_nonce, .. } | PeerControlResponse::PubsubSubscribed { nonce: response_nonce, .. } | PeerControlResponse::PipeConnected { nonce: response_nonce, .. } | PeerControlResponse::PipeListening { nonce: response_nonce, .. } | PeerControlResponse::CasRootSynced { nonce: response_nonce, .. } | PeerControlResponse::SshProxyConnected { nonce: response_nonce, .. } | PeerControlResponse::DocumentSynced { nonce: response_nonce, .. } | PeerControlResponse::DbSynced { nonce: response_nonce, .. } | PeerControlResponse::SyncStatus { nonce: response_nonce, .. } if response_nonce == &nonce => Ok(response), PeerControlResponse::Error { .. } => Ok(response), _ => Err(NodeError::IrohPeer(format!( "peer {operation} response did not match request" ))), } } async fn request_pipe_wire( node: &LocalNode, peer_node: &str, operation: &str, build_request: impl FnOnce(PeerCard, String) -> PipeWireRequest, ) -> Result { 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)?; ensure_peer_card_matches_endpoint(&peer_card, &candidate.endpoint_id)?; 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{}\0{}", node.node_id, peer_node, operation, geth_store::now_ms() ) .as_bytes(), ); let request = build_request(self_card, nonce.clone()); let conn = endpoint .endpoint() .connect(node_addr, geth_iroh::ALPN_PIPE) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let (mut send, mut recv) = conn .open_bi() .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; send.write_all(geth_control::encode_pipe_wire_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(16 * 1024 * 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 = geth_control::decode_pipe_wire_response(text)?; match &response { PipeWireResponse::Sent { nonce: response_nonce, .. } if response_nonce == &nonce => Ok(response), PipeWireResponse::Error { .. } => Ok(response), _ => Err(NodeError::IrohPeer(format!( "peer {operation} response did not match request" ))), } } 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"); } }); } }); } fn spawn_background_live_sync(node: LocalNode, interval_duration: Duration) { tokio::spawn(async move { if let Err(error) = run_live_sync_once(&node).await { tracing::debug!(%error, "initial live sync tick failed"); } let mut interval = tokio::time::interval(interval_duration); loop { interval.tick().await; if let Err(error) = run_live_sync_once(&node).await { tracing::debug!(%error, "live sync tick failed"); } } }); } async fn run_sync_for_peer(node: &LocalNode, peer_node: &str) -> Result { let kv_stores = Store::open(&node.paths.metadata_db())?.list_kv_stores()?; let documents = Store::open(&node.paths.metadata_db())?.list_document_resources()?; let dbs = Store::open(&node.paths.metadata_db())?.list_db_resources()?; let mut streams = Vec::new(); let remote_watermarks = match sync_status_from_peer(node, peer_node) .await .map(|watermarks| { watermarks .into_iter() .map(|watermark| (watermark.stream, watermark.high_water)) .collect::>() }) { Ok(watermarks) => { let store = Store::open(&node.paths.metadata_db())?; let cursor_ms = record_live_sync_success(&store, peer_node, "sync-status", 0, 0, None)?; streams.push(SyncStreamRun { stream: "sync-status".to_owned(), attempted: true, success: true, imported: 0, rejected: 0, cursor_ms, error: None, }); Some(watermarks) } Err(error) => { let store = Store::open(&node.paths.metadata_db())?; let cursor_ms = record_live_sync_failure(&store, peer_node, "sync-status", &error)?; streams.push(SyncStreamRun { stream: "sync-status".to_owned(), attempted: true, success: false, imported: 0, rejected: 0, cursor_ms, error: Some(error.to_string()), }); None } }; sync_keychain_stream(node, peer_node, remote_watermarks.as_ref(), &mut streams).await?; sync_auth_stream(node, peer_node, remote_watermarks.as_ref(), &mut streams).await?; sync_ssh_cert_stream(node, peer_node, remote_watermarks.as_ref(), &mut streams).await?; sync_ssh_revocation_stream(node, peer_node, remote_watermarks.as_ref(), &mut streams).await?; if let Some(remote_watermarks) = remote_watermarks.as_ref() { for stream in remote_watermarks .keys() .filter(|stream| stream.starts_with("cas-tree:")) { sync_cas_tree_stream( node, peer_node, stream, Some(remote_watermarks), &mut streams, ) .await?; } } for kv in &kv_stores { let stream = format!("kv:{}", kv.name); sync_kv_stream( node, peer_node, &kv.name, &stream, remote_watermarks.as_ref(), &mut streams, ) .await?; } for document in &documents { let stream = format!("document:{}", document.name); sync_document_stream( node, peer_node, &document.name, &stream, remote_watermarks.as_ref(), &mut streams, ) .await?; } for db in &dbs { let stream = format!("db:{}", db.name); sync_db_stream( node, peer_node, &db.name, &stream, remote_watermarks.as_ref(), &mut streams, ) .await?; } Ok(SyncPeerRun { peer_node_id: peer_node.to_owned(), streams, }) } async fn sync_now( node: &LocalNode, requested_peer: Option, ) -> Result { if node .iroh_endpoint .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)? .is_none() { return Err(NodeError::IrohEndpointUnavailable); } let peers = if let Some(peer) = requested_peer { vec![peer] } else { Store::open(&node.paths.metadata_db())? .list_peer_cards()? .into_iter() .map(|peer| peer.peer_id) .collect() }; let mut runs = Vec::new(); for peer in peers { runs.push(run_sync_for_peer(node, &peer).await?); } Ok(ControlResponse::SyncRan { peers: runs, note: "ran best-effort sync over Iroh; each imported keychain/auth operation was still verified from signed operation logs".to_owned(), }) } fn push_skipped_sync_stream( store: &Store, peer_node: &str, stream: &str, streams: &mut Vec, ) -> Result<(), NodeError> { streams.push(SyncStreamRun { stream: stream.to_owned(), attempted: false, success: true, imported: 0, rejected: 0, cursor_ms: load_live_sync_cursor(store, peer_node, stream)?, error: None, }); Ok(()) } fn push_failed_sync_stream( store: &Store, peer_node: &str, stream: &str, error: NodeError, streams: &mut Vec, ) -> Result<(), NodeError> { let cursor_ms = record_live_sync_failure(store, peer_node, stream, &error)?; streams.push(SyncStreamRun { stream: stream.to_owned(), attempted: true, success: false, imported: 0, rejected: 0, cursor_ms, error: Some(error.to_string()), }); Ok(()) } async fn sync_keychain_stream( node: &LocalNode, peer_node: &str, remote_watermarks: Option<&BTreeMap>, streams: &mut Vec, ) -> Result<(), NodeError> { let stream = "keychain"; let store = Store::open(&node.paths.metadata_db())?; if !should_live_sync_stream(&store, peer_node, stream, remote_watermarks)? { return push_skipped_sync_stream(&store, peer_node, stream, streams); } let since_ms = load_live_sync_cursor(&store, peer_node, stream)?; match keychain_sync_from_peer_since(node, peer_node, since_ms).await { Ok(ControlResponse::KeychainSynced { ops_imported, signatures_imported, invalid_ops_rejected, high_water_ms, .. }) => { let store = Store::open(&node.paths.metadata_db())?; let cursor_ms = record_live_sync_success( &store, peer_node, stream, ops_imported + signatures_imported, invalid_ops_rejected, Some(high_water_ms), )?; streams.push(SyncStreamRun { stream: stream.to_owned(), attempted: true, success: true, imported: ops_imported + signatures_imported, rejected: invalid_ops_rejected, cursor_ms, error: None, }); Ok(()) } Ok(other) => push_failed_sync_stream( &store, peer_node, stream, NodeError::IrohPeer(format!("unexpected keychain sync response: {other:?}")), streams, ), Err(error) => push_failed_sync_stream(&store, peer_node, stream, error, streams), } } async fn sync_auth_stream( node: &LocalNode, peer_node: &str, remote_watermarks: Option<&BTreeMap>, streams: &mut Vec, ) -> Result<(), NodeError> { let stream = "auth"; let store = Store::open(&node.paths.metadata_db())?; if !should_live_sync_stream(&store, peer_node, stream, remote_watermarks)? { return push_skipped_sync_stream(&store, peer_node, stream, streams); } let since_ms = load_live_sync_cursor(&store, peer_node, stream)?; match auth_sync_from_peer_since(node, peer_node, since_ms).await { Ok(ControlResponse::AuthSynced { ops_imported, signatures_imported, invalid_ops_rejected, high_water_ms, .. }) => { let store = Store::open(&node.paths.metadata_db())?; let cursor_ms = record_live_sync_success( &store, peer_node, stream, ops_imported + signatures_imported, invalid_ops_rejected, Some(high_water_ms), )?; streams.push(SyncStreamRun { stream: stream.to_owned(), attempted: true, success: true, imported: ops_imported + signatures_imported, rejected: invalid_ops_rejected, cursor_ms, error: None, }); Ok(()) } Ok(other) => push_failed_sync_stream( &store, peer_node, stream, NodeError::IrohPeer(format!("unexpected auth sync response: {other:?}")), streams, ), Err(error) => push_failed_sync_stream(&store, peer_node, stream, error, streams), } } async fn sync_ssh_cert_stream( node: &LocalNode, peer_node: &str, remote_watermarks: Option<&BTreeMap>, streams: &mut Vec, ) -> Result<(), NodeError> { let stream = "ssh-certs"; let store = Store::open(&node.paths.metadata_db())?; if !should_live_sync_stream(&store, peer_node, stream, remote_watermarks)? { return push_skipped_sync_stream(&store, peer_node, stream, streams); } match ssh_cert_sync_from_peer(node, peer_node, None).await { Ok(ControlResponse::SshCertSynced { requests_imported, certificates_imported, .. }) => { let store = Store::open(&node.paths.metadata_db())?; let cursor_ms = record_live_sync_success( &store, peer_node, stream, requests_imported + certificates_imported, 0, None, )?; streams.push(SyncStreamRun { stream: stream.to_owned(), attempted: true, success: true, imported: requests_imported + certificates_imported, rejected: 0, cursor_ms, error: None, }); Ok(()) } Ok(other) => push_failed_sync_stream( &store, peer_node, stream, NodeError::IrohPeer(format!("unexpected SSH cert sync response: {other:?}")), streams, ), Err(error) => push_failed_sync_stream(&store, peer_node, stream, error, streams), } } async fn sync_ssh_revocation_stream( node: &LocalNode, peer_node: &str, remote_watermarks: Option<&BTreeMap>, streams: &mut Vec, ) -> Result<(), NodeError> { let stream = "ssh-revocations"; let store = Store::open(&node.paths.metadata_db())?; if !should_live_sync_stream(&store, peer_node, stream, remote_watermarks)? { return push_skipped_sync_stream(&store, peer_node, stream, streams); } match ssh_revocation_sync_from_peer(node, peer_node, None).await { Ok(ControlResponse::SshRevocationSynced { revocations_imported, .. }) => { let store = Store::open(&node.paths.metadata_db())?; let cursor_ms = record_live_sync_success(&store, peer_node, stream, revocations_imported, 0, None)?; streams.push(SyncStreamRun { stream: stream.to_owned(), attempted: true, success: true, imported: revocations_imported, rejected: 0, cursor_ms, error: None, }); Ok(()) } Ok(other) => push_failed_sync_stream( &store, peer_node, stream, NodeError::IrohPeer(format!( "unexpected SSH revocation sync response: {other:?}" )), streams, ), Err(error) => push_failed_sync_stream(&store, peer_node, stream, error, streams), } } async fn sync_cas_tree_stream( node: &LocalNode, peer_node: &str, stream: &str, remote_watermarks: Option<&BTreeMap>, streams: &mut Vec, ) -> Result<(), NodeError> { let store = Store::open(&node.paths.metadata_db())?; if !should_live_sync_stream(&store, peer_node, stream, remote_watermarks)? { return push_skipped_sync_stream(&store, peer_node, stream, streams); } let name = stream.trim_start_matches("cas-tree:"); match cas_root_sync_from_peer(node, peer_node, name, None).await { Ok(ControlResponse::CasRootSynced { tree_bytes_imported, sync_conflicts, .. }) => { let store = Store::open(&node.paths.metadata_db())?; let cursor = load_live_sync_cursor(&store, peer_node, stream)?; let cursor_ms = record_live_sync_success( &store, peer_node, stream, usize::from(tree_bytes_imported), sync_conflicts.len(), Some(cursor), )?; streams.push(SyncStreamRun { stream: stream.to_owned(), attempted: true, success: true, imported: usize::from(tree_bytes_imported), rejected: sync_conflicts.len(), cursor_ms, error: None, }); Ok(()) } Ok(other) => push_failed_sync_stream( &store, peer_node, stream, NodeError::IrohPeer(format!("unexpected file-root sync response: {other:?}")), streams, ), Err(error) => push_failed_sync_stream(&store, peer_node, stream, error, streams), } } async fn sync_kv_stream( node: &LocalNode, peer_node: &str, name: &str, stream: &str, remote_watermarks: Option<&BTreeMap>, streams: &mut Vec, ) -> Result<(), NodeError> { let store = Store::open(&node.paths.metadata_db())?; if !should_live_sync_stream(&store, peer_node, stream, remote_watermarks)? { return push_skipped_sync_stream(&store, peer_node, stream, streams); } match kv_sync_from_peer(node, peer_node, name, None).await { Ok(ControlResponse::KvSynced { entries_imported, .. }) => { let store = Store::open(&node.paths.metadata_db())?; let cursor = load_live_sync_cursor(&store, peer_node, stream)?; let cursor_ms = record_live_sync_success( &store, peer_node, stream, entries_imported, 0, Some(cursor), )?; streams.push(SyncStreamRun { stream: stream.to_owned(), attempted: true, success: true, imported: entries_imported, rejected: 0, cursor_ms, error: None, }); Ok(()) } Ok(other) => push_failed_sync_stream( &store, peer_node, stream, NodeError::IrohPeer(format!("unexpected KV sync response: {other:?}")), streams, ), Err(error) => push_failed_sync_stream(&store, peer_node, stream, error, streams), } } async fn sync_document_stream( node: &LocalNode, peer_node: &str, name: &str, stream: &str, remote_watermarks: Option<&BTreeMap>, streams: &mut Vec, ) -> Result<(), NodeError> { let store = Store::open(&node.paths.metadata_db())?; if !should_live_sync_stream(&store, peer_node, stream, remote_watermarks)? { return push_skipped_sync_stream(&store, peer_node, stream, streams); } match document_sync_from_peer(node, peer_node, name, None).await { Ok(ControlResponse::DocumentSynced { updated, .. }) => { let store = Store::open(&node.paths.metadata_db())?; let cursor = load_live_sync_cursor(&store, peer_node, stream)?; let cursor_ms = record_live_sync_success( &store, peer_node, stream, usize::from(updated), 0, Some(cursor), )?; streams.push(SyncStreamRun { stream: stream.to_owned(), attempted: true, success: true, imported: usize::from(updated), rejected: 0, cursor_ms, error: None, }); Ok(()) } Ok(other) => push_failed_sync_stream( &store, peer_node, stream, NodeError::IrohPeer(format!("unexpected document sync response: {other:?}")), streams, ), Err(error) => push_failed_sync_stream(&store, peer_node, stream, error, streams), } } async fn sync_db_stream( node: &LocalNode, peer_node: &str, name: &str, stream: &str, remote_watermarks: Option<&BTreeMap>, streams: &mut Vec, ) -> Result<(), NodeError> { let store = Store::open(&node.paths.metadata_db())?; if !should_live_sync_stream(&store, peer_node, stream, remote_watermarks)? { return push_skipped_sync_stream(&store, peer_node, stream, streams); } match db_sync_from_peer(node, peer_node, name, 100, None).await { Ok(ControlResponse::DbSynced { changes_applied, max_db_version, .. }) => { let store = Store::open(&node.paths.metadata_db())?; let cursor = load_live_sync_cursor(&store, peer_node, stream)?; let cursor_ms = record_live_sync_success( &store, peer_node, stream, changes_applied, 0, Some(max_db_version.unwrap_or(cursor)), )?; streams.push(SyncStreamRun { stream: stream.to_owned(), attempted: true, success: true, imported: changes_applied, rejected: 0, cursor_ms, error: None, }); Ok(()) } Ok(other) => push_failed_sync_stream( &store, peer_node, stream, NodeError::IrohPeer(format!("unexpected DB sync response: {other:?}")), streams, ), Err(error) => push_failed_sync_stream(&store, peer_node, stream, error, streams), } } async fn run_live_sync_once(node: &LocalNode) -> Result<(), NodeError> { if node .iroh_endpoint .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)? .is_none() { return Ok(()); } let peers = Store::open(&node.paths.metadata_db())?.list_peer_cards()?; for peer in peers { let run = run_sync_for_peer(node, &peer.peer_id).await?; for stream in run.streams.iter().filter(|stream| !stream.success) { if let Some(error) = &stream.error { tracing::debug!(peer = %peer.peer_id, stream = %stream.stream, %error, "live sync stream failed"); } } } Ok(()) } 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_id().to_string(); let alpn = display_alpn(conn.alpn()); if alpn == display_alpn(iroh_blobs::ALPN) { let blob_store = iroh_blob_store(&node)?.ok_or_else(|| { NodeError::IrohPeer("iroh-blobs ALPN accepted but blob store is unavailable".to_owned()) })?; let protocol = iroh_blobs::BlobsProtocol::new(&blob_store, None); return protocol .accept(conn) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-blobs accept failed: {error}"))); } if alpn == display_alpn(iroh_docs::ALPN) { let docs = iroh_docs(&node)?.ok_or_else(|| { NodeError::IrohPeer( "iroh-docs ALPN accepted but docs runtime is unavailable".to_owned(), ) })?; return docs .accept(conn) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-docs accept failed: {error}"))); } if alpn == display_alpn(iroh_gossip::ALPN) { let gossip = iroh_gossip(&node)?.ok_or_else(|| { NodeError::IrohPeer( "iroh-gossip ALPN accepted but gossip runtime is unavailable".to_owned(), ) })?; return gossip .accept(conn) .await .map_err(|error| NodeError::IrohPeer(format!("iroh-gossip accept failed: {error}"))); } let (mut send, mut recv) = conn .accept_bi() .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; if alpn == display_alpn(geth_iroh::ALPN_SSH_PROXY) { return handle_ssh_proxy_wire_connection(node, remote_endpoint_id, send, recv).await; } if alpn == display_alpn(geth_iroh::ALPN_PIPE) { return handle_pipe_wire_connection(node, remote_endpoint_id, send, recv).await; } 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()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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(), } } PeerControlRequest::SyncStatus { peer_card, nonce } => { peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let watermarks = sync_watermarks_for_peer(&store, peer_card.node_id.as_str())?; PeerControlResponse::SyncStatus { 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, watermarks, nonce, note: "sync status authenticated endpoint/card binding and returns only streams for capabilities already granted to the caller".to_owned(), } } PeerControlRequest::KeychainSync { peer_card, since_ms, nonce, } => { peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let ops = load_keychain_ops(&store)?; let signatures = load_keychain_signatures(&store)?; let returned_op_ids = ops .iter() .filter(|op| op.created_at.0 >= since_ms) .map(|op| op.id.to_string()) .collect::>(); let high_water_ms = ops .iter() .map(|op| op.created_at.0) .chain(signatures.iter().map(|signature| signature.created_at.0)) .max() .unwrap_or(0); PeerControlResponse::KeychainSynced { 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, ops: ops .into_iter() .filter(|op| op.created_at.0 >= since_ms) .collect(), signatures: signatures .into_iter() .filter(|signature| { signature.created_at.0 >= since_ms || returned_op_ids.contains(signature.op_id.as_str()) }) .collect(), high_water_ms, nonce, note: "keychain sync returns signed operation-log data; receiver must verify OpenSSH signatures before import".to_owned(), } } PeerControlRequest::AuthSync { peer_card, since_ms, nonce, } => { peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let ops = load_auth_ops(&store)?; let signatures = load_auth_signatures(&store)?; let returned_op_ids = ops .iter() .filter(|op| op.created_at.0 >= since_ms) .map(|op| op.id.to_string()) .collect::>(); let high_water_ms = ops .iter() .map(|op| op.created_at.0) .chain(signatures.iter().map(|signature| signature.created_at.0)) .max() .unwrap_or(0); PeerControlResponse::AuthSynced { 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, ops: ops .into_iter() .filter(|op| op.created_at.0 >= since_ms) .collect(), signatures: signatures .into_iter() .filter(|signature| { signature.created_at.0 >= since_ms || returned_op_ids.contains(signature.op_id.as_str()) }) .collect(), high_water_ms, nonce, note: "auth sync returns signed resource operation-log data; receiver must verify admin signatures before import".to_owned(), } } PeerControlRequest::NodeEnrollmentSubmit { peer_card, request, nonce, } => { peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let accepted = insert_node_enrollment_request_if_not_conflicting(&store, &request)?; PeerControlResponse::NodeEnrollmentSubmitted { 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, request_id: request.id.to_string(), accepted, nonce, note: "stored signed node enrollment request for owner review".to_owned(), } } PeerControlRequest::AuthCheck { peer_card, resource, capability, nonce, } => { peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let explanation = geth_auth::explain_auth_ops( &load_auth_ops_for_resource(&store, &resource)?, PrincipalId::new(peer_card.node_id.to_string()), ResourceId::new(resource.clone()), Capability::new(capability.clone()), ); PeerControlResponse::AuthChecked { 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, resource, capability, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "protected peer request authenticated endpoint/card binding before resource capability evaluation".to_owned(), } } PeerControlRequest::CasFetch { peer_card, hash, nonce, bearer_proof, } => { peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = "resource:cas:local".to_owned(); let capability = "cas.fetch".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; if explanation.allowed { match LocalCas::new(node.paths.cas_dir()).read_bytes(&hash) { Ok(content) => { let size_bytes = content.len() as u64; PeerControlResponse::CasFetched { 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, hash, size_bytes, content_base64: Some( base64::engine::general_purpose::STANDARD.encode(content), ), allowed: true, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "CAS fetch authenticated endpoint/card binding and required cas.fetch on resource:cas:local".to_owned(), } } Err(error) => PeerControlResponse::Error { message: format!("CAS blob {hash} is not available: {error}"), }, } } else { PeerControlResponse::CasFetched { 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, hash, size_bytes: 0, content_base64: None, allowed: false, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "CAS fetch authenticated endpoint/card binding and required cas.fetch on resource:cas:local".to_owned(), } } } PeerControlRequest::CasRootSync { peer_card, name, nonce, bearer_proof, } => { geth_cas::validate_file_root_name(&name)?; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = format!("resource:cas-tree:{name}"); let capability = "cas.fetch".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; let (root, tree_content_base64) = if explanation.allowed { if let Some(root) = store.get_file_root_by_name(&name)? { let root_view = file_root_from_stored(&root); let tree_content_base64 = root .latest_tree_hash .as_ref() .map(|hash| -> Result, NodeError> { let hash = BlobHash::new(hash.clone()); let content = LocalCas::new(node.paths.cas_dir()).read_bytes(&hash)?; Ok(Some( base64::engine::general_purpose::STANDARD.encode(content), )) }) .transpose()? .flatten(); (Some(root_view), tree_content_base64) } else { (None, None) } } else { (None, None) }; PeerControlResponse::CasRootSynced { 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, name, root, tree_content_base64, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "file-root sync authenticated endpoint/card binding and required cas.fetch on the remote cas-tree resource; it imports tree metadata and CAS tree bytes but never writes files".to_owned(), } } PeerControlRequest::SshCertSync { peer_card, since_ms, nonce, bearer_proof, } => { peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = "resource:ssh:certs".to_owned(); let capability = "ssh_cert.sync".to_owned(); let high_water_ms = geth_store::now_ms(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; let (requests, certificates, log_entries) = if explanation.allowed { ( store .list_ssh_cert_requests_since(since_ms)? .into_iter() .map(ssh_cert_request_from_stored) .collect::, _>>()?, store .list_ssh_certificates_since(since_ms)? .into_iter() .map(ssh_certificate_from_stored) .collect::, _>>()?, Vec::new(), ) } else { (Vec::new(), Vec::new(), Vec::new()) }; let log_entries = if explanation.allowed { ssh_cert_distribution_log_entries(requests.clone(), certificates.clone()) } else { log_entries }; PeerControlResponse::SshCertSynced { 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, requests, certificates, log_entries, high_water_ms, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "SSH certificate metadata sync authenticated endpoint/card binding and required ssh_cert.sync on resource:ssh:certs".to_owned(), } } PeerControlRequest::SshRevocationSync { peer_card, since_ms, nonce, bearer_proof, } => { peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = "resource:ssh:revocations".to_owned(); let capability = "ssh_revocation.sync".to_owned(); let high_water_ms = geth_store::now_ms(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; let (revocations, log_entries) = if explanation.allowed { store .list_ssh_revocations_since(since_ms)? .into_iter() .map(ssh_revocation_from_stored) .collect::, _>>() .map(|revocations| { let log_entries = ssh_revocation_distribution_log_entries(revocations.clone()); (revocations, log_entries) })? } else { (Vec::new(), Vec::new()) }; PeerControlResponse::SshRevocationSynced { 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, revocations, log_entries, high_water_ms, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "SSH revocation sync authenticated endpoint/card binding and required ssh_revocation.sync on resource:ssh:revocations".to_owned(), } } PeerControlRequest::KvSync { peer_card, name, since_ms, nonce, bearer_proof, } => { geth_kv::validate_kv_name(&name).map_err(|_| NodeError::InvalidKvName(name.clone()))?; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; if let Some(kv) = store.get_kv_store_by_name(&name)? { let capability = "kv.read".to_owned(); let high_water_ms = geth_store::now_ms(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &kv.resource_id, &capability, &nonce, bearer_proof.as_ref(), )?; let entries = if explanation.allowed { store .list_kv_entries_since(&kv.kv_id, since_ms)? .into_iter() .map(|entry| KvSyncEntry { key: entry.key, value: entry.value, updated_at_ms: entry.updated_at_ms, }) .collect() } else { Vec::new() }; let docs_ticket = if explanation.allowed { drop(store); mirror_kv_store_to_iroh_docs(&node, &name).await?; kv_docs_read_ticket(&node, &name).await? } else { None }; PeerControlResponse::KvSynced { 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, name, entries, high_water_ms, docs_ticket, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "KV sync authenticated endpoint/card binding and required kv.read on the remote KV resource".to_owned(), } } else { PeerControlResponse::Error { message: format!("kv store not found: {name}"), } } } PeerControlRequest::PubsubPublish { peer_card, topic, message, nonce, bearer_proof, } => { geth_pubsub::validate_topic(&topic)?; geth_pubsub::validate_message(&message)?; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = format!("resource:pubsub:{topic}"); let capability = "pubsub.publish".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; drop(store); let published = if explanation.allowed { let bootstrap_peer = remote_endpoint_id .parse::() .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let published = record_pubsub_message(&node, topic.clone(), message.clone())?; broadcast_pubsub_gossip( &node, topic.as_str(), message.as_str(), vec![bootstrap_peer], ) .await?; Some(published) } else { None }; PeerControlResponse::PubsubPublished { 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, message: published, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "pubsub publish authenticated endpoint/card binding and required pubsub.publish on the remote topic resource; pubsub is lossy".to_owned(), } } PeerControlRequest::PubsubSubscribe { peer_card, topic, nonce, bearer_proof, } => { geth_pubsub::validate_topic(&topic)?; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = format!("resource:pubsub:{topic}"); let capability = "pubsub.subscribe".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; drop(store); let messages = if explanation.allowed { let bootstrap_peer = remote_endpoint_id .parse::() .map_err(|error| NodeError::IrohPeer(error.to_string()))?; ensure_pubsub_gossip_topic(&node, topic.as_str(), vec![bootstrap_peer]).await?; pubsub_messages_for_topic(&node, &topic)? } else { Vec::new() }; PeerControlResponse::PubsubSubscribed { 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, topic, messages, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "pubsub subscribe authenticated endpoint/card binding and required pubsub.subscribe on the remote topic resource; pubsub is lossy daemon-lifetime state".to_owned(), } } PeerControlRequest::PipeConnect { peer_card, target, nonce, bearer_proof, } => { geth_pipe::validate_pipe_name(&target)?; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = format!("resource:pipe:{target}"); let capability = "pipe.connect".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; let connection = if explanation.allowed { Some(record_pipe_connection( &node, target, "remote pipe connect over protected Iroh control path; byte messages use the dedicated Iroh pipe ALPN".to_owned(), )?) } else { None }; PeerControlResponse::PipeConnected { 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, connection, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "pipe connect authenticated endpoint/card binding and required pipe.connect on the remote pipe resource; byte messages use the dedicated Iroh pipe ALPN".to_owned(), } } PeerControlRequest::PipeListen { peer_card, name, nonce, bearer_proof, } => { geth_pipe::validate_pipe_name(&name)?; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = format!("resource:pipe:{name}"); let capability = "pipe.listen".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; let listener = if explanation.allowed { Some(record_pipe_listener(&node, name)?) } else { None }; PeerControlResponse::PipeListening { 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, listener, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "pipe listen authenticated endpoint/card binding and required pipe.listen on the remote pipe resource; listeners receive daemon-lifetime byte messages".to_owned(), } } PeerControlRequest::SshProxyConnect { peer_card, nonce, bearer_proof, } => { peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = "resource:ssh-proxy:local".to_owned(); let capability = "ssh_proxy.connect".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; let connection = if explanation.allowed { Some(SshProxyConnection { target_node: NodeId::new(node.node_id.clone()), connected_at: UnixMillis(geth_store::now_ms()), local_sshd_target: Some("127.0.0.1:22".to_owned()), admin_shell_available: true, note: "authorized SSH proxy metadata check passed; run geth ssh proxy for the dedicated byte stream".to_owned(), }) } else { None }; PeerControlResponse::SshProxyConnected { 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, connection, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "SSH proxy authenticated endpoint/card binding and required ssh_proxy.connect on resource:ssh-proxy:local; SSH is not a geth transport, and byte proxying uses the dedicated /geth/ssh-proxy/1 stream".to_owned(), } } PeerControlRequest::SshAdminShell { peer_card, command, nonce, bearer_proof, } => { peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = "resource:ssh-proxy:local".to_owned(); let capability = "ssh_proxy.admin_shell".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; let output = if explanation.allowed { restricted_admin_shell_output(&node, &command)? } else { String::new() }; PeerControlResponse::SshAdminShellOutput { 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, command, output, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "restricted geth admin shell authenticated endpoint/card binding and required ssh_proxy.admin_shell; no host shell commands are executed".to_owned(), } } PeerControlRequest::DocumentSync { peer_card, name, since_ms, nonce, bearer_proof, } => { geth_document::validate_document_name(&name) .map_err(|_| NodeError::InvalidDocumentName(name.clone()))?; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; if let Some(document) = store.get_document_resource_by_name(&name)? { let capability = "document.read".to_owned(); let high_water_ms = geth_store::now_ms(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &document.resource_id, &capability, &nonce, bearer_proof.as_ref(), )?; let state = if explanation.allowed && document.updated_at_ms >= since_ms { Some(document_sync_state_from_stored(&document)?) } else { None }; PeerControlResponse::DocumentSynced { 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, name, state, high_water_ms, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "document sync authenticated endpoint/card binding and required document.read on the remote document resource; durable Automerge state is exchanged with a JSON view for CLI output".to_owned(), } } else { PeerControlResponse::Error { message: format!("document not found: {name}"), } } } PeerControlRequest::DbSync { peer_card, name, after_db_version, limit, nonce, bearer_proof, } => { geth_db::validate_db_name(&name).map_err(|_| NodeError::InvalidDbName(name.clone()))?; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; if let Some(db) = store.get_db_resource_by_name(&name)? { let capability = "db.sync".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &db.resource_id, &capability, &nonce, bearer_proof.as_ref(), )?; let sync_result: Result<_, String> = if explanation.allowed { let path = Path::new(&db.path); match geth_db::extract_crsqlite_changes(path, after_db_version, limit) { Ok(batch) => match geth_db::crsqlite_change_metadata(path) { Ok(metadata) => { let high_water_db_version = metadata.max_db_version.or(batch.max_db_version); Ok((Some(batch), high_water_db_version)) } Err(error) => Err(format!( "db sync cannot read crsql_changes metadata for {name}: {error}" )), }, Err(error) => Err(format!( "db sync cannot read crsql_changes for {name}: {error}" )), } } else { Ok((None, None)) }; match sync_result { Ok((batch, high_water_db_version)) => PeerControlResponse::DbSynced { 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, name, batch, high_water_db_version, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "DB sync authenticated endpoint/card binding and required db.sync on the remote DB resource; bootstrap exchanges typed crsql_changes and applies compatible batches through local crsql_changes".to_owned(), }, Err(message) => PeerControlResponse::Error { message }, } } else { PeerControlResponse::Error { message: format!("db resource not found: {name}"), } } } }; 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(()) } async fn handle_ssh_proxy_wire_connection( node: LocalNode, remote_endpoint_id: String, mut send: iroh::endpoint::SendStream, recv: iroh::endpoint::RecvStream, ) -> Result<(), NodeError> { let mut reader = BufReader::new(recv); let mut line = String::new(); reader .read_line(&mut line) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let request = geth_control::decode_peer_request(&line)?; let PeerControlRequest::SshProxyConnect { peer_card, nonce, bearer_proof, } = request else { let response = PeerControlResponse::Error { message: "SSH proxy stream expected ssh-proxy-connect handshake".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()))?; return Ok(()); }; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?; 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, })?; let resource = "resource:ssh-proxy:local".to_owned(); let capability = "ssh_proxy.connect".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; let connection = if explanation.allowed { Some(SshProxyConnection { target_node: node.node_id.clone().into(), connected_at: UnixMillis(geth_store::now_ms()), local_sshd_target: Some("127.0.0.1:22".to_owned()), admin_shell_available: true, note: "authorized SSH proxy byte stream over Iroh; SSH is not a geth transport" .to_owned(), }) } else { None }; if !explanation.allowed { let response = PeerControlResponse::SshProxyConnected { 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, connection, allowed: false, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "SSH proxy denied before opening sshd; SSH is not a geth transport".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()))?; return Ok(()); } let tcp = match TcpStream::connect("127.0.0.1:22").await { Ok(tcp) => tcp, Err(error) => { let response = PeerControlResponse::Error { message: format!("authorized SSH proxy could not connect to 127.0.0.1:22: {error}"), }; 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()))?; return Ok(()); } }; let response = PeerControlResponse::SshProxyConnected { 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, connection, allowed: true, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "SSH proxy authenticated endpoint/card binding and required ssh_proxy.connect before connecting to local sshd; SSH is not a geth transport".to_owned(), }; send.write_all(geth_control::encode_peer_response(&response)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let mut recv = reader.into_inner(); let (mut tcp_read, mut tcp_write) = tcp.into_split(); let inbound = async { tokio::io::copy(&mut recv, &mut tcp_write) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; tcp_write.shutdown().await.map_err(NodeError::from) }; let outbound = async { tokio::io::copy(&mut tcp_read, &mut send) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; send.finish() .map_err(|error| NodeError::IrohPeer(error.to_string())) }; tokio::try_join!(inbound, outbound)?; Ok(()) } async fn handle_pipe_wire_connection( node: LocalNode, remote_endpoint_id: String, mut send: iroh::endpoint::SendStream, recv: iroh::endpoint::RecvStream, ) -> Result<(), NodeError> { let mut reader = BufReader::new(recv); let mut line = String::new(); reader .read_line(&mut line) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; match geth_control::decode_pipe_wire_request(&line)? { PipeWireRequest::Send { peer_card, target, data_base64, nonce, bearer_proof, } => { let response = handle_pipe_send_wire_request( &node, &remote_endpoint_id, peer_card, target, data_base64, nonce, bearer_proof, )?; send.write_all(geth_control::encode_pipe_wire_response(&response)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; send.finish() .map_err(|error| NodeError::IrohPeer(error.to_string()))?; Ok(()) } PipeWireRequest::TcpConnect { peer_card, target_addr, nonce, bearer_proof, } => { handle_pipe_tcp_wire_connection( node, &remote_endpoint_id, PipeTcpConnectWire { peer_card, target_addr, nonce, bearer_proof, }, send, reader.into_inner(), ) .await } PipeWireRequest::UnixConnect { peer_card, target_path, nonce, bearer_proof, } => { handle_pipe_unix_wire_connection( node, &remote_endpoint_id, PipeUnixConnectWire { peer_card, target_path, nonce, bearer_proof, }, send, reader.into_inner(), ) .await } } } fn handle_pipe_send_wire_request( node: &LocalNode, remote_endpoint_id: &str, peer_card: PeerCard, target: String, data_base64: String, nonce: String, bearer_proof: Option, ) -> Result { geth_pipe::validate_pipe_name(&target)?; base64::engine::general_purpose::STANDARD .decode(&data_base64) .map_err(|error| NodeError::IrohPeer(format!("invalid pipe payload: {error}")))?; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, remote_endpoint_id)?; 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, })?; let resource = format!("resource:pipe:{target}"); let capability = "pipe.connect".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; let (message, listener_found) = if explanation.allowed { record_pipe_message( node, target, data_base64, Some(peer_card.node_id.to_string()), "remote pipe byte message over dedicated Iroh pipe ALPN".to_owned(), )? } else { (None, false) }; Ok(PipeWireResponse::Sent { 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: remote_endpoint_id.to_owned(), message: Box::new(message), listener_found, allowed: explanation.allowed, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "pipe send authenticated endpoint/card binding and required pipe.connect on the remote pipe resource; payload used the dedicated Iroh pipe ALPN".to_owned(), }) } async fn handle_pipe_tcp_wire_connection( node: LocalNode, remote_endpoint_id: &str, request: PipeTcpConnectWire, mut send: iroh::endpoint::SendStream, recv: iroh::endpoint::RecvStream, ) -> Result<(), NodeError> { let PipeTcpConnectWire { peer_card, target_addr, nonce, bearer_proof, } = request; let target = geth_pipe::validate_tcp_forward_target_addr(&target_addr)?; peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, remote_endpoint_id)?; 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, })?; let resource = format!("resource:pipe-tcp:{target_addr}"); let capability = "pipe.forward".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; if !explanation.allowed { let response = PipeWireResponse::Connected { 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: remote_endpoint_id.to_owned(), connection: None, allowed: false, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "pipe TCP forward denied before opening the remote TCP target".to_owned(), }; send.write_all(geth_control::encode_pipe_wire_response(&response)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; return send .finish() .map_err(|error| NodeError::IrohPeer(error.to_string())); } let tcp = match TcpStream::connect(target).await { Ok(tcp) => tcp, Err(error) => { let response = PipeWireResponse::Error { message: format!( "authorized pipe TCP forward could not connect to {target_addr}: {error}" ), }; send.write_all(geth_control::encode_pipe_wire_response(&response)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; return send .finish() .map_err(|error| NodeError::IrohPeer(error.to_string())); } }; let response = PipeWireResponse::Connected { 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: remote_endpoint_id.to_owned(), connection: Some(PipeConnection { target: target_addr.clone(), connected_at: UnixMillis(geth_store::now_ms()), local_listener_found: true, note: "authorized TCP byte stream over the dedicated Iroh pipe ALPN".to_owned(), }), allowed: true, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "pipe TCP forward authenticated endpoint/card binding and required pipe.forward before connecting to the loopback TCP target".to_owned(), }; send.write_all(geth_control::encode_pipe_wire_response(&response)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let (mut tcp_read, mut tcp_write) = tcp.into_split(); let mut recv = recv; let inbound = async { tokio::io::copy(&mut recv, &mut tcp_write) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; tcp_write.shutdown().await.map_err(NodeError::from) }; let outbound = async { tokio::io::copy(&mut tcp_read, &mut send) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; send.finish() .map_err(|error| NodeError::IrohPeer(error.to_string())) }; tokio::try_join!(inbound, outbound)?; Ok(()) } async fn handle_pipe_unix_wire_connection( node: LocalNode, remote_endpoint_id: &str, request: PipeUnixConnectWire, mut send: iroh::endpoint::SendStream, recv: iroh::endpoint::RecvStream, ) -> Result<(), NodeError> { let PipeUnixConnectWire { peer_card, target_path, nonce, bearer_proof, } = request; let target_path = geth_pipe::validate_unix_forward_path(&target_path)?; let target_display = target_path.display().to_string(); peer_card.validate_candidate()?; ensure_peer_card_matches_endpoint(&peer_card, remote_endpoint_id)?; 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, })?; let resource = format!("resource:pipe-unix:{target_display}"); let capability = "pipe.forward".to_owned(); let explanation = explain_peer_or_bearer( &store, peer_card.node_id.as_str(), &resource, &capability, &nonce, bearer_proof.as_ref(), )?; if !explanation.allowed { let response = PipeWireResponse::Connected { 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: remote_endpoint_id.to_owned(), connection: None, allowed: false, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "pipe Unix forward denied before opening the remote Unix socket".to_owned(), }; send.write_all(geth_control::encode_pipe_wire_response(&response)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; return send .finish() .map_err(|error| NodeError::IrohPeer(error.to_string())); } let unix = match UnixStream::connect(&target_path).await { Ok(unix) => unix, Err(error) => { let response = PipeWireResponse::Error { message: format!( "authorized pipe Unix forward could not connect to {}: {error}", target_path.display() ), }; send.write_all(geth_control::encode_pipe_wire_response(&response)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; return send .finish() .map_err(|error| NodeError::IrohPeer(error.to_string())); } }; let response = PipeWireResponse::Connected { 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: remote_endpoint_id.to_owned(), connection: Some(PipeConnection { target: target_display.clone(), connected_at: UnixMillis(geth_store::now_ms()), local_listener_found: true, note: "authorized Unix socket byte stream over the dedicated Iroh pipe ALPN".to_owned(), }), allowed: true, reason: explanation.reason, evaluated_ops: explanation.evaluated_ops, nonce, note: "pipe Unix forward authenticated endpoint/card binding and required pipe.forward before connecting to the Unix socket target".to_owned(), }; send.write_all(geth_control::encode_pipe_wire_response(&response)?.as_bytes()) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let (mut unix_read, mut unix_write) = unix.into_split(); let mut recv = recv; let inbound = async { tokio::io::copy(&mut recv, &mut unix_write) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; unix_write.shutdown().await.map_err(NodeError::from) }; let outbound = async { tokio::io::copy(&mut unix_read, &mut send) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; send.finish() .map_err(|error| NodeError::IrohPeer(error.to_string())) }; tokio::try_join!(inbound, outbound)?; Ok(()) } fn iroh_node_addr_from_candidate( candidate: &EndpointCandidate, ) -> Result { let endpoint_id = candidate .endpoint_id .parse::() .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let direct_addresses = candidate .direct_addresses .iter() .map(|addr| { addr.parse::() .map_err(|error| NodeError::IrohPeer(error.to_string())) }) .collect::, _>>()?; let mut node_addr = iroh::EndpointAddr::new(endpoint_id); for address in direct_addresses { node_addr = node_addr.with_ip_addr(address); } if let Some(relay_url) = &candidate.relay_url { node_addr = node_addr.with_relay_url( relay_url .parse::() .map_err(|error| NodeError::IrohPeer(error.to_string()))?, ); } Ok(node_addr) } fn ensure_peer_card_matches_endpoint(card: &PeerCard, endpoint_id: &str) -> Result<(), NodeError> { if card .endpoints .iter() .any(|candidate| candidate.endpoint_id == endpoint_id) { Ok(()) } else { Err(NodeError::IrohPeer(format!( "signed peer card for {} does not bind Iroh endpoint {}", card.node_id, endpoint_id ))) } } fn peer_gossip_endpoint_id( node: &LocalNode, peer_node: &str, ) -> Result { let store = Store::open(&node.paths.metadata_db())?; let peer_node = resolve_peer_node_for_control(&store, peer_node); let stored = store .get_peer_card(&peer_node)? .ok_or_else(|| NodeError::PeerNotFound(peer_node.clone()))?; 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)?; ensure_peer_card_matches_endpoint(&peer_card, &candidate.endpoint_id)?; candidate .endpoint_id .parse::() .map_err(|error| NodeError::IrohPeer(error.to_string())) } fn restricted_admin_shell_output(node: &LocalNode, command: &str) -> Result { match command { "help" => Ok("commands: help, status, node-id".to_owned()), "node-id" => Ok(node.node_id.clone()), "status" => Ok(format!( "node_id={}\nagent_id={}\nendpoint_id={}\nhome={}", node.node_id, node.agent_id, node.iroh_status .endpoint_id .as_deref() .unwrap_or("unavailable"), node.paths.home().display() )), other => Err(NodeError::IrohPeer(format!( "unsupported restricted admin shell command {other:?}; supported commands: help, status, node-id" ))), } } fn display_alpn(alpn: &[u8]) -> String { String::from_utf8_lossy(alpn).into_owned() } pub fn handle_request( node: &LocalNode, request: ControlRequest, ) -> Result { let store = Store::open(&node.paths.metadata_db())?; match request { ControlRequest::Status => Ok(ControlResponse::Status(StatusResponse { home: node.paths.home().to_path_buf(), socket: node.paths.socket_path(), agent_id: node.agent_id.clone(), node_id: node.node_id.clone(), iroh_enabled: node.iroh_status.enabled, endpoint_id: node.iroh_status.endpoint_id.clone(), iroh_relay_mode: node.iroh_status.relay_mode.clone(), iroh_local_discovery: node.iroh_status.local_discovery, iroh: node.iroh_status.note.clone(), native_backends: native_backend_statuses(), })), ControlRequest::NodeId => Ok(ControlResponse::NodeId(NodeIdResponse { agent_id: node.agent_id.clone(), node_id: node.node_id.clone(), endpoint_id: node.iroh_status.endpoint_id.clone(), })), ControlRequest::PeerCardExport { out } => { let endpoint_id = node .iroh_status .endpoint_id .clone() .ok_or(NodeError::IrohEndpointUnavailable)?; let key = AgentKey::load(&node.paths.agent_key())?; let card = PeerCard::signed( NodeId::new(node.node_id.clone()), &key, vec![EndpointCandidate { endpoint_id, relay_url: None, direct_addresses: Vec::new(), source: DiscoverySource::Manual, }], UnixMillis(geth_store::now_ms()), )?; 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(), }) } ControlRequest::PeerCardImport { path } => { let card_json = std::fs::read_to_string(&path)?; let card: PeerCard = serde_json::from_str(&card_json)?; card.validate_candidate()?; let discovered = DiscoveredPeer::candidate( card.clone(), UnixMillis(geth_store::now_ms()), DiscoverySource::Imported, )?; store.upsert_peer_card(&StoredPeerCard { peer_id: card.node_id.to_string(), card_json: serde_json::to_string(&card)?, updated_at_ms: discovered.discovered_at.0, })?; Ok(ControlResponse::PeerCardImported { peer: discovered, note: discovery_is_untrusted_note().to_owned(), }) } ControlRequest::PeerCardList => { let peers = store .list_peer_cards()? .into_iter() .map(discovered_peer_from_stored) .collect::, _>>()?; Ok(ControlResponse::PeerCardList { peers, note: discovery_is_untrusted_note().to_owned(), }) } ControlRequest::SyncStatus => sync_status_local(&store), ControlRequest::PeerPing { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::PeerAuthCheck { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::SyncNow { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::CasFetch { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::CasRootSync { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::SshCertSync { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::SshRevocationSync { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::KvSync { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::DbSync { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::DocumentSync { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::ResourceList => Ok(ControlResponse::ResourceList { resources: store .list_resources()? .into_iter() .map(stored_resource_to_descriptor) .collect::, _>>()?, }), ControlRequest::ResourceCreate { kind, name } => { let kind = kind .parse::() .map_err(|_| NodeError::InvalidResourceKind(kind.clone()))?; let id = format!("resource:{}:{}", kind, name); let stored = StoredResource { resource_id: id, kind: kind.to_string(), name, status: "active".to_owned(), }; store.insert_resource(&stored)?; Ok(ControlResponse::ResourceCreated { resource: stored_resource_to_descriptor(stored)?, }) } ControlRequest::OverlayStatus => Ok(ControlResponse::OverlayStatus { networks: Vec::new(), note: geth_overlay::overlay_status_note().to_owned(), }), ControlRequest::OverlayPlan { name, cidr } => { let plan = geth_overlay::plan_overlay( &name, cidr.as_deref(), &String::from_utf8_lossy(geth_iroh::ALPN_OVERLAY), )?; Ok(ControlResponse::OverlayPlanned { plan }) } ControlRequest::OverlayJoin { name, secret, cidr } => { geth_overlay::validate_overlay_secret(&secret)?; let plan = geth_overlay::plan_overlay( &name, cidr.as_deref(), &String::from_utf8_lossy(geth_iroh::ALPN_OVERLAY), )?; Ok(ControlResponse::OverlayJoined { join: geth_overlay::OverlayJoinPlan { plan, enabled: false, note: "overlay join is recorded as an implementation plan only; this prototype does not create a TUN/Wintun interface or route packets".to_owned(), }, }) } ControlRequest::OverlayLeave { name } => { geth_overlay::validate_overlay_name(&name)?; Ok(ControlResponse::OverlayLeft { name, stopped: false, note: "no overlay packet runtime is active in this prototype".to_owned(), }) } ControlRequest::CasAdd { path } => { let cas = LocalCas::new(node.paths.cas_dir()); let info = cas.add_path(&path)?; store.record_cas_object( info.hash.as_str(), info.size_bytes, &info.path.to_string_lossy(), )?; Ok(ControlResponse::CasAdded { hash: info.hash, size_bytes: info.size_bytes, }) } ControlRequest::CasAddPrivate { resource, path } => { ensure_resource_exists(&store, &resource)?; let secret = store.latest_resource_secret(&resource)?.ok_or_else(|| { NodeError::IrohPeer(format!( "resource {resource} has no active resource secret; run geth secret create {resource}" )) })?; let plaintext = std::fs::read(&path)?; let plaintext_hash = geth_cas::hash_bytes(&plaintext); let nonce = geth_crypto::blake3_hex( format!( "{}\0{}\0{}\0{}", resource, secret.secret_id, secret.epoch, geth_store::now_ms() ) .as_bytes(), ); let encrypted = geth_cas::encrypt_private_blob( &resource, &secret.secret_id, secret.epoch, &nonce, &plaintext, )?; let cas = LocalCas::new(node.paths.cas_dir()); let info = cas.add_bytes(&encrypted)?; store.record_cas_object( info.hash.as_str(), info.size_bytes, &info.path.to_string_lossy(), )?; Ok(ControlResponse::CasPrivateAdded { resource, epoch: secret.epoch, plaintext_hash, encrypted_hash: info.hash, size_bytes: info.size_bytes, note: "stored prototype encrypted private blob envelope; no forward secrecy or post-compromise security is claimed".to_owned(), }) } ControlRequest::CasGet { hash, out } => { let cas = LocalCas::new(node.paths.cas_dir()); let size_bytes = cas.get_to_path(&hash, &out)?; Ok(ControlResponse::CasGot { hash, out, size_bytes, }) } ControlRequest::CasGetPrivate { resource, hash, out, } => { ensure_resource_exists(&store, &resource)?; let cas = LocalCas::new(node.paths.cas_dir()); let encrypted = cas.read_bytes(&hash)?; let secrets = store .list_resource_secrets()? .into_iter() .filter(|secret| secret.resource_id == resource) .collect::>(); if secrets.is_empty() { return Err(NodeError::IrohPeer(format!( "resource {resource} has no resource secrets; cannot decrypt private blob" ))); } let mut plaintext = None; for secret in secrets { if let Ok(bytes) = geth_cas::decrypt_private_blob(&resource, &secret.secret_id, &encrypted) { plaintext = Some(bytes); break; } } let plaintext = plaintext.ok_or_else(|| { NodeError::IrohPeer(format!( "no local resource secret epoch could decrypt private blob {hash}" )) })?; if let Some(parent) = out.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(&out, &plaintext)?; Ok(ControlResponse::CasPrivateGot { resource, hash, plaintext_hash: geth_cas::hash_bytes(&plaintext), out, size_bytes: plaintext.len() as u64, note: "decrypted prototype private blob envelope with the latest local resource secret epoch".to_owned(), }) } ControlRequest::CasHash { path } => Ok(ControlResponse::CasHash { hash: hash_path(&path)?, }), ControlRequest::CasHas { hash } => { let cas = LocalCas::new(node.paths.cas_dir()); let present = cas.has(&hash)?; Ok(ControlResponse::CasHas { hash, present }) } ControlRequest::CasPin { hash } => { let cas = LocalCas::new(node.paths.cas_dir()); if !cas.has(&hash)? { return Err(NodeError::Cas(geth_cas::CasError::NotFound( hash.to_string(), ))); } store.pin_cas_object(hash.as_str())?; Ok(ControlResponse::CasPinned { hash, pinned: true }) } ControlRequest::CasUnpin { hash } => { geth_cas::validate_hash(&hash)?; store.unpin_cas_object(hash.as_str())?; Ok(ControlResponse::CasPinned { hash, pinned: false, }) } ControlRequest::CasCleanup { dry_run } => { let cas = LocalCas::new(node.paths.cas_dir()); let mut removed = Vec::new(); let mut retained_pinned = Vec::new(); for blob in cas.list()? { if store.is_cas_object_pinned(blob.hash.as_str())? { retained_pinned.push(blob.hash); continue; } if !dry_run && cas.remove(&blob.hash)? { store.delete_cas_object(blob.hash.as_str())?; } removed.push(blob.hash); } Ok(ControlResponse::CasCleanup { removed, retained_pinned, dry_run, }) } ControlRequest::CasProviders { hash } => { geth_cas::validate_hash(&hash)?; Ok(ControlResponse::CasProviders { providers: store .list_cas_providers(hash.as_str())? .into_iter() .map(|provider| CasProvider { peer_node_id: provider.peer_node_id, endpoint_id: provider.endpoint_id, last_seen_ms: provider.last_seen_ms, }) .collect(), hash, }) } ControlRequest::CasList => { let cas = LocalCas::new(node.paths.cas_dir()); let blobs = cas .list()? .into_iter() .map(|blob| { Ok(CasBlob { pinned: store.is_cas_object_pinned(blob.hash.as_str())?, hash: blob.hash, size_bytes: blob.size_bytes, }) }) .collect::, NodeError>>()?; Ok(ControlResponse::CasList { blobs }) } ControlRequest::CasRootAdd { name, path } => { geth_cas::validate_file_root_name(&name)?; if !path.is_dir() { return Err(NodeError::Cas(geth_cas::CasError::TreeRootNotDirectory( path.display().to_string(), ))); } let path = std::fs::canonicalize(path)?; let resource_id = format!("resource:cas-tree:{name}"); store.insert_resource(&StoredResource { resource_id: resource_id.clone(), kind: ResourceKind::Cas.to_string(), name: format!("file-root:{name}"), status: "active".to_owned(), })?; let stored = StoredFileRoot { root_id: format!("file-root:{name}"), resource_id, name, path: path.display().to_string(), latest_tree_hash: None, latest_tree_json: None, updated_at_ms: geth_store::now_ms(), }; store.upsert_file_root(&stored)?; Ok(ControlResponse::CasRootAdded { root: file_root_from_stored(&stored), }) } ControlRequest::CasRootList => Ok(ControlResponse::CasRootList { roots: store .list_file_roots()? .iter() .map(file_root_from_stored) .collect(), }), ControlRequest::CasRootScan { name } => { geth_cas::validate_file_root_name(&name)?; let mut stored = store .get_file_root_by_name(&name)? .ok_or_else(|| NodeError::ResourceNotFound(format!("file-root:{name}")))?; let previous_tree = stored .latest_tree_json .as_deref() .map(serde_json::from_str) .transpose()?; let cas = LocalCas::new(node.paths.cas_dir()); let scanned = cas.add_tree_path(Path::new(&stored.path))?; store.record_cas_object( scanned.object.hash.as_str(), scanned.object.size_bytes, &scanned.object.path.to_string_lossy(), )?; let changes = geth_cas::diff_tree_objects(previous_tree.as_ref(), &scanned.tree); stored.latest_tree_hash = Some(scanned.object.hash.to_string()); stored.latest_tree_json = Some(serde_json::to_string(&scanned.tree)?); stored.updated_at_ms = geth_store::now_ms(); store.upsert_file_root(&stored)?; Ok(ControlResponse::CasRootScanned { scan: FileRootScan { root: file_root_from_stored(&stored), tree: BlobInfoSummary { hash: scanned.object.hash, size_bytes: scanned.object.size_bytes, }, changes, note: geth_cas::file_root_scan_note().to_owned(), }, }) } ControlRequest::CasRootApply { source, target, dry_run, } => apply_file_root_tree(&store, node, &source, target, dry_run), ControlRequest::CasConflictRecord { root, path, kind, detail, base_tree, local_tree, remote_tree, } => { geth_cas::validate_file_root_name(&root)?; if let Some(hash) = &base_tree { geth_cas::validate_hash(hash)?; } if let Some(hash) = &local_tree { geth_cas::validate_hash(hash)?; } if let Some(hash) = &remote_tree { geth_cas::validate_hash(hash)?; } let root_record = store .get_file_root_by_name(&root)? .ok_or_else(|| NodeError::ResourceNotFound(format!("file-root:{root}")))?; let kind = FileConflictKind::parse(&kind)?; let created_at = geth_store::now_ms(); let conflict_id = generated_file_conflict_id(&root, &path, kind.as_str(), created_at); let conflict = StoredFileConflict { conflict_id, root_name: root, resource_id: root_record.resource_id, path, kind: kind.as_str().to_owned(), status: FileConflictStatus::Open.as_str().to_owned(), base_tree_hash: base_tree.map(|hash| hash.to_string()), local_tree_hash: local_tree.map(|hash| hash.to_string()), remote_tree_hash: remote_tree.map(|hash| hash.to_string()), detail, resolution: None, resolution_note: None, created_at_ms: created_at, resolved_at_ms: None, }; store.upsert_file_conflict(&conflict)?; Ok(ControlResponse::CasConflictRecorded { conflict: file_conflict_from_stored(conflict)?, }) } ControlRequest::CasConflictList { root } => { if let Some(root) = root.as_deref() { geth_cas::validate_file_root_name(root)?; } let conflicts = store .list_file_conflicts(root.as_deref())? .into_iter() .map(file_conflict_from_stored) .collect::, _>>()?; Ok(ControlResponse::CasConflictList { conflicts }) } ControlRequest::CasConflictResolve { conflict_id, resolution, note, } => { let resolution = FileConflictResolution::parse(&resolution)?; let mut conflict = store .get_file_conflict(&conflict_id)? .ok_or_else(|| NodeError::ResourceNotFound(conflict_id.clone()))?; conflict.status = FileConflictStatus::Resolved.as_str().to_owned(); conflict.resolution = Some(resolution.as_str().to_owned()); conflict.resolution_note = note; conflict.resolved_at_ms = Some(geth_store::now_ms()); store.upsert_file_conflict(&conflict)?; Ok(ControlResponse::CasConflictResolved { conflict: file_conflict_from_stored(conflict)?, }) } ControlRequest::KeychainInit { admin_key_path, signing_key_path, } => { let mut ops = Vec::new(); let created_at = UnixMillis(geth_store::now_ms()); let init = KeychainOp { id: generated_keychain_op_id("keychain-init", "local", created_at), created_at, kind: KeychainOpKind::KeychainInit, }; store_keychain_op(&store, &init)?; ops.push(init); if let Some(admin_key_path) = admin_key_path.as_ref() { let public_key = std::fs::read_to_string(admin_key_path)?; let created_at = UnixMillis(geth_store::now_ms()); let admin_key = KeyId::new(ssh_public_key_fingerprint(&public_key)); let op = KeychainOp { id: generated_keychain_op_id("admin-key-add", admin_key.as_str(), created_at), created_at, kind: KeychainOpKind::AdminKeyAdd { key: admin_key }, }; store_keychain_op(&store, &op)?; ops.push(op); } let signatures = store_and_sign_keychain_ops( &store, node, &ops, signing_key_path.as_deref(), admin_key_path.as_deref(), )?; Ok(ControlResponse::KeychainInitialized { ops, signatures }) } ControlRequest::KeychainStatus => { let view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(&store)?); let signature_status = verify_keychain_signatures(&store, node)?; Ok(ControlResponse::KeychainStatus(KeychainStatusResponse { initialized: view.initialized, admin_keys: view.admin_keys.len(), signatures: signature_status.total, verified_signatures: signature_status.verified, failed_signatures: signature_status.failed, users: view.users.len(), devices: view.devices.len(), nodes: view.nodes.len(), })) } ControlRequest::NodeList => { let view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(&store)?); Ok(ControlResponse::NodeList { nodes: view.nodes.into_values().collect(), note: "nodes are reduced from the signed keychain operation log; revoked devices and nodes are omitted".to_owned(), }) } ControlRequest::NodeRename { node: target, name, signing_key_path, } => { let signing_key_path = signing_key_path .ok_or_else(|| NodeError::SigningKeyRequired("node rename".to_owned()))?; let target = resolve_keychain_node(&store, &target)?; let created_at = UnixMillis(geth_store::now_ms()); let op = KeychainOp { id: generated_keychain_op_id("node-rename", target.as_str(), created_at), created_at, kind: KeychainOpKind::NodeRename { node: target, name }, }; let signatures = store_and_sign_keychain_ops( &store, node, std::slice::from_ref(&op), Some(signing_key_path.as_path()), None, )?; Ok(ControlResponse::NodeKeychainUpdated { ops: vec![op], signatures, note: "recorded signed node rename as a keychain operation".to_owned(), }) } ControlRequest::NodeRevoke { node: target, signing_key_path, } => { let signing_key_path = signing_key_path .ok_or_else(|| NodeError::SigningKeyRequired("node revoke".to_owned()))?; let target = resolve_keychain_node(&store, &target)?; let created_at = UnixMillis(geth_store::now_ms()); let op = KeychainOp { id: generated_keychain_op_id("node-revoke", target.as_str(), created_at), created_at, kind: KeychainOpKind::NodeRevoke { node: target }, }; let signatures = store_and_sign_keychain_ops( &store, node, std::slice::from_ref(&op), Some(signing_key_path.as_path()), None, )?; Ok(ControlResponse::NodeKeychainUpdated { ops: vec![op], signatures, note: "recorded node revocation as a keychain operation; replicated peers will omit the node after verified keychain sync".to_owned(), }) } ControlRequest::NodeGrant { node: target, resource, capability, grant_id, signing_key_path, admin_key_path, } => { let target = resolve_keychain_node(&store, &target)?; let op = node_grant_op(target.as_str(), &resource, &capability, grant_id); let signatures = if let Some(signing_key_path) = signing_key_path { store_and_sign_auth_ops( &store, node, std::slice::from_ref(&op), &signing_key_path, admin_key_path.as_deref(), )? } else { store_auth_op(&store, &op)?; Vec::new() }; Ok(ControlResponse::NodeGrantUpdated { op, signatures, note: "recorded signed resource-scoped node capability grant; auth explain can show the grant path".to_owned(), }) } ControlRequest::NodeRevokeGrant { resource, grant_id, signing_key_path, admin_key_path, } => { let created_at = UnixMillis(geth_store::now_ms()); let op = AuthOp { id: generated_auth_op_id("grant-revoke", &resource, &grant_id, created_at), resource: ResourceId::new(resource), created_at, kind: AuthOpKind::GrantRevoke { grant_id }, }; let signatures = if let Some(signing_key_path) = signing_key_path { store_and_sign_auth_ops( &store, node, std::slice::from_ref(&op), &signing_key_path, admin_key_path.as_deref(), )? } else { store_auth_op(&store, &op)?; Vec::new() }; Ok(ControlResponse::NodeGrantUpdated { op, signatures, note: "recorded signed capability grant revocation".to_owned(), }) } ControlRequest::NodeEndpointAdd { node: target, endpoint, signing_key_path, } => { let signing_key_path = signing_key_path .ok_or_else(|| NodeError::SigningKeyRequired("node endpoint add".to_owned()))?; let target = resolve_keychain_node(&store, &target)?; let created_at = UnixMillis(geth_store::now_ms()); let op = KeychainOp { id: generated_keychain_op_id("node-endpoint-add", &endpoint, created_at), created_at, kind: KeychainOpKind::NodeEndpointAdd { node: target, endpoint, }, }; let signatures = store_and_sign_keychain_ops( &store, node, std::slice::from_ref(&op), Some(signing_key_path.as_path()), None, )?; Ok(ControlResponse::NodeKeychainUpdated { ops: vec![op], signatures, note: "recorded signed node endpoint binding".to_owned(), }) } ControlRequest::NodeEndpointRevoke { node: target, endpoint, signing_key_path, } => { let signing_key_path = signing_key_path .ok_or_else(|| NodeError::SigningKeyRequired("node endpoint revoke".to_owned()))?; let target = resolve_keychain_node(&store, &target)?; let created_at = UnixMillis(geth_store::now_ms()); let op = KeychainOp { id: generated_keychain_op_id("node-endpoint-revoke", &endpoint, created_at), created_at, kind: KeychainOpKind::NodeEndpointRevoke { node: target, endpoint, }, }; let signatures = store_and_sign_keychain_ops( &store, node, std::slice::from_ref(&op), Some(signing_key_path.as_path()), None, )?; Ok(ControlResponse::NodeKeychainUpdated { ops: vec![op], signatures, note: "recorded signed node endpoint revocation".to_owned(), }) } ControlRequest::NodeEnrollRequest { node_name, capabilities, reason, out, } => { let mut request = create_node_enrollment_request(node, node_name, capabilities, reason)?; sign_node_enrollment_request(node, &mut request)?; store_node_enrollment_request(&store, &request)?; if let Some(path) = out.as_ref() { write_node_enrollment_request_file(path, &request)?; } Ok(ControlResponse::NodeEnrollmentRequested { request, out, note: "created signed node enrollment request; submit it to an owner node or move the JSON file to the owner machine".to_owned(), }) } ControlRequest::NodeEnrollImport { path } => { let request = read_node_enrollment_request_file(&path)?; insert_node_enrollment_request_if_not_conflicting(&store, &request)?; Ok(ControlResponse::NodeEnrollmentImported { request, note: "imported signed node enrollment request for owner review".to_owned(), }) } ControlRequest::NodeEnrollList { status } => { let requests = load_node_enrollment_requests(&store)? .into_iter() .filter(|request| { status .as_ref() .is_none_or(|status| request.status.as_str() == status) }) .collect(); Ok(ControlResponse::NodeEnrollmentList { requests, note: "node enrollment requests are signed by the requesting agent; approve on an owner machine with an admin signing key".to_owned(), }) } ControlRequest::NodeEnrollApprove { request_id, signing_key_path, admin_key_path, node_name, capabilities, } => approve_node_enrollment( &store, node, &request_id, &signing_key_path, admin_key_path.as_deref(), node_name, capabilities, ), ControlRequest::SecretStatus => Ok(ControlResponse::SecretStatus { secrets: store .list_resource_secrets()? .into_iter() .map(resource_secret_from_stored) .collect(), }), ControlRequest::SecretCreate { resource } => { ensure_resource_exists(&store, &resource)?; let secret = create_resource_secret(&store, &resource, 1)?; Ok(ControlResponse::SecretCreated { secret }) } ControlRequest::SecretRotate { resource } => { ensure_resource_exists(&store, &resource)?; let next_epoch = store .latest_resource_secret(&resource)? .map(|secret| secret.epoch + 1) .unwrap_or(1); let secret = create_resource_secret(&store, &resource, next_epoch)?; Ok(ControlResponse::SecretCreated { secret }) } ControlRequest::SecretBearerCreate { resource, capabilities, expires_at_ms, } => { ensure_resource_exists(&store, &resource)?; let capabilities = capabilities .into_iter() .map(Capability::new) .collect::>(); geth_secrets::validate_bearer_capabilities(&capabilities)?; let created_at = UnixMillis(geth_store::now_ms()); let token = geth_types::SecretId::new(format!( "gbt_{}", geth_crypto::blake3_hex( format!( "{}\0{resource}\0{}\0{}", node.agent_id, capabilities .iter() .map(ToString::to_string) .collect::>() .join(","), created_at.0 ) .as_bytes() ) )); let secret = geth_secrets::bearer_id_for_token(&token); let token_hash = geth_secrets::bearer_token_hash(&token); let access = BearerAccess::resource_scoped( secret.clone(), ResourceId::new(resource.clone()), capabilities.clone(), ); let op = AuthOp { id: generated_auth_op_id("bearer-create", &resource, secret.as_str(), created_at), resource: ResourceId::new(resource), created_at, kind: AuthOpKind::BearerAccessCreate { secret, token_hash: Some(token_hash), capabilities, expires_at: expires_at_ms.map(UnixMillis), }, }; store_auth_op(&store, &op)?; Ok(ControlResponse::SecretBearerCreated { access: BearerAccess { token: Some(token), expires_at: expires_at_ms.map(UnixMillis), ..access }, }) } ControlRequest::SecretBearerList => Ok(ControlResponse::SecretBearerList { access: load_bearer_access(&store)?, }), ControlRequest::SecretBearerChallenge { resource, capabilities, } => { ensure_resource_exists(&store, &resource)?; let capabilities = capabilities .into_iter() .map(Capability::new) .collect::>(); geth_secrets::validate_bearer_capabilities(&capabilities)?; let issued_at = UnixMillis(geth_store::now_ms()); let nonce = geth_crypto::blake3_hex( format!("{}\0{}\0{}", node.agent_id, resource, issued_at.0).as_bytes(), ); Ok(ControlResponse::SecretBearerChallenge { challenge: BearerChallenge { resource: ResourceId::new(resource), capabilities, nonce, issued_at, }, }) } ControlRequest::SecretBearerProve { secret, resource, capabilities, nonce, } => { ensure_resource_exists(&store, &resource)?; let capabilities = capabilities .into_iter() .map(Capability::new) .collect::>(); geth_secrets::validate_bearer_capabilities(&capabilities)?; let token = geth_types::SecretId::new(secret); let secret = geth_secrets::bearer_id_for_token(&token); let resource = ResourceId::new(resource); let response = geth_secrets::bearer_response(&token, &resource, &capabilities, &nonce); Ok(ControlResponse::SecretBearerProof { proof: BearerProof { secret, resource, capabilities, nonce, response, }, }) } ControlRequest::SecretBearerVerify { secret, resource, capabilities, nonce, response, } => { ensure_resource_exists(&store, &resource)?; let requested_capabilities = capabilities .iter() .cloned() .map(Capability::new) .collect::>(); geth_secrets::validate_bearer_capabilities(&requested_capabilities)?; let token = geth_types::SecretId::new(secret.clone()); let secret_id = geth_secrets::bearer_id_for_token(&token); let resource_id = ResourceId::new(resource.clone()); let access = load_bearer_access(&store)?.into_iter().find(|access| { access.secret == secret_id && access.resource == resource_id && access .expires_at .is_none_or(|expires_at| expires_at.0 >= geth_store::now_ms()) }); let (verified, reason) = if let Some(access) = access { let has_capabilities = requested_capabilities .iter() .all(|capability| access.capabilities.contains(capability)); if !has_capabilities { ( false, "bearer secret lacks requested capabilities".to_owned(), ) } else if access.token_hash.as_deref().is_some_and(|token_hash| { geth_secrets::verify_bearer_response_with_token_hash( token_hash, &resource_id, &requested_capabilities, &nonce, &response, ) }) { ( true, "bearer proof verified for resource-scoped capabilities".to_owned(), ) } else { ( false, "bearer proof response did not match challenge".to_owned(), ) } } else { (false, "bearer access not found or expired".to_owned()) }; Ok(ControlResponse::SecretBearerVerified { secret, resource, capabilities, verified, reason, }) } ControlRequest::SecretBearerRevoke { resource, secret } => { ensure_resource_exists(&store, &resource)?; let created_at = UnixMillis(geth_store::now_ms()); let op = AuthOp { id: generated_auth_op_id("bearer-revoke", &resource, &secret, created_at), resource: ResourceId::new(resource.clone()), created_at, kind: AuthOpKind::BearerAccessRevoke { secret: secret.clone().into(), }, }; store_auth_op(&store, &op)?; Ok(ControlResponse::SecretBearerRevoked { resource, secret }) } ControlRequest::AuthExplain { subject, resource, capability, } => Ok(ControlResponse::AuthExplain(explain_auth_for_operator( &store, &subject, &resource, &capability, )?)), ControlRequest::AuthGrant { subject, resource, capability, grant_id, signing_key_path, admin_key_path, } => { let created_at = UnixMillis(geth_store::now_ms()); let grant_id = grant_id.unwrap_or_else(|| generated_grant_id(&subject, &resource, &capability)); let op = AuthOp { id: generated_auth_op_id("grant-create", &resource, &grant_id, created_at), resource: ResourceId::new(resource), created_at, kind: AuthOpKind::GrantCreate { grant_id, principal: PrincipalId::new(subject), capabilities: vec![Capability::new(capability)], }, }; let signatures = if let Some(signing_key_path) = signing_key_path { store_and_sign_auth_ops( &store, node, std::slice::from_ref(&op), &signing_key_path, admin_key_path.as_deref(), )? } else { store_auth_op(&store, &op)?; Vec::new() }; Ok(ControlResponse::AuthOpRecorded { op, signatures }) } ControlRequest::AuthRevoke { resource, grant_id, signing_key_path, admin_key_path, } => { let created_at = UnixMillis(geth_store::now_ms()); let op = AuthOp { id: generated_auth_op_id("grant-revoke", &resource, &grant_id, created_at), resource: ResourceId::new(resource), created_at, kind: AuthOpKind::GrantRevoke { grant_id }, }; let signatures = if let Some(signing_key_path) = signing_key_path { store_and_sign_auth_ops( &store, node, std::slice::from_ref(&op), &signing_key_path, admin_key_path.as_deref(), )? } else { store_auth_op(&store, &op)?; Vec::new() }; Ok(ControlResponse::AuthOpRecorded { op, signatures }) } ControlRequest::SshCertRequest { public_key_path, cert_kind, principals, requested_validity, renewal_of, reason, subject, } => { ensure_subject_authorized( &store, node, subject.as_deref(), "resource:ssh:certs", "ssh_cert.request", )?; if principals.is_empty() { return Err(NodeError::MissingSshCertPrincipal); } let cert_kind = cert_kind .parse::() .map_err(|_| NodeError::InvalidSshCertKind(cert_kind.clone()))?; let public_key = std::fs::read_to_string(&public_key_path)?; let created_at = UnixMillis(geth_store::now_ms()); let mut request = SshCertRequest { id: cert_request_id( &NodeId::new(node.node_id.clone()), &public_key, &principals, created_at, ), requester_node: NodeId::new(node.node_id.clone()), public_key_fingerprint: ssh_public_key_fingerprint(&public_key), public_key, cert_kind, principals, requested_validity, renewal_of: renewal_of.map(SshCertId::new), reason, status: SshCertRequestStatus::Pending, created_at, provenance: None, }; sign_ssh_cert_request(node, &mut request)?; store.insert_ssh_cert_request(&stored_from_ssh_cert_request(&request))?; Ok(ControlResponse::SshCertRequested { request }) } ControlRequest::SshCertRequests { subject } => { ensure_subject_authorized( &store, node, subject.as_deref(), "resource:ssh:certs", "ssh_cert.read", )?; Ok(ControlResponse::SshCertRequests { requests: store .list_ssh_cert_requests()? .into_iter() .map(ssh_cert_request_from_stored) .collect::, _>>()?, }) } ControlRequest::SshCertApprove { request_id, ca_key_path, valid_for, serial, out, sign, subject, } => { ensure_subject_authorized( &store, node, subject.as_deref(), "resource:ssh:certs", "ssh_cert.approve", )?; let stored = store .get_ssh_cert_request(&request_id)? .ok_or_else(|| NodeError::SshCertRequestNotFound(request_id.clone()))?; let mut request = ssh_cert_request_from_stored(stored)?; request.status = SshCertRequestStatus::Approved; sign_ssh_cert_request(node, &mut request)?; store.insert_ssh_cert_request(&stored_from_ssh_cert_request(&request))?; let public_key_path = out.clone().unwrap_or_else(|| { node.paths .home() .join("ssh-cert-requests") .join(format!("{}.pub", request.id)) }); if let Some(parent) = public_key_path.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(&public_key_path, &request.public_key)?; let valid_for = valid_for .or_else(|| request.requested_validity.clone()) .unwrap_or_else(|| "+52w".to_owned()); let signing_command = build_ssh_cert_sign_command( &request, &ca_key_path, &public_key_path, &valid_for, serial, )?; let expected_certificate_path = expected_openssh_cert_path(&public_key_path); let mut signed = false; let mut certificate_id_value = None; let mut note = "request approved; run the signing command on the CA/YubiKey machine, then import the resulting -cert.pub file".to_owned(); if sign { geth_ssh_identity::ensure_ssh_keygen_available()?; let output = std::process::Command::new(&signing_command[0]) .args(&signing_command[1..]) .output()?; if !output.status.success() { return Err(geth_ssh_identity::SshIdentityError::SshKeygenFailed( String::from_utf8_lossy(&output.stderr).trim().to_owned(), ) .into()); } let certificate = std::fs::read_to_string(&expected_certificate_path)?; let mut record = SshCertificateRecord { id: certificate_id(&certificate), request_id: request.id.clone(), certificate_fingerprint: ssh_public_key_fingerprint(&certificate), certificate, imported_at: UnixMillis(geth_store::now_ms()), provenance: None, }; sign_ssh_certificate(node, &mut record)?; store.insert_ssh_certificate(&stored_from_ssh_certificate(&record))?; request.status = SshCertRequestStatus::Signed; sign_ssh_cert_request(node, &mut request)?; store.insert_ssh_cert_request(&stored_from_ssh_cert_request(&request))?; signed = true; certificate_id_value = Some(record.id); note = "request approved, signed with ssh-keygen, and imported into local certificate metadata".to_owned(); } let approval = SshCertApproval { request_id: request.id, approved_by_node: NodeId::new(node.node_id.clone()), ca_key_path: ca_key_path.display().to_string(), key_id: request_id, valid_for, serial, output_path: Some(expected_certificate_path), signing_command, signed, certificate_id: certificate_id_value, note, }; Ok(ControlResponse::SshCertApproved { approval }) } ControlRequest::SshCertImport { request_id, cert_path, subject, } => { ensure_subject_authorized( &store, node, subject.as_deref(), "resource:ssh:certs", "ssh_cert.import", )?; let certificate = std::fs::read_to_string(&cert_path)?; let mut record = SshCertificateRecord { id: certificate_id(&certificate), request_id: SshCertRequestId::new(request_id.clone()), certificate_fingerprint: ssh_public_key_fingerprint(&certificate), certificate, imported_at: UnixMillis(geth_store::now_ms()), provenance: None, }; sign_ssh_certificate(node, &mut record)?; store.insert_ssh_certificate(&stored_from_ssh_certificate(&record))?; if let Some(stored) = store.get_ssh_cert_request(&request_id)? { let mut request = ssh_cert_request_from_stored(stored)?; request.status = SshCertRequestStatus::Signed; sign_ssh_cert_request(node, &mut request)?; store.insert_ssh_cert_request(&stored_from_ssh_cert_request(&request))?; } Ok(ControlResponse::SshCertImported { certificate: record, }) } ControlRequest::SshCertList { subject } => { ensure_subject_authorized( &store, node, subject.as_deref(), "resource:ssh:certs", "ssh_cert.read", )?; Ok(ControlResponse::SshCertList { requests: store .list_ssh_cert_requests()? .into_iter() .map(ssh_cert_request_from_stored) .collect::, _>>()?, certificates: store .list_ssh_certificates()? .into_iter() .map(ssh_certificate_from_stored) .collect::, _>>()?, }) } ControlRequest::SshRevocationAdd { kind, target, reason, subject, } => { ensure_subject_authorized( &store, node, subject.as_deref(), "resource:ssh:revocations", "ssh_revocation.publish", )?; let kind = kind .parse::() .map_err(|_| NodeError::InvalidSshRevocationKind(kind.clone()))?; let created_at = UnixMillis(geth_store::now_ms()); let mut revocation = SshRevocationEntry { id: revocation_id(&kind, &target, created_at), kind, target, reason, created_at, published: true, provenance: None, }; sign_ssh_revocation(node, &mut revocation)?; store.insert_ssh_revocation(&stored_from_ssh_revocation(&revocation))?; Ok(ControlResponse::SshRevocationAdded { revocation }) } ControlRequest::SshRevocationList { subject } => { ensure_subject_authorized( &store, node, subject.as_deref(), "resource:ssh:revocations", "ssh_revocation.read", )?; Ok(ControlResponse::SshRevocationList { revocations: store .list_ssh_revocations()? .into_iter() .map(ssh_revocation_from_stored) .collect::, _>>()?, }) } ControlRequest::SshRevocationExport { out, format, ca_public, subject, } => { ensure_subject_authorized( &store, node, subject.as_deref(), "resource:ssh:revocations", "ssh_revocation.read", )?; let revocations = store .list_ssh_revocations()? .into_iter() .map(ssh_revocation_from_stored) .collect::, _>>()?; let format = format .parse::() .map_err(NodeError::SshIdentity)?; if let Some(parent) = out.parent() { std::fs::create_dir_all(parent)?; } let (body, note) = match format { SshRevocationExportFormat::Jsonl => { let mut body = String::new(); for revocation in &revocations { body.push_str(&serde_json::to_string(revocation)?); body.push('\n'); } ( body, "JSONL geth revocation metadata; not an OpenSSH KRL binary".to_owned(), ) } SshRevocationExportFormat::OpenSshKrlSpec => ( openssh_krl_spec(&revocations)?, "OpenSSH KRL specification; generate a binary KRL with ssh-keygen -k -f [-s ] ".to_owned(), ), SshRevocationExportFormat::OpenSshKrl => { write_openssh_krl(&revocations, &out, ca_public.as_deref())?; ( String::new(), "OpenSSH binary KRL generated with ssh-keygen; use ssh-keygen -Q -f to query it".to_owned(), ) } }; if format != SshRevocationExportFormat::OpenSshKrl { std::fs::write(&out, body)?; } Ok(ControlResponse::SshRevocationExported { out, format: format.to_string(), count: revocations.len(), note, }) } ControlRequest::SshRevocationImport { path, format, subject, } => { ensure_subject_authorized( &store, node, subject.as_deref(), "resource:ssh:revocations", "ssh_revocation.import", )?; let body = std::fs::read_to_string(&path)?; let created_at = UnixMillis(geth_store::now_ms()); let mut revocations = match format.as_str() { "jsonl" => body .lines() .filter(|line| !line.trim().is_empty()) .map(|line| serde_json::from_str(line).map_err(NodeError::from)) .collect::, NodeError>>()?, "openssh-krl-spec" | "krl-spec" => parse_openssh_krl_spec(&body)? .into_iter() .enumerate() .map(|(index, (kind, target))| { let created_at = UnixMillis(created_at.0 + index as i64); SshRevocationEntry { id: revocation_id(&kind, &target, created_at), kind, target, reason: Some(format!("imported from {}", path.display())), created_at, published: true, provenance: None, } }) .collect(), "openssh-krl" | "krl" => { return Err(NodeError::SshIdentity( geth_ssh_identity::SshIdentityError::BinaryKrlImportUnsupported, )); } _ => { return Err(NodeError::SshIdentity( geth_ssh_identity::SshIdentityError::InvalidRevocationExportFormat( format.clone(), ), )); } }; for revocation in &mut revocations { sign_ssh_revocation(node, revocation)?; } for revocation in &revocations { store.insert_ssh_revocation(&stored_from_ssh_revocation(revocation))?; } Ok(ControlResponse::SshRevocationImported { count: revocations.len(), revocations, format, note: "imported revocation metadata; binary OpenSSH KRL files cannot be enumerated, import JSONL or the KRL spec source instead".to_owned(), }) } ControlRequest::DbAdd { name, path } => { geth_db::validate_db_name(&name).map_err(|_| NodeError::InvalidDbName(name.clone()))?; if !path.is_file() { return Err(NodeError::InvalidDbPath(path.display().to_string())); } let path = std::fs::canonicalize(path)?; let schema_metadata = geth_db::schema_metadata(&path)?; let crsqlite_changes = geth_db::crsqlite_change_metadata(&path)?; let resource_id = format!("resource:db:{name}"); let db_id = format!("db:{name}"); let resource = StoredResource { resource_id: resource_id.clone(), kind: ResourceKind::Db.to_string(), name: name.clone(), status: "active".to_owned(), }; store.insert_resource(&resource)?; let stored = StoredDbResource { db_id, resource_id, name, path: path.display().to_string(), }; store.insert_db_resource(&stored)?; Ok(ControlResponse::DbAdded { db: db_resource_from_stored_with_schema(&stored, schema_metadata, crsqlite_changes), }) } ControlRequest::DbStatus { name } => { geth_db::validate_db_name(&name).map_err(|_| NodeError::InvalidDbName(name.clone()))?; let stored = store .get_db_resource_by_name(&name)? .ok_or_else(|| NodeError::DbNotFound(name.clone()))?; Ok(ControlResponse::DbStatus { db: db_resource_from_stored(&stored)?, }) } ControlRequest::DbChanges { name, after_db_version, limit, } => { geth_db::validate_db_name(&name).map_err(|_| NodeError::InvalidDbName(name.clone()))?; let stored = store .get_db_resource_by_name(&name)? .ok_or_else(|| NodeError::DbNotFound(name.clone()))?; let batch = geth_db::extract_crsqlite_changes( Path::new(&stored.path), after_db_version, limit, )?; Ok(ControlResponse::DbChanges { db: db_resource_from_stored(&stored)?, batch, }) } ControlRequest::KvCreate { name } => { geth_kv::validate_kv_name(&name).map_err(|_| NodeError::InvalidKvName(name.clone()))?; let resource_id = format!("resource:kv:{name}"); let kv_id = format!("kv:{name}"); let resource = StoredResource { resource_id: resource_id.clone(), kind: ResourceKind::Kv.to_string(), name: name.clone(), status: "active".to_owned(), }; store.insert_resource(&resource)?; let stored = StoredKvStore { kv_id, resource_id, name, }; store.insert_kv_store(&stored)?; Ok(ControlResponse::KvCreated { kv: kv_resource_from_stored(&stored), }) } ControlRequest::KvSet { name, key, value, subject, } => { geth_kv::validate_kv_name(&name).map_err(|_| NodeError::InvalidKvName(name.clone()))?; geth_kv::validate_kv_key(&key).map_err(|_| NodeError::InvalidKvKey(key.clone()))?; let kv = store .get_kv_store_by_name(&name)? .ok_or_else(|| NodeError::KvNotFound(name.clone()))?; ensure_kv_write_authorized(&store, node, subject, &kv.resource_id, &key)?; let stored = StoredKvEntry { kv_id: kv.kv_id, key, value, updated_at_ms: geth_store::now_ms(), }; store.set_kv_entry(&stored)?; Ok(ControlResponse::KvSet { entry: kv_entry_from_stored(stored), }) } ControlRequest::KvGet { name, key } => { geth_kv::validate_kv_name(&name).map_err(|_| NodeError::InvalidKvName(name.clone()))?; geth_kv::validate_kv_key(&key).map_err(|_| NodeError::InvalidKvKey(key.clone()))?; let kv = store .get_kv_store_by_name(&name)? .ok_or_else(|| NodeError::KvNotFound(name.clone()))?; let entry = store .get_kv_entry(&kv.kv_id, &key)? .map(kv_entry_from_stored); Ok(ControlResponse::KvGet { entry }) } ControlRequest::DocumentCreate { name } => { geth_document::validate_document_name(&name) .map_err(|_| NodeError::InvalidDocumentName(name.clone()))?; let resource_id = format!("resource:document:{name}"); let document_id = format!("document:{name}"); let resource = StoredResource { resource_id: resource_id.clone(), kind: ResourceKind::Document.to_string(), name: name.clone(), status: "active".to_owned(), }; store.insert_resource(&resource)?; let stored = StoredDocumentResource { document_id, resource_id, name, state_json: geth_document::create_automerge_state("{}")?, updated_at_ms: geth_store::now_ms(), }; store.insert_document_resource(&stored)?; Ok(ControlResponse::DocumentCreated { document: document_resource_from_stored(&stored), }) } ControlRequest::DocumentStatus { name } => { geth_document::validate_document_name(&name) .map_err(|_| NodeError::InvalidDocumentName(name.clone()))?; let stored = store .get_document_resource_by_name(&name)? .ok_or_else(|| NodeError::DocumentNotFound(name.clone()))?; Ok(ControlResponse::DocumentStatus { document: document_resource_from_stored(&stored), }) } ControlRequest::DocumentSet { name, state_json } => { geth_document::validate_document_name(&name) .map_err(|_| NodeError::InvalidDocumentName(name.clone()))?; let mut stored = store .get_document_resource_by_name(&name)? .ok_or_else(|| NodeError::DocumentNotFound(name.clone()))?; stored.state_json = geth_document::create_automerge_state(&state_json)?; stored.updated_at_ms = geth_store::now_ms(); store.insert_document_resource(&stored)?; Ok(ControlResponse::DocumentSet { state: document_state_from_stored(&stored)?, }) } ControlRequest::DocumentGet { name } => { geth_document::validate_document_name(&name) .map_err(|_| NodeError::InvalidDocumentName(name.clone()))?; let stored = store .get_document_resource_by_name(&name)? .ok_or_else(|| NodeError::DocumentNotFound(name.clone()))?; Ok(ControlResponse::DocumentGet { state: document_state_from_stored(&stored)?, }) } ControlRequest::PubsubPub { topic, message, node: None, .. } => { geth_pubsub::validate_topic(&topic)?; geth_pubsub::validate_message(&message)?; let message = record_pubsub_message(node, topic, message)?; Ok(ControlResponse::PubsubPublished { message }) } ControlRequest::PubsubPub { node: Some(_), .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::PubsubSub { topic, node: None, .. } => { geth_pubsub::validate_topic(&topic)?; let messages = pubsub_messages_for_topic(node, &topic)?; Ok(ControlResponse::PubsubMessages { topic, messages, note: geth_pubsub::pubsub_storage_warning().to_owned(), }) } ControlRequest::PubsubSub { node: Some(_), .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::PipeListen { name, node: None, .. } => { geth_pipe::validate_pipe_name(&name)?; let listener = record_pipe_listener(node, name)?; Ok(ControlResponse::PipeListening { listener }) } ControlRequest::PipeListen { node: Some(_), .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::PipeConnect { target, node: None, .. } => { geth_pipe::validate_pipe_name(&target)?; let connection = record_pipe_connection( node, target, geth_pipe::local_pipe_runtime_note().to_owned(), )?; Ok(ControlResponse::PipeConnected { connection }) } ControlRequest::PipeConnect { node: Some(_), .. } => { Err(NodeError::IrohEndpointUnavailable) } ControlRequest::PipeSend { target, data_base64, node: None, .. } => { geth_pipe::validate_pipe_name(&target)?; base64::engine::general_purpose::STANDARD .decode(&data_base64) .map_err(|error| NodeError::IrohPeer(format!("invalid pipe payload: {error}")))?; let (message, listener_found) = record_pipe_message( node, target, data_base64, Some(node.node_id.clone()), "local pipe byte message".to_owned(), )?; Ok(ControlResponse::PipeSent { message, listener_found, note: geth_pipe::local_pipe_runtime_note().to_owned(), }) } ControlRequest::PipeSend { node: Some(_), .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::PipeTcpForward { .. } | ControlRequest::PipeTcpStream { .. } | ControlRequest::PipeUnixForward { .. } | ControlRequest::PipeUnixStream { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::PipeRecv { name, peek } => { geth_pipe::validate_pipe_name(&name)?; let messages = pipe_messages(node, &name, !peek)?; Ok(ControlResponse::PipeMessages { name, messages, drained: !peek, note: geth_pipe::local_pipe_runtime_note().to_owned(), }) } ControlRequest::SshProxyConnect { .. } | ControlRequest::SshProxyStream { .. } | ControlRequest::SshAdminShell { .. } | ControlRequest::KeychainSync { .. } | ControlRequest::AuthSync { .. } | ControlRequest::NodeEnrollSubmit { .. } | ControlRequest::NodeEnrollSync { .. } => Err(NodeError::IrohEndpointUnavailable), ControlRequest::ModuleStub { module, command } => { Ok(ControlResponse::NotImplemented { module, command }) } } } fn native_backend_statuses() -> Vec { geth_iroh::native_iroh_libraries() .into_iter() .map(|library| { let (status, blocker) = match library.module.as_str() { "cas" => ( "wired", format!( "none; CAS payload fetches use native ALPN {} after geth control authorization preflight", library.alpn ), ), "kv" => ( "wired", format!( "none; KV stores mirror into Iroh Documents over native ALPN {} after geth control authorization preflight", library.alpn ), ), "pubsub" => ( "wired", format!( "none; pubsub topics broadcast over native ALPN {} after geth control authorization preflight", library.alpn ), ), _ => ( "ready-to-wire", format!( "none; native ALPN {} is compiled against the daemon-owned iroh {} endpoint, but the module still needs its bootstrap control-path implementation replaced", library.alpn, geth_iroh::IROH_VERSION ), ), }; NativeBackendStatus { module: library.module, current_backend: "shared-iroh-endpoint-ready".to_owned(), target_crate: library.crate_name, target_version: library.crate_version, status: status.to_owned(), blocker, } }) .collect() } fn stored_resource_to_descriptor(stored: StoredResource) -> Result { let kind = stored .kind .parse::() .map_err(|_| NodeError::InvalidResourceKind(stored.kind.clone()))?; Ok(ResourceDescriptor::local( ResourceId::new(stored.resource_id), kind, ResourceName::new(stored.name), )) } fn db_resource_from_stored(stored: &StoredDbResource) -> Result { let path = Path::new(&stored.path); let metadata = path.metadata().ok(); let (schema_metadata, crsqlite_changes) = if metadata .as_ref() .is_some_and(std::fs::Metadata::is_file) { ( geth_db::schema_metadata(path).unwrap_or_else(|error| format!("unavailable:{error}")), geth_db::crsqlite_change_metadata(path) .unwrap_or_else(|error| geth_db::CrSqliteChangeMetadata::error(error.to_string())), ) } else { ( "unavailable:path-missing".to_owned(), geth_db::CrSqliteChangeMetadata::error("path-missing"), ) }; Ok(db_resource_from_stored_with_schema( stored, schema_metadata, crsqlite_changes, )) } fn db_resource_from_stored_with_schema( stored: &StoredDbResource, schema_metadata: String, crsqlite_changes: geth_db::CrSqliteChangeMetadata, ) -> DbResource { let path = Path::new(&stored.path); let metadata = path.metadata().ok(); DbResource { id: stored.db_id.clone().into(), resource: stored.resource_id.clone().into(), name: stored.name.clone(), path: stored.path.clone(), path_exists: metadata.as_ref().is_some_and(std::fs::Metadata::is_file), size_bytes: metadata.map(|metadata| metadata.len()), schema_metadata, crsqlite_changes, sync_status: "local-only".to_owned(), } } fn kv_resource_from_stored(stored: &StoredKvStore) -> KvResource { KvResource { id: stored.kv_id.clone().into(), resource: stored.resource_id.clone().into(), name: stored.name.clone(), sync_status: "local-only".to_owned(), } } fn kv_entry_from_stored(stored: StoredKvEntry) -> KvEntry { KvEntry { store: stored.kv_id.into(), key: stored.key, value: stored.value, } } fn ensure_local_kv_store(store: &Store, name: &str) -> Result { if let Some(kv) = store.get_kv_store_by_name(name)? { return Ok(kv); } let stored = StoredKvStore { kv_id: format!("kv:{name}"), resource_id: format!("resource:kv:{name}"), name: name.to_owned(), }; store.insert_kv_store(&stored)?; Ok(stored) } fn record_pubsub_message( node: &LocalNode, topic: String, message: String, ) -> Result { record_pubsub_message_at(node, topic, message, UnixMillis(geth_store::now_ms())) } fn record_pubsub_message_at( node: &LocalNode, topic: String, message: String, published_at: UnixMillis, ) -> Result { let message = PubsubMessage { topic: topic.into(), message, published_at, }; let mut runtime = node .runtime .pubsub .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)?; runtime.messages.push_back(message.clone()); while runtime.messages.len() > PUBSUB_RING_LIMIT { runtime.messages.pop_front(); } Ok(message) } fn pubsub_messages_for_topic( node: &LocalNode, topic: &str, ) -> Result, NodeError> { let runtime = node .runtime .pubsub .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)?; Ok(runtime .messages .iter() .filter(|message| message.topic.as_str() == topic) .cloned() .collect()) } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] struct PubsubGossipPayload { version: u8, source_node_id: String, source_agent_id: String, topic: String, message: String, published_at_ms: i64, } fn pubsub_gossip_topic_id(topic: &str) -> iroh_gossip::TopicId { blake3::hash(format!("geth.pubsub.v1\0{topic}").as_bytes()).into() } async fn ensure_pubsub_gossip_topic( node: &LocalNode, topic: &str, bootstrap: Vec, ) -> Result, NodeError> { let existing = { node.runtime .pubsub .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)? .gossip_topics .get(topic) .cloned() }; if let Some(existing) = existing { if !bootstrap.is_empty() { existing .sender .join_peers(bootstrap) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; } return Ok(Some(existing.sender)); } let Some(gossip) = iroh_gossip(node)? else { return Ok(None); }; let topic_id = pubsub_gossip_topic_id(topic); let subscribed = gossip .subscribe(topic_id, bootstrap) .await .map_err(|error| NodeError::IrohPeer(error.to_string()))?; let (sender, mut receiver) = subscribed.split(); let topic_name = topic.to_owned(); let receiver_node = node.clone(); tokio::spawn(async move { while let Some(event) = receiver.next().await { match event { Ok(iroh_gossip::api::Event::Received(message)) => { if message.content.len() > 64 * 1024 { tracing::warn!( topic = %topic_name, bytes = message.content.len(), "dropping oversized pubsub gossip message" ); continue; } match serde_json::from_slice::(&message.content) { Ok(payload) => { if let Err(error) = record_pubsub_gossip_payload(&receiver_node, &topic_name, payload) { tracing::warn!(%error, topic = %topic_name, "dropping invalid pubsub gossip message"); } } Err(error) => { tracing::warn!(%error, topic = %topic_name, "dropping undecodable pubsub gossip message"); } } } Ok(iroh_gossip::api::Event::Lagged) => { tracing::warn!(topic = %topic_name, "pubsub gossip receiver lagged"); } Ok(iroh_gossip::api::Event::NeighborUp(endpoint_id)) => { tracing::debug!(%endpoint_id, topic = %topic_name, "pubsub gossip neighbor up"); } Ok(iroh_gossip::api::Event::NeighborDown(endpoint_id)) => { tracing::debug!(%endpoint_id, topic = %topic_name, "pubsub gossip neighbor down"); } Err(error) => { tracing::warn!(%error, topic = %topic_name, "pubsub gossip receiver closed"); break; } } } }); node.runtime .pubsub .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)? .gossip_topics .insert( topic.to_owned(), PubsubGossipTopicRuntime { sender: sender.clone(), }, ); Ok(Some(sender)) } fn record_pubsub_gossip_payload( node: &LocalNode, expected_topic: &str, payload: PubsubGossipPayload, ) -> Result<(), NodeError> { if payload.version != 1 || payload.topic != expected_topic || payload.source_node_id == node.node_id { return Ok(()); } geth_pubsub::validate_topic(&payload.topic)?; geth_pubsub::validate_message(&payload.message)?; record_pubsub_message_at( node, payload.topic, payload.message, UnixMillis(payload.published_at_ms), )?; Ok(()) } async fn broadcast_pubsub_gossip( node: &LocalNode, topic: &str, message: &str, bootstrap: Vec, ) -> Result<(), NodeError> { let Some(sender) = ensure_pubsub_gossip_topic(node, topic, bootstrap).await? else { return Ok(()); }; let payload = PubsubGossipPayload { version: 1, source_node_id: node.node_id.clone(), source_agent_id: node.agent_id.clone(), topic: topic.to_owned(), message: message.to_owned(), published_at_ms: geth_store::now_ms(), }; let bytes = serde_json::to_vec(&payload)?; sender .broadcast(bytes::Bytes::from(bytes)) .await .map_err(|error| NodeError::IrohPeer(error.to_string())) } fn record_pipe_listener(node: &LocalNode, name: String) -> Result { let listener = PipeListener { id: format!("pipe:{name}").into(), name: name.clone(), listened_at: UnixMillis(geth_store::now_ms()), note: geth_pipe::local_pipe_runtime_note().to_owned(), }; let mut runtime = node .runtime .pipes .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)?; runtime.listeners.insert(name, listener.clone()); Ok(listener) } fn record_pipe_connection( node: &LocalNode, target: String, note: String, ) -> Result { let mut runtime = node .runtime .pipes .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)?; let local_listener_found = runtime.listeners.contains_key(&target); let connection = PipeConnection { target, connected_at: UnixMillis(geth_store::now_ms()), local_listener_found, note, }; runtime.connections.push_back(connection.clone()); while runtime.connections.len() > PIPE_CONNECTION_RING_LIMIT { runtime.connections.pop_front(); } Ok(connection) } fn record_pipe_message( node: &LocalNode, target: String, data_base64: String, source_node: Option, note: String, ) -> Result<(Option, bool), NodeError> { let mut runtime = node .runtime .pipes .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)?; let listener_found = runtime.listeners.contains_key(&target); if !listener_found { return Ok((None, false)); } let message = PipeMessage { pipe: target, data_base64, received_at: UnixMillis(geth_store::now_ms()), source_node, note, }; runtime.messages.push_back(message.clone()); while runtime.messages.len() > PIPE_MESSAGE_RING_LIMIT { runtime.messages.pop_front(); } Ok((Some(message), true)) } fn pipe_messages(node: &LocalNode, name: &str, drain: bool) -> Result, NodeError> { let mut runtime = node .runtime .pipes .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)?; let messages = runtime .messages .iter() .filter(|message| message.pipe == name) .cloned() .collect::>(); if drain { runtime.messages.retain(|message| message.pipe != name); } Ok(messages) } fn ensure_local_document(store: &Store, name: &str) -> Result { if let Some(document) = store.get_document_resource_by_name(name)? { return Ok(document); } let stored = StoredDocumentResource { document_id: format!("document:{name}"), resource_id: format!("resource:document:{name}"), name: name.to_owned(), state_json: geth_document::create_automerge_state("{}")?, updated_at_ms: 0, }; store.insert_document_resource(&stored)?; Ok(stored) } fn document_resource_from_stored(stored: &StoredDocumentResource) -> DocumentResource { DocumentResource { id: stored.document_id.clone().into(), resource: stored.resource_id.clone().into(), name: stored.name.clone(), sync_status: "automerge-local".to_owned(), state_bytes: geth_document::document_state_bytes(&stored.state_json), } } fn document_state_from_stored(stored: &StoredDocumentResource) -> Result { Ok(DocumentState { document: document_resource_from_stored(stored), state_json: geth_document::document_view_json(&stored.state_json)?, updated_at: UnixMillis(stored.updated_at_ms), }) } fn document_sync_state_from_stored( stored: &StoredDocumentResource, ) -> Result { Ok(DocumentState { document: document_resource_from_stored(stored), state_json: stored.state_json.clone(), updated_at: UnixMillis(stored.updated_at_ms), }) } fn file_root_from_stored(stored: &StoredFileRoot) -> FileRoot { FileRoot { id: stored.root_id.clone(), resource: stored.resource_id.clone(), name: stored.name.clone(), path: stored.path.clone(), latest_tree: stored.latest_tree_hash.clone().map(Into::into), updated_at_ms: stored.updated_at_ms, } } fn discovered_peer_from_stored(stored: StoredPeerCard) -> Result { let card: PeerCard = serde_json::from_str(&stored.card_json)?; Ok(DiscoveredPeer::candidate( card, UnixMillis(stored.updated_at_ms), DiscoverySource::Imported, )?) } fn file_conflict_from_stored(stored: StoredFileConflict) -> Result { Ok(FileConflict { id: stored.conflict_id, root: stored.root_name, resource: stored.resource_id, path: stored.path, kind: FileConflictKind::parse(&stored.kind)?, status: FileConflictStatus::parse(&stored.status)?, base_tree: stored.base_tree_hash.map(Into::into), local_tree: stored.local_tree_hash.map(Into::into), remote_tree: stored.remote_tree_hash.map(Into::into), detail: stored.detail, resolution: stored .resolution .as_deref() .map(FileConflictResolution::parse) .transpose()?, resolution_note: stored.resolution_note, created_at_ms: stored.created_at_ms, resolved_at_ms: stored.resolved_at_ms, }) } fn apply_file_root_tree( store: &Store, node: &LocalNode, source: &str, target: PathBuf, dry_run: bool, ) -> Result { geth_cas::validate_file_root_name(source)?; let source_root = store .get_file_root_by_name(source)? .ok_or_else(|| NodeError::ResourceNotFound(format!("file-root:{source}")))?; let tree_hash = source_root .latest_tree_hash .as_ref() .ok_or_else(|| NodeError::ResourceNotFound(format!("file-root-tree:{source}")))?; let tree_hash = BlobHash::new(tree_hash.clone()); let cas = LocalCas::new(node.paths.cas_dir()); let tree_bytes = cas.read_bytes(&tree_hash)?; let tree: CasTreeObject = geth_codec::decode_canonical(&tree_bytes)?; if target.exists() && !target.is_dir() { return Err(NodeError::Cas(geth_cas::CasError::TreeRootNotDirectory( target.display().to_string(), ))); } if !dry_run { std::fs::create_dir_all(&target)?; } if let Some((local_root, base_tree, local_tree, local_tree_hash)) = load_apply_three_way_state(store, &cas, &target)? { return apply_file_root_tree_three_way( store, &source_root, &local_root, &cas, target, dry_run, &tree_hash, &tree, &base_tree, &local_tree, local_tree_hash.as_ref(), ); } apply_file_root_tree_conservative( store, &source_root, &cas, target, dry_run, &tree_hash, &tree, ) } fn apply_file_root_tree_conservative( store: &Store, source_root: &StoredFileRoot, cas: &LocalCas, target: PathBuf, dry_run: bool, tree_hash: &BlobHash, tree: &CasTreeObject, ) -> Result { let mut files_written = 0; let mut dirs_created = 0; let mut conflicts = Vec::new(); for entry in &tree.entries { let out = safe_tree_output_path(&target, &entry.path)?; match entry.kind { CasTreeEntryKind::Directory => { if out.exists() { if !out.is_dir() { conflicts.push(record_apply_conflict( store, source_root, &entry.path, tree_hash, "remote directory conflicts with a local non-directory path", dry_run, )?); } continue; } dirs_created += 1; if !dry_run { std::fs::create_dir_all(&out)?; } } CasTreeEntryKind::File => { let Some(blob) = &entry.blob else { conflicts.push(record_apply_conflict( store, source_root, &entry.path, tree_hash, "remote file entry is missing a CAS blob hash", dry_run, )?); continue; }; if out.exists() { if !out.is_file() { conflicts.push(record_apply_conflict( store, source_root, &entry.path, tree_hash, "remote file conflicts with a local non-file path", dry_run, )?); continue; } if hash_path(&out)? != *blob { conflicts.push(record_apply_conflict( store, source_root, &entry.path, tree_hash, "local file differs from remote CAS tree; refusing to overwrite", dry_run, )?); } continue; } files_written += 1; if !dry_run { let bytes = cas.read_bytes(blob)?; write_file_atomic(&out, &bytes)?; set_executable_bit(&out, entry.executable)?; } } } } Ok(ControlResponse::CasRootApplied { source: source_root.name.clone(), target, files_written, dirs_created, conflicts, dry_run, note: "safe file-root apply creates missing paths but never deletes extra files or overwrites differing local files; conflicts are recorded for manual resolution".to_owned(), }) } type ApplyThreeWayState = ( StoredFileRoot, CasTreeObject, CasTreeObject, Option, ); fn load_apply_three_way_state( store: &Store, cas: &LocalCas, target: &Path, ) -> Result, NodeError> { if !target.is_dir() { return Ok(None); } let target = std::fs::canonicalize(target)?; let Some(local_root) = store .list_file_roots()? .into_iter() .filter(|root| !root.path.starts_with("remote:")) .find(|root| { std::fs::canonicalize(&root.path) .map(|path| path == target) .unwrap_or(false) }) else { return Ok(None); }; let Some(base_tree_json) = local_root.latest_tree_json.as_deref() else { return Ok(None); }; let base_tree = serde_json::from_str(base_tree_json)?; let local_scan = cas.add_tree_path(&target)?; Ok(Some(( local_root, base_tree, local_scan.tree, Some(local_scan.object.hash), ))) } #[allow(clippy::too_many_arguments)] fn apply_file_root_tree_three_way( store: &Store, source_root: &StoredFileRoot, local_root: &StoredFileRoot, cas: &LocalCas, target: PathBuf, dry_run: bool, remote_tree_hash: &BlobHash, remote_tree: &CasTreeObject, base_tree: &CasTreeObject, local_tree: &CasTreeObject, local_tree_hash: Option<&BlobHash>, ) -> Result { let mut files_written = 0; let mut dirs_created = 0; let mut conflicts = record_three_way_apply_conflicts( store, source_root, local_root, base_tree, local_tree_hash, remote_tree_hash, remote_tree, local_tree, dry_run, )?; let conflict_paths = conflicts .iter() .map(|conflict| conflict.path.clone()) .collect::>(); let base_entries = owned_tree_entry_map(base_tree); let local_entries = owned_tree_entry_map(local_tree); let remote_entries = owned_tree_entry_map(remote_tree); let mut consumed_base_paths = BTreeSet::new(); let mut consumed_remote_paths = BTreeSet::new(); for (from, to) in remote_rename_pairs(&base_entries, &remote_entries) { if conflict_paths.contains(&from) || conflict_paths.contains(&to) { continue; } let Some(base_entry) = base_entries.get(&from) else { continue; }; if local_entries.get(&from) != Some(base_entry) { continue; } let Some(remote_entry) = remote_entries.get(&to) else { continue; }; let from_path = safe_tree_output_path(&target, &from)?; let to_path = safe_tree_output_path(&target, &to)?; if to_path.exists() { if path_matches_entry(cas, &to_path, remote_entry)? { consumed_base_paths.insert(from); consumed_remote_paths.insert(to); } else { conflicts.push(record_apply_conflict_with_trees( store, source_root, &to, local_root.latest_tree_hash.as_deref(), local_tree_hash, remote_tree_hash, "remote rename target conflicts with an existing local path", dry_run, )?); } continue; } if !from_path.exists() { continue; } if !dry_run { if let Some(parent) = to_path.parent() { std::fs::create_dir_all(parent)?; } std::fs::rename(&from_path, &to_path)?; set_executable_bit(&to_path, remote_entry.executable)?; } consumed_base_paths.insert(from); consumed_remote_paths.insert(to); } let mut remote_paths = remote_entries.keys().cloned().collect::>(); remote_paths.sort_by(|left, right| { kind_order_for_apply(remote_entries.get(left)) .cmp(&kind_order_for_apply(remote_entries.get(right))) .then_with(|| left.cmp(right)) }); for path in remote_paths { if consumed_remote_paths.contains(&path) || conflict_paths.contains(&path) { continue; } let Some(remote_entry) = remote_entries.get(&path) else { continue; }; let base_entry = base_entries.get(&path); let local_entry = local_entries.get(&path); if local_entry == Some(remote_entry) || base_entry == Some(remote_entry) { continue; } if base_entry.is_some() && local_entry != base_entry { continue; } if base_entry.is_none() && local_entry.is_some() { conflicts.push(record_apply_conflict_with_trees( store, source_root, &path, local_root.latest_tree_hash.as_deref(), local_tree_hash, remote_tree_hash, "remote create conflicts with an existing local path", dry_run, )?); continue; } match remote_entry.kind { CasTreeEntryKind::Directory => { let out = safe_tree_output_path(&target, &path)?; if !out.exists() { dirs_created += 1; if !dry_run { std::fs::create_dir_all(out)?; } } } CasTreeEntryKind::File => { let Some(blob) = &remote_entry.blob else { conflicts.push(record_apply_conflict_with_trees( store, source_root, &path, local_root.latest_tree_hash.as_deref(), local_tree_hash, remote_tree_hash, "remote file entry is missing a CAS blob hash", dry_run, )?); continue; }; let out = safe_tree_output_path(&target, &path)?; files_written += 1; if !dry_run { let bytes = cas.read_bytes(blob)?; write_file_atomic(&out, &bytes)?; set_executable_bit(&out, remote_entry.executable)?; } } } } let mut deleted_paths = base_entries .keys() .filter(|path| { !remote_entries.contains_key(*path) && !consumed_base_paths.contains(*path) && !conflict_paths.contains(*path) }) .cloned() .collect::>(); deleted_paths.sort_by(|left, right| right.cmp(left)); for path in deleted_paths { let Some(base_entry) = base_entries.get(&path) else { continue; }; if local_entries.get(&path) != Some(base_entry) { continue; } let out = safe_tree_output_path(&target, &path)?; if dry_run || !out.exists() { continue; } match base_entry.kind { CasTreeEntryKind::Directory => { let _ = std::fs::remove_dir(&out); } CasTreeEntryKind::File => { std::fs::remove_file(&out)?; } } } Ok(ControlResponse::CasRootApplied { source: source_root.name.clone(), target, files_written, dirs_created, conflicts, dry_run, note: "three-way file-root apply updated only paths where local state still matched the recorded base; ambiguous edits remain durable conflicts".to_owned(), }) } #[allow(clippy::too_many_arguments)] fn record_three_way_apply_conflicts( store: &Store, source_root: &StoredFileRoot, local_root: &StoredFileRoot, base_tree: &CasTreeObject, local_tree_hash: Option<&BlobHash>, remote_tree_hash: &BlobHash, remote_tree: &CasTreeObject, local_tree: &CasTreeObject, dry_run: bool, ) -> Result, NodeError> { geth_cas::detect_tree_conflicts(base_tree, local_tree, remote_tree) .into_iter() .map(|conflict| { record_apply_conflict_with_trees( store, source_root, &conflict.path, local_root.latest_tree_hash.as_deref(), local_tree_hash, remote_tree_hash, &conflict.detail, dry_run, ) }) .collect() } fn owned_tree_entry_map(tree: &CasTreeObject) -> BTreeMap { tree.entries .iter() .map(|entry| (entry.path.clone(), entry.clone())) .collect() } fn remote_rename_pairs( base_entries: &BTreeMap, remote_entries: &BTreeMap, ) -> Vec<(String, String)> { let mut pairs = Vec::new(); for (base_path, base_entry) in base_entries { let Some(base_blob) = &base_entry.blob else { continue; }; if remote_entries.contains_key(base_path) { continue; } if let Some((remote_path, _)) = remote_entries.iter().find(|(remote_path, remote_entry)| { !base_entries.contains_key(*remote_path) && remote_entry.blob.as_ref() == Some(base_blob) }) { pairs.push((base_path.clone(), remote_path.clone())); } } pairs.sort(); pairs } fn path_matches_entry( cas: &LocalCas, path: &Path, entry: &geth_cas::CasTreeEntry, ) -> Result { match entry.kind { CasTreeEntryKind::Directory => Ok(path.is_dir()), CasTreeEntryKind::File => { let Some(blob) = &entry.blob else { return Ok(false); }; Ok(path.is_file() && hash_path(path)? == *blob && cas.has(blob)?) } } } fn kind_order_for_apply(entry: Option<&geth_cas::CasTreeEntry>) -> u8 { match entry.map(|entry| &entry.kind) { Some(CasTreeEntryKind::Directory) => 0, Some(CasTreeEntryKind::File) => 1, None => 2, } } fn safe_tree_output_path(root: &Path, relative: &str) -> Result { let relative_path = Path::new(relative); if relative_path.is_absolute() { return Err(NodeError::Cas(geth_cas::CasError::NonRelativeTreePath( relative.to_owned(), ))); } for component in relative_path.components() { if !matches!(component, Component::Normal(_)) { return Err(NodeError::Cas(geth_cas::CasError::NonRelativeTreePath( relative.to_owned(), ))); } } Ok(root.join(relative_path)) } fn record_apply_conflict( store: &Store, root: &StoredFileRoot, path: &str, remote_tree: &BlobHash, detail: &str, dry_run: bool, ) -> Result { let created_at = geth_store::now_ms(); let stored = StoredFileConflict { conflict_id: generated_file_conflict_id( &root.name, path, FileConflictKind::ConcurrentEdit.as_str(), created_at, ), root_name: root.name.clone(), resource_id: root.resource_id.clone(), path: path.to_owned(), kind: FileConflictKind::ConcurrentEdit.as_str().to_owned(), status: FileConflictStatus::Open.as_str().to_owned(), base_tree_hash: None, local_tree_hash: None, remote_tree_hash: Some(remote_tree.to_string()), detail: detail.to_owned(), resolution: None, resolution_note: None, created_at_ms: created_at, resolved_at_ms: None, }; if !dry_run { store.upsert_file_conflict(&stored)?; } file_conflict_from_stored(stored) } #[allow(clippy::too_many_arguments)] fn record_apply_conflict_with_trees( store: &Store, root: &StoredFileRoot, path: &str, base_tree: Option<&str>, local_tree: Option<&BlobHash>, remote_tree: &BlobHash, detail: &str, dry_run: bool, ) -> Result { let created_at = geth_store::now_ms(); let stored = StoredFileConflict { conflict_id: generated_file_conflict_id( &root.name, path, FileConflictKind::ConcurrentEdit.as_str(), created_at, ), root_name: root.name.clone(), resource_id: root.resource_id.clone(), path: path.to_owned(), kind: FileConflictKind::ConcurrentEdit.as_str().to_owned(), status: FileConflictStatus::Open.as_str().to_owned(), base_tree_hash: base_tree.map(ToOwned::to_owned), local_tree_hash: local_tree.map(ToString::to_string), remote_tree_hash: Some(remote_tree.to_string()), detail: detail.to_owned(), resolution: None, resolution_note: None, created_at_ms: created_at, resolved_at_ms: None, }; if !dry_run { store.upsert_file_conflict(&stored)?; } file_conflict_from_stored(stored) } fn record_sync_tree_conflicts( store: &Store, local_root: &StoredFileRoot, base_tree_hash: Option<&str>, base_tree: &CasTreeObject, local_tree_hash: Option<&str>, remote_tree_hash: Option<&BlobHash>, remote_tree: &CasTreeObject, ) -> Result, NodeError> { let Some(local_tree_json) = local_root.latest_tree_json.as_deref() else { return Ok(Vec::new()); }; let local_tree: CasTreeObject = serde_json::from_str(local_tree_json)?; let tree_conflicts = geth_cas::detect_tree_conflicts(base_tree, &local_tree, remote_tree); let created_at = geth_store::now_ms(); tree_conflicts .into_iter() .enumerate() .map(|(index, conflict)| { let created_at = created_at + index as i64; let stored = StoredFileConflict { conflict_id: generated_file_conflict_id( &local_root.name, &conflict.path, conflict.kind.as_str(), created_at, ), root_name: local_root.name.clone(), resource_id: local_root.resource_id.clone(), path: conflict.path, kind: conflict.kind.as_str().to_owned(), status: FileConflictStatus::Open.as_str().to_owned(), base_tree_hash: base_tree_hash.map(ToOwned::to_owned), local_tree_hash: local_tree_hash.map(ToOwned::to_owned), remote_tree_hash: remote_tree_hash.map(ToString::to_string), detail: format!( "{}; detected while syncing remote file-root metadata", conflict.detail ), resolution: None, resolution_note: None, created_at_ms: created_at, resolved_at_ms: None, }; store.upsert_file_conflict(&stored)?; file_conflict_from_stored(stored) }) .collect() } fn write_file_atomic(path: &Path, bytes: &[u8]) -> Result<(), NodeError> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } let tmp = path.with_extension(format!( "geth-tmp-{}", geth_crypto::blake3_hex(path.display().to_string().as_bytes()) )); std::fs::write(&tmp, bytes)?; std::fs::rename(tmp, path)?; Ok(()) } #[cfg(unix)] fn set_executable_bit(path: &Path, executable: bool) -> Result<(), NodeError> { use std::os::unix::fs::PermissionsExt; let mut permissions = std::fs::metadata(path)?.permissions(); let mut mode = permissions.mode(); if executable { mode |= 0o111; } else { mode &= !0o111; } permissions.set_mode(mode); std::fs::set_permissions(path, permissions)?; Ok(()) } #[cfg(not(unix))] fn set_executable_bit(_path: &Path, _executable: bool) -> Result<(), NodeError> { Ok(()) } fn ensure_resource_exists(store: &Store, resource_id: &str) -> Result<(), NodeError> { if store .list_resources()? .into_iter() .any(|resource| resource.resource_id == resource_id) { Ok(()) } else { Err(NodeError::ResourceNotFound(resource_id.to_owned())) } } fn ensure_kv_write_authorized( store: &Store, node: &LocalNode, subject: Option, resource_id: &str, key: &str, ) -> Result<(), NodeError> { let Some(subject) = subject else { return Ok(()); }; let capability = format!("kv.write_key:{key}"); ensure_subject_authorized(store, node, Some(&subject), resource_id, &capability) } fn ensure_subject_authorized( store: &Store, node: &LocalNode, subject: Option<&str>, resource_id: &str, capability: &str, ) -> Result<(), NodeError> { let Some(subject) = subject else { return Ok(()); }; if subject == node.node_id || subject == node.agent_id { return Ok(()); } let ops = load_auth_ops_for_resource(store, resource_id)?; let explanation = geth_auth::explain_auth_ops( &ops, PrincipalId::new(subject.to_owned()), ResourceId::new(resource_id.to_owned()), Capability::new(capability.to_owned()), ); if explanation.allowed { Ok(()) } else { Err(NodeError::Unauthorized(format!( "{subject} lacks {capability} on {resource_id}: {}", explanation.reason ))) } } fn create_resource_secret( store: &Store, resource_id: &str, epoch: u64, ) -> Result { let created_at = UnixMillis(geth_store::now_ms()); let secret_id = format!( "secret:{}", geth_crypto::blake3_hex(format!("{resource_id}\0{epoch}\0{}", created_at.0).as_bytes()) ); let stored = StoredResourceSecret { secret_id, resource_id: resource_id.to_owned(), epoch, status: "active".to_owned(), created_at_ms: created_at.0, }; store.insert_resource_secret(&stored)?; Ok(resource_secret_from_stored(stored)) } fn resource_secret_from_stored(stored: StoredResourceSecret) -> ResourceMasterSecret { ResourceMasterSecret { id: stored.secret_id.into(), resource: stored.resource_id.into(), epoch: stored.epoch, created_at: UnixMillis(stored.created_at_ms), } } fn load_bearer_access(store: &Store) -> Result, NodeError> { let ops = store .list_auth_ops()? .into_iter() .map(|stored| serde_json::from_str(&stored.op_json).map_err(NodeError::from)) .collect::, NodeError>>()?; let view = geth_auth::reduce_auth_ops(&ops); Ok(view .bearer_access .into_values() .map(|record| BearerAccess { secret: record.secret, token: None, token_hash: record.token_hash, resource: record.resource, capabilities: record.capabilities, expires_at: record.expires_at, may_delegate: false, }) .collect()) } fn store_auth_op(store: &Store, op: &AuthOp) -> Result<(), NodeError> { store.insert_auth_op(&StoredAuthOp { op_id: op.id.to_string(), resource_id: op.resource.to_string(), op_json: serde_json::to_string(op)?, created_at_ms: op.created_at.0, })?; Ok(()) } fn store_and_sign_auth_ops( store: &Store, node: &LocalNode, ops: &[AuthOp], signing_key_path: &Path, admin_key_path: Option<&Path>, ) -> Result, NodeError> { for op in ops { store_auth_op(store, op)?; } let (signer, signer_public_key) = keychain_signer_from_paths(signing_key_path, admin_key_path)?; ops.iter() .map(|op| { sign_auth_op_with_ssh( store, node, op, signing_key_path, &signer, &signer_public_key, ) }) .collect() } fn sign_auth_op_with_ssh( store: &Store, node: &LocalNode, op: &AuthOp, signing_key_path: &Path, signer: &KeyId, signer_public_key: &str, ) -> Result { geth_ssh_identity::ensure_ssh_keygen_available()?; let signature_dir = node.paths.home().join("auth-signatures"); std::fs::create_dir_all(&signature_dir)?; let payload_path = signature_dir.join(format!( "{}.payload", geth_crypto::blake3_hex(op.id.as_str().as_bytes()) )); std::fs::write(&payload_path, geth_auth::auth_signing_payload(op)?)?; let output = geth_ssh_identity::sign_command(signing_key_path, AUTH_SIGNATURE_NAMESPACE, &payload_path) .output()?; if !output.status.success() { return Err(geth_ssh_identity::SshIdentityError::SshKeygenFailed( String::from_utf8_lossy(&output.stderr).trim().to_owned(), ) .into()); } let signature_path = Path::new(&format!("{}.sig", payload_path.display())).to_path_buf(); let signature_bytes = std::fs::read(signature_path)?; let created_at = UnixMillis(geth_store::now_ms()); let signature = AuthOpSignature { op_id: op.id.clone(), signer: signer.clone(), signer_public_key: signer_public_key.to_owned(), namespace: AUTH_SIGNATURE_NAMESPACE.to_owned(), signature: signature_bytes, created_at, }; store.insert_auth_signature(&StoredAuthSignature { op_id: signature.op_id.to_string(), signer: signature.signer.to_string(), signer_public_key: signature.signer_public_key.clone(), namespace: signature.namespace.clone(), signature: signature.signature.clone(), created_at_ms: signature.created_at.0, })?; Ok(signature) } fn load_auth_ops_for_resource(store: &Store, resource: &str) -> Result, NodeError> { store .list_auth_ops_for_resource(resource)? .into_iter() .map(|stored| serde_json::from_str(&stored.op_json).map_err(NodeError::from)) .collect() } fn load_auth_ops(store: &Store) -> Result, NodeError> { store .list_auth_ops()? .into_iter() .map(|stored| serde_json::from_str(&stored.op_json).map_err(NodeError::from)) .collect() } fn load_auth_signatures(store: &Store) -> Result, NodeError> { Ok(store .list_auth_signatures()? .into_iter() .map(|stored| AuthOpSignature { op_id: AuthOpId::new(stored.op_id), signer: KeyId::new(stored.signer), signer_public_key: stored.signer_public_key, namespace: stored.namespace, signature: stored.signature, created_at: UnixMillis(stored.created_at_ms), }) .collect()) } fn stored_auth_signature_from_signature(signature: &AuthOpSignature) -> StoredAuthSignature { StoredAuthSignature { op_id: signature.op_id.to_string(), signer: signature.signer.to_string(), signer_public_key: signature.signer_public_key.clone(), namespace: signature.namespace.clone(), signature: signature.signature.clone(), created_at_ms: signature.created_at.0, } } fn auth_signature_uses_claimed_key(signature: &AuthOpSignature) -> bool { KeyId::new(ssh_public_key_fingerprint(&signature.signer_public_key)) == signature.signer } fn verify_auth_signature_with_ssh( node: &LocalNode, op: &AuthOp, signature: &StoredAuthSignature, ) -> Result { if signature.signer_public_key.trim().is_empty() { return Ok(false); } if geth_ssh_identity::ensure_ssh_keygen_available().is_err() { return Ok(false); } let verify_dir = node.paths.home().join("auth-signatures").join("verify"); std::fs::create_dir_all(&verify_dir)?; let stable_id = geth_crypto::blake3_hex( format!( "{}\0{}\0{}\0{}", op.id, signature.signer, signature.namespace, signature.created_at_ms ) .as_bytes(), ); let payload_path = verify_dir.join(format!("{stable_id}.payload")); let signature_path = verify_dir.join(format!("{stable_id}.sig")); let allowed_signers_path = verify_dir.join(format!("{stable_id}.allowed-signers")); std::fs::write(&payload_path, geth_auth::auth_signing_payload(op)?)?; std::fs::write(&signature_path, &signature.signature)?; std::fs::write( &allowed_signers_path, format!( "{} {}\n", signature.signer, signature.signer_public_key.trim() ), )?; let payload = std::fs::File::open(&payload_path)?; let output = std::process::Command::new("ssh-keygen") .arg("-Y") .arg("verify") .arg("-f") .arg(&allowed_signers_path) .arg("-I") .arg(&signature.signer) .arg("-n") .arg(&signature.namespace) .arg("-s") .arg(&signature_path) .stdin(std::process::Stdio::from(payload)) .output()?; Ok(output.status.success()) } fn store_keychain_op(store: &Store, op: &KeychainOp) -> Result<(), NodeError> { store.insert_keychain_op(&StoredKeychainOp { op_id: op.id.to_string(), op_json: serde_json::to_string(op)?, created_at_ms: op.created_at.0, })?; Ok(()) } fn store_and_sign_keychain_ops( store: &Store, node: &LocalNode, ops: &[KeychainOp], signing_key_path: Option<&Path>, admin_key_path: Option<&Path>, ) -> Result, NodeError> { for op in ops { store_keychain_op(store, op)?; } let Some(signing_key_path) = signing_key_path else { return Ok(Vec::new()); }; let (signer, signer_public_key) = keychain_signer_from_paths(signing_key_path, admin_key_path)?; ops.iter() .map(|op| { sign_keychain_op_with_ssh( store, node, op, signing_key_path, &signer, &signer_public_key, ) }) .collect() } fn keychain_signer_from_paths( signing_key_path: &Path, admin_key_path: Option<&Path>, ) -> Result<(KeyId, String), NodeError> { let public_key_path = admin_key_path .map(Path::to_path_buf) .unwrap_or_else(|| Path::new(&format!("{}.pub", signing_key_path.display())).to_path_buf()); let public_key = std::fs::read_to_string(public_key_path)?; Ok(( KeyId::new(ssh_public_key_fingerprint(&public_key)), public_key, )) } fn sign_keychain_op_with_ssh( store: &Store, node: &LocalNode, op: &KeychainOp, signing_key_path: &Path, signer: &KeyId, signer_public_key: &str, ) -> Result { geth_ssh_identity::ensure_ssh_keygen_available()?; let signature_dir = node.paths.home().join("keychain-signatures"); std::fs::create_dir_all(&signature_dir)?; let payload_path = signature_dir.join(format!( "{}.payload", geth_crypto::blake3_hex(op.id.as_str().as_bytes()) )); std::fs::write(&payload_path, geth_keychain::keychain_signing_payload(op)?)?; let output = geth_ssh_identity::sign_command( signing_key_path, geth_keychain::KEYCHAIN_SIGNATURE_NAMESPACE, &payload_path, ) .output()?; if !output.status.success() { return Err(geth_ssh_identity::SshIdentityError::SshKeygenFailed( String::from_utf8_lossy(&output.stderr).trim().to_owned(), ) .into()); } let signature_path = Path::new(&format!("{}.sig", payload_path.display())).to_path_buf(); let signature_bytes = std::fs::read(signature_path)?; let created_at = UnixMillis(geth_store::now_ms()); let signature = KeychainOpSignature { op_id: op.id.clone(), signer: signer.clone(), signer_public_key: signer_public_key.to_owned(), namespace: geth_keychain::KEYCHAIN_SIGNATURE_NAMESPACE.to_owned(), signature: signature_bytes, created_at, }; store.insert_keychain_signature(&StoredKeychainSignature { op_id: signature.op_id.to_string(), signer: signature.signer.to_string(), signer_public_key: signature.signer_public_key.clone(), namespace: signature.namespace.clone(), signature: signature.signature.clone(), created_at_ms: signature.created_at.0, })?; Ok(signature) } fn stored_keychain_signature_from_signature( signature: &KeychainOpSignature, ) -> StoredKeychainSignature { StoredKeychainSignature { op_id: signature.op_id.to_string(), signer: signature.signer.to_string(), signer_public_key: signature.signer_public_key.clone(), namespace: signature.namespace.clone(), signature: signature.signature.clone(), created_at_ms: signature.created_at.0, } } fn keychain_signature_uses_claimed_key(signature: &KeychainOpSignature) -> bool { KeyId::new(ssh_public_key_fingerprint(&signature.signer_public_key)) == signature.signer } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] struct KeychainSignatureStatus { total: usize, verified: usize, failed: usize, } fn verify_keychain_signatures( store: &Store, node: &LocalNode, ) -> Result { let ops = load_keychain_ops(store)?; let ops_by_id = ops .into_iter() .map(|op| (op.id.to_string(), op)) .collect::>(); let signatures = store.list_keychain_signatures()?; let total = signatures.len(); let mut verified = 0; let mut failed = 0; for signature in signatures { let Some(op) = ops_by_id.get(&signature.op_id) else { failed += 1; continue; }; if verify_keychain_signature_with_ssh(node, op, &signature)? { verified += 1; } else { failed += 1; } } Ok(KeychainSignatureStatus { total, verified, failed, }) } fn verify_keychain_signature_with_ssh( node: &LocalNode, op: &KeychainOp, signature: &StoredKeychainSignature, ) -> Result { if signature.signer_public_key.trim().is_empty() { return Ok(false); } if geth_ssh_identity::ensure_ssh_keygen_available().is_err() { return Ok(false); } let verify_dir = node.paths.home().join("keychain-signatures").join("verify"); std::fs::create_dir_all(&verify_dir)?; let stable_id = geth_crypto::blake3_hex( format!( "{}\0{}\0{}\0{}", op.id, signature.signer, signature.namespace, signature.created_at_ms ) .as_bytes(), ); let payload_path = verify_dir.join(format!("{stable_id}.payload")); let signature_path = verify_dir.join(format!("{stable_id}.sig")); let allowed_signers_path = verify_dir.join(format!("{stable_id}.allowed-signers")); std::fs::write(&payload_path, geth_keychain::keychain_signing_payload(op)?)?; std::fs::write(&signature_path, &signature.signature)?; std::fs::write( &allowed_signers_path, format!( "{} {}\n", signature.signer, signature.signer_public_key.trim() ), )?; let payload = std::fs::File::open(&payload_path)?; let output = std::process::Command::new("ssh-keygen") .arg("-Y") .arg("verify") .arg("-f") .arg(&allowed_signers_path) .arg("-I") .arg(&signature.signer) .arg("-n") .arg(&signature.namespace) .arg("-s") .arg(&signature_path) .stdin(std::process::Stdio::from(payload)) .output()?; Ok(output.status.success()) } fn load_keychain_ops(store: &Store) -> Result, NodeError> { store .list_keychain_ops()? .into_iter() .map(|stored| serde_json::from_str(&stored.op_json).map_err(NodeError::from)) .collect() } fn load_keychain_signatures(store: &Store) -> Result, NodeError> { Ok(store .list_keychain_signatures()? .into_iter() .map(|stored| KeychainOpSignature { op_id: AuthOpId::new(stored.op_id), signer: KeyId::new(stored.signer), signer_public_key: stored.signer_public_key, namespace: stored.namespace, signature: stored.signature, created_at: UnixMillis(stored.created_at_ms), }) .collect()) } fn resolve_keychain_node(store: &Store, node_or_name: &str) -> Result { let view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(store)?); if let Some((node_id, _)) = view .nodes .iter() .find(|(node_id, record)| node_id.as_str() == node_or_name || record.name == node_or_name) { Ok(node_id.clone()) } else { Err(NodeError::ResourceNotFound(format!("node:{node_or_name}"))) } } fn resolve_peer_node_for_control(store: &Store, node_or_name: &str) -> String { resolve_keychain_node(store, node_or_name) .map(|node| node.to_string()) .unwrap_or_else(|_| node_or_name.to_owned()) } fn record_node_grant( store: &Store, node_id: &str, resource: &str, capability: &str, grant_id: Option, ) -> Result { let op = node_grant_op(node_id, resource, capability, grant_id); store_auth_op(store, &op)?; Ok(op) } fn node_grant_op( node_id: &str, resource: &str, capability: &str, grant_id: Option, ) -> AuthOp { let created_at = UnixMillis(geth_store::now_ms()); let grant_id = grant_id.unwrap_or_else(|| generated_grant_id(node_id, resource, capability)); AuthOp { id: generated_auth_op_id("grant-create", resource, &grant_id, created_at), resource: ResourceId::new(resource.to_owned()), created_at, kind: AuthOpKind::GrantCreate { grant_id, principal: PrincipalId::new(node_id.to_owned()), capabilities: vec![Capability::new(capability.to_owned())], }, } } fn stable_slug(value: &str) -> String { let slug = value .chars() .map(|ch| { if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { ch.to_ascii_lowercase() } else { '-' } }) .collect::() .trim_matches('-') .to_owned(); if slug.is_empty() { "unnamed".to_owned() } else { slug } } fn parse_enrollment_capabilities( capabilities: Vec, ) -> Result, NodeError> { capabilities .into_iter() .map(|value| { let (resource, capability) = value .split_once('=') .ok_or_else(|| NodeError::InvalidEnrollmentCapability(value.clone()))?; Ok(NodeEnrollmentCapability { resource: ResourceId::new(resource.to_owned()), capability: Capability::new(capability.to_owned()), }) }) .collect() } fn create_node_enrollment_request( node: &LocalNode, node_name: String, capabilities: Vec, reason: Option, ) -> Result { let key = AgentKey::load(&node.paths.agent_key())?; let created_at = UnixMillis(geth_store::now_ms()); let requested_capabilities = parse_enrollment_capabilities(capabilities)?; let endpoint_id = node.iroh_status.endpoint_id.clone(); let stable = format!( "{}\0{}\0{}\0{}", node.node_id, node.agent_id, node_name, created_at.0 ); Ok(NodeEnrollmentRequest { id: AuthOpId::new(format!( "node-enrollment:{}", geth_crypto::blake3_hex(stable.as_bytes()) )), requester_node: NodeId::new(node.node_id.clone()), requester_agent: node.agent_id.clone().into(), requester_agent_public_key: key.public_key_hex(), requested_node_name: node_name, requested_capabilities, endpoint_id, reason, status: NodeEnrollmentStatus::Pending, created_at, provenance: None, }) } fn sign_node_enrollment_request( node: &LocalNode, request: &mut NodeEnrollmentRequest, ) -> Result<(), NodeError> { let key = AgentKey::load(&node.paths.agent_key())?; let signature = key.sign_canonical( NODE_ENROLLMENT_REQUEST_NAMESPACE, &node_enrollment_request_signing_payload(request), )?; request.provenance = Some(NodeEnrollmentProvenance { namespace: NODE_ENROLLMENT_REQUEST_NAMESPACE.to_owned(), signer_node: NodeId::new(node.node_id.clone()), signer_agent: node.agent_id.clone().into(), signer_public_key: key.public_key_hex(), signature_hex: hex::encode(signature), signed_at: UnixMillis(geth_store::now_ms()), }); Ok(()) } fn verify_node_enrollment_request(request: &NodeEnrollmentRequest) -> Result<(), NodeError> { let provenance = request .provenance .as_ref() .ok_or_else(|| NodeError::InvalidNodeEnrollment("missing signed provenance".to_owned()))?; if provenance.namespace != NODE_ENROLLMENT_REQUEST_NAMESPACE { return Err(NodeError::InvalidNodeEnrollment(format!( "invalid namespace {}", provenance.namespace ))); } if provenance.signer_node != request.requester_node || provenance.signer_agent != request.requester_agent || provenance.signer_public_key != request.requester_agent_public_key { return Err(NodeError::InvalidNodeEnrollment( "provenance does not match requesting node/agent".to_owned(), )); } verify_record_provenance( &provenance.signer_public_key, NODE_ENROLLMENT_REQUEST_NAMESPACE, &node_enrollment_request_signing_payload(request), &provenance.signature_hex, ) } fn store_node_enrollment_request( store: &Store, request: &NodeEnrollmentRequest, ) -> Result<(), NodeError> { store.insert_node_enrollment_request(&StoredNodeEnrollmentRequest { request_id: request.id.to_string(), requester_node: request.requester_node.to_string(), request_json: serde_json::to_string(request)?, status: request.status.to_string(), created_at_ms: request.created_at.0, })?; Ok(()) } fn insert_node_enrollment_request_if_not_conflicting( store: &Store, request: &NodeEnrollmentRequest, ) -> Result { verify_node_enrollment_request(request)?; let stored = StoredNodeEnrollmentRequest { request_id: request.id.to_string(), requester_node: request.requester_node.to_string(), request_json: serde_json::to_string(request)?, status: request.status.to_string(), created_at_ms: request.created_at.0, }; if let Some(existing) = store.get_node_enrollment_request(&stored.request_id)? { return Ok(existing == stored); } store.insert_node_enrollment_request(&stored)?; Ok(true) } fn load_node_enrollment_request( store: &Store, request_id: &str, ) -> Result { store .get_node_enrollment_request(request_id)? .ok_or_else(|| NodeError::NodeEnrollmentRequestNotFound(request_id.to_owned())) .and_then(node_enrollment_request_from_stored) } fn load_node_enrollment_requests(store: &Store) -> Result, NodeError> { store .list_node_enrollment_requests()? .into_iter() .map(node_enrollment_request_from_stored) .collect() } fn node_enrollment_request_from_stored( stored: StoredNodeEnrollmentRequest, ) -> Result { let mut request: NodeEnrollmentRequest = serde_json::from_str(&stored.request_json)?; request.status = stored .status .parse() .map_err(|error: geth_keychain::KeychainError| { NodeError::InvalidNodeEnrollment(error.to_string()) })?; Ok(request) } fn latest_pending_node_enrollment_request(store: &Store) -> Option { load_node_enrollment_requests(store) .ok() .and_then(|requests| { requests .into_iter() .filter(|request| request.status == NodeEnrollmentStatus::Pending) .max_by_key(|request| request.created_at.0) }) } fn write_node_enrollment_request_file( path: &Path, request: &NodeEnrollmentRequest, ) -> Result<(), NodeError> { std::fs::write(path, serde_json::to_vec_pretty(request)?)?; Ok(()) } fn read_node_enrollment_request_file(path: &Path) -> Result { let request = serde_json::from_slice(&std::fs::read(path)?)?; Ok(request) } fn approve_node_enrollment( store: &Store, node: &LocalNode, request_id: &str, signing_key_path: &Path, admin_key_path: Option<&Path>, node_name: Option, extra_capabilities: Vec, ) -> Result { let mut request = load_node_enrollment_request(store, request_id)?; verify_node_enrollment_request(&request)?; let now = UnixMillis(geth_store::now_ms()); let owner = first_keychain_user(store).unwrap_or_else(|| UserId::new("user:owner")); let device = DeviceId::new(format!( "device:{}", stable_slug(request.requester_node.as_str()) )); let node_id = request.requester_node.clone(); let name = node_name.unwrap_or_else(|| request.requested_node_name.clone()); let mut keychain_ops = vec![ KeychainOp { id: generated_keychain_op_id("device-add", device.as_str(), now), created_at: now, kind: KeychainOpKind::DeviceAdd { device: device.clone(), user: owner, }, }, KeychainOp { id: generated_keychain_op_id("node-add", node_id.as_str(), now), created_at: now, kind: KeychainOpKind::NodeAdd { node: node_id.clone(), device, name, }, }, KeychainOp { id: generated_keychain_op_id("agent-bind", request.requester_agent.as_str(), now), created_at: now, kind: KeychainOpKind::AgentBind { agent: request.requester_agent.clone(), node: node_id.clone(), }, }, ]; if let Some(endpoint) = request.endpoint_id.clone() { keychain_ops.push(KeychainOp { id: generated_keychain_op_id("node-endpoint-add", &endpoint, now), created_at: now, kind: KeychainOpKind::NodeEndpointAdd { node: node_id.clone(), endpoint, }, }); } let keychain_signatures = store_and_sign_keychain_ops( store, node, &keychain_ops, Some(signing_key_path), admin_key_path, )?; let mut grant_caps = request.requested_capabilities.clone(); grant_caps.extend(parse_enrollment_capabilities(extra_capabilities)?); let auth_ops = grant_caps .into_iter() .map(|grant| { let resource = grant.resource.to_string(); let capability = grant.capability.to_string(); let grant_id = generated_grant_id(node_id.as_str(), &resource, &capability); AuthOp { id: generated_auth_op_id("grant-create", &resource, &grant_id, now), resource: grant.resource, created_at: now, kind: AuthOpKind::GrantCreate { grant_id, principal: PrincipalId::new(node_id.to_string()), capabilities: vec![grant.capability], }, } }) .collect::>(); let auth_signatures = store_and_sign_auth_ops(store, node, &auth_ops, signing_key_path, admin_key_path)?; request.status = NodeEnrollmentStatus::Approved; store_node_enrollment_request(store, &request)?; Ok(ControlResponse::NodeEnrollmentApproved { request, keychain_ops, keychain_signatures, auth_ops, auth_signatures, note: "approved enrollment; requester can run keychain sync and auth sync from this owner node".to_owned(), }) } fn first_keychain_user(store: &Store) -> Option { geth_keychain::reduce_keychain_ops(&load_keychain_ops(store).ok()?) .users .keys() .next() .cloned() } fn generated_grant_id(subject: &str, resource: &str, capability: &str) -> String { format!( "grant:{}", geth_crypto::blake3_hex(format!("{subject}\0{resource}\0{capability}").as_bytes()) ) } fn generated_file_conflict_id(root: &str, path: &str, kind: &str, created_at_ms: i64) -> String { format!( "file-conflict:{}", geth_crypto::blake3_hex(format!("{created_at_ms}\0{root}\0{path}\0{kind}").as_bytes()) ) } fn generated_auth_op_id( kind: &str, resource: &str, stable_id: &str, created_at: UnixMillis, ) -> AuthOpId { AuthOpId::new(format!( "auth-op:{}", geth_crypto::blake3_hex( format!("{}\0{kind}\0{resource}\0{stable_id}", created_at.0).as_bytes() ) )) } fn generated_keychain_op_id(kind: &str, stable_id: &str, created_at: UnixMillis) -> AuthOpId { AuthOpId::new(format!( "keychain-op:{}", geth_crypto::blake3_hex(format!("{}\0{kind}\0{stable_id}", created_at.0).as_bytes()) )) } fn stable_node_id(agent_id: &str) -> String { format!("node:{agent_id}") } async fn start_daemon_iroh_endpoint( node: &mut LocalNode, ) -> Result, NodeError> { let node_config = GethConfig::load(&node.paths.config_file())?; let relay_mode = node_config.iroh.relay_mode.clone(); let relay_mode_label = relay_mode.label(); let local_discovery = node_config.iroh.local_discovery; let iroh_relay_mode = config_relay_mode_to_iroh(&relay_mode, &node_config.iroh.relay_maps); let mut config = GethIrohConfig::local_with_relay(node.paths.iroh_key(), iroh_relay_mode); config.local_discovery = local_discovery; 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")?; } let blob_store = iroh_blobs::store::fs::FsStore::load(node.paths.cas_dir().join("iroh-blobs")) .await .map_err(|error| { NodeError::IrohPeer(format!("failed to open iroh-blobs store: {error}")) })?; let gossip = iroh_gossip::net::Gossip::builder().spawn(endpoint.endpoint()); let blob_api: iroh_blobs::api::Store = blob_store.clone().into(); let docs = iroh_docs::protocol::Docs::persistent(node.paths.home().join("iroh-docs")) .spawn(endpoint.endpoint(), blob_api, gossip.clone()) .await .map_err(|error| { NodeError::IrohPeer(format!("failed to start iroh-docs: {error}")) })?; let mirrored = mirror_local_cas_to_iroh_blobs(&node.paths, &blob_store).await?; tracing::info!( mirrored_blobs = mirrored, "native iroh-blobs store is ready" ); node.iroh_status = status; *node .iroh_endpoint .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)? = Some(endpoint.clone()); *node .iroh_blob_store .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)? = Some(blob_store); *node .iroh_gossip .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)? = Some(gossip); *node .iroh_docs .lock() .map_err(|_| NodeError::RuntimeLockPoisoned)? = Some(docs); let mirrored_kv = mirror_all_kv_stores_to_iroh_docs(node).await?; tracing::info!( mirrored_kv_stores = mirrored_kv, "native iroh-docs KV store is ready" ); Ok(Some(endpoint)) } Err(error) => { node.iroh_status = EndpointStatus { enabled: false, endpoint_id: None, relay_mode: relay_mode_label, local_discovery, note: format!("Iroh endpoint failed to start: {error}"), }; Ok(None) } } } fn config_relay_mode_to_iroh( mode: &RelayMode, relay_maps: &std::collections::BTreeMap, ) -> GethRelayMode { match mode { RelayMode::Disabled => GethRelayMode::Disabled, RelayMode::Default => GethRelayMode::Default, RelayMode::Staging => GethRelayMode::Staging, RelayMode::Custom { map } => { let relay_map = relay_maps .get(map) .expect("custom relay map was validated during config load"); GethRelayMode::Custom { name: map.clone(), relay_urls: relay_map.urls.clone(), } } } } fn expected_openssh_cert_path(public_key_path: &Path) -> String { let text = public_key_path.display().to_string(); if let Some(prefix) = text.strip_suffix(".pub") { format!("{prefix}-cert.pub") } else { format!("{text}-cert.pub") } } fn sign_ssh_cert_request(node: &LocalNode, request: &mut SshCertRequest) -> Result<(), NodeError> { let key = AgentKey::load(&node.paths.agent_key())?; let signature = key.sign_canonical( SSH_CERT_REQUEST_NAMESPACE, &ssh_cert_request_signing_payload(request), )?; request.provenance = Some(SshRecordProvenance { namespace: SSH_CERT_REQUEST_NAMESPACE.to_owned(), signer_node: NodeId::new(node.node_id.clone()), signer_agent: node.agent_id.clone(), signer_public_key: key.public_key_hex(), signature_hex: hex::encode(signature), signed_at: UnixMillis(geth_store::now_ms()), }); Ok(()) } fn sign_ssh_certificate( node: &LocalNode, certificate: &mut SshCertificateRecord, ) -> Result<(), NodeError> { let key = AgentKey::load(&node.paths.agent_key())?; let signature = key.sign_canonical( SSH_CERT_ISSUANCE_NAMESPACE, &ssh_certificate_signing_payload(certificate), )?; certificate.provenance = Some(SshRecordProvenance { namespace: SSH_CERT_ISSUANCE_NAMESPACE.to_owned(), signer_node: NodeId::new(node.node_id.clone()), signer_agent: node.agent_id.clone(), signer_public_key: key.public_key_hex(), signature_hex: hex::encode(signature), signed_at: UnixMillis(geth_store::now_ms()), }); Ok(()) } fn sign_ssh_revocation( node: &LocalNode, revocation: &mut SshRevocationEntry, ) -> Result<(), NodeError> { let key = AgentKey::load(&node.paths.agent_key())?; let signature = key.sign_canonical( SSH_REVOCATION_LIST_NAMESPACE, &ssh_revocation_signing_payload(revocation), )?; revocation.provenance = Some(SshRecordProvenance { namespace: SSH_REVOCATION_LIST_NAMESPACE.to_owned(), signer_node: NodeId::new(node.node_id.clone()), signer_agent: node.agent_id.clone(), signer_public_key: key.public_key_hex(), signature_hex: hex::encode(signature), signed_at: UnixMillis(geth_store::now_ms()), }); Ok(()) } fn verify_ssh_cert_request_provenance(request: &SshCertRequest) -> Result<(), NodeError> { let provenance = request.provenance.as_ref().ok_or_else(|| { NodeError::IrohPeer(format!( "SSH cert request {} is missing signed provenance", request.id )) })?; if provenance.namespace != SSH_CERT_REQUEST_NAMESPACE { return Err(NodeError::IrohPeer(format!( "SSH cert request {} uses invalid provenance namespace {}", request.id, provenance.namespace ))); } verify_record_provenance( &provenance.signer_public_key, SSH_CERT_REQUEST_NAMESPACE, &ssh_cert_request_signing_payload(request), &provenance.signature_hex, ) } fn verify_ssh_certificate_provenance(certificate: &SshCertificateRecord) -> Result<(), NodeError> { let provenance = certificate.provenance.as_ref().ok_or_else(|| { NodeError::IrohPeer(format!( "SSH certificate {} is missing signed provenance", certificate.id )) })?; if provenance.namespace != SSH_CERT_ISSUANCE_NAMESPACE { return Err(NodeError::IrohPeer(format!( "SSH certificate {} uses invalid provenance namespace {}", certificate.id, provenance.namespace ))); } verify_record_provenance( &provenance.signer_public_key, SSH_CERT_ISSUANCE_NAMESPACE, &ssh_certificate_signing_payload(certificate), &provenance.signature_hex, ) } fn verify_ssh_revocation_provenance(revocation: &SshRevocationEntry) -> Result<(), NodeError> { let provenance = revocation.provenance.as_ref().ok_or_else(|| { NodeError::IrohPeer(format!( "SSH revocation {} is missing signed provenance", revocation.id )) })?; if provenance.namespace != SSH_REVOCATION_LIST_NAMESPACE { return Err(NodeError::IrohPeer(format!( "SSH revocation {} uses invalid provenance namespace {}", revocation.id, provenance.namespace ))); } verify_record_provenance( &provenance.signer_public_key, SSH_REVOCATION_LIST_NAMESPACE, &ssh_revocation_signing_payload(revocation), &provenance.signature_hex, ) } fn verify_record_provenance( public_key_hex: &str, namespace: &str, payload: &T, signature_hex: &str, ) -> Result<(), NodeError> { let public_key = hex::decode(public_key_hex).map_err(|error| NodeError::IrohPeer(error.to_string()))?; let signature = hex::decode(signature_hex).map_err(|error| NodeError::IrohPeer(error.to_string()))?; geth_crypto::verify_canonical(&public_key, namespace, payload, &signature)?; Ok(()) } fn stored_from_ssh_cert_request(request: &SshCertRequest) -> StoredSshCertRequest { StoredSshCertRequest { request_id: request.id.to_string(), requester_node: request.requester_node.to_string(), public_key: request.public_key.clone(), public_key_fingerprint: request.public_key_fingerprint.clone(), cert_kind: request.cert_kind.to_string(), principals: request.principals.clone(), requested_validity: request.requested_validity.clone(), renewal_of: request.renewal_of.as_ref().map(ToString::to_string), reason: request.reason.clone(), status: request.status.to_string(), created_at_ms: request.created_at.0, provenance_json: request .provenance .as_ref() .map(serde_json::to_string) .transpose() .expect("SSH provenance serializes"), } } fn insert_ssh_cert_request_if_not_conflicting( store: &Store, request: &SshCertRequest, ) -> Result { let stored = stored_from_ssh_cert_request(request); if let Some(existing) = store.get_ssh_cert_request(&stored.request_id)? { if existing != stored { tracing::warn!( request_id = %stored.request_id, "rejected conflicting SSH certificate request during sync" ); } return Ok(false); } verify_ssh_cert_request_provenance(request)?; store.insert_ssh_cert_request(&stored)?; Ok(true) } fn ssh_cert_request_from_stored(stored: StoredSshCertRequest) -> Result { let cert_kind = stored .cert_kind .parse::() .map_err(|_| NodeError::InvalidSshCertKind(stored.cert_kind.clone()))?; let status = stored .status .parse::() .map_err(|_| NodeError::InvalidSshCertStatus(stored.status.clone()))?; Ok(SshCertRequest { id: SshCertRequestId::new(stored.request_id), requester_node: NodeId::new(stored.requester_node), public_key: stored.public_key, public_key_fingerprint: stored.public_key_fingerprint, cert_kind, principals: stored.principals, requested_validity: stored.requested_validity, renewal_of: stored.renewal_of.map(SshCertId::new), reason: stored.reason, status, created_at: UnixMillis(stored.created_at_ms), provenance: stored .provenance_json .map(|json| serde_json::from_str(&json)) .transpose()?, }) } fn stored_from_ssh_certificate(certificate: &SshCertificateRecord) -> StoredSshCertificate { StoredSshCertificate { cert_id: certificate.id.to_string(), request_id: certificate.request_id.to_string(), certificate: certificate.certificate.clone(), certificate_fingerprint: certificate.certificate_fingerprint.clone(), imported_at_ms: certificate.imported_at.0, provenance_json: certificate .provenance .as_ref() .map(serde_json::to_string) .transpose() .expect("SSH provenance serializes"), } } fn insert_ssh_certificate_if_not_conflicting( store: &Store, certificate: &SshCertificateRecord, ) -> Result { let stored = stored_from_ssh_certificate(certificate); if let Some(existing) = store .list_ssh_certificates()? .into_iter() .find(|existing| existing.cert_id == stored.cert_id) { if existing != stored { tracing::warn!( cert_id = %stored.cert_id, "rejected conflicting SSH certificate during sync" ); } return Ok(false); } verify_ssh_certificate_provenance(certificate)?; store.insert_ssh_certificate(&stored)?; Ok(true) } fn ssh_certificate_from_stored( stored: StoredSshCertificate, ) -> Result { Ok(SshCertificateRecord { id: SshCertId::new(stored.cert_id), request_id: SshCertRequestId::new(stored.request_id), certificate: stored.certificate, certificate_fingerprint: stored.certificate_fingerprint, imported_at: UnixMillis(stored.imported_at_ms), provenance: stored .provenance_json .map(|json| serde_json::from_str(&json)) .transpose()?, }) } fn stored_from_ssh_revocation(revocation: &SshRevocationEntry) -> StoredSshRevocation { StoredSshRevocation { revocation_id: revocation.id.to_string(), kind: revocation.kind.to_string(), target: revocation.target.clone(), reason: revocation.reason.clone(), created_at_ms: revocation.created_at.0, published: revocation.published, provenance_json: revocation .provenance .as_ref() .map(serde_json::to_string) .transpose() .expect("SSH provenance serializes"), } } fn insert_ssh_revocation_if_not_conflicting( store: &Store, revocation: &SshRevocationEntry, ) -> Result { let stored = stored_from_ssh_revocation(revocation); if let Some(existing) = store .list_ssh_revocations()? .into_iter() .find(|existing| existing.revocation_id == stored.revocation_id) { if existing != stored { tracing::warn!( revocation_id = %stored.revocation_id, "rejected conflicting SSH revocation during sync" ); } return Ok(false); } verify_ssh_revocation_provenance(revocation)?; store.insert_ssh_revocation(&stored)?; Ok(true) } fn ssh_cert_distribution_log_entries( requests: Vec, certificates: Vec, ) -> Vec { let mut entries = requests .into_iter() .map(SshDistributionLogEntry::cert_request) .chain( certificates .into_iter() .map(SshDistributionLogEntry::certificate), ) .collect::>(); entries.sort_by(|left, right| { left.timestamp() .0 .cmp(&right.timestamp().0) .then_with(|| left.entry_id().cmp(right.entry_id())) }); entries } fn ssh_revocation_distribution_log_entries( revocations: Vec, ) -> Vec { let mut entries = revocations .into_iter() .map(SshDistributionLogEntry::revocation) .collect::>(); entries.sort_by(|left, right| { left.timestamp() .0 .cmp(&right.timestamp().0) .then_with(|| left.entry_id().cmp(right.entry_id())) }); entries } fn import_ssh_distribution_log_entries( store: &Store, entries: &[SshDistributionLogEntry], ) -> Result<(usize, usize, usize), NodeError> { let mut requests_imported = 0; let mut certificates_imported = 0; let mut revocations_imported = 0; for entry in entries { match entry { SshDistributionLogEntry::CertRequest { request, .. } => { if insert_ssh_cert_request_if_not_conflicting(store, request)? { requests_imported += 1; } } SshDistributionLogEntry::Certificate { certificate, .. } => { if insert_ssh_certificate_if_not_conflicting(store, certificate)? { certificates_imported += 1; } } SshDistributionLogEntry::Revocation { revocation, .. } => { if insert_ssh_revocation_if_not_conflicting(store, revocation)? { revocations_imported += 1; } } } } Ok(( requests_imported, certificates_imported, revocations_imported, )) } fn ssh_revocation_from_stored( stored: StoredSshRevocation, ) -> Result { let kind = stored .kind .parse::() .map_err(|_| NodeError::InvalidSshRevocationKind(stored.kind.clone()))?; Ok(SshRevocationEntry { id: geth_types::SshRevocationId::new(stored.revocation_id), kind, target: stored.target, reason: stored.reason, created_at: UnixMillis(stored.created_at_ms), published: stored.published, provenance: stored .provenance_json .map(|json| serde_json::from_str(&json)) .transpose()?, }) } #[cfg(test)] mod tests { use super::*; use tokio::io::AsyncReadExt; #[derive(Debug, PartialEq, Eq)] enum RemoteGuardKind { Capability, SignedLog, OwnerReview, CandidateOnly, } #[derive(Debug)] struct RemoteGuardAudit { operation: &'static str, mutates_or_opens_service: bool, guard: RemoteGuardKind, resource: Option<&'static str>, capability: Option<&'static str>, } fn remote_guard_audit_matrix() -> Vec { vec![ RemoteGuardAudit { operation: "peer.ping", mutates_or_opens_service: false, guard: RemoteGuardKind::CandidateOnly, resource: None, capability: None, }, RemoteGuardAudit { operation: "sync.status", mutates_or_opens_service: false, guard: RemoteGuardKind::Capability, resource: Some("per-stream"), capability: Some("per-stream"), }, RemoteGuardAudit { operation: "keychain.sync", mutates_or_opens_service: true, guard: RemoteGuardKind::SignedLog, resource: Some("keychain"), capability: None, }, RemoteGuardAudit { operation: "auth.sync", mutates_or_opens_service: true, guard: RemoteGuardKind::SignedLog, resource: Some("resource-auth"), capability: None, }, RemoteGuardAudit { operation: "node.enrollment.submit", mutates_or_opens_service: true, guard: RemoteGuardKind::OwnerReview, resource: Some("node-enrollment"), capability: None, }, RemoteGuardAudit { operation: "cas.fetch", mutates_or_opens_service: false, guard: RemoteGuardKind::Capability, resource: Some("resource:cas:local"), capability: Some("cas.fetch"), }, RemoteGuardAudit { operation: "cas.root.sync", mutates_or_opens_service: false, guard: RemoteGuardKind::Capability, resource: Some("resource:cas-tree:"), capability: Some("cas.fetch"), }, RemoteGuardAudit { operation: "ssh.cert.sync", mutates_or_opens_service: false, guard: RemoteGuardKind::Capability, resource: Some("resource:ssh:certs"), capability: Some("ssh_cert.sync"), }, RemoteGuardAudit { operation: "ssh.revocation.sync", mutates_or_opens_service: false, guard: RemoteGuardKind::Capability, resource: Some("resource:ssh:revocations"), capability: Some("ssh_revocation.sync"), }, RemoteGuardAudit { operation: "kv.sync", mutates_or_opens_service: false, guard: RemoteGuardKind::Capability, resource: Some("resource:kv:"), capability: Some("kv.read"), }, RemoteGuardAudit { operation: "document.sync", mutates_or_opens_service: false, guard: RemoteGuardKind::Capability, resource: Some("resource:document:"), capability: Some("document.read"), }, RemoteGuardAudit { operation: "db.sync", mutates_or_opens_service: false, guard: RemoteGuardKind::Capability, resource: Some("resource:db:"), capability: Some("db.sync"), }, RemoteGuardAudit { operation: "pubsub.publish", mutates_or_opens_service: true, guard: RemoteGuardKind::Capability, resource: Some("resource:pubsub:"), capability: Some("pubsub.publish"), }, RemoteGuardAudit { operation: "pubsub.subscribe", mutates_or_opens_service: false, guard: RemoteGuardKind::Capability, resource: Some("resource:pubsub:"), capability: Some("pubsub.subscribe"), }, RemoteGuardAudit { operation: "pipe.connect", mutates_or_opens_service: true, guard: RemoteGuardKind::Capability, resource: Some("resource:pipe:"), capability: Some("pipe.connect"), }, RemoteGuardAudit { operation: "pipe.listen", mutates_or_opens_service: true, guard: RemoteGuardKind::Capability, resource: Some("resource:pipe:"), capability: Some("pipe.listen"), }, RemoteGuardAudit { operation: "pipe.send", mutates_or_opens_service: true, guard: RemoteGuardKind::Capability, resource: Some("resource:pipe:"), capability: Some("pipe.connect"), }, RemoteGuardAudit { operation: "pipe.tcp.forward", mutates_or_opens_service: true, guard: RemoteGuardKind::Capability, resource: Some("resource:pipe-tcp:"), capability: Some("pipe.forward"), }, RemoteGuardAudit { operation: "pipe.unix.forward", mutates_or_opens_service: true, guard: RemoteGuardKind::Capability, resource: Some("resource:pipe-unix:"), capability: Some("pipe.forward"), }, RemoteGuardAudit { operation: "ssh.proxy.connect", mutates_or_opens_service: true, guard: RemoteGuardKind::Capability, resource: Some("resource:ssh-proxy:local"), capability: Some("ssh_proxy.connect"), }, RemoteGuardAudit { operation: "ssh.admin-shell", mutates_or_opens_service: true, guard: RemoteGuardKind::Capability, resource: Some("resource:ssh-proxy:local"), capability: Some("ssh_proxy.admin_shell"), }, ] } #[test] fn remote_authorization_audit_matrix_covers_mutating_and_service_operations() { let matrix = remote_guard_audit_matrix(); assert_eq!(matrix.len(), 21); for item in matrix.iter().filter(|item| item.mutates_or_opens_service) { assert_ne!( item.guard, RemoteGuardKind::CandidateOnly, "{} must not rely on peer-card discovery alone", item.operation ); if item.guard == RemoteGuardKind::Capability { assert!( item.resource.is_some() && item.capability.is_some(), "{} capability guard must document resource and capability", item.operation ); } } } fn write_offline_iroh_config(paths: &GethPaths) { std::fs::write( paths.config_file(), "[iroh]\nrelay_mode = \"disabled\"\nlocal_discovery = false\n", ) .expect("write config"); } fn create_mock_crsqlite_db(path: &Path, change_db_version: Option) { let conn = rusqlite::Connection::open(path).expect("open sqlite"); conn.execute("CREATE TABLE notes(id INTEGER PRIMARY KEY, body TEXT)", []) .expect("create notes"); conn.execute( r#"CREATE TABLE crsql_changes( table_name TEXT NOT NULL, pk BLOB NOT NULL, cid TEXT NOT NULL, val BLOB, col_version INTEGER NOT NULL, db_version INTEGER NOT NULL, site_id BLOB, cl INTEGER, seq INTEGER )"#, [], ) .expect("create crsql_changes"); if let Some(db_version) = change_db_version { conn.execute( "INSERT INTO crsql_changes(table_name, pk, cid, val, col_version, db_version, site_id, cl, seq) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", ( "notes", vec![db_version as u8], "body", Vec::from("hello".as_bytes()), 1_i64, db_version, vec![1_u8], 1_i64, db_version, ), ) .expect("insert initial mock crsqlite change"); } } fn insert_mock_crsqlite_change(path: &Path, db_version: i64, body: &str) { let conn = rusqlite::Connection::open(path).expect("open sqlite"); conn.execute( "INSERT INTO crsql_changes(table_name, pk, cid, val, col_version, db_version, site_id, cl, seq) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", ( "notes", vec![db_version as u8], "body", body.as_bytes().to_vec(), 1_i64, db_version, vec![1_u8], 1_i64, db_version, ), ) .expect("insert mock crsqlite change"); } fn grant_test_capability(store: &Store, peer: &str, resource: &str, capability: &str) { let created_at = UnixMillis(geth_store::now_ms()); store_auth_op( store, &AuthOp { id: generated_auth_op_id("grant-create", resource, capability, created_at), resource: ResourceId::new(resource.to_owned()), created_at, kind: AuthOpKind::GrantCreate { grant_id: format!("grant:{peer}:{resource}:{capability}"), principal: PrincipalId::new(peer.to_owned()), capabilities: vec![Capability::new(capability.to_owned())], }, }, ) .expect("store auth grant"); } #[test] fn ssh_sync_import_rejects_conflicting_records() { let store = Store::open_memory().expect("open"); let mut request = SshCertRequest { id: SshCertRequestId::new("ssh-cert-request:1"), requester_node: NodeId::new("node:right"), public_key: "ssh-ed25519 AAAA".to_owned(), public_key_fingerprint: "SHA256:one".to_owned(), cert_kind: SshCertKind::User, principals: vec!["eric".to_owned()], requested_validity: None, renewal_of: None, reason: Some("original".to_owned()), status: SshCertRequestStatus::Pending, created_at: UnixMillis(1), provenance: None, }; add_test_ssh_cert_request_provenance(&mut request); assert!( insert_ssh_cert_request_if_not_conflicting(&store, &request).expect("insert request") ); let mut conflicting_request = request.clone(); conflicting_request.reason = Some("conflict".to_owned()); assert!( !insert_ssh_cert_request_if_not_conflicting(&store, &conflicting_request) .expect("reject conflicting request") ); let mut unsigned_request = request.clone(); unsigned_request.id = SshCertRequestId::new("ssh-cert-request:unsigned"); unsigned_request.provenance = None; assert!(insert_ssh_cert_request_if_not_conflicting(&store, &unsigned_request).is_err()); assert_eq!( store .get_ssh_cert_request(request.id.as_str()) .expect("get request") .expect("request") .reason .as_deref(), Some("original") ); let mut revocation = SshRevocationEntry { id: geth_types::SshRevocationId::new("ssh-revocation:1"), kind: SshRevocationKind::KeyId, target: "old-key".to_owned(), reason: Some("original".to_owned()), created_at: UnixMillis(2), published: true, provenance: None, }; add_test_ssh_revocation_provenance(&mut revocation); assert!( insert_ssh_revocation_if_not_conflicting(&store, &revocation) .expect("insert revocation") ); let mut conflicting_revocation = revocation.clone(); conflicting_revocation.target = "other-key".to_owned(); assert!( !insert_ssh_revocation_if_not_conflicting(&store, &conflicting_revocation) .expect("reject conflicting revocation") ); let mut unsigned_revocation = revocation.clone(); unsigned_revocation.id = geth_types::SshRevocationId::new("ssh-revocation:unsigned"); unsigned_revocation.provenance = None; assert!(insert_ssh_revocation_if_not_conflicting(&store, &unsigned_revocation).is_err()); assert_eq!( store .list_ssh_revocations() .expect("list revocations") .first() .expect("revocation") .target, "old-key" ); } fn add_test_ssh_cert_request_provenance(request: &mut SshCertRequest) { let key = AgentKey::generate(); let signature = key .sign_canonical( SSH_CERT_REQUEST_NAMESPACE, &ssh_cert_request_signing_payload(request), ) .expect("sign request"); request.provenance = Some(SshRecordProvenance { namespace: SSH_CERT_REQUEST_NAMESPACE.to_owned(), signer_node: NodeId::new("node:right"), signer_agent: key.agent_id().to_string(), signer_public_key: key.public_key_hex(), signature_hex: hex::encode(signature), signed_at: UnixMillis(1), }); } fn add_test_ssh_revocation_provenance(revocation: &mut SshRevocationEntry) { let key = AgentKey::generate(); let signature = key .sign_canonical( SSH_REVOCATION_LIST_NAMESPACE, &ssh_revocation_signing_payload(revocation), ) .expect("sign revocation"); revocation.provenance = Some(SshRecordProvenance { namespace: SSH_REVOCATION_LIST_NAMESPACE.to_owned(), signer_node: NodeId::new("node:right"), signer_agent: key.agent_id().to_string(), signer_public_key: key.public_key_hex(), signature_hex: hex::encode(signature), signed_at: UnixMillis(2), }); } #[test] fn sync_watermarks_include_only_authorized_streams() { let store = Store::open_memory().expect("open"); store .insert_resource(&StoredResource { resource_id: "resource:kv:prefs".to_owned(), kind: "kv".to_owned(), name: "prefs".to_owned(), status: "active".to_owned(), }) .expect("insert kv resource"); store .insert_kv_store(&StoredKvStore { kv_id: "kv:prefs".to_owned(), resource_id: "resource:kv:prefs".to_owned(), name: "prefs".to_owned(), }) .expect("insert kv"); store .set_kv_entry(&StoredKvEntry { kv_id: "kv:prefs".to_owned(), key: "theme".to_owned(), value: "dark".to_owned(), updated_at_ms: 42, }) .expect("set kv"); store .insert_resource(&StoredResource { resource_id: "resource:document:notes".to_owned(), kind: "document".to_owned(), name: "notes".to_owned(), status: "active".to_owned(), }) .expect("insert document resource"); store .insert_document_resource(&StoredDocumentResource { document_id: "document:notes".to_owned(), resource_id: "resource:document:notes".to_owned(), name: "notes".to_owned(), state_json: "{}".to_owned(), updated_at_ms: 99, }) .expect("insert document"); let dir = tempfile::tempdir().expect("tempdir"); let db_path = dir.path().join("notes.sqlite"); create_mock_crsqlite_db(&db_path, Some(7)); store .insert_resource(&StoredResource { resource_id: "resource:db:notes".to_owned(), kind: "db".to_owned(), name: "notes".to_owned(), status: "active".to_owned(), }) .expect("insert db resource"); store .insert_db_resource(&StoredDbResource { db_id: "db:notes".to_owned(), resource_id: "resource:db:notes".to_owned(), name: "notes".to_owned(), path: db_path.display().to_string(), }) .expect("insert db"); store .upsert_file_root(&StoredFileRoot { root_id: "file-root:shared".to_owned(), resource_id: "resource:cas-tree:shared".to_owned(), name: "shared".to_owned(), path: dir.path().join("shared").display().to_string(), latest_tree_hash: Some("abc".to_owned()), latest_tree_json: None, updated_at_ms: 123, }) .expect("insert file root"); store .upsert_file_root(&StoredFileRoot { root_id: "file-root:remote:node:right:shared".to_owned(), resource_id: "resource:cas-tree:shared".to_owned(), name: "remote-node-right-shared".to_owned(), path: "remote:node:right:shared".to_owned(), latest_tree_hash: Some("def".to_owned()), latest_tree_json: None, updated_at_ms: 456, }) .expect("insert imported remote file root"); grant_test_capability(&store, "node:left", "resource:kv:prefs", "kv.read"); grant_test_capability(&store, "node:left", "resource:db:notes", "db.sync"); grant_test_capability(&store, "node:left", "resource:cas-tree:shared", "cas.fetch"); let watermarks = sync_watermarks_for_peer(&store, "node:left").expect("watermarks"); assert!( watermarks .iter() .any(|watermark| watermark.stream == "auth") ); assert!( watermarks .iter() .any(|watermark| watermark.stream == "keychain") ); assert!(watermarks.contains(&SyncWatermark { stream: "kv:prefs".to_owned(), high_water: 42, })); assert!(watermarks.contains(&SyncWatermark { stream: "db:notes".to_owned(), high_water: 7, })); assert!(watermarks.contains(&SyncWatermark { stream: "cas-tree:shared".to_owned(), high_water: 123, })); assert!( !watermarks .iter() .any(|watermark| watermark.stream == "cas-tree:remote-node-right-shared") ); assert!( !watermarks .iter() .any(|watermark| watermark.stream == "document:notes") ); assert!( !watermarks .iter() .any(|watermark| watermark.stream == "ssh-certs") ); } #[test] fn sync_status_local_reports_recorded_stream_health() { let store = Store::open_memory().expect("open"); store .upsert_peer_card(&StoredPeerCard { peer_id: "node:left".to_owned(), card_json: "{}".to_owned(), updated_at_ms: 1, }) .expect("insert peer"); store_live_sync_cursor(&store, "node:left", "keychain", 42).expect("store cursor"); record_live_sync_success(&store, "node:left", "keychain", 3, 1, Some(42)) .expect("record success"); let response = sync_status_local(&store).expect("sync status"); let ControlResponse::SyncStatus { peers, .. } = response else { panic!("unexpected sync status response"); }; assert_eq!(peers.len(), 1); assert_eq!(peers[0].peer_node_id, "node:left"); assert_eq!(peers[0].streams.len(), 1); assert_eq!(peers[0].streams[0].stream, "keychain"); assert_eq!(peers[0].streams[0].cursor_ms, 42); assert_eq!(peers[0].streams[0].state, "ok"); assert!(!peers[0].streams[0].stale); assert_eq!(peers[0].streams[0].stale_after_ms, LIVE_SYNC_STALE_AFTER_MS); assert_eq!(peers[0].streams[0].last_imported, 3); assert_eq!(peers[0].streams[0].last_rejected, 1); assert!(peers[0].streams[0].last_error.is_none()); assert_eq!(peers[0].streams[0].next_action, "none"); } #[test] fn operator_errors_include_recovery_commands() { let peer_error = operator_error_message(&NodeError::PeerNotFound("node:missing".to_owned())); assert!(peer_error.contains("geth peer import")); assert!(peer_error.contains("geth peer list")); let denied_error = operator_error_message(&NodeError::Unauthorized("missing grant".to_owned())); assert!(denied_error.contains("geth auth explain")); assert!(denied_error.contains("geth auth grant")); } #[test] fn file_root_sync_records_automatic_three_tree_conflicts() { let store = Store::open_memory().expect("open"); let base_tree = test_tree(vec![ test_tree_file("edit.txt", "01"), test_tree_file("delete-edit.txt", "02"), test_tree_file("rename.txt", "03"), ]); let local_tree = test_tree(vec![ test_tree_file("edit.txt", "04"), test_tree_file("delete-edit.txt", "05"), test_tree_file("local-rename.txt", "03"), ]); let remote_tree = test_tree(vec![ test_tree_file("edit.txt", "06"), test_tree_file("remote-rename.txt", "03"), ]); let local_root = StoredFileRoot { root_id: "file-root:shared".to_owned(), resource_id: "resource:cas-tree:shared".to_owned(), name: "shared".to_owned(), path: "/tmp/shared".to_owned(), latest_tree_hash: Some("local-tree".to_owned()), latest_tree_json: Some(serde_json::to_string(&local_tree).expect("local tree json")), updated_at_ms: 2, }; store.upsert_file_root(&local_root).expect("upsert root"); let conflicts = record_sync_tree_conflicts( &store, &local_root, Some("base-tree"), &base_tree, local_root.latest_tree_hash.as_deref(), Some(&BlobHash::new("remote-tree".to_owned())), &remote_tree, ) .expect("record conflicts"); assert_eq!(conflicts.len(), 3); assert!(conflicts.iter().any(|conflict| { conflict.path == "edit.txt" && conflict.kind == FileConflictKind::ConcurrentEdit })); assert!(conflicts.iter().any(|conflict| { conflict.path == "delete-edit.txt" && conflict.kind == FileConflictKind::DeleteEdit })); assert!(conflicts.iter().any(|conflict| { conflict.path == "rename.txt" && conflict.kind == FileConflictKind::Rename })); let stored = store .list_file_conflicts(Some("shared")) .expect("list conflicts"); assert_eq!(stored.len(), 3); assert!(stored.iter().all(|conflict| { conflict.base_tree_hash.as_deref() == Some("base-tree") && conflict.local_tree_hash.as_deref() == Some("local-tree") && conflict.remote_tree_hash.as_deref() == Some("remote-tree") && conflict .detail .contains("syncing remote file-root metadata") })); } #[test] fn lan_discovery_address_selection_uses_iroh_direct_addresses() { let key = AgentKey::generate(); let card = PeerCard::signed( "node:test".into(), &key, vec![EndpointCandidate { endpoint_id: "endpoint:test".to_owned(), relay_url: None, direct_addresses: vec![ "127.0.0.1:1111".to_owned(), "127.0.0.2:1111".to_owned(), "127.0.0.3:2222".to_owned(), ], source: DiscoverySource::Mdns, }], UnixMillis(1), ) .expect("peer card"); let (port, addrs) = lan_discovery_addrs(&card).expect("lan addresses"); assert_eq!(port, 1111); assert_eq!(addrs.len(), 2); } #[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 right_blob = LocalCas::new(right.paths.cas_dir()) .add_bytes(b"remote cas bytes") .expect("right cas add"); let left_file_root = left_home.path().join("files"); std::fs::create_dir_all(&left_file_root).expect("create left file root"); std::fs::write(left_file_root.join("note.txt"), b"local tree").expect("write left file"); handle_request( &left, ControlRequest::CasRootAdd { name: "shared".to_owned(), path: left_file_root.clone(), }, ) .expect("left file root add"); let right_file_root = right_home.path().join("files"); std::fs::create_dir_all(&right_file_root).expect("create right file root"); std::fs::write(right_file_root.join("note.txt"), b"synced tree").expect("write right file"); handle_request( &right, ControlRequest::CasRootAdd { name: "shared".to_owned(), path: right_file_root.clone(), }, ) .expect("right file root add"); let right_tree_hash = match handle_request( &right, ControlRequest::CasRootScan { name: "shared".to_owned(), }, ) .expect("right file root scan") { ControlResponse::CasRootScanned { scan } => scan.tree.hash, other => panic!("unexpected right file root scan response: {other:?}"), }; let right_pubkey = right_home.path().join("request.pub"); std::fs::write( &right_pubkey, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGV0aA== node\n", ) .expect("write right public key"); let requested = handle_request( &right, ControlRequest::SshCertRequest { public_key_path: right_pubkey, cert_kind: "user".to_owned(), principals: vec!["eric".to_owned()], requested_validity: Some("+52w".to_owned()), renewal_of: None, reason: Some("test sync".to_owned()), subject: None, }, ) .expect("right ssh cert request"); let right_request_id = match requested { ControlResponse::SshCertRequested { request } => request.id, other => panic!("unexpected SSH cert request response: {other:?}"), }; let added_revocation = handle_request( &right, ControlRequest::SshRevocationAdd { kind: "key-id".to_owned(), target: "old-node-key".to_owned(), reason: Some("test sync".to_owned()), subject: None, }, ) .expect("right ssh revocation"); let right_revocation_id = match added_revocation { ControlResponse::SshRevocationAdded { revocation } => revocation.id, other => panic!("unexpected SSH revocation response: {other:?}"), }; handle_request( &right, ControlRequest::KvCreate { name: "prefs".to_owned(), }, ) .expect("right kv create"); handle_request( &right, ControlRequest::KvSet { name: "prefs".to_owned(), key: "apps/foo/theme".to_owned(), value: "dark".to_owned(), subject: None, }, ) .expect("right kv set"); handle_request( &right, ControlRequest::PipeListen { name: "inbox".to_owned(), node: None, bearer_secret: None, }, ) .expect("right pipe listen"); handle_request( &right, ControlRequest::DocumentCreate { name: "notes".to_owned(), }, ) .expect("right document create"); handle_request( &right, ControlRequest::DocumentSet { name: "notes".to_owned(), state_json: r#"{"title":"remote"}"#.to_owned(), }, ) .expect("right document set"); let left_db_path = left_home.path().join("notes.sqlite"); let right_db_path = right_home.path().join("notes.sqlite"); create_mock_crsqlite_db(&left_db_path, None); create_mock_crsqlite_db(&right_db_path, Some(7)); handle_request( &left, ControlRequest::DbAdd { name: "notes".to_owned(), path: left_db_path.clone(), }, ) .expect("left db add"); handle_request( &right, ControlRequest::DbAdd { name: "notes".to_owned(), path: right_db_path.clone(), }, ) .expect("right db add"); 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:?}"), } let denied = handle_request_async( &left, ControlRequest::PeerAuthCheck { node: right_card.node_id.to_string(), resource: "resource:cas:local".to_owned(), capability: "cas.fetch".to_owned(), }, ) .await .expect("denied peer auth check"); match denied { ControlResponse::PeerAuthChecked { allowed, reason, note, .. } => { assert!(!allowed); assert!(reason.contains("no active direct or group grant")); assert!(note.contains("authenticated endpoint/card binding")); } other => panic!("unexpected denied auth response: {other:?}"), } let denied_fetch = handle_request_async( &left, ControlRequest::CasFetch { node: right_card.node_id.to_string(), hash: right_blob.hash.clone(), bearer_secret: None, }, ) .await .expect("denied cas fetch"); match denied_fetch { ControlResponse::CasFetched { allowed, reason, size_bytes, .. } => { assert!(!allowed); assert_eq!(size_bytes, 0); assert!(reason.contains("no active direct or group grant")); assert!( !LocalCas::new(left.paths.cas_dir()) .has(&right_blob.hash) .expect("left cas has after denied fetch") ); } other => panic!("unexpected denied CAS fetch response: {other:?}"), } let denied_root_sync = handle_request_async( &left, ControlRequest::CasRootSync { node: right_card.node_id.to_string(), name: "shared".to_owned(), bearer_secret: None, }, ) .await .expect("denied file root sync"); match denied_root_sync { ControlResponse::CasRootSynced { allowed, root, reason, .. } => { assert!(!allowed); assert!(root.is_none()); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied file root sync response: {other:?}"), } let denied_kv_sync = handle_request_async( &left, ControlRequest::KvSync { node: right_card.node_id.to_string(), name: "prefs".to_owned(), bearer_secret: None, }, ) .await .expect("denied kv sync"); match denied_kv_sync { ControlResponse::KvSynced { allowed, entries_imported, reason, .. } => { assert!(!allowed); assert_eq!(entries_imported, 0); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied KV sync response: {other:?}"), } let denied_pubsub = handle_request_async( &left, ControlRequest::PubsubPub { topic: "presence/test".to_owned(), message: "hello".to_owned(), node: Some(right_card.node_id.to_string()), bearer_secret: None, }, ) .await .expect("denied remote pubsub publish"); match denied_pubsub { ControlResponse::PubsubRemotePublished { allowed, reason, message, .. } => { assert!(!allowed); assert!(message.message.is_empty()); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied pubsub publish response: {other:?}"), } let denied_pubsub_subscribe = handle_request_async( &left, ControlRequest::PubsubSub { topic: "presence/test".to_owned(), node: Some(right_card.node_id.to_string()), bearer_secret: None, }, ) .await .expect("denied remote pubsub subscribe"); match denied_pubsub_subscribe { ControlResponse::PubsubRemoteMessages { allowed, messages, reason, .. } => { assert!(!allowed); assert!(messages.is_empty()); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied pubsub subscribe response: {other:?}"), } let denied_pipe = handle_request_async( &left, ControlRequest::PipeConnect { target: "inbox".to_owned(), node: Some(right_card.node_id.to_string()), bearer_secret: None, }, ) .await .expect("denied remote pipe connect"); match denied_pipe { ControlResponse::PipeRemoteConnected { allowed, reason, connection, .. } => { assert!(!allowed); assert!(!connection.local_listener_found); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied pipe connect response: {other:?}"), } let denied_pipe_listen = handle_request_async( &left, ControlRequest::PipeListen { name: "remote-inbox".to_owned(), node: Some(right_card.node_id.to_string()), bearer_secret: None, }, ) .await .expect("denied remote pipe listen"); match denied_pipe_listen { ControlResponse::PipeRemoteListening { allowed, reason, listener, .. } => { assert!(!allowed); assert!(listener.is_none()); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied pipe listen response: {other:?}"), } let denied_ssh_proxy = handle_request_async( &left, ControlRequest::SshProxyConnect { node: right_card.node_id.to_string(), bearer_secret: None, }, ) .await .expect("denied SSH proxy connect"); match denied_ssh_proxy { ControlResponse::SshProxyConnected { allowed, reason, connection, .. } => { assert!(!allowed); assert!(connection.is_none()); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied SSH proxy response: {other:?}"), } let denied_admin_shell = handle_request_async( &left, ControlRequest::SshAdminShell { node: right_card.node_id.to_string(), command: "status".to_owned(), bearer_secret: None, }, ) .await .expect("denied SSH admin shell"); match denied_admin_shell { ControlResponse::SshAdminShellOutput { allowed, output, reason, .. } => { assert!(!allowed); assert!(output.is_empty()); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied SSH admin shell response: {other:?}"), } let denied_document = handle_request_async( &left, ControlRequest::DocumentSync { node: right_card.node_id.to_string(), name: "notes".to_owned(), bearer_secret: None, }, ) .await .expect("denied document sync"); match denied_document { ControlResponse::DocumentSynced { allowed, updated, reason, .. } => { assert!(!allowed); assert!(!updated); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied document sync response: {other:?}"), } let denied_db_sync = handle_request_async( &left, ControlRequest::DbSync { node: right_card.node_id.to_string(), name: "notes".to_owned(), limit: 10, bearer_secret: None, }, ) .await .expect("denied db sync"); match denied_db_sync { ControlResponse::DbSynced { allowed, changes_received, changes_applied, schema_match, reason, .. } => { assert!(!allowed); assert_eq!(changes_received, 0); assert_eq!(changes_applied, 0); assert!(!schema_match); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied DB sync response: {other:?}"), } let bearer_secret = match handle_request( &right, ControlRequest::SecretBearerCreate { resource: "resource:cas:local".to_owned(), capabilities: vec!["cas.fetch".to_owned()], expires_at_ms: None, }, ) .expect("create remote CAS bearer access") { ControlResponse::SecretBearerCreated { access } => access .token .expect("private bearer token on create") .to_string(), other => panic!("unexpected bearer create response: {other:?}"), }; let bearer_fetch = handle_request_async( &left, ControlRequest::CasFetch { node: right_card.node_id.to_string(), hash: right_blob.hash.clone(), bearer_secret: Some(bearer_secret), }, ) .await .expect("bearer cas fetch"); match bearer_fetch { ControlResponse::CasFetched { allowed, hash, size_bytes, reason, .. } => { assert!(allowed); assert_eq!(hash, right_blob.hash); assert_eq!(size_bytes, right_blob.size_bytes); assert!(reason.contains("bearer proof")); } other => panic!("unexpected bearer CAS fetch response: {other:?}"), } handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:cas:local".to_owned(), capability: "cas.fetch".to_owned(), grant_id: Some("grant:left-cas-fetch".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left peer"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:cas-tree:shared".to_owned(), capability: "cas.fetch".to_owned(), grant_id: Some("grant:left-cas-tree-fetch".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left file root fetch"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:kv:prefs".to_owned(), capability: "kv.read".to_owned(), grant_id: Some("grant:left-kv-read".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left kv read"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:pubsub:presence/test".to_owned(), capability: "pubsub.publish".to_owned(), grant_id: Some("grant:left-pubsub-publish".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left pubsub publish"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:pubsub:presence/test".to_owned(), capability: "pubsub.subscribe".to_owned(), grant_id: Some("grant:left-pubsub-subscribe".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left pubsub subscribe"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:pipe:inbox".to_owned(), capability: "pipe.connect".to_owned(), grant_id: Some("grant:left-pipe-connect".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left pipe connect"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:pipe:remote-inbox".to_owned(), capability: "pipe.listen".to_owned(), grant_id: Some("grant:left-pipe-listen".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left pipe listen"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:ssh-proxy:local".to_owned(), capability: "ssh_proxy.connect".to_owned(), grant_id: Some("grant:left-ssh-proxy-connect".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left ssh proxy connect"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:ssh-proxy:local".to_owned(), capability: "ssh_proxy.admin_shell".to_owned(), grant_id: Some("grant:left-ssh-admin-shell".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left ssh admin shell"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:document:notes".to_owned(), capability: "document.read".to_owned(), grant_id: Some("grant:left-document-read".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left document read"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:db:notes".to_owned(), capability: "db.sync".to_owned(), grant_id: Some("grant:left-db-sync".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left db sync"); let tcp_echo = TcpListener::bind("127.0.0.1:0") .await .expect("bind tcp echo"); let tcp_target = tcp_echo.local_addr().expect("tcp echo addr").to_string(); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: format!("resource:pipe-tcp:{tcp_target}"), capability: "pipe.forward".to_owned(), grant_id: Some("grant:left-pipe-tcp-forward".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left pipe tcp forward"); let echo_task = tokio::spawn(async move { let (mut socket, _) = tcp_echo.accept().await.expect("accept tcp echo"); let mut buf = [0_u8; 4]; socket.read_exact(&mut buf).await.expect("read tcp echo"); assert_eq!(&buf, b"ping"); socket.write_all(b"pong").await.expect("write tcp echo"); socket.shutdown().await.expect("shutdown tcp echo"); }); let (forward_client, forward_daemon) = UnixStream::pair().expect("unix stream pair"); let right_node_id = right_card.node_id.to_string(); let left_for_forward = left.clone(); let tcp_target_for_forward = tcp_target.clone(); let forward_task = tokio::spawn(async move { handle_local_pipe_tcp_stream( left_for_forward, &right_node_id, tcp_target_for_forward, None, forward_daemon, ) .await }); let mut forward_reader = BufReader::new(forward_client); let mut line = String::new(); forward_reader .read_line(&mut line) .await .expect("read pipe tcp response"); match geth_control::decode_response(&line).expect("decode pipe tcp response") { ControlResponse::PipeRemoteConnected { allowed, connection, .. } => { assert!(allowed); assert_eq!(connection.target, tcp_target); } other => panic!("unexpected pipe TCP stream response: {other:?}"), } let stream = forward_reader.into_inner(); let (mut forward_read, mut forward_write) = stream.into_split(); forward_write .write_all(b"ping") .await .expect("write through pipe tcp forward"); forward_write .shutdown() .await .expect("shutdown pipe tcp forward write"); let mut forwarded = Vec::new(); forward_read .read_to_end(&mut forwarded) .await .expect("read through pipe tcp forward"); assert_eq!(forwarded, b"pong"); forward_task .await .expect("join pipe tcp forward") .expect("pipe tcp forward"); echo_task.await.expect("join tcp echo"); #[cfg(unix)] { let unix_target = right_home.path().join("echo.sock"); let unix_echo = UnixListener::bind(&unix_target).expect("bind unix echo"); let unix_target_display = unix_target.display().to_string(); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: format!("resource:pipe-unix:{unix_target_display}"), capability: "pipe.forward".to_owned(), grant_id: Some("grant:left-pipe-unix-forward".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left pipe unix forward"); let unix_echo_task = tokio::spawn(async move { let (mut socket, _) = unix_echo.accept().await.expect("accept unix echo"); let mut buf = [0_u8; 5]; socket.read_exact(&mut buf).await.expect("read unix echo"); assert_eq!(&buf, b"hello"); socket.write_all(b"world").await.expect("write unix echo"); socket.shutdown().await.expect("shutdown unix echo"); }); let (unix_forward_client, unix_forward_daemon) = UnixStream::pair().expect("unix stream pair for unix forward"); let right_node_id = right_card.node_id.to_string(); let left_for_forward = left.clone(); let unix_target_for_forward = unix_target.clone(); let unix_forward_task = tokio::spawn(async move { handle_local_pipe_unix_stream( left_for_forward, &right_node_id, unix_target_for_forward, None, unix_forward_daemon, ) .await }); let mut unix_forward_reader = BufReader::new(unix_forward_client); let mut unix_line = String::new(); unix_forward_reader .read_line(&mut unix_line) .await .expect("read pipe unix response"); match geth_control::decode_response(&unix_line).expect("decode pipe unix response") { ControlResponse::PipeRemoteConnected { allowed, connection, .. } => { assert!(allowed); assert_eq!(connection.target, unix_target_display); } other => panic!("unexpected pipe Unix stream response: {other:?}"), } let stream = unix_forward_reader.into_inner(); let (mut unix_forward_read, mut unix_forward_write) = stream.into_split(); unix_forward_write .write_all(b"hello") .await .expect("write through pipe unix forward"); unix_forward_write .shutdown() .await .expect("shutdown pipe unix forward write"); let mut unix_forwarded = Vec::new(); unix_forward_read .read_to_end(&mut unix_forwarded) .await .expect("read through pipe unix forward"); assert_eq!(unix_forwarded, b"world"); unix_forward_task .await .expect("join pipe unix forward") .expect("pipe unix forward"); unix_echo_task.await.expect("join unix echo"); } let allowed = handle_request_async( &left, ControlRequest::PeerAuthCheck { node: right_card.node_id.to_string(), resource: "resource:cas:local".to_owned(), capability: "cas.fetch".to_owned(), }, ) .await .expect("allowed peer auth check"); match allowed { ControlResponse::PeerAuthChecked { allowed, reason, evaluated_ops, .. } => { assert!(allowed); assert!(reason.contains("direct grant")); assert_eq!(evaluated_ops, 1); } other => panic!("unexpected allowed auth response: {other:?}"), } let fetched = handle_request_async( &left, ControlRequest::CasFetch { node: right_card.node_id.to_string(), hash: right_blob.hash.clone(), bearer_secret: None, }, ) .await .expect("allowed cas fetch"); match fetched { ControlResponse::CasFetched { allowed, hash, size_bytes, reason, note, .. } => { assert!(allowed); assert_eq!(hash, right_blob.hash); assert_eq!(size_bytes, right_blob.size_bytes); assert!(reason.contains("direct grant")); assert!(note.contains("required cas.fetch")); assert_eq!( LocalCas::new(left.paths.cas_dir()) .read_bytes(&right_blob.hash) .expect("left cas read fetched blob"), b"remote cas bytes" ); } other => panic!("unexpected allowed CAS fetch response: {other:?}"), } let providers = Store::open(&left_paths.metadata_db()) .expect("open left after cas fetch") .list_cas_providers(right_blob.hash.as_str()) .expect("list cas providers"); assert_eq!(providers.len(), 1); assert_eq!(providers[0].peer_node_id, right.node_id); let root_sync = handle_request_async( &left, ControlRequest::CasRootSync { node: right_card.node_id.to_string(), name: "shared".to_owned(), bearer_secret: None, }, ) .await .expect("allowed file root sync"); match root_sync { ControlResponse::CasRootSynced { allowed, root, tree_bytes_imported, reason, note, .. } => { assert!(allowed); assert!(tree_bytes_imported); let root = root.expect("synced root"); assert_ne!(root.name, "shared"); assert!(root.name.starts_with("remote-")); assert!(root.name.ends_with("-shared")); assert_eq!(root.latest_tree, Some(right_tree_hash.clone())); assert!(root.path.starts_with("remote:")); assert!(reason.contains("direct grant")); assert!(note.contains("never writes files")); } other => panic!("unexpected allowed file root sync response: {other:?}"), } assert!( LocalCas::new(left.paths.cas_dir()) .has(&right_tree_hash) .expect("left has synced tree object") ); let local_shared = match handle_request( &left, ControlRequest::CasRootScan { name: "shared".to_owned(), }, ) .expect("left local shared root remains scannable") { ControlResponse::CasRootScanned { scan } => scan.root, other => panic!("unexpected left local file root scan response: {other:?}"), }; assert_eq!(local_shared.path, left_file_root.to_string_lossy()); let kv_synced = handle_request_async( &left, ControlRequest::KvSync { node: right_card.node_id.to_string(), name: "prefs".to_owned(), bearer_secret: None, }, ) .await .expect("allowed kv sync"); match kv_synced { ControlResponse::KvSynced { allowed, entries_imported, reason, note, .. } => { assert!(allowed); assert_eq!(entries_imported, 1); assert!(reason.contains("direct grant")); assert!(note.contains("kv.read")); } other => panic!("unexpected allowed KV sync response: {other:?}"), } let synced_theme = handle_request( &left, ControlRequest::KvGet { name: "prefs".to_owned(), key: "apps/foo/theme".to_owned(), }, ) .expect("left kv get synced"); match synced_theme { ControlResponse::KvGet { entry: Some(entry) } => { assert_eq!(entry.value, "dark"); } other => panic!("unexpected synced KV get response: {other:?}"), } let left_kv_doc_state = Store::open(&left_paths.metadata_db()) .expect("open left store for kv doc state") .get_module_state("kv-doc:prefs") .expect("get left kv doc state") .expect("left imported kv docs state"); let left_kv_doc_state: KvDocsState = serde_json::from_str(&left_kv_doc_state.state_json).expect("parse kv docs state"); assert_eq!(left_kv_doc_state.name, "prefs"); assert!(!left_kv_doc_state.namespace_id.is_empty()); assert!(left_kv_doc_state.read_ticket.starts_with("doc")); let published = handle_request_async( &left, ControlRequest::PubsubPub { topic: "presence/test".to_owned(), message: "hello".to_owned(), node: Some(right_card.node_id.to_string()), bearer_secret: None, }, ) .await .expect("allowed remote pubsub publish"); match published { ControlResponse::PubsubRemotePublished { allowed, message, reason, note, .. } => { assert!(allowed); assert_eq!(message.topic.as_str(), "presence/test"); assert_eq!(message.message, "hello"); assert!(reason.contains("direct grant")); assert!(note.contains("lossy")); } other => panic!("unexpected allowed pubsub publish response: {other:?}"), } let mut gossiped_to_left = false; for _ in 0..20 { let local_messages = pubsub_messages_for_topic(&left, "presence/test") .expect("left pubsub local messages"); if local_messages .iter() .any(|message| message.message == "hello") { gossiped_to_left = true; break; } tokio::time::sleep(Duration::from_millis(50)).await; } assert!( gossiped_to_left, "authorized remote pubsub publish should also arrive through iroh-gossip" ); let remote_messages = handle_request( &right, ControlRequest::PubsubSub { topic: "presence/test".to_owned(), node: None, bearer_secret: None, }, ) .expect("right pubsub sub after remote publish"); match remote_messages { ControlResponse::PubsubMessages { messages, .. } => { assert_eq!(messages.len(), 1); assert_eq!(messages[0].message, "hello"); } other => panic!("unexpected remote pubsub messages response: {other:?}"), } let subscribed = handle_request_async( &left, ControlRequest::PubsubSub { topic: "presence/test".to_owned(), node: Some(right_card.node_id.to_string()), bearer_secret: None, }, ) .await .expect("allowed remote pubsub subscribe"); match subscribed { ControlResponse::PubsubRemoteMessages { allowed, messages, reason, note, .. } => { assert!(allowed); assert_eq!(messages.len(), 1); assert_eq!(messages[0].message, "hello"); assert!(reason.contains("direct grant")); assert!(note.contains("pubsub.subscribe")); } other => panic!("unexpected allowed pubsub subscribe response: {other:?}"), } let remote_pipe = handle_request_async( &left, ControlRequest::PipeConnect { target: "inbox".to_owned(), node: Some(right_card.node_id.to_string()), bearer_secret: None, }, ) .await .expect("allowed remote pipe connect"); match remote_pipe { ControlResponse::PipeRemoteConnected { allowed, connection, reason, note, .. } => { assert!(allowed); assert!(connection.local_listener_found); assert!(reason.contains("direct grant")); assert!(note.contains("pipe.connect")); } other => panic!("unexpected allowed pipe connect response: {other:?}"), } let remote_pipe_send = handle_request_async( &left, ControlRequest::PipeSend { target: "inbox".to_owned(), data_base64: "aGVsbG8gZnJvbSBsZWZ0".to_owned(), node: Some(right_card.node_id.to_string()), bearer_secret: None, }, ) .await .expect("allowed remote pipe send"); match remote_pipe_send { ControlResponse::PipeRemoteSent { allowed, listener_found, message, reason, note, .. } => { assert!(allowed); assert!(listener_found); let message = message.expect("remote pipe message"); assert_eq!(message.pipe, "inbox"); assert_eq!(message.data_base64, "aGVsbG8gZnJvbSBsZWZ0"); assert!(reason.contains("direct grant")); assert!(note.contains("Iroh pipe ALPN")); } other => panic!("unexpected allowed pipe send response: {other:?}"), } let right_messages = handle_request( &right, ControlRequest::PipeRecv { name: "inbox".to_owned(), peek: false, }, ) .expect("right pipe recv after remote send"); match right_messages { ControlResponse::PipeMessages { messages, drained, .. } => { assert!(drained); assert_eq!(messages.len(), 1); assert_eq!(messages[0].data_base64, "aGVsbG8gZnJvbSBsZWZ0"); assert_eq!( messages[0].source_node.as_deref(), Some(left.node_id.as_str()) ); } other => panic!("unexpected right pipe messages response: {other:?}"), } let remote_pipe_listen = handle_request_async( &left, ControlRequest::PipeListen { name: "remote-inbox".to_owned(), node: Some(right_card.node_id.to_string()), bearer_secret: None, }, ) .await .expect("allowed remote pipe listen"); match remote_pipe_listen { ControlResponse::PipeRemoteListening { allowed, listener, reason, note, .. } => { assert!(allowed); let listener = listener.expect("remote listener metadata"); assert_eq!(listener.name, "remote-inbox"); assert!(reason.contains("direct grant")); assert!(note.contains("pipe.listen")); } other => panic!("unexpected allowed pipe listen response: {other:?}"), } let ssh_proxy = handle_request_async( &left, ControlRequest::SshProxyConnect { node: right_card.node_id.to_string(), bearer_secret: None, }, ) .await .expect("allowed SSH proxy connect"); match ssh_proxy { ControlResponse::SshProxyConnected { allowed, connection, reason, note, .. } => { assert!(allowed); let connection = connection.expect("proxy connection metadata"); assert_eq!(connection.target_node.to_string(), right.node_id); assert_eq!( connection.local_sshd_target.as_deref(), Some("127.0.0.1:22") ); assert!(reason.contains("direct grant")); assert!(note.contains("SSH is not a geth transport")); } other => panic!("unexpected allowed SSH proxy response: {other:?}"), } let admin_shell = handle_request_async( &left, ControlRequest::SshAdminShell { node: right_card.node_id.to_string(), command: "status".to_owned(), bearer_secret: None, }, ) .await .expect("allowed SSH admin shell"); match admin_shell { ControlResponse::SshAdminShellOutput { allowed, output, reason, note, .. } => { assert!(allowed); assert!(output.contains(&format!("node_id={}", right.node_id))); assert!(output.contains("agent_id=")); assert!(reason.contains("direct grant")); assert!(note.contains("no host shell commands")); } other => panic!("unexpected allowed SSH admin shell response: {other:?}"), } let document_sync = handle_request_async( &left, ControlRequest::DocumentSync { node: right_card.node_id.to_string(), name: "notes".to_owned(), bearer_secret: None, }, ) .await .expect("allowed document sync"); match document_sync { ControlResponse::DocumentSynced { allowed, updated, reason, note, .. } => { assert!(allowed); assert!(updated); assert!(reason.contains("direct grant")); assert!(note.contains("durable Automerge")); } other => panic!("unexpected allowed document sync response: {other:?}"), } let synced_document = handle_request( &left, ControlRequest::DocumentGet { name: "notes".to_owned(), }, ) .expect("left document get synced"); match synced_document { ControlResponse::DocumentGet { state } => { assert_eq!(state.state_json, r#"{"title":"remote"}"#); } other => panic!("unexpected synced document get response: {other:?}"), } let db_sync = handle_request_async( &left, ControlRequest::DbSync { node: right_card.node_id.to_string(), name: "notes".to_owned(), limit: 10, bearer_secret: None, }, ) .await .expect("allowed db sync"); match db_sync { ControlResponse::DbSynced { allowed, changes_received, changes_applied, max_db_version, schema_match, reason, note, .. } => { assert!(allowed); assert_eq!(changes_received, 1); assert_eq!(changes_applied, 1); assert_eq!(max_db_version, Some(7)); assert!(schema_match); assert!(reason.contains("direct grant")); assert!(note.contains("db.sync")); assert!(note.contains("applies compatible batches")); } other => panic!("unexpected allowed DB sync response: {other:?}"), } let left_changes = geth_db::extract_crsqlite_changes(&left_db_path, None, 10) .expect("left db changes after apply"); assert_eq!(left_changes.changes.len(), 1); assert_eq!(left_changes.max_db_version, Some(7)); let denied_cert_sync = handle_request_async( &left, ControlRequest::SshCertSync { node: right_card.node_id.to_string(), bearer_secret: None, }, ) .await .expect("denied ssh cert sync"); match denied_cert_sync { ControlResponse::SshCertSynced { allowed, requests_imported, certificates_imported, reason, .. } => { assert!(!allowed); assert_eq!(requests_imported, 0); assert_eq!(certificates_imported, 0); assert!(reason.contains("no active direct or group grant")); } other => panic!("unexpected denied SSH cert sync response: {other:?}"), } handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:ssh:certs".to_owned(), capability: "ssh_cert.sync".to_owned(), grant_id: Some("grant:left-ssh-cert-sync".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left ssh cert sync"); handle_request( &right, ControlRequest::AuthGrant { subject: left.node_id.clone(), resource: "resource:ssh:revocations".to_owned(), capability: "ssh_revocation.sync".to_owned(), grant_id: Some("grant:left-ssh-revocation-sync".to_owned()), signing_key_path: None, admin_key_path: None, }, ) .expect("grant left ssh revocation sync"); let cert_sync = handle_request_async( &left, ControlRequest::SshCertSync { node: right_card.node_id.to_string(), bearer_secret: None, }, ) .await .expect("allowed ssh cert sync"); match cert_sync { ControlResponse::SshCertSynced { allowed, requests_imported, certificates_imported, reason, note, .. } => { assert!(allowed); assert_eq!(requests_imported, 1); assert_eq!(certificates_imported, 0); assert!(reason.contains("direct grant")); assert!(note.contains("ssh_cert.sync")); } other => panic!("unexpected allowed SSH cert sync response: {other:?}"), } let left_requests = Store::open(&left_paths.metadata_db()) .expect("open left after cert sync") .list_ssh_cert_requests() .expect("list synced requests"); assert!( left_requests .iter() .any(|request| request.request_id == right_request_id.as_str()) ); let revocation_sync = handle_request_async( &left, ControlRequest::SshRevocationSync { node: right_card.node_id.to_string(), bearer_secret: None, }, ) .await .expect("allowed ssh revocation sync"); match revocation_sync { ControlResponse::SshRevocationSynced { allowed, revocations_imported, reason, note, .. } => { assert!(allowed); assert_eq!(revocations_imported, 1); assert!(reason.contains("direct grant")); assert!(note.contains("ssh_revocation.sync")); } other => panic!("unexpected allowed SSH revocation sync response: {other:?}"), } let left_revocations = Store::open(&left_paths.metadata_db()) .expect("open left after revocation sync") .list_ssh_revocations() .expect("list synced revocations"); assert!( left_revocations .iter() .any(|revocation| revocation.revocation_id == right_revocation_id.as_str()) ); tokio::time::sleep(Duration::from_millis(2)).await; let second_pubkey = right_home.path().join("request-2.pub"); std::fs::write( &second_pubkey, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGV0aDI= node2\n", ) .expect("write second public key"); let second_requested = handle_request( &right, ControlRequest::SshCertRequest { public_key_path: second_pubkey, cert_kind: "user".to_owned(), principals: vec!["admin".to_owned()], requested_validity: Some("+4w".to_owned()), renewal_of: None, reason: Some("background sync test".to_owned()), subject: None, }, ) .expect("right second ssh cert request"); let second_request_id = match second_requested { ControlResponse::SshCertRequested { request } => request.id, other => panic!("unexpected second SSH cert request response: {other:?}"), }; let second_revocation = handle_request( &right, ControlRequest::SshRevocationAdd { kind: "key-id".to_owned(), target: "newly-revoked-key".to_owned(), reason: Some("background sync test".to_owned()), subject: None, }, ) .expect("right second ssh revocation"); let second_revocation_id = match second_revocation { ControlResponse::SshRevocationAdded { revocation } => revocation.id, other => panic!("unexpected second SSH revocation response: {other:?}"), }; handle_request( &right, ControlRequest::KvSet { name: "prefs".to_owned(), key: "apps/foo/live".to_owned(), value: "synced".to_owned(), subject: None, }, ) .expect("right second kv set"); handle_request( &right, ControlRequest::DocumentSet { name: "notes".to_owned(), state_json: r#"{"title":"live"}"#.to_owned(), }, ) .expect("right live document set"); std::fs::write(right_file_root.join("live.txt"), b"live file root sync") .expect("write live file"); let second_tree_hash = match handle_request( &right, ControlRequest::CasRootScan { name: "shared".to_owned(), }, ) .expect("right live file root scan") { ControlResponse::CasRootScanned { scan } => scan.tree.hash, other => panic!("unexpected right live file root scan response: {other:?}"), }; insert_mock_crsqlite_change(&right_db_path, 8, "live"); run_live_sync_once(&left) .await .expect("background live sync tick"); let left_store = Store::open(&left_paths.metadata_db()).expect("open left after live sync"); assert!( left_store .list_ssh_cert_requests() .expect("list live-synced requests") .iter() .any(|request| request.request_id == second_request_id.as_str()) ); assert!( left_store .list_ssh_revocations() .expect("list live-synced revocations") .iter() .any(|revocation| revocation.revocation_id == second_revocation_id.as_str()) ); assert_eq!( left_store .get_kv_entry("kv:prefs", "apps/foo/live") .expect("get live-synced kv") .map(|entry| entry.value), Some("synced".to_owned()) ); assert_eq!( left_store .get_document_resource_by_name("notes") .expect("get live-synced document") .map(|document| { geth_document::document_view_json(&document.state_json) .expect("document view json") }), Some(r#"{"title":"live"}"#.to_owned()) ); let live_remote_root = left_store .list_file_roots() .expect("list live-synced file roots") .into_iter() .find(|root| root.path == format!("remote:{}:shared", right.node_id)) .expect("live-synced remote root"); assert_eq!( live_remote_root.latest_tree_hash.as_deref(), Some(second_tree_hash.as_str()) ); assert!( LocalCas::new(left.paths.cas_dir()) .has(&second_tree_hash) .expect("left has live-synced tree object") ); let file_root_cursor = left_store .get_module_state(&live_sync_cursor_key( right_card.node_id.as_str(), "cas-tree:shared", )) .expect("get live-synced file root cursor") .expect("file root cursor exists"); assert_eq!( serde_json::from_str::(&file_root_cursor.state_json) .expect("parse file root cursor") .cursor_ms, live_remote_root.updated_at_ms ); let db_cursor = left_store .get_module_state(&live_sync_cursor_key( right_card.node_id.as_str(), "db:notes", )) .expect("get live-synced db cursor") .expect("db cursor exists"); assert_eq!( serde_json::from_str::(&db_cursor.state_json) .expect("parse db cursor") .cursor_ms, 8 ); left_endpoint.shutdown().await; right_endpoint.shutdown().await; } fn test_tree(entries: Vec) -> CasTreeObject { CasTreeObject { version: geth_cas::CAS_TREE_OBJECT_VERSION, entries, } } fn test_tree_file(path: &str, byte: &str) -> geth_cas::CasTreeEntry { geth_cas::CasTreeEntry { path: path.to_owned(), kind: CasTreeEntryKind::File, blob: Some(BlobHash::new(byte.repeat(32))), size_bytes: 1, executable: false, } } }