Mechanically rewrite IRCnet ircd 2.11 from C to Rust (strangler fig)#
Context#
ircd.rs is the IRCnet ircd 2.11 IRC daemon: ~51k LOC of C across common/, ircd/, and iauth/,
producing 5 binaries (ircd, iauth, chkconf, ircdwatch, mkpasswd). It is a single‑threaded
select()/poll() server with a table‑driven command dispatcher, a bundled BIND DNS resolver, pervasive
fixed‑size buffers, and ~230 strcpy/sprintf‑family calls — i.e. exactly the memory‑safety profile a
Rust rewrite is meant to eliminate. There are no existing tests (only a clang‑format CI check).
Goal: convert the codebase to Rust module‑by‑module, keeping the program building and running at every step, with every important part fully tested against the original C as a reference oracle.
This plan reflects four locked decisions from the user:
- Faithful mechanical port now, then dedicated idiomatic‑Rust passes at the very end (after C is gone). ⚠️ Superseded (2026‑06‑08): decision 1 originally meant an in‑place idiomatic transformation of the mechanical port (the old P9–P12). The project instead forked a parallel greenfield idiomatic daemon,
leveva, tested differentially against the now‑complete mechanical port. So the idiomatic end‑state is reached by growinglevevato parity and deleting the mechanical port, not by polishing it in place. See the reframed P9–P12 below. - Rust‑driven build from day one:
fn main()is Rust; remaining C compiles as a*-syscrate; we strangle inward. - iauth rewritten first, standalone greenfield Rust binary speaking the identical pipe protocol.
- Full 4‑layer testing (differential unit, golden, fuzz, conformance) + a permanent C reference build in CI.
The leverage point: every foo.c already ships foo_ext.h (its public extern API) — 41 headers / ~417 symbols that form a ready‑made per‑module C ABI contract. Dispatch is a function‑pointer table (msgtab[] in common/parse.c). These two facts make the strangler seam tractable.
The migration model (the seam)#
Workspace. A cargo workspace at repo root, coexisting with the untouched autoconf tree:
ircd-sys/—build.rsrunssupport/configureto generatesetup.h/config.h, thencc::Buildcompiles the C objects (mirroringIRCD_COMMON_OBJS+IRCD_OBJSfromsupport/Makefile.in:144‑149) minus whatever has been ported, plusbindgenover awrapper.hthat#includescommon/struct_def.h+ all*_ext.h/*_def.h.ircd-rs/— the binary crate;src/main.rsowns the realfn main(). Ported modules live here / in leaf sub‑crates.iauth-rs/— standalone greenfieldiauthbinary (noircd-sysdep).ircd-testkit/— the L1–L4 harness.ircd-fuzz/—cargo-fuzztargets.
Runnable baseline (day one). Compile the C main (ircd/ircd.c:766) as c_ircd_main via cc::Build::define("main", Some("c_ircd_main")). Rust main() marshals argc/argv and calls it. The product is byte‑for‑byte the C ircd, just entered from Rust. (Also generate version.c from ircd/version.c.SH.in in build.rs and compile it — the C link needs version.o.)
Strangling inward (per module). To port foo.c: read foo_ext.h for the exact ABI; write Rust #[no_mangle] pub extern "C" fn … (and #[no_mangle] pub static … for exported data) matching the exact signatures (note: C uses mutable char*, not const); remove foo.c from build.rs's source list. Because the .c is dropped (not overridden), exactly one definition of each symbol exists at all times → no duplicate‑symbol/LTO games, program always links. Cross‑language LTO stays off during migration. CI builds + boots the server after every move.
Single source of truth for compile defines. build.rs must mirror Makefile.in per‑object defines (e.g. -DFNAME_USERLOG=…, -DIRCDCONF_PATH=…, CLIENT_COMPILE), and feed the identical -include config.h/setup.h + -D set to cc::Build, to bindgen, and to the reference‑archive build. A divergence here silently corrupts aClient layout. Default config to lock: USE_IAUTH on, XLINE off, ZIP_LINKS off, AFINET=AF_INET6 (so IN_ADDR = libc in6_addr).
Non‑ircd binaries are out of strangler scope. ircdwatch, chkconf, mkpasswd and the cl*.o duplicate‑compiled common objects (-DCLIENT_COMPILE) stay in the permanent C reference tree only; they are never routed through ircd-sys. chkconf gets a Rust port opportunistically in P6.
Keystone hazards & how we neutralize them (all verified in‑tree)#
| Hazard | Reality (verified) | Neutralization |
|---|---|---|
| C‑variadic exports | ~14 in send_ext.h (sendto_one, sendto_flag, …) + irc_sprintf; stable Rust cannot define an extern "C" variadic fn. Each already has a va_list sibling (vsendto_one, irc_vsprintf). |
Keep a thin C trampoline per variadic symbol (cc‑compiled): va_start → format via irc_vsprintf into a BUFSIZE buffer → call a non‑variadic Rust core rs_sendto_one_str(to, msg). Rust never sees va_list. These ~25 trampolines persist through the faithful port and are deleted in P8 (call sites switch to Rust format!). ⇒ "100% Rust" is a P8 goal, not P7. |
aClient interior self‑pointers |
make_client sets cptr->name = cptr->namebuf (list.c:121,226); me is a by‑value global with client = &me. |
Invariant: never bind/move/copy an aClient by value in Rust. All access through stable raw pointers; name set via addr_of_mut!((*c).namebuf). |
CLIENT_REMOTE_SIZE trick |
Remote clients allocated offsetof(aClient,count) bytes; local‑only tail must never be touched when from != self. |
Port make_client to branch on offset_of!(aClient,count) vs size_of::<aClient>(); build‑test asserts both equal the C offsetof/sizeof, plus offset_of! of every local‑tail field (count,buffer,sendQ,recvQ,ip,hostp,…). |
repr(C) layout drift |
Layout gated by USE_IAUTH/XLINE/ZIP_LINKS/INET6/TKLINE/…. |
bindgen + cc share one define set (above); compile‑time static_asserts on size/offset; incomplete‑array externs (local[], tolowertab[]) get element counts from bindgen constants (MAXCONNECTIONS) with a C static_assert that sizeof(arr)/sizeof(arr[0]) matches the Rust length. |
| Mutable dispatch table | msgtab[] entries are struct Cmd{handler;count;rcount;bytes;rbytes} mutated every dispatch, read by STATS. |
Rust static mut msgtab: [Message; …] of the bindgen type, exact field order; handlers point at still‑C m_* until ported; L1 test asserts identical count/bytes after N dispatches. |
| Function‑like macros | bindgen drops IsServer, MyConnect, MyFree, MyMalloc, DBufLength, SetXxx, … |
Hand‑audited macros.rs of inline equivalents, each cross‑checked by an L1 test (C macro via tiny accessor vs Rust inline). |
| Intrusive lists + manual refcounts | next/prev/hnext, Link union, anUser.refcnt/aServer.refcnt. |
Invariant during faithful port: lists stay raw‑pointer intrusive lists — no Box/Drop/Rust ownership; refcnt inc/dec byte‑for‑byte as C. Enum‑Link and real ownership are P8 only. |
find_* ownership |
find_client/uid/server/name/person live in parse.c:144‑417 (not hash.c), wrapping hash_find_*. |
Port these with parse (P4); hash_find_* with hash (P2). |
The 4‑layer test harness (built in P0, used every phase)#
- Permanent C reference build. The pristine C tree stays in‑repo and buildable for the project's life; CI builds it every run as the oracle.
- L1 — differential unit. Build the reference
.cinto a separate static archive whose symbols are bulk‑renamed (objcopy --redefine-syms/--prefix-symbols=cref_, applied over the full transitive symbol set so internal callers follow the rename), exportingcref_match,cref_MyMalloc, … Link that archive beside the Rust crate in the test binary and assert Rust‑vs‑cref_identical outputs (and, for data structures, identical bytes/pointer‑walk order/counters). This is the workhorse for leaf/data/format tiers. - L2 — golden / characterization. Run reference‑C ircd and the in‑progress Rust ircd side‑by‑side, diff wire output. Determinism is load‑bearing:
- Clock pinning:
LD_PRELOADshim overridingtime()/gettimeofday()to a fixed value in both processes (the loop readstime(NULL)directly every iteration — a tune/faketime file is insufficient). - Output canonicalizer: mask known‑volatile token positions (WHOISIDLE idle/signon, STATSUPTIME, RPL_TIME/RPL_CREATED, fd‑derived ids). Gate = byte‑identical after clock‑pin + canonicalization.
- Strict serialization: drive scripted clients one action at a time with barriers — hash buckets prepend at head, so WHO/NAMES/LINKS/LIST order follows arrival order. No concurrent clients.
- S2S: server‑burst diffing uses a TS‑aware linking peer (or link‑against‑reference‑C‑and‑observe), not canned replay.
- Clock pinning:
- L3 — differential fuzzing. Feed identical bytes through the real reassembly path (
dbuf_getmsginpacket.c, to hit 512‑byte truncation/framing) to Cparse()vs Rust; diff the full post‑state — mutatedaClientbytes,ircstpcounters, and selected handler identity — not justparv[]. - L4 — conformance. Rescoped: (1) extract the numeric→format table from
s_err.creplies[]and assert the Rust port reproduces it (an L1 test); (2) curated scripted‑client assertions on numeric code + arg arity/semantics fromdoc/whox.md,doc/whoistls.txt,rfc2812.txt. (There is no machine‑readable RFC oracle;replies[]is the truth.)
Phases#
Through P7, each phase keeps the program building + L2/L4 green ("Drop" = remove the
.cfrombuild.rs). P8 retires the oracle harness after a final green run.⚠️ Roadmap pivot (2026‑06‑08). P9–P12 below are reframed. The original plan transformed the mechanical port (
ircd-common) into idiomatic Rust in place. Instead the project forked a greenfield idiomatic daemon,leveva(its own crate: async tokio,rustlsTLS, KDL config, typed identifier strings — already ~6.3k LOC of foundations). The strangler pattern now repeats one level up:ircd-common(the verified‑faithful mechanical port) is the behavioral oracle forleveva, exactly as the C tree was the oracle forircd-common.leveva-integrationholds the cross‑crate differential tests. The idiomatic end‑state is reached by growinglevevato feature parity, differentially pinned againstircd-common, then deleting the mechanical port (ircd-common/ircd-rs+ the C‑eraiauth-rsscaffolding). An oracle only needs to keep building + passing its tests — it does not need to be idiomatic, so the in‑place cleanup (old P9) is retired as throwaway work.
| Phase | Modules | Approach & gotchas | Test gate (exit) |
|---|---|---|---|
| P0 — Scaffold + baseline + harness 🔶 PARTIAL | ircd-sys (cc+bindgen, main→c_ircd_main, version.c.SH), ircd-rs/main.rs, ircd-testkit, ircd-fuzz, frozen C reference build |
Establish the Rust‑driven build; lock the configure‑define single source of truth; emit layout static_asserts; stand up all 4 layers. Done as P0a (baseline), P0b (bindgen + layout drift-net), P0c (L1 differential harness), P0‑L2 (golden harness ircd-golden: reference‑C binary + scripted‑client diff; registration scenario byte‑identical). Not yet built: L3 fuzz (ircd-fuzz), L4 conformance, iauth-rs. |
cargo build boots an ircd that links iauth and diffs byte‑identical (post‑canonicalization) vs reference‑C across the full scripted suite; layout asserts pass. (L1 + layout MET; L2/L4 gate pending those layers.) |
| P‑IAUTH — greenfield iauth (parallel, starts at P0) 🔶 ALL 5 MODULES DONE | iauth/* → iauth-rs |
Idiomatic Rust allowed (separate process, narrow text ABI). Reimplement event loop (mio), cldata fd‑slab, the <id> <category> <info> parser, the 8‑fn aModule vtable as a trait with 5 impls (dnsbl/socks/webproxy/rfc931/pipe); replace dlopen DSM with a static registry from iauth.conf. |
Drive C‑iauth vs iauth‑rs with an identical scripted <id> sequence (ids are fixed in input, since <id>=ircd fd); byte‑identical replies per category + per‑module fixtures; end‑to‑end ircd+iauth‑rs golden identical. |
| P1 — Leaf / pure utils ✅ DONE | common/match.c (+tolowertab/touppertab/char_atribs), dbuf.c, pure subset of support.c (mystrdup/strtoken/myctime/mybasename/inetntop/inetpton/irc_memcmp). irc_sprintf.cva_arg engine; stable Rust can't consume va_list, and it has no post-format core to hand a string to Rust → stays C, no trampoline). snprintf_append, dgets, make_isupport, ipv6string also stay C for now. |
Faithful; exact char* signatures; tables #[no_mangle] static [u8;256] (byte-equality asserted); dbuf owns poolsize/freelist. support.c partial port via inert #ifndef PORT_SUPPORT_P1 guards + a support_link.o second-compile substituted into the link set; MyMalloc/MyRealloc/MyFree + outofmemory() defer to P2. |
MET: L1 zero-diff over all corpora (match incl. tables + MAX_ITERATIONS cap; dbuf incl. getmsg framing + over-poolsize rollback; support incl. 8-case inet round-trip). Rust-driven ircd binary links; layout net green; 0 warnings. (L3 fuzz / L2/L4 not built yet — deferred from P0.) |
| P2 — Data & lookup ✅ DONE | patricia.c, class.c, whowas.c (ring; m_whowas stays C), hash.c (6 tables; m_hash stays C), list.c (allocators, make_client, svrtop/numclients) + MyMalloc/MyRealloc + outofmemory |
Faithful; preserved intrusive pointers + refcounts (raw‑pointer invariant); replicated CLIENT_REMOTE_SIZE branch + hash iteration order exactly. Verified findings: ENABLE_CIDR_LIMITS ON (patricia live, add_class carries cidrlen_s); USE_HOSTHASH+USE_IPHASH ON (6 tables); MyFree is a macro (trio = MyMalloc+MyRealloc+outofmemory). m_whowas/m_hash kept in C via whowas_link.o/hash_link.o partial‑ports (-DPORT_*_P2). |
MET: L1 zero‑diff over all corpora (patricia tree walk; class list + CIDR; whowas ring/index + uwas; hash client/channel/uid bucket chains + counters + stored hashv; make_client local/remote self‑pointers). Rust‑driven ircd binary links with list.o dropped; layout net green; 0 warnings. whowas/class notification (m_whowas/report_classes) sendto_* path deferred to post‑P3 (L2). |
| P3 — Transport & format ✅ DONE | s_err.c (replies[]), s_id.c (UID/SID/CID), s_numeric.c (do_numeric), send.c delivery core (send_message/send_queued/flush_*). Finding: the whole sendto_*/esendto_* family is irreducibly C trampolines (variadic; vsendpreprep consumes va_arg per‑recipient) → no non‑variadic cores extractable; senders + sendto_flog/logfiles/setup_svchans stay C (move to P7/P8). |
Faithful; replies[] as Rust static (dumped from the oracle so config‑gated #if entries resolve); exact UID/SID base‑N; send.c partial‑ported via send_link.o (-DPORT_SEND_P3, dead_link exposed). ZIP/USE_SERVICES/LOG_SERVER_CHANNELS/etc. all OFF. |
MET (L1): zero‑diff vs cref_ on replies[] (all 1000), ltoid/idtol/sid_valid/cid_ok/check_uid, do_numeric guards, send_message sendQ buffering + send_queued early returns. L2 (post‑P3): do_numeric/sender wire broadcasts + deliver_it write path. |
| P4 — Parser & dispatch ✅ DONE | common/parse.c (tokenizer, msgtab[], all 11 find_* wrappers, getfield, m_nop/m_nopriv/m_unreg/m_reg, find_sender/cancel_clients/remove_unknown) |
Faithful; msgtab as #[no_mangle] static mut [Message; 66] (config‑resolved: TKLINE on, SERVSET/KLINE/SIDTRACE off, MOTD→m_unreg; mutable counters); the ~60 real m_* handlers stay C and are referenced as fn pointers; parse.o dropped outright (every symbol ported — no parse_link.o). Rust calls the C variadic senders (sendto_one/sendto_flag/sendto_serv_butone) via bindgen decls (the s_numeric.rs pattern). Exact tokenize semantics (512 trunc, leading/trailing colon, empty trailing param, MPAR cap). Deliverable: per‑handler‑file static‑symbol cluster map (specs/2026-06-02-p5-handler-cluster-map.md). |
MET (L1): zero‑diff vs cref_ on the msgtab[] structure (cmd/min/max per row + row count), getfield (delimiter/escape/trailing/empty), find_mask/find_name fallbacks, and parse()'s no‑server paths (IsDead, empty, unknown‑cmd, numeric, ENCAP→m_nop dispatch — return code + cptr.flags + ircstp deltas). Full parse() post‑state + hash‑populated find_* = L3/L2 (need the server fixture). |
P5 — Command handlers (bulk) ✅ DONE (all m_* handlers; s_auth.c socket/ident/iauth‑pipe I/O → P7) |
channel.c, s_user.c+s_auth.c, s_serv.c+s_service.c+s_zip.c (zlib→flate2, bit‑exact), s_misc.c (exit_client), s_debug.c. Per‑cluster sub‑phase progress log (P5a–P5whowas, 56 clusters): see PLAN-P5-progress.md. channel.c (P5hh), s_serv.c (P5bbb) and s_user.c (P5ccc) are now FULLY ported → all three .os dropped. The P2‑deferred m_whowas handler is also ported (P5whowas) → whowas.o dropped, whowas.c fully Rust; the P2‑deferred m_hash (HAZH) handler is ported (P5hash) → hash.o dropped, the hash_link.o partial‑compile gone. Of the P5 TUs, only s_auth.c remains C (no m_* handlers; pure socket/ident/iauth‑pipe I/O → deferred to P7/P‑IAUTH). |
Faithful, but cluster‑based, not per‑function: channel.c has 37 file‑statics (set_mode, can_join, get_channel, …); migrate each connected component over shared statics as a unit via the msgtab fn‑pointer seam. s_zip.o is empty (ZIP_LINKS off) → drop for free, no port. |
Per‑cluster L2 golden + L4 conformance byte‑identical incl. WHO/WHOIS/WHOX, full server‑burst/netsplit; zlib bytes match. |
| P6 — Config & DNS ✅ DONE | s_conf.c+config_read.c, chkconf.c, res.c+res_comp.c+res_init.c+res_mkquery.c. Per‑cluster sub‑phase progress log (P6a–P6hh): see PLAN-P6-progress.md (one‑line summaries) and docs/progress-log/p6.md (full entries). Outcome: s_conf.c (config grammar + rehash + the conf/kconf/networkname globals) and the whole resolver (res.c + the res_comp.c/res_init.c/res_mkquery.c leaves) are now FULLY Rust → all .os dropped; every msgtab handler is Rust; chkconf.c is a standalone chkconf-rs binary. → P6 COMPLETE. |
Faithful config grammar + rehash. Resolver: faithful port or hickory‑dns behind res_ext.h, but golden runs use a fixture nameserver for determinism. |
Config‑parse + chkconf L1/L3 zero‑diff on fixtures; resolver encode/decode identical; L2/L4 identical with fixture DNS. |
| P7 — I/O core + event loop + glue (last) ✅ DONE (all C logic ported) | s_bsd.c (select/poll loop, local[], highest_fd, timeofday, FdAry), packet.c, bsd.c, ircd.c (main, io_loop, signals, rehash/restart, tune file), s_auth.c (socket/ident/iauth‑pipe I/O, deferred from P5). Per‑cluster sub‑phase progress log (P7a–P7oo): see PLAN-P7-progress.md (one‑line summaries) and docs/progress-log/p7.md (full entries). Ported bottom‑up (leaf‑first), as in P6. Done: bsd.c (deliver_it) + packet.c (dopacket) dropped outright; s_bsd.c fully drained of logic (P7d–P7ff: socket/file leaves → fd/connection teardown → listeners → add_connection/connect_server → start_iauth/daemonize → the read_message select/poll event loop) — only its data globals remain; s_auth.c ident/iauth‑pipe I/O ported (P7u–P7y) — only the variadic sendto_iauth/vsendto_iauth remain; ircd.c fully drained of logic (P7c/P7t/P7gg–P7oo: ircd_writetune/ircd_readtune/calculate_preference/try_connections/check_pings/delayed_kills/setup_me + the signal‑handler/restart/server_reboot cluster + setup_signals + io_loop + the bad_command/open_debugfile CLI helpers + c_ircd_main the boot spine (P7oo) via ircd_link.o) — only its data globals remain. ALL C logic is now Rust → ircd_link.o/s_bsd_link.o/s_auth_link.o/send_link.o/support_link.o hold only data globals + the ~25 variadic sender trampolines. |
Faithful: keep single‑threaded select/poll via mio (not tokio); preserve per‑client state machine; globals become Rust‑owned statics; signals via signal‑hook/nix + atomics. Removes c_ircd_main. |
All C logic gone (only variadic trampolines remain); full L2+L3+L4 + soak byte‑identical under connect storms, partial reads, sendQ backpressure, netsplit, rehash, restart. |
P8 — Delete the C + retire the oracle harness ✅ DONE (per-sub-phase: PLAN-P8-progress.md + docs/progress-log/p8.md) |
remaining C (the ~25 variadic sender trampolines + residual data globals) + ircd-sys C build + ircd-golden (L2) + ircd-testkit/cref_* archives (L1) + the frozen reference‑C tree |
With all C logic already Rust (P7 exit), delete the last C: replace the variadic sender trampolines with a non‑variadic Rust sender API at every call site (faithful wire!/reply_one!/format! first; leveva typed messages adopted alongside the IRC senders), drop the residual data globals via the data‑symbol seam (bindgen extern decls resolve to #[no_mangle] Rust defs at link), then drop all C compilation from ircd-sys (no more cc::Build), retire the differential harness (cref_*/L1 ircd-testkit/ircd-golden L2 can no longer be built once the oracle is gone), and migrate the load‑bearing tests into ircd-common as self‑contained Rust tests (own fixtures, no cref_ oracle). Progress (P8a–P8r — full entries in the progress logs): every variadic sender trampoline is now Rust (P8a–P8m, incl. the keystones sendto_one/sendto_flag/esendto_*), and the five data‑global .os are dropped outright (s_auth.o iauth globals P8n, send.o P8o, s_bsd.o local[]/highest_fd/… P8p, ircd.o me/client/timers/… P8q). P8r dropped the last C‑logic TU support.o — dgets/make_isupport ported to Rust + the ipv6string/minus_one data globals defined as #[no_mangle] statics (snprintf_append is dead in Rust — WHOX is field‑by‑field — so it died with the .o; irc_sprintf is deleted not ported, in P11). Every ircd C TU is now Rust: the link set holds only the generated version.o + the L1 harness's ctruth.o. P8s migrated all 115 L1 cref_ differentials into self‑contained ircd-common insta snapshot tests (drive only the Rust port via link_anchor(), no oracle; soundness via the capture chain) — the suite is green + 0 warnings. P8t ported the last generated C TU version.c to Rust (ircd-common/src/version.rs): the data globals generation/creation/pass_version/infotext/isupport are now #[no_mangle] statics via the data‑symbol seam — generation/creation sourced from crate metadata at build time (CARGO_PKG_VERSION + an ircd-common/build.rs UTC build stamp), pass_version/infotext copied verbatim. version.c.SH generation + the version.o compile/archive are gone from ircd-sys/build.rs; libircd_c.a now holds only the L1 harness ctruth.o. (creation is now genuinely build‑time volatile, so the golden canonicalizer masks RPL_CREATED 003, matching the existing 371 Birth‑Date rule.) P8u (FINAL) retired the oracle. Ran the differential suite a last time green — L1 ircd-testkit all pass; L2 ircd-golden 86 pass + only the documented reference‑C‑garbage s_serv_stats flake (where Rust is the correct side) — then dropped all C compilation from ircd-sys/build.rs: the make object build, the cref_*/ctruth.o/res.o recompiles, the libircd_c.a archive, and the whole‑archive link + -lz/-lm/-lcrypt (none needed by Rust — pow resolves from glibc 2.29+). build.rs now only runs configure + bindgen over the C headers (the struct view ircd-common still uses until P9–P12) and expands the install‑path Makefile vars. ircd-testkit (L1) + ircd-golden (L2) are excluded from the workspace (mothballed, git‑recoverable); the ctruth.c/layout.rs drift‑net is deleted. The workspace is now 100% Rust — zero C TUs compiled. P8v (literal end of C) deleted the C source tree itself + retired ircd-sys: froze the generated bindings.rs into a committed ircd-common/src/bindings.rs (a self‑alias extern crate self as ircd_sys; keeps every ircd_sys::bindings::* path resolving, zero src churn) + the install‑path consts as literals, then deleted ircd-sys, the mothballed ircd-testkit/ircd-golden oracle crates, the iauth-rs C‑iauth differential, and all C source (common/ ircd/ iauth/ support/ contrib/ cbuild/ configure .clang-format* clangformat.yml). ircd-rs calls ircd_common::ircd::c_ircd_main directly. Not one line of C remains in the repo. |
MET. Final differential run green; oracle mothballed; cargo build --workspace 0 warnings links pure Rust; cargo test --workspace 695 pass / 0 fail (P8v; was 698 before deleting the C‑oracle tests); the ircd-common snapshot tests carry the reference‑C correctness forward. C source tree deleted — repo is 100% Rust at the file level. |
| P9 — std‑library cleanup ❌ RETIRED (2026‑06‑08) | — (was: whole Rust tree) | Skipped by decision — superseded by the leveva greenfield. P9 assumed an in‑place idiomatic transformation of the mechanical port (MyMalloc/MyFree→Box/Vec, dbuf freelist→owned, raw libc→std/nix, [c_char;N]→[u8;N]). Because the idiomatic end‑state is now a separate greenfield crate (leveva), ircd-common is throwaway code (the oracle, deleted at parity), so polishing it earns nothing. Nothing was implemented; the row is kept for provenance. |
n/a — retired. |
P10 — leveva greenfield foundations 🔶 IN PROGRESS (docs/progress-log/p10.md) |
new leveva crate (lib) + leveva-integration differential tests |
The idiomatic‑Rust product, built from scratch (not transformed from the port). Done so far: RFC 1459 case‑folding (casemap); typed identifier strings (IrcStr/IrcString + Nick/ChanName/Uid/Sid/Cid/ServerName/UserName/HostName validating newtypes via ident_newtype!); Message/MessageBuilder; the full Numeric reply/error enum (184 codes, discriminant = wire code); a generic safe patricia trie; glob matching; mode; KDL config (model/parse/password/privilege); rustls/tokio tls endpoint with hot reload; ident. The async boot path (main.rs) binds listeners + watches certs but currently drops accepted connections — no protocol layer yet. Idiomatic Rust allowed everywhere; pulls real crates (tokio/rustls/kdl/nom/clap). |
cargo test -p leveva green + clippy clean per module; leveva-integration pins each greenfield reimplementation against ircd-common (the oracle) within their shared domain (documented divergences asserted explicitly). |
P11 — leveva protocol layer → feature parity 🔶 IN PROGRESS (docs/progress-log/p11.md) |
leveva (client/server state machine + handlers) |
The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and leveva-iauth (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, Result/thiserror, format!/typed builders — no sprintf/global buf), unit‑tested + boot‑golden + proptest‑fuzzed, and differentially pinned against ircd-common (or iauth-rs for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. Done: 309 slices (309 = channel mode +M (operpeace) — a port of charybdis extensions/chm_operpeace.c: a +M channel flag that prohibits a non-operator from kicking an IRC operator out of the channel (the kick-shaped sibling of +Y immunity / elemental rank protection); pure fuzz seam channel::operpeace_blocks(operpeace_set, kicker_is_oper, victim_is_oper) = faithful charybdis if (IsOper(source_p)) return; + +M && receive_immunity, with operpeace_enabled the read accessor and the shared command::{operpeace_protects, operpeace_refusal} resolving live state (victim oper-status from the registry mirror → works for remote victims); enforced in command/kick.rs and command/remove.rs (a blocked kick → 482 :Cannot kick IRC operators from that channel + a +s audit snotice, victim untouched) — OKICK is unaffected (oper-gated, always bypasses); new ChanMode::OperPeace letter M bit 0x2_0000_0000, oper-only to set (like +P/+L via permanent_set_allowed, a non-oper MODE +M → 481); divergences = leveva-native (no oracle), refusal reuses 482 not charybdis's 484 ERR_ISCHANSERVICE (leveva's 484 = ERR_RESTRICTED), oper-bit immunity not per-privilege, mode change shown to all members (no ONLY_OPERS); 10 lib units + operpeace_proptest (3 props) + golden_operpeace, with the isupport/registration asserts + 10 CHANMODES/MYINFO snapshots regenerated, help/CHANNEL_MODES.md gains the +M row; 308 = roleplay system — a port of charybdis extensions/m_roleplay.c: a +N (roleplay) channel flag enabling NPC/NPCA/SCENE(+AMBIANCE alias)/FSAY/FACTION commands that speak to the channel from a fake nickname (source <fake>!<author>@npc.fakeuser.invalid, the body suffixed (<author>) and the ident = the real author, so authorship is always recoverable); NPC/NPCA underline the fake nick, SCENE narrates under a fixed =Scene=, FSAY/FACTION give opers a clean nick (underlined NPC fallback for non-opers), and *A variants wrap the body as a CTCP ACTION; first widened the full 32-bit channel-flags bitfield to u64 (ChannelModes.flags/ChanMode::bit()/MemberStatus.bits/default-channel-modes plumbing — ExtendLimit had taken the last u32 bit) so Roleplay could claim 0x1_0000_0000; pure fuzz seam roleplay::compose = faithful charybdis m_displaymsg (NICKLEN truncate → strip_unprintable dropping control chars <0x20 + eating mIRC \x03 colour runs + trimming trailing spaces → None if empty → \x1F-underline → (author) suffix → \x01ACTION …\x01 500-cap), the gate ladder reusing display_name/is_member/roleplay_enabled/can_send (461→403→442→573-+N-off→573-cant-send→573-empty-nick), fanning the fake PRIVMSG to all local members incl. the issuer and propagating :<uid> ENCAP * ROLEPLAY <chan> <display> :<body> (new s2s/roleplay.rs, re-sourced + onward-relayed split-horizon like KNOCK); leveva-native (no oracle), new numeric 573 ERR_ROLEPLAY, six help pages; 20 command units + 5 s2s units + roleplay_proptest (3 props: compose total/never-panics + structural soundness + underline-only-wraps + alphanumeric-always-Some) + golden_roleplay, with the isupport asserts + 9 CHANMODES/MYINFO snapshots regenerated; 307 = combination extbans $& (AND) / `$ |
(OR), a port of charybdisextensions/extb_combi.c: two extban **operator** types whose data is a comma-separated list of child extbans ([~][:], no leading \(`), optionally paren-wrapped, with paren-aware comma splitting and backslash escaping — `\)&matches iff **all** children match,$ |
P12 — Retire the mechanical port; leveva is the product ✅ DONE (2026‑06‑13) (docs/progress-log/p12.md) |
deleted ircd-common/ircd-rs + C‑era iauth-rs + leveva-integration + the mechanical port's deployment rigging; workspace = leveva + leveva-iauth |
Ran the differential suite a final time green (cargo test -p leveva-integration, skeleton‑identical to the oracle), then deleted the mechanical port (ircd-common, ircd-rs), its C‑era auth scaffolding (iauth-rs, long since subsumed by the native leveva-iauth), and the oracle differential crate (leveva-integration). The oracle tests retired with their oracle — the load‑bearing behavior was already carried by leveva's self‑contained 212‑file golden + proptest suite (verified: leveva/leveva-iauth have zero Cargo/use dep on the deleted crates). Also stripped the mechanical port's deployment rigging (docker/Dockerfile.ircd, the ircd bake target, the entire docs/k8s/ example network) and updated the README to describe the finished strangler. This is the literal end of the strangler — same lifecycle the C tree had at P8. |
MET. Final differential run green; ircd-common/ircd-rs/iauth-rs/leveva-integration deleted; cargo build --workspace 0 warnings; cargo clippy --workspace --tests clean; cargo test -p leveva -p leveva-iauth green — the behavioral guarantees carry forward. The workspace is now leveva + leveva-iauth. |
Progress log#
The per-phase progress log lives in docs/progress-log/, one file per top-level phase. Each P5 command-handler cluster appends an entry to p5.md (see the command-cluster-port skill).
- P0 — Scaffold + baseline + harness
- P1 — Leaf / pure utils
- P2 — Data & lookup
- P3 — Transport & format
- P4 — Parser & dispatch
- P5 — Command handlers
- P6 — Config & DNS
- P7 — I/O core + event loop + glue
- P8 — Delete the C + retire the oracle harness
- P10 — Rename to
leveva+ typed identifier strings - P11 — leveva protocol layer → feature parity
- P12 — Retire the mechanical port; leveva is the product
- P‑IAUTH — greenfield iauth-rs
Key decisions & open risks#
- "100% Rust" is P8, not P7. ~25 trivial C variadic trampolines persist through the faithful port (stable Rust can't define variadic
extern "C"). They are the last C deleted — P8 replaces them with a non‑variadic Rust sender API at every call site (the fullformat!‑based cleanup of string formatting then completes in P11). (Alternative: nightlyc_variadic— not recommended; pins the toolchain.) - Determinism is the project's spine. Hash/list iteration order, clocks, UID/SID sequence, DNS, and fd ordering must all be pinned, or L2 becomes noisy and stops catching regressions. A "faithful" port that changes iteration order passes L1 but fails L2 — by design.
- zlib bit‑exactness (
ZIP_LINKSserver links): flate2 must match compression level/strategy/flush points, or P5 golden fails. Validate early in P5. - Resolver swap risk: hickory‑dns caching/timeout drift is visible via WHOIS/WHO hostnames; keep a faithful
res*.cport as fallback if golden diffs appear (P6). - crypt() legacy: keep libc
cryptfor oper‑password dual‑validation; divergence locks out opers; availability varies by platform. - Reference‑build longevity: the pristine C tree must keep compiling as the Rust product diverges; OS/toolchain drift is a maintenance cost (pin a container image).
Verification (end‑to‑end)#
- Per module:
cargo test -p <crate>runs L1 differential (Rust vscref_…) to zero diffs;cargo fuzz run <target>for parser/format/config tiers. - Per phase:
ircd-testkitboots reference‑C + Rust ircd under theLD_PRELOADclock shim, replays the scripted serialized session suite (registration, JOIN/PRIVMSG/MODE/KICK/TOPIC, WHO/WHOIS/WHOX, OPER/STATS, server link burst, netsplit, rehash, restart), and asserts canonicalized wire output is byte‑identical. L4 conformance assertions run in the same harness. - iauth:
iauth-rsvs C‑iauth replay with fixed<id>scripts + per‑module fixtures; then end‑to‑end ircd+iauth‑rs golden. - CI (every commit): build reference‑C; build Rust workspace; boot the server; run L1 (all crates), L2 golden, L4 conformance; nightly L3 fuzz + soak. Green CI after a module's
.cis dropped is the definition of "that module is migrated and fully tested." - Final (P7 exit):
ircd-syscompiles no C logic; the entire L1–L4 + soak suite is byte‑identical to the archived reference‑C across the full scenario matrix.