From a7aecc43a7f8a35ae9c9e6528284f2f421fd653a Mon Sep 17 00:00:00 2001 From: Martin Mares Date: Sun, 6 Nov 2016 23:37:39 +0100 Subject: [PATCH] New state logic, which uses a per-culprit timer LibUCW timers are light enough for this and it saves a lot of bookkeepin inside bouncer. --- bouncer.c | 236 +++++++++++++++++++++++++++--------------------------- config | 27 ++++--- 2 files changed, 135 insertions(+), 128 deletions(-) diff --git a/bouncer.c b/bouncer.c index a86bafd..03e739c 100644 --- 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 -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 10c72b6..cb7d746 100644 --- 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 -- 2.39.2