]> mj.ucw.cz Git - bouncer.git/blobdiff - bouncer.c
make release: Reorganization of directory structure
[bouncer.git] / bouncer.c
index cedf14c65549a77b64d4badf10f2623dc318054c..08f65716f499ee17422b985bd8a792611284601f 100644 (file)
--- a/bouncer.c
+++ b/bouncer.c
@@ -2,22 +2,25 @@
  *     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
+#undef LOCAL_DEBUG
 
 #include <ucw/lib.h>
 #include <ucw/clists.h>
-#include <ucw/stkstring.h>
+#include <ucw/conf.h>
+#include <ucw/log.h>
+#include <ucw/mainloop.h>
+#include <ucw/opt.h>
 #include <ucw/string.h>
 
+#include <alloca.h>
 #include <arpa/inet.h>
+#include <errno.h>
 #include <string.h>
+#include <sys/types.h>
 #include <sys/socket.h>
+#include <sys/stat.h>
 #include <sys/un.h>
 #include <time.h>
 #include <unistd.h>
 #include <libipset/session.h>
 #include <libipset/types.h>
 
-/*** IP sets ***/
-
-static struct ipset_session *is_sess;
+/*** Internal representation of IPv4/IPv6 addresses ***/
 
-enum is_index {
-  IS_IPV4,
-  IS_IPV6,
+// In network byte order, IPv4 represented as ::ffff:1.2.3.4
+struct addr {
+  u32 a[4];
 };
 
-static const char * const is_names[] = {
-  "bouncer4",
-  "bouncer6",
+#define ADDR_BUFSIZE 64
+#define AFMT(_a) ({ char *_buf = alloca(ADDR_BUFSIZE); addr_format(_buf, _a); _buf; })
+
+static bool addr_is_v4(struct addr addr)
+{
+  return !addr.a[0] && !addr.a[1] && addr.a[2] == htonl(0xffff);
+}
+
+static void addr_format(char *buf, struct addr addr)
+{
+  const char *ok;
+  if (addr_is_v4(addr))
+    {
+      struct in_addr in4;
+      in4.s_addr = addr.a[3];
+      ok = inet_ntop(AF_INET, &in4, buf, ADDR_BUFSIZE);
+    }
+  else
+    {
+      struct in6_addr in6;
+      memcpy(&in6, &addr, sizeof(addr));
+      ok = inet_ntop(AF_INET6, &in6, buf, ADDR_BUFSIZE);
+    }
+  if (!ok)
+    snprintf(buf, ADDR_BUFSIZE, "<error %d>", errno);
+}
+
+static bool addr_parse(struct addr *addr, const char *src)
+{
+  struct in_addr a4;
+  struct in6_addr a6;
+
+  if (inet_pton(AF_INET, src, &a4))
+    {
+      addr->a[0] = addr->a[1] = 0;
+      addr->a[2] = htonl(0xffff);
+      addr->a[3] = a4.s_addr;
+      return 1;
+    }
+  else if (inet_pton(AF_INET6, src, &a6))
+    {
+      memcpy(addr, &a6, 16);
+      return 1;
+    }
+  else
+    return 0;
+}
+
+/*** Configuration ***/
+
+static char *listen_on = "/var/run/bouncer.sock";
+static uns max_failures = ~0U;
+static uns suspect_time = 86400;
+static uns banned_time = 86400;
+static uns max_probation = ~0U;
+static double banned_again_coeff = 2;
+static uns max_banned_time = 86400;
+static uns probation_time = 86400;
+static uns max_culprits = ~0U;
+static char *config_log_stream;
+static char *ipv4_set;
+static char *ipv6_set;
+
+static struct cf_section bouncer_cf = {
+  CF_ITEMS {
+    CF_STRING("ListenOn", &listen_on),
+    CF_UNS("MaxFailures", &max_failures),
+    CF_UNS("SuspectTime", &suspect_time),
+    CF_UNS("BannedTime", &banned_time),
+    CF_UNS("MaxProbation", &max_probation),
+    CF_DOUBLE("BannedAgainCoeff", &banned_again_coeff),
+    CF_UNS("MaxBannedTime", &max_banned_time),
+    CF_UNS("ProbationTime", &probation_time),
+    CF_UNS("MaxCulprits", &max_culprits),
+    CF_STRING("LogStream", &config_log_stream),
+    CF_STRING("IPv4Set", &ipv4_set),
+    CF_STRING("IPv6Set", &ipv6_set),
+    CF_END
+  }
 };
 
+/*** An interface to IP sets ***/
+
+static struct ipset_session *is_sess;
+
 static const char *trim_eol(const char *msg)
 {
   int len = strlen(msg);
@@ -84,140 +165,199 @@ static void is_init(void)
     die("Unable to initialize ipset session");
 }
 
-static void is_setup(int set)
+static bool is_setup(char *set)
 {
-  if (ipset_parse_setname(is_sess, IPSET_SETNAME, is_names[set]) < 0)
+  if (!set)
+    return 0;
+
+  if (ipset_parse_setname(is_sess, IPSET_SETNAME, set) < 0)
     is_die("ipset_parse_setname");
+  return 1;
 }
 
-static void is_flush(int set)
+static void is_flush(char *set)
 {
-  is_setup(set);
+  if (!is_setup(set))
+    return;
 
   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)
+static bool is_modify(bool add, struct addr addr)
 {
-  is_setup(set);
+  int cmd = add ? IPSET_CMD_ADD : IPSET_CMD_DEL;
+  char *set = addr_is_v4(addr) ? ipv4_set : ipv6_set;
+  if (!is_setup(set))
+    return 0;
 
   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);
+  const struct ipset_type *is_type = ipset_type_get(is_sess, cmd);
   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");
+  char buf[ADDR_BUFSIZE];
+  addr_format(buf, addr);
+  if (ipset_parse_elem(is_sess, 0, buf) < 0)
+    {
+      is_err("ipset_parse_elem");
+      return 0;
+    }
+
+  if (ipset_cmd(is_sess, cmd, 0) < 0)
+    {
+      is_err(add ? "IPSET_CMD_ADD" : "IPSET_CMD_DEL");
+      return 0;
+    }
 
-  if (ipset_cmd(is_sess, IPSET_CMD_ADD, 0) < 0)
-    return is_err("IPSET_CMD_ADD");
+  return 1;
 }
 
 /*** Handling of login failures ***/
 
-struct addr {
-  u32 a[4];
+enum culprit_state {
+  STATE_INVALID,
+  STATE_SUSPECT,
+  STATE_BANNED,
+  STATE_PROBATION,
 };
 
-// FIXME: call ntohl
-#define AFMT(_a) stk_printf("%08x:%08x:%08x:%08x", _a.a[0], _a.a[1], _a.a[2], _a.a[3])
+static const char * const culprit_states[] = {
+  "invalid",
+  "suspect",
+  "banned",
+  "probation",
+};
 
-struct culprit_node {
-  cnode n;
+struct culprit {
   union {
     struct addr addr;
     byte addr_bytes[16];
   };
-  time_t first_fail;
+  enum culprit_state state;
   uns fail_count;
-  bool banned;
+  uns sentence_count;
+  uns ban_time;
+  struct main_timer timer;
 };
 
-#define HASH_NODE struct culprit_node
-#define HASH_PREFIX(x) culprit_##x
+static uns num_culprits;
+static void culprit_timer(struct main_timer *tm);
+
+static void culprit_hash_init_data(struct culprit *c)
+{
+  c->state = STATE_SUSPECT;
+  c->fail_count = 0;
+  bzero(&c->timer, sizeof(c->timer));
+  c->timer.handler = culprit_timer;
+  c->timer.data = c;
+  num_culprits++;
+}
+
+#define HASH_NODE struct culprit
+#define HASH_PREFIX(x) culprit_hash_##x
 #define HASH_KEY_MEMORY addr_bytes
 #define HASH_KEY_SIZE 16
+#define HASH_GIVE_INIT_DATA
 #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 culprit_delete(struct culprit *c)
+{
+  timer_del(&c->timer);
+  culprit_hash_remove(c);
+  num_culprits--;
+}
 
-static void handle_failed_login(struct addr addr)
+static void culprit_set_state(struct culprit *c, enum culprit_state state, uns timeout)
 {
-  int is_new;
-  time_t now = time(NULL);
+  c->state = state;
+  timer_add_rel(&c->timer, (timestamp_t)timeout * 1000);
+  msg(L_DEBUG, "Suspect %s: state=%s failures=%u timeout=%u", AFMT(c->addr), culprit_states[state], c->fail_count, timeout);
+}
 
-  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);
-    }
+static void handle_failed_login(struct addr addr, int cnt)
+{
+  timestamp_t now = main_get_now();
 
-  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);
-    }
+  struct culprit *c = culprit_hash_lookup((byte *) &addr);
 
