#include "gline.h"
#include "modcmd.h"
#include "proto.h"
+#include "sar.h"
const char *blacklist_module_deps[] = { NULL };
+struct dnsbl_zone {
+ struct string_list reasons;
+ const char *description;
+ const char *reason;
+ unsigned int duration;
+ unsigned int mask;
+ unsigned int debug : 1;
+ char zone[1];
+};
+
+struct dnsbl_data {
+ char client_ip[IRC_NTOP_MAX_SIZE];
+ char zone_name[1];
+};
+
static struct log_type *bl_log;
+static dict_t blacklist_zones; /* contains struct dnsbl_zone */
static dict_t blacklist_hosts; /* maps IPs or hostnames to reasons from blacklist_reasons */
static dict_t blacklist_reasons; /* maps strings to themselves (poor man's data sharing) */
static struct {
+ struct userNode *debug_bot;
+ struct chanNode *debug_channel;
unsigned long gline_duration;
} conf;
-static int
+#if defined(GCC_VARMACROS)
+# define blacklist_debug(ARGS...) do { if (conf.debug_bot && conf.debug_channel) send_channel_notice(conf.debug_channel, conf.debug_bot, ARGS); } while (0)
+#elif defined(C99_VARMACROS)
+# define blacklist_debug(...) do { if (conf.debug_bot && conf.debug_channel) send_channel_notice(conf.debug_channel, conf.debug_bot, __VA_ARGS__); } while (0)
+#endif
+
+static void
+do_expandos(char *output, unsigned int out_len, const char *input, ...)
+{
+ va_list args;
+ const char *key;
+ const char *datum;
+ char *found;
+ unsigned int klen;
+ unsigned int dlen;
+ unsigned int rlen;
+
+ safestrncpy(output, input, out_len);
+ va_start(args, input);
+ while ((key = va_arg(args, const char*)) != NULL) {
+ datum = va_arg(args, const char *);
+ klen = strlen(key);
+ dlen = strlen(datum);
+ for (found = output; (found = strstr(output, key)) != NULL; found += dlen) {
+ rlen = strlen(found + klen);
+ if ((dlen > klen) && ((unsigned)(found + dlen + rlen - output) > out_len))
+ rlen = output + out_len - found - dlen;
+ memmove(found + dlen, found + klen, rlen);
+ memcpy(found, datum, dlen + 1);
+ }
+ }
+ va_end(args);
+}
+
+static void
+dnsbl_hit(struct sar_request *req, struct dns_header *hdr, struct dns_rr *rr, unsigned char *raw, unsigned int raw_size)
+{
+ struct dnsbl_data *data;
+ struct dnsbl_zone *zone;
+ const char *message;
+ char *txt;
+ unsigned int mask;
+ unsigned int pos;
+ unsigned int len;
+ unsigned int ii;
+ char reason[MAXLEN];
+ char target[IRC_NTOP_MAX_SIZE + 2];
+
+ /* Get the DNSBL zone (to make sure it has not disappeared in a rehash). */
+ data = (struct dnsbl_data*)(req + 1);
+ zone = dict_find(blacklist_zones, data->zone_name, NULL);
+ if (!zone)
+ return;
+
+ /* Scan the results. */
+ for (mask = 0, ii = 0, txt = NULL; ii < hdr->ancount; ++ii) {
+ pos = rr[ii].rd_start;
+ switch (rr[ii].type) {
+ case REQ_TYPE_A:
+ if (rr[ii].rdlength != 4)
+ break;
+ if (pos + 3 < raw_size)
+ mask |= (1 << raw[pos + 3]);
+ break;
+ case REQ_TYPE_TXT:
+ len = raw[pos];
+ txt = malloc(len + 1);
+ memcpy(txt, raw + pos + 1, len);
+ txt[len] = '\0';
+ break;
+ }
+ }
+
+ /* Do we care about one of the masks we found? */
+ if (mask & zone->mask) {
+ /* See if a per-result message was provided. */
+ for (ii = 0, message = NULL; mask && (ii < zone->reasons.used); ++ii, mask >>= 1) {
+ if (0 == (mask & 1))
+ continue;
+ if (NULL != (message = zone->reasons.list[ii]))
+ break;
+ }
+
+ /* If not, use a standard fallback. */
+ if (message == NULL) {
+ message = zone->reason;
+ if (message == NULL)
+ message = "client is blacklisted";
+ }
+
+ /* Prepend "AUTO " prefix so the g-lined are put in a different snomask */
+ strcpy(reason, "AUTO ");
+
+ /* Expand elements of the message as necessary. */
+ do_expandos(reason + 5, sizeof(reason) - 5, message, "%txt%", (txt ? txt : "(no-txt)"), "%ip%", data->client_ip, NULL);
+
+ if (zone->debug) {
+ blacklist_debug("DNSBL match: [%s] %s (%s)", zone->zone, data->client_ip, reason);
+ } else {
+ /* Now generate the G-line. */
+ target[0] = '*';
+ target[1] = '@';
+ strcpy(target + 2, data->client_ip);
+ gline_add(self->name, target, zone->duration, reason, now, now, 0, 1);
+ }
+ }
+ free(txt);
+}
+
+static void
blacklist_check_user(struct userNode *user)
{
+ static const char *hexdigits = "0123456789abcdef";
+ dict_iterator_t it;
const char *reason;
const char *host;
+ unsigned int dnsbl_len;
+ unsigned int ii;
char ip[IRC_NTOP_MAX_SIZE];
+ char dnsbl_target[128];
+
+ /* Users added during burst should not be checked. */
+ if (user->uplink->burst)
+ return;
+ /* Users with bogus IPs are probably service bots. */
+ if (!irc_in_addr_is_valid(user->ip))
+ return;
+
+ /* Check local file-based blacklist. */
irc_ntop(ip, sizeof(ip), &user->ip);
reason = dict_find(blacklist_hosts, host = ip, NULL);
if (reason == NULL) {
target[0] = '*';
target[1] = '@';
strcpy(target + 2, host);
- gline_add(self->name, target, conf.gline_duration, reason, now, now, 1);
+ /* We do not prepend AUTO here so it can be done in the blacklist file. */
+ gline_add(self->name, target, conf.gline_duration, reason, now, now, 0, 1);
+ }
+
+ /* Figure out the base part of a DNS blacklist hostname. */
+ if (irc_in_addr_is_ipv4(user->ip)) {
+ 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]);
+ } else if (irc_in_addr_is_ipv6(user->ip)) {
+ for (ii = 0; ii < 16; ++ii) {
+ dnsbl_target[ii * 4 + 0] = hexdigits[user->ip.in6_8[15 - ii] & 15];
+ dnsbl_target[ii * 4 + 1] = '.';
+ dnsbl_target[ii * 4 + 2] = hexdigits[user->ip.in6_8[15 - ii] >> 4];
+ dnsbl_target[ii * 4 + 3] = '.';
+ }
+ dnsbl_len = 48;
+ } else {
+ return;
+ }
+
+ /* Start a lookup for the appropriate hostname in each DNSBL. */
+ for (it = dict_first(blacklist_zones); it; it = iter_next(it)) {
+ struct dnsbl_data *data;
+ struct sar_request *req;
+ const char *zone;
+
+ zone = iter_key(it);
+ safestrncpy(dnsbl_target + dnsbl_len, zone, sizeof(dnsbl_target) - dnsbl_len);
+ req = sar_request_simple(sizeof(*data) + strlen(zone), dnsbl_hit, NULL, dnsbl_target, REQ_QTYPE_ALL, NULL);
+ if (req) {
+ data = (struct dnsbl_data*)(req + 1);
+ strcpy(data->client_ip, ip);
+ strcpy(data->zone_name, zone);
+ }
}
- return 0;
}
static void
fclose(file);
}
+static void
+dnsbl_zone_free(void *pointer)
+{
+ struct dnsbl_zone *zone;
+ zone = pointer;
+ free(zone->reasons.list);
+ free(zone);
+}
+
static void
blacklist_conf_read(void)
{
dict_t node;
+ dict_t subnode;
const char *str1;
const char *str2;
+ dict_delete(blacklist_zones);
+ blacklist_zones = dict_new();
+ dict_set_free_data(blacklist_zones, dnsbl_zone_free);
+
dict_delete(blacklist_hosts);
blacklist_hosts = dict_new();
dict_set_free_keys(blacklist_hosts, free);
if (node == NULL)
return;
+ str1 = database_get_data(node, "debug_bot", RECDB_QSTRING);
+ if (str1)
+ conf.debug_bot = GetUserH(str1);
+
+ str1 = database_get_data(node, "debug_channel", RECDB_QSTRING);
+ if (conf.debug_bot && str1) {
+ str2 = database_get_data(node, "debug_channel_modes", RECDB_QSTRING);
+ if (!str2)
+ str2 = "+tinms";
+ conf.debug_channel = AddChannel(str1, now, str2, NULL);
+ AddChannelUser(conf.debug_bot, conf.debug_channel)->modes |= MODE_CHANOP;
+ } else {
+ conf.debug_channel = NULL;
+ }
+
str1 = database_get_data(node, "file", RECDB_QSTRING);
str2 = database_get_data(node, "file_reason", RECDB_QSTRING);
blacklist_load_file(str1, str2);
if (str1 == NULL)
str1 = "1h";
conf.gline_duration = ParseInterval(str1);
+
+ subnode = database_get_data(node, "dnsbl", RECDB_OBJECT);
+ if (subnode) {
+ static const char *reason_prefix = "reason_";
+ static const unsigned int max_id = 255;
+ struct dnsbl_zone *zone;
+ dict_iterator_t it;
+ dict_iterator_t it2;
+ dict_t dnsbl;
+ unsigned int id;
+
+ for (it = dict_first(subnode); it; it = iter_next(it)) {
+ dnsbl = GET_RECORD_OBJECT((struct record_data*)iter_data(it));
+ if (!dnsbl)
+ continue;
+
+ zone = malloc(sizeof(*zone) + strlen(iter_key(it)));
+ strcpy(zone->zone, iter_key(it));
+ zone->description = database_get_data(dnsbl, "description", RECDB_QSTRING);
+ zone->reason = database_get_data(dnsbl, "reason", RECDB_QSTRING);
+ str1 = database_get_data(dnsbl, "duration", RECDB_QSTRING);
+ zone->duration = str1 ? ParseInterval(str1) : 3600;
+ str1 = database_get_data(dnsbl, "mask", RECDB_QSTRING);
+ zone->mask = str1 ? strtoul(str1, NULL, 0) : ~0u;
+ str1 = database_get_data(dnsbl, "debug", RECDB_QSTRING);
+ zone->debug = str1 ? enabled_string(str1) : 0;
+ zone->reasons.used = 0;
+ zone->reasons.size = 0;
+ zone->reasons.list = NULL;
+ dict_insert(blacklist_zones, zone->zone, zone);
+
+ for (it2 = dict_first(dnsbl); it2; it2 = iter_next(it2)) {
+ str1 = GET_RECORD_QSTRING((struct record_data*)(iter_data(it2)));
+ if (!str1 || memcmp(iter_key(it2), reason_prefix, strlen(reason_prefix)))
+ continue;
+ id = strtoul(iter_key(it2) + strlen(reason_prefix), NULL, 0);
+ if (id > max_id) {
+ log_module(bl_log, LOG_ERROR, "Invalid code for DNSBL %s %s -- only %d responses supported.", iter_key(it), iter_key(it2), max_id);
+ continue;
+ }
+ if (zone->reasons.size < id + 1) {
+ zone->reasons.size = id + 1;
+ zone->reasons.list = realloc(zone->reasons.list, zone->reasons.size * sizeof(zone->reasons.list[0]));
+ }
+ zone->reasons.list[id] = (char*)str1;
+ if (zone->reasons.used < id + 1)
+ zone->reasons.used = id + 1;
+ }
+ }
+ }
}
static void
blacklist_cleanup(void)
{
+ dict_delete(blacklist_zones);
dict_delete(blacklist_hosts);
dict_delete(blacklist_reasons);
}
int
blacklist_init(void)
{
- bl_log = log_register_type("Blacklist", "file:blacklist.log");
+ bl_log = log_register_type("blacklist", "file:blacklist.log");
conf_register_reload(blacklist_conf_read);
reg_new_user_func(blacklist_check_user);
reg_exit_func(blacklist_cleanup);