iauth-rs#
iauth-rs is an async Rust port of the IRCnet ircd iauth daemon — the
authentication slave that ircd spawns to vet incoming client connections
(identd lookups, proxy scans, blocklists, and custom checks) before they are
allowed onto the network.
It is a standalone binary that speaks the identical line protocol the C
iauth uses, so it is a drop-in replacement from ircd's point of view. It is
the first component of the larger C→Rust migration (see the repo PLAN.md,
decision #3) and is built greenfield: idiomatic async Rust rather than a
mechanical transliteration, because it is a separate process behind a narrow
text ABI.
- Design spec:
../superpowers/specs/2026-06-03-iauth-rs-port-design.md - Implementation plan:
../superpowers/plans/2026-06-03-iauth-rs-port.md - Protocol reference:
../../doc/iauth-internals.txt - Sample config:
iauth.example.kdl - Source:
iauth-rs/src/
How it fits with ircd#
ircd creates an AF_UNIX socketpair, forks, and dup2s the iauth end onto the
child's file descriptor 0. That fd is bidirectional: ircd writes
<id> <category> <info> request lines to iauth, and iauth writes
<category> <info> reply lines back, both \n-terminated. The <id> is the
ircd file descriptor of the client being authenticated.
ircd ──spawn──> iauth-rs
│ socketpair (fd 0, bidirectional)
└──────────────┘
"2 C 1.2.3.4 54321 10.0.0.1 6667\n" (new connection on fd 2)
─────────────────────►
◄─────────────────────
"U 2 1.2.3.4 54321 alice\n" (identd said "alice")
"D 2 1.2.3.4 54321 \n" (done with fd 2)
Architecture#
A single engine task owns all per-client state. The ircd link is read and written by the engine; each module invocation runs as a spawned per-client async future that reports a structured outcome back over an mpsc channel. This preserves the C invariant that exactly one owner mutates client state — keeping the port deterministic — without locks.
┌──────────── engine task (owns the client slab) ────────────┐
ircd ⇄ fd0 ───► │ loop select! { │
(UnixStream) │ line = ircd reader → handle ircd message (C/O/D/R/N/…) │ ──► ircd writer
│ out = module chan → apply outcome, advance next_io() │
│ tick = stats timer → emit `s` + per-module `S` lines │
│ } │
└────────────┬──────────────────────────────────────────────-┘
│ spawn(snapshot) [at most one in flight per client]
▼
async module.run(ctx) -> outcome
rfc931 → TcpStream:113, "port , port\r\n", parse USERID
pipe → run an external program, read its Y/N verdict
The C module vtable (init / start / work / timeout / clean, an fd state
machine) collapses into a single async fn run per module, with
tokio::time::timeout replacing the manual timeout fd-watch.
Source layout (iauth-rs/src/)#
| File | Responsibility |
|---|---|
main.rs |
tokio bootstrap; dispatch CLI → check-mode vs daemon |
cli.rs |
clap argument parsing + run_check (config validation report) |
protocol.rs |
parse ircd→iauth messages, encode iauth→ircd messages |
config.rs |
KDL → typed Config; policy string, defaults, validation |
client.rs |
per-client AuthData, state flags, conf_match, CIDR/glob |
module.rs |
the async Module trait, run context, outcome, registry |
modules/rfc931.rs |
identd (RFC 1413) lookup module |
modules/pipe.rs |
external-program module |
ircd_link.rs |
line-oriented transport over fd 0 (+ in-memory test double) |
engine.rs |
the actor: orchestration (next_io), startup handshake, stats |
log.rs |
best-effort auth-log file (reopened on SIGUSR2) |
Building and running#
# build
cargo build -p iauth-rs # debug binary at target/debug/iauth
# validate a config (no tty required)
iauth --check --config docs/iauth/iauth.example.kdl
# liveness probe (ircd uses this) — exits 0 immediately
iauth -X
# as a daemon: ircd spawns it with fd 0 wired to the socketpair and no tty.
# Point it at a config file with --config (defaults to ./iauth.kdl).
iauth --config /etc/ircd/iauth.kdl
Command line#
| Invocation | Behavior |
|---|---|
iauth [--config <FILE>] (no tty) |
daemon mode: read <FILE> (default ./iauth.kdl), run the engine on fd 0 |
iauth --check [--config <FILE>] |
check mode: validate <FILE>, run each module's init, print a report, exit |
iauth (tty, no --check) |
print the version banner and feature flags, exit |
iauth -X |
exit 0 immediately (ircd's liveness probe) |
--config may be abbreviated -c. --config defaults to iauth.kdl in the
working directory.
SIGUSR2 causes the auth log file to be reopened (for log rotation). The
SIGHUP/SIGINT/SIGTERM rehash/restart/die handlers are intentionally absent
(they are commented out in the C as well).
Configuration#
Configuration is KDL (kdl.dev). See the fully commented
iauth.example.kdl. Summary:
Global options (top-level nodes)#
| Node | Effect |
|---|---|
required |
ircd must let iauth authenticate clients (policy R) |
notimeout |
reject a client if iauth times out (policy T) |
extinfo |
ask ircd to forward client username/password (policy A) |
timeout <n> |
default per-module timeout in seconds (default 30) |
debuglvl <hex> |
debug mask (best-effort logging) |
A W is added to the policy automatically if any module needs hostname info.
Bare toggles (required, etc.) are on by their presence.
Module blocks#
module "<name>" {
// properties (all optional unless noted)
host "<mask>" "!<mask>" ... // glob host masks; "!" = negative; defers to DNS
ip "<mask>" "!<mask>" ... // CIDR or glob; "!" = negative; matched before host
timeout <seconds> // per-instance override
reason "<text>" // shown to rejected clients
delayed // delayed-execution mode
// module-specific properties below
}
| Module | Status | Module-specific properties |
|---|---|---|
rfc931 |
✅ implemented | lazy, protocol (toggles). At most one; auto-added if omitted. |
pipe |
✅ implemented | prog "<path>" (required) — the program to run |
socks |
⏳ not yet | port <n> (required) |
webproxy |
⏳ not yet | port <n> (required) |
dnsbl |
⏳ not yet | — |
Matching rules (faithful to the C conf_match):
- No
hostand noip→ the module runs for every connection. ipis checked beforehost. A!-prefixed mask that matches means "do not run for this client."- A module with a
hostmask is deferred to a later pass, once DNS resolves. host "*"is special: it forces the module to wait for DNS and run once hostname info (or a definitive "no hostname") is available.
Wire protocol (summary)#
Full details are in doc/iauth-internals.txt.
ircd → iauth (<id> <category> <info>): C new connection, O update,
D disconnect, R fd remap, N hostname, d/n no-hostname, A host alias
(ignored), U username, P password, T registering, E error, M ircd
name.
iauth → ircd (<category> <info>): V version, O policy, G debug
toggle, a rehash-begin, A config line, s stats-reset, S stats,
U/u username usable/unusable, K/k kill (loud/quiet), D done,
> notice to &AUTH.
On startup iauth-rs sends, in order: V <version>, O <policy>, then a plus
one A <hostmask> <module> <opts> per configured matcher, then G 0.
Testing#
cargo test -p iauth-rs # 29 unit + integration tests
cargo clippy -p iauth-rs # zero warnings
Coverage in this pass: protocol parse/encode round-trips, KDL config parsing and
validation, conf_match/CIDR/glob matching, the rfc931 module against a fake
identd (TcpListener), the pipe module against a stub program, the line
transport, and an end-to-end engine integration test driving a scripted
connection through an in-memory link.
The full C-differential golden harness (running the C iauth and iauth-rs
on identical scripted input and diffing byte-for-byte) is the eventual gate from
PLAN.md and is a deferred follow-up.
Scope and known limitations (this pass)#
Implemented: the engine + orchestration, KDL config, the wire protocol in both
directions, and the rfc931 and pipe modules.
Deferred to later passes:
- The
socks,webproxy, anddnsblmodules. They are recognized by the config validator (so existing configs still check, andportis enforced for socks/webproxy), but an instance of them does no work in this build. - The C-differential golden harness and end-to-end ircd + iauth-rs run.
- Full syslog parity. Logging is best-effort: the auth-log file is written
and reopened on
SIGUSR2, but not every C log site is reproduced. In particular, somerfc931stat counters that the C bumps only on timeout/abort/skip paths (aborted,skipped) are not yet tracked, so those columns in theS rfc931 …line report 0. - Stats cadence. The first periodic stats burst fires at ~60s (then every 60s); the C waits 90s for the first. A minor, documented timing difference.
- fd remap (
R). Remaps are handled, but because module work runs as an async task bound to the original id rather than a live fd that follows the remap, a remap that lands while a module is mid-flight restarts that client's pass rather than continuing the in-flight check. Remaps are rare in practice.