-  // 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))
+  if (num_culprits > max_culprits)
     {
-      DBG("%s: unbanned", AFMT(d->addr));
-      clist_remove(&d->n);
-      culprit_remove(d);
-      num_culprits--;
+      // This can happen only when this lookup created a new culprit.
+      static timestamp_t last_overflow_warning;
+      if (last_overflow_warning + 60000 < now)
+       {
+         last_overflow_warning = now;
+         msg(L_WARN, "Too many culprits, dropping some. Try increasing MaxCulprits.");
+         culprit_delete(c);
+         return;
+       }
     }
-  while ((d = clist_head(&culprit_lru)) && (now - d->first_fail >= max_idle_time || num_culprits > max_culprits))
+
+  switch (c->state)
     {
-      DBG("%s: removing from LRU", AFMT(d->addr));
-      clist_remove(&d->n);
-      culprit_remove(d);
+    case STATE_SUSPECT:
+      c->fail_count += cnt;
+      if (c->fail_count > max_failures)
+       {
+         c->ban_time = banned_time;
+         msg(L_INFO, "Banned %s: failures=%u ban_time=%u", AFMT(addr), c->fail_count, c->ban_time);
+         c->sentence_count = 1;
+         culprit_set_state(c, STATE_BANNED, c->ban_time);
+         is_modify(1, c->addr);
+       }
+      else
+       culprit_set_state(c, STATE_SUSPECT, suspect_time);
+      break;
+    case STATE_BANNED:
+      break;
+    case STATE_PROBATION:
+      c->fail_count += cnt;
+      if (c->fail_count > max_probation)
+       {
+         c->sentence_count++;
+         c->ban_time = MIN((uns)(c->ban_time * banned_again_coeff), max_banned_time);
+         msg(L_INFO, "Re-banned %s: failures=%u sentences=%u ban_time=%u", AFMT(c->addr), c->fail_count, c->sentence_count, c->ban_time);
+         culprit_set_state(c, STATE_BANNED, c->ban_time);
+         is_modify(1, c->addr);
+       }
+      else
+       culprit_set_state(c, STATE_PROBATION, probation_time);
+      break;
+    default:
+      ASSERT(0);
     }
