From: djm Date: Fri, 6 Jan 2023 02:47:18 +0000 (+0000) Subject: Implement channel inactivity timeouts X-Git-Url: http://artulab.com/gitweb/?a=commitdiff_plain;h=eb1834773e881283e57207db7c4301eaa3ab880a;p=openbsd Implement channel inactivity timeouts 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 --- diff --git a/usr.bin/ssh/channels.c b/usr.bin/ssh/channels.c index 1fff7bf3ef4..c7b4c2a235e 100644 --- a/usr.bin/ssh/channels.c +++ b/usr.bin/ssh/channels.c @@ -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 * Copyright (c) 1995 Tatu Ylonen , 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; diff --git a/usr.bin/ssh/channels.h b/usr.bin/ssh/channels.h index c51c104af6a..8d4699a6a53 100644 --- a/usr.bin/ssh/channels.h +++ b/usr.bin/ssh/channels.h @@ -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 @@ -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); diff --git a/usr.bin/ssh/monitor_wrap.c b/usr.bin/ssh/monitor_wrap.c index 812c72ec5b0..5f5a72001ae 100644 --- a/usr.bin/ssh/monitor_wrap.c +++ b/usr.bin/ssh/monitor_wrap.c @@ -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 * Copyright 2002 Markus Friedl @@ -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); diff --git a/usr.bin/ssh/servconf.c b/usr.bin/ssh/servconf.c index 78dcf447220..8343ae5873c 100644 --- a/usr.bin/ssh/servconf.c +++ b/usr.bin/ssh/servconf.c @@ -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 , 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++) diff --git a/usr.bin/ssh/servconf.h b/usr.bin/ssh/servconf.h index 990c4121cc8..f4dd15ffc43 100644 --- a/usr.bin/ssh/servconf.h +++ b/usr.bin/ssh/servconf.h @@ -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 @@ -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); diff --git a/usr.bin/ssh/sshd.c b/usr.bin/ssh/sshd.c index b4451f5bb0a..bf0bba41398 100644 --- a/usr.bin/ssh/sshd.c +++ b/usr.bin/ssh/sshd.c @@ -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 * Copyright (c) 1995 Tatu Ylonen , 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. */ diff --git a/usr.bin/ssh/sshd_config.5 b/usr.bin/ssh/sshd_config.5 index 7fe1dce1968..5e054d137d8 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.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