use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use axum::{
Json,
extract::{ConnectInfo, State},
http::StatusCode,
response::IntoResponse,
};
use trusty_common::mcp::Request as McpRequest;
use crate::daemon::mcp_backend::StateBackend;
use crate::daemon::state::DaemonState;
pub fn is_loopback(addr: &SocketAddr) -> bool {
match addr.ip() {
IpAddr::V4(v4) => v4.is_loopback(),
IpAddr::V6(v6) => v6.is_loopback(),
}
}
pub async fn rpc_handler(
State(state): State<Arc<DaemonState>>,
ConnectInfo(peer): ConnectInfo<SocketAddr>,
Json(req): Json<McpRequest>,
) -> axum::response::Response {
if !is_loopback(&peer) {
tracing::warn!(
%peer,
"rejected non-loopback POST /rpc — the RPC endpoint is loopback-only"
);
return (
StatusCode::FORBIDDEN,
"POST /rpc is loopback-only; remote access is forbidden",
)
.into_response();
}
let backend = StateBackend::new(Arc::clone(&state));
let resp = crate::mcp::dispatch(&backend, req).await;
Json(resp).into_response()
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn is_loopback_accepts_v4_v6_loopback() {
let v4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 7880);
let v4_8 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 4, 5, 6)), 7880);
let v6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 7880);
assert!(is_loopback(&v4));
assert!(is_loopback(&v4_8), "all of 127.0.0.0/8 is loopback");
assert!(is_loopback(&v6));
}
#[test]
fn is_loopback_rejects_public() {
let lan = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)), 5000);
let public = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 443);
let v6_pub = SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
443,
);
assert!(!is_loopback(&lan));
assert!(!is_loopback(&public));
assert!(!is_loopback(&v6_pub));
}
}