Add authorized remote pipe listen

This commit is contained in:
Eric Wendland 2026-05-19 19:21:42 +02:00
commit 3382899628
8 changed files with 322 additions and 21 deletions

View file

@ -287,6 +287,11 @@ pub async fn handle_request_async(
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),
@ -582,6 +587,7 @@ async fn peer_ping(node: &LocalNode, peer_node: &str) -> Result<ControlResponse,
| PeerControlResponse::PubsubPublished { .. }
| PeerControlResponse::PubsubSubscribed { .. }
| PeerControlResponse::PipeConnected { .. }
| PeerControlResponse::PipeListening { .. }
| PeerControlResponse::SshProxyConnected { .. }
| PeerControlResponse::DocumentSynced { .. }
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
@ -697,6 +703,7 @@ async fn peer_auth_check(
| PeerControlResponse::PubsubPublished { .. }
| PeerControlResponse::PubsubSubscribed { .. }
| PeerControlResponse::PipeConnected { .. }
| PeerControlResponse::PipeListening { .. }
| PeerControlResponse::SshProxyConnected { .. }
| PeerControlResponse::DocumentSynced { .. }
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
@ -844,6 +851,7 @@ async fn cas_fetch_from_peer(
| PeerControlResponse::PubsubPublished { .. }
| PeerControlResponse::PubsubSubscribed { .. }
| PeerControlResponse::PipeConnected { .. }
| PeerControlResponse::PipeListening { .. }
| PeerControlResponse::SshProxyConnected { .. }
| PeerControlResponse::DocumentSynced { .. }
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
@ -1257,6 +1265,53 @@ async fn pipe_connect_to_peer(
}
}
async fn pipe_listen_on_peer(
node: &LocalNode,
peer_node: &str,
name: String,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
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 ssh_proxy_connect_to_peer(
node: &LocalNode,
peer_node: &str,
@ -1862,6 +1917,10 @@ async fn request_peer_control(
nonce: response_nonce,
..
}
| PeerControlResponse::PipeListening {
nonce: response_nonce,
..
}
| PeerControlResponse::SshProxyConnected {
nonce: response_nonce,
..
@ -2513,6 +2572,54 @@ async fn handle_iroh_control_connection(
note: "pipe connect authenticated endpoint/card binding and required pipe.connect on the remote pipe resource; byte streams are not implemented yet".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; byte streams are not implemented yet".to_owned(),
}
}
PeerControlRequest::SshProxyConnect {
peer_card,
nonce,
@ -3981,22 +4088,14 @@ pub fn handle_request(
})
}
ControlRequest::PubsubSub { node: Some(_), .. } => Err(NodeError::IrohEndpointUnavailable),
ControlRequest::PipeListen { name } => {
ControlRequest::PipeListen {
name, node: None, ..
} => {
geth_pipe::validate_pipe_name(&name)?;
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());
let listener = record_pipe_listener(node, name)?;
Ok(ControlResponse::PipeListening { listener })
}
ControlRequest::PipeListen { node: Some(_), .. } => Err(NodeError::IrohEndpointUnavailable),
ControlRequest::PipeConnect {
target, node: None, ..
} => {
@ -4144,6 +4243,22 @@ fn pubsub_messages_for_topic(
.collect())
}
fn record_pipe_listener(node: &LocalNode, name: String) -> Result<PipeListener, NodeError> {
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,
@ -5041,6 +5156,8 @@ mod tests {
&right,
ControlRequest::PipeListen {
name: "inbox".to_owned(),
node: None,
bearer_secret: None,
},
)
.expect("right pipe listen");
@ -5255,6 +5372,30 @@ mod tests {
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 {
@ -5420,6 +5561,16 @@ mod tests {
},
)
.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()),
},
)
.expect("grant left pipe listen");
handle_request(
&right,
ControlRequest::AuthGrant {
@ -5652,6 +5803,33 @@ mod tests {
other => panic!("unexpected allowed pipe connect 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 {