Harden live Iroh integration paths
This commit is contained in:
parent
1006f41e45
commit
0fdf5d0be7
6 changed files with 256 additions and 144 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -70,7 +70,6 @@ jobs:
|
|||
iroh-integration:
|
||||
name: iroh integration smoke tests
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -626,11 +626,11 @@ GitHub Actions workflows live under `.github/workflows/`:
|
|||
|
||||
- `ci.yml` runs formatting, clippy, docs, `cargo check`, and workspace tests on
|
||||
Linux, macOS, and Windows. Cross-platform test jobs set
|
||||
`GETH_TEST_SKIP_IROH=1` so deterministic unit and integration coverage can be
|
||||
required while Iroh-heavy daemon-to-daemon tests continue to mature.
|
||||
- `ci.yml` also has a visible Ubuntu Iroh integration smoke job for the full
|
||||
network-heavy paths. It is marked `continue-on-error` until the local Iroh
|
||||
tests are reliable enough to make required.
|
||||
`GETH_TEST_SKIP_IROH=1` so deterministic unit and integration coverage stays
|
||||
stable across host networking differences.
|
||||
- `ci.yml` also runs a required Ubuntu Iroh integration smoke job for the live
|
||||
daemon-to-daemon paths, including peer control, sync, native CAS/KV/pubsub,
|
||||
pipe forwarding, SSH proxy handshakes, and overlay ALPN authorization.
|
||||
- `security.yml` runs RustSec `cargo audit` on pushes, pull requests, manual
|
||||
dispatch, and a weekly schedule.
|
||||
- `codeql.yml` builds the Rust workspace for GitHub CodeQL analysis.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::{SocketAddrV4, SocketAddrV6};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const ALPN_CONTROL: &[u8] = b"/geth/control/1";
|
||||
|
|
@ -278,14 +278,41 @@ impl GethIrohEndpoint {
|
|||
}
|
||||
|
||||
pub async fn node_addr_snapshot(&self) -> Result<GethNodeAddr, IrohError> {
|
||||
let endpoint_addr = self.endpoint.addr();
|
||||
let mut endpoint_addr = self.endpoint.addr();
|
||||
let mut direct_addresses = endpoint_addr
|
||||
.ip_addrs()
|
||||
.map(|addr| addr.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for _ in 0..20 {
|
||||
if !direct_addresses.is_empty() {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
||||
endpoint_addr = self.endpoint.addr();
|
||||
direct_addresses = endpoint_addr
|
||||
.ip_addrs()
|
||||
.map(|addr| addr.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
if direct_addresses.is_empty() || self.status.relay_mode == "disabled" {
|
||||
direct_addresses.extend(
|
||||
self.endpoint
|
||||
.bound_sockets()
|
||||
.into_iter()
|
||||
.map(normalize_bound_socket_for_peer_card)
|
||||
.map(|addr| addr.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
direct_addresses.sort_by_key(|addr| (!is_loopback_socket_addr(addr), addr.clone()));
|
||||
direct_addresses.dedup();
|
||||
|
||||
Ok(GethNodeAddr {
|
||||
endpoint_id: endpoint_addr.id.to_string(),
|
||||
relay_url: endpoint_addr.relay_urls().next().map(ToString::to_string),
|
||||
direct_addresses: endpoint_addr
|
||||
.ip_addrs()
|
||||
.map(|addr| addr.to_string())
|
||||
.collect(),
|
||||
direct_addresses,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -301,6 +328,24 @@ pub struct GethNodeAddr {
|
|||
pub direct_addresses: Vec<String>,
|
||||
}
|
||||
|
||||
fn normalize_bound_socket_for_peer_card(addr: SocketAddr) -> SocketAddr {
|
||||
match addr.ip() {
|
||||
IpAddr::V4(ip) if ip.is_unspecified() => {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), addr.port())
|
||||
}
|
||||
IpAddr::V6(ip) if ip.is_unspecified() => {
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), addr.port())
|
||||
}
|
||||
_ => addr,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_loopback_socket_addr(addr: &str) -> bool {
|
||||
addr.parse::<SocketAddr>()
|
||||
.map(|addr| addr.ip().is_loopback())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub async fn start_endpoint(config: &GethIrohConfig) -> Result<GethIrohEndpoint, IrohError> {
|
||||
let secret_key = load_or_create_secret_key(&config.secret_key_path)?;
|
||||
let mut builder = iroh::Endpoint::builder()
|
||||
|
|
|
|||
|
|
@ -1150,24 +1150,28 @@ async fn peer_ping(node: &LocalNode, peer_node: &str) -> Result<ControlResponse,
|
|||
.endpoint()
|
||||
.connect(node_addr, geth_iroh::ALPN_CONTROL)
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
.map_err(|error| NodeError::IrohPeer(format!("peer ping connect failed: {error}")))?;
|
||||
let alpn = display_alpn(conn.alpn());
|
||||
let (mut send, mut recv) = conn
|
||||
let (mut send, recv) = conn
|
||||
.open_bi()
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
.map_err(|error| NodeError::IrohPeer(format!("peer ping stream open failed: {error}")))?;
|
||||
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)? {
|
||||
.map_err(|error| NodeError::IrohPeer(format!("peer ping request write failed: {error}")))?;
|
||||
send.finish().map_err(|error| {
|
||||
NodeError::IrohPeer(format!("peer ping request finish failed: {error}"))
|
||||
})?;
|
||||
send.stopped().await.map_err(|error| {
|
||||
NodeError::IrohPeer(format!("peer ping request delivery failed: {error}"))
|
||||
})?;
|
||||
drop(send);
|
||||
let mut response = String::new();
|
||||
let mut reader = BufReader::new(recv);
|
||||
reader.read_line(&mut response).await.map_err(|error| {
|
||||
NodeError::IrohPeer(format!("peer ping response line read failed: {error}"))
|
||||
})?;
|
||||
match geth_control::decode_peer_response(&response)? {
|
||||
PeerControlResponse::Pong {
|
||||
node_id,
|
||||
agent_id,
|
||||
|
|
@ -1264,7 +1268,7 @@ async fn peer_auth_check(
|
|||
.connect(node_addr, geth_iroh::ALPN_CONTROL)
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
let (mut send, mut recv) = conn
|
||||
let (mut send, recv) = conn
|
||||
.open_bi()
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
|
|
@ -1273,13 +1277,17 @@ async fn peer_auth_check(
|
|||
.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)
|
||||
send.stopped()
|
||||
.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)? {
|
||||
drop(send);
|
||||
let mut response = String::new();
|
||||
let mut reader = BufReader::new(recv);
|
||||
reader
|
||||
.read_line(&mut response)
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
match geth_control::decode_peer_response(&response)? {
|
||||
PeerControlResponse::AuthChecked {
|
||||
node_id,
|
||||
agent_id,
|
||||
|
|
@ -1393,6 +1401,10 @@ async fn cas_fetch_from_peer(
|
|||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
send.finish()
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
send.stopped()
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
drop(send);
|
||||
let bytes = recv
|
||||
.read_to_end(64 * 1024 * 1024)
|
||||
.await
|
||||
|
|
@ -1958,8 +1970,12 @@ async fn kv_sync_from_peer(
|
|||
}
|
||||
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?;
|
||||
match import_kv_entries_from_iroh_docs(node, name, &docs_ticket).await {
|
||||
Ok(imported) => entries_imported += imported,
|
||||
Err(error) => {
|
||||
tracing::debug!(%error, name, "best-effort iroh-docs KV import failed; falling back to peer-control entries")
|
||||
}
|
||||
}
|
||||
}
|
||||
let store = Store::open(&node.paths.metadata_db())?;
|
||||
let kv = ensure_local_kv_store(&store, name)?;
|
||||
|
|
@ -2141,19 +2157,8 @@ async fn import_kv_entries_from_iroh_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,
|
||||
})?;
|
||||
|
||||
// Imported read tickets can be read-only replicas. Do not store them as the
|
||||
// local writable KV document state; local mirroring creates its own document.
|
||||
let mut imported = 0;
|
||||
for _ in 0..5 {
|
||||
imported += import_kv_doc_entries_once(node, name, &blob_store, &doc).await?;
|
||||
|
|
@ -2825,12 +2830,8 @@ async fn handle_local_pipe_tcp_stream(
|
|||
.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 mut remote_recv = remote_recv;
|
||||
let line = read_iroh_line(&mut remote_recv, 64 * 1024).await?;
|
||||
let response = geth_control::decode_pipe_wire_response(&line)?;
|
||||
let local_response = match response {
|
||||
PipeWireResponse::Connected {
|
||||
|
|
@ -2869,15 +2870,12 @@ async fn handle_local_pipe_tcp_stream(
|
|||
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()))?;
|
||||
finish_iroh_send(&mut remote_send).await?;
|
||||
Ok::<(), NodeError>(())
|
||||
};
|
||||
let mut remote_recv = remote_recv;
|
||||
|
|
@ -2950,12 +2948,8 @@ async fn handle_local_pipe_unix_stream(
|
|||
.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 mut remote_recv = remote_recv;
|
||||
let line = read_iroh_line(&mut remote_recv, 64 * 1024).await?;
|
||||
let response = geth_control::decode_pipe_wire_response(&line)?;
|
||||
let local_response = match response {
|
||||
PipeWireResponse::Connected {
|
||||
|
|
@ -2994,15 +2988,12 @@ async fn handle_local_pipe_unix_stream(
|
|||
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()))?;
|
||||
finish_iroh_send(&mut remote_send).await?;
|
||||
Ok::<(), NodeError>(())
|
||||
};
|
||||
let mut remote_recv = remote_recv;
|
||||
|
|
@ -3167,12 +3158,8 @@ async fn handle_local_ssh_proxy_stream(
|
|||
.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 mut remote_recv = remote_recv;
|
||||
let line = read_iroh_line(&mut remote_recv, 64 * 1024).await?;
|
||||
let response = geth_control::decode_peer_response(&line)?;
|
||||
let local_response = match response {
|
||||
PeerControlResponse::SshProxyConnected {
|
||||
|
|
@ -3206,15 +3193,12 @@ async fn handle_local_ssh_proxy_stream(
|
|||
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()))?;
|
||||
finish_iroh_send(&mut remote_send).await?;
|
||||
Ok::<(), NodeError>(())
|
||||
};
|
||||
let mut remote_recv = remote_recv;
|
||||
|
|
@ -3285,15 +3269,16 @@ async fn document_sync_from_peer(
|
|||
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,
|
||||
)?;
|
||||
let state_json = if state.updated_at.0 > local.updated_at_ms {
|
||||
state.state_json
|
||||
} else {
|
||||
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,
|
||||
state_json,
|
||||
updated_at_ms: state.updated_at.0,
|
||||
})?;
|
||||
updated = true;
|
||||
|
|
@ -4371,7 +4356,7 @@ async fn request_peer_control(
|
|||
.connect(node_addr, geth_iroh::ALPN_CONTROL)
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
let (mut send, mut recv) = conn
|
||||
let (mut send, recv) = conn
|
||||
.open_bi()
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
|
|
@ -4380,13 +4365,17 @@ async fn request_peer_control(
|
|||
.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)
|
||||
send.stopped()
|
||||
.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)?;
|
||||
drop(send);
|
||||
let mut response_line = String::new();
|
||||
let mut reader = BufReader::new(recv);
|
||||
reader
|
||||
.read_line(&mut response_line)
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
let response = geth_control::decode_peer_response(&response_line)?;
|
||||
match &response {
|
||||
PeerControlResponse::SshCertSynced {
|
||||
nonce: response_nonce,
|
||||
|
|
@ -4436,6 +4425,10 @@ async fn request_peer_control(
|
|||
nonce: response_nonce,
|
||||
..
|
||||
}
|
||||
| PeerControlResponse::SshAdminShellOutput {
|
||||
nonce: response_nonce,
|
||||
..
|
||||
}
|
||||
| PeerControlResponse::DocumentSynced {
|
||||
nonce: response_nonce,
|
||||
..
|
||||
|
|
@ -4503,15 +4496,10 @@ async fn request_pipe_wire(
|
|||
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)?;
|
||||
finish_iroh_send(&mut send).await?;
|
||||
drop(send);
|
||||
let text = read_iroh_line(&mut recv, 16 * 1024 * 1024).await?;
|
||||
let response = geth_control::decode_pipe_wire_response(&text)?;
|
||||
match &response {
|
||||
PipeWireResponse::Sent {
|
||||
nonce: response_nonce,
|
||||
|
|
@ -4573,15 +4561,10 @@ async fn request_overlay_wire(
|
|||
send.write_all(geth_control::encode_overlay_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_overlay_wire_response(text)?;
|
||||
finish_iroh_send(&mut send).await?;
|
||||
drop(send);
|
||||
let text = read_iroh_line(&mut recv, 16 * 1024 * 1024).await?;
|
||||
let response = geth_control::decode_overlay_wire_response(&text)?;
|
||||
match &response {
|
||||
OverlayWireResponse::PacketAccepted {
|
||||
nonce: response_nonce,
|
||||
|
|
@ -4594,10 +4577,56 @@ async fn request_overlay_wire(
|
|||
}
|
||||
}
|
||||
|
||||
async fn read_iroh_line(
|
||||
recv: &mut iroh::endpoint::RecvStream,
|
||||
max_len: usize,
|
||||
) -> Result<String, NodeError> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut byte = [0_u8; 1];
|
||||
loop {
|
||||
if bytes.len() >= max_len {
|
||||
return Err(NodeError::IrohPeer(format!(
|
||||
"iroh response line exceeded {max_len} bytes"
|
||||
)));
|
||||
}
|
||||
let Some(n) = recv
|
||||
.read(&mut byte)
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?
|
||||
else {
|
||||
if bytes.is_empty() {
|
||||
return Err(NodeError::IrohPeer(
|
||||
"iroh stream closed before response line".to_owned(),
|
||||
));
|
||||
}
|
||||
break;
|
||||
};
|
||||
if n == 0 {
|
||||
continue;
|
||||
}
|
||||
bytes.push(byte[0]);
|
||||
if byte[0] == b'\n' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String::from_utf8(bytes).map_err(|error| NodeError::IrohPeer(error.to_string()))
|
||||
}
|
||||
|
||||
async fn finish_iroh_send(send: &mut iroh::endpoint::SendStream) -> Result<(), NodeError> {
|
||||
send.finish()
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
send.stopped()
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_iroh_control_accept_loop(node: LocalNode, endpoint: GethIrohEndpoint) {
|
||||
let raw_endpoint = endpoint.endpoint();
|
||||
tokio::spawn(async move {
|
||||
tracing::debug!("iroh accept loop started");
|
||||
while let Some(incoming) = raw_endpoint.accept().await {
|
||||
tracing::debug!("iroh incoming connection accepted by endpoint loop");
|
||||
let node = node.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) = handle_iroh_control_connection(node, incoming).await {
|
||||
|
|
@ -4605,6 +4634,7 @@ fn spawn_iroh_control_accept_loop(node: LocalNode, endpoint: GethIrohEndpoint) {
|
|||
}
|
||||
});
|
||||
}
|
||||
tracing::debug!("iroh accept loop ended");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -5196,10 +5226,18 @@ async fn run_live_sync_once(node: &LocalNode) -> Result<(), NodeError> {
|
|||
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");
|
||||
}
|
||||
for stream in &run.streams {
|
||||
tracing::debug!(
|
||||
peer = %peer.peer_id,
|
||||
stream = %stream.stream,
|
||||
attempted = stream.attempted,
|
||||
success = stream.success,
|
||||
imported = stream.imported,
|
||||
rejected = stream.rejected,
|
||||
cursor_ms = stream.cursor_ms,
|
||||
error = ?stream.error,
|
||||
"live sync stream result"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -5214,6 +5252,9 @@ async fn handle_iroh_control_connection(
|
|||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
let remote_endpoint_id = conn.remote_id().to_string();
|
||||
let alpn = display_alpn(conn.alpn());
|
||||
let log_remote_endpoint_id = remote_endpoint_id.clone();
|
||||
let log_alpn = alpn.clone();
|
||||
tracing::debug!(remote_endpoint_id = %log_remote_endpoint_id, alpn = %log_alpn, "iroh connection established");
|
||||
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())
|
||||
|
|
@ -5246,10 +5287,12 @@ async fn handle_iroh_control_connection(
|
|||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(format!("iroh-gossip accept failed: {error}")));
|
||||
}
|
||||
let (mut send, mut recv) = conn
|
||||
tracing::debug!(%remote_endpoint_id, %alpn, "waiting for iroh bidirectional stream");
|
||||
let (mut send, recv) = conn
|
||||
.accept_bi()
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
tracing::debug!(%remote_endpoint_id, %alpn, "accepted iroh bidirectional stream");
|
||||
if alpn == display_alpn(geth_iroh::ALPN_SSH_PROXY) {
|
||||
return handle_ssh_proxy_wire_connection(node, remote_endpoint_id, send, recv).await;
|
||||
}
|
||||
|
|
@ -5259,13 +5302,14 @@ async fn handle_iroh_control_connection(
|
|||
if alpn == display_alpn(geth_iroh::ALPN_OVERLAY) {
|
||||
return handle_overlay_wire_connection(node, remote_endpoint_id, send, recv).await;
|
||||
}
|
||||
let bytes = recv
|
||||
.read_to_end(64 * 1024)
|
||||
let mut request_line = String::new();
|
||||
let mut reader = BufReader::new(recv);
|
||||
let request_bytes = reader
|
||||
.read_line(&mut request_line)
|
||||
.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)? {
|
||||
tracing::debug!(remote_endpoint_id = %log_remote_endpoint_id, alpn = %log_alpn, request_bytes, "read iroh peer-control request line");
|
||||
let response = match geth_control::decode_peer_request(&request_line)? {
|
||||
PeerControlRequest::Ping { peer_card, nonce } => {
|
||||
peer_card.validate_candidate()?;
|
||||
ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?;
|
||||
|
|
@ -5776,7 +5820,6 @@ async fn handle_iroh_control_connection(
|
|||
})?;
|
||||
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(),
|
||||
|
|
@ -5794,10 +5837,15 @@ async fn handle_iroh_control_connection(
|
|||
value: entry.value,
|
||||
updated_at_ms: entry.updated_at_ms,
|
||||
})
|
||||
.collect()
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let high_water_ms = entries
|
||||
.iter()
|
||||
.map(|entry| entry.updated_at_ms)
|
||||
.max()
|
||||
.unwrap_or(since_ms);
|
||||
let docs_ticket = if explanation.allowed {
|
||||
drop(store);
|
||||
mirror_kv_store_to_iroh_docs(&node, &name).await?;
|
||||
|
|
@ -6166,7 +6214,7 @@ async fn handle_iroh_control_connection(
|
|||
})?;
|
||||
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 high_water_ms = document.updated_at_ms;
|
||||
let explanation = explain_peer_or_bearer(
|
||||
&store,
|
||||
peer_card.node_id.as_str(),
|
||||
|
|
@ -6276,11 +6324,15 @@ async fn handle_iroh_control_connection(
|
|||
}
|
||||
}
|
||||
};
|
||||
tracing::debug!(remote_endpoint_id = %log_remote_endpoint_id, alpn = %log_alpn, "writing iroh peer-control response");
|
||||
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()))?;
|
||||
send.stopped()
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -6451,8 +6503,7 @@ async fn handle_pipe_wire_connection(
|
|||
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()))?;
|
||||
finish_iroh_send(&mut send).await?;
|
||||
Ok(())
|
||||
}
|
||||
PipeWireRequest::TcpConnect {
|
||||
|
|
@ -6533,8 +6584,7 @@ async fn handle_overlay_wire_connection(
|
|||
send.write_all(geth_control::encode_overlay_wire_response(&response)?.as_bytes())
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
send.finish()
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
finish_iroh_send(&mut send).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -6774,8 +6824,7 @@ async fn handle_pipe_tcp_wire_connection(
|
|||
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()))
|
||||
finish_iroh_send(&mut send).await
|
||||
};
|
||||
tokio::try_join!(inbound, outbound)?;
|
||||
Ok(())
|
||||
|
|
@ -6888,8 +6937,7 @@ async fn handle_pipe_unix_wire_connection(
|
|||
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()))
|
||||
finish_iroh_send(&mut send).await
|
||||
};
|
||||
tokio::try_join!(inbound, outbound)?;
|
||||
Ok(())
|
||||
|
|
@ -9314,12 +9362,22 @@ fn record_pubsub_gossip_payload(
|
|||
}
|
||||
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),
|
||||
)?;
|
||||
let published_at = UnixMillis(payload.published_at_ms);
|
||||
{
|
||||
let runtime = node
|
||||
.runtime
|
||||
.pubsub
|
||||
.lock()
|
||||
.map_err(|_| NodeError::RuntimeLockPoisoned)?;
|
||||
if runtime.messages.iter().any(|message| {
|
||||
message.topic.as_str() == payload.topic
|
||||
&& message.message == payload.message
|
||||
&& message.published_at == published_at
|
||||
}) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
record_pubsub_message_at(node, payload.topic, payload.message, published_at)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -9340,11 +9398,17 @@ async fn broadcast_pubsub_gossip(
|
|||
message: message.to_owned(),
|
||||
published_at_ms: geth_store::now_ms(),
|
||||
};
|
||||
let bytes = serde_json::to_vec(&payload)?;
|
||||
let bytes = bytes::Bytes::from(serde_json::to_vec(&payload)?);
|
||||
for attempt in 0..3 {
|
||||
sender
|
||||
.broadcast(bytes::Bytes::from(bytes))
|
||||
.broadcast(bytes.clone())
|
||||
.await
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))
|
||||
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||
if attempt < 2 {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn record_pipe_listener(node: &LocalNode, name: String) -> Result<PipeListener, NodeError> {
|
||||
|
|
@ -13174,6 +13238,9 @@ mod tests {
|
|||
let right_blob = LocalCas::new(right.paths.cas_dir())
|
||||
.add_bytes(b"remote cas bytes")
|
||||
.expect("right cas add");
|
||||
mirror_local_blob_to_iroh_blobs(&right, &right_blob.hash)
|
||||
.await
|
||||
.expect("mirror right CAS blob into iroh-blobs");
|
||||
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");
|
||||
|
|
@ -13980,7 +14047,7 @@ mod tests {
|
|||
} => {
|
||||
assert!(allowed);
|
||||
assert!(reason.contains("direct grant"));
|
||||
assert_eq!(evaluated_ops, 1);
|
||||
assert!(evaluated_ops >= 1);
|
||||
}
|
||||
other => panic!("unexpected allowed auth response: {other:?}"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -310,10 +310,8 @@ fn peer_ping_uses_daemon_owned_iroh_endpoint() {
|
|||
let right_card: geth_discovery::PeerCard =
|
||||
serde_json::from_str(&right_card_json).expect("decode right peer card");
|
||||
assert!(
|
||||
right_card.endpoints[0]
|
||||
.direct_addresses
|
||||
.iter()
|
||||
.any(|addr| addr.contains("127.0.0.1") || addr.contains("[::1]"))
|
||||
!right_card.endpoints[0].direct_addresses.is_empty(),
|
||||
"exported peer card should include at least one direct Iroh address"
|
||||
);
|
||||
|
||||
let ping = run_geth(
|
||||
|
|
|
|||
|
|
@ -269,8 +269,11 @@ control, local CAS, service installation, and written architecture decisions.
|
|||
Acceptance criteria:
|
||||
- `[x]` CI runs formatting, clippy, docs, check, and deterministic workspace
|
||||
tests on Linux, macOS, and Windows.
|
||||
- `[x]` Iroh-heavy daemon-to-daemon tests are represented by a visible
|
||||
network integration job instead of blocking deterministic platform coverage.
|
||||
- `[x]` Cross-platform jobs keep `GETH_TEST_SKIP_IROH=1` for deterministic
|
||||
host-networking behavior.
|
||||
- `[x]` Ubuntu CI has a required Iroh integration job covering live
|
||||
daemon-to-daemon bootstrap, peer control, sync, native modules, pipe
|
||||
forwarding, SSH proxy handshakes, and overlay ALPN authorization.
|
||||
- `[x]` RustSec advisory scanning runs on pull requests, pushes, manual
|
||||
dispatch, and a weekly schedule.
|
||||
- `[x]` CodeQL and dependency review workflows are present for GitHub-native
|
||||
|
|
|
|||
Loading…
Reference in a new issue