Index | Thread | Search

From:
Damien Miller <djm@mindrot.org>
Subject:
ssh/sshd: support TCP keepalives on forwarding sockets too
To:
tech@openbsd.org
Cc:
openssh@openssh.com
Date:
Fri, 26 Jun 2026 14:01:04 +1000

Download raw body.

Thread
  • Damien Miller:

    ssh/sshd: support TCP keepalives on forwarding sockets too

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