From 745baf76240de545fb4ff514b5e0a08e2f042aac Mon Sep 17 00:00:00 2001 From: djm Date: Mon, 19 Jul 2010 09:15:12 +0000 Subject: [PATCH] add a "ControlPersist" option that automatically starts a background ssh(1) multiplex master when connecting. This connection can stay alive indefinitely, or can be set to automatically close after a user-specified duration of inactivity. bz#1330 - patch by dwmw2 AT infradead.org, but further hacked on by wmertens AT cisco.com, apb AT cequrux.com, martin-mindrot-bugzilla AT earth.li and myself; "looks ok" markus@ --- usr.bin/ssh/clientloop.c | 63 +++++++++++++++++++-- usr.bin/ssh/readconf.c | 36 +++++++++++- usr.bin/ssh/readconf.h | 4 +- usr.bin/ssh/ssh.c | 117 +++++++++++++++++++++++++++++++-------- usr.bin/ssh/ssh_config.5 | 26 ++++++++- 5 files changed, 215 insertions(+), 31 deletions(-) diff --git a/usr.bin/ssh/clientloop.c b/usr.bin/ssh/clientloop.c index be01467e9d4..263e41943c0 100644 --- a/usr.bin/ssh/clientloop.c +++ b/usr.bin/ssh/clientloop.c @@ -1,4 +1,4 @@ -/* $OpenBSD: clientloop.c,v 1.221 2010/06/25 23:15:36 djm Exp $ */ +/* $OpenBSD: clientloop.c,v 1.222 2010/07/19 09:15:12 djm Exp $ */ /* * Author: Tatu Ylonen * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -137,6 +137,9 @@ static volatile sig_atomic_t received_signal = 0; /* Flag indicating whether the user's terminal is in non-blocking mode. */ static int in_non_blocking_mode = 0; +/* Time when backgrounded control master using ControlPersist should exit */ +static time_t control_persist_exit_time = 0; + /* Common data for the client loop code. */ volatile sig_atomic_t quit_pending; /* Set non-zero to quit the loop. */ static int escape_char1; /* Escape character. (proto1 only) */ @@ -244,6 +247,34 @@ get_current_time(void) return (double) tv.tv_sec + (double) tv.tv_usec / 1000000.0; } +/* + * Sets control_persist_exit_time to the absolute time when the + * backgrounded control master should exit due to expiry of the + * ControlPersist timeout. Sets it to 0 if we are not a backgrounded + * control master process, or if there is no ControlPersist timeout. + */ +static void +set_control_persist_exit_time(void) +{ + if (muxserver_sock == -1 || !options.control_persist + || options.control_persist_timeout == 0) + /* not using a ControlPersist timeout */ + control_persist_exit_time = 0; + else if (channel_still_open()) { + /* some client connections are still open */ + if (control_persist_exit_time > 0) + debug2("%s: cancel scheduled exit", __func__); + control_persist_exit_time = 0; + } else if (control_persist_exit_time <= 0) { + /* a client connection has recently closed */ + control_persist_exit_time = time(NULL) + + (time_t)options.control_persist_timeout; + debug2("%s: schedule exit in %d seconds", __func__, + options.control_persist_timeout); + } + /* else we are already counting down to the timeout */ +} + #define SSH_X11_PROTO "MIT-MAGIC-COOKIE-1" void client_x11_get_proto(const char *display, const char *xauth_path, @@ -525,6 +556,7 @@ client_wait_until_can_do_something(fd_set **readsetp, fd_set **writesetp, int *maxfdp, u_int *nallocp, int rekeying) { struct timeval tv, *tvp; + int timeout_secs; int ret; /* Add any selections by the channel mechanism. */ @@ -568,16 +600,27 @@ client_wait_until_can_do_something(fd_set **readsetp, fd_set **writesetp, /* * Wait for something to happen. This will suspend the process until * some selected descriptor can be read, written, or has some other - * event pending. + * event pending, or a timeout expires. */ - if (options.server_alive_interval == 0 || !compat20) + timeout_secs = INT_MAX; /* we use INT_MAX to mean no timeout */ + if (options.server_alive_interval > 0 && compat20) + timeout_secs = options.server_alive_interval; + set_control_persist_exit_time(); + if (control_persist_exit_time > 0) { + timeout_secs = MIN(timeout_secs, + control_persist_exit_time - time(NULL)); + if (timeout_secs < 0) + timeout_secs = 0; + } + if (timeout_secs == INT_MAX) tvp = NULL; else { - tv.tv_sec = options.server_alive_interval; + tv.tv_sec = timeout_secs; tv.tv_usec = 0; tvp = &tv; } + ret = select((*maxfdp)+1, *readsetp, *writesetp, NULL, tvp); if (ret < 0) { char buf[100]; @@ -1466,6 +1509,18 @@ client_loop(int have_pty, int escape_char_arg, int ssh2_chan_id) */ if (FD_ISSET(connection_out, writeset)) packet_write_poll(); + + /* + * If we are a backgrounded control master, and the + * timeout has expired without any active client + * connections, then quit. + */ + if (control_persist_exit_time > 0) { + if (time(NULL) >= control_persist_exit_time) { + debug("ControlPersist timeout expired"); + break; + } + } } if (readset) xfree(readset); diff --git a/usr.bin/ssh/readconf.c b/usr.bin/ssh/readconf.c index da717e6462c..56f37b7786b 100644 --- a/usr.bin/ssh/readconf.c +++ b/usr.bin/ssh/readconf.c @@ -1,4 +1,4 @@ -/* $OpenBSD: readconf.c,v 1.186 2010/06/25 23:15:36 djm Exp $ */ +/* $OpenBSD: readconf.c,v 1.187 2010/07/19 09:15:12 djm Exp $ */ /* * Author: Tatu Ylonen * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -125,7 +125,8 @@ typedef enum { oEnableSSHKeysign, oRekeyLimit, oVerifyHostKeyDNS, oConnectTimeout, oAddressFamily, oGssAuthentication, oGssDelegateCreds, oServerAliveInterval, oServerAliveCountMax, oIdentitiesOnly, - oSendEnv, oControlPath, oControlMaster, oHashKnownHosts, + oSendEnv, oControlPath, oControlMaster, oControlPersist, + oHashKnownHosts, oTunnel, oTunnelDevice, oLocalCommand, oPermitLocalCommand, oVisualHostKey, oUseRoaming, oZeroKnowledgePasswordAuthentication, oDeprecated, oUnsupported @@ -222,6 +223,7 @@ static struct { { "sendenv", oSendEnv }, { "controlpath", oControlPath }, { "controlmaster", oControlMaster }, + { "controlpersist", oControlPersist }, { "hashknownhosts", oHashKnownHosts }, { "tunnel", oTunnel }, { "tunneldevice", oTunnelDevice }, @@ -878,6 +880,30 @@ parse_int: *intptr = value; break; + case oControlPersist: + /* no/false/yes/true, or a time spec */ + intptr = &options->control_persist; + arg = strdelim(&s); + if (!arg || *arg == '\0') + fatal("%.200s line %d: Missing ControlPersist" + " argument.", filename, linenum); + value = 0; + value2 = 0; /* timeout */ + if (strcmp(arg, "no") == 0 || strcmp(arg, "false") == 0) + value = 0; + else if (strcmp(arg, "yes") == 0 || strcmp(arg, "true") == 0) + value = 1; + else if ((value2 = convtime(arg)) >= 0) + value = 1; + else + fatal("%.200s line %d: Bad ControlPersist argument.", + filename, linenum); + if (*activep && *intptr == -1) { + *intptr = value; + options->control_persist_timeout = value2; + } + break; + case oHashKnownHosts: intptr = &options->hash_known_hosts; goto parse_flag; @@ -1079,6 +1105,8 @@ initialize_options(Options * options) options->num_send_env = 0; options->control_path = NULL; options->control_master = -1; + options->control_persist = -1; + options->control_persist_timeout = 0; options->hash_known_hosts = -1; options->tun_open = -1; options->tun_local = -1; @@ -1214,6 +1242,10 @@ fill_default_options(Options * options) options->server_alive_count_max = 3; if (options->control_master == -1) options->control_master = 0; + if (options->control_persist == -1) { + options->control_persist = 0; + options->control_persist_timeout = 0; + } if (options->hash_known_hosts == -1) options->hash_known_hosts = 0; if (options->tun_open == -1) diff --git a/usr.bin/ssh/readconf.h b/usr.bin/ssh/readconf.h index 66acafdefa1..95d10467472 100644 --- a/usr.bin/ssh/readconf.h +++ b/usr.bin/ssh/readconf.h @@ -1,4 +1,4 @@ -/* $OpenBSD: readconf.h,v 1.85 2010/06/25 23:15:36 djm Exp $ */ +/* $OpenBSD: readconf.h,v 1.86 2010/07/19 09:15:12 djm Exp $ */ /* * Author: Tatu Ylonen @@ -114,6 +114,8 @@ typedef struct { char *control_path; int control_master; + int control_persist; /* ControlPersist flag */ + int control_persist_timeout; /* ControlPersist timeout (seconds) */ int hash_known_hosts; diff --git a/usr.bin/ssh/ssh.c b/usr.bin/ssh/ssh.c index 2aab1048a31..0c18fec527b 100644 --- a/usr.bin/ssh/ssh.c +++ b/usr.bin/ssh/ssh.c @@ -1,4 +1,4 @@ -/* $OpenBSD: ssh.c,v 1.343 2010/07/12 22:41:13 djm Exp $ */ +/* $OpenBSD: ssh.c,v 1.344 2010/07/19 09:15:12 djm Exp $ */ /* * Author: Tatu Ylonen * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -118,6 +118,15 @@ int no_shell_flag = 0; */ int stdin_null_flag = 0; +/* + * Flag indicating that the current process should be backgrounded and + * a new slave launched in the foreground for ControlPersist. + */ +int need_controlpersist_detach = 0; + +/* Copies of flags for ControlPersist foreground slave */ +int ostdin_null_flag, ono_shell_flag, ono_tty_flag, otty_flag; + /* * Flag indicating that ssh should fork after authentication. This is useful * so that the passphrase can be entered manually, and then ssh goes to the @@ -858,6 +867,50 @@ main(int ac, char **av) return exit_status; } +static void +control_persist_detach(void) +{ + pid_t pid; + + debug("%s: backgrounding master process", __func__); + + /* + * master (current process) into the background, and make the + * foreground process a client of the backgrounded master. + */ + switch ((pid = fork())) { + case -1: + fatal("%s: fork: %s", __func__, strerror(errno)); + case 0: + /* Child: master process continues mainloop */ + break; + default: + /* Parent: set up mux slave to connect to backgrounded master */ + debug2("%s: background process is %ld", __func__, (long)pid); + stdin_null_flag = ostdin_null_flag; + no_shell_flag = ono_shell_flag; + no_tty_flag = ono_tty_flag; + tty_flag = otty_flag; + close(muxserver_sock); + muxserver_sock = -1; + muxclient(options.control_path); + /* muxclient() doesn't return on success. */ + fatal("Failed to connect to new control master"); + } +} + +/* Do fork() after authentication. Used by "ssh -f" */ +static void +fork_postauth(void) +{ + if (need_controlpersist_detach) + control_persist_detach(); + debug("forking to background"); + fork_after_authentication_flag = 0; + if (daemon(1, 1) < 0) + fatal("daemon() failed: %.200s", strerror(errno)); +} + /* Callback for remote forward global requests */ static void ssh_confirm_remote_forward(int type, u_int32_t seq, void *ctxt) @@ -885,12 +938,8 @@ ssh_confirm_remote_forward(int type, u_int32_t seq, void *ctxt) } if (++remote_forward_confirms_received == options.num_remote_forwards) { debug("All remote forwarding requests processed"); - if (fork_after_authentication_flag) { - fork_after_authentication_flag = 0; - if (daemon(1, 1) < 0) - fatal("daemon() failed: %.200s", - strerror(errno)); - } + if (fork_after_authentication_flag) + fork_postauth(); } } @@ -1134,12 +1183,13 @@ ssh_session(void) * If requested and we are not interested in replies to remote * forwarding requests, then let ssh continue in the background. */ - if (fork_after_authentication_flag && - (!options.exit_on_forward_failure || - options.num_remote_forwards == 0)) { - fork_after_authentication_flag = 0; - if (daemon(1, 1) < 0) - fatal("daemon() failed: %.200s", strerror(errno)); + if (fork_after_authentication_flag) { + if (options.exit_on_forward_failure && + options.num_remote_forwards > 0) { + debug("deferring postauth fork until remote forward " + "confirmation received"); + } else + fork_postauth(); } /* @@ -1262,6 +1312,31 @@ ssh_session2(void) /* XXX should be pre-session */ ssh_init_forwarding(); + /* Start listening for multiplex clients */ + muxserver_listen(); + + /* + * If we are in control persist mode, then prepare to background + * ourselves and have a foreground client attach as a control + * slave. NB. we must save copies of the flags that we override for + * the backgrounding, since we defer attachment of the slave until + * after the connection is fully established (in particular, + * async rfwd replies have been received for ExitOnForwardFailure). + */ + if (options.control_persist && muxserver_sock != -1) { + ostdin_null_flag = stdin_null_flag; + ono_shell_flag = no_shell_flag; + ono_tty_flag = no_tty_flag; + otty_flag = tty_flag; + stdin_null_flag = 1; + no_shell_flag = 1; + no_tty_flag = 1; + tty_flag = 0; + if (!fork_after_authentication_flag) + need_controlpersist_detach = 1; + fork_after_authentication_flag = 1; + } + if (!no_shell_flag || (datafellows & SSH_BUG_DUMMYCHAN)) id = ssh_session2_open(); @@ -1280,19 +1355,17 @@ ssh_session2(void) options.permit_local_command) ssh_local_cmd(options.local_command); - /* Start listening for multiplex clients */ - muxserver_listen(); - /* * If requested and we are not interested in replies to remote * forwarding requests, then let ssh continue in the background. */ - if (fork_after_authentication_flag && - (!options.exit_on_forward_failure || - options.num_remote_forwards == 0)) { - fork_after_authentication_flag = 0; - if (daemon(1, 1) < 0) - fatal("daemon() failed: %.200s", strerror(errno)); + if (fork_after_authentication_flag) { + if (options.exit_on_forward_failure && + options.num_remote_forwards > 0) { + debug("deferring postauth fork until remote forward " + "confirmation received"); + } else + fork_postauth(); } if (options.use_roaming) diff --git a/usr.bin/ssh/ssh_config.5 b/usr.bin/ssh/ssh_config.5 index e7bb21ebb43..04df8184c13 100644 --- a/usr.bin/ssh/ssh_config.5 +++ b/usr.bin/ssh/ssh_config.5 @@ -34,8 +34,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.136 2010/07/12 22:41:13 djm Exp $ -.Dd $Mdocdate: July 12 2010 $ +.\" $OpenBSD: ssh_config.5,v 1.137 2010/07/19 09:15:12 djm Exp $ +.Dd $Mdocdate: July 19 2010 $ .Dt SSH_CONFIG 5 .Os .Sh NAME @@ -319,6 +319,28 @@ It is recommended that any used for opportunistic connection sharing include at least %h, %p, and %r. This ensures that shared connections are uniquely identified. +.It Cm ControlPersist +When used in conjunction with +.Cm ControlMaster , +specifies that the master connection should remain open +in the background (waiting for future client connections) +after the initial client connection has been closed. +If set to +.Dq no , +then the master connection will not be placed into the background, +and will close as soon as the initial client connection is closed. +If set to +.Dq yes , +then the master connection will remain in the background indefinitely +(until killed or closed via a mechanism such as the +.Xr ssh 1 +.Dq Fl O No exit +option). +If set to a time in seconds, or a time in any of the formats documented in +.Xr sshd_config 5 , +then the backgrounded master connection will automatically terminate +after it has remained idle (with no client connections) for the +specified time. .It Cm DynamicForward Specifies that a TCP port on the local machine be forwarded over the secure channel, and the application -- 2.20.1