diff --git a/configs/mod_events.conf b/configs/mod_ip_blocker.conf similarity index 74% rename from configs/mod_events.conf rename to configs/mod_ip_blocker.conf index 8a2654a4..2f84746b 100644 --- a/configs/mod_events.conf +++ b/configs/mod_ip_blocker.conf @@ -1,9 +1,9 @@ -; mod_events.conf - BBS event handlers +; mod_ip_blocker.conf - BBS event handlers [whitelist] ; List of IP addresses or CIDR ranges to whitelist from autoblocking. ; These IP addresses will not be automatically blocked -; by mod_events if the abuse threshold is reached. +; by mod_ip_blocker if the abuse threshold is reached. ; Key contains the IP/CIDR, the value does not matter and is ignored. ; ; You should add any trusted IP addresses here (e.g. sysop network): diff --git a/configs/modules.conf b/configs/modules.conf index c1cfa5d4..8286fb33 100644 --- a/configs/modules.conf +++ b/configs/modules.conf @@ -5,21 +5,110 @@ autoload=yes ; By default, all modules are loaded automatically on startup. ; You can explicitly load only the modules you want by using autoload=no. [modules] -; You can specify modules to preload, for module dependencies that must be met -; before dependent modules load. -; For all operations here, the order of preload, load, and noload does not matter -; (and should not be relied upon). +; You can specify modules to preload, for module dependencies that must be met before dependent modules load. +; For all operations here, the order of preload, load, and noload does not matter (and should not be relied upon). ; require is equivalent to load, but will abort startup if the module fails to load. ; To preload and require a module, specify both directives. -preload = mod_mysql.so ; mod_auth_mysql.so and mod_chanserv.so depend on this module -preload = net_irc.so ; mod_discord.so, mod_chanserv.so, and mod_irc_relay.so depend on this module -preload = mod_irc_client.so ; mod_irc_relay.so depends on this module -; You can specify modules to load or not load here. + +; You can specify modules to automatically load or not load at startup here. ; You MUST specify the .so extension in modules.conf (but optional in sysop console) ; Modules can always be explicitly loaded from the sysop console ; using /load , even if they were not autoloaded. + +; Library modules +preload = io_compress.so +preload = io_tls.so +preload = mod_lmdb.so +preload = mod_mysql.so ; mod_auth_mysql.so and mod_chanserv.so depend on this module + +; Basic doors +load = door_usermgmt.so +load = door_tutorial.so +load = door_stats.so + +; Authentication modules (only load one) +load = mod_auth_mysql.so noload = mod_auth_static.so ; Development module that should not be used for production systems. Conflicts with mod_auth_mysql.so. -require = mod_sysop.so ; You probably don't want to forget this one if autoload=no. require will force the module to load or abort startup on failure. -load = net_ssh.so ; Always load SSH server + +; Modules required for basic node operation +load = mod_menu_handlers.so +load = mod_node_callbacks.so + +; Management and utility modules +load = mod_sysop.so ; You probably don't want to forget this one if autoload=no. require will force the module to load or abort startup on failure. +load = mod_version.so +load = mod_ip_blocker.so + +; Email and messaging doors +load = door_chat.so +load = door_evergreen.so +load = door_irc.so +load = door_msg.so + +; Misc doors +load = door_utilities.so +load = door_ibbs.so + +; IRC modules +preload = net_irc.so ; mod_discord.so, mod_chanserv.so, and mod_irc_relay.so depend on this module +load = mod_irc_client.so ; mod_irc_relay.so depends on this module +load = mod_irc_relay.so +load = mod_chanserv.so +load = mod_discord.so +load = mod_slack.so + +; Email modules +preload = mod_mail.so +preload = mod_mimeparse.so +load = mod_mail_trash.so +load = mod_mail_events.so +load = mod_mailscript.so +load = mod_oauth.so +load = mod_sendmail.so +load = mod_sieve.so +load = mod_smtp_client.so +load = mod_smtp_delivery_external.so +load = mod_smtp_delivery_local.so +load = mod_smtp_fetchmail.so +load = mod_smtp_filter.so +load = mod_smtp_filter_arc.so +load = mod_smtp_filter_dkim.so +load = mod_smtp_filter_dmarc.so +load = mod_smtp_mailing_lists.so +load = mod_spamassassin.so +load = mod_webmail.so +load = net_imap.so +load = net_nntp.so +load = net_pop3.so +load = net_sieve.so +load = net_smtp.so + +; Web server +preload = mod_http.so +load = mod_http_proxy.so +load = net_http.so +load = net_ws.so + +; Terminal protocols +load = net_rlogin.so +load = net_ssh.so +load = net_telnet.so +load = net_unix.so + +; Misc. network protocols noload = net_finger.so ; This module exposes user information, so don't load by default. -;noload = net_rlogin.so ; Don't load RLogin server +load = net_ftp.so +load = net_gopher.so +load = net_msp.so + +; Asterisk modules +preload = mod_ncurses.so +preload = mod_asterisk_ami.so +load = mod_asterisk_queues.so +load = mod_operator.so + +; Test modules +noload = mod_tests.so +noload = mod_test_config.so +noload = mod_test_http.so +noload = mod_test_range.so diff --git a/include/version.h b/include/version.h index 19e9afaa..25219b04 100644 --- a/include/version.h +++ b/include/version.h @@ -14,5 +14,5 @@ */ #define BBS_MAJOR_VERSION 0 -#define BBS_MINOR_VERSION 6 -#define BBS_PATCH_VERSION 2 +#define BBS_MINOR_VERSION 7 +#define BBS_PATCH_VERSION 0 diff --git a/modules/mod_ip_blocker.c b/modules/mod_ip_blocker.c new file mode 100755 index 00000000..3b2cb379 --- /dev/null +++ b/modules/mod_ip_blocker.c @@ -0,0 +1,332 @@ +/* + * LBBS -- The Lightweight Bulletin Board System + * + * Copyright (C) 2023, Naveen Albert + * + * Naveen Albert + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief fail2ban-like IP banner + * + * \author Naveen Albert + */ + +#include "include/bbs.h" + +#include +#include +#include +#include + +#include "include/module.h" +#include "include/config.h" +#include "include/node.h" +#include "include/user.h" +#include "include/event.h" +#include "include/system.h" +#include "include/linkedlists.h" +#include "include/stringlist.h" +#include "include/utils.h" /* use bbs_str_isprint */ +#include "include/cli.h" +#include "include/variables.h" + +/* Don't ban private IP addresses, under any circumstances */ +#define IGNORE_LOCAL_NETS + +/*! \brief Whitelisted IP addresses */ +struct stringlist ip_whitelist; + +struct ip_block { + struct in_addr addr; /* IP address */ + time_t epoch; /* Epoch time of last auth failure */ + struct timeval lastfail; /* Granular time of last auth failure */ + unsigned int authfails; /* Total number of auth fails */ + unsigned int authhits; /* Total number of uncompleted connections (never logged in) */ + unsigned int quickfails; /* Total number of auth fails in succession */ + unsigned int quickhits; /* Total number of hits in succession */ + unsigned int banned:1; /* Whether this has been blocked */ + RWLIST_ENTRY(ip_block) entry; +}; + +static RWLIST_HEAD_STATIC(ipblocks, ip_block); + +/* Maximum number of IPs to keep track of. This must be limited, because this could be unbounded. */ +#define MAX_BAD_IPS 100 + +/* If no hits in 2 hours, purge it */ +#define GOOD_NEIGHBOR_SEC 7200 + +#pragma GCC diagnostic ignored "-Wdiscarded-qualifiers" +static void ban_ip(const char *addr) +{ + int res; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" + char *ipaddr = (char*) addr; + + if (is_root()) { + char *argv[] = { "/usr/sbin/iptables", "-A", "INPUT", "-s", ipaddr, "-j", "DROP", NULL }; + res = bbs_execvp_fd(NULL, -1, -1, "/usr/sbin/iptables", argv); + } else { + /* There's no guarantee that the BBS user is in the sudoers file for this command, or even that sudo is installed, + * but this is the only way it could even work, so give it a try. */ + char *argv[] = { "/usr/bin/sudo", "-n", "/usr/sbin/iptables", "-A", "INPUT", "-s", ipaddr, "-j", "DROP", NULL }; + res = bbs_execvp_fd(NULL, -1, -1, "/usr/bin/sudo", argv); + } + if (res) { + bbs_warning("Failed to block %s using iptables: %s\n", ipaddr, strerror(res)); + } else { + bbs_auth("Blocked IP address %s (too many failed connections)\n", ipaddr); + } +} +#pragma GCC diagnostic pop +#pragma GCC diagnostic pop + +static void process_bad_ip(struct in_addr *addr, const char *straddr, const char *username, enum bbs_event_type type) +{ + int c = 0; + struct ip_block *ip, *oldest_offender = NULL; + struct timeval nowtime; + time_t now, nowthresh; + unsigned long diff; + time_t least_recent_offend_time = 0; + int repeat_offender, do_ban; + + now = time(NULL); + nowthresh = now - GOOD_NEIGHBOR_SEC; + gettimeofday(&nowtime, NULL); + + RWLIST_WRLOCK(&ipblocks); + RWLIST_TRAVERSE_SAFE_BEGIN(&ipblocks, ip, entry) { + int purge = 0; + c++; + /* Purge any IPs that haven't had a hit in some amount of time */ + if (ip->epoch < nowthresh) { + purge = 1; + } + if (purge) { + /* It's been long enough we can purge this guy. */ + RWLIST_REMOVE_CURRENT(entry); + free(ip); + continue; + } + if (!memcmp(&ip->addr, addr, sizeof(struct in_addr))) { + break; /* Found it, repeat offender */ + } + if (!oldest_offender || ip->epoch < least_recent_offend_time) { + least_recent_offend_time = ip->epoch; + oldest_offender = ip; + } + } + RWLIST_TRAVERSE_SAFE_END; + + if (c >= MAX_BAD_IPS) { + RWLIST_REMOVE(&ipblocks, oldest_offender, entry); /* Capacity is full, purge the oldest offender to make room for newer ones */ + free(oldest_offender); + } + + repeat_offender = ip ? 1 : 0; + if (!ip) { + ip = calloc(1, sizeof(*ip)); + if (ALLOC_FAILURE(ip)) { + RWLIST_UNLOCK(&ipblocks); + return; /* Not much we can do */ + } + memcpy(&ip->addr, addr, sizeof(ip->addr)); + } else { + time_t secsince = now - ip->epoch; + /* If it's been at least 30 seconds since the last offense, reset quickhits to 0. */ + if (secsince > 30) { + ip->quickhits = 0; + } + /* If it's been at least 5 minutes since the last offense, reset authhits to 0. */ + if (secsince > 300) { + ip->authhits = 0; + } + } + + /* Treat failed logins much more severely than short sessions */ + if (type == EVENT_NODE_LOGIN_FAILED || type == EVENT_NODE_BAD_REQUEST || type == EVENT_NODE_ENCRYPTION_FAILED) { + int bad_username = 0; + if (!strlen_zero(username)) { + bad_username = !strcmp(username, "root") || !strcmp(username, "shell") || !strcmp(username, "system") || !bbs_str_isprint(username); + } + ip->authfails++; + if (bad_username || type == EVENT_NODE_ENCRYPTION_FAILED) { /* 99% sure this is spam, expedite the block */ + ip->authfails += 4; + } + } else { + ip->authhits++; + } + if (repeat_offender) { + diff = (unsigned long) (nowtime.tv_sec - ip->lastfail.tv_sec) * 1000000 + (unsigned long) (nowtime.tv_usec - ip->lastfail.tv_usec); + if (diff < 200000) { + /* Less than 200 ms since the last hit. Almost certainly automated scanning. */ + if (type == EVENT_NODE_LOGIN_FAILED || type == EVENT_NODE_BAD_REQUEST || type == EVENT_NODE_ENCRYPTION_FAILED) { + ip->quickfails++; + } else { + ip->quickhits++; + } + } + bbs_debug(2, "IP address %s blacklist score: %d/%d/%d/%d (last offense: %" TIME_T_FMT "s/%luus ago)\n", straddr, ip->authfails, ip->authhits, ip->quickfails, ip->quickhits, now - ip->epoch, diff); + } else { + bbs_debug(2, "IP address %s blacklist score: %d/%d/%d/%d (first offense)\n", straddr, ip->authfails, ip->authhits, ip->quickfails, ip->quickhits); + } + ip->epoch = now; + memcpy(&ip->lastfail, &nowtime, sizeof(ip->lastfail)); + + if (!repeat_offender) { + RWLIST_INSERT_HEAD(&ipblocks, ip, entry); + } + + /* These thresholds are specifically intended to be tolerant of email client "autoconfiguration", + * which may probe the SMTP, POP3, and IMAP server all in a split second. + * For example, a single "autoconfiguration" brings the score to 0/6/0/5 after one attempt and 0/12/0/10 after two. + * If you keep running autoconfig, it will still block you eventually, but this should grant some leeway. + */ + do_ban = (ip->authfails >= 10 || ip->authhits >= 50 || ip->quickfails >= 2 || ip->quickhits >= 15) && !ip->banned; + if (do_ban) { + /* If we get a flurry of connects, it suffices to block once, + * otherwise we're just wasting resources. */ + ip->banned = 1; + } + RWLIST_UNLOCK(&ipblocks); + + if (do_ban) { + ban_ip(straddr); /* Make the system call after unlocking the list */ + } +} + +static int cli_ips(struct bbs_cli_args *a) +{ + struct ip_block *ip; + time_t now = time(NULL); + + bbs_dprintf(a->fdout, "%-55s %13s %11s %11s %11s %11s\n", "Address", "Last Fail (s)", "Auth Fails", "Auth Hits", "Quick Fails", "Quick Hits"); + RWLIST_RDLOCK(&ipblocks); + RWLIST_TRAVERSE(&ipblocks, ip, entry) { + char buf[56]; + time_t ago; + if (!inet_ntop(AF_INET, &ip->addr, buf, (socklen_t) sizeof(buf))) { /* XXX Assumes IPv4 */ + bbs_error("Failed to get IP address: %s\n", strerror(errno)); + continue; + } + ago = now - ip->epoch; + bbs_dprintf(a->fdout, "%-55s %13" TIME_T_FMT " %11d %11d %11d %11d\n", buf, ago, ip->authfails, ip->authhits, ip->quickfails, ip->quickhits); + } + RWLIST_UNLOCK(&ipblocks); + return 0; +} + +static int ip_whitelisted(const char *ip) +{ + const char *s; + struct stringitem *i = NULL; + + while ((s = stringlist_next(&ip_whitelist, &i))) { + if (bbs_ip_match_ipv4(ip, s)) { + return 1; + } + } + return 0; +} + +static int event_cb(struct bbs_event *event) +{ + struct sockaddr_in sa; + + switch (event->type) { + case EVENT_NODE_LOGIN_FAILED: + case EVENT_NODE_SHORT_SESSION: + case EVENT_NODE_BAD_REQUEST: + case EVENT_NODE_ENCRYPTION_FAILED: + if (strlen_zero(event->ipaddr)) { + bbs_error("Missing IP address\n"); + return -1; + } + if (bbs_is_loopback_ipv4(event->ipaddr)) { + return 1; /* Ignore localhost */ + } +#ifdef IGNORE_LOCAL_NETS + if (bbs_ip_is_private_ipv4(event->ipaddr)) { + return 1; /* Ignore private CIDR ranges */ + } +#endif + if (ip_whitelisted(event->ipaddr)) { + return 1; /* Ignore event if IP is whitelisted */ + } + /*! \todo IPs are currently stored as strings throughout the BBS (e.g. node->ipaddr). We should store them as an struct in_addr instead for efficiency. */ + /*! \todo Some protocols probably need to be exempted from this, e.g. Finger, Gopher, HTTP (to some extent), etc. + * For HTTP, if the request is bad, we should send an event, but if it's a successful request, then it's okay. */ + if (!inet_pton(AF_INET, event->ipaddr, &(sa.sin_addr))) { + bbs_error("Invalid IP address: %s\n", event->ipaddr); /* Bug somewhere else */ + return -1; + } + process_bad_ip(&sa.sin_addr, event->ipaddr, event->username, event->type); + return 1; + default: + return 0; + } + return 0; +} + +static int load_config(void) +{ + struct bbs_config *cfg; + struct bbs_config_section *section = NULL; + + cfg = bbs_config_load("mod_ip_blocker.conf", 1); + if (!cfg) { + return 0; + } + + /* IP whitelist */ + while ((section = bbs_config_walk(cfg, section))) { + struct bbs_keyval *keyval = NULL; + if (strcmp(bbs_config_section_name(section), "whitelist")) { + continue; /* Not the whitelist section */ + } + while ((keyval = bbs_config_section_walk(section, keyval))) { + const char *key = bbs_keyval_key(keyval); + if (!stringlist_contains(&ip_whitelist, key)) { + stringlist_push(&ip_whitelist, key); + bbs_verb(5, "Whitelisted IP/CIDR %s\n", key); + } + } + } + + return 0; +} + +static struct bbs_cli_entry cli_commands_events[] = { + BBS_CLI_COMMAND(cli_ips, "ips", 1, "List flagged IP addresses", NULL), +}; + +static int load_module(void) +{ + RWLIST_HEAD_INIT(&ip_whitelist); + + if (load_config()) { + stringlist_empty_destroy(&ip_whitelist); + return -1; + } + bbs_cli_register_multiple(cli_commands_events); + return bbs_register_event_consumer(event_cb); +} + +static int unload_module(void) +{ + int res = bbs_unregister_event_consumer(event_cb); + bbs_cli_unregister_multiple(cli_commands_events); + RWLIST_WRLOCK_REMOVE_ALL(&ipblocks, entry, free); + stringlist_empty_destroy(&ip_whitelist); + return res; +} + +BBS_MODULE_INFO_STANDARD("Malicious IP Blocker"); diff --git a/modules/mod_handlers.c b/modules/mod_menu_handlers.c similarity index 100% rename from modules/mod_handlers.c rename to modules/mod_menu_handlers.c diff --git a/modules/mod_events.c b/modules/mod_node_callbacks.c old mode 100644 new mode 100755 similarity index 55% rename from modules/mod_events.c rename to modules/mod_node_callbacks.c index 1301ccbc..1fd5c137 --- a/modules/mod_events.c +++ b/modules/mod_node_callbacks.c @@ -12,7 +12,7 @@ /*! \file * - * \brief Event callbacks + * \brief Node callbacks * * \author Naveen Albert */ @@ -20,225 +20,16 @@ #include "include/bbs.h" #include -#include -#include -#include #include "include/module.h" -#include "include/config.h" #include "include/node.h" #include "include/user.h" #include "include/event.h" #include "include/notify.h" -#include "include/system.h" -#include "include/linkedlists.h" -#include "include/stringlist.h" -#include "include/utils.h" /* use bbs_str_isprint */ -#include "include/cli.h" +#include "include/utils.h" #include "include/variables.h" #include "include/os.h" /* use bbs_get_osver */ -/* Don't ban private IP addresses, under any circumstances */ -#define IGNORE_LOCAL_NETS - -/*! \brief Whitelisted IP addresses */ -struct stringlist ip_whitelist; - -struct ip_block { - struct in_addr addr; /* IP address */ - time_t epoch; /* Epoch time of last auth failure */ - struct timeval lastfail; /* Granular time of last auth failure */ - unsigned int authfails; /* Total number of auth fails */ - unsigned int authhits; /* Total number of uncompleted connections (never logged in) */ - unsigned int quickfails; /* Total number of auth fails in succession */ - unsigned int quickhits; /* Total number of hits in succession */ - unsigned int banned:1; /* Whether this has been blocked */ - RWLIST_ENTRY(ip_block) entry; -}; - -static RWLIST_HEAD_STATIC(ipblocks, ip_block); - -/* Maximum number of IPs to keep track of. This must be limited, because this could be unbounded. */ -#define MAX_BAD_IPS 100 - -/* If no hits in 2 hours, purge it */ -#define GOOD_NEIGHBOR_SEC 7200 - -#pragma GCC diagnostic ignored "-Wdiscarded-qualifiers" -static void ban_ip(const char *addr) -{ - int res; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wcast-qual" - char *ipaddr = (char*) addr; - - if (is_root()) { - char *argv[] = { "/usr/sbin/iptables", "-A", "INPUT", "-s", ipaddr, "-j", "DROP", NULL }; - res = bbs_execvp_fd(NULL, -1, -1, "/usr/sbin/iptables", argv); - } else { - /* There's no guarantee that the BBS user is in the sudoers file for this command, or even that sudo is installed, - * but this is the only way it could even work, so give it a try. */ - char *argv[] = { "/usr/bin/sudo", "-n", "/usr/sbin/iptables", "-A", "INPUT", "-s", ipaddr, "-j", "DROP", NULL }; - res = bbs_execvp_fd(NULL, -1, -1, "/usr/bin/sudo", argv); - } - if (res) { - bbs_warning("Failed to block %s using iptables: %s\n", ipaddr, strerror(res)); - } else { - bbs_auth("Blocked IP address %s (too many failed connections)\n", ipaddr); - } -} -#pragma GCC diagnostic pop -#pragma GCC diagnostic pop - -static void process_bad_ip(struct in_addr *addr, const char *straddr, const char *username, enum bbs_event_type type) -{ - int c = 0; - struct ip_block *ip, *oldest_offender = NULL; - struct timeval nowtime; - time_t now, nowthresh; - unsigned long diff; - time_t least_recent_offend_time = 0; - int repeat_offender, do_ban; - - now = time(NULL); - nowthresh = now - GOOD_NEIGHBOR_SEC; - gettimeofday(&nowtime, NULL); - - RWLIST_WRLOCK(&ipblocks); - RWLIST_TRAVERSE_SAFE_BEGIN(&ipblocks, ip, entry) { - int purge = 0; - c++; - /* Purge any IPs that haven't had a hit in some amount of time */ - if (ip->epoch < nowthresh) { - purge = 1; - } - if (purge) { - /* It's been long enough we can purge this guy. */ - RWLIST_REMOVE_CURRENT(entry); - free(ip); - continue; - } - if (!memcmp(&ip->addr, addr, sizeof(struct in_addr))) { - break; /* Found it, repeat offender */ - } - if (!oldest_offender || ip->epoch < least_recent_offend_time) { - least_recent_offend_time = ip->epoch; - oldest_offender = ip; - } - } - RWLIST_TRAVERSE_SAFE_END; - - if (c >= MAX_BAD_IPS) { - RWLIST_REMOVE(&ipblocks, oldest_offender, entry); /* Capacity is full, purge the oldest offender to make room for newer ones */ - free(oldest_offender); - } - - repeat_offender = ip ? 1 : 0; - if (!ip) { - ip = calloc(1, sizeof(*ip)); - if (ALLOC_FAILURE(ip)) { - RWLIST_UNLOCK(&ipblocks); - return; /* Not much we can do */ - } - memcpy(&ip->addr, addr, sizeof(ip->addr)); - } else { - time_t secsince = now - ip->epoch; - /* If it's been at least 30 seconds since the last offense, reset quickhits to 0. */ - if (secsince > 30) { - ip->quickhits = 0; - } - /* If it's been at least 5 minutes since the last offense, reset authhits to 0. */ - if (secsince > 300) { - ip->authhits = 0; - } - } - - /* Treat failed logins much more severely than short sessions */ - if (type == EVENT_NODE_LOGIN_FAILED || type == EVENT_NODE_BAD_REQUEST || type == EVENT_NODE_ENCRYPTION_FAILED) { - int bad_username = 0; - if (!strlen_zero(username)) { - bad_username = !strcmp(username, "root") || !strcmp(username, "shell") || !strcmp(username, "system") || !bbs_str_isprint(username); - } - ip->authfails++; - if (bad_username || type == EVENT_NODE_ENCRYPTION_FAILED) { /* 99% sure this is spam, expedite the block */ - ip->authfails += 4; - } - } else { - ip->authhits++; - } - if (repeat_offender) { - diff = (unsigned long) (nowtime.tv_sec - ip->lastfail.tv_sec) * 1000000 + (unsigned long) (nowtime.tv_usec - ip->lastfail.tv_usec); - if (diff < 200000) { - /* Less than 200 ms since the last hit. Almost certainly automated scanning. */ - if (type == EVENT_NODE_LOGIN_FAILED || type == EVENT_NODE_BAD_REQUEST || type == EVENT_NODE_ENCRYPTION_FAILED) { - ip->quickfails++; - } else { - ip->quickhits++; - } - } - bbs_debug(2, "IP address %s blacklist score: %d/%d/%d/%d (last offense: %" TIME_T_FMT "s/%luus ago)\n", straddr, ip->authfails, ip->authhits, ip->quickfails, ip->quickhits, now - ip->epoch, diff); - } else { - bbs_debug(2, "IP address %s blacklist score: %d/%d/%d/%d (first offense)\n", straddr, ip->authfails, ip->authhits, ip->quickfails, ip->quickhits); - } - ip->epoch = now; - memcpy(&ip->lastfail, &nowtime, sizeof(ip->lastfail)); - - if (!repeat_offender) { - RWLIST_INSERT_HEAD(&ipblocks, ip, entry); - } - - /* These thresholds are specifically intended to be tolerant of email client "autoconfiguration", - * which may probe the SMTP, POP3, and IMAP server all in a split second. - * For example, a single "autoconfiguration" brings the score to 0/6/0/5 after one attempt and 0/12/0/10 after two. - * If you keep running autoconfig, it will still block you eventually, but this should grant some leeway. - */ - do_ban = (ip->authfails >= 10 || ip->authhits >= 50 || ip->quickfails >= 2 || ip->quickhits >= 15) && !ip->banned; - if (do_ban) { - /* If we get a flurry of connects, it suffices to block once, - * otherwise we're just wasting resources. */ - ip->banned = 1; - } - RWLIST_UNLOCK(&ipblocks); - - if (do_ban) { - ban_ip(straddr); /* Make the system call after unlocking the list */ - } -} - -static int cli_ips(struct bbs_cli_args *a) -{ - struct ip_block *ip; - time_t now = time(NULL); - - bbs_dprintf(a->fdout, "%-55s %13s %11s %11s %11s %11s\n", "Address", "Last Fail (s)", "Auth Fails", "Auth Hits", "Quick Fails", "Quick Hits"); - RWLIST_RDLOCK(&ipblocks); - RWLIST_TRAVERSE(&ipblocks, ip, entry) { - char buf[56]; - time_t ago; - if (!inet_ntop(AF_INET, &ip->addr, buf, (socklen_t) sizeof(buf))) { /* XXX Assumes IPv4 */ - bbs_error("Failed to get IP address: %s\n", strerror(errno)); - continue; - } - ago = now - ip->epoch; - bbs_dprintf(a->fdout, "%-55s %13" TIME_T_FMT " %11d %11d %11d %11d\n", buf, ago, ip->authfails, ip->authhits, ip->quickfails, ip->quickhits); - } - RWLIST_UNLOCK(&ipblocks); - return 0; -} - -static int ip_whitelisted(const char *ip) -{ - const char *s; - struct stringitem *i = NULL; - - while ((s = stringlist_next(&ip_whitelist, &i))) { - if (bbs_ip_match_ipv4(ip, s)) { - return 1; - } - } - return 0; -} - static int interactive_start(struct bbs_node *node) { char timebuf[29]; @@ -453,38 +244,9 @@ static int interactive_login(struct bbs_node *node) static int event_cb(struct bbs_event *event) { - struct sockaddr_in sa; const struct bbs_file_transfer_event *tevent; switch (event->type) { - case EVENT_NODE_LOGIN_FAILED: - case EVENT_NODE_SHORT_SESSION: - case EVENT_NODE_BAD_REQUEST: - case EVENT_NODE_ENCRYPTION_FAILED: - if (strlen_zero(event->ipaddr)) { - bbs_error("Missing IP address\n"); - return -1; - } - if (bbs_is_loopback_ipv4(event->ipaddr)) { - return 1; /* Ignore localhost */ - } -#ifdef IGNORE_LOCAL_NETS - if (bbs_ip_is_private_ipv4(event->ipaddr)) { - return 1; /* Ignore private CIDR ranges */ - } -#endif - if (ip_whitelisted(event->ipaddr)) { - return 1; /* Ignore event if IP is whitelisted */ - } - /*! \todo IPs are currently stored as strings throughout the BBS (e.g. node->ipaddr). We should store them as an struct in_addr instead for efficiency. */ - /*! \todo Some protocols probably need to be exempted from this, e.g. Finger, Gopher, HTTP (to some extent), etc. - * For HTTP, if the request is bad, we should send an event, but if it's a successful request, then it's okay. */ - if (!inet_pton(AF_INET, event->ipaddr, &(sa.sin_addr))) { - bbs_error("Invalid IP address: %s\n", event->ipaddr); /* Bug somewhere else */ - return -1; - } - process_bad_ip(&sa.sin_addr, event->ipaddr, event->username, event->type); - return 1; case EVENT_USER_REGISTRATION: /* Relatively speaking, it's a pretty big deal whenever a new user registers. * Notify the sysop. */ @@ -526,57 +288,14 @@ static int event_cb(struct bbs_event *event) return 0; } -static int load_config(void) -{ - struct bbs_config *cfg; - struct bbs_config_section *section = NULL; - - cfg = bbs_config_load("mod_events.conf", 1); - if (!cfg) { - return 0; - } - - /* IP whitelist */ - while ((section = bbs_config_walk(cfg, section))) { - struct bbs_keyval *keyval = NULL; - if (strcmp(bbs_config_section_name(section), "whitelist")) { - continue; /* Not the whitelist section */ - } - while ((keyval = bbs_config_section_walk(section, keyval))) { - const char *key = bbs_keyval_key(keyval); - if (!stringlist_contains(&ip_whitelist, key)) { - stringlist_push(&ip_whitelist, key); - bbs_verb(5, "Whitelisted IP/CIDR %s\n", key); - } - } - } - - return 0; -} - -static struct bbs_cli_entry cli_commands_events[] = { - BBS_CLI_COMMAND(cli_ips, "ips", 1, "List flagged IP addresses", NULL), -}; - static int load_module(void) { - RWLIST_HEAD_INIT(&ip_whitelist); - - if (load_config()) { - stringlist_empty_destroy(&ip_whitelist); - return -1; - } - bbs_cli_register_multiple(cli_commands_events); return bbs_register_event_consumer(event_cb); } static int unload_module(void) { - int res = bbs_unregister_event_consumer(event_cb); - bbs_cli_unregister_multiple(cli_commands_events); - RWLIST_WRLOCK_REMOVE_ALL(&ipblocks, entry, free); - stringlist_empty_destroy(&ip_whitelist); - return res; + return bbs_unregister_event_consumer(event_cb); } -BBS_MODULE_INFO_STANDARD("Core Event Handlers"); +BBS_MODULE_INFO_STANDARD("Node Callbacks"); diff --git a/tests/test_menus.c b/tests/test_menus.c index e8fb5095..345451ad 100644 --- a/tests/test_menus.c +++ b/tests/test_menus.c @@ -27,8 +27,8 @@ static int pre(void) { - test_load_module("mod_handlers.so"); - test_load_module("mod_events.so"); + test_load_module("mod_menu_handlers.so"); + test_load_module("mod_node_callbacks.so"); test_load_module("net_telnet.so"); /* no net_telnet.conf needed, defaults are sufficient */ diff --git a/tests/test_terminals.c b/tests/test_terminals.c index e55497ec..f06cb975 100755 --- a/tests/test_terminals.c +++ b/tests/test_terminals.c @@ -28,8 +28,8 @@ static int pre(void) { - test_load_module("mod_events.so"); /* To consume EVENT_NODE_INTERACTIVE_START, prompt for ENTER */ - test_load_module("mod_handlers.so"); + test_load_module("mod_node_callbacks.so"); /* To consume EVENT_NODE_INTERACTIVE_START, prompt for ENTER */ + test_load_module("mod_menu_handlers.so"); test_load_module("net_rlogin.so"); test_load_module("net_telnet.so");