mirror of https://github.com/pissnet/pissircd.git
1062 lines
27 KiB
C
1062 lines
27 KiB
C
/*
|
|
* Blacklist support (currently only DNS Blacklists)
|
|
* (C) Copyright 2015-.. Bram Matthys (Syzop) and the UnrealIRCd team
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 1, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#include "unrealircd.h"
|
|
|
|
ModuleHeader MOD_HEADER
|
|
= {
|
|
"blacklist",
|
|
"5.0",
|
|
"Check connecting users against DNS Blacklists",
|
|
"UnrealIRCd Team",
|
|
"unrealircd-6",
|
|
};
|
|
|
|
/* In this module and the config syntax I tried to 'abstract' things
|
|
* a little, so things could later be extended if we ever want
|
|
* to introduce another blacklist type (other than DNSBL).
|
|
* Once that happens, best to re-check/audit the source.
|
|
*/
|
|
|
|
/* The first "quick" recheck: */
|
|
static long BLACKLIST_RECHECK_TIME_FIRST = 120;
|
|
|
|
/* After that, check every <this>: */
|
|
static long BLACKLIST_RECHECK_TIME = 900;
|
|
|
|
#define LastBLCheck(x) (moddata_client(x, blacklistrecheck_md).l)
|
|
#define SetLastBLCheck(x, y) do { moddata_client(x, blacklistrecheck_md).l = y; } while(0)
|
|
|
|
/* Types */
|
|
|
|
typedef enum {
|
|
DNSBL_RECORD=1, DNSBL_BITMASK=2
|
|
} DNSBLType;
|
|
|
|
typedef struct DNSBL DNSBL;
|
|
struct DNSBL {
|
|
char *name;
|
|
DNSBLType type;
|
|
int *reply;
|
|
};
|
|
|
|
typedef union BlacklistBackend BlacklistBackend;
|
|
union BlacklistBackend
|
|
{
|
|
DNSBL *dns;
|
|
};
|
|
|
|
typedef enum {
|
|
BLACKLIST_BACKEND_DNS = 1
|
|
} BlacklistBackendType;
|
|
|
|
typedef struct Blacklist Blacklist;
|
|
struct Blacklist {
|
|
Blacklist *prev, *next;
|
|
char *name;
|
|
BlacklistBackendType backend_type;
|
|
BlacklistBackend *backend;
|
|
BanAction *action;
|
|
long ban_time;
|
|
char *reason;
|
|
SecurityGroup *except;
|
|
int recheck;
|
|
};
|
|
|
|
/* Blacklist user struct. In the c-ares DNS reply callback we need to pass
|
|
* some metadata. We can't use client directly there as the client may
|
|
* be gone already by the time we receive the DNS reply.
|
|
*/
|
|
typedef struct BLUser BLUser;
|
|
struct BLUser {
|
|
Client *client;
|
|
int is_ipv6;
|
|
int refcnt;
|
|
/* The following save_* fields are used by softbans: */
|
|
BanAction *save_action;
|
|
long save_tkltime;
|
|
char *save_opernotice;
|
|
char *save_reason;
|
|
char *save_blacklist;
|
|
char *save_blacklist_dns_name;
|
|
int save_blacklist_dns_reply;
|
|
};
|
|
|
|
/* Global variables */
|
|
ModDataInfo *blacklist_md = NULL;
|
|
ModDataInfo *blacklistrecheck_md = NULL;
|
|
Blacklist *conf_blacklist = NULL;
|
|
|
|
/* Forward declarations */
|
|
int blacklist_config_test(ConfigFile *, ConfigEntry *, int, int *);
|
|
int blacklist_config_run(ConfigFile *, ConfigEntry *, int);
|
|
int blacklist_set_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
|
|
int blacklist_set_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
|
|
void blacklist_free_conf(void);
|
|
void delete_blacklist_block(Blacklist *e);
|
|
void blacklist_md_free(ModData *md);
|
|
int blacklist_handshake(Client *client);
|
|
int blacklist_ip_change(Client *client, const char *oldip);
|
|
int blacklist_quit(Client *client, MessageTag *mtags, const char *comment);
|
|
int blacklist_preconnect(Client *client);
|
|
void blacklist_resolver_callback(void *arg, int status, int timeouts, struct hostent *he);
|
|
int blacklist_start_check(Client *client, int recheck);
|
|
int blacklist_dns_request(Client *client, Blacklist *bl);
|
|
int blacklist_rehash(void);
|
|
int blacklist_rehash_complete(void);
|
|
void blacklist_set_handshake_delay(void);
|
|
void blacklist_free_bluser_if_able(BLUser *bl);
|
|
EVENT(blacklist_recheck);
|
|
|
|
#define SetBLUser(x, y) do { moddata_client(x, blacklist_md).ptr = y; } while(0)
|
|
#define BLUSER(x) ((BLUser *)moddata_client(x, blacklist_md).ptr)
|
|
|
|
MOD_TEST()
|
|
{
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, blacklist_config_test);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, blacklist_set_config_test);
|
|
|
|
CallbackAdd(modinfo->handle, CALLBACKTYPE_BLACKLIST_CHECK, blacklist_start_check);
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
/** Called upon module init */
|
|
MOD_INIT()
|
|
{
|
|
ModDataInfo mreq;
|
|
|
|
MARK_AS_OFFICIAL_MODULE(modinfo);
|
|
|
|
memset(&mreq, 0, sizeof(mreq));
|
|
mreq.name = "blacklist";
|
|
mreq.type = MODDATATYPE_CLIENT;
|
|
mreq.free = blacklist_md_free;
|
|
blacklist_md = ModDataAdd(modinfo->handle, mreq);
|
|
if (!blacklist_md)
|
|
{
|
|
config_error("could not register blacklist moddata");
|
|
return MOD_FAILED;
|
|
}
|
|
|
|
memset(&mreq, 0, sizeof(mreq));
|
|
mreq.name = "blacklistrecheck";
|
|
mreq.type = MODDATATYPE_CLIENT;
|
|
blacklistrecheck_md = ModDataAdd(modinfo->handle, mreq);
|
|
if (!blacklistrecheck_md)
|
|
{
|
|
config_error("[blacklist] failed adding moddata for blacklistrecheck. "
|
|
"Do you perhaps still have third/blacklistrecheck loaded? That module is no longer needed!");
|
|
return MOD_FAILED;
|
|
}
|
|
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, blacklist_config_run);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, blacklist_set_config_run);
|
|
HookAdd(modinfo->handle, HOOKTYPE_HANDSHAKE, 0, blacklist_handshake);
|
|
HookAdd(modinfo->handle, HOOKTYPE_IP_CHANGE, 0, blacklist_ip_change);
|
|
HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 0, blacklist_preconnect);
|
|
HookAdd(modinfo->handle, HOOKTYPE_REHASH, 0, blacklist_rehash);
|
|
HookAdd(modinfo->handle, HOOKTYPE_REHASH_COMPLETE, 0, blacklist_rehash_complete);
|
|
HookAdd(modinfo->handle, HOOKTYPE_LOCAL_QUIT, 0, blacklist_quit);
|
|
|
|
EventAdd(modinfo->handle, "blacklist_recheck", blacklist_recheck, NULL, 2000, 0);
|
|
|
|
RegisterApiCallbackResolverHost(modinfo->handle, "blacklist_resolver_callback", blacklist_resolver_callback);
|
|
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
/** Called upon module load */
|
|
MOD_LOAD()
|
|
{
|
|
blacklist_set_handshake_delay();
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
/** Called upon unload */
|
|
MOD_UNLOAD()
|
|
{
|
|
blacklist_free_conf();
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
int blacklist_rehash(void)
|
|
{
|
|
blacklist_free_conf();
|
|
return 0;
|
|
}
|
|
|
|
int blacklist_rehash_complete(void)
|
|
{
|
|
blacklist_set_handshake_delay();
|
|
return 0;
|
|
}
|
|
|
|
void blacklist_set_handshake_delay(void)
|
|
{
|
|
if ((iConf.handshake_delay == -1) && conf_blacklist)
|
|
{
|
|
/*
|
|
Too noisy?
|
|
config_status("[blacklist] I'm setting set::handshake-delay to 2 seconds. "
|
|
"You may wish to set an explicit setting in the configuration file.");
|
|
config_status("See https://www.unrealircd.org/docs/Set_block#set::handshake-delay");
|
|
*/
|
|
iConf.handshake_delay = 2;
|
|
}
|
|
}
|
|
|
|
/** Find blacklist { } block */
|
|
Blacklist *blacklist_find_block_by_dns(char *name)
|
|
{
|
|
Blacklist *d;
|
|
|
|
for (d = conf_blacklist; d; d = d->next)
|
|
if ((d->backend_type == BLACKLIST_BACKEND_DNS) && !strcmp(name, d->backend->dns->name))
|
|
return d;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void blacklist_free_conf(void)
|
|
{
|
|
Blacklist *d, *d_next;
|
|
|
|
for (d = conf_blacklist; d; d = d_next)
|
|
{
|
|
d_next = d->next;
|
|
delete_blacklist_block(d);
|
|
}
|
|
conf_blacklist = NULL;
|
|
}
|
|
|
|
void delete_blacklist_block(Blacklist *e)
|
|
{
|
|
if (e->backend_type == BLACKLIST_BACKEND_DNS)
|
|
{
|
|
if (e->backend->dns)
|
|
{
|
|
safe_free(e->backend->dns->name);
|
|
safe_free(e->backend->dns->reply);
|
|
safe_free(e->backend->dns);
|
|
}
|
|
}
|
|
|
|
safe_free(e->backend);
|
|
|
|
safe_free(e->name);
|
|
safe_free(e->reason);
|
|
safe_free_all_ban_actions(e->action);
|
|
free_security_group(e->except);
|
|
|
|
safe_free(e);
|
|
}
|
|
|
|
int blacklist_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
|
|
{
|
|
ConfigEntry *cep, *cepp, *ceppp;
|
|
int errors = 0;
|
|
char has_reason = 0, has_ban_time = 0, has_action = 0;
|
|
char has_dns_type = 0, has_dns_reply = 0, has_dns_name = 0, has_recheck = 0;
|
|
|
|
if (type != CONFIG_MAIN)
|
|
return 0;
|
|
|
|
if (!ce)
|
|
return 0;
|
|
|
|
if (strcmp(ce->name, "blacklist"))
|
|
return 0; /* not interested in non-blacklist stuff.. */
|
|
|
|
if (!ce->value)
|
|
{
|
|
config_error("%s:%i: blacklist block without name (use: blacklist somename { })",
|
|
ce->file->filename, ce->line_number);
|
|
*errs = 1;
|
|
return -1;
|
|
}
|
|
|
|
/* Now actually go parse the blacklist { } block */
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strcmp(cep->name, "dns"))
|
|
{
|
|
for (cepp = cep->items; cepp; cepp = cepp->next)
|
|
{
|
|
if (!strcmp(cepp->name, "reply"))
|
|
{
|
|
if (has_dns_reply)
|
|
{
|
|
/* this is an error (not a warning) */
|
|
config_error("%s:%i: blacklist block may contain only one blacklist::dns::reply item. "
|
|
"You can specify multiple replies by using: reply { 1; 2; 4; };",
|
|
cepp->file->filename, cepp->line_number);
|
|
errors++;
|
|
continue;
|
|
}
|
|
if (!cepp->value && !cepp->items)
|
|
{
|
|
config_error_blank(cepp->file->filename, cepp->line_number, "blacklist::dns::reply");
|
|
errors++;
|
|
continue;
|
|
}
|
|
has_dns_reply = 1; /* we have a reply. now whether it's actually valid is another story.. */
|
|
if (cepp->value && cepp->items)
|
|
{
|
|
config_error("%s:%i: blacklist::dns::reply must be either using format 'reply 1;' or "
|
|
"'reply { 1; 2; 4; }; but not both formats at the same time.",
|
|
cepp->file->filename, cepp->line_number);
|
|
errors++;
|
|
continue;
|
|
}
|
|
if (cepp->value)
|
|
{
|
|
if (atoi(cepp->value) <= 0)
|
|
{
|
|
config_error("%s:%i: blacklist::dns::reply must be >0",
|
|
cepp->file->filename, cepp->line_number);
|
|
errors++;
|
|
continue;
|
|
}
|
|
}
|
|
if (cepp->items)
|
|
{
|
|
for (ceppp = cepp->items; ceppp; ceppp=ceppp->next)
|
|
{
|
|
if (atoi(ceppp->name) <= 0)
|
|
{
|
|
config_error("%s:%i: all items in blacklist::dns::reply must be >0",
|
|
cepp->file->filename, cepp->line_number);
|
|
errors++;
|
|
}
|
|
}
|
|
}
|
|
} else
|
|
if (!cepp->value)
|
|
{
|
|
config_error_empty(cepp->file->filename, cepp->line_number,
|
|
"blacklist::dns", cepp->name);
|
|
errors++;
|
|
continue;
|
|
} else
|
|
if (!strcmp(cepp->name, "name"))
|
|
{
|
|
if (has_dns_name)
|
|
{
|
|
config_warn_duplicate(cepp->file->filename,
|
|
cepp->line_number, "blacklist::dns::name");
|
|
}
|
|
has_dns_name = 1;
|
|
} else
|
|
if (!strcmp(cepp->name, "type"))
|
|
{
|
|
if (has_dns_type)
|
|
{
|
|
config_warn_duplicate(cepp->file->filename,
|
|
cepp->line_number, "blacklist::dns::type");
|
|
}
|
|
has_dns_type = 1;
|
|
if (!strcmp(cepp->value, "record"))
|
|
;
|
|
else if (!strcmp(cepp->value, "bitmask"))
|
|
;
|
|
else
|
|
{
|
|
config_error("%s:%i: unknown blacklist::dns::type '%s', must be either 'record' or 'bitmask'",
|
|
cepp->file->filename, cepp->line_number, cepp->value);
|
|
errors++;
|
|
}
|
|
}
|
|
}
|
|
} else
|
|
if (!strcmp(cep->name, "except"))
|
|
{
|
|
test_match_block(cf, cep, &errors);
|
|
} else
|
|
if (!cep->value)
|
|
{
|
|
config_error_empty(cep->file->filename, cep->line_number,
|
|
"blacklist", cep->name);
|
|
errors++;
|
|
continue;
|
|
}
|
|
else if (!strcmp(cep->name, "action"))
|
|
{
|
|
if (has_action)
|
|
{
|
|
config_warn_duplicate(cep->file->filename,
|
|
cep->line_number, "blacklist::action");
|
|
continue;
|
|
}
|
|
has_action = 1;
|
|
errors += test_ban_action_config(cep);
|
|
}
|
|
else if (!strcmp(cep->name, "ban-time"))
|
|
{
|
|
if (has_ban_time)
|
|
{
|
|
config_warn_duplicate(cep->file->filename,
|
|
cep->line_number, "blacklist::ban-time");
|
|
continue;
|
|
}
|
|
has_ban_time = 1;
|
|
}
|
|
else if (!strcmp(cep->name, "reason"))
|
|
{
|
|
if (has_reason)
|
|
{
|
|
config_warn_duplicate(cep->file->filename,
|
|
cep->line_number, "blacklist::reason");
|
|
continue;
|
|
}
|
|
has_reason = 1;
|
|
}
|
|
else if (!strcmp(cep->name, "recheck"))
|
|
{
|
|
if (has_recheck)
|
|
{
|
|
config_warn_duplicate(cep->file->filename,
|
|
cep->line_number, "blacklist::recheck");
|
|
continue;
|
|
}
|
|
has_recheck = 1;
|
|
}
|
|
else
|
|
{
|
|
config_error_unknown(cep->file->filename, cep->line_number,
|
|
"blacklist", cep->name);
|
|
errors++;
|
|
}
|
|
}
|
|
|
|
if (!has_action)
|
|
{
|
|
config_error_missing(ce->file->filename, ce->line_number,
|
|
"blacklist::action");
|
|
errors++;
|
|
}
|
|
|
|
if (!has_reason)
|
|
{
|
|
config_error_missing(ce->file->filename, ce->line_number,
|
|
"blacklist::reason");
|
|
errors++;
|
|
}
|
|
|
|
if (!has_dns_name)
|
|
{
|
|
config_error_missing(ce->file->filename, ce->line_number,
|
|
"blacklist::dns::name");
|
|
errors++;
|
|
}
|
|
|
|
if (!has_dns_type)
|
|
{
|
|
config_error_missing(ce->file->filename, ce->line_number,
|
|
"blacklist::dns::type");
|
|
errors++;
|
|
}
|
|
|
|
if (!has_dns_reply)
|
|
{
|
|
config_error_missing(ce->file->filename, ce->line_number,
|
|
"blacklist::dns::reply");
|
|
errors++;
|
|
}
|
|
|
|
*errs = errors;
|
|
return errors ? -1 : 1;
|
|
}
|
|
|
|
int blacklist_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
|
|
{
|
|
ConfigEntry *cep, *cepp, *ceppp;
|
|
Blacklist *d = NULL;
|
|
|
|
if (type != CONFIG_MAIN)
|
|
return 0;
|
|
|
|
if (!ce || !ce->name || strcmp(ce->name, "blacklist"))
|
|
return 0; /* not interested */
|
|
|
|
d = safe_alloc(sizeof(Blacklist));
|
|
safe_strdup(d->name, ce->value);
|
|
/* set some defaults */
|
|
d->ban_time = 3600;
|
|
d->recheck = 1;
|
|
|
|
/* assume dns for now ;) */
|
|
d->backend_type = BLACKLIST_BACKEND_DNS;
|
|
d->backend = safe_alloc(sizeof(BlacklistBackend));
|
|
d->backend->dns = safe_alloc(sizeof(DNSBL));
|
|
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strcmp(cep->name, "dns"))
|
|
{
|
|
for (cepp = cep->items; cepp; cepp = cepp->next)
|
|
{
|
|
if (!strcmp(cepp->name, "reply"))
|
|
{
|
|
if (cepp->value)
|
|
{
|
|
/* single reply */
|
|
d->backend->dns->reply = safe_alloc(sizeof(int)*2);
|
|
d->backend->dns->reply[0] = atoi(cepp->value);
|
|
d->backend->dns->reply[1] = 0;
|
|
} else
|
|
if (cepp->items)
|
|
{
|
|
/* (potentially) multiple reply values */
|
|
int cnt = 0;
|
|
for (ceppp = cepp->items; ceppp; ceppp = ceppp->next)
|
|
{
|
|
if (ceppp->name)
|
|
cnt++;
|
|
}
|
|
|
|
if (cnt == 0)
|
|
abort(); /* impossible */
|
|
|
|
d->backend->dns->reply = safe_alloc(sizeof(int)*(cnt+1));
|
|
|
|
cnt = 0;
|
|
for (ceppp = cepp->items; ceppp; ceppp = ceppp->next)
|
|
{
|
|
d->backend->dns->reply[cnt++] = atoi(ceppp->name);
|
|
}
|
|
d->backend->dns->reply[cnt] = 0;
|
|
}
|
|
} else
|
|
if (!strcmp(cepp->name, "type"))
|
|
{
|
|
if (!strcmp(cepp->value, "record"))
|
|
d->backend->dns->type = DNSBL_RECORD;
|
|
else if (!strcmp(cepp->value, "bitmask"))
|
|
d->backend->dns->type = DNSBL_BITMASK;
|
|
} else
|
|
if (!strcmp(cepp->name, "name"))
|
|
{
|
|
safe_strdup(d->backend->dns->name, cepp->value);
|
|
}
|
|
}
|
|
}
|
|
else if (!strcmp(cep->name, "action"))
|
|
{
|
|
parse_ban_action_config(cep, &d->action);
|
|
}
|
|
else if (!strcmp(cep->name, "ban-time"))
|
|
{
|
|
d->ban_time = config_checkval(cep->value, CFG_TIME);
|
|
}
|
|
else if (!strcmp(cep->name, "reason"))
|
|
{
|
|
safe_strdup(d->reason, cep->value);
|
|
}
|
|
else if (!strcmp(cep->name, "except"))
|
|
{
|
|
conf_match_block(cf, cep, &d->except);
|
|
}
|
|
else if (!strcmp(cep->name, "recheck"))
|
|
{
|
|
d->recheck = config_checkval(cep->value, CFG_YESNO);
|
|
}
|
|
}
|
|
|
|
AddListItem(d, conf_blacklist);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/** Test the set::blacklist configuration */
|
|
int blacklist_set_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
|
|
{
|
|
int errors = 0;
|
|
ConfigEntry *cep, *cepp;
|
|
|
|
if (type != CONFIG_SET)
|
|
return 0;
|
|
|
|
/* We are only interrested in set::blacklist.. */
|
|
if (!ce || !ce->name || strcmp(ce->name, "blacklist"))
|
|
return 0;
|
|
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strcmp(cep->name, "recheck-time-first"))
|
|
{
|
|
int v;
|
|
if (!cep->value)
|
|
{
|
|
config_error("%s:%i: set::blacklist::recheck-time-first with no value",
|
|
cep->file->filename, cep->line_number);
|
|
errors++;
|
|
continue;
|
|
}
|
|
if (!strcmp(cep->value, "never"))
|
|
{
|
|
config_error("%s:%i: if you want to disable blacklist rechecks, then you "
|
|
"should set set::blacklist::recheck-time to 'never' and "
|
|
"don't set set::blacklist::recheck-time-first.",
|
|
cep->file->filename, cep->line_number);
|
|
errors++;
|
|
continue;
|
|
}
|
|
v = config_checkval(cep->value, CFG_TIME);
|
|
if (v < 60)
|
|
{
|
|
config_error("%s:%i: set::blacklist::recheck-time-first cannot be less than 60 seconds",
|
|
cep->file->filename, cep->line_number);
|
|
errors++;
|
|
}
|
|
} else
|
|
if (!strcmp(cep->name, "recheck-time"))
|
|
{
|
|
int v;
|
|
if (!cep->value)
|
|
{
|
|
config_error("%s:%i: set::blacklist::recheck-time with no value",
|
|
cep->file->filename, cep->line_number);
|
|
errors++;
|
|
continue;
|
|
}
|
|
if (strcmp(cep->value, "never"))
|
|
{
|
|
v = config_checkval(cep->value, CFG_TIME);
|
|
if (v < 60)
|
|
{
|
|
config_error("%s:%i: set::blacklist::recheck-time cannot be less than 60 seconds",
|
|
cep->file->filename, cep->line_number);
|
|
errors++;
|
|
}
|
|
}
|
|
} else
|
|
{
|
|
config_error("%s:%i: unknown directive set::blacklist::%s",
|
|
cep->file->filename, cep->line_number, cep->name);
|
|
errors++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
*errs = errors;
|
|
return errors ? -1 : 1;
|
|
}
|
|
|
|
/* Configure ourselves based on the set::blacklist settings */
|
|
int blacklist_set_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
|
|
{
|
|
ConfigEntry *cep, *cepp;
|
|
|
|
if (type != CONFIG_SET)
|
|
return 0;
|
|
|
|
/* We are only interrested in set::blacklist.. */
|
|
if (!ce || !ce->name || strcmp(ce->name, "blacklist"))
|
|
return 0;
|
|
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strcmp(cep->name, "recheck-time"))
|
|
BLACKLIST_RECHECK_TIME = config_checkval(cep->value, CFG_TIME);
|
|
if (!strcmp(cep->name, "recheck-time-first"))
|
|
BLACKLIST_RECHECK_TIME_FIRST = config_checkval(cep->value, CFG_TIME);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
void blacklist_md_free(ModData *md)
|
|
{
|
|
BLUser *bl = md->ptr;
|
|
|
|
/* Mark bl->client as dead. Free the struct, if able. */
|
|
blacklist_free_bluser_if_able(bl);
|
|
|
|
md->ptr = NULL;
|
|
}
|
|
|
|
int blacklist_handshake(Client *client)
|
|
{
|
|
blacklist_start_check(client, 0);
|
|
return 0;
|
|
}
|
|
|
|
int blacklist_ip_change(Client *client, const char *oldip)
|
|
{
|
|
blacklist_start_check(client, 0);
|
|
return 0;
|
|
}
|
|
|
|
int blacklist_start_check(Client *client, int recheck)
|
|
{
|
|
Blacklist *bl;
|
|
|
|
if (find_tkl_exception(TKL_BLACKLIST, client))
|
|
{
|
|
/* If the user is exempt from DNSBL checking then:
|
|
* 1) Don't bother checking DNSBL's
|
|
* 2) Disable handshake delay for this user, since it serves no purpose.
|
|
*/
|
|
SetNoHandshakeDelay(client);
|
|
return 0;
|
|
}
|
|
|
|
if (!BLUSER(client))
|
|
{
|
|
SetBLUser(client, safe_alloc(sizeof(BLUser)));
|
|
BLUSER(client)->client = client;
|
|
}
|
|
|
|
for (bl = conf_blacklist; bl; bl = bl->next)
|
|
{
|
|
/* Stop processing if client is (being) killed already */
|
|
if (!BLUSER(client))
|
|
break;
|
|
|
|
if (recheck && !bl->recheck)
|
|
continue; /* blacklist::recheck is no */
|
|
|
|
/* Check if user is exempt (then don't bother checking) */
|
|
if (user_allowed_by_security_group(client, bl->except))
|
|
continue;
|
|
|
|
/* Initiate blacklist requests */
|
|
if (bl->backend_type == BLACKLIST_BACKEND_DNS)
|
|
blacklist_dns_request(client, bl);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int blacklist_dns_request(Client *client, Blacklist *d)
|
|
{
|
|
char buf[256], wbuf[128];
|
|
unsigned int e[8];
|
|
char *ip = GetIP(client);
|
|
|
|
if (!ip)
|
|
return 0;
|
|
|
|
memset(&e, 0, sizeof(e));
|
|
|
|
if (strchr(ip, '.'))
|
|
{
|
|
/* IPv4 */
|
|
if (sscanf(ip, "%u.%u.%u.%u", &e[0], &e[1], &e[2], &e[3]) != 4)
|
|
return 0;
|
|
|
|
snprintf(buf, sizeof(buf), "%u.%u.%u.%u.%s", e[3], e[2], e[1], e[0], d->backend->dns->name);
|
|
} else
|
|
if (strchr(ip, ':'))
|
|
{
|
|
/* IPv6 */
|
|
int i;
|
|
BLUSER(client)->is_ipv6 = 1;
|
|
if (sscanf(ip, "%x:%x:%x:%x:%x:%x:%x:%x",
|
|
&e[0], &e[1], &e[2], &e[3], &e[4], &e[5], &e[6], &e[7]) != 8)
|
|
{
|
|
return 0;
|
|
}
|
|
*buf = '\0';
|
|
for (i = 7; i >= 0; i--)
|
|
{
|
|
snprintf(wbuf, sizeof(wbuf), "%x.%x.%x.%x.",
|
|
(unsigned int)(e[i] & 0xf),
|
|
(unsigned int)((e[i] >> 4) & 0xf),
|
|
(unsigned int)((e[i] >> 8) & 0xf),
|
|
(unsigned int)((e[i] >> 12) & 0xf));
|
|
strlcat(buf, wbuf, sizeof(buf));
|
|
}
|
|
strlcat(buf, d->backend->dns->name, sizeof(buf));
|
|
}
|
|
else
|
|
return 0; /* unknown IP format */
|
|
|
|
BLUSER(client)->refcnt++; /* one (more) blacklist result remaining */
|
|
|
|
unreal_gethostbyname_api(buf, AF_INET, "blacklist_resolver_callback", BLUSER(client));
|
|
|
|
return 0;
|
|
}
|
|
|
|
void blacklist_cancel(BLUser *bl)
|
|
{
|
|
bl->client = NULL;
|
|
}
|
|
|
|
int blacklist_quit(Client *client, MessageTag *mtags, const char *comment)
|
|
{
|
|
if (BLUSER(client))
|
|
blacklist_cancel(BLUSER(client));
|
|
|
|
return 0;
|
|
}
|
|
|
|
/** Free the BLUSER() struct, if we are able to do so.
|
|
* This should only be called if the underlying client is dead or dyeing
|
|
* and not earlier.
|
|
* Reasons why we 'are not able' are: refcnt is non-zero, that is:
|
|
* there is still an outstanding resolver request (eg: slow blacklist).
|
|
* In that case, no worries, we will be called again after that request
|
|
* is finished.
|
|
*/
|
|
void blacklist_free_bluser_if_able(BLUser *bl)
|
|
{
|
|
if (bl->client)
|
|
bl->client = NULL;
|
|
|
|
if (bl->refcnt > 0)
|
|
return; /* unable, still have DNS requests/replies in-flight */
|
|
|
|
safe_free(bl->save_opernotice);
|
|
safe_free(bl->save_reason);
|
|
free_all_ban_actions(bl->save_action);
|
|
safe_free(bl);
|
|
}
|
|
|
|
char *getdnsblname(char *p, Client *client)
|
|
{
|
|
int dots = 0;
|
|
int dots_count;
|
|
|
|
if (!client)
|
|
return NULL;
|
|
|
|
if (BLUSER(client)->is_ipv6)
|
|
dots_count = 32;
|
|
else
|
|
dots_count = 4;
|
|
|
|
for (; *p; p++)
|
|
{
|
|
if (*p == '.')
|
|
{
|
|
dots++;
|
|
if (dots == dots_count)
|
|
return p+1;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Parse DNS reply.
|
|
* A reply will be an A record in the format x.x.x.<reply>
|
|
*/
|
|
int blacklist_parse_reply(struct hostent *he, int entry)
|
|
{
|
|
char ipbuf[64];
|
|
char *p;
|
|
|
|
if ((he->h_addrtype != AF_INET) || (he->h_length != 4))
|
|
return 0;
|
|
|
|
*ipbuf = '\0';
|
|
if (!inet_ntop(AF_INET, he->h_addr_list[entry], ipbuf, sizeof(ipbuf)))
|
|
return 0;
|
|
|
|
p = strrchr(ipbuf, '.');
|
|
if (!p)
|
|
return 0;
|
|
|
|
return atoi(p+1);
|
|
}
|
|
|
|
/** Take the actual ban action.
|
|
* Called from blacklist_hit() and for immediate bans and
|
|
* from blacklist_preconnect() for softbans that need to be delayed
|
|
* as to give the user the opportunity to do SASL Authentication.
|
|
*/
|
|
int blacklist_action(Client *client, char *opernotice, BanAction *ban_action, char *ban_reason, long ban_time,
|
|
char *blacklist, char *blacklist_dns_name, int blacklist_dns_reply)
|
|
{
|
|
unreal_log_raw(ULOG_INFO, "blacklist", "BLACKLIST_HIT", client,
|
|
opernotice,
|
|
log_data_string("blacklist_name", blacklist),
|
|
log_data_string("blacklist_dns_name", blacklist_dns_name),
|
|
log_data_integer("blacklist_dns_reply", blacklist_dns_reply),
|
|
log_data_string("ban_action", ban_actions_to_string(ban_action)),
|
|
log_data_string("ban_reason", ban_reason),
|
|
log_data_integer("ban_time", ban_time));
|
|
return take_action(client, ban_action, ban_reason, ban_time, 0, NULL);
|
|
}
|
|
|
|
void blacklist_hit(Client *client, Blacklist *bl, int reply)
|
|
{
|
|
char opernotice[512], banbuf[512], reply_num[5];
|
|
const char *name[6], *value[6];
|
|
BLUser *blu = BLUSER(client);
|
|
|
|
if (find_tkline_match(client, 1))
|
|
return; /* already klined/glined. Don't send the warning from below. */
|
|
|
|
if (IsUser(client))
|
|
snprintf(opernotice, sizeof(opernotice), "[Blacklist] IP %s (%s) matches blacklist %s (%s/reply=%d)",
|
|
GetIP(client), client->name, bl->name, bl->backend->dns->name, reply);
|
|
else
|
|
snprintf(opernotice, sizeof(opernotice), "[Blacklist] IP %s matches blacklist %s (%s/reply=%d)",
|
|
GetIP(client), bl->name, bl->backend->dns->name, reply);
|
|
|
|
snprintf(reply_num, sizeof(reply_num), "%d", reply);
|
|
|
|
name[0] = "ip";
|
|
value[0] = GetIP(client);
|
|
name[1] = "server";
|
|
value[1] = me.name;
|
|
name[2] = "blacklist";
|
|
value[2] = bl->name;
|
|
name[3] = "dnsname";
|
|
value[3] = bl->backend->dns->name;
|
|
name[4] = "dnsreply";
|
|
value[4] = reply_num;
|
|
name[5] = NULL;
|
|
value[5] = NULL;
|
|
/* when adding more, be sure to update the array elements number in the definition of const char *name[] and value[] */
|
|
|
|
buildvarstring(bl->reason, banbuf, sizeof(banbuf), name, value);
|
|
|
|
if (only_soft_actions(bl->action) && blu)
|
|
{
|
|
/* For soft bans, delay the action until later (so user can do SASL auth) */
|
|
blu->save_action = duplicate_ban_actions(bl->action);
|
|
blu->save_tkltime = bl->ban_time;
|
|
safe_strdup(blu->save_opernotice, opernotice);
|
|
safe_strdup(blu->save_reason, banbuf);
|
|
safe_strdup(blu->save_blacklist, bl->name);
|
|
safe_strdup(blu->save_blacklist_dns_name, bl->backend->dns->name);
|
|
blu->save_blacklist_dns_reply = reply;
|
|
} else {
|
|
/* Otherwise, execute the action immediately */
|
|
blacklist_action(client, opernotice, bl->action, banbuf, bl->ban_time, bl->name, bl->backend->dns->name, reply);
|
|
}
|
|
}
|
|
|
|
void blacklist_process_result(Client *client, int status, struct hostent *he)
|
|
{
|
|
Blacklist *bl;
|
|
char *domain;
|
|
int reply;
|
|
int i;
|
|
int replycnt;
|
|
|
|
if ((status != 0) || (he->h_length != 4) || !he->h_name)
|
|
return; /* invalid reply */
|
|
|
|
domain = getdnsblname(he->h_name, client);
|
|
if (!domain)
|
|
return; /* odd */
|
|
bl = blacklist_find_block_by_dns(domain);
|
|
if (!bl)
|
|
return; /* possibly just rehashed and the blacklist block is gone now */
|
|
|
|
/* walk through all replies for this record... until we have a hit */
|
|
for (replycnt=0; he->h_addr_list[replycnt]; replycnt++)
|
|
{
|
|
reply = blacklist_parse_reply(he, replycnt);
|
|
|
|
for (i = 0; bl->backend->dns->reply[i]; i++)
|
|
{
|
|
if ((bl->backend->dns->reply[i] == -1) ||
|
|
( (bl->backend->dns->type == DNSBL_BITMASK) && (reply & bl->backend->dns->reply[i]) ) ||
|
|
( (bl->backend->dns->type == DNSBL_RECORD) && (bl->backend->dns->reply[i] == reply) ) )
|
|
{
|
|
blacklist_hit(client, bl, reply);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void blacklist_resolver_callback(void *arg, int status, int timeouts, struct hostent *he)
|
|
{
|
|
BLUser *blu = (BLUser *)arg;
|
|
Client *client = blu->client;
|
|
|
|
#ifdef DEBUGMODE
|
|
unreal_log(ULOG_DEBUG, "blacklist", "BLACKLIST_RESOLVER_CALLBACK", client,
|
|
"Called for client $client.details");
|
|
#endif
|
|
|
|
blu->refcnt--; /* one less outstanding DNS request remaining */
|
|
|
|
/* If we are the last to resolve something and the client is gone
|
|
* already then free the struct.
|
|
*/
|
|
if ((blu->refcnt == 0) && !client)
|
|
blacklist_free_bluser_if_able(blu);
|
|
|
|
blu = NULL;
|
|
|
|
if (!client)
|
|
return; /* Client left already */
|
|
/* ^^ note: do not merge this with the other 'if' a few lines up (refcnt!) */
|
|
|
|
blacklist_process_result(client, status, he);
|
|
}
|
|
|
|
int blacklist_preconnect(Client *client)
|
|
{
|
|
BLUser *blu = BLUSER(client);
|
|
|
|
if (!blu || !blu->save_action)
|
|
return HOOK_CONTINUE;
|
|
|
|
/* There was a pending softban... has the user authenticated via SASL by now? */
|
|
if (IsLoggedIn(client))
|
|
return HOOK_CONTINUE; /* yup, so the softban does not apply. */
|
|
|
|
if (blacklist_action(client, blu->save_opernotice, blu->save_action, blu->save_reason, blu->save_tkltime,
|
|
blu->save_blacklist, blu->save_blacklist_dns_name, blu->save_blacklist_dns_reply) > 0)
|
|
{
|
|
return HOOK_DENY;
|
|
}
|
|
return HOOK_CONTINUE; /* exempt */
|
|
}
|
|
|
|
void blacklist_recheck_user(Client *client)
|
|
{
|
|
SetLastBLCheck(client, TStime());
|
|
blacklist_start_check(client, 1);
|
|
}
|
|
|
|
EVENT(blacklist_recheck)
|
|
{
|
|
Client *client;
|
|
time_t last_check;
|
|
|
|
if (BLACKLIST_RECHECK_TIME == 0)
|
|
return;
|
|
|
|
list_for_each_entry(client, &lclient_list, lclient_node)
|
|
{
|
|
/* Only check connected users */
|
|
if (!IsUser(client))
|
|
continue;
|
|
|
|
last_check = LastBLCheck(client);
|
|
if ((last_check == 0) && (TStime() - client->local->creationtime >= BLACKLIST_RECHECK_TIME_FIRST))
|
|
{
|
|
/* First time: check after 60 seconds already */
|
|
blacklist_recheck_user(client);
|
|
} else /* After that, check every <...> seconds */
|
|
if (last_check && (TStime() - last_check) >= BLACKLIST_RECHECK_TIME)
|
|
{
|
|
blacklist_recheck_user(client);
|
|
}
|
|
}
|
|
}
|