1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! The daemon's hardened Unix-domain-socket listener (#6288 slice 1).
//!
//! Why: ADR-0032 moves every trusty-* service off loopback TCP and onto a Unix
//! socket, and trusty-mpm's HTTP surface is ~35k SLOC across a dozen route
//! files. A single-PR cutover is not viable, so the socket was added ALONGSIDE
//! `127.0.0.1:7880` (slice 1) and route families move onto it one slice at a
//! time; the retire slice deletes the axum surface.
//!
//! What: the socket path (derived, never published to a discovery file), the
//! bind, and the serve loop. Which METHODS are served is `daemon::rpc`'s —
//! `build_router` names one `register` call per family, and slice 2 mounted
//! the first twenty. A name no family claims still answers `method_not_found`.
//!
//! The trust boundary is the socket, not the payload: `bind_singleton_hardened`
//! puts a `0600` socket in a `0700` directory, and `serve_until` runs the
//! peer-uid check on every accepted connection before a byte is read. None of
//! `daemon::api`'s origin-guard machinery is carried over — it is browser-CSRF
//! defence for a listener a page can reach, and it guards nothing here.
//!
//! Test: `socket_tests.rs`.
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context, Result};
use tokio::net::UnixListener;
use trusty_common::uds::server::{RpcRouter, RpcServeOptions, serve_until};
use super::rpc;
use super::state::DaemonState;
#[cfg(test)]
#[path = "socket_tests.rs"]
mod tests;
/// The socket this daemon binds, as both it and its consumers resolve it.
///
/// Why: derived, not published. `trusty_common::daemon_socket_path` is the same
/// entry point trusty-memory, trusty-review, and trusty-analyze resolve, so the
/// daemon and every consumer compute the identical path and there is no
/// discovery file for a stale write to contradict. It resolves
/// `<data dir>/trusty-mpm.sock` through `trusty_common::resolve_data_dir`,
/// which this crate already routes its bug-report store through.
///
/// # Errors
///
/// When the data directory cannot be resolved or created.
///
/// Test: `socket_path_is_the_product_named_socket_under_the_data_dir`.
pub fn socket_path() -> Result<PathBuf> {
// #6288: the ONE data-dir entry point; never a second hand-rolled resolver.
trusty_common::daemon_socket_path("trusty-mpm")
}
/// The methods this listener serves.
///
/// Why: the transport is fixed and this is the only part that grows. Slice 1
/// registered nothing; each later slice adds one `register` call for its own
/// route family, so a family arrives without the accept loop, the framing, or
/// the peer check being touched.
///
/// A name no family claims still answers `method_not_found` — the contract
/// slice 1 established for the whole surface now holds for the remainder of it.
///
/// Test: `unknown_method_gets_a_method_not_found_frame`,
/// `rpc_router_registers_every_documented_method`,
/// `every_scoped_route_has_a_method`.
fn build_router(state: &Arc<DaemonState>) -> RpcRouter {
// #6288 slice 2: the core request/response families. Slice 6 appends here.
let router = rpc::core::register(RpcRouter::new(), state);
// #6288 slice 3: the legacy session registry, hooks, and the polled feeds.
let router = rpc::sessions_legacy::register(router, state);
// #6288 slice 4: managed sessions, the control plane, and the L2 proxy.
let router = rpc::managed::register(router, state);
// #6288 slice 5: projects, deliverables/milestones, manager, bus, pairing,
// delegation.
let router = rpc::registry::register(router, state);
// #6288 step 1: the routes the sandboxed `tm` CLI still reached over HTTP.
let router = rpc::cli_socket::register(router, state);
// #6288 step 2a: MCP dispatch (`POST /rpc`) and the coordinator routes.
let router = rpc::mcp::register(router, state);
rpc::coordinator::register(router, state)
}
/// Per-connection budgets for this listener.
///
/// The shared defaults: a 30-second bound on delivering a REQUEST frame, and
/// the shared control-plane frame budget. Neither bounds a handler.
/// `mpm.mcp.dispatch` and `mpm.sessions.chat` run unbounded server-side on the
/// socket, where HTTP caps them at 175 s and 125 s (`request_deadline.rs`);
/// steps 2b/2c of #6288 close that gap. A later slice that moves a bulk route
/// across raises `max_frame_bytes` here and on the client together.
fn serve_options() -> RpcServeOptions {
RpcServeOptions::default()
}
/// A bound RPC socket, with the path it occupies.
///
/// Why the two travel together: the listener must be served and the path must
/// be unlinked afterwards, and the unlink has to name the path this listener
/// actually took. Passing them separately let a caller hand
/// [`serve_until_shutdown`] a path that was not the one [`bind`] used, which
/// unlinks somebody else's socket. One value cannot be assembled wrong.
///
/// Test: exercised by every test in `socket_tests.rs`.
#[derive(Debug)]
pub struct BoundSocket {
/// The listener [`bind`] took.
pub listener: UnixListener,
/// The path it occupies, unlinked by [`serve_until_shutdown`].
pub path: PathBuf,
}
/// Bind `socket`, refusing to start when another daemon is live on it.
///
/// Why `bind_singleton_hardened` rather than `bind_hardened`: this daemon is
/// supervised by launchd, and a predecessor that is SIGKILLed never reaches the
/// unlink in [`serve_until_shutdown`]. `bind_hardened` refuses an occupied path
/// outright, so the replacement launchd starts would fail its bind, exit, be
/// restarted, and fail again — a crash loop with no operator-visible cause.
/// `bind_singleton_hardened` probes first and takes over only a socket the
/// kernel proves nobody is serving, so a LIVE daemon is still never clobbered.
///
/// # Errors
///
/// Any bind failure, including `UdsSecurityError::AlreadyServing` — which means
/// another trusty-mpm is answering this path and this process must not start.
/// The error names the path; the caller propagates it rather than degrading to
/// an HTTP-only daemon (the Fail-Open Check, #6288).
///
/// Test: `bind_refuses_a_socket_another_process_is_serving`,
/// `bind_reclaims_a_stale_socket_file`.
pub async fn bind(socket: &Path) -> Result<BoundSocket> {
let listener = trusty_common::uds::bind_singleton_hardened(socket)
.await
.with_context(|| format!("bind trusty-mpm socket at {}", socket.display()))?;
Ok(BoundSocket {
listener,
path: socket.to_path_buf(),
})
}
/// Serve `listener` until `shutdown` resolves, then unlink `socket`.
///
/// What: the accept loop from `trusty_common::uds::server`, then the socket
/// file is removed BEFORE the listener is dropped. That order is the one
/// `webhook_relay::listener` records: reversed, there is a window in which
/// nothing answers the path but the file is still there, and a successor that
/// rebinds in that window has its fresh socket deleted by this process.
///
/// A SIGKILLed daemon never reaches this unlink. That is not a leak to guard
/// against here — [`bind`] reclaims a socket file nobody is serving.
///
/// Test: `serve_unlinks_its_socket_on_shutdown`,
/// `unknown_method_gets_a_method_not_found_frame`.
pub async fn serve_until_shutdown(
bound: BoundSocket,
state: Arc<DaemonState>,
shutdown: impl std::future::Future<Output = ()> + Send,
) {
let BoundSocket { listener, path } = bound;
let router = Arc::new(build_router(&state));
tracing::info!(
socket = %path.display(),
methods = router.method_names().count(),
"trusty-mpm serving rpc"
);
serve_until(&listener, router, serve_options(), shutdown).await;
if let Err(e) = std::fs::remove_file(&path) {
tracing::debug!(socket = %path.display(), error = %e, "socket already gone");
}
drop(listener);
}