1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-06-30 15:26:37 +02:00
Files
unrealircd/src/modules/maxperip.c
T
Bram Matthys 8c21472d03 Move allow::maxperip to its own module (maxperip), add HOOKTYPE_ALLOW_CLIENT.
Also fix documentation for ~10 hooks to mention the hook name.

Obviously, the maxperip module is loaded by default (in modules.default.conf)
but it is nice to have the 400+ lines contained in a separate module
rather than being in the nick module that does NICK/UID handling.
Will look at moving more later..
2025-03-22 07:42:00 +01:00

407 lines
10 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",
"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 = "<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 = inetntop(AF_INET6, e->rawip, ipbuf, sizeof(ipbuf));
if (!ip)
ip = "<invalid>";
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;
}