Download | Plain Text | Line Numbers
/*
* ProFTPD: mod_dovecot_anvil -- slows down bruteforce attacks with the use
* of dovecot's anvil/penalty backend
*
* Copyright (c) 2016 Manuel Mausz
*
* 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 2 of the License, 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*/
#define MOD_DOVECOT_ANVIL_VERSION "mod_dovecot_anvil/0.3.0"
#define ANVIL_DEFAULT_URI "/var/run/dovecot/anvil-auth-penalty"
#define ANVIL_HANDSHAKE "VERSION\tanvil\t1\t0\n"
#define ANVIL_PENALTY_INIT_SECS 2
#define ANVIL_PENALTY_MAX_SECS 60
#define ANVIL_PENALTY_IPV6_MASK_BITS 48
#include "conf.h"
#include "privs.h"
static const char *trace_channel = "dovecot_anvil";
static struct {
int fd;
const char *ident;
int penalty;
uint32_t checksum;
} anvil = { .fd = -1, .checksum = 0 };
module dovecot_anvil_module;
static int dovecot_anvil_sess_init(void);
static uint32_t crc32_str(const char *str);
static uint32_t crc32_str_more(uint32_t crc, const char *str);
static const char *anvil_get_ident(pool *p)
{
const char *addr = pr_table_get(session.notes, "mod_xclient.addr", NULL);
const pr_netaddr_t *remote_addr = (addr)
? pr_netaddr_get_addr2(session.pool, addr, NULL, PR_NETADDR_GET_ADDR_FL_EXCL_DNS)
: pr_netaddr_get_sess_remote_addr();
pr_trace_msg(trace_channel, 5, "fetching ident for %s",
pr_netaddr_get_ipstr(remote_addr));
#ifdef PR_USE_IPV6
if (pr_netaddr_use_ipv6() && pr_netaddr_get_family(remote_addr) == AF_INET6) {
pr_netaddr_t *remote_addr4 = pr_netaddr_v6tov4(p, remote_addr);
if (remote_addr4 != NULL)
return pr_netaddr_get_ipstr(remote_addr4);
struct in6_addr ip = *(struct in6_addr *)pr_netaddr_get_inaddr(remote_addr);
memset(ip.s6_addr + ANVIL_PENALTY_IPV6_MASK_BITS/CHAR_BIT, 0,
sizeof(ip.s6_addr) - ANVIL_PENALTY_IPV6_MASK_BITS/CHAR_BIT);
char remote_ip6[INET6_ADDRSTRLEN];
pr_inet_ntop(AF_INET6, &ip, remote_ip6, INET6_ADDRSTRLEN);
return pstrdup(p, remote_ip6);
}
#endif
return pr_netaddr_get_ipstr(remote_addr);
}
static int net_connect_inet(const char *host, const char *port)
{
struct addrinfo hints = {
.ai_family = AF_UNSPEC,
.ai_socktype = SOCK_STREAM
};
struct addrinfo *result;
pr_trace_msg(trace_channel, 5, "connecting to tcp://%s:%s", host, port);
int s = getaddrinfo(host, port, &hints, &result);
if (s != 0) {
pr_log_pri(PR_LOG_ERR, MOD_DOVECOT_ANVIL_VERSION ": getaddrinfo: %s",
gai_strerror(s));
return -1;
}
//FIXME: connect might hang
int sockfd = -1;
struct addrinfo *r;
for (r = result; r != NULL; r=r->ai_next) {
sockfd = socket(r->ai_family, r->ai_socktype | SOCK_CLOEXEC,
r->ai_protocol);
if (sockfd == -1)
continue;
if (connect(sockfd, r->ai_addr, r->ai_addrlen) != -1)
break; /* success */
close(sockfd);
sockfd = -1;
break; /* we only try the first record */
}
freeaddrinfo(result);
return sockfd;
}
static int net_connect_unix(const char *path)
{
pr_trace_msg(trace_channel, 5, "connecting to unix://%s", path);
int sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (sockfd < 0)
return -1;
struct sockaddr_un saddr;
saddr.sun_family = AF_UNIX;
strcpy(saddr.sun_path, path);
PRIVS_ROOT
int conn = connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
PRIVS_RELINQUISH
if (conn != -1)
return sockfd;
close(sockfd);
return -1;
}
static int anvil_connect(config_rec *c)
{
int sockfd = (c->argc == 2) ? net_connect_inet(c->argv[0], c->argv[1]) :
net_connect_unix(c->argv[0]);
if (sockfd < 0) {
pr_log_pri(PR_LOG_ERR, MOD_DOVECOT_ANVIL_VERSION
": Unable to connect to socket: %s", strerror(errno));
return -1;
}
if (send(sockfd, ANVIL_HANDSHAKE, strlen(ANVIL_HANDSHAKE), 0)
!= strlen(ANVIL_HANDSHAKE)) {
pr_log_pri(PR_LOG_ERR, MOD_DOVECOT_ANVIL_VERSION
": Error while sending handshake: %s", strerror(errno));
close(sockfd);
return -1;
}
return sockfd;
}
typedef int anvil_penalty_callback_t(uint16_t penalty,
unsigned long last_penalty);
static int anvil_penalty_get(int sockfd, const char *ident,
anvil_penalty_callback_t *callback)
{
char buf[100];
int len = sprintf(buf, "PENALTY-GET\t%s\n", ident);
if (send(sockfd, buf, len, 0) != len)
return -1;
if ((len = recv(sockfd, &buf, sizeof(buf), 0)) < 0)
return -1;
buf[len] = '\0';
uint16_t penalty = 0;
unsigned long last_penalty = 0;
if (sscanf(buf, "%" SCNu16 " %lu", &penalty, &last_penalty) != 2) {
pr_log_pri(PR_LOG_ERR, MOD_DOVECOT_ANVIL_VERSION
"Invalid PENALTY-GET reply: %s", buf);
return -1;
}
pr_trace_msg(trace_channel, 5, "penalty for '%s' is %u", ident, penalty);
return callback(penalty, last_penalty);
}
static void anvil_penalty_inc(int sockfd, const char *ident, uint32_t checksum)
{
char buf[100];
pr_trace_msg(trace_channel, 5, "incrementing penalty for '%s'", ident);
int len = sprintf(buf, "PENALTY-INC\t%s\t%u\n", ident, checksum);
(void)send(sockfd, buf, len, 0);
}
static void anvil_penalty_set(int sockfd, const char *ident, uint16_t value)
{
char buf[100];
pr_trace_msg(trace_channel, 5, "setting penalty for '%s' to %" PRIu16,
ident, value);
int len = sprintf(buf, "PENALTY-SET\t%s\t%u\n", ident, value);
(void)send(sockfd, buf, len, 0);
}
static unsigned int anvil_penalty_to_secs(uint16_t penalty)
{
unsigned int i, secs = ANVIL_PENALTY_INIT_SECS;
for (i = 0; i < penalty; i++)
secs *= 2;
return secs < ANVIL_PENALTY_MAX_SECS ? secs : ANVIL_PENALTY_MAX_SECS;
}
static void mask_sigalarm(unsigned char block)
{
static sigset_t mask_sigset;
if (block) {
sigemptyset(&mask_sigset);
sigaddset(&mask_sigset, SIGALRM);
if (sigprocmask(SIG_BLOCK, &mask_sigset, NULL) < 0)
pr_log_pri(PR_LOG_NOTICE, "unable to block signal set: %s",
strerror(errno));
}
else {
if (sigprocmask(SIG_UNBLOCK, &mask_sigset, NULL) < 0)
pr_log_pri(PR_LOG_NOTICE, "unable to unblock signal set: %s",
strerror(errno));
}
}
static int anvil_penalty_callback_sleep(uint16_t penalty,
unsigned long last_penalty)
{
if (penalty > 0) {
unsigned int secs = anvil_penalty_to_secs(penalty);
pr_trace_msg(trace_channel, 5, "sleeping %u secs", secs);
mask_sigalarm(TRUE);
struct timeval tv = { .tv_sec = secs, .tv_usec = 0 };
(void)select(0, NULL, NULL, NULL, &tv);
mask_sigalarm(FALSE);
pr_timer_reset(PR_TIMER_LOGIN, ANY_MODULE);
}
return penalty;
}
/* command handlers */
MODRET dovecot_anvil_pre_pass(cmd_rec *cmd)
{
const char *user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
if (user == NULL)
return PR_DECLINED(cmd);
config_rec *c;
c = find_config(main_server->conf, CONF_PARAM, "DovecotAnvil", FALSE);
if (!c)
{
pr_trace_msg(trace_channel, 5, "directive DovecotAnvil not enabled."
" mod_dovecot_anvil not enabled.");
return PR_DECLINED(cmd);
}
// our auth handler will be called in the middle of command handling, so
// lifetime of cmd->pool is ok
if ((anvil.ident = anvil_get_ident(cmd->pool)) == NULL)
{
pr_log_pri(PR_LOG_ERR, MOD_DOVECOT_ANVIL_VERSION ": Unable to get ident");
return PR_DECLINED(cmd);
}
if ((anvil.fd = anvil_connect(c)) == -1)
return PR_DECLINED(cmd);
anvil.penalty = anvil_penalty_get(anvil.fd, anvil.ident,
anvil_penalty_callback_sleep);
if (anvil.penalty == -1) {
pr_log_pri(PR_LOG_ERR, MOD_DOVECOT_ANVIL_VERSION
"Error while fetching penalty: %s", strerror(errno));
close(anvil.fd);
anvil.fd = -1;
return PR_DECLINED(cmd);
}
return PR_DECLINED(cmd);
}
MODRET dovecot_anvil_post_pass(cmd_rec *cmd)
{
if (anvil.fd != -1) {
(void)anvil_penalty_set(anvil.fd, anvil.ident, 0);
close(anvil.fd);
anvil.fd = -1;
}
return PR_DECLINED(cmd);
}
MODRET dovecot_anvil_post_pass_err(cmd_rec *cmd)
{
if (anvil.fd != -1) {
int do_increment = TRUE;
if ((cmd->cmd_class & CL_SFTP) || (cmd->cmd_class & CL_SSH)) {
const char *sftp_method = pr_env_get(cmd->pool, "SFTP_USER_AUTH_METHOD");
do_increment = (sftp_method != NULL && strncmp(sftp_method, "password", 9) == 0)
? TRUE : FALSE;
}
if (do_increment)
(void)anvil_penalty_inc(anvil.fd, anvil.ident, anvil.checksum);
close(anvil.fd);
anvil.fd = -1;
}
return PR_DECLINED(cmd);
}
/* configuration handlers */
MODRET set_dovecot_anvil(cmd_rec *cmd)
{
CHECK_ARGS(cmd, 1);
CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
int val = get_boolean(cmd, 1);
if (val == TRUE)
add_config_param_str(cmd->argv[0], 1, ANVIL_DEFAULT_URI);
else if (val == -1) {
if (strncmp(cmd->argv[1], "tcp://", strlen("tcp://")) == 0) {
config_rec *c = add_config_param(cmd->argv[0], 2, NULL, NULL);
char *path = pstrdup(c->pool, cmd->argv[1] + strlen("tcp://"));
char *portptr = rindex(path, ':');
if (portptr == NULL)
CONF_ERROR(cmd, "Invalid uri: port missing?");
*portptr = '\0';
char *host = path;
if (host[0] == '[' && *(portptr - 1) == ']') {
host++;
*(portptr - 1) = '\0';
}
c->argv[0] = host;
c->argv[1] = portptr + 1;
}
else if (strncmp(cmd->argv[1], "unix://", strlen("unix://")) == 0)
add_config_param_str(cmd->argv[0], 1, cmd->argv[1] + strlen("unix://"));
else
CONF_ERROR(cmd, "has invalid uri syntax");
}
return PR_HANDLED(cmd);
}
/* authentication handlers */
MODRET dovecot_anvil_auth(cmd_rec *cmd)
{
if (cmd->argc != 2)
return PR_DECLINED(cmd);
if (anvil.fd != -1)
anvil.checksum = crc32_str_more(crc32_str(cmd->argv[1]), cmd->argv[0]);
return PR_DECLINED(cmd);
}
/* event handlers */
#if defined(PR_SHARED_MODULE)
static void dovecot_anvil_mod_unload_ev(const void *event_data, void *user_data)
{
if (strcmp("mod_dovecot_anvil.c", (const char *)event_data) == 0)
pr_event_unregister(&dovecot_anvil_module, NULL, NULL);
}
#endif
static void dovecot_anvil_sess_reinit_ev(const void *event_data, void *user_data)
{
/* A HOST command changed the main_server pointer, reinitialize ourselves. */
pr_event_unregister(&dovecot_anvil_module, "core.session-reinit",
dovecot_anvil_sess_reinit_ev);
pr_auth_remove_auth_only_module("mod_dovecot_anvil.c");
if (dovecot_anvil_sess_init() < 0)
pr_session_disconnect(&dovecot_anvil_module,
PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
}
/* initialization routines */
static int dovecot_anvil_sess_init(void)
{
pr_event_register(&dovecot_anvil_module, "core.session-reinit",
dovecot_anvil_sess_reinit_ev, NULL);
config_rec *c;
c = find_config(main_server->conf, CONF_PARAM, "DovecotAnvil", FALSE);
if (!c)
{
pr_trace_msg(trace_channel, 5, "directive DovecotAnvil not enabled."
" mod_dovecot_anvil not enabled.");
return 0;
}
if (pr_auth_add_auth_only_module("mod_dovecot_anvil.c") < 0) {
int xerrno = errno;
pr_log_pri(PR_LOG_NOTICE, MOD_DOVECOT_ANVIL_VERSION
": unable to add 'mod_dovecot_anvil.c' as an auth-only module: %s",
strerror(xerrno));
errno = xerrno;
return -1;
}
return 0;
}
static int dovecot_anvil_init(void)
{
#if defined(PR_SHARED_MODULE)
pr_event_register(&dovecot_anvil_module, "core.module-unload",
dovecot_anvil_mod_unload_ev, NULL);
#endif
return 0;
}
/* module api tables */
static conftable dovecot_anvil_conftab[] =
{
{ "DovecotAnvil", set_dovecot_anvil, NULL },
{ NULL }
};
static cmdtable dovecot_anvil_cmdtab[] =
{
{ PRE_CMD, C_PASS, G_NONE, dovecot_anvil_pre_pass,
FALSE, FALSE, CL_AUTH },
{ POST_CMD, C_PASS, G_NONE, dovecot_anvil_post_pass,
FALSE, FALSE, CL_AUTH },
{ POST_CMD_ERR, C_PASS, G_NONE, dovecot_anvil_post_pass_err,
FALSE, FALSE, CL_AUTH },
{ 0, NULL }
};
static authtable dovecot_anvil_authtab[] = {
{ 0, "auth", dovecot_anvil_auth },
{ 0, NULL, NULL }
};
module dovecot_anvil_module =
{
/* always NULL */
NULL, NULL,
/* module api version 2.0 */
0x20,
/* module name */
"dovecot_anvil",
/* module configuration handler table */
dovecot_anvil_conftab,
/* module command handler table */
dovecot_anvil_cmdtab,
/* module authentication handler table */
dovecot_anvil_authtab,
/* module initialization function */
dovecot_anvil_init,
/* module session initialization function */
dovecot_anvil_sess_init,
/* module version */
MOD_DOVECOT_ANVIL_VERSION
};
/* copied from dovecot/lib/crc32.c, MIT licence */
static uint32_t crc32tab[256] = {
0x00000000,
0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E,
0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D,
0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0,
0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63,
0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA,
0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75,
0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180,
0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87,
0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106,
0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5,
0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818,
0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4,
0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B,
0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA,
0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541,
0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC,
0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F,
0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086,
0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E,
0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C,
0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B,
0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2,
0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671,
0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8,
0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767,
0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6,
0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795,
0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28,
0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B,
0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82,
0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D,
0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8,
0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF,
0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE,
0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D,
0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0,
0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C,
0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693,
0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02,
0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
};
static uint32_t crc32_str(const char *str)
{
return crc32_str_more(0, str);
}
static uint32_t crc32_str_more(uint32_t crc, const char *str)
{
const uint8_t *p = (const uint8_t *)str;
crc ^= 0xffffffff;
for (; *p != '\0'; p++)
crc = (crc >> 8) ^ crc32tab[((crc ^ *p) & 0xff)];
crc ^= 0xffffffff;
return crc;
}