Implement channel inactivity timeouts
authordjm <djm@openbsd.org>
Fri, 6 Jan 2023 02:47:18 +0000 (02:47 +0000)
committerdjm <djm@openbsd.org>
Fri, 6 Jan 2023 02:47:18 +0000 (02:47 +0000)
This adds a sshd_config ChannelTimeouts directive that allows channels that
have not seen traffic in a configurable interval to be automatically closed.
Different timeouts may be applied to session, X11, agent and TCP forwarding
channels.

Note: this only affects channels over an opened SSH connection and not
the connection itself. Most clients close the connection when their channels
go away, with a notable exception being ssh(1) in multiplexing mode.

ok markus dtucker

usr.bin/ssh/channels.c
usr.bin/ssh/channels.h
usr.bin/ssh/monitor_wrap.c
usr.bin/ssh/servconf.c
usr.bin/ssh/servconf.h
usr.bin/ssh/sshd.c
usr.bin/ssh/sshd_config.5

index 1fff7bf..c7b4c2a 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: channels.c,v 1.425 2023/01/06 02:42:34 djm Exp $ */
+/* $OpenBSD: channels.c,v 1.426 2023/01/06 02:47:18 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -143,6 +143,12 @@ struct permission_set {
        int all_permitted;
 };
 
+/* Used to record timeouts per channel type */
+struct ssh_channel_timeout {
+       char *type_pattern;
+       u_int timeout_secs;
+};
+
 /* Master structure for channels state */
 struct ssh_channels {
        /*
@@ -196,6 +202,10 @@ struct ssh_channels {
 
        /* AF_UNSPEC or AF_INET or AF_INET6 */
        int IPv4or6;
+
+       /* Channel timeouts by type */
+       struct ssh_channel_timeout *timeouts;
+       size_t ntimeouts;
 };
 
 /* helper */
@@ -288,10 +298,59 @@ channel_lookup(struct ssh *ssh, int id)
        return NULL;
 }
 
+/*
+ * Add a timeout for open channels whose c->ctype (or c->xctype if it is set)
+ * match type_pattern.
+ */
+void
+channel_add_timeout(struct ssh *ssh, const char *type_pattern,
+    u_int timeout_secs)
+{
+       struct ssh_channels *sc = ssh->chanctxt;
+
+       debug2_f("channel type \"%s\" timeout %u seconds",
+           type_pattern, timeout_secs);
+       sc->timeouts = xrecallocarray(sc->timeouts, sc->ntimeouts,
+           sc->ntimeouts + 1, sizeof(*sc->timeouts));
+       sc->timeouts[sc->ntimeouts].type_pattern = xstrdup(type_pattern);
+       sc->timeouts[sc->ntimeouts].timeout_secs = timeout_secs;
+       sc->ntimeouts++;
+}
+
+/* Clears all previously-added channel timeouts */
+void
+channel_clear_timeouts(struct ssh *ssh)
+{
+       struct ssh_channels *sc = ssh->chanctxt;
+       size_t i;
+
+       debug3_f("clearing");
+       for (i = 0; i < sc->ntimeouts; i++)
+               free(sc->timeouts[i].type_pattern);
+       free(sc->timeouts);
+       sc->timeouts = NULL;
+       sc->ntimeouts = 0;
+}
+
+static u_int
+lookup_timeout(struct ssh *ssh, const char *type)
+{
+       struct ssh_channels *sc = ssh->chanctxt;
+       size_t i;
+
+       for (i = 0; i < sc->ntimeouts; i++) {
+               if (match_pattern(type, sc->timeouts[i].type_pattern))
+                       return sc->timeouts[i].timeout_secs;
+       }
+
+       return 0;
+}
+
 /*
  * Sets "extended type" of a channel; used by session layer to add additional
  * information about channel types (e.g. shell, login, subsystem) that can then
  * be used to select timeouts.
+ * Will reset c->inactive_deadline as a side-effect.
  */
 void
 channel_set_xtype(struct ssh *ssh, int id, const char *xctype)
