diff --git a/Makefile.windows b/Makefile.windows index dab8e5ac8..340fbc7d3 100644 --- a/Makefile.windows +++ b/Makefile.windows @@ -314,6 +314,7 @@ DLL_FILES=\ src/modules/lusers.dll \ src/modules/map.dll \ src/modules/max-unknown-connections-per-ip.dll \ + src/modules/maxperip.dll \ src/modules/md.dll \ src/modules/message.dll \ src/modules/message-ids.dll \ @@ -1054,6 +1055,9 @@ src/modules/map.dll: src/modules/map.c $(INCLUDES) src/modules/max-unknown-connections-per-ip.dll: src/modules/max-unknown-connections-per-ip.c $(INCLUDES) $(CC) $(MODCFLAGS) src/modules/max-unknown-connections-per-ip.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/max-unknown-connections-per-ip.pdb $(MODLFLAGS) +src/modules/maxperip.dll: src/modules/maxperip.c $(INCLUDES) + $(CC) $(MODCFLAGS) src/modules/maxperip.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/maxperip.pdb $(MODLFLAGS) + src/modules/md.dll: src/modules/md.c $(INCLUDES) $(CC) $(MODCFLAGS) src/modules/md.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/md.pdb $(MODLFLAGS) diff --git a/doc/conf/modules.default.conf b/doc/conf/modules.default.conf index ae37eb360..34a06ed9e 100644 --- a/doc/conf/modules.default.conf +++ b/doc/conf/modules.default.conf @@ -298,6 +298,7 @@ loadmodule "geoip_base"; /* needed for ALL geoip functions */ loadmodule "websocket_common"; /* helper functions for websocket (internal) */ loadmodule "spamreport"; /* Spam reporting to a blacklist */ loadmodule "crule"; /* Rules in spamfilter::rule and deny link::rule */ +loadmodule "maxperip"; /* allow::maxperip restrictions */ loadmodule "geoip_classic"; @if module-loaded("geoip_classic") diff --git a/include/modules.h b/include/modules.h index 4c8d07bfc..e9cabb7d5 100644 --- a/include/modules.h +++ b/include/modules.h @@ -1310,6 +1310,8 @@ extern APICallback *APICallbackAdd(Module *module, APICallback *mreq); #define HOOKTYPE_SASL_AUTHENTICATE 124 /** See hooktype_sasl_mechs */ #define HOOKTYPE_SASL_MECHS 125 +/* See hooktype_allow_client */ +#define HOOKTYPE_ALLOW_CLIENT 126 /* Adding a new hook here? * 1) Add the #define HOOKTYPE_.... with a new number @@ -2292,7 +2294,7 @@ int hooktype_post_local_nickchange(Client *client, MessageTag *mtags, const char */ int hooktype_post_remote_nickchange(Client *client, MessageTag *mtags, const char *oldnick); -/** Called when user name or user host has changed. +/** Called when user name or user host has changed (function prototype for HOOKTYPE_USERHOST_CHANGE). * @param client The client whose user@host has changed * @param olduser Old username of the client * @param oldhost Old hostname of the client @@ -2300,14 +2302,14 @@ int hooktype_post_remote_nickchange(Client *client, MessageTag *mtags, const cha */ int hooktype_userhost_change(Client *client, const char *olduser, const char *oldhost); -/** Called when user realname has changed. +/** Called when user realname has changed. (function prototype for HOOKTYPE_REALNAME_CHANGE). * @param client The client whose realname has changed * @param oldinfo Old realname of the client * @return The return value is ignored (use return 0) */ int hooktype_realname_change(Client *client, const char *oldinfo); -/** Called when changing IP (eg due to PROXY/WEBIRC/etc). +/** Called when changing IP (eg due to PROXY/WEBIRC/etc) (function prototype for HOOKTYPE_IP_CHANGE). * @param client The client whose IP has changed * @param oldip Old IP of the client * @returns If you reject the user then use dead_link() and return HOOK_DENY @@ -2316,7 +2318,7 @@ int hooktype_realname_change(Client *client, const char *oldinfo); */ int hooktype_ip_change(Client *client, const char *oldip); -/** Called when json_expand_client() is called. +/** Called when json_expand_client() is called (function prototype for HOOKTYPE_JSON_EXPAND_CLIENT). * Used for expanding information about 'client' in logging routines. * @param client The client that should be expanded * @param detail The amount of detail to provide (always 0 at the moment) @@ -2325,7 +2327,7 @@ int hooktype_ip_change(Client *client, const char *oldip); */ int hooktype_json_expand_client(Client *client, int detail, json_t *j); -/** Called when json_expand_client_user() is called. +/** Called when json_expand_client_user() is called (function prototype for HOOKTYPE_JSON_EXPAND_CLIENT_USER). * Used for expanding information about 'client' in logging routines * when the client is a USER. * @param client The client that should be expanded @@ -2336,7 +2338,7 @@ int hooktype_json_expand_client(Client *client, int detail, json_t *j); */ int hooktype_json_expand_client_user(Client *client, int detail, json_t *j, json_t *child); -/** Called when json_expand_client_server() is called. +/** Called when json_expand_client_server() is called (function prototype for HOOKTYPE_JSON_EXPAND_CLIENT_SERVER). * Used for expanding information about 'client' in logging routines * when the client is a SERVER. * @param client The client that should be expanded @@ -2347,7 +2349,7 @@ int hooktype_json_expand_client_user(Client *client, int detail, json_t *j, json */ int hooktype_json_expand_client_server(Client *client, int detail, json_t *j, json_t *child); -/** Called when json_expand_channel() is called. +/** Called when json_expand_channel() is called (function prototype for HOOKTYPE_JSON_EXPAND_CHANNEL). * Used for expanding information about 'channel' in logging routines. * @param channel The channel that should be expanded * @param detail The amount of detail to provide (always 0 at the moment) @@ -2366,29 +2368,28 @@ int hooktype_json_expand_channel(Channel *channel, int detail, json_t *j); */ int hooktype_pre_local_handshake_timeout(Client *client, const char **comment); -/** Called when a REHASH completed (either succesfully or with a failure). - * This gives the full rehash log. Used by the JSON-RPC interface. (function prototype for HOOKTYPE_REHASH_LOG) +/** Called when a REHASH completed (either succesfully or with a failure) (function prototype for HOOKTYPE_REHASH_LOG). + * This gives the full rehash log. Used by the JSON-RPC interface. * @param failure Set to 1 if the rehash failed, otherwise 0. * @param t The JSON object containing the rehash log and other information. * @return The return value is ignored (use return 0) */ int hooktype_rehash_log(int failure, json_t *rehash_log); -/** Called when DNS has been done for a client (or has not been done because it was skipped). - * (function prototype for HOOKTYPE_DNS_FINISHED) +/** Called when DNS has been done for a client (or has not been done because it was skipped) + * (function prototype for HOOKTYPE_DNS_FINISHED). * @param client The client * @return The return value is ignored (use return 0) */ int hooktype_dns_finished(Client *client); -/** Called after an listener block is processed - * (function prototype for HOOKTYPE_CONFIG_LISTENER) +/** Called after an listener block is processed (function prototype for HOOKTYPE_CONFIG_LISTENER). * @param listener The listener * @return The return value is ignored (use return 0) */ int hooktype_config_listener(ConfigItem_listen *listener); -/** Called after an entry is added to a WATCH (or MONITOR) list. +/** Called after an entry is added to a WATCH (or MONITOR) list (function prototype for HOOKTYPE_WATCH_ADD). * @param nick Name of the new entry (watched user's nick) * @param client Owner of the watch list * @param flags Flags for the entry (WATCH_FLAG_TYPE_*) @@ -2396,7 +2397,7 @@ int hooktype_config_listener(ConfigItem_listen *listener); */ int hooktype_watch_add(char *nick, Client *client, int flags); -/** Called after an entry is removed from a WATCH (or MONITOR) list. +/** Called after an entry is removed from a WATCH (or MONITOR) list (function prototype for HOOKTYPE_WATCH_DEL). * @param nick Name of the entry (watched user's nick) to be deleted * @param client Owner of the watch list * @param flags Flags for the entry (WATCH_FLAG_TYPE_*) to be deleted @@ -2404,7 +2405,8 @@ int hooktype_watch_add(char *nick, Client *client, int flags); */ int hooktype_watch_del(char *nick, Client *client, int flags); -/** Called when an user is notified about a MONITORed nick coming off- or online. +/** Called when an user is notified about a MONITORed nick coming off- or online + * (function prototype for HOOKTYPE_MONITOR_NOTIFICATION). * @param watcher The user being notified * @param client The user (dis)appearing * @param online 1 if it's coming online, 0 otherwise @@ -2412,7 +2414,8 @@ int hooktype_watch_del(char *nick, Client *client, int flags); */ int hooktype_monitor_notification(Client *watcher, Client *client, int online); -/** Called when an AUTHENTICATE command is sent by the client, for SASL authentication. +/** Called when an AUTHENTICATE command is sent by the client, for SASL authentication + * (function prototype for HOOKTYPE_SASL_AUTHENTICATE). * This can be used by authentication modules. * @param client The client (user) * @param first Set to 1 if this is the first AUTHENTICATE, set to 0 if it is a continuation. @@ -2426,6 +2429,18 @@ int hooktype_sasl_authenticate(Client *client, int first, const char *param); * @return The saslmechlist */ const char *hooktype_sasl_mechs(Client *client); + +/** Called from AllowClient() to see if the client should be allowed in based + * on allow block restrictions (function prototype for HOOKTYPE_ALLOW_CLIENT). + * NOTE for 3rd party modules: usually you will want to use + * HOOKTYPE_PRE_LOCAL_CONNECT instead (or sometimes HOOKTYPE_IS_HANDSHAKE_FINISHED), + * as this HOOKTYPE_ALLOW_CLIENT is really meant for allow-block-specific stuff. + * @param client The client + * @param aconf The allow block being evaluated + * @return A string that will be used to exit_client() to reject the user, + * or NULL to allow the user in. + */ +const char *hooktype_allow_client(Client *client, ConfigItem_allow *aconf); /** @} */ #ifdef GCC_TYPECHECKING @@ -2553,7 +2568,8 @@ _UNREAL_ERROR(_hook_error_incompatible, "Incompatible hook function. Check argum ((hooktype == HOOKTYPE_WATCH_DEL) && !ValidateHook(hooktype_watch_del, func)) || \ ((hooktype == HOOKTYPE_MONITOR_NOTIFICATION) && !ValidateHook(hooktype_monitor_notification, func)) || \ ((hooktype == HOOKTYPE_SASL_AUTHENTICATE) && !ValidateHook(hooktype_sasl_authenticate, func)) || \ - ((hooktype == HOOKTYPE_SASL_MECHS) && !ValidateHook(hooktype_sasl_mechs, func))) \ + ((hooktype == HOOKTYPE_SASL_MECHS) && !ValidateHook(hooktype_sasl_mechs, func)) || \ + ((hooktype == HOOKTYPE_ALLOW_CLIENT) && !ValidateHook(hooktype_allow_client, func))) \ _hook_error_incompatible(); #endif /* GCC_TYPECHECKING */ diff --git a/src/conf.c b/src/conf.c index b234f1c26..de57573ec 100644 --- a/src/conf.c +++ b/src/conf.c @@ -5877,10 +5877,6 @@ int _conf_allow(ConfigFile *conf, ConfigEntry *ce) allow->class = default_class; } } - else if (!strcmp(cep->name, "maxperip")) - allow->maxperip = atoi(cep->value); - else if (!strcmp(cep->name, "global-maxperip")) - allow->global_maxperip = atoi(cep->value); else if (!strcmp(cep->name, "redirect-server")) safe_strdup(allow->server, cep->value); else if (!strcmp(cep->name, "redirect-port")) @@ -5916,14 +5912,6 @@ int _conf_allow(ConfigFile *conf, ConfigEntry *ce) } } - /* 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; - AddListItem(allow, conf_allow); return 1; } @@ -5934,7 +5922,7 @@ int _test_allow(ConfigFile *conf, ConfigEntry *ce) int errors = 0; Hook *h; int has_ip = 0, has_hostname = 0, has_mask = 0, has_match = 0; - int has_maxperip = 0, has_global_maxperip = 0, has_password = 0, has_class = 0; + int has_password = 0, has_class = 0; int has_redirectserver = 0, has_redirectport = 0, has_options = 0; int hostname_possible_silliness = 0; @@ -6056,40 +6044,6 @@ int _test_allow(ConfigFile *conf, ConfigEntry *ce) has_match = 1; test_match_block(conf, cep, &errors); } - else if (!strcmp(cep->name, "maxperip")) - { - int v = atoi(cep->value); - if (has_maxperip) - { - config_warn_duplicate(cep->file->filename, - cep->line_number, "allow::maxperip"); - continue; - } - has_maxperip = 1; - if ((v <= 0) || (v > 1000000)) - { - config_error("%s:%i: allow::maxperip with illegal value (must be 1-1000000)", - cep->file->filename, cep->line_number); - errors++; - } - } - else if (!strcmp(cep->name, "global-maxperip")) - { - int v = atoi(cep->value); - if (has_global_maxperip) - { - config_warn_duplicate(cep->file->filename, - cep->line_number, "allow::global-maxperip"); - continue; - } - has_global_maxperip = 1; - if ((v <= 0) || (v > 1000000)) - { - config_error("%s:%i: allow::global-maxperip with illegal value (must be 1-1000000)", - cep->file->filename, cep->line_number); - errors++; - } - } else if (!strcmp(cep->name, "ipv6-clone-mask")) { /* keep this in sync with _test_set() */ @@ -6249,12 +6203,6 @@ int _test_allow(ConfigFile *conf, ConfigEntry *ce) errors++; } - if (!has_maxperip) - { - config_error_missing(ce->file->filename, ce->line_number, - "allow::maxperip"); - errors++; - } return errors; } diff --git a/src/modules/Makefile.in b/src/modules/Makefile.in index 1428776a9..91664e2ca 100644 --- a/src/modules/Makefile.in +++ b/src/modules/Makefile.in @@ -85,7 +85,7 @@ MODULES= \ real-quit-reason.so \ spamreport.so crule.so \ central-api.so central-blocklist.so \ - no-implicit-names.so \ + no-implicit-names.so maxperip.so \ $(GEOIP_CLASSIC_OBJECTS) $(GEOIP_MAXMIND_OBJECTS) MODULEFLAGS=@MODULEFLAGS@ diff --git a/src/modules/maxperip.c b/src/modules/maxperip.c new file mode 100644 index 000000000..ae2104192 --- /dev/null +++ b/src/modules/maxperip.c @@ -0,0 +1,406 @@ +/* + * 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 0; +} + +/** 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; +} diff --git a/src/modules/nick.c b/src/modules/nick.c index 23de92a80..df1af4c06 100644 --- a/src/modules/nick.c +++ b/src/modules/nick.c @@ -45,23 +45,7 @@ ModuleHeader MOD_HEADER */ #define ASSUME_NICK_IN_FLIGHT -#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 */ static char spamfilter_user[NICKLEN + USERLEN + HOSTLEN + REALLEN + 64]; -IpUsersBucket **IpUsersHash_ipv4 = NULL; -IpUsersBucket **IpUsersHash_ipv6 = NULL; -char *siphashkey_ipusers = NULL; /* Forward declarations */ CMD_FUNC(cmd_nick); @@ -71,14 +55,6 @@ CMD_FUNC(cmd_uid); int _register_user(Client *client); void nick_collision(Client *cptr, const char *newnick, const char *newid, Client *new, Client *existing, int type); int AllowClient(Client *client); -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); char *_unreal_expand_string(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client); MOD_TEST() @@ -93,24 +69,9 @@ 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); - CommandAdd(modinfo->handle, "NICK", cmd_nick, MAXPARA, CMD_USER|CMD_SERVER|CMD_UNREGISTERED); CommandAdd(modinfo->handle, "UID", cmd_uid, MAXPARA, CMD_SERVER); - HookAdd(modinfo->handle, HOOKTYPE_FREE_USER, 0, decrease_ipusers_bucket_wrapper); - HookAdd(modinfo->handle, HOOKTYPE_STATS, 0, stats_maxperip); return MOD_SUCCESS; } @@ -121,179 +82,9 @@ MOD_LOAD() MOD_UNLOAD() { - SavePersistentPointer(modinfo, siphashkey_ipusers); - SavePersistentPointer(modinfo, IpUsersHash_ipv4); - SavePersistentPointer(modinfo, IpUsersHash_ipv6); return MOD_SUCCESS; } -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 0; -} - /** Hmm.. don't we already have such a function? */ void set_user_modes_dont_spread(Client *client, const char *umode) { @@ -925,9 +716,6 @@ nickkill2done: if (*virthost != '*') safe_strdup(client->user->virthost, virthost); - /* Add to ipusers hash table (to track global maxperip) */ - exceeds_maxperip(client, NULL); - build_umode_string(client, 0, SEND_UMODES|UMODE_SERVNOTICE, buf); sendto_serv_butone_nickcmd(client->direction, recv_mtags, client, (*buf == '\0' ? "+" : buf)); @@ -1449,53 +1237,6 @@ void nick_collision(Client *cptr, const char *newnick, const char *newid, Client } } -/** 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; -} - /** Allow or reject the client based on allow { } blocks and all other restrictions. * @param client Client to check (local) * @param username Username, for some reason... @@ -1509,6 +1250,7 @@ int AllowClient(Client *client) char *hname; static char uhost[HOSTLEN + USERLEN + 3]; static char fullname[HOSTLEN + 1]; + Hook *h; if (!IsSecure(client) && !IsLocalhost(client) && (iConf.plaintext_policy_user == POLICY_DENY)) { @@ -1550,11 +1292,14 @@ int AllowClient(Client *client) if (aconf->flags.useip) set_sockhost(client, GetIP(client)); - if (exceeds_maxperip(client, aconf)) + for (h = Hooks[HOOKTYPE_ALLOW_CLIENT]; h; h = h->next) { - /* Already got too many with that ip# */ - exit_client(client, NULL, iConf.reject_message_too_many_connections); - return 0; + const char *reject_reason = (*(h->func.stringfunc))(client, aconf); + if (reject_reason) + { + exit_client(client, NULL, reject_reason); + return 0; + } } if (!((aconf->class->clients + 1) > aconf->class->maxclients))