diff --git a/include/dynconf.h b/include/dynconf.h index 408e05bc5..10cac0d18 100644 --- a/include/dynconf.h +++ b/include/dynconf.h @@ -1,6 +1,7 @@ /************************************************************************ * Unreal Internet Relay Chat Daemon, include/dynconf.h - * Copyright (C) 1999 Carsten Munk + * Copyright (C) 1999-2003 Carsten Munk + * Copyright (C) 2003-2021 Bram Matthys * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,6 +23,11 @@ #define DYNCONF_H +typedef struct FloodSettings { + int limit[MAXFLOODOPTIONS]; + long period[MAXFLOODOPTIONS]; +} FloodSettings; + typedef struct NetworkConfiguration NetworkConfiguration; struct NetworkConfiguration { unsigned x_inah:1; @@ -118,14 +124,7 @@ struct Configuration { int handshake_data_flood_ban_action; struct ChMode modes_on_join; int level_on_join; - unsigned char away_count; - long away_period; - unsigned char nick_count; - long nick_period; - unsigned char invite_count; - long invite_period; - unsigned char knock_count; - long knock_period; + FloodSettings *floodsettings; unsigned char max_concurrent_conversations_users; unsigned char max_concurrent_conversations_new_user_every; int ident_connect_timeout; @@ -234,15 +233,6 @@ extern MODVAR int ipv6_disabled; #define MODES_ON_JOIN iConf.modes_on_join.mode #define LEVEL_ON_JOIN iConf.level_on_join -#define AWAY_PERIOD iConf.away_period -#define AWAY_COUNT iConf.away_count -#define NICK_PERIOD iConf.nick_period -#define NICK_COUNT iConf.nick_count -#define KNOCK_PERIOD iConf.knock_period -#define KNOCK_COUNT iConf.knock_count -#define INVITE_PERIOD iConf.invite_period -#define INVITE_COUNT iConf.invite_count - #define IDENT_CONNECT_TIMEOUT iConf.ident_connect_timeout #define IDENT_READ_TIMEOUT iConf.ident_read_timeout diff --git a/include/h.h b/include/h.h index bfaf574a5..38ef1cc5c 100644 --- a/include/h.h +++ b/include/h.h @@ -1065,3 +1065,4 @@ extern Secret *secrets; /* end */ extern int check_password_strength(char *pass, int min_length, int strict, char **err); extern int valid_secret_password(char *pass, char **err); +extern int flood_limit_exceeded(Client *client, FloodOption opt); diff --git a/include/struct.h b/include/struct.h index 452212f8e..1954c554e 100644 --- a/include/struct.h +++ b/include/struct.h @@ -1210,6 +1210,23 @@ extern void unload_all_unused_moddata(void); #define TLSFLAG_NOSTARTTLS 0x8 #define TLSFLAG_DISABLECLIENTCERT 0x10 +/** Flood counters for local clients */ +typedef struct FloodCounter { + int count; + long t; +} FloodCounter; + +/** This is the list of different flood counters that we keep for local clients. */ +typedef enum FloodOption { + FLD_NICK = 0, /**< nick-flood */ + FLD_JOIN = 1, /**< join-flood */ + FLD_AWAY = 2, /**< away-flood */ + FLD_INVITE = 3, /**< invite-flood */ + FLD_KNOCK = 4, /**< knock-flood */ +} FloodOption; +#define MAXFLOODOPTIONS 10 + + /** This shows the Client struct (any client), the User struct (a user), Server (a server) that are commonly accessed both in the core and by 3rd party coders. * @defgroup CommonStructs Common structs * @{ @@ -1289,6 +1306,7 @@ struct LocalClient { struct hostent *hostp; /**< Host record for this client (used by DNS code) */ char sockhost[HOSTLEN + 1]; /**< Hostname from the socket */ u_short port; /**< Remote TCP port of client */ + FloodCounter flood[MAXFLOODOPTIONS]; }; /** User information (persons, not servers), you use client->user to access these (see also @link Client @endlink). @@ -1311,11 +1329,9 @@ struct User { char *operlogin; /**< Which oper { } block was used to oper up, otherwise NULL - used by oper::maxlogins */ struct { time_t nick_t; /**< For set::anti-flood::nick-flood: time */ - time_t away_t; /**< For set::anti-flood::away-flood: time */ time_t knock_t; /**< For set::anti-flood::knock-flood: time */ time_t invite_t; /**< For set::anti-flood::invite-flood: time */ unsigned char nick_c; /**< For set::anti-flood::nick-flood: counter */ - unsigned char away_c; /**< For set::anti-flood::away-flood: counter */ unsigned char knock_c; /**< For set::anti-flood::knock-flood: counter */ unsigned char invite_c; /**< For set::anti-flood::invite-flood: counter */ } flood; /**< Anti-flood counters */ diff --git a/src/conf.c b/src/conf.c index bebebcebf..76846f6a4 100644 --- a/src/conf.c +++ b/src/conf.c @@ -362,6 +362,36 @@ char *x; return 1; } +/** Parses a value like '5:60s' into a flood setting that we can store. + * @param str The string to parse (eg: '5:60s') + * @param settings The FloodSettings block to store the result in + * @param opt The option (eg: FLD_AWAY) + * @returns 1 if OK, 0 for parse error. + */ +int config_parse_flood_generic(const char *str, FloodSettings *settings, FloodOption opt) +{ + char buf[64], *p; + + /* Work on a copy so we don't destroy 'str' */ + strlcpy(buf, str, sizeof(buf)); + + /* Initialize to zero first */ + settings->limit[opt] = 0; + settings->period[opt] = 0; + + p = strchr(buf, ':'); + + /* 'blah', ':blah', '1:' */ + if (!p || (p == buf) || (*(p+1) == '\0')) + return 0; + + *p++ = '\0'; + settings->limit[opt] = atoi(buf); + settings->period[opt] = config_checkval(p, CFG_TIME); + + return 1; +} + long config_checkval(char *orig, unsigned short flags) { char *value = raw_strdup(orig); char *text; @@ -1635,11 +1665,6 @@ void config_setdefaultsettings(Configuration *i) safe_strdup(i->oper_snomask, SNO_DEFOPER); i->ident_read_timeout = 7; i->ident_connect_timeout = 3; - i->nick_count = 3; i->nick_period = 60; /* NICK flood protection: max 3 per 60s */ - i->away_count = 4; i->away_period = 120; /* AWAY flood protection: max 4 per 120s */ - i->invite_count = 4; i->invite_period = 60; /* INVITE flood protection: max 4 per 60s */ - i->knock_count = 4; i->knock_period = 120; /* KNOCK protection: max 4 per 120s */ - i->throttle_count = 3; i->throttle_period = 60; /* throttle protection: max 3 per 60s */ i->ban_version_tkl_time = 86400; /* 1d */ i->spamfilter_ban_time = 86400; /* 1d */ safe_strdup(i->spamfilter_ban_reason, "Spam/advertising"); @@ -1678,6 +1703,14 @@ void config_setdefaultsettings(Configuration *i) i->handshake_delay = -1; i->broadcast_channel_messages = BROADCAST_CHANNEL_MESSAGES_AUTO; + /* Flood options */ + i->throttle_count = 3; i->throttle_period = 60; /* throttle protection: max 3 per 60s */ + i->floodsettings = safe_alloc(sizeof(FloodCounter) * MAXFLOODOPTIONS); + config_parse_flood_generic("3:60", i->floodsettings, FLD_NICK); /* NICK flood protection: max 3 per 60s */ + config_parse_flood_generic("4:120", i->floodsettings, FLD_AWAY); /* AWAY flood protection: max 4 per 120s */ + config_parse_flood_generic("4:60", i->floodsettings, FLD_INVITE); /* INVITE flood protection: max 4 per 60s */ + config_parse_flood_generic("4:120", i->floodsettings, FLD_KNOCK); /* KNOCK protection: max 4 per 120s */ + /* SSL/TLS options */ i->tls_options = safe_alloc(sizeof(TLSOptions)); snprintf(tmp, sizeof(tmp), "%s/tls/server.cert.pem", CONFDIR); @@ -7543,44 +7576,21 @@ int _conf_set(ConfigFile *conf, ConfigEntry *ce) tempiConf.handshake_data_flood_ban_action = banact_stringtoval(ceppp->ce_vardata); } } - else if (!strcmp(cepp->ce_varname, "away-count")) - tempiConf.away_count = atol(cepp->ce_vardata); - else if (!strcmp(cepp->ce_varname, "away-period")) - tempiConf.away_period = config_checkval(cepp->ce_vardata, CFG_TIME); else if (!strcmp(cepp->ce_varname, "away-flood")) { - int cnt, period; - config_parse_flood(cepp->ce_vardata, &cnt, &period); - tempiConf.away_count = cnt; - tempiConf.away_period = period; + config_parse_flood_generic(cepp->ce_vardata, tempiConf.floodsettings, FLD_AWAY); } else if (!strcmp(cepp->ce_varname, "nick-flood")) { - int cnt, period; - config_parse_flood(cepp->ce_vardata, &cnt, &period); - tempiConf.nick_count = cnt; - tempiConf.nick_period = period; - } - else if (!strcmp(cepp->ce_varname, "away-flood")) - { - int cnt, period; - config_parse_flood(cepp->ce_vardata, &cnt, &period); - tempiConf.away_count = cnt; - tempiConf.away_period = period; + config_parse_flood_generic(cepp->ce_vardata, tempiConf.floodsettings, FLD_NICK); } else if (!strcmp(cepp->ce_varname, "invite-flood")) { - int cnt, period; - config_parse_flood(cepp->ce_vardata, &cnt, &period); - tempiConf.invite_count = cnt; - tempiConf.invite_period = period; + config_parse_flood_generic(cepp->ce_vardata, tempiConf.floodsettings, FLD_INVITE); } else if (!strcmp(cepp->ce_varname, "knock-flood")) { - int cnt, period; - config_parse_flood(cepp->ce_vardata, &cnt, &period); - tempiConf.knock_count = cnt; - tempiConf.knock_period = period; + config_parse_flood_generic(cepp->ce_vardata, tempiConf.floodsettings, FLD_KNOCK); } else if (!strcmp(cepp->ce_varname, "connect-flood")) { diff --git a/src/modules/away.c b/src/modules/away.c index 3a741f505..a760c83c1 100644 --- a/src/modules/away.c +++ b/src/modules/away.c @@ -89,21 +89,13 @@ CMD_FUNC(cmd_away) if (MyUser(client) && match_spamfilter(client, new_reason, SPAMF_AWAY, "AWAY", NULL, 0, NULL)) return; - /* Check set::anti-flood::away-flood */ - if (MyUser(client) && AWAY_PERIOD && !ValidatePermissionsForPath("immune:away-flood",client,NULL,NULL,NULL)) + /* Check away-flood */ + if (MyUser(client) && + !ValidatePermissionsForPath("immune:away-flood",client,NULL,NULL,NULL) && + flood_limit_exceeded(client, FLD_AWAY)) { - if ((client->user->flood.away_t + AWAY_PERIOD) <= timeofday) - { - client->user->flood.away_c = 0; - client->user->flood.away_t = timeofday; - } - if (client->user->flood.away_c <= AWAY_COUNT) - client->user->flood.away_c++; - if (client->user->flood.away_c > AWAY_COUNT) - { - sendnumeric(client, ERR_TOOMANYAWAY); - return; - } + sendnumeric(client, ERR_TOOMANYAWAY); + return; } /* Obey set::away-length */ diff --git a/src/modules/invite.c b/src/modules/invite.c index 93fd7e7a0..906f5ccd5 100644 --- a/src/modules/invite.c +++ b/src/modules/invite.c @@ -166,25 +166,16 @@ CMD_FUNC(cmd_invite) return; } - if (MyConnect(client)) + if (MyUser(client)) { if (target_limit_exceeded(client, target, target->name)) return; - if (!ValidatePermissionsForPath("immune:invite-flood",client,NULL,NULL,NULL)) + if (!ValidatePermissionsForPath("immune:invite-flood",client,NULL,NULL,NULL) && + flood_limit_exceeded(client, FLD_INVITE)) { - if ((client->user->flood.invite_t + INVITE_PERIOD) <= timeofday) - { - client->user->flood.invite_c = 0; - client->user->flood.invite_t = timeofday; - } - if (client->user->flood.invite_c <= INVITE_COUNT) - client->user->flood.invite_c++; - if (client->user->flood.invite_c > INVITE_COUNT) - { - sendnumeric(client, RPL_TRYAGAIN, "INVITE"); - return; - } + sendnumeric(client, RPL_TRYAGAIN, "INVITE"); + return; } if (!override) diff --git a/src/modules/knock.c b/src/modules/knock.c index 3cdb48582..d2197af8f 100644 --- a/src/modules/knock.c +++ b/src/modules/knock.c @@ -132,21 +132,12 @@ CMD_FUNC(cmd_knock) if (i == HOOK_DENY) return; - if (MyUser(client) && !ValidatePermissionsForPath("immune:knock-flood",client,NULL,NULL,NULL)) + if (MyUser(client) && + !ValidatePermissionsForPath("immune:knock-flood",client,NULL,NULL,NULL) && + flood_limit_exceeded(client, FLD_KNOCK)) { - if ((client->user->flood.knock_t + KNOCK_PERIOD) <= timeofday) - { - client->user->flood.knock_c = 0; - client->user->flood.knock_t = timeofday; - } - if (client->user->flood.knock_c <= KNOCK_COUNT) - client->user->flood.knock_c++; - if (client->user->flood.knock_c > KNOCK_COUNT) - { - sendnumeric(client, ERR_CANNOTKNOCK, parv[1], - "You are KNOCK flooding"); - return; - } + sendnumeric(client, ERR_CANNOTKNOCK, parv[1], "You are KNOCK flooding"); + return; } new_message(&me, NULL, &mtags); diff --git a/src/modules/nick.c b/src/modules/nick.c index 01c619dfe..22f4340b1 100644 --- a/src/modules/nick.c +++ b/src/modules/nick.c @@ -274,19 +274,6 @@ CMD_FUNC(cmd_nick_local) return; } - /* set::anti-flood::nick-flood */ - if (client->user && !ValidatePermissionsForPath("immune:nick-flood",client,NULL,NULL,NULL)) - { - if ((client->user->flood.nick_c >= NICK_COUNT) && - (TStime() - client->user->flood.nick_t < NICK_PERIOD)) - { - /* Throttle... */ - sendnumeric(client, ERR_NCHANGETOOFAST, nick, - (int)(NICK_PERIOD - (TStime() - client->user->flood.nick_t))); - return; - } - } - /* Check for collisions / in use */ if (!strcasecmp("ircd", nick) || !strcasecmp("irc", nick)) { @@ -321,6 +308,16 @@ CMD_FUNC(cmd_nick_local) /* fallthrough for ircops that have sufficient privileges */ } + /* set::anti-flood::nick-flood */ + if (client->user && + !ValidatePermissionsForPath("immune:nick-flood",client,NULL,NULL,NULL) && + flood_limit_exceeded(client, FLD_NICK)) + { + /* Throttle... */ + sendnumeric(client, ERR_NCHANGETOOFAST, nick); + return; + } + if (!ValidatePermissionsForPath("immune:nick-flood",client,NULL,NULL,NULL)) cptr->local->since += 3; /* Nick-flood prot. -Donwulff */ @@ -439,13 +436,6 @@ CMD_FUNC(cmd_nick_local) } } - if (TStime() - client->user->flood.nick_t >= NICK_PERIOD) - { - client->user->flood.nick_t = TStime(); - client->user->flood.nick_c = 1; - } else - client->user->flood.nick_c++; - sendto_snomask(SNO_NICKCHANGE, "*** %s (%s@%s) has changed their nickname to %s", client->name, client->user->username, client->user->realhost, nick); diff --git a/src/modules/stats.c b/src/modules/stats.c index b01531b29..ee1741572 100644 --- a/src/modules/stats.c +++ b/src/modules/stats.c @@ -875,9 +875,9 @@ int stats_set(Client *client, char *para) sendtxtnumeric(client, "anti-flood::handshake-data-flood::amount: %ld bytes", iConf.handshake_data_flood_amount); sendtxtnumeric(client, "anti-flood::handshake-data-flood::ban-action: %s", banact_valtostring(iConf.handshake_data_flood_ban_action)); sendtxtnumeric(client, "anti-flood::handshake-data-flood::ban-time: %s", pretty_time_val(iConf.handshake_data_flood_ban_time)); - if (AWAY_PERIOD) - sendtxtnumeric(client, "anti-flood::away-flood: %d per %s", AWAY_COUNT, pretty_time_val(AWAY_PERIOD)); - sendtxtnumeric(client, "anti-flood::nick-flood: %d per %s", NICK_COUNT, pretty_time_val(NICK_PERIOD)); + //if (AWAY_PERIOD) + // sendtxtnumeric(client, "anti-flood::away-flood: %d per %s", AWAY_COUNT, pretty_time_val(AWAY_PERIOD)); + //sendtxtnumeric(client, "anti-flood::nick-flood: %d per %s", NICK_COUNT, pretty_time_val(NICK_PERIOD)); sendtxtnumeric(client, "handshake-timeout: %s", pretty_time_val(iConf.handshake_timeout)); sendtxtnumeric(client, "sasl-timeout: %s", pretty_time_val(iConf.sasl_timeout)); sendtxtnumeric(client, "ident::connect-timeout: %s", pretty_time_val(IDENT_CONNECT_TIMEOUT)); diff --git a/src/numeric.c b/src/numeric.c index ce4c1331c..14812401b 100644 --- a/src/numeric.c +++ b/src/numeric.c @@ -478,7 +478,7 @@ static char *replies[] = { /* 435 */ NULL, /* bahamut */ /* 436 ERR_NICKCOLLISION */ "%s :Nickname collision KILL", /* 437 ERR_BANNICKCHANGE */ "%s :Cannot change nickname while banned on channel", -/* 438 ERR_NCHANGETOOFAST */ "%s :Nick change too fast. Please wait %d seconds", +/* 438 ERR_NCHANGETOOFAST */ "%s :Nick change too fast. Please try again later.", /* 439 ERR_TARGETTOOFAST */ "%s :Message target change too fast. Please wait %ld seconds", /* 440 ERR_SERVICESDOWN */ "%s :Services are currently down. Please try again later.", /* 441 ERR_USERNOTINCHANNEL */ "%s %s :They aren't on that channel", diff --git a/src/user.c b/src/user.c index 57ad8fc12..dce06eecd 100644 --- a/src/user.c +++ b/src/user.c @@ -925,3 +925,36 @@ char *get_connect_extinfo(Client *client) return retbuf; } + +/** Is the flood limit exceeded for an option? eg for away-flood. + * @param client The client to check flood for (local user) + * @param opt The flood option (eg FLD_AWAY) + * @note This increments the flood counter as well. + * @returns 1 if exceeded, 0 if not. + */ +int flood_limit_exceeded(Client *client, FloodOption opt) +{ + if (!MyUser(client)) + return 0; + + if ((opt < 0) || (opt >= MAXFLOODOPTIONS)) + abort(); + + /* Is the protection enabled at all? */ + if (iConf.floodsettings->limit[opt] == 0) + return 0; + + /* Ok, let's do the flood check */ + if ((client->local->flood[opt].t + iConf.floodsettings->period[opt]) <= timeofday) + { + /* Time exceeded, reset */ + client->local->flood[opt].count = 0; + client->local->flood[opt].t = timeofday; + } + if (client->local->flood[opt].count <= iConf.floodsettings->limit[opt]) + client->local->flood[opt].count++; + if (client->local->flood[opt].count > iConf.floodsettings->limit[opt]) + return 1; /* Flood limit hit! */ + + return 0; +}