@@ -303,7 +362,10 @@ channel_set_xtype(struct ssh *ssh, int id, const char *xctype)
        if (c->xctype != NULL)
                free(c->xctype);
        c->xctype = xstrdup(xctype);
-       debug2_f("labeled channel %d as %s", id, xctype);
+       /* Type has changed, so look up inactivity deadline again */
+       c->inactive_deadline = lookup_timeout(ssh, c->xctype);
+       debug2_f("labeled channel %d as %s (inactive timeout %u)", id, xctype,
+           c->inactive_deadline);
 }
 
 /*
@@ -421,8 +483,10 @@ channel_new(struct ssh *ssh, char *ctype, int type, int rfd, int wfd, int efd,
        c->remote_name = xstrdup(remote_name);
        c->ctl_chan = -1;
        c->delayed = 1;         /* prevent call to channel_post handler */
+       c->inactive_deadline = lookup_timeout(ssh, c->ctype);
        TAILQ_INIT(&c->status_confirms);
-       debug("channel %d: new [%s]", found, remote_name);
+       debug("channel %d: new %s [%s] (inactive timeout: %u)",
+           found, c->ctype, remote_name, c->inactive_deadline);
        return c;
 }
 
@@ -1095,6 +1159,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();
        c->local_window = c->local_window_max = window_max;
 
        if ((r = sshpkt_start(ssh, SSH2_MSG_CHANNEL_WINDOW_ADJUST)) != 0 ||
@@ -1251,6 +1316,9 @@ channel_force_close(struct ssh *ssh, Channel *c, int abandon)
                channel_close_fd(ssh, c, &c->efd);
        if (abandon)
                c->type = SSH_CHANNEL_ABANDONED;
+       /* exempt from inactivity timeouts */
+       c->inactive_deadline = 0;
+       c->lastused = 0;
 }
 
 static void
@@ -1262,6 +1330,7 @@ channel_pre_x11_open(struct ssh *ssh, Channel *c)
 
        if (ret == 1) {
                c->type = SSH_CHANNEL_OPEN;
+               c->lastused = monotime();
                channel_pre_open(ssh, c);
        } else if (ret == -1) {
                logit("X11 connection rejected because of wrong "
@@ -1905,6 +1974,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();
                if (isopen) {
                        /* no message necessary */
                } else {
@@ -1953,7 +2023,7 @@ channel_handle_rfd(struct ssh *ssh, Channel *c)
        char buf[CHAN_RBUF];
        ssize_t len;
        int r;
-       size_t have, avail, maxlen = CHANNEL_MAX_READ;
+       size_t nr = 0, have, avail, maxlen = CHANNEL_MAX_READ;
 
        if ((c->io_ready & SSH_CHAN_IO_RFD) == 0)
                return 1; /* Shouldn't happen */
@@ -1974,13 +2044,15 @@ channel_handle_rfd(struct ssh *ssh, Channel *c)
                }
                if (maxlen > avail)
                        maxlen = avail;
-               if ((r = sshbuf_read(c->rfd, c->input, maxlen, NULL)) != 0) {
+               if ((r = sshbuf_read(c->rfd, c->input, maxlen, &nr)) != 0) {
                        if (errno == EINTR || errno == EAGAIN)
                                return 1;
                        debug2("channel %d: read failed rfd %d maxlen %zu: %s",
                            c->self, c->rfd, maxlen, ssh_err(r));
                        goto rfail;
                }
+               if (nr != 0)
+                       c->lastused = monotime();
                return 1;
        }
 
@@ -2001,6 +2073,7 @@ channel_handle_rfd(struct ssh *ssh, Channel *c)
                }
                return -1;
        }
+       c->lastused = monotime();
        if (c->input_filter != NULL) {
                if (c->input_filter(ssh, c, buf, len) == -1) {
                        debug2("channel %d: filter stops", c->self);
@@ -2071,6 +2144,7 @@ channel_handle_wfd(struct ssh *ssh, Channel *c)
                }
                return -1;
        }
