[openssh-commits] [openssh] 02/14: upstream: Extend TCPKeepAlive to support forwardings too
git+noreply at mindrot.org
git+noreply at mindrot.org
Wed Sep 16 11:15:56 AEST 2026
This is an automated email from the git hooks/post-receive script.
djm pushed a commit to branch master
in repository openssh.
commit 8895b5ae1b638148d35c2ee0b0726c66cdd39fd0
Author: djm at openbsd.org <djm at openbsd.org>
AuthorDate: Wed Sep 16 00:13:58 2026 +0000
upstream: Extend TCPKeepAlive to support forwardings too
This allows the existing client and server TCPKeepAlive option to optionally
enable keepalives on TCP connections that are created for forwardings.
Previously this option controlled keepalives on the connection socket only.
TCPKeepAlive "yes" or "transport" enables keepalives on the connection
socket. "TCPKeepAlive all" additionally enables them for forwarding
sockets.
bz3921. ok markus, deraadt
OpenBSD-Commit-ID: c29b075968e6fa88fd9773bdea8174e70229262e
---
channels.c | 106 ++++++++++++++++++++++++++++++++++++++++-----------------
channels.h | 14 +++-----
misc.c | 15 +++++++-
misc.h | 3 +-
monitor_wrap.c | 4 ++-
readconf.c | 18 ++++++++--
readconf.h | 6 +++-
servconf.c | 16 +++++++--
servconf.h | 9 +++--
ssh.c | 4 ++-
ssh_config.5 | 37 ++++++++++++--------
sshconnect.c | 10 +++---
sshd-auth.c | 4 ++-
sshd-session.c | 11 +++---
sshd_config.5 | 42 +++++++++++++----------
15 files changed, 200 insertions(+), 99 deletions(-)
diff --git a/channels.c b/channels.c
index 24de19546..c854db649 100644
--- a/channels.c
+++ b/channels.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: channels.c,v 1.466 2026/09/15 07:08:09 djm Exp $ */
+/* $OpenBSD: channels.c,v 1.467 2026/09/16 00:13:58 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo at cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo at cs.hut.fi>, Espoo, Finland
@@ -111,6 +111,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).
@@ -197,6 +204,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;
@@ -214,8 +224,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 *);
@@ -301,6 +310,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.
@@ -821,6 +860,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);
@@ -1944,6 +1984,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",
@@ -2072,8 +2114,11 @@ channel_post_port_listener(struct ssh *ssh, Channel *c)
c->notbefore = monotime() + 1;
return;
}
- if (addr.ss_family == AF_INET || addr.ss_family == AF_INET6)
+ if (addr.ss_family == AF_INET || addr.ss_family == AF_INET6) {
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;
@@ -2128,6 +2173,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);
@@ -2139,8 +2187,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) {
@@ -2164,11 +2212,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 {
@@ -4675,14 +4723,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 */
@@ -4698,6 +4747,7 @@ connect_next(struct channel_connect *cctx)
error_f("getnameinfo failed");
continue;
}
+ sock_is_network = 1;
break;
default:
continue;
@@ -4714,6 +4764,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",
@@ -4733,19 +4785,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.
@@ -4771,7 +4810,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));
@@ -4805,7 +4844,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;
@@ -4827,14 +4866,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;
}
@@ -4941,7 +4981,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;
}
@@ -4949,7 +4989,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;
}
@@ -5073,11 +5114,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 f69096498..8311749e2 100644
--- a/channels.h
+++ b/channels.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: channels.h,v 1.167 2026/06/24 06:53:11 djm Exp $ */
+/* $OpenBSD: channels.h,v 1.168 2026/09/16 00:13:58 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo at cs.hut.fi>
@@ -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 *);
@@ -206,8 +200,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;
@@ -319,6 +312,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 b41984de4..b6e05461b 100644
--- a/misc.c
+++ b/misc.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: misc.c,v 1.218 2026/09/15 08:31:46 djm Exp $ */
+/* $OpenBSD: misc.c,v 1.219 2026/09/16 00:13:58 djm Exp $ */
/*
* Copyright (c) 2000 Markus Friedl. All rights reserved.
* Copyright (c) 2005-2020 Damien Miller. All rights reserved.
@@ -240,6 +240,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 c9afe0eca..87899a90d 100644
--- a/misc.h
+++ b/misc.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: misc.h,v 1.117 2026/09/07 20:24:22 job Exp $ */
+/* $OpenBSD: misc.h,v 1.118 2026/09/16 00:13:58 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo at cs.hut.fi>
@@ -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 e9a771b34..859e10f72 100644
--- a/monitor_wrap.c
+++ b/monitor_wrap.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: monitor_wrap.c,v 1.148 2026/07/21 06:17:42 djm Exp $ */
+/* $OpenBSD: monitor_wrap.c,v 1.149 2026/09/16 00:13:58 djm Exp $ */
/*
* Copyright 2002 Niels Provos <provos at citi.umich.edu>
* Copyright 2002 Markus Friedl <markus at openbsd.org>
@@ -383,6 +383,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 c746ec3f7..b61755c18 100644
--- a/readconf.c
+++ b/readconf.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: readconf.c,v 1.416 2026/09/15 05:16:43 djm Exp $ */
+/* $OpenBSD: readconf.c,v 1.417 2026/09/16 00:13:58 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo at cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo at cs.hut.fi>, Espoo, Finland
@@ -1089,6 +1089,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 },
@@ -1348,7 +1357,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;
@@ -2901,7 +2911,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)
@@ -3609,6 +3619,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 dbcb41725..3a7dfc1ff 100644
--- a/readconf.h
+++ b/readconf.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: readconf.h,v 1.163 2026/03/30 07:18:24 djm Exp $ */
+/* $OpenBSD: readconf.h,v 1.164 2026/09/16 00:13:58 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo at cs.hut.fi>
@@ -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 ac0c31d95..d6f344989 100644
--- a/servconf.c
+++ b/servconf.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: servconf.c,v 1.452 2026/07/11 11:16:47 naddy Exp $ */
+/* $OpenBSD: servconf.c,v 1.453 2026/09/16 00:13:58 djm Exp $ */
/*
* Copyright (c) 1995 Tatu Ylonen <ylo at cs.hut.fi>, Espoo, Finland
* All rights reserved
@@ -1090,6 +1090,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,
@@ -1503,7 +1512,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;
@@ -4048,6 +4058,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 de5fe6dad..a394274e2 100644
--- a/servconf.h
+++ b/servconf.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: servconf.h,v 1.179 2026/07/07 01:00:22 djm Exp $ */
+/* $OpenBSD: servconf.h,v 1.180 2026/09/16 00:13:58 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo at cs.hut.fi>
@@ -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 e9f99c433..b87d4c1cc 100644
--- a/ssh.c
+++ b/ssh.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: ssh.c,v 1.637 2026/08/07 05:03:56 djm Exp $ */
+/* $OpenBSD: ssh.c,v 1.638 2026/09/16 00:13:58 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo at cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo at cs.hut.fi>, Espoo, Finland
@@ -1703,6 +1703,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 3240732e7..8b15cc863 100644
--- a/ssh_config.5
+++ b/ssh_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: ssh_config.5,v 1.429 2026/09/02 00:37:59 djm Exp $
-.Dd $Mdocdate: September 2 2026 $
+.\" $OpenBSD: ssh_config.5,v 1.430 2026/09/16 00:13:58 djm Exp $
+.Dd $Mdocdate: September 16 2026 $
.Dt SSH_CONFIG 5
.Os
.Sh NAME
@@ -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/sshconnect.c b/sshconnect.c
index 0ddfc76b3..c8f8e24e1 100644
--- a/sshconnect.c
+++ b/sshconnect.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: sshconnect.c,v 1.384 2026/07/06 07:49:58 djm Exp $ */
+/* $OpenBSD: sshconnect.c,v 1.385 2026/09/16 00:13:58 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo at cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo at cs.hut.fi>, Espoo, Finland
@@ -478,7 +478,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;
@@ -559,10 +559,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 4888c8e62..f1ff432be 100644
--- a/sshd-auth.c
+++ b/sshd-auth.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: sshd-auth.c,v 1.18 2026/07/27 12:28:52 markus Exp $ */
+/* $OpenBSD: sshd-auth.c,v 1.19 2026/09/16 00:13:58 djm Exp $ */
/*
* SSH2 implementation:
* Privilege Separation:
@@ -702,6 +702,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 06a72daaf..7d7bbb40d 100644
--- a/sshd-session.c
+++ b/sshd-session.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: sshd-session.c,v 1.25 2026/07/09 02:20:19 djm Exp $ */
+/* $OpenBSD: sshd-session.c,v 1.26 2026/09/16 00:13:58 djm Exp $ */
/*
* SSH2 implementation:
* Privilege Separation:
@@ -786,7 +786,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;
@@ -1165,13 +1165,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 24a6f1e2a..1bb4a3791 100644
--- a/sshd_config.5
+++ b/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.403 2026/09/02 00:37:59 djm Exp $
-.Dd $Mdocdate: September 2 2026 $
+.\" $OpenBSD: sshd_config.5,v 1.404 2026/09/16 00:13:58 djm Exp $
+.Dd $Mdocdate: September 16 2026 $
.Dt SSHD_CONFIG 5
.Os
.Sh NAME
@@ -1993,26 +1993,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
--
To stop receiving notification emails like this one, please contact
djm at mindrot.org.
More information about the openssh-commits
mailing list