use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use mcpmesh_net::ALPN_PING;
use mcpmesh_net::framing::{FrameReader, Inbound, write_frame};
use crate::util::epoch_now_i64;
use super::MeshState;
#[derive(Clone)]
pub struct ReachEntry {
pub reachable: bool,
pub rtt_ms: Option<u64>,
pub probed_at: i64,
pub meta: String,
pub services: Vec<String>,
}
pub const REACH_TTL_SECS: i64 = 20;
const PROBE_TIMEOUT: Duration = Duration::from_secs(3);
pub async fn probe_peer(mesh: &Arc<MeshState>, endpoint_id: [u8; 32]) -> ReachEntry {
let started = std::time::Instant::now();
let outcome = tokio::time::timeout(PROBE_TIMEOUT, probe_once(mesh, endpoint_id)).await;
let (reachable, meta, services) = match outcome {
Ok(Ok((meta, services))) => (true, meta, services),
_ => (false, String::new(), Vec::new()),
};
let entry = ReachEntry {
reachable,
rtt_ms: reachable.then(|| started.elapsed().as_millis() as u64),
probed_at: epoch_now_i64(),
meta,
services,
};
mesh.reachability
.lock()
.expect("reachability lock not poisoned")
.insert(endpoint_id, entry.clone());
entry
}
async fn probe_once(mesh: &Arc<MeshState>, endpoint_id: [u8; 32]) -> Result<(String, Vec<String>)> {
let id = iroh::EndpointId::from_bytes(&endpoint_id)
.map_err(|e| anyhow::anyhow!("invalid endpoint id: {e}"))?;
let store = mesh.store.clone();
let last_addr = tokio::task::spawn_blocking(move || store.resolve(&endpoint_id))
.await
.map_err(|e| anyhow::anyhow!("join peer resolve for probe: {e}"))?
.ok()
.flatten()
.and_then(|e| e.last_addr);
let addr = super::dial::stored_dial_addr(last_addr.as_deref(), id);
let conn = mesh.endpoint.connect(addr, ALPN_PING).await?;
let (mut send, recv) = conn.open_bi().await?;
write_frame(&mut send, &serde_json::json!({ "ping": true })).await?;
let _ = send.finish();
let mut reader = FrameReader::new(
tokio::io::BufReader::new(recv),
mcpmesh_net::framing::MAX_FRAME_BYTES,
);
match reader.next().await? {
Some(Inbound::Frame(v)) => Ok((pong_meta(&v), pong_services(&v))),
_ => anyhow::bail!("no pong from peer"),
}
}
fn pong_services(pong: &serde_json::Value) -> Vec<String> {
pong.get("services")
.and_then(|s| s.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
}
fn pong_meta(pong: &serde_json::Value) -> String {
pong.get("meta")
.and_then(|m| m.as_str())
.filter(|s| s.len() <= crate::roster::presence::APP_METADATA_MAX_BYTES)
.unwrap_or_default()
.to_string()
}
pub(crate) fn caller_admitted_services(
mesh: &Arc<MeshState>,
identity: &mcpmesh_net::PeerIdentity,
) -> Vec<String> {
use std::collections::HashSet;
let eid = identity.endpoint.principal();
let principals: HashSet<&str> =
mcpmesh_local_api::principal_set(Some(&eid), identity.user_id.as_deref(), &identity.groups)
.into_iter()
.collect();
let admits = |allow: &[String]| allow.iter().any(|a| principals.contains(a.as_str()));
let mut out: Vec<String> = Vec::new();
if let Ok(cfg) = crate::config::Config::load(&mesh.config_path) {
for (name, svc) in &cfg.services {
if admits(&svc.allow) {
out.push(name.clone());
}
}
}
for (name, eph) in mesh
.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned")
.iter()
{
if admits(&eph.allow) && !out.contains(name) {
out.push(name.clone());
}
}
out.sort();
out
}
pub fn reachability_of(mesh: &Arc<MeshState>) -> Vec<mcpmesh_local_api::PeerReachability> {
let now = epoch_now_i64();
let peers: Vec<(String, [u8; 32])> = mesh
.store
.list()
.unwrap_or_default()
.into_iter()
.map(|e| (e.nickname, e.endpoint_id))
.collect();
let cache = mesh
.reachability
.lock()
.expect("reachability lock not poisoned")
.clone();
let mut stale: Vec<[u8; 32]> = Vec::new();
let mut out = Vec::with_capacity(peers.len());
for (nickname, eid) in peers {
match cache.get(&eid) {
Some(e) => {
let age = (now - e.probed_at).max(0);
if age > REACH_TTL_SECS {
stale.push(eid);
}
out.push(mcpmesh_local_api::PeerReachability {
name: nickname,
reachable: e.reachable,
rtt_ms: e.rtt_ms,
age_secs: Some(age as u64),
meta: e.meta.clone(),
principal: Some(mcpmesh_net::EndpointId::from_bytes(eid).principal()),
});
}
None => {
stale.push(eid);
out.push(mcpmesh_local_api::PeerReachability {
name: nickname,
reachable: false,
rtt_ms: None,
age_secs: None, meta: String::new(),
principal: Some(mcpmesh_net::EndpointId::from_bytes(eid).principal()),
});
}
}
}
for eid in stale {
let mesh = mesh.clone();
tokio::spawn(async move {
probe_peer(&mesh, eid).await;
});
}
out
}
#[cfg(test)]
mod tests {
use super::pong_meta;
use crate::roster::presence::APP_METADATA_MAX_BYTES;
#[test]
fn pong_services_parses_the_array_and_tolerates_hostile_shapes() {
use super::pong_services;
assert_eq!(
pong_services(&serde_json::json!({"services": ["notes", "kb"]})),
vec!["notes".to_string(), "kb".to_string()]
);
assert!(pong_services(&serde_json::json!({"stack_version": "1"})).is_empty());
assert!(pong_services(&serde_json::json!({"services": 42})).is_empty());
assert!(pong_services(&serde_json::json!({"services": [1, {"x": 2}]})).is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn caller_admitted_services_returns_only_admitted() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
let caller_eid = mcpmesh_net::EndpointId::from_bytes([7u8; 32]).principal();
std::fs::write(
&config_path,
format!(
"[services.shared]\nsocket = \"/run/a.sock\"\nallow = [\"{caller_eid}\"]\n [services.grouped]\nsocket = \"/run/b.sock\"\nallow = [\"team-eng\"]\n [services.private]\nsocket = \"/run/c.sock\"\nallow = [\"eid:other\"]\n"
),
)
.unwrap();
let mesh = crate::daemon::testutil::hermetic_mesh(config_path).await;
let identity = mcpmesh_net::PeerIdentity {
endpoint: mcpmesh_net::EndpointId::from_bytes([7u8; 32]),
name: "bob".into(),
user_id: None,
groups: vec!["team-eng".into()],
};
let admitted = super::caller_admitted_services(&mesh, &identity);
assert_eq!(admitted, vec!["grouped".to_string(), "shared".to_string()]);
assert!(
!admitted.contains(&"private".to_string()),
"never a non-admitted service"
);
}
#[test]
fn pong_meta_extracts_within_cap_and_drops_the_rest() {
assert_eq!(
pong_meta(&serde_json::json!({"stack_version": "1", "meta": "v=1.2.3"})),
"v=1.2.3"
);
assert_eq!(pong_meta(&serde_json::json!({"stack_version": "1"})), "");
assert_eq!(pong_meta(&serde_json::json!({"meta": 42})), "");
assert_eq!(pong_meta(&serde_json::json!({"meta": {"x": 1}})), "");
let at = "x".repeat(APP_METADATA_MAX_BYTES);
assert_eq!(pong_meta(&serde_json::json!({"meta": at.clone()})), at);
let over = "x".repeat(APP_METADATA_MAX_BYTES + 1);
assert_eq!(
pong_meta(&serde_json::json!({"meta": over})),
"",
"oversized meta dropped"
);
}
}