]> mj.ucw.cz Git - bouncer.git/blobdiff - bouncer.c
make release: Reorganization of directory structure
[bouncer.git] / bouncer.c
index 25e3e30d0a2c3f38062fe134d93ff3ae07db68f4..08f65716f499ee17422b985bd8a792611284601f 100644 (file)
--- a/bouncer.c
+++ b/bouncer.c
@@ -2,15 +2,9 @@
  *     Bouncer -- A Daemon for Turning Away Mischievous Guests
  *
  *     (c) 2016 Martin Mares <mj@ucw.cz>
- *
- *     FIXME: ipset create bouncer4 hash:ip family inet
- *     FIXME: ipset create bouncer6 hash:ip family inet6
- *     FIXME: sshd_config: UseDNS no
- *     FIXME: PAM module names should be made configurable
- *     FIXME: Parse "N more failures" messages
  */
 
-#define LOCAL_DEBUG
+#undef LOCAL_DEBUG
 
 #include <ucw/lib.h>
 #include <ucw/clists.h>
@@ -83,7 +77,7 @@ static bool addr_parse(struct addr *addr, const char *src)
     }
   else if (inet_pton(AF_INET6, src, &a6))
     {
-      memcpy(&addr, &a6, 16);
+      memcpy(addr, &a6, 16);
       return 1;
     }
   else
@@ -94,25 +88,31 @@ static bool addr_parse(struct addr *addr, const char *src)
 
 static char *listen_on = "/var/run/bouncer.sock";
 static uns max_failures = ~0U;
-static uns max_suspect_time = 86400;
+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 max_suspects = ~0U;
-static uns max_banned = ~0U;
+static uns probation_time = 86400;
+static uns max_culprits = ~0U;
+static char *config_log_stream;
 static char *ipv4_set;
 static char *ipv6_set;
-static char *config_log_stream;
 
 static struct cf_section bouncer_cf = {
   CF_ITEMS {
     CF_STRING("ListenOn", &listen_on),
-    CF_UNS("MaxSuspects", &max_suspects),
-    CF_UNS("MaxBanned", &max_banned),
-    CF_UNS("MaxSuspectTime", &max_suspect_time),
-    CF_UNS("MaxBannedTime", &max_banned_time),
     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_STRING("LogStream", &config_log_stream),
     CF_END
   }
 };
@@ -220,134 +220,144 @@ static bool is_modify(bool add, struct addr addr)
 
 /*** Handling of login failures ***/
 
