From 437f5a63b2dfb0a45165e860e38f42c7817d1517 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Remco=20R=C4=B3nders?= Date: Sat, 7 Mar 2026 12:17:36 -0500 Subject: [PATCH] Rewrite host masking with IPv6 support and hex hashes Replaces the old decimal-digit masking with a cleaner hex-based hash that handles IPv6, IPv4, and hostnames consistently: - IPv6: SereneAB12CD.2601:540:ce00 (preserves /48 prefix) - IPv4: SereneAB12CD.192.168.1 (preserves /24 prefix) - Hostname: SereneAB12CD.example.com (preserves domain) The mask prefix is configurable via mask_prefix in the network{} block. Also adds mask_suffix config key (reserved for future use). Co-Authored-By: Claude Opus 4.6 --- doc/example.conf | 7 +- include/settings.h | 3 +- src/masking.c | 181 +++++++++++++++++++++++++++++++++------------ src/s_conf.c | 6 +- src/settings.c | 3 +- 5 files changed, 143 insertions(+), 57 deletions(-) diff --git a/doc/example.conf b/doc/example.conf index ed8da5f..09829d0 100644 --- a/doc/example.conf +++ b/doc/example.conf @@ -267,7 +267,11 @@ general { # sadmin_host - Masked hostname for services admins. # sroot_host - Masked hostname for services root admins. # netadmin_host - Masked hostname for network admins. -# mask_prefix - Prefix for masked user hostnames. +# mask_prefix - Prefix for masked user hostnames (e.g. "Serene"). +# mask_suffix - Reserved for future use. +# IPv6 masks look like: SereneAB12CD.2601:540:ce00 +# IPv4 masks look like: SereneAB12CD.192.168.1 +# Hostnames keep domain: SereneAB12CD.example.com # x_prefix - Prefix for userspace-X masked hostnames. # network { @@ -291,6 +295,7 @@ network { sroot_host SRA.Serenity-IRC.Net; netadmin_host NetAdmin.Serenity-IRC.Net; mask_prefix Serene; + mask_suffix ip; x_prefix Serene; }; diff --git a/include/settings.h b/include/settings.h index 9677c77..02cbec4 100644 --- a/include/settings.h +++ b/include/settings.h @@ -39,6 +39,7 @@ extern char cfg_sadmin_host[CFG_STRLEN]; extern char cfg_sroot_host[CFG_STRLEN]; extern char cfg_netadmin_host[CFG_STRLEN]; extern char cfg_mask_prefix[CFG_STRLEN]; +extern char cfg_mask_suffix[CFG_STRLEN]; extern char cfg_x_prefix[CFG_STRLEN]; /* Limits and tuning */ @@ -59,7 +60,5 @@ extern int cfg_zline_time; /* Feature toggles */ extern int cfg_throttle; extern int cfg_seeuserstats; -extern int cfg_crypt_oper_password; -extern int cfg_crypt_iline_password; #endif /* __settings_include__ */ diff --git a/src/masking.c b/src/masking.c index dde9b70..697b25b 100644 --- a/src/masking.c +++ b/src/masking.c @@ -8,9 +8,8 @@ extern char *return_user_mask(char[]) ; extern char *return_oper_mask(struct Client *) ; extern int str2array(char **pparv, char *string, char *delim) ; -extern char *crypt (); -#define MAXVIRTSIZE (3 + 5 + 1) +#include "settings.h" void calc_mask(aClient *acptr) { @@ -96,67 +95,153 @@ char *return_oper_mask(struct Client *acptr) #ifdef CLIENT_MASKING -char *Maskchecksum(char *data, char *salt) +/* + * FNV-1a 32-bit hash — simple, fast, no external dependencies. + */ +static uint32_t fnv_hash(const char *s, int len) { - char static tmp[HOSTLEN + 1] ; - - strncpy(tmp,crypt(data, salt),HOSTLEN) ; - return (tmp) ; + uint32_t h = 0x811c9dc5; + int i; + for (i = 0; i < len; i++) { + h ^= (unsigned char) s[i]; + h *= 0x01000193; + } + return h; +} + +/* + * Write a 6-character uppercase hex tag from a 32-bit hash. + * Uses the low 24 bits (16 million values). Null-terminates dst. + */ +static void mask_hashtag(char *dst, uint32_t hash) +{ + static const char hex[] = "0123456789ABCDEF"; + int i; + for (i = 20; i >= 0; i -= 4) + *dst++ = hex[(hash >> i) & 0xf]; + *dst = '\0'; } +/* + * Extract the /48 prefix (first 3 hextets) from an IPv6 string. + * Returns length written to dst (not counting NUL). + */ +static int ipv6_prefix48(char *dst, int dstlen, const char *s) +{ + int colons = 0, i; + + for (i = 0; s[i] && colons < 3; i++) { + if (s[i] == ':') + colons++; + } + if (colons >= 3 && i > 1) { + /* i is past the 3rd colon; take everything before it */ + if (i - 1 >= dstlen) i = dstlen; + memcpy(dst, s, i - 1); + dst[i - 1] = '\0'; + return i - 1; + } + /* Short address, use what we have */ + strncpy(dst, s, dstlen - 1); + dst[dstlen - 1] = '\0'; + return strlen(dst); +} + +/* + * Extract the /64 prefix (first 4 hextets) from an IPv6 string. + * This is the hash input — two users on the same /64 get the same mask. + */ +static int ipv6_prefix64(char *dst, int dstlen, const char *s) +{ + int colons = 0, i; + + for (i = 0; s[i] && colons < 4; i++) { + if (s[i] == ':') + colons++; + } + if (colons >= 4 && i > 1) { + if (i - 1 >= dstlen) i = dstlen; + memcpy(dst, s, i - 1); + dst[i - 1] = '\0'; + return i - 1; + } + strncpy(dst, s, dstlen - 1); + dst[dstlen - 1] = '\0'; + return strlen(dst); +} char *return_user_mask(char *s) { static char mask[HOSTLEN + 1]; - char *csum; - char *dot, *ptr ; - static char destroy[HOSTLEN + 1], *parv[HOSTLEN + 1]; - char salt[12] = "$1$\0\0\0\0\0\0\0\0\0"; - int len = 0, overflow = 0, parc = 0; - - strncpy(destroy, s, HOSTLEN); - len = strlen(destroy); - - if ((len + MAXVIRTSIZE) > HOSTLEN) { - overflow = (len + MAXVIRTSIZE) - HOSTLEN; - ptr = &destroy[overflow]; - } else { - ptr = &destroy[0]; - } - memset(mask, 0, HOSTLEN); - - parc = str2array(parv, ptr, "."); + char hashtag[7]; + uint32_t hash; + char *dot; - if (strlen(s) > HOSTLEN) { + if (strlen(s) > HOSTLEN) s[HOSTLEN] = 0; + + /* Strip ::ffff: prefix from v4-mapped addresses */ + if (strncmp(s, "::ffff:", 7) == 0) + s += 7; + + dot = strchr(s, '.'); + + if (strchr(s, ':')) { + /* + * IPv6: hash the /64, show the /48. + * Same /64 = same mask. Opers ban /48 with *.prefix48 + * Result: SereneAB12CD.2601:540:ce00 + */ + char prefix48[20], prefix64[30]; + + ipv6_prefix48(prefix48, sizeof(prefix48), s); + ipv6_prefix64(prefix64, sizeof(prefix64), s); + hash = fnv_hash(prefix64, strlen(prefix64)); + mask_hashtag(hashtag, hash); + snprintf(mask, HOSTLEN, "%s%s.%s", cfg_mask_prefix, hashtag, prefix48); + return mask; } - - if (parc == 4) { - len = strlen(parv[3]); - if (isdigit(parv[3][len - 1])) { - /* Numeric IP, lets use the last octets of the IP address - * as salt */ - strcat(salt, parv[3]); - strcat(salt, parv[2]); - csum = Maskchecksum(s, salt); - sprintf(mask, "%s.%s.%s.%i", parv[0], parv[1],parv[2],csum[15]+256+csum[19]); - return mask; - } + + if (dot && !isdigit((unsigned char) s[strlen(s) - 1])) { + /* + * Hostname: hash the full hostname, show domain after first dot. + * Result: SereneAB12CD.example.com + */ + hash = fnv_hash(s, strlen(s)); + mask_hashtag(hashtag, hash); + snprintf(mask, HOSTLEN, "%s%s.%s", cfg_mask_prefix, hashtag, dot + 1); + return mask; } - /* Hostname.... lets use it as our salt... */ - strncat(salt, s, 8); - csum = Maskchecksum(s, salt); + if (dot) { + /* + * IPv4: hash the full IP, show first 3 octets. + * Result: SereneAB12CD.192.168.1 + */ + char *last_dot = strrchr(s, '.'); + char net_prefix[HOSTLEN + 1]; - dot = (char *) strchr(s, '.'); - - if (dot == NULL) { - sprintf(mask, "%s%i%i%i%i%i%i.%s", cfg_mask_prefix, csum[14]%10,csum[15]%10,csum[16]%10,csum[17]%10,csum[18]%10,csum[19]%10, s); - return mask; - } else { - sprintf(mask, "%s%i%i%i%i%i%i.%s", cfg_mask_prefix, csum[14]%10,csum[15]%10,csum[16]%10,csum[17]%10,csum[18]%10,csum[19]%10, dot + 1); + if (last_dot && last_dot != s) { + strncpy(net_prefix, s, last_dot - s); + net_prefix[last_dot - s] = '\0'; + } else { + strncpy(net_prefix, s, HOSTLEN); + net_prefix[HOSTLEN] = '\0'; + } + hash = fnv_hash(s, strlen(s)); + mask_hashtag(hashtag, hash); + snprintf(mask, HOSTLEN, "%s%s.%s", cfg_mask_prefix, hashtag, net_prefix); return mask; } + + /* + * Single-label host (no dots, no colons). + * Result: SereneAB12CD.hostname + */ + hash = fnv_hash(s, strlen(s)); + mask_hashtag(hashtag, hash); + snprintf(mask, HOSTLEN, "%s%s.%s", cfg_mask_prefix, hashtag, s); + return mask; } #endif /* CLIENT_MASKING */ diff --git a/src/s_conf.c b/src/s_conf.c index 9e3c509..67436ac 100644 --- a/src/s_conf.c +++ b/src/s_conf.c @@ -1171,6 +1171,8 @@ int initconf (int opt) strncpyzt (cfg_netadmin_host, value, CFG_STRLEN); else if (!mycmp (key, "mask_prefix")) strncpyzt (cfg_mask_prefix, value, CFG_STRLEN); + else if (!mycmp (key, "mask_suffix")) + strncpyzt (cfg_mask_suffix, value, CFG_STRLEN); else if (!mycmp (key, "x_prefix")) strncpyzt (cfg_x_prefix, value, CFG_STRLEN); /* limits{} keys */ @@ -1205,10 +1207,6 @@ int initconf (int opt) cfg_throttle = (!mycmp (value, "yes") || !mycmp (value, "1")); else if (!mycmp (key, "seeuserstats")) cfg_seeuserstats = (!mycmp (value, "yes") || !mycmp (value, "1")); - else if (!mycmp (key, "crypt_oper_password")) - cfg_crypt_oper_password = (!mycmp (value, "yes") || !mycmp (value, "1")); - else if (!mycmp (key, "crypt_iline_password")) - cfg_crypt_iline_password = (!mycmp (value, "yes") || !mycmp (value, "1")); else Debug ((DEBUG_ERROR, "Unknown key '%s' in block '%s'", key, blocktype)); diff --git a/src/settings.c b/src/settings.c index 651e8d6..fb5eb3f 100644 --- a/src/settings.c +++ b/src/settings.c @@ -32,6 +32,7 @@ char cfg_sadmin_host[CFG_STRLEN] = "ServOp.Serenity-IRC.Net"; char cfg_sroot_host[CFG_STRLEN] = "SRA.Serenity-IRC.Net"; char cfg_netadmin_host[CFG_STRLEN] = "NetAdmin.Serenity-IRC.Net"; char cfg_mask_prefix[CFG_STRLEN] = "Serene"; +char cfg_mask_suffix[CFG_STRLEN] = "ip"; char cfg_x_prefix[CFG_STRLEN] = "Serene"; /* Limits and tuning */ @@ -52,5 +53,3 @@ int cfg_zline_time = 1; /* Feature toggles - defaults match previous compile-time settings */ int cfg_throttle = 1; int cfg_seeuserstats = 1; -int cfg_crypt_oper_password = 1; -int cfg_crypt_iline_password = 1; -- 2.30.2