Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
46 kB
P6 — Config & DNS: per-cluster summary#
One-line top-level summary per P6 cluster (detailed entries live in
docs/progress-log/p6.md).
- P6a DONE:
s_conf.cTKLINE cluster (m_tkline/m_untkline/prep_kline/do_kline/wdhms2sec/tkline_expire+ thetkconflist) — the oper temporary-kill family ported toircd-common/s_conf.rsvia ans_conf_link.opartial port (-DPORT_TKLINE_P6; the rest ofs_conf.c's config grammar stays C). Handler cluster, not server-reachable (msgtab col 2/3; do_kline emits only localSCH_NOTICE→&NOTICESnotices + reaps local clients). Gates: L1 (s_conf_tkline_diff: wdhms2sec corpus + tkconf add/dedup/expire lifecycle with inverses, highest_fd pinned -1) + L2 (golden_s_conf_tkline: oper TKLINE notice/UNTKLINE/bad-format, the two-client reap → 465+disconnect, non-oper 481 inverse). No S2S (no IsServer/remote-field/propagation path). - P6b DONE:
res.cm_dns(the resolver/cache debug command — the last command handler still in C) — ported toircd-common/res.rsvia ares_link.opartial port (-DPORT_DNS_M_DNS, de-static'ingcachetop/cainfo/reinfo); the resolver proper stays C. Everymsgtabhandler is now Rust. Handler cluster, msgtab col 2 (STAT_OPER) only + theis_allowed(ACL_DNS)O-linedgate. L2 (golden_s_dns,dns_oper.conf): operDNSstats +DNS lempty cache dump, non-oper 481 inverse. No S2S. - P6c DONE:
s_conf.cflags cluster (the six pure config-flag converters{iline,pline,oline}_flags_parse/_to_string) — sames_conf_link.opartial port (-DPORT_SCONF_FLAGS_P6); first bite into the config grammar. Utility/callee (none in msgtab). L1 (s_conf_flags_diff: all six vscref_) + L2 (golden_s_conf_flags,flags_oper.conf). No S2S. - P6d DONE:
s_conf.cmatch_ipmask(the CIDR/#IP-bitmask matcher — a pure leaf predicate overcptr->ip/username, used by Rust channel ban-matching + the TKLINE/conf-lookup paths) — sames_conf_link.opartial port (-DPORT_SCONF_IPMASK_P6). Utility/callee. L1 (s_conf_match_ipmask_diff: v4-mapped /32//24//0, j-boundary, user@host/cidr, badmask, native v6, each with its inverse) + L2 (golden_s_conf_match_ipmask: channel CIDR+b). No S2S. - P6e DONE:
s_conf.cfind_admin/find_me(two leaf read-only walks of the globalconflist for theCONF_ADMIN/CONF_MEentry) — sames_conf_link.opartial port (-DPORT_SCONF_FINDADMIN_P6). Utility/callee. L1 (s_conf_find_admin_me_diff: head/non-head/absent/combined-bits incl. inverses, both globals pointed at one list) + L2 (golden_s_conf_find_admin: theADMINcommand via Rustm_admin). No S2S. - P6f DONE:
s_conf.cconfig-file parser (openconf+initconf— the wholeircd.confgrammar) — sames_conf_link.opartial port (-DPORT_INITCONF_P6, un-static'inglookup_confhost); the line lexer is a newnom-based module (config_parse.rs— faithfulprocess_escapes→split_fieldsport of the escape/comment pass +getfield), the semantic switch builds byte-identicalaConfItemlists/classes + side effects. Not in msgtab. L1 (config_parse_diff: nomsplit_fieldsvscref_getfield;s_conf_initconf_diff: full conf-list build vscref_initconfover A/I/i/O/o/Y/C/c/N/D/H/L/V/Q/B/K/k) + L2 (golden_s_conf_initconf: STATS y/i/o over the parsed config). No S2S. - P6g DONE:
s_conf.cfind_Oline(the read-onlyconf-list scanm_operuses to resolveOPER <name> <pass>: bestCONF_OPSentry by namemycmp+ hostmatch/match_ipmask, honouringclients < MaxLinks(Class)with the over-limittmp2fallback) — sames_conf_link.opartial port (-DPORT_SCONF_FINDOLINE_P6). Utility/callee (m_opercalls it). L1 (s_conf_find_oline_diff: name/host-glob/CIDR-in-out/over-limit-fallback/wrong-status/empty incl. inverses) + L2 (golden_s_conf_find_oline,find_oline.conf: a10.0.0.0/8O-line that misses the 127.0.0.1 client → 491, vs a*@*O-line → 381 — the host/CIDR-miss branch the existing OPER golden can't reach). No S2S (no IsServer/remote-field path). - P6h DONE:
s_conf.cfind_conf*family (the seven read-only conf lookupsfind_conf_exact/find_conf_name/find_conf/find_conf_host/find_conf_host_sid/find_conf_ip/find_conf_entry) — sames_conf_link.opartial port (-DPORT_SCONF_FINDCONF_P6). Utility/callee (none in msgtab; the still-Cs_serv.c/s_bsd.c/s_misc.cserver-link + registration paths call them). L1 (s_conf_find_conf_diff: all seven vscref_over positive + inverse — name/host/status/port/passwd-asymmetry misses, the CONF_OPERATOR over-limit skip, the server-maskmycmp-exact-vs-match-glob split, the NUL HOSTLEN early-outs, thefind_conf_ipin-place@-split restore; the differential caught an inverted!match(passwd,sid)sid gate in the first draft) + S2S (golden_s2s_s_conf_find_conf: a peer link drives the C/N-linefind_confaccept +find_conf_host/find_conf_host_sidleaf/hub checks → byte-identical burst + post-link routing; the 37-testgolden_s2s_*suite + registration goldens are the broader regression). No client-only L2 numeric. - P6i DONE:
s_conf.cK-line lookup family (find_kill/find_two_masks/find_conf_flags/find_denied) — the read-only K-line / version-mask / deny lookups named in the PLAN P6 row, ported toircd-common/s_conf.rsvia thes_conf_link.opartial port (-DPORT_SCONF_FINDKILL_P6, three guard regions;find_bouncestays C). Config-resolved: TKLINE on (find_kill's tkconf→kconf two-pass loop compiles), KLINE off, TIMEDKLINES off (the reply/check_time_interval block not compiled;timedklinesarg inert). Utility/callees (none in msgtab); the still-Cs_serv.ccheck_version, Rusts_user.cregister_user, andircd.cresolve to the Rust defs at link. L1 (s_conf_find_kill_diff: all four vscref_over positive + inverse — find_kill tkconf/kconf hit, expired-TK fallthrough, OTHERKILL→ident vs KILL→usernamecheck, port gate, CIDR in/out, '='-numeric-only skip, resolved-hostname match, IsKlineExempt short-circuit, no-user guard; find_two_masks/find_conf_flags status/host/name/no-slash/prefix/strpbrk inverses; find_denied host-scan/reversed/digit-passwd/no-D-line) + L2 (golden_s_conf_find_kill,kline.conf: a 127.0.0.1 client hitsK|127.0.0.1|…|*|0|mid-registration → 465 +K-lined:Closing Link) + S2S (golden_s2s_s_conf_find_two_masks,vline_reject.conf= s2s.conf +V|*||peer.test|: m_server_estab check_version → find_two_masks → "Bad version" exit). The no-match version-check path is exercised by every othergolden_s2s_*test; find_denied is reachable only fromtry_connectionsserver auto-connect (not in the golden harness) → L1 only. - P6j DONE:
s_conf.cfind_bounce(the RPL_BOUNCE sender — the read-only conf-list scan that emits010 :Please use this Server/Port insteadto a rejected connection) — ported toircd-common/s_conf.rsvia thes_conf_link.opartial port (-DPORT_SCONF_FINDBOUNCE_P6, one guard region; the mutatingattach/detach/rehashfamily +lookup_confhost/ipv6_convertstay C). Three dispatch modes byfd:fd>=0early-reject (cptr NULL →sprintf+rawsendto),fd==-1class-number host match,fd==-2host-match-only. Config:CONF_BOUNCE=262144,RPL_BOUNCE=10. Utility/callee (not in msgtab); callers s_bsd.c (all-conns-in-use), Ccount_cnlines, and Rustm_nick/m_user(server-only P-line) resolve to the Rust def at link. L1 (s_conf_find_bounce_diff: 9 tests diffing sent bytes — sendQ via sendto_one for fd<0, socketpair recv for fd>=0 — over host-glob hit/miss, CIDR in/out, class-number host on fd==-1 vs the fd==-2 skip, non-bounce-skip + first-match-wins, the cptr-NULL guard, empty list, and the fd>=0 empty-host write vs non-empty-host no-op) + L2 (golden_s_conf_find_bounce,bounce.conf: a server-only P-lineP||||16667|0|S|+B|127.0.0.1||bounce.elsewhere|7000|→ client NICK on the server-only port →010bounce +Server only portexit, byte-identical). No S2S (no IsServer/remote-field path; the fd>=0 raw-sendto branch needs MAXCLIENTS conns → L1 only). - P6k DONE:
s_conf.cattach/detach mutating cluster (attach_conf/detach_conf/det_confs_butmask/attach_confs/attach_confs_host+ the privateis_attached/add_cidr_limit/remove_cidr_limit) — the connected component of conf-attachment mutators (grow/shrinkcptr->confs+ the globalconflist, mutate class refcountsaconf->clients/ConfLinksand the per-class CIDR-limit patricia trees), ported toircd-common/s_conf.rsvia thes_conf_link.opartial port (-DPORT_SCONF_ATTACH_P6, three guard regions). Config-resolved: ENABLE_CIDR_LIMITS on (add/remove_cidr_limit compile;pnode->datavoid* counter), YLINE_LIMITS_IPHASH on (per-host limit loop walks the IP hash), YLINE_LIMITS_OLD_BEHAVIOUR off (max-links gateConfLinks >= ConfMaxLinks). Utility/callees (none in msgtab); callers — Cattach_Iline/count_cnlines/s_bsd.c/s_serv.c/s_service.c+ Rustregister_user/m_oper/m_umode— resolve to the Rust defs at link. Gates: L1 (s_conf_attach_diff: two fully-isolated worlds via anApiof fn-pointers; round-trip/inverse invariants — attach→detach unlink, is_attached idempotency, -3 Y-line-max + -4 CIDR gates, CIDR freed-slot reuse, free-class on maxLinks==-1, illegal-conf removal from global list, det_confs_butmask, attach_confs/host glob-vs-mycmp; zero diff) + L2 (golden_s_conf_attach,ylinemax.conf: 2 clients in a maxlinks-1 class → bob rejected via attach_conf -3 → "Too many connections", byte-identical; success+detach paths covered pervasively by the registration + golden_s2s_* suite). No S2S (no IsServer/remote-field path; attach_confs C/N + det_confs_butmask SQUIT covered by the 37-test golden_s2s_* establishment/teardown suite). What stays C in s_conf.c is now onlyrehash,attach_Iline/count_cnlines(callers of this core), and the initconf-privatelookup_confhost/ipv6_convert. - P6l DONE:
s_conf.cattach_Iline+count_cnlines(the two callers of the P6k attach/detach core) — the local-client I-line lookup used during registration (attach_Iline: scanconffor the bestCONF_CLIENTline matching the client's port/name/host/password, set the +r/kline-exempt conf flags ontocptr->user->flags, copy the resolved host intosockhost, thenattach_conf; emits464 ERR_PASSWDMISMATCH/find_bounceon rejection) and the read-only single-N-line counter used during inbound server-link establishment (count_cnlines), ported toircd-common/s_conf.rsvia thes_conf_link.opartial port (-DPORT_SCONF_ILINE_P6, one guard region s_conf.c:483–664). Config-resolved: UNIXPORT off (no unix-socket host branch), XLINE off (no XlineExempt clear), NO_DNS_LOOKUP on (hp usually NULL). Utility/callees (none in msgtab); callers — Ccheck_client/check_server(s_bsd.c) — resolve to the Rust defs at link. What stays C ins_conf.cis now onlyrehash+ the initconf-privatelookup_confhost/ipv6_convert. Gates: L1 (s_conf_iline_diff: two fully-isolated worlds via anApiof fn-pointers — happy glob/CIDR/empty-mask attach + the no-I-line-2inverse, port gate, password fall-through vs-8+464, +r/kline-exempt flag-set, hp!=NULL name-match +get_sockhost;count_cnlinessingle-N/zero/two/non-server-skip/empty; zero diff over 8 tests) + L2 (golden_s_conf_iline,iline_pass.conf: a password I-line with noFflag → a client sending noPASS→464+ "Bad password" Closing Link, byte-identical). No S2S (neither has anIsServerbranch / remote-field formatting;count_cnlines's server-link path is covered by the 37-testgolden_s2s_*link suite). - P6m DONE:
s_conf.cfinalize —rehash+ipv6_convert+lookup_confhost+ theconf/kconf/networknamedata globals (the last three functions + the last three data globals s_conf.c owned) — ported toircd-common/s_conf.rs, sos_conf.cis now FULLY Rust →s_conf.odropped outright (added toPORTED; thes_conf_link.opartial-compile + the"s_conf.o" => "s_conf_link.o"link map removed; the cref oracle keeps the unguardeds_conf.o).rehash= the faithful config reload (free conf/kconf, mark classesMaxLinks=-1,initconf(0), close/reopen listeners, re-read MOTD, the sig1/'a'/'d'/'t'branches; ULTRIX/USE_SYSLOG off, TKLINE on);ipv6_convert= the pure[user@]addr[/cidr]canonicaliser (in-place@//poke + restore);lookup_confhost= numeric host viainetpton(the NO_DNS path) / alpha host via the still-Cgethost_byname,badlookupAND16==255 reset. The conf/kconf/networkname globals flip from bindgen externs to#[no_mangle] pub static mutdefs (list.rs/s_serv.rs importers resolve to them at link, same astkconf/classes).build.rsexpandsIRCDMOTD_PATHvia make → theIRCD_MOTD_PATHrustc-env →ircd_sys::MOTD_PATH, so the Rustrehash'sread_motdreads the identical file reference-C does. Utility/callees (none in msgtab);rehashreached from Rustm_rehash(oper REHASH) + still-Circd.cSIGHUP. Gates: L1 (s_conf_rehash_diff:ipv6_convertvscref_over bare-v6/user@/cidr/v4-mapped/passthrough + the input-restore invariant) + L2 (golden_s_serv_maintREHASH 382 success/481 reject now drives the Rustrehash;golden_s_conf_initconfboot exerciseslookup_confhost/ipv6_convert). No S2S (noIsServer/remote-field path).lookup_confhostisstaticin the unguarded oracle → nocref_symbol → covered by the boot golden, not L1. - P6n DONE:
res.c's pure wire-format leafres_comp.c(the BIND DNS name compression/expansionircd_dn_expand/ircd_dn_comp/__ircd_dn_skipname+ the big-endian integer marshalersircd_getshort/ircd_getlong/ircd__putshort/ircd__putlong) — ported outright toircd-common/res_comp.rs→res_comp.odropped (added toPORTED; no_link.o). Self-contained leaf:nm --undefined-onlyshows zero ircd-internal deps (only libcmemcpy/strchr/errno); the 7 BINDstatichelpers (ns_name_*/special/printable/mklower/dn_find) ported as private Rust fns. Faithful raw-pointer port;errnoset (EMSGSIZE/ENOENT) on every error path as the C does. Utility/leaf (none inmsgtab); callers — still-Cres.c/res_mkquery.c— resolve to the Rust defs at link. Gate: L1 only (res_comp_diff: get/put match + put→get round-trip;dn_comp→dn_expandpack/expand round-trip; the compression-pointer reuse diffed as buffer-relativednptrsoffsets + emitted wire bytes; a hand-built compressed-pointer message;dn_skipnameplain/compressed/truncated; the dstsiz-too-small, malformed-pointer, overlength-1inverses witherrnocompared). No L2/S2S — pure wire-format leaf with no client/server-reachable command path under the locked config; end-to-end DNS coverage arrives when the resolver proper is ported under a fixture nameserver. - P6o DONE:
res.c's DNS query-packet builderres_mkquery.c(ircd_res_mkquery— forms QUERY/NS_NOTIFY_OP/IQUERY messages into a caller buffer, returns the packet length or-1) — ported outright toircd-common/res_mkquery.rs→res_mkquery.odropped (added toPORTED; no_link.o). Deps resolve at link:ircd_dn_comp/ircd__putshort/ircd__putlongare the P6n Rust defs, the resolver-state globalircd_res+ircd_res_initstay C (res_init.o). Layout hazard neutralized: the 12-byte DNS header is written through the bindgenHEADERbitfield type (size 12,set_id/set_opcode/set_rd/set_rcode/set_qdcount/set_ancount/set_arcountbyte-identical to the gcc little-endiannameser_def.hlayout) instead of hand-packing bitfields;h_errnoset via__h_errno_locationexactly as the C. Config-resolved:DEBUGoff → theRES_DEBUGprintf block not compiled. Utility/leaf (none inmsgtab); callers — still-Cres.c(query_name/do_query_*) — resolve to the Rust def at link. Gate: L1 only (res_mkquery_diff: Rust vscref_byte-identical packets over QUERY±recurse, NS_NOTIFY_OP with/without the additional-record branch, IQUERY answer section with/without rdata, plus the-1inverses — bad op, NULL buf,buflen<HFIXEDSZ, and the question-sectionbuflen/dn_compoverflow — and the shared++ircd_res.idside effect diffed identically; bothircd_res/cref_ircd_respinned with RES_INIT to skip the nondeterministicres_init/res_randomidpath, a process-globalRES_LOCKserializing the parallel#[test]threads). No L2/S2S — pure wire-format leaf with no client/server-reachable command path under NO_DNS_LOOKUP; end-to-end DNS coverage arrives when the resolver proper (res.c+res_init.c) is ported under a fixture nameserver. - P6p DONE:
res.c's resolver-state bootstrapres_init.c(ircd_res_init— reads/etc/resolv.conf+LOCALDOMAIN/RES_OPTIONSenv into the globalircd_res; the staticres_setoptionsit drives; the random message-idircd_res_randomid) — ported outright toircd-common/res_init.rs→res_init.odropped (added toPORTED; no_link.o). Theircd_resglobal moves from C to a Rust#[no_mangle] static mut(MaybeUninit::zeroed);res_mkquery.rs/res.c/s_bsd.cresolve theirircd_res/ircd_res_initreferences to the Rust defs at link. Self-contained leaf — the only non-libc dep isinetpton(P1support.rs). Config-resolved:DEBUG/NEXT/RESOLVSORT/RFC1535/HAVE_GETIPNODEBYNAME/USELOOPBACK/__BIND_RES_TEXTall OFF → only the simple path compiles (no NetInfo, no sortlist, no debug printfs, default-dnsrchderivation active,inet6→RES_USE_INET6;AFINET=AF_INET6so nameservers parse as IPv6). Utility/leaf (none inmsgtab). Gate: L1 only (res_init_diff: theircd_resstruct produced byircd_res_initdiffed vscref_field-by-field — scalars, ndots/nsort bitfields, the 3nsaddr_listsockaddr_in6byte-for-byte,defdnamebytes,dnsrch[]as offsets intodefdname— over the default-dnsrch/LOCALDOMAIN-search-tokenizer/RES_OPTIONSndots:+inet6+RES_MAXNDOTS-clamp paths, both globals zeroed +.idpinned nonzero to skip the nondeterministicres_randomid, env-set/init/restore under a process-globalRES_LOCK;res_randomidis nondeterministic → only its& 0xffffmask smoke-tested). Boot/registration +golden_s_dnsgoldens confirm the Rustircd_res_init/ircd_resboot byte-identical. No L2/S2S (pure leaf, no client/server command path underNO_DNS_LOOKUP; end-to-end DNS coverage arrives when the resolver properres.cis ported under a fixture nameserver).res.cis the only DNS file still in C. - P6q DONE:
res.ccache-hash leaveshash_number/hash_name(the two pure DNS-cache bucket hashes — first bite of theres.ccache cluster, ported bottom-up from the resolver's dependency floor) — ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_CACHE_P6q, dropping their bodies + declaring them extern so the still-C cache walkersfind_cache_name/add_to_cache/rem_cache/find_cache_numbercall the Rust defs). Both are upstream file-static, so a new three-wayRES_CACHE_LINKAGEmacro de-statics them to GLOBAL in the cref oracle'sres.o(-DRES_CACHE_EXPOSE, behavior-neutral; res.o is consumed only bylibcref.a) socref_hash_*exists for the L1 differential — neutralizing thel1-cref-static-symbol-limit. Faithful:hash_numberis the unrolledhashv=ip[0]; for i in 1..16 { hashv=2*hashv+ip[i] }over a wrapping u32 (% 1009=ARES_CACSIZE only at the end; reads exactly 16 bytes = sizeof in6_addr, AFINET=AF_INET6);hash_namesums bytes to the first./NUL with signed-char promotion (i8 as i32 as u32). Utility/leaf (not inmsgtab). Gate: L1 only (res_hash_diff: Rust vscref_over 16-byte v6 addrs incl. all-0xff u32-overflow/v4-mapped/high-bit, names incl. empty/dot-first/embedded-dot/8-bit/long; range-checked[0,1009)). No L2/S2S (no client/server command path; resolver inert underNO_DNS_LOOKUP, cache never populates at runtime — end-to-end DNS coverage arrives when the resolver proper is ported under a fixture nameserver).golden_s_dns+ boot goldens confirm the still-C cache walkers call the Rust hashes without regression. - P6r DONE:
res.ccache removal/expiry corerem_list/rem_cache/expire_cache/flush_cache(the second bite of theres.ccache cluster, building on the P6q hashes) — the connected component that deletes cache entries, ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_CACHE_REM_P6r, generalizing the P6q three-wayRES_CACHE_LINKAGEtorem_cache/rem_list).rem_listunlinks one entry fromcachetoponly (leaves the hash buckets +incacheuntouched — the faithful quirk);rem_cachedoes the full delete (NULL anylocal[]->hostpat thishe, unlink fromcachetop+ bothhashtablebuckets via the Rusthash_name/hash_number, free name/aliases/addrs/entry,incache--+ca_dels++);expire_cachereaps the past-due + returns the next poll time;flush_cachereaps everything. The cache data globalshashtable/incachejoincachetop/cainfoonRES_DNS_STATIC, now also exposed under-DRES_CACHE_EXPOSEso the cref oracle getscref_cachetop/cref_hashtable/cref_incache/cref_cainfofor the L1 isolated worlds. Utility/leaf (not inmsgtab). Gate: L1 only (res_cache_rem_diff: two fully-isolatedWorlds of fn-pointers + global base pointers, entries hand-built à laadd_to_cache; flush/expire/rem_cache-hostp/rem_list each with its inverse — flush empties + re-inserts clean, expire keeps survivors in order + returns min-future/now+AR_TTL, rem_cache clears a per-world danglinghostp, rem_list returns the successor + leaksincacheby design; zero diff). No L2/S2S (resolver inert underNO_DNS_LOOKUP;golden_s_dns+ registration goldens confirm the still-C cache walkers call the Rust removal core without regression). Still C inres.c: the resolver proper + the cache build/insert/lookup half (add_to_cache/update_list/find_cache_*/make_cache). - P6s DONE:
res.ccache lookup/reorder halfupdate_list/find_cache_name/find_cache_number(the third bite of theres.cport — the half of the hostent cache that looks up and reorders/merges entries, built on the P6q hashes + P6r removal core) — the connected component ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_CACHE_LOOKUP_P6s, extending the three-wayRES_CACHE_LINKAGEto the three upstream file-statics so the still-Cgethost_byname_type/gethost_byaddr/proc_answer/make_cachecallers resolve to the Rust defs at link, and the cref oracle de-statics them tocref_*for L1).update_listreorders a hit to thecachetophead and (ifrptr != NULL) merges new aliases (T_PTR) / addresses (T_A/T_AAAA) from the ResRQ reply into the cachedhostent— the gnarly double-MyReallocof the contiguous IP block + the pointer array, then theFLG_PTR_*flag update byaddrcount;find_cache_name/find_cache_numberdo the hashed-bucket lookup + the full-list fallback (the fallback's fixed-s=alias[0] quirk + thefind_cache_numberrem_listdegenerate-entry reap preserved),update_liston hit,ca_na_hits/ca_nu_hitscounters. Faithful raw-pointer port preserving the C post-increment index walks (s = h_aliases[i++]) and the for-loopcontinue→increment semantics. Utility/leaf (not inmsgtab). Gate: L1 only (res_cache_lookup_diff: two isolatedWorlds of fn-pointers + global base pointers —cachetop/hashtable/cainfo; find_cache_name hashed hit+reorder+counter / name-miss / flags-gate-miss / alias full-list fallback; find_cache_number hashed hit / absent-addr miss / FLG_PTR_VALID gate / multi-addr fallback; update_list rptr=NULL reorder-only + T_PTR alias-merge & T_A addr-merge each with the already-present no-growth inverse; zero diff over 6 tests). No L2/S2S (resolver inert underNO_DNS_LOOKUP;golden_s_dns+golden_registrationconfirm the still-C build/insert half calls the Rust lookup/reorder core without regression). Still C inres.c: the resolver proper + the cache build/insert half (add_to_cache/make_cache). - P6t DONE:
res.ccache build/insert halfadd_to_cache/make_cache(the fourth and final bite of theres.ccache cluster — the two functions that create and link in hostent-cache entries, built on the P6q hashes + P6r removal core + P6s lookup/reorder) →res.c's hostent cache is now FULLY Rust; only the resolver proper remains C. Ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_CACHE_BUILD_P6t, extending the three-wayRES_CACHE_LINKAGEto both upstream file-statics so the still-Cproc_answercaller ofmake_cacheresolves to the Rust def at link, and the cref oracle de-statics them tocref_*for L1).add_to_cache= list-head push + both hash-bucket links (hash_name/hash_number) +++incache > MAXCACHEDLRU tail-evict viarem_cache+ca_adds++;make_cache= dedup-or-build (T_PTR→find_cache_numberper reply addr / T_A·T_AAAA→find_cache_nameby name — a hit returns the already-reordered+merged entry; a missMyMallocs a fresh entry, builds the contiguous IP block + pointer array, steals the ResRQ aliases +h_name(source nulled), clampsttl<600→600 +re_shortttl++,expireat = timeofday + ttl, sets theFLG_A_VALID/FLG_AAAA_VALIDflag, thenadd_to_cache). Config: DEBUG off (no Debug blocks), AFINET=AF_INET6 (16-byte IN_ADDR), MAXCACHED 512. Utility/leaf (not inmsgtab). Gate: L1 only (res_cache_build_diff: two fully-isolatedWorlds of fn-pointers + base pointers incl.incache/reinfo/timeofday— build-by-name A/AAAA with the steal inverse, short-ttl clamp + the ≥600 inverse, dedup-by-name + dedup-by-addr-PTR no-add, multi-addr contiguous-block build,add_to_cacheLRU boundary at MAXCACHED (evict via ca_dels delta) + the under-cap inverse, both-bucket linkage via the two finders;timeofdaypinned soexpireatis deterministic; zero diff over 8 tests). No L2/S2S (resolver inert underNO_DNS_LOOKUP, cache never populates at runtime;golden_s_dns+golden_registrationconfirm the still-C resolver calls the Rust build/insert core without regression). Still C inres.c: only the resolver proper (request queue + answer parser + socket I/O). - P6u DONE:
res.crequest-queue coreadd_request/rem_request/make_request/find_id(the first bite of theres.cresolver proper — the queue-management primitives the rest of the resolver builds on, ported bottom-up from the outstanding-DNS-request floor after the P6q–P6t cache cluster) — ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_REQ_P6u: a parallel three-wayRES_REQ_LINKAGEde-statics the four fns —externin res_link.o so the still-C walkersdel_queries/timeout_query_list/get_resresolve to the Rust defs, GLOBAL in the cref oracle forcref_*L1, static otherwise — and thefirst/lastqueue heads flip fromstatictoRES_DNS_STATICso the Rust core + the still-C walkers share one storage, same as the cache globals).add_requesttail-appends +reinfo.re_requests++(NULL → -1 no-op);rem_requestunlinks (repointinglaston tail removal) + freeshe.h_name/he.h_aliases[]/name/the node;make_requestMyMalloc+bzero+ seedssentat=timeofday/retries=3/resend=1/srch=-1/timeout=4/he.h_addrtype=AFINET+ copies theLinkintocinfo+add_request;find_idlinear-scansfirst. Config: AFINET=AF_INET6, DEBUG off (Debug() traces gone), MyFree→libc::free. Utility/callees (not inmsgtab). Gate: L1 only (res_request_diff: two isolatedWorlds of fn-pointers +first/last/reinfobase pointers — make_request seeds+appends (NULL-lp zeroes cinfo), add_request order+counter + the NULL no-op, find_id hit/miss + misses-after-rem, rem_request head/middle/tail unlink+last-repoint+free, drained queuefirst==last==NULL+reusable;timeofdaypinned, a process-global LOCK serializes the parallel threads; zero diff over 8 tests). No L2/S2S (resolver inert underNO_DNS_LOOKUP, the queue never populates at runtime;golden_s_dns+golden_registrationconfirm the still-C walkers call the Rust queue core without regression). Still C inres.c:del_queries/timeout_query_list+ the resolver socket/answer half (send_res_msg/do_query_*/query_name/resend_query/proc_answer/get_res/gethost_*/init_resolver/cres_mem). - P6v DONE:
res.cqueue walkersdel_queries/timeout_query_list(the second bite of theres.cresolver proper — the reaper/timeout layer that walks the outstanding-DNS request queue, built on the P6u queue core) — ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_WALK_P6v).del_queries(cp)reaps every node whosecinfo.value.cpmatches;timeout_query_list(now)walksfirst, reaping past-due requests whose--retriesis exhausted (reinfo.re_timeouts+++ the per-cinfo.flagsnotify — ASYNC_CLIENTsendto_iauth+ClearDNS, ASYNC_CONNECTsendto_flagSCH_ERROR — thenrem_request) or backing off the rest (sentat=now,timeoutdoubled,resend_query,tout=now+timeout), returning(next>now)?next:now+AR_TTL. A newRES_RESEND_LINKAGEseam de-statics the still-Cresend_query(extern in res_link.o so the Rusttimeout_query_listresolves to it; the cref oracle keeps it static —cref_timeout_query_listresolves to the objcopy-renamed local, self-contained sinceresend_queryhas no out-of-TU caller). Config-resolved: DEBUG off (Debug/myctimetraces gone), USE_IAUTH on;retriesis unsignedcharon this platform → faithfulwrapping_sub. Utility/callees (not inmsgtab); still-C callerss_bsd.c/ircd.c+ Rustlist.rsresolve to the Rust defs at link. Gate: L1 only (res_walk_diff: two fully-isolatedWorlds of fn-pointers +first/last/reinfobase pointers — del_queries head/middle/tail match + absent-cp no-op + all-match drain; timeout future-survive (mintout) / exhausted ASYNC_CONF·ASYNC_CLIENT (ClearDNS clears the DNS bit only)·ASYNC_CONNECT / retry back-off (resend=0 →resend_queryno-op) / a mixed survivor+reaped+retried walk — return value, queue shape,re_timeoutsall diffed;timeofdaypinned,adfd=-1+ BSS-zerosvchansneutralize the variadic notifies, process LOCK serializes; zero diff over 9 tests). No L2/S2S (resolver inert underNO_DNS_LOOKUP, the queue never populates at runtime;golden_s_dns+golden_registrationconfirm the still-C resolver calls the Rust walkers without regression). Still C inres.c: only the answer parser + socket I/O half (send_res_msg/do_query_*/query_name/resend_query/proc_answer/get_res/gethost_*/init_resolver/cres_mem). - P6w DONE:
res.cbad_hostname(the pure RESTRICT_HOSTNAMES character validator used by the still-C answer parserproc_answer) — the next bottom-up leaf of the resolver proper, ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_HOSTNAME_P6w; a new three-wayRES_HOSTNAME_LINKAGEseam:externin res_link.o so still-Cproc_answerresolves to the Rust def, GLOBAL in the cref oracle under-DRES_CACHE_EXPOSEforcref_bad_hostname, static otherwise). Utility/callee (not in msgtab — called only fromproc_answer). Faithful to the compiled body (HOSTNAMES_UNDERSCOREoff): the for-increment-on-continuesemantics + theisalnum/s[1]short-circuit ordering. Gate: L1 (res_bad_hostname_diff: a hostname corpus × every prefix length 0..=len+1, asserting identical return; valid names, hyphen/dot edge cases incl. thelen==1trailing-hyphen guard, illegal chars space/_///*/:/control/high-bit, and thelenboundary). No L2/S2S path (unreachable without the still-C answer parser). - P6x DONE:
res.ccres_mem(the DNS-cache memory-usage reporter, called froms_debug.c'scount_memoryviaSTATS z) — the next bottom-up leaf of the resolver proper, ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_CRES_MEM_P6x). Unlike the file-static cache/queue leaves,cres_memis exported (res_ext.h) → the cref oracle already carriescref_cres_mem, so noRES_*_LINKAGE/*_EXPOSEseam was needed — the guard just drops the C body and the Rust def resolves it forcount_memoryat link. Utility/callee (not inmsgtab). Faithful to thenm += i - 1unsigned modular arithmetic (0 aliases →i==0wraps −1, cancelled by the following+= sizeof(char*); reproduced withwrapping_add) and the%d-on-u_longformat quirk. Gates: L1 (res_cres_mem_diff: two-world return-value + RPL_STATSDEBUG sendQ byte diff over empty/0-alias-wrap/multi-alias/null-name/multi-entry caches — the per-entry summation the empty boot cache never exercises) + L2 (golden_debug::stats_memory_match_reference, STATS z, stays green). No S2S (oper STATS only, noIsServer/remote-field path). - P6y DONE:
res.csend_res_msg(the resolver UDP datagram-send leaf — the bottom of the send treedo_query_*/resend_query→query_name→send_res_msg) — ported toircd-common/res.rsviares_link.o(-DPORT_RES_SEND_P6y; a new three-wayRES_SEND_LINKAGEseam de-statics it: extern in res_link.o so the still-Cquery_nameresolves the Rust def at link, GLOBAL in the cref oracle forcref_send_res_msg). Reads the Rustircd_res(P6p) + the s_bsd.cresfdglobal; fansmsg[0..len]tomax=MIN(nscount,rcount)nameservers (clamped to 1 onRES_PRIMARY, floored to 1), bumpingreinfo.re_sentonly on a full-lengthsendto. Utility/callee (not in msgtab; onlyquery_namecalls it). Gate: L1 (res_send_msg_diff: two isolatedWorlds each with own loopback::1UDP recv+send sockets; single-send / NULL-msg / rcount=0 floor / RES_PRIMARY override / two-nameserver / bad-resfd cases diffed on{ret, datagram bytes, re_sent delta, family}). No L2 (DNS path disabled project-wide viaNO_DNS_LOOKUP; boot goldens stay byte-identical), no S2S. - P6z DONE:
res.cquery_name(the DNS query builder/sender — the leaf above the P6ysend_res_msg:do_query_name/do_query_number/resend_query/get_rescall it) — ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_QUERY_P6z; a new three-wayRES_QUERY_LINKAGEseam de-statics it: extern in res_link.o so the still-C callers resolve the Rust def at link, GLOBAL in the cref oracle forcref_query_name). Builds the query into aMAXPACKETbuffer via the Rustircd_res_mkquery(P6o), picks a DNS id that doesn't collide with the outstanding-request queue (do { nstmp = ntohs(hptr->id)+k + (tv.tv_usec & 0xffff); … } while (find_id(...))— LRAND48 off →gettimeofday-seeded), recordsid/sends++/senton theResRQ, and fans the packet viasend_res_msg(P6y);r<=0→h_errno=NO_RECOVERYreturnr, send-1→h_errno=TRY_AGAIN(withsendsalready bumped). Utility/callee (not inmsgtab). Gate: L1 (res_query_name_diff: two isolatedWorlds each with own loopback::1UDP recv+send sockets +ircd_res/reinfo/resfd/firstbase pointers; a strong interposedgettimeofdaypins the id randomization so packets are byte-identical between worlds + thefind_idcollision loop is deterministic — A-record/PTR queries, a forced one-collision id advance vs the no-collision inverse, mkquery-failure NO_RECOVERY no-send, send-failure TRY_AGAIN withsendsstill bumped; diffed on{ret, datagram bytes, rptr.{id,sends,sent}, re_sent delta, h_errno}). No L2 (DNS path disabled project-wide viaNO_DNS_LOOKUP; boot goldensgolden_registration/golden_s_dnsstay byte-identical — the still-C resolver calls the Rustquery_name), no S2S. Still C inres.c: the answer parser + socket I/O (do_query_name/do_query_number/resend_query/proc_answer/get_res/gethost_*/init_resolver). - P6aa DONE:
res.cdo_query_name/do_query_number(the query-dispatch layer) — the next bottom-up cluster of the resolver proper, above the Rustquery_name(P6z) and below the still-Cgethost_*/get_res/resend_querycallers.do_query_namebuilds the forward-lookup hostname (truncate toHOSTLEN, appendircd_res.defdnameon the dotless+RES_DEFNAMESresend path),do_query_numberbuilds the reverse-lookup name (IPv4-mapped →d.d.d.d.in-addr.arpa., else the 32-nibble…ip6.arpa.form); both allocate theResRQviamake_request(P6u) when none was passed, then call the Rustquery_name(P6z). Ported toircd-common/res.rsviares_link.o(-DPORT_RES_DOQUERY_P6aa) + a new three-wayRES_DOQUERY_LINKAGE(extern in res_link.o / GLOBAL in the cref oracle / static otherwise). Utility/callee leaf (not inmsgtab), inert underNO_DNS_LOOKUP→ L1 is the gate (res_do_query_diff, 7 tests, zero diff over a loopback UDP harness withgettimeofdayinterposed; fresh-vs-resend, domain-append vs already-dotted, overlong-defdname-1, all three reverse-name arms, resend no-alloc). No L2/S2S (noIsServer/remote-field path;golden_s_dns+golden_registrationstay green). - P6bb DONE:
res.cgethost_byname_type/gethost_byname/gethost_byaddr(the public host-lookup entry points, the layer above the cache lookup + query dispatch) — ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_GETHOST_P6bb). No linkage seam — they are exportedres_ext.hsymbols, so thecref_*oracle already exists (like P6xcres_mem). Each bumps a lookup counter (re_na_look/re_nu_look), consults the Rust cache (find_cache_name/find_cache_number), and on a miss fires the Rustdo_query_name/do_query_numberwhenlp != NULL(else returns NULL).gethost_byaddr's only caller (s_bsd.c) is gated behindNO_DNS_LOOKUP's#else→ compiled out of the locked binary (ported + L1-verified regardless). Utility/leaf — none inmsgtab→ L1 only. L1 (res_gethost_diff, 8 cases): two isolated cache+query worlds (live vs cref) over the cache globals + resolver-query globals, each with a loopback::1UDP recv socket + pinnedgettimeofday; branch matrix + inverses (bad-type early-out, cache HIT vs MISS,lp==NULLshort-circuit vslp!=NULLAAAA/PTR query issue, byname→T_AAAA delegation,re_na_lookvsre_nu_looksplit). No S2S (resolver is inert underNO_DNS_LOOKUP; no IsServer/remote-field path). - P6cc DONE:
res.cresend_query(the DNS request-retry dispatcher) — ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_RESEND_P6cc); the next bottom-up unit above the P6aa query-dispatch layer (do_query_name/do_query_number) and below the still-Cget_res. Upstream file-static → the P6vRES_RESEND_LINKAGEseam promoted to the standard three-way shape (externin res_link.o, GLOBAL in the cref oracle →cref_resend_queryfor L1, else static). A no-op whenresend==0, else bumpsreinfo.re_resendsand re-dispatches by record type (T_PTR→do_query_number,T_A/T_AAAA→do_query_name, default→no-op). Utility/callee (file-static, not in msgtab; callers = Rusttimeout_query_list+ still-Cget_res). L1 gate (res_resend_query_diff: two isolated cache+query worlds over a loopback UDP harness withgettimeofdaypinned; matrix =resend==0no-op /T_Adispatch /T_PTRdispatch / default-arm counter-only). No L2 (resolver is dead code underNO_DNS_LOOKUP). No S2S. Still C in res.c: onlyproc_answer/get_res/init_resolver. - P6dd DONE:
res.cproc_answer(the DNS answer-section parser — fillsrptr->hefrom a nameserver datagram) — ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_PROC_ANSWER_P6dd; a new three-wayRES_PROC_ANSWER_LINKAGEde-statics the upstream file-static fn for the Rust def + the cref oraclecref_proc_answer). Walks the answer section (skip questions via__ircd_dn_skipname+QFIXEDSZ; per answer expand name + read type/class/ttl/dlen, optionalRES_DEFNAMESdefdname append, wrong-type skip, A/AAAA→v4-mapped addr slot, PTR→validate+find_cache_nameflag fixup, CNAME→alias) using the already-Rustircd_dn_expand/ircd_getshort/ircd_getlong(P6n),bad_hostname(P6w),find_cache_name(P6s),MyMalloc(P2) + the still-C variadicsendto_flag; the res.c-private scratchhostbufmoved into Rust with it. Utility/callee (file-static, sole caller the still-Cget_res); L1 is the gate, no L2 path (resolver dead-code underNO_DNS_LOOKUP), no S2S. L1 (res_proc_answer_diff: 12 cases over two isolated worlds parsing constructed DNS reply buffers — single A/AAAA, multi-A dedup, good vs bad-hostname PTR, CNAME-then-A, wrong-type skip, bad-IP-len -2, RES_DEFNAMES dotless-append vs dotted-untouched, two-question skip, empty answers). Onlyget_res/init_resolver(socket I/O) remain C inres.c. - P6ee DONE:
res.cinit_resolver(the bit-flag resolver bootstrap —RES_INITLIST/RES_CALLINIT/RES_INITSOCK/RES_INITCACH+ bareop==0) — ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_INIT_P6ee; exportedres_ext.hsymbol →cref_init_resolveralready exists, no linkage seam). Under the locked config LRAND48/DEBUG are off so thesrand48line +RES_INITDEBGblock drop out. Still-C I/O callers (s_bsd.crehash /ircd.cboot) resolve to the Rust def at link. Utility/callee (nomsgtabentry; no client path → L1 is the gate). L1 (res_init_resolver_diff: isolated-worlds vscref_per op flag — INITLIST zeroes reinfo+nulls queue, INITCACH zeroes cainfo+hashtable, CALLINIT diffsircd_resmasking the random.idincl. thenscount==0::1-fallback, INITSOCK asserts return==resfd>=0 + AF_INET6 socket, op==0 returns resfd). No S2S. Onlyget_res(recvfrom answer driver) remains C in res.c. - P6ff DONE:
res.cget_res— the LAST C function in res.c (the recvfrom DNS answer driver) — ported toircd-common/res.rsvia theres_link.opartial port (-DPORT_RES_GET_RES_P6ff; exportedres_ext.hsymbol →cref_get_resalready exists, no linkage seam). Reads one nameserver datagram offresfd, ntohs the header,find_ids the request, anti‑spoofs the source againstircd_res.nsaddr_list, branches on rcode (NOERROR/NXDOMAIN/SERVFAIL→TRY_AGAIN, NOERROR+ancount0→NO_DATA, else NO_RECOVERY), parses via the Rustproc_answer, thenmake_caches an A/AAAA result / PTR‑relookups viagethost_byname_type/ retries viaresend_query— all callees already Rust.getres_errfactored to an early‑return helper. Utility/callee (nomsgtabentry; sole caller is the still‑Cs_bsd.cio‑loop, inert underNO_DNS_LOOKUP→ L1 is the gate). L1 (res_get_res_diff: two isolated loopback‑UDP worlds —resfdrecv socket + an::1ns socket that sends the crafted reply and captures follow‑ups — with a fixedgettimeofday; diffed{ret, hostent, reinfo deltas, incache, ca_adds, request‑gone, h_errno, follow‑up bytes}across A‑success/unknown‑id/short‑packet/NXDOMAIN/SERVFAIL/REFUSED/NO_DATA/anti‑spoof/PTR‑relookup).res.cnow has NO C functions left —res_link.odefines only the resolver data globals; the finalres.odrop is the last P6 res step. No S2S. - P6gg DONE:
res.cdata globals → Rust + the finalres.odrop (reinfo/cachetop/cainfo/hashtable/incache/first/last— the resolver's private data globals, the last C remnant after P6ff leftres.cwith no functions) — moved intoircd-common/res.rsas#[no_mangle] pub static mutdefinitions (null_mut()/0/MaybeUninit::zeroed()reproduce C BSS), sores.ois added toPORTEDand dropped outright (no moreres_link.o; the long-DPORT_RES_*second-compile is gone).res.cis now FULLY Rust. The cref oracle keeps the full unguardedres.o(-DRES_CACHE_EXPOSE) so the 19res_*_diffL1 tests still resolvecref_*. Data-global move (no new function) → no new L1 test; gate is the existing res L1 suite (all 19, 130 assertions) still zero-diff with the live-world globals now resolving to the Rust storage, + clean build (0 warnings) +golden_registration/golden_s_dnsbyte-identical. No L2 resolver path (dead-code underNO_DNS_LOOKUP), no S2S. - P6hh DONE:
chkconf.c→ standalonechkconf-rsbinary (the IRCnet config-file checker; the last C in P6) — faithful byte-for-byte port of every path compiled under the locked config (M4_PREPROC/CONFIG_DIRECTIVE_INCLUDE/DEBUGMODE/XLINEOFF):main+arg-parse,new_class/get_class,dgets,getfield(incl. the escape-loop quirk),initconf,validate,confchar,checkSID,config_error(CHKCONF non-CDI branch),mycmp. Standalone crate (noircd-sysdep, likeiauth-rs); the single-file config collapses themywc/files/findConfLineNumbermachinery to the constant per-line location ("ircd.conf", linenr).config_read.cneeds no separate Rust lib (ircd usesconfig_parse.rsfrom P6f;chkconf-rsreimplementsconfig_error). Gate: L1 black-box differential (tests/differential.rs) — builds the reference Cchkconforacle (make -C cbuild chkconf) and diffs stdout+stderr+exit byte-identical over 13 fixtures × {none,-d,-d3,-d9,-s} +-h(argv0-masked). Two C UB paths (non-NUL-terminateddgetsbuffer: ≥511-char line echo under-d, and a config with no trailing newline) are documented and excluded. No L2/S2S (standalone CLI, not amsgtabhandler).