unrealircd

- supernets unrealircd source & configuration
git clone git://git.acid.vegas/unrealircd.git
Log | Files | Refs | Archive | README | LICENSE

connthrottle.c (17346B)

      1 /*
      2  * connthrottle - Connection throttler
      3  * (C) Copyright 2004-2020 Bram Matthys (Syzop) and the UnrealIRCd team
      4  * License: GPLv2 or later
      5  * See https://www.unrealircd.org/docs/Connthrottle
      6  */
      7 
      8 #include "unrealircd.h"
      9 
     10 #define CONNTHROTTLE_VERSION "1.3"
     11 
     12 #ifndef CALLBACKTYPE_REPUTATION_STARTTIME
     13  #define CALLBACKTYPE_REPUTATION_STARTTIME 5
     14 #endif
     15 
     16 ModuleHeader MOD_HEADER
     17   = {
     18 	"connthrottle",
     19 	CONNTHROTTLE_VERSION,
     20 	"Connection throttler - by Syzop",
     21 	"UnrealIRCd Team",
     22 	"unrealircd-6",
     23     };
     24 
     25 typedef struct {
     26 	int count;
     27 	int period;
     28 } ThrottleSetting;
     29 
     30 struct cfgstruct {
     31 	/* set::connthrottle::known-users: */
     32 	ThrottleSetting local;
     33 	ThrottleSetting global;
     34 	/* set::connthrottle::except: */
     35 	SecurityGroup *except;
     36 	/* set::connthrottle::disabled-when: */
     37 	long reputation_gathering;
     38 	int start_delay;
     39 	/* set::connthrottle (generic): */
     40 	char *reason;
     41 };
     42 static struct cfgstruct cfg;
     43 
     44 typedef struct {
     45 	int count;
     46 	long t;
     47 } ThrottleCounter;
     48 
     49 typedef struct UCounter UCounter;
     50 struct UCounter {
     51 	ThrottleCounter local;		/**< Local counter */
     52 	ThrottleCounter global;		/**< Global counter */
     53 	int rejected_clients;		/**< Number of rejected clients this minute */
     54 	int allowed_except;		/**< Number of allowed clients - on except list */
     55 	int allowed_unknown_users;	/**< Number of allowed clients - not on except list */
     56 	char disabled;			/**< Module disabled by oper? */
     57 	int throttling_this_minute;	/**< Did we do any throttling this minute? */
     58 	int throttling_previous_minute;	/**< Did we do any throttling previous minute? */
     59 	int throttling_banner_displayed;/**< Big we-are-now-throttling banner displayed? */
     60 	time_t next_event;		/**< When is next event? (for "last 60 seconds" stats) */
     61 };
     62 UCounter *ucounter = NULL;
     63 
     64 #define MSG_THROTTLE "THROTTLE"
     65 
     66 /* Forward declarations */
     67 int ct_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
     68 int ct_config_posttest(int *errs);
     69 int ct_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
     70 int ct_pre_lconnect(Client *client);
     71 int ct_lconnect(Client *);
     72 int ct_rconnect(Client *);
     73 CMD_FUNC(ct_throttle);
     74 EVENT(connthrottle_evt);
     75 void ucounter_free(ModData *m);
     76 
     77 MOD_TEST()
     78 {
     79 	memset(&cfg, 0, sizeof(cfg));
     80 	
     81 	/* Defaults: */
     82 	cfg.local.count = 20; cfg.local.period = 60;
     83 	cfg.global.count = 30; cfg.global.period = 60;
     84 	cfg.start_delay = 180;		/* 3 minutes */
     85 	safe_strdup(cfg.reason, "Throttled: Too many users trying to connect, please wait a while and try again");
     86 	cfg.except = safe_alloc(sizeof(SecurityGroup));
     87 	cfg.except->reputation_score = 24;
     88 	cfg.except->identified = 1;
     89 	cfg.except->webirc = 0;
     90 
     91 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, ct_config_test);
     92 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, ct_config_posttest);
     93 	return MOD_SUCCESS;
     94 }
     95 
     96 MOD_INIT()
     97 {
     98 	MARK_AS_OFFICIAL_MODULE(modinfo);
     99 	LoadPersistentPointer(modinfo, ucounter, ucounter_free);
    100 	if (!ucounter)
    101 		ucounter = safe_alloc(sizeof(UCounter));
    102 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, ct_config_run);
    103 	HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 0, ct_pre_lconnect);
    104 	HookAdd(modinfo->handle, HOOKTYPE_LOCAL_CONNECT, 0, ct_lconnect);
    105 	HookAdd(modinfo->handle, HOOKTYPE_REMOTE_CONNECT, 0, ct_rconnect);
    106 	CommandAdd(modinfo->handle, MSG_THROTTLE, ct_throttle, MAXPARA, CMD_USER|CMD_SERVER);
    107 	return MOD_SUCCESS;
    108 }
    109 
    110 MOD_LOAD()
    111 {
    112 	EventAdd(modinfo->handle, "connthrottle_evt", connthrottle_evt, NULL, 1000, 0);
    113 	return MOD_SUCCESS;
    114 }
    115 
    116 MOD_UNLOAD()
    117 {
    118 	SavePersistentPointer(modinfo, ucounter);
    119 	safe_free(cfg.reason);
    120 	free_security_group(cfg.except);
    121 	return MOD_SUCCESS;
    122 }
    123 
    124 /** This function checks if the reputation module is loaded.
    125  * If not, then the module will error, since we depend on it.
    126  */
    127 int ct_config_posttest(int *errs)
    128 {
    129 	int errors = 0;
    130 
    131 	/* Note: we use Callbacks[] here, but this is only for checking. Don't
    132 	 * let this confuse you. At any other place you must use RCallbacks[].
    133 	 */
    134 	if (Callbacks[CALLBACKTYPE_REPUTATION_STARTTIME] == NULL)
    135 	{
    136 		config_error("The 'connthrottle' module requires the 'reputation' "
    137 		             "module to be loaded as well.");
    138 		config_error("Add the following to your configuration file: "
    139 		             "loadmodule \"reputation\";");
    140 		errors++;
    141 	}
    142 
    143 	*errs = errors;
    144 	return errors ? -1 : 1;
    145 }
    146 
    147 /** Test the set::connthrottle configuration */
    148 int ct_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
    149 {
    150 	int errors = 0;
    151 	ConfigEntry *cep, *cepp;
    152 
    153 	if (type != CONFIG_SET)
    154 		return 0;
    155 	
    156 	/* We are only interrested in set::connthrottle.. */
    157 	if (!ce || !ce->name || strcmp(ce->name, "connthrottle"))
    158 		return 0;
    159 	
    160 	for (cep = ce->items; cep; cep = cep->next)
    161 	{
    162 		if (!strcmp(cep->name, "except"))
    163 		{
    164 			test_match_block(cf, cep, &errors);
    165 		} else
    166 		if (!strcmp(cep->name, "known-users"))
    167 		{
    168 			for (cepp = cep->items; cepp; cepp = cepp->next)
    169 			{
    170 				CheckNull(cepp);
    171 				if (!strcmp(cepp->name, "minimum-reputation-score"))
    172 				{
    173 					int cnt = atoi(cepp->value);
    174 					if (cnt < 1)
    175 					{
    176 						config_error("%s:%i: set::connthrottle::known-users::minimum-reputation-score should be at least 1",
    177 							cepp->file->filename, cepp->line_number);
    178 						errors++;
    179 						continue;
    180 					}
    181 				} else
    182 				if (!strcmp(cepp->name, "sasl-bypass"))
    183 				{
    184 				} else
    185 				if (!strcmp(cepp->name, "webirc-bypass"))
    186 				{
    187 				} else
    188 				{
    189 					config_error_unknown(cepp->file->filename, cepp->line_number,
    190 					                     "set::connthrottle::known-users", cepp->name);
    191 					errors++;
    192 				}
    193 			}
    194 		} else
    195 		if (!strcmp(cep->name, "new-users"))
    196 		{
    197 			for (cepp = cep->items; cepp; cepp = cepp->next)
    198 			{
    199 				CheckNull(cepp);
    200 				if (!strcmp(cepp->name, "local-throttle"))
    201 				{
    202 					int cnt, period;
    203 					if (!config_parse_flood(cepp->value, &cnt, &period) ||
    204 					    (cnt < 1) || (cnt > 2000000000) || (period > 2000000000))
    205 					{
    206 						config_error("%s:%i: set::connthrottle::new-users::local-throttle error. "
    207 							     "Syntax is <count>:<period> (eg 6:60), "
    208 							     "and count and period should be non-zero.",
    209 							     cepp->file->filename, cepp->line_number);
    210 						errors++;
    211 						continue;
    212 					}
    213 				} else
    214 				if (!strcmp(cepp->name, "global-throttle"))
    215 				{
    216 					int cnt, period;
    217 					if (!config_parse_flood(cepp->value, &cnt, &period) ||
    218 					    (cnt < 1) || (cnt > 2000000000) || (period > 2000000000))
    219 					{
    220 						config_error("%s:%i: set::connthrottle::new-users::global-throttle error. "
    221 							     "Syntax is <count>:<period> (eg 6:60), "
    222 							     "and count and period should be non-zero.",
    223 							     cepp->file->filename, cepp->line_number);
    224 						errors++;
    225 						continue;
    226 					}
    227 				} else
    228 				{
    229 					config_error_unknown(cepp->file->filename, cepp->line_number,
    230 					                     "set::connthrottle::new-users", cepp->name);
    231 					errors++;
    232 				}
    233 			}
    234 		} else
    235 		if (!strcmp(cep->name, "disabled-when"))
    236 		{
    237 			for (cepp = cep->items; cepp; cepp = cepp->next)
    238 			{
    239 				CheckNull(cepp);
    240 				if (!strcmp(cepp->name, "start-delay"))
    241 				{
    242 					int cnt = config_checkval(cepp->value, CFG_TIME);
    243 					if ((cnt < 0) || (cnt > 3600))
    244 					{
    245 						config_error("%s:%i: set::connthrottle::disabled-when::start-delay should be in range 0-3600",
    246 							cepp->file->filename, cepp->line_number);
    247 						errors++;
    248 						continue;
    249 					}
    250 				} else
    251 				if (!strcmp(cepp->name, "reputation-gathering"))
    252 				{
    253 				} else
    254 				{
    255 					config_error_unknown(cepp->file->filename, cepp->line_number,
    256 					                     "set::connthrottle::disabled-when", cepp->name);
    257 					errors++;
    258 				}
    259 			}
    260 		} else
    261 		if (!strcmp(cep->name, "reason"))
    262 		{
    263 			CheckNull(cep);
    264 		} else
    265 		{
    266 			config_error("%s:%i: unknown directive set::connthrottle::%s",
    267 				cep->file->filename, cep->line_number, cep->name);
    268 			errors++;
    269 			continue;
    270 		}
    271 	}
    272 	
    273 	*errs = errors;
    274 	return errors ? -1 : 1;
    275 }
    276 
    277 /* Configure ourselves based on the set::connthrottle settings */
    278 int ct_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
    279 {
    280 	ConfigEntry *cep, *cepp;
    281 
    282 	if (type != CONFIG_SET)
    283 		return 0;
    284 	
    285 	/* We are only interrested in set::connthrottle.. */
    286 	if (!ce || !ce->name || strcmp(ce->name, "connthrottle"))
    287 		return 0;
    288 	
    289 	for (cep = ce->items; cep; cep = cep->next)
    290 	{
    291 		if (!strcmp(cep->name, "except"))
    292 		{
    293 			conf_match_block(cf, cep, &cfg.except);
    294 		} else
    295 		if (!strcmp(cep->name, "known-users"))
    296 		{
    297 			for (cepp = cep->items; cepp; cepp = cepp->next)
    298 			{
    299 				if (!strcmp(cepp->name, "minimum-reputation-score"))
    300 					cfg.except->reputation_score = atoi(cepp->value);
    301 				else if (!strcmp(cepp->name, "sasl-bypass"))
    302 					cfg.except->identified = config_checkval(cepp->value, CFG_YESNO);
    303 				else if (!strcmp(cepp->name, "webirc-bypass"))
    304 					cfg.except->webirc = config_checkval(cepp->value, CFG_YESNO);
    305 			}
    306 		} else
    307 		if (!strcmp(cep->name, "new-users"))
    308 		{
    309 			for (cepp = cep->items; cepp; cepp = cepp->next)
    310 			{
    311 				if (!strcmp(cepp->name, "local-throttle"))
    312 					config_parse_flood(cepp->value, &cfg.local.count, &cfg.local.period);
    313 				else if (!strcmp(cepp->name, "global-throttle"))
    314 					config_parse_flood(cepp->value, &cfg.global.count, &cfg.global.period);
    315 			}
    316 		} else
    317 		if (!strcmp(cep->name, "disabled-when"))
    318 		{
    319 			for (cepp = cep->items; cepp; cepp = cepp->next)
    320 			{
    321 				if (!strcmp(cepp->name, "start-delay"))
    322 					cfg.start_delay = config_checkval(cepp->value, CFG_TIME);
    323 				else if (!strcmp(cepp->name, "reputation-gathering"))
    324 					cfg.reputation_gathering = config_checkval(cepp->value, CFG_TIME);
    325 			}
    326 		} else
    327 		if (!strcmp(cep->name, "reason"))
    328 		{
    329 			safe_free(cfg.reason);
    330 			cfg.reason = safe_alloc(strlen(cep->value)+16);
    331 			sprintf(cfg.reason, "Throttled: %s", cep->value);
    332 		}
    333 	}
    334 	return 1;
    335 }
    336 
    337 /** Returns 1 if the 'reputation' module is still gathering
    338  * data, such as in the first week of when it is loaded.
    339  * This behavior is configured via set::disabled-when::reputation-gathering
    340  */
    341 int still_reputation_gathering(void)
    342 {
    343 	int v;
    344 
    345 	if (RCallbacks[CALLBACKTYPE_REPUTATION_STARTTIME] == NULL)
    346 		return 1; /* Reputation module not loaded, disable us */
    347 
    348 	v = RCallbacks[CALLBACKTYPE_REPUTATION_STARTTIME]->func.intfunc();
    349 
    350 	if (TStime() - v < cfg.reputation_gathering)
    351 		return 1; /* Still gathering reputation data (eg: first week) */
    352 
    353 	return 0;
    354 }
    355 
    356 EVENT(connthrottle_evt)
    357 {
    358 	char buf[512];
    359 
    360 	if (ucounter->next_event > TStime())
    361 		return;
    362 	ucounter->next_event = TStime() + 60;
    363 
    364 	if (ucounter->rejected_clients)
    365 	{
    366 		unreal_log(ULOG_INFO, "connthrottle", "CONNTHROTLE_REPORT", NULL,
    367 		           "ConnThrottle] Stats for this server past 60 secs: "
    368 		           "Connections rejected: $num_rejected. "
    369 		           "Accepted: $num_accepted_except except user(s) and "
    370 		           "$num_accepted_unknown_users new user(s).",
    371 		           log_data_integer("num_rejected", ucounter->rejected_clients),
    372 		           log_data_integer("num_accepted_except", ucounter->allowed_except),
    373 		           log_data_integer("num_accepted_unknown_users", ucounter->allowed_unknown_users));
    374 	}
    375 
    376 	/* Reset stats for next message */
    377 	ucounter->rejected_clients = 0;
    378 	ucounter->allowed_except = 0;
    379 	ucounter->allowed_unknown_users = 0;
    380 
    381 	ucounter->throttling_previous_minute = ucounter->throttling_this_minute;
    382 	ucounter->throttling_this_minute = 0; /* reset */
    383 	ucounter->throttling_banner_displayed = 0; /* reset */
    384 }
    385 
    386 #define THROT_LOCAL 1
    387 #define THROT_GLOBAL 2
    388 int ct_pre_lconnect(Client *client)
    389 {
    390 	int throttle=0;
    391 	int score;
    392 
    393 	if (me.local->creationtime + cfg.start_delay > TStime())
    394 		return HOOK_CONTINUE; /* no throttle: start delay */
    395 
    396 	if (ucounter->disabled)
    397 		return HOOK_CONTINUE; /* protection disabled: allow user */
    398 
    399 	if (still_reputation_gathering())
    400 		return HOOK_CONTINUE; /* still gathering reputation data */
    401 
    402 	if (user_allowed_by_security_group(client, cfg.except))
    403 		return HOOK_CONTINUE; /* allowed: user is exempt (known user or otherwise) */
    404 
    405 	/* If we reach this then the user is NEW */
    406 
    407 	/* +1 global client would reach global limit? */
    408 	if ((TStime() - ucounter->global.t < cfg.global.period) && (ucounter->global.count+1 > cfg.global.count))
    409 		throttle |= THROT_GLOBAL;
    410 
    411 	/* +1 local client would reach local limit? */
    412 	if ((TStime() - ucounter->local.t < cfg.local.period) && (ucounter->local.count+1 > cfg.local.count))
    413 		throttle |= THROT_LOCAL;
    414 
    415 	if (throttle)
    416 	{
    417 		ucounter->throttling_this_minute = 1;
    418 		ucounter->rejected_clients++;
    419 		/* We send the LARGE banner if throttling was activated */
    420 		if (!ucounter->throttling_previous_minute && !ucounter->throttling_banner_displayed)
    421 		{
    422 			unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTLE_ACTIVATED", NULL,
    423 			           "[ConnThrottle] Connection throttling has been ACTIVATED due to a HIGH CONNECTION RATE.\n"
    424 			           "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"
    425 			           "or more information see https://www.unrealircd.org/docs/ConnThrottle");
    426 			ucounter->throttling_banner_displayed = 1;
    427 		}
    428 		exit_client(client, NULL, cfg.reason);
    429 		return HOOK_DENY;
    430 	}
    431 
    432 	return HOOK_CONTINUE;
    433 }
    434 
    435 /** Increase the connect counter(s), nothing else. */
    436 void bump_connect_counter(int local_connect)
    437 {
    438 	if (local_connect)
    439 	{
    440 		/* Bump local connect counter */
    441 		if (TStime() - ucounter->local.t >= cfg.local.period)
    442 		{
    443 			ucounter->local.t = TStime();
    444 			ucounter->local.count = 1;
    445 		} else {
    446 			ucounter->local.count++;
    447 		}
    448 	}
    449 
    450 	/* Bump global connect counter */
    451 	if (TStime() - ucounter->global.t >= cfg.global.period)
    452 	{
    453 		ucounter->global.t = TStime();
    454 		ucounter->global.count = 1;
    455 	} else {
    456 		ucounter->global.count++;
    457 	}
    458 }
    459 
    460 int ct_lconnect(Client *client)
    461 {
    462 	int score;
    463 
    464 	if (me.local->creationtime + cfg.start_delay > TStime())
    465 		return 0; /* no throttle: start delay */
    466 
    467 	if (ucounter->disabled)
    468 		return 0; /* protection disabled: allow user */
    469 
    470 	if (still_reputation_gathering())
    471 		return 0; /* still gathering reputation data */
    472 
    473 	if (user_allowed_by_security_group(client, cfg.except))
    474 	{
    475 		ucounter->allowed_except++;
    476 		return HOOK_CONTINUE; /* allowed: user is exempt (known user or otherwise) */
    477 	}
    478 
    479 	/* Allowed NEW user */
    480 	ucounter->allowed_unknown_users++;
    481 
    482 	bump_connect_counter(1);
    483 
    484 	return 0;
    485 }
    486 
    487 int ct_rconnect(Client *client)
    488 {
    489 	int score;
    490 
    491 	if (client->uplink && !IsSynched(client->uplink))
    492 		return 0; /* Netmerge: skip */
    493 
    494 	if (IsULine(client))
    495 		return 0; /* U:lined, such as services: skip */
    496 
    497 #if UNREAL_VERSION_TIME >= 201915
    498 	/* On UnrealIRCd 4.2.3+ we can see the boot time (start time)
    499 	 * of the remote server. This way we can apply the
    500 	 * set::disabled-when::start-delay restriction on remote
    501 	 * servers as well.
    502 	 */
    503 	if (client->uplink && client->uplink->server && client->uplink->server->boottime &&
    504 	    (TStime() - client->uplink->server->boottime < cfg.start_delay))
    505 	{
    506 		return 0;
    507 	}
    508 #endif
    509 
    510 	if (user_allowed_by_security_group(client, cfg.except))
    511 		return 0; /* user is on except list (known user or otherwise) */
    512 
    513 	bump_connect_counter(0);
    514 
    515 	return 0;
    516 }
    517 
    518 static void ct_throttle_usage(Client *client)
    519 {
    520 	sendnotice(client, "Usage: /THROTTLE [ON|OFF|STATUS|RESET]");
    521 	sendnotice(client, " ON:     Enabled protection");
    522 	sendnotice(client, " OFF:    Disables protection");
    523 	sendnotice(client, " STATUS: Status report");
    524 	sendnotice(client, " RESET:  Resets all counters(&more)");
    525 	sendnotice(client, "NOTE: All commands only affect this server. Remote servers are not affected.");
    526 }
    527 
    528 CMD_FUNC(ct_throttle)
    529 {
    530 	if (!IsOper(client))
    531 	{
    532 		sendnumeric(client, ERR_NOPRIVILEGES);
    533 		return;
    534 	}
    535 
    536 	if ((parc < 2) || BadPtr(parv[1]))
    537 	{
    538 		ct_throttle_usage(client);
    539 		return;
    540 	}
    541 
    542 	if (!strcasecmp(parv[1], "STATS") || !strcasecmp(parv[1], "STATUS"))
    543 	{
    544 		sendnotice(client, "STATUS:");
    545 		if (ucounter->disabled)
    546 		{
    547 			sendnotice(client, "Module DISABLED on oper request. To re-enable, type: /THROTTLE ON");
    548 		} else {
    549 			if (still_reputation_gathering())
    550 			{
    551 				sendnotice(client, "Module DISABLED because the 'reputation' module has not gathered enough data yet (set::connthrottle::disabled-when::reputation-gathering).");
    552 			} else
    553 			if (me.local->creationtime + cfg.start_delay > TStime())
    554 			{
    555 				sendnotice(client, "Module DISABLED due to start-delay (set::connthrottle::disabled-when::start-delay), will be enabled in %lld second(s).",
    556 					(long long)((me.local->creationtime + cfg.start_delay) - TStime()));
    557 			} else
    558 			{
    559 				sendnotice(client, "Module ENABLED");
    560 			}
    561 		}
    562 	} else 
    563 	if (!strcasecmp(parv[1], "OFF"))
    564 	{
    565 		if (ucounter->disabled == 1)
    566 		{
    567 			sendnotice(client, "Already OFF");
    568 			return;
    569 		}
    570 		ucounter->disabled = 1;
    571 		unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTLE_MODULE_DISABLED", client,
    572 			   "[ConnThrottle] $client.details DISABLED the connthrottle module.");
    573 	} else
    574 	if (!strcasecmp(parv[1], "ON"))
    575 	{
    576 		if (ucounter->disabled == 0)
    577 		{
    578 			sendnotice(client, "Already ON");
    579 			return;
    580 		}
    581 		unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTLE_MODULE_ENABLED", client,
    582 			   "[ConnThrottle] $client.details ENABLED the connthrottle module.");
    583 		ucounter->disabled = 0;
    584 	} else
    585 	if (!strcasecmp(parv[1], "RESET"))
    586 	{
    587 		memset(ucounter, 0, sizeof(UCounter));
    588 		unreal_log(ULOG_WARNING, "connthrottle", "CONNTHROTLE_RESET", client,
    589 			   "[ConnThrottle] $client.details did a RESET on the statistics/counters.");
    590 	} else
    591 	{
    592 		sendnotice(client, "Unknown option '%s'", parv[1]);
    593 		ct_throttle_usage(client);
    594 	}
    595 }
    596 
    597 void ucounter_free(ModData *m)
    598 {
    599 	safe_free(ucounter);
    600 }