Add spamfilter hits and hits for exempted users.

* This means we always run spamfilters, even if users are exempts
* This way we can gather hits for exempted users on individual
  spamfilter entries, and possibly detect false positives
  (which relies on the assumption that those users are innocent)
* The hit counters are shown in in RPL_STATSSPAMF and also
  exposed via the JSON-RCP API.
* This commit also adds set::central-spamfilter::except but more
  on that later since i still want to set a default for that in
  a future commit.
* This also changes take_action() to take flags and adds the
  option TAKE_ACTION_SIMULATE_USER_ACTION which i intended to
  use but didn't in the end... not sure if i should keep it :D
This commit is contained in:
Bram Matthys 2023-07-10 11:28:20 +02:00
parent 0c622c0a73
commit c18c79e88b
No known key found for this signature in database
GPG key ID: BF8116B163EAAE98
9 changed files with 114 additions and 38 deletions

View file

@ -37,6 +37,11 @@ in progress and may not be a stable version.
include "some-file-or-url" { restrict-config { name-of-block; name-of-block2; } }
```
### Developers and protocol:
* Changes in numeric 229 (RPL_STATSSPAMF): Now includes hits and hits for
users that are exempt, two counters right inserted right before the last one.
* Several API changes, like `place_host_ban` to `take_action`
UnrealIRCd 6.1.1.1
-------------------
This 6.1.1.1 version is an update to 6.1.1: a bug and memory leak was fixed

View file

@ -129,6 +129,7 @@ struct Configuration {
long central_spamfilter_refresh_time;
int central_spamfilter_verbose;
int central_spamfilter_enabled;
SecurityGroup *central_spamfilter_except;
int maxbans;
int watch_away_notification;
int uhnames;

View file

@ -841,7 +841,7 @@ extern MODVAR TKL *(*find_tkline_match_zap)(Client *cptr);
extern MODVAR void (*tkl_stats)(Client *cptr, int type, const char *para, int *cnt);
extern MODVAR void (*tkl_sync)(Client *client);
extern MODVAR void (*cmd_tkl)(Client *client, MessageTag *recv_mtags, int parc, const char *parv[]);
extern MODVAR int (*take_action)(Client *client, BanAction *actions, const char *reason, long duration, int skip_set);
extern MODVAR int (*take_action)(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags);
extern MODVAR int (*match_spamfilter)(Client *client, const char *str_in, int type, const char *cmd, const char *target, int flags, TKL **rettk);
extern MODVAR int (*match_spamfilter_mtags)(Client *client, MessageTag *mtags, const char *cmd);
extern MODVAR int (*join_viruschan)(Client *client, TKL *tk, int type);

View file

@ -380,7 +380,7 @@
#define STR_RPL_STATSNLINE /* 226 */ "n %s %s"
#define STR_RPL_STATSVLINE /* 227 */ "v %s %s %s"
#define STR_RPL_STATSBANVER /* 228 */ "%s %s"
#define STR_RPL_STATSSPAMF /* 229 */ "%c %s %s %s %lld %lld %lld %s %s :%s"
#define STR_RPL_STATSSPAMF /* 229 */ "%c %s %s %s %lld %lld %lld %s %s %lld %lld :%s"
#define STR_RPL_STATSEXCEPTTKL /* 230 */ "%s %s %lld %lld %s :%s"
#define STR_RPL_RULES /* 232 */ ":- %s"
#define STR_RPL_STATSLLINE /* 241 */ "%c %s * %s %d %d"

View file

@ -1174,6 +1174,7 @@ typedef enum BanActionValue {
// do not use 99, it is special in tkl take_action
BAN_ACT_SOFT_WARN = 50,
BAN_ACT_REPORT = 40,
// anything above BAN_ACT_SET will will cause a log message to be emitted
BAN_ACT_SET = 30,
} BanActionValue;
@ -1198,6 +1199,10 @@ struct BanAction {
(x == BAN_ACT_SOFT_DCCBLOCK) || (x == BAN_ACT_SOFT_BLOCK) || \
(x == BAN_ACT_SOFT_WARN))
/** Skip BAN_ACT_SET (eg because you already processed them earlier, like in match_spamfilter) */
#define TAKE_ACTION_SKIP_SET 0x1
/** Don't ban/kill/block/etc, but do return value as if we did */
#define TAKE_ACTION_SIMULATE_USER_ACTION 0x2
/** Server ban sub-struct of TKL entry (KLINE/GLINE/ZLINE/GZLINE/SHUN) */
struct ServerBan {
@ -1224,6 +1229,8 @@ struct Spamfilter {
char *tkl_reason; /**< Reason to use for bans placed by this spamfilter, escaped by unreal_encodespace(). */
time_t tkl_duration; /**< Duration of bans placed by this spamfilter */
char *id; /**< ID */
long long hits; /**< Spamfilter hits (except exempts) */
long long hits_except; /**< Spamfilter hits by exempt clients */
};
/** Ban exception sub-struct of TKL entry (ELINE) */

View file

@ -74,7 +74,7 @@ TKL *(*find_tkline_match_zap)(Client *client);
void (*tkl_stats)(Client *client, int type, const char *para, int *cnt);
void (*tkl_sync)(Client *client);
void (*cmd_tkl)(Client *client, MessageTag *mtags, int parc, const char *parv[]);
int (*take_action)(Client *client, BanAction *action, const char *reason, long duration, int skip_set);
int (*take_action)(Client *client, BanAction *action, const char *reason, long duration, int take_action_flags);
int (*match_spamfilter)(Client *client, const char *str_in, int type, const char *cmd, const char *target, int flags, TKL **rettk);
int (*match_spamfilter_mtags)(Client *client, MessageTag *mtags, const char *cmd);
int (*join_viruschan)(Client *client, TKL *tk, int type);

View file

@ -1651,6 +1651,7 @@ void free_iConf(Configuration *i)
safe_free(i->sasl_server);
safe_free_all_ban_actions(i->handshake_data_flood_ban_action);
safe_free(i->central_spamfilter_url);
free_security_group(i->central_spamfilter_except);
// anti-flood:
for (f = i->floodsettings; f; f = f_next)
{
@ -8057,7 +8058,8 @@ int _conf_set(ConfigFile *conf, ConfigEntry *ce)
tempiConf.central_spamfilter_verbose = atoi(cepp->value);
else if (!strcmp(cepp->name, "enabled"))
tempiConf.central_spamfilter_enabled = config_checkval(cepp->value, CFG_YESNO);
// TODO: except, with a default of identified users.
else if (!strcmp(cepp->name, "except"))
conf_match_block(conf, cepp, &tempiConf.central_spamfilter_except);
}
}
else if (!strcmp(cep->name, "default-bantime"))
@ -9180,6 +9182,14 @@ int _test_set(ConfigFile *conf, ConfigEntry *ce)
{
for (cepp = cep->items; cepp; cepp = cepp->next)
{
if (!strcmp(cepp->name, "except"))
{
test_match_block(conf, cepp, &errors);
} else
if (!cepp->value)
{
CheckNull(cepp);
} else
if (!strcmp(cepp->name, "url"))
{
} else
@ -9194,11 +9204,20 @@ int _test_set(ConfigFile *conf, ConfigEntry *ce)
errors++;
}
#endif
}
else if (!strcmp(cepp->name, "verbose"))
} else
if (!strcmp(cepp->name, "verbose"))
{
} else
if (!strcmp(cepp->name, "enabled"))
{
} else
{
config_error_unknown(cepp->file->filename,
cepp->line_number, "set::central-spamfilter",
cepp->name);
errors++;
continue;
}
// TODO: except
}
}
else if (!strcmp(cep->name, "default-bantime"))

View file

@ -581,5 +581,7 @@ void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail)
json_object_set_new(j, "ban_duration_string", json_string_unreal(pretty_time_val_r(buf, sizeof(buf), tkl->ptr.spamfilter->tkl_duration)));
json_object_set_new(j, "spamfilter_targets", json_string_unreal(spamfilter_target_inttostring(tkl->ptr.spamfilter->target)));
json_object_set_new(j, "reason", json_string_unreal(unreal_decodespace(tkl->ptr.spamfilter->tkl_reason)));
json_object_set_new(j, "hits", json_integer(tkl->ptr.spamfilter->hits));
json_object_set_new(j, "hits_except", json_integer(tkl->ptr.spamfilter->hits_except));
}
}

View file

@ -90,7 +90,7 @@ TKL *_find_tkline_match_zap(Client *client);
void _tkl_stats(Client *client, int type, const char *para, int *cnt);
void _tkl_sync(Client *client);
CMD_FUNC(_cmd_tkl);
int _take_action(Client *client, BanAction *action, char *reason, long duration, int skip_set);
int _take_action(Client *client, BanAction *action, char *reason, long duration, int take_action_flags);
int _match_spamfilter(Client *client, const char *str_in, int type, const char *cmd, const char *target, int flags, TKL **rettk);
int _match_spamfilter_mtags(Client *client, MessageTag *mtags, char *cmd);
int check_mtag_spamfilters_present(void);
@ -3762,6 +3762,8 @@ int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklfl
(long long)tkl->ptr.spamfilter->tkl_duration,
tkl->ptr.spamfilter->tkl_reason,
tkl->set_by,
tkl->ptr.spamfilter->hits,
tkl->ptr.spamfilter->hits_except,
tkl->ptr.spamfilter->match->str);
if (para && !strcasecmp(para, "del"))
{
@ -4771,11 +4773,11 @@ void ban_action_run_all_sets(Client *client, BanAction *action)
/** Take an action on the user, such as banning or killing.
* @author Bram Matthys (Syzop), 2003-present
* @param client The client which is affected.
* @param action The type of ban (one of BAN_ACT_*).
* @param reason The ban reason.
* @param duration The ban duration in seconds.
* @param skip_set Skip BAN_ACT_SET (eg because you already processed them earlier, like in match_spamfilter)
* @param client The client which is affected.
* @param action The type of ban (one of BAN_ACT_*).
* @param reason The ban reason.
* @param duration The ban duration in seconds.
* @param take_action_flags One of TAKE_ACTION_*
* @note This function assumes that client is a locally connected user.
* @retval 0 user is exempt or no action needs to be taken for other reasons (eg only var setting)
* @retval BAN_ACT_* the highest BAN_ACT_xxx value, eg BAN_ACT_BLOCK or BAN_ACT_GLINE, etc...
@ -4783,7 +4785,7 @@ void ban_action_run_all_sets(Client *client, BanAction *action)
* @note Be sure to check IsDead(client) if return value is 1 and you are
* considering to continue processing.
*/
int _take_action(Client *client, BanAction *actions, char *reason, long duration, int skip_set)
int _take_action(Client *client, BanAction *actions, char *reason, long duration, int take_action_flags)
{
BanAction *action;
int previous_highest = 0;
@ -4823,6 +4825,8 @@ int _take_action(Client *client, BanAction *actions, char *reason, long duration
NULL /*8 reason */
};
if (take_action_flags & TAKE_ACTION_SIMULATE_USER_ACTION)
break;
ban_target_to_tkl_layer(iConf.automatic_ban_target, action->action, client, &tkllayer[3], &tkllayer[4]);
/* For soft bans we need to prefix the % in the username */
@ -4869,11 +4873,15 @@ int _take_action(Client *client, BanAction *actions, char *reason, long duration
}
case BAN_ACT_SOFT_KILL:
case BAN_ACT_KILL:
if (take_action_flags & TAKE_ACTION_SIMULATE_USER_ACTION)
break;
RunHookReturnInt(HOOKTYPE_TAKE_ACTION, !=99, client, action->action, reason, duration);
exit_client(client, NULL, reason);
break;
case BAN_ACT_SOFT_TEMPSHUN:
case BAN_ACT_TEMPSHUN:
if (take_action_flags & TAKE_ACTION_SIMULATE_USER_ACTION)
break;
/* We simply mark this connection as shunned and do not add a ban record */
unreal_log(ULOG_INFO, "tkl", "TKL_ADD_TEMPSHUN", &me,
"Temporary shun added on user $target.details [reason: $shun_reason] [by: $client]",
@ -4882,10 +4890,12 @@ int _take_action(Client *client, BanAction *actions, char *reason, long duration
SetShunned(client);
break;
case BAN_ACT_REPORT:
if (take_action_flags & TAKE_ACTION_SIMULATE_USER_ACTION)
break;
spamreport(client, client->ip, NULL, action->var);
break;
case BAN_ACT_SET:
if (!skip_set)
if (!(take_action_flags & TAKE_ACTION_SKIP_SET))
ban_act_set(client, action);
break;
default:
@ -5007,6 +5017,15 @@ int _join_viruschan(Client *client, TKL *tkl, int type)
return 1;
}
int match_spamfilter_exempt(TKL *tkl, char user_is_exempt_general, char user_is_exempt_central)
{
if (user_is_exempt_general)
return 1;
if ((tkl->flags & TKL_FLAG_CENTRAL_SPAMFILTER) && user_is_exempt_central)
return 1;
return 0;
}
/** match_spamfilter: executes the spamfilter on the input string.
* @param str The text (eg msg text, notice text, part text, quit text, etc
* @param target The spamfilter target (SPAMF_*)
@ -5030,6 +5049,8 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char
long ms_past;
#endif
int tags_serial = client->local ? client->local->tags_serial : 0;
char user_is_exempt_general = 0;
char user_is_exempt_central = 0;
if (rettkl)
*rettkl = NULL; /* initialize to NULL */
@ -5052,7 +5073,10 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char
* Let's check that early: going through elines is likely faster than running the regex(es).
*/
if (find_tkl_exception(TKL_SPAMF, client))
return 0;
user_is_exempt_general = 1;
if (user_allowed_by_security_group(client, iConf.central_spamfilter_except))
user_is_exempt_central = 1;
for (tkl = tklines[tkl_hash('F')]; tkl; tkl = tkl->next)
{
@ -5127,17 +5151,25 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char
if (!winner_tkl && destination && target_is_spamexcept(destination))
return 0; /* No problem! */
// TODO: if (only_actions_of_type(tkl->ptr.spamfilter->action, BAN_ACT_SET)) then don't show the warning unless debugging or something :)
unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client,
"[Spamfilter] $client.details matches filter '$tkl': [cmd: $command$_space$destination: '$str'] [reason: $tkl.reason] [action: $tkl.ban_action]",
log_data_tkl("tkl", tkl),
log_data_string("command", cmd),
log_data_string("_space", destination ? " " : ""),
log_data_string("destination", destination ? destination : ""),
log_data_string("str", str));
if (match_spamfilter_exempt(tkl, user_is_exempt_general, user_is_exempt_central))
{
tkl->ptr.spamfilter->hits_except++;
} else
{
tkl->ptr.spamfilter->hits++;
if (highest_spamfilter_action(tkl->ptr.spamfilter->action) > BAN_ACT_SET)
{
unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client,
"[Spamfilter] $client.details matches filter '$tkl': [cmd: $command$_space$destination: '$str'] [reason: $tkl.reason] [action: $tkl.ban_action]",
log_data_tkl("tkl", tkl),
log_data_string("command", cmd),
log_data_string("_space", destination ? " " : ""),
log_data_string("destination", destination ? destination : ""),
log_data_string("str", str));
RunHook(HOOKTYPE_LOCAL_SPAMFILTER, client, str, str_in, target, destination, tkl);
RunHook(HOOKTYPE_LOCAL_SPAMFILTER, client, str, str_in, target, destination, tkl);
}
}
/* Run any SET actions */
ban_action_run_all_sets(client, tkl->ptr.spamfilter->action);
@ -5195,17 +5227,25 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char
if (!winner_tkl && destination && target_is_spamexcept(destination))
return 0; /* No problem! */
// TODO: if (only_actions_of_type(tkl->ptr.spamfilter->action, BAN_ACT_SET)) then don't show the warning unless debugging or something :)
unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client,
"[Spamfilter] $client.details matches filter '$tkl': [cmd: $command$_space$destination: '$str'] [reason: $tkl.reason] [action: $tkl.ban_action]",
log_data_tkl("tkl", tkl),
log_data_string("command", cmd),
log_data_string("_space", destination ? " " : ""),
log_data_string("destination", destination ? destination : ""),
log_data_string("str", str));
if (match_spamfilter_exempt(tkl, user_is_exempt_general, user_is_exempt_central))
{
tkl->ptr.spamfilter->hits_except++;
} else
{
tkl->ptr.spamfilter->hits++;
if (highest_spamfilter_action(tkl->ptr.spamfilter->action) > BAN_ACT_SET)
{
unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client,
"[Spamfilter] $client.details matches filter '$tkl': [cmd: $command$_space$destination: '$str'] [reason: $tkl.reason] [action: $tkl.ban_action]",
log_data_tkl("tkl", tkl),
log_data_string("command", cmd),
log_data_string("_space", destination ? " " : ""),
log_data_string("destination", destination ? destination : ""),
log_data_string("str", str));
RunHook(HOOKTYPE_LOCAL_SPAMFILTER, client, str, str_in, target, destination, tkl);
RunHook(HOOKTYPE_LOCAL_SPAMFILTER, client, str, str_in, target, destination, tkl);
}
}
/* Run any SET actions */
ban_action_run_all_sets(client, tkl->ptr.spamfilter->action);
@ -5225,10 +5265,12 @@ int _match_spamfilter(Client *client, const char *str_in, int target, const char
if (!tkl)
return 0; /* NOMATCH, we are done */
/* Spamfilter matched, take action: */
if (match_spamfilter_exempt(tkl, user_is_exempt_general, user_is_exempt_central))
return 0;
/* Spamfilter matched */
reason = unreal_decodespace(tkl->ptr.spamfilter->tkl_reason);
ret = take_action(client, tkl->ptr.spamfilter->action, reason, tkl->ptr.spamfilter->tkl_duration, 1);
ret = take_action(client, tkl->ptr.spamfilter->action, reason, tkl->ptr.spamfilter->tkl_duration, TAKE_ACTION_SKIP_SET);
if (!IsDead(client))
{
if ((ret == BAN_ACT_BLOCK) || (ret == BAN_ACT_SOFT_BLOCK))