add a "global" ChannelTimeout type to ssh(1) and sshd(8) that watches
authordjm <djm@openbsd.org>
Tue, 9 Jan 2024 22:19:00 +0000 (22:19 +0000)
committerdjm <djm@openbsd.org>
Tue, 9 Jan 2024 22:19:00 +0000 (22:19 +0000)
all open channels and will close all open channels if there is no
traffic on any of them for the specified interval. This is in addition
to the existing per-channel timeouts added a few releases ago.

This supports use-cases like having a session + x11 forwarding channel
open where one may be idle for an extended period but the other is
actively used. The global timeout would allow closing both channels when
both have been idle for too long.

ok dtucker@

usr.bin/ssh/channels.c
usr.bin/ssh/ssh_config.5
usr.bin/ssh/sshd_config.5

index 0431b8c..9640962 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: channels.c,v 1.435 2023/12/18 14:47:20 djm Exp $ */
+/* $OpenBSD: channels.c,v 1.436 2024/01/09 22:19:00 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -206,6 +206,9 @@ struct ssh_channels {
        /* Channel timeouts by type */
        struct ssh_channel_timeout *timeouts;
        size_t ntimeouts;
+       /* Global timeout for all OPEN channels */
+       int global_deadline;
+       time_t lastused;
 };
 
 /* helper */
@@ -308,6 +311,11 @@ channel_add_timeout(struct ssh *ssh, const char *type_pattern,
 {
        struct ssh_channels *sc = ssh->chanctxt;
 
+       if (strcmp(type_pattern, "global") == 0) {
+               debug2_f("global channel timeout %d seconds", timeout_secs);
+               sc->global_deadline = timeout_secs;
+               return;
+       }
        debug2_f("channel type \"%s\" timeout %d seconds",
            type_pattern, timeout_secs);
        sc->timeouts = xrecallocarray(sc->timeouts, sc->ntimeouts,
@@ -368,6 +376,38 @@ channel_set_xtype(struct ssh *ssh, int id, const char *xctype)
            c->inactive_deadline);
 }
 
+/*
+ * update "last used" time on a channel.
+ * NB. nothing else should update lastused except to clear it.
+ */
+static void
+channel_set_used_time(struct ssh *ssh, Channel *c)
+{
+       ssh->chanctxt->lastused = monotime();
+       if (c != NULL)
+               c->lastused = ssh->chanctxt->lastused;
+}
+
+/*
+ * Get the time at which a channel is due to time out for inactivity.
+ * Returns 0 if the channel is not due to time out ever.
+ */
+static time_t
+channel_get_expiry(struct ssh *ssh, Channel *c)
+{
+       struct ssh_channels *sc = ssh->chanctxt;
+       time_t expiry = 0, channel_expiry;
+
+       if (sc->lastused != 0 && sc->global_deadline != 0)
+               expiry = sc->lastused + sc->global_deadline;
+       if (c->lastused != 0 && c->inactive_deadline != 0) {
+               channel_expiry = c->lastused + c->inactive_deadline;
+               if (expiry == 0 || channel_expiry < expiry)
+                       expiry = channel_expiry;
+       }
+       return expiry;
+}
+
 /*
  * Register filedescriptors for a channel, used when allocating a channel or
  * when the channel consumer/producer is ready, e.g. shell exec'd
@@ -429,6 +469,8 @@ channel_register_fds(struct ssh *ssh, Channel *c, int rfd, int wfd, int efd,
                if (efd != -1)
                        set_nonblock(efd);
        }
+       /* channel might be entering a larval state, so reset global timeout */
+       channel_set_used_time(ssh, NULL);
 }
 
 /*
@@ -1185,7 +1227,7 @@ channel_set_fds(struct ssh *ssh, int id, int rfd, int wfd, int efd,
 
        channel_register_fds(ssh, c, rfd, wfd, efd, extusage, nonblock, is_tty);
        c->type = SSH_CHANNEL_OPEN;
-       c->lastused = monotime();
+       channel_set_used_time(ssh, c);
        c->local_window = c->local_window_max = window_max;
 
        if ((r = sshpkt_start(ssh, SSH2_MSG_CHANNEL_WINDOW_ADJUST)) != 0 ||
@@ -1356,7 +1398,7 @@ channel_pre_x11_open(struct ssh *ssh, Channel *c)
 
        if (ret == 1) {
                c->type = SSH_CHANNEL_OPEN;
-               c->lastused = monotime();
+               channel_set_used_time(ssh, c);
                channel_pre_open(ssh, c);
        } else if (ret == -1) {
                logit("X11 connection rejected because of wrong "
@@ -2004,7 +2046,7 @@ channel_post_connecting(struct ssh *ssh, Channel *c)
                    c->self, c->connect_ctx.host, c->connect_ctx.port);
                channel_connect_ctx_free(&c->connect_ctx);
                c->type = SSH_CHANNEL_OPEN;
-               c->lastused = monotime();
+               channel_set_used_time(ssh, c);
                if (isopen) {
                        /* no message necessary */
                } else {
@@ -2087,7 +2129,7 @@ channel_handle_rfd(struct ssh *ssh, Channel *c)
                        goto rfail;
                }
                if (nr != 0)
