1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-07-06 04:53:13 +02:00
Files
unrealircd/src/modules/connthrottle.c
T

1407 lines
42 KiB
C

/*
* connthrottle - Connection throttler
* (C) Copyright 2004-2020 Bram Matthys (Syzop) and the UnrealIRCd team
* License: GPLv2 or later
* See https://www.unrealircd.org/docs/Connthrottle
*/
#include "unrealircd.h"
#define CONNTHROTTLE_VERSION "2.0.0"
#ifndef CALLBACKTYPE_REPUTATION_STARTTIME
#define CALLBACKTYPE_REPUTATION_STARTTIME 5
#endif
ModuleHeader MOD_HEADER = {
"connthrottle",
CONNTHROTTLE_VERSION,
"Connection throttler - by Syzop",
"UnrealIRCd Team",
"unrealircd-6",
};
typedef struct {
int count;
int period;
} ThrottleSetting;
struct cfgstruct {
/* set::connthrottle::known-users: */
ThrottleSetting local;
ThrottleSetting global;
/* set::connthrottle::except: */
SecurityGroup *except;
/* set::connthrottle::disabled-when: */
long reputation_gathering;
int start_delay;
/* set::connthrottle (generic): */
char *reason;
/* set::connthrottle::ipv6-unknown-users-limit (one entry per tier) */
int ipv6_unknown_users_limit[3];
};
static struct cfgstruct cfg;
typedef struct {
int count;
long t;
} ThrottleCounter;
typedef struct UCounter UCounter;
struct UCounter {
ThrottleCounter local; /**< Local counter */
ThrottleCounter global; /**< Global counter */
int rejected_clients; /**< Number of rejected clients this minute */
int allowed_except; /**< Number of allowed clients - on except list */
int allowed_unknown_users; /**< Number of allowed clients - not on except list */
char disabled; /**< Module disabled by oper? */
int throttling_this_minute; /**< Did we do any throttling this minute? */
int throttling_previous_minute; /**< Did we do any throttling previous minute? */
int throttling_banner_displayed; /**< Big we-are-now-throttling banner displayed? */
time_t next_event; /**< When is next event? (for "last 60 seconds" stats) */
};
UCounter *ucounter = NULL;
#define MSG_THROTTLE "THROTTLE"
/* Forward declarations */
int ct_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int ct_config_posttest(int *errs);
int ct_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
int ct_pre_lconnect(Client *client);
int ct_lconnect(Client *);
int ct_rconnect(Client *);
CMD_FUNC(ct_throttle);
EVENT(connthrottle_evt);
void ucounter_free(ModData *m);
RPC_CALL_FUNC(rpc_connthrottle_status);
RPC_CALL_FUNC(rpc_connthrottle_set);
RPC_CALL_FUNC(rpc_connthrottle_reset);
/* IPv6 wider-prefix bucket tracking (/56, /48, /32) */
#define CT_NUM_TIERS 3
#define CT_BUCKET_HASH_SIZE 2048
#ifdef DEBUGMODE
/** Self-check of IPv6 CIDR limits every <this> msec */
#define CONNTHROTTLE_CHECK 1000
#endif
/* Per-client classification stored in our ModData slot.
* CT_CATEGORY_NONE (value 0) is the moddata default and means
* "this client has not been added to our buckets yet".
*/
typedef enum {
CT_CATEGORY_NONE = 0,
CT_CATEGORY_KNOWN_USERS = 1,
CT_CATEGORY_EXCEPTED_UNKNOWNS = 2,
CT_CATEGORY_UNKNOWN_USERS = 3,
} ConnThrottleCategory;
typedef struct ConnThrottleBucket ConnThrottleBucket;
struct ConnThrottleBucket {
ConnThrottleBucket *prev, *next;
char rawip[16];
int known_users;
int excepted_unknowns;
int unknown_users;
#if defined(CONNTHROTTLE_CHECK)
int check_known;
int check_excepted;
int check_unknown;
#endif
};
static const int ct_tier_prefix[CT_NUM_TIERS] = {56, 48, 32};
static ConnThrottleBucket **ct_bucket_hash[CT_NUM_TIERS];
static char *siphashkey_ct_buckets = NULL;
static ModDataInfo *connthrottle_md = NULL;
/* Per-client classification cached in ModData. CT_CATEGORY_NONE means
* "this client has not been added to our buckets yet" (or has been removed).
*/
#define CT_CATEGORY(client) moddata_client((client), connthrottle_md).l
static void ct_make_rawip(Client *client, int tier, char *out);
static uint64_t ct_hash_bucket(const char *masked);
static ConnThrottleBucket *ct_find_bucket(int tier, const char *masked);
static ConnThrottleBucket *ct_add_bucket(int tier, const char *masked);
static void ct_bucket_increment(ConnThrottleBucket *b, ConnThrottleCategory category);
static void ct_bucket_decrement(ConnThrottleBucket *b, ConnThrottleCategory category);
static ConnThrottleCategory ct_classify(Client *client);
static void ct_bucket_bump_client(Client *client, ConnThrottleCategory category);
static void ct_bucket_unbump_client(Client *client, ConnThrottleCategory category);
static void ct_buckets_rebuild(void);
static void ct_buckets_free(void);
static const char *ct_module_status_text(void);
const char *ct_allow_client(Client *client, ConfigItem_allow *aconf);
int ct_remote_connect_buckets(Client *client);
int ct_quit(Client *client, MessageTag *mtags, const char *comment);
int ct_known_user_cache_change(Client *client);
int stats_connthrottle(Client *client, const char *para);
#if defined(CONNTHROTTLE_CHECK)
EVENT(ct_check);
#endif
MOD_TEST()
{
memset(&cfg, 0, sizeof(cfg));
/* Defaults: */
cfg.local.count = 20;
cfg.local.period = 60;
cfg.global.count = 30;
cfg.global.period = 60;
cfg.start_delay = 180; /* 3 minutes */
cfg.reputation_gathering = 7 * 86400; /* 1 week */
safe_strdup(cfg.reason, "Throttled: Too many users trying to connect, please wait a while and try again");
cfg.except = safe_alloc(sizeof(SecurityGroup));
cfg.except->reputation_score = 24;
cfg.except->identified = 1;
cfg.except->webirc = 0;
cfg.ipv6_unknown_users_limit[0] = 8; /* /56 */
cfg.ipv6_unknown_users_limit[1] = 32; /* /48 */
cfg.ipv6_unknown_users_limit[2] = 256; /* /32 */
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, ct_config_test);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, ct_config_posttest);
return MOD_SUCCESS;
}
MOD_INIT()
{
RPCHandlerInfo r;
ModDataInfo mreq;
MARK_AS_OFFICIAL_MODULE(modinfo);
LoadPersistentPointer(modinfo, ucounter, ucounter_free);
if (!ucounter)
ucounter = safe_alloc(sizeof(UCounter));
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, ct_config_run);
HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 0, ct_pre_lconnect);
HookAdd(modinfo->handle, HOOKTYPE_LOCAL_CONNECT, 0, ct_lconnect);
HookAdd(modinfo->handle, HOOKTYPE_REMOTE_CONNECT, 0, ct_rconnect);
/* IPv6 wider-prefix bucket tracking */
memset(&mreq, 0, sizeof(mreq));
mreq.name = "connthrottle_category";
mreq.type = MODDATATYPE_CLIENT;
mreq.sync = 0; /* local only, no S2S sync */
connthrottle_md = ModDataAdd(modinfo->handle, mreq);
if (!connthrottle_md)
{
config_error("[connthrottle] Could not register ModData 'connthrottle_category'");
return MOD_FAILED;
}
siphashkey_ct_buckets = safe_alloc(SIPHASH_KEY_LENGTH);
siphash_generate_key(siphashkey_ct_buckets);
ct_bucket_hash[0] = safe_alloc(sizeof(ConnThrottleBucket *) * CT_BUCKET_HASH_SIZE);
ct_bucket_hash[1] = safe_alloc(sizeof(ConnThrottleBucket *) * CT_BUCKET_HASH_SIZE);
ct_bucket_hash[2] = safe_alloc(sizeof(ConnThrottleBucket *) * CT_BUCKET_HASH_SIZE);
HookAddConstString(modinfo->handle, HOOKTYPE_ALLOW_CLIENT, 0, ct_allow_client);
HookAdd(modinfo->handle, HOOKTYPE_REMOTE_CONNECT, 0, ct_remote_connect_buckets);
HookAdd(modinfo->handle, HOOKTYPE_LOCAL_QUIT, 0, ct_quit);
HookAdd(modinfo->handle, HOOKTYPE_UNKUSER_QUIT, 0, ct_quit);
HookAdd(modinfo->handle, HOOKTYPE_REMOTE_QUIT, 0, ct_quit);
HookAdd(modinfo->handle, HOOKTYPE_KNOWN_USER_CACHE_CHANGE, 0, ct_known_user_cache_change);
HookAdd(modinfo->handle, HOOKTYPE_STATS, 0, stats_connthrottle);
CommandAdd(modinfo->handle, MSG_THROTTLE, ct_throttle, MAXPARA, CMD_USER | CMD_SERVER);
/* RPC handlers */
memset(&r, 0, sizeof(r));
r.method = "connthrottle.status";
r.loglevel = ULOG_DEBUG;
r.call = rpc_connthrottle_status;
if (!RPCHandlerAdd(modinfo->handle, &r))
{
config_error("[connthrottle] Could not register RPC handler 'connthrottle.status'");
return MOD_FAILED;
}
memset(&r, 0, sizeof(r));
r.method = "connthrottle.set";
r.call = rpc_connthrottle_set;
if (!RPCHandlerAdd(modinfo->handle, &r))
{
config_error("[connthrottle] Could not register RPC handler 'connthrottle.set'");
return MOD_FAILED;
}
memset(&r, 0, sizeof(r));
r.method = "connthrottle.reset";
r.call = rpc_connthrottle_reset;
if (!RPCHandlerAdd(modinfo->handle, &r))
{
config_error("[connthrottle] Could not register RPC handler 'connthrottle.reset'");
return MOD_FAILED;
}
return MOD_SUCCESS;
}
MOD_LOAD()
{
EventAdd(modinfo->handle, "connthrottle_evt", connthrottle_evt, NULL, 1000, 0);
#if defined(CONNTHROTTLE_CHECK)
EventAdd(modinfo->handle, "ct_check", ct_check, NULL, CONNTHROTTLE_CHECK, 0);
#endif
ct_buckets_rebuild();
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
SavePersistentPointer(modinfo, ucounter);
safe_free(cfg.reason);
free_security_group(cfg.except);
/* IPv6 wider-prefix bucket tracking */
ct_buckets_free();
safe_free(siphashkey_ct_buckets);
return MOD_SUCCESS;
}
/** This function checks if the reputation module is loaded.
* If not, then the module will error, since we depend on it.
*/
int ct_config_posttest(int *errs)
{
int errors = 0;
/* Note: we use Callbacks[] here, but this is only for checking. Don't
* let this confuse you. At any other place you must use RCallbacks[].
*/
if (Callbacks[CALLBACKTYPE_REPUTATION_STARTTIME] == NULL)
{
config_error("The 'connthrottle' module requires the 'reputation' "
"module to be loaded as well.");
config_error("Add the following to your configuration file: "
"loadmodule \"reputation\";");
errors++;
}
*errs = errors;
return errors ? -1 : 1;
}
/** Test the set::connthrottle configuration */
int ct_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep, *cepp, *ceppp;
if (type != CONFIG_SET)
return 0;
/* We are only interrested in set::connthrottle.. */
if (!ce || !ce->name || strcmp(ce->name, "connthrottle"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "except"))
{
test_match_block(cf, cep, &errors);
} else if (!strcmp(cep->name, "known-users"))
{
for (cepp = cep->items; cepp; cepp = cepp->next)
{
CheckNull(cepp);
if (!strcmp(cepp->name, "minimum-reputation-score"))
{
int cnt = atoi(cepp->value);
if (cnt < 1)
{
config_error("%s:%i: set::connthrottle::known-users::minimum-reputation-score should be at least 1",
cepp->file->filename, cepp->line_number);
errors++;
continue;
}
} else if (!strcmp(cepp->name, "sasl-bypass"))
{
} else if (!strcmp(cepp->name, "webirc-bypass"))
{
} else
{
config_error_unknown(cepp->file->filename, cepp->line_number,
"set::connthrottle::known-users", cepp->name);
errors++;
}
}
} else if (!strcmp(cep->name, "new-users"))
{
for (cepp = cep->items; cepp; cepp = cepp->next)
{
CheckNull(cepp);
if (!strcmp(cepp->name, "local-throttle"))
{
int cnt, period;
if (!config_parse_flood(cepp->value, &cnt, &period) ||
(cnt < 1) || (cnt > 2000000000) || (period > 2000000000))
{
config_error("%s:%i: set::connthrottle::new-users::local-throttle error. "
"Syntax is <count>:<period> (eg 6:60), "
"and count and period should be non-zero.",
cepp->file->filename, cepp->line_number);
errors++;
continue;
}
} else if (!strcmp(cepp->name, "global-throttle"))
{
int cnt, period;
if (!config_parse_flood(cepp->value, &cnt, &period) ||
(cnt < 1) || (cnt > 2000000000) || (period > 2000000000))
{
config_error("%s:%i: set::connthrottle::new-users::global-throttle error. "
"Syntax is <count>:<period> (eg 6:60), "
"and count and period should be non-zero.",
cepp->file->filename, cepp->line_number);
errors++;
continue;
}
} else
{
config_error_unknown(cepp->file->filename, cepp->line_number,
"set::connthrottle::new-users", cepp->name);
errors++;
}
}
} else if (!strcmp(cep->name, "disabled-when"))
{
for (cepp = cep->items; cepp; cepp = cepp->next)
{
CheckNull(cepp);
if (!strcmp(cepp->name, "start-delay"))
{
int cnt = config_checkval(cepp->value, CFG_TIME);
if ((cnt < 0) || (cnt > 3600))
{
config_error("%s:%i: set::connthrottle::disabled-when::start-delay should be in range 0-3600",
cepp->file->filename, cepp->line_number);
errors++;
continue;
}
} else if (!strcmp(cepp->name, "reputation-gathering"))
{
} else
{
config_error_unknown(cepp->file->filename, cepp->line_number,
"set::connthrottle::disabled-when", cepp->name);
errors++;
}
}
} else if (!strcmp(cep->name, "reason"))
{
CheckNull(cep);
} else if (!strcmp(cep->name, "ipv6-unknown-users-limit"))
{
for (cepp = cep->items; cepp; cepp = cepp->next)
{
if (strcmp(cepp->name, "cidr-56") &&
strcmp(cepp->name, "cidr-48") &&
strcmp(cepp->name, "cidr-32"))
{
config_error_unknown(cepp->file->filename, cepp->line_number,
"set::connthrottle::ipv6-unknown-users-limit", cepp->name);
errors++;
continue;
}
for (ceppp = cepp->items; ceppp; ceppp = ceppp->next)
{
CheckNull(ceppp);
if (!strcmp(ceppp->name, "max"))
{
int v = atoi(ceppp->value);
if ((v < 0) || (v > 1000000))
{
config_error("%s:%i: set::connthrottle::ipv6-unknown-users-limit::%s::max should be in range 0-1000000",
ceppp->file->filename, ceppp->line_number, cepp->name);
errors++;
}
} else
{
config_error_unknown(ceppp->file->filename, ceppp->line_number,
"set::connthrottle::ipv6-unknown-users-limit::cidr-N", ceppp->name);
errors++;
}
}
}
} else
{
config_error("%s:%i: unknown directive set::connthrottle::%s",
cep->file->filename, cep->line_number, cep->name);
errors++;
continue;
}
}
*errs = errors;
return errors ? -1 : 1;
}
/* Configure ourselves based on the set::connthrottle settings */
int ct_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
ConfigEntry *cep, *cepp, *ceppp;
if (type != CONFIG_SET)
return 0;
/* We are only interrested in set::connthrottle.. */
if (!ce || !ce->name || strcmp(ce->name, "connthrottle"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "except"))
{
conf_match_block(cf, cep, &cfg.except);
} else if (!strcmp(cep->name, "known-users"))
{
for (cepp = cep->items; cepp; cepp = cepp->next)
{
if (!strcmp(cepp->name, "minimum-reputation-score"))
cfg.except->reputation_score = atoi(cepp->value);
else if (!strcmp(cepp->name, "sasl-bypass"))
cfg.except->identified = config_checkval(cepp->value, CFG_YESNO);
else if (!strcmp(cepp->name, "webirc-bypass"))
cfg.except->webirc = config_checkval(cepp->value, CFG_YESNO);
}
} else if (!strcmp(cep->name, "new-users"))
{
for (cepp = cep->items; cepp; cepp = cepp->next)
{
if (!strcmp(cepp->name, "local-throttle"))
config_parse_flood(cepp->value, &cfg.local.count, &cfg.local.period);
else if (!strcmp(cepp->name, "global-throttle"))
config_parse_flood(cepp->value, &cfg.global.count, &cfg.global.period);
}
} else if (!strcmp(cep->name, "disabled-when"))
{
for (cepp = cep->items; cepp; cepp = cepp->next)
{
if (!strcmp(cepp->name, "start-delay"))
cfg.start_delay = config_checkval(cepp->value, CFG_TIME);
else if (!strcmp(cepp->name, "reputation-gathering"))
cfg.reputation_gathering = config_checkval(cepp->value, CFG_TIME);
}
} else if (!strcmp(cep->name, "reason"))
{
safe_free(cfg.reason);
cfg.reason = safe_alloc(strlen(cep->value) + 16);
sprintf(cfg.reason, "Throttled: %s", cep->value);
} else if (!strcmp(cep->name, "ipv6-unknown-users-limit"))
{
for (cepp = cep->items; cepp; cepp = cepp->next)
{
int tier;
if (!strcmp(cepp->name, "cidr-56"))
tier = 0;
else if (!strcmp(cepp->name, "cidr-48"))
tier = 1;
else if (!strcmp(cepp->name, "cidr-32"))
tier = 2;
else
continue;
for (ceppp = cepp->items; ceppp; ceppp = ceppp->next)
{
if (!strcmp(ceppp->name, "max"))
cfg.ipv6_unknown_users_limit[tier] = atoi(ceppp->value);
}
}
}
}
return 1;
}
/** Returns 1 if the 'reputation' module is still gathering
* data, such as in the first week of when it is loaded.
* This behavior is configured via set::disabled-when::reputation-gathering
*/
int still_reputation_gathering(void)
{
int v;
if (RCallbacks[CALLBACKTYPE_REPUTATION_STARTTIME] == NULL)
return 1; /* Reputation module not loaded, disable us */
v = RCallbacks[CALLBACKTYPE_REPUTATION_STARTTIME]->func.intfunc();
if (TStime() - v < cfg.reputation_gathering)
return 1; /* Still gathering reputation data (eg: first week) */
return 0;
}
EVENT(connthrottle_evt)
{
char buf[512];
if (ucounter->next_event > TStime())
return;
ucounter->next_event = TStime() + 60;
if (ucounter->rejected_clients)
{
unreal_log(ULOG_INFO, "connthrottle", "CONNTHROTTLE_REPORT", NULL,
"ConnThrottle] Stats for this server past 60 secs: "
"Connections rejected: $num_rejected. "
"Accepted: $num_accepted_except except user(s) and "
"$num_accepted_unknown_users new user(s).",
log_data_integer("num_rejected", ucounter->rejected_clients),
log_data_integer("num_accepted_except", ucounter->allowed_except),
log_data_integer("num_accepted_unknown_users", ucounter->allowed_unknown_users));
}
/* Reset stats for next message */
ucounter->rejected_clients = 0;
ucounter->allowed_except = 0;
ucounter->allowed_unknown_users = 0;
ucounter->throttling_previous_minute = ucounter->throttling_this_minute;
ucounter->throttling_this_minute = 0; /* reset */
ucounter->throttling_banner_displayed = 0; /* reset */
}
#define THROT_LOCAL 1
#define THROT_GLOBAL 2
int ct_pre_lconnect(Client *client)
{
int throttle = 0;
int score;
if (me.local->creationtime + cfg.start_delay > TStime())
return HOOK_CONTINUE; /* no throttle: start delay */
if (ucounter->disabled)
return HOOK_CONTINUE; /* protection disabled: allow user */
if (still_reputation_gathering())
return HOOK_CONTINUE; /* still gathering reputation data */
if (user_allowed_by_security_group(client, cfg.except) ||
find_tkl_exception(TKL_CONNECT_FLOOD, client))
return HOOK_CONTINUE; /* allowed: user is exempt */
/* If we reach this then the user is NEW */
/* +1 global client would reach global limit? */
if ((TStime() - ucounter->global.t < cfg.global.period) && (ucounter->global.count + 1 > cfg.global.count))
throttle |= THROT_GLOBAL;
/* +1 local client would reach local limit? */
if ((TStime() - ucounter->local.t < cfg.local.period) && (ucounter->local.count + 1 > cfg.local.count))
throttle |= THROT_LOCAL;
if (throttle)
{
ucounter->throttling_this_minute = 1;
ucounter->rejected_clients++;
/* We send the LARGE banner if throttling was activated */
if (!ucounter->throttling_previous_minute && !ucounter->throttling_banner_displayed)
{
unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTTLE_ACTIVATED", NULL,
"[ConnThrottle] Connection throttling has been ACTIVATED due to a HIGH CONNECTION RATE.\n"
"Users with IP addresses that have not been seen before will be rejected above the set connection rate. Known users can still get in.\n"
"or more information see https://www.unrealircd.org/docs/ConnThrottle");
ucounter->throttling_banner_displayed = 1;
}
exit_client(client, NULL, cfg.reason);
return HOOK_DENY;
}
return HOOK_CONTINUE;
}
/** Increase the connect counter(s), nothing else. */
void bump_connect_counter(int local_connect)
{
if (local_connect)
{
/* Bump local connect counter */
if (TStime() - ucounter->local.t >= cfg.local.period)
{
ucounter->local.t = TStime();
ucounter->local.count = 1;
} else
{
ucounter->local.count++;
}
}
/* Bump global connect counter */
if (TStime() - ucounter->global.t >= cfg.global.period)
{
ucounter->global.t = TStime();
ucounter->global.count = 1;
} else
{
ucounter->global.count++;
}
}
int ct_lconnect(Client *client)
{
int score;
if (me.local->creationtime + cfg.start_delay > TStime())
return 0; /* no throttle: start delay */
if (ucounter->disabled)
return 0; /* protection disabled: allow user */
if (still_reputation_gathering())
return 0; /* still gathering reputation data */
if (user_allowed_by_security_group(client, cfg.except) ||
find_tkl_exception(TKL_CONNECT_FLOOD, client))
{
ucounter->allowed_except++;
return HOOK_CONTINUE; /* allowed: user is exempt */
}
/* Allowed NEW user */
ucounter->allowed_unknown_users++;
bump_connect_counter(1);
return 0;
}
int ct_rconnect(Client *client)
{
int score;
if (!IsSynched(client->uplink))
return 0; /* Netmerge: skip */
if (IsULine(client))
return 0; /* U:lined, such as services: skip */
#if UNREAL_VERSION_TIME >= 201915
/* On UnrealIRCd 4.2.3+ we can see the boot time (start time)
* of the remote server. This way we can apply the
* set::disabled-when::start-delay restriction on remote
* servers as well.
*/
if (client->uplink && client->uplink->server && client->uplink->server->boottime &&
(TStime() - client->uplink->server->boottime < cfg.start_delay))
{
return 0;
}
#endif
if (user_allowed_by_security_group(client, cfg.except) ||
find_tkl_exception(TKL_CONNECT_FLOOD, client))
return 0; /* user is exempt */
bump_connect_counter(0);
return 0;
}
static const char *ct_module_status_text(void)
{
static char buf[256];
if (ucounter->disabled)
return "Module DISABLED on oper request. To re-enable, type: /THROTTLE ON";
if (still_reputation_gathering())
return "Module DISABLED because the 'reputation' module has not gathered enough data yet (set::connthrottle::disabled-when::reputation-gathering).";
if (me.local->creationtime + cfg.start_delay > TStime())
{
snprintf(buf, sizeof(buf),
"Module DISABLED due to start-delay (set::connthrottle::disabled-when::start-delay), will be enabled in %lld second(s).",
(long long)((me.local->creationtime + cfg.start_delay) - TStime()));
return buf;
}
return "Module ENABLED";
}
static void ct_throttle_usage(Client *client)
{
sendnotice(client, "Usage: /THROTTLE [ON|OFF|STATUS|RESET]");
sendnotice(client, " ON: Enabled protection");
sendnotice(client, " OFF: Disables protection");
sendnotice(client, " STATUS: Status report");
sendnotice(client, " RESET: Resets all counters(&more)");
sendnotice(client, "NOTE: All commands only affect this server. Remote servers are not affected.");
}
void ct_off(Client *client)
{
if (ucounter->disabled)
return; /* Already off */
ucounter->disabled = 1;
unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTTLE_MODULE_DISABLED", client,
"[ConnThrottle] $client.details DISABLED the connthrottle module.");
}
void ct_on(Client *client)
{
if (!ucounter->disabled)
return; /* Already on */
unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTTLE_MODULE_ENABLED", client,
"[ConnThrottle] $client.details ENABLED the connthrottle module.");
ucounter->disabled = 0;
}
void ct_reset(Client *client)
{
memset(ucounter, 0, sizeof(UCounter));
unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTTLE_RESET", client,
"[ConnThrottle] $client.details did a RESET on the statistics/counters.");
}
CMD_FUNC(ct_throttle)
{
if (!IsOper(client))
{
sendnumeric(client, ERR_NOPRIVILEGES);
return;
}
if ((parc < 2) || BadPtr(parv[1]))
{
ct_throttle_usage(client);
return;
}
if (!strcasecmp(parv[1], "STATS") || !strcasecmp(parv[1], "STATUS"))
{
sendnotice(client, "STATUS:");
sendnotice(client, "%s", ct_module_status_text());
} else if (!strcasecmp(parv[1], "OFF"))
{
if (ucounter->disabled == 1)
{
sendnotice(client, "Already OFF");
return;
}
ct_off(client);
} else if (!strcasecmp(parv[1], "ON"))
{
if (ucounter->disabled == 0)
{
sendnotice(client, "Already ON");
return;
}
ct_on(client);
} else if (!strcasecmp(parv[1], "RESET"))
{
ct_reset(client);
} else
{
sendnotice(client, "Unknown option '%s'", parv[1]);
ct_throttle_usage(client);
}
}
void ucounter_free(ModData *m)
{
safe_free(ucounter);
}
/* /STATS 9 (or /STATS connthrottle) — dumps module status, configured per-tier
* limits, and the IPv6 prefix bucket contents. Empty buckets are filtered.
*/
int stats_connthrottle(Client *client, const char *para)
{
int tier, i;
ConnThrottleBucket *b;
char ipbuf[64];
if (strcmp(para, "9") && strcasecmp(para, "connthrottle"))
return 0;
if (!ValidatePermissionsForPath("server:info:stats", client, NULL, NULL, NULL))
{
sendnumeric(client, ERR_NOPRIVILEGES);
return 0;
}
sendtxtnumeric(client, "%s", ct_module_status_text());
for (tier = 0; tier < CT_NUM_TIERS; tier++)
sendtxtnumeric(client, "Limit /%d = %d",
ct_tier_prefix[tier],
cfg.ipv6_unknown_users_limit[tier]);
sendtxtnumeric(client, "IPv6 prefix buckets:");
for (tier = 0; tier < CT_NUM_TIERS; tier++)
{
for (i = 0; i < CT_BUCKET_HASH_SIZE; i++)
{
for (b = ct_bucket_hash[tier][i]; b; b = b->next)
{
if (b->known_users == 0 && b->excepted_unknowns == 0 && b->unknown_users == 0)
continue;
if (!inet_ntop(AF_INET6, b->rawip, ipbuf, sizeof(ipbuf)))
strlcpy(ipbuf, "<invalid>", sizeof(ipbuf));
sendtxtnumeric(client, "%s/%d known=%d excepted=%d unknown=%d",
ipbuf, ct_tier_prefix[tier],
b->known_users, b->excepted_unknowns, b->unknown_users);
}
}
}
return 1;
}
#if defined(CONNTHROTTLE_CHECK)
static void ct_check_walk_one(Client *client)
{
ConnThrottleBucket *b;
ConnThrottleCategory category;
int tier;
char masked[16];
category = CT_CATEGORY(client);
if (category == CT_CATEGORY_NONE)
return; /* not in our buckets */
if (!IsIPV6(client) || !client->ip)
{
unreal_log(ULOG_ERROR, "connthrottle", "BUG_CT_CHECK_NO_IP", client,
"[BUG] connthrottle counter check: client has category but no IPv6 IP");
#ifdef DEBUGMODE
abort();
#endif
return;
}
for (tier = 0; tier < CT_NUM_TIERS; tier++)
{
ct_make_rawip(client, tier, masked);
b = ct_find_bucket(tier, masked);
if (!b)
{
unreal_log(ULOG_ERROR, "connthrottle", "BUG_CT_CHECK_NO_BUCKET", client,
"[BUG] connthrottle counter check: client has category but no bucket at /$prefix",
log_data_integer("prefix", ct_tier_prefix[tier]));
#ifdef DEBUGMODE
abort();
#endif
continue;
}
switch (category)
{
case CT_CATEGORY_KNOWN_USERS:
b->check_known++;
break;
case CT_CATEGORY_EXCEPTED_UNKNOWNS:
b->check_excepted++;
break;
case CT_CATEGORY_UNKNOWN_USERS:
b->check_unknown++;
break;
case CT_CATEGORY_NONE:
break; /* unreachable per filter */
}
}
}
EVENT(ct_check)
{
ConnThrottleBucket *b;
Client *client;
int tier, i;
for (tier = 0; tier < CT_NUM_TIERS; tier++)
for (i = 0; i < CT_BUCKET_HASH_SIZE; i++)
for (b = ct_bucket_hash[tier][i]; b; b = b->next)
b->check_known = b->check_excepted = b->check_unknown = 0;
list_for_each_entry(client, &client_list, client_node)
ct_check_walk_one(client);
list_for_each_entry(client, &unknown_list, lclient_node)
ct_check_walk_one(client);
for (tier = 0; tier < CT_NUM_TIERS; tier++)
{
for (i = 0; i < CT_BUCKET_HASH_SIZE; i++)
{
for (b = ct_bucket_hash[tier][i]; b; b = b->next)
{
if (b->check_known != b->known_users ||
b->check_excepted != b->excepted_unknowns ||
b->check_unknown != b->unknown_users)
{
unreal_log(ULOG_ERROR, "connthrottle", "BUG_CT_CHECK_DRIFT", NULL,
"[BUG] connthrottle bucket counter drift at /$prefix: "
"live=(known=$live_k excepted=$live_e unknown=$live_u) "
"computed=(known=$exp_k excepted=$exp_e unknown=$exp_u)",
log_data_integer("prefix", ct_tier_prefix[tier]),
log_data_integer("live_k", b->known_users),
log_data_integer("live_e", b->excepted_unknowns),
log_data_integer("live_u", b->unknown_users),
log_data_integer("exp_k", b->check_known),
log_data_integer("exp_e", b->check_excepted),
log_data_integer("exp_u", b->check_unknown));
#ifdef DEBUGMODE
abort();
#endif
}
}
}
}
}
#endif
/* ==================== RPC HANDLERS ==================== */
RPC_CALL_FUNC(rpc_connthrottle_status)
{
json_t *result, *counters, *config, *stats;
time_t start_delay_end;
int start_delay_remaining;
int reputation_gathering;
result = json_object();
/* Basic state */
json_object_set_new(result, "enabled", json_boolean(!ucounter->disabled));
json_object_set_new(result, "throttling_this_minute", json_boolean(ucounter->throttling_this_minute));
json_object_set_new(result, "throttling_previous_minute", json_boolean(ucounter->throttling_previous_minute));
/* Calculate state info */
start_delay_end = me.local->creationtime + cfg.start_delay;
if (start_delay_end > TStime())
start_delay_remaining = (int)(start_delay_end - TStime());
else
start_delay_remaining = 0;
reputation_gathering = still_reputation_gathering();
/* Determine overall state */
if (ucounter->disabled)
json_object_set_new(result, "state", json_string_unreal("disabled_by_oper"));
else if (start_delay_remaining > 0)
json_object_set_new(result, "state", json_string_unreal("start_delay"));
else if (reputation_gathering)
json_object_set_new(result, "state", json_string_unreal("reputation_gathering"));
else if (ucounter->throttling_this_minute)
json_object_set_new(result, "state", json_string_unreal("throttling"));
else
json_object_set_new(result, "state", json_string_unreal("monitoring"));
json_object_set_new(result, "start_delay_remaining", json_integer(start_delay_remaining));
json_object_set_new(result, "reputation_gathering", json_boolean(reputation_gathering));
/* Current counters */
counters = json_object();
json_object_set_new(counters, "local_count", json_integer(ucounter->local.count));
json_object_set_new(counters, "global_count", json_integer(ucounter->global.count));
if (ucounter->local.t > 0)
json_object_set_new(counters, "local_period_start", json_timestamp(ucounter->local.t));
if (ucounter->global.t > 0)
json_object_set_new(counters, "global_period_start", json_timestamp(ucounter->global.t));
json_object_set_new(result, "counters", counters);
/* Statistics for last minute */
stats = json_object();
json_object_set_new(stats, "rejected_clients", json_integer(ucounter->rejected_clients));
json_object_set_new(stats, "allowed_except", json_integer(ucounter->allowed_except));
json_object_set_new(stats, "allowed_unknown_users", json_integer(ucounter->allowed_unknown_users));
json_object_set_new(result, "stats_last_minute", stats);
/* Configuration */
config = json_object();
json_object_set_new(config, "local_throttle_count", json_integer(cfg.local.count));
json_object_set_new(config, "local_throttle_period", json_integer(cfg.local.period));
json_object_set_new(config, "global_throttle_count", json_integer(cfg.global.count));
json_object_set_new(config, "global_throttle_period", json_integer(cfg.global.period));
json_object_set_new(config, "start_delay", json_integer(cfg.start_delay));
json_expand_security_group(config, "except", cfg.except, 1);
json_object_set_new(result, "config", config);
rpc_response(client, request, result);
json_decref(result);
}
RPC_CALL_FUNC(rpc_connthrottle_set)
{
json_t *result;
int enabled;
const char *parv[3];
REQUIRE_PARAM_BOOLEAN("enabled", enabled);
if (enabled)
ct_on(client);
else
ct_off(client);
result = json_object();
json_object_set_new(result, "success", json_boolean(1));
json_object_set_new(result, "enabled", json_boolean(!ucounter->disabled));
rpc_response(client, request, result);
json_decref(result);
}
RPC_CALL_FUNC(rpc_connthrottle_reset)
{
json_t *result;
const char *parv[3];
ct_reset(client);
result = json_object();
json_object_set_new(result, "success", json_boolean(1));
rpc_response(client, request, result);
json_decref(result);
}
/* ===== IPv6 wider-prefix bucket tracking ===== */
/** Build the masked rawip used as the bucket key for this tier. */
static void ct_make_rawip(Client *client, int tier, char *out)
{
mask_ipv6_rawip(client->rawip, ct_tier_prefix[tier], out);
}
static uint64_t ct_hash_bucket(const char *masked)
{
return siphash_raw(masked, 16, siphashkey_ct_buckets) % CT_BUCKET_HASH_SIZE;
}
static ConnThrottleBucket *ct_find_bucket(int tier, const char *masked)
{
int hash = ct_hash_bucket(masked);
ConnThrottleBucket *p;
for (p = ct_bucket_hash[tier][hash]; p; p = p->next)
if (memcmp(p->rawip, masked, 16) == 0)
return p;
return NULL;
}
static ConnThrottleBucket *ct_add_bucket(int tier, const char *masked)
{
int hash = ct_hash_bucket(masked);
ConnThrottleBucket *n = safe_alloc(sizeof(ConnThrottleBucket));
memcpy(n->rawip, masked, 16);
AddListItem(n, ct_bucket_hash[tier][hash]);
return n;
}
static void ct_bucket_increment(ConnThrottleBucket *b, ConnThrottleCategory category)
{
switch (category)
{
case CT_CATEGORY_NONE:
break; /* not classified: no-op */
case CT_CATEGORY_KNOWN_USERS:
b->known_users++;
break;
case CT_CATEGORY_EXCEPTED_UNKNOWNS:
b->excepted_unknowns++;
break;
case CT_CATEGORY_UNKNOWN_USERS:
b->unknown_users++;
break;
}
}
static void ct_bucket_decrement(ConnThrottleBucket *b, ConnThrottleCategory category)
{
switch (category)
{
case CT_CATEGORY_NONE:
break; /* not classified: no-op */
case CT_CATEGORY_KNOWN_USERS:
b->known_users--;
break;
case CT_CATEGORY_EXCEPTED_UNKNOWNS:
b->excepted_unknowns--;
break;
case CT_CATEGORY_UNKNOWN_USERS:
b->unknown_users--;
break;
}
if ((b->known_users < 0) || (b->excepted_unknowns < 0) || (b->unknown_users < 0))
{
unreal_log(ULOG_ERROR, "connthrottle", "BUG_CT_NEGATIVE_COUNTER", NULL,
"[BUG] connthrottle bucket counter went negative: known=$known excepted=$excepted unknown=$unknown",
log_data_integer("known", b->known_users),
log_data_integer("excepted", b->excepted_unknowns),
log_data_integer("unknown", b->unknown_users));
#ifdef DEBUGMODE
abort();
#endif
}
}
/** Classify a client into one of CT_CATEGORY_*.
* Reads client->known_user_cached, the cfg.except SecurityGroup, and TKL
* exceptions of type maxperip. Does not modify state.
*/
static ConnThrottleCategory ct_classify(Client *client)
{
if (client->known_user_cached)
return CT_CATEGORY_KNOWN_USERS;
if (user_allowed_by_security_group(client, cfg.except) ||
find_tkl_exception(TKL_MAXPERIP, client))
return CT_CATEGORY_EXCEPTED_UNKNOWNS;
return CT_CATEGORY_UNKNOWN_USERS;
}
/** Bump the bucket counter for this client's category at all 3 tiers. */
static void ct_bucket_bump_client(Client *client, ConnThrottleCategory category)
{
int tier;
char masked[16];
ConnThrottleBucket *b;
if (!IsIPV6(client) || !client->ip)
return;
for (tier = 0; tier < CT_NUM_TIERS; tier++)
{
ct_make_rawip(client, tier, masked);
b = ct_find_bucket(tier, masked);
if (!b)
b = ct_add_bucket(tier, masked);
ct_bucket_increment(b, category);
}
}
/** Decrement the bucket counter for this client's category at all 3 tiers,
* freeing any bucket whose total reaches 0.
*/
static void ct_bucket_unbump_client(Client *client, ConnThrottleCategory category)
{
int tier;
char masked[16];
ConnThrottleBucket *b;
if (!IsIPV6(client) || !client->ip)
return;
for (tier = 0; tier < CT_NUM_TIERS; tier++)
{
ct_make_rawip(client, tier, masked);
b = ct_find_bucket(tier, masked);
if (!b)
{
unreal_log(ULOG_ERROR, "connthrottle", "BUG_CT_BUCKET_MISSING", client,
"[BUG] connthrottle bucket missing on disconnect for client $client.details");
#ifdef DEBUGMODE
abort();
#endif
continue;
}
ct_bucket_decrement(b, category);
if ((b->known_users == 0) && (b->excepted_unknowns == 0) && (b->unknown_users == 0))
{
DelListItem(b, ct_bucket_hash[tier][ct_hash_bucket(masked)]);
safe_free(b);
}
}
}
/** HOOKTYPE_ALLOW_CLIENT: classify the client, add to buckets, and
* reject if the unknown_users count for this client's category exceeds
* the limit at any tier. Multiple matched allow blocks may invoke this
* hook for the same client; the cached CT_CATEGORY (non-NONE means
* "already bucketed") prevents double-counting.
*/
const char *ct_allow_client(Client *client, ConfigItem_allow *aconf)
{
ConnThrottleCategory category;
int tier;
char masked[16];
ConnThrottleBucket *b;
int global_limit, effective_limit;
if (!IsIPV6(client) || !client->ip)
return NULL;
if (CT_CATEGORY(client) != CT_CATEGORY_NONE)
return NULL;
category = ct_classify(client);
CT_CATEGORY(client) = category;
ct_bucket_bump_client(client, category);
/* Limits fire only on unknown users; known + excepted bypass. */
if (category != CT_CATEGORY_UNKNOWN_USERS)
return NULL;
/* start-delay only applies to the rate-throttle, not here.
* The other two restrictions still apply.
*/
if (ucounter->disabled)
return NULL;
if (still_reputation_gathering())
return NULL;
/* effective_limit = max(global tier limit, allow::global-maxperip).
* A tier with limit 0 is disabled.
* We compare against allow::global-maxperip (not allow::maxperip) because
* our buckets count globally (local + remote), so the matched-scope cap
* is global-maxperip — same scope-pairing maxperip itself uses.
*/
for (tier = 0; tier < CT_NUM_TIERS; tier++)
{
global_limit = cfg.ipv6_unknown_users_limit[tier];
if (global_limit == 0)
continue;
effective_limit = global_limit;
if (aconf && aconf->global_maxperip > effective_limit)
effective_limit = aconf->global_maxperip;
ct_make_rawip(client, tier, masked);
b = ct_find_bucket(tier, masked);
if (b && (b->unknown_users > effective_limit))
{
unreal_log(ULOG_INFO, "connthrottle", "CONNTHROTTLE_IPV6_LIMIT", client,
"Client $client.name with IP $client.ip rejected: connthrottle ipv6-unknown-users-limit (cidr-$prefix_len, max $max) exceeded for $prefix_addr/$prefix_len ($unknown_users unknown / $excepted_users excepted / $known_users known)",
log_data_string("prefix_addr", format_ipv6_addr(masked)),
log_data_integer("prefix_len", ct_tier_prefix[tier]),
log_data_integer("max", effective_limit),
log_data_integer("unknown_users", b->unknown_users),
log_data_integer("excepted_users", b->excepted_unknowns),
log_data_integer("known_users", b->known_users));
return format_ipv6_prefix_reject_message(
iConf.reject_message_too_many_new_connections_ipv6_range,
masked, ct_tier_prefix[tier]);
}
}
return NULL;
}
/** HOOKTYPE_REMOTE_CONNECT: track remote IPv6 clients in the global buckets.
* No netmerge skip (unlike ct_rconnect): netmerge'd users are real concurrent
* presence, and our limits are state-based, not rate-based.
*/
int ct_remote_connect_buckets(Client *client)
{
ConnThrottleCategory category;
if (IsULine(client))
return 0; /* U-lined service: skip */
if (!IsIPV6(client) || !client->ip)
return 0;
if (CT_CATEGORY(client) != CT_CATEGORY_NONE)
return 0;
category = ct_classify(client);
CT_CATEGORY(client) = category;
ct_bucket_bump_client(client, category);
return 0;
}
int ct_quit(Client *client, MessageTag *mtags, const char *comment)
{
ConnThrottleCategory category = CT_CATEGORY(client);
if (category == CT_CATEGORY_NONE)
return 0;
ct_bucket_unbump_client(client, category);
CT_CATEGORY(client) = CT_CATEGORY_NONE;
return 0;
}
/** HOOKTYPE_KNOWN_USER_CACHE_CHANGE: when client->known_user_cached
* flips, our classification may change. Recompute and update the
* three bucket counters in place if the category actually changed.
*/
int ct_known_user_cache_change(Client *client)
{
ConnThrottleCategory old_category, new_category;
int tier;
char masked[16];
ConnThrottleBucket *b;
if (!IsIPV6(client) || !client->ip)
return 0;
old_category = CT_CATEGORY(client);
if (old_category == CT_CATEGORY_NONE)
return 0;
new_category = ct_classify(client);
if (new_category == old_category)
return 0;
for (tier = 0; tier < CT_NUM_TIERS; tier++)
{
ct_make_rawip(client, tier, masked);
b = ct_find_bucket(tier, masked);
if (!b)
{
unreal_log(ULOG_ERROR, "connthrottle", "BUG_CT_BUCKET_MISSING", client,
"[BUG] connthrottle bucket missing on transition for client $client.details");
#ifdef DEBUGMODE
abort();
#endif
continue;
}
ct_bucket_decrement(b, old_category);
ct_bucket_increment(b, new_category);
}
CT_CATEGORY(client) = new_category;
return 0;
}
/** Walk live clients and rebuild the three bucket hash tables.
* Called from MOD_LOAD; mirrors maxperip's rebuild pattern.
* Cost is negligible (a few tens of milliseconds even for ~10k clients).
*/
static void ct_buckets_rebuild(void)
{
Client *client;
ConnThrottleCategory category;
list_for_each_entry(client, &client_list, client_node)
{
if (!IsUser(client) || IsULine(client))
continue; /* only regular IRC users; skip servers, services, pre-registration */
if (!IsIPV6(client) || !client->ip)
continue;
category = ct_classify(client);
CT_CATEGORY(client) = category;
ct_bucket_bump_client(client, category);
}
list_for_each_entry(client, &unknown_list, lclient_node)
{
if (!IsUser(client) || IsULine(client))
continue; /* only regular IRC users; skip servers, services, pre-registration */
if (!IsIPV6(client) || !client->ip)
continue;
category = ct_classify(client);
CT_CATEGORY(client) = category;
ct_bucket_bump_client(client, category);
}
}
/** Free every bucket in every tier hash table, then free the tables themselves. */
static void ct_buckets_free(void)
{
int tier, i;
ConnThrottleBucket *p, *next;
for (tier = 0; tier < CT_NUM_TIERS; tier++)
{
if (!ct_bucket_hash[tier])
continue;
for (i = 0; i < CT_BUCKET_HASH_SIZE; i++)
{
for (p = ct_bucket_hash[tier][i]; p; p = next)
{
next = p->next;
safe_free(p);
}
ct_bucket_hash[tier][i] = NULL;
}
safe_free(ct_bucket_hash[tier]);
ct_bucket_hash[tier] = NULL;
}
}