]> mj.ucw.cz Git - bouncer.git/commitdiff
New state logic, which uses a per-culprit timer
authorMartin Mares <mj@ucw.cz>
Sun, 6 Nov 2016 22:37:39 +0000 (23:37 +0100)
committerMartin Mares <mj@ucw.cz>
Sun, 6 Nov 2016 22:37:39 +0000 (23:37 +0100)
LibUCW timers are light enough for this and it saves a lot of
bookkeepin inside bouncer.

bouncer.c
config

index a86bafd9e22a6a66af38697ea582dc29353e35a2..03e739cde31afd8a101582dfea19b6a3021e8397 100644 (file)
--- a/bouncer.c
+++ b/bouncer.c
@@ -88,27 +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;
+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("Probation", &probation),
+    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
   }
 };
@@ -216,147 +220,143 @@ 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 cleanup_list(clist *list, uns *counter, timestamp_t max_time, uns max_count, timestamp_t *next)
+static void culprit_set_state(struct culprit *c, enum culprit_state state, uns timeout)
 {
-  timestamp_t now = main_get_now();
+  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);
+}
 
-  for (;;)
-    {
-      struct culprit_node *c = clist_head(list);
-      if (!c)
-       break;
+static void handle_failed_login(struct addr addr, int cnt)
+{
+  timestamp_t now = main_get_now();
 
-      timestamp_t expire_in = c->last_fail + max_time;
-      if (*counter > max_count)
-       {
-         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;
-       }
+  struct culprit *c = culprit_hash_lookup((byte *) &addr);
 
-      if (expire_in > now)
+  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)
        {
-         *next = MIN(*next, expire_in);
-         break;
+         last_overflow_warning = now;
+         msg(L_WARN, "Too many culprits, dropping some. Try increasing MaxCulprits.");
+         culprit_delete(c);
+         return;
        }
+    }
 
-      clist_remove(&c->n);
-      (*counter)--;
-
-      if (c->banned)
+  switch (c->state)
+    {
+    case STATE_SUSPECT:
+      c->fail_count += cnt;
+      if (c->fail_count > max_failures)
        {
-         msg(L_INFO, "Unbanning %s", AFMT(c->addr));
-         is_modify(0, c->addr);
-         if (probation)
-           {
-             c->banned = 0;
-             c->last_fail = now;
-             c->fail_count = max_failures - probation;
-             clist_add_tail(&suspect_list, &c->n);
-             num_suspects++;
-             msg(L_DEBUG, "Suspect %s: probation, failures=%u", AFMT(c->addr), c->fail_count);
-           }
-         else
-           culprit_remove(c);
+         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)
        {
-         msg(L_DEBUG, "Suspect %s: acquitted", AFMT(c->addr));
-         culprit_remove(c);
+         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);
     }
 }
 
-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(&banned_list, &num_banned, (timestamp_t)max_banned_time * 1000, max_banned, &next_cleanup);
-  cleanup_list(&suspect_list, &num_suspects, (timestamp_t)max_suspect_time * 1000, max_suspects, &next_cleanup);
-  timer_add(&cleanup_timer, next_cleanup);
-}
-
-static void handle_failed_login(struct addr addr, int cnt)
-{
-  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 = cnt;
-      c->banned = 0;
-      clist_add_tail(&suspect_list, &c->n);
-      num_suspects++;
-      msg(L_DEBUG, "Suspect %s: new, failures=%u", AFMT(addr), c->fail_count);
+    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));
+      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 += cnt;
-      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 ***/
@@ -554,7 +554,7 @@ int main(int argc UNUSED, char **argv)
 
   main_init();
   is_init();
-  fail_init();
+  culprit_hash_init();
   sk_init();
 
   is_flush(ipv4_set);
diff --git a/config b/config
index 10c72b6a6696532d6bd217382bbbb2a9ca1063df..cb7d746279e9e65765b3dded755a659076082a6b 100644 (file)
--- a/config
+++ b/config
@@ -7,22 +7,29 @@ ListenOn      /var/run/bouncer.sock
 
 # On the first login failure, we remember that an IP address is suspect
 # and start counting failures. After too much failures, the address is banned.
-MaxFailures    10
+MaxFailures    9
 
-# When a suspect address generates no more failure for this many seconds,
-# it is forgotten.
-MaxSuspectTime 600
+# When a suspect address produces no further failures within this time [sec],
+# it is acquitted and forgotten.
+SuspectTime    600
 
 # Bans are lifted after this many seconds.
-MaxBannedTime  3600
+BannedTime     3600
 
-# When a ban is lifted, the address is again considered suspect
-# and its number of failures is set to MaxFailures - Probation (0=disable).
-Probation      2
+# After a ban is lifted, the IP address undergoes further probation. If it
+# produces more failures within the probation period, it is banned again.
+MaxProbation   1
+
+# When an address is banned again during probation, its ban time is multiplied
+# by BannedAgainCoeff, but it cannot exceed MaxBannedTime [sec].
+BannedAgainCoeff       2
+MaxBannedtime          86400
+
+# Probation expires after [sec]
+ProbationTime  600
 
 # Limit on the number of suspect addresses and bans we keep in memory
-MaxSuspects    1000
-MaxBanned      1000
+MaxCulprits    1000
 
 # We log all messages to the log stream configured below
 LogStream      syslog