pissircd/src/modules/connthrottle.c

635 lines
18 KiB
C

/*
* connthrottle - Connection throttler
* (C) Copyright 2004-2020 Bram Matthys (Syzop) and the UnrealIRCd team
* License: GPLv2
* See https://www.unrealircd.org/docs/Connthrottle
*/
#include "unrealircd.h"
#define CONNTHROTTLE_VERSION "1.3"
#ifndef CALLBACKTYPE_REPUTATION_STARTTIME
#define CALLBACKTYPE_REPUTATION_STARTTIME 5
#endif
ModuleHeader MOD_HEADER
= {
"connthrottle",
CONNTHROTTLE_VERSION,
"Connection throttler - by Syzop",
"UnrealIRCd Team",
"unrealircd-5",
};
typedef struct {
int count;
int period;
} ThrottleSetting;
struct cfgstruct {
/* set::connthrottle::known-users: */
ThrottleSetting local;
ThrottleSetting global;
/* set::connthrottle::new-users: */
int minimum_reputation_score;
int sasl_bypass;
int webirc_bypass;
/* set::connthrottle::disabled-when: */
long reputation_gathering;
int start_delay;
/* set::connthrottle (generic): */
char *reason;
};
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_score; /**< Number of allowed clients of type known-user */
int allowed_sasl; /**< Number of allowed clients of type SASL */
int allowed_webirc; /**< Number of allowed clients of type WEBIRC */
int allowed_other; /**< Number of allowed clients of type other (new) */
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);
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 */
safe_strdup(cfg.reason, "Throttled: Too many users trying to connect, please wait a while and try again");
cfg.minimum_reputation_score = 24;
cfg.sasl_bypass = 1;
cfg.webirc_bypass = 0;
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, ct_config_test);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, ct_config_posttest);
return MOD_SUCCESS;
}
MOD_INIT()
{
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);
CommandAdd(modinfo->handle, MSG_THROTTLE, ct_throttle, MAXPARA, CMD_USER|CMD_SERVER);
return MOD_SUCCESS;
}
MOD_LOAD()
{
EventAdd(modinfo->handle, "connthrottle_evt", connthrottle_evt, NULL, 1000, 0);
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
SavePersistentPointer(modinfo, ucounter);
safe_free(cfg.reason);
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;
}
#ifndef CheckNull
#define CheckNull(x) if ((!(x)->ce_vardata) || (!(*((x)->ce_vardata)))) { config_error("%s:%i: missing parameter", (x)->ce_fileptr->cf_filename, (x)->ce_varlinenum); errors++; continue; }
#endif
/** Test the set::connthrottle configuration */
int ct_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep, *cepp;
if (type != CONFIG_SET)
return 0;
/* We are only interrested in set::connthrottle.. */
if (!ce || !ce->ce_varname || strcmp(ce->ce_varname, "connthrottle"))
return 0;
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
{
if (!strcmp(cep->ce_varname, "known-users"))
{
for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next)
{
CheckNull(cepp);
if (!strcmp(cepp->ce_varname, "minimum-reputation-score"))
{
int cnt = atoi(cepp->ce_vardata);
if (cnt < 1)
{
config_error("%s:%i: set::connthrottle::known-users::minimum-reputation-score should be at least 1",
cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
errors++;
continue;
}
} else
if (!strcmp(cepp->ce_varname, "sasl-bypass"))
{
} else
if (!strcmp(cepp->ce_varname, "webirc-bypass"))
{
} else
{
config_error_unknown(cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum,
"set::connthrottle::known-users", cepp->ce_varname);
errors++;
}
}
} else
if (!strcmp(cep->ce_varname, "new-users"))
{
for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next)
{
CheckNull(cepp);
if (!strcmp(cepp->ce_varname, "local-throttle"))
{
int cnt, period;
if (!config_parse_flood(cepp->ce_vardata, &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->ce_fileptr->cf_filename, cepp->ce_varlinenum);
errors++;
continue;
}
} else
if (!strcmp(cepp->ce_varname, "global-throttle"))
{
int cnt, period;
if (!config_parse_flood(cepp->ce_vardata, &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->ce_fileptr->cf_filename, cepp->ce_varlinenum);
errors++;
continue;
}
} else
{
config_error_unknown(cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum,
"set::connthrottle::new-users", cepp->ce_varname);
errors++;
}
}
} else
if (!strcmp(cep->ce_varname, "disabled-when"))
{
for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next)
{
CheckNull(cepp);
if (!strcmp(cepp->ce_varname, "start-delay"))
{
int cnt = config_checkval(cepp->ce_vardata, CFG_TIME);
if ((cnt < 0) || (cnt > 3600))
{
config_error("%s:%i: set::connthrottle::disabled-when::start-delay should be in range 0-3600",
cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
errors++;
continue;
}
} else
if (!strcmp(cepp->ce_varname, "reputation-gathering"))
{
} else
{
config_error_unknown(cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum,
"set::connthrottle::disabled-when", cepp->ce_varname);
errors++;
}
}
} else
if (!strcmp(cep->ce_varname, "reason"))
{
CheckNull(cep);
} else
{
config_error("%s:%i: unknown directive set::connthrottle::%s",
cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
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;
if (type != CONFIG_SET)
return 0;
/* We are only interrested in set::connthrottle.. */
if (!ce || !ce->ce_varname || strcmp(ce->ce_varname, "connthrottle"))
return 0;
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
{
if (!strcmp(cep->ce_varname, "known-users"))
{
for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next)
{
if (!strcmp(cepp->ce_varname, "minimum-reputation-score"))
cfg.minimum_reputation_score = atoi(cepp->ce_vardata);
else if (!strcmp(cepp->ce_varname, "sasl-bypass"))
cfg.sasl_bypass = config_checkval(cepp->ce_vardata, CFG_YESNO);
else if (!strcmp(cepp->ce_varname, "webirc-bypass"))
cfg.webirc_bypass = config_checkval(cepp->ce_vardata, CFG_YESNO);
}
} else
if (!strcmp(cep->ce_varname, "new-users"))
{
for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next)
{
if (!strcmp(cepp->ce_varname, "local-throttle"))
config_parse_flood(cepp->ce_vardata, &cfg.local.count, &cfg.local.period);
else if (!strcmp(cepp->ce_varname, "global-throttle"))
config_parse_flood(cepp->ce_vardata, &cfg.global.count, &cfg.global.period);
}
} else
if (!strcmp(cep->ce_varname, "disabled-when"))
{
for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next)
{
if (!strcmp(cepp->ce_varname, "start-delay"))
cfg.start_delay = config_checkval(cepp->ce_vardata, CFG_TIME);
else if (!strcmp(cepp->ce_varname, "reputation-gathering"))
cfg.reputation_gathering = config_checkval(cepp->ce_vardata, CFG_TIME);
}
} else
if (!strcmp(cep->ce_varname, "reason"))
{
safe_free(cfg.reason);
cfg.reason = safe_alloc(strlen(cep->ce_vardata)+16);
sprintf(cfg.reason, "Throttled: %s", cep->ce_vardata);
}
}
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)
{
snprintf(buf, sizeof(buf),
"[ConnThrottle] Stats for this server past 60 secs: Connections rejected: %d. Accepted: %d known user(s), %d SASL, %d WEBIRC and %d new user(s).",
ucounter->rejected_clients,
ucounter->allowed_score,
ucounter->allowed_sasl,
ucounter->allowed_webirc,
ucounter->allowed_other);
sendto_realops("%s", buf);
ircd_log(LOG_ERROR, "%s", buf);
}
/* Reset stats for next message */
ucounter->rejected_clients = 0;
ucounter->allowed_score = 0;
ucounter->allowed_sasl = 0;
ucounter->allowed_webirc = 0;
ucounter->allowed_other = 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->firsttime + 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 (cfg.sasl_bypass && IsLoggedIn(client))
{
/* Allowed in: user authenticated using SASL */
return HOOK_CONTINUE;
}
if (cfg.webirc_bypass && moddata_client_get(client, "webirc"))
{
/* Allowed in: user using WEBIRC */
return HOOK_CONTINUE;
}
score = GetReputation(client);
if (score >= cfg.minimum_reputation_score)
{
/* Allowed in: IP has enough reputation ("known user") */
return HOOK_CONTINUE;
}
/* 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)
{
ircd_log(LOG_ERROR, "[ConnThrottle] Connection throttling has been ACTIVATED due to a HIGH CONNECTION RATE.");
sendto_realops("[ConnThrottle] Connection throttling has been ACTIVATED due to a HIGH CONNECTION RATE.");
sendto_realops("[ConnThrottle] Users with IP addresses that have not been seen before will be rejected above the set connection rate. Known users can still get in.");
sendto_realops("[ConnThrottle] For 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->firsttime + 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 (cfg.sasl_bypass && IsLoggedIn(client))
{
/* Allowed in: user authenticated using SASL */
ucounter->allowed_sasl++;
return 0;
}
if (cfg.webirc_bypass && moddata_client_get(client, "webirc"))
{
/* Allowed in: user using WEBIRC */
ucounter->allowed_webirc++;
return 0;
}
score = GetReputation(client);
if (score >= cfg.minimum_reputation_score)
{
/* Allowed in: IP has enough reputation ("known user") */
ucounter->allowed_score++;
return 0;
}
/* Allowed NEW user */
ucounter->allowed_other++;
bump_connect_counter(1);
return 0;
}
int ct_rconnect(Client *client)
{
int score;
if (client->srvptr && !IsSynched(client->srvptr))
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->srvptr && client->srvptr->serv && client->srvptr->serv->boottime &&
(TStime() - client->srvptr->serv->boottime < cfg.start_delay))
{
return 0;
}
#endif
score = GetReputation(client);
if (score >= cfg.minimum_reputation_score)
return 0; /* sufficient reputation: "known-user" */
bump_connect_counter(0);
return 0;
}
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.");
}
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:");
if (ucounter->disabled)
{
sendnotice(client, "Module DISABLED on oper request. To re-enable, type: /THROTTLE ON");
} else {
if (still_reputation_gathering())
{
sendnotice(client, "Module DISABLED because the 'reputation' module has not gathered enough data yet (set::connthrottle::disabled-when::reputation-gathering).");
} else
if (me.local->firsttime + cfg.start_delay > TStime())
{
sendnotice(client, "Module DISABLED due to start-delay (set::connthrottle::disabled-when::start-delay), will be enabled in %lld second(s).",
(long long)((me.local->firsttime + cfg.start_delay) - TStime()));
} else
{
sendnotice(client, "Module ENABLED");
}
}
} else
if (!strcasecmp(parv[1], "OFF"))
{
if (ucounter->disabled == 1)
{
sendnotice(client, "Already OFF");
return;
}
ucounter->disabled = 1;
sendto_realops("[connthrottle] %s (%s@%s) DISABLED the connthrottle module.",
client->name, client->user->username, client->user->realhost);
} else
if (!strcasecmp(parv[1], "ON"))
{
if (ucounter->disabled == 0)
{
sendnotice(client, "Already ON");
return;
}
sendto_realops("[connthrottle] %s (%s@%s) ENABLED the connthrottle module.",
client->name, client->user->username, client->user->realhost);
ucounter->disabled = 0;
} else
if (!strcasecmp(parv[1], "RESET"))
{
memset(ucounter, 0, sizeof(UCounter));
sendto_realops("[connthrottle] %s (%s@%s) did a RESET on the stats/counters!!",
client->name, client->user->username, client->user->realhost);
} else
{
sendnotice(client, "Unknown option '%s'", parv[1]);
ct_throttle_usage(client);
}
}
void ucounter_free(ModData *m)
{
safe_free(ucounter);
}