+       c->lastused = monotime();
        if (c->isatty && dlen >= 1 && buf[0] != '\r') {
                if (tcgetattr(c->wfd, &tio) == 0 &&
                    !(tio.c_lflag & ECHO) && (tio.c_lflag & ICANON)) {
@@ -2116,6 +2190,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();
        }
        return 1;
 }
@@ -2137,7 +2212,10 @@ channel_handle_efd_read(struct ssh *ssh, Channel *c)
        if (len <= 0) {
                debug2("channel %d: closing read-efd %d", c->self, c->efd);
                channel_close_fd(ssh, c, &c->efd);
-       } else if (c->extended_usage == CHAN_EXTENDED_IGNORE)
+               return 1;
+       }
+       c->lastused = monotime();
+       if (c->extended_usage == CHAN_EXTENDED_IGNORE)
                debug3("channel %d: discard efd", c->self);
        else if ((r = sshbuf_put(c->extended, buf, len)) != 0)
                fatal_fr(r, "channel %i: append", c->self);
@@ -2426,14 +2504,29 @@ channel_handler(struct ssh *ssh, int table, struct timespec *timeout)
                                continue;
                }
                if (ftab[c->type] != NULL) {
-                       /*
-                        * Run handlers that are not paused.
-                        */
-                       if (c->notbefore <= now)
+                       if (table == CHAN_PRE &&
+                           c->type == SSH_CHANNEL_OPEN &&
+                           c->inactive_deadline != 0 && c->lastused != 0 &&
+                           now >= c->lastused + c->inactive_deadline) {
+                               /* channel closed for inactivity */
+                               verbose("channel %d: closing after %u seconds "
+                                   "of inactivity", c->self,
+                                   c->inactive_deadline);
+                               channel_force_close(ssh, c, 1);
+                       } else if (c->notbefore <= now) {
+                               /* Run handlers that are not paused. */
                                (*ftab[c->type])(ssh, c);
-                       else if (timeout != NULL) {
+                               /* inactivity timeouts must interrupt poll() */
+                               if (timeout != NULL &&
+                                   c->type == SSH_CHANNEL_OPEN &&
+                                   c->lastused != 0 &&
+                                   c->inactive_deadline != 0) {
+                                       ptimeout_deadline_monotime(timeout,
+                                           c->lastused + c->inactive_deadline);
+                               }
+                       } else if (timeout != NULL) {
                                /*
-                                * Arrange for poll wakeup when channel pause
+                                * Arrange for poll() wakeup when channel pause
                                 * timer expires.
                                 */
                                ptimeout_deadline_monotime(timeout,
@@ -3370,6 +3463,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();
        debug2("channel %d: open confirm rwindow %u rmax %u", c->self,
            c->remote_window, c->remote_maxpacket);
        return 0;
index c51c104..8d4699a 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: channels.h,v 1.146 2023/01/06 02:42:34 djm Exp $ */
+/* $OpenBSD: channels.h,v 1.147 2023/01/06 02:47:18 djm Exp $ */
 
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -200,6 +200,13 @@ struct Channel {
        void                    *mux_ctx;
        int                     mux_pause;
        int                     mux_downstream_id;
+
+       /* Inactivity timeouts */
+
+       /* Last traffic seen for OPEN channels */
+       time_t                  lastused;
+       /* Inactivity timeout deadline in seconds (0 = no timeout) */
+       u_int                   inactive_deadline;
 };
 
 #define CHAN_EXTENDED_IGNORE           0
@@ -296,6 +303,10 @@ void        channel_cancel_cleanup(struct ssh *, int);
 int     channel_close_fd(struct ssh *, Channel *, int *);
 void    channel_send_window_changes(struct ssh *);
 
+/* channel inactivity timeouts */
+void channel_add_timeout(struct ssh *, const char *, u_int);
+void channel_clear_timeouts(struct ssh *);
+
 /* mux proxy support */
 
 int     channel_proxy_downstream(struct ssh *, Channel *mc);
index 812c72e..5f5a720 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: monitor_wrap.c,v 1.125 2022/06/15 16:08:25 djm Exp $ */
+/* $OpenBSD: monitor_wrap.c,v 1.126 2023/01/06 02:47:18 djm Exp $ */
 /*
  * Copyright 2002 Niels Provos <provos@citi.umich.edu>
  * Copyright 2002 Markus Friedl <markus@openbsd.org>
@@ -326,6 +326,7 @@ out:
        for (i = 0; i < options.num_log_verbose; i++)
                log_verbose_add(options.log_verbose[i]);
        process_permitopen(ssh, &options);
+       process_channel_timeouts(ssh, &options);
        free(newopts);
 
        sshbuf_free(m);
index 78dcf44..8343ae5 100644 (file)
@@ -1,5 +1,5 @@
 
-/* $OpenBSD: servconf.c,v 1.388 2022/11/07 10:05:39 dtucker Exp $ */
+/* $OpenBSD: servconf.c,v 1.389 2023/01/06 02:47:18 djm Exp $ */
 /*
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
  *                    All rights reserved
@@ -178,6 +178,8 @@ initialize_server_options(ServerOptions *options)
        options->disable_forwarding = -1;
        options->expose_userauth_info = -1;
        options->required_rsa_size = -1;
+       options->channel_timeouts = NULL;
+       options->num_channel_timeouts = 0;
 }
 
 /* Returns 1 if a string option is unset or set to "none" or 0 otherwise. */
@@ -433,6 +435,16 @@ fill_default_server_options(ServerOptions *options)
                        v = NULL; \
                } \
        } while(0)
+#define CLEAR_ON_NONE_ARRAY(v, nv, none) \
+       do { \
+               if (options->nv == 1 && \
+                   strcasecmp(options->v[0], none) == 0) { \
+                       free(options->v[0]); \
+                       free(options->v); \
+                       options->v = NULL; \
+                       options->nv = 0; \
+               } \
+       } while (0)
        CLEAR_ON_NONE(options->pid_file);
        CLEAR_ON_NONE(options->xauth_location);
        CLEAR_ON_NONE(options->banner);
@@ -444,19 +456,16 @@ 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);
+
        for (i = 0; i < options->num_host_key_files; i++)
                CLEAR_ON_NONE(options->host_key_files[i]);
        for (i = 0; i < options->num_host_cert_files; i++)
                CLEAR_ON_NONE(options->host_cert_files[i]);
-#undef CLEAR_ON_NONE
 
-       /* Similar handling for AuthenticationMethods=any */
-       if (options->num_auth_methods == 1 &&
-           strcmp(options->auth_methods[0], "any") == 0) {
-               free(options->auth_methods[0]);
-               options->auth_methods[0] = NULL;
-               options->num_auth_methods = 0;
-       }
+       CLEAR_ON_NONE_ARRAY(channel_timeouts, num_channel_timeouts, "none");
+       CLEAR_ON_NONE_ARRAY(auth_methods, num_auth_methods, "any");
+#undef CLEAR_ON_NONE
+#undef CLEAR_ON_NONE_ARRAY
 }
 
 /* Keyword tokens. */
