Skip to content

Commit b75f3fd

Browse files
committed
Add "bypass" capability to spamfilter
A new flag ACT_BYPASS was added as a generic flag to bypass +u if set. Opers remain immune, both as senders and recipients, so that spamfilter cannot be abused to lock people out of managing filters, talking to services, etc. Three new configuration items were added to the general section of ircd.conf to control filter behaviour: - filter_sees_user_info: This replaces the old #defines for FILTER_NICK, FILTER_IDENT, and FILTER_HOST to determine whether the hostmask of the user is passed to the spamfilter or whether we pass a dummy *!*@*. - filter_bypass_all: If set, the new ACT_BYPASS can allow every filter action (DROP, KILL, ALARM) to bypass +u if set in conjunction with those flags. If unset, only the DROP action can bypass +u and ACT_BYPASS has no effect on other actions. - filter_exit_message: This is the quit reason displayed, to move it out of a #define (we still keep a define around for a default reason in case it is unspecified). If either filter_sees_user_info or filter_bypass_all are set, the VERSION command will display a new 'F' flag to indicate the ability for filter to spy on users.
1 parent 54286cf commit b75f3fd

File tree

3 files changed

+148
-44
lines changed

3 files changed

+148
-44
lines changed

doc/reference.conf

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,6 +1535,23 @@ general {
15351535
* requires extensions/drain to be loaded.
15361536
*/
15371537
drain_reason = "This server is not accepting connections.";
1538+
1539+
/* filter_sees_user_info: Whether the filter is given the nick!user@host of message senders.
1540+
* If set to NO, the filter is given a literal "*!*@*" instead.
1541+
* Requires extension/filter to be loaded.
1542+
*/
1543+
#filter_sees_user_info = no;
1544+
1545+
/* filter_bypass_all: If set to NO, the only filter action that can bypass +u is DROP when paired with BYPASS.
1546+
* If set to YES, all filter actions can bypass +u when paired with BYPASS.
1547+
* Requires extension/filter to be loaded.
1548+
*/
1549+
#filter_bypass_all = no;
1550+
1551+
/* filter_exit_message: The QUIT message shown when a user is disconnected due to the filter KILL action.
1552+
* Requires extension/filter to be loaded.
1553+
*/
1554+
#filter_exit_message = "Connection closed";
15381555
};
15391556

