ssh/sshd: support TCP keepalives on forwarding sockets too

Damien Miller <[email protected]>
Newsgroups gmane.os.openbsd.tech
Message-ID <[email protected]>
Hi,

ssh and sshd have both long supported a TCPKeepAlive option for enabling
SO_KEEPALIVE on the main connection socket. A few people (e.g. [1]) have
asked for this on forwarding sockets too.

This extends the existing option to support this case, as
TCPKeepAlive=all

ok?

Also fixes a minor memory leak in the channels code if a channel gets
closed while still in the connect() phase.

[1] https://bugzilla.mindrot.org/show_bug.cgi?id=3921

diff --git a/channels.c b/channels.c
index 2066aee..ab97097 100644
--- a/channels.c
+++ b/channels.c
@@ -109,6 +109,13 @@ struct permission {
 	Channel *downstream;		/* Downstream mux*/
 };
 
+/* Context for non-blocking connects */
+struct channel_connect {
+	char *host;
+	int port;
+	struct addrinfo *ai, *aitop;
+};
+
 /*
  * Stores the forwarding permission state for a single direction (local or
  * remote).
@@ -195,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;
@@ -212,8 +222,7 @@ static void port_open_helper(struct ssh *ssh, Channel *c, char *rtype);
 static const char *channel_rfwd_bind_host(const char *listen_host);
 
 /* non-blocking connect helpers */
-static int connect_next(struct channel_connect *);
-static void channel_connect_ctx_free(struct channel_connect *);
+static int connect_next(struct ssh *, struct channel_connect *);
 static Channel *rdynamic_connect_prepare(struct ssh *, char *, char *);
 static int rdynamic_connect_finish(struct ssh *, Channel *);
 
@@ -299,6 +308,36 @@ channel_lookup(struct ssh *ssh, int id)
 	return NULL;
 }
 
+static void
+free_connect_ctx(struct channel_connect *cctx)
+{
+	free(cctx->host);
+	if (cctx->aitop) {
+		if (cctx->aitop->ai_family == AF_UNIX)
+			free(cctx->aitop);
+		else
+			freeaddrinfo(cctx->aitop);
+	}
+	memset(cctx, 0, sizeof(*cctx));
+}
+
+static void
+channel_free_connect_ctx(Channel *c)
+{
+	if (c == NULL || c->connect_ctx)
+		return
+	free_connect_ctx(c->connect_ctx);
+	free(c->connect_ctx);
+	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.
@@ -805,6 +844,7 @@ channel_free(struct ssh *ssh, Channel *c)
 	c->listening_addr = NULL;
 	free(c->xctype);
 	c->xctype = NULL;
+	channel_free_connect_ctx(c);
 	while ((cc = TAILQ_FIRST(&c->status_confirms)) != NULL) {
 		if (cc->abandon_cb != NULL)
 			cc->abandon_cb(ssh, c, cc->ctx);
@@ -1928,6 +1968,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",
@@ -2056,8 +2098,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;
@@ -2112,6 +2157,9 @@ channel_post_connecting(struct ssh *ssh, Channel *c)
 		return;
 	if (!c->have_remote_id)
 		fatal_f("channel %d: no remote id", c->self);
+	if (c->connect_ctx == NULL)
+		fatal_f("channel %d: context missing", c->self);
+
 	/* for rdynamic the OPEN_CONFIRMATION has been sent already */
 	isopen = (c->type == SSH_CHANNEL_RDYNAMIC_FINISH);
 
@@ -2123,8 +2171,8 @@ channel_post_connecting(struct ssh *ssh, Channel *c)
 	if (err == 0) {
 		/* Non-blocking connection completed */
 		debug("channel %d: connected to %s port %d",
-		    c->self, c->connect_ctx.host, c->connect_ctx.port);
-		channel_connect_ctx_free(&c->connect_ctx);
+		    c->self, c->connect_ctx->host, c->connect_ctx->port);
+		channel_free_connect_ctx(c);
 		c->type = SSH_CHANNEL_OPEN;
 		channel_set_used_time(ssh, c);
 		if (isopen) {
@@ -2148,11 +2196,11 @@ channel_post_connecting(struct ssh *ssh, Channel *c)
 	debug("channel %d: connection failed: %s", c->self, strerror(err));
 
 	/* Try next address, if any */
-	if ((sock = connect_next(&c->connect_ctx)) == -1) {
+	if ((sock = connect_next(ssh, c->connect_ctx)) == -1) {
 		/* Exhausted all addresses for this destination */
 		error("connect_to %.100s port %d: failed.",
-		    c->connect_ctx.host, c->connect_ctx.port);
-		channel_connect_ctx_free(&c->connect_ctx);
+		    c->connect_ctx->host, c->connect_ctx->port);
+		channel_free_connect_ctx(c);
 		if (isopen) {
 			rdynamic_close(ssh, c);
 		} else {
@@ -4621,14 +4669,15 @@ channel_update_permission(struct ssh *ssh, int idx, int newport)
 
 /* Try to start non-blocking connect to next host in cctx list */
 static int
-connect_next(struct channel_connect *cctx)
+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 */
@@ -4644,6 +4693,7 @@ connect_next(struct channel_connect *cctx)
 				error_f("getnameinfo failed");
 				continue;
 			}
+			sock_is_network = 1;
 			break;
 		default:
 			continue;
@@ -4660,6 +4710,8 @@ connect_next(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",
@@ -4679,19 +4731,6 @@ connect_next(struct channel_connect *cctx)
 	return -1;
 }
 
-static void
-channel_connect_ctx_free(struct channel_connect *cctx)
-{
-	free(cctx->host);
-	if (cctx->aitop) {
-		if (cctx->aitop->ai_family == AF_UNIX)
-			free(cctx->aitop);
-		else
-			freeaddrinfo(cctx->aitop);
-	}
-	memset(cctx, 0, sizeof(*cctx));
-}
-
 /*
  * Return connecting socket to remote host:port or local socket path,
  * passing back the failure reason if appropriate.
@@ -4717,7 +4756,7 @@ connect_to_helper(struct ssh *ssh, const char *name, int port, int socktype,
 
 		/*
 		 * Fake up a struct addrinfo for AF_UNIX connections.
-		 * channel_connect_ctx_free() must check ai_family
+		 * free_connect_ctx() must check ai_family
 		 * and use free() not freeaddrinfo() for AF_UNIX.
 		 */
 		ai = xcalloc(1, sizeof(*ai) + sizeof(*sunaddr));
@@ -4751,7 +4790,7 @@ connect_to_helper(struct ssh *ssh, const char *name, int port, int socktype,
 	cctx->port = port;
 	cctx->ai = cctx->aitop;
 
-	if ((sock = connect_next(cctx)) == -1) {
+	if ((sock = connect_next(ssh, cctx)) == -1) {
 		error("connect to %.100s port %d failed: %s",
 		    name, port, strerror(errno));
 		return -1;
@@ -4773,14 +4812,15 @@ connect_to(struct ssh *ssh, const char *host, int port,
 	sock = connect_to_helper(ssh, host, port, SOCK_STREAM, ctype, rname,
 	    &cctx, NULL, NULL);
 	if (sock == -1) {
-		channel_connect_ctx_free(&cctx);
+		free_connect_ctx(&cctx);
 		return NULL;
 	}
 	c = channel_new(ssh, ctype, SSH_CHANNEL_CONNECTING, sock, sock, -1,
 	    CHAN_TCP_WINDOW_DEFAULT, CHAN_TCP_PACKET_DEFAULT, 0, rname, 1);
 	c->host_port = port;
 	c->path = xstrdup(host);
-	c->connect_ctx = cctx;
+	c->connect_ctx = xmalloc(sizeof(cctx));
+	memcpy(c->connect_ctx, &cctx, sizeof(cctx));
 
 	return c;
 }
@@ -4887,7 +4927,7 @@ channel_connect_to_port(struct ssh *ssh, const char *host, u_short port,
 	sock = connect_to_helper(ssh, host, port, SOCK_STREAM, ctype, rname,
 	    &cctx, reason, errmsg);
 	if (sock == -1) {
-		channel_connect_ctx_free(&cctx);
+		free_connect_ctx(&cctx);
 		return NULL;
 	}
 
@@ -4895,7 +4935,8 @@ channel_connect_to_port(struct ssh *ssh, const char *host, u_short port,
 	    CHAN_TCP_WINDOW_DEFAULT, CHAN_TCP_PACKET_DEFAULT, 0, rname, 1);
 	c->host_port = port;
 	c->path = xstrdup(host);
-	c->connect_ctx = cctx;
+	c->connect_ctx = xmalloc(sizeof(cctx));
+	memcpy(c->connect_ctx, &cctx, sizeof(cctx));
 
 	return c;
 }
@@ -5019,11 +5060,12 @@ rdynamic_connect_finish(struct ssh *ssh, Channel *c)
 	sock = connect_to_helper(ssh, c->path, c->host_port, SOCK_STREAM, NULL,
 	    NULL, &cctx, NULL, NULL);
 	if (sock == -1)
-		channel_connect_ctx_free(&cctx);
+		free_connect_ctx(&cctx);
 	else {
 		/* similar to SSH_CHANNEL_CONNECTING but we've already sent the open */
 		c->type = SSH_CHANNEL_RDYNAMIC_FINISH;
-		c->connect_ctx = cctx;
+		c->connect_ctx = xmalloc(sizeof(cctx));
+		memcpy(c->connect_ctx, &cctx, sizeof(cctx));
 		channel_register_fds(ssh, c, sock, sock, -1, 0, 1, 0);
 	}
 	return sock;
diff --git a/channels.h b/channels.h
index 9027640..a514098 100644
--- a/channels.h
+++ b/channels.h
@@ -90,6 +90,7 @@
 struct ssh;
 struct Channel;
 typedef struct Channel Channel;
+struct channel_connect;
 
 typedef void channel_open_fn(struct ssh *, int, int, void *);
 typedef void channel_callback_fn(struct ssh *, int, int, void *);
@@ -109,13 +110,6 @@ struct channel_confirm {
 };
 TAILQ_HEAD(channel_confirms, channel_confirm);
 
-/* Context for non-blocking connects */
-struct channel_connect {
-	char *host;
-	int port;
-	struct addrinfo *ai, *aitop;
-};
-
 /* Callbacks for mux channels back into client-specific code */
 typedef int mux_callback_fn(struct ssh *, struct Channel *);
 
@@ -203,8 +197,7 @@ struct Channel {
 	int			datagram;
 
 	/* non-blocking connect */
-	/* XXX make this a pointer so the structure can be opaque */
-	struct channel_connect	connect_ctx;
+	struct channel_connect	*connect_ctx;
 
 	/* multiplexing protocol hook, called for each packet received */
 	mux_callback_fn		*mux_rcb;
@@ -316,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/misc.c b/misc.c
index 4ce2e4e..34df55c 100644
--- a/misc.c
+++ b/misc.c
@@ -233,6 +233,19 @@ set_reuseaddr(int fd)
 	return 0;
 }
 
+/* Set TCP keepalives */
+int
+set_keepalive(int fd)
+{
+	int on = 1;
+
+	if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)) == -1) {
+		error("setsockopt SO_KEEPALIVE fd %d: %s", fd, strerror(errno));
+		return -1;
+	}
+	return 0;
+}
+
 /* Get/set routing domain */
 char *
 get_rdomain(int fd)
diff --git a/misc.h b/misc.h
index 6ec6c3a..8426cae 100644
--- a/misc.h
+++ b/misc.h
@@ -65,6 +65,7 @@ int	 set_nonblock(int);
 int	 unset_nonblock(int);
 void	 set_nodelay(int);
 int	 set_reuseaddr(int);
+int	 set_keepalive(int);
 char	*get_rdomain(int);
 int	 set_rdomain(int, const char *);
 int	 get_sock_af(int);
diff --git a/monitor_wrap.c b/monitor_wrap.c
index 7e8149d..4a635f3 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);
 	sshbuf_free(m);
 
diff --git a/readconf.c b/readconf.c
index 57b5cee..7dfe2b3 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)
@@ -3590,6 +3600,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 f9ea23a..9038824 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 f6c4672..9ca25ae 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 de75dcf..d718833 100644
--- a/ssh.c
+++ b/ssh.c
@@ -1665,6 +1665,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 50962fa..81b97c1 100644
--- a/ssh_config.5
+++ b/ssh_config.5
@@ -2065,25 +2065,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/sshconnect.c b/sshconnect.c
index 87726f2..63b3d22 100644
--- a/sshconnect.c
+++ b/sshconnect.c
@@ -422,7 +422,7 @@ ssh_connect_direct(struct ssh *ssh, const char *host, struct addrinfo *aitop,
     struct sockaddr_storage *hostaddr, u_short port, int connection_attempts,
     int *timeout_ms, int want_keepalive)
 {
-	int on = 1, saved_timeout_ms = *timeout_ms;
+	int saved_timeout_ms = *timeout_ms;
 	int oerrno, sock = -1, attempt;
 	char ntop[NI_MAXHOST], strport[NI_MAXSERV];
 	struct addrinfo *ai;
@@ -503,10 +503,8 @@ ssh_connect_direct(struct ssh *ssh, const char *host, struct addrinfo *aitop,
 	debug("Connection established.");
 
 	/* Set SO_KEEPALIVE if requested. */
-	if (want_keepalive &&
-	    setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (void *)&on,
-	    sizeof(on)) == -1)
-		error("setsockopt SO_KEEPALIVE: %.100s", strerror(errno));
+	if (want_keepalive)
+		set_keepalive(sock); /* logs errors */
 
 	/* Set the connection. */
 	if (ssh_packet_set_connection(ssh, sock, sock) == NULL)
diff --git a/sshd-auth.c b/sshd-auth.c
index 0a42681..ddc7aca 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 0bc7986..9713784 100644
--- a/sshd-session.c
+++ b/sshd-session.c
@@ -735,7 +735,7 @@ main(int ac, char **av)
 	struct ssh *ssh = NULL;
 	extern char *optarg;
 	extern int optind;
-	int devnull, r, opt, on = 1, remote_port;
+	int devnull, r, opt, remote_port;
 	int sock_in = -1, sock_out = -1, rexeced_flag = 0, have_key = 0;
 	const char *remote_ip, *rdomain;
 	char *line, *laddr, *logfile = NULL;
@@ -1081,13 +1081,14 @@ 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);
 
 	/* Set SO_KEEPALIVE if requested. */
-	if (options.tcp_keep_alive && ssh_packet_connection_is_on_socket(ssh) &&
-	    setsockopt(sock_in, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)) == -1)
-		error("setsockopt SO_KEEPALIVE: %.100s", strerror(errno));
+	if (options.tcp_keep_alive && ssh_packet_connection_is_on_socket(ssh))
+		set_keepalive(sock_in); /* logs errors */
 
 	if ((remote_port = ssh_remote_port(ssh)) < 0) {
 		debug("ssh_remote_port failed");
diff --git a/sshd_config.5 b/sshd_config.5
index 485c4ba..8744649 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -1974,26 +1974,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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.