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)]
#[non_exhaustive]
pub struct ReachEntry {
pub reachable: bool,
pub rtt_ms: Option<u64>,
pub probed_at: i64,
pub meta: String,
pub services: Vec<String>,
pub pong_at: Option<i64>,
pub seq: u64,
pub observed: u64,
pub path: mcpmesh_local_api::PeerPath,
}
#[derive(Clone, Debug)]
pub struct ReachTransition {
pub peer: mcpmesh_local_api::PeerReachability,
pub source: mcpmesh_local_api::ReachabilitySource,
}
pub const REACH_TTL_SECS: i64 = 20;
const PROBE_TIMEOUT: Duration = Duration::from_secs(3);
pub(crate) const PING_THROTTLE_CLOSE: &[u8] = b"ping rate limited";
#[derive(Debug)]
struct ProbeThrottled;
impl std::fmt::Display for ProbeThrottled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "probe refused: ping rate limited")
}
}
impl std::error::Error for ProbeThrottled {}
fn throttle_closed(conn: &iroh::endpoint::Connection) -> bool {
matches!(
conn.close_reason(),
Some(iroh::endpoint::ConnectionError::ApplicationClosed(ac))
if ac.reason.as_ref() == PING_THROTTLE_CLOSE
)
}
pub async fn probe_peer(mesh: &Arc<MeshState>, endpoint_id: [u8; 32]) -> ReachEntry {
let seq = mesh
.probe_seq
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let m = mesh.clone();
let refused = tokio::task::spawn_blocking(move || super::dial::dial_refused(&m, &endpoint_id))
.await
.unwrap_or(true);
if refused {
return ReachEntry {
reachable: false,
rtt_ms: None,
probed_at: epoch_now_i64(),
meta: String::new(),
services: Vec::new(),
pong_at: None,
seq,
observed: seq,
path: mcpmesh_local_api::PeerPath::Unknown,
};
}
let started = tokio::time::Instant::now();
let outcome = tokio::time::timeout(PROBE_TIMEOUT, probe_once(mesh, endpoint_id, started)).await;
if let Ok(Err(e)) = &outcome
&& e.downcast_ref::<ProbeThrottled>().is_some()
{
let previous = mesh
.reachability
.lock()
.expect("reachability lock not poisoned")
.get(&endpoint_id)
.cloned();
return previous.unwrap_or(ReachEntry {
reachable: false,
rtt_ms: None,
probed_at: epoch_now_i64(),
meta: String::new(),
services: Vec::new(),
pong_at: None,
seq,
observed: seq,
path: mcpmesh_local_api::PeerPath::Unknown,
});
}
let (reachable, meta, services, path, rtt_ms) =
classify(outcome, |conn| async move { settled_path(&conn).await }).await;
let observed = mesh
.probe_seq
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let probed_at = epoch_now_i64();
let entry = ReachEntry {
reachable,
rtt_ms,
probed_at,
meta,
services,
pong_at: reachable.then_some(probed_at),
seq,
observed,
path,
};
let outcome = {
let mut cache = mesh
.reachability
.lock()
.expect("reachability lock not poisoned");
match cache.get_mut(&endpoint_id) {
Some(newer) if !supersedes(seq, newer) => {
if newer.reachable && newer.pong_at.is_none() && entry.pong_at.is_some() {
newer.meta.clone_from(&entry.meta);
newer.services.clone_from(&entry.services);
newer.pong_at = entry.pong_at;
}
Outcome::Superseded(newer.clone())
}
Some(pong) if contradicted_by(&entry, seq, pong) => Outcome::Superseded(pong.clone()),
other => {
let previous = other.map(|e| e.clone());
cache.insert(endpoint_id, entry.clone());
Outcome::Committed(previous)
}
}
};
let previous = match outcome {
Outcome::Superseded(newer) => return newer,
Outcome::Committed(previous) => previous,
};
if is_transition(previous.as_ref(), &entry) {
if let Some(peer) = stored_row(mesh, endpoint_id, &entry) {
let _ = mesh.reach_bcast.send(ReachTransition {
peer,
source: mcpmesh_local_api::ReachabilitySource::Probe,
});
}
}
entry
}
pub(crate) fn supersedes(seq: u64, existing: &ReachEntry) -> bool {
seq >= existing.seq
}
fn contradicted_by(ours: &ReachEntry, seq: u64, existing: &ReachEntry) -> bool {
!ours.reachable && existing.reachable && existing.observed > seq
}
enum Outcome {
Committed(Option<ReachEntry>),
Superseded(ReachEntry),
}
const PATH_SETTLE: Duration = Duration::from_millis(600);
async fn settled_path(conn: &iroh::endpoint::Connection) -> mcpmesh_local_api::PeerPath {
settle(PATH_SETTLE, || selected_path(conn)).await
}
pub(crate) async fn settle<F>(window: Duration, mut probe: F) -> mcpmesh_local_api::PeerPath
where
F: FnMut() -> mcpmesh_local_api::PeerPath,
{
let deadline = tokio::time::Instant::now() + window;
loop {
let path = probe();
if path == mcpmesh_local_api::PeerPath::Direct || tokio::time::Instant::now() >= deadline {
return path;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
pub(crate) fn selected_path(conn: &iroh::endpoint::Connection) -> mcpmesh_local_api::PeerPath {
let paths = conn.paths();
classify_paths(
paths
.iter()
.map(|p| (path_kind(p.remote_addr()), p.is_selected())),
)
}
pub(crate) fn classify_paths<I>(paths: I) -> mcpmesh_local_api::PeerPath
where
I: IntoIterator<Item = (mcpmesh_local_api::PeerPath, bool)>,
{
let mut only: Option<mcpmesh_local_api::PeerPath> = None;
let mut count = 0usize;
for (kind, selected) in paths {
if selected {
return kind;
}
count += 1;
only = Some(kind);
}
match (count, only) {
(1, Some(kind)) => kind,
_ => mcpmesh_local_api::PeerPath::Unknown,
}
}
fn path_kind(addr: &iroh::TransportAddr) -> mcpmesh_local_api::PeerPath {
match addr {
iroh::TransportAddr::Relay(url) => mcpmesh_local_api::PeerPath::Relay {
url: Some(sanitize_relay_url(url)),
},
iroh::TransportAddr::Ip(_) => mcpmesh_local_api::PeerPath::Direct,
_ => mcpmesh_local_api::PeerPath::Unknown,
}
}
pub(crate) const PATH_MEASURE_WINDOW: Duration = Duration::from_millis(250);
pub(crate) const PATH_MEASURE_WINDOWS: usize = 3;
const _: () = assert!(
PATH_MEASURE_WINDOWS > 1,
"PATH_MEASURE_WINDOWS must allow a retry"
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct AppFrames(pub(crate) u64);
impl AppFrames {
pub(crate) fn of(stats: &iroh::endpoint::PathStats) -> Self {
Self(
stats.frame_tx.stream
+ stats.frame_tx.datagram
+ stats.frame_rx.stream
+ stats.frame_rx.datagram,
)
}
fn since(self, then: Self) -> u64 {
self.0.saturating_sub(then.0)
}
}
pub(crate) type PathSample = (
iroh::endpoint::PathId,
mcpmesh_local_api::PeerPath,
AppFrames,
);
#[derive(Debug, Clone)]
pub(crate) enum ObservedEvent {
Opened {
id: iroh::endpoint::PathId,
},
Closed {
id: iroh::endpoint::PathId,
remote_addr: iroh::TransportAddr,
last_stats: Box<iroh::endpoint::PathStats>,
},
Selected,
Lagged,
Unrecognised,
}
impl From<iroh::endpoint::PathEvent> for ObservedEvent {
fn from(event: iroh::endpoint::PathEvent) -> Self {
match event {
iroh::endpoint::PathEvent::Opened { id, .. } => Self::Opened { id },
iroh::endpoint::PathEvent::Closed {
id,
remote_addr,
last_stats,
..
} => Self::Closed {
id,
remote_addr,
last_stats,
},
iroh::endpoint::PathEvent::Selected { .. } => Self::Selected,
iroh::endpoint::PathEvent::Lagged { .. } => Self::Lagged,
_ => Self::Unrecognised,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum WindowEvent {
Opened { id: iroh::endpoint::PathId },
Closed {
id: iroh::endpoint::PathId,
kind: mcpmesh_local_api::PeerPath,
frames: AppFrames,
},
Lagged,
Unrecognised,
}
impl WindowEvent {
pub(crate) fn from_observed(event: ObservedEvent) -> Option<Self> {
match event {
ObservedEvent::Opened { id } => Some(Self::Opened { id }),
ObservedEvent::Closed {
id,
remote_addr,
last_stats,
} => Some(Self::Closed {
id,
kind: path_kind(&remote_addr),
frames: AppFrames::of(&last_stats),
}),
ObservedEvent::Selected => None,
ObservedEvent::Lagged => Some(Self::Lagged),
ObservedEvent::Unrecognised => Some(Self::Unrecognised),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum WindowReading {
Moved(mcpmesh_local_api::PeerPath),
Idle(mcpmesh_local_api::PeerPath),
Unobservable,
}
pub(crate) fn classify_window(
before: &[PathSample],
after: &[PathSample],
events: &[WindowEvent],
fallback: mcpmesh_local_api::PeerPath,
) -> WindowReading {
let baseline =
|id: &iroh::endpoint::PathId| before.iter().find(|(b, _, _)| b == id).map(|(_, _, f)| *f);
let opened: Vec<iroh::endpoint::PathId> = events
.iter()
.filter_map(|e| match e {
WindowEvent::Opened { id } => Some(*id),
_ => None,
})
.collect();
let mut relay: Option<mcpmesh_local_api::PeerPath> = None;
let mut direct_moved = false;
let mut unobservable = false;
let mut mark = |kind: &mcpmesh_local_api::PeerPath| match kind {
mcpmesh_local_api::PeerPath::Relay { .. } => {
relay.get_or_insert_with(|| kind.clone());
}
mcpmesh_local_api::PeerPath::Direct => direct_moved = true,
_ => unobservable = true,
};
let mut closed = Vec::new();
let mut untrusted_stream = false;
for event in events {
match event {
WindowEvent::Closed { id, kind, frames } => {
closed.push(*id);
let from = match baseline(id) {
Some(base) => base,
None if opened.contains(id) => AppFrames::default(),
None => continue,
};
if frames.since(from) > 0 {
mark(kind);
}
}
WindowEvent::Lagged | WindowEvent::Unrecognised => untrusted_stream = true,
WindowEvent::Opened { .. } => {}
}
}
for (id, kind, now) in after {
if now.since(baseline(id).unwrap_or_default()) > 0 {
mark(kind);
}
}
let accounted =
|id: &iroh::endpoint::PathId| after.iter().any(|(a, _, _)| a == id) || closed.contains(id);
let disappeared =
before.iter().any(|(id, _, _)| !accounted(id)) || opened.iter().any(|id| !accounted(id));
if let Some(relay) = relay {
return WindowReading::Moved(relay);
}
if unobservable || untrusted_stream || disappeared {
return WindowReading::Unobservable;
}
if direct_moved {
WindowReading::Moved(mcpmesh_local_api::PeerPath::Direct)
} else {
WindowReading::Idle(fallback)
}
}
pub(crate) async fn combine_windows<F, Fut>(
windows: usize,
mut measure: F,
) -> mcpmesh_local_api::PeerPath
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = WindowReading>,
{
for _ in 0..windows {
match measure().await {
WindowReading::Moved(path) => return path,
WindowReading::Unobservable => return mcpmesh_local_api::PeerPath::Unknown,
WindowReading::Idle(mcpmesh_local_api::PeerPath::Unknown) => continue,
WindowReading::Idle(path) => return path,
}
}
mcpmesh_local_api::PeerPath::Unknown
}
pub(crate) trait WindowSource {
fn is_closed(&self) -> bool;
fn subscribe(&self) -> n0_future::boxed::BoxStream<ObservedEvent>;
fn sample(&self) -> Vec<PathSample>;
fn fallback(&self) -> mcpmesh_local_api::PeerPath;
}
impl WindowSource for iroh::endpoint::Connection {
fn is_closed(&self) -> bool {
self.close_reason().is_some()
}
fn subscribe(&self) -> n0_future::boxed::BoxStream<ObservedEvent> {
use n0_future::StreamExt as _;
Box::pin(self.path_events().map(ObservedEvent::from))
}
fn sample(&self) -> Vec<PathSample> {
self.paths()
.iter()
.map(|p| {
(
p.id(),
path_kind(p.remote_addr()),
AppFrames::of(&p.stats()),
)
})
.collect()
}
fn fallback(&self) -> mcpmesh_local_api::PeerPath {
selected_path(self)
}
}
pub(crate) async fn measure_window<S: WindowSource>(source: &S, window: Duration) -> WindowReading {
use n0_future::StreamExt as _;
if source.is_closed() {
return WindowReading::Unobservable;
}
let mut events = source.subscribe();
let before = source.sample();
let mut seen = Vec::new();
let deadline = tokio::time::Instant::now() + window;
loop {
match tokio::time::timeout_at(deadline, events.next()).await {
Ok(Some(event)) => seen.extend(WindowEvent::from_observed(event)),
Ok(None) => return WindowReading::Unobservable,
Err(_elapsed) => break,
}
}
if source.is_closed() {
return WindowReading::Unobservable;
}
let after = source.sample();
loop {
match n0_future::future::now_or_never(tokio::task::coop::unconstrained(events.next())) {
Some(Some(event)) => seen.extend(WindowEvent::from_observed(event)),
Some(None) => return WindowReading::Unobservable,
None => break,
}
}
classify_window(&before, &after, &seen, source.fallback())
}
pub(crate) async fn measured_path(
conn: &iroh::endpoint::Connection,
window: Duration,
) -> mcpmesh_local_api::PeerPath {
combine_windows(PATH_MEASURE_WINDOWS, || measure_window(conn, window)).await
}
pub fn sanitize_relay_url(url: &iroh::RelayUrl) -> String {
let u: &url::Url = url; match (u.host_str(), u.port()) {
(Some(host), Some(port)) => format!("{}://{host}:{port}", u.scheme()),
(Some(host), None) => format!("{}://{host}", u.scheme()),
(None, _) => u.scheme().to_string(),
}
}
pub fn normalize_relay_url(raw: &str) -> Option<String> {
raw.parse::<iroh::RelayUrl>()
.ok()
.map(|u| sanitize_relay_url(&u))
}
fn is_transition(previous: Option<&ReachEntry>, current: &ReachEntry) -> bool {
match previous {
None => current.reachable,
Some(prev) => prev.reachable != current.reachable || prev.path != current.path,
}
}
pub(crate) fn reachability_row(
nickname: String,
endpoint_id: [u8; 32],
entry: Option<&ReachEntry>,
age_secs: Option<u64>,
) -> mcpmesh_local_api::PeerReachability {
mcpmesh_local_api::PeerReachability {
path: entry.map(|e| e.path.clone()).unwrap_or_default(),
name: nickname,
reachable: entry.is_some_and(|e| e.reachable),
rtt_ms: entry.and_then(|e| e.rtt_ms),
age_secs,
meta: entry.map(|e| e.meta.clone()).unwrap_or_default(),
principal: Some(mcpmesh_net::EndpointId::from_bytes(endpoint_id).principal()),
}
}
fn stored_row(
mesh: &Arc<MeshState>,
endpoint_id: [u8; 32],
entry: &ReachEntry,
) -> Option<mcpmesh_local_api::PeerReachability> {
let nickname = mesh.store.resolve(&endpoint_id).ok().flatten()?.nickname;
Some(reachability_row(
nickname,
endpoint_id,
Some(entry),
Some(0),
))
}
async fn probe_once(
mesh: &Arc<MeshState>,
endpoint_id: [u8; 32],
started: tokio::time::Instant,
) -> Result<(String, Vec<String>, iroh::endpoint::Connection, u64)> {
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 exchanged = exchange(&conn, started).await;
match exchanged {
Ok((meta, services, rtt_ms)) => Ok((meta, services, conn, rtt_ms)),
Err(e) if throttle_closed(&conn) => Err(e.context(ProbeThrottled)),
Err(e) => Err(e),
}
}
async fn exchange(
conn: &iroh::endpoint::Connection,
started: tokio::time::Instant,
) -> Result<(String, Vec<String>, u64)> {
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)) => {
let rtt_ms = started.elapsed().as_millis() as u64;
Ok((pong_meta(&v), pong_services(&v), rtt_ms))
}
_ => anyhow::bail!("no pong from peer"),
}
}
type ExchangeOutcome<C> =
std::result::Result<Result<(String, Vec<String>, C, u64)>, tokio::time::error::Elapsed>;
async fn classify<C, F, Fut>(
outcome: ExchangeOutcome<C>,
settle: F,
) -> (
bool,
String,
Vec<String>,
mcpmesh_local_api::PeerPath,
Option<u64>,
)
where
F: FnOnce(C) -> Fut,
Fut: std::future::Future<Output = mcpmesh_local_api::PeerPath>,
{
match outcome {
Ok(Ok((meta, services, conn, rtt_ms))) => {
let path = settle(conn).await;
(true, meta, services, path, Some(rtt_ms))
}
_ => (
false,
String::new(),
Vec::new(),
mcpmesh_local_api::PeerPath::Unknown,
None,
),
}
}
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> = mesh
.services
.get()
.iter()
.filter(|(_, entry)| admits(&entry.allow))
.map(|(name, _)| name.clone())
.collect();
out.sort();
out
}
pub(crate) async fn probe_peer_cached(mesh: &Arc<MeshState>, endpoint_id: [u8; 32]) -> ReachEntry {
let fresh = {
let cache = mesh
.reachability
.lock()
.expect("reachability lock not poisoned");
cache.get(&endpoint_id).and_then(|e| {
let age = (epoch_now_i64() - e.probed_at).max(0);
let has_payload = !e.reachable || e.pong_at.is_some();
(age <= REACH_TTL_SECS && has_payload).then(|| e.clone())
})
};
match fresh {
Some(e) => e,
None => probe_peer(mesh, endpoint_id).await,
}
}
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(reachability_row(nickname, eid, Some(e), Some(age as u64)));
}
None => {
stale.push(eid);
out.push(reachability_row(nickname, eid, None, None));
}
}
}
for (eid, guard) in claim_refreshes(mesh, stale) {
let mesh = mesh.clone();
tokio::spawn(async move {
let _guard = guard; probe_peer(&mesh, eid).await;
});
}
out
}
fn claim_refreshes(mesh: &Arc<MeshState>, stale: Vec<[u8; 32]>) -> Vec<([u8; 32], InFlight)> {
stale
.into_iter()
.filter_map(|eid| InFlight::claim(mesh, eid).map(|g| (eid, g)))
.collect()
}
struct InFlight {
mesh: Arc<MeshState>,
endpoint_id: [u8; 32],
}
impl InFlight {
fn claim(mesh: &Arc<MeshState>, endpoint_id: [u8; 32]) -> Option<Self> {
mesh.probes_inflight
.lock()
.expect("probes_inflight lock not poisoned")
.insert(endpoint_id)
.then(|| Self {
mesh: mesh.clone(),
endpoint_id,
})
}
}
impl Drop for InFlight {
fn drop(&mut self) {
self.mesh
.probes_inflight
.lock()
.expect("probes_inflight lock not poisoned")
.remove(&self.endpoint_id);
}
}
#[cfg(test)]
mod tests {
use super::pong_meta;
use super::{
AppFrames, ObservedEvent, ReachEntry, WindowEvent, WindowReading, WindowSource,
classify_paths, classify_window, combine_windows, contradicted_by, is_transition,
measure_window, sanitize_relay_url, supersedes,
};
use crate::roster::presence::APP_METADATA_MAX_BYTES;
use mcpmesh_local_api::PeerPath;
fn entry(reachable: bool, rtt_ms: Option<u64>) -> ReachEntry {
ReachEntry {
reachable,
rtt_ms,
probed_at: 1_700_000_000,
meta: String::new(),
services: Vec::new(),
pong_at: None,
seq: 0,
observed: 0,
path: mcpmesh_local_api::PeerPath::Unknown,
}
}
fn relay() -> PeerPath {
PeerPath::Relay {
url: Some("https://relay.example".into()),
}
}
#[test]
fn a_single_unselected_path_is_where_the_bytes_go() {
assert_eq!(
classify_paths([(PeerPath::Direct, false)]),
PeerPath::Direct
);
assert_eq!(classify_paths([(relay(), false)]), relay());
assert_eq!(
classify_paths([(PeerPath::Direct, false), (relay(), false)]),
PeerPath::Unknown,
"two unselected paths must NOT be resolved to either"
);
assert_eq!(
classify_paths([(PeerPath::Direct, false), (relay(), true)]),
relay()
);
assert_eq!(
classify_paths([(relay(), false), (PeerPath::Direct, true)]),
PeerPath::Direct
);
assert_eq!(classify_paths([]), PeerPath::Unknown);
assert_eq!(
classify_paths([(PeerPath::Unknown, false)]),
PeerPath::Unknown
);
}
fn ids() -> (
iroh::endpoint::PathId,
iroh::endpoint::PathId,
iroh::endpoint::PathId,
) {
use iroh::endpoint::PathId;
(PathId::ZERO, PathId::from(1u32), PathId::from(2u32))
}
#[test]
fn the_path_that_moved_application_frames_carried_the_data() {
let (p0, p1, _) = ids();
let before = [
(p0, PeerPath::Direct, AppFrames(10)),
(p1, relay(), AppFrames(4)),
];
let after = [
(p0, PeerPath::Direct, AppFrames(12)),
(p1, relay(), AppFrames(4)),
];
assert_eq!(
classify_window(&before, &after, &[], PeerPath::Unknown),
WindowReading::Moved(PeerPath::Direct)
);
let after = [
(p0, PeerPath::Direct, AppFrames(12)),
(p1, relay(), AppFrames(5)),
];
assert_eq!(
classify_window(&before, &after, &[], PeerPath::Direct),
WindowReading::Moved(relay())
);
assert_eq!(
classify_window(&before, &before, &[], PeerPath::Unknown),
WindowReading::Idle(PeerPath::Unknown)
);
assert_eq!(
classify_window(&before, &before, &[], PeerPath::Direct),
WindowReading::Idle(PeerPath::Direct)
);
}
#[test]
fn a_relay_path_that_closed_mid_window_is_attributed_from_its_closed_event() {
let (p0, p1, _) = ids();
let before = [
(p0, PeerPath::Direct, AppFrames(1000)),
(p1, relay(), AppFrames(4)),
];
let after = [(p0, PeerPath::Direct, AppFrames(1100))];
let closed = [WindowEvent::Closed {
id: p1,
kind: relay(),
frames: AppFrames(60),
}];
assert_eq!(
classify_window(&before, &after, &closed, PeerPath::Direct),
WindowReading::Moved(relay()),
"56 frames over a relay that closed mid-window must read Relay, never Direct"
);
let idle_close = [WindowEvent::Closed {
id: p1,
kind: relay(),
frames: AppFrames(4),
}];
assert_eq!(
classify_window(&before, &after, &idle_close, PeerPath::Unknown),
WindowReading::Moved(PeerPath::Direct)
);
}
#[test]
fn lagged_path_events_make_the_window_unobservable() {
let (p0, _, _) = ids();
let before = [(p0, PeerPath::Direct, AppFrames(10))];
let after = [(p0, PeerPath::Direct, AppFrames(12))];
assert_eq!(
classify_window(&before, &after, &[WindowEvent::Lagged], PeerPath::Direct),
WindowReading::Unobservable,
"a Lagged window must never read Direct"
);
}
#[test]
fn a_path_that_vanished_without_a_closed_event_is_unobservable() {
let (p0, _, p2) = ids();
let before = [(p0, relay(), AppFrames(4))];
let after = [(p2, PeerPath::Direct, AppFrames(500))];
assert_eq!(
classify_window(&before, &after, &[], PeerPath::Direct),
WindowReading::Unobservable,
"no evidence the vanished relay did not move: not Direct"
);
}
#[test]
fn a_relay_path_that_opened_mid_window_and_carried_frames_is_relay() {
let (p0, _, p2) = ids();
let before = [(p0, PeerPath::Direct, AppFrames(10))];
let after = [
(p0, PeerPath::Direct, AppFrames(12)),
(p2, relay(), AppFrames(99)),
];
assert_eq!(
classify_window(&before, &after, &[], PeerPath::Direct),
WindowReading::Moved(relay())
);
let after = [(p0, PeerPath::Direct, AppFrames(12))];
let closed = [
WindowEvent::Opened { id: p2 },
WindowEvent::Closed {
id: p2,
kind: relay(),
frames: AppFrames(99),
},
];
assert_eq!(
classify_window(&before, &after, &closed, PeerPath::Direct),
WindowReading::Moved(relay())
);
let after = [
(p0, PeerPath::Direct, AppFrames(12)),
(p2, relay(), AppFrames(0)),
];
assert_eq!(
classify_window(&before, &after, &[], PeerPath::Unknown),
WindowReading::Moved(PeerPath::Direct)
);
}
#[test]
fn a_moving_unmodelled_path_is_unobservable() {
let (p0, p1, _) = ids();
let before = [
(p0, PeerPath::Direct, AppFrames(10)),
(p1, PeerPath::Unknown, AppFrames(0)),
];
let after = [
(p0, PeerPath::Direct, AppFrames(12)),
(p1, PeerPath::Unknown, AppFrames(3)),
];
assert_eq!(
classify_window(&before, &after, &[], PeerPath::Direct),
WindowReading::Unobservable
);
}
#[test]
fn a_close_from_before_the_window_is_not_traffic() {
let (p0, p1, _) = ids();
let paths = [(p1, relay(), AppFrames(4))];
let stale_close = [WindowEvent::Closed {
id: p0,
kind: PeerPath::Direct,
frames: AppFrames(1000),
}];
assert_eq!(
classify_window(&paths, &paths, &stale_close, relay()),
WindowReading::Idle(relay()),
"a pre-window close must not read as Direct traffic"
);
}
#[test]
fn a_path_opened_in_the_window_and_still_open_is_attributed() {
let (p0, _, p2) = ids();
let before = [(p0, PeerPath::Direct, AppFrames(10))];
let opened = [WindowEvent::Opened { id: p2 }];
let after = [
(p0, PeerPath::Direct, AppFrames(10)),
(p2, PeerPath::Direct, AppFrames(5)),
];
assert_eq!(
classify_window(&before, &after, &opened, PeerPath::Unknown),
WindowReading::Moved(PeerPath::Direct),
"a direct path that opened and carried frames is Direct"
);
let after = [
(p0, PeerPath::Direct, AppFrames(10)),
(p2, relay(), AppFrames(5)),
];
assert_eq!(
classify_window(&before, &after, &opened, PeerPath::Direct),
WindowReading::Moved(relay()),
"a relay path that opened and carried frames is Relay"
);
}
#[test]
fn a_path_opened_in_the_window_that_disappeared_is_unobservable() {
let (p0, _, p2) = ids();
let before = [(p0, PeerPath::Direct, AppFrames(10))];
let after = [(p0, PeerPath::Direct, AppFrames(12))];
assert_eq!(
classify_window(
&before,
&after,
&[WindowEvent::Opened { id: p2 }],
PeerPath::Direct
),
WindowReading::Unobservable,
"an opened path that vanished may have been a relay carrying frames"
);
}
#[test]
fn every_path_event_kind_has_an_explicit_policy() {
let (p0, _, _) = ids();
assert_eq!(
WindowEvent::from_observed(ObservedEvent::Opened { id: p0 }),
Some(WindowEvent::Opened { id: p0 })
);
let mut stats = iroh::endpoint::PathStats::default();
stats.frame_tx.datagram = 7;
assert_eq!(
WindowEvent::from_observed(ObservedEvent::Closed {
id: p0,
remote_addr: iroh::TransportAddr::Ip("127.0.0.1:1".parse().unwrap()),
last_stats: Box::new(stats),
}),
Some(WindowEvent::Closed {
id: p0,
kind: PeerPath::Direct,
frames: AppFrames(7),
})
);
assert_eq!(WindowEvent::from_observed(ObservedEvent::Selected), None);
assert_eq!(
WindowEvent::from_observed(ObservedEvent::Lagged),
Some(WindowEvent::Lagged),
"a lost event must reach classification"
);
assert_eq!(
WindowEvent::from_observed(ObservedEvent::Unrecognised),
Some(WindowEvent::Unrecognised)
);
let paths = [(p0, PeerPath::Direct, AppFrames(10))];
let moved = [(p0, PeerPath::Direct, AppFrames(12))];
assert_eq!(
classify_window(
&paths,
&moved,
&[WindowEvent::Unrecognised],
PeerPath::Direct
),
WindowReading::Unobservable
);
}
#[derive(Clone, Default)]
struct Script(std::sync::Arc<std::sync::Mutex<ScriptState>>);
#[derive(Default)]
struct ScriptState {
subscribed: bool,
queue: std::collections::VecDeque<ObservedEvent>,
steps: std::collections::VecDeque<(Vec<super::PathSample>, Vec<ObservedEvent>)>,
stream_ends: bool,
}
impl Script {
fn new(steps: Vec<(Vec<super::PathSample>, Vec<ObservedEvent>)>) -> Self {
let script = Self::default();
script.0.lock().unwrap().steps = steps.into();
script
}
}
struct ScriptStream(Script);
impl n0_future::Stream for ScriptStream {
type Item = ObservedEvent;
fn poll_next(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<ObservedEvent>> {
let mut st = self.0.0.lock().unwrap();
match st.queue.pop_front() {
Some(event) => std::task::Poll::Ready(Some(event)),
None if st.stream_ends => std::task::Poll::Ready(None),
None => std::task::Poll::Pending,
}
}
}
impl WindowSource for Script {
fn is_closed(&self) -> bool {
false
}
fn subscribe(&self) -> n0_future::boxed::BoxStream<ObservedEvent> {
self.0.lock().unwrap().subscribed = true;
Box::pin(ScriptStream(self.clone()))
}
fn sample(&self) -> Vec<super::PathSample> {
let mut st = self.0.lock().unwrap();
let (paths, publish) = st.steps.pop_front().expect("scripted sample");
if st.subscribed {
st.queue.extend(publish);
}
paths
}
fn fallback(&self) -> PeerPath {
PeerPath::Unknown
}
}
fn relay_closed(id: iroh::endpoint::PathId, frames: u64) -> ObservedEvent {
let mut last_stats = iroh::endpoint::PathStats::default();
last_stats.frame_tx.datagram = frames;
ObservedEvent::Closed {
id,
remote_addr: iroh::TransportAddr::Relay(
"https://relay.example".parse().expect("relay url"),
),
last_stats: Box::new(last_stats),
}
}
#[tokio::test(start_paused = true)]
async fn the_subscription_is_taken_before_the_baseline() {
let (p0, p1, _) = ids();
let script = Script::new(vec![
(
vec![
(p0, PeerPath::Direct, AppFrames(10)),
(p1, relay(), AppFrames(4)),
],
vec![relay_closed(p1, 60)],
),
(vec![(p0, PeerPath::Direct, AppFrames(12))], vec![]),
]);
let reading = measure_window(&script, std::time::Duration::from_millis(250)).await;
assert!(
matches!(reading, WindowReading::Moved(PeerPath::Relay { .. })),
"a relay close right after the baseline must be seen: {reading:?}"
);
}
#[tokio::test(start_paused = true)]
async fn events_queued_after_the_final_sample_are_drained() {
let (p0, p1, _) = ids();
let script = Script::new(vec![
(
vec![
(p0, PeerPath::Direct, AppFrames(10)),
(p1, relay(), AppFrames(4)),
],
vec![],
),
(
vec![(p0, PeerPath::Direct, AppFrames(12))],
vec![relay_closed(p1, 60)],
),
]);
let reading = measure_window(&script, std::time::Duration::from_millis(250)).await;
assert!(
matches!(reading, WindowReading::Moved(PeerPath::Relay { .. })),
"a Closed queued just after the final sample must be drained: {reading:?}"
);
}
type BroadcastStep = (Vec<super::PathSample>, Vec<ObservedEvent>, bool);
struct BroadcastSource {
tx: tokio::sync::broadcast::Sender<ObservedEvent>,
steps: std::sync::Mutex<std::collections::VecDeque<BroadcastStep>>,
burned: std::sync::atomic::AtomicUsize,
}
impl WindowSource for BroadcastSource {
fn is_closed(&self) -> bool {
false
}
fn subscribe(&self) -> n0_future::boxed::BoxStream<ObservedEvent> {
Box::pin(n0_future::stream::unfold(
self.tx.subscribe(),
|mut rx| async move {
match rx.recv().await {
Ok(event) => Some((event, rx)),
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
Some((ObservedEvent::Lagged, rx))
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => None,
}
},
))
}
fn sample(&self) -> Vec<super::PathSample> {
let (paths, publish, burn) = self.steps.lock().unwrap().pop_front().expect("step");
if burn {
const SIDE: usize = 1000;
let (side_tx, mut side_rx) = tokio::sync::broadcast::channel::<u8>(SIDE);
for _ in 0..SIDE {
side_tx.send(0).unwrap();
}
let mut polls = 0;
while let Some(Ok(_)) = n0_future::future::now_or_never(side_rx.recv()) {
polls += 1;
}
self.burned
.store(polls, std::sync::atomic::Ordering::Relaxed);
}
for event in publish {
self.tx.send(event).expect("subscribed");
}
paths
}
fn fallback(&self) -> PeerPath {
PeerPath::Unknown
}
}
#[tokio::test(start_paused = true)]
async fn the_final_drain_is_not_cut_short_by_the_coop_budget() {
let (p0, _, p2) = ids();
let (tx, _keep) = tokio::sync::broadcast::channel(16);
let source = BroadcastSource {
tx,
steps: std::sync::Mutex::new(
vec![
(vec![(p0, PeerPath::Direct, AppFrames(10))], vec![], false),
(
vec![(p0, PeerPath::Direct, AppFrames(12))],
vec![ObservedEvent::Opened { id: p2 }, relay_closed(p2, 60)],
true,
),
]
.into(),
),
burned: std::sync::atomic::AtomicUsize::new(0),
};
let reading = measure_window(&source, std::time::Duration::from_millis(250)).await;
let burned = source.burned.load(std::sync::atomic::Ordering::Relaxed);
assert!(
burned < 1000,
"fixture: the burn must actually exhaust the coop budget (read {burned} of 1000), or \
this test measures nothing"
);
assert!(
matches!(reading, WindowReading::Moved(PeerPath::Relay { .. })),
"events queued on a budget-exhausted task must still be drained: {reading:?}"
);
}
#[tokio::test(start_paused = true)]
async fn an_event_stream_that_ends_early_is_unobservable() {
let (p0, _, _) = ids();
let script = Script::new(vec![
(vec![(p0, PeerPath::Direct, AppFrames(10))], vec![]),
(vec![(p0, PeerPath::Direct, AppFrames(12))], vec![]),
]);
script.0.lock().unwrap().stream_ends = true;
assert_eq!(
measure_window(&script, std::time::Duration::from_millis(250)).await,
WindowReading::Unobservable
);
assert_eq!(
script.0.lock().unwrap().steps.len(),
1,
"the window must stop when the stream ends, not carry on to a final sample"
);
}
async fn run_script(windows: usize, script: &[WindowReading]) -> (PeerPath, usize) {
let mut used = 0usize;
let path = combine_windows(windows, || {
let reading = script[used].clone();
used += 1;
async move { reading }
})
.await;
(path, used)
}
#[tokio::test]
async fn only_an_idle_unknown_window_is_retried() {
let idle_unknown = WindowReading::Idle(PeerPath::Unknown);
assert_eq!(
run_script(
3,
&[idle_unknown.clone(), WindowReading::Moved(PeerPath::Direct)]
)
.await,
(PeerPath::Direct, 2)
);
assert_eq!(
run_script(
3,
&[
WindowReading::Moved(PeerPath::Direct),
WindowReading::Moved(relay())
]
)
.await,
(PeerPath::Direct, 1),
"Moved(Direct) must not be retried"
);
assert_eq!(
run_script(
3,
&[
WindowReading::Moved(relay()),
WindowReading::Moved(PeerPath::Direct)
]
)
.await,
(relay(), 1)
);
assert_eq!(
run_script(
3,
&[
WindowReading::Idle(PeerPath::Direct),
WindowReading::Moved(relay())
]
)
.await,
(PeerPath::Direct, 1)
);
assert_eq!(
run_script(
3,
&[
WindowReading::Unobservable,
WindowReading::Moved(PeerPath::Direct)
]
)
.await,
(PeerPath::Unknown, 1),
"no Direct after an unobservable window"
);
assert_eq!(
run_script(
3,
&[
idle_unknown.clone(),
idle_unknown.clone(),
idle_unknown,
WindowReading::Moved(PeerPath::Direct)
]
)
.await,
(PeerPath::Unknown, 3)
);
}
#[test]
fn keepalives_on_a_standby_relay_path_are_not_application_data() {
fn every_counter(v: u64) -> iroh::endpoint::FrameStats {
let mut f = iroh::endpoint::FrameStats::default();
f.acks = v;
f.path_acks = v;
f.ack_frequency = v;
f.crypto = v;
f.connection_close = v;
f.data_blocked = v;
f.datagram = v;
f.handshake_done = 1;
f.immediate_ack = v;
f.max_data = v;
f.max_stream_data = v;
f.max_streams_bidi = v;
f.max_streams_uni = v;
f.new_connection_id = v;
f.path_new_connection_id = v;
f.new_token = v;
f.path_challenge = v;
f.path_response = v;
f.ping = v;
f.reset_stream = v;
f.retire_connection_id = v;
f.path_retire_connection_id = v;
f.stream_data_blocked = v;
f.streams_blocked_bidi = v;
f.streams_blocked_uni = v;
f.stop_sending = v;
f.stream = v;
f.observed_addr = v;
f.path_abandon = v;
f.path_status_available = v;
f.path_status_backup = v;
f.max_path_id = v;
f.paths_blocked = v;
f.path_cids_blocked = v;
f.add_address = v;
f.reach_out = v;
f.remove_address = v;
f
}
let mut idle = iroh::endpoint::PathStats::default();
idle.frame_tx = every_counter(3);
idle.frame_rx = every_counter(5);
idle.frame_tx.stream = 0;
idle.frame_tx.datagram = 0;
idle.frame_rx.stream = 0;
idle.frame_rx.datagram = 0;
idle.udp_tx.bytes = 4096;
idle.udp_tx.datagrams = 40;
idle.udp_rx.bytes = 4096;
idle.udp_rx.datagrams = 40;
idle.lost_packets = 2;
assert_eq!(
AppFrames::of(&idle),
AppFrames(0),
"no non-application counter may leak into the reading"
);
let mut busy = idle;
busy.frame_tx.stream = 3;
busy.frame_rx.datagram = 2;
assert_eq!(
AppFrames::of(&busy),
AppFrames(5),
"stream + datagram, both directions"
);
}
#[tokio::test(start_paused = true)]
async fn classification_costs_neither_the_verdict_nor_the_rtt() {
use mcpmesh_local_api::PeerPath;
use std::time::Duration;
let started = tokio::time::Instant::now();
let late = super::PROBE_TIMEOUT - Duration::from_millis(200);
tokio::time::sleep(late).await;
let rtt_at_pong = started.elapsed().as_millis() as u64;
let outcome: super::ExchangeOutcome<()> = Ok(Ok((
"meta".to_string(),
vec!["echo".to_string()],
(),
rtt_at_pong,
)));
let (reachable, _meta, _services, path, rtt_ms) = super::classify(outcome, |()| async {
tokio::time::sleep(super::PATH_SETTLE).await;
PeerPath::Relay { url: None }
})
.await;
assert!(
late + super::PATH_SETTLE > super::PROBE_TIMEOUT,
"the fixture must exceed the OLD combined budget, or it proves nothing"
);
assert!(
reachable,
"a pong inside super::PROBE_TIMEOUT must survive classification — reporting a peer offline \
while it is answering is #128, and it is wrong in the dangerous direction"
);
assert_eq!(path, PeerPath::Relay { url: None });
assert_eq!(
rtt_ms,
Some(2800),
"rtt_ms must be the pong stamp, NOT restamped after the settle window — restamping \
is a one-line edit away and silently restores #123"
);
let timed_out: super::ExchangeOutcome<()> =
tokio::time::timeout(Duration::ZERO, std::future::pending()).await;
let (reachable, _m, _s, path, rtt_ms) =
super::classify(timed_out, |()| async { PeerPath::Direct }).await;
assert!(!reachable);
assert_eq!(path, PeerPath::Unknown, "never a guessed route");
assert_eq!(rtt_ms, None, "never a fabricated measurement");
}
#[tokio::test(start_paused = true)]
async fn the_probe_waits_for_the_direct_path_instead_of_classifying_immediately() {
use mcpmesh_local_api::PeerPath;
use std::time::Duration;
let mut polls = 0;
let path = super::settle(super::PATH_SETTLE, || {
polls += 1;
if polls > 3 {
PeerPath::Direct
} else {
PeerPath::Relay { url: None }
}
})
.await;
assert_eq!(
path,
PeerPath::Direct,
"the settle window must outlast a punch that takes a few polls — classifying on the \
first look is what made Direct unreachable on every relay-enabled node"
);
assert_eq!(polls, 4, "it must stop polling as soon as Direct appears");
let mut polls = 0;
let path = super::settle(Duration::ZERO, || {
polls += 1;
if polls > 3 {
PeerPath::Direct
} else {
PeerPath::Relay { url: None }
}
})
.await;
assert_eq!(
path,
PeerPath::Relay { url: None },
"with no settle window the relay answer wins — this assertion is what fails if \
PATH_SETTLE is ever zeroed or the wait is removed"
);
let path = super::settle(super::PATH_SETTLE, || PeerPath::Relay { url: None }).await;
assert_eq!(
path,
PeerPath::Relay { url: None },
"a peer with no direct path must report Relay, not Unknown or Direct"
);
}
#[test]
fn sanitize_relay_url_drops_credentials_and_path() {
let u = |s: &str| -> iroh::RelayUrl { s.parse().expect("relay url") };
assert_eq!(
sanitize_relay_url(&u("https://user:token@relay.internal/")),
"https://relay.internal",
"userinfo must never reach the wire"
);
assert_eq!(
sanitize_relay_url(&u("https://relay.example:4433/some/path?x=1")),
"https://relay.example:4433",
"port is kept (it names the relay); path/query are not"
);
assert_eq!(
sanitize_relay_url(&u("https://relay.example/")),
"https://relay.example"
);
assert_eq!(
sanitize_relay_url(&u("http://192.168.1.5:4433/")),
"http://192.168.1.5:4433"
);
}
#[test]
fn a_probe_never_overwrites_a_newer_one() {
let mut newer = entry(true, Some(50));
newer.seq = 7;
assert!(
!supersedes(3, &newer),
"an older probe (ticket 3) must not overwrite ticket 7's result"
);
assert!(
supersedes(9, &newer),
"a newer probe (ticket 9) must overwrite"
);
assert!(
supersedes(7, &newer),
"equal tickets cannot happen, but must not wedge the guard"
);
}
#[test]
fn a_timeout_never_overwrites_a_pong_that_landed_inside_its_window() {
let ours = entry(false, None);
let mut pong_inside = entry(true, Some(50));
pong_inside.observed = 9;
assert!(
contradicted_by(&ours, 5, &pong_inside),
"a pong observed at 9 is inside the window of a probe that started at 5 — our timeout \
says nothing about a peer that answered someone else meanwhile"
);
assert!(
!contradicted_by(&ours, 11, &pong_inside),
"a probe that started after the last pong must be free to commit reachable: false"
);
let ours_ok = entry(true, Some(12));
assert!(
!contradicted_by(&ours_ok, 5, &pong_inside),
"the guard is about timeouts; a pong must always be free to commit"
);
let mut cached_down = entry(false, None);
cached_down.observed = 9;
assert!(
!contradicted_by(&ours, 5, &cached_down),
"an unreachable cached row is not evidence the peer answered; ours must commit"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_peer_does_not_commit_a_timeout_over_a_pong_from_its_own_window() {
let dir = tempfile::tempdir().unwrap();
let cfg = dir.path().join("config.toml");
std::fs::write(&cfg, "").unwrap();
let mesh = crate::daemon::testutil::hermetic_mesh(cfg).await;
let eid = [0x5Au8; 32];
let mut pong = entry(true, Some(42));
pong.seq = 0;
pong.observed = u64::MAX; mesh.reachability.lock().unwrap().insert(eid, pong);
let got = super::probe_peer(&mesh, eid).await;
assert!(
got.reachable,
"the unreachable probe must be DISCARDED and the surviving pong reported"
);
assert_eq!(
got.rtt_ms,
Some(42),
"and it must be that pong's row, not a synthesised one"
);
assert!(
mesh.reachability
.lock()
.unwrap()
.get(&eid)
.expect("row still cached")
.reachable,
"the CACHE must be left holding the pong — this is what pinned reachable:false for a \
full TTL"
);
let mut stale_pong = entry(true, Some(42));
stale_pong.seq = 0;
stale_pong.observed = 0;
mesh.reachability.lock().unwrap().insert(eid, stale_pong);
let got = super::probe_peer(&mesh, eid).await;
assert!(
!got.reachable,
"a peer that stopped answering must still be reported down — the guard only refuses \
probes that were already in flight when the last pong landed"
);
}
#[tokio::test]
async fn a_peer_already_being_refreshed_is_not_probed_again() {
let dir = tempfile::tempdir().unwrap();
let cfg = dir.path().join("config.toml");
std::fs::write(&cfg, "").unwrap();
let mesh = crate::daemon::testutil::hermetic_mesh(cfg).await;
let (a, b) = ([1u8; 32], [2u8; 32]);
let first = super::claim_refreshes(&mesh, vec![a, b]);
assert_eq!(
first.len(),
2,
"both peers are unclaimed, so both are refreshed"
);
assert!(
super::claim_refreshes(&mesh, vec![a, b]).is_empty(),
"a peer with a refresh already in flight must not be dialled again"
);
assert_eq!(
super::claim_refreshes(&mesh, vec![[3u8; 32]]).len(),
1,
"an unclaimed peer must still be refreshed while others are in flight"
);
drop(first);
assert_eq!(
super::claim_refreshes(&mesh, vec![a, b]).len(),
2,
"claims must be released when the refresh ends"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn reachability_of_holds_its_refresh_claim_across_the_probe() {
let dir = tempfile::tempdir().unwrap();
let cfg = dir.path().join("config.toml");
std::fs::write(&cfg, "").unwrap();
let mesh = crate::daemon::testutil::hermetic_mesh(cfg).await;
let peer_key = iroh::SecretKey::generate();
let peer_id = peer_key.public();
let eid = *peer_id.as_bytes();
let black_hole: std::net::SocketAddr = "127.0.0.1:1".parse().unwrap();
let addr = iroh::EndpointAddr::from_parts(peer_id, [iroh::TransportAddr::Ip(black_hole)]);
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: eid,
nickname: "never-probed".into(),
services: vec![],
paired_at: None,
user_id: None,
last_addr: Some(serde_json::to_string(&addr).unwrap()),
})
.unwrap();
let rows = super::reachability_of(&mesh);
assert_eq!(rows.len(), 1, "the stored peer must be projected");
assert!(
mesh.probes_inflight.lock().unwrap().contains(&eid),
"the refresh claim must still be HELD while the probe runs — a claim taken and \
released before the spawn dedups nothing"
);
assert!(
super::claim_refreshes(&mesh, vec![eid]).is_empty(),
"a second status poll inside the probe window must not spawn another dial"
);
}
#[test]
fn only_a_change_in_the_reachable_verdict_is_a_transition() {
assert!(
is_transition(None, &entry(true, Some(9))),
"first probe finds the peer UP — news"
);
assert!(
!is_transition(None, &entry(false, None)),
"first probe confirms DOWN — the snapshot already said so"
);
assert!(
is_transition(Some(&entry(false, None)), &entry(true, Some(9))),
"came back online"
);
assert!(
is_transition(Some(&entry(true, Some(9))), &entry(false, None)),
"went offline"
);
assert!(
!is_transition(Some(&entry(true, Some(9))), &entry(true, Some(9))),
"unchanged refresh"
);
assert!(
!is_transition(Some(&entry(true, Some(9))), &entry(true, Some(120))),
"rtt drift alone is not a transition"
);
assert!(
!is_transition(Some(&entry(false, None)), &entry(false, None)),
"still offline"
);
let mut relayed = entry(true, Some(9));
relayed.path = mcpmesh_local_api::PeerPath::Relay { url: None };
let mut direct = entry(true, Some(9));
direct.path = mcpmesh_local_api::PeerPath::Direct;
assert!(
is_transition(Some(&direct), &relayed),
"Direct -> Relay must emit: the privacy indicator just became wrong"
);
assert!(
is_transition(Some(&relayed), &direct),
"and Relay -> Direct, so a recovered session can be relabelled"
);
assert!(
!is_transition(Some(&direct), &direct.clone()),
"but an unchanged path still does not emit once per TTL"
);
let mut with_meta = entry(true, Some(9));
with_meta.meta = "status: away".into();
assert!(
!is_transition(Some(&entry(true, Some(9))), &with_meta),
"meta drift alone is not a transition"
);
}
#[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"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn peer_services_after_a_session_opens_still_fetches_the_pong() {
use crate::daemon::conn_cache::testpeer::{
dialer_id, dialer_mesh, dialer_principal, loopback_peer,
};
use std::sync::atomic::Ordering;
let dir = tempfile::tempdir().unwrap();
let me = dialer_principal();
let peer = loopback_peer(dir.path(), 52, dialer_id(), &[("echo", &[me.as_str()])]).await;
let mesh = dialer_mesh(dir.path(), &peer).await;
let _session = crate::daemon::dial::dial_service(&mesh, "bob", "echo")
.await
.expect("session");
let seq = mesh
.probe_seq
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
crate::daemon::path_watch::commit_observation(
&mesh,
peer.id,
seq,
&mcpmesh_local_api::PeerPath::Direct,
)
.expect("precondition: the watcher seeds a fresh, pong-less row");
let got = super::probe_peer_cached(&mesh, peer.id).await;
assert_eq!(
got.services,
vec!["echo".to_string()],
"a row with no pong must not answer peer_services — `[]` here reads as 'offers nothing'"
);
assert_eq!(
peer.ping_accepts.load(Ordering::SeqCst),
1,
"the payload can only come from a ping dial"
);
assert!(
got.pong_at.is_some(),
"and the refreshed row now carries its pong"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_superseded_probe_still_delivers_its_pong() {
use crate::daemon::conn_cache::testpeer::{
dialer_id, dialer_mesh, dialer_principal, loopback_peer,
};
let dir = tempfile::tempdir().unwrap();
let me = dialer_principal();
let peer = loopback_peer(dir.path(), 54, dialer_id(), &[("echo", &[me.as_str()])]).await;
let mesh = dialer_mesh(dir.path(), &peer).await;
let watcher_seq = 1u64 << 40;
crate::daemon::path_watch::commit_observation(
&mesh,
peer.id,
watcher_seq,
&mcpmesh_local_api::PeerPath::Direct,
)
.expect(
"precondition: the watcher seeds a pong-less row with a ticket the probe cannot beat",
);
let seeded = mesh
.reachability
.lock()
.unwrap()
.get(&peer.id)
.cloned()
.unwrap();
let got = super::probe_peer_cached(&mesh, peer.id).await;
assert_eq!(
got.services,
vec!["echo".to_string()],
"a superseded probe must still hand its pong to the caller"
);
let row = mesh
.reachability
.lock()
.unwrap()
.get(&peer.id)
.cloned()
.unwrap();
assert_eq!(row.services, vec!["echo".to_string()], "and to the cache");
assert!(row.pong_at.is_some(), "the cached row now carries a pong");
assert_eq!(
(row.seq, row.probed_at, row.observed),
(seeded.seq, seeded.probed_at, seeded.observed),
"the merge adds the payload only — it must not re-stamp the winning row's freshness or \
its ordering tickets"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn peer_services_right_after_a_session_opens_returns_the_services_every_time() {
use crate::daemon::conn_cache::testpeer::{
dialer_id, dialer_mesh, dialer_principal, loopback_peer,
};
let me = dialer_principal();
for run in 0..10u8 {
let dir = tempfile::tempdir().unwrap();
let peer = loopback_peer(
dir.path(),
90 + run,
dialer_id(),
&[("echo", &[me.as_str()])],
)
.await;
let mesh = dialer_mesh(dir.path(), &peer).await;
let _session = crate::daemon::dial::dial_service(&mesh, "bob", "echo")
.await
.expect("session");
let got = super::probe_peer_cached(&mesh, peer.id).await;
assert_eq!(
got.services,
vec!["echo".to_string()],
"run {run}: services right after a session opens"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn peer_services_never_answers_empty_from_a_throttled_probe() {
use crate::daemon::conn_cache::testpeer::{
dialer_id, dialer_mesh, dialer_principal, loopback_peer_with,
};
let dir = tempfile::tempdir().unwrap();
let me = dialer_principal();
let peer = loopback_peer_with(
dir.path(),
55,
dialer_id(),
&[("echo", &[me.as_str()])],
None,
true,
)
.await;
let mesh = dialer_mesh(dir.path(), &peer).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let seq = mesh
.probe_seq
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
crate::daemon::path_watch::commit_observation(
&mesh,
peer.id,
seq,
&mcpmesh_local_api::PeerPath::Direct,
)
.expect("precondition: a pong-less reachable row");
let got = crate::daemon::handlers::peer_services(&state, "bob".into()).await;
match got {
Ok(r) => panic!(
"a throttled probe fetched no services; answering {:?} claims the peer offers \
nothing",
r.services
),
Err(e) => {
let msg = format!("{e:#}");
assert!(
msg.contains("could not be fetched"),
"the refusal says the answer is unknown, not empty: {msg}"
);
}
}
}
#[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"
);
}
}