Skip to content

Repository files navigation

SecureSessionDaemon

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.

Motivation

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 with serde/toml, comprehensive error handling with a custom Error type, and both unit and integration tests.

Features

  • 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)

Architecture

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&lt;ID, Session&gt;)"]
        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
Loading

Crypto Flow

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)
Loading

Thread Model

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
Loading

Session Lifecycle

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.

Build Instructions

cargo build --release

The compiled binary will be at target/release/secure-session-daemon.

Run Instructions

Start the daemon with default configuration:

mkdir -p /tmp
cargo run

Or run the built binary directly:

./target/release/secure-session-daemon

The 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.

Example Client

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" }

Example Server

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);

Configuration

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)

Project Layout

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

Testing

Run all tests (unit + integration):

cargo test

Run only unit tests:

cargo test --lib

Run only integration tests:

cargo test --test integration_test

The 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.

Benchmark Notes

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.

Future Scope

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_UNIX abstract 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/Otokio/async version 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

License

MIT

About

Lightweight secure IPC daemon: ECDH key exchange + AES-256-GCM over Unix Domain Sockets. Pure Rust, no async runtime.

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages