Files
saslauthd_yaml/CLAUDE.md
T

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.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.
  • clientsaslauthd_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-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.