From 0293c19807f83141cdf33b443154459f9ee471f6 Mon Sep 17 00:00:00 2001 From: "djm@openbsd.org" Date: Tue, 17 Jan 2023 09:44:48 +0000 Subject: upstream: Add a sshd_config UnusedConnectionTimeout option to terminate client connections that have no open channels for some length of time. This complements the recently-added ChannelTimeout option that terminates inactive channels after a timeout. ok markus@ OpenBSD-Commit-ID: ca983be74c0350364c11f8ba3bd692f6f24f5da9 --- servconf.c | 25 +++++++++++++++++++++++-- servconf.h | 4 +++- serverloop.c | 33 ++++++++++++++++++++++++++++----- sshd_config.5 | 36 ++++++++++++++++++++++++++++++++++-- 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/servconf.c b/servconf.c index d4c1f9df..2e039da8 100644 --- a/servconf.c +++ b/servconf.c @@ -1,5 +1,5 @@ -/* $OpenBSD: servconf.c,v 1.389 2023/01/06 02:47:18 djm Exp $ */ +/* $OpenBSD: servconf.c,v 1.390 2023/01/17 09:44:48 djm Exp $ */ /* * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland * All rights reserved @@ -198,6 +198,7 @@ initialize_server_options(ServerOptions *options) options->required_rsa_size = -1; options->channel_timeouts = NULL; options->num_channel_timeouts = 0; + options->unused_connection_timeout = -1; } /* Returns 1 if a string option is unset or set to "none" or 0 otherwise. */ @@ -446,6 +447,8 @@ fill_default_server_options(ServerOptions *options) options->sk_provider = xstrdup("internal"); if (options->required_rsa_size == -1) options->required_rsa_size = SSH_RSA_MINIMUM_MODULUS_SIZE; + if (options->unused_connection_timeout == -1) + options->unused_connection_timeout = 0; assemble_algorithms(options); @@ -529,7 +532,7 @@ typedef enum { sStreamLocalBindMask, sStreamLocalBindUnlink, sAllowStreamLocalForwarding, sFingerprintHash, sDisableForwarding, sExposeAuthInfo, sRDomain, sPubkeyAuthOptions, sSecurityKeyProvider, - sRequiredRSASize, sChannelTimeout, + sRequiredRSASize, sChannelTimeout, sUnusedConnectionTimeout, sDeprecated, sIgnore, sUnsupported } ServerOpCodes; @@ -691,6 +694,7 @@ static struct { { "securitykeyprovider", sSecurityKeyProvider, SSHCFG_GLOBAL }, { "requiredrsasize", sRequiredRSASize, SSHCFG_ALL }, { "channeltimeout", sChannelTimeout, SSHCFG_ALL }, + { "unusedconnectiontimeout", sUnusedConnectionTimeout, SSHCFG_ALL }, { NULL, sBadOption, 0 } }; @@ -2537,6 +2541,17 @@ process_server_config_line_depth(ServerOptions *options, char *line, } break; + case sUnusedConnectionTimeout: + intptr = &options->unused_connection_timeout; + /* peek at first arg for "none" so we can reuse parse_time */ + if (av[0] != NULL && strcasecmp(av[0], "none") == 0) { + (void)argv_next(&ac, &av); /* consume arg */ + if (*activep) + *intptr = 0; + break; + } + goto parse_time; + case sDeprecated: case sIgnore: case sUnsupported: @@ -2709,6 +2724,7 @@ copy_set_server_options(ServerOptions *dst, ServerOptions *src, int preauth) M_CP_INTOPT(rekey_interval); M_CP_INTOPT(log_level); M_CP_INTOPT(required_rsa_size); + M_CP_INTOPT(unused_connection_timeout); /* * The bind_mask is a mode_t that may be unsigned, so we can't use @@ -2861,6 +2877,10 @@ fmt_intarg(ServerOpCodes code, int val) static void dump_cfg_int(ServerOpCodes code, int val) { + if (code == sUnusedConnectionTimeout && val == 0) { + printf("%s none\n", lookup_opcode_name(code)); + return; + } printf("%s %d\n", lookup_opcode_name(code), val); } @@ -2977,6 +2997,7 @@ dump_config(ServerOptions *o) dump_cfg_int(sClientAliveCountMax, o->client_alive_count_max); dump_cfg_int(sRequiredRSASize, o->required_rsa_size); dump_cfg_oct(sStreamLocalBindMask, o->fwd_opts.streamlocal_bind_mask); + dump_cfg_int(sUnusedConnectionTimeout, o->unused_connection_timeout); /* formatted integer arguments */ dump_cfg_fmtint(sPermitRootLogin, o->permit_root_login); diff --git a/servconf.h b/servconf.h index 4745e58a..7ad43de8 100644 --- a/servconf.h +++ b/servconf.h @@ -1,4 +1,4 @@ -/* $OpenBSD: servconf.h,v 1.158 2023/01/06 02:47:19 djm Exp $ */ +/* $OpenBSD: servconf.h,v 1.159 2023/01/17 09:44:48 djm Exp $ */ /* * Author: Tatu Ylonen @@ -233,6 +233,8 @@ typedef struct { char **channel_timeouts; /* inactivity timeout by channel type */ u_int num_channel_timeouts; + + int unused_connection_timeout; } ServerOptions; /* Information about the incoming connection as used by Match */ diff --git a/serverloop.c b/serverloop.c index 2d46feb9..6db0916d 100644 --- a/serverloop.c +++ b/serverloop.c @@ -1,4 +1,4 @@ -/* $OpenBSD: serverloop.c,v 1.233 2023/01/06 02:38:23 djm Exp $ */ +/* $OpenBSD: serverloop.c,v 1.234 2023/01/17 09:44:48 djm Exp $ */ /* * Author: Tatu Ylonen * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -172,17 +172,19 @@ wait_until_can_do_something(struct ssh *ssh, int *conn_in_readyp, int *conn_out_readyp) { struct timespec timeout; + char remote_id[512]; int ret; int client_alive_scheduled = 0; u_int p; - /* time we last heard from the client OR sent a keepalive */ - static time_t last_client_time; + time_t now; + static time_t last_client_time, unused_connection_expiry; *conn_in_readyp = *conn_out_readyp = 0; /* Prepare channel poll. First two pollfd entries are reserved */ ptimeout_init(&timeout); channel_prepare_poll(ssh, pfdp, npfd_allocp, npfd_activep, 2, &timeout); + now = monotime(); if (*npfd_activep < 2) fatal_f("bad npfd %u", *npfd_activep); /* shouldn't happen */ if (options.rekey_interval > 0 && !ssh_packet_is_rekeying(ssh)) { @@ -190,6 +192,18 @@ wait_until_can_do_something(struct ssh *ssh, ssh_packet_get_rekey_timeout(ssh)); } + /* + * If no channels are open and UnusedConnectionTimeout is set, then + * start the clock to terminate the connection. + */ + if (options.unused_connection_timeout != 0) { + if (channel_still_open(ssh) || unused_connection_expiry == 0) { + unused_connection_expiry = now + + options.unused_connection_timeout; + } + ptimeout_deadline_monotime(&timeout, unused_connection_expiry); + } + /* * if using client_alive, set the max timeout accordingly, * and indicate that this particular timeout was for client @@ -199,8 +213,9 @@ wait_until_can_do_something(struct ssh *ssh, * analysis more difficult, but we're not doing it yet. */ if (options.client_alive_interval) { + /* Time we last heard from the client OR sent a keepalive */ if (last_client_time == 0) - last_client_time = monotime(); + last_client_time = now; ptimeout_deadline_sec(&timeout, options.client_alive_interval); /* XXX ? deadline_monotime(last_client_time + alive_interval) */ client_alive_scheduled = 1; @@ -237,9 +252,9 @@ wait_until_can_do_something(struct ssh *ssh, *conn_in_readyp = (*pfdp)[0].revents != 0; *conn_out_readyp = (*pfdp)[1].revents != 0; + now = monotime(); /* need to reset after ppoll() */ /* ClientAliveInterval probing */ if (client_alive_scheduled) { - time_t now = monotime(); if (ret == 0 && now > last_client_time + options.client_alive_interval) { /* ppoll timed out and we're due to probe */ @@ -250,6 +265,14 @@ wait_until_can_do_something(struct ssh *ssh, last_client_time = now; } } + + /* UnusedConnectionTimeout handling */ + if (unused_connection_expiry != 0 && + now > unused_connection_expiry && !channel_still_open(ssh)) { + sshpkt_fmt_connection_id(ssh, remote_id, sizeof(remote_id)); + logit("terminating inactive connection from %s", remote_id); + cleanup_exit(255); + } } /* diff --git a/sshd_config.5 b/sshd_config.5 index 16f1c713..cd83bf43 100644 --- a/sshd_config.5 +++ b/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.345 2023/01/06 08:44:11 jmc Exp $ -.Dd $Mdocdate: January 6 2023 $ +.\" $OpenBSD: sshd_config.5,v 1.346 2023/01/17 09:44:48 djm Exp $ +.Dd $Mdocdate: January 17 2023 $ .Dt SSHD_CONFIG 5 .Os .Sh NAME @@ -459,6 +459,9 @@ close the SSH connection, nor does it prevent a client from requesting another channel of the same type. In particular, expiring an inactive forwarding session does not prevent another identical forwarding from being subsequently created. +See also +.Cm UnusedConnectionTimeout , +which may be used in conjunction with this option. .Pp The default is not to expire channels of any type for inactivity. .It Cm ChrootDirectory @@ -1256,6 +1259,7 @@ Available keywords are .Cm AuthorizedPrincipalsFile , .Cm Banner , .Cm CASignatureAlgorithms , +.Cm ChannelTimeout , .Cm ChrootDirectory , .Cm ClientAliveCountMax , .Cm ClientAliveInterval , @@ -1295,6 +1299,7 @@ Available keywords are .Cm StreamLocalBindMask , .Cm StreamLocalBindUnlink , .Cm TrustedUserCAKeys , +.Cm UnusedConnectionTimeout , .Cm X11DisplayOffset , .Cm X11Forwarding and @@ -1811,6 +1816,33 @@ for authentication using .Cm TrustedUserCAKeys . For more details on certificates, see the CERTIFICATES section in .Xr ssh-keygen 1 . +.It Cm UnusedConnectionTimeout +Specifies whether and how quickly +.Xr sshd 8 +should close client connections with no open channels. +Open channels include active shell, command execution or subsystem +sessions, connected network, socket, agent of X11 forwardings. +Forwarding listeners, such as those from the +.Xr ssh 1 +.Fl R +flag are not considered as open channels and do not prevent the timeout. +The timeout value +is specified in seconds or may use any of the units documented in the +.Sx TIME FORMATS +section. +.Pp +Note that this timeout starts when the client connection completes +user authentication but before the client has an opportunity to open any +channels. +Caution should be used when using short timeout values, as they may not +provide sufficient time for the client to request and open its channels +before terminating the connection. +.Pp +The default +.Cm none +is to never expire connections for having no open channels. +This option may be useful in conjunction with +.Cm ChannelTimeout . .It Cm UseDNS Specifies whether .Xr sshd 8 -- cgit v1.2.3