From 1df465a6a53c161d7ae291254d2672824f35574e Mon Sep 17 00:00:00 2001 From: Bram Matthys Date: Mon, 30 Mar 2026 14:40:16 +0200 Subject: [PATCH] Add +f subtype 'p' (for 'paste'). So [2p]:15 means max 2 pastes per 15s. This way you can limit the number of pastes going on in a channel, as this is from everyone in that channel (like 'm') not individual (like 't'). If it is exceeded then we will simply reject the BATCH, similar to how action d(rop) works for some other subtypes. You won't see the paste on the channel, only the sending user receives an error (MULTILINE_PASTE_LIMIT). Small note: a multiline BATCH of just 2 lines is not considered a paste. We consider a multiline of 3+ lines as a paste. I think that is reasonable, since a two-line-multiline is not that much of a paste ;). In the default anti-flood profile (+F normal) we also set 2p per 15s, so this means channels are by default limited to 2 pastes per 15s max. Of course, you can override this with +f [4p]:15 or whatever you like. In terms of +F profiles, the defaults are (maximum x pastes per 15 seconds): very-strict: 1p strict: 1p normal: 2p relaxed: 2p very-relaxed: 3p --- doc/conf/help/help.conf | 2 + include/h.h | 2 + include/modules.h | 1 + src/api-efunctions.c | 2 + src/misc.c | 5 ++ src/modules/chanmodes/floodprot.c | 81 ++++++++++++++++++++----------- src/modules/multiline.c | 7 +++ 7 files changed, 73 insertions(+), 27 deletions(-) diff --git a/doc/conf/help/help.conf b/doc/conf/help/help.conf index 745d75abe..e0553063c 100644 --- a/doc/conf/help/help.conf +++ b/doc/conf/help/help.conf @@ -403,11 +403,13 @@ help Chmodef { " k Knock +K"; " m Messages +m M"; " n Nickchange +N"; + " p Paste drop m, M"; " t Text kick b, d"; " r Repeat kick d, b"; " -"; " The difference between type m and t is that m is tallied for the entire"; " channel whereas t is tallied per user."; + " Type p counts multiline paste events (3+ lines) for the entire channel."; " If you choose to specify an action for a mode, you may also specify a"; " time (in minutes) after which the specific action will be reversed."; " See also https://www.unrealircd.org/docs/Channel_anti-flood_settings#Channel_mode_f"; diff --git a/include/h.h b/include/h.h index 5f1a2d0da..f6df191ca 100644 --- a/include/h.h +++ b/include/h.h @@ -951,6 +951,7 @@ extern MODVAR void (*send_isupport)(Client *client); extern MODVAR void (*isupport_check_for_changes)(void); extern MODVAR int (*get_connections_from_ip)(Client *client); extern MODVAR int (*get_floodprot_channel_max_lines)(Channel *channel); +extern MODVAR int (*floodprot_check_multiline_batch)(Channel *channel, Client *client, int line_count); /* /Efuncs */ /* TLS functions */ @@ -991,6 +992,7 @@ extern int del_silence_default_handler(Client *client, const char *mask); extern int is_silenced_default_handler(Client *client, Client *acptr); extern int get_connections_from_ip_default_handler(Client *client); extern int get_floodprot_channel_max_lines_default_handler(Channel *channel); +extern int floodprot_check_multiline_batch_default_handler(Channel *channel, Client *client, int line_count); extern void do_unreal_log_remote_deliver_default_handler(LogLevel loglevel, const char *subsystem, const char *event_id, MultiLine *msg, const char *json_serialized); extern int make_oper_default_handler(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost, const char *autojoin_channels); extern void webserver_send_response_default_handler(Client *client, int status, char *msg); diff --git a/include/modules.h b/include/modules.h index bf5c0d67b..12066935a 100644 --- a/include/modules.h +++ b/include/modules.h @@ -2806,6 +2806,7 @@ enum EfunctionType { EFUNC_ISUPPORT_CHECK_FOR_CHANGES, EFUNC_GET_CONNECTIONS_FROM_IP, EFUNC_GET_FLOODPROT_CHANNEL_MAX_LINES, + EFUNC_FLOODPROT_CHECK_MULTILINE_BATCH, }; /* Module flags */ diff --git a/src/api-efunctions.c b/src/api-efunctions.c index 17ac0d42f..e85d00bd2 100644 --- a/src/api-efunctions.c +++ b/src/api-efunctions.c @@ -193,6 +193,7 @@ void (*send_isupport)(Client *client); void (*isupport_check_for_changes)(void); int (*get_connections_from_ip)(Client *client); int (*get_floodprot_channel_max_lines)(Channel *channel); +int (*floodprot_check_multiline_batch)(Channel *channel, Client *client, int line_count); Efunction *EfunctionAddMain(Module *module, EfunctionType eftype, int (*func)(), void (*vfunc)(), void *(*pvfunc)(), char *(*stringfunc)(), const char *(*conststringfunc)()) { @@ -537,4 +538,5 @@ void efunctions_init(void) efunc_init_function(EFUNC_ISUPPORT_CHECK_FOR_CHANGES, isupport_check_for_changes, NULL, 0); efunc_init_function(EFUNC_GET_CONNECTIONS_FROM_IP, get_connections_from_ip, get_connections_from_ip_default_handler, 0); efunc_init_function(EFUNC_GET_FLOODPROT_CHANNEL_MAX_LINES, get_floodprot_channel_max_lines, get_floodprot_channel_max_lines_default_handler, 0); + efunc_init_function(EFUNC_FLOODPROT_CHECK_MULTILINE_BATCH, floodprot_check_multiline_batch, floodprot_check_multiline_batch_default_handler, 0); } diff --git a/src/misc.c b/src/misc.c index 1f180dbb3..c67ab0bc7 100644 --- a/src/misc.c +++ b/src/misc.c @@ -1474,6 +1474,11 @@ int get_floodprot_channel_max_lines_default_handler(Channel *channel) return INT_MAX; } +int floodprot_check_multiline_batch_default_handler(Channel *channel, Client *client, int line_count) +{ + return 0; +} + int make_oper_default_handler(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost, const char *autojoin_channels) diff --git a/src/modules/chanmodes/floodprot.c b/src/modules/chanmodes/floodprot.c index 5824cc4ea..d776f18be 100644 --- a/src/modules/chanmodes/floodprot.c +++ b/src/modules/chanmodes/floodprot.c @@ -36,8 +36,9 @@ typedef enum Flood { CHFLD_NICK = 4, CHFLD_TEXT = 5, CHFLD_REPEAT = 6, + CHFLD_PASTE = 7, } Flood; -#define NUMFLD 7 /* 7 flood types */ +#define NUMFLD 8 /* 8 flood types */ /** Configuration settings */ struct { @@ -69,6 +70,7 @@ FloodType floodtypes[] = { { 'k', CHFLD_KNOCK, "knockflood", 'K', "", NULL, 0, }, { 'm', CHFLD_MSG, "msg/noticeflood", 'm', "M", "~quiet:~security-group:unknown-users", 0, }, { 'n', CHFLD_NICK, "nickflood", 'N', "", "~nickchange:~security-group:unknown-users", 0, }, + { 'p', CHFLD_PASTE, "pasteflood", '\0', "mM", "~quiet:~security-group:unknown-users", 0, }, { 't', CHFLD_TEXT, "msg/noticeflood", '\0', "bd", NULL, 1, }, { 'r', CHFLD_REPEAT, "repeating", '\0', "bd", NULL, 1, }, }; @@ -182,12 +184,15 @@ int floodprot_server_quit(Client *client, MessageTag *mtags); void inherit_settings(ChannelFloodProtection *from, ChannelFloodProtection *to); void reapply_profiles(void); int _get_floodprot_channel_max_lines(Channel *channel); +int _floodprot_check_multiline_batch(Channel *channel, Client *client, int line_count); MOD_TEST() { + MARK_AS_OFFICIAL_MODULE(modinfo); HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, floodprot_config_test_set_block); HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, floodprot_config_test_antiflood_block); EfunctionAdd(modinfo->handle, EFUNC_GET_FLOODPROT_CHANNEL_MAX_LINES, _get_floodprot_channel_max_lines); + EfunctionAdd(modinfo->handle, EFUNC_FLOODPROT_CHECK_MULTILINE_BATCH, _floodprot_check_multiline_batch); return MOD_SUCCESS; } @@ -334,27 +339,27 @@ static void init_default_channel_flood_profiles(void) ChannelFloodProfile *f; f = safe_alloc(sizeof(ChannelFloodProfile)); - cmodef_put_param(&f->settings, "[10j#R10,30m#M10,7c#C15,5n#N15,10k#K15]:15"); + cmodef_put_param(&f->settings, "[10j#R10,30m#M10,7c#C15,5n#N15,10k#K15,1p]:15"); safe_strdup(f->settings.profile, "very-strict"); AddListItem(f, channel_flood_profiles); f = safe_alloc(sizeof(ChannelFloodProfile)); - cmodef_put_param(&f->settings, "[15j#R10,40m#M10,7c#C15,8n#N15,10k#K15]:15"); + cmodef_put_param(&f->settings, "[15j#R10,40m#M10,7c#C15,8n#N15,10k#K15,1p]:15"); safe_strdup(f->settings.profile, "strict"); AddListItem(f, channel_flood_profiles); f = safe_alloc(sizeof(ChannelFloodProfile)); - cmodef_put_param(&f->settings, "[30j#R10,40m#M10,7c#C15,8n#N15,10k#K15]:15"); + cmodef_put_param(&f->settings, "[30j#R10,40m#M10,7c#C15,8n#N15,10k#K15,2p]:15"); safe_strdup(f->settings.profile, "normal"); AddListItem(f, channel_flood_profiles); f = safe_alloc(sizeof(ChannelFloodProfile)); - cmodef_put_param(&f->settings, "[45j#R10,60m#M10,7c#C15,10n#N15,10k#K15]:15"); + cmodef_put_param(&f->settings, "[45j#R10,60m#M10,7c#C15,10n#N15,10k#K15,2p]:15"); safe_strdup(f->settings.profile, "relaxed"); AddListItem(f, channel_flood_profiles); f = safe_alloc(sizeof(ChannelFloodProfile)); - cmodef_put_param(&f->settings, "[60j#R10,90m#M10,7c#C15,10n#N15,10k#K15]:15"); + cmodef_put_param(&f->settings, "[60j#R10,90m#M10,7c#C15,10n#N15,10k#K15,3p]:15"); safe_strdup(f->settings.profile, "very-relaxed"); AddListItem(f, channel_flood_profiles); @@ -1292,12 +1297,12 @@ ChannelFloodProtection *get_channel_flood_settings(Channel *channel, int what) if (channel->mode.mode & EXTMODE_FLOODLIMIT) { fld = (ChannelFloodProtection *)GETPARASTRUCT(channel, 'f'); - if (fld->action[what]) + if (fld->limit[what]) return fld; } fld = (ChannelFloodProtection *)GETPARASTRUCT(channel, 'F'); - if (fld && fld->action[what]) + if (fld && fld->limit[what]) return fld; return NULL; @@ -1322,6 +1327,31 @@ int _get_floodprot_channel_max_lines(Channel *channel) return result; } +/** Check if a multiline batch is allowed by the channel's +f 'p' (paste) limit. + * Called from multiline.c before delivering a batch to the channel. + * @param channel The target channel + * @param client The user sending the batch + * @param line_count Number of lines in the batch + * @returns 0 if allowed, 1 if denied (paste limit exceeded) + */ +int _floodprot_check_multiline_batch(Channel *channel, Client *client, int line_count) +{ + /* Exempt short batches (2 lines or less) — that's just normal conversation */ + if (line_count < 3) + return 0; + + if (!IsFloodLimit(channel)) + return 0; + + if (check_channel_access(client, channel, "hoaq") || IsULine(client)) + return 0; + + if (do_floodprot(channel, client, CHFLD_PASTE)) + return 1; /* paste flood detected, reject batch */ + + return 0; +} + int floodprot_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype, ClientContext *clictx) { Membership *mb; @@ -1735,28 +1765,25 @@ int do_floodprot(Channel *channel, Client *client, int what) unknown_user = user_allowed_by_security_group_name(client, "known-users") ? 0 : 1; - if (fld->limit[what]) + if (TStime() - fld->timer[what] >= fld->per) { - if (TStime() - fld->timer[what] >= fld->per) - { - /* reset */ - fld->timer[what] = TStime(); - fld->counter[what] = 1; - fld->counter_unknown_users[what] = unknown_user; - } else - { - fld->counter[what]++; + /* reset */ + fld->timer[what] = TStime(); + fld->counter[what] = 1; + fld->counter_unknown_users[what] = unknown_user; + } else + { + fld->counter[what]++; - if (unknown_user) - fld->counter_unknown_users[what]++; + if (unknown_user) + fld->counter_unknown_users[what]++; - if ((fld->counter[what] > fld->limit[what]) && - (TStime() - fld->timer[what] < fld->per)) - { - if (MyUser(client)) - do_floodprot_action(channel, what); - return 1; /* flood detected! */ - } + if ((fld->counter[what] > fld->limit[what]) && + (TStime() - fld->timer[what] < fld->per)) + { + if (MyUser(client)) + do_floodprot_action(channel, what); + return 1; /* flood detected! */ } } return 0; diff --git a/src/modules/multiline.c b/src/modules/multiline.c index abc41baba..0b3259f2f 100644 --- a/src/modules/multiline.c +++ b/src/modules/multiline.c @@ -1204,6 +1204,13 @@ static void multiline_deliver_channel(Client *client, MultilineBatch *batch, Cha safe_free(concat_text); } + /* Check channel +f 'p' (paste flood) limit */ + if (MyUser(client) && floodprot_check_multiline_batch(channel, client, batch->line_count)) + { + sendto_one(client, NULL, ":%s FAIL BATCH MULTILINE_PASTE_LIMIT :Too many paste events in channel (+f)", me.name); + return; + } + /* Generate outgoing message tags */ new_message(client, batch->client_mtags, &mtags);