use std::io;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use tokio::io::AsyncWrite;
use tokio::net::{UnixListener, UnixStream};
use crate::client::{ClientError, connect_control};
use crate::codec::write_frame;
use crate::protocol::{BackendSpec, Hello, Request};
pub fn ensure_private_dir(dir: &Path) -> io::Result<()> {
use std::os::unix::fs::MetadataExt;
std::fs::create_dir_all(dir)?;
let is_symlink = std::fs::symlink_metadata(dir)?.file_type().is_symlink();
if is_symlink {
return Err(io::Error::other(format!(
"runtime dir {} is a symlink; refusing",
dir.display()
)));
}
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
let meta = std::fs::metadata(dir)?;
if meta.uid() != rustix::process::geteuid().as_raw() {
return Err(io::Error::other(format!(
"runtime dir {} is not owned by us",
dir.display()
)));
}
Ok(())
}
pub fn bind_uds(path: &Path) -> io::Result<UnixListener> {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
ensure_private_dir(parent)?;
}
let _ = std::fs::remove_file(path);
let listener = UnixListener::bind(path)
.map_err(|e| io::Error::new(e.kind(), format!("bind {path:?}: {e}")))?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.map_err(|e| io::Error::new(e.kind(), format!("chmod 0600 {path:?}: {e}")))?;
Ok(listener)
}
pub fn check_peer_uid(stream: &UnixStream) -> bool {
let Ok(cred) = stream.peer_cred() else {
tracing::warn!("peer_cred unreadable: refusing local connection");
return false;
};
let peer = cred.uid();
let me = rustix::process::geteuid().as_raw();
if peer != me {
tracing::warn!(peer, me, "refusing cross-uid local connection");
return false;
}
true
}
pub fn peer_audiences(peer: &Value) -> Vec<String> {
let groups: Vec<String> = peer
.get("groups")
.and_then(|g| g.as_array())
.map(|arr| {
arr.iter()
.filter_map(|g| g.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default();
crate::principal_set(
peer.get("name").and_then(|v| v.as_str()),
peer.get("user_id").and_then(|v| v.as_str()),
&groups,
)
.into_iter()
.map(str::to_owned)
.collect()
}
pub async fn register_service(
control_sock: &Path,
service_name: &str,
backend_sock: &Path,
) -> Result<(), ClientError> {
let result = async {
let mut client = connect_control(control_sock).await?;
client
.request(Request::RegisterService {
name: service_name.to_string(),
backend: BackendSpec::Socket {
path: backend_sock.to_string_lossy().into_owned(),
},
allow: vec![],
})
.await?;
Ok(())
}
.await;
if let Err(e) = &result {
tracing::warn!(
service = service_name,
control_sock = %control_sock.display(),
error = %e,
"mcpmesh service registration failed — service stays unregistered until the daemon restarts"
);
}
result
}
pub fn mcpmesh_control_socket_from(base: &Path) -> PathBuf {
base.join("mcpmesh").join("mcpmesh.sock")
}
pub const ERR_PARAMS: i64 = -32602;
pub const ERR_INTERNAL: i64 = -32603;
pub fn ok(id: Option<Value>, result: Value) -> Value {
json!({"jsonrpc":"2.0","id": id.unwrap_or(Value::Null),"result": result})
}
pub fn err(id: Option<Value>, code: i64, message: &str) -> Value {
json!({"jsonrpc":"2.0","id": id.unwrap_or(Value::Null),"error":{"code":code,"message":message}})
}
pub fn reply(id: Value, r: Result<Value, (i64, String)>) -> Value {
match r {
Ok(v) => json!({"jsonrpc":"2.0","id":id,"result":v}),
Err((code, message)) => {
json!({"jsonrpc":"2.0","id":id,"error":{"code":code,"message":message}})
}
}
}
pub fn internal(e: impl std::fmt::Display) -> (i64, String) {
tracing::warn!(error = %e, "internal error (detail withheld from the caller)");
(ERR_INTERNAL, "internal error".to_string())
}
pub fn required_string_array(params: &Value, key: &str) -> Result<Vec<String>, (i64, String)> {
let arr = params
.get(key)
.and_then(|v| v.as_array())
.ok_or((ERR_PARAMS, format!("{key} (array of strings) is required")))?;
arr.iter()
.map(|v| {
v.as_str()
.map(str::to_owned)
.ok_or((ERR_PARAMS, format!("{key} must contain only strings")))
})
.collect()
}
pub fn people_from_status(status: &Value) -> Vec<Value> {
status["peers"]
.as_array()
.map(|peers| {
peers
.iter()
.filter_map(|p| {
let name = p["name"].as_str()?;
Some(json!({ "name": name, "user_id": p["user_id"].as_str() }))
})
.collect()
})
.unwrap_or_default()
}
pub async fn send_hello<W: AsyncWrite + Unpin>(
writer: &mut W,
api: &str,
api_version: &str,
stack_version: &str,
) -> io::Result<()> {
let hello = serde_json::to_value(Hello {
api: api.into(),
api_version: api_version.into(),
stack_version: stack_version.into(),
})
.expect("Hello serializes");
write_frame(writer, &hello).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::{FrameReader, Inbound, MAX_FRAME_BYTES};
use crate::protocol::{API_NAME, API_VERSION};
use serde_json::json;
#[test]
fn peer_audiences_is_groups_union_name_union_user_id() {
let peer = json!({"name":"bob-laptop","user_id":"b64u:BOB","groups":["eng","ops"]});
let mut a = peer_audiences(&peer);
a.sort();
assert_eq!(a, vec!["b64u:BOB", "bob-laptop", "eng", "ops"]);
assert!(peer_audiences(&json!({})).is_empty());
assert_eq!(
peer_audiences(&json!({"name":"bob","user_id":"","groups":[]})),
vec!["bob"]
);
}
#[test]
fn people_from_status_extracts_petname_and_user_id() {
let status = json!({"peers":[
{"name":"bob","services":["kb"],"user_id":"b64u:CGnYVhFY"},
{"name":"carol","services":[]}
]});
assert_eq!(
people_from_status(&status),
vec![
json!({"name":"bob","user_id":"b64u:CGnYVhFY"}),
json!({"name":"carol","user_id":null}),
]
);
assert!(people_from_status(&json!({})).is_empty());
}
#[test]
fn mcpmesh_control_socket_is_the_mcpmesh_sibling_dir() {
assert_eq!(
mcpmesh_control_socket_from(Path::new("/run/user/1000")),
Path::new("/run/user/1000/mcpmesh/mcpmesh.sock")
);
}
#[test]
fn internal_error_does_not_echo_detail() {
let (code, msg) =
internal("open /home/me/.local/share/kb/index/abc123def456/notes.jsonl: No such file");
assert_eq!(code, ERR_INTERNAL);
assert!(
!msg.contains("/home/me"),
"no filesystem path in the caller-visible message"
);
assert!(
!msg.contains("index/"),
"no index dir in the caller-visible message"
);
assert_eq!(msg, "internal error");
}
#[test]
fn required_string_array_is_strict() {
let ok_p = json!({"audiences": ["b64u:BOB", "eng"]});
assert_eq!(
required_string_array(&ok_p, "audiences").unwrap(),
vec!["b64u:BOB".to_string(), "eng".to_string()]
);
assert_eq!(
required_string_array(&json!({"audiences": []}), "audiences").unwrap(),
Vec::<String>::new()
);
for bad in [
json!({}),
json!({"audiences": "eng"}),
json!({"audiences": 42}),
json!({"audiences": ["eng", 7]}),
json!({"audiences": null}),
] {
let e = required_string_array(&bad, "audiences").unwrap_err();
assert_eq!(e.0, ERR_PARAMS, "payload {bad} must be a params error");
}
}
#[test]
fn ok_err_and_reply_shape_json_rpc_frames() {
let o = ok(Some(json!(1)), json!({"x": true}));
assert_eq!(o["id"], 1);
assert_eq!(o["result"]["x"], true);
let e = err(None, ERR_PARAMS, "bad");
assert_eq!(e["id"], Value::Null);
assert_eq!(e["error"]["code"], ERR_PARAMS);
let r = reply(json!(7), Err((ERR_INTERNAL, "internal error".into())));
assert_eq!(r["error"]["code"], ERR_INTERNAL);
assert_eq!(
reply(json!(8), Ok(json!({"ok":true})))["result"]["ok"],
true
);
}
#[tokio::test]
async fn bind_uds_forces_0600_socket_and_0700_parent() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let run = dir.path().join("plug");
std::fs::create_dir_all(&run).unwrap();
std::fs::set_permissions(&run, std::fs::Permissions::from_mode(0o755)).unwrap();
let sock = run.join("plug.sock");
let _listener = bind_uds(&sock).unwrap();
let dir_mode = std::fs::metadata(&run).unwrap().permissions().mode() & 0o777;
assert_eq!(dir_mode, 0o700, "runtime dir forced private");
let sock_mode = std::fs::metadata(&sock).unwrap().permissions().mode() & 0o777;
assert_eq!(sock_mode, 0o600, "socket is owner-only");
drop(_listener);
let _again = bind_uds(&sock).unwrap();
}
#[tokio::test]
async fn bind_uds_refuses_a_symlinked_runtime_dir() {
let dir = tempfile::tempdir().unwrap();
let real = dir.path().join("real");
std::fs::create_dir_all(&real).unwrap();
let link = dir.path().join("link");
std::os::unix::fs::symlink(&real, &link).unwrap();
let err = bind_uds(&link.join("plug.sock")).unwrap_err();
assert!(
err.to_string().contains("symlink"),
"refusal names the symlink: {err}"
);
assert!(ensure_private_dir(&link).is_err());
let _ok = bind_uds(&real.join("plug.sock")).unwrap();
}
#[tokio::test]
async fn check_peer_uid_accepts_a_same_uid_peer() {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("uid.sock");
let listener = bind_uds(&sock).unwrap();
let client = UnixStream::connect(&sock).await.unwrap();
let (server, _) = listener.accept().await.unwrap();
assert!(check_peer_uid(&server));
assert!(check_peer_uid(&client));
}
#[tokio::test]
async fn send_hello_writes_the_family_hello_frame() {
let (mut a, b) = tokio::io::duplex(1024);
send_hello(&mut a, "loc-local/1", "1", "0.1.0")
.await
.unwrap();
drop(a);
let mut reader = FrameReader::new(b, MAX_FRAME_BYTES);
let frame = match reader.next().await.unwrap().unwrap() {
Inbound::Frame(v) => v,
Inbound::Violation(v) => panic!("violation: {v:?}"),
};
assert_eq!(frame["api"], "loc-local/1");
assert_eq!(frame["api_version"], "1");
assert_eq!(frame["stack_version"], "0.1.0");
}
#[tokio::test]
async fn register_service_registers_a_socket_backend_with_empty_allow() {
let dir = tempfile::tempdir().unwrap();
let control = dir.path().join("mcpmesh.sock");
let listener = tokio::net::UnixListener::bind(&control).unwrap();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let (read_half, mut writer) = stream.into_split();
write_frame(
&mut writer,
&serde_json::to_value(Hello {
api: API_NAME.into(),
api_version: API_VERSION.into(),
stack_version: "0.1.0".into(),
})
.unwrap(),
)
.await
.unwrap();
let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
let req = match reader.next().await.unwrap().unwrap() {
Inbound::Frame(v) => v,
Inbound::Violation(_) => panic!("violation"),
};
assert_eq!(req["method"], "register_service");
assert_eq!(req["params"]["name"], "loc");
assert_eq!(
req["params"]["backend"]["socket"]["path"],
"/run/x/loc/loc.sock"
);
assert_eq!(req["params"]["allow"], json!([]));
write_frame(
&mut writer,
&json!({"jsonrpc":"2.0","id":1,"result":{"ok":true}}),
)
.await
.unwrap();
});
register_service(&control, "loc", Path::new("/run/x/loc/loc.sock"))
.await
.unwrap();
server.await.unwrap();
let gone = dir.path().join("nobody-home.sock");
assert!(
register_service(&gone, "loc", Path::new("/x"))
.await
.is_err()
);
}
}