6.8 KiB
6.8 KiB
shepherd-ipc
IPC layer for Shepherd.
Overview
This crate provides the local inter-process communication infrastructure between the Shepherd service (shepherdd) and its clients (launcher UI, HUD overlay, admin tools). It includes:
- Unix domain socket server - Listens for client connections
- NDJSON protocol - Newline-delimited JSON message framing
- Client management - Connection tracking and cleanup
- Peer authentication - UID-based role assignment
- Event broadcasting - Push events to subscribed clients
Architecture
┌─────────────────────────────────────────────────────────┐
│ shepherdd │
│ ┌──────────────────────────────────────────────────┐ │
│ │ IpcServer │ │
│ │ ┌──────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │Client 1 │ │Client 2 │ │Client 3 │ ... │ │
│ │ │(Launcher)│ │ (HUD) │ │ (Admin) │ │ │
│ │ └────┬─────┘ └────┬────┘ └────┬────┘ │ │
│ │ │ │ │ │ │
│ │ └────────────┴───────────┘ │ │
│ │ Unix Domain Socket │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│Launcher │ │ HUD │ │ Admin │
│ UI │ │ Overlay │ │ Tool │
└─────────┘ └─────────┘ └─────────┘
Server Usage
Starting the Server
use shepherd_ipc::IpcServer;
let mut server = IpcServer::new("/run/shepherdd/shepherdd.sock");
server.start().await?;
// Get message receiver for the main loop
let mut messages = server.take_message_receiver().await.unwrap();
// Accept connections in background
tokio::spawn(async move {
server.run().await
});
// Process messages in main loop
while let Some(msg) = messages.recv().await {
match msg {
ServerMessage::Request { client_id, request } => {
// Handle request, send response
let response = handle_request(request);
server.send_response(&client_id, response).await?;
}
ServerMessage::ClientConnected { client_id, info } => {
println!("Client {} connected as {:?}", client_id, info.role);
}
ServerMessage::ClientDisconnected { client_id } => {
println!("Client {} disconnected", client_id);
}
}
}
Broadcasting Events
use shepherd_api::Event;
// Send to all subscribed clients
server.broadcast_event(Event::new(EventPayload::StateChanged(snapshot))).await;
Client Roles
Clients are assigned roles based on their peer UID:
| UID | Role | Permissions |
|---|---|---|
| root (0) | Admin |
All commands |
| Service user | Admin |
All commands |
| Other | Shell |
Read + Launch/Stop |
// Role-based command filtering
match (request.command, client_info.role) {
(Command::ReloadConfig, ClientRole::Admin) => { /* allowed */ }
(Command::ReloadConfig, ClientRole::Shell) => { /* denied */ }
(Command::Launch { .. }, _) => { /* allowed for all */ }
// ...
}
Client Usage
Connecting
use shepherd_ipc::IpcClient;
let mut client = IpcClient::connect("/run/shepherdd/shepherdd.sock").await?;
Sending Commands
use shepherd_api::{Command, Response};
// Request current state
client.send(Command::GetState).await?;
let response: Response = client.recv().await?;
// Launch an entry
client.send(Command::Launch {
entry_id: "minecraft".into()
}).await?;
let response = client.recv().await?;
Subscribing to Events
// Subscribe to event stream
client.send(Command::SubscribeEvents).await?;
// Receive events
loop {
match client.recv_event().await {
Ok(event) => {
match event.payload {
EventPayload::WarningIssued { remaining, .. } => {
println!("Warning: {} seconds remaining", remaining.as_secs());
}
EventPayload::SessionEnded { .. } => {
println!("Session ended");
}
_ => {}
}
}
Err(IpcError::ConnectionClosed) => break,
Err(e) => eprintln!("Error: {}", e),
}
}
Protocol
Message Format
Messages use NDJSON (newline-delimited JSON):
{"type":"request","id":1,"command":"get_state"}\n
{"type":"response","id":1,"payload":{"api_version":1,...}}\n
{"type":"event","payload":{"type":"state_changed",...}}\n
Request/Response
Each request has an ID, matched in the response:
// Request
{"type":"request","id":42,"command":{"type":"launch","entry_id":"minecraft"}}
// Response
{"type":"response","id":42,"success":true,"payload":{...}}
Events
Events are pushed without request IDs:
{"type":"event","payload":{"type":"warning_issued","threshold":60,"remaining":{"secs":60}}}
Socket Permissions
The socket is created with mode 0660:
- Owner can read/write
- Group can read/write
- Others have no access
This allows the service to run as a dedicated user while permitting group members (e.g., shepherd group) to connect.
Rate Limiting
Per-client rate limiting prevents buggy or malicious clients from overwhelming the service:
// Default: 10 commands per second per client
if rate_limiter.check(&client_id) {
// Process command
} else {
// Respond with rate limit error
}
Error Handling
use shepherd_ipc::IpcError;
match result {
Err(IpcError::ConnectionClosed) => {
// Client disconnected
}
Err(IpcError::Json(e)) => {
// Protocol error
}
Err(IpcError::Io(e)) => {
// Socket error
}
_ => {}
}
Dependencies
tokio- Async runtimeserde/serde_json- JSON serializationnix- Unix socket peer credentialsshepherd-api- Message typesshepherd-util- Client IDs