Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0d78534
proto-p10: GetUserN_silent variant for callers that legitimately probe
MrLenin Jun 6, 2026
5918165
proto-p10: BX P merge case for in-place conversion
MrLenin Jun 6, 2026
61f8e11
hash: add RenameChannel() + channel-rename hook array
MrLenin Jul 31, 2026
b62a9f6
modules: channel-rename hooks re-point chanNode holders
MrLenin Jul 31, 2026
a21f605
proto-p10: disambiguate AC R rename queries from account stamps
MrLenin Jul 31, 2026
eb236a0
proto-p10: handle RN channel rename; chanserv: timed DNR on old name
MrLenin Jul 31, 2026
867c97f
fixup: guard NULL chanserv bot + check RenameChannel result in cmd_re…
MrLenin Jul 31, 2026
cad2a2d
rename: cap new channel name at CHANNELLEN (no length ceiling in IsCh…
MrLenin Jul 31, 2026
719593e
chanserv: mark registered channels +R on the wire (remap MODE_REGISTE…
MrLenin Jul 31, 2026
01a4c4f
hash: parse burst modes as server-origin in wipeout_channel
MrLenin Jul 31, 2026
2b0e89c
chanserv: register rename hook regardless of bot presence
MrLenin Jul 31, 2026
9feff55
proto-p10: transfer memberships to survivor in BX P merge
MrLenin Jul 31, 2026
d213568
proto-p10: advertise r flag; RENAME discriminator on rename reply
MrLenin Jul 31, 2026
d9aa287
proto-p10: registered marker stays 'z' on the wire (parse z+R, emit z)
MrLenin Aug 1, 2026
1d80d7d
chanserv/proto-p10: split z (persist) from R (registered) properly
MrLenin Aug 1, 2026
459b04b
relocate: RN C marker — consent split keeps tombstone node, moves iss…
MrLenin Aug 2, 2026
e797c35
relocate: preserve service-bot status modes across the split; re-arm …
MrLenin Aug 2, 2026
3104409
relocate: stop cmd_burst stripping a live tombstone's persist exmode
MrLenin Aug 2, 2026
42d22ed
docs: document rename_dnr_duration and relocate_grace in x3.conf.example
MrLenin Aug 2, 2026
d7945ca
relocate: drop FLAGS_FOLLOW (+F), issuer-only mover set (design "D")
MrLenin Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
374 changes: 357 additions & 17 deletions src/chanserv.c

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions src/chanserv.h
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,44 @@ struct do_not_register
struct userData *_GetChannelUser(struct chanData *channel, struct handle_info *handle, int override, int allow_suspended);
struct banData *add_channel_ban(struct chanData *channel, const char *mask, char *owner, time_t set, time_t triggered, time_t expires, char *reason);

/* Rename authorization check, used by the AC R RENAME query handler in
* proto-p10.c (cmd_account). Returns 1 = allow; 0 = deny with *reason
* set to a static user-visible string. */
int chanserv_rename_allowed(struct userNode *user, struct chanNode *chan, const char *new_name, const char **reason);

/* Called by the RN handler in proto-p10.c (cmd_rename) after a registered
* channel is renamed, to place a timed do-not-register entry on the old
* name. No-op if chanserv_conf.rename_dnr_duration is 0, or if old_name
* is already covered by an existing (non-expired) DNR. proto-p10.c must
* not reach into chanserv's DNR internals (plain_dnrs/mask_dnrs/etc are
* file-static) — this wrapper is the exported escape hatch. */
void chanserv_rename_dnr(const char *old_name);

/* evilnet/channel-relocate, both called by cmd_rename's consent path in
* proto-p10.c after the channel has been split.
*
* chanserv_relocate_bots() re-asserts ChanServ's (and SpamServ's) ops on the
* new node, once the bots have followed the registration there. No-op for an
* off-channel or suspended registration.
*
* chanserv_relocate_tombstone() arms the X3-side reap of the old node. The
* ircd dissolves its tombstone with local-only PARTs that never reach us, so
* this is the only thing that stops the husk's member list going stale
* forever. old_name/timestamp identify the husk at fire time; a channel
* re-created on that name in the meantime is left alone. */
void chanserv_relocate_bots(struct chanNode *new_chan);
void chanserv_relocate_tombstone(const char *old_name, time_t timestamp);

/* Does this channel NAME currently carry the relocation fingerprint -- a
* live do-not-register stamped with chanserv_rename_dnr()'s own reason?
*
* The DNR tables are file-static in chanserv.c and the reason string is a
* private constant, so callers outside chanserv get this predicate rather
* than the lookup. Used by the burst re-arm hook and by cmd_burst's
* unregistered-channel correction, which must not strip the persist exmode
* off a live tombstone. Returns 1 = looks like a relocation tombstone name. */
int chanserv_is_relocation_dnr(const char *chan_name);

void init_chanserv(const char *nick);
void del_channel_user(struct userData *user, int do_gc);
struct channelList *chanserv_support_channels(void);
Expand Down
148 changes: 147 additions & 1 deletion src/hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,9 @@ wipeout_channel(struct chanNode *cNode, time_t new_time, char **modes, unsigned
strcpy(orig_upass, cNode->upass);
strcpy(orig_apass, cNode->apass);
cNode->modes = 0;
mod_chanmode(NULL, cNode, modes, modec, 0);
/* burst data IS server-origin; without the flag one unknown letter
* aborts the whole parse and strands modes at zero */
mod_chanmode(NULL, cNode, modes, modec, MCP_FROM_SERVER);
cNode->timestamp = new_time;

/* remove our old ban list, replace it with the new one */
Expand Down Expand Up @@ -704,6 +706,28 @@ reg_del_channel_func(del_channel_func_t handler, void *extra)
dcf_list_extra[dcf_used++] = extra;
}

static channel_rename_func_t *crf_list;
static void **crf_list_extra;
static unsigned int crf_size = 0, crf_used = 0;

void
reg_channel_rename_func(channel_rename_func_t handler, void *extra)
{
if (crf_used == crf_size) {
if (crf_size) {
crf_size <<= 1;
crf_list = realloc(crf_list, crf_size*sizeof(crf_list[0]));
crf_list_extra = realloc(crf_list_extra, crf_size*sizeof(void*));
} else {
crf_size = 8;
crf_list = malloc(crf_size*sizeof(crf_list[0]));
crf_list_extra = malloc(crf_size*sizeof(void*));
}
}
crf_list[crf_used] = handler;
crf_list_extra[crf_used++] = extra;
}

static void
DelChannel(struct chanNode *channel)
{
Expand Down Expand Up @@ -739,6 +763,126 @@ DelChannel(struct chanNode *channel)
free(channel);
}

struct chanNode *
RenameChannel(struct chanNode *channel, const char *new_name)
{
struct chanNode *nNode;
unsigned int n;

if (!IsChannelName(new_name) || GetChannel(new_name) || strlen(new_name) > CHANNELLEN)
return NULL; /* IsChannelName has no length cap; an overlong name would enter fixed-buffer sprintf paths downstream */
nNode = calloc(1, sizeof(*nNode) + strlen(new_name));
/* Copy the fixed head wholesale: modes, limit, LOCKS (inherited — chanserv
* registration lock + alert/support locks count on this node), keys,
* timestamp, topic, list headers (heap arrays move ownership), and the
* channel_info pointer. name[] is then overwritten. */
memcpy(nNode, channel, sizeof(*channel));
strcpy(nNode->name, new_name);
for (n = 0; n < nNode->members.used; n++)
nNode->members.list[n]->channel = nNode;
/* Old node's dict key is an interior pointer into its name[] — remove
* while it is still alive. */
dict_remove(channels, channel->name);
dict_insert(channels, nNode->name, nNode);
/* Modules re-point their own holders (design doc §8) with BOTH nodes
* alive: pointer-compare holders need old; name-keyed dicts need
* old->name intact. */
for (n = 0; n < crf_used; n++)
crf_list[n](channel, nNode, crf_list_extra[n]);
free(channel);
return nNode;
}

struct chanNode *
RelocateChannel(struct chanNode *channel, const char *new_name)
{
struct chanNode *nNode;
unsigned int n;

/* Same guards RenameChannel() applies, and for the same reasons. */
if (!IsChannelName(new_name) || GetChannel(new_name) || strlen(new_name) > CHANNELLEN)
return NULL;

/* The new node carries the OLD node's creation timestamp: the ircd's
* relocate_execute() does exactly this (newchan->creationtime =
* chptr->creationtime), and our own irc_join() puts that timestamp on
* the wire when a service bot follows the community over. Create it
* with AddChannel() rather than by hand so the new-channel hooks
* (opserv's join policer / bad-channel check) run for it the way they
* do for any other channel we learn about; they see channel_info == NULL
* and so do nothing registration-shaped this early. */
nNode = AddChannel(new_name, channel->timestamp, NULL, NULL, NULL);
if (!nNode)
return NULL;

/* ---- State transfer ----
* The same set of fields RenameChannel()'s memcpy() carries wholesale,
* copied explicitly because here BOTH nodes have to survive. Deliberately
* excluded: name[] (the dict key -- not re-keyed, that is the whole point)
* and members (the caller partitions those). */
nNode->modes = channel->modes;
nNode->limit = channel->limit;
strcpy(nNode->key, channel->key);
strcpy(nNode->upass, channel->upass);
strcpy(nNode->apass, channel->apass);
strcpy(nNode->topic, channel->topic);
strcpy(nNode->topic_nick, channel->topic_nick);
nNode->topic_time = channel->topic_time;
nNode->join_policer = channel->join_policer;
nNode->join_flooded = channel->join_flooded;
nNode->channel_help = channel->channel_help;

/* Locks MOVE, all of them. Every lock holder (chanserv's registration
* lock, opserv's alert-discrim channels, chanserv's support channels) is
* also a rename-hook registrant that re-points its holder at the new node
* below -- so a lock left behind would pin a husk nobody references, and
* a lock not moved would leave the new node collectable out from under a
* live reference. This is what RenameChannel()'s memcpy() did implicitly. */
nNode->locks = channel->locks;
channel->locks = 0;

/* Ban and exempt lists MOVE (ownership transfer, no deep copy): the
* tombstone is unregistered and about to dissolve, so X3 keeps no
* enforcement state for it. The ircd COPIES its ban lists instead, which
* matters there (the tombstone still enforces bans for its stayers) but
* not here (services enforce through channel_info, which the new node now
* owns). */
for (n = 0; n < channel->banlist.used; n++)
banList_append(&nNode->banlist, channel->banlist.list[n]);
channel->banlist.used = 0;
for (n = 0; n < channel->exemptlist.used; n++)
exemptList_append(&nNode->exemptlist, channel->exemptlist.list[n]);
channel->exemptlist.used = 0;

/* ---- Tombstone the old node ----
* Mirrors ircd relocate_execute() exactly, and nothing here goes on the
* wire: every server ran that same code off the RN marker, so these bits
* are already clear network-wide. Registration, the oplevel credentials
* and the +l limit belong to the community, which is now at the new name;
* the persist marker is what keeps the ircd's tombstone alive across the
* grace period. */
channel->modes &= ~(MODE_REGISTERED | MODE_APASS | MODE_UPASS | MODE_LIMIT);
channel->modes |= MODE_PERSIST;
channel->limit = 0;
channel->apass[0] = '\0';
channel->upass[0] = '\0';

/* ---- Registration follows the community ---- */
nNode->channel_info = channel->channel_info;
channel->channel_info = NULL;

/* Hooks run with the registration ALREADY on the new node (chanserv's
* hook re-points channel_info->channel through it) and with both nodes
* alive -- which is the contract they were written for, RenameChannel()
* having kept the old node alive across them too. Every registrant
* re-points at the REGISTRATION, which is what moved; see the audit in
* the task-5 report. */
for (n = 0; n < crf_used; n++)
crf_list[n](channel, nNode, crf_list_extra[n]);

return nNode;
}

struct modeNode *
AddChannelUser(struct userNode *user, struct chanNode* channel)
{
Expand Down Expand Up @@ -1103,6 +1247,8 @@ hash_cleanup(UNUSED_ARG(void *extra))
free_hook_func_list(&jf_list);
free(dcf_list);
free(dcf_list_extra);
free(crf_list);
free(crf_list_extra);
free(pf_list);
free(pf_list_extra);
free(kf_list);
Expand Down
18 changes: 17 additions & 1 deletion src/hash.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
#define MODE_REGONLY 0x00001000 /* ircu +r */
#define MODE_NOCOLORS 0x00002000 /* +c */
#define MODE_NOCTCPS 0x00004000 /* +C */
#define MODE_REGISTERED 0x00008000 /* Bahamut +r */
#define MODE_REGISTERED 0x00008000 /* server-settable channel registration marker; wire letter +R (nefarious channel.c MODE_REGISTERED) -- NOT Bahamut +r, and NOT the fork's +z persist exmode */
#define MODE_STRIPCOLOR 0x00010000 /* +S Strip mirc color codes */
#define MODE_MODUNREG 0x00020000 /* +M mod unregister */
#define MODE_NONOTICE 0x00040000 /* +N no notices */
Expand All @@ -56,6 +56,10 @@
#define MODE_APASS 0x04000000 /* +A adminpass */
#define MODE_UPASS 0x08000000 /* +U userpass */
#define MODE_ADMINSONLY 0x10000000 /* +a Admins only */
#define MODE_PERSIST 0x20000000 /* +z nefarious persist exmode (server-settable; channel survives while
* empty). ChanServ sets it alongside +R when off_channel>0 -- i.e.
* exactly when no bot presence holds the channel open. NOT the
* registered marker; that is MODE_REGISTERED (+R). */
#define MODE_REMOVE 0x80000000

#define FLAGS_OPER 0x00000001 /* Operator +o */
Expand Down Expand Up @@ -445,6 +449,18 @@ void reg_join_func(join_func_t handler, void *extra);
typedef void (*del_channel_func_t) (struct chanNode *chan, void *extra);
void reg_del_channel_func(del_channel_func_t handler, void *extra);

typedef void (*channel_rename_func_t)(struct chanNode *old_chan,
struct chanNode *new_chan, void *extra);
void reg_channel_rename_func(channel_rename_func_t handler, void *extra);
/* Returns the NEW node, or NULL (bad name / target exists). Old node freed. */
struct chanNode *RenameChannel(struct chanNode *channel, const char *new_name);
/* Consent split (evilnet/channel-relocate): everything RenameChannel() does
* EXCEPT the dict re-key and the membership move -- BOTH nodes survive, the
* old one as an unregistered tombstone husk. Returns the NEW node, or NULL
* (bad name / target exists); on NULL the old node is untouched. The caller
* owns the membership partition afterwards (see cmd_rename in proto-p10.c). */
struct chanNode *RelocateChannel(struct chanNode *channel, const char *new_name);

struct chanNode* AddChannel(const char *name, time_t time_, const char *modes, char *banlist, char *exemptlist);
void LockChannel(struct chanNode *channel);
void UnlockChannel(struct chanNode *channel);
Expand Down
8 changes: 8 additions & 0 deletions src/mod-blacklist.c
Original file line number Diff line number Diff line change
Expand Up @@ -393,13 +393,21 @@ blacklist_cleanup(void)
dict_delete(blacklist_reasons);
}

static void
blacklist_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra))
{
if (conf.debug_channel == old_chan)
conf.debug_channel = new_chan;
}

