From: Damien Miller 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 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