-
-#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)
+static void culprit_timer(struct main_timer *tm)
 {
-  culprit_init();
-  clist_init(&culprit_lru);
-  clist_init(&culprit_bans);
+  struct culprit *c = tm->data;
+
+  switch (c->state)
+    {
+    case STATE_SUSPECT:
+    case STATE_PROBATION:
+      msg(L_DEBUG, "Suspect %s: acquitted", AFMT(c->addr));
+      culprit_delete(c);
+      break;
+    case STATE_BANNED:
+      msg(L_INFO, "Unbanned %s", AFMT(c->addr));
+      c->fail_count = 0;
+      culprit_set_state(c, STATE_PROBATION, probation_time);
+      is_modify(0, c->addr);
+      break;
+    default:
+      ASSERT(0);
+    }
 }
 
 /*** Parsing of log messages ***/
@@ -234,35 +374,59 @@ static bool check_next(char **pp, char *want)
   return 1;
 }
 
-static void parse_failed_login(char *rhost)
+static void parse_failure(char *p, int cnt)
 {
-  struct in_addr a4;
-  struct in6_addr a6;
-  struct addr addr;
+  DBG("Parse 4: <%s> cnt=%d", p, cnt);
 
-  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))
+  // Decode attributes
+  bool done = 0;
+  char *rhost = NULL;
+  while (!done)
     {
-      memcpy(&addr, &a6, 16);
-      handle_failed_login(addr);
+      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;
     }