int
blacklist_init(void)
{
bl_log = log_register_type("blacklist", "file:blacklist.log");
conf_register_reload(blacklist_conf_read);
reg_new_user_func(blacklist_check_user);
reg_exit_func(blacklist_cleanup);
reg_channel_rename_func(blacklist_channel_rename, NULL);
return 1;
}

Expand Down
31 changes: 31 additions & 0 deletions src/mod-helpserv.c
Original file line number Diff line number Diff line change
Expand Up @@ -4915,6 +4915,36 @@ static void helpserv_db_cleanup(UNUSED_ARG(void *extra)) {
fclose(reqlog_f);
}

static void
helpserv_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra))
{
dict_iterator_t it;
struct helpserv_bot *hs;
struct helpserv_botlist *botlist;
unsigned int i;

for (it = dict_first(helpserv_bots_dict); it; it = iter_next(it)) {
hs = iter_data(it);

if (hs->helpchan == old_chan)
hs->helpchan = new_chan;

for (i = 0; i < PGSRC_COUNT; i++)
if (hs->page_targets[i] == old_chan)
hs->page_targets[i] = new_chan;
}

/* helpserv_bots_bychan_dict is keyed on the interior helpchan->name
* pointer; only bots whose helpchan was the renamed channel need the
* dict entry re-keyed (there is at most one entry, shared by every bot
* on that channel). */
botlist = dict_find(helpserv_bots_bychan_dict, old_chan->name, NULL);
if (botlist) {
dict_remove2(helpserv_bots_bychan_dict, old_chan->name, 1);
dict_insert(helpserv_bots_bychan_dict, new_chan->name, botlist);
}
}

