Skip to content

evilnet/channel-relocate: consent-based channel relocation (stacked on #94) - #96

Draft
MrLenin wants to merge 11 commits into
evilnet:masterfrom
MrLenin:backport/channel-relocate
Draft

evilnet/channel-relocate: consent-based channel relocation (stacked on #94)#96
MrLenin wants to merge 11 commits into
evilnet:masterfrom
MrLenin:backport/channel-relocate

Conversation

@MrLenin

@MrLenin MrLenin commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Adds the vendor extension evilnet/channel-relocateconsent-based channel relocation layered on top of draft/channel-rename's force-move. Draft — for review. Behind a default-off feature flag.

Stacked on #94. This branch is #94's backport/channel-rename plus one commit; since GitHub can't base against #94's fork branch, the diff below includes #94's rename commits. The relocation-specific change is the single commit relocate: consent-mode channel relocation — review that commit (or git diff against #94's head) for what's new here.

What this does

draft/channel-rename force-moves everyone to the new name. Relocation mode instead partitions: a rename moves only the people who consented, and the old channel becomes a redirecting tombstone that lets the rest follow at their own pace (or not).

With FEAT_RENAME_CONSENT on, a RENAME <old> <new> :<reason>:

  • Moves only the issuer and +F (auto-follow) users to <new>.
  • Leaves everyone else where they are; <old> becomes a tombstone (+z persist + +L <new> redirect) for FEAT_RELOCATE_GRACE seconds (default 900), so a JOIN <old> during the window forwards to <new> and stayers get a one-time notice that the channel moved.
  • After the grace period the tombstone is swept: stragglers are PARTed and the channel dissolves.

Wire

  • Marker: RN <old> <new> C :<reason> — the extra C parameter (five-parameter shape only) distinguishes a relocation from a classic rename on the S2S relay. A four-parameter RN <old> <new> :C stays a classic rename whose reason is C; a mis-spliced marker (C:reason, no separating space) is detected and warned about but not reinterpreted, since guessing would partition on one server and force-move on another. When the marker is present, relocation semantics apply regardless of the receiving server's local FEAT_RENAME_CONSENT setting.
  • Propagated to rename-capable (r) peers via sendcmdto_flag_serv_butone, exactly like a classic RN.

Pieces

  • FEAT_RENAME_CONSENT (bool, default off) — gates the relocation behavior.
  • FEAT_RELOCATE_GRACE (int, default 900s) — tombstone lifetime.
  • umode +F (FLAG_RELOCATE_FOLLOW) — advance consent to be moved by any relocation; freely user-settable, like +L.
  • CAP evilnet/channel-relocate + FEAT_CAP_evilnet_channel_relocate (advertised whenever the build supports it; the cap alone is harmless — the feature flag gates behavior).
  • RELOCATE=<grace> ISUPPORT token (only when FEAT_RENAME_CONSENT is on).
  • RLO command token — a server→client relocation notice placeholder; there is no client RELOCATE command.
  • Tombstone + snapshot engine in m_rename.c: partition, redirecting tombstone, per-member status snapshot, grace sweep, and early-death cleanup hooked from destruct_channel().
  • Grace-period status preservation: a former op/halfop/voice member (with oplevel) who follows to <new> within the window gets their status restored — snapshot taken at partition, consumed on the follow-JOIN in do_join(), and applied as an ordinary network MODE from the joiner's own server.

Relationship to the fork

This is a straight backport of the fork's feature/channel-relocate (MrLenin/nefarious2) with the fork-only bouncer/alias subsystem stripped and forwarding reconciled to this tree's model — a single sendcmdto_flag_serv_butone on FLAG_RENAME_CAPABLE rather than the fork's v3-broadcast + targeted-forward pair (the same simplification #94 already made for classic RN). No bouncer, CRDT, or v3-aware-vs-rename-capable machinery is carried over.

Dependency for end-to-end use

Same as #94: a services package participates by advertising r and understanding relocation. The reference services side (consent handling, +z/+R correction, tombstone re-arm on burst) lives in the paired evilnet/x3 PR. The full design spec is docs/specs/channel-relocate.md in the testnet repo (gisted separately).


Revision 2026-08-03 — umode +F withdrawn (design "D")

The +F auto-follow umode has been removed (commit e14f607).
The relocation now moves only the issuer (issuing the rename IS consent).
Every other member stays in the tombstone and follows by their own
JOIN <new> — the +L redirect already forwards JOIN <old> during grace, and
the existing per-member snapshot restores their status. The change is one line
in the classifier (user == sptr || IsRelocateFollow(user)user == sptr)
plus deletion of the umode; the member walk, snapshot, tombstone lifecycle, and
RELOCATE/NOTICE notify class are unchanged. Auto-follow becomes a client
behavior (optionally a client-read metadata key; server-side auto-follow is
deferred to a deployment-gated phase). Live-gated on the AfterNET testnet
(8-case E2E suite green ×3). Spec docs/specs/channel-relocate.md updated.

MrLenin and others added 10 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>
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>
Backport of the fork's feature/channel-relocate onto the rename backport,
with the fork-only bouncer/alias subsystem stripped and forwarding
reconciled to this tree's model (a single sendcmdto_flag_serv_butone on
FLAG_RENAME_CAPABLE, not the fork's v3-broadcast + rename_forward_rcapable
pair; the rename backport already made that simplification).

Adds, on top of draft/channel-rename's force-move:

  - RELOCATE mode gated by FEAT_RENAME_CONSENT (default off): a rename
    partitions the channel rather than force-moving it -- only the issuer
    and +F (FLAG_RELOCATE_FOLLOW) users move; everyone else stays and the
    old channel becomes a redirecting tombstone for FEAT_RELOCATE_GRACE
    seconds (default 900).
  - Wire marker: RN <old> <new> C :<reason> (five-parameter shape only)
    distinguishes a relocation from a classic rename on the S2S relay.
  - umode +F auto-follow; cap evilnet/channel-relocate; RELOCATE=<grace>
    ISUPPORT token; RLO command token (server->client notice only).
  - Grace-period status preservation: a former op/halfop/voice member who
    follows to the new channel within the grace window gets their status
    (and oplevel) restored, via a per-member snapshot taken at partition
    time and consumed on the follow-join.
  - Tombstone lifecycle: grace sweep PARTs stragglers, persist/redirect
    modes are cleared, and an early channel death (services DESTRUCT, or
    -z then last part) retires the record via destruct_channel().

See docs/specs/channel-relocate.md in the testnet repo for the spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the withdrawal of the +F auto-follow umode on the upstream
backport. The relocation moves only the issuer (issuing the rename IS
consent); every other member stays in the tombstone and follows by
their own JOIN #new. The per-member snapshot is now the only way a
non-issuer keeps op/voice across a relocation.

A scarce, undiscoverable, server-unique umode letter is the wrong
resource for a rarely-set client-local preference; auto-follow becomes
a client behavior. See docs/specs/channel-relocate.md, design "D".

- m_rename.c: classifier -> `user == sptr`
- client.h: delete FLAG_RELOCATE_FOLLOW + Is/Set/Clear macros
- s_user.c: delete userModeList 'F' + set_user_mode case 'F'

Tombstone/snapshot/RELOCATE cap/verb/ISUPPORT untouched.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant