feat(shipote): throughput + stats persistente + auth peer (fase P)

- FlowMeter (atomic u64 + rolling window 32 samples) en cada FlowChannel.
  flow_throughput() → (socket, bytes_total, bytes_per_sec). CLI:
  shipote flow throughput. Idle threshold 5s = rate 0.0.
- Snapshot v4 con stats_history persistente por workspace (cap 16).
  PersistedStats separado para evitar Instant. Restore hidrata el VecDeque
  con source="persisted".
- Auth SO_PEERCRED: daemon rechaza peers con uid distinto al propio.
  SHIPOTE_TRUST_ANYONE=1 = escape hatch documentado.

84 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 25, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-11 13:58:41 +00:00
parent 1cce50b290
commit 3486949d24
8 changed files with 273 additions and 12 deletions
+1
View File
@@ -26,3 +26,4 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
ulid = { workspace = true }
nix = { workspace = true }
libc = { workspace = true }
+62 -2
View File
@@ -17,8 +17,8 @@ use shipote_core::WorkspaceManager;
use shipote_discern::{DiscernPipeline, Hint};
use shipote_protocol::{
default_socket_path, read_frame, write_frame, CommandInfo as ProtoCommandInfo,
EdgeDiscernmentInfo, FlowInfo, QuotaReportInfo, Request, Response, WorkspaceStatsInfo,
WorkspaceSummary,
EdgeDiscernmentInfo, FlowInfo, FlowThroughputInfo, QuotaReportInfo, Request, Response,
WorkspaceStatsInfo, WorkspaceSummary,
};
use std::sync::Arc;
use tokio::net::{UnixListener, UnixStream};
@@ -209,9 +209,34 @@ async fn main() -> anyhow::Result<()> {
});
}
// UID propio (para auth). SHIPOTE_TRUST_ANYONE=1 deshabilita.
let own_uid = nix::unistd::getuid().as_raw();
let trust_anyone = std::env::var("SHIPOTE_TRUST_ANYONE").as_deref() == Ok("1");
if trust_anyone {
warn!("SHIPOTE_TRUST_ANYONE=1 — accepting any peer uid");
}
loop {
match listener.accept().await {
Ok((stream, _)) => {
// Auth: SO_PEERCRED es automático en Unix sockets. Si
// el uid del peer no coincide con el nuestro, rechazo
// antes de procesar nada (a menos que esté permitido).
if !trust_anyone {
match peer_uid(&stream) {
Ok(peer) if peer == own_uid => {}
Ok(peer) => {
warn!(peer, own = own_uid, "rejecting peer with different uid");
drop(stream);
continue;
}
Err(e) => {
warn!(?e, "could not read peer uid — rejecting");
drop(stream);
continue;
}
}
}
let mgr = mgr.clone();
let disc = discerner.clone();
let pool = sidecar_pool.clone();
@@ -229,6 +254,27 @@ async fn main() -> anyhow::Result<()> {
}
}
/// Lee SO_PEERCRED del Unix socket conectado. Devuelve el uid del peer.
fn peer_uid(stream: &tokio::net::UnixStream) -> std::io::Result<u32> {
use std::os::fd::AsRawFd;
let fd = stream.as_raw_fd();
let mut ucred: libc::ucred = unsafe { std::mem::zeroed() };
let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
let r = unsafe {
libc::getsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_PEERCRED,
&mut ucred as *mut _ as *mut _,
&mut len,
)
};
if r != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(ucred.uid)
}
async fn handle_client(
mut stream: UnixStream,
mgr: Arc<WorkspaceManager>,
@@ -559,6 +605,20 @@ async fn dispatch(
Response::FlowList { items }
}
Request::FlowThroughput => {
let items = mgr
.flow_throughput()
.await
.into_iter()
.map(|(socket, bytes_total, bytes_per_sec)| FlowThroughputInfo {
socket,
bytes_total,
bytes_per_sec,
})
.collect();
Response::FlowThroughput { items }
}
Request::FlowDrop { pipeline } => {
let existed = mgr.drop_pipeline_flows(pipeline).await;
Response::FlowDropped { pipeline, existed }