+
+  if (!rhost || !rhost[0])
+    return;
+
+  // Act on the message
+  struct addr addr;
+  if (addr_parse(&addr, rhost))
+    handle_failed_login(addr, cnt);
   else
     msg(L_WARN, "Unable to parse address %s", rhost);
 }
 
 static void process_msg(char *line)
 {
-  msg(L_DEBUG, "Received <%s>", line);
+  DBG("Parse: <%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
+  // 2016-11-05T12:49:52.418880+01:00 sshd[16271]: PAM 2 more authentication failures; logname= uid=0 euid=0 tty=ssh ruser= rhost=116.31.116.26  user=root
 
   // We shall start with 32 non-spaces
   for (int i=0; i<32; i++)
@@ -282,117 +446,125 @@ static void process_msg(char *line)
     return;
   DBG("Parse 2: <%s>", p);
 
-  // pam_unix(something), colon, space (FIXME: make configurable)
-  if (!check_next(&p, "pam_unix("))
-    return;
-  do
+  // pam_unix(something), colon, space
+  if (check_next(&p, "pam_unix("))
     {
-      c = *p++;
-      if (!c || c == ' ')
+      do
+       {
+         c = *p++;
+         if (!c || c == ' ')
+           return;
+       }
+      while (c != ')');
+      if (!check_next(&p, ": "))
        return;
-    }
-  while (c != ')');
-  if (!check_next(&p, ": "))
-    return;
-  DBG("Parse 3: <%s>", p);
+      DBG("Parse 3: <%s>", p);
 
-  // "authentication failure;"
-  if (!check_next(&p, "authentication failure; "))
-    return;
-  DBG("Parse 4: <%s>", p);
+      if (!check_next(&p, "authentication failure; "))
+       return;
 
-  // Decode attributes
-  bool done = 0;
-  char *rhost = NULL;
-  while (!done)
-    {
-      while (*p == ' ')
-       p++;
-      if (!*p)
-       break;
+      parse_failure(p, 1);
+    }
 
-      char *key = p;
-      while (*p && *p != ' ' && *p != '=')
+  // "PAM <n> more authentication failures;"
+  if (check_next(&p, "PAM "))
+    {
+      if (!(*p >= '0' && *p <= '9'))
+       return;
+      int cnt = atoi(p);
+      while (*p >= '0' && *p <= '9')
        p++;
-      if (*p != '=')
-       continue;
-      *p++ = 0;
 
-      char *val = p;
-      while (*p && *p != ' ')
-       p++;
-      if (*p)
-       *p++ = 0;
-      else
-       done = 1;
+      if (!check_next(&p, " more authentication failures; "))
+       return;
 
-      DBG("Parse KV: %s=<%s>", key, val);
-      if (!strcmp(key, "rhost"))
-       rhost = val;
+      parse_failure(p, cnt);
     }
-
-  // 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;
+struct main_file sk_file;
 
-static void sk_init(void)
+static int sk_read(struct main_file *mf)
 {
-  unlink(sk_name);
+  char line[1024];
+  int len = recv(mf->fd, line, sizeof(line), MSG_TRUNC);
+  if (len < 0)
+    {
+      if (errno == EINTR || errno == EAGAIN)
+       return HOOK_IDLE;
+      die("recv: %m");
+    }
 
-  if ((sk_fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0)
-    die("Cannot create PF_UNIX socket: %m");
+  if (len >= (int) sizeof(line))
+    {
+      msg(L_WARN, "Truncated message received (length=%d)", len);
+      len = sizeof(line) - 1;
+    }
+  line[len] = 0;
 
-  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);
+  if (len > 0 && line[len-1] == '\n')
+    line[--len] = 0;
+  if (len > 0 && line[len-1] == '\r')
+    line[--len] = 0;
 
-  // FIXME: Permissions
+  process_msg(line);
+  return HOOK_RETRY;
 }
 
-static void sk_loop(void)
+static void sk_init(void)
 {
-  for (;;)
-    {
-      char line[1024];
-      int len = recv(sk_fd, line, sizeof(line), MSG_TRUNC);
-      if (len < 0)
-       die("recv: %m");
+  unlink(listen_on);
+  mode_t old_umask = umask(0077);
 
-      if (len >= (int) sizeof(line))
-       {
-         msg(L_WARN, "Truncated message received (length=%d)", len);
-         len = sizeof(line) - 1;
-       }
-      line[len] = 0;
+  int fd;
+  if ((fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0)
+    die("Cannot create PF_UNIX socket: %m");
 
-      if (len > 0 && line[len-1] == '\n')
-       line[--len] = 0;
-      if (len > 0 && line[len-1] == '\r')
-       line[--len] = 0;
+  struct sockaddr_un sa = { .sun_family = AF_UNIX };
+  strcpy(sa.sun_path, listen_on);
+  if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
+    die("Cannot bind socket %s: %m", listen_on);
 
-      process_msg(line);
-    }
+  sk_file.fd = fd;
+  sk_file.read_handler = sk_read;
+  file_add(&sk_file);
+
+  umask(old_umask);
 }
 
 /*** Main ***/
 
-int main(void)
+static struct opt_section options = {
+  OPT_ITEMS {
+    OPT_HELP("Bouncer -- A Daemon for Turning Away Mischievous Guests"),
+    OPT_HELP(""),
+    OPT_HELP("Options:"),
+    OPT_HELP_OPTION,
+    OPT_CONF_OPTIONS,
+    OPT_END
+  }
+};
+
+int main(int argc UNUSED, char **argv)
 {
-  is_init();
-  fail_init();
+  cf_def_file = "/etc/bouncer";
+  cf_declare_section("Bouncer", &bouncer_cf, 0);
+  opt_parse(&options, argv+1);
 
-  msg(L_INFO, "Clearing previous state");
-  is_flush(IS_IPV4);
-  is_flush(IS_IPV6);
+  if (config_log_stream)
+    log_configured(config_log_stream);
 
+  main_init();
+  is_init();
+  culprit_hash_init();
   sk_init();
-  sk_loop();
 
+  is_flush(ipv4_set);
+  is_flush(ipv6_set);
+
+  msg(L_INFO, "Starting");
+  main_loop();
   return 0;
 }