Live sync SSH metadata in background
This commit is contained in:
parent
9887b47a40
commit
68153be5d5
9 changed files with 318 additions and 13 deletions
|
|
@ -7,6 +7,7 @@ license.workspace = true
|
|||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ use geth_ssh_identity::{
|
|||
};
|
||||
use geth_store::{
|
||||
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredFileConflict,
|
||||
StoredFileRoot, StoredKeychainOp, StoredKvEntry, StoredKvStore, StoredPeerCard, StoredResource,
|
||||
StoredResourceSecret, StoredSshCertRequest, StoredSshCertificate, StoredSshRevocation,
|
||||
StoredFileRoot, StoredKeychainOp, StoredKvEntry, StoredKvStore, StoredModuleState,
|
||||
StoredPeerCard, StoredResource, StoredResourceSecret, StoredSshCertRequest,
|
||||
StoredSshCertificate, StoredSshRevocation,
|
||||
};
|
||||
use geth_types::{
|
||||
AuthOpId, BlobHash, Capability, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind,
|
||||
|
|
@ -43,6 +44,7 @@ use geth_types::{
|
|||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
|
||||
|
|
@ -148,8 +150,14 @@ struct PipeRuntime {
|
|||
}
|
||||
|
||||
const PUBSUB_RING_LIMIT: usize = 256;
|
||||
const LIVE_SYNC_INTERVAL: Duration = Duration::from_secs(30);
|
||||
const PIPE_CONNECTION_RING_LIMIT: usize = 256;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
struct LiveSyncCursor {
|
||||
cursor_ms: i64,
|
||||
}
|
||||
|
||||
pub fn init_node(paths: &GethPaths) -> Result<LocalNode, NodeError> {
|
||||
paths.ensure_base_dirs()?;
|
||||
if !paths.config_file().exists() {
|
||||
|
|
@ -194,6 +202,7 @@ pub async fn run_daemon(paths: GethPaths) -> Result<(), NodeError> {
|
|||
.clone()
|
||||
{
|
||||
spawn_iroh_control_accept_loop(node.clone(), endpoint);
|
||||
spawn_background_live_sync(node.clone());
|
||||
}
|
||||
let _peer_card_lan_discovery = start_peer_card_lan_discovery(&node).await;
|
||||
if Path::new(&paths.socket_path()).exists() {
|
||||
|
|
@ -778,8 +787,17 @@ async fn ssh_cert_sync_from_peer(
|
|||
node: &LocalNode,
|
||||
peer_node: &str,
|
||||
) -> Result<ControlResponse, NodeError> {
|
||||
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, nonce }
|
||||
PeerControlRequest::SshCertSync {
|
||||
peer_card,
|
||||
since_ms,
|
||||
nonce,
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
match response {
|
||||
|
|
@ -789,6 +807,7 @@ async fn ssh_cert_sync_from_peer(
|
|||
endpoint_id,
|
||||
requests,
|
||||
certificates,
|
||||
high_water_ms,
|
||||
allowed,
|
||||
reason,
|
||||
note,
|
||||
|
|
@ -815,6 +834,7 @@ async fn ssh_cert_sync_from_peer(
|
|||
for certificate in &certificates {
|
||||
store.insert_ssh_certificate(&stored_from_ssh_certificate(certificate))?;
|
||||
}
|
||||
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,
|
||||
|
|
@ -837,11 +857,20 @@ async fn ssh_revocation_sync_from_peer(
|
|||
node: &LocalNode,
|
||||
peer_node: &str,
|
||||
) -> Result<ControlResponse, NodeError> {
|
||||
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, nonce },
|
||||
|peer_card, nonce| PeerControlRequest::SshRevocationSync {
|
||||
peer_card,
|
||||
since_ms,
|
||||
nonce,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
match response {
|
||||
|
|
@ -850,6 +879,7 @@ async fn ssh_revocation_sync_from_peer(
|
|||
agent_id,
|
||||
endpoint_id,
|
||||
revocations,
|
||||
high_water_ms,
|
||||
allowed,
|
||||
reason,
|
||||
note,
|
||||
|
|
@ -871,6 +901,7 @@ async fn ssh_revocation_sync_from_peer(
|
|||
for revocation in &revocations {
|
||||
store.insert_ssh_revocation(&stored_from_ssh_revocation(revocation))?;
|
||||
}
|
||||
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,
|
||||
|
|
@ -888,6 +919,33 @@ async fn ssh_revocation_sync_from_peer(
|
|||
}
|
||||
}
|
||||
|
||||
fn live_sync_cursor_key(peer_node: &str, stream: &str) -> String {
|
||||
format!("live-sync:{peer_node}:{stream}")
|
||||
}
|
||||
|
||||
fn load_live_sync_cursor(store: &Store, peer_node: &str, stream: &str) -> Result<i64, NodeError> {
|
||||
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(())
|
||||
}
|
||||
|
||||
async fn request_peer_control(
|
||||
node: &LocalNode,
|
||||
peer_node: &str,
|
||||
|
|
@ -975,6 +1033,42 @@ fn spawn_iroh_control_accept_loop(node: LocalNode, endpoint: GethIrohEndpoint) {
|
|||
});
|
||||
}
|
||||
|
||||
fn spawn_background_live_sync(node: LocalNode) {
|
||||
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(LIVE_SYNC_INTERVAL);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Err(error) = run_live_sync_once(&node).await {
|
||||
tracing::debug!(%error, "live sync tick failed");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
if let Err(error) = ssh_cert_sync_from_peer(node, &peer.peer_id).await {
|
||||
tracing::debug!(peer = %peer.peer_id, %error, "SSH cert live sync failed");
|
||||
}
|
||||
if let Err(error) = ssh_revocation_sync_from_peer(node, &peer.peer_id).await {
|
||||
tracing::debug!(peer = %peer.peer_id, %error, "SSH revocation live sync failed");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_iroh_control_connection(
|
||||
node: LocalNode,
|
||||
incoming: iroh::endpoint::Incoming,
|
||||
|
|
@ -1132,7 +1226,11 @@ async fn handle_iroh_control_connection(
|
|||
}
|
||||
}
|
||||
}
|
||||
PeerControlRequest::SshCertSync { peer_card, nonce } => {
|
||||
PeerControlRequest::SshCertSync {
|
||||
peer_card,
|
||||
since_ms,
|
||||
nonce,
|
||||
} => {
|
||||
peer_card.validate_candidate()?;
|
||||
ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?;
|
||||
let discovered = DiscoveredPeer::candidate(
|
||||
|
|
@ -1148,6 +1246,7 @@ async fn handle_iroh_control_connection(
|
|||
})?;
|
||||
let resource = "resource:ssh:certs".to_owned();
|
||||
let capability = "ssh_cert.sync".to_owned();
|
||||
let high_water_ms = geth_store::now_ms();
|
||||
let explanation = geth_auth::explain_auth_ops(
|
||||
&load_auth_ops_for_resource(&store, &resource)?,
|
||||
PrincipalId::new(peer_card.node_id.to_string()),
|
||||
|
|
@ -1157,12 +1256,12 @@ async fn handle_iroh_control_connection(
|
|||
let (requests, certificates) = if explanation.allowed {
|
||||
(
|
||||
store
|
||||
.list_ssh_cert_requests()?
|
||||
.list_ssh_cert_requests_since(since_ms)?
|
||||
.into_iter()
|
||||
.map(ssh_cert_request_from_stored)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
store
|
||||
.list_ssh_certificates()?
|
||||
.list_ssh_certificates_since(since_ms)?
|
||||
.into_iter()
|
||||
.map(ssh_certificate_from_stored)
|
||||
.collect(),
|
||||
|
|
@ -1177,6 +1276,7 @@ async fn handle_iroh_control_connection(
|
|||
remote_endpoint_id,
|
||||
requests,
|
||||
certificates,
|
||||
high_water_ms,
|
||||
allowed: explanation.allowed,
|
||||
reason: explanation.reason,
|
||||
evaluated_ops: explanation.evaluated_ops,
|
||||
|
|
@ -1184,7 +1284,11 @@ async fn handle_iroh_control_connection(
|
|||
note: "SSH certificate metadata sync authenticated endpoint/card binding and required ssh_cert.sync on resource:ssh:certs".to_owned(),
|
||||
}
|
||||
}
|
||||
PeerControlRequest::SshRevocationSync { peer_card, nonce } => {
|
||||
PeerControlRequest::SshRevocationSync {
|
||||
peer_card,
|
||||
since_ms,
|
||||
nonce,
|
||||
} => {
|
||||
peer_card.validate_candidate()?;
|
||||
ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?;
|
||||
let discovered = DiscoveredPeer::candidate(
|
||||
|
|
@ -1200,6 +1304,7 @@ async fn handle_iroh_control_connection(
|
|||
})?;
|
||||
let resource = "resource:ssh:revocations".to_owned();
|
||||
let capability = "ssh_revocation.sync".to_owned();
|
||||
let high_water_ms = geth_store::now_ms();
|
||||
let explanation = geth_auth::explain_auth_ops(
|
||||
&load_auth_ops_for_resource(&store, &resource)?,
|
||||
PrincipalId::new(peer_card.node_id.to_string()),
|
||||
|
|
@ -1208,7 +1313,7 @@ async fn handle_iroh_control_connection(
|
|||
);
|
||||
let revocations = if explanation.allowed {
|
||||
store
|
||||
.list_ssh_revocations()?
|
||||
.list_ssh_revocations_since(since_ms)?
|
||||
.into_iter()
|
||||
.map(ssh_revocation_from_stored)
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
|
|
@ -1221,6 +1326,7 @@ async fn handle_iroh_control_connection(
|
|||
endpoint_id: node.iroh_status.endpoint_id.clone().unwrap_or_default(),
|
||||
remote_endpoint_id,
|
||||
revocations,
|
||||
high_water_ms,
|
||||
allowed: explanation.allowed,
|
||||
reason: explanation.reason,
|
||||
evaluated_ops: explanation.evaluated_ops,
|
||||
|
|
@ -3138,6 +3244,62 @@ mod tests {
|
|||
.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()),
|
||||
},
|
||||
)
|
||||
.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()),
|
||||
},
|
||||
)
|
||||
.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:?}"),
|
||||
};
|
||||
|
||||
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())
|
||||
);
|
||||
|
||||
left_endpoint.shutdown().await;
|
||||
right_endpoint.shutdown().await;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue