]>
Commit | Line | Data |
---|---|---|
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 3 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_notice(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_notice(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 | /* Expand elements of the message as necessary. */ | |
146 | do_expandos(reason, sizeof(reason), message, "%txt%", (txt ? txt : "(no-txt)"), "%ip%", data->client_ip, NULL); | |
147 | ||
148 | if (zone->debug) { | |
149 | blacklist_debug("DNSBL match: [%s] %s (%s)", zone->zone, data->client_ip, reason); | |
150 | } else { | |
151 | /* Now generate the G-line. */ | |
152 | target[0] = '*'; | |
153 | target[1] = '@'; | |
154 | strcpy(target + 2, data->client_ip); | |
155 | gline_add(self->name, target, zone->duration, reason, now, 1, 0); | |
156 | } | |
157 | } | |
158 | free(txt); | |
159 | } | |
160 | ||
161 | static int | |
162 | blacklist_check_user(struct userNode *user) | |
163 | { | |
164 | static const char *hexdigits = "0123456789abcdef"; | |
165 | dict_iterator_t it; | |
166 | const char *reason; | |
167 | const char *host; | |
168 | unsigned int dnsbl_len; | |
169 | unsigned int ii; | |
170 | char ip[IRC_NTOP_MAX_SIZE]; | |
171 | char dnsbl_target[128]; | |
172 | ||
173 | /* Users added during burst should not be checked. */ | |
174 | if (user->uplink->burst) | |
175 | return 0; | |
176 | ||
177 | /* Users with bogus IPs are probably service bots. */ | |
178 | if (!irc_in_addr_is_valid(user->ip)) | |
179 | return 0; | |
180 | ||
181 | /* Check local file-based blacklist. */ | |
182 | irc_ntop(ip, sizeof(ip), &user->ip); | |
183 | reason = dict_find(blacklist_hosts, host = ip, NULL); | |
184 | if (reason == NULL) { | |
185 | reason = dict_find(blacklist_hosts, host = user->hostname, NULL); | |
186 | } | |
187 | if (reason != NULL) { | |
188 | char *target; | |
189 | target = alloca(strlen(host) + 3); | |
190 | target[0] = '*'; | |
191 | target[1] = '@'; | |
192 | strcpy(target + 2, host); | |
193 | gline_add(self->name, target, conf.gline_duration, reason, now, 1, 0); | |
194 | } | |
195 | ||
196 | /* Figure out the base part of a DNS blacklist hostname. */ | |
197 | if (irc_in_addr_is_ipv4(user->ip)) { | |
198 | 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]); | |
199 | } else if (irc_in_addr_is_ipv6(user->ip)) { | |
200 | for (ii = 0; ii < 16; ++ii) { | |
201 | dnsbl_target[ii * 4 + 0] = hexdigits[user->ip.in6_8[15 - ii] & 15]; | |
202 | dnsbl_target[ii * 4 + 1] = '.'; | |
203 | dnsbl_target[ii * 4 + 2] = hexdigits[user->ip.in6_8[15 - ii] >> 4]; | |
204 | dnsbl_target[ii * 4 + 3] = '.'; | |
205 | } | |
206 | dnsbl_len = 48; | |
207 | } else { | |
208 | return 0; | |
209 | } | |
210 | ||
211 | /* Start a lookup for the appropriate hostname in each DNSBL. */ | |
212 | for (it = dict_first(blacklist_zones); it; it = iter_next(it)) { | |
213 | struct dnsbl_data *data; | |
214 | struct sar_request *req; | |
215 | const char *zone; | |
216 | ||
217 | zone = iter_key(it); | |
218 | safestrncpy(dnsbl_target + dnsbl_len, zone, sizeof(dnsbl_target) - dnsbl_len); | |
219 | req = sar_request_simple(sizeof(*data) + strlen(zone), dnsbl_hit, NULL, dnsbl_target, REQ_QTYPE_ALL, NULL); | |
220 | if (req) { | |
221 | data = (struct dnsbl_data*)(req + 1); | |
222 | strcpy(data->client_ip, ip); | |
223 | strcpy(data->zone_name, zone); | |
224 | } | |
225 | } | |
226 | ||
227 | return 0; | |
228 | } | |
229 | ||
230 | static void | |
231 | blacklist_load_file(const char *filename, const char *default_reason) | |
232 | { | |
233 | FILE *file; | |
234 | const char *reason; | |
235 | char *mapped_reason; | |
236 | char *sep; | |
237 | size_t len; | |
238 | char linebuf[MAXLEN]; | |
239 | ||
240 | if (!filename) | |
241 | return; | |
242 | if (!default_reason) | |
243 | default_reason = "client is blacklisted"; | |
244 | file = fopen(filename, "r"); | |
245 | if (!file) { | |
246 | log_module(bl_log, LOG_ERROR, "Unable to open %s for reading: %s", filename, strerror(errno)); | |
247 | return; | |
248 | } | |
249 | log_module(bl_log, LOG_DEBUG, "Loading blacklist from %s.", filename); | |
250 | while (fgets(linebuf, sizeof(linebuf), file)) { | |
251 | /* Trim whitespace from end of line. */ | |
252 | len = strlen(linebuf); | |
253 | while (isspace(linebuf[len-1])) | |
254 | linebuf[--len] = '\0'; | |
255 | ||
256 | /* Figure out which reason string we should use. */ | |
257 | reason = default_reason; | |
258 | sep = strchr(linebuf, ' '); | |
259 | if (sep) { | |
260 | *sep++ = '\0'; | |
261 | while (isspace(*sep)) | |
262 | sep++; | |
263 | if (*sep != '\0') | |
264 | reason = sep; | |
265 | } | |
266 | ||
267 | /* See if the reason string is already known. */ | |
268 | mapped_reason = dict_find(blacklist_reasons, reason, NULL); | |
269 | if (!mapped_reason) { | |
270 | mapped_reason = strdup(reason); | |
271 | dict_insert(blacklist_reasons, mapped_reason, (char*)mapped_reason); | |
272 | } | |
273 | ||
274 | /* Store the blacklist entry. */ | |
275 | dict_insert(blacklist_hosts, strdup(linebuf), mapped_reason); | |
276 | } | |
277 | fclose(file); | |
278 | } | |
279 | ||
280 | static void | |
281 | dnsbl_zone_free(void *pointer) | |
282 | { | |
283 | struct dnsbl_zone *zone; | |
284 | zone = pointer; | |
285 | free(zone->reasons.list); | |
286 | free(zone); | |
287 | } | |
288 | ||
289 | static void | |
290 | blacklist_conf_read(void) | |
291 | { | |
292 | dict_t node; | |
293 | dict_t subnode; | |
294 | const char *str1; | |
295 | const char *str2; | |
296 | ||
297 | dict_delete(blacklist_zones); | |
298 | blacklist_zones = dict_new(); | |
299 | dict_set_free_data(blacklist_zones, dnsbl_zone_free); | |
300 | ||
301 | dict_delete(blacklist_hosts); | |
302 | blacklist_hosts = dict_new(); | |
303 | dict_set_free_keys(blacklist_hosts, free); | |
304 | ||
305 | dict_delete(blacklist_reasons); | |
306 | blacklist_reasons = dict_new(); | |
307 | dict_set_free_keys(blacklist_reasons, free); | |
308 | ||
309 | node = conf_get_data("modules/blacklist", RECDB_OBJECT); | |
310 | if (node == NULL) | |
311 | return; | |
312 | ||
313 | str1 = database_get_data(node, "debug_bot", RECDB_QSTRING); | |
314 | if (str1) | |
315 | conf.debug_bot = GetUserH(str1); | |
316 | ||
317 | str1 = database_get_data(node, "debug_channel", RECDB_QSTRING); | |
318 | if (conf.debug_bot && str1) { | |
319 | str2 = database_get_data(node, "debug_channel_modes", RECDB_QSTRING); | |
320 | if (!str2) | |
321 | str2 = "+tinms"; | |
322 | conf.debug_channel = AddChannel(str1, now, str2, NULL, NULL); | |
323 | AddChannelUser(conf.debug_bot, conf.debug_channel)->modes |= MODE_CHANOP; | |
324 | } else { | |
325 | conf.debug_channel = NULL; | |
326 | } | |
327 | ||
328 | str1 = database_get_data(node, "file", RECDB_QSTRING); | |
329 | str2 = database_get_data(node, "file_reason", RECDB_QSTRING); | |
330 | blacklist_load_file(str1, str2); | |
331 | ||
332 | str1 = database_get_data(node, "gline_duration", RECDB_QSTRING); | |
333 | if (str1 == NULL) | |
334 | str1 = "1h"; | |
335 | conf.gline_duration = ParseInterval(str1); | |
336 | ||
337 | subnode = database_get_data(node, "dnsbl", RECDB_OBJECT); | |
338 | if (subnode) { | |
339 | static const char *reason_prefix = "reason_"; | |
340 | static const unsigned int max_id = 255; | |
341 | struct dnsbl_zone *zone; | |
342 | dict_iterator_t it; | |
343 | dict_iterator_t it2; | |
344 | dict_t dnsbl; | |
345 | unsigned int id; | |
346 | ||
347 | for (it = dict_first(subnode); it; it = iter_next(it)) { | |
348 | dnsbl = GET_RECORD_OBJECT((struct record_data*)iter_data(it)); | |
349 | if (!dnsbl) | |
350 | continue; | |
351 | ||
352 | zone = malloc(sizeof(*zone) + strlen(iter_key(it))); | |
353 | strcpy(zone->zone, iter_key(it)); | |
354 | zone->description = database_get_data(dnsbl, "description", RECDB_QSTRING); | |
355 | zone->reason = database_get_data(dnsbl, "reason", RECDB_QSTRING); | |
356 | str1 = database_get_data(dnsbl, "duration", RECDB_QSTRING); | |
357 | zone->duration = str1 ? ParseInterval(str1) : 3600; | |
358 | str1 = database_get_data(dnsbl, "mask", RECDB_QSTRING); | |
359 | zone->mask = str1 ? strtoul(str1, NULL, 0) : ~0u; | |
360 | str1 = database_get_data(dnsbl, "debug", RECDB_QSTRING); | |
361 | zone->debug = str1 ? enabled_string(str1) : 0; | |
362 | zone->reasons.used = 0; | |
363 | zone->reasons.size = 0; | |
364 | zone->reasons.list = NULL; | |
365 | dict_insert(blacklist_zones, zone->zone, zone); | |
366 | ||
367 | for (it2 = dict_first(dnsbl); it2; it2 = iter_next(it2)) { | |
368 | str1 = GET_RECORD_QSTRING((struct record_data*)(iter_data(it2))); | |
369 | if (!str1 || memcmp(iter_key(it2), reason_prefix, strlen(reason_prefix))) | |
370 | continue; | |
371 | id = strtoul(iter_key(it2) + strlen(reason_prefix), NULL, 0); | |
372 | if (id > max_id) { | |
373 | log_module(bl_log, LOG_ERROR, "Invalid code for DNSBL %s %s -- only %d responses supported.", iter_key(it), iter_key(it2), max_id); | |
374 | continue; | |
375 | } | |
376 | if (zone->reasons.size < id + 1) { | |
377 | zone->reasons.size = id + 1; | |
378 | zone->reasons.list = realloc(zone->reasons.list, zone->reasons.size * sizeof(zone->reasons.list[0])); | |
379 | } | |
380 | zone->reasons.list[id] = (char*)str1; | |
381 | if (zone->reasons.used < id + 1) | |
382 | zone->reasons.used = id + 1; | |
383 | } | |
384 | } | |
385 | } | |
386 | } | |
387 | ||
388 | static void | |
389 | blacklist_cleanup(void) | |
390 | { | |
391 | dict_delete(blacklist_zones); | |
392 | dict_delete(blacklist_hosts); | |
393 | dict_delete(blacklist_reasons); | |
394 | } | |
395 | ||
396 | int | |
397 | blacklist_init(void) | |
398 | { | |
399 | bl_log = log_register_type("blacklist", "file:blacklist.log"); | |
400 | conf_register_reload(blacklist_conf_read); | |
401 | reg_new_user_func(blacklist_check_user); | |
402 | reg_exit_func(blacklist_cleanup); | |
403 | return 1; | |
404 | } | |
405 | ||
406 | int | |
407 | blacklist_finalize(void) | |
408 | { | |
409 | return 1; | |
410 | } |