Add a sshd_config UnusedConnectionTimeout option to terminate
authordjm <djm@openbsd.org>
Tue, 17 Jan 2023 09:44:48 +0000 (09:44 +0000)
committerdjm <djm@openbsd.org>
Tue, 17 Jan 2023 09:44:48 +0000 (09:44 +0000)
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@

usr.bin/ssh/servconf.c
usr.bin/ssh/servconf.h
usr.bin/ssh/serverloop.c
usr.bin/ssh/sshd_config.5

index 8343ae5..50cd910 100644 (file)
@@ -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 <ylo@cs.hut.fi>, Espoo, Finland
  *                    All rights reserved
@@ -180,6 +180,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. */
@@ -421,6 +422,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);
 
@@ -501,7 +504,7 @@ typedef enum {
        sStreamLocalBindMask, sStreamLocalBindUnlink,
        sAllowStreamLocalForwarding, sFingerprintHash, sDisableForwarding,
        sExposeAuthInfo, sRDomain, sPubkeyAuthOptions, sSecurityKeyProvider,
-       sRequiredRSASize, sChannelTimeout,
+       sRequiredRSASize, sChannelTimeout, sUnusedConnectionTimeout,
        sDeprecated, sIgnore, sUnsupported
 } ServerOpCodes;
 
@@ -647,6 +650,7 @@ static struct {
        { "securitykeyprovider", sSecurityKeyProvider, SSHCFG_GLOBAL },
        { "requiredrsasize", sRequiredRSASize, SSHCFG_ALL },
        { "channeltimeout", sChannelTimeout, SSHCFG_ALL },
+       { "unusedconnectiontimeout", sUnusedConnectionTimeout, SSHCFG_ALL },
        { NULL, sBadOption, 0 }
 };
 
@@ -2476,6 +2480,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:
@@ -2648,6 +2663,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
@@ -2800,6 +2816,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);
 }
 
@@ -2913,6 +2933,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);
index f4dd15f..1a2411e 100644 (file)
@@ -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 <ylo@cs.hut.fi>
@@ -231,6 +231,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 */
index 5347e33..9b8ef09 100644 (file)
@@ -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 <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -166,17 +166,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)) {
@@ -184,6 +186,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
@@ -193,8 +207,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;
@@ -231,9 +246,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 */
@@ -244,6 +259,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);
+       }
 }
 
 /*
index 85a671a..829312b 100644 (file)
@@ -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
@@ -460,6 +460,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
@@ -1257,6 +1260,7 @@ Available keywords are
 .Cm AuthorizedPrincipalsFile ,
 .Cm Banner ,
 .Cm CASignatureAlgorithms ,
+.Cm ChannelTimeout ,
 .Cm ChrootDirectory ,
 .Cm ClientAliveCountMax ,
 .Cm ClientAliveInterval ,
@@ -1296,6 +1300,7 @@ Available keywords are
 .Cm StreamLocalBindMask ,
 .Cm StreamLocalBindUnlink ,
 .Cm TrustedUserCAKeys ,
+.Cm UnusedConnectionTimeout ,
 .Cm X11DisplayOffset ,
 .Cm X11Forwarding
 and
@@ -1812,6 +1817,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