diff options
-rw-r--r-- | usr.bin/ssh/misc.c | 18 | ||||
-rw-r--r-- | usr.bin/ssh/misc.h | 3 | ||||
-rw-r--r-- | usr.bin/ssh/monitor.c | 8 | ||||
-rw-r--r-- | usr.bin/ssh/monitor_wrap.c | 35 | ||||
-rw-r--r-- | usr.bin/ssh/servconf.c | 134 | ||||
-rw-r--r-- | usr.bin/ssh/servconf.h | 18 | ||||
-rw-r--r-- | usr.bin/ssh/srclimit.c | 322 | ||||
-rw-r--r-- | usr.bin/ssh/srclimit.h | 22 | ||||
-rw-r--r-- | usr.bin/ssh/sshd-session.c | 15 | ||||
-rw-r--r-- | usr.bin/ssh/sshd.c | 420 | ||||
-rw-r--r-- | usr.bin/ssh/sshd_config.5 | 66 |
11 files changed, 972 insertions, 89 deletions
diff --git a/usr.bin/ssh/misc.c b/usr.bin/ssh/misc.c index 38f987549d0..33327ad5c73 100644 --- a/usr.bin/ssh/misc.c +++ b/usr.bin/ssh/misc.c @@ -1,4 +1,4 @@ -/* $OpenBSD: misc.c,v 1.195 2024/05/17 06:11:17 deraadt Exp $ */ +/* $OpenBSD: misc.c,v 1.196 2024/06/06 17:15:25 djm Exp $ */ /* * Copyright (c) 2000 Markus Friedl. All rights reserved. * Copyright (c) 2005-2020 Damien Miller. All rights reserved. @@ -2947,3 +2947,19 @@ lib_contains_symbol(const char *path, const char *s) free(nl[0].n_name); return ret; } + +int +signal_is_crash(int sig) +{ + switch (sig) { + case SIGSEGV: + case SIGBUS: + case SIGTRAP: + case SIGSYS: + case SIGFPE: + case SIGILL: + case SIGABRT: + return 1; + } + return 0; +} diff --git a/usr.bin/ssh/misc.h b/usr.bin/ssh/misc.h index 00b8bc5d06c..7589d28e8f3 100644 --- a/usr.bin/ssh/misc.h +++ b/usr.bin/ssh/misc.h @@ -1,4 +1,4 @@ -/* $OpenBSD: misc.h,v 1.108 2024/05/17 00:30:24 djm Exp $ */ +/* $OpenBSD: misc.h,v 1.109 2024/06/06 17:15:25 djm Exp $ */ /* * Author: Tatu Ylonen <ylo@cs.hut.fi> @@ -250,6 +250,7 @@ void notify_complete(struct notifier_ctx *, const char *, ...) typedef void (*sshsig_t)(int); sshsig_t ssh_signal(int, sshsig_t); +int signal_is_crash(int); /* On OpenBSD time_t is int64_t which is long long. */ #define SSH_TIME_T_MAX LLONG_MAX diff --git a/usr.bin/ssh/monitor.c b/usr.bin/ssh/monitor.c index b5bbdf30587..0ada9a5b968 100644 --- a/usr.bin/ssh/monitor.c +++ b/usr.bin/ssh/monitor.c @@ -1,4 +1,4 @@ -/* $OpenBSD: monitor.c,v 1.239 2024/05/17 06:42:04 jsg Exp $ */ +/* $OpenBSD: monitor.c,v 1.240 2024/06/06 17:15:25 djm Exp $ */ /* * Copyright 2002 Niels Provos <provos@citi.umich.edu> * Copyright 2002 Markus Friedl <markus@openbsd.org> @@ -132,6 +132,7 @@ static char *auth_submethod = NULL; static u_int session_id2_len = 0; static u_char *session_id2 = NULL; static pid_t monitor_child_pid; +int auth_attempted = 0; struct mon_table { enum monitor_reqtype type; @@ -248,6 +249,10 @@ monitor_child_preauth(struct ssh *ssh, struct monitor *pmonitor) authenticated = (monitor_read(ssh, pmonitor, mon_dispatch, &ent) == 1); + /* Record that auth was attempted to set exit status later */ + if ((ent->flags & MON_AUTH) != 0) + auth_attempted = 1; + /* Special handling for multiple required authentications */ if (options.num_auth_methods != 0) { if (authenticated && @@ -290,6 +295,7 @@ monitor_child_preauth(struct ssh *ssh, struct monitor *pmonitor) fatal_f("authentication method name unknown"); debug_f("user %s authenticated by privileged process", authctxt->user); + auth_attempted = 0; ssh->authctxt = NULL; ssh_packet_set_log_preamble(ssh, "user %s", authctxt->user); diff --git a/usr.bin/ssh/monitor_wrap.c b/usr.bin/ssh/monitor_wrap.c index 6287a8c7110..ae254bc2674 100644 --- a/usr.bin/ssh/monitor_wrap.c +++ b/usr.bin/ssh/monitor_wrap.c @@ -1,4 +1,4 @@ -/* $OpenBSD: monitor_wrap.c,v 1.130 2024/05/17 00:30:24 djm Exp $ */ +/* $OpenBSD: monitor_wrap.c,v 1.131 2024/06/06 17:15:25 djm Exp $ */ /* * Copyright 2002 Niels Provos <provos@citi.umich.edu> * Copyright 2002 Markus Friedl <markus@openbsd.org> @@ -28,6 +28,7 @@ #include <sys/types.h> #include <sys/uio.h> #include <sys/queue.h> +#include <sys/wait.h> #include <errno.h> #include <pwd.h> @@ -69,6 +70,7 @@ #include "session.h" #include "servconf.h" #include "monitor_wrap.h" +#include "srclimit.h" #include "ssherr.h" @@ -133,6 +135,36 @@ mm_request_send(int sock, enum monitor_reqtype type, struct sshbuf *m) fatal_f("write: %s", strerror(errno)); } +static void +mm_reap(void) +{ + int status = -1; + + if (!mm_is_monitor()) + return; + while (waitpid(pmonitor->m_pid, &status, 0) == -1) { + if (errno == EINTR) + continue; + pmonitor->m_pid = -1; + fatal_f("waitpid: %s", strerror(errno)); + } + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) != 0) { + debug_f("preauth child exited with status %d", + WEXITSTATUS(status)); + cleanup_exit(255); + } + } else if (WIFSIGNALED(status)) { + error_f("preauth child terminated by signal %d", + WTERMSIG(status)); + cleanup_exit(signal_is_crash(WTERMSIG(status)) ? + EXIT_CHILD_CRASH : 255); + } else { + error_f("preauth child terminated abnormally"); + cleanup_exit(EXIT_CHILD_CRASH); + } +} + void mm_request_receive(int sock, struct sshbuf *m) { @@ -145,6 +177,7 @@ mm_request_receive(int sock, struct sshbuf *m) if (atomicio(read, sock, buf, sizeof(buf)) != sizeof(buf)) { if (errno == EPIPE) { debug3_f("monitor fd closed"); + mm_reap(); cleanup_exit(255); } fatal_f("read: %s", strerror(errno)); diff --git a/usr.bin/ssh/servconf.c b/usr.bin/ssh/servconf.c index 02ec30553bf..51855b8c46b 100644 --- a/usr.bin/ssh/servconf.c +++ b/usr.bin/ssh/servconf.c @@ -1,4 +1,4 @@ -/* $OpenBSD: servconf.c,v 1.407 2024/05/17 01:17:40 djm Exp $ */ +/* $OpenBSD: servconf.c,v 1.408 2024/06/06 17:15:25 djm Exp $ */ /* * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland * All rights reserved @@ -145,6 +145,16 @@ initialize_server_options(ServerOptions *options) options->per_source_max_startups = -1; options->per_source_masklen_ipv4 = -1; options->per_source_masklen_ipv6 = -1; + options->per_source_penalty_exempt = NULL; + options->per_source_penalty.enabled = -1; + options->per_source_penalty.max_sources = -1; + options->per_source_penalty.overflow_mode = -1; + options->per_source_penalty.penalty_crash = -1; + options->per_source_penalty.penalty_authfail = -1; + options->per_source_penalty.penalty_noauth = -1; + options->per_source_penalty.penalty_grace = -1; + options->per_source_penalty.penalty_max = -1; + options->per_source_penalty.penalty_min = -1; options->max_authtries = -1; options->max_sessions = -1; options->banner = NULL; @@ -377,6 +387,24 @@ fill_default_server_options(ServerOptions *options) options->per_source_masklen_ipv4 = 32; if (options->per_source_masklen_ipv6 == -1) options->per_source_masklen_ipv6 = 128; + if (options->per_source_penalty.enabled == -1) + options->per_source_penalty.enabled = 0; + if (options->per_source_penalty.max_sources == -1) + options->per_source_penalty.max_sources = 65536; + if (options->per_source_penalty.overflow_mode == -1) + options->per_source_penalty.overflow_mode = PER_SOURCE_PENALTY_OVERFLOW_PERMISSIVE; + if (options->per_source_penalty.penalty_crash == -1) + options->per_source_penalty.penalty_crash = 90; + if (options->per_source_penalty.penalty_grace == -1) + options->per_source_penalty.penalty_grace = 20; + if (options->per_source_penalty.penalty_authfail == -1) + options->per_source_penalty.penalty_authfail = 5; + if (options->per_source_penalty.penalty_noauth == -1) + options->per_source_penalty.penalty_noauth = 1; + if (options->per_source_penalty.penalty_min == -1) + options->per_source_penalty.penalty_min = 15; + if (options->per_source_penalty.penalty_max == -1) + options->per_source_penalty.penalty_max = 600; if (options->max_authtries == -1) options->max_authtries = DEFAULT_AUTH_FAIL_MAX; if (options->max_sessions == -1) @@ -454,6 +482,7 @@ fill_default_server_options(ServerOptions *options) CLEAR_ON_NONE(options->chroot_directory); CLEAR_ON_NONE(options->routing_domain); CLEAR_ON_NONE(options->host_key_agent); + CLEAR_ON_NONE(options->per_source_penalty_exempt); for (i = 0; i < options->num_host_key_files; i++) CLEAR_ON_NONE(options->host_key_files[i]); @@ -485,6 +514,7 @@ typedef enum { sBanner, sUseDNS, sHostbasedAuthentication, sHostbasedUsesNameFromPacketOnly, sHostbasedAcceptedAlgorithms, sHostKeyAlgorithms, sPerSourceMaxStartups, sPerSourceNetBlockSize, + sPerSourcePenalties, sPerSourcePenaltyExemptList, sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile, sGssAuthentication, sGssCleanupCreds, sGssStrictAcceptor, sAcceptEnv, sSetEnv, sPermitTunnel, @@ -601,6 +631,8 @@ static struct { { "maxstartups", sMaxStartups, SSHCFG_GLOBAL }, { "persourcemaxstartups", sPerSourceMaxStartups, SSHCFG_GLOBAL }, { "persourcenetblocksize", sPerSourceNetBlockSize, SSHCFG_GLOBAL }, + { "persourcepenalties", sPerSourcePenalties, SSHCFG_GLOBAL }, + { "persourcepenaltyexemptlist", sPerSourcePenaltyExemptList, SSHCFG_GLOBAL }, { "maxauthtries", sMaxAuthTries, SSHCFG_ALL }, { "maxsessions", sMaxSessions, SSHCFG_ALL }, { "banner", sBanner, SSHCFG_ALL }, @@ -1888,6 +1920,89 @@ process_server_config_line_depth(ServerOptions *options, char *line, options->per_source_max_startups = value; break; + case sPerSourcePenaltyExemptList: + charptr = &options->per_source_penalty_exempt; + arg = argv_next(&ac, &av); + if (!arg || *arg == '\0') + fatal("%s line %d: missing file name.", + filename, linenum); + if (addr_match_list(NULL, arg) != 0) { + fatal("%s line %d: keyword %s " + "invalid address argument.", + filename, linenum, keyword); + } + if (*activep && *charptr == NULL) + *charptr = xstrdup(arg); + break; + + case sPerSourcePenalties: + while ((arg = argv_next(&ac, &av)) != NULL) { + found = 1; + value = -1; + value2 = 0; + p = NULL; + /* Allow no/yes only in first position */ + if (strcasecmp(arg, "no") == 0 || + (value2 = (strcasecmp(arg, "yes") == 0))) { + if (ac > 0) { + fatal("%s line %d: keyword %s \"%s\" " + "argument must appear alone.", + filename, linenum, keyword, arg); + } + if (*activep && + options->per_source_penalty.enabled == -1) + options->per_source_penalty.enabled = value2; + continue; + } else if (strncmp(arg, "crash:", 6) == 0) { + p = arg + 6; + intptr = &options->per_source_penalty.penalty_crash; + } else if (strncmp(arg, "authfail:", 9) == 0) { + p = arg + 9; + intptr = &options->per_source_penalty.penalty_authfail; + } else if (strncmp(arg, "noauth:", 7) == 0) { + p = arg + 7; + intptr = &options->per_source_penalty.penalty_noauth; + } else if (strncmp(arg, "grace-exceeded:", 15) == 0) { + p = arg + 15; + intptr = &options->per_source_penalty.penalty_grace; + } else if (strncmp(arg, "max:", 4) == 0) { + p = arg + 4; + intptr = &options->per_source_penalty.penalty_max; + } else if (strncmp(arg, "min:", 4) == 0) { + p = arg + 4; + intptr = &options->per_source_penalty.penalty_min; + } else if (strncmp(arg, "max-sources:", 12) == 0) { + intptr = &options->per_source_penalty.max_sources; + if ((errstr = atoi_err(arg+12, &value)) != NULL) + fatal("%s line %d: %s value %s.", + filename, linenum, keyword, errstr); + } else if (strcmp(arg, "overflow:deny-all") == 0) { + intptr = &options->per_source_penalty.overflow_mode; + value = PER_SOURCE_PENALTY_OVERFLOW_DENY_ALL; + } else if (strcmp(arg, "overflow:permissive") == 0) { + intptr = &options->per_source_penalty.overflow_mode; + value = PER_SOURCE_PENALTY_OVERFLOW_PERMISSIVE; + } else { + fatal("%s line %d: unsupported %s keyword %s", + filename, linenum, keyword, arg); + } + /* If no value was parsed above, assume it's a time */ + if (value == -1 && (value = convtime(p)) == -1) { + fatal("%s line %d: invalid %s time value.", + filename, linenum, keyword); + } + if (*activep && *intptr == -1) { + *intptr = value; + /* any option implicitly enables penalties */ + options->per_source_penalty.enabled = 1; + } + } + if (!found) { + fatal("%s line %d: no %s specified", + filename, linenum, keyword); + } + break; + case sMaxAuthTries: intptr = &options->max_authtries; goto parse_int; @@ -3012,6 +3127,7 @@ dump_config(ServerOptions *o) dump_cfg_string(sPubkeyAcceptedAlgorithms, o->pubkey_accepted_algos); dump_cfg_string(sRDomain, o->routing_domain); dump_cfg_string(sSshdSessionPath, o->sshd_session_path); + dump_cfg_string(sPerSourcePenaltyExemptList, o->per_source_penalty_exempt); /* string arguments requiring a lookup */ dump_cfg_string(sLogLevel, log_level_name(o->log_level)); @@ -3099,4 +3215,20 @@ dump_config(ServerOptions *o) if (o->pubkey_auth_options & PUBKEYAUTH_VERIFY_REQUIRED) printf(" verify-required"); printf("\n"); + + if (o->per_source_penalty.enabled) { + printf("persourcepenalties crash:%d authfail:%d noauth:%d " + "grace-exceeded:%d max:%d min:%d max-sources:%d " + "overflow:%s\n", o->per_source_penalty.penalty_crash, + o->per_source_penalty.penalty_authfail, + o->per_source_penalty.penalty_noauth, + o->per_source_penalty.penalty_grace, + o->per_source_penalty.penalty_max, + o->per_source_penalty.penalty_min, + o->per_source_penalty.max_sources, + o->per_source_penalty.overflow_mode == + PER_SOURCE_PENALTY_OVERFLOW_DENY_ALL ? + "deny-all" : "permissive"); + } else + printf("persourcepenalties no\n"); } diff --git a/usr.bin/ssh/servconf.h b/usr.bin/ssh/servconf.h index 8ebdca5e2d5..4a4ac1cdb49 100644 --- a/usr.bin/ssh/servconf.h +++ b/usr.bin/ssh/servconf.h @@ -1,4 +1,4 @@ -/* $OpenBSD: servconf.h,v 1.163 2024/05/23 23:47:16 jsg Exp $ */ +/* $OpenBSD: servconf.h,v 1.164 2024/06/06 17:15:25 djm Exp $ */ /* * Author: Tatu Ylonen <ylo@cs.hut.fi> @@ -65,6 +65,20 @@ struct listenaddr { struct addrinfo *addrs; }; +#define PER_SOURCE_PENALTY_OVERFLOW_DENY_ALL 1 +#define PER_SOURCE_PENALTY_OVERFLOW_PERMISSIVE 2 +struct per_source_penalty { + int enabled; + int max_sources; + int overflow_mode; + int penalty_crash; + int penalty_grace; + int penalty_authfail; + int penalty_noauth; + int penalty_max; + int penalty_min; +}; + typedef struct { u_int num_ports; u_int ports_from_cmdline; @@ -172,6 +186,8 @@ typedef struct { int per_source_max_startups; int per_source_masklen_ipv4; int per_source_masklen_ipv6; + char *per_source_penalty_exempt; + struct per_source_penalty per_source_penalty; int max_authtries; int max_sessions; char *banner; /* SSH-2 banner message */ diff --git a/usr.bin/ssh/srclimit.c b/usr.bin/ssh/srclimit.c index 853a0eef9d3..2a8dffd7d66 100644 --- a/usr.bin/ssh/srclimit.c +++ b/usr.bin/ssh/srclimit.c @@ -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 @@ -16,11 +17,13 @@ #include <sys/socket.h> #include <sys/types.h> +#include <sys/tree.h> #include <limits.h> #include <netdb.h> #include <stdio.h> #include <string.h> +#include <stdlib.h> #include "addr.h" #include "canohost.h" @@ -28,8 +31,12 @@ #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 { @@ -37,8 +44,58 @@ static struct child_info { struct xaddr addr; } *child; +/* + * 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_addr; +RB_HEAD(penalties_by_expiry, penalty) penalties_by_expiry; +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 npenalties; + +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; @@ -46,6 +103,9 @@ srclimit_init(int max, int persource, int ipv4len, int ipv6len) ipv4_masklen = ipv4len; ipv6_masklen = ipv6len; max_persource = persource; + penalty_cfg = *penalty_conf; + penalty_exempt = penalty_exempt_conf == NULL ? + NULL : xstrdup(penalty_exempt_conf); if (max_persource == INT_MAX) /* no limit */ return; debug("%s: max connections %d, per source %d, masks %d,%d", __func__, @@ -55,16 +115,15 @@ srclimit_init(int max, int persource, int ipv4len, int ipv6len) child = xcalloc(max_children, sizeof(*child)); for (i = 0; i < max_children; i++) child[i].id = -1; + RB_INIT(&penalties_by_addr); + RB_INIT(&penalties_by_expiry); } /* 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]; @@ -72,18 +131,11 @@ 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. */ @@ -136,3 +188,243 @@ srclimit_done(int id) } } } + +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(time_t now) +{ + struct penalty *penalty, *tmp; + + /* XXX avoid full scan of tree, e.g. min-heap */ + RB_FOREACH_SAFE(penalty, penalties_by_expiry, + &penalties_by_expiry, tmp) { + if (penalty->expiry >= now) + break; + if (RB_REMOVE(penalties_by_expiry, &penalties_by_expiry, + penalty) != penalty || + RB_REMOVE(penalties_by_addr, &penalties_by_addr, + penalty) != penalty) + fatal_f("internal error: penalty tables corrupt"); + free(penalty); + if (npenalties-- == 0) + fatal_f("internal error: npenalties underflow"); + } +} + +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; + char addr_s[NI_MAXHOST]; + + 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; + } + } + if (npenalties > (size_t)penalty_cfg.max_sources && + penalty_cfg.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; + now = monotime(); + if ((penalty = RB_FIND(penalties_by_addr, + &penalties_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_remove_expired_penalties(void) +{ + struct penalty *p = NULL; + int bits; + char s[NI_MAXHOST + 4]; + + /* Delete the soonest-to-expire penalties. */ + while (npenalties > (size_t)penalty_cfg.max_sources) { + if ((p = RB_MIN(penalties_by_expiry, + &penalties_by_expiry)) == NULL) + break; /* shouldn't happen */ + bits = p->addr.af == AF_INET ? ipv4_masklen : ipv6_masklen; + addr_masklen_ntop(&p->addr, bits, s, sizeof(s)); + debug3_f("overflow, remove %s", s); + if (RB_REMOVE(penalties_by_expiry, + &penalties_by_expiry, p) != p || + RB_REMOVE(penalties_by_addr, &penalties_by_addr, p) != p) + fatal_f("internal error: penalty tables corrupt"); + free(p); + npenalties--; + } +} + +void +srclimit_penalise(struct xaddr *addr, int penalty_type) +{ + struct xaddr masked; + struct penalty *penalty, *existing; + time_t now; + int bits, penalty_secs; + char addrnetmask[NI_MAXHOST + 4]; + const char *reason = 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); + if (npenalties > (size_t)penalty_cfg.max_sources && + penalty_cfg.overflow_mode == PER_SOURCE_PENALTY_OVERFLOW_DENY_ALL) { + verbose_f("penalty table full, cannot penalise %s for %s", + 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, &penalties_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, &penalties_by_expiry, + penalty) != NULL) + fatal_f("internal error: penalty tables corrupt"); + verbose_f("%s: new %s penalty of %d seconds for %s", + addrnetmask, penalty->active ? "active" : "deferred", + penalty_secs, reason); + if (++npenalties > (size_t)penalty_cfg.max_sources) + srclimit_remove_expired_penalties(); /* permissive */ + return; + } + debug_f("%s penalty for %s already exists, %lld seconds remaining", + existing->active ? "active" : "inactive", + addrnetmask, (long long)(existing->expiry - now)); + /* Expiry information is about to change, remove from tree */ + if (RB_REMOVE(penalties_by_expiry, &penalties_by_expiry, + existing) != existing) + fatal_f("internal error: penalty tables corrupt (remove)"); + /* 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 penalty of %lld seconds for %s", + addrnetmask, (long long)(existing->expiry - now), reason); + existing->active = 1; + } + existing->reason = penalty->reason; + free(penalty); + /* Re-insert into expiry tree */ + if (RB_INSERT(penalties_by_expiry, &penalties_by_expiry, + existing) != NULL) + fatal_f("internal error: penalty tables corrupt (insert)"); +} + +void +srclimit_penalty_info(void) +{ + struct penalty *p = NULL; + int bits; + char s[NI_MAXHOST + 4]; + time_t now; + + now = monotime(); + logit("%zu active penalties", npenalties); + RB_FOREACH(p, penalties_by_expiry, &penalties_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)); + } + } +} diff --git a/usr.bin/ssh/srclimit.h b/usr.bin/ssh/srclimit.h index 6e04f32b3ff..74a6f2b836d 100644 --- a/usr.bin/ssh/srclimit.h +++ b/usr.bin/ssh/srclimit.h @@ -13,6 +13,26 @@ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -void srclimit_init(int, int, int, int); +struct xaddr; + +struct per_source_penalty; + +void srclimit_init(int, int, int, int, + struct per_source_penalty *, const char *); int srclimit_check_allow(int, int); void srclimit_done(int); + +#define SRCLIMIT_PENALTY_NONE 0 +#define SRCLIMIT_PENALTY_CRASH 1 +#define SRCLIMIT_PENALTY_AUTHFAIL 2 +#define SRCLIMIT_PENALTY_GRACE_EXCEEDED 3 +#define SRCLIMIT_PENALTY_NOAUTH 4 + +/* meaningful exit values, used by sshd listener for penalties */ +#define EXIT_LOGIN_GRACE 3 /* login grace period exceeded */ +#define EXIT_CHILD_CRASH 4 /* preauth child crashed */ +#define EXIT_AUTH_ATTEMPTED 5 /* at least one auth attempt made */ + +void srclimit_penalise(struct xaddr *, int); +int srclimit_penalty_check_allow(int, const char **); +void srclimit_penalty_info(void); diff --git a/usr.bin/ssh/sshd-session.c b/usr.bin/ssh/sshd-session.c index b800f2b5a67..e75b4f80bca 100644 --- a/usr.bin/ssh/sshd-session.c +++ b/usr.bin/ssh/sshd-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: sshd-session.c,v 1.2 2024/05/17 02:39:11 jsg Exp $ */ +/* $OpenBSD: sshd-session.c,v 1.3 2024/06/06 17:15:25 djm Exp $ */ /* * SSH2 implementation: * Privilege Separation: @@ -188,11 +188,7 @@ grace_alarm_handler(int sig) ssh_signal(SIGTERM, SIG_IGN); kill(0, SIGTERM); } - - /* Log error and exit. */ - sigdie("Timeout before authentication for %s port %d", - ssh_remote_ipaddr(the_active_state), - ssh_remote_port(the_active_state)); + _exit(EXIT_LOGIN_GRACE); } /* Destroy the host and server keys. They will no longer be needed. */ @@ -1220,6 +1216,8 @@ main(int ac, char **av) ssh_signal(SIGALRM, SIG_DFL); authctxt->authenticated = 1; if (startup_pipe != -1) { + /* signal listener that authentication completed successfully */ + (void)atomicio(vwrite, startup_pipe, "\001", 1); close(startup_pipe); startup_pipe = -1; } @@ -1338,6 +1336,8 @@ do_ssh2_kex(struct ssh *ssh) void cleanup_exit(int i) { + extern int auth_attempted; /* monitor.c */ + if (the_active_state != NULL && the_authctxt != NULL) { do_cleanup(the_active_state, the_authctxt); if (privsep_is_preauth && @@ -1350,5 +1350,8 @@ cleanup_exit(int i) } } } + /* Override default fatal exit value when auth was attempted */ + if (i == 255 && auth_attempted) + _exit(EXIT_AUTH_ATTEMPTED); _exit(i); } diff --git a/usr.bin/ssh/sshd.c b/usr.bin/ssh/sshd.c index d310779be0f..33e35284767 100644 --- a/usr.bin/ssh/sshd.c +++ b/usr.bin/ssh/sshd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: sshd.c,v 1.605 2024/06/01 07:03:37 djm Exp $ */ +/* $OpenBSD: sshd.c,v 1.606 2024/06/06 17:15:25 djm Exp $ */ /* * Copyright (c) 2000, 2001, 2002 Markus Friedl. All rights reserved. * Copyright (c) 2002 Niels Provos. All rights reserved. @@ -72,6 +72,7 @@ #include "version.h" #include "ssherr.h" #include "sk-api.h" +#include "addr.h" #include "srclimit.h" /* Re-exec fds */ @@ -120,6 +121,8 @@ struct { } sensitive_data; /* This is set to true when a signal is received. */ +static volatile sig_atomic_t received_siginfo = 0; +static volatile sig_atomic_t received_sigchld = 0; static volatile sig_atomic_t received_sighup = 0; static volatile sig_atomic_t received_sigterm = 0; @@ -127,8 +130,9 @@ static volatile sig_atomic_t received_sigterm = 0; u_int utmp_len = HOST_NAME_MAX+1; /* - * startup_pipes/flags are used for tracking children of the listening sshd - * process early in their lifespans. This tracking is needed for three things: + * The early_child/children array below is used for tracking children of the + * listening sshd process early in their lifespans, before they have + * completed authentication. This tracking is needed for four things: * * 1) Implementing the MaxStartups limit of concurrent unauthenticated * connections. @@ -137,14 +141,31 @@ u_int utmp_len = HOST_NAME_MAX+1; * after it restarts. * 3) Ensuring that rexec'd sshd processes have received their initial state * from the parent listen process before handling SIGHUP. + * 4) Tracking and logging unsuccessful exits from the preauth sshd monitor, + * including and especially those for LoginGraceTime timeouts. * * Child processes signal that they have completed closure of the listen_socks * and (if applicable) received their rexec state by sending a char over their - * sock. Child processes signal that authentication has completed by closing - * the sock (or by exiting). + * sock. + * + * Child processes signal that authentication has completed by sending a + * second char over the socket before closing it, otherwise the listener will + * continue tracking the child (and using up a MaxStartups slot) until the + * preauth subprocess exits, whereupon the listener will log its exit status. + * preauth processes will exit with a status of EXIT_LOGIN_GRACE to indicate + * they did not authenticate before the LoginGraceTime alarm fired. */ -static int *startup_pipes = NULL; -static int *startup_flags = NULL; /* Indicates child closed listener */ +struct early_child { + int pipefd; + int early; /* Indicates child closed listener */ + char *id; /* human readable connection identifier */ + pid_t pid; + struct xaddr addr; + int have_addr; + int status, have_status; +}; +static struct early_child *children; +static int children_active; static int startup_pipe = -1; /* in child */ /* sshd_config buffer */ @@ -171,15 +192,257 @@ close_listen_socks(void) num_listen_socks = 0; } +/* Allocate and initialise the children array */ +static void +child_alloc(void) +{ + int i; + + children = xcalloc(options.max_startups, sizeof(*children)); + for (i = 0; i < options.max_startups; i++) { + children[i].pipefd = -1; + children[i].pid = -1; + } +} + +/* Register a new connection in the children array; child pid comes later */ +static struct early_child * +child_register(int pipefd, int sockfd) +{ + int i, lport, rport; + char *laddr = NULL, *raddr = NULL; + struct early_child *child = NULL; + struct sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + struct sockaddr *sa = (struct sockaddr *)&addr; + + for (i = 0; i < options.max_startups; i++) { + if (children[i].pipefd != -1 || children[i].pid > 0) + continue; + child = &(children[i]); + break; + } + if (child == NULL) { + fatal_f("error: accepted connection when all %d child " + " slots full", options.max_startups); + } + child->pipefd = pipefd; + child->early = 1; + /* record peer address, if available */ + if (getpeername(sockfd, sa, &addrlen) == 0 && + addr_sa_to_xaddr(sa, addrlen, &child->addr) == 0) + child->have_addr = 1; + /* format peer address string for logs */ + if ((lport = get_local_port(sockfd)) == 0 || + (rport = get_peer_port(sockfd)) == 0) { + /* Not a TCP socket */ + raddr = get_peer_ipaddr(sockfd); + xasprintf(&child->id, "connection from %s", raddr); + } else { + laddr = get_local_ipaddr(sockfd); + raddr = get_peer_ipaddr(sockfd); + xasprintf(&child->id, "connection from %s to %s", laddr, raddr); + } + free(laddr); + free(raddr); + if (++children_active > options.max_startups) + fatal_f("internal error: more children than max_startups"); + + return child; +} + +/* + * Finally free a child entry. Don't call this directly. + */ +static void +child_finish(struct early_child *child) +{ + if (children_active == 0) + fatal_f("internal error: children_active underflow"); + if (child->pipefd != -1) + close(child->pipefd); + free(child->id); + memset(child, '\0', sizeof(*child)); + child->pipefd = -1; + child->pid = -1; + children_active--; +} + +/* + * Close a child's pipe. This will not stop tracking the child immediately + * (it will still be tracked for waitpid()) unless force_final is set, or + * child has already exited. + */ +static void +child_close(struct early_child *child, int force_final, int quiet) +{ + if (!quiet) + debug_f("enter%s", force_final ? " (forcing)" : ""); + if (child->pipefd != -1) { + close(child->pipefd); + child->pipefd = -1; + } + if (child->pid == -1 || force_final) + child_finish(child); +} + +/* Record a child exit. Safe to call from signal handlers */ +static void +child_exit(pid_t pid, int status) +{ + int i; + + if (children == NULL || pid <= 0) + return; + for (i = 0; i < options.max_startups; i++) { + if (children[i].pid == pid) { + children[i].have_status = 1; + children[i].status = status; + break; + } + } +} + +/* + * Reap a child entry that has exited, as previously flagged + * using child_exit(). + * Handles logging of exit condition and will finalise the child if its pipe + * had already been closed. + */ +static void +child_reap(struct early_child *child) +{ + LogLevel level = SYSLOG_LEVEL_DEBUG1; + int was_crash, penalty_type = SRCLIMIT_PENALTY_NONE; + + /* Log exit information */ + if (WIFSIGNALED(child->status)) { + /* + * Increase logging for signals potentially associated + * with serious conditions. + */ + if ((was_crash = signal_is_crash(WTERMSIG(child->status)))) + level = SYSLOG_LEVEL_ERROR; + do_log2(level, "session process %ld for %s killed by " + "signal %d%s", (long)child->pid, child->id, + WTERMSIG(child->status), child->early ? " (early)" : ""); + if (was_crash) + penalty_type = SRCLIMIT_PENALTY_CRASH; + } else if (!WIFEXITED(child->status)) { + penalty_type = SRCLIMIT_PENALTY_CRASH; + error("session process %ld for %s terminated abnormally, " + "status=0x%x%s", (long)child->pid, child->id, child->status, + child->early ? " (early)" : ""); + } else { + /* Normal exit. We care about the status */ + switch (WEXITSTATUS(child->status)) { + case 0: + debug3_f("preauth child %ld for %s completed " + "normally %s", (long)child->pid, child->id, + child->early ? " (early)" : ""); + break; + case EXIT_LOGIN_GRACE: + penalty_type = SRCLIMIT_PENALTY_GRACE_EXCEEDED; + logit("Timeout before authentication for %s, " + "pid = %ld%s", child->id, (long)child->pid, + child->early ? " (early)" : ""); + break; + case EXIT_CHILD_CRASH: + penalty_type = SRCLIMIT_PENALTY_CRASH; + logit("Session process %ld unpriv child crash for %s%s", + (long)child->pid, child->id, + child->early ? " (early)" : ""); + break; + case EXIT_AUTH_ATTEMPTED: + penalty_type = SRCLIMIT_PENALTY_AUTHFAIL; + debug_f("preauth child %ld for %s exited " + "after unsuccessful auth attempt %s", + (long)child->pid, child->id, + child->early ? " (early)" : ""); + break; + default: + penalty_type = SRCLIMIT_PENALTY_NOAUTH; + debug_f("preauth child %ld for %s exited " + "with status %d%s", (long)child->pid, child->id, + WEXITSTATUS(child->status), + child->early ? " (early)" : ""); + break; + } + } + /* + * XXX would be nice to have more subtlety here. + * - Different penalties + * a) authentication failures without success (e.g. brute force) + * b) login grace exceeded (penalise DoS) + * c) monitor crash (penalise exploit attempt) + * d) unpriv preauth crash (penalise exploit attempt) + * - Unpriv auth exit status/WIFSIGNALLED is not available because + * the "mm_request_receive: monitor fd closed" fatal kills the + * monitor before waitpid() can occur. It would be good to use the + * unpriv exit status to detect crashes. + * + * For now, just penalise (a), (b) and (c), since that is what we have + * readily available. The authentication failures detection cannot + * discern between failed authentication and other connection problems + * until we have the unpriv exist status plumbed through (and the unpriv + * child modified to use a different exit status when auth has been + * attempted), but it's a start. + */ + if (child->have_addr) + srclimit_penalise(&child->addr, penalty_type); + + child->pid = -1; + child->have_status = 0; + if (child->pipefd == -1) + child_finish(child); +} + +/* Reap all children that have exited; called after SIGCHLD */ +static void +child_reap_all_exited(void) +{ + int i; + + if (children == NULL) + return; + for (i = 0; i < options.max_startups; i++) { + if (!children[i].have_status) + continue; + child_reap(&(children[i])); + } +} + static void close_startup_pipes(void) { int i; - if (startup_pipes) - for (i = 0; i < options.max_startups; i++) - if (startup_pipes[i] != -1) - close(startup_pipes[i]); + if (children == NULL) + return; + for (i = 0; i < options.max_startups; i++) { + if (children[i].pipefd != -1) + child_close(&(children[i]), 1, 1); + } +} + +/* Called after SIGINFO */ +static void +show_info(void) +{ + int i; + + /* XXX print listening sockets here too */ + if (children == NULL) + return; + logit("%d active startups", children_active); + for (i = 0; i < options.max_startups; i++) { + if (children[i].pipefd == -1 && children[i].pid <= 0) + continue; + logit("child %d: fd=%d pid=%ld %s%s", i, children[i].pipefd, + (long)children[i].pid, children[i].id, + children[i].early ? " (early)" : ""); + } + srclimit_penalty_info(); } /* @@ -222,6 +485,12 @@ sigterm_handler(int sig) received_sigterm = sig; } +static void +siginfo_handler(int sig) +{ + received_siginfo = 1; +} + /* * SIGCHLD handler. This is called whenever a child dies. This will then * reap any zombies left by exited children. @@ -233,9 +502,17 @@ main_sigchld_handler(int sig) pid_t pid; int status; - while ((pid = waitpid(-1, &status, WNOHANG)) > 0 || - (pid == -1 && errno == EINTR)) - ; + for (;;) { + if ((pid = waitpid(-1, &status, WNOHANG)) == 0) + break; + else if (pid == -1) { + if (errno == EINTR) + continue; + break; + } + child_exit(pid, status); + received_sigchld = 1; + } errno = save_errno; } @@ -268,7 +545,7 @@ should_drop_connection(int startups) } /* - * Check whether connection should be accepted by MaxStartups. + * Check whether connection should be accepted by MaxStartups or for penalty. * Returns 0 if the connection is accepted. If the connection is refused, * returns 1 and attempts to send notification to client. * Logs when the MaxStartups condition is entered or exited, and periodically @@ -278,12 +555,17 @@ static int drop_connection(int sock, int startups, int notify_pipe) { char *laddr, *raddr; - const char msg[] = "Exceeded MaxStartups\r\n"; + const char *reason = NULL, msg[] = "Not allowed at this time\r\n"; static time_t last_drop, first_drop; static u_int ndropped; LogLevel drop_level = SYSLOG_LEVEL_VERBOSE; time_t now; + if (!srclimit_penalty_check_allow(sock, &reason)) { + drop_level = SYSLOG_LEVEL_INFO; + goto handle; + } + now = monotime(); if (!should_drop_connection(startups) && srclimit_check_allow(sock, notify_pipe) == 1) { @@ -313,12 +595,16 @@ drop_connection(int sock, int startups, int notify_pipe) } last_drop = now; ndropped++; + reason = "past Maxstartups"; + handle: laddr = get_local_ipaddr(sock); raddr = get_peer_ipaddr(sock); - do_log2(drop_level, "drop connection #%d from [%s]:%d on [%s]:%d " - "past MaxStartups", startups, raddr, get_peer_port(sock), - laddr, get_local_port(sock)); + do_log2(drop_level, "drop connection #%d from [%s]:%d on [%s]:%d %s", + startups, + raddr, get_peer_port(sock), + laddr, get_local_port(sock), + reason); free(laddr); free(raddr); /* best-effort notification to client */ @@ -521,8 +807,12 @@ server_listen(void) u_int i; /* Initialise per-source limit tracking. */ - srclimit_init(options.max_startups, options.per_source_max_startups, - options.per_source_masklen_ipv4, options.per_source_masklen_ipv6); + srclimit_init(options.max_startups, + options.per_source_max_startups, + options.per_source_masklen_ipv4, + options.per_source_masklen_ipv6, + &options.per_source_penalty, + options.per_source_penalty_exempt); for (i = 0; i < options.num_listen_addrs; i++) { listen_on_addrs(&options.listen_addrs[i]); @@ -548,32 +838,30 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, int log_stderr) { struct pollfd *pfd = NULL; - int i, j, ret, npfd; - int ostartups = -1, startups = 0, listening = 0, lameduck = 0; + int i, ret, npfd; + int oactive = -1, listening = 0, lameduck = 0; int startup_p[2] = { -1 , -1 }, *startup_pollfd; char c = 0; struct sockaddr_storage from; + struct early_child *child; socklen_t fromlen; - pid_t pid; sigset_t nsigset, osigset; /* setup fd set for accept */ /* pipes connected to unauthenticated child sshd processes */ - startup_pipes = xcalloc(options.max_startups, sizeof(int)); - startup_flags = xcalloc(options.max_startups, sizeof(int)); + child_alloc(); startup_pollfd = xcalloc(options.max_startups, sizeof(int)); - for (i = 0; i < options.max_startups; i++) - startup_pipes[i] = -1; /* * Prepare signal mask that we use to block signals that might set - * received_sigterm or received_sighup, so that we are guaranteed + * received_sigterm/hup/chld/info, so that we are guaranteed * to immediately wake up the ppoll if a signal is received after * the flag is checked. */ sigemptyset(&nsigset); sigaddset(&nsigset, SIGHUP); sigaddset(&nsigset, SIGCHLD); + sigaddset(&nsigset, SIGINFO); sigaddset(&nsigset, SIGTERM); sigaddset(&nsigset, SIGQUIT); @@ -595,11 +883,19 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, unlink(options.pid_file); exit(received_sigterm == SIGTERM ? 0 : 255); } - if (ostartups != startups) { + if (received_sigchld) { + child_reap_all_exited(); + received_sigchld = 0; + } + if (received_siginfo) { + show_info(); + received_siginfo = 0; + } + if (oactive != children_active) { setproctitle("%s [listener] %d of %d-%d startups", - listener_proctitle, startups, + listener_proctitle, children_active, options.max_startups_begin, options.max_startups); - ostartups = startups; + oactive = children_active; } if (received_sighup) { if (!lameduck) { @@ -620,8 +916,8 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, npfd = num_listen_socks; for (i = 0; i < options.max_startups; i++) { startup_pollfd[i] = -1; - if (startup_pipes[i] != -1) { - pfd[npfd].fd = startup_pipes[i]; + if (children[i].pipefd != -1) { + pfd[npfd].fd = children[i].pipefd; pfd[npfd].events = POLLIN; startup_pollfd[i] = npfd++; } @@ -639,34 +935,46 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, continue; for (i = 0; i < options.max_startups; i++) { - if (startup_pipes[i] == -1 || + if (children[i].pipefd == -1 || startup_pollfd[i] == -1 || !(pfd[startup_pollfd[i]].revents & (POLLIN|POLLHUP))) continue; - switch (read(startup_pipes[i], &c, sizeof(c))) { + switch (read(children[i].pipefd, &c, sizeof(c))) { case -1: if (errno == EINTR || errno == EAGAIN) continue; if (errno != EPIPE) { error_f("startup pipe %d (fd=%d): " - "read %s", i, startup_pipes[i], + "read %s", i, children[i].pipefd, strerror(errno)); } /* FALLTHROUGH */ case 0: - /* child exited or completed auth */ - close(startup_pipes[i]); - srclimit_done(startup_pipes[i]); - startup_pipes[i] = -1; - startups--; - if (startup_flags[i]) + /* child exited preauth */ + if (children[i].early) listening--; + srclimit_done(children[i].pipefd); + child_close(&(children[i]), 0, 0); break; case 1: - /* child has finished preliminaries */ - if (startup_flags[i]) { + if (children[i].early && c == '\0') { + /* child has finished preliminaries */ listening--; - startup_flags[i] = 0; + children[i].early = 0; + debug2_f("child %lu for %s received " + "config", (long)children[i].pid, + children[i].id); + } else if (!children[i].early && c == '\001') { + /* child has completed auth */ + debug2_f("child %lu for %s auth done", + (long)children[i].pid, + children[i].id); + child_close(&(children[i]), 1, 0); + } else { + error_f("unexpected message 0x%02x " + "child %ld for %s in state %d", + (int)c, (long)children[i].pid, + children[i].id, children[i].early); } break; } @@ -695,7 +1003,8 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, close(*newsock); continue; } - if (drop_connection(*newsock, startups, startup_p[0])) { + if (drop_connection(*newsock, + children_active, startup_p[0])) { close(*newsock); close(startup_p[0]); close(startup_p[1]); @@ -712,14 +1021,6 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, continue; } - for (j = 0; j < options.max_startups; j++) - if (startup_pipes[j] == -1) { - startup_pipes[j] = startup_p[0]; - startups++; - startup_flags[j] = 1; - break; - } - /* * Got connection. Fork a child to handle it, unless * we are in debugging mode. @@ -737,7 +1038,6 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, close(startup_p[0]); close(startup_p[1]); startup_pipe = -1; - pid = getpid(); send_rexec_state(config_s[0], cfg); close(config_s[0]); free(pfd); @@ -750,7 +1050,8 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, * parent continues listening. */ listening++; - if ((pid = fork()) == 0) { + child = child_register(startup_p[0], *newsock); + if ((child->pid = fork()) == 0) { /* * Child. Close the listening and * max_startup sockets. Start using @@ -774,10 +1075,10 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, } /* Parent. Stay in the loop. */ - if (pid == -1) + if (child->pid == -1) error("fork: %.100s", strerror(errno)); else - debug("Forked child %ld.", (long)pid); + debug("Forked child %ld.", (long)child->pid); close(startup_p[1]); @@ -1340,6 +1641,7 @@ main(int ac, char **av) ssh_signal(SIGCHLD, main_sigchld_handler); ssh_signal(SIGTERM, sigterm_handler); ssh_signal(SIGQUIT, sigterm_handler); + ssh_signal(SIGINFO, siginfo_handler); /* * Write out the pid file after the sigterm handler diff --git a/usr.bin/ssh/sshd_config.5 b/usr.bin/ssh/sshd_config.5 index 93afc3eeb70..430de76071a 100644 --- a/usr.bin/ssh/sshd_config.5 +++ b/usr.bin/ssh/sshd_config.5 @@ -33,8 +33,8 @@ .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .\" -.\" $OpenBSD: sshd_config.5,v 1.355 2024/02/21 06:17:29 djm Exp $ -.Dd $Mdocdate: February 21 2024 $ +.\" $OpenBSD: sshd_config.5,v 1.356 2024/06/06 17:15:25 djm Exp $ +.Dd $Mdocdate: June 6 2024 $ .Dt SSHD_CONFIG 5 .Os .Sh NAME @@ -1558,6 +1558,68 @@ Values for IPv4 and optionally IPv6 may be specified, separated by a colon. The default is .Cm 32:128 , which means each address is considered individually. +.It Cm PerSourcePenalties +Controls penalties for various conditions that may represent attacks on +.Xr sshd 8 . +If a penalty is enforced against a client then its source address and any +others in the +.Cm PerSourceNetBlockSize +will be refused connection for a period. +Multiple penalties from the same source from concurrent connections will +accumulate up to a maximum. +Conversely, penalties are not applied until a minimum threshold time has been +accumulated. +Penalties are off by default but may be enabled using default settings using the +.Cm yes +keyword or by specifying one or more of the keywords below. +.Pp +Penalties are controlled using the following keywords, all of which accept +arguments, e.g. +.Qq crash:2m . +.Bl -tag -width Ds +.It Cm crash:duration +Specifies how long to refuse clients that cause a crash of +.Xr sshd 8 . +.It Cm authfail:duration +Specifies how long to refuse clients that disconnect after making one or more +unsuccessful authentication attempts. +.It Cm noauth:duration +Specifies how long to refuse clients that disconnect without attempting +authentication. +This timeout should be used cautiously otherwise it may penalise legitimate +scanning tools such as +.Xr ssh-keyscan 1 . +.It Cm grace-exceeded:duration +Specifies how long to refuse clients that fail to authenticate after +.Cm LoginGraceTime . +.It Cm max:duration +Specifies the maximum time a particular source address range will be refused +access for. +Repeated penalties will accumulate up to this maximum. +.It Cm min:duration +Specifies the minimum penalty that must accrue before enforcement begins. +.It Cm max-sources:number +Specifies the maximum number of penalise client address ranges to track. +.It Cm overflow:mode +Controls how the server behaves when +.Cm max-sources +is exceeded. +There are two operating modes: +.Cm deny-all , +which denies all incoming connections other than those exempted via +.Cm PerSourcePenaltyExemptList +until a penalty expires, and +.Cm permissive , +which allows new connections by removing existing penalties early. +.El +.It Cm PerSourcePenaltyExemptList +Specifies a comma-separated list of addresses to exempt from penalties. +This list may contain wildcards and CIDR address/masklen ranges. +Note that the mask length provided must be consistent with the address - +it is an error to specify a mask length that is too long for the address +or one with bits set in this host portion of the address. +For example, 192.0.2.0/33 and 192.0.2.0/8, respectively. +The default is not to exempt any addresses. .It Cm PidFile Specifies the file that contains the process ID of the SSH daemon, or |