From e11e1ea5d475ee8be0038d64aa3e47c776295ac2 Mon Sep 17 00:00:00 2001 From: Damien Miller Date: Tue, 3 Aug 2010 16:04:46 +1000 Subject: - djm@cvs.openbsd.org 2010/07/19 09:15:12 [clientloop.c readconf.c readconf.h ssh.c ssh_config.5] 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@ --- ChangeLog | 8 ++++ clientloop.c | 63 ++++++++++++++++++++++++++++++-- readconf.c | 36 +++++++++++++++++- readconf.h | 4 +- ssh.c | 117 ++++++++++++++++++++++++++++++++++++++++++++++++----------- ssh_config.5 | 26 ++++++++++++- 6 files changed, 223 insertions(+), 31 deletions(-) diff --git a/ChangeLog b/ChangeLog index f4fb5f05..b43074ec 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,6 +16,14 @@ bz#1797: fix swapped args in upload_dir_internal(), breaking recursive upload depth checks and causing verbose printing of transfers to always be turned on; patch from imorgan AT nas.nasa.gov + - djm@cvs.openbsd.org 2010/07/19 09:15:12 + [clientloop.c readconf.c readconf.h ssh.c ssh_config.5] + 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@ 20100819 - (dtucker) [contrib/ssh-copy-ud.1] Bug #1786: update ssh-copy-id.1 with more diff --git a/clientloop.c b/clientloop.c index 5608bcc2..de797936 100644 --- a/clientloop.c +++ b/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 @@ -145,6 +145,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) */ @@ -252,6 +255,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, @@ -533,6 +564,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. */ @@ -576,16 +608,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]; @@ -1478,6 +1521,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/readconf.c b/readconf.c index da48ae7d..0296590e 100644 --- a/readconf.c +++ b/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 @@ -128,7 +128,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 @@ -225,6 +226,7 @@ static struct { { "sendenv", oSendEnv }, { "controlpath", oControlPath }, { "controlmaster", oControlMaster }, + { "controlpersist", oControlPersist }, { "hashknownhosts", oHashKnownHosts }, { "tunnel", oTunnel }, { "tunneldevice", oTunnelDevice }, @@ -882,6 +884,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; @@ -1083,6 +1109,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; @@ -1218,6 +1246,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/readconf.h b/readconf.h index 66acafde..95d10467 100644 --- a/readconf.h +++ b/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/ssh.c b/ssh.c index 61fe10df..249be2db 100644 --- a/ssh.c +++ b/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 @@ -127,6 +127,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 @@ -877,6 +886,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) @@ -904,12 +957,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(); } } @@ -1153,12 +1202,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(); } /* @@ -1281,6 +1331,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(); @@ -1299,19 +1374,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/ssh_config.5 b/ssh_config.5 index e7bb21eb..04df8184 100644 --- a/ssh_config.5 +++ b/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 -- cgit v1.2.3