Rewrite host masking with IPv6 support and hex hashes
authorRemco Rijnders <remmy@serenity-irc.net>
Sat, 7 Mar 2026 17:17:36 +0000 (12:17 -0500)
committerRemco Rijnders <remmy@serenity-irc.net>
Sat, 7 Mar 2026 17:17:36 +0000 (12:17 -0500)
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 <noreply@anthropic.com>
doc/example.conf
include/settings.h
src/masking.c
src/s_conf.c
src/settings.c

index ed8da5fa27e29469654872b6113d4dd3d1e123d4..09829d0ad69ea6dd248857d2773b98e922385b60 100644 (file)
@@ -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;
 };
 
index 9677c7767c8f24662adf3c273922832fd91357a3..02cbec4b048d5dd71adf13ae2e0eaa0061c8d1ce 100644 (file)
@@ -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__ */
index dde9b709b33380993b98fe597e5d3b6fc81b7faa..697b25b6c8ba7b9d76f4bd048e43c68a4eea5e0e 100644 (file)
@@ -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 */
index 9e3c509a5e508154c3b3b4e53413d7170d8fd7f9..67436acaad8f4695d7b30af64883c88cae63a31b 100644 (file)
@@ -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));
index 651e8d6b4dc1cd4a409e9ef3fc6e97c3ac980ce0..fb5eb3f0ddf391c9d55c9db7ce93f155bca6d429 100644 (file)
@@ -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;