15401557
modules {

doc/server-version-info.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
+----------------------------+
1818
| 'e' | USE_EXCEPT |
1919
|------+---------------------|
20+
| 'F' | FILTER_CAN_SPY |
21+
|------+---------------------|
2022
| 'I' | USE_INVEX |
2123
|------+---------------------|
2224
| 'K' | USE_KNOCK |

extensions/filter.c

Lines changed: 129 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "numeric.h"
3434
#include "send.h"
3535
#include "s_newconf.h"
36+
#include "s_conf.h"
3637
#include "s_serv.h"
3738
#include "s_user.h"
3839
#include "msg.h"
@@ -41,22 +42,27 @@
4142
#include "operhash.h"
4243
#include "inline/stringops.h"
4344
#include "msgbuf.h"
45+
#include "newconf.h"
4446

4547
#include <hs_common.h>
4648
#include <hs_runtime.h>
4749

48-
#define FILTER_NICK 0
49-
#define FILTER_USER 0
50-
#define FILTER_HOST 0
51-
52-
#define FILTER_EXIT_MSG "Connection closed"
50+
#define FILTER_DEFAULT_EXIT_MSG "Connection closed"
5351

5452
static const char filter_desc[] = "Filter messages using a precompiled Hyperscan database";
5553

54+
static int modinit(void);
55+
static void moddeinit(void);
5656
static void filter_msg_user(void *data);
5757
static void filter_msg_channel(void *data);
5858
static void filter_client_quit(void *data);
5959
static void on_client_exit(void *data);
60+
static void filter_init_conf(void *data);
61+
static void filter_conf_info(void *data);
62+
static void filter_version_info(void *data);
63+
static void filter_conf_set_sees_user_info(void *data);
64+
static void filter_conf_set_bypass_all(void *data);
65+
static void filter_conf_set_exit_message(void *data);
6066

6167
static void mo_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
6268
static void me_setfilter(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
@@ -67,6 +73,9 @@ static hs_database_t *filter_db;
6773
static hs_scratch_t *filter_scratch;
6874

6975
static int filter_enable = 1;
76+
static int filter_sees_user_info = 0;
77+
static int filter_bypass_all = 0;
78+
static char *filter_exit_message = NULL;
7079

7180
static const char *cmdname[MESSAGE_TYPE_COUNT] = {
7281
[MESSAGE_TYPE_PRIVMSG] = "PRIVMSG",
@@ -81,9 +90,15 @@ enum filter_state {
8190
FILTER_LOADED
8291
};
8392

93+
struct match_context {
94+
unsigned int actions;
95+
bool require_bypass;
96+
};
97+
8498
#define ACT_DROP (1 << 0)
8599
#define ACT_KILL (1 << 1)
86100
#define ACT_ALARM (1 << 2)
101+
#define ACT_BYPASS (1 << 3)
87102

88103
static enum filter_state state = FILTER_EMPTY;
89104
static char check_str[21] = "";
@@ -95,21 +110,30 @@ mapi_hfn_list_av1 filter_hfnlist[] = {
95110
{ "privmsg_channel", filter_msg_channel },
96111
{ "client_quit", filter_client_quit },
97112
{ "client_exit", on_client_exit },
113+
{ "conf_read_start", filter_init_conf },
114+
{ "doing_info_conf", filter_conf_info },
115+
{ "doing_version_confopts", filter_version_info },
98116
{ NULL, NULL }
99117
};
100118

101-
102119
struct Message setfilter_msgtab = {
103120
"SETFILTER", 0, 0, 0, 0,
104121
{mg_unreg, mg_not_oper, mg_ignore, mg_ignore, {me_setfilter, 2}, {mo_setfilter, 2}}
105122
};
106123

124+
mapi_clist_av1 filter_clist[] = { &setfilter_msgtab, NULL };
125+
126+
DECLARE_MODULE_AV2(filter, modinit, moddeinit, filter_clist, NULL, filter_hfnlist, NULL, "0.5", filter_desc);
127+
107128
static int
108129
modinit(void)
109130
{
110131
filter_umode = user_modes['u'] = find_umode_slot();
111132
construct_umodebuf();
112133
filter_chmode = cflag_add('u', chm_simple);
134+
add_conf_item("general", "filter_sees_user_info", CF_YESNO, filter_conf_set_sees_user_info);
135+
add_conf_item("general", "filter_bypass_all", CF_YESNO, filter_conf_set_bypass_all);
136+
add_conf_item("general", "filter_exit_message", CF_QSTRING, filter_conf_set_exit_message);
113137
return 0;
114138
}
115139

@@ -128,12 +152,73 @@ moddeinit(void)
128152
hs_free_database(filter_db);
129153
if (filter_data)
130154
rb_free(filter_data);
155+
if (filter_exit_message)
156+
rb_free(filter_exit_message);
157+
remove_conf_item("general", "filter_sees_user_info");
158+
remove_conf_item("general", "filter_bypass_all");
159+
remove_conf_item("general", "filter_exit_message");
131160
}
132161

162+
static void
163+
filter_init_conf(void *data)
164+
{
165+
if (filter_exit_message)
166+
{
167+
rb_free(filter_exit_message);
168+
filter_exit_message = NULL;
169+
}
170+
}
133171

134-
mapi_clist_av1 filter_clist[] = { &setfilter_msgtab, NULL };
172+
static void
173+
filter_conf_info(void *data_)
174+
{
175+
hook_data *data = data_;
176+
sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]",
177+
get_id(&me, data->client), RPL_INFO,
178+
get_id(data->client, data->client),
179+
"filter_sees_user_info",
180+
filter_sees_user_info ? "YES" : "NO",
181+
"Let the spamfilter engine see the hostmasks of senders");
182+
183+
sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]",
184+
get_id(&me, data->client), RPL_INFO,
185+
get_id(data->client, data->client),
186+
"filter_bypass_all",
187+
filter_bypass_all ? "YES" : "NO",
188+
"Let the spamfilter BYPASS action work on KILL/ALARM in addition to DROP");
189+
190+
sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]",
191+
get_id(&me, data->client), RPL_INFO,
192+
get_id(data->client, data->client),
193+
"filter_exit_message",
194+
filter_exit_message != NULL ? filter_exit_message : FILTER_DEFAULT_EXIT_MSG,
195+
"Message to quit users with if they hit a filter KILL action");
196+
}
197+
198+
static void
199+
filter_version_info(void *data)
200+
{
201+
if (filter_sees_user_info || filter_bypass_all)
202+
((char *)data)['F'] = 1;
203+
}
204+
205+
static void
206+
filter_conf_set_sees_user_info(void *data)
207+
{
208+
filter_sees_user_info = *(int *)data;
209+
}
135210

