Daemon and IPC
xrat includes a long-lived daemon supervisor process that manages proxy sessions, monitors health, and handles auto-rotation. CLI commands communicate with the daemon via Unix domain socket IPC.
Daemon Architecture
The daemon runs as a background process with three main responsibilities:
- IPC Server β Listens for commands from CLI clients
- Health Monitor β Periodically checks proxy liveness
- Rotation Scheduler β Manages automatic proxy switching
Supervisor Event Loop
The daemon runs a tokio::select! loop:
#![allow(unused)]
fn main() {
loop {
tokio::select! {
_ = health_tick.tick() => {
check_proxy_health().await;
}
Some(event) = ipc_rx.recv() => {
handle_ipc_event(event).await;
}
_ = rotation_timer.tick() => {
trigger_rotation().await;
}
}
}
}
IPC Protocol
The daemon uses JSON over Unix domain socket with protocol version 1.
Socket Location
<runtime_dir>/daemon.sock
Default: ~/.config/xrat/runtime/daemon.sock
Connection Flow
- CLI command connects to the Unix socket
- Sends a JSON request
- Receives a JSON response
- Closes the connection
Request Format
{
"protocol_version": 1,
"request": {
"type": "RuntimeConnect",
"payload": {
"config_id": 42
}
}
}
Response Format
{
"protocol_version": 1,
"ok": true,
"code": 200,
"message": "success",
"payload": { ... }
}
Request Types
| Type | Description | Payload |
|---|---|---|
DaemonPing | Check daemon reachability | None |
DaemonShutdown | Request graceful shutdown | None |
RuntimeStatus | Get proxy runtime status | None |
RuntimeConnect | Start a proxy session | { config_id: i64 } |
RuntimeReplace | Atomic disconnect + connect | { trigger, candidate_id } |
RuntimeDisconnect | Stop the active proxy session | None |
ProxyStart | Enable auto-rotation | None |
ProxyStatus | Get rotation status | None |
ProxyStop | Disable auto-rotation | None |
Manual xrat proxy rotate calls RuntimeReplace with trigger = manual and an
optional candidate_id. There is no separate ProxyRotate request type.
Response Codes
| Code | Description |
|---|---|
200 | Success |
400 | Bad request (invalid payload) |
404 | Not found (no active session) |
409 | Conflict (session already running) |
500 | Internal error |
Daemon Lifecycle
Starting the Daemon
xrat daemon start
- Check if daemon is already running (try connecting to socket)
- Fork a background process
- Create the Unix socket
- Run the supervisor event loop
- Reattach to any stale sessions from previous daemon runs
Stopping the Daemon
xrat daemon stop
- Connect to the daemon socket
- Send
DaemonShutdownrequest - Daemon gracefully terminates:
- Stops the active proxy session (if running)
- Closes the IPC socket
- Exits cleanly
Checking Daemon Status
xrat daemon status
Attempts to connect to the socket and sends a DaemonPing request. Reports
whether the daemon is reachable and the protocol version.
Health Monitoring
The daemon periodically checks the health of the active proxy session.
Health Tick Interval
Every 15 seconds, the daemon:
- Loads the active session from the database
- Checks if the PID is still running
- Tests TCP reachability of the SOCKS port
- If health check fails:
- Logs the failure
- Triggers auto-rotation (if
health_trigger_enabled)
Health Check Implementation
#![allow(unused)]
fn main() {
async fn check_proxy_health() -> HealthStatus {
let session = db.get_latest_running_session().await?;
if !process_is_running(session.process_id) {
return HealthStatus::ProcessDead;
}
if !tcp_connect(session.socks_host, session.socks_port).await {
return HealthStatus::PortUnreachable;
}
HealthStatus::Healthy
}
}
Failure Handling
When a health check fails:
- Log the failure β Record in daemon logs
- Update session β Mark as
failedwith reason - Trigger rotation β If
health_trigger_enabled, start rotation - Respect cooldown β Donβt rotate if cooldown is active
Session Reconciliation
When the daemon starts, it reconciles stale sessions from previous runs.
Stale Session Detection
Query for sessions with:
status = 'running'stopped_at IS NULL
Reconciliation Logic
For each stale session:
- Check PID liveness β Is the process still running?
- Verify process identity β Does
/proc/<pid>/cmdlinematch the expected binary? - Decision:
- PID alive + cmdline matches β reattach (keep as
running) - PID alive + cmdline mismatch β mark failed (different process reused PID)
- PID dead β mark failed
- PID alive + cmdline matches β reattach (keep as
Reattach Validation
#![allow(unused)]
fn main() {
fn validate_reattach(pid: i64, expected_binary: &Path) -> bool {
let cmdline_path = format!("/proc/{}/cmdline", pid);
let cmdline = std::fs::read_to_string(cmdline_path).ok()?;
cmdline.contains(expected_binary.to_str()?)
}
}
This prevents reattaching to a different process that happens to have the same PID.
IPC Client
CLI commands use an IPC client to communicate with the daemon.
Connection Retry
The client attempts to connect with retry:
#![allow(unused)]
fn main() {
async fn connect_with_retry(socket_path: &Path, max_attempts: u32) -> Result<UnixStream> {
for attempt in 1..=max_attempts {
match UnixStream::connect(socket_path).await {
Ok(stream) => return Ok(stream),
Err(_) if attempt < max_attempts => {
sleep(Duration::from_millis(100)).await;
}
Err(e) => return Err(e),
}
}
}
}
Error Handling
If the daemon is not running or unreachable:
Error: daemon socket not reachable at /home/user/.config/xrat/runtime/daemon.sock
Hint: start the daemon with 'xrat daemon start'
Daemon Integration
CLI Commands via IPC
When the daemon is running, these commands route through IPC:
| Command | IPC Request |
|---|---|
xrat connect <id> | RuntimeConnect |
xrat disconnect | RuntimeDisconnect |
xrat status | RuntimeStatus |
xrat proxy start | ProxyStart |
xrat proxy status | ProxyStatus |
xrat proxy rotate | RuntimeReplace |
xrat proxy stop | ProxyStop |
Daemon Required
Runtime and proxy commands require the daemon IPC path. If the daemon is not
running, xrat connect, xrat status, xrat disconnect, and xrat proxy ...
return a hint to start it:
xrat daemon start
Supervisor State
The daemon maintains internal state:
#![allow(unused)]
fn main() {
struct SupervisorState {
rotation_enabled: bool,
last_rotation_at: Option<DateTime<Utc>>,
last_trigger: Option<RotationTrigger>,
cooldown_until: Option<DateTime<Utc>>,
next_timer_at: Option<DateTime<Utc>>,
instance_id: String,
}
}
Instance ID
Each daemon instance has a unique ID (UUID v4) used to track session ownership:
#![allow(unused)]
fn main() {
let instance_id = Uuid::new_v4().to_string();
}
Sessions created by the daemon include:
{
"owner_kind": "daemon",
"owner_instance_id": "550e8400-e29b-41d4-a716-446655440000"
}
Logging
The daemon logs to stderr and optionally to a file:
xrat daemon start 2> daemon.log
Or with systemd:
[Service]
StandardOutput=journal
StandardError=journal
Log Levels
Control verbosity with RUST_LOG:
RUST_LOG=info xrat daemon start
RUST_LOG=debug xrat daemon start
Security Considerations
Socket Permissions
The Unix socket is created with default permissions (usually 0755). On
multi-user systems, consider restricting access:
chmod 700 ~/.config/xrat/runtime/daemon.sock
API Key
The daemon does not enforce authentication for IPC. Security relies on Unix socket permissions and filesystem access control.
For the HTTP API (if enabled), use the key field in config.toml for
authentication.
Troubleshooting
Daemon Not Starting
Symptom: xrat daemon start fails or exits immediately
Check:
- Is a daemon already running?
xrat daemon status - Check logs:
RUST_LOG=debug xrat daemon start - Verify socket directory exists and is writable
IPC Connection Failed
Symptom: CLI commands fail with βdaemon socket not reachableβ
Check:
- Is the daemon running?
ps aux | grep xrat - Does the socket file exist?
ls -la ~/.config/xrat/runtime/daemon.sock - Check socket permissions
Stale Sessions
Symptom: xrat status shows a session but no proxy is running
Fix:
xrat disconnect
xrat daemon stop
xrat daemon start
The daemon will reconcile stale sessions on startup.
Related
daemonCLI β command reference- Auto-Rotation β rotation scheduling
- Runtime Management β session lifecycle