@@ -492,7 +501,7 @@ typedef enum {
        sStreamLocalBindMask, sStreamLocalBindUnlink,
        sAllowStreamLocalForwarding, sFingerprintHash, sDisableForwarding,
        sExposeAuthInfo, sRDomain, sPubkeyAuthOptions, sSecurityKeyProvider,
-       sRequiredRSASize,
+       sRequiredRSASize, sChannelTimeout,
        sDeprecated, sIgnore, sUnsupported
 } ServerOpCodes;
 
@@ -637,6 +646,7 @@ static struct {
        { "casignaturealgorithms", sCASignatureAlgorithms, SSHCFG_ALL },
        { "securitykeyprovider", sSecurityKeyProvider, SSHCFG_GLOBAL },
        { "requiredrsasize", sRequiredRSASize, SSHCFG_ALL },
+       { "channeltimeout", sChannelTimeout, SSHCFG_ALL },
        { NULL, sBadOption, 0 }
 };
 
@@ -893,6 +903,58 @@ process_permitopen(struct ssh *ssh, ServerOptions *options)
            options->num_permitted_listens);
 }
 
+/* Parse a ChannelTimeout clause "pattern=interval" */
+static int
+parse_timeout(const char *s, char **typep, u_int *secsp)
+{
+       char *cp, *sdup;
+       int secs;
+
+       if (typep != NULL)
+               *typep = NULL;
+       if (secsp != NULL)
+               *secsp = 0;
+       if (s == NULL)
+               return -1;
+       sdup = xstrdup(s);
+
+       if ((cp = strchr(sdup, '=')) == NULL || cp == sdup) {
+               free(sdup);
+               return -1;
+       }
+       *cp++ = '\0';
+       if ((secs = convtime(cp)) < 0) {
+               free(sdup);
+               return -1;
+       }
+       /* success */
+       if (typep != NULL)
+               *typep = xstrdup(sdup);
+       if (secsp != NULL)
+               *secsp = (u_int)secs;
+       free(sdup);
+       return 0;
+}
+
+void
+process_channel_timeouts(struct ssh *ssh, ServerOptions *options)
+{
+       u_int i, secs;
+       char *type;
+
+       debug3_f("setting %u timeouts", options->num_channel_timeouts);
+       channel_clear_timeouts(ssh);
+       for (i = 0; i < options->num_channel_timeouts; i++) {
+               if (parse_timeout(options->channel_timeouts[i],
+                   &type, &secs) != 0) {
+                       fatal_f("internal error: bad timeout %s",
+                           options->channel_timeouts[i]);
+               }
+               channel_add_timeout(ssh, type, secs);
+               free(type);
+       }
+}
+
 struct connection_info *
 get_connection_info(struct ssh *ssh, int populate, int use_dns)
 {
@@ -2390,6 +2452,30 @@ process_server_config_line_depth(ServerOptions *options, char *line,
                intptr = &options->required_rsa_size;
                goto parse_int;
 
+       case sChannelTimeout:
+               uvalue = options->num_channel_timeouts;
+               i = 0;
+               while ((arg = argv_next(&ac, &av)) != NULL) {
+                       /* Allow "none" only in first position */
+                       if (strcasecmp(arg, "none") == 0) {
+                               if (i > 0 || ac > 0) {
+                                       error("%s line %d: keyword %s \"none\" "
+                                           "argument must appear alone.",
+                                           filename, linenum, keyword);
+                                       goto out;
+                               }
+                       } else if (parse_timeout(arg, NULL, NULL) != 0) {
+                               fatal("%s line %d: invalid channel timeout %s",
+                                   filename, linenum, arg);
+                       }
+                       if (!*activep || uvalue != 0)
+                               continue;
+                       opt_array_append(filename, linenum, keyword,
+                           &options->channel_timeouts,
+                           &options->num_channel_timeouts, arg);
+               }
+               break;
+
        case sDeprecated:
        case sIgnore:
        case sUnsupported:
@@ -2757,6 +2843,8 @@ dump_cfg_strarray_oneline(ServerOpCodes code, u_int count, char **vals)
                printf(" %s",  vals[i]);
        if (code == sAuthenticationMethods && count == 0)
                printf(" any");
+       else if (code == sChannelTimeout && count == 0)
+               printf(" none");
        printf("\n");
 }
 
@@ -2916,6 +3004,8 @@ dump_config(ServerOptions *o)
            o->num_auth_methods, o->auth_methods);
        dump_cfg_strarray_oneline(sLogVerbose,
            o->num_log_verbose, o->log_verbose);
