1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-06-12 17:14:46 +02:00

Add helper functions and start the IPv6 /128 to /64 transition in

connect-flood and maxperip module. This so they actually take
set::default-ipv6-clone-mask into account.

This also changes the maxperip module to a more simple method of
just freeing all entries and rebuilding the hash table on load.
That's necessary since now set::default-ipv6-clone-mask can change.
This commit is contained in:
Bram Matthys
2026-05-03 19:21:06 +02:00
parent 4adaddeee1
commit 3a429dbd42
4 changed files with 192 additions and 59 deletions
+3
View File
@@ -527,6 +527,9 @@ extern int checkprotoflags(Client *, int, const char *, int);
extern const char *inetntop(int af, const void *in, char *local_dummy, size_t the_size); extern const char *inetntop(int af, const void *in, char *local_dummy, size_t the_size);
extern void mask_ipv6_rawip(const char *src, int prefix, char *dst);
extern const char *get_clone_mask_ipstr(Client *client, char *buf, size_t buflen);
extern void delletterfromstring(char *s, char letter); extern void delletterfromstring(char *s, char letter);
extern void addlettertodynamicstringsorted(char **str, char letter); extern void addlettertodynamicstringsorted(char **str, char letter);
extern int sort_character_lowercase_before_uppercase(char x, char y); extern int sort_character_lowercase_before_uppercase(char x, char y);
+60
View File
@@ -879,3 +879,63 @@ void badword_config_free(ConfigItem_badword *e)
pcre2_code_free(e->pcre2_expr); pcre2_code_free(e->pcre2_expr);
safe_free(e); safe_free(e);
} }
/** Mask the lower bits of an IPv6 raw address.
*
* Bits past 'prefix' are zeroed, leaving only the upper 'prefix' bits set
* to whatever they were in 'src'.
*
* @param src 16-byte source raw IPv6 address.
* @param prefix Prefix length in bits (0-128).
* @param dst 16-byte destination buffer (may alias src).
*/
void mask_ipv6_rawip(const char *src, int prefix, char *dst)
{
int full_bytes = prefix / 8;
int leftover_bits = prefix % 8;
if (src != dst)
memcpy(dst, src, 16);
if (leftover_bits > 0 && full_bytes < 16)
{
unsigned char mask = (unsigned char)(0xFF << (8 - leftover_bits));
dst[full_bytes] = (char)((unsigned char)src[full_bytes] & mask);
full_bytes++;
}
if (full_bytes < 16)
memset(dst + full_bytes, 0, 16 - full_bytes);
}
/** Get the IP address string of a client, masked according to
* set::default-ipv6-clone-mask.
*
* For IPv4 clients: returns client->ip unchanged (no masking applies).
* For IPv6 clients: returns the canonical form with bits past
* iConf.default_ipv6_clone_mask zeroed (e.g., "2001:db8:1:2::" for /64).
*
* Useful for any per-host bookkeeping that should treat all addresses
* within a /N as a single "host" — maxperip, connect-flood, reputation, etc.
*
* @param client The client.
* @param buf Output buffer.
* @param buflen Length of buf (recommended: HOSTLEN+1 or larger).
* @return Pointer to buf on success, or NULL on failure.
*/
const char *get_clone_mask_ipstr(Client *client, char *buf, size_t buflen)
{
char masked[16];
if (!client || !client->ip || !buf || buflen == 0)
return NULL;
if (!IsIPV6(client))
{
strlcpy(buf, client->ip, buflen);
return buf;
}
mask_ipv6_rawip(client->rawip, iConf.default_ipv6_clone_mask, masked);
return inetntop(AF_INET6, masked, buf, buflen);
}
+20 -5
View File
@@ -173,13 +173,21 @@ uint64_t hash_throttling(const char *ip)
ThrottlingBucket *find_throttling_bucket(Client *client) ThrottlingBucket *find_throttling_bucket(Client *client)
{ {
int hash = 0; int hash;
ThrottlingBucket *p; ThrottlingBucket *p;
hash = hash_throttling(client->ip); char ip[HOSTLEN+1];
/* Apply set::default-ipv6-clone-mask: bucket is keyed by the network
* portion of the IP, so all addresses in the same /64 share one bucket.
*/
if (!get_clone_mask_ipstr(client, ip, sizeof(ip)))
return NULL;
hash = hash_throttling(ip);
for (p = ThrottlingHash[hash]; p; p = p->next) for (p = ThrottlingHash[hash]; p; p = p->next)
{ {
if (!strcmp(p->ip, client->ip)) if (!strcmp(p->ip, ip))
return p; return p;
} }
@@ -210,13 +218,20 @@ void add_throttling_bucket(Client *client)
{ {
int hash; int hash;
ThrottlingBucket *n; ThrottlingBucket *n;
char ip[HOSTLEN+1];
/* Apply set::default-ipv6-clone-mask: bucket is keyed by the network
* portion of the IP, so all addresses in the same /64 share one bucket.
*/
if (!get_clone_mask_ipstr(client, ip, sizeof(ip)))
return;
n = safe_alloc(sizeof(ThrottlingBucket)); n = safe_alloc(sizeof(ThrottlingBucket));
n->next = n->prev = NULL; n->next = n->prev = NULL;
safe_strdup(n->ip, client->ip); safe_strdup(n->ip, ip);
n->since = TStime(); n->since = TStime();
n->count = 1; n->count = 1;
hash = hash_throttling(client->ip); hash = hash_throttling(ip);
AddListItem(n, ThrottlingHash[hash]); AddListItem(n, ThrottlingHash[hash]);
return; return;
} }
+109 -54
View File
@@ -51,12 +51,12 @@ int maxperip_config_test_allow(ConfigFile *cf, ConfigEntry *ce, int type, int *e
int maxperip_config_run_allow(ConfigFile *cf, ConfigEntry *ce, int type, void *ptr); int maxperip_config_run_allow(ConfigFile *cf, ConfigEntry *ce, int type, void *ptr);
void maxperip_postconf(void); void maxperip_postconf(void);
int exceeds_maxperip(Client *client, ConfigItem_allow *aconf); int exceeds_maxperip(Client *client, ConfigItem_allow *aconf);
void siphashkey_ipusers_free(ModData *m); IpUsersBucket *find_ipusers_bucket(Client *client);
void ipusershash_free_4(ModData *m);
void ipusershash_free_6(ModData *m);
IpUsersBucket *add_ipusers_bucket(Client *client); IpUsersBucket *add_ipusers_bucket(Client *client);
void decrease_ipusers_bucket(Client *client); void decrease_ipusers_bucket(Client *client);
int decrease_ipusers_bucket_wrapper(Client *client); int decrease_ipusers_bucket_wrapper(Client *client);
static void rebuild_ipusers_buckets(void);
static void free_ipusers_buckets(void);
int stats_maxperip(Client *client, const char *para); int stats_maxperip(Client *client, const char *para);
int maxperip_remote_connect(Client *client); int maxperip_remote_connect(Client *client);
const char *maxperip_allow_client(Client *client, ConfigItem_allow *aconf); const char *maxperip_allow_client(Client *client, ConfigItem_allow *aconf);
@@ -73,18 +73,11 @@ MOD_TEST()
MOD_INIT() MOD_INIT()
{ {
MARK_AS_OFFICIAL_MODULE(modinfo); 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);
siphashkey_ipusers = safe_alloc(SIPHASH_KEY_LENGTH); IpUsersHash_ipv4 = safe_alloc(sizeof(IpUsersBucket *) * IPUSERS_HASH_TABLE_SIZE);
siphash_generate_key(siphashkey_ipusers); IpUsersHash_ipv6 = safe_alloc(sizeof(IpUsersBucket *) * IPUSERS_HASH_TABLE_SIZE);
}
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_CONFIGRUN_EX, 0, maxperip_config_run_allow);
HookAdd(modinfo->handle, HOOKTYPE_FREE_USER, 0, decrease_ipusers_bucket_wrapper); HookAdd(modinfo->handle, HOOKTYPE_FREE_USER, 0, decrease_ipusers_bucket_wrapper);
@@ -98,14 +91,14 @@ MOD_INIT()
MOD_LOAD() MOD_LOAD()
{ {
maxperip_postconf(); maxperip_postconf();
rebuild_ipusers_buckets();
return MOD_SUCCESS; return MOD_SUCCESS;
} }
MOD_UNLOAD() MOD_UNLOAD()
{ {
SavePersistentPointer(modinfo, siphashkey_ipusers); free_ipusers_buckets();
SavePersistentPointer(modinfo, IpUsersHash_ipv4); safe_free(siphashkey_ipusers);
SavePersistentPointer(modinfo, IpUsersHash_ipv6);
return MOD_SUCCESS; return MOD_SUCCESS;
} }
@@ -178,51 +171,48 @@ void maxperip_postconf(void)
} }
} }
void siphashkey_ipusers_free(ModData *m) /** Build the rawip used to identify this client's ipusers bucket.
{ *
safe_free(siphashkey_ipusers); * For IPv4: copies the 4 raw bytes of client->rawip.
m->ptr = NULL; * For IPv6: copies and masks client->rawip according to
} * iConf.default_ipv6_clone_mask, so all addresses within the
* same /N share one bucket.
void ipusershash_free_4(ModData *m) *
{ * The 'rawip' buffer must be at least 16 bytes (only first 4 used for IPv4).
// FIXME: need to free every bucket in a for loop */
// and then end with this: static void make_ipusers_rawip(Client *client, char *rawip)
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)) if (IsIPV6(client))
return siphash_raw(client->rawip, 16, siphashkey_ipusers) % IPUSERS_HASH_TABLE_SIZE; mask_ipv6_rawip(client->rawip, iConf.default_ipv6_clone_mask, rawip);
else else
return siphash_raw(client->rawip, 4, siphashkey_ipusers) % IPUSERS_HASH_TABLE_SIZE; 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) IpUsersBucket *find_ipusers_bucket(Client *client)
{ {
int hash = 0; int hash;
IpUsersBucket *p; IpUsersBucket *p;
char rawip[16];
hash = hash_ipusers(client); make_ipusers_rawip(client, rawip);
hash = hash_ipusers(client, rawip);
if (IsIPV6(client)) if (IsIPV6(client))
{ {
for (p = IpUsersHash_ipv6[hash]; p; p = p->next) for (p = IpUsersHash_ipv6[hash]; p; p = p->next)
if (memcmp(p->rawip, client->rawip, 16) == 0) if (memcmp(p->rawip, rawip, 16) == 0)
return p; return p;
} else { } else {
for (p = IpUsersHash_ipv4[hash]; p; p = p->next) for (p = IpUsersHash_ipv4[hash]; p; p = p->next)
if (memcmp(p->rawip, client->rawip, 4) == 0) if (memcmp(p->rawip, rawip, 4) == 0)
return p; return p;
} }
@@ -240,16 +230,18 @@ IpUsersBucket *add_ipusers_bucket(Client *client)
{ {
int hash; int hash;
IpUsersBucket *n; IpUsersBucket *n;
char rawip[16];
hash = hash_ipusers(client); make_ipusers_rawip(client, rawip);
hash = hash_ipusers(client, rawip);
n = safe_alloc(sizeof(IpUsersBucket)); n = safe_alloc(sizeof(IpUsersBucket));
if (IsIPV6(client)) if (IsIPV6(client))
{ {
memcpy(n->rawip, client->rawip, 16); memcpy(n->rawip, rawip, 16);
AddListItem(n, IpUsersHash_ipv6[hash]); AddListItem(n, IpUsersHash_ipv6[hash]);
} else { } else {
memcpy(n->rawip, client->rawip, 4); memcpy(n->rawip, rawip, 4);
AddListItem(n, IpUsersHash_ipv4[hash]); AddListItem(n, IpUsersHash_ipv4[hash]);
} }
return n; return n;
@@ -257,24 +249,26 @@ IpUsersBucket *add_ipusers_bucket(Client *client)
void decrease_ipusers_bucket(Client *client) void decrease_ipusers_bucket(Client *client)
{ {
int hash = 0; int hash;
IpUsersBucket *p; IpUsersBucket *p;
char rawip[16];
if (!(client->flags & CLIENT_FLAG_IPUSERS_BUMPED)) if (!(client->flags & CLIENT_FLAG_IPUSERS_BUMPED))
return; /* nothing to do */ return; /* nothing to do */
client->flags &= ~CLIENT_FLAG_IPUSERS_BUMPED; client->flags &= ~CLIENT_FLAG_IPUSERS_BUMPED;
hash = hash_ipusers(client); make_ipusers_rawip(client, rawip);
hash = hash_ipusers(client, rawip);
if (IsIPV6(client)) if (IsIPV6(client))
{ {
for (p = IpUsersHash_ipv6[hash]; p; p = p->next) for (p = IpUsersHash_ipv6[hash]; p; p = p->next)
if (memcmp(p->rawip, client->rawip, 16) == 0) if (memcmp(p->rawip, rawip, 16) == 0)
break; break;
} else { } else {
for (p = IpUsersHash_ipv4[hash]; p; p = p->next) for (p = IpUsersHash_ipv4[hash]; p; p = p->next)
if (memcmp(p->rawip, client->rawip, 4) == 0) if (memcmp(p->rawip, rawip, 4) == 0)
break; break;
} }
@@ -299,6 +293,67 @@ void decrease_ipusers_bucket(Client *client)
} }
} }
/* 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 stats_maxperip(Client *client, const char *para) int stats_maxperip(Client *client, const char *para)
{ {
int i; int i;