]> mj.ucw.cz Git - bouncer.git/commitdiff
First experiments
authorMartin Mares <mj@ucw.cz>
Fri, 4 Nov 2016 19:19:33 +0000 (20:19 +0100)
committerMartin Mares <mj@ucw.cz>
Fri, 4 Nov 2016 19:19:33 +0000 (20:19 +0100)
Makefile [new file with mode: 0644]
bouncer.c [new file with mode: 0644]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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 (file)
index 0000000..cedf14c
--- /dev/null
+++ b/bouncer.c
@@ -0,0 +1,398 @@
+/*
+ *     Bouncer -- A Daemon for Turning Away Mischievous Guests
+ *
+ *     (c) 2016 Martin Mares <mj@ucw.cz>
+ *
+ *     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 <ucw/lib.h>
+#include <ucw/clists.h>
+#include <ucw/stkstring.h>
+#include <ucw/string.h>
+
+#include <arpa/inet.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <libipset/data.h>
+#include <libipset/session.h>
+#include <libipset/types.h>
+
+/*** 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 <ucw/hashtable.h>
+
+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;
+}