+       dump_cfg_strarray_oneline(sChannelTimeout,
+           o->num_channel_timeouts, o->channel_timeouts);
 
        /* other arguments */
        for (i = 0; i < o->num_subsystems; i++)
index 990c412..f4dd15f 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: servconf.h,v 1.157 2022/09/17 10:34:29 djm Exp $ */
+/* $OpenBSD: servconf.h,v 1.158 2023/01/06 02:47:19 djm Exp $ */
 
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -228,6 +228,9 @@ typedef struct {
        u_int64_t timing_secret;
        char   *sk_provider;
        int     required_rsa_size;      /* minimum size of RSA keys */
+
+       char    **channel_timeouts;     /* inactivity timeout by channel type */
+       u_int   num_channel_timeouts;
 }       ServerOptions;
 
 /* Information about the incoming connection as used by Match */
@@ -285,6 +288,7 @@ TAILQ_HEAD(include_list, include_item);
                M_CP_STRARRAYOPT(auth_methods, num_auth_methods); \
                M_CP_STRARRAYOPT(permitted_opens, num_permitted_opens); \
                M_CP_STRARRAYOPT(permitted_listens, num_permitted_listens); \
+               M_CP_STRARRAYOPT(channel_timeouts, num_channel_timeouts); \
                M_CP_STRARRAYOPT(log_verbose, num_log_verbose); \
        } while (0)
 
