upstream: 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

OpenBSD-Commit-ID: ae8bba3ed9d9f95ff2e2dc8dcadfa36b48e6c0b8
This commit is contained in:
djm@openbsd.org 2023-01-06 02:47:18 +00:00 committed by Damien Miller
parent 0e34348d0b
commit 2d1ff2b943
No known key found for this signature in database
7 changed files with 295 additions and 28 deletions

View 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
@ -151,6 +151,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 {
/*
@ -204,6 +210,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 */
@ -296,10 +306,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)
@ -311,7 +370,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);
}
/*
@ -433,8 +495,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;
}
@ -1107,6 +1171,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 ||
@ -1263,6 +1328,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
@ -1274,6 +1342,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 "
@ -1917,6 +1986,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 {
@ -1965,7 +2035,7 @@ channel_handle_rfd(struct ssh *ssh, Channel *c)
char buf[CHAN_RBUF];
ssize_t len;
int r, force;
size_t have, avail, maxlen = CHANNEL_MAX_READ;
size_t nr = 0, have, avail, maxlen = CHANNEL_MAX_READ;
int pty_zeroread = 0;
#ifdef PTY_ZEROREAD
@ -1994,7 +2064,7 @@ 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 || (!force &&
(errno == EAGAIN || errno == EWOULDBLOCK)))
return 1;
@ -2002,6 +2072,8 @@ channel_handle_rfd(struct ssh *ssh, Channel *c)
c->self, c->rfd, maxlen, ssh_err(r));
goto rfail;
}
if (nr != 0)
c->lastused = monotime();
return 1;
}
@ -2027,6 +2099,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);
@ -2107,6 +2180,7 @@ channel_handle_wfd(struct ssh *ssh, Channel *c)
}
return -1;
}
c->lastused = monotime();
#ifndef BROKEN_TCGETATTR_ICANON
if (c->isatty && dlen >= 1 && buf[0] != '\r') {
if (tcgetattr(c->wfd, &tio) == 0 &&
@ -2155,6 +2229,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;
}
@ -2179,7 +2254,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);
@ -2468,14 +2546,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,
@ -3412,6 +3505,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;

View 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>
@ -203,6 +203,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
@ -299,6 +306,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);

View 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>
@ -339,6 +339,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);

View 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
@ -196,6 +196,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. */
@ -458,6 +460,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);
@ -469,19 +481,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. */
@ -520,7 +529,7 @@ typedef enum {
sStreamLocalBindMask, sStreamLocalBindUnlink,
sAllowStreamLocalForwarding, sFingerprintHash, sDisableForwarding,
sExposeAuthInfo, sRDomain, sPubkeyAuthOptions, sSecurityKeyProvider,
sRequiredRSASize,
sRequiredRSASize, sChannelTimeout,
sDeprecated, sIgnore, sUnsupported
} ServerOpCodes;
@ -681,6 +690,7 @@ static struct {
{ "casignaturealgorithms", sCASignatureAlgorithms, SSHCFG_ALL },
{ "securitykeyprovider", sSecurityKeyProvider, SSHCFG_GLOBAL },
{ "requiredrsasize", sRequiredRSASize, SSHCFG_ALL },
{ "channeltimeout", sChannelTimeout, SSHCFG_ALL },
{ NULL, sBadOption, 0 }
};
@ -944,6 +954,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)
{
@ -2451,6 +2513,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:
@ -2818,6 +2904,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");
}
@ -2986,6 +3074,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++)

View 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>
@ -230,6 +230,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 */
@ -287,6 +290,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)
@ -296,6 +300,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);

3
sshd.c
View 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
@ -2158,6 +2158,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. */

View 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
@ -395,6 +395,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