diff --git a/src/chanserv.c b/src/chanserv.c index 2303a76..69983b0 100644 --- a/src/chanserv.c +++ b/src/chanserv.c @@ -61,8 +61,22 @@ #define KEY_NODELETE_LEVEL "nodelete_level" #define KEY_MAX_USERINFO_LENGTH "max_userinfo_length" #define KEY_GIVEOWNERSHIP_PERIOD "giveownership_timeout" +#define KEY_RENAME_DNR_DURATION "rename_dnr_duration" +#define KEY_RELOCATE_GRACE "relocate_grace" #define KEY_VALID_CHANNEL_REGEX "valid_channel_regex" +/* The reason string chanserv_rename_dnr() stamps on the do-not-register it + * places on a renamed/relocated channel's old name. It is not just a + * message: chanserv_relocate_husk_check() reads it back as the fingerprint + * that identifies a relocation tombstone after an X3 restart. Changing it + * breaks that re-arm for every DNR already in the saxdb. */ +#define RENAME_DNR_REASON "Channel was renamed" + +/* Slack added to relocate_grace before X3 reaps a relocation husk, so a + * config that is a little out of step with the ircd's FEAT_RELOCATE_GRACE + * errs on the late side. See chanserv_relocate_tombstone(). */ +#define RELOCATE_SWEEP_MARGIN 60 + /* ChanServ database */ #define KEY_VERSION_CONTROL "version_control" #define KEY_CHANNELS "channels" @@ -631,6 +645,8 @@ static struct unsigned int greeting_length; unsigned int refresh_period; unsigned int giveownership_period; + unsigned long rename_dnr_duration; + unsigned long relocate_grace; unsigned int max_owned; unsigned int max_chan_users; @@ -1735,12 +1751,14 @@ unregister_channel(struct chanData *channel, const char *reason) timeq_del(0, NULL, channel, TIMEQ_IGNORE_FUNC | TIMEQ_IGNORE_WHEN); - if(off_channel > 0) - { - mod_chanmode_init(&change); - change.modes_clear |= MODE_REGISTERED; - mod_chanmode_announce(chanserv, channel->channel, &change); - } + /* Always clear the ircd's registration marker (+R) on unregistration, + * independent of off_channel (which only governs whether ChanServ + * itself leaves the channel). Clear the persist exmode (+z) too: + * an unregistered channel must be able to die when it empties (the + * ircd schedules the destruct when -z lands on an empty channel). */ + mod_chanmode_init(&change); + change.modes_clear |= MODE_REGISTERED | MODE_PERSIST; + mod_chanmode_announce(chanserv, channel->channel, &change); wipe_adduser_pending(channel->channel, NULL); @@ -2107,6 +2125,146 @@ chanserv_is_dnr(const char *chan_name, struct handle_info *handle) return dnr; } +void +chanserv_rename_dnr(const char *old_name) +{ + const char *setter; + + if(!chanserv_conf.rename_dnr_duration) + return; + if(chanserv_is_dnr(old_name, NULL)) + return; /* already covered by a DNR (plain or mask); don't stack */ + /* chanserv is NULL when the bot's nick is disabled via the "." + * convention (init_chanserv only AddLocalUser()s it when nick != + * NULL) -- but a channel can still be registered (saxdb load doesn't + * care whether the bot exists) and RN can still arrive over the + * wire. Fall back to a literal setter string rather than dereference + * a NULL chanserv; it's a plain string and serializes to saxdb the + * same as any other setter value. */ + setter = chanserv ? chanserv->nick : "ChanServ"; + chanserv_add_dnr(old_name, setter, now + chanserv_conf.rename_dnr_duration, + RENAME_DNR_REASON); +} + +/* A relocation tombstone we are waiting to reap out of X3's channel dict. + * Keyed by NAME plus the creation timestamp captured at relocation time: + * the pointer would dangle (the husk can be collected the moment its last + * member leaves) and the name alone is not an identity -- a brand new + * channel can be created on the old name once the ircd's grace period ends + * and its redirect is gone. Same re-authentication the ircd's own + * relocate_tombstone_sweep() performs before it touches anything. */ +struct relocate_husk { + time_t timestamp; + char name[1]; +}; + +static void +chanserv_relocate_husk_expire(void *data) +{ + struct relocate_husk *husk = data; + struct chanNode *chan = GetChannel(husk->name); + unsigned int n; + + if(chan && !chan->channel_info && chan->timestamp == husk->timestamp) + { + /* Silent removal, no wire traffic, MCP_FROM_SERVER in spirit: the + * ircd already parted every one of these members locally on every + * server when its own grace timer fired, and told nobody -- this is + * X3 catching up with a decision that has already happened, not X3 + * parting anyone. Held across the walk so the last removal cannot + * free the node before we are done with it; the UnlockChannel() is + * then what collects it, through the ordinary empty-channel path. */ + LockChannel(chan); + for(n = chan->members.used; n > 0; ) + DelChannelUser(chan->members.list[--n]->user, chan, NULL, 0); + if(chan->members.used) + log_module(CS_LOG, LOG_WARNING, + "Relocation husk %s still holds %u member(s) after its " + "sweep; leaving the node in place.", + husk->name, chan->members.used); + UnlockChannel(chan); + } + free(husk); +} + +void +chanserv_relocate_tombstone(const char *old_name, time_t timestamp) +{ + struct relocate_husk *husk; + + if(!chanserv_conf.relocate_grace) + return; + + husk = malloc(sizeof(*husk) + strlen(old_name)); + strcpy(husk->name, old_name); + husk->timestamp = timestamp; + + /* relocate_grace mirrors the ircd's FEAT_RELOCATE_GRACE (default 900); + * the extra margin is deliberate slack in the safe direction. Sweeping + * LATE only means X3 carries a stale husk a little longer -- it is + * unregistered, has no bots in it and nothing consults it. Sweeping + * EARLY would blank X3's view of members who are still legitimately + * sitting in the ircd's live tombstone, talking, for the remainder of + * the grace period. */ + timeq_add(now + chanserv_conf.relocate_grace + RELOCATE_SWEEP_MARGIN, + chanserv_relocate_husk_expire, husk); +} + +/* Re-arm the husk sweep for a tombstone we are meeting for the first time -- + * which in practice means one that outlived an X3 restart, since the timer + * armed by cmd_rename dies with the process and the ircd's own dissolve is + * invisible to services (its grace-expiry PARTs are local-only on every + * server, and nothing else is emitted). Runs as a new-channel hook, so it + * sees every channel learned from a BURST. + * + * There is no relocation flag on the wire to test, so this is a heuristic + * over state X3 already tracks. A channel that is + * + * +z (persist) AND unregistered AND DNR'd with the rename reason + * + * is a relocation tombstone with very little room left for anything else: the + * persist bit on a services network is set by ChanServ for its own registered + * channels (off_channel) or by relocate_execute() on a tombstone, and the + * first of those is registered by definition -- so persist WITHOUT a + * registration already excludes the ordinary case. The DNR with our own + * rename reason is then the fingerprint proper: only chanserv_rename_dnr() + * writes it, and only for a name a rename or relocation just vacated. + * + * The bounds, deliberately: a false positive costs a timer that silently + * removes X3-side memberships from an unregistered channel roughly + * relocate_grace after it is learned. It emits nothing on the wire, so it + * can only make X3's view catch up or briefly lag -- never the network's -- + * and the sweep re-authenticates the node by name AND creation timestamp + * first, so it cannot follow a name onto a different channel. Repeated + * bursts of the same husk arm redundant timers; the identity re-check makes + * the later ones no-ops. A network that runs with rename_dnr_duration 0 (no + * DNR) or relocate_grace 0 gets no re-arm at all, which is the same + * pre-existing "husk lives until its last member quits" behaviour. */ +int +chanserv_is_relocation_dnr(const char *chan_name) +{ + struct do_not_register *dnr; + + if(!(dnr = chanserv_is_dnr(chan_name, NULL))) + return 0; + return !strcmp(dnr->reason, RENAME_DNR_REASON); +} + +static void +chanserv_relocate_husk_check(struct chanNode *channel, UNUSED_ARG(void *extra)) +{ + if(channel->channel_info || !(channel->modes & MODE_PERSIST)) + return; + if(!chanserv_is_relocation_dnr(channel->name)) + return; + + log_module(CS_LOG, LOG_INFO, + "Channel %s looks like a relocation tombstone (persist, " + "unregistered, rename DNR); re-arming its husk sweep.", + channel->name); + chanserv_relocate_tombstone(channel->name, channel->timestamp); +} + static unsigned int send_dnrs(struct userNode *user, dict_t dict) { struct do_not_register *dnr; @@ -2618,8 +2776,15 @@ static CHANSERV_FUNC(cmd_register) cData = register_channel(channel, user->handle_info->handle); scan_user_presence(add_channel_user(cData, handle, UL_OWNER, 0, NULL, 0), NULL); cData->modes = chanserv_conf.default_modes; + /* Always announce the ircd's registration marker (+R) on registration; + * off_channel only governs whether ChanServ itself joins/leaves the + * channel below, not whether the ircd learns it's registered. + * With off_channel>0 there is no bot presence to hold the channel + * open, so also set the persist exmode (+z) -- its original intended + * use -- to keep the registered channel alive while empty. */ + cData->modes.modes_set |= MODE_REGISTERED; if(off_channel > 0) - cData->modes.modes_set |= MODE_REGISTERED; + cData->modes.modes_set |= MODE_PERSIST; if (IsOffChannel(cData)) { mod_chanmode_announce(chanserv, channel, &cData->modes); @@ -2744,6 +2909,40 @@ ss_cs_join_channel(struct chanNode *channel, int spamserv_join) mod_chanmode_free(change); } +void +chanserv_relocate_bots(struct chanNode *new_chan) +{ + extern struct userNode *spamserv; + struct chanData *cData = new_chan->channel_info; + struct modeNode *mn_cs, *mn_ss; + + /* Nothing to do when there is no registration to serve, when the channel + * is suspended (the bots stay out of it by design), or when the bot nick + * is disabled via the "." convention. */ + if(!cData || IsSuspended(cData) || !chanserv) + return; + /* cmd_rename's consent path has already walked our local users over to + * the new node with real JOINs, so ChanServ's presence here is the test + * for "was ChanServ in the community at all" -- an off_channel + * registration has no bot to re-op and must not gain one. */ + if(!(mn_cs = GetUserMode(new_chan, chanserv))) + return; + + /* That same follow already re-asserted whatever modes each bot held in + * the tombstone, generically, so in the ordinary case the bots are + * already opped here and this must NOT fire -- a second identical MODE on + * the wire is pure noise. What is left is the repair case: a REGISTERED + * channel whose ChanServ (or SpamServ) was somehow not opped in the old + * channel. There the registration's own invariant outranks mirroring the + * husk, and ss_cs_join_channel() restores it (its AddChannelUser() is + * idempotent -- it returns the existing modeNode). */ + mn_ss = spamserv ? GetUserMode(new_chan, spamserv) : NULL; + if((mn_cs->modes & MODE_CHANOP) && (!mn_ss || (mn_ss->modes & MODE_CHANOP))) + return; + + ss_cs_join_channel(new_chan, mn_ss != NULL); +} + static CHANSERV_FUNC(cmd_move) { struct mod_chanmode change; @@ -2809,16 +3008,19 @@ static CHANSERV_FUNC(cmd_move) else if(!IsSuspended(channel->channel_info)) chanserv_join = 1; + /* Clear the server-managed markers from the old channel, add them to + * the new. Always, not just under off_channel -- the ircd's +R must + * follow registration regardless of whether ChanServ itself occupies + * the channel. The persist exmode (+z) moves too, but is only SET + * when off_channel>0 (bot presence otherwise holds the channel). */ + change.argc = 0; + change.modes_clear = MODE_REGISTERED | MODE_PERSIST; + mod_chanmode_announce(chanserv, channel, &change); + change.modes_clear = 0; + change.modes_set = MODE_REGISTERED; if(off_channel > 0) - { - /* Clear MODE_REGISTERED from old channel, add it to new. */ - change.argc = 0; - change.modes_clear = MODE_REGISTERED; - mod_chanmode_announce(chanserv, channel, &change); - change.modes_clear = 0; - change.modes_set = MODE_REGISTERED; - mod_chanmode_announce(chanserv, target, &change); - } + change.modes_set |= MODE_PERSIST; + mod_chanmode_announce(chanserv, target, &change); /* Move the channel_info to the target channel; it shouldn't be necessary to clear timeq callbacks @@ -8321,6 +8523,90 @@ handle_new_channel(struct chanNode *channel, UNUSED_ARG(void *extra)) SetChannelTopic(channel, chanserv, chanserv, channel->channel_info->topic, 1); } +static void +chanserv_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) +{ + struct adduserPending *ap; + unsigned int ii; + + /* memcpy() moved channel_info onto new_chan, but the chanData's back + * pointer still targets the old node. */ + if (new_chan->channel_info) + new_chan->channel_info->channel = new_chan; + + for (ap = adduser_pendings; ap; ap = ap->next) + if (ap->channel == old_chan) + ap->channel = new_chan; + + for (ii = 0; ii < chanserv_conf.support_channels.used; ++ii) + if (chanserv_conf.support_channels.list[ii] == old_chan) + chanserv_conf.support_channels.list[ii] = new_chan; +} + +/* Rename authorization check for the AC R RENAME query (proto-p10.c + * cmd_account). Owner-only, mirroring cmd_move's DNR gating against the + * NEW name — minus the IsHelping/"force" bypass, since this path has no + * force; staff bypass comes only from _GetChannelUser()'s override=1 + * synthetic access entry. */ +int +chanserv_rename_allowed(struct userNode *user, struct chanNode *chan, const char *new_name, const char **reason) +{ + struct chanData *cData; + struct userData *uData; + struct do_not_register *dnr; + + if(!user->handle_info) + { + *reason = "You must be authenticated"; + return 0; + } + + if(strlen(new_name) > CHANNELLEN) + { + *reason = "New channel name is too long"; + return 0; + } + + if(!(cData = chan->channel_info)) + return 1; /* Nothing registered here to protect. */ + + if(IsProtected(cData) || IsSuspended(cData)) + { + *reason = "Channel may not be renamed"; + return 0; + } + + uData = _GetChannelUser(cData, user->handle_info, 1, 0); + if(!uData || (uData->access < UL_OWNER)) + { + *reason = "You must be the channel owner"; + return 0; + } + + if(opserv_bad_channel(new_name)) + { + *reason = "New channel name is not allowed"; + return 0; + } + + if(GetChannel(new_name) && GetChannel(new_name)->channel_info) + { + *reason = "New channel name is already registered"; + return 0; + } + + for(uData = cData->users; uData; uData = uData->next) + { + if((uData->access == UL_OWNER) && (dnr = chanserv_is_dnr(new_name, uData->handle))) + { + *reason = "New channel name is blocked (do-not-register)"; + return 0; + } + } + + return 1; +} + int trace_check_bans(struct userNode *user, struct chanNode *chan) { @@ -8438,6 +8724,33 @@ handle_join(struct modeNode *mNode, UNUSED_ARG(void *extra)) if(channel->members.used > cData->max) cData->max = channel->members.used; + /* Self-heal the ircd's server-managed markers (+R always; +z persist + * when off_channel>0, since no bot presence then) for a channel we + * know is registered (channel_info set, not suspended -- both already + * checked above) but which the ircd doesn't currently show as such. + * This covers ircd restarts (fresh channel, no memory of +R), X3 + * restarts racing a channel's first post-restart JOIN before the + * BURST/DB-load path re-set it, and channel re-creation. Unlike the + * join-flood/burst guards below (dynamic-limit timer resets, + * automode, greetings) this isn't gated on user->uplink->burst: it + * sends no message to the user and touches no timer, it only + * corrects channel state, and mod_chanmode_announce() applies the + * change to channel->modes immediately, so only the first joiner + * (burst or not) actually triggers the wire MODE -- every later + * handle_join() call in the same burst sees MODE_REGISTERED already + * set and no-ops here. */ + { + unsigned int needed = MODE_REGISTERED + | ((off_channel > 0) ? MODE_PERSIST : 0); + if((channel->modes & needed) != needed) + { + struct mod_chanmode reg_change; + mod_chanmode_init(®_change); + reg_change.modes_set = needed & ~channel->modes; + mod_chanmode_announce(chanserv, channel, ®_change); + } + } + #ifdef notdef /* Check for bans. If they're joining through a ban, one of two * cases applies: @@ -9124,6 +9437,15 @@ chanserv_conf_read(void) chanserv_conf.refresh_period = str ? ParseInterval(str) : 3*60*60; str = database_get_data(conf_node, KEY_GIVEOWNERSHIP_PERIOD, RECDB_QSTRING); chanserv_conf.giveownership_period = str ? ParseInterval(str) : 0; + str = database_get_data(conf_node, KEY_RENAME_DNR_DURATION, RECDB_QSTRING); + chanserv_conf.rename_dnr_duration = str ? ParseInterval(str) : 86400; + /* Must track the ircd's FEAT_RELOCATE_GRACE (same 900s default): it is + * how long the ircd keeps a relocation tombstone alive, and therefore + * how long X3's matching husk is still a truthful view of who is in it. + * Set to 0 to disable the husk sweep entirely (the node then survives + * until its last member quits). */ + str = database_get_data(conf_node, KEY_RELOCATE_GRACE, RECDB_QSTRING); + chanserv_conf.relocate_grace = str ? ParseInterval(str) : 900; str = database_get_data(conf_node, KEY_CTCP_SHORT_BAN_DURATION, RECDB_QSTRING); chanserv_conf.ctcp_short_ban_duration = str ? str : "3m"; str = database_get_data(conf_node, KEY_CTCP_LONG_BAN_DURATION, RECDB_QSTRING); @@ -9617,8 +9939,14 @@ chanserv_channel_read(const char *key, struct record_data *hir) && (modes = mod_chanmode_parse(cNode, argv, argc, MCP_KEY_FREE, 0))) { cData->modes = *modes; + /* Always re-assert +R on DB-load reregistration; see + * unregister_channel()/register path. When this block doesn't run + * at all (channel has no stored KEY_MODES), handle_join()'s + * heal-on-join covers it instead. +z persist rides along when + * off_channel>0 (no bot presence to keep the channel alive). */ + cData->modes.modes_set |= MODE_REGISTERED; if(off_channel > 0) - cData->modes.modes_set |= MODE_REGISTERED; + cData->modes.modes_set |= MODE_PERSIST; if(cData->modes.argc > 1) cData->modes.argc = 1; mod_chanmode_announce(chanserv, cNode, &cData->modes); @@ -10044,6 +10372,18 @@ init_chanserv(const char *nick) reg_auth_func(handle_auth, NULL); } + /* Registered channels load from the DB regardless of whether the + * ChanServ bot nick is enabled ("." convention); an RN arriving over + * the wire must still repoint channel_info->channel via RenameChannel, + * or the DB is left holding a stale pointer into a freed chanNode + * (use-after-free at the next saxdb write). Keep this outside if(nick). + */ + reg_channel_rename_func(chanserv_channel_rename, NULL); + /* Outside if(nick) for the same reason: a relocation tombstone learned + * from a BURST has to be recognised and reaped whether or not the + * ChanServ bot nick exists -- the DNR table it is fingerprinted against + * loads from the DB either way. */ + reg_new_channel_func(chanserv_relocate_husk_check, NULL); reg_handle_rename_func(handle_rename, NULL); reg_unreg_func(handle_unreg, NULL); diff --git a/src/chanserv.h b/src/chanserv.h index 1bd5868..96ee6b7 100644 --- a/src/chanserv.h +++ b/src/chanserv.h @@ -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); diff --git a/src/hash.c b/src/hash.c index 8f6a3df..0f4c786 100644 --- a/src/hash.c +++ b/src/hash.c @@ -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 */ @@ -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) { @@ -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) { @@ -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); diff --git a/src/hash.h b/src/hash.h index 3c36823..3079cf0 100644 --- a/src/hash.h +++ b/src/hash.h @@ -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 */ @@ -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 */ @@ -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); diff --git a/src/mod-blacklist.c b/src/mod-blacklist.c index 39b2d5e..d9aeadc 100644 --- a/src/mod-blacklist.c +++ b/src/mod-blacklist.c @@ -393,6 +393,13 @@ 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) { @@ -400,6 +407,7 @@ blacklist_init(void) 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; } diff --git a/src/mod-helpserv.c b/src/mod-helpserv.c index 23d0e7e..58eb4c4 100644 --- a/src/mod-helpserv.c +++ b/src/mod-helpserv.c @@ -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); @@ -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); diff --git a/src/mod-snoop.c b/src/mod-snoop.c index 4eb3969..d97f141 100644 --- a/src/mod-snoop.c +++ b/src/mod-snoop.c @@ -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); @@ -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; } diff --git a/src/mod-track.c b/src/mod-track.c index a14d8ec..3509568 100644 --- a/src/mod-track.c +++ b/src/mod-track.c @@ -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(); @@ -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); diff --git a/src/opserv.c b/src/opserv.c index 69fff83..3776c8f 100644 --- a/src/opserv.c +++ b/src/opserv.c @@ -2989,6 +2989,34 @@ opserv_channel_delete(struct chanNode *chan, UNUSED_ARG(void *extra)) timeq_del(0, opserv_part_channel, chan, TIMEQ_IGNORE_WHEN); } +static void +opserv_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) +{ + dict_iterator_t it; + unsigned int i; + + if (opserv_conf.debug_channel == old_chan) + opserv_conf.debug_channel = new_chan; + if (opserv_conf.alert_channel == old_chan) + opserv_conf.alert_channel = new_chan; + if (opserv_conf.staff_auth_channel == old_chan) + opserv_conf.staff_auth_channel = new_chan; + + for (it = dict_first(opserv_user_alerts); it; it = iter_next(it)) { + struct opserv_user_alert *alert = iter_data(it); + for (i = 0; i < alert->discrim->channel_count; i++) + if (alert->discrim->channels[i] == old_chan) + alert->discrim->channels[i] = new_chan; + } + + /* Same removal shape as opserv_channel_delete's purge-lock timer, but we + * do NOT re-add: the purge-lock re-evaluates against the renamed node on + * its own schedule. */ + timeq_del(0, opserv_part_channel, old_chan, TIMEQ_IGNORE_WHEN | TIMEQ_IGNORE_FUNC); + + new_chan->bad_channel = opserv_bad_channel(new_chan->name); +} + static void opserv_notice_handler(struct userNode *user, struct userNode *bot, const char *text, UNUSED_ARG(int server_qualified)) { @@ -7565,8 +7593,9 @@ init_opserv(const char *nick) reg_new_user_func(opserv_new_user_check, NULL); reg_nick_change_func(opserv_alert_check_nick, NULL); reg_del_user_func(opserv_user_cleanup, NULL); - reg_new_channel_func(opserv_channel_check, NULL); + reg_new_channel_func(opserv_channel_check, NULL); reg_del_channel_func(opserv_channel_delete, NULL); + reg_channel_rename_func(opserv_channel_rename, NULL); reg_join_func(opserv_join_check, NULL); reg_auth_func(opserv_staff_alert, NULL); reg_auth_func(opserv_alert_check_account, NULL); diff --git a/src/proto-p10.c b/src/proto-p10.c index 44a7cb9..51407f5 100644 --- a/src/proto-p10.c +++ b/src/proto-p10.c @@ -81,6 +81,7 @@ #define CMD_QUIT "QUIT" #define CMD_REHASH "REHASH" #define CMD_REMOVE "REMOVE" +#define CMD_RENAME "RENAME" #define CMD_RESET "RESET" #define CMD_RESTART "RESTART" #define CMD_RPING "RPING" @@ -183,6 +184,7 @@ #define TOK_QUIT "Q" #define TOK_REHASH "REHASH" #define TOK_REMOVE "RM" +#define TOK_RENAME "RN" #define TOK_RESET "RESET" #define TOK_RESTART "RESTART" #define TOK_RPING "RI" @@ -388,8 +390,13 @@ GetServerN(const char *numeric) } } -struct userNode* -GetUserN(const char *numeric) /* using numeric */ +/* Shared internal numeric-to-userNode lookup. Wraps both the noisy + * GetUserN (callers expect success — a miss is bug-worthy) and the + * quiet GetUserN_silent (callers know the target can legitimately be + * absent — e.g., snoop/track on transient targets, BX P probing for + * the alias/primary pair). */ +static struct userNode * +GetUserN_impl(const char *numeric, int quiet) { struct userNode *un; struct server *s; @@ -397,26 +404,46 @@ GetUserN(const char *numeric) /* using numeric */ switch (strlen(numeric)) { default: - log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): numeric too long!", numeric); + if (!quiet) + log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): numeric too long!", numeric); return NULL; case 5: slen = 2; ulen = 3; break; case 4: slen = 1; ulen = 3; break; case 3: slen = 1; ulen = 2; break; case 2: case 1: case 0: - log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): numeric too short!", numeric); + if (!quiet) + log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): numeric too short!", numeric); return NULL; } if (!(s = servers_num[base64toint(numeric, slen)])) { - log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): couldn't find server (len=%d)!", numeric, slen); + if (!quiet) + log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): couldn't find server (len=%d)!", numeric, slen); return NULL; } n = base64toint(numeric+slen, ulen) & s->num_mask; if (!(un = s->users[n])) { - log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s) couldn't find user!", numeric); + if (!quiet) + log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s) couldn't find user!", numeric); } return un; } +struct userNode* +GetUserN(const char *numeric) /* using numeric */ +{ + return GetUserN_impl(numeric, 0); +} + +/* Same as GetUserN but suppresses all warnings on lookup miss. Use + * for callers that legitimately probe numerics which may not exist + * (snoop / track tracking transient PRIVMSG/NOTICE targets, BX P + * lookup probes during alias reconciliation, etc.). */ +struct userNode* +GetUserN_silent(const char *numeric) +{ + return GetUserN_impl(numeric, 1); +} + extern struct userNode *opserv; static void check_ctcp(struct userNode *user, struct userNode *bot, char *text, UNUSED_ARG(int server_qualified)) @@ -504,10 +531,12 @@ irc_server(struct server *srv) inttobase64(extranum, srv->num_mask, (srv->numeric[1] || (srv->num_mask >= 64*64)) ? 3 : 2); if (srv == self) { - putsock(P10_SERVER " %s %d " FMT_TIME_T " " FMT_TIME_T " J10 %s%s +s6o :%s", + /* r = rename-capable: ircd delivers RN only to r-advertising peers + * (upstream); harmless on the fork which routes via IsService */ + putsock(P10_SERVER " %s %d " FMT_TIME_T " " FMT_TIME_T " J10 %s%s +s6or :%s", srv->name, srv->hops+1, srv->boot, srv->link_time, srv->numeric, extranum, srv->description); } else { - putsock("%s " P10_SERVER " %s %d " FMT_TIME_T " " FMT_TIME_T " %c10 %s%s +s6o :%s", + putsock("%s " P10_SERVER " %s %d " FMT_TIME_T " " FMT_TIME_T " %c10 %s%s +s6or :%s", self->numeric, srv->name, srv->hops+1, srv->boot, srv->link_time, (srv->self_burst ? 'J' : 'P'), srv->numeric, extranum, srv->description); } } @@ -1706,7 +1735,33 @@ static CMD_FUNC(cmd_account) return 1; } else if(!strcmp(argv[2],"R")) - call_account_func(user, argv[3]); + { + if(argc >= 7 && !strcmp(argv[5],"RENAME")) + { + /* Rename permission query: AC R <#chan> RENAME . + * Reply shape: cookie FIRST (ircd m_account.c keys pending renames on + * parv[1] not being a server numeric) — deliberately NOT the LOC reply + * shape (AC A ) used above. Never GetUserN() the + * cookie; argv[3] is opaque to us. This disambiguates from the legit + * legacy account stamp "AC R " (argc==4), which + * keeps falling through to call_account_func() below unchanged. + * + * The reply itself carries an explicit RENAME discriminator token + * after the A/D type so ircd m_account.c can route it by cookie + * without a FindNServer() guess — a decimal cookie can otherwise + * alias a server numeric (F2). */ + const char *reason = "Permission denied"; + struct chanNode *chan = GetChannel(argv[4]); + /* user is GetUserN(argv[1]) from the prologue above, which already + * returns early when NULL — the check here is paranoia. */ + if(user && chan && chanserv_rename_allowed(user, chan, argv[6], &reason)) + putsock("%s " P10_ACCOUNT " %s A RENAME", self->numeric, argv[3]); + else + putsock("%s " P10_ACCOUNT " %s D RENAME :%s", self->numeric, argv[3], reason); + return 1; + } + call_account_func(user, argv[3]); /* legacy account stamp — unchanged */ + } else call_account_func(user, argv[2]); /* For backward compatability */ return 1; @@ -1735,22 +1790,113 @@ static CMD_FUNC(cmd_bouncer_transfer) if (argc < 6) return 0; - old_primary = GetUserN(argv[2]); - new_node = GetUserN(argv[3]); + /* Both lookups can legitimately miss — old_primary may have + * already been cleaned up by a prior event, new_node is + * normally absent (the swap path is the common case) but + * may exist for the in-place-conversion / merge case below. + * Use the silent variant so neither probe spams the snoop + * channel. */ + old_primary = GetUserN_silent(argv[2]); + new_node = GetUserN_silent(argv[3]); if (!old_primary) return 1; /* Already gone, nothing to do */ - /* Numeric swap: move old_primary's numeric routing to the new - * server/slot. The userNode keeps its nick, clients dict entry, - * handle_info, channels — only the P10 numeric changes. + /* Two BX P shapes need handling: + * + * 1. **Numeric swap (new_node absent)** — classic promote-or- + * transfer. Move old_primary's numeric routing to the new + * server/slot. The userNode keeps its nick, clients dict + * entry, handle_info, channels — only the P10 numeric changes. + * This is the case the original handler was written for, and + * is what fires when nefarious never bursts an alias to us. * - * Nefarious never bursts aliases as N tokens (they're introduced - * via BX C only), so X3 won't have a node for the alias numeric. - * If new_node somehow exists (shouldn't happen), log and ignore. */ + * 2. **In-place conversion / merge (new_node exists with same + * account)** — modern fork peers emit BX P for the case where + * an N-introduced client (old) is being absorbed into an + * existing primary (new), e.g. burst-ordering caused us to + * receive N for the would-be-alias before its BX C. Both + * nodes exist on X3. We merge: delete old_primary's + * userNode (channels and dict entry cleaned up via DelUser, + * no QUIT broadcast), keep new_node intact as the surviving + * identity. Gate strictly on same-handle to avoid + * accidentally merging unrelated collisions. + * + * 3. **new_node exists but different/no handle** — genuinely + * unexpected. Keep the original "log and ignore" behaviour + * so we don't silently corrupt state across an account + * mismatch. */ if (new_node) { + if (old_primary->handle_info + && old_primary->handle_info == new_node->handle_info) { + /* Merge: old absorbed into new. Before deleting + * old_primary, transfer its channel memberships onto + * new_node. This mirrors nefarious's own both-exist + * swap semantics (upstream m_bouncer_transfer.c:88-113, + * fork bouncer_session.c:7070-7078): the ghost's + * channels must survive on the merged identity. + * + * Ordering is load-bearing: this MUST run before + * DelUser(). DelUser's own cleanup loop + * (DelChannelUser(..., NULL, 0)) lets an unregistered + * channel self-destruct when its last member leaves + * (see DelChannelUser's tail, hash.c). If old_primary + * were deleted first while still holding memberships + * new_node lacks, any unregistered channel where + * old_primary was the only member in X3's view would + * be destroyed here — even though the surviving + * identity (and the rest of the network) is still in + * it. Transferring first means old_primary's channel + * list is already empty by the time DelUser runs, so + * that loop body never executes. + * + * Walk old_primary->channels the same way DelUser's + * loop does: always pop the last element, since + * AddChannelUser/DelChannelUser mutate both the + * channel's member list and the user's channel list + * out from under us. */ + while (old_primary->channels.used > 0) { + struct modeNode *mn = old_primary->channels.list[old_primary->channels.used - 1]; + struct chanNode *chan = mn->channel; + + if (!GetUserMode(chan, new_node)) { + /* AddChannelUser() only fires irc_join() when + * the joining user IsLocal() (i.e. an X3 + * service bot) — new_node here is always a + * network user, so no JOIN hits the wire. It + * unconditionally runs call_join_funcs() + * though, which fires the same on-join hooks + * (chanserv presence/ban/oplevel bookkeeping) + * a real join would. The numeric-swap path + * above never touches channel membership at + * all, so there's no existing both-exist + * precedent that's hook-free; there's no + * lower-level "add to channel, no hooks" + * primitive to reach for instead. Correctness + * of membership state takes priority, so we + * accept the hook firing here as a known + * side effect rather than leaving the ghost's + * channels to be silently dropped. */ + struct modeNode *new_mn = AddChannelUser(new_node, chan); + new_mn->modes = mn->modes; + new_mn->oplevel = mn->oplevel; + } + + /* No announce (reason NULL), same call shape + * DelUser's own loop uses: this is internal + * bookkeeping, not a real part. */ + DelChannelUser(old_primary, chan, NULL, 0); + } + + /* DelUser with announce=0 suppresses both QUIT and KILL + * emission — this is internal cleanup, the network + * isn't supposed to see the alias's identity leave. */ + DelUser(old_primary, NULL, 0, "Bouncer transfer"); + return 1; + } log_module(MAIN_LOG, LOG_WARNING, - "BX P: new_node %s already exists as %s — ignoring promote", + "BX P: new_node %s already exists as %s with mismatched " + "handle — ignoring promote", argv[3], new_node->nick); return 1; } @@ -2032,9 +2178,46 @@ static CMD_FUNC(cmd_burst) cData = cNode->channel_info; if (!cData) { - if (cNode->modes & MODE_REGISTERED) { + /* Channel is not registered with us but carries server-managed + * markers. The two markers no longer have the same truth value, so + * they are decided separately: + * + * -R ALWAYS. MODE_REGISTERED on a channel we hold no channel_info + * for is stale in every scenario there is, relocation included: + * relocate_execute() clears R on the tombstone itself, and our + * own husk has channel_info NULL by construction, so an R that + * arrives on a burst for either is drift to be corrected. + * + * -z ONLY when the name does NOT carry the relocation fingerprint. + * The old premise -- "only ChanServ sets +z, on registered + * channels" -- stopped being true when relocate_execute() began + * marking tombstones EXMODE_PERSIST: a live tombstone is +z AND + * unregistered by construction, and that persist bit is the only + * thing keeping the ircd from collecting the channel the moment + * it empties. Stripping it there would dissolve a tombstone + * early and take the +L redirect and the member status snapshots + * with it, mid-grace, network-wide. Any OTHER unregistered +z + * is still stale and still gets stripped. + * + * KNOWN BOUNDED HOLE: the fingerprint is a live DNR carrying + * chanserv_rename_dnr()'s reason, and DNRs live in saxdb, which is + * written on a save tick (db_backup_frequency) and at clean shutdown. + * A burst arriving after an X3 crash that lost the DNR sees no + * fingerprint and strips a legitimate tombstone's z. Blast radius is + * one early tombstone dissolve -- the relocation itself already + * happened on every server -- which degrades exactly to pre-relocate + * behaviour, and it is the same window that already disarms the husk + * re-arm hook. The clean fix is an explicit ircd-side dissolve / + * tombstone signal on the wire, recorded as deferred future work + * (new wire surface on a frozen RN shape). */ + int strip_r = (cNode->modes & MODE_REGISTERED) ? 1 : 0; + int strip_z = (cNode->modes & MODE_PERSIST) + && !chanserv_is_relocation_dnr(cNode->name); + + if (strip_r || strip_z) { irc_join(opserv, cNode); - irc_mode(opserv, cNode, "-z"); + irc_mode(opserv, cNode, + (strip_r && strip_z) ? "-zR" : (strip_z ? "-z" : "-R")); irc_part(opserv, cNode, ""); } } @@ -2316,6 +2499,261 @@ static CMD_FUNC(cmd_topic) return 1; } +/** Relocation marker, as the parameter immediately before the trailing + * reason (evilnet/channel-relocate): + * + * classic: RN : + * relocation: RN C : + * + * Honoured ONLY in the five-parameter shape (argv[0] is the command token, + * so that is argc > 4), exactly as the ircd's ms_rename() honours it only + * for parc > 4. A four-parameter "RN #a #b :C" is a CLASSIC rename whose + * reason happens to be the letter C: reading a marker there would make X3 + * partition a membership the ircd force-moved, which is permanent state + * divergence. The ircd guarantees the trailing reason parameter is always + * emitted (possibly empty), so the shorter shape never carries a marker. */ +#define RELOCATE_MARKER "C" + +/* Move one member's record from the old node to the new one, preserving + * everything the ircd's add_user_to_channel() preserves for a mover. No + * wire traffic: the ircd already moved this user on every server off the + * RN marker, so an emitted JOIN/PART here would be a second, contradictory + * event. Same transfer-before-delete discipline as the BX P merge above -- + * and here it is load-bearing for a second reason: DelChannelUser() collects + * an unregistered channel that just lost its last member, and the caller + * holds the old node across this loop precisely so that cannot happen + * mid-partition. */ +static void +relocate_move_member(struct modeNode *mn, struct chanNode *newchan) +{ + struct userNode *user = mn->user; + struct chanNode *oldchan = mn->channel; + long modes = mn->modes; + short oplevel = mn->oplevel; + time_t idle_since = mn->idle_since; + struct modeNode *newmn; + + AddChannelUser(user, newchan); + /* Re-look-up rather than trusting AddChannelUser()'s return value: it + * ends in call_join_funcs(), which ignores handler return codes, and + * chanserv's join handler can KickChannelUser() a user who matches a + * stored ban -- freeing the very modeNode we were handed. The member + * state was snapshotted above for the same reason. */ + if((newmn = GetUserMode(newchan, user))) { + newmn->modes = modes; + newmn->oplevel = oplevel; + newmn->idle_since = idle_since; + } + DelChannelUser(user, oldchan, NULL, 0); +} + +/* Re-assert one member's status modes on the new node, for real. Used for + * our own service bots: they follow a relocation with an ordinary JOIN, and + * the ircd does not carry a joining client's modes over -- so without this a + * bot that was opped in the old channel lands unopped in the new one. The + * ircd force-accepts modes from a +k service (m_mode.c:305-306), which is why + * a plain announce is enough and no OPMODE is needed. + * + * Callers MUST have already written the snapshot into the new modeNode: this + * announce ends in mod_chanmode_apply(), which ORs the bits in, so it can + * only ever ADD to X3's view -- it cannot clear a spurious auto-op. */ +static void +relocate_reassert_modes(struct userNode *user, struct chanNode *chan, + struct modeNode *mn, long modes) +{ + struct mod_chanmode *change = mod_chanmode_alloc(1); + + change->argc = 1; + change->args[0].mode = modes; + change->args[0].u.member = mn; + mod_chanmode_announce(user, chan, change); + mod_chanmode_free(change); +} + +/* RN [C] : -- the ircd broadcasts this after an approved + * rename has already executed. Authorization happened at AC R query time + * (chanserv_rename_allowed, see cmd_account); this handler only migrates + * state (design §3a: authorize-at-query, apply-at-RN). + * + * Classic path: the channel is re-keyed in place, everyone comes along. + * + * Consent path (the C marker): the channel SPLITS. Registration, channel + * state and the module holders move to the new name; the old node survives + * as an unregistered tombstone husk holding the members who did not consent. + * Movers are the ircd's mover set and nothing else -- the RN source user + * (issuing the rename is consent) plus every user with umode +F -- because + * X3's membership view has to match what relocate_execute() did on every + * server, member for member. X3's own service bots are a separate class: + * they follow the REGISTRATION, wire-visibly, on their own JOIN/PART (see + * below), which is the spec's ordinary consent primitive rather than a + * silent move. + * + * The source prefix is a nick here (parse_line resolves the numeric before + * dispatch) and may resolve to nothing at all -- a server-sourced RN has no + * issuer, which is exactly how the ircd treats it too. */ +static CMD_FUNC(cmd_rename) +{ + struct chanNode *chan; + char old_name[CHANNELLEN+1]; + time_t old_timestamp; + int was_registered; + int relocate; + + if(argc < 3) return 0; + relocate = (argc > 4) && !strcmp(argv[3], RELOCATE_MARKER); + if(!(chan = GetChannel(argv[1]))) return 1; /* never knew it; nothing to move */ + if(GetChannel(argv[2])) { + log_module(MAIN_LOG, LOG_ERROR, + "RENAME %s -> %s: target already exists, state diverged", + argv[1], argv[2]); + return 1; + } + was_registered = chan->channel_info != NULL; + safestrncpy(old_name, argv[1], sizeof(old_name)); + old_timestamp = chan->timestamp; + + if(!relocate) { + if(!RenameChannel(chan, argv[2])) { + /* RenameChannel rejected it (e.g. !IsChannelName(new_name)) and + * left the old node untouched/unfreed -- nothing was actually + * renamed, so don't mark the (still current) old name do-not- + * register. */ + log_module(MAIN_LOG, LOG_ERROR, + "RENAME %s -> %s: rejected by RenameChannel", + argv[1], argv[2]); + return 1; + } + } else { + struct userNode *issuer = origin ? GetUserH(origin) : NULL; + struct chanNode *newchan; + struct userNode **movers; + unsigned int nmovers = 0, nmoved = 0, nstayers, n; + char reason[MAXLEN]; + + if(!(newchan = RelocateChannel(chan, argv[2]))) { + log_module(MAIN_LOG, LOG_ERROR, + "RELOCATE %s -> %s: rejected by RelocateChannel", + argv[1], argv[2]); + return 1; + } + + /* Hold the husk across the partition. RelocateChannel() moved every + * lock to the new node, so without this the first DelChannelUser() + * that empties the (now unregistered) old node would free it under + * the loop. The matching UnlockChannel() at the tail is what lets an + * emptied husk be collected -- normally, and by the normal path. */ + LockChannel(chan); + + snprintf(reason, sizeof(reason), "Channel relocated to %s.", newchan->name); + + /* Classify first, move second -- the same two-pass split the ircd's + * relocate_execute() makes, for the same reason: pass two runs + * AddChannelUser()/DelChannelUser(), which fire join and part hooks + * into every service, and a hook is free to mutate the very member + * list a single fused walk would still be indexing. Pass one only + * reads it, so it sees a stable list; pass two re-resolves each + * candidate through GetUserMode() and skips anyone a hook has since + * taken out. + * + * The candidate set is the ircd's mover set exactly -- the RN source + * user (issuing the rename is consent) -- plus our own local service + * bots, which are not a mover class at all but a separate + * wire-visible follow (see pass two). Every other member stays in + * the tombstone and follows by their own JOIN (design "D"). */ + movers = malloc(sizeof(*movers) * (chan->members.used + 1)); + for(n = 0; n < chan->members.used; n++) { + struct userNode *user = chan->members.list[n]->user; + + if(IsLocal(user) || user == issuer) + movers[nmovers++] = user; + } + + for(n = 0; n < nmovers; n++) { + struct userNode *user = movers[n]; + struct modeNode *mn = GetUserMode(chan, user); + + if(!mn) + continue; /* a hook fired by an earlier iteration removed them */ + + if(IsLocal(user)) { + /* Our own service bots. The rename hooks just re-pointed + * every module holder (chanserv's channel_info and support + * channels, opserv's alert/debug channels, helpserv's + * helpchan, spamserv's chanInfo) at the new node, so a bot + * left sitting in the husk would be a bot whose own service + * believes it is somewhere else. They follow, and unlike the + * movers they do it ON THE WIRE: AddChannelUser()/ + * DelChannelUser() emit a real JOIN and PART for a local + * user, which is what keeps the ircd's view (where nothing + * moved a service bot, because a bot is neither the issuer + * nor +F) in agreement with ours. The JOIN carries the new + * node's timestamp, which RelocateChannel() took from the old + * channel and therefore matches the creationtime the ircd + * gave the new channel. + * + * Status modes do NOT ride along with a JOIN, so they are + * snapshotted here and re-asserted below -- for EVERY bot, + * not just ChanServ and SpamServ: HelpServ, OpServ and module + * bots hold ops in their own channels too and would otherwise + * arrive powerless. The overwrite is separately load-bearing + * on the UNREGISTERED path: AddChannelUser() hands + * MODE_CHANOP to the first member of a channel that is + * neither +R nor +A, which the ircd did not do -- writing the + * snapshot back (rather than OR-ing) is what keeps that + * phantom op out of X3's view. Same discipline as + * relocate_move_member(). */ + long botmodes = mn->modes & (MODE_CHANOP | MODE_HALFOP | MODE_VOICE); + short botoplevel = mn->oplevel; + struct modeNode *botmn; + + AddChannelUser(user, newchan); + if((botmn = GetUserMode(newchan, user))) { + botmn->modes = botmodes; + botmn->oplevel = botoplevel; + if(botmodes) + relocate_reassert_modes(user, newchan, botmn, botmodes); + } + DelChannelUser(user, chan, reason, 0); + nmoved++; + continue; + } + + relocate_move_member(mn, newchan); + nmoved++; + } + free(movers); + nstayers = chan->members.used; + + /* Re-assert ChanServ's (and SpamServ's) ops on the new node: the bots + * followed with a plain JOIN above, and the ircd does not op a + * joining service. No-op when the registration is off-channel or + * suspended -- i.e. when no bot followed. */ + chanserv_relocate_bots(newchan); + + /* X3-side tombstone expiry. The ircd dissolves its tombstone at + * grace expiry with local-only PARTs (sendcmdto_channel_butserv_*), + * so NONE of them reach us: without this timer the husk would sit in + * X3's channel dict with a stale member list until every one of those + * users happened to quit, and a fresh channel created on the old name + * after the grace period would collide with it. */ + chanserv_relocate_tombstone(old_name, old_timestamp); + + /* The one line an operator reading logs after a relocation wants: + * who moved, who did not, and whether the registration went with + * them. A partition is not an error, so it is not logged as one. */ + log_module(MAIN_LOG, LOG_INFO, + "RELOCATE %s -> %s: %u moved, %u left in the tombstone%s", + old_name, newchan->name, nmoved, nstayers, + was_registered ? " (registration followed)" : ""); + + UnlockChannel(chan); /* may collect an already-empty husk; chan is dead after this */ + } + + if(was_registered) + chanserv_rename_dnr(old_name); + return 1; +} + static CMD_FUNC(cmd_num_topic) { struct chanNode *cn; @@ -2794,6 +3232,8 @@ init_parse(void) dict_insert(irc_func_dict, TOK_ERROR, cmd_error); dict_insert(irc_func_dict, CMD_TOPIC, cmd_topic); dict_insert(irc_func_dict, TOK_TOPIC, cmd_topic); + dict_insert(irc_func_dict, CMD_RENAME, cmd_rename); + dict_insert(irc_func_dict, TOK_RENAME, cmd_rename); dict_insert(irc_func_dict, CMD_AWAY, cmd_away); dict_insert(irc_func_dict, TOK_AWAY, cmd_away); dict_insert(irc_func_dict, CMD_SILENCE, cmd_silence); @@ -3617,7 +4057,13 @@ mod_chanmode_parse(struct chanNode *channel, char **modes, unsigned int argc, un case 'a': do_chan_mode(MODE_ADMINSONLY); break; case 'Z': do_chan_mode(MODE_SSLONLY); break; case 'L': do_chan_mode(MODE_HIDEMODE); break; - case 'z': + case 'R': + /* MODE_REGISTERED wire letter (nefarious channel.c MODE_REGISTERED, + * server-origin-only). MCP_REGISTERED is passed by the ChanServ + * MODE-lock/mode-command call sites to forbid a user-typed mode + * string from toggling registration state; server-origin calls + * (cmd_mode, AddChannel/BURST via MCP_FROM_SERVER) don't set it, + * so the ircd is free to correct our copy. */ if (!(flags & MCP_REGISTERED)) { do_chan_mode(MODE_REGISTERED); } else { @@ -3625,6 +4071,21 @@ mod_chanmode_parse(struct chanNode *channel, char **modes, unsigned int argc, un return NULL; } break; + case 'z': + /* Nefarious persist exmode: server-settable-only channel + * keep-alive, tracked as MODE_PERSIST -- NOT the registered + * marker (historical X3 misread it as MODE_REGISTERED). + * Same guard as 'R': user-typed mode strings (MCP_REGISTERED + * call sites, i.e. ChanServ modelock/mode-command) may not + * toggle a server-managed marker; server-origin strings parse + * it so our copy tracks the ircd. */ + if (!(flags & MCP_REGISTERED)) { + do_chan_mode(MODE_PERSIST); + } else { + mod_chanmode_free(change); + return NULL; + } + break; #undef do_chan_mode case 'l': if (add) { @@ -3850,7 +4311,8 @@ mod_chanmode_announce(struct userNode *who, struct chanNode *channel, struct mod DO_MODE_CHAR(NOAMSG, 'T'); DO_MODE_CHAR(OPERSONLY, 'O'); DO_MODE_CHAR(ADMINSONLY, 'a'); - DO_MODE_CHAR(REGISTERED, 'z'); + DO_MODE_CHAR(REGISTERED, 'R'); + DO_MODE_CHAR(PERSIST, 'z'); DO_MODE_CHAR(SSLONLY, 'Z'); DO_MODE_CHAR(HIDEMODE, 'L'); #undef DO_MODE_CHAR @@ -3907,7 +4369,8 @@ mod_chanmode_announce(struct userNode *who, struct chanNode *channel, struct mod DO_MODE_CHAR(NOAMSG, 'T'); DO_MODE_CHAR(OPERSONLY, 'O'); DO_MODE_CHAR(ADMINSONLY, 'a'); - DO_MODE_CHAR(REGISTERED, 'z'); + DO_MODE_CHAR(REGISTERED, 'R'); + DO_MODE_CHAR(PERSIST, 'z'); DO_MODE_CHAR(SSLONLY, 'Z'); DO_MODE_CHAR(HIDEMODE, 'L'); #undef DO_MODE_CHAR @@ -3983,7 +4446,8 @@ mod_chanmode_format(struct mod_chanmode *change, char *outbuff) DO_MODE_CHAR(NOAMSG, 'T'); DO_MODE_CHAR(OPERSONLY, 'O'); DO_MODE_CHAR(ADMINSONLY, 'a'); - DO_MODE_CHAR(REGISTERED, 'z'); + DO_MODE_CHAR(REGISTERED, 'R'); + DO_MODE_CHAR(PERSIST, 'z'); DO_MODE_CHAR(SSLONLY, 'Z'); DO_MODE_CHAR(HIDEMODE, 'L'); #undef DO_MODE_CHAR @@ -4008,7 +4472,8 @@ mod_chanmode_format(struct mod_chanmode *change, char *outbuff) DO_MODE_CHAR(NOAMSG, 'T'); DO_MODE_CHAR(OPERSONLY, 'O'); DO_MODE_CHAR(ADMINSONLY, 'a'); - DO_MODE_CHAR(REGISTERED, 'z'); + DO_MODE_CHAR(REGISTERED, 'R'); + DO_MODE_CHAR(PERSIST, 'z'); DO_MODE_CHAR(SSLONLY, 'Z'); DO_MODE_CHAR(HIDEMODE, 'L'); @@ -4074,7 +4539,8 @@ clear_chanmode(struct chanNode *channel, const char *modes) case 'T': cleared |= MODE_NOAMSG; break; case 'O': cleared |= MODE_OPERSONLY; break; case 'a': cleared |= MODE_ADMINSONLY; break; - case 'z': cleared |= MODE_REGISTERED; break; + case 'R': cleared |= MODE_REGISTERED; break; + case 'z': cleared |= MODE_PERSIST; break; case 'Z': cleared |= MODE_SSLONLY; break; case 'L': cleared |= MODE_HIDEMODE; break; } diff --git a/src/proto.h b/src/proto.h index 75a5e24..cf27204 100644 --- a/src/proto.h +++ b/src/proto.h @@ -84,6 +84,7 @@ struct cManagerNode #ifdef WITH_PROTOCOL_P10 struct server* GetServerN(const char *numeric); struct userNode* GetUserN(const char *numeric); +struct userNode* GetUserN_silent(const char *numeric); #endif /* Basic protocol parsing support. */ diff --git a/src/spamserv.c b/src/spamserv.c index eeff76c..af34164 100644 --- a/src/spamserv.c +++ b/src/spamserv.c @@ -398,6 +398,43 @@ spamserv_cs_move_merge(struct userNode *user, struct chanNode *channel, struct c return 0; } +static void +spamserv_channel_rename(struct chanNode *old_chan, struct chanNode *new_chan, UNUSED_ARG(void *extra)) +{ + struct chanInfo *cInfo = get_chanInfo(old_chan->name); + dict_iterator_t it; + struct userInfo *uInfo; + struct spamNode *sNode; + struct floodNode *fNode; + + if(cInfo) + { + cInfo->channel = new_chan; + + dict_remove(registered_channels_dict, old_chan->name); + dict_insert(registered_channels_dict, strdup(new_chan->name), cInfo); + } + + for(it = dict_first(connected_users_dict); it; it = iter_next(it)) + { + uInfo = iter_data(it); + if(!uInfo) + continue; + + for(sNode = uInfo->spam; sNode; sNode = sNode->next) + if(sNode->channel == old_chan) + sNode->channel = new_chan; + + for(fNode = uInfo->flood; fNode; fNode = fNode->next) + if(fNode->channel == old_chan) + fNode->channel = new_chan; + + for(fNode = uInfo->joinflood; fNode; fNode = fNode->next) + if(fNode->channel == old_chan) + fNode->channel = new_chan; + } +} + void spamserv_cs_unregister(struct userNode *user, struct chanNode *channel, enum cs_unreg type, char *reason) { @@ -3249,6 +3286,7 @@ init_spamserv(const char *nick) reg_nick_change_func(spamserv_nick_change_func, NULL); reg_join_func(spamserv_user_join, NULL); reg_part_func(spamserv_user_part, NULL); + reg_channel_rename_func(spamserv_channel_rename, NULL); timeq_add(now + FLOOD_TIMEQ_FREQ, timeq_flood, NULL); timeq_add(now + JOINFLOOD_TIMEQ_FREQ, timeq_joinflood, NULL); diff --git a/x3.conf.example b/x3.conf.example index 35893be..7f199ec 100644 --- a/x3.conf.example +++ b/x3.conf.example @@ -488,6 +488,20 @@ // How often to look for dnrs that have expired? "dnr_expire_freq" "1h"; + // How long a channel's OLD name stays do-not-register after a + // RENAME/relocation, so nobody can re-register the vacated name + // out from under the move. Set to 0 to disable. + "rename_dnr_duration" "1d"; + + // How long X3 keeps a relocation tombstone's member view before + // sweeping its husk. (seconds) + // MUST be kept in step with the ircd's RELOCATE_GRACE feature: a + // value lower than the ircd's dissolves X3's view of the tombstone + // early, while the ircd still considers it live; a value higher + // just delays X3's own cleanup. Default matches the ircd's default + // of 900. + "relocate_grace" "900"; + // what !set options should we show when user calls "!set" with no arguments? "set_shows" ("DefaultTopic", "TopicMask", "Greeting", "UserGreeting", "Modes", "PubCmd", "InviteMe", "UserInfo", "EnfOps", "EnfModes", "EnfTopic", "TopicSnarf", "Setters", "CtcpReaction", "BanTimeout", "Protect", "Toys", "DynLimit", "NoDelete");