@@ -294,6 +298,7 @@ void         fill_default_server_options(ServerOptions *);
 int     process_server_config_line(ServerOptions *, char *, const char *, int,
            int *, struct connection_info *, struct include_list *includes);
 void    process_permitopen(struct ssh *ssh, ServerOptions *options);
+void    process_channel_timeouts(struct ssh *ssh, ServerOptions *);
 void    load_server_config(const char *, struct sshbuf *);
 void    parse_server_config(ServerOptions *, const char *, struct sshbuf *,
            struct include_list *includes, struct connection_info *, int);
index b4451f5..bf0bba4 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: sshd.c,v 1.594 2022/12/16 06:56:47 djm Exp $ */
+/* $OpenBSD: sshd.c,v 1.595 2023/01/06 02:47:19 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -2027,6 +2027,7 @@ main(int ac, char **av)
        /* Prepare the channels layer */
        channel_init_channels(ssh);
        channel_set_af(ssh, options.address_family);
+       process_channel_timeouts(ssh, &options);
        process_permitopen(ssh, &options);
 
        /* Set SO_KEEPALIVE if requested. */
index 7fe1dce..5e054d1 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.343 2022/09/17 10:34:29 djm Exp $
-.Dd $Mdocdate: September 17 2022 $
+.\" $OpenBSD: sshd_config.5,v 1.344 2023/01/06 02:47:19 djm Exp $
+.Dd $Mdocdate: January 6 2023 $
 .Dt SSHD_CONFIG 5
 .Os
 .Sh NAME
@@ -396,6 +396,71 @@ from the default set instead of replacing them.
 .Pp
 Certificates signed using other algorithms will not be accepted for
 public key or host-based authentication.
+.It Cm ChannelTimeout
+Specifies whether and how quickly
+.Xr sshd 8
+should close inactive channels.
+Timeouts for 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.
+.Pp
+The timeout value
+.Dq interval
+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.
+Specifying a zero value disables the inactivity timeout.
+.Pp
+The available channel types include:
+.Bl -tag -width Ds
+.It Cm agent-connection
+Open connections to
+.Xr ssh-agent 1 .
+.It Cm direct-tcpip Cm direct-streamlocal@openssh.com
+Open TCP or Unix socket (respectively) connections that have
+been established from a
+.Xr ssh 1
+local forwarding, i.e.
+.Cm LocalForward or
+.Cm DynamicForward .
+.It Cm forwarded-tcpip Cm forwarded-streamlocal@openssh.com
+Open TCP or Unix socket (respectively) connections that have been
+established to a
+.Xr sshd 8
+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
+.Xr sftp 1 ,
+which could be identified as
+.Cm session:subsystem:sftp .
+.It Cm x11-connection
+Open X11 forwarding sessions.
+.El
+.Pp
+Note that, in all the above cases, terminating an inactive session does not
+guarantee to remove all resources associated with the session, e.g. shell
+processes or X11 clients relating to the session may continue to execute.
+.Pp
+Moreover, terminating an inactive channel or session does necessarily
+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.
+.Pp
+The default is not to expire channels of any type for inactivity.
 .It Cm ChrootDirectory
 Specifies the pathname of a directory to
 .Xr chroot 2