Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
1

Configure Feed

Select the types of activity you want to include in your feed.

README.md

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.

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 host and no ip → the module runs for every connection.
  • ip is checked before host. A !-prefixed mask that matches means "do not run for this client."
  • A module with a host mask 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, and dnsbl modules. They are recognized by the config validator (so existing configs still check, and port is 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, some rfc931 stat counters that the C bumps only on timeout/abort/skip paths (aborted, skipped) are not yet tracked, so those columns in the S 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.