int helpserv_init() {
HS_LOG = log_register_type("HelpServ", "file:helpserv.log");
conf_register_reload(helpserv_conf_read);
Expand Down Expand Up @@ -5022,6 +5052,7 @@ int helpserv_init() {
reg_part_func(handle_part, NULL); /* also deals with kick */
reg_nick_change_func(handle_nickchange, NULL);
reg_del_user_func(handle_quit, NULL);
reg_channel_rename_func(helpserv_channel_rename, NULL);

reg_auth_func(handle_nickserv_auth, NULL);
reg_handle_rename_func(handle_nickserv_rename, NULL);
Expand Down
7 changes: 7 additions & 0 deletions src/mod-snoop.c
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,12 @@ snoop_cleanup(UNUSED_ARG(void *extra)) {
unreg_del_user_func(snoop_del_user, NULL);
}

static void
snoop_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) {
if (snoop_cfg.channel == old_chan)
snoop_cfg.channel = new_chan;
}

int
snoop_init(void) {
reg_exit_func(snoop_cleanup, NULL);
Expand All @@ -325,6 +331,7 @@ snoop_init(void) {
reg_channel_mode_func(snoop_channel_mode, NULL);
reg_user_mode_func(snoop_user_mode, NULL);
reg_oper_func(snoop_oper, NULL);
reg_channel_rename_func(snoop_channel_rename, NULL);

return 1;
}
Expand Down
7 changes: 7 additions & 0 deletions src/mod-track.c
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,12 @@ track_cleanup(UNUSED_ARG(void *extra)) {
dict_delete(track_db);
}

static void
track_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) {
if (track_cfg.channel == old_chan)
track_cfg.channel = new_chan;
}

int
track_init(void) {
track_db = dict_new();
Expand All @@ -674,6 +680,7 @@ track_init(void) {
reg_channel_mode_func(track_channel_mode, NULL);
reg_user_mode_func(track_user_mode, NULL);
reg_oper_func(track_oper, NULL);
reg_channel_rename_func(track_channel_rename, NULL);
opserv_define_func("TRACK", cmd_track, 800, 0, 0);
opserv_define_func("DELTRACK", cmd_deltrack, 800, 0, 0);
opserv_define_func("ADDTRACK", cmd_addtrack, 800, 0, 0);
Expand Down
Loading