Skip to content

draft/channel-rename: services-arbitrated registered-channel rename - #94

Draft
MrLenin wants to merge 9 commits into
evilnet:masterfrom
MrLenin:backport/channel-rename
Draft

draft/channel-rename: services-arbitrated registered-channel rename#94
MrLenin wants to merge 9 commits into
evilnet:masterfrom
MrLenin:backport/channel-rename

Conversation

@MrLenin

@MrLenin MrLenin commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Adds IRCv3 draft/channel-rename with services arbitration for registered channels. Draft — for review; the end-to-end flow needs a services build (X3) that advertises the new r server flag (see Dependency below), and the feature is behind a default-off feature flag.

What this does

A client with draft/channel-rename sends RENAME <old> <new> :<reason>. For an unregistered channel the server renames in place and relays. For a registered channel (+R), the server asks the services server for permission and applies the rename only on approval:

  • Client RENAME → server emits AC <unum> R <cookie> <#chan> RENAME <new> to the services server.
  • Services replies AC <cookie> A (approve) / AC <cookie> D :<reason> (deny).
  • On approve: rename_channel() re-keys the channel (in-place or realloc, hChangeChannel), members see PART/JOIN + TOPIC/NAMES re-presentation, and RN <old> <new> :<reason> relays to rename-capable peers.

Pieces

  • r (rename-capable) SERVER flag (FLAG_RENAME_CAPABLE): advertised in our SERVER line, parsed in set_server_flags, and — importantly — re-emitted at every flag-synthesis site (relay + burst), since upstream rebuilds flag strings from bitflags rather than relaying them verbatim. A flag that isn't carried at every emit site dies crossing a hub.
  • Dynamic guard: RENAME is refused (honest FAIL) while any non-rename-capable, non-services server is linked — so a legacy server that can neither apply nor relay RN never causes divergence. Auto-relaxes when the last one delinks.
  • RN broadcast to rename-capable peers via the existing sendcmdto_flag_serv_butone.
  • hChangeChannel + rename_channel channel-rename mechanics.
  • FEAT_RENAME_SERVICES (default off) + draft/channel-rename CAP + FEAT_CAP_draft_channel_rename.
  • FAIL/standard-replies: adds a minimal send_fail (source-prefixed, per the standard-replies spec).
  • AC A/D cookie path in ms_account to route the approve/deny reply back to the pending client.
  • Pending-rename table with a 10s timeout, request-time dedup, and a completion-time re-validation (member + chanop + still-+R) that closes the ZANNELS-off destruct-and-recreate window.

Dependency for end-to-end use

RN is delivered only to r-advertising peers. A services package must advertise r (and implement the RN handler + AC R authorization) to participate; without it, services would authorize a rename it never learns completed. The companion X3 work is a separate draft PR.

Notes

Deliberately excludes the fork's bouncer/message-tag/batch machinery — plain CAP, bare RENAME/PART/JOIN presentation. Backported from the evilnet fork's implementation, verified end-to-end on a test network against X3.

🤖 Generated with Claude Code

MrLenin and others added 8 commits July 31, 2026 02:26
…nd burst

Upstream re-synthesizes SERVER-line flags from bitflags at every relay and
burst site rather than echoing parv[7] verbatim (set_server_flags() has no
default case, so unknown letters are silently discarded). This means a
capability flag only survives crossing a hub if it is plumbed into (a) the
parser, (b) a Client flag bit, and (c) every single site that reconstructs
the flag string for a peer. Miss one and the flag silently dies one hop out.

Add FLAG_RENAME_CAPABLE ('r') as the dedicated signal for "this server
applies and relays RENAME" (decision recorded in
nefarious-upstream-rename-backport.md §3 — a dedicated flag rather than
overloading 'v', which would falsely advertise the full fork v3 S2S dialect
to a peer that only understands RENAME).

Plumbing:
- include/client.h: FLAG_RENAME_CAPABLE in enum Flag (grouped with the other
  server-capability flags: HUB/IPV6/SERVICE/OPLEVELS) + IsRenameCapable()/
  SetRenameCapable() macros.
- ircd/m_server.c set_server_flags(): parse 'r'.
- Five emit sites, all updated:
  1. ircd/s_bsd.c (own SERVER line, pre-registration handshake)
  2. ircd/s_serv.c server_estab() (own SERVER line, post-link)
  3. ircd/m_server.c (relaying a remote server's introduction to peers)
  4. ircd/s_serv.c (bursting a newly-linked server's info to peers)
  5. ircd/s_serv.c (bursting already-known servers to the new link)

Sites 1-2 advertise 'r' unconditionally: this branch is atomic for PR
purposes and the RN application/relay machinery lands in the same branch,
so there is no window where we'd claim the flag without honoring it. Sites
3-5 relay/burst IsRenameCapable(<the other server>) conditionally, exactly
mirroring how IsHub/IsService/IsIPv6/IsOpLevels are already handled at each
of those call sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Full RENAME (IRCv3 draft/channel-rename) support ported from the fork's
feature/channel-rename branch (@ 6ad0c49), building on the previous
commit's 'r' (rename-capable) SERVER flag. Ported pieces:

- hChangeChannel() (hash.c/hash.h): rehash a channel under its new name.
- rename_channel() (channel.c/channel.h): in-place or reallocating rename
  preserving members, modes, invites, and any pending destruct event.
- draft/channel-rename client capability (capab.h + m_cap.c), gated by
  new FEAT_CAP_draft_channel_rename (default off, like the fork).
- FEAT_RENAME_SERVICES (default off): gates the AC R services query; only
  a services build that disambiguates RENAME requests from account-stamp
  notifications on the AC 'R' subcommand may enable it.
- send_fail() (send.c/send.h): minimal tag-free IRCv3 standard-replies
  FAIL emitter; wire format matches the fork's untagged branch exactly
  (bare "FAIL <cmd> <code> [<context>] :<description>").
- RENAME/RN message token (msg.h) + parse.c msgtab entry.
- m_rename.c: client handler (cap/membership/chanop checks, legacy-server
  guard, immediate rename for unregistered channels, AC R cookie round-
  trip to services for +R channels with pending-rename table, 10s timeout,
  client-exit cleanup, and ZANNELS-off completion re-validation), server
  handler (apply + relay), RENAME-to-capable-clients with PART/JOIN+TOPIC+
  NAMES fallback for clients without the cap.
- m_account.c AC A/D branch: parc<4 relaxed to parc<3; non-server parv[1]
  is treated as a pending-rename cookie (complete/deny); the LOC reply
  path is untouched otherwise.
- s_misc.c: pending_rename_client_exit() on client exit.
- ircd.c: SetRenameCapable(&me) so GlobalClientList scans (which include
  &me) see this server as capable of what its SERVER lines advertise.

Deliberate simplifications relative to the fork:
- No bouncer subsystem: all alias/primary source rewrites dropped.
- No batch/labeled-response/server-time decoration; send_fail is tag-free.
- The guard and the RN broadcast key on FLAG_RENAME_CAPABLE (the 'r'
  SERVER flag) instead of the fork's IRCv3-aware 'v' flag; the broadcast
  is sendcmdto_flag_serv_butone(..., FLAG_RENAME_CAPABLE, FLAG_LAST_FLAG,
  ...) rather than the fork's sendcmdto_serv_butone_v3.
- No separate legacy-services RN forward: a services package that wants
  RN advertises 'r' itself and is reached by the flag-filtered broadcast;
  non-capable services remain exempt from the guard and get nothing.
- CapOwnHas -> CapActive (upstream's native active-cap check).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
struct PendingRename embeds its own struct Timer, but
pending_rename_timeout_cb() was calling MyFree(pr) from the ET_EXPIRE
branch. timer_run() (ircd_events.c) sets GEN_MARKED on the timer
generator before dispatching ET_EXPIRE and keeps touching that same
memory after the callback returns (clearing GEN_MARKED, then generating
ET_DESTROY, which bumps gh_ref and calls through gh_call) -- all of
which lands on freed memory once ET_EXPIRE has freed the container.
The two timer_del() calls that used to sit alongside the free could
not prevent this: timer_del() called while mid-dispatch just hits its
"timer is being used" early return and does nothing.

Fix: split unlink from free. pending_rename_unlink() removes pr from
the pending_renames list without touching the timer or memory.
ET_EXPIRE now calls only that, plus pr->timer_active = 0, and returns
without freeing -- the struct survives until timer_run's post-callback
bookkeeping is done. The actual MyFree(pr) moved to the ET_DESTROY
case, which fires either automatically right after ET_EXPIRE (the
timeout path) or synchronously out of timer_del() when the timer is
NOT mid-dispatch (the complete/deny/client-exit paths, via the new
pr->timer_active guard in pending_rename_remove() and
pending_rename_client_exit()).

This keeps a single free-once invariant across all five teardown paths
(complete-success, complete-failure, deny, timeout, client-exit): every
one of them ends up unlinking via pending_rename_unlink() and then
either triggering timer_del()'s synchronous ET_DESTROY or leaving that
to timer_run(), with MyFree(pr) happening in exactly one place --
pending_rename_timeout_cb()'s ET_DESTROY case.

Mirrors the established ET_DESTROY-defers-the-free idiom already used
for embedded-timer containers in this tree (s_auth.c's
auth_timeout_callback, bouncer-fork descendants of this same file
family).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
X3's AC rename-permission reply now carries an explicit RENAME
discriminator token after the A/D type ("AC <unum> A RENAME" / "AC
<unum> D RENAME :<reason>"). Restructure the type=='A'||'D' branch in
ms_account() to check for that discriminator FIRST and route straight
to pending_rename_find()-by-cookie, before ever calling FindNServer().

Previously the rename reply and a LOC reply were disambiguated only by
FindNServer(parv[1]) failing to resolve a server numeric — a decimal
rename cookie could alias a live server numeric on the wire and get
silently misrouted or dropped (F2). With the discriminator in place
first, cookie routing no longer depends on that guess, and the
now-redundant no-discriminator cookie fallback is removed; the LOC
path keeps its own explicit "if (!acptr) return 0" for genuine unknown
numerics.

The reason string shifts from parv[3] to parv[4] on deny, since parv[3]
is now the RENAME token itself.

Ported identically from the nefarious fork (feature/channel-rename);
this repo's pre-change m_account.c matched the fork's byte-for-byte in
this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When a channel has oplevels active (+A / MODE_APASS), ownership is
founder-based; a plain chanop opped by the founder must not rename the
channel out from under them.  Gate both the request-time authorization
and the services-completion re-validation on OpLevel(member)==0 when the
channel carries +A.  No effect on channels without oplevels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per draft/channel-rename the capability governs only how a client is
NOTIFIED (RENAME message vs PART/JOIN fallback), not whether it may issue
the command.  Stop returning ERR_UNKNOWNCOMMAND to non-cap senders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Companion to the X3 decision to keep 'z' as the registered-channel wire
letter: without a bridge, no channel ever carries MODE_REGISTERED and
m_rename's services-arbitration gate is unreachable -- a RENAME of a
registered channel silently takes the direct path, bypassing services
and desyncing the registration.

New FEAT_REGISTERED_FROM_PERSIST (default ON): in mode_parse's flush, a
persist-exmode transition also flips MODE_REGISTERED in t_mode.
Internal-only mirror -- modebuf emissions are already built at that
point, so no 'R' is relayed for a mirrored flip; the wire stays
'z'-driven. The 'z' setter gate (burst/services sources only) is the
authority gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MrLenin MrLenin self-assigned this Aug 1, 2026
do_join() blocks BADCHAN-glined channels per joiner, but RENAME moved an
entire membership onto a banned name without ever consulting the gline
table — a wholesale bypass of the ban. Mirror do_join's exact check
(GLINE_BADCHAN | GLINE_EXACT, active, oper-exempt) at request validation,
and re-run it in pending_rename_complete since a badchan can land during
the services round-trip and services arbitrate ownership, not G-line
policy. Deliberately not enforced in ms_rename: local badchans never
blocked remote users' presence (join-time, local policy), so remote-origin
renames are accepted the same way remote joins are.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant