Download raw body.
ssh: support TCP_KEEPALIVE on forwarding sockets
Hi,
This builds on the last to diffs I sent to allow TCP forwarding sockets
to optionally enable SO_KEEPALIVE. This is controlled by extending the
existing ssh_config/sshd_config TCPKeepAlive option to accept some
additional arguments.
TCPKeepAlive=yes retains the current behaviour of enabling keepalives
for the connection socket. TCPKeepAlive=transport is a new synonym
for this.
TCPKeepAlive=all also enables keepalives for sockets opened by
-L / -R forwards. I didn't bother adding an option to enable them
for just the forwarding sockets and not the main transport one,
but that would be easy to add if someone could convince me that
it's useful.
ok?
diff --git a/channels.c b/channels.c
index b08ec3c..fa659f9 100644
--- a/channels.c
+++ b/channels.c
@@ -202,6 +202,9 @@ struct ssh_channels {
/* AF_UNSPEC or AF_INET or AF_INET6 */
int IPv4or6;
+ /* Set SO_KEEPALIVE on TCP connections */
+ int want_tcp_keepalive;
+
/* Channel timeouts by type */
struct ssh_channel_timeout *timeouts;
size_t ntimeouts;
@@ -328,6 +331,13 @@ channel_free_connect_ctx(Channel *c)
c->connect_ctx = NULL;
}
+/* Enable/disable TCP keepalives for X11 and port-forwarding sockets */
+void
+channel_set_tcp_keepalives(struct ssh *ssh, int on)
+{
+ ssh->chanctxt->want_tcp_keepalive = on;
+}
+
/*
* Add a timeout for open channels whose c->ctype (or c->xctype if it is set)
* match type_pattern.
@@ -1962,6 +1972,8 @@ channel_post_x11_listener(struct ssh *ssh, Channel *c)
return;
}
set_nodelay(newsock);
+ if (ssh->chanctxt->want_tcp_keepalive)
+ set_keepalive(newsock); /* logs errors */
remote_ipaddr = get_peer_ipaddr(newsock);
remote_port = get_peer_port(newsock);
snprintf(buf, sizeof buf, "X11 connection from %.200s port %d",
@@ -2090,8 +2102,11 @@ channel_post_port_listener(struct ssh *ssh, Channel *c)
c->notbefore = monotime() + 1;
return;
}
- if (c->host_port != PORT_STREAMLOCAL)
+ if (c->host_port != PORT_STREAMLOCAL) {
set_nodelay(newsock);
+ if (ssh->chanctxt->want_tcp_keepalive)
+ set_keepalive(newsock); /* logs errors */
+ }
nc = channel_new(ssh, rtype, nextstate, newsock, newsock, -1,
c->local_window_max, c->local_maxpacket, 0, rtype, 1);
nc->listening_port = c->listening_port;
@@ -4660,12 +4675,13 @@ channel_update_permission(struct ssh *ssh, int idx, int newport)
static int
connect_next(struct ssh *ssh, struct channel_connect *cctx)
{
- int sock, saved_errno;
+ int sock, sock_is_network, saved_errno;
struct sockaddr_un *sunaddr;
char ntop[NI_MAXHOST];
char strport[MAXIMUM(NI_MAXSERV, sizeof(sunaddr->sun_path))];
for (; cctx->ai; cctx->ai = cctx->ai->ai_next) {
+ sock_is_network = 0;
switch (cctx->ai->ai_family) {
case AF_UNIX:
/* unix:pathname instead of host:port */
@@ -4681,6 +4697,7 @@ connect_next(struct ssh *ssh, struct channel_connect *cctx)
error_f("getnameinfo failed");
continue;
}
+ sock_is_network = 1;
break;
default:
continue;
@@ -4697,6 +4714,8 @@ connect_next(struct ssh *ssh, struct channel_connect *cctx)
}
if (set_nonblock(sock) == -1)
fatal_f("set_nonblock(%d)", sock);
+ if (sock_is_network && ssh->chanctxt->want_tcp_keepalive)
+ set_keepalive(sock); /* logs errors */
if (connect(sock, cctx->ai->ai_addr,
cctx->ai->ai_addrlen) == -1 && errno != EINPROGRESS) {
debug_f("host %.100s ([%.100s]:%s): %.100s",
diff --git a/channels.h b/channels.h
index 103f725..1c04552 100644
--- a/channels.h
+++ b/channels.h
@@ -309,6 +309,7 @@ void channel_cancel_cleanup(struct ssh *, int);
int channel_close_fd(struct ssh *, Channel *, int *);
void channel_send_window_changes(struct ssh *);
int channel_has_bulk(struct ssh *);
+void channel_set_tcp_keepalives(struct ssh *, int);
/* channel inactivity timeouts */
void channel_add_timeout(struct ssh *, const char *, int);
diff --git a/monitor_wrap.c b/monitor_wrap.c
index e7bdf71..ba21a10 100644
--- a/monitor_wrap.c
+++ b/monitor_wrap.c
@@ -371,6 +371,8 @@ out:
mm_decode_activate_server_options(ssh, m);
server_process_permitopen(ssh);
server_process_channel_timeouts(ssh);
+ channel_set_tcp_keepalives(ssh,
+ options.tcp_keep_alive == SSH_KEEPALIVES_ALL);
kex_set_server_sig_algs(ssh, options.pubkey_accepted_algos);
ssh_packet_set_rekey_limits(ssh, options.rekey_limit,
options.rekey_interval);
diff --git a/readconf.c b/readconf.c
index c6484ae..c4e54bb 100644
--- a/readconf.c
+++ b/readconf.c
@@ -1077,6 +1077,15 @@ static const struct multistate multistate_compression[] = {
{ "no", COMP_NONE },
{ NULL, -1 }
};
+static const struct multistate multistate_keepalives[] = {
+ { "true", SSH_KEEPALIVES_TRANSPORT },
+ { "false", SSH_KEEPALIVES_OFF },
+ { "yes", SSH_KEEPALIVES_TRANSPORT },
+ { "no", SSH_KEEPALIVES_OFF },
+ { "transport", SSH_KEEPALIVES_TRANSPORT },
+ { "all", SSH_KEEPALIVES_ALL },
+ { NULL, -1 }
+};
/* XXX this will need to be replaced with a bitmask if we add more flags */
static const struct multistate multistate_warnweakcrypto[] = {
{ "true", 1 },
@@ -1336,7 +1345,8 @@ parse_time:
case oTCPKeepAlive:
intptr = &options->tcp_keep_alive;
- goto parse_flag;
+ multistate_ptr = multistate_keepalives;
+ goto parse_multistate;
case oNoHostAuthenticationForLocalhost:
intptr = &options->no_host_authentication_for_localhost;
@@ -2889,7 +2899,7 @@ fill_default_options(Options * options)
if (options->compression == -1)
options->compression = 0;
if (options->tcp_keep_alive == -1)
- options->tcp_keep_alive = 1;
+ options->tcp_keep_alive = SSH_KEEPALIVES_TRANSPORT;
if (options->port == -1)
options->port = 0; /* Filled in ssh_connect. */
if (options->address_family == -1)
@@ -3592,6 +3602,8 @@ fmt_intarg(OpCodes code, int val)
return fmt_multistate_int(val, multistate_yesnoaskconfirm);
case oPubkeyAuthentication:
return fmt_multistate_int(val, multistate_pubkey_auth);
+ case oTCPKeepAlive:
+ return fmt_multistate_int(val, multistate_keepalives);
case oFingerprintHash:
return ssh_digest_alg_name(val);
default:
diff --git a/readconf.h b/readconf.h
index dbcb417..895bf2b 100644
--- a/readconf.h
+++ b/readconf.h
@@ -235,6 +235,10 @@ typedef struct {
#define SSH_KEYSTROKE_CHAFF_MIN_MS 1024
#define SSH_KEYSTROKE_CHAFF_RNG_MS 2048
+#define SSH_KEEPALIVES_OFF 0
+#define SSH_KEEPALIVES_TRANSPORT 1
+#define SSH_KEEPALIVES_ALL 2
+
const char *kex_default_pk_alg(void);
char *ssh_connection_hash(const char *thishost, const char *host,
const char *portstr, const char *user, const char *jump_host);
diff --git a/servconf.c b/servconf.c
index 0f9c059..753c6f9 100644
--- a/servconf.c
+++ b/servconf.c
@@ -1068,6 +1068,15 @@ static const struct multistate multistate_tcpfwd[] = {
{ "local", FORWARD_LOCAL },
{ NULL, -1 }
};
+static const struct multistate multistate_keepalives[] = {
+ { "true", SSH_KEEPALIVES_TRANSPORT },
+ { "false", SSH_KEEPALIVES_OFF },
+ { "yes", SSH_KEEPALIVES_TRANSPORT },
+ { "no", SSH_KEEPALIVES_OFF },
+ { "transport", SSH_KEEPALIVES_TRANSPORT },
+ { "all", SSH_KEEPALIVES_ALL },
+ { NULL, -1 }
+};
static int
process_server_config_line_depth(ServerOptions *options, char *line,
@@ -1460,7 +1469,8 @@ process_server_config_line_depth(ServerOptions *options, char *line,
case sTCPKeepAlive:
intptr = &options->tcp_keep_alive;
- goto parse_flag;
+ multistate_ptr = multistate_keepalives;
+ goto parse_multistate;
case sPermitEmptyPasswords:
intptr = &options->permit_empty_passwd;
@@ -4001,6 +4011,8 @@ fmt_intarg(ServerOpCodes code, int val)
return fmt_multistate_int(val, multistate_tcpfwd);
case sIgnoreRhosts:
return fmt_multistate_int(val, multistate_ignore_rhosts);
+ case sTCPKeepAlive:
+ return fmt_multistate_int(val, multistate_keepalives);
case sFingerprintHash:
return ssh_digest_alg_name(val);
default:
diff --git a/servconf.h b/servconf.h
index 8ed333d..bd1ec18 100644
--- a/servconf.h
+++ b/servconf.h
@@ -57,6 +57,11 @@ struct sshbuf;
#define SSHD_DEFAULT_COMPRESSION COMP_NONE
#endif
+/* TCPKeepAlive flags */
+#define SSH_KEEPALIVES_OFF 0
+#define SSH_KEEPALIVES_TRANSPORT 1
+#define SSH_KEEPALIVES_ALL 2
+
struct ssh;
/*
@@ -177,7 +182,7 @@ SSHCONF_STRING(xauth_location, XAuthLocation, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
SSHCONF_INTFLAG(permit_tty, PermitTTY, SSHCFG_ALL, 1, SSHCFG_COPY_MATCH) \
SSHCONF_INTFLAG(permit_user_rc, PermitUserRC, SSHCFG_ALL, 1, SSHCFG_COPY_MATCH) \
SSHCONF_INTFLAG(strict_modes, StrictModes, SSHCFG_GLOBAL, 1, SSHCFG_COPY_NONE) \
-SSHCONF_INTFLAG(tcp_keep_alive, TCPKeepAlive, SSHCFG_GLOBAL, 1, SSHCFG_COPY_NONE) \
+SSHCONF_INTFLAG(tcp_keep_alive, TCPKeepAlive, SSHCFG_GLOBAL, SSH_KEEPALIVES_TRANSPORT, SSHCFG_COPY_NONE) \
SSHCONF_STRING(ciphers, Ciphers, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
SSHCONF_STRING(macs, Macs, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
SSHCONF_STRING(kex_algorithms, KexAlgorithms, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
diff --git a/ssh.c b/ssh.c
index f828625..98b61ca 100644
--- a/ssh.c
+++ b/ssh.c
@@ -1681,6 +1681,8 @@ main(int ac, char **av)
channel_add_timeout(ssh, cp, i);
free(cp);
}
+ channel_set_tcp_keepalives(ssh,
+ options.tcp_keep_alive == SSH_KEEPALIVES_ALL);
/* Open a connection to the remote host. */
if (ssh_connect(ssh, host, options.host_arg, addrs, &hostaddr,
diff --git a/ssh_config.5 b/ssh_config.5
index bdaa49c..e3c3c81 100644
--- a/ssh_config.5
+++ b/ssh_config.5
@@ -2078,25 +2078,32 @@ The possible values are: DAEMON, USER, AUTH, LOCAL0, LOCAL1, LOCAL2,
LOCAL3, LOCAL4, LOCAL5, LOCAL6, LOCAL7.
The default is USER.
.It Cm TCPKeepAlive
-Specifies whether the system should send TCP keepalive messages to the
-other side.
-If they are sent, death of the connection or crash of one
-of the machines will be properly noticed.
+Specifies whether the system should send keepalive messages on TCP sockets it
+has opened.
+If they are sent, failure of the connection or crash of one
+of the endpoins may more promptly detected.
However, this means that
-connections will die if the route is down temporarily, and some people
-find it annoying.
+connections may terminate if the connection is suffers a transient disruption.
.Pp
-The default is
+The argument must be
+.Cm transport
+to enable TCP keepalive messages on the SSH transport connection to the server,
.Cm yes
-(to send TCP keepalive messages), and the client will notice
-if the network goes down or the remote host dies.
-This is important in scripts, and many users want it too.
+which is an alias for
+.Cm transport ,
+.Cm all
+to enable TCP keepalive messages on all sockets opened by
+.Xr ssh 1 ,
+including sockets created for X11 or port forwarding, or
+.Cm no
+to disable TCP keepalive messages.
+The default is
+.Cm transport .
.Pp
-To disable TCP keepalive messages, the value should be set to
-.Cm no .
See also
.Cm ServerAliveInterval
-for protocol-level keepalives.
+for a more robust connection failure detection mechanism that works at the
+SSH protocol level.
.It Cm Tag
Specify a configuration tag name that may be later used by a
.Cm Match
diff --git a/sshd-auth.c b/sshd-auth.c
index 0d28ec0..549fd67 100644
--- a/sshd-auth.c
+++ b/sshd-auth.c
@@ -659,6 +659,8 @@ main(int ac, char **av)
/* Prepare the channels layer */
channel_init_channels(ssh);
channel_set_af(ssh, options.address_family);
+ channel_set_tcp_keepalives(ssh,
+ options.tcp_keep_alive == SSH_KEEPALIVES_ALL);
server_process_channel_timeouts(ssh);
server_process_permitopen(ssh);
diff --git a/sshd-session.c b/sshd-session.c
index db49983..30928fa 100644
--- a/sshd-session.c
+++ b/sshd-session.c
@@ -1081,6 +1081,8 @@ main(int ac, char **av)
/* Prepare the channels layer */
channel_init_channels(ssh);
channel_set_af(ssh, options.address_family);
+ channel_set_tcp_keepalives(ssh,
+ options.tcp_keep_alive == SSH_KEEPALIVES_ALL);
server_process_channel_timeouts(ssh);
server_process_permitopen(ssh);
diff --git a/sshd_config.5 b/sshd_config.5
index 51620b0..cf7b010 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -1985,26 +1985,32 @@ The possible values are: DAEMON, USER, AUTH, LOCAL0, LOCAL1, LOCAL2,
LOCAL3, LOCAL4, LOCAL5, LOCAL6, LOCAL7.
The default is AUTH.
.It Cm TCPKeepAlive
-Specifies whether the system should send TCP keepalive messages to the
-other side.
-If they are sent, death of the connection or crash of one
-of the machines will be properly noticed.
+Specifies whether the system should send keepalive messages on TCP sockets it
+has opened.
+If they are sent, failure of the connection or crash of one
+of the endpoins may more promptly detected.
However, this means that
-connections will die if the route is down temporarily, and some people
-find it annoying.
-On the other hand, if TCP keepalives are not sent,
-sessions may hang indefinitely on the server, leaving
-.Qq ghost
-users and consuming server resources.
+connections may terminate if the connection is suffers a transient disruption.
.Pp
-The default is
+The argument must be
+.Cm transport
+to enable TCP keepalive messages on the SSH transport connection to the client,
.Cm yes
-(to send TCP keepalive messages), and the server will notice
-if the network goes down or the client host crashes.
-This avoids infinitely hanging sessions.
+which is an alias for
+.Cm transport ,
+.Cm all
+to enable TCP keepalive messages on all sockets opened by
+.Xr sshd 8 ,
+including sockets created for X11 or port forwarding, or
+.Cm no
+to disable TCP keepalive messages.
+The default is
+.Cm transport .
.Pp
-To disable TCP keepalive messages, the value should be set to
-.Cm no .
+See also
+.Cm ClientAliveInterval
+for a more robust connection failure detection mechanism that works at the
+SSH protocol level.
.It Cm TrustedUserCAKeys
Specifies a file containing public keys of certificate authorities that are
trusted to sign user certificates for authentication, or
ssh: support TCP_KEEPALIVE on forwarding sockets