add a "ControlPersist" option that automatically starts a background
authordjm <djm@openbsd.org>
Mon, 19 Jul 2010 09:15:12 +0000 (09:15 +0000)
committerdjm <djm@openbsd.org>
Mon, 19 Jul 2010 09:15:12 +0000 (09:15 +0000)
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
usr.bin/ssh/readconf.c
usr.bin/ssh/readconf.h
usr.bin/ssh/ssh.c
usr.bin/ssh/ssh_config.5

index be01467..263e419 100644 (file)
@@ -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 <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, 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);
index da717e6..56f37b7 100644 (file)
@@ -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 <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, 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)
index 66acafd..95d1046 100644 (file)
@@ -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 <ylo@cs.hut.fi>
@@ -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;
 
index 2aab104..0c18fec 100644 (file)
@@ -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 <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, 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)
index e7bb21e..04df818 100644 (file)
@@ -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