diff options
Diffstat (limited to 'srclimit.c')
-rw-r--r-- | srclimit.c | 396 |
1 files changed, 372 insertions, 24 deletions
@@ -1,5 +1,6 @@ /* * Copyright (c) 2020 Darren Tucker <dtucker@openbsd.org> + * Copyright (c) 2024 Damien Miller <djm@mindrot.org> * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -18,11 +19,13 @@ #include <sys/socket.h> #include <sys/types.h> +#include <openbsd-compat/sys-tree.h> #include <limits.h> #include <netdb.h> #include <stdio.h> #include <string.h> +#include <stdlib.h> #include "addr.h" #include "canohost.h" @@ -30,17 +33,71 @@ #include "misc.h" #include "srclimit.h" #include "xmalloc.h" +#include "servconf.h" +#include "match.h" static int max_children, max_persource, ipv4_masklen, ipv6_masklen; +static struct per_source_penalty penalty_cfg; +static char *penalty_exempt; /* Per connection state, used to enforce unauthenticated connection limit. */ static struct child_info { int id; struct xaddr addr; -} *child; +} *children; + +/* + * Penalised addresses, active entries here prohibit connections until expired. + * Entries become active when more than penalty_min seconds of penalty are + * outstanding. + */ +struct penalty { + struct xaddr addr; + time_t expiry; + int active; + const char *reason; + RB_ENTRY(penalty) by_addr; + RB_ENTRY(penalty) by_expiry; +}; +static int penalty_addr_cmp(struct penalty *a, struct penalty *b); +static int penalty_expiry_cmp(struct penalty *a, struct penalty *b); +RB_HEAD(penalties_by_addr, penalty) penalties_by_addr4, penalties_by_addr6; +RB_HEAD(penalties_by_expiry, penalty) penalties_by_expiry4, penalties_by_expiry6; +RB_GENERATE_STATIC(penalties_by_addr, penalty, by_addr, penalty_addr_cmp) +RB_GENERATE_STATIC(penalties_by_expiry, penalty, by_expiry, penalty_expiry_cmp) +static size_t npenalties4, npenalties6; + +static int +srclimit_mask_addr(const struct xaddr *addr, int bits, struct xaddr *masked) +{ + struct xaddr xmask; + + /* Mask address off address to desired size. */ + if (addr_netmask(addr->af, bits, &xmask) != 0 || + addr_and(masked, addr, &xmask) != 0) { + debug3_f("%s: invalid mask %d bits", __func__, bits); + return -1; + } + return 0; +} + +static int +srclimit_peer_addr(int sock, struct xaddr *addr) +{ + struct sockaddr_storage storage; + socklen_t addrlen = sizeof(storage); + struct sockaddr *sa = (struct sockaddr *)&storage; + + if (getpeername(sock, sa, &addrlen) != 0) + return 1; /* not remote socket? */ + if (addr_sa_to_xaddr(sa, addrlen, addr) != 0) + return 1; /* unknown address family? */ + return 0; +} void -srclimit_init(int max, int persource, int ipv4len, int ipv6len) +srclimit_init(int max, int persource, int ipv4len, int ipv6len, + struct per_source_penalty *penalty_conf, const char *penalty_exempt_conf) { int i; @@ -48,25 +105,31 @@ srclimit_init(int max, int persource, int ipv4len, int ipv6len) ipv4_masklen = ipv4len; ipv6_masklen = ipv6len; max_persource = persource; + penalty_cfg = *penalty_conf; + if (penalty_cfg.max_sources4 < 0 || penalty_cfg.max_sources6 < 0) + fatal_f("invalid max_sources"); /* shouldn't happen */ + penalty_exempt = penalty_exempt_conf == NULL ? + NULL : xstrdup(penalty_exempt_conf); + RB_INIT(&penalties_by_addr4); + RB_INIT(&penalties_by_expiry4); + RB_INIT(&penalties_by_addr6); + RB_INIT(&penalties_by_expiry6); if (max_persource == INT_MAX) /* no limit */ return; debug("%s: max connections %d, per source %d, masks %d,%d", __func__, max, persource, ipv4len, ipv6len); if (max <= 0) fatal("%s: invalid number of sockets: %d", __func__, max); - child = xcalloc(max_children, sizeof(*child)); + children = xcalloc(max_children, sizeof(*children)); for (i = 0; i < max_children; i++) - child[i].id = -1; + children[i].id = -1; } /* returns 1 if connection allowed, 0 if not allowed. */ int srclimit_check_allow(int sock, int id) { - struct xaddr xa, xb, xmask; - struct sockaddr_storage addr; - socklen_t addrlen = sizeof(addr); - struct sockaddr *sa = (struct sockaddr *)&addr; + struct xaddr xa, xb; int i, bits, first_unused, count = 0; char xas[NI_MAXHOST]; @@ -74,26 +137,19 @@ srclimit_check_allow(int sock, int id) return 1; debug("%s: sock %d id %d limit %d", __func__, sock, id, max_persource); - if (getpeername(sock, sa, &addrlen) != 0) - return 1; /* not remote socket? */ - if (addr_sa_to_xaddr(sa, addrlen, &xa) != 0) - return 1; /* unknown address family? */ - - /* Mask address off address to desired size. */ + if (srclimit_peer_addr(sock, &xa) != 0) + return 1; bits = xa.af == AF_INET ? ipv4_masklen : ipv6_masklen; - if (addr_netmask(xa.af, bits, &xmask) != 0 || - addr_and(&xb, &xa, &xmask) != 0) { - debug3("%s: invalid mask %d bits", __func__, bits); + if (srclimit_mask_addr(&xa, bits, &xb) != 0) return 1; - } first_unused = max_children; /* Count matching entries and find first unused one. */ for (i = 0; i < max_children; i++) { - if (child[i].id == -1) { + if (children[i].id == -1) { if (i < first_unused) first_unused = i; - } else if (addr_cmp(&child[i].addr, &xb) == 0) { + } else if (addr_cmp(&children[i].addr, &xb) == 0) { count++; } } @@ -116,8 +172,8 @@ srclimit_check_allow(int sock, int id) return 0; /* Connection allowed, store masked address. */ - child[first_unused].id = id; - memcpy(&child[first_unused].addr, &xb, sizeof(xb)); + children[first_unused].id = id; + memcpy(&children[first_unused].addr, &xb, sizeof(xb)); return 1; } @@ -132,9 +188,301 @@ srclimit_done(int id) debug("%s: id %d", __func__, id); /* Clear corresponding state entry. */ for (i = 0; i < max_children; i++) { - if (child[i].id == id) { - child[i].id = -1; + if (children[i].id == id) { + children[i].id = -1; + return; + } + } +} + +static int +penalty_addr_cmp(struct penalty *a, struct penalty *b) +{ + return addr_cmp(&a->addr, &b->addr); + /* Addresses must be unique in by_addr, so no need to tiebreak */ +} + +static int +penalty_expiry_cmp(struct penalty *a, struct penalty *b) +{ + if (a->expiry != b->expiry) + return a->expiry < b->expiry ? -1 : 1; + /* Tiebreak on addresses */ + return addr_cmp(&a->addr, &b->addr); +} + +static void +expire_penalties_from_tree(time_t now, const char *t, + struct penalties_by_expiry *by_expiry, + struct penalties_by_addr *by_addr, size_t *npenaltiesp) +{ + struct penalty *penalty, *tmp; + + /* XXX avoid full scan of tree, e.g. min-heap */ + RB_FOREACH_SAFE(penalty, penalties_by_expiry, by_expiry, tmp) { + if (penalty->expiry >= now) + break; + if (RB_REMOVE(penalties_by_expiry, by_expiry, + penalty) != penalty || + RB_REMOVE(penalties_by_addr, by_addr, + penalty) != penalty) + fatal_f("internal error: %s penalty table corrupt", t); + free(penalty); + if ((*npenaltiesp)-- == 0) + fatal_f("internal error: %s npenalties underflow", t); + } +} + +static void +expire_penalties(time_t now) +{ + expire_penalties_from_tree(now, "ipv4", + &penalties_by_expiry4, &penalties_by_addr4, &npenalties4); + expire_penalties_from_tree(now, "ipv6", + &penalties_by_expiry6, &penalties_by_addr6, &npenalties6); +} + +static void +addr_masklen_ntop(struct xaddr *addr, int masklen, char *s, size_t slen) +{ + size_t o; + + if (addr_ntop(addr, s, slen) != 0) { + strlcpy(s, "UNKNOWN", slen); + return; + } + if ((o = strlen(s)) < slen) + snprintf(s + o, slen - o, "/%d", masklen); +} + +int +srclimit_penalty_check_allow(int sock, const char **reason) +{ + struct xaddr addr; + struct penalty find, *penalty; + time_t now; + int bits, max_sources, overflow_mode; + char addr_s[NI_MAXHOST]; + struct penalties_by_addr *by_addr; + size_t npenalties; + + if (!penalty_cfg.enabled) + return 1; + if (srclimit_peer_addr(sock, &addr) != 0) + return 1; + if (penalty_exempt != NULL) { + if (addr_ntop(&addr, addr_s, sizeof(addr_s)) != 0) + return 1; /* shouldn't happen */ + if (addr_match_list(addr_s, penalty_exempt) == 1) { + return 1; + } + } + now = monotime(); + expire_penalties(now); + by_addr = addr.af == AF_INET ? + &penalties_by_addr4 : &penalties_by_addr6; + max_sources = addr.af == AF_INET ? + penalty_cfg.max_sources4 : penalty_cfg.max_sources6; + overflow_mode = addr.af == AF_INET ? + penalty_cfg.overflow_mode : penalty_cfg.overflow_mode6; + npenalties = addr.af == AF_INET ? npenalties4 : npenalties6; + if (npenalties >= (size_t)max_sources && + overflow_mode == PER_SOURCE_PENALTY_OVERFLOW_DENY_ALL) { + *reason = "too many penalised addresses"; + return 0; + } + bits = addr.af == AF_INET ? ipv4_masklen : ipv6_masklen; + memset(&find, 0, sizeof(find)); + if (srclimit_mask_addr(&addr, bits, &find.addr) != 0) + return 1; + if ((penalty = RB_FIND(penalties_by_addr, by_addr, &find)) == NULL) + return 1; /* no penalty */ + if (penalty->expiry < now) { + expire_penalties(now); + return 1; /* expired penalty */ + } + if (!penalty->active) + return 1; /* Penalty hasn't hit activation threshold yet */ + *reason = penalty->reason; + return 0; +} + +static void +srclimit_early_expire_penalties_from_tree(const char *t, + struct penalties_by_expiry *by_expiry, + struct penalties_by_addr *by_addr, size_t *npenaltiesp, size_t max_sources) +{ + struct penalty *p = NULL; + int bits; + char s[NI_MAXHOST + 4]; + + /* Delete the soonest-to-expire penalties. */ + while (*npenaltiesp > max_sources) { + if ((p = RB_MIN(penalties_by_expiry, by_expiry)) == NULL) + fatal_f("internal error: %s table corrupt (find)", t); + bits = p->addr.af == AF_INET ? ipv4_masklen : ipv6_masklen; + addr_masklen_ntop(&p->addr, bits, s, sizeof(s)); + debug3_f("%s overflow, remove %s", t, s); + if (RB_REMOVE(penalties_by_expiry, by_expiry, p) != p || + RB_REMOVE(penalties_by_addr, by_addr, p) != p) + fatal_f("internal error: %s table corrupt (remove)", t); + free(p); + (*npenaltiesp)--; + } +} + +static void +srclimit_early_expire_penalties(void) +{ + srclimit_early_expire_penalties_from_tree("ipv4", + &penalties_by_expiry4, &penalties_by_addr4, &npenalties4, + (size_t)penalty_cfg.max_sources4); + srclimit_early_expire_penalties_from_tree("ipv6", + &penalties_by_expiry6, &penalties_by_addr6, &npenalties6, + (size_t)penalty_cfg.max_sources6); +} + +void +srclimit_penalise(struct xaddr *addr, int penalty_type) +{ + struct xaddr masked; + struct penalty *penalty = NULL, *existing = NULL; + time_t now; + int bits, penalty_secs, max_sources = 0, overflow_mode; + char addrnetmask[NI_MAXHOST + 4]; + const char *reason = NULL, *t; + size_t *npenaltiesp = NULL; + struct penalties_by_addr *by_addr = NULL; + struct penalties_by_expiry *by_expiry = NULL; + + if (!penalty_cfg.enabled) + return; + if (penalty_exempt != NULL) { + if (addr_ntop(addr, addrnetmask, sizeof(addrnetmask)) != 0) + return; /* shouldn't happen */ + if (addr_match_list(addrnetmask, penalty_exempt) == 1) { + debug3_f("address %s is exempt", addrnetmask); return; } } + + switch (penalty_type) { + case SRCLIMIT_PENALTY_NONE: + return; + case SRCLIMIT_PENALTY_CRASH: + penalty_secs = penalty_cfg.penalty_crash; + reason = "penalty: caused crash"; + break; + case SRCLIMIT_PENALTY_AUTHFAIL: + penalty_secs = penalty_cfg.penalty_authfail; + reason = "penalty: failed authentication"; + break; + case SRCLIMIT_PENALTY_NOAUTH: + penalty_secs = penalty_cfg.penalty_noauth; + reason = "penalty: connections without attempting authentication"; + break; + case SRCLIMIT_PENALTY_GRACE_EXCEEDED: + penalty_secs = penalty_cfg.penalty_crash; + reason = "penalty: exceeded LoginGraceTime"; + break; + default: + fatal_f("internal error: unknown penalty %d", penalty_type); + } + bits = addr->af == AF_INET ? ipv4_masklen : ipv6_masklen; + if (srclimit_mask_addr(addr, bits, &masked) != 0) + return; + addr_masklen_ntop(addr, bits, addrnetmask, sizeof(addrnetmask)); + + now = monotime(); + expire_penalties(now); + by_expiry = addr->af == AF_INET ? + &penalties_by_expiry4 : &penalties_by_expiry6; + by_addr = addr->af == AF_INET ? + &penalties_by_addr4 : &penalties_by_addr6; + max_sources = addr->af == AF_INET ? + penalty_cfg.max_sources4 : penalty_cfg.max_sources6; + overflow_mode = addr->af == AF_INET ? + penalty_cfg.overflow_mode : penalty_cfg.overflow_mode6; + npenaltiesp = addr->af == AF_INET ? &npenalties4 : &npenalties6; + t = addr->af == AF_INET ? "ipv4" : "ipv6"; + if (*npenaltiesp >= (size_t)max_sources && + overflow_mode == PER_SOURCE_PENALTY_OVERFLOW_DENY_ALL) { + verbose_f("%s penalty table full, cannot penalise %s for %s", t, + addrnetmask, reason); + return; + } + + penalty = xcalloc(1, sizeof(*penalty)); + penalty->addr = masked; + penalty->expiry = now + penalty_secs; + penalty->reason = reason; + if ((existing = RB_INSERT(penalties_by_addr, by_addr, + penalty)) == NULL) { + /* penalty didn't previously exist */ + if (penalty_secs > penalty_cfg.penalty_min) + penalty->active = 1; + if (RB_INSERT(penalties_by_expiry, by_expiry, penalty) != NULL) + fatal_f("internal error: %s penalty tables corrupt", t); + verbose_f("%s: new %s %s penalty of %d seconds for %s", t, + addrnetmask, penalty->active ? "active" : "deferred", + penalty_secs, reason); + if (++(*npenaltiesp) > (size_t)max_sources) + srclimit_early_expire_penalties(); /* permissive */ + return; + } + debug_f("%s penalty for %s %s already exists, %lld seconds remaining", + existing->active ? "active" : "inactive", t, + addrnetmask, (long long)(existing->expiry - now)); + /* Expiry information is about to change, remove from tree */ + if (RB_REMOVE(penalties_by_expiry, by_expiry, existing) != existing) + fatal_f("internal error: %s penalty table corrupt (remove)", t); + /* An entry already existed. Accumulate penalty up to maximum */ + existing->expiry += penalty_secs; + if (existing->expiry - now > penalty_cfg.penalty_max) + existing->expiry = now + penalty_cfg.penalty_max; + if (existing->expiry - now > penalty_cfg.penalty_min && + !existing->active) { + verbose_f("%s: activating %s penalty of %lld seconds for %s", + addrnetmask, t, (long long)(existing->expiry - now), + reason); + existing->active = 1; + } + existing->reason = penalty->reason; + free(penalty); + penalty = NULL; + /* Re-insert into expiry tree */ + if (RB_INSERT(penalties_by_expiry, by_expiry, existing) != NULL) + fatal_f("internal error: %s penalty table corrupt (insert)", t); +} + +static void +srclimit_penalty_info_for_tree(const char *t, + struct penalties_by_expiry *by_expiry, size_t npenalties) +{ + struct penalty *p = NULL; + int bits; + char s[NI_MAXHOST + 4]; + time_t now; + + now = monotime(); + logit("%zu active %s penalties", npenalties, t); + RB_FOREACH(p, penalties_by_expiry, by_expiry) { + bits = p->addr.af == AF_INET ? ipv4_masklen : ipv6_masklen; + addr_masklen_ntop(&p->addr, bits, s, sizeof(s)); + if (p->expiry < now) + logit("client %s %s (expired)", s, p->reason); + else { + logit("client %s %s (%llu secs left)", s, p->reason, + (long long)(p->expiry - now)); + } + } +} + +void +srclimit_penalty_info(void) +{ + srclimit_penalty_info_for_tree("ipv4", + &penalties_by_expiry4, npenalties4); + srclimit_penalty_info_for_tree("ipv6", + &penalties_by_expiry6, npenalties6); } |