A lightweight, secure IPC daemon for Unix Domain Sockets featuring ephemeral X25519 ECDH key exchange, AES-256-GCM encrypted communication, and a configurable thread-pool concurrency model.
This project exists to demonstrate clean, idiomatic Rust applied to a practical systems-programming problem. It brings together several foundational concepts in a single cohesive codebase:
- Cryptography fundamentals — ephemeral Diffie-Hellman key agreement, HKDF key derivation, and AEAD encryption, all implemented with well-audited crates (
x25519-dalek,aes-gcm,hkdf). - Concurrency patterns — a hand-rolled thread pool with a work queue, interior mutability via
Arc<RwLock<>>, atomic signaling for graceful shutdown, and a dedicated accept/cleanup/signal thread architecture. - Protocol design — a simple binary framing protocol (magic bytes, versioning, message types, sequence numbers) that is easy to implement from any language.
- Production-adjacent practices — structured logging with
tracing, file-based configuration withserde/toml, comprehensive error handling with a customErrortype, and both unit and integration tests.
- Listens on a Unix Domain Socket (abstract or filesystem path)
- Accepts multiple concurrent client connections
- Ephemeral X25519 (Curve25519) ECDH key exchange per connection
- AES-256-GCM encryption via HKDF-SHA256 derived keys
- Random per-connection session IDs
- Binary framing protocol with magic, version, message type, and sequence numbers
- Configurable thread pool (accept → work queue → worker threads)
- Configurable max concurrent sessions
- Session timeout and periodic cleanup
- Graceful shutdown on SIGINT / SIGTERM
- Structured logging via
tracing/tracing-subscriber - Echo server built-in (for demonstration and testing)
flowchart TD
subgraph OS
SIGINT
SIGTERM
UDS["Unix Domain Socket"]
end
subgraph Daemon
AT["Accept Thread<br/>poll listener.accept()"]
WQ["Work Queue<br/>(Mutex + Condvar)"]
TP["Thread Pool<br/>(N workers)"]
SM["Session Manager<br/>(HashMap<ID, Session>)"]
CT["Cleanup Thread<br/>(periodic timeout sweep)"]
ST["Signal Handler Thread<br/>(sigwait)"]
end
UDS -->|new connection| AT
AT -->|dispatch stream| WQ
WQ -->|pop| TP
TP -->|register / unregister| SM
CT -->|cleanup_timed_out| SM
SIGINT --> ST
SIGTERM --> ST
ST -->|set running = false| AT
ST -->|set running = false| TP
ST -->|set running = false| CT
sequenceDiagram
participant Client
participant Server
Note over Client,Server: --- Handshake ---
Client->>Client: Generate ephemeral X25519 keypair
Client->>Server: Frame(HandshakeInit, client_public_key)
Server->>Server: Generate ephemeral X25519 keypair
Server->>Server: shared = ECDH(server_secret, client_public)
Server->>Server: aes_key = HKDF-SHA256(shared)
Server->>Client: Frame(HandshakeAck, server_public_key)
Client->>Client: shared = ECDH(client_secret, server_public)
Client->>Client: aes_key = HKDF-SHA256(shared)
Note over Client,Server: --- Encrypted ---
Client->>Client: nonce + AES-256-GCM(plaintext)
Client->>Server: Frame(EncryptedData, nonce || ciphertext)
Server->>Server: AES-256-GCM-decrypt(nonce || ciphertext)
Server->>Server: process request (echo)
Server->>Server: nonce + AES-256-GCM(response)
Server->>Client: Frame(EncryptedData, nonce || ciphertext)
Note over Client,Server: --- Close ---
Client->>Server: Frame(Close)
Server->>Client: Frame(Close)
flowchart LR
subgraph "Accept Thread"
A["loop: accept()<br/>dispatch to work queue"]
end
subgraph "Worker Threads (N)"
W1["Worker 0<br/>handshake + session loop"]
W2["Worker 1<br/>handshake + session loop"]
W3["Worker …<br/>handshake + session loop"]
end
subgraph "Session Cleanup Thread"
C["loop: sleep 30s<br/>cleanup_timed_out()"]
end
subgraph "Signal Handler Thread"
S["sigwait(SIGINT, SIGTERM)<br/>set running = false"]
end
A -->|stream| W1
A -->|stream| W2
A -->|stream| W3
S -->|running = false| A
S -->|running = false| W1
S -->|running = false| W2
S -->|running = false| W3
S -->|running = false| C
Each connection progresses through three states:
| State | Description |
|---|---|
| Handshake | Initial state. The worker reads a HandshakeInit frame containing the client's X25519 public key, generates its own ephemeral keypair, computes the shared secret via ECDH, derives the AES-256-GCM key via HKDF, and responds with a HandshakeAck containing its public key. On success, transitions to Active. |
| Active | Encrypted frames (EncryptedData) are exchanged bidirectionally. Each message is encrypted with AES-256-GCM using a random 12-byte nonce. The server echoes the decrypted payload back. Receiving a Close frame transitions to Closed. |
| Closed | The session is unregistered from the SessionManager, the stream is dropped, and the worker returns to the pool to pick up the next connection. |
cargo build --releaseThe compiled binary will be at target/release/secure-session-daemon.
Start the daemon with default configuration:
mkdir -p /tmp
cargo runOr run the built binary directly:
./target/release/secure-session-daemonThe daemon listens on /tmp/secure-session-daemon.sock by default. Send SIGINT or SIGTERM to shut down gracefully. Logs are printed to stderr via tracing.
Below is a complete Rust client that connects, performs the ECDH handshake, sends an encrypted message, reads the echo response, and closes the session:
use std::io::Write;
use std::os::unix::net::UnixStream;
use secure_session_daemon::crypto::KeyPair;
use secure_session_daemon::protocol::{self, Frame, MessageType};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut stream = UnixStream::connect("/tmp/secure-session-daemon.sock")?;
stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
// ---- Handshake ----
let client_kp = KeyPair::generate();
let client_pub = client_kp.public_bytes();
let init = Frame::new(MessageType::HandshakeInit, 0, client_pub.to_vec());
stream.write_all(&init.encode())?;
let ack = protocol::read_frame(&mut stream)?;
assert_eq!(ack.header.msg_type, MessageType::HandshakeAck);
let mut server_pub = [0u8; 32];
server_pub.copy_from_slice(&ack.payload);
let shared = client_kp.derive_shared(&server_pub);
println!("Handshake complete. Session key established.");
// ---- Encrypted Communication ----
let plaintext = b"Hello, SecureSessionDaemon!";
let encrypted = shared.encrypt(plaintext)?;
let data = Frame::new(MessageType::EncryptedData, 1, encrypted);
stream.write_all(&data.encode())?;
let resp = protocol::read_frame(&mut stream)?;
let decrypted = shared.decrypt(&resp.payload)?;
println!("Server echoed: {}", String::from_utf8_lossy(&decrypted));
// ---- Close ----
let close = Frame::new(MessageType::Close, 2, vec![]);
stream.write_all(&close.encode())?;
Ok(())
}Add the crate as a dependency to use it from your own project:
[dependencies]
secure-session-daemon = { git = "https://github.com/anomalyco/SecureSessionDaemon" }Create a config.toml in the working directory and run the binary:
socket_path = "/tmp/secure-session-daemon.sock"
worker_threads = 4
max_sessions = 64
session_timeout_secs = 300
log_level = "info"Starting the server from code (e.g. in tests):
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use secure_session_daemon::config::Config;
use secure_session_daemon::session_manager::SessionManager;
use secure_session_daemon::thread_pool::ThreadPool;
use secure_session_daemon::{logging, server};
let config = Config {
socket_path: "/tmp/mydaemon.sock".to_string(),
worker_threads: 4,
max_sessions: 16,
session_timeout_secs: 300,
log_level: "info".to_string(),
};
logging::init(&config.log_level);
let running = Arc::new(AtomicBool::new(true));
let session_manager = Arc::new(SessionManager::new(
config.max_sessions,
config.session_timeout_secs,
));
let pool = ThreadPool::new(
config.worker_threads,
Arc::clone(&session_manager),
Arc::clone(&running),
);
server::run(&config, session_manager, pool, running);All settings are defined in config.toml. Each field has a sensible default.
| Field | Type | Default | Description |
|---|---|---|---|
socket_path |
String |
/tmp/secure-session-daemon.sock |
Filesystem path for the Unix Domain Socket |
worker_threads |
usize |
4 |
Number of worker threads in the pool (minimum 1) |
max_sessions |
usize |
64 |
Maximum concurrent sessions across all workers (minimum 1) |
session_timeout_secs |
u64 |
300 |
Seconds of inactivity before a session is timed out and cleaned up (minimum 1) |
log_level |
String |
"info" |
Log level passed to EnvFilter (e.g. error, warn, info, debug, trace) |
SecureSessionDaemon/
├── Cargo.toml # Package metadata and dependencies
├── config.toml # Default runtime configuration
├── src/
│ ├── main.rs # Entry point: config load, thread spawn, signal handling
│ ├── lib.rs # Module declarations
│ ├── config.rs # Config struct, TOML deserialization, validation
│ ├── crypto.rs # X25519 KeyPair, HKDF SharedKey, AES-256-GCM encrypt/decrypt
│ ├── error.rs # Custom Error enum and Result type
│ ├── logging.rs # tracing-subscriber initialization
│ ├── protocol.rs # Binary frame encoding/decoding, MessageType enum
│ ├── server.rs # Accept loop on UnixListener, connection dispatch
│ ├── session.rs # Session struct, state machine, handshake helpers
│ ├── session_manager.rs # Thread-safe registry with capacity enforcement and timeout cleanup
│ └── thread_pool.rs # Work queue + worker threads, per-session handler
└── tests/
└── integration_test.rs # Integration tests: handshake, encrypted comm, limits, concurrency
Run all tests (unit + integration):
cargo testRun only unit tests:
cargo test --libRun only integration tests:
cargo test --test integration_testThe test suite covers:
crypto— keypair generation uniqueness, ECDH shared secret agreement, AES-256-GCM encrypt/decrypt round-trips, empty plaintext, tampered ciphertext rejection, wrong-key rejection, nonce uniqueness, short-data rejection, session ID uniqueness.protocol— frame encode/decode round-trips for all message types, invalid magic/version/type, truncated payloads, sequence number preservation, large payloads.session_manager— register/unregister, max-sessions enforcement,is_at_capacity, timed-out cleanup, no-op cleanup on active sessions, multiple session registration.- Integration — full handshake + encrypted communication + close, invalid handshake rejection, session limit enforcement, concurrent client handling.
This is a proof-of-concept and educational project. No formal benchmarks have been run. Performance characteristics (throughput, latency, connection churn) will depend heavily on the workload, platform, and configuration. The crypto operations (X25519, HKDF, AES-256-GCM) use constant-time implementations from dalek and aes-gcm crates, but no protocol-level optimizations (batching, zero-copy, io_uring, etc.) have been applied.
The following are intentionally not implemented but would be natural extensions:
- Mutual authentication — client certificates or pre-shared keys
- Perfect Forward Secrecy (PFS) support — re-keying on long-lived sessions
- Abstract socket namespace — Linux
AF_UNIXabstract sockets (trivial to add) - Systemd socket activation — pass already-bound socket via file descriptor
- Access control — UID/GID checks on the peer credentials (
SO_PEERCRED) - Asynchronous I/O —
tokio/asyncversion with non-blocking everything - Arbitrary message routing — beyond the current echo-server model
- Pluggable transports — TCP/TLS, VSOCK, or Windows named pipes
- Metrics / observability — Prometheus metrics endpoint, OpenTelemetry tracing
MIT