From ddb2b3768868283490f967b6de006a19fe7bc4ce Mon Sep 17 00:00:00 2001 From: Martin Mares Date: Fri, 4 Nov 2016 20:19:33 +0100 Subject: [PATCH 1/1] First experiments --- Makefile | 13 ++ bouncer.c | 398 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 Makefile create mode 100644 bouncer.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bfd6263 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +PKG_CFLAGS := $(shell pkg-config --cflags libucw libipset) +PKG_LIBS := $(shell pkg-config --libs libucw libipset) + +CFLAGS=-O2 -Wall -W -Wno-parentheses -Wstrict-prototypes -Wmissing-prototypes -Wundef -Wredundant-decls -std=gnu99 $(PKG_CFLAGS) +LDLIBS=$(PKG_LIBS) + +all: bouncer + +bouncer: bouncer.c + +clean: + rm -f `find . -name "*~" -or -name "*.[oa]" -or -name "\#*\#" -or -name TAGS -or -name core -or -name .depend -or -name .#*` + diff --git a/bouncer.c b/bouncer.c new file mode 100644 index 0000000..cedf14c --- /dev/null +++ b/bouncer.c @@ -0,0 +1,398 @@ +/* + * Bouncer -- A Daemon for Turning Away Mischievous Guests + * + * (c) 2016 Martin Mares + * + * FIXME: ipset create bouncer4 hash:ip family inet timeout 10000 maxelem 100 forceadd + * FIXME: ipset create bouncer6 hash:ip family inet6 timeout 10000 maxelem 100 forceadd + * FIXME: sshd_config: UseDNS no + */ + +#define LOCAL_DEBUG + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/*** IP sets ***/ + +static struct ipset_session *is_sess; + +enum is_index { + IS_IPV4, + IS_IPV6, +}; + +static const char * const is_names[] = { + "bouncer4", + "bouncer6", +}; + +static const char *trim_eol(const char *msg) +{ + int len = strlen(msg); + if (!len || msg[len-1] != '\n') + return msg; + else + { + char *x = xstrdup(msg); + x[len-1] = 0; + return x; + } +} + +static void is_die(const char *when) +{ + const char *warn = ipset_session_warning(is_sess); + if (warn) + msg(L_WARN, "%s: %s", when, trim_eol(warn)); + + const char *err = ipset_session_error(is_sess); + die("%s: %s", when, err ? trim_eol(err) : "Unknown error"); +} + +static void is_err(const char *when) +{ + const char *warn = ipset_session_warning(is_sess); + if (warn) + msg(L_WARN, "%s: %s", when, trim_eol(warn)); + + const char *err = ipset_session_error(is_sess); + msg(L_ERROR, "%s: %s", when, err ? trim_eol(err) : "Unknown error"); + + ipset_session_report_reset(is_sess); +} + +static void is_init(void) +{ + ipset_load_types(); + + is_sess = ipset_session_init(printf); + if (!is_sess) + die("Unable to initialize ipset session"); +} + +static void is_setup(int set) +{ + if (ipset_parse_setname(is_sess, IPSET_SETNAME, is_names[set]) < 0) + is_die("ipset_parse_setname"); +} + +static void is_flush(int set) +{ + is_setup(set); + + if (ipset_cmd(is_sess, IPSET_CMD_FLUSH, 0) < 0) + return is_err("IPSET_CMD_FLUSH"); +} + +static void is_add(int set, const char *elt) +{ + is_setup(set); + + if (ipset_envopt_parse(is_sess, IPSET_ENV_EXIST, NULL) < 0) + is_die("IPSET_ENV_EXIST"); + + const struct ipset_type *is_type = ipset_type_get(is_sess, IPSET_CMD_ADD); + if (!is_type) + is_die("ipset_type_get"); + + if (is_type->dimension != 1) + die("Invalid ipset dimension %d", is_type->dimension); + + if (ipset_parse_elem(is_sess, 0, elt) < 0) + return is_err("ipset_parse_elem"); + + if (ipset_cmd(is_sess, IPSET_CMD_ADD, 0) < 0) + return is_err("IPSET_CMD_ADD"); +} + +/*** Handling of login failures ***/ + +struct addr { + u32 a[4]; +}; + +// FIXME: call ntohl +#define AFMT(_a) stk_printf("%08x:%08x:%08x:%08x", _a.a[0], _a.a[1], _a.a[2], _a.a[3]) + +struct culprit_node { + cnode n; + union { + struct addr addr; + byte addr_bytes[16]; + }; + time_t first_fail; + uns fail_count; + bool banned; +}; + +#define HASH_NODE struct culprit_node +#define HASH_PREFIX(x) culprit_##x +#define HASH_KEY_MEMORY addr_bytes +#define HASH_KEY_SIZE 16 +#define HASH_WANT_LOOKUP +#define HASH_WANT_REMOVE +#define HASH_USE_AUTO_ELTPOOL 1000 +#define HASH_ZERO_FILL +#define HASH_LOOKUP_DETECT_NEW +#include + +static uns num_culprits; +static uns max_culprits = 3; // FIXME +static uns max_failures = 3; // FIXME +static uns max_idle_time = 600; // FIXME +static uns max_banned_time = 600; // FIXME +static clist culprit_lru, culprit_bans; + +static void handle_failed_login(struct addr addr) +{ + int is_new; + time_t now = time(NULL); + + struct culprit_node *c = culprit_lookup((byte *) &addr, &is_new); + if (is_new) + { + c->first_fail = now; + c->fail_count = 1; + c->banned = 0; + clist_add_tail(&culprit_lru, &c->n); + num_culprits++; + // FIXME: Warn on overflow, but not too frequently + DBG("%s: first fail", AFMT(addr)); + } + else if (!c->banned) + { + c->fail_count++; + clist_remove(&c->n); + clist_add_tail(&culprit_lru, &c->n); + DBG("%s: next fail, cnt=%u", AFMT(addr), c->fail_count); + } + + if (!c->banned && c->fail_count >= max_failures) + { + DBG("%s: banned", AFMT(addr)); + c->banned = 1; + clist_remove(&c->n); + clist_add_tail(&culprit_bans, &c->n); + } + + // FIXME: This must be called from main loop, not from here + struct culprit_node *d; + while ((d = clist_head(&culprit_bans)) && (now - d->first_fail >= max_banned_time || num_culprits > max_culprits)) + { + DBG("%s: unbanned", AFMT(d->addr)); + clist_remove(&d->n); + culprit_remove(d); + num_culprits--; + } + while ((d = clist_head(&culprit_lru)) && (now - d->first_fail >= max_idle_time || num_culprits > max_culprits)) + { + DBG("%s: removing from LRU", AFMT(d->addr)); + clist_remove(&d->n); + culprit_remove(d); + } + +#if 0 // FIXME + int set = strchr(rhost, ':') ? IS_IPV6 : IS_IPV4; + + msg(L_INFO, "Banning %s", rhost); + is_add(set, rhost); +#endif +} + +static void fail_init(void) +{ + culprit_init(); + clist_init(&culprit_lru); + clist_init(&culprit_bans); +} + +/*** Parsing of log messages ***/ + +static bool check_next(char **pp, char *want) +{ + char *p = *pp; + while (*want) + { + if (*p++ != *want++) + return 0; + } + *pp = p; + return 1; +} + +static void parse_failed_login(char *rhost) +{ + struct in_addr a4; + struct in6_addr a6; + struct addr addr; + + if (inet_pton(AF_INET, rhost, &a4)) + { + addr.a[0] = addr.a[1] = 0; + addr.a[2] = 0xffff; + addr.a[3] = a4.s_addr; + handle_failed_login(addr); + } + else if (inet_pton(AF_INET6, rhost, &a6)) + { + memcpy(&addr, &a6, 16); + handle_failed_login(addr); + } + else + msg(L_WARN, "Unable to parse address %s", rhost); +} + +static void process_msg(char *line) +{ + msg(L_DEBUG, "Received <%s>", line); + + char *p = line; + int c; + // 2016-11-04T17:18:54.825821+01:00 sshd[6733]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=1.2.3.4 + + // We shall start with 32 non-spaces + for (int i=0; i<32; i++) + { + c = *p++; + if (!c || c == ' ') + return; + } + DBG("Parse 1: <%s>", p); + + // Space, something, colon, space + if (*p++ != ' ') + return; + while (*p && *p != ' ' && *p != ':') + p++; + if (!check_next(&p, ": ")) + return; + DBG("Parse 2: <%s>", p); + + // pam_unix(something), colon, space (FIXME: make configurable) + if (!check_next(&p, "pam_unix(")) + return; + do + { + c = *p++; + if (!c || c == ' ') + return; + } + while (c != ')'); + if (!check_next(&p, ": ")) + return; + DBG("Parse 3: <%s>", p); + + // "authentication failure;" + if (!check_next(&p, "authentication failure; ")) + return; + DBG("Parse 4: <%s>", p); + + // Decode attributes + bool done = 0; + char *rhost = NULL; + while (!done) + { + while (*p == ' ') + p++; + if (!*p) + break; + + char *key = p; + while (*p && *p != ' ' && *p != '=') + p++; + if (*p != '=') + continue; + *p++ = 0; + + char *val = p; + while (*p && *p != ' ') + p++; + if (*p) + *p++ = 0; + else + done = 1; + + DBG("Parse KV: %s=<%s>", key, val); + if (!strcmp(key, "rhost")) + rhost = val; + } + + // Act on the message + parse_failed_login(rhost); +} + +/*** Socket for receiving messages from rsyslog ***/ + +static const char sk_name[] = "/var/run/bouncer.sock"; +static int sk_fd; + +static void sk_init(void) +{ + unlink(sk_name); + + if ((sk_fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0) + die("Cannot create PF_UNIX socket: %m"); + + struct sockaddr_un sa = { .sun_family = AF_UNIX }; + strcpy(sa.sun_path, sk_name); + if (bind(sk_fd, (struct sockaddr *) &sa, sizeof(sa)) < 0) + die("Cannot bind socket %s: %m", sk_name); + + // FIXME: Permissions +} + +static void sk_loop(void) +{ + for (;;) + { + char line[1024]; + int len = recv(sk_fd, line, sizeof(line), MSG_TRUNC); + if (len < 0) + die("recv: %m"); + + if (len >= (int) sizeof(line)) + { + msg(L_WARN, "Truncated message received (length=%d)", len); + len = sizeof(line) - 1; + } + line[len] = 0; + + if (len > 0 && line[len-1] == '\n') + line[--len] = 0; + if (len > 0 && line[len-1] == '\r') + line[--len] = 0; + + process_msg(line); + } +} + +/*** Main ***/ + +int main(void) +{ + is_init(); + fail_init(); + + msg(L_INFO, "Clearing previous state"); + is_flush(IS_IPV4); + is_flush(IS_IPV6); + + sk_init(); + sk_loop(); + + return 0; +} -- 2.39.2