changed debug notices to channel messages
[srvx.git] / src / mod-blacklist.c
1 /* Blacklist module for srvx 1.x
2  * Copyright 2007 Michael Poole <mdpoole@troilus.org>
3  *
4  * This file is part of srvx.
5  *
6  * srvx is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with srvx; if not, write to the Free Software Foundation,
18  * Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
19  */
20
21 #include "conf.h"
22 #include "gline.h"
23 #include "modcmd.h"
24 #include "proto.h"
25 #include "sar.h"
26
27 const char *blacklist_module_deps[] = { NULL };
28
29 struct dnsbl_zone {
30     struct string_list reasons;
31     const char *description;
32     const char *reason;
33     unsigned int duration;
34     unsigned int mask;
35     unsigned int debug : 1;
36     char zone[1];
37 };
38
39 struct dnsbl_data {
40     char client_ip[IRC_NTOP_MAX_SIZE];
41     char zone_name[1];
42 };
43
44 static struct log_type *bl_log;
45 static dict_t blacklist_zones; /* contains struct dnsbl_zone */
46 static dict_t blacklist_hosts; /* maps IPs or hostnames to reasons from blacklist_reasons */
47 static dict_t blacklist_reasons; /* maps strings to themselves (poor man's data sharing) */
48
49 static struct {
50     struct userNode *debug_bot;
51     struct chanNode *debug_channel;
52     unsigned long gline_duration;
53 } conf;
54
55 #if defined(GCC_VARMACROS)
56 # define blacklist_debug(ARGS...) do { if (conf.debug_bot && conf.debug_channel) send_channel_message(conf.debug_channel, conf.debug_bot, ARGS); } while (0)
57 #elif defined(C99_VARMACROS)
58 # define blacklist_debug(...) do { if (conf.debug_bot && conf.debug_channel) send_channel_message(conf.debug_channel, conf.debug_bot, __VA_ARGS__); } while (0)
59 #endif
60
61 static void
62 do_expandos(char *output, unsigned int out_len, const char *input, ...)
63 {
64     va_list args;
65     const char *key;
66     const char *datum;
67     char *found;
68     unsigned int klen;
69     unsigned int dlen;
70     unsigned int rlen;
71
72     safestrncpy(output, input, out_len);
73     va_start(args, input);
74     while ((key = va_arg(args, const char*)) != NULL) {
75         datum = va_arg(args, const char *);
76         klen = strlen(key);
77         dlen = strlen(datum);
78         for (found = output; (found = strstr(output, key)) != NULL; found += dlen) {
79             rlen = strlen(found + klen);
80             if ((dlen > klen) && ((unsigned)(found + dlen + rlen - output) > out_len))
81                 rlen = output + out_len - found - dlen;
82             memmove(found + dlen, found + klen, rlen);
83             memcpy(found, datum, dlen + 1);
84         }
85     }
86     va_end(args);
87 }
88
89 static void
90 dnsbl_hit(struct sar_request *req, struct dns_header *hdr, struct dns_rr *rr, unsigned char *raw, unsigned int raw_size)
91 {
92     struct dnsbl_data *data;
93     struct dnsbl_zone *zone;
94     const char *message;
95     char *txt;
96     unsigned int mask;
97     unsigned int pos;
98     unsigned int len;
99     unsigned int ii;
100     char reason[MAXLEN];
101     char target[IRC_NTOP_MAX_SIZE + 2];
102
103     /* Get the DNSBL zone (to make sure it has not disappeared in a rehash). */
104     data = (struct dnsbl_data*)(req + 1);
105     zone = dict_find(blacklist_zones, data->zone_name, NULL);
106     if (!zone)
107         return;
108
109     /* Scan the results. */
110     for (mask = 0, ii = 0, txt = NULL; ii < hdr->ancount; ++ii) {
111         pos = rr[ii].rd_start;
112         switch (rr[ii].type) {
113         case REQ_TYPE_A:
114             if (rr[ii].rdlength != 4)
115                 break;
116             if (pos + 3 < raw_size)
117                 mask |= (1 << raw[pos + 3]);
118             break;
119         case REQ_TYPE_TXT:
120             len = raw[pos];
121             txt = malloc(len + 1);
122             memcpy(txt, raw + pos + 1, len);
123             txt[len] = '\0';
124             break;
125         }
126     }
127
128     /* Do we care about one of the masks we found? */
129     if (mask & zone->mask) {
130         /* See if a per-result message was provided. */
131         for (ii = 0, message = NULL; mask && (ii < zone->reasons.used); ++ii, mask >>= 1) {
132             if (0 == (mask & 1))
133                 continue;
134             if (NULL != (message = zone->reasons.list[ii]))
135                 break;
136         }
137
138         /* If not, use a standard fallback. */
139         if (message == NULL) {
140             message = zone->reason;
141             if (message == NULL)
142                 message = "client is blacklisted";
143         }
144
145         /* Prepend "AUTO " prefix so the g-lined are put in a different snomask */
146         strcpy(reason, "AUTO ");
147
148         /* Expand elements of the message as necessary. */
149         do_expandos(reason + 5, sizeof(reason) - 5, message, "%txt%", (txt ? txt : "(no-txt)"), "%ip%", data->client_ip, NULL);
150
151         if (zone->debug) {
152             blacklist_debug("DNSBL match: [%s] %s (%s)", zone->zone, data->client_ip, reason);
153         } else {
154             /* Now generate the G-line. */
155             target[0] = '*';
156             target[1] = '@';
157             strcpy(target + 2, data->client_ip);
158             gline_add(self->name, target, zone->duration, reason, now, now, 0, 1);
159         }
160     }
161     free(txt);
162 }
163
164 static void
165 blacklist_check_user(struct userNode *user)
166 {
167     static const char *hexdigits = "0123456789abcdef";
168     dict_iterator_t it;
169     const char *reason;
170     const char *host;
171     unsigned int dnsbl_len;
172     unsigned int ii;
173     char ip[IRC_NTOP_MAX_SIZE];
174     char dnsbl_target[128];
175
176     /* Users added during burst should not be checked. */
177     if (user->uplink->burst)
178         return;
179
180     /* Users with bogus IPs are probably service bots. */
181     if (!irc_in_addr_is_valid(user->ip))
182         return;
183
184     /* Check local file-based blacklist. */
185     irc_ntop(ip, sizeof(ip), &user->ip);
186     reason = dict_find(blacklist_hosts, host = ip, NULL);
187     if (reason == NULL) {
188         reason = dict_find(blacklist_hosts, host = user->hostname, NULL);
189     }
190     if (reason != NULL) {
191         char *target;
192         target = alloca(strlen(host) + 3);
193         target[0] = '*';
194         target[1] = '@';
195         strcpy(target + 2, host);
196         /* We do not prepend AUTO here so it can be done in the blacklist file. */
197         gline_add(self->name, target, conf.gline_duration, reason, now, now, 0, 1);
198     }
199
200     /* Figure out the base part of a DNS blacklist hostname. */
201     if (irc_in_addr_is_ipv4(user->ip)) {
202         dnsbl_len = snprintf(dnsbl_target, sizeof(dnsbl_target), "%d.%d.%d.%d.", user->ip.in6_8[15], user->ip.in6_8[14], user->ip.in6_8[13], user->ip.in6_8[12]);
203     } else if (irc_in_addr_is_ipv6(user->ip)) {
204         for (ii = 0; ii < 16; ++ii) {
205             dnsbl_target[ii * 4 + 0] = hexdigits[user->ip.in6_8[15 - ii] & 15];
206             dnsbl_target[ii * 4 + 1] = '.';
207             dnsbl_target[ii * 4 + 2] = hexdigits[user->ip.in6_8[15 - ii] >> 4];
208             dnsbl_target[ii * 4 + 3] = '.';
209         }
210         dnsbl_len = 48;
211     } else {
212         return;
213     }
214
215     /* Start a lookup for the appropriate hostname in each DNSBL. */
216     for (it = dict_first(blacklist_zones); it; it = iter_next(it)) {
217         struct dnsbl_data *data;
218         struct sar_request *req;
219         const char *zone;
220
221         zone = iter_key(it);
222         safestrncpy(dnsbl_target + dnsbl_len, zone, sizeof(dnsbl_target) - dnsbl_len);
223         req = sar_request_simple(sizeof(*data) + strlen(zone), dnsbl_hit, NULL, dnsbl_target, REQ_QTYPE_ALL, NULL);
224         if (req) {
225             data = (struct dnsbl_data*)(req + 1);
226             strcpy(data->client_ip, ip);
227             strcpy(data->zone_name, zone);
228         }
229     }
230 }
231
232 static void
233 blacklist_load_file(const char *filename, const char *default_reason)
234 {
235     FILE *file;
236     const char *reason;
237     char *mapped_reason;
238     char *sep;
239     size_t len;
240     char linebuf[MAXLEN];
241
242     if (!filename)
243         return;
244     if (!default_reason)
245         default_reason = "client is blacklisted";
246     file = fopen(filename, "r");
247     if (!file) {
248         log_module(bl_log, LOG_ERROR, "Unable to open %s for reading: %s", filename, strerror(errno));
249         return;
250     }
251     log_module(bl_log, LOG_DEBUG, "Loading blacklist from %s.", filename);
252     while (fgets(linebuf, sizeof(linebuf), file)) {
253         /* Trim whitespace from end of line. */
254         len = strlen(linebuf);
255         while (isspace(linebuf[len-1]))
256             linebuf[--len] = '\0';
257
258         /* Figure out which reason string we should use. */
259         reason = default_reason;
260         sep = strchr(linebuf, ' ');
261         if (sep) {
262             *sep++ = '\0';
263             while (isspace(*sep))
264                 sep++;
265             if (*sep != '\0')
266                 reason = sep;
267         }
268
269         /* See if the reason string is already known. */
270         mapped_reason = dict_find(blacklist_reasons, reason, NULL);
271         if (!mapped_reason) {
272             mapped_reason = strdup(reason);
273             dict_insert(blacklist_reasons, mapped_reason, (char*)mapped_reason);
274         }
275
276         /* Store the blacklist entry. */
277         dict_insert(blacklist_hosts, strdup(linebuf), mapped_reason);
278     }
279     fclose(file);
280 }
281
282 static void
283 dnsbl_zone_free(void *pointer)
284 {
285     struct dnsbl_zone *zone;
286     zone = pointer;
287     free(zone->reasons.list);
288     free(zone);
289 }
290
291 static void
292 blacklist_conf_read(void)
293 {
294     dict_t node;
295     dict_t subnode;
296     const char *str1;
297     const char *str2;
298
299     dict_delete(blacklist_zones);
300     blacklist_zones = dict_new();
301     dict_set_free_data(blacklist_zones, dnsbl_zone_free);
302
303     dict_delete(blacklist_hosts);
304     blacklist_hosts = dict_new();
305     dict_set_free_keys(blacklist_hosts, free);
306
307     dict_delete(blacklist_reasons);
308     blacklist_reasons = dict_new();
309     dict_set_free_keys(blacklist_reasons, free);
310
311     node = conf_get_data("modules/blacklist", RECDB_OBJECT);
312     if (node == NULL)
313         return;
314
315     str1 = database_get_data(node, "debug_bot", RECDB_QSTRING);
316     if (str1)
317         conf.debug_bot = GetUserH(str1);
318
319     str1 = database_get_data(node, "debug_channel", RECDB_QSTRING);
320     if (conf.debug_bot && str1) {
321         str2 = database_get_data(node, "debug_channel_modes", RECDB_QSTRING);
322         if (!str2)
323             str2 = "+tinms";
324         conf.debug_channel = AddChannel(str1, now, str2, NULL);
325         AddChannelUser(conf.debug_bot, conf.debug_channel)->modes |= MODE_CHANOP;
326     } else {
327         conf.debug_channel = NULL;
328     }
329
330     str1 = database_get_data(node, "file", RECDB_QSTRING);
331     str2 = database_get_data(node, "file_reason", RECDB_QSTRING);
332     blacklist_load_file(str1, str2);
333
334     str1 = database_get_data(node, "gline_duration", RECDB_QSTRING);
335     if (str1 == NULL)
336         str1 = "1h";
337     conf.gline_duration = ParseInterval(str1);
338
339     subnode = database_get_data(node, "dnsbl", RECDB_OBJECT);
340     if (subnode) {
341         static const char *reason_prefix = "reason_";
342         static const unsigned int max_id = 255;
343         struct dnsbl_zone *zone;
344         dict_iterator_t it;
345         dict_iterator_t it2;
346         dict_t dnsbl;
347         unsigned int id;
348
349         for (it = dict_first(subnode); it; it = iter_next(it)) {
350             dnsbl = GET_RECORD_OBJECT((struct record_data*)iter_data(it));
351             if (!dnsbl)
352                 continue;
353
354             zone = malloc(sizeof(*zone) + strlen(iter_key(it)));
355             strcpy(zone->zone, iter_key(it));
356             zone->description = database_get_data(dnsbl, "description", RECDB_QSTRING);
357             zone->reason = database_get_data(dnsbl, "reason", RECDB_QSTRING);
358             str1 = database_get_data(dnsbl, "duration", RECDB_QSTRING);
359             zone->duration = str1 ? ParseInterval(str1) : 3600;
360             str1 = database_get_data(dnsbl, "mask", RECDB_QSTRING);
361             zone->mask = str1 ? strtoul(str1, NULL, 0) : ~0u;
362             str1 = database_get_data(dnsbl, "debug", RECDB_QSTRING);
363             zone->debug = str1 ? enabled_string(str1) : 0;
364             zone->reasons.used = 0;
365             zone->reasons.size = 0;
366             zone->reasons.list = NULL;
367             dict_insert(blacklist_zones, zone->zone, zone);
368
369             for (it2 = dict_first(dnsbl); it2; it2 = iter_next(it2)) {
370                 str1 = GET_RECORD_QSTRING((struct record_data*)(iter_data(it2)));
371                 if (!str1 || memcmp(iter_key(it2), reason_prefix, strlen(reason_prefix)))
372                     continue;
373                 id = strtoul(iter_key(it2) + strlen(reason_prefix), NULL, 0);
374                 if (id > max_id) {
375                     log_module(bl_log, LOG_ERROR, "Invalid code for DNSBL %s %s -- only %d responses supported.", iter_key(it), iter_key(it2), max_id);
376                     continue;
377                 }
378                 if (zone->reasons.size < id + 1) {
379                     zone->reasons.size = id + 1;
380                     zone->reasons.list = realloc(zone->reasons.list, zone->reasons.size * sizeof(zone->reasons.list[0]));
381                 }
382                 zone->reasons.list[id] = (char*)str1;
383                 if (zone->reasons.used < id + 1)
384                     zone->reasons.used = id + 1;
385             }
386         }
387     }
388 }
389
390 static void
391 blacklist_cleanup(void)
392 {
393     dict_delete(blacklist_zones);
394     dict_delete(blacklist_hosts);
395     dict_delete(blacklist_reasons);
396 }
397
398 int
399 blacklist_init(void)
400 {
401     bl_log = log_register_type("blacklist", "file:blacklist.log");
402     conf_register_reload(blacklist_conf_read);
403     reg_new_user_func(blacklist_check_user);
404     reg_exit_func(blacklist_cleanup);
405     return 1;
406 }
407
408 int
409 blacklist_finalize(void)
410 {
411     return 1;
412 }