136-
DECLARE_MODULE_AV2(filter, modinit, moddeinit, filter_clist, NULL, filter_hfnlist, NULL, "0.4", filter_desc);
211+
static void
212+
filter_conf_set_bypass_all(void *data)
213+
{
214+
filter_bypass_all = *(int *)data;
215+
}
216+
217+
static void
218+
filter_conf_set_exit_message(void *data)
219+
{
220+
filter_exit_message = (char *)data;
221+
}
137222

138223
static int
139224
setfilter(const char *check, const char *data, const char **error)
@@ -313,8 +398,6 @@ me_setfilter(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *sour
313398
if (r) {
314399
sendto_one_notice(source_p, ":SETFILTER failed: %s", error);
315400
}
316-
317-
return;
318401
}
319402

320403
/* will be called for every match
@@ -328,8 +411,17 @@ int match_callback(unsigned id,
328411
unsigned flags,
329412
void *context_)
330413
{
331-
unsigned *context = context_;
332-
*context |= id;
414+
struct match_context *context = context_;
415+
416+
if (context->require_bypass)
417+
{
418+
unsigned int bypass_mask = filter_bypass_all ? UINT_MAX : ACT_DROP;
419+
if (id & ACT_BYPASS)
420+
context->actions |= id & bypass_mask;
421+
}
422+
else
423+
context->actions |= id;
424+
333425
return 0;
334426
}
335427

@@ -338,50 +430,42 @@ static char clean_buffer[BUFSIZE];
338430

339431
unsigned match_message(const char *prefix,
340432
struct Client *source,
433+
bool require_bypass,
341434
const char *command,
342435
const char *target,
343436
const char *msg)
344437
{
345-
unsigned state = 0;
438+
struct match_context ctx = { 0, require_bypass };
439+
346440
if (!filter_enable)
347441
return 0;
348442
if (!filter_db)
349443
return 0;
350444
if (!command)
351445
return 0;
446+
352447
snprintf(check_buffer, sizeof check_buffer, "%s:%s!%s@%s#%c %s%s%s :%s",
353448
prefix,
354-
#if FILTER_NICK
355-
source->name,
356-
#else
357-
"*",
358-
#endif
359-
#if FILTER_USER
360-
source->username,
361-
#else
362-
"*",
363-
#endif
364-
#if FILTER_HOST
365-
source->host,
366-
#else
367-
"*",
368-
#endif
449+
filter_sees_user_info ? source->name : "*",
450+
filter_sees_user_info ? source->username : "*",
451+
filter_sees_user_info ? source->host : "*",
369452
source->user && source->user->suser[0] != '\0' ? '1' : '0',
370453
command,
371454
target ? " " : "",
372455
target ? target : "",
373456
msg);
374-
hs_error_t r = hs_scan(filter_db, check_buffer, strlen(check_buffer), 0, filter_scratch, match_callback, &state);
457+
hs_error_t r = hs_scan(filter_db, check_buffer, strlen(check_buffer), 0, filter_scratch, match_callback, &ctx);
375458
if (r != HS_SUCCESS && r != HS_SCAN_TERMINATED)
376459
return 0;
377-
return state;
460+
return ctx.actions;
378461
}
379462

380463
void
381464
filter_msg_user(void *data_)
382465
{
383466
hook_data_privmsg_user *data = data_;
384467
struct Client *s = data->source_p;
468+
385469
/* we only need to filter once */
386470
if (!MyClient(s)) {
387471
return;
@@ -392,14 +476,14 @@ filter_msg_user(void *data_)
392476
if (IsOper(s) || IsOper(data->target_p)) {
393477
return;
394478
}
395-
if (data->target_p->umodes & filter_umode) {
396-
return;
397-
}
479+
480+
bool require_bypass = (data->target_p->umodes & filter_umode) == filter_umode;
398481
char *text = strcpy(clean_buffer, data->text);
399482
strip_colour(text);
400483
strip_unprintable(text);
401-
unsigned r = match_message("0", s, cmdname[data->msgtype], "0", data->text) |
402-
match_message("1", s, cmdname[data->msgtype], "0", text);
484+
unsigned r = match_message("0", s, require_bypass, cmdname[data->msgtype], "0", data->text) |
485+
match_message("1", s, require_bypass, cmdname[data->msgtype], "0", text);
486+
403487
if (r & ACT_DROP) {
404488
if (data->msgtype == MESSAGE_TYPE_PRIVMSG) {
405489
sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN,
@@ -415,7 +499,7 @@ filter_msg_user(void *data_)
415499
}
416500
if (r & ACT_KILL) {
417501
data->approved = 1;
418-
exit_client(NULL, s, s, FILTER_EXIT_MSG);
502+
exit_client(NULL, s, s, filter_exit_message != NULL ? filter_exit_message : FILTER_DEFAULT_EXIT_MSG);
419503
}
420504
}
421505

@@ -424,6 +508,7 @@ filter_msg_channel(void *data_)
424508
{
425509
hook_data_privmsg_channel *data = data_;
426510
struct Client *s = data->source_p;
511+
427512
/* we only need to filter once */
428513
if (!MyClient(s)) {
429514
return;
@@ -433,14 +518,14 @@ filter_msg_channel(void *data_)
433518
if (IsOper(s)) {
434519
return;
435520
}
436-
if (data->chptr->mode.mode & filter_chmode) {
437-
return;
438-
}
521+
522+
bool require_bypass = (data->chptr->mode.mode & filter_chmode) == filter_chmode;
439523
char *text = strcpy(clean_buffer, data->text);
440524
strip_colour(text);
441525
strip_unprintable(text);
442-
unsigned r = match_message("0", s, cmdname[data->msgtype], data->chptr->chname, data->text) |
443-
match_message("1", s, cmdname[data->msgtype], data->chptr->chname, text);
526+
unsigned r = match_message("0", s, require_bypass, cmdname[data->msgtype], data->chptr->chname, data->text) |
527+
match_message("1", s, require_bypass, cmdname[data->msgtype], data->chptr->chname, text);
528+
444529
if (r & ACT_DROP) {
445530
if (data->msgtype == MESSAGE_TYPE_PRIVMSG) {
446531
sendto_one_numeric(s, ERR_CANNOTSENDTOCHAN,
@@ -456,7 +541,7 @@ filter_msg_channel(void *data_)
456541
}
457542
if (r & ACT_KILL) {
458543
data->approved = 1;
459-
exit_client(NULL, s, s, FILTER_EXIT_MSG);
544+
exit_client(NULL, s, s, filter_exit_message != NULL ? filter_exit_message : FILTER_DEFAULT_EXIT_MSG);
460545
}
461546
}
462547

@@ -471,8 +556,8 @@ filter_client_quit(void *data_)
471556
char *text = strcpy(clean_buffer, data->orig_reason);
472557
strip_colour(text);
473558
strip_unprintable(text);
474-
unsigned r = match_message("0", s, "QUIT", NULL, data->orig_reason) |
475-
match_message("1", s, "QUIT", NULL, text);
559+
unsigned r = match_message("0", s, false, "QUIT", NULL, data->orig_reason) |
560+
match_message("1", s, false, "QUIT", NULL, text);
476561
if (r & ACT_DROP) {
477562
data->reason = NULL;
478563
}

0 commit comments

Comments
 (0)