/* * 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; }