-                       c->lastused = monotime();
+                       channel_set_used_time(ssh, c);
                return 1;
        }
 
@@ -2108,7 +2150,7 @@ channel_handle_rfd(struct ssh *ssh, Channel *c)
                }
                return -1;
        }
-       c->lastused = monotime();
+       channel_set_used_time(ssh, c);
        if (c->input_filter != NULL) {
                if (c->input_filter(ssh, c, buf, len) == -1) {
                        debug2("channel %d: filter stops", c->self);
@@ -2179,7 +2221,7 @@ channel_handle_wfd(struct ssh *ssh, Channel *c)
                }
                return -1;
        }
-       c->lastused = monotime();
+       channel_set_used_time(ssh, c);
        if (c->isatty && dlen >= 1 && buf[0] != '\r') {
                if (tcgetattr(c->wfd, &tio) == 0 &&
                    !(tio.c_lflag & ECHO) && (tio.c_lflag & ICANON)) {
@@ -2225,7 +2267,7 @@ channel_handle_efd_write(struct ssh *ssh, Channel *c)
                if ((r = sshbuf_consume(c->extended, len)) != 0)
                        fatal_fr(r, "channel %i: consume", c->self);
                c->local_consumed += len;
-               c->lastused = monotime();
+               channel_set_used_time(ssh, c);
        }
        return 1;
 }
@@ -2249,7 +2291,7 @@ channel_handle_efd_read(struct ssh *ssh, Channel *c)
                channel_close_fd(ssh, c, &c->efd);
                return 1;
        }
-       c->lastused = monotime();
+       channel_set_used_time(ssh, c);
        if (c->extended_usage == CHAN_EXTENDED_IGNORE)
                debug3("channel %d: discard efd", c->self);
        else if ((r = sshbuf_put(c->extended, buf, len)) != 0)
