diff options
-rw-r--r-- | usr.bin/ssh/servconf.c | 61 | ||||
-rw-r--r-- | usr.bin/ssh/servconf.h | 5 | ||||
-rw-r--r-- | usr.bin/ssh/srclimit.c | 138 | ||||
-rw-r--r-- | usr.bin/ssh/srclimit.h | 18 | ||||
-rw-r--r-- | usr.bin/ssh/sshd.c | 20 | ||||
-rw-r--r-- | usr.bin/ssh/sshd/Makefile | 4 | ||||
-rw-r--r-- | usr.bin/ssh/sshd_config.5 | 21 |
7 files changed, 255 insertions, 12 deletions
diff --git a/usr.bin/ssh/servconf.c b/usr.bin/ssh/servconf.c index 85629048945..c52855504e9 100644 --- a/usr.bin/ssh/servconf.c +++ b/usr.bin/ssh/servconf.c @@ -1,5 +1,5 @@ -/* $OpenBSD: servconf.c,v 1.371 2020/10/18 11:32:02 djm Exp $ */ +/* $OpenBSD: servconf.c,v 1.372 2021/01/09 12:10:02 dtucker Exp $ */ /* * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland * All rights reserved @@ -147,6 +147,9 @@ initialize_server_options(ServerOptions *options) options->max_startups_begin = -1; options->max_startups_rate = -1; options->max_startups = -1; + options->per_source_max_startups = -1; + options->per_source_masklen_ipv4 = -1; + options->per_source_masklen_ipv6 = -1; options->max_authtries = -1; options->max_sessions = -1; options->banner = NULL; @@ -394,6 +397,12 @@ fill_default_server_options(ServerOptions *options) options->max_startups_rate = 30; /* 30% */ if (options->max_startups_begin == -1) options->max_startups_begin = 10; + if (options->per_source_max_startups == -1) + options->per_source_max_startups = INT_MAX; + if (options->per_source_masklen_ipv4 == -1) + options->per_source_masklen_ipv4 = 32; + if (options->per_source_masklen_ipv6 == -1) + options->per_source_masklen_ipv6 = 128; if (options->max_authtries == -1) options->max_authtries = DEFAULT_AUTH_FAIL_MAX; if (options->max_sessions == -1) @@ -493,7 +502,7 @@ typedef enum { sXAuthLocation, sSubsystem, sMaxStartups, sMaxAuthTries, sMaxSessions, sBanner, sUseDNS, sHostbasedAuthentication, sHostbasedUsesNameFromPacketOnly, sHostbasedAcceptedKeyTypes, - sHostKeyAlgorithms, + sHostKeyAlgorithms, sPerSourceMaxStartups, sPerSourceNetBlockSize, sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile, sGssAuthentication, sGssCleanupCreds, sGssStrictAcceptor, sAcceptEnv, sSetEnv, sPermitTunnel, @@ -603,6 +612,8 @@ static struct { { "gatewayports", sGatewayPorts, SSHCFG_ALL }, { "subsystem", sSubsystem, SSHCFG_GLOBAL }, { "maxstartups", sMaxStartups, SSHCFG_GLOBAL }, + { "persourcemaxstartups", sPerSourceMaxStartups, SSHCFG_GLOBAL }, + { "persourcenetblocksize", sPerSourceNetBlockSize, SSHCFG_GLOBAL }, { "maxauthtries", sMaxAuthTries, SSHCFG_ALL }, { "maxsessions", sMaxSessions, SSHCFG_ALL }, { "banner", sBanner, SSHCFG_ALL }, @@ -1833,6 +1844,45 @@ process_server_config_line_depth(ServerOptions *options, char *line, options->max_startups = options->max_startups_begin; break; + case sPerSourceNetBlockSize: + arg = strdelim(&cp); + if (!arg || *arg == '\0') + fatal("%s line %d: Missing PerSourceNetBlockSize spec.", + filename, linenum); + switch (n = sscanf(arg, "%d:%d", &value, &value2)) { + case 2: + if (value2 < 0 || value2 > 128) + n = -1; + /* FALLTHROUGH */ + case 1: + if (value < 0 || value > 32) + n = -1; + } + if (n != 1 && n != 2) + fatal("%s line %d: Invalid PerSourceNetBlockSize" + " spec.", filename, linenum); + if (*activep) { + options->per_source_masklen_ipv4 = value; + options->per_source_masklen_ipv6 = value2; + } + break; + + case sPerSourceMaxStartups: + arg = strdelim(&cp); + if (!arg || *arg == '\0') + fatal("%s line %d: Missing PerSourceMaxStartups spec.", + filename, linenum); + if (strcmp(arg, "none") == 0) { /* no limit */ + value = INT_MAX; + } else { + if ((errstr = atoi_err(arg, &value)) != NULL) + fatal("%s line %d: integer value %s.", + filename, linenum, errstr); + } + if (*activep) + options->per_source_max_startups = value; + break; + case sMaxAuthTries: intptr = &options->max_authtries; goto parse_int; @@ -2834,6 +2884,13 @@ dump_config(ServerOptions *o) printf("maxstartups %d:%d:%d\n", o->max_startups_begin, o->max_startups_rate, o->max_startups); + printf("persourcemaxstartups "); + if (o->per_source_max_startups == INT_MAX) + printf("none\n"); + else + printf("%d\n", o->per_source_max_startups); + printf("persourcnetblocksize %d:%d\n", o->per_source_masklen_ipv4, + o->per_source_masklen_ipv6); s = NULL; for (i = 0; tunmode_desc[i].val != -1; i++) { diff --git a/usr.bin/ssh/servconf.h b/usr.bin/ssh/servconf.h index 6917b43ecfd..f4ca62f2b46 100644 --- a/usr.bin/ssh/servconf.h +++ b/usr.bin/ssh/servconf.h @@ -1,4 +1,4 @@ -/* $OpenBSD: servconf.h,v 1.148 2020/10/29 03:13:06 djm Exp $ */ +/* $OpenBSD: servconf.h,v 1.149 2021/01/09 12:10:02 dtucker Exp $ */ /* * Author: Tatu Ylonen <ylo@cs.hut.fi> @@ -177,6 +177,9 @@ typedef struct { int max_startups_begin; int max_startups_rate; int max_startups; + int per_source_max_startups; + int per_source_masklen_ipv4; + int per_source_masklen_ipv6; 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 new file mode 100644 index 00000000000..f40fb19801c --- /dev/null +++ b/usr.bin/ssh/srclimit.c @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020 Darren Tucker <dtucker@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/socket.h> +#include <sys/types.h> + +#include <limits.h> +#include <netdb.h> +#include <stdio.h> +#include <string.h> + +#include "addr.h" +#include "canohost.h" +#include "log.h" +#include "misc.h" +#include "srclimit.h" +#include "xmalloc.h" + +static int max_children, max_persource, ipv4_masklen, ipv6_masklen; + +/* Per connection state, used to enforce unauthenticated connection limit. */ +static struct child_info { + int id; + struct xaddr addr; +} *child; + +void +srclimit_init(int max, int persource, int ipv4len, int ipv6len) +{ + int i; + + max_children = max; + ipv4_masklen = ipv4len; + ipv6_masklen = ipv6len; + max_persource = persource; + if (max_persource == INT_MAX) /* no limit */ + return; + debug("%s: max connections %d, per source %d, masks %d,%d", __func__, + max, persource, ipv4len, ipv6len); + if (max <= 0) + fatal("%s: invalid number of sockets: %d", __func__, max); + child = xcalloc(max_children, sizeof(*child)); + for (i = 0; i < max_children; i++) + child[i].id = -1; +} + +/* returns 1 if connection allowed, 0 if not allowed. */ +int +srclimit_check_allow(int sock, int id) +{ + struct xaddr xa, xb, xmask; + struct sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + struct sockaddr *sa = (struct sockaddr *)&addr; + int i, bits, first_unused, count = 0; + char xas[NI_MAXHOST]; + + if (max_persource == INT_MAX) /* no limit */ + 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. */ + 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); + return 1; + } + + first_unused = max_children; + /* Count matching entries and find first unused one. */ + for (i = 0; i < max_children; i++) { + if (child[i].id == -1) { + if (i < first_unused) + first_unused = i; + } else if (addr_cmp(&child[i].addr, &xb) == 0) { + count++; + } + } + if (addr_ntop(&xa, xas, sizeof(xas)) != 0) { + debug3("%s: addr ntop failed", __func__); + return 1; + } + debug3("%s: new unauthenticated connection from %s/%d, at %d of %d", + __func__, xas, bits, count, max_persource); + + if (first_unused == max_children) { /* no free slot found */ + debug3("%s: no free slot", __func__); + return 0; + } + if (first_unused < 0 || first_unused >= max_children) + fatal("%s: internal error: first_unused out of range", + __func__); + + if (count >= max_persource) + return 0; + + /* Connection allowed, store masked address. */ + child[first_unused].id = id; + memcpy(&child[first_unused].addr, &xb, sizeof(xb)); + return 1; +} + +void +srclimit_done(int id) +{ + int i; + + if (max_persource == INT_MAX) /* no limit */ + return; + + debug("%s: id %d", __func__, id); + /* Clear corresponding state entry. */ + for (i = 0; i < max_children; i++) { + if (child[i].id == id) { + child[i].id = -1; + return; + } + } +} diff --git a/usr.bin/ssh/srclimit.h b/usr.bin/ssh/srclimit.h new file mode 100644 index 00000000000..6e04f32b3ff --- /dev/null +++ b/usr.bin/ssh/srclimit.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2020 Darren Tucker <dtucker@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * 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); +int srclimit_check_allow(int, int); +void srclimit_done(int); diff --git a/usr.bin/ssh/sshd.c b/usr.bin/ssh/sshd.c index ad6f4c6eb1e..8b8c9d1158f 100644 --- a/usr.bin/ssh/sshd.c +++ b/usr.bin/ssh/sshd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: sshd.c,v 1.566 2020/12/29 00:59:15 djm Exp $ */ +/* $OpenBSD: sshd.c,v 1.567 2021/01/09 12:10:02 dtucker Exp $ */ /* * Author: Tatu Ylonen <ylo@cs.hut.fi> * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland @@ -106,6 +106,7 @@ #include "version.h" #include "ssherr.h" #include "sk-api.h" +#include "srclimit.h" /* Re-exec fds */ #define REEXEC_DEVCRYPTO_RESERVED_FD (STDERR_FILENO + 1) @@ -812,7 +813,7 @@ should_drop_connection(int startups) * while in that state. */ static int -drop_connection(int sock, int startups) +drop_connection(int sock, int startups, int notify_pipe) { char *laddr, *raddr; const char msg[] = "Exceeded MaxStartups\r\n"; @@ -822,7 +823,8 @@ drop_connection(int sock, int startups) time_t now; now = monotime(); - if (!should_drop_connection(startups)) { + if (!should_drop_connection(startups) && + srclimit_check_allow(sock, notify_pipe) == 1) { if (last_drop != 0 && startups < options.max_startups_begin - 1) { /* XXX maybe need better hysteresis here */ @@ -1056,6 +1058,10 @@ 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); + for (i = 0; i < options.num_listen_addrs; i++) { listen_on_addrs(&options.listen_addrs[i]); freeaddrinfo(options.listen_addrs[i].addrs); @@ -1161,6 +1167,7 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s) 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]) @@ -1191,9 +1198,12 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s) continue; } if (unset_nonblock(*newsock) == -1 || - drop_connection(*newsock, startups) || - pipe(startup_p) == -1) { + pipe(startup_p) == -1) + continue; + if (drop_connection(*newsock, startups, startup_p[0])) { close(*newsock); + close(startup_p[0]); + close(startup_p[1]); continue; } diff --git a/usr.bin/ssh/sshd/Makefile b/usr.bin/ssh/sshd/Makefile index 51a2fefd9d1..0768b2fb4cd 100644 --- a/usr.bin/ssh/sshd/Makefile +++ b/usr.bin/ssh/sshd/Makefile @@ -1,4 +1,4 @@ -# $OpenBSD: Makefile,v 1.104 2020/01/25 23:02:14 djm Exp $ +# $OpenBSD: Makefile,v 1.105 2021/01/09 12:10:02 dtucker Exp $ .PATH: ${.CURDIR}/.. @@ -6,7 +6,7 @@ SRCS= sshd.c auth-rhosts.c auth-passwd.c sshpty.c sshlogin.c servconf.c \ serverloop.c auth.c auth2.c auth-options.c session.c auth2-chall.c \ groupaccess.c auth-bsdauth.c auth2-hostbased.c auth2-kbdint.c \ auth2-none.c auth2-passwd.c auth2-pubkey.c monitor.c monitor_wrap.c \ - sftp-server.c sftp-common.c sftp-realpath.c sandbox-pledge.c + sftp-server.c sftp-common.c sftp-realpath.c sandbox-pledge.c srclimit.c SRCS+= authfd.c compat.c dns.c fatal.c hostfile.c readpass.c utf8.c uidswap.c SRCS+= ${SRCS_BASE} ${SRCS_KEX} ${SRCS_KEXS} ${SRCS_KEY} ${SRCS_KEYP} \ ${SRCS_KRL} ${SRCS_PROT} ${SRCS_PKT} ${SRCS_UTL} ${SRCS_PKCS11} \ diff --git a/usr.bin/ssh/sshd_config.5 b/usr.bin/ssh/sshd_config.5 index 1dfd463eece..bac5b4f03c9 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.320 2021/01/08 02:19:24 djm Exp $ -.Dd $Mdocdate: January 8 2021 $ +.\" $OpenBSD: sshd_config.5,v 1.321 2021/01/09 12:10:02 dtucker Exp $ +.Dd $Mdocdate: January 9 2021 $ .Dt SSHD_CONFIG 5 .Os .Sh NAME @@ -1436,6 +1436,23 @@ SSH daemon, or to not write one. The default is .Pa /var/run/sshd.pid . +.It Cm PerSourceMaxStartups +Specifies the number of unauthenticated connections allowed from a +given source address, or +.Dq none +if there is no limit. +This limit is applied in addition to +.Cm MaxStartups , +whichever is lower. +The default is +.Cm none . +.It Cm PerSourceNetBlockSize +Specifies the number of bits of source address that are grouped together +for the purposes of applying PerSourceMaxStartups limits. +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 Port Specifies the port number that .Xr sshd 8 |