/* * 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", "1.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; /* 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); void maxperip_postconf(void); int exceeds_maxperip(Client *client, ConfigItem_allow *aconf); void siphashkey_ipusers_free(ModData *m); void ipusershash_free_4(ModData *m); void ipusershash_free_6(ModData *m); IpUsersBucket *add_ipusers_bucket(Client *client); void decrease_ipusers_bucket(Client *client); int decrease_ipusers_bucket_wrapper(Client *client); int stats_maxperip(Client *client, const char *para); int maxperip_remote_connect(Client *client); const char *maxperip_allow_client(Client *client, ConfigItem_allow *aconf); MOD_TEST() { MARK_AS_OFFICIAL_MODULE(modinfo); HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, maxperip_config_test_allow); return MOD_SUCCESS; } MOD_INIT() { MARK_AS_OFFICIAL_MODULE(modinfo); LoadPersistentPointer(modinfo, siphashkey_ipusers, siphashkey_ipusers_free); if (!siphashkey_ipusers) { siphashkey_ipusers = safe_alloc(SIPHASH_KEY_LENGTH); siphash_generate_key(siphashkey_ipusers); } LoadPersistentPointer(modinfo, IpUsersHash_ipv4, ipusershash_free_4); if (!IpUsersHash_ipv4) IpUsersHash_ipv4 = safe_alloc(sizeof(IpUsersBucket *) * IPUSERS_HASH_TABLE_SIZE); LoadPersistentPointer(modinfo, IpUsersHash_ipv6, ipusershash_free_6); if (!IpUsersHash_ipv6) IpUsersHash_ipv6 = safe_alloc(sizeof(IpUsersBucket *) * IPUSERS_HASH_TABLE_SIZE); HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN_EX, 0, maxperip_config_run_allow); 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(); return MOD_SUCCESS; } MOD_UNLOAD() { SavePersistentPointer(modinfo, siphashkey_ipusers); SavePersistentPointer(modinfo, IpUsersHash_ipv4); SavePersistentPointer(modinfo, IpUsersHash_ipv6); 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; } } void siphashkey_ipusers_free(ModData *m) { safe_free(siphashkey_ipusers); m->ptr = NULL; } void ipusershash_free_4(ModData *m) { // FIXME: need to free every bucket in a for loop // and then end with this: safe_free(IpUsersHash_ipv4); m->ptr = NULL; } void ipusershash_free_6(ModData *m) { // FIXME: need to free every bucket in a for loop // and then end with this: safe_free(IpUsersHash_ipv6); m->ptr = NULL; } uint64_t hash_ipusers(Client *client) { if (IsIPV6(client)) return siphash_raw(client->rawip, 16, siphashkey_ipusers) % IPUSERS_HASH_TABLE_SIZE; else return siphash_raw(client->rawip, 4, siphashkey_ipusers) % IPUSERS_HASH_TABLE_SIZE; } IpUsersBucket *find_ipusers_bucket(Client *client) { int hash = 0; IpUsersBucket *p; hash = hash_ipusers(client); if (IsIPV6(client)) { for (p = IpUsersHash_ipv6[hash]; p; p = p->next) if (memcmp(p->rawip, client->rawip, 16) == 0) return p; } else { for (p = IpUsersHash_ipv4[hash]; p; p = p->next) if (memcmp(p->rawip, client->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; hash = hash_ipusers(client); n = safe_alloc(sizeof(IpUsersBucket)); if (IsIPV6(client)) { memcpy(n->rawip, client->rawip, 16); AddListItem(n, IpUsersHash_ipv6[hash]); } else { memcpy(n->rawip, client->rawip, 4); AddListItem(n, IpUsersHash_ipv4[hash]); } return n; } void decrease_ipusers_bucket(Client *client) { int hash = 0; IpUsersBucket *p; if (!(client->flags & CLIENT_FLAG_IPUSERS_BUMPED)) return; /* nothing to do */ client->flags &= ~CLIENT_FLAG_IPUSERS_BUMPED; hash = hash_ipusers(client); if (IsIPV6(client)) { for (p = IpUsersHash_ipv6[hash]; p; p = p->next) if (memcmp(p->rawip, client->rawip, 16) == 0) break; } else { for (p = IpUsersHash_ipv4[hash]; p; p = p->next) if (memcmp(p->rawip, client->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); } } 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 = ""; 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 = inetntop(AF_INET6, e->rawip, ipbuf, sizeof(ipbuf)); if (!ip) ip = ""; sendtxtnumeric(client, "IPv6 #%d %s: %d local / %d global", i, ip, 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)) return iConf.reject_message_too_many_connections; return NULL; }