Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kmrgkpp9YBJ6AMaWYPPZuD
3.5 KiB
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
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.rsreads/writes length-prefixed strings (u16 big-endian length + bytes).request.rsis the four-field request (userid, password, service, realm) in that order.server— thesaslauthdbinary.Serverowns theUnixListener, spawns one thread per connection runningHandler, which parses aRequest, asksPasswordDirectorywhether it is valid, and writes backOKorNOas 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 non-blocking and polls a flag set by signal-hook for SIGTERM/SIGINT.
On a signal it stops accepting, waits up to 3 s for in-flight handler threads, then
Drop removes the socket file. Exit status is 0. 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-dirflags; 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 substitutingaarch64-unknown-linux-muslin a copy of the Dockerfile instead.