Files
barrypandClaude Fable 5.1 1467b71037 Make shutdown event-driven and address review findings
Replace the non-blocking accept + 50ms poll with a plain blocking
accept(). A signal-hook thread waits for SIGTERM/SIGINT, sets a flag,
and wakes accept() with a throwaway connection to the socket.

On shutdown the listener is closed and the socket file unlinked before
the grace period, so clients that connect late fail fast with ENOENT
instead of queueing and being reset. In-flight handlers are tracked by
a counter + Condvar rather than a Vec of JoinHandles.

Accepted streams now get a 5s read/write timeout so a silent client
cannot pin a handler thread or stretch every shutdown to the full
grace period. Transient accept() errors are logged and retried instead
of exiting the daemon. A second SIGTERM/SIGINT during the grace period
forces an immediate exit with status 1.

Tests: wrap the child in the kill-on-drop guard immediately after
spawn, drop the redundant wall-clock assertion, and add a case for a
client that connects but never sends.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kmrgkpp9YBJ6AMaWYPPZuD
2026-09-12 13:19:36 -07:00

78 lines
3.8 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A minimal Rust reimplementation of the Cyrus `saslauthd` daemon. It listens on a Unix
socket, speaks the saslauthd wire protocol, and checks credentials against per-user
password files in a directory. In production it runs as PID 1 in an Alpine container
(`pobox_saslauthd`), and Cyrus IMAP / Exim authenticate through its socket.
## Commands
```sh
cargo build --workspace
cargo test --workspace # unit + integration tests
cargo test -p server --test shutdown_test # one integration test file
cargo test -p server it_checks_argon2 # one test by name
cargo clippy --workspace --all-targets # plain `cargo clippy` skips test code
cargo run -p server -- -f /tmp/saslauthd.sock -p ./test_passwords
RUST_LOG=info cargo run -p server # env_logger; info shows auth results + shutdown
cargo run -p client -- -f /tmp/saslauthd.sock bp some-rad-password # manual round-trip
docker build -t saslauthd . # musl static build, targets x86_64 only
```
Tests spawn the real `saslauthd` binary on a socket under the OS temp dir and signal it
with `kill`, so they are Unix-only and rely on `test_passwords/bp` at the repo root.
## Architecture
Cargo workspace (edition 2024) with three crates:
- `common` — the wire protocol shared by server and client. `io.rs` reads/writes
length-prefixed strings (u16 big-endian length + bytes). `request.rs` is the four-field
request (userid, password, service, realm) in that order.
- `server` — the `saslauthd` binary. `Server` owns the `UnixListener`, spawns one thread
per connection running `Handler`, which parses a `Request`, asks `PasswordDirectory`
whether it is valid, and writes back `OK` or `NO` as a length-prefixed string.
- `client``saslauthd_test_client`, a tiny CLI for exercising a running server.
Top-level `tests/` holds the `common` crate's protocol tests; `server/tests/` holds the
process-level shutdown tests.
### Authentication model (`server/src/repository.rs`)
The password directory contains one file per userid. Blank lines and `#` comments are
ignored. Each remaining line is either:
- a plain-text password, matched exactly, or
- `[key]<argon2 PHC hash>`, matched only when the client sends the password as
`[key]<plaintext>` and the plaintext verifies against the hash for that key.
The client's password prefix decides which path is taken. Userids containing `/` are
rejected before touching the filesystem. `service` and `realm` are read but ignored.
### Server lifecycle (`server/src/server.rs`)
Startup removes any stale file at the socket path, binds, and chmods the socket 0777.
The accept loop is a plain blocking `accept()`. A `signal-hook` thread waits for
SIGTERM/SIGINT; on the first one it sets a shutdown flag and makes a throwaway
connection to the socket to wake the accept loop, which then closes the listener,
unlinks the socket file, and waits up to 3 s for in-flight handler threads before
exiting 0. A second signal exits immediately with status 1. Accepted streams get a 5 s
read/write timeout so a silent client cannot pin a thread. This matters because as
container PID 1 the process gets no default signal handling; without it `docker stop`
hangs for 10 s and host reboots stall.
CLI options live in `options.rs` as a `LazyLock<Opt>` (clap derive) and are read from
anywhere via `OPTIONS`.
## Constraints
- Do not change the wire protocol or the `--socket-name` / `--password-dir` flags;
Cyrus and Exim depend on both.
- The Dockerfile hardcodes `x86_64-unknown-linux-musl`. On an arm64 Mac the cross-build
segfaults under qemu; verify container behaviour by substituting
`aarch64-unknown-linux-musl` in a copy of the Dockerfile instead.