From 3a429dbd426c3aca796098f99d08df82f5a3b0b4 Mon Sep 17 00:00:00 2001 From: Bram Matthys Date: Sun, 3 May 2026 19:21:06 +0200 Subject: [PATCH] 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. --- include/h.h | 3 + src/match.c | 60 +++++++++++++ src/modules/connect-flood.c | 25 ++++-- src/modules/maxperip.c | 163 ++++++++++++++++++++++++------------ 4 files changed, 192 insertions(+), 59 deletions(-) diff --git a/include/h.h b/include/h.h index 825273e46..a796829dc 100644 --- a/include/h.h +++ b/include/h.h @@ -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 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 addlettertodynamicstringsorted(char **str, char letter); extern int sort_character_lowercase_before_uppercase(char x, char y); diff --git a/src/match.c b/src/match.c index 403f787e0..6ed7afbb4 100644 --- a/src/match.c +++ b/src/match.c @@ -879,3 +879,63 @@ void badword_config_free(ConfigItem_badword *e) pcre2_code_free(e->pcre2_expr); 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); +} diff --git a/src/modules/connect-flood.c b/src/modules/connect-flood.c index bfa4bc37e..38e6ed60e 100644 --- a/src/modules/connect-flood.c +++ b/src/modules/connect-flood.c @@ -173,13 +173,21 @@ uint64_t hash_throttling(const char *ip) ThrottlingBucket *find_throttling_bucket(Client *client) { - int hash = 0; + int hash; 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) { - if (!strcmp(p->ip, client->ip)) + if (!strcmp(p->ip, ip)) return p; } @@ -210,13 +218,20 @@ void add_throttling_bucket(Client *client) { int hash; 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->next = n->prev = NULL; - safe_strdup(n->ip, client->ip); + safe_strdup(n->ip, ip); n->since = TStime(); n->count = 1; - hash = hash_throttling(client->ip); + hash = hash_throttling(ip); AddListItem(n, ThrottlingHash[hash]); return; } diff --git a/src/modules/maxperip.c b/src/modules/maxperip.c index e774ccab3..f80b7f33c 100644 --- a/src/modules/maxperip.c +++ b/src/modules/maxperip.c @@ -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); 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 *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); int stats_maxperip(Client *client, const char *para); int maxperip_remote_connect(Client *client); const char *maxperip_allow_client(Client *client, ConfigItem_allow *aconf); @@ -73,18 +73,11 @@ MOD_TEST() 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); + + 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); HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN_EX, 0, maxperip_config_run_allow); HookAdd(modinfo->handle, HOOKTYPE_FREE_USER, 0, decrease_ipusers_bucket_wrapper); @@ -98,14 +91,14 @@ MOD_INIT() MOD_LOAD() { maxperip_postconf(); + rebuild_ipusers_buckets(); return MOD_SUCCESS; } MOD_UNLOAD() { - SavePersistentPointer(modinfo, siphashkey_ipusers); - SavePersistentPointer(modinfo, IpUsersHash_ipv4); - SavePersistentPointer(modinfo, IpUsersHash_ipv6); + free_ipusers_buckets(); + safe_free(siphashkey_ipusers); return MOD_SUCCESS; } @@ -178,51 +171,48 @@ void maxperip_postconf(void) } } -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) +/** 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)) - return siphash_raw(client->rawip, 16, siphashkey_ipusers) % IPUSERS_HASH_TABLE_SIZE; + mask_ipv6_rawip(client->rawip, iConf.default_ipv6_clone_mask, rawip); 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) { - int hash = 0; + int hash; IpUsersBucket *p; + char rawip[16]; - hash = hash_ipusers(client); + 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, client->rawip, 16) == 0) + if (memcmp(p->rawip, rawip, 16) == 0) return p; } else { 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; } @@ -240,16 +230,18 @@ IpUsersBucket *add_ipusers_bucket(Client *client) { int hash; IpUsersBucket *n; + char rawip[16]; - hash = hash_ipusers(client); + make_ipusers_rawip(client, rawip); + hash = hash_ipusers(client, rawip); n = safe_alloc(sizeof(IpUsersBucket)); if (IsIPV6(client)) { - memcpy(n->rawip, client->rawip, 16); + memcpy(n->rawip, rawip, 16); AddListItem(n, IpUsersHash_ipv6[hash]); } else { - memcpy(n->rawip, client->rawip, 4); + memcpy(n->rawip, rawip, 4); AddListItem(n, IpUsersHash_ipv4[hash]); } return n; @@ -257,24 +249,26 @@ IpUsersBucket *add_ipusers_bucket(Client *client) void decrease_ipusers_bucket(Client *client) { - int hash = 0; + int hash; IpUsersBucket *p; + char rawip[16]; if (!(client->flags & CLIENT_FLAG_IPUSERS_BUMPED)) return; /* nothing to do */ client->flags &= ~CLIENT_FLAG_IPUSERS_BUMPED; - hash = hash_ipusers(client); + 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, client->rawip, 16) == 0) + if (memcmp(p->rawip, rawip, 16) == 0) break; } else { 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; } @@ -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 i;