From 62f3cda8f27998fb21dca41de569945aea431f81 Mon Sep 17 00:00:00 2001 From: Bram Matthys Date: Wed, 10 Jun 2026 15:29:26 +0200 Subject: [PATCH] Make spamfilter IDs start with "SPAM" to be more visible. And this also means shun IDs now start with "H". Update release notes. This, after i realized that for like *LINEs that are added by spamfilter the two ID fields in "STATS gline" are a bit confusing as to which ID is what. Now the spamfilter one starts with "SPAM" so there can be no confusion. The gline one still starts with "G" as before. Since I kept the generated ID length the same, this means there is less bits available for the spamfilter ID, but there are rarely more than 1000 spamfilters, and in that scenario there's just as little birthday attack collision % as with 200k glines, just to illustrate (~0.0015% vs ~0.0018%) --- doc/RELEASE-NOTES.md | 2 +- src/modules/tkl.c | 99 +++++++++++++++++++++++++++----------------- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/doc/RELEASE-NOTES.md b/doc/RELEASE-NOTES.md index 7418fd1c2..288e55cd8 100644 --- a/doc/RELEASE-NOTES.md +++ b/doc/RELEASE-NOTES.md @@ -7,7 +7,7 @@ This is work in progress and may not always be a stable version. ### Enhancements: * Server bans and Spamfilters now have a unique ID, like `G7K2MP9WQX3`: * The first letter denotes the type: `G` for gline, `K` for kline, - `Z` for (g)zline. + `Z` for (g)zline, `H` for shun. Spamfilter IDs start with `SPAM`. * The ID is shown to the affected user, so they can paste the ID back to network staff. It is `$banid` in [set::reject-message](https://www.unrealircd.org/docs/Set_block#set::reject-message) diff --git a/src/modules/tkl.c b/src/modules/tkl.c index 6a4318af8..8f4832a79 100644 --- a/src/modules/tkl.c +++ b/src/modules/tkl.c @@ -137,6 +137,7 @@ struct TKLTypeTable unsigned tkltype:1; /**< Is a type available in cmd_tkl() and friends */ unsigned exceptiontype:1; /**< Is a type available for exceptions */ unsigned needip:1; /**< When using this exempt option, only IP addresses are permitted (processed before DNS/ident lookups etc) */ + char *id_prefix; /**< Prefix for generated TKL ids, eg "G", "K", "SPAM". NULL for exempt-only options. Note that shun has "H" here while its type.letter is 's'. */ }; /** This table which defines all TKL types and TKL exception types. @@ -149,26 +150,26 @@ struct TKLTypeTable * - more? */ TKLTypeTable tkl_types[] = { - /* */ - { "gline", 'G', TKL_KILL | TKL_GLOBAL, "G-Line", 1, 1, 0 }, - { "kline", 'k', TKL_KILL, "K-Line", 1, 1, 0 }, - { "gzline", 'Z', TKL_ZAP | TKL_GLOBAL, "Global Z-Line", 1, 1, 1 }, - { "zline", 'z', TKL_ZAP, "Z-Line", 1, 1, 1 }, - { "spamfilter", 'F', TKL_SPAMF | TKL_GLOBAL, "Spamfilter", 1, 1, 0 }, - { "qline", 'Q', TKL_NAME | TKL_GLOBAL, "Q-Line", 1, 1, 0 }, - { "except", 'E', TKL_EXCEPTION | TKL_GLOBAL, "Exception", 1, 0, 0 }, - { "shun", 's', TKL_SHUN | TKL_GLOBAL, "Shun", 1, 1, 0 }, - { "local-qline", 'q', TKL_NAME, "Local Q-Line", 1, 0, 0 }, - { "local-exception", 'e', TKL_EXCEPTION, "Local Exception", 1, 0, 0 }, - { "local-spamfilter", 'f', TKL_SPAMF, "Local Spamfilter", 1, 0, 0 }, - { "blacklist", 'b', TKL_BLACKLIST, "Blacklist", 0, 1, 1 }, - { "connect-flood", 'c', TKL_CONNECT_FLOOD, "Connect flood", 0, 1, 0 }, - { "maxperip", 'm', TKL_MAXPERIP, "Max-per-IP", 0, 1, 0 }, - { "handshake-data-flood", 'd', TKL_HANDSHAKE_DATA_FLOOD, "Handshake data flood", 0, 1, 1 }, - { "antirandom", 'r', TKL_ANTIRANDOM, "Antirandom", 0, 1, 0 }, - { "antimixedutf8", '8', TKL_ANTIMIXEDUTF8, "Antimixedutf8", 0, 1, 0 }, - { "ban-version", 'v', TKL_BAN_VERSION, "Ban Version", 0, 1, 0 }, - { NULL, '\0', 0, NULL, 0, 0, 0 }, + /* */ + { "gline", 'G', TKL_KILL | TKL_GLOBAL, "G-Line", 1, 1, 0, "G" }, + { "kline", 'k', TKL_KILL, "K-Line", 1, 1, 0, "K" }, + { "gzline", 'Z', TKL_ZAP | TKL_GLOBAL, "Global Z-Line", 1, 1, 1, "Z" }, + { "zline", 'z', TKL_ZAP, "Z-Line", 1, 1, 1, "Z" }, + { "spamfilter", 'F', TKL_SPAMF | TKL_GLOBAL, "Spamfilter", 1, 1, 0, "SPAM" }, + { "qline", 'Q', TKL_NAME | TKL_GLOBAL, "Q-Line", 1, 1, 0, "Q" }, + { "except", 'E', TKL_EXCEPTION | TKL_GLOBAL, "Exception", 1, 0, 0, "E" }, + { "shun", 's', TKL_SHUN | TKL_GLOBAL, "Shun", 1, 1, 0, "H" }, + { "local-qline", 'q', TKL_NAME, "Local Q-Line", 1, 0, 0, "Q" }, + { "local-exception", 'e', TKL_EXCEPTION, "Local Exception", 1, 0, 0, "E" }, + { "local-spamfilter", 'f', TKL_SPAMF, "Local Spamfilter", 1, 0, 0, "SPAM" }, + { "blacklist", 'b', TKL_BLACKLIST, "Blacklist", 0, 1, 1, NULL }, + { "connect-flood", 'c', TKL_CONNECT_FLOOD, "Connect flood", 0, 1, 0, NULL }, + { "maxperip", 'm', TKL_MAXPERIP, "Max-per-IP", 0, 1, 0, NULL }, + { "handshake-data-flood", 'd', TKL_HANDSHAKE_DATA_FLOOD, "Handshake data flood", 0, 1, 1, NULL }, + { "antirandom", 'r', TKL_ANTIRANDOM, "Antirandom", 0, 1, 0, NULL }, + { "antimixedutf8", '8', TKL_ANTIMIXEDUTF8, "Antimixedutf8", 0, 1, 0, NULL }, + { "ban-version", 'v', TKL_BAN_VERSION, "Ban Version", 0, 1, 0, NULL }, + { NULL, '\0', 0, NULL, 0, 0, 0, NULL }, }; #define ALL_VALID_EXCEPTION_TYPES "kline, gline, zline, gzline, spamfilter, shun, qline, blacklist, connect-flood, handshake-data-flood, antirandom, antimixedutf8, ban-version" @@ -1331,20 +1332,34 @@ void check_set_spamfilter_utf8_setting_changed(void) } /* === TKL unique IDs === - * Every TKL has a unique id (struct TKL.id): either assigned (a random id generated - * by the server that originates the entry, or an id supplied by an external setter - * such as services), or an empty string if none is known. A native (assigned-here) - * id is <10 x base32>, eg "G7K2MP9WQX3". It is carried over S2S in the - * s2s-tkl/id message tag and persisted by tkldb; there is no derivation. + * Every TKL typically has a unique id (tkl->id), such as "G7K2MP9WQX3" for a gline. + * See the comment below about TKL_GENERATED_ID_LEN for more info. + * These TKLIDs are communicated in S2S via @s2s-tkl/id mtag, and they + * also persist via tkldb. */ -/** Single-letter prefix for a native TKL id, derived from the TKL type. - * Uppercase, with global/local collapsed (eg gzline and zline both 'Z', spamfilter - * and local-spamfilter both 'F'). gline stays 'G' and kline 'K' as their letters differ. +/* The generated TKLID (not to be confused with the maximum allowed length of TKLID): + * This consists of the prefix + the random/hashed base32 chars. Obviously, the idea + * is that this will never clash, so we have to look at birthday attack collision rates: + * - Spamfilter: "SPAM" prefix + 7 chars = 35 bits = ~0.0015% for 1000 spamfilters. + * - All the rest: 1 letter prefix + 10 chars = 50 bits = ~0.0018% for 200k entries + * And obviously both the 1000 spamfilters and 200k GLINE/whatever are a rather + * absurd number of entries, but possible in odd/attack scenarios. */ -static char tkl_id_prefix(int type) +#define TKL_GENERATED_ID_LEN 11 + +/** The TKL ID prefix string. Eg. "G" for a gline or "SPAM" for a spamfilter. */ +static const char *tkl_id_prefix(int type) { - return toupper(_tkl_typetochar(type)); + int i; + + for (i = 0; tkl_types[i].config_name; i++) + if ((tkl_types[i].type == type) && tkl_types[i].id_prefix) + return tkl_types[i].id_prefix; +#ifdef DEBUGMODE + abort(); /* this should never happen */ +#endif + return "?"; } /** Find a TKL by its exact id (case-insensitive). Returns NULL on no match or empty id. */ @@ -1377,16 +1392,18 @@ static void tkl_generate_id(TKL *tkl) { /* Crockford base32 (no I/L/O/U) so the id survives being read aloud */ static const char b32[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; - char prefix = tkl_id_prefix(tkl->type); + const char *prefix = tkl_id_prefix(tkl->type); + int prefixlen = strlen(prefix); + int nrandom = TKL_GENERATED_ID_LEN - prefixlen; TKL *other; int attempt, i; for (attempt = 0; attempt < 16; attempt++) { - tkl->id[0] = prefix; - for (i = 1; i <= 10; i++) - tkl->id[i] = b32[getrandom8() % 32]; - tkl->id[i] = '\0'; + strlcpy(tkl->id, prefix, sizeof(tkl->id)); + for (i = 0; i < nrandom; i++) + tkl->id[prefixlen + i] = b32[getrandom8() % 32]; + tkl->id[prefixlen + nrandom] = '\0'; other = find_tkl_by_id(tkl->id); if (!other || (other == tkl)) return; /* unique (or only matches ourselves, if already linked) */ @@ -1511,6 +1528,8 @@ static const char *spamfilter_fallback_id(TKL *tkl) * key just makes the hash deterministic and identical across servers. */ static const char key[16] = "UnrealIRCd rocks"; static char buf[16]; + const char *prefix; + int prefixlen; char content[8192]; char actbuf[2]; uint64_t h; @@ -1526,13 +1545,15 @@ static const char *spamfilter_fallback_id(TKL *tkl) h = siphash(content, key); - buf[0] = tkl_id_prefix(tkl->type); - for (i = 1; i <= 10; i++) + prefix = tkl_id_prefix(tkl->type); + prefixlen = strlen(prefix); + strlcpy(buf, prefix, sizeof(buf)); + for (i = 0; i < TKL_GENERATED_ID_LEN - prefixlen; i++) { - buf[i] = b32[h & 31]; + buf[prefixlen + i] = b32[h & 31]; h >>= 5; } - buf[i] = '\0'; + buf[prefixlen + i] = '\0'; return buf; }