1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-07-05 06:33:14 +02:00
Files
unrealircd/src/modules/maxperip.c
T
2026-05-13 15:40:50 +02:00

602 lines
16 KiB
C

/*
* Unreal Internet Relay Chat Daemon, src/modules/maxperip.c
* (C) 2025 Bram Matthys and the UnrealIRCd Team
*
* 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
* the Free Software Foundation; either version 1, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "unrealircd.h"
ModuleHeader MOD_HEADER
= {
"maxperip",
"2.0.0",
"Limit user connections based on ip address",
"UnrealIRCd Team",
"unrealircd-6",
};
/* Defines and macros */
#define IPUSERS_HASH_TABLE_SIZE 8192
/* Structs */
typedef struct IpUsersBucket IpUsersBucket;
struct IpUsersBucket
{
IpUsersBucket *prev, *next;
char rawip[16];
int local_clients;
int global_clients;
};
/* Variables */
IpUsersBucket **IpUsersHash_ipv4 = NULL;
IpUsersBucket **IpUsersHash_ipv6 = NULL;
char *siphashkey_ipusers = NULL;
/** set::known-cloud-services (enabled by default) */
static int known_cloud_services = 1; /* default: enabled */
/** IRCCloud gateway CIDRs.
* See https://www.irccloud.com/networks and https://www.irccloud.com/static/hosts.json
*/
static const char *irccloud_cidrs[] = {
"5.254.36.56/29",
"5.254.36.104/29",
"2a03:5180:f::/62",
"2a03:5180:f:4::/63",
"2a03:5180:f:6::/64",
NULL
};
/* Forward declarations */
int maxperip_config_test_allow(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int maxperip_config_run_allow(ConfigFile *cf, ConfigEntry *ce, int type, void *ptr);
int maxperip_config_test_set(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int maxperip_config_run_set(ConfigFile *cf, ConfigEntry *ce, int type);
void maxperip_postconf(void);
int exceeds_maxperip(Client *client, ConfigItem_allow *aconf);
IpUsersBucket *find_ipusers_bucket(Client *client);
IpUsersBucket *add_ipusers_bucket(Client *client);
void decrease_ipusers_bucket(Client *client);
int decrease_ipusers_bucket_wrapper(Client *client);
static void rebuild_ipusers_buckets(void);
static void free_ipusers_buckets(void);
static void add_known_cloud_services_exempts(void);
int stats_maxperip(Client *client, const char *para);
int maxperip_remote_connect(Client *client);
const char *maxperip_allow_client(Client *client, ConfigItem_allow *aconf);
int _get_connections_from_ip(Client *client);
MOD_TEST()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, maxperip_config_test_allow);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, maxperip_config_test_set);
EfunctionAdd(modinfo->handle, EFUNC_GET_CONNECTIONS_FROM_IP, _get_connections_from_ip);
return MOD_SUCCESS;
}
MOD_INIT()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
siphashkey_ipusers = safe_alloc(SIPHASH_KEY_LENGTH);
siphash_generate_key(siphashkey_ipusers);
IpUsersHash_ipv4 = safe_alloc(sizeof(IpUsersBucket *) * IPUSERS_HASH_TABLE_SIZE);
IpUsersHash_ipv6 = safe_alloc(sizeof(IpUsersBucket *) * IPUSERS_HASH_TABLE_SIZE);
known_cloud_services = 1; /* reset to default before CONFIGRUN may change it */
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN_EX, 0, maxperip_config_run_allow);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, maxperip_config_run_set);
HookAdd(modinfo->handle, HOOKTYPE_FREE_USER, 0, decrease_ipusers_bucket_wrapper);
HookAdd(modinfo->handle, HOOKTYPE_STATS, 0, stats_maxperip);
HookAdd(modinfo->handle, HOOKTYPE_REMOTE_CONNECT, 0, maxperip_remote_connect);
HookAddConstString(modinfo->handle, HOOKTYPE_ALLOW_CLIENT, 0, maxperip_allow_client);
return MOD_SUCCESS;
}
MOD_LOAD()
{
maxperip_postconf();
rebuild_ipusers_buckets();
add_known_cloud_services_exempts();
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
free_ipusers_buckets();
safe_free(siphashkey_ipusers);
return MOD_SUCCESS;
}
int maxperip_config_test_allow(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
int ext = 0;
if ((type != CONFIG_ALLOW_BLOCK) || !ce || !ce->name)
return 0;
if (!strcmp(ce->name, "maxperip") || !strcmp(ce->name, "global-maxperip"))
{
if (!ce->value)
{
config_error("%s:%i: missing parameter", ce->file->filename, ce->line_number);
errors++;
} else {
int v = atoi(ce->value);
if ((v <= 0) || (v > 1000000))
{
config_error("%s:%i: allow::%s with illegal value (must be 1-1000000)",
ce->file->filename, ce->line_number, ce->name);
errors++;
}
}
} else {
return 0; /* Unknown option for us */
}
*errs = errors;
return errors ? -1 : 1;
}
int maxperip_config_run_allow(ConfigFile *cf, ConfigEntry *ce, int type, void *ptr)
{
ConfigEntry *cep, *cepp;
ConfigItem_allow *allow = (ConfigItem_allow *)ptr;
if ((type != CONFIG_ALLOW_BLOCK) || !ce || !ce->name)
return 0;
if (!strcmp(ce->name, "maxperip"))
{
allow->maxperip = atoi(ce->value);
} else
if (!strcmp(ce->name, "global-maxperip"))
{
allow->global_maxperip = atoi(ce->value);
} else
{
return 0; /* Unknown option for us */
}
return 1; /* Handled */
}
void maxperip_postconf(void)
{
ConfigItem_allow *allow;
for (allow = conf_allow; allow; allow = allow->next)
{
/* Default: global-maxperip = maxperip+1 */
if (allow->global_maxperip == 0)
allow->global_maxperip = allow->maxperip+1;
/* global-maxperip < maxperip makes no sense */
if (allow->global_maxperip < allow->maxperip)
allow->global_maxperip = allow->maxperip;
}
}
/** Build the rawip used to identify this client's ipusers bucket.
*
* For IPv4: copies the 4 raw bytes of client->rawip.
* For IPv6: copies and masks client->rawip according to
* iConf.default_ipv6_clone_mask, so all addresses within the
* same /N share one bucket.
*
* The 'rawip' buffer must be at least 16 bytes (only first 4 used for IPv4).
*/
static void make_ipusers_rawip(Client *client, char *rawip)
{
if (IsIPV6(client))
mask_ipv6_rawip(client->rawip, iConf.default_ipv6_clone_mask, rawip);
else
memcpy(rawip, client->rawip, 4);
}
uint64_t hash_ipusers(Client *client, const char *rawip)
{
if (IsIPV6(client))
return siphash_raw(rawip, 16, siphashkey_ipusers) % IPUSERS_HASH_TABLE_SIZE;
else
return siphash_raw(rawip, 4, siphashkey_ipusers) % IPUSERS_HASH_TABLE_SIZE;
}
IpUsersBucket *find_ipusers_bucket(Client *client)
{
int hash;
IpUsersBucket *p;
char rawip[16];
make_ipusers_rawip(client, rawip);
hash = hash_ipusers(client, rawip);
if (IsIPV6(client))
{
for (p = IpUsersHash_ipv6[hash]; p; p = p->next)
if (memcmp(p->rawip, rawip, 16) == 0)
return p;
} else {
for (p = IpUsersHash_ipv4[hash]; p; p = p->next)
if (memcmp(p->rawip, rawip, 4) == 0)
return p;
}
return NULL;
}
/* (wrapper needed because hook has return type 'int' and function is 'void' */
int decrease_ipusers_bucket_wrapper(Client *client)
{
decrease_ipusers_bucket(client);
return 0;
}
IpUsersBucket *add_ipusers_bucket(Client *client)
{
int hash;
IpUsersBucket *n;
char rawip[16];
make_ipusers_rawip(client, rawip);
hash = hash_ipusers(client, rawip);
n = safe_alloc(sizeof(IpUsersBucket));
if (IsIPV6(client))
{
memcpy(n->rawip, rawip, 16);
AddListItem(n, IpUsersHash_ipv6[hash]);
} else {
memcpy(n->rawip, rawip, 4);
AddListItem(n, IpUsersHash_ipv4[hash]);
}
return n;
}
void decrease_ipusers_bucket(Client *client)
{
int hash;
IpUsersBucket *p;
char rawip[16];
if (!(client->flags & CLIENT_FLAG_IPUSERS_BUMPED))
return; /* nothing to do */
client->flags &= ~CLIENT_FLAG_IPUSERS_BUMPED;
make_ipusers_rawip(client, rawip);
hash = hash_ipusers(client, rawip);
if (IsIPV6(client))
{
for (p = IpUsersHash_ipv6[hash]; p; p = p->next)
if (memcmp(p->rawip, rawip, 16) == 0)
break;
} else {
for (p = IpUsersHash_ipv4[hash]; p; p = p->next)
if (memcmp(p->rawip, rawip, 4) == 0)
break;
}
if (!p)
{
unreal_log(ULOG_INFO, "user", "BUG_DECREASE_IPUSERS_BUCKET", client,
"[BUG] decrease_ipusers_bucket() called but bucket is gone for client $client.details");
return;
}
p->global_clients--;
if (MyConnect(client))
p->local_clients--;
if ((p->global_clients == 0) && (p->local_clients == 0))
{
if (IsIPV6(client))
DelListItem(p, IpUsersHash_ipv6[hash]);
else
DelListItem(p, IpUsersHash_ipv4[hash]);
safe_free(p);
}
}
/* Restore the buckets by walking current clients with the bumped flag.
* Cost is negligible (a few tens of milliseconds even for ~10k clients).
*/
static void rebuild_ipusers_buckets(void)
{
Client *client;
IpUsersBucket *bucket;
list_for_each_entry(client, &client_list, client_node)
{
if (!(client->flags & CLIENT_FLAG_IPUSERS_BUMPED))
continue;
if (!client->ip)
continue; /* defensive */
bucket = find_ipusers_bucket(client);
if (!bucket)
bucket = add_ipusers_bucket(client);
bucket->global_clients++;
if (MyConnect(client))
bucket->local_clients++;
}
list_for_each_entry(client, &unknown_list, lclient_node)
{
if (!(client->flags & CLIENT_FLAG_IPUSERS_BUMPED))
continue;
if (!client->ip)
continue;
bucket = find_ipusers_bucket(client);
if (!bucket)
bucket = add_ipusers_bucket(client);
bucket->global_clients++;
if (MyConnect(client))
bucket->local_clients++;
}
}
/* Free every bucket in both hash tables, then free the tables themselves. */
static void free_ipusers_buckets(void)
{
int i;
IpUsersBucket *p, *next;
for (i = 0; i < IPUSERS_HASH_TABLE_SIZE; i++)
{
for (p = IpUsersHash_ipv4[i]; p; p = next)
{
next = p->next;
safe_free(p);
}
IpUsersHash_ipv4[i] = NULL;
for (p = IpUsersHash_ipv6[i]; p; p = next)
{
next = p->next;
safe_free(p);
}
IpUsersHash_ipv6[i] = NULL;
}
safe_free(IpUsersHash_ipv4);
safe_free(IpUsersHash_ipv6);
}
int maxperip_config_test_set(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
if (type != CONFIG_SET)
return 0;
if (!strcmp(ce->name, "known-cloud-services"))
{
if (!ce->value)
{
config_error("%s:%i: set::known-cloud-services: no value specified",
ce->file->filename, ce->line_number);
errors++;
}
*errs = errors;
return errors ? -1 : 1;
}
return 0;
}
int maxperip_config_run_set(ConfigFile *cf, ConfigEntry *ce, int type)
{
if (type != CONFIG_SET)
return 0;
if (!strcmp(ce->name, "known-cloud-services"))
{
known_cloud_services = config_checkval(ce->value, CFG_YESNO);
return 1;
}
return 0;
}
/* Install default maxperip/connect-flood exception for IRC platforms
* that are so big that they are known to trip default maxperip restrictions
* (per IPv4 IP or per IPv6 /64: 3 local users, 4 network-wide users)
* on dozens of networks and that publish a stable list of IP ranges.
* Currently only IRCCloud qualifies for this.
* IRCCloud is in example conf since May 2023 (commit 82dbc4a29716) as:
* except ban { mask *.irccloud.com; type { maxperip; connect-flood; } }.
* Unfortunately DNS sometimes fails to resolve. We have seen this happen
* during an outage or server restart. People then mass-connect, but DNS is
* not fully working (yet), leading to unresolved hostnames.
* In May 2026 we added stricter maxperip treatment for /64 IPv6, and in
* connthrottle we added /56, /48 and /32 restrictions. Without these IP
* exceptions this would cause unwanted rejections.
*/
static void add_known_cloud_services_exempts(void)
{
int i;
if (!known_cloud_services)
return;
for (i = 0; irccloud_cidrs[i]; i++)
{
tkl_add_banexception(TKL_EXCEPTION, "*", irccloud_cidrs[i], NULL,
"IRCCloud default maxperip/connect-flood exemption", "-default-",
0, TStime(), 0, "mc", TKL_FLAG_CONFIG);
}
}
int stats_maxperip(Client *client, const char *para)
{
int i;
IpUsersBucket *e;
char ipbuf[256];
const char *ip;
/* '/STATS 8' or '/STATS maxperip' is for us... */
if (strcmp(para, "8") && strcasecmp(para, "maxperip"))
return 0;
if (!ValidatePermissionsForPath("server:info:stats",client,NULL,NULL,NULL))
{
sendnumeric(client, ERR_NOPRIVILEGES);
return 0;
}
sendtxtnumeric(client, "MaxPerIp IPv4 hash table:");
for (i=0; i < IPUSERS_HASH_TABLE_SIZE; i++)
{
for (e = IpUsersHash_ipv4[i]; e; e = e->next)
{
ip = inetntop(AF_INET, e->rawip, ipbuf, sizeof(ipbuf));
if (!ip)
ip = "<invalid>";
sendtxtnumeric(client, "IPv4 #%d %s: %d local / %d global",
i, ip, e->local_clients, e->global_clients);
}
}
sendtxtnumeric(client, "MaxPerIp IPv6 hash table:");
for (i=0; i < IPUSERS_HASH_TABLE_SIZE; i++)
{
for (e = IpUsersHash_ipv6[i]; e; e = e->next)
{
ip = inet_ntop(AF_INET6, e->rawip, ipbuf, sizeof(ipbuf));
if (!ip)
ip = "<invalid>";
sendtxtnumeric(client, "IPv6 #%d %s/%d: %d local / %d global",
i, ip, iConf.default_ipv6_clone_mask,
e->local_clients, e->global_clients);
}
}
return 1;
}
/** Returns 1 if allow::maxperip is exceeded by 'client' */
int exceeds_maxperip(Client *client, ConfigItem_allow *aconf)
{
Client *acptr;
IpUsersBucket *bucket;
if (!client->ip)
return 0; /* eg. services */
bucket = find_ipusers_bucket(client);
if (!bucket)
{
client->flags |= CLIENT_FLAG_IPUSERS_BUMPED;
bucket = add_ipusers_bucket(client);
bucket->global_clients = 1;
if (MyConnect(client))
bucket->local_clients = 1;
return 0;
}
/* Bump if we haven't done so yet
* (Actually not sure if this can ever be false, but...
* who knows with some 3rd party or some future change)
*/
if (!(client->flags & CLIENT_FLAG_IPUSERS_BUMPED))
{
bucket->global_clients++;
if (MyConnect(client))
bucket->local_clients++;
client->flags |= CLIENT_FLAG_IPUSERS_BUMPED;
}
if (find_tkl_exception(TKL_MAXPERIP, client))
return 0; /* exempt */
if (aconf)
{
if ((bucket->local_clients > aconf->maxperip) ||
(bucket->global_clients > aconf->global_maxperip))
{
return 1;
}
}
return 0;
}
/** Called for remote connects, to track global max restrictions */
int maxperip_remote_connect(Client *client)
{
exceeds_maxperip(client, NULL);
return 0;
}
/** Called from AllowClient(), to deal with restrictions */
const char *maxperip_allow_client(Client *client, ConfigItem_allow *aconf)
{
if (exceeds_maxperip(client, aconf))
{
IpUsersBucket *bucket = find_ipusers_bucket(client);
if (IsIPV6(client) && iConf.default_ipv6_clone_mask < 128)
{
char masked[16];
mask_ipv6_rawip(client->rawip, iConf.default_ipv6_clone_mask, masked);
if (bucket && bucket->local_clients > aconf->maxperip)
unreal_log(ULOG_INFO, "maxperip", "MAXPERIP_LIMIT", client,
"Client $client.name with IP $client.ip rejected: maxperip limit exceeded for $prefix_addr/$prefix_len ($count local, max $max)",
log_data_string("prefix_addr", format_ipv6_addr(masked)),
log_data_integer("prefix_len", iConf.default_ipv6_clone_mask),
log_data_integer("count", bucket->local_clients),
log_data_integer("max", aconf->maxperip));
else
unreal_log(ULOG_INFO, "maxperip", "MAXPERIP_LIMIT", client,
"Client $client.name with IP $client.ip rejected: maxperip limit exceeded for $prefix_addr/$prefix_len ($count global, max $max)",
log_data_string("prefix_addr", format_ipv6_addr(masked)),
log_data_integer("prefix_len", iConf.default_ipv6_clone_mask),
log_data_integer("count", bucket ? bucket->global_clients : 0),
log_data_integer("max", aconf->global_maxperip));
return format_ipv6_prefix_reject_message(
iConf.reject_message_too_many_connections_ipv6_range,
masked, iConf.default_ipv6_clone_mask);
}
if (bucket && bucket->local_clients > aconf->maxperip)
unreal_log(ULOG_INFO, "maxperip", "MAXPERIP_LIMIT", client,
"Client $client.name with IP $client.ip rejected: maxperip limit exceeded ($count local, max $max)",
log_data_integer("count", bucket->local_clients),
log_data_integer("max", aconf->maxperip));
else
unreal_log(ULOG_INFO, "maxperip", "MAXPERIP_LIMIT", client,
"Client $client.name with IP $client.ip rejected: maxperip limit exceeded ($count global, max $max)",
log_data_integer("count", bucket ? bucket->global_clients : 0),
log_data_integer("max", aconf->global_maxperip));
return iConf.reject_message_too_many_connections;
}
return NULL;
}
/** Return the number of connections from the same IP as 'client' */
int _get_connections_from_ip(Client *client)
{
IpUsersBucket *bucket;
if (!client->ip)
return 0;
bucket = find_ipusers_bucket(client);
if (!bucket)
return 0;
return bucket->global_clients;
}