-struct culprit_node {
-  cnode n;                     // In either suspect_list or banned_list
+enum culprit_state {
+  STATE_INVALID,
+  STATE_SUSPECT,
+  STATE_BANNED,
+  STATE_PROBATION,
+};
+
+static const char * const culprit_states[] = {
+  "invalid",
+  "suspect",
+  "banned",
+  "probation",
+};
+
+struct culprit {
   union {
     struct addr addr;
     byte addr_bytes[16];
   };
+  enum culprit_state state;
   uns fail_count;
-  bool banned;
-  timestamp_t last_fail;       // Not updated when 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 clist suspect_list, banned_list;
-static uns num_suspects, num_banned;
-static struct main_timer cleanup_timer;
+static void culprit_delete(struct culprit *c)
+{
+  timer_del(&c->timer);
+  culprit_hash_remove(c);
+  num_culprits--;
+}
+
+static void culprit_set_state(struct culprit *c, enum culprit_state state, uns timeout)
+{
+  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);
+}
 
-static void cleanup_list(clist *list, uns *counter, timestamp_t max_time, uns max_count, timestamp_t *next)
+static void handle_failed_login(struct addr addr, int cnt)
 {
   timestamp_t now = main_get_now();
 
-  for (;;)
-    {
-      struct culprit_node *c = clist_head(list);
-      if (!c)
-       break;
+  struct culprit *c = culprit_hash_lookup((byte *) &addr);
 
-      timestamp_t expire_in = c->last_fail + max_time - now;
-      if (*counter > max_count)
+  if (num_culprits > max_culprits)
+    {
+      // This can happen only when this lookup created a new culprit.
+      static timestamp_t last_overflow_warning;
+      if (last_overflow_warning + 60000 < now)
        {
-         static timestamp_t last_overflow_warning;
-         if (last_overflow_warning + 60000 < now)
-           {
-             last_overflow_warning = now;
-             if (c->banned)
-               msg(L_WARN, "Too many bans, dropping some. Try increasing MaxBanned.");
-             else
-               msg(L_WARN, "Too many suspects, dropping some. Try increasing MaxSuspects.");
-           }
-         expire_in = 0;
+         last_overflow_warning = now;
+         msg(L_WARN, "Too many culprits, dropping some. Try increasing MaxCulprits.");
+         culprit_delete(c);
+         return;
        }
+    }
 
-      if (expire_in > now)
+  switch (c->state)
+    {
+    case STATE_SUSPECT:
+      c->fail_count += cnt;
+      if (c->fail_count > max_failures)
        {
-         *next = MIN(*next, expire_in);
-         break;
+         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);
        }
-
-      if (c->banned)
+      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)
        {
-         msg(L_INFO, "Unbanning %s", AFMT(c->addr));
-         is_modify(0, c->addr);
+         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
-       msg(L_DEBUG, "Suspect %s: acquitted", AFMT(c->addr));
-
-      clist_remove(&c->n);
-      culprit_remove(c);
-      (*counter)--;
+       culprit_set_state(c, STATE_PROBATION, probation_time);
+      break;
+    default:
+      ASSERT(0);
     }
 }
 
-static void culprit_cleanup(void)
+static void culprit_timer(struct main_timer *tm)
 {
-  timestamp_t next_cleanup = main_get_now() + (timestamp_t)3600 * 1000;
-  cleanup_list(&suspect_list, &num_suspects, (timestamp_t)max_suspect_time * 1000, max_suspects, &next_cleanup);
-  cleanup_list(&banned_list, &num_banned, (timestamp_t)max_banned_time * 1000, max_banned, &next_cleanup);
-  timer_add(&cleanup_timer, next_cleanup);
-}
-
-static void handle_failed_login(struct addr addr)
-{
-  int is_new;
-  timestamp_t now = main_get_now();
+  struct culprit *c = tm->data;
 
-  struct culprit_node *c = culprit_lookup((byte *) &addr, &is_new);
-  if (is_new)
+  switch (c->state)
     {
-      c->last_fail = now;
-      c->fail_count = 1;
-      c->banned = 0;
-      clist_add_tail(&suspect_list, &c->n);
-      num_suspects++;
-      msg(L_DEBUG, "Suspect %s: new", AFMT(addr));
+    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);
     }
-  else if (!c->banned)
-    {
-      c->last_fail = now;
-      c->fail_count++;
-      clist_remove(&c->n);
-      clist_add_tail(&suspect_list, &c->n);
-      msg(L_DEBUG, "Suspect %s: failures=%u", AFMT(addr), c->fail_count);
-    }
-
-  if (!c->banned && c->fail_count >= max_failures)
-    {
-      msg(L_INFO, "Banning %s: failures=%u", AFMT(addr), c->fail_count);
-      c->banned = 1;
-      clist_remove(&c->n);
-      num_suspects--;
-      clist_add_tail(&banned_list, &c->n);
-      num_banned++;
-      is_modify(1, c->addr);
-    }
-
-  culprit_cleanup();
-}
-
-static void culprit_timer(struct main_timer *tm UNUSED)
-{
-  culprit_cleanup();
-}
-
-static void fail_init(void)
-{
-  culprit_init();
-  clist_init(&suspect_list);
-  clist_init(&banned_list);
-  cleanup_timer.handler = culprit_timer;
 }
 
 /*** Parsing of log messages ***/
@@ -364,50 +374,9 @@ static bool check_next(char **pp, char *want)
   return 1;
 }
 
-static void process_msg(char *line)
+static void parse_failure(char *p, int cnt)
 {
-  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
-
-  // 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
-  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);
+  DBG("Parse 4: <%s> cnt=%d", p, cnt);
 
   // Decode attributes
   bool done = 0;
@@ -439,14 +408,80 @@ static void process_msg(char *line)
        rhost = val;
     }
 
+  if (!rhost || !rhost[0])
+    return;
+
   // Act on the message
   struct addr addr;
   if (addr_parse(&addr, rhost))
-    handle_failed_login(addr);
+    handle_failed_login(addr, cnt);
   else
     msg(L_WARN, "Unable to parse address %s", rhost);
 }
 
+static void process_msg(char *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++)
+    {
+      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
+  if (check_next(&p, "pam_unix("))
+    {
+      do
+       {
+         c = *p++;
+         if (!c || c == ' ')
+           return;
+       }
+      while (c != ')');
+      if (!check_next(&p, ": "))
+       return;
+      DBG("Parse 3: <%s>", p);
+
+      if (!check_next(&p, "authentication failure; "))
+       return;
+
+      parse_failure(p, 1);
+    }
+
+  // "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 (!check_next(&p, " more authentication failures; "))
+       return;
+
+      parse_failure(p, cnt);
+    }
+}
+
 /*** Socket for receiving messages from rsyslog ***/
 
 struct main_file sk_file;
@@ -514,7 +549,7 @@ static struct opt_section options = {
 
 int main(int argc UNUSED, char **argv)
 {
-  cf_def_file = "config";      // FIXME
+  cf_def_file = "/etc/bouncer";
   cf_declare_section("Bouncer", &bouncer_cf, 0);
   opt_parse(&options, argv+1);
 
@@ -523,7 +558,7 @@ int main(int argc UNUSED, char **argv)
 
   main_init();
   is_init();
-  fail_init();
+  culprit_hash_init();
   sk_init();
 
   is_flush(ipv4_set);