@@ -2539,10 +2581,9 @@ channel_handler(struct ssh *ssh, int table, struct timespec *timeout)
                                continue;
                }
                if (ftab[c->type] != NULL) {
-                       if (table == CHAN_PRE &&
-                           c->type == SSH_CHANNEL_OPEN &&
-                           c->inactive_deadline != 0 && c->lastused != 0 &&
-                           now >= c->lastused + c->inactive_deadline) {
+                       if (table == CHAN_PRE && c->type == SSH_CHANNEL_OPEN &&
+                           channel_get_expiry(ssh, c) != 0 &&
+                           now >= channel_get_expiry(ssh, c)) {
                                /* channel closed for inactivity */
                                verbose("channel %d: closing after %u seconds "
                                    "of inactivity", c->self,
@@ -2554,10 +2595,9 @@ channel_handler(struct ssh *ssh, int table, struct timespec *timeout)
                                /* inactivity timeouts must interrupt poll() */
                                if (timeout != NULL &&
                                    c->type == SSH_CHANNEL_OPEN &&
-                                   c->lastused != 0 &&
-                                   c->inactive_deadline != 0) {
+                                   channel_get_expiry(ssh, c) != 0) {
                                        ptimeout_deadline_monotime(timeout,
-                                           c->lastused + c->inactive_deadline);
+                                           channel_get_expiry(ssh, c));
                                }
                        } else if (timeout != NULL) {
                                /*
@@ -3516,7 +3556,7 @@ channel_input_open_confirmation(int type, u_int32_t seq, struct ssh *ssh)
                c->open_confirm(ssh, c->self, 1, c->open_confirm_ctx);
                debug2_f("channel %d: callback done", c->self);
        }
-       c->lastused = monotime();
+       channel_set_used_time(ssh, c);
        debug2("channel %d: open confirm rwindow %u rmax %u", c->self,
            c->remote_window, c->remote_maxpacket);
        return 0;
index f137d30..56f871f 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: ssh_config.5,v 1.391 2023/10/12 02:18:18 djm Exp $
-.Dd $Mdocdate: October 12 2023 $
+.\" $OpenBSD: ssh_config.5,v 1.392 2024/01/09 22:19:00 djm Exp $
+.Dd $Mdocdate: January 9 2024 $
 .Dt SSH_CONFIG 5
 .Os
 .Sh NAME
@@ -463,8 +463,10 @@ Timeouts are specified as one or more
 .Dq type=interval
 pairs separated by whitespace, where the
 .Dq type
-must be a channel type name (as described in the table below), optionally
-containing wildcard characters.
+must be the special keyword
+.Dq global
+or a channel type name from the list below, optionally containing
+wildcard characters.
 .Pp
 The timeout value
 .Dq interval
@@ -473,11 +475,19 @@ is specified in seconds or may use any of the units documented in the
 section.
 For example,
 .Dq session=5m
-would cause the interactive session to terminate after five minutes of
+would cause interactive sessions to terminate after five minutes of
 inactivity.
 Specifying a zero value disables the inactivity timeout.
 .Pp
-The available channel types include:
+The special timeout
+.Dq global
+Applies to all active channels, taken together.
+Traffic on any active channel will reset the timeout, but when the timeout
+expires then all open channels will be closed.
+Note that this global timeout is not matched by wildcards and must be
+specified explicitly.
+.Pp
+The available channel type names include:
 .Bl -tag -width Ds
 .It Cm agent-connection
 Open connections to
index dd62459..0f859de 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.350 2023/07/28 05:42:36 jmc Exp $
-.Dd $Mdocdate: July 28 2023 $
+.\" $OpenBSD: sshd_config.5,v 1.351 2024/01/09 22:19:00 djm Exp $
+.Dd $Mdocdate: January 9 2024 $
 .Dt SSHD_CONFIG 5
 .Os
 .Sh NAME
@@ -410,8 +410,10 @@ Timeouts are specified as one or more
 .Dq type=interval
 pairs separated by whitespace, where the
 .Dq type
-must be a channel type name (as described in the table below), optionally
-containing wildcard characters.
+must be the special keyword
+.Dq global
+or a channel type name from the list below, optionally containing
+wildcard characters.
 .Pp
 The timeout value
 .Dq interval
@@ -419,11 +421,20 @@ is specified in seconds or may use any of the units documented in the
 .Sx TIME FORMATS
 section.
 For example,
-.Dq session:*=5m
-would cause all sessions to terminate after five minutes of inactivity.
+.Dq session=5m
+would cause interactive sessions to terminate after five minutes of
+inactivity.
 Specifying a zero value disables the inactivity timeout.
 .Pp
-The available channel types include:
+The special timeout
+.Dq global
+Applies to all active channels, taken together.
+Traffic on any active channel will reset the timeout, but when the timeout
+expires then all open channels will be closed.
+Note that this global timeout is not matched by wildcards and must be
+specified explicitly.
+.Pp
+The available channel type names include:
 .Bl -tag -width Ds
 .It Cm agent-connection
 Open connections to
@@ -444,15 +455,15 @@ listening on behalf of a
 .Xr ssh 1
 remote forwarding, i.e.\&
 .Cm RemoteForward .
-.It Cm session:command
-Command execution sessions.
-.It Cm session:shell
-Interactive shell sessions.
-.It Cm session:subsystem:...
-Subsystem sessions, e.g. for
+.It Cm session
+The interactive main session, including shell session, command execution,
+.Xr scp 1 ,
 .Xr sftp 1 ,
-which could be identified as
-.Cm session:subsystem:sftp .
+etc.
+.It Cm tun-connection
+Open
+.Cm TunnelForward
+connections.
 .It Cm x11-connection
 Open X11 forwarding sessions.
 .El
@@ -466,9 +477,6 @@ 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