Wire pubsub through iroh-gossip
This commit is contained in:
parent
59c463eb40
commit
eec4c92145
8 changed files with 291 additions and 38 deletions
|
|
@ -7,6 +7,8 @@ license.workspace = true
|
|||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
blake3.workspace = true
|
||||
bytes.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
|
|
|||
|
|
@ -175,6 +175,12 @@ struct NodeRuntime {
|
|||
#[derive(Debug, Default)]
|
||||
struct PubsubRuntime {
|
||||
messages: VecDeque<PubsubMessage>,
|
||||
gossip_topics: BTreeMap<String, PubsubGossipTopicRuntime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PubsubGossipTopicRuntime {
|
||||
sender: iroh_gossip::api::GossipSender,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
@ -785,6 +791,15 @@ pub async fn handle_request_async(
|
|||
.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)
|
||||
|
|
@ -2169,6 +2184,8 @@ async fn pubsub_publish_to_peer(
|
|||
) -> Result<ControlResponse, NodeError> {
|
||||
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,
|
||||
|
|
@ -2224,6 +2241,8 @@ async fn pubsub_subscribe_from_peer(
|
|||
bearer_secret: Option<String>,
|
||||
) -> Result<ControlResponse, NodeError> {
|
||||
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,
|
||||
|
|
@ -5429,8 +5448,20 @@ async fn handle_iroh_control_connection(
|
|||
&nonce,
|
||||
bearer_proof.as_ref(),
|
||||
)?;
|
||||
drop(store);
|
||||
let published = if explanation.allowed {
|
||||
Some(record_pubsub_message(&node, topic, message)?)
|
||||
let bootstrap_peer = remote_endpoint_id
|
||||
.parse::<iroh::EndpointId>()
|
||||
.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
|
||||
};
|
||||
|
|
@ -5477,7 +5508,12 @@ async fn handle_iroh_control_connection(
|
|||
&nonce,
|
||||
bearer_proof.as_ref(),
|
||||
)?;
|
||||
drop(store);
|
||||
let messages = if explanation.allowed {
|
||||
let bootstrap_peer = remote_endpoint_id
|
||||
.parse::<iroh::EndpointId>()
|
||||
.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()
|
||||
|
|
@ -6384,6 +6420,28 @@ fn ensure_peer_card_matches_endpoint(card: &PeerCard, endpoint_id: &str) -> Resu
|
|||
}
|
||||
}
|
||||
|
||||
fn peer_gossip_endpoint_id(
|
||||
node: &LocalNode,
|
||||
peer_node: &str,
|
||||
) -> Result<iroh::EndpointId, NodeError> {
|
||||
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::<iroh::EndpointId>()
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))
|
||||
}
|
||||
|
||||
fn restricted_admin_shell_output(node: &LocalNode, command: &str) -> Result<String, NodeError> {
|
||||
match command {
|
||||
"help" => Ok("commands: help, status, node-id".to_owned()),
|
||||
|
|
@ -8078,6 +8136,13 @@ fn native_backend_statuses() -> Vec<NativeBackendStatus> {
|
|||
library.alpn
|
||||
),
|
||||
),
|
||||
"pubsub" => (
|
||||
"wired",
|
||||
format!(
|
||||
"none; pubsub topics broadcast over native ALPN {} after geth control authorization preflight",
|
||||
library.alpn
|
||||
),
|
||||
),
|
||||
_ => (
|
||||
"ready-to-wire",
|
||||
format!(
|
||||
|
|
@ -8190,11 +8255,20 @@ fn record_pubsub_message(
|
|||
node: &LocalNode,
|
||||
topic: String,
|
||||
message: String,
|
||||
) -> Result<PubsubMessage, NodeError> {
|
||||
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<PubsubMessage, NodeError> {
|
||||
let message = PubsubMessage {
|
||||
topic: topic.into(),
|
||||
message,
|
||||
published_at: UnixMillis(geth_store::now_ms()),
|
||||
published_at,
|
||||
};
|
||||
let mut runtime = node
|
||||
.runtime
|
||||
|
|
@ -8225,6 +8299,158 @@ fn pubsub_messages_for_topic(
|
|||
.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<iroh::EndpointId>,
|
||||
) -> Result<Option<iroh_gossip::api::GossipSender>, 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::<PubsubGossipPayload>(&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<iroh::EndpointId>,
|
||||
) -> 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<PipeListener, NodeError> {
|
||||
let listener = PipeListener {
|
||||
id: format!("pipe:{name}").into(),
|
||||
|
|
@ -11874,6 +12100,23 @@ mod tests {
|
|||
}
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ fn geth_status_against_running_daemon() {
|
|||
)
|
||||
);
|
||||
assert!(stdout.contains(
|
||||
"native backend pubsub: shared-iroh-endpoint-ready target iroh-gossip 0.95.0 (ready-to-wire)"
|
||||
"native backend pubsub: shared-iroh-endpoint-ready target iroh-gossip 0.95.0 (wired)"
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue