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

Add TKL IDs via message tags in S2S.

By default - assuming you don't set set::reject-message things by yourself -
the *LINE id is appended at the end of the rejection that is shown to the
user, like: [ID: G7K2MP9WQX3].

Also new is spamfilter to *LINE mapping, so you can see which *LINE was
set by which SPAMFILTER. For this STATS gline and friends were enhanced.
In fact, multiple fields were added there, including some that are 0
(zero) placeholders at the moment. These will be set in a future commit.
Some things were combined here so we only have to break STATS and tkldb
database format once (unless i made a mistake, then the follow up commit
will correct that i guess :D).

This was requested by Hero in https://bugs.unrealircd.org/view.php?id=4397
in 2015. Again by musk in https://bugs.unrealircd.org/view.php?id=4397
in 2022. And on IRC by Chris and others.

As you can see it was not SUPER easy and a lot of thought went into this
(and in terms of S2S traffic it is part of something bigger too)
This commit is contained in:
Bram Matthys
2026-06-07 16:54:34 +02:00
parent b19573d562
commit 27a086b03a
14 changed files with 496 additions and 70 deletions
+47
View File
@@ -5,6 +5,28 @@ This is the git version (development version) for future UnrealIRCd 6.2.6.
This is work in progress and may not always be a stable version. This is work in progress and may not always be a stable version.
### Enhancements: ### 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.
* 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)
and included by default. If you have a custom set::reject-message in
your config file then you will have to add it in manually.
* IRCOps see the ID in `STATS gline` and similar and can search for it
with e.g. `STATS gline +i G7K2MP9WQX3`.
* Spamfilters also have an ID, and we already had something similar that
could be used for `SPAMFILTER del <id>` that was only local-server.
For new spamfilters this is now a network wide ID as well.
* When a server ban was placed by a spamfilter, `STATS gline` shows the
originating spamfilter's ID, so it can be traced back.
* The `STATS gline` and other TKL stats changed format, see
*Developers and protocol* if you use scripts or your client depends
on the exact numeric format.
* For TKL IDs to reach all servers, all servers need to be on 6.2.6
or later, especially the hubs. If there is one server in-between
that is older, then TKL IDs don't propagate properly and the ID
will be empty.
### Changes: ### Changes:
* Spamfilter regexes now use more sensible defaults in terms of "max effort", * Spamfilter regexes now use more sensible defaults in terms of "max effort",
@@ -34,6 +56,31 @@ This is work in progress and may not always be a stable version.
`update_known_user_cache(client);` to update the known users cache. `update_known_user_cache(client);` to update the known users cache.
For example, if you have some authentication module that marks a user For example, if you have some authentication module that marks a user
as IsLoggedIn. as IsLoggedIn.
* TKL entries can now use message tags to carry extra data, similar to how
`s2s-md` worked for early moddata, we have `s2s-tkl`. We have two at
the moment:
* `s2s-tkl/id`: network-wide unique ID for the TKL entry
* `s2s-tkl/spamfilter_id`: for server bans, if they were set by spamfilter,
the spamfilter TKL ID.
* Example: `@s2s-tkl/id=xxxx;s2s-tkl/spamfilter_id=yyyy :server TKL .....`
* Older servers will not pass these along, so for this functionality to
work all servers need to be on latest version, especially your hub.
* The TKL `STATS` numerics has more fields now, they are added right before
last (so before the `:reason` or `:regex`):
* `RPL_STATSGLINE` (223) ends with `hits lasthit spamfilter_id id :reason`
* `RPL_STATSQLINE` (217) ends with `hits lasthit id :reason`
* `RPL_STATSSPAMF` (229) ends with `lasthit lasthit_except id :regex`
(which comes right after `hits hits_except`, which was already there)
* `RPL_STATSEXCEPTTKL` (230) ends with `id :reason`
* An absent id or spamfilter_id is sent as `-`. The hits/lasthit/lasthit_except
fields are reserved and currently always `0`; FIXME in later commit.
* `banned_client()` has an extra parameter `const char *tklid` for the
TKL ID (or NULL if none).
* The tkldb database version is now 6260 and stores the id and spamfilter_id.
FIXME: also prepared for hit stats, so update this release note line in later commit.
Older databases still load. Downside: you cannot downgrade UnrealIRCd.
* JSON for TKL entries (server logs and JSON-RPC) now includes `id`, and
`spamfilter_id` for spamfilter-created server bans.
UnrealIRCd 6.2.5 UnrealIRCd 6.2.5
----------------- -----------------
+1 -1
View File
@@ -953,7 +953,7 @@ extern MODVAR int (*decode_authenticate_plain)(const char *param, char **authori
extern MODVAR void (*exit_client)(Client *client, MessageTag *recv_mtags, const char *comment); extern MODVAR void (*exit_client)(Client *client, MessageTag *recv_mtags, const char *comment);
extern MODVAR void (*exit_client_fmt)(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...) __attribute__((format(printf, 3, 4))); extern MODVAR void (*exit_client_fmt)(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...) __attribute__((format(printf, 3, 4)));
extern MODVAR void (*exit_client_ex)(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment); extern MODVAR void (*exit_client_ex)(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment);
extern MODVAR void (*banned_client)(Client *client, const char *bantype, const char *reason, int global, int noexit); extern MODVAR void (*banned_client)(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit);
extern MODVAR char *(*unreal_expand_string)(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client); extern MODVAR char *(*unreal_expand_string)(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client);
extern MODVAR char *(*utf8_convert_confusables)(const char *i, char *obuf, int olen); extern MODVAR char *(*utf8_convert_confusables)(const char *i, char *obuf, int olen);
extern MODVAR const char *(*utf8_get_block_name)(int i); extern MODVAR const char *(*utf8_get_block_name)(int i);
+4 -4
View File
@@ -372,17 +372,17 @@
#define STR_RPL_STATSCOMMANDS /* 212 */ "%s %u %lu" #define STR_RPL_STATSCOMMANDS /* 212 */ "%s %u %lu"
#define STR_RPL_STATSCLINE /* 213 */ "%c %s * %s %d %d %s" #define STR_RPL_STATSCLINE /* 213 */ "%c %s * %s %d %d %s"
#define STR_RPL_STATSILINE /* 215 */ "I %s %s %d %d %s %s %d" #define STR_RPL_STATSILINE /* 215 */ "I %s %s %d %d %s %s %d"
#define STR_RPL_STATSQLINE /* 217 */ "%c %s %lld %lld %s :%s" #define STR_RPL_STATSQLINE /* 217 */ "%c %s %lld %lld %s %lld %lld %s :%s"
#define STR_RPL_STATSYLINE /* 218 */ "Y %s %d %d %d %d %d" #define STR_RPL_STATSYLINE /* 218 */ "Y %s %d %d %d %d %d"
#define STR_RPL_ENDOFSTATS /* 219 */ "%c :End of /STATS report" #define STR_RPL_ENDOFSTATS /* 219 */ "%c :End of /STATS report"
#define STR_RPL_UMODEIS /* 221 */ "%s" #define STR_RPL_UMODEIS /* 221 */ "%s"
#define STR_RPL_STATSGLINE /* 223 */ "%c %s %lld %lld %s :%s" #define STR_RPL_STATSGLINE /* 223 */ "%c %s %lld %lld %s %lld %lld %s %s :%s"
#define STR_RPL_STATSTLINE /* 224 */ "T %s %s %s" #define STR_RPL_STATSTLINE /* 224 */ "T %s %s %s"
#define STR_RPL_STATSNLINE /* 226 */ "n %s %s" #define STR_RPL_STATSNLINE /* 226 */ "n %s %s"
#define STR_RPL_STATSVLINE /* 227 */ "v %s %s %s" #define STR_RPL_STATSVLINE /* 227 */ "v %s %s %s"
#define STR_RPL_STATSBANVER /* 228 */ "%s %s" #define STR_RPL_STATSBANVER /* 228 */ "%s %s"
#define STR_RPL_STATSSPAMF /* 229 */ "%c %s %s %s %lld %lld %lld %s %s %lld %lld :%s" #define STR_RPL_STATSSPAMF /* 229 */ "%c %s %s %s %lld %lld %lld %s %s %lld %lld %lld %lld %s :%s"
#define STR_RPL_STATSEXCEPTTKL /* 230 */ "%s %s %lld %lld %s :%s" #define STR_RPL_STATSEXCEPTTKL /* 230 */ "%s %s %lld %lld %s %s :%s"
#define STR_RPL_RULES /* 232 */ ":- %s" #define STR_RPL_RULES /* 232 */ ":- %s"
#define STR_RPL_STATSLLINE /* 241 */ "%c %s * %s %d %d" #define STR_RPL_STATSLLINE /* 241 */ "%c %s * %s %d %d"
#define STR_RPL_STATSUPTIME /* 242 */ ":Server Up %lld days, %lld:%02lld:%02lld" #define STR_RPL_STATSUPTIME /* 242 */ ":Server Up %lld days, %lld:%02lld:%02lld"
+4 -1
View File
@@ -204,7 +204,6 @@ typedef OperPermission (*OperClassEntryEvalCallback)(OperClassACLEntryVar* varia
#define MAXCCUSERS 20 /* Maximum for set::anti-flood::max-concurrent-conversations */ #define MAXCCUSERS 20 /* Maximum for set::anti-flood::max-concurrent-conversations */
#define BATCHLEN 22 #define BATCHLEN 22
#define MAXBATCHREFLEN 48 /* Max length of client-sent batch reference tags */ #define MAXBATCHREFLEN 48 /* Max length of client-sent batch reference tags */
#define MAXSPAMFILTERIDLEN 24
/* /*
* Watch it - Don't change this unless you also change the ERR_TOOMANYWATCH * Watch it - Don't change this unless you also change the ERR_TOOMANYWATCH
@@ -1319,6 +1318,8 @@ struct BanException {
#define TKL_FLAG_CONFIG 0x0001 /* Entry from configuration file. Cannot be removed by using commands. */ #define TKL_FLAG_CONFIG 0x0001 /* Entry from configuration file. Cannot be removed by using commands. */
#define TKL_FLAG_CENTRAL_SPAMFILTER 0x0002 /* Entry from central spamfilter. */ #define TKL_FLAG_CENTRAL_SPAMFILTER 0x0002 /* Entry from central spamfilter. */
#define TKLIDLEN 16 /**< TKL id buffer size: native 'prefix+body' or an external id. Max 15 chars + NUL. */
/** A TKL entry, such as a KLINE, GLINE, Spamfilter, QLINE, Exception, .. */ /** A TKL entry, such as a KLINE, GLINE, Spamfilter, QLINE, Exception, .. */
struct TKL { struct TKL {
TKL *prev, *next; TKL *prev, *next;
@@ -1327,6 +1328,8 @@ struct TKL {
char *set_by; /**< By who was this entry added */ char *set_by; /**< By who was this entry added */
time_t set_at; /**< When this entry was added */ time_t set_at; /**< When this entry was added */
time_t expire_at; /**< When this entry will expire */ time_t expire_at; /**< When this entry will expire */
char id[TKLIDLEN]; /**< Unique ID: assigned (random, generated by originating server) or external (eg from services), or empty string if none */
char spamfilter_id[TKLIDLEN]; /**< For server bans created by a spamfilter: that spamfilter's id. Empty otherwise. */
union { union {
Spamfilter *spamfilter; Spamfilter *spamfilter;
ServerBan *serverban; ServerBan *serverban;
+1 -1
View File
@@ -184,7 +184,7 @@ int (*decode_authenticate_plain)(const char *param, char **authorization_id, cha
void (*exit_client)(Client *client, MessageTag *recv_mtags, const char *comment); void (*exit_client)(Client *client, MessageTag *recv_mtags, const char *comment);
void (*exit_client_fmt)(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...); void (*exit_client_fmt)(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...);
void (*exit_client_ex)(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment); void (*exit_client_ex)(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment);
void (*banned_client)(Client *client, const char *bantype, const char *reason, int global, int noexit); void (*banned_client)(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit);
char *(*unreal_expand_string)(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client); char *(*unreal_expand_string)(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, int buildvarstring_options, Client *client);
char *(*utf8_convert_confusables)(const char *i, char *obuf, int olen); char *(*utf8_convert_confusables)(const char *i, char *obuf, int olen);
const char *(*utf8_get_block_name)(int i); const char *(*utf8_get_block_name)(int i);
+2 -2
View File
@@ -1935,8 +1935,8 @@ void config_setdefaultsettings(Configuration *i)
safe_strdup(i->reject_message_too_many_new_connections_ipv6_range, "Too many new connections from this IPv6 range ($prefix_addr/$prefix_len) [connthrottle]"); safe_strdup(i->reject_message_too_many_new_connections_ipv6_range, "Too many new connections from this IPv6 range ($prefix_addr/$prefix_len) [connthrottle]");
safe_strdup(i->reject_message_server_full, "This server is full"); safe_strdup(i->reject_message_server_full, "This server is full");
safe_strdup(i->reject_message_unauthorized, "You are not authorized to connect to this server"); safe_strdup(i->reject_message_unauthorized, "You are not authorized to connect to this server");
safe_strdup(i->reject_message_kline, "You are not welcome on this server. $bantype: $banreason. Email $klineaddr for more information."); safe_strdup(i->reject_message_kline, "You are not welcome on this server. $bantype: $banreason. Email $klineaddr for more information.$banid");
safe_strdup(i->reject_message_gline, "You are not welcome on this network. $bantype: $banreason. Email $glineaddr for more information."); safe_strdup(i->reject_message_gline, "You are not welcome on this network. $bantype: $banreason. Email $glineaddr for more information.$banid");
i->topic_setter = SETTER_NICK_USER_HOST; i->topic_setter = SETTER_NICK_USER_HOST;
i->ban_setter = SETTER_NICK_USER_HOST; i->ban_setter = SETTER_NICK_USER_HOST;
+4
View File
@@ -565,6 +565,8 @@ void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail)
json_object_set_new(j, "type", json_string_unreal(tkl_type_config_string(tkl))); // Eg 'kline' json_object_set_new(j, "type", json_string_unreal(tkl_type_config_string(tkl))); // Eg 'kline'
json_object_set_new(j, "type_string", json_string_unreal(tkl_type_string(tkl))); // Eg 'Soft K-Line' json_object_set_new(j, "type_string", json_string_unreal(tkl_type_string(tkl))); // Eg 'Soft K-Line'
json_object_set_new(j, "set_by", json_string_unreal(tkl->set_by)); json_object_set_new(j, "set_by", json_string_unreal(tkl->set_by));
if (tkl->id[0])
json_object_set_new(j, "id", json_string_unreal(tkl->id));
json_object_set_new(j, "set_at", json_timestamp(tkl->set_at)); json_object_set_new(j, "set_at", json_timestamp(tkl->set_at));
json_object_set_new(j, "expire_at", json_timestamp(tkl->expire_at)); json_object_set_new(j, "expire_at", json_timestamp(tkl->expire_at));
*buf = '\0'; *buf = '\0';
@@ -592,6 +594,8 @@ void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail)
else else
json_object_set_new(j, "name", json_string_unreal(tkl_uhost(tkl, buf, sizeof(buf), 0))); json_object_set_new(j, "name", json_string_unreal(tkl_uhost(tkl, buf, sizeof(buf), 0)));
json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.serverban->reason)); json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.serverban->reason));
if (tkl->spamfilter_id[0])
json_object_set_new(j, "spamfilter_id", json_string_unreal(tkl->spamfilter_id));
} else } else
if (TKLIsNameBan(tkl)) if (TKLIsNameBan(tkl))
{ {
+1 -1
View File
@@ -2967,7 +2967,7 @@ const char *command_issued_by_rpc(MessageTag *mtags)
/** Is 's' a valid spamfilter id? A-Z, 0-9 and _ and with a max length. */ /** Is 's' a valid spamfilter id? A-Z, 0-9 and _ and with a max length. */
int valid_spamfilter_id(const char *s) int valid_spamfilter_id(const char *s)
{ {
if (strlen(s) > MAXSPAMFILTERIDLEN) if (strlen(s) >= TKLIDLEN)
return 0; return 0;
return 1; return 1;
} }
+1 -1
View File
@@ -124,7 +124,7 @@ CMD_FUNC(cmd_chgname)
if (!ValidatePermissionsForPath("immune:server-ban:ban-realname",target,NULL,NULL,NULL) && if (!ValidatePermissionsForPath("immune:server-ban:ban-realname",target,NULL,NULL,NULL) &&
((bconf = find_ban(NULL, target->info, CONF_BAN_REALNAME)))) ((bconf = find_ban(NULL, target->info, CONF_BAN_REALNAME))))
{ {
banned_client(target, "realname", bconf->reason?bconf->reason:"", 0, 0); banned_client(target, "realname", bconf->reason?bconf->reason:"", NULL, 0, 0);
return; return;
} }
} }
+1 -1
View File
@@ -1097,7 +1097,7 @@ int _register_user(Client *client)
if ((bconf = find_ban(NULL, client->info, CONF_BAN_REALNAME))) if ((bconf = find_ban(NULL, client->info, CONF_BAN_REALNAME)))
{ {
ircstats.is_ref++; ircstats.is_ref++;
banned_client(client, "realname", bconf->reason?bconf->reason:"", 0, 0); banned_client(client, "realname", bconf->reason?bconf->reason:"", NULL, 0, 0);
return 0; return 0;
} }
/* Check G/Z lines before shuns -- kill before quite -- codemastr */ /* Check G/Z lines before shuns -- kill before quite -- codemastr */
+19 -8
View File
@@ -38,7 +38,7 @@ CMD_FUNC(cmd_quit);
void _exit_client(Client *client, MessageTag *recv_mtags, const char *comment); void _exit_client(Client *client, MessageTag *recv_mtags, const char *comment);
void _exit_client_fmt(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...) __attribute__((format(printf, 3, 4))); void _exit_client_fmt(Client *client, MessageTag *recv_mtags, FORMAT_STRING(const char *pattern), ...) __attribute__((format(printf, 3, 4)));
void _exit_client_ex(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment); void _exit_client_ex(Client *client, Client *origin, MessageTag *recv_mtags, const char *comment);
void _banned_client(Client *client, const char *bantype, const char *reason, int global, int noexit); void _banned_client(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit);
static void remove_dependents(Client *client, Client *from, MessageTag *mtags, const char *comment, const char *splitstr); static void remove_dependents(Client *client, Client *from, MessageTag *mtags, const char *comment, const char *splitstr);
static void exit_one_client(Client *, MessageTag *mtags_i, const char *); static void exit_one_client(Client *, MessageTag *mtags_i, const char *);
static int should_hide_ban_reason(Client *client, const char *reason); static int should_hide_ban_reason(Client *client, const char *reason);
@@ -491,11 +491,12 @@ static void exit_one_client(Client *client, MessageTag *mtags_i, const char *com
* *
* @note This function will call exit_client() appropriately. * @note This function will call exit_client() appropriately.
*/ */
void _banned_client(Client *client, const char *bantype, const char *reason, int global, int noexit) void _banned_client(Client *client, const char *bantype, const char *reason, const char *tklid, int global, int noexit)
{ {
char buf[512]; char buf[512];
char idbuf[64];
char *fmt = global ? iConf.reject_message_gline : iConf.reject_message_kline; char *fmt = global ? iConf.reject_message_gline : iConf.reject_message_kline;
const char *vars[6], *values[6]; const char *vars[7], *values[7];
MessageTag *mtags = NULL; MessageTag *mtags = NULL;
if (!MyConnect(client)) if (!MyConnect(client))
@@ -503,6 +504,14 @@ void _banned_client(Client *client, const char *bantype, const char *reason, int
RunHook(HOOKTYPE_BANNED_CLIENT, client, bantype, reason, global); RunHook(HOOKTYPE_BANNED_CLIENT, client, bantype, reason, global);
/* The " [ID: xxx]" fragment, empty when there is no id. Used both as the $banid
* reject-message variable and appended to the quit reason / real-quit-reason mtag.
*/
if (!BadPtr(tklid))
snprintf(idbuf, sizeof(idbuf), " [ID: %s]", tklid);
else
idbuf[0] = '\0';
/* This was: "You are not welcome on this %s. %s: %s. %s" but is now dynamic: */ /* This was: "You are not welcome on this %s. %s: %s. %s" but is now dynamic: */
vars[0] = "bantype"; vars[0] = "bantype";
values[0] = bantype; values[0] = bantype;
@@ -514,8 +523,10 @@ void _banned_client(Client *client, const char *bantype, const char *reason, int
values[3] = GLINE_ADDRESS ? GLINE_ADDRESS : KLINE_ADDRESS; /* fallback to klineaddr */ values[3] = GLINE_ADDRESS ? GLINE_ADDRESS : KLINE_ADDRESS; /* fallback to klineaddr */
vars[4] = "ip"; vars[4] = "ip";
values[4] = GetIP(client); values[4] = GetIP(client);
vars[5] = NULL; vars[5] = "banid";
values[5] = NULL; values[5] = idbuf;
vars[6] = NULL;
values[6] = NULL;
buildvarstring(fmt, buf, sizeof(buf), vars, values); buildvarstring(fmt, buf, sizeof(buf), vars, values);
/* This is a bit extensive but we will send both a YOUAREBANNEDCREEP /* This is a bit extensive but we will send both a YOUAREBANNEDCREEP
@@ -537,13 +548,13 @@ void _banned_client(Client *client, const char *bantype, const char *reason, int
/* Hide the ban reason, but put the real reason in unrealircd.org/real-quit-reason */ /* Hide the ban reason, but put the real reason in unrealircd.org/real-quit-reason */
MessageTag *m = safe_alloc(sizeof(MessageTag)); MessageTag *m = safe_alloc(sizeof(MessageTag));
safe_strdup(m->name, "unrealircd.org/real-quit-reason"); safe_strdup(m->name, "unrealircd.org/real-quit-reason");
snprintf(buf, sizeof(buf), "Banned (%s): %s", bantype, reason); snprintf(buf, sizeof(buf), "Banned (%s): %s%s", bantype, reason, idbuf);
safe_strdup(m->value, buf); safe_strdup(m->value, buf);
AddListItem(m, mtags); AddListItem(m, mtags);
/* And the quit reason for anyone else, goes here.. */ /* And the quit reason for anyone else, goes here.. */
snprintf(buf, sizeof(buf), "Banned (%s)", bantype); snprintf(buf, sizeof(buf), "Banned (%s)%s", bantype, idbuf);
} else { } else {
snprintf(buf, sizeof(buf), "Banned (%s): %s", bantype, reason); snprintf(buf, sizeof(buf), "Banned (%s): %s%s", bantype, reason, idbuf);
} }
if (noexit != NO_EXIT_CLIENT) if (noexit != NO_EXIT_CLIENT)
+1 -1
View File
@@ -145,7 +145,7 @@ CMD_FUNC(cmd_setname)
if (!ValidatePermissionsForPath("immune:server-ban:ban-realname",client,NULL,NULL,NULL) && if (!ValidatePermissionsForPath("immune:server-ban:ban-realname",client,NULL,NULL,NULL) &&
((bconf = find_ban(NULL, client->info, CONF_BAN_REALNAME)))) ((bconf = find_ban(NULL, client->info, CONF_BAN_REALNAME))))
{ {
banned_client(client, "realname", bconf->reason?bconf->reason:"", 0, 0); banned_client(client, "realname", bconf->reason?bconf->reason:"", NULL, 0, 0);
return; return;
} }
} else { } else {
+361 -44
View File
@@ -175,6 +175,25 @@ int confusables_spamfilters_present = 0; /**< Are any spamfilters with input-con
long previous_spamfilter_utf8 = 0; long previous_spamfilter_utf8 = 0;
static int firstboot = 0; static int firstboot = 0;
/* s2s-tkl/<field>: per-TKL fields carried over S2S as @s2s-tkl/<field>=<value> on the
* TKL command (modeled on s2s-md/). Old servers ignore unknown tags. To add a field,
* add a row here plus its serialize/unserialize pair (defined further down).
*/
typedef struct {
const char *name;
const char *(*serialize)(TKL *tkl); /**< value to send, or NULL to omit the tag */
void (*unserialize)(TKL *tkl, const char *value);
} TKLS2SField;
static const char *tkl_s2s_get_id(TKL *tkl);
static void tkl_s2s_set_id(TKL *tkl, const char *value);
static const char *tkl_s2s_get_spamfilter_id(TKL *tkl);
static void tkl_s2s_set_spamfilter_id(TKL *tkl, const char *value);
static const TKLS2SField tkl_s2s_fields[] = {
{ "id", tkl_s2s_get_id, tkl_s2s_set_id },
{ "spamfilter_id", tkl_s2s_get_spamfilter_id, tkl_s2s_set_spamfilter_id },
{ NULL, NULL, NULL },
};
MOD_TEST() MOD_TEST()
{ {
MARK_AS_OFFICIAL_MODULE(modinfo); MARK_AS_OFFICIAL_MODULE(modinfo);
@@ -320,7 +339,7 @@ int tkl_config_test_spamfilter(ConfigFile *cf, ConfigEntry *ce, int type, int *e
{ {
config_error("%s:%i: spamfilter::id invalid: maximum size (%d chars) exceeded " config_error("%s:%i: spamfilter::id invalid: maximum size (%d chars) exceeded "
"or forbidden characters encountered: only A-Z, 0-9 and _ are permitted.", "or forbidden characters encountered: only A-Z, 0-9 and _ are permitted.",
cep->file->filename, cep->line_number, MAXSPAMFILTERIDLEN); cep->file->filename, cep->line_number, TKLIDLEN-1);
errors++; errors++;
} }
} else } else
@@ -1272,12 +1291,209 @@ void check_set_spamfilter_utf8_setting_changed(void)
previous_spamfilter_utf8 = iConf.spamfilter_utf8; previous_spamfilter_utf8 = iConf.spamfilter_utf8;
} }
/** Return unique spamfilter id for TKL */ /* === TKL unique IDs ===
char *spamfilter_id(TKL *tk) * 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
static char buf[128]; * such as services), or an empty string if none is known. A native (assigned-here)
* id is <prefix><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.
*/
snprintf(buf, sizeof(buf), "%p", (void *)tk); /** 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.
*/
static char tkl_id_prefix(int type)
{
return toupper(_tkl_typetochar(type));
}
/** Find a TKL by its exact id (case-insensitive). Returns NULL on no match or empty id. */
static TKL *find_tkl_by_id(const char *id)
{
TKL *tkl;
int index, index2;
if (BadPtr(id))
return NULL;
for (index = 0; index < TKLIPHASHLEN1; index++)
for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
if (!strcasecmp(tkl->id, id))
return tkl;
for (index = 0; index < TKLISTLEN; index++)
for (tkl = tklines[index]; tkl; tkl = tkl->next)
if (!strcasecmp(tkl->id, id))
return tkl;
return NULL;
}
/** Assign a fresh, locally-unique random id to 'tkl' (overwrites tkl->id).
* Called when this server is the origin of the entry.
*/
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);
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';
other = find_tkl_by_id(tkl->id);
if (!other || (other == tkl))
return; /* unique (or only matches ourselves, if already linked) */
}
/* 16 collisions in a row is astronomically unlikely; keep the last candidate. */
}
/* s2s-tkl/xxx message-tag field handlers */
static const char *tkl_s2s_get_id(TKL *tkl)
{
return tkl->id[0] ? tkl->id : NULL;
}
static void tkl_s2s_set_id(TKL *tkl, const char *value)
{
if (!BadPtr(value) && (strlen(value) < sizeof(tkl->id)))
strlcpy(tkl->id, value, sizeof(tkl->id));
}
static const char *tkl_s2s_get_spamfilter_id(TKL *tkl)
{
return tkl->spamfilter_id[0] ? tkl->spamfilter_id : NULL;
}
static void tkl_s2s_set_spamfilter_id(TKL *tkl, const char *value)
{
if (!BadPtr(value) && (strlen(value) < sizeof(tkl->spamfilter_id)))
strlcpy(tkl->spamfilter_id, value, sizeof(tkl->spamfilter_id));
}
/** Append s2s-tkl/<field> message tags describing 'tkl' onto '*mtags' (for sending over S2S). */
static void tkl_add_s2s_mtags(TKL *tkl, MessageTag **mtags)
{
const TKLS2SField *f;
const char *value;
MessageTag *m;
char name[64];
for (f = tkl_s2s_fields; f->name; f++)
{
value = f->serialize(tkl);
if (!value)
continue;
snprintf(name, sizeof(name), "s2s-tkl/%s", f->name);
m = safe_alloc(sizeof(MessageTag));
safe_strdup(m->name, name);
safe_strdup(m->value, value);
AddListItem(m, *mtags);
}
}
/** Build a one-element s2s-tkl/spamfilter_id message tag from 'spamfilter_id', or NULL if it is empty.
* Lets a locally-created server ban carry its spamfilter id through the same path as a remote one.
*/
static MessageTag *tkl_spamfilter_id_mtag(const char *spamfilter_id)
{
MessageTag *m;
if (BadPtr(spamfilter_id))
return NULL;
m = safe_alloc(sizeof(MessageTag));
safe_strdup(m->name, "s2s-tkl/spamfilter_id");
safe_strdup(m->value, spamfilter_id);
return m;
}
/** Read s2s-tkl/<field> message tags from 'mtags' into 'tkl' (on receiving an S2S TKL command). */
static void tkl_extract_s2s_mtags(TKL *tkl, MessageTag *mtags)
{
MessageTag *m;
const TKLS2SField *f;
for (m = mtags; m; m = m->next)
{
if (strncmp(m->name, "s2s-tkl/", 8))
continue;
for (f = tkl_s2s_fields; f->name; f++)
{
if (!strcmp(f->name, m->name + 8))
{
f->unserialize(tkl, m->value);
break;
}
}
}
}
/** Return the value of the s2s-tkl/<field> message tag in 'mtags', or NULL if absent.
* Used by the merge logic to peek at an incoming field without overwriting the existing entry.
*/
static const char *tkl_s2s_mtag_value(MessageTag *mtags, const char *field)
{
MessageTag *m;
char name[64];
snprintf(name, sizeof(name), "s2s-tkl/%s", field);
for (m = mtags; m; m = m->next)
if (!strcmp(m->name, name))
return m->value;
return NULL;
}
/** A fallback spamfilter id (not the normal randomly generated ID).
* This ID is a hash based on (target, action, match method+string). So when those
* properties are the same, it will create the same ID. Used for:
* - Config spamfilter or Central Spamfilter without an explicit id.
* For these it behaves very much like a regular tkl->id, is shown in STATS etc.
* These entries never propagate servers, as they are local, but it is still nice
* for the ID to be unique on each server if you use the exact same spamfilter
* (think remote includes or central spamfilter).
* - Global spamfilter with a blank id (eg from an older server).
* In this case it is ONLY used for '/SPAMFILTER del' and not in regular STATS
* or used as a true id.
* And to be clear: for both cases they never appear in S2S and not on disk either
* (it is computed, each time).
*/
static const char *spamfilter_fallback_id(TKL *tkl)
{
static const char b32[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
/* Fixed, non-secret key: the fallback id is opers-only and not security-sensitive, the
* key just makes the hash deterministic and identical across servers. */
static const char key[16] = "UnrealIRCd rocks";
static char buf[16];
char content[8192];
char actbuf[2];
uint64_t h;
int i;
actbuf[0] = banact_valtochar(tkl->ptr.spamfilter->action->action);
actbuf[1] = '\0';
snprintf(content, sizeof(content), "%s\t%s\t%s\t%s",
spamfilter_target_inttostring(tkl->ptr.spamfilter->target),
actbuf,
unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type),
tkl->ptr.spamfilter->match->str);
h = siphash(content, key);
buf[0] = tkl_id_prefix(tkl->type);
for (i = 1; i <= 10; i++)
{
buf[i] = b32[h & 31];
h >>= 5;
}
buf[i] = '\0';
return buf; return buf;
} }
@@ -1291,9 +1507,8 @@ static void spamfilter_regex_error(TKL *tkl, const char *regex_error)
{ {
unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_REGEX_ERROR", NULL, unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_REGEX_ERROR", NULL,
"[Spamfilter] Regex aborted ($regex_error) for '$tkl'. Possibly too complex regex? " "[Spamfilter] Regex aborted ($regex_error) for '$tkl'. Possibly too complex regex? "
"To delete, use: /SPAMFILTER del $spamfilter_id", "To delete, use: /SPAMFILTER del $tkl.id",
log_data_string("regex_error", regex_error), log_data_string("regex_error", regex_error),
log_data_string("spamfilter_id", spamfilter_id(tkl)),
log_data_tkl("tkl", tkl)); log_data_tkl("tkl", tkl));
} else { } else {
unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_REGEX_ERROR", NULL, unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_REGEX_ERROR", NULL,
@@ -1308,7 +1523,7 @@ int tkl_ip_change(Client *client, const char *oldip)
{ {
TKL *tkl; TKL *tkl;
if ((tkl = find_tkline_match_zap(client))) if ((tkl = find_tkline_match_zap(client)))
banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT); banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT);
return 0; return 0;
} }
@@ -1317,7 +1532,7 @@ int tkl_accept(Client *client)
TKL *tkl; TKL *tkl;
if ((tkl = find_tkline_match_zap(client))) if ((tkl = find_tkline_match_zap(client)))
{ {
banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT); banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT);
return 2; // TODO: HOOK_DENY_ALWAYS; return 2; // TODO: HOOK_DENY_ALWAYS;
} }
return 0; return 0;
@@ -2431,7 +2646,8 @@ void spamfilter_del_by_id(Client *client, const char *id)
{ {
int index; int index;
TKL *tk; TKL *tk;
int found = 0; int matches = 0;
TKL *match = NULL;
char mo[32], mo2[32]; char mo[32], mo2[32];
const char *tkllayer[13] = { const char *tkllayer[13] = {
me.name, /* 0 server.name */ me.name, /* 0 server.name */
@@ -2453,21 +2669,30 @@ void spamfilter_del_by_id(Client *client, const char *id)
{ {
for (tk = tklines[index]; tk; tk = tk->next) for (tk = tklines[index]; tk; tk = tk->next)
{ {
if (((tk->type & (TKL_GLOBAL|TKL_SPAMF)) == (TKL_GLOBAL|TKL_SPAMF)) && !strcmp(spamfilter_id(tk), id)) if ((tk->type & (TKL_GLOBAL|TKL_SPAMF)) != (TKL_GLOBAL|TKL_SPAMF))
continue;
/* Match the real id, or for an id-less spamfilter (eg from an old server)
* its locally-computed fallback handle.
*/
if (tk->id[0] ? !strcmp(tk->id, id) : !strcmp(spamfilter_fallback_id(tk), id))
{ {
found = 1; match = tk;
break; matches++;
} }
} }
if (found)
break; /* break outer loop */
} }
if (!tk) if (matches == 0)
{ {
sendnotice(client, "Sorry, no spamfilter found with that ID. Did you run '/spamfilter del' to get the appropriate id?"); sendnotice(client, "Sorry, no spamfilter found with that ID. Did you run '/spamfilter del' to get the appropriate id?");
return; return;
} }
if (matches > 1)
{
sendnotice(client, "That ID is ambiguous (it matches multiple spamfilters). Please remove the spamfilter by its full definition instead.");
return;
}
tk = match;
/* Spamfilter found. Now fill the tkllayer */ /* Spamfilter found. Now fill the tkllayer */
tkllayer[1] = "-"; tkllayer[1] = "-";
@@ -3000,6 +3225,19 @@ TKL *_tkl_add_spamfilter(int type, const char *id, unsigned short target, BanAct
safe_strdup(tkl->ptr.spamfilter->id, id); safe_strdup(tkl->ptr.spamfilter->id, id);
tkl->ptr.spamfilter->show_message_content_on_hit = show_message_content_on_hit; tkl->ptr.spamfilter->show_message_content_on_hit = show_message_content_on_hit;
/* Config (and central) spamfilters are local per server, so they never receive
* a random network id. Give them a deterministic id instead, identical on every
* server: the config/central id label if set, otherwise a content hash. This is
* what lets a central spamfilter be referenced by the same id on every server.
*/
if (flags & (TKL_FLAG_CONFIG | TKL_FLAG_CENTRAL_SPAMFILTER))
{
if (!BadPtr(id))
strlcpy(tkl->id, id, sizeof(tkl->id));
else
strlcpy(tkl->id, spamfilter_fallback_id(tkl), sizeof(tkl->id));
}
if (tkl->ptr.spamfilter->target & SPAMF_USER) if (tkl->ptr.spamfilter->target & SPAMF_USER)
loop.do_bancheck_spamf_user = 1; loop.do_bancheck_spamf_user = 1;
if (tkl->ptr.spamfilter->target & SPAMF_AWAY) if (tkl->ptr.spamfilter->target & SPAMF_AWAY)
@@ -3679,15 +3917,15 @@ int _find_tkline_match(Client *client, int skip_soft)
{ {
ircstats.is_ref++; ircstats.is_ref++;
if (tkl->type & TKL_GLOBAL) if (tkl->type & TKL_GLOBAL)
banned_client(client, "G-Lined", tkl->ptr.serverban->reason, 1, 0); banned_client(client, "G-Lined", tkl->ptr.serverban->reason, tkl->id, 1, 0);
else else
banned_client(client, "K-Lined", tkl->ptr.serverban->reason, 0, 0); banned_client(client, "K-Lined", tkl->ptr.serverban->reason, tkl->id, 0, 0);
return 1; /* killed */ return 1; /* killed */
} else } else
if (tkl->type & TKL_ZAP) if (tkl->type & TKL_ZAP)
{ {
ircstats.is_ref++; ircstats.is_ref++;
banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, (tkl->type & TKL_GLOBAL)?1:0, 0); banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, 0);
return 1; /* killed */ return 1; /* killed */
} }
@@ -3929,12 +4167,15 @@ TKL *_find_tkline_match_zap(Client *client)
#define NOT_BY_REASON 0x8 #define NOT_BY_REASON 0x8
#define BY_SETBY 0x10 #define BY_SETBY 0x10
#define NOT_BY_SETBY 0x20 #define NOT_BY_SETBY 0x20
#define BY_ID 0x40
#define NOT_BY_ID 0x80
typedef struct { typedef struct {
int flags; int flags;
const char *mask; const char *mask;
const char *reason; const char *reason;
const char *set_by; const char *set_by;
const char *id;
} TKLFlag; } TKLFlag;
/** Parse STATS tkl parameters. /** Parse STATS tkl parameters.
@@ -3989,6 +4230,15 @@ static void parse_stats_params(const char *para, TKLFlag *flag)
flag->flags |= NOT_BY_SETBY; flag->flags |= NOT_BY_SETBY;
flag->set_by = tmp; flag->set_by = tmp;
break; break;
case 'i':
if (flag->id || !(tmp = strtok(NULL, " ")))
continue;
if (what == '+')
flag->flags |= BY_ID;
else
flag->flags |= NOT_BY_ID;
flag->id = tmp;
break;
} }
} }
} }
@@ -3998,6 +4248,9 @@ static void parse_stats_params(const char *para, TKLFlag *flag)
*/ */
int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklflags, TKL *tkl) int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklflags, TKL *tkl)
{ {
const char *id_str = tkl->id[0] ? tkl->id : "-";
const char *spamfilter_id_str = tkl->spamfilter_id[0] ? tkl->spamfilter_id : "-";
/***** First, handle the selection ******/ /***** First, handle the selection ******/
if (!BadPtr(para)) if (!BadPtr(para))
@@ -4008,6 +4261,12 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl
if (tklflags->flags & NOT_BY_SETBY) if (tklflags->flags & NOT_BY_SETBY)
if (match_simple(tklflags->set_by, tkl->set_by)) if (match_simple(tklflags->set_by, tkl->set_by))
return 0; return 0;
if (tklflags->flags & BY_ID)
if (!match_simple(tklflags->id, tkl->id))
return 0;
if (tklflags->flags & NOT_BY_ID)
if (match_simple(tklflags->id, tkl->id))
return 0;
if (TKLIsServerBan(tkl)) if (TKLIsServerBan(tkl))
{ {
if (tklflags->flags & BY_MASK) if (tklflags->flags & BY_MASK)
@@ -4078,7 +4337,7 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl
{ {
sendnumeric(client, RPL_STATSGLINE, 'K', namevalue_nospaces(m), sendnumeric(client, RPL_STATSGLINE, 'K', namevalue_nospaces(m),
(tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
(long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
} }
} else { } else {
@@ -4088,31 +4347,31 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl
{ {
sendnumeric(client, RPL_STATSGLINE, 'G', uhost, sendnumeric(client, RPL_STATSGLINE, 'G', uhost,
(tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
(long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
} else } else
if (tkl->type == (TKL_ZAP | TKL_GLOBAL)) if (tkl->type == (TKL_ZAP | TKL_GLOBAL))
{ {
sendnumeric(client, RPL_STATSGLINE, 'Z', uhost, sendnumeric(client, RPL_STATSGLINE, 'Z', uhost,
(tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
(long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
} else } else
if (tkl->type == (TKL_SHUN | TKL_GLOBAL)) if (tkl->type == (TKL_SHUN | TKL_GLOBAL))
{ {
sendnumeric(client, RPL_STATSGLINE, 's', uhost, sendnumeric(client, RPL_STATSGLINE, 's', uhost,
(tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
(long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
} else } else
if (tkl->type == (TKL_KILL)) if (tkl->type == (TKL_KILL))
{ {
sendnumeric(client, RPL_STATSGLINE, 'K', uhost, sendnumeric(client, RPL_STATSGLINE, 'K', uhost,
(tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
(long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
} else } else
if (tkl->type == (TKL_ZAP)) if (tkl->type == (TKL_ZAP))
{ {
sendnumeric(client, RPL_STATSGLINE, 'z', uhost, sendnumeric(client, RPL_STATSGLINE, 'z', uhost,
(tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
(long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.serverban->reason); (long long)(TStime() - tkl->set_at), tkl->set_by, (long long)0, (long long)0, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
} }
} }
} else } else
@@ -4130,18 +4389,17 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl
tkl->set_by, tkl->set_by,
tkl->ptr.spamfilter->hits, tkl->ptr.spamfilter->hits,
tkl->ptr.spamfilter->hits_except, tkl->ptr.spamfilter->hits_except,
(long long)0,
(long long)0,
id_str,
tkl->ptr.spamfilter->match->str); tkl->ptr.spamfilter->match->str);
if (para && !strcasecmp(para, "del")) if (para && !strcasecmp(para, "del"))
{ {
char *hash = spamfilter_id(tkl); if (!(tkl->type & TKL_GLOBAL))
if (tkl->type & TKL_GLOBAL)
{
sendtxtnumeric(client, "To delete this spamfilter, use /SPAMFILTER del %s", hash);
sendtxtnumeric(client, "-");
} else {
sendtxtnumeric(client, "This spamfilter is stored in the configuration file and cannot be removed with /SPAMFILTER del"); sendtxtnumeric(client, "This spamfilter is stored in the configuration file and cannot be removed with /SPAMFILTER del");
sendtxtnumeric(client, "-"); else
} sendtxtnumeric(client, "To delete this spamfilter, use /SPAMFILTER del %s", tkl->id[0] ? tkl->id : spamfilter_fallback_id(tkl));
sendtxtnumeric(client, "-");
} }
} else } else
if (TKLIsNameBan(tkl)) if (TKLIsNameBan(tkl))
@@ -4152,6 +4410,9 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl
(tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
(long long)(TStime() - tkl->set_at), (long long)(TStime() - tkl->set_at),
tkl->set_by, tkl->set_by,
(long long)0,
(long long)0,
id_str,
tkl->ptr.nameban->reason); tkl->ptr.nameban->reason);
} else } else
if (TKLIsBanException(tkl)) if (TKLIsBanException(tkl))
@@ -4165,7 +4426,7 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl
sendnumeric(client, RPL_STATSEXCEPTTKL, namevalue_nospaces(m), sendnumeric(client, RPL_STATSEXCEPTTKL, namevalue_nospaces(m),
tkl->ptr.banexception->bantypes, tkl->ptr.banexception->bantypes,
(tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
(long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.banexception->reason); (long long)(TStime() - tkl->set_at), tkl->set_by, id_str, tkl->ptr.banexception->reason);
} }
} else { } else {
/* IRC-added: uses simple user/host mask */ /* IRC-added: uses simple user/host mask */
@@ -4174,7 +4435,7 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl
sendnumeric(client, RPL_STATSEXCEPTTKL, uhost, sendnumeric(client, RPL_STATSEXCEPTTKL, uhost,
tkl->ptr.banexception->bantypes, tkl->ptr.banexception->bantypes,
(tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0, (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
(long long)(TStime() - tkl->set_at), tkl->set_by, tkl->ptr.banexception->reason); (long long)(TStime() - tkl->set_at), tkl->set_by, id_str, tkl->ptr.banexception->reason);
} }
} else } else
{ {
@@ -4255,6 +4516,7 @@ void _tkl_stats(Client *client, int type, const char *para, int *cnt)
*/ */
void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl) void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl)
{ {
MessageTag *mtags = NULL;
char typ; char typ;
if (!(tkl->type & TKL_GLOBAL)) if (!(tkl->type & TKL_GLOBAL))
@@ -4262,9 +4524,13 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl)
typ = tkl_typetochar(tkl->type); typ = tkl_typetochar(tkl->type);
/* Carry the s2s-tkl/ fields (id, spamfilter_id) on add; del matches by mask so needs no tags. */
if (add)
tkl_add_s2s_mtags(tkl, &mtags);
if (TKLIsServerBan(tkl)) if (TKLIsServerBan(tkl))
{ {
sendto_one(to, NULL, ":%s TKL %c %c %s%s %s %s %lld %lld :%s", sender->name, sendto_one(to, mtags, ":%s TKL %c %c %s%s %s %s %lld %lld :%s", sender->name,
add ? '+' : '-', add ? '+' : '-',
typ, typ,
(tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) ? "%" : "", (tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) ? "%" : "",
@@ -4275,7 +4541,7 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl)
} else } else
if (TKLIsNameBan(tkl)) if (TKLIsNameBan(tkl))
{ {
sendto_one(to, NULL, ":%s TKL %c %c %c %s %s %lld %lld :%s", sender->name, sendto_one(to, mtags, ":%s TKL %c %c %c %s %s %lld %lld :%s", sender->name,
add ? '+' : '-', add ? '+' : '-',
typ, typ,
tkl->ptr.nameban->hold ? 'H' : '*', tkl->ptr.nameban->hold ? 'H' : '*',
@@ -4286,7 +4552,7 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl)
} else } else
if (TKLIsSpamfilter(tkl)) if (TKLIsSpamfilter(tkl))
{ {
sendto_one(to, NULL, ":%s TKL %c %c %s %c %s %lld %lld %lld %s %s :%s", sender->name, sendto_one(to, mtags, ":%s TKL %c %c %s %c %s %lld %lld %lld %s %s :%s", sender->name,
add ? '+' : '-', add ? '+' : '-',
typ, typ,
spamfilter_target_inttostring(tkl->ptr.spamfilter->target), spamfilter_target_inttostring(tkl->ptr.spamfilter->target),
@@ -4299,7 +4565,7 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl)
} else } else
if (TKLIsBanException(tkl)) if (TKLIsBanException(tkl))
{ {
sendto_one(to, NULL, ":%s TKL %c %c %s%s %s %s %lld %lld %s :%s", sender->name, sendto_one(to, mtags, ":%s TKL %c %c %s%s %s %s %lld %lld %s :%s", sender->name,
add ? '+' : '-', add ? '+' : '-',
typ, typ,
(tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT) ? "%" : "", (tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT) ? "%" : "",
@@ -4316,6 +4582,8 @@ void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl)
log_data_integer("tkl_type_int", typ)); log_data_integer("tkl_type_int", typ));
abort(); abort();
} }
safe_free_message_tags(mtags);
} }
/** Broadcast a TKL entry. /** Broadcast a TKL entry.
@@ -4822,6 +5090,30 @@ CMD_FUNC(cmd_tkl_add)
* Note that we only update common fields, * Note that we only update common fields,
* which is acceptable to me. -- Syzop * which is acceptable to me. -- Syzop
*/ */
const char *incoming_id = tkl_s2s_mtag_value(recv_mtags, "id");
const char *incoming_spamfilter_id = NULL;
int changed = 0;
/* id: an assigned id beats a blank one; two assigned ids tie-break by older
* set_at, then lexicographically. The "spamfilter_id" travels with the winning "id".
* This is done before set_at is lowered below, so the tie-break sees the original set_at.
*/
if (!BadPtr(incoming_id) && (strlen(incoming_id) < sizeof(tkl->id)) && strcmp(tkl->id, incoming_id))
{
if (!tkl->id[0] ||
(set_at < tkl->set_at) ||
((set_at == tkl->set_at) && (strcmp(incoming_id, tkl->id) < 0)))
{
strlcpy(tkl->id, incoming_id, sizeof(tkl->id));
incoming_spamfilter_id = tkl_s2s_mtag_value(recv_mtags, "spamfilter_id");
if (!BadPtr(incoming_spamfilter_id) && (strlen(incoming_spamfilter_id) < sizeof(tkl->spamfilter_id)))
strlcpy(tkl->spamfilter_id, incoming_spamfilter_id, sizeof(tkl->spamfilter_id));
else
tkl->spamfilter_id[0] = '\0';
changed = 1;
}
}
if ((set_at < tkl->set_at) || (expire_at != tkl->expire_at) || strcmp(tkl->set_by, parv[5])) if ((set_at < tkl->set_at) || (expire_at != tkl->expire_at) || strcmp(tkl->set_by, parv[5]))
{ {
/* here's how it goes: /* here's how it goes:
@@ -4844,12 +5136,22 @@ CMD_FUNC(cmd_tkl_add)
if (strcmp(tkl->set_by, parv[5]) < 0) if (strcmp(tkl->set_by, parv[5]) < 0)
safe_strdup(tkl->set_by, parv[5]); safe_strdup(tkl->set_by, parv[5]);
if (type & TKL_GLOBAL) changed = 1;
tkl_broadcast_entry(1, client, client, tkl);
} }
if (changed && (type & TKL_GLOBAL))
tkl_broadcast_entry(1, client, client, tkl);
return; return;
} }
/* New entry: assign its unique id. Honor a supplied s2s-tkl/id (and spamfilter_id) from a
* remote server or services; if none and we are the originating server, generate a random one;
* otherwise leave it blank (eg the tag was dropped by an old server in the path).
*/
tkl_extract_s2s_mtags(tkl, recv_mtags);
if (!tkl->id[0] && IsMe(client))
tkl_generate_id(tkl);
tkl_added(client, tkl); tkl_added(client, tkl);
} }
@@ -5169,7 +5471,18 @@ void ban_action_run_all_sets_and_stops(Client *client, BanAction *action, int *s
* @note Be sure to check IsDead(client) if return value is 1 and you are * @note Be sure to check IsDead(client) if return value is 1 and you are
* considering to continue processing. * considering to continue processing.
*/ */
static int take_action_ex(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped, const char *spamfilter_id);
int _take_action(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped) int _take_action(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped)
{
return take_action_ex(client, actions, reason, duration, take_action_flags, stopped, NULL);
}
/** Internal variant of take_action() that records the spamfilter id on any
* *LINE/SHUN it creates, for the gline->spamfilter trace. Kept internal so the public
* take_action() efunc stays free of this spamfilter-specific concept.
*/
static int take_action_ex(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped, const char *spamfilter_id)
{ {
BanAction *action; BanAction *action;
int previous_highest = 0; int previous_highest = 0;
@@ -5244,7 +5557,11 @@ int _take_action(Client *client, BanAction *actions, const char *reason, long du
tkllayer[6] = mo; tkllayer[6] = mo;
tkllayer[7] = mo2; tkllayer[7] = mo2;
tkllayer[8] = reason; tkllayer[8] = reason;
cmd_tkl(NULL, &me, NULL, 9, tkllayer); {
MessageTag *m = tkl_spamfilter_id_mtag(spamfilter_id); /* NULL unless set by a spamfilter */
cmd_tkl(NULL, &me, m, 9, tkllayer);
safe_free_message_tags(m);
}
RunHookReturnInt(HOOKTYPE_TAKE_ACTION, !=99, client, action->action, reason, duration); RunHookReturnInt(HOOKTYPE_TAKE_ACTION, !=99, client, action->action, reason, duration);
if ((action->action == BAN_ACT_SHUN) || (action->action == BAN_ACT_SOFT_SHUN)) if ((action->action == BAN_ACT_SHUN) || (action->action == BAN_ACT_SOFT_SHUN))
{ {
@@ -5754,7 +6071,7 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char
/* Spamfilter matched */ /* Spamfilter matched */
reason = unreal_decodespace(tkl->ptr.spamfilter->tkl_reason); reason = unreal_decodespace(tkl->ptr.spamfilter->tkl_reason);
ret = take_action(client, tkl->ptr.spamfilter->action, reason, tkl->ptr.spamfilter->tkl_duration, TAKE_ACTION_SKIP_SET, NULL); ret = take_action_ex(client, tkl->ptr.spamfilter->action, reason, tkl->ptr.spamfilter->tkl_duration, TAKE_ACTION_SKIP_SET, NULL, tkl->id);
if (!IsDead(client)) if (!IsDead(client))
{ {
if ((ret == BAN_ACT_BLOCK) || (ret == BAN_ACT_SOFT_BLOCK)) if ((ret == BAN_ACT_BLOCK) || (ret == BAN_ACT_SOFT_BLOCK))
+49 -5
View File
@@ -29,7 +29,7 @@ ModuleHeader MOD_HEADER = {
#define TKLDB_MAGIC 0x10101010 #define TKLDB_MAGIC 0x10101010
/* Database version */ /* Database version */
#define TKLDB_VERSION 4999 #define TKLDB_VERSION 6260
/* Save tkls to file every <this> seconds */ /* Save tkls to file every <this> seconds */
#define TKLDB_SAVE_EVERY 300 #define TKLDB_SAVE_EVERY 300
/* The very first save after boot, apply this delta, this /* The very first save after boot, apply this delta, this
@@ -401,6 +401,15 @@ int write_tkline(UnrealDB *db, const char *tmpfname, TKL *tkl)
W_SAFE(unrealdb_write_str(db, tkl->set_by)); W_SAFE(unrealdb_write_str(db, tkl->set_by));
W_SAFE(unrealdb_write_int64(db, tkl->set_at)); W_SAFE(unrealdb_write_int64(db, tkl->set_at));
W_SAFE(unrealdb_write_int64(db, tkl->expire_at)); W_SAFE(unrealdb_write_int64(db, tkl->expire_at));
W_SAFE(unrealdb_write_str(db, tkl->id)); /* since TKLDB_VERSION 6260 */
W_SAFE(unrealdb_write_str(db, tkl->spamfilter_id)); /* since TKLDB_VERSION 6260 */
/* Reserved hit-stat fields for all TKL types (since TKLDB_VERSION 6260):
* hits, lasthit. Written as 0 for now; a later release will populate them
* (for non-config TKLs).
*/
W_SAFE(unrealdb_write_int64(db, 0));
W_SAFE(unrealdb_write_int64(db, 0));
if (TKLIsServerBan(tkl)) if (TKLIsServerBan(tkl))
{ {
@@ -446,6 +455,11 @@ int write_tkline(UnrealDB *db, const char *tmpfname, TKL *tkl)
W_SAFE(unrealdb_write_char(db, action)); W_SAFE(unrealdb_write_char(db, action));
W_SAFE(unrealdb_write_str(db, tkl->ptr.spamfilter->tkl_reason)); W_SAFE(unrealdb_write_str(db, tkl->ptr.spamfilter->tkl_reason));
W_SAFE(unrealdb_write_int64(db, tkl->ptr.spamfilter->tkl_duration)); W_SAFE(unrealdb_write_int64(db, tkl->ptr.spamfilter->tkl_duration));
/* Reserved spamfilter-only hit-stat fields (since TKLDB_VERSION 6260):
* hits_except, lasthit_except. Written as 0 for now.
*/
W_SAFE(unrealdb_write_int64(db, 0));
W_SAFE(unrealdb_write_int64(db, 0));
} }
return 1; return 1;
@@ -530,6 +544,7 @@ int read_tkldb(void)
for (cnt = 0; cnt < tklcount; cnt++) for (cnt = 0; cnt < tklcount; cnt++)
{ {
int do_not_add = 0; int do_not_add = 0;
TKL *added = NULL;
tkl = safe_alloc(sizeof(TKL)); tkl = safe_alloc(sizeof(TKL));
@@ -556,6 +571,22 @@ int read_tkldb(void)
R_SAFE(unrealdb_read_int64(db, &v)); R_SAFE(unrealdb_read_int64(db, &v));
tkl->expire_at = v; tkl->expire_at = v;
/* id and spamfilter_id were added in TKLDB_VERSION 6260 */
if (version >= 6260)
{
R_SAFE(unrealdb_read_str(db, &str));
strlcpy(tkl->id, str, sizeof(tkl->id));
safe_free(str);
R_SAFE(unrealdb_read_str(db, &str));
strlcpy(tkl->spamfilter_id, str, sizeof(tkl->spamfilter_id));
safe_free(str);
/* Reserved hit-stat fields for all TKL types (hits, lasthit).
* Read and discarded for now; a later release will use them.
*/
R_SAFE(unrealdb_read_int64(db, &v));
R_SAFE(unrealdb_read_int64(db, &v));
}
/* Save some CPU... if it's already expired then don't bother adding */ /* Save some CPU... if it's already expired then don't bother adding */
if (tkl->expire_at != 0 && tkl->expire_at <= TStime()) if (tkl->expire_at != 0 && tkl->expire_at <= TStime())
do_not_add = 1; do_not_add = 1;
@@ -599,7 +630,7 @@ int read_tkldb(void)
if (!do_not_add) if (!do_not_add)
{ {
tkl_add_serverban(tkl->type, tkl->ptr.serverban->usermask, added = tkl_add_serverban(tkl->type, tkl->ptr.serverban->usermask,
tkl->ptr.serverban->hostmask, tkl->ptr.serverban->hostmask,
NULL, NULL,
tkl->ptr.serverban->reason, tkl->ptr.serverban->reason,
@@ -639,7 +670,7 @@ int read_tkldb(void)
if (!do_not_add) if (!do_not_add)
{ {
tkl_add_banexception(tkl->type, tkl->ptr.banexception->usermask, added = tkl_add_banexception(tkl->type, tkl->ptr.banexception->usermask,
tkl->ptr.banexception->hostmask, tkl->ptr.banexception->hostmask,
NULL, NULL,
tkl->ptr.banexception->reason, tkl->ptr.banexception->reason,
@@ -668,7 +699,7 @@ int read_tkldb(void)
if (!do_not_add) if (!do_not_add)
{ {
tkl_add_nameban(tkl->type, tkl->ptr.nameban->name, added = tkl_add_nameban(tkl->type, tkl->ptr.nameban->name,
tkl->ptr.nameban->hold, tkl->ptr.nameban->hold,
tkl->ptr.nameban->reason, tkl->ptr.nameban->reason,
tkl->set_by, tkl->expire_at, tkl->set_by, tkl->expire_at,
@@ -726,6 +757,13 @@ int read_tkldb(void)
R_SAFE(unrealdb_read_str(db, &tkl->ptr.spamfilter->tkl_reason)); R_SAFE(unrealdb_read_str(db, &tkl->ptr.spamfilter->tkl_reason));
R_SAFE(unrealdb_read_int64(db, &v)); R_SAFE(unrealdb_read_int64(db, &v));
tkl->ptr.spamfilter->tkl_duration = v; tkl->ptr.spamfilter->tkl_duration = v;
/* Reserved spamfilter-only hit-stat fields (hits_except,
* lasthit_except). Read and discarded for now. */
if (version >= 6260)
{
R_SAFE(unrealdb_read_int64(db, &v));
R_SAFE(unrealdb_read_int64(db, &v));
}
if (!do_not_add && if (!do_not_add &&
find_tkl_spamfilter(tkl->type, tkl->ptr.spamfilter->match->str, find_tkl_spamfilter(tkl->type, tkl->ptr.spamfilter->match->str,
@@ -744,7 +782,7 @@ int read_tkldb(void)
if (!do_not_add) if (!do_not_add)
{ {
tkl_add_spamfilter(tkl->type, NULL, tkl->ptr.spamfilter->target, added = tkl_add_spamfilter(tkl->type, NULL, tkl->ptr.spamfilter->target,
tkl->ptr.spamfilter->action, tkl->ptr.spamfilter->action,
tkl->ptr.spamfilter->match, tkl->ptr.spamfilter->match,
NULL, NULL,
@@ -771,6 +809,12 @@ int read_tkldb(void)
break; /* we MUST stop reading */ break; /* we MUST stop reading */
} }
if (added)
{
strlcpy(added->id, tkl->id, sizeof(added->id));
strlcpy(added->spamfilter_id, tkl->spamfilter_id, sizeof(added->spamfilter_id));
}
if (!do_not_add) if (!do_not_add)
added_cnt++; added_cnt++;