mod accept;
pub(crate) mod boot;
pub(crate) mod config_write;
mod dial;
mod handlers;
mod reach;
mod roster_install;
mod status;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use mcpmesh_net::registry::ConnRegistry;
use mcpmesh_net::{ServiceEntry, Services, SessionBackend, TrustGate};
use mcpmesh_trust::paths;
use tokio::sync::Semaphore;
use tokio::task::JoinHandle;
use crate::allowlist::{AllowlistGate, PeerStore};
use crate::audit::AuditSink;
use crate::backends::socket::SocketBackend;
use crate::backends::spawn::SpawnBackend;
use crate::config::{Backend, Config};
use crate::control::DaemonState;
use crate::pairing::LiveInvites;
use crate::roster::freshness::FreshnessStore;
use crate::roster::gate::RosterGate;
use crate::util::blocking;
use roster_install::roster_confirmed_path;
pub use accept::spawn_accept_loop;
pub use boot::serve_forever;
pub use dial::{dial_service, pipe_session, race_dial};
pub use handlers::{grant_service_access, remove_peer, rename_peer};
pub use reach::{REACH_TTL_SECS, ReachEntry, probe_peer, reachability_of};
pub use roster_install::{
install_roster_view_and_sever, should_staleness_sever, staleness_sweep_once,
};
pub use boot::{NetPlan, net_plan};
pub(crate) use handlers::{
add_peer, blob_fetch, blob_grant, blob_list, blob_publish, mint_invite, open_session, redeem,
register_service, unregister_ephemeral,
};
pub(crate) use roster_install::{install_roster, org_join, set_nickname, set_roster_url};
pub(crate) use status::{peer_infos, presence_peers, roster_status, service_infos};
pub const STACK_VERSION: &str = env!("CARGO_PKG_VERSION");
#[allow(dead_code)] const SPAWN_CONCURRENCY: usize = 4;
pub(crate) fn spawn_concurrency(cfg: &Config) -> usize {
(cfg.limits.max_sessions.max(1)) as usize
}
pub struct MeshState {
pub(crate) endpoint: iroh::Endpoint,
pub(crate) gate: Arc<dyn TrustGate>,
pub(crate) store: Arc<PeerStore>,
pub(crate) invites: Arc<LiveInvites>,
pub(crate) self_nickname: std::sync::RwLock<String>,
pub(crate) accept_task: tokio::sync::Mutex<Option<JoinHandle<()>>>,
pub(crate) poll_loop: tokio::sync::Mutex<Option<JoinHandle<()>>>,
pub(crate) reload_lock: tokio::sync::Mutex<()>,
pub(crate) config_path: PathBuf,
pub(crate) roster: Arc<RosterGate>,
pub(crate) conn_registry: Arc<ConnRegistry>,
pub(crate) gossip: Option<iroh_gossip::net::Gossip>,
pub(crate) blobs: Option<crate::roster::transport::RosterBlobs>,
pub(crate) roster_topic: tokio::sync::Mutex<Option<crate::roster::transport::RosterGossip>>,
pub(crate) presence_topic:
Arc<tokio::sync::Mutex<Option<crate::roster::transport::RosterGossip>>>,
pub(crate) presence_table: Arc<crate::roster::presence::PresenceTable>,
pub(crate) app_blobs: tokio::sync::Mutex<Option<Arc<crate::blobs::provider::AppBlobs>>>,
pub(crate) audit: std::sync::OnceLock<AuditSink>,
pub(crate) limits: std::sync::OnceLock<Arc<crate::limits::MeshLimiters>>,
pub(crate) roster_addr_book:
std::sync::OnceLock<std::sync::Arc<crate::roster::transport::RosterAddrBook>>,
pub(crate) self_binding: std::sync::OnceLock<Option<crate::pairing::rendezvous::SelfBinding>>,
pub(crate) recent_pairings:
std::sync::Mutex<std::collections::VecDeque<mcpmesh_local_api::RecentPairing>>,
pub(crate) reachability: std::sync::Mutex<std::collections::HashMap<[u8; 32], ReachEntry>>,
pub(crate) ephemeral_services:
std::sync::Mutex<std::collections::HashMap<String, EphemeralService>>,
}
#[derive(Clone)]
pub struct EphemeralService {
pub backend: mcpmesh_local_api::BackendSpec,
pub allow: Vec<String>,
}
const RECENT_PAIRINGS_CAP: usize = 8;
impl MeshState {
#[allow(clippy::too_many_arguments)]
pub fn new(
endpoint: iroh::Endpoint,
gate: Arc<dyn TrustGate>,
store: Arc<PeerStore>,
invites: Arc<LiveInvites>,
self_nickname: String,
config_path: PathBuf,
roster: Arc<RosterGate>,
conn_registry: Arc<ConnRegistry>,
gossip: Option<iroh_gossip::net::Gossip>,
blobs: Option<crate::roster::transport::RosterBlobs>,
roster_topic: Option<crate::roster::transport::RosterGossip>,
presence_topic: Option<crate::roster::transport::RosterGossip>,
) -> Arc<Self> {
Arc::new(Self {
endpoint,
gate,
store,
invites,
self_nickname: std::sync::RwLock::new(self_nickname),
accept_task: tokio::sync::Mutex::new(None),
poll_loop: tokio::sync::Mutex::new(None),
reload_lock: tokio::sync::Mutex::new(()),
config_path,
roster,
conn_registry,
gossip,
blobs,
roster_topic: tokio::sync::Mutex::new(roster_topic),
presence_topic: Arc::new(tokio::sync::Mutex::new(presence_topic)),
presence_table: Arc::new(crate::roster::presence::PresenceTable::new()),
app_blobs: tokio::sync::Mutex::new(None),
audit: std::sync::OnceLock::new(),
limits: std::sync::OnceLock::new(),
roster_addr_book: std::sync::OnceLock::new(),
self_binding: std::sync::OnceLock::new(),
recent_pairings: std::sync::Mutex::new(std::collections::VecDeque::new()),
reachability: std::sync::Mutex::new(std::collections::HashMap::new()),
ephemeral_services: std::sync::Mutex::new(std::collections::HashMap::new()),
})
}
pub(crate) fn record_pairing(
&self,
peer_nickname: String,
sas_code: String,
paired_at_epoch: u64,
) {
let mut ring = self
.recent_pairings
.lock()
.expect("recent_pairings lock not poisoned");
if ring.len() >= RECENT_PAIRINGS_CAP {
ring.pop_front();
}
ring.push_back(mcpmesh_local_api::RecentPairing {
peer_nickname,
sas_code,
paired_at_epoch,
});
}
pub(crate) fn recent_pairings(&self) -> Vec<mcpmesh_local_api::RecentPairing> {
self.recent_pairings
.lock()
.expect("recent_pairings lock not poisoned")
.iter()
.rev()
.cloned()
.collect()
}
pub async fn set_app_blobs(&self, provider: Arc<crate::blobs::provider::AppBlobs>) {
*self.app_blobs.lock().await = Some(provider);
}
pub async fn app_blobs(&self) -> Option<Arc<crate::blobs::provider::AppBlobs>> {
self.app_blobs.lock().await.clone()
}
pub(crate) fn self_nickname(&self) -> String {
self.self_nickname
.read()
.expect("self_nickname lock not poisoned")
.clone()
}
pub(crate) fn set_self_nickname(&self, nickname: String) {
*self
.self_nickname
.write()
.expect("self_nickname lock not poisoned") = nickname;
}
pub fn set_audit(&self, sink: AuditSink) {
let _ = self.audit.set(sink);
}
pub(crate) fn audit(&self) -> AuditSink {
self.audit.get().cloned().unwrap_or_default()
}
pub fn set_self_binding(&self, binding: Option<crate::pairing::rendezvous::SelfBinding>) {
let _ = self.self_binding.set(binding);
}
pub(crate) fn self_binding(&self) -> Option<crate::pairing::rendezvous::SelfBinding> {
self.self_binding.get().cloned().flatten()
}
pub fn set_limits(&self, limits: Arc<crate::limits::MeshLimiters>) {
let _ = self.limits.set(limits);
}
pub(crate) fn limits(&self) -> Arc<crate::limits::MeshLimiters> {
self.limits
.get()
.cloned()
.unwrap_or_else(crate::limits::MeshLimiters::unlimited)
}
pub(crate) fn roster_addr_book(
&self,
) -> Option<std::sync::Arc<crate::roster::transport::RosterAddrBook>> {
self.roster_addr_book.get().cloned()
}
pub async fn roster_topic_sender(&self) -> Option<iroh_gossip::api::GossipSender> {
self.roster_topic
.lock()
.await
.as_ref()
.map(|g| g.sender.clone())
}
pub async fn take_roster_topic_receiver(&self) -> Option<iroh_gossip::api::GossipReceiver> {
self.roster_topic
.lock()
.await
.as_mut()
.and_then(|g| g.receiver.take())
}
pub(crate) fn presence_ctx(&self) -> crate::roster::presence::PresenceCtx {
crate::roster::presence::PresenceCtx {
roster: self.roster.clone(),
table: self.presence_table.clone(),
topic: self.presence_topic.clone(),
}
}
pub(crate) fn inviter_ctx(self: &Arc<Self>) -> crate::pairing::rendezvous::InviterCtx {
let grant_mesh = self.clone();
let record_mesh = self.clone();
crate::pairing::rendezvous::InviterCtx {
store: self.store.clone(),
invites: self.invites.clone(),
config_path: self.config_path.clone(),
self_binding: self.self_binding(),
grant: Box::new(move |nickname, services| {
let mesh = grant_mesh.clone();
Box::pin(async move { grant_service_access(&mesh, &nickname, &services).await })
}),
record_pairing: Box::new(move |nickname, sas, paired_at| {
record_mesh.record_pairing(nickname, sas, paired_at);
}),
}
}
pub(crate) async fn confirm_roster_current(&self, now: i64) {
self.roster.set_last_confirmed(now);
let store = FreshnessStore::new(roster_confirmed_path(&self.config_path));
match blocking("join roster freshness persist", move || store.store(now)).await {
Ok(Ok(())) => {}
Ok(Err(e)) | Err(e) => {
tracing::warn!(%e, "persist roster freshness (in-memory freshness still applied)")
}
}
}
pub async fn set_accept_task(&self, handle: JoinHandle<()>) {
let mut guard = self.accept_task.lock().await;
if let Some(old) = guard.take() {
old.abort();
}
*guard = Some(handle);
}
}
impl crate::roster::distribute::DistributionHost for MeshState {
fn endpoint(&self) -> &iroh::Endpoint {
&self.endpoint
}
fn roster(&self) -> &RosterGate {
&self.roster
}
fn blobs(&self) -> Option<&crate::roster::transport::RosterBlobs> {
self.blobs.as_ref()
}
fn gossip_active(&self) -> bool {
self.gossip.is_some()
}
fn installed_roster_path(&self) -> PathBuf {
roster_install::installed_roster_path(self)
}
fn pinned_org_root_pk(&self) -> anyhow::Result<Option<String>> {
roster_install::mesh_config_org_root_pk(self)
}
fn addr_book(&self) -> Option<Arc<crate::roster::transport::RosterAddrBook>> {
self.roster_addr_book()
}
fn roster_topic_sender(
&self,
) -> impl std::future::Future<Output = Option<iroh_gossip::api::GossipSender>> + Send {
MeshState::roster_topic_sender(self)
}
fn take_roster_topic_receiver(
&self,
) -> impl std::future::Future<Output = Option<iroh_gossip::api::GossipReceiver>> + Send {
MeshState::take_roster_topic_receiver(self)
}
fn confirm_roster_current(&self, now: i64) -> impl std::future::Future<Output = ()> + Send {
MeshState::confirm_roster_current(self, now)
}
fn install_roster_bytes(
&self,
bytes: &[u8],
serial: u64,
channel: &'static str,
) -> impl std::future::Future<Output = anyhow::Result<bool>> + Send {
roster_install::converge_roster_bytes(self, bytes, serial, channel)
}
}
pub fn build_services(cfg: &Config) -> Services {
build_services_audited(
cfg,
&AuditSink::disabled(),
&crate::limits::MeshLimiters::unlimited(),
)
}
pub fn build_services_audited(
cfg: &Config,
audit: &AuditSink,
limiters: &Arc<crate::limits::MeshLimiters>,
) -> Services {
build_services_with_ephemeral(cfg, audit, limiters, &HashMap::new())
}
pub fn build_services_with_ephemeral(
cfg: &Config,
audit: &AuditSink,
limiters: &Arc<crate::limits::MeshLimiters>,
ephemeral: &HashMap<String, EphemeralService>,
) -> Services {
let mut map: HashMap<String, ServiceEntry> = HashMap::new();
for (name, svc) in &cfg.services {
let backend = match svc.backend_result() {
Ok(Backend::Run(cmd)) => session_backend_run(cmd, name, cfg, audit, limiters),
Ok(Backend::Socket(path)) => session_backend_socket(path, name, audit, limiters),
Err(e) => {
tracing::warn!(service = %name, %e, "skipping malformed service");
continue;
}
};
map.insert(
name.clone(),
ServiceEntry {
backend,
allow: svc.allow.clone(),
},
);
}
for (name, eph) in ephemeral {
let backend = match &eph.backend {
mcpmesh_local_api::BackendSpec::Run { cmd } => {
session_backend_run(cmd, name, cfg, audit, limiters)
}
mcpmesh_local_api::BackendSpec::Socket { path } => {
session_backend_socket(path, name, audit, limiters)
}
};
map.insert(
name.clone(),
ServiceEntry {
backend,
allow: eph.allow.clone(),
},
);
}
Services::new(map)
}
fn session_backend_run(
cmd: &[String],
name: &str,
cfg: &Config,
audit: &AuditSink,
limiters: &Arc<crate::limits::MeshLimiters>,
) -> Arc<dyn SessionBackend> {
Arc::new(SpawnBackend {
cmd: cmd.to_vec(),
concurrency: Arc::new(Semaphore::new(spawn_concurrency(cfg))),
service: name.to_string(),
audit: audit.clone(),
limiter: limiters.requests.clone(),
})
}
fn session_backend_socket(
path: &str,
name: &str,
audit: &AuditSink,
limiters: &Arc<crate::limits::MeshLimiters>,
) -> Arc<dyn SessionBackend> {
Arc::new(SocketBackend {
path: path.to_string(),
service: name.to_string(),
audit: audit.clone(),
limiter: limiters.requests.clone(),
})
}
fn short_fingerprint(id: &iroh::EndpointId) -> String {
id.to_string().chars().take(8).collect()
}
fn default_self_nickname(id: &iroh::EndpointId) -> String {
hostname_nickname().unwrap_or_else(|| short_fingerprint(id))
}
fn hostname_nickname() -> Option<String> {
let out = std::process::Command::new("hostname").output().ok()?;
sanitize_hostname(&String::from_utf8_lossy(&out.stdout))
}
fn sanitize_hostname(raw: &str) -> Option<String> {
let short = raw
.trim()
.split('.')
.next()
.unwrap_or("")
.to_ascii_lowercase();
let cleaned: String = short
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-')
.collect();
(!cleaned.is_empty()).then_some(cleaned)
}
pub fn serving_state(endpoint: iroh::Endpoint, store: Arc<PeerStore>) -> Arc<DaemonState> {
let gate: Arc<dyn TrustGate> = Arc::new(AllowlistGate::new(store.clone()));
let self_nickname = short_fingerprint(&endpoint.id());
let mesh = MeshState::new(
endpoint,
gate,
store,
Arc::new(LiveInvites::new()),
self_nickname,
paths::default_config_path().unwrap_or_default(),
Arc::new(RosterGate::empty()),
Arc::new(ConnRegistry::new()),
None,
None,
None,
None,
);
Arc::new(DaemonState::with_mesh(STACK_VERSION, mesh))
}
#[cfg(test)]
pub(crate) mod testutil {
use std::path::PathBuf;
use std::sync::Arc;
use mcpmesh_net::TrustGate;
use mcpmesh_net::registry::ConnRegistry;
use crate::allowlist::{AllowlistGate, PeerStore};
use crate::pairing::LiveInvites;
use crate::roster::gate::{ComposedGate, RosterGate};
use super::MeshState;
use super::boot::build_endpoint;
pub(crate) async fn hermetic_mesh(config_path: PathBuf) -> Arc<MeshState> {
let dir = config_path.parent().unwrap();
let store = Arc::new(PeerStore::open(&dir.join("state.redb")).unwrap());
let pairs = Arc::new(AllowlistGate::new(store.clone()));
let roster = Arc::new(RosterGate::empty());
let gate: Arc<dyn TrustGate> = Arc::new(ComposedGate::new(roster.clone(), pairs));
let hermetic = crate::config::NetworkCfg {
relay_mode: "disabled".into(),
..Default::default()
};
let endpoint = build_endpoint(iroh::SecretKey::from_bytes(&[7u8; 32]), &hermetic, false)
.await
.unwrap();
MeshState::new(
endpoint,
gate,
store,
Arc::new(LiveInvites::new()),
"test".into(),
config_path,
roster,
Arc::new(ConnRegistry::new()),
None,
None,
None,
None,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn recent_pairings_surfaces_the_inviter_side_sas() {
let dir = tempfile::tempdir().unwrap();
let mesh = super::testutil::hermetic_mesh(dir.path().join("config.toml")).await;
assert!(mesh.recent_pairings().is_empty(), "no pairings yet");
mesh.record_pairing("bob".into(), "tango-fig-cabbage".into(), 1000);
mesh.record_pairing("carol".into(), "delta-hop-iron".into(), 2000);
let recent = mesh.recent_pairings();
assert_eq!(recent.len(), 2);
assert_eq!(recent[0].peer_nickname, "carol");
assert_eq!(recent[0].sas_code, "delta-hop-iron");
assert_eq!(recent[1].peer_nickname, "bob");
assert_eq!(recent[1].sas_code, "tango-fig-cabbage");
}
#[test]
fn sanitize_hostname_makes_a_friendly_nickname() {
assert_eq!(sanitize_hostname("jetson\n").as_deref(), Some("jetson"));
assert_eq!(
sanitize_hostname("Johns-MacBook-Pro.local").as_deref(),
Some("johns-macbook-pro"),
"strip the domain, lowercase, keep dashes"
);
assert_eq!(
sanitize_hostname("nvidia jetson!").as_deref(),
Some("nvidiajetson"),
"drop spaces + punctuation"
);
assert_eq!(sanitize_hostname(" ").as_deref(), None);
assert_eq!(sanitize_hostname("").as_deref(), None);
assert_eq!(sanitize_hostname(".local").as_deref(), None);
}
#[test]
fn spawn_concurrency_reads_max_sessions_with_a_safe_floor() {
let c = Config::from_toml_str("[limits]\nmax_sessions = 2\n").unwrap();
assert_eq!(super::spawn_concurrency(&c), 2);
let dflt = Config::from_toml_str("").unwrap();
assert_eq!(super::spawn_concurrency(&dflt), 4, "default max_sessions");
let zero = Config::from_toml_str("[limits]\nmax_sessions = 0\n").unwrap();
assert_eq!(
super::spawn_concurrency(&zero),
1,
"a 0 misconfig floors to 1, never no-permits"
);
assert_eq!(
super::SPAWN_CONCURRENCY as u32,
crate::config::LimitsCfg::default().max_sessions,
"the documented default matches the config default"
);
}
}