summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--usr.bin/ssh/misc.c18
-rw-r--r--usr.bin/ssh/misc.h3
-rw-r--r--usr.bin/ssh/monitor.c8
-rw-r--r--usr.bin/ssh/monitor_wrap.c35
-rw-r--r--usr.bin/ssh/servconf.c134
-rw-r--r--usr.bin/ssh/servconf.h18
-rw-r--r--usr.bin/ssh/srclimit.c322
-rw-r--r--usr.bin/ssh/srclimit.h22
-rw-r--r--usr.bin/ssh/sshd-session.c15
-rw-r--r--usr.bin/ssh/sshd.c420
-rw-r--r--usr.bin/ssh/sshd_config.566
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