Add authorized pipe messages over Iroh
This commit is contained in:
parent
3eb6b623e7
commit
78dbbf240b
11 changed files with 712 additions and 31 deletions
|
|
@ -9,7 +9,8 @@ use geth_cas::{
|
|||
use geth_config::{GethConfig, GethPaths, RelayMode};
|
||||
use geth_control::{
|
||||
CasBlob, CasProvider, ControlRequest, ControlResponse, KeychainStatusResponse, NodeIdResponse,
|
||||
PeerControlRequest, PeerControlResponse, StatusResponse, SyncWatermark,
|
||||
PeerControlRequest, PeerControlResponse, PipeWireRequest, PipeWireResponse, StatusResponse,
|
||||
SyncWatermark,
|
||||
};
|
||||
use geth_crypto::AgentKey;
|
||||
use geth_db::DbResource;
|
||||
|
|
@ -21,7 +22,7 @@ use geth_document::{DocumentResource, DocumentState};
|
|||
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
|
||||
use geth_keychain::{KeychainOp, KeychainOpKind, KeychainOpSignature};
|
||||
use geth_kv::{KvEntry, KvResource, KvSyncEntry};
|
||||
use geth_pipe::{PipeConnection, PipeListener};
|
||||
use geth_pipe::{PipeConnection, PipeListener, PipeMessage};
|
||||
use geth_pubsub::PubsubMessage;
|
||||
use geth_resource::ResourceDescriptor;
|
||||
use geth_secrets::{BearerAccess, BearerChallenge, BearerProof, ResourceMasterSecret};
|
||||
|
|
@ -150,10 +151,12 @@ struct PubsubRuntime {
|
|||
struct PipeRuntime {
|
||||
listeners: BTreeMap<String, PipeListener>,
|
||||
connections: VecDeque<PipeConnection>,
|
||||
messages: VecDeque<PipeMessage>,
|
||||
}
|
||||
|
||||
const PUBSUB_RING_LIMIT: usize = 256;
|
||||
const PIPE_CONNECTION_RING_LIMIT: usize = 256;
|
||||
const PIPE_MESSAGE_RING_LIMIT: usize = 1024;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
struct LiveSyncCursor {
|
||||
|
|
@ -307,6 +310,12 @@ pub async fn handle_request_async(
|
|||
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::SshProxyConnect {
|
||||
node: peer_node,
|
||||
bearer_secret,
|
||||
|
|
@ -1472,6 +1481,57 @@ async fn pipe_listen_on_peer(
|
|||
}
|
||||
}
|
||||
|
||||
async fn pipe_send_to_peer(
|
||||
node: &LocalNode,
|
||||
peer_node: &str,
|
||||
target: String,
|
||||
data_base64: String,
|
||||
bearer_secret: Option<String>,
|
||||
) -> Result<ControlResponse, NodeError> {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn ssh_proxy_connect_to_peer(
|
||||
node: &LocalNode,
|
||||
peer_node: &str,
|
||||
|
|
@ -2125,6 +2185,75 @@ async fn request_peer_control(
|
|||
}
|
||||
}
|
||||
|
||||
async fn request_pipe_wire(
|
||||
node: &LocalNode,
|
||||
peer_node: &str,
|
||||
operation: &str,
|
||||
build_request: impl FnOnce(PeerCard, String) -> PipeWireRequest,
|
||||
) -> Result<PipeWireResponse, 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{}\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 {
|
||||
|
|
@ -2279,6 +2408,15 @@ async fn handle_iroh_control_connection(
|
|||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
let text =
|
||||
std::str::from_utf8(&bytes).map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
if alpn == display_alpn(geth_iroh::ALPN_PIPE.to_vec()) {
|
||||
let response = handle_pipe_wire_request(&node, &remote_endpoint_id, text)?;
|
||||
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()))?;
|
||||
return Ok(());
|
||||
}
|
||||
let response = match geth_control::decode_peer_request(text)? {
|
||||
PeerControlRequest::Ping { peer_card, nonce } => {
|
||||
peer_card.validate_candidate()?;
|
||||
|
|
@ -2818,7 +2956,7 @@ async fn handle_iroh_control_connection(
|
|||
Some(record_pipe_connection(
|
||||
&node,
|
||||
target,
|
||||
"remote pipe connect over protected Iroh control path; byte streams are not implemented yet".to_owned(),
|
||||
"remote pipe connect over protected Iroh control path; byte messages use the dedicated Iroh pipe ALPN".to_owned(),
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
|
|
@ -2833,7 +2971,7 @@ async fn handle_iroh_control_connection(
|
|||
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 streams are not implemented yet".to_owned(),
|
||||
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 {
|
||||
|
|
@ -2881,7 +3019,7 @@ async fn handle_iroh_control_connection(
|
|||
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; byte streams are not implemented yet".to_owned(),
|
||||
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 {
|
||||
|
|
@ -3078,6 +3216,74 @@ async fn handle_iroh_control_connection(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_pipe_wire_request(
|
||||
node: &LocalNode,
|
||||
remote_endpoint_id: &str,
|
||||
text: &str,
|
||||
) -> Result<PipeWireResponse, NodeError> {
|
||||
match geth_control::decode_pipe_wire_request(text)? {
|
||||
PipeWireRequest::Send {
|
||||
peer_card,
|
||||
target,
|
||||
data_base64,
|
||||
nonce,
|
||||
bearer_proof,
|
||||
} => {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn iroh_node_addr_from_candidate(
|
||||
candidate: &EndpointCandidate,
|
||||
) -> Result<iroh::NodeAddr, NodeError> {
|
||||
|
|
@ -4389,6 +4595,40 @@ pub fn handle_request(
|
|||
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::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 { .. } => Err(NodeError::IrohEndpointUnavailable),
|
||||
ControlRequest::ModuleStub { module, command } => {
|
||||
Ok(ControlResponse::NotImplemented { module, command })
|
||||
|
|
@ -4562,6 +4802,54 @@ fn record_pipe_connection(
|
|||
Ok(connection)
|
||||
}
|
||||
|
||||
fn record_pipe_message(
|
||||
node: &LocalNode,
|
||||
target: String,
|
||||
data_base64: String,
|
||||
source_node: Option<String>,
|
||||
note: String,
|
||||
) -> Result<(Option<PipeMessage>, 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<Vec<PipeMessage>, NodeError> {
|
||||
let mut runtime = node
|
||||
.runtime
|
||||
.pipes
|
||||
.lock()
|
||||
.map_err(|_| NodeError::RuntimeLockPoisoned)?;
|
||||
let messages = runtime
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| message.pipe == name)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if drain {
|
||||
runtime.messages.retain(|message| message.pipe != name);
|
||||
}
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
fn ensure_local_document(store: &Store, name: &str) -> Result<StoredDocumentResource, NodeError> {
|
||||
if let Some(document) = store.get_document_resource_by_name(name)? {
|
||||
return Ok(document);
|
||||
|
|
@ -6554,11 +6842,64 @@ mod tests {
|
|||
assert!(allowed);
|
||||
assert!(connection.local_listener_found);
|
||||
assert!(reason.contains("direct grant"));
|
||||
assert!(note.contains("byte streams are not implemented yet"));
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue