- djm@cvs.openbsd.org 2004/06/13 15:03:02
     [channels.c channels.h clientloop.c clientloop.h includes.h readconf.c]
     [readconf.h scp.1 sftp.1 ssh.1 ssh.c ssh_config.5]
     implement session multiplexing in the client (the server has supported
     this since 2.0); ok markus@
diff --git a/ChangeLog b/ChangeLog
index 3edf2d1..36aeb85 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -24,6 +24,11 @@
      [ssh.1 ssh_config.5 sshd_config.5]
      List supported ciphers in man pages, tidy up ssh -c;
      "looks fine" jmc@, ok markus@
+   - djm@cvs.openbsd.org 2004/06/13 15:03:02
+     [channels.c channels.h clientloop.c clientloop.h includes.h readconf.c] 
+     [readconf.h scp.1 sftp.1 ssh.1 ssh.c ssh_config.5]
+     implement session multiplexing in the client (the server has supported 
+     this since 2.0); ok markus@
 
 20040603
  - (dtucker) [auth-pam.c] Don't use pam_* namespace for sshd's PAM functions.
@@ -1208,4 +1213,4 @@
    - (djm) Trim deprecated options from INSTALL. Mention UsePAM
    - (djm) Fix quote handling in sftp; Patch from admorten AT umich.edu
 
-$Id: ChangeLog,v 1.3381 2004/06/15 00:30:39 djm Exp $
+$Id: ChangeLog,v 1.3382 2004/06/15 00:34:08 djm Exp $
diff --git a/channels.c b/channels.c
index 437befa..1fb1092 100644
--- a/channels.c
+++ b/channels.c
@@ -39,7 +39,7 @@
  */
 
 #include "includes.h"
-RCSID("$OpenBSD: channels.c,v 1.203 2004/05/26 23:02:39 markus Exp $");
+RCSID("$OpenBSD: channels.c,v 1.204 2004/06/13 15:03:02 djm Exp $");
 
 #include "ssh.h"
 #include "ssh1.h"
@@ -172,6 +172,7 @@
 	c->rfd = rfd;
 	c->wfd = wfd;
 	c->sock = (rfd == wfd) ? rfd : -1;
+	c->ctl_fd = -1; /* XXX: set elsewhere */
 	c->efd = efd;
 	c->extended_usage = extusage;
 
@@ -263,6 +264,7 @@
 	c->single_connection = 0;
 	c->detach_user = NULL;
 	c->confirm = NULL;
+	c->confirm_ctx = NULL;
 	c->input_filter = NULL;
 	debug("channel %d: new [%s]", found, remote_name);
 	return c;
@@ -304,10 +306,11 @@
 static void
 channel_close_fds(Channel *c)
 {
-	debug3("channel %d: close_fds r %d w %d e %d",
-	    c->self, c->rfd, c->wfd, c->efd);
+	debug3("channel %d: close_fds r %d w %d e %d c %d",
+	    c->self, c->rfd, c->wfd, c->efd, c->ctl_fd);
 
 	channel_close_fd(&c->sock);
+	channel_close_fd(&c->ctl_fd);
 	channel_close_fd(&c->rfd);
 	channel_close_fd(&c->wfd);
 	channel_close_fd(&c->efd);
@@ -333,6 +336,8 @@
 
 	if (c->sock != -1)
 		shutdown(c->sock, SHUT_RDWR);
+	if (c->ctl_fd != -1)
+		shutdown(c->ctl_fd, SHUT_RDWR);
 	channel_close_fds(c);
 	buffer_free(&c->input);
 	buffer_free(&c->output);
@@ -550,12 +555,13 @@
 		case SSH_CHANNEL_X11_OPEN:
 		case SSH_CHANNEL_INPUT_DRAINING:
 		case SSH_CHANNEL_OUTPUT_DRAINING:
-			snprintf(buf, sizeof buf, "  #%d %.300s (t%d r%d i%d/%d o%d/%d fd %d/%d)\r\n",
+			snprintf(buf, sizeof buf,
+			    "  #%d %.300s (t%d r%d i%d/%d o%d/%d fd %d/%d cfd %d)\r\n",
 			    c->self, c->remote_name,
 			    c->type, c->remote_id,
 			    c->istate, buffer_len(&c->input),
 			    c->ostate, buffer_len(&c->output),
-			    c->rfd, c->wfd);
+			    c->rfd, c->wfd, c->ctl_fd);
 			buffer_append(&buffer, buf, strlen(buf));
 			continue;
 		default:
@@ -596,14 +602,14 @@
 		logit("channel_request_start: %d: unknown channel id", id);
 		return;
 	}
-	debug2("channel %d: request %s", id, service) ;
+	debug2("channel %d: request %s confirm %d", id, service, wantconfirm);
 	packet_start(SSH2_MSG_CHANNEL_REQUEST);
 	packet_put_int(c->remote_id);
 	packet_put_cstring(service);
 	packet_put_char(wantconfirm);
 }
 void
-channel_register_confirm(int id, channel_callback_fn *fn)
+channel_register_confirm(int id, channel_callback_fn *fn, void *ctx)
 {
 	Channel *c = channel_lookup(id);
 
@@ -612,6 +618,7 @@
 		return;
 	}
 	c->confirm = fn;
+	c->confirm_ctx = ctx;
 }
 void
 channel_register_cleanup(int id, channel_callback_fn *fn)
@@ -729,6 +736,10 @@
 		    buffer_len(&c->extended) < c->remote_window)
 			FD_SET(c->efd, readset);
 	}
+	/* XXX: What about efd? races? */
+	if (compat20 && c->ctl_fd != -1 && 
+	    c->istate == CHAN_INPUT_OPEN && c->ostate == CHAN_OUTPUT_OPEN)
+		FD_SET(c->ctl_fd, readset);
 }
 
 static void
@@ -1482,6 +1493,33 @@
 	return 1;
 }
 static int
+channel_handle_ctl(Channel *c, fd_set * readset, fd_set * writeset)
+{
+	char buf[16];
+	int len;
+
+	/* Monitor control fd to detect if the slave client exits */
+	if (c->ctl_fd != -1 && FD_ISSET(c->ctl_fd, readset)) {
+		len = read(c->ctl_fd, buf, sizeof(buf));
+		if (len < 0 && (errno == EINTR || errno == EAGAIN))
+			return 1;
+		if (len <= 0) {
+			debug2("channel %d: ctl read<=0", c->self);
+			if (c->type != SSH_CHANNEL_OPEN) {
+				debug2("channel %d: not open", c->self);
+				chan_mark_dead(c);
+				return -1;
+			} else {
+				chan_read_failed(c);
+				chan_write_failed(c);
+			}
+			return -1;
+		} else
+			fatal("%s: unexpected data on ctl fd", __func__);
+	}
+	return 1;
+}
+static int
 channel_check_window(Channel *c)
 {
 	if (c->type == SSH_CHANNEL_OPEN &&
@@ -1511,6 +1549,7 @@
 	if (!compat20)
 		return;
 	channel_handle_efd(c, readset, writeset);
+	channel_handle_ctl(c, readset, writeset);
 	channel_check_window(c);
 }
 
@@ -2011,7 +2050,7 @@
 		c->remote_maxpacket = packet_get_int();
 		if (c->confirm) {
 			debug2("callback start");
-			c->confirm(c->self, NULL);
+			c->confirm(c->self, c->confirm_ctx);
 			debug2("callback done");
 		}
 		debug2("channel %d: open confirm rwindow %u rmax %u", c->self,
@@ -2531,6 +2570,27 @@
 	return connect_to(host, port);
 }
 
+void
+channel_send_window_changes(void)
+{
+	int i;
+	struct winsize ws;
+
+	for (i = 0; i < channels_alloc; i++) {
+		if (channels[i] == NULL ||
+		    channels[i]->type != SSH_CHANNEL_OPEN)
+			continue;
+		if (ioctl(channels[i]->rfd, TIOCGWINSZ, &ws) < 0)
+			continue;
+		channel_request_start(i, "window-change", 0);
+		packet_put_int(ws.ws_col);
+		packet_put_int(ws.ws_row);
+		packet_put_int(ws.ws_xpixel);
+		packet_put_int(ws.ws_ypixel);
+		packet_send();
+	}
+}
+
 /* -- X11 forwarding */
 
 /*
diff --git a/channels.h b/channels.h
index 0a49c55..41f3ced 100644
--- a/channels.h
+++ b/channels.h
@@ -1,4 +1,4 @@
-/*	$OpenBSD: channels.h,v 1.72 2004/05/21 11:33:11 djm Exp $	*/
+/*	$OpenBSD: channels.h,v 1.73 2004/06/13 15:03:02 djm Exp $	*/
 
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -76,6 +76,7 @@
 	int     wfd;		/* write fd */
 	int     efd;		/* extended fd */
 	int     sock;		/* sock fd */
+	int     ctl_fd;		/* control fd (client sharing) */
 	int     isatty;		/* rfd is a tty */
 	int     wfd_isatty;	/* wfd is a tty */
 	int     force_drain;	/* force close on iEOF */
@@ -105,6 +106,7 @@
 	/* callback */
 	channel_callback_fn	*confirm;
 	channel_callback_fn	*detach_user;
+	void			*confirm_ctx;
 
 	/* filter */
 	channel_filter_fn	*input_filter;
@@ -161,10 +163,11 @@
 void	 channel_send_open(int);
 void	 channel_request_start(int, char *, int);
 void	 channel_register_cleanup(int, channel_callback_fn *);
-void	 channel_register_confirm(int, channel_callback_fn *);
+void	 channel_register_confirm(int, channel_callback_fn *, void *);
 void	 channel_register_filter(int, channel_filter_fn *);
 void	 channel_cancel_cleanup(int);
 int	 channel_close_fd(int *);
+void	 channel_send_window_changes(void);
 
 /* protocol handler */
 
diff --git a/clientloop.c b/clientloop.c
index 31e6041..6401588 100644
--- a/clientloop.c
+++ b/clientloop.c
@@ -59,7 +59,7 @@
  */
 
 #include "includes.h"
-RCSID("$OpenBSD: clientloop.c,v 1.122 2004/05/22 06:32:12 djm Exp $");
+RCSID("$OpenBSD: clientloop.c,v 1.123 2004/06/13 15:03:02 djm Exp $");
 
 #include "ssh.h"
 #include "ssh1.h"
@@ -81,6 +81,9 @@
 #include "atomicio.h"
 #include "sshpty.h"
 #include "misc.h"
+#include "monitor_fdpass.h"
+#include "match.h"
+#include "msg.h"
 
 /* import options */
 extern Options options;
@@ -91,6 +94,9 @@
 /* Flag indicating that no shell has been requested */
 extern int no_shell_flag;
 
+/* Control socket */
+extern int control_fd;
+
 /*
  * Name of the host we are connecting to.  This is the name given on the
  * command line, or the HostName specified for the user-supplied name in a
@@ -131,9 +137,19 @@
 static void client_init_dispatch(void);
 int	session_ident = -1;
 
+struct confirm_ctx {
+	int want_tty;
+	int want_subsys;
+	Buffer cmd;
+	char *term;
+	struct termios tio;
+};
+
 /*XXX*/
 extern Kex *xxx_kex;
 
+void ssh_process_session2_setup(int, int, int, Buffer *);
+
 /* Restores stdin to blocking mode. */
 
 static void
@@ -291,19 +307,13 @@
 	/** XXX race */
 	received_window_change_signal = 0;
 
-	if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) < 0)
-		return;
-
 	debug2("client_check_window_change: changed");
 
 	if (compat20) {
-		channel_request_start(session_ident, "window-change", 0);
-		packet_put_int(ws.ws_col);
-		packet_put_int(ws.ws_row);
-		packet_put_int(ws.ws_xpixel);
-		packet_put_int(ws.ws_ypixel);
-		packet_send();
+		channel_send_window_changes();
 	} else {
+		if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) < 0)
+			return;
 		packet_start(SSH_CMSG_WINDOW_SIZE);
 		packet_put_int(ws.ws_row);
 		packet_put_int(ws.ws_col);
@@ -335,7 +345,6 @@
  * Waits until the client can do something (some data becomes available on
  * one of the file descriptors).
  */
-
 static void
 client_wait_until_can_do_something(fd_set **readsetp, fd_set **writesetp,
     int *maxfdp, int *nallocp, int rekeying)
@@ -381,6 +390,9 @@
 	if (packet_have_data_to_write())
 		FD_SET(connection_out, *writesetp);
 
+	if (control_fd != -1)
+		FD_SET(control_fd, *readsetp);
+
 	/*
 	 * Wait for something to happen.  This will suspend the process until
 	 * some selected descriptor can be read, written, or has some other
@@ -500,6 +512,176 @@
 }
 
 static void
+client_subsystem_reply(int type, u_int32_t seq, void *ctxt)
+{
+	int id;
+	Channel *c;
+	
+	id = packet_get_int();
+	packet_check_eom();
+
+	if ((c = channel_lookup(id)) == NULL) {
+		error("%s: no channel for id %d", __func__, id);
+		return;
+	}
+
+	if (type == SSH2_MSG_CHANNEL_SUCCESS)
+		debug2("Request suceeded on channel %d", id);
+	else if (type == SSH2_MSG_CHANNEL_FAILURE) {
+		error("Request failed on channel %d", id);
+		channel_free(c);
+	}
+}
+
+static void
+client_extra_session2_setup(int id, void *arg)
+{
+	struct confirm_ctx *cctx = arg;
+	Channel *c;
+	
+	if (cctx == NULL)
+		fatal("%s: cctx == NULL", __func__);
+	if ((c = channel_lookup(id)) == NULL)
+		fatal("%s: no channel for id %d", __func__, id);
+
+	client_session2_setup(id, cctx->want_tty, cctx->want_subsys, 
+	    cctx->term, &cctx->tio, c->rfd, &cctx->cmd, 
+	    client_subsystem_reply);
+	
+	c->confirm_ctx = NULL;
+	buffer_free(&cctx->cmd);
+	free(cctx->term);
+	free(cctx);
+}
+
+static void
+client_process_control(fd_set * readset)
+{
+	Buffer m;
+	Channel *c;
+	int client_fd, new_fd[3], ver;
+	socklen_t addrlen;
+	struct sockaddr_storage addr;
+	struct confirm_ctx *cctx;
+	char *cmd;
+	u_int len;
+	uid_t euid;
+	gid_t egid;
+
+	/*
+	 * Accept connection on control socket
+	 */
+	if (control_fd == -1 || !FD_ISSET(control_fd, readset))
+		return;
+
+	memset(&addr, 0, sizeof(addr));
+	addrlen = sizeof(addr);
+	if ((client_fd = accept(control_fd,
+	    (struct sockaddr*)&addr, &addrlen)) == -1) {
+		error("%s accept: %s", __func__, strerror(errno));
+		return;
+	}
+
+	if (getpeereid(client_fd, &euid, &egid) < 0) {
+		error("%s getpeereid failed: %s", __func__, strerror(errno));
+		close(client_fd);
+		return;
+	}
+	if ((euid != 0) && (getuid() != euid)) {
+		error("control mode uid mismatch: peer euid %u != uid %u",
+		    (u_int) euid, (u_int) getuid());
+		close(client_fd);
+		return;
+	}
+	/* XXX: implement use of ssh-askpass to confirm additional channels */
+
+	unset_nonblock(client_fd);
+
+	buffer_init(&m);
+
+	buffer_put_int(&m, getpid());
+	if (ssh_msg_send(client_fd, /* version */0, &m) == -1) {
+		error("%s: client msg_send failed", __func__);
+		close(client_fd);
+		return;
+	}
+	buffer_clear(&m);
+
+	if (ssh_msg_recv(client_fd, &m) == -1) {
+		error("%s: client msg_recv failed", __func__);
+		close(client_fd);
+		return;
+	}
+
+	if ((ver = buffer_get_char(&m)) != 0) {
+		error("%s: wrong client version %d", __func__, ver);
+		buffer_free(&m);
+		close(client_fd);
+		return;
+	}
+
+	cctx = xmalloc(sizeof(*cctx));
+	memset(cctx, 0, sizeof(*cctx));
+
+	cctx->want_tty = buffer_get_int(&m);
+	cctx->want_subsys = buffer_get_int(&m);
+	cctx->term = buffer_get_string(&m, &len);
+
+	cmd = buffer_get_string(&m, &len);
+	buffer_init(&cctx->cmd);
+	buffer_append(&cctx->cmd, cmd, strlen(cmd));
+
+	debug2("%s: accepted tty %d, subsys %d, cmd %s", __func__,
+	    cctx->want_tty, cctx->want_subsys, cmd);
+
+	/* Gather fds from client */
+	new_fd[0] = mm_receive_fd(client_fd);
+	new_fd[1] = mm_receive_fd(client_fd);
+	new_fd[2] = mm_receive_fd(client_fd);
+
+	debug2("%s: got fds stdin %d, stdout %d, stderr %d", __func__,
+	    new_fd[0], new_fd[1], new_fd[2]);
+
+	/* Try to pick up ttymodes from client before it goes raw */
+	if (cctx->want_tty && tcgetattr(new_fd[0], &cctx->tio) == -1)
+		error("%s: tcgetattr: %s", __func__, strerror(errno));
+
+	buffer_clear(&m);
+	if (ssh_msg_send(client_fd, /* version */0, &m) == -1) {
+		error("%s: client msg_send failed", __func__);
+		close(client_fd);
+		close(new_fd[0]);
+		close(new_fd[1]);
+		close(new_fd[2]);
+		return;
+	}
+	buffer_free(&m);
+
+	/* enable nonblocking unless tty */
+	if (!isatty(new_fd[0]))
+		set_nonblock(new_fd[0]);
+	if (!isatty(new_fd[1]))
+		set_nonblock(new_fd[1]);
+	if (!isatty(new_fd[2]))
+		set_nonblock(new_fd[2]);
+
+	set_nonblock(client_fd);
+
+	c = channel_new("session", SSH_CHANNEL_OPENING, 
+	    new_fd[0], new_fd[1], new_fd[2],
+	    CHAN_SES_WINDOW_DEFAULT, CHAN_SES_PACKET_DEFAULT,
+	    CHAN_EXTENDED_WRITE, "client-session", /*nonblock*/0);
+
+	/* XXX */
+	c->ctl_fd = client_fd;
+
+	debug3("%s: channel_new: %d", __func__, c->self);
+
+	channel_send_open(c->self);
+	channel_register_confirm(c->self, client_extra_session2_setup, cctx);
+}
+
+static void
 process_cmdline(void)
 {
 	void (*handler)(int);
@@ -901,9 +1083,6 @@
 static void
 client_channel_closed(int id, void *arg)
 {
-	if (id != session_ident)
-		error("client_channel_closed: id %d != session_ident %d",
-		    id, session_ident);
 	channel_cancel_cleanup(id);
 	session_closed = 1;
 	leave_raw_mode();
@@ -937,6 +1116,8 @@
 	connection_in = packet_get_connection_in();
 	connection_out = packet_get_connection_out();
 	max_fd = MAX(connection_in, connection_out);
+	if (control_fd != -1)
+		max_fd = MAX(max_fd, control_fd);
 
 	if (!compat20) {
 		/* enable nonblocking unless tty */
@@ -1054,6 +1235,9 @@
 		/* Buffer input from the connection.  */
 		client_process_net_input(readset);
 
+		/* Accept control connections.  */
+		client_process_control(readset);
+
 		if (quit_pending)
 			break;
 
@@ -1385,7 +1569,7 @@
 client_input_channel_req(int type, u_int32_t seq, void *ctxt)
 {
 	Channel *c = NULL;
-	int id, reply, success = 0;
+	int exitval, id, reply, success = 0;
 	char *rtype;
 
 	id = packet_get_int();
@@ -1395,18 +1579,21 @@
 	debug("client_input_channel_req: channel %d rtype %s reply %d",
 	    id, rtype, reply);
 
-	if (session_ident == -1) {
-		error("client_input_channel_req: no channel %d", session_ident);
-	} else if (id != session_ident) {
-		error("client_input_channel_req: channel %d: wrong channel: %d",
-		    session_ident, id);
-	}
 	c = channel_lookup(id);
 	if (c == NULL) {
 		error("client_input_channel_req: channel %d: unknown channel", id);
 	} else if (strcmp(rtype, "exit-status") == 0) {
-		success = 1;
-		exit_status = packet_get_int();
+		exitval = packet_get_int();
+		if (id == session_ident) {
+			success = 1;
+			exit_status = exitval;
+		} else if (c->ctl_fd == -1) {
+			error("client_input_channel_req: unexpected channel %d",
+			    session_ident);
+		} else {
+			atomicio(vwrite, c->ctl_fd, &exitval, sizeof(exitval));
+			success = 1;
+		}
 		packet_check_eom();
 	}
 	if (reply) {
@@ -1437,6 +1624,98 @@
 	xfree(rtype);
 }
 
+void
+client_session2_setup(int id, int want_tty, int want_subsystem, 
+    const char *term, struct termios *tiop, int in_fd, Buffer *cmd, 
+    dispatch_fn *subsys_repl)
+{
+	int len;
+
+	debug2("%s: id %d", __func__, id);
+
+	if (want_tty) {
+		struct winsize ws;
+		struct termios tio;
+
+		/* Store window size in the packet. */
+		if (ioctl(in_fd, TIOCGWINSZ, &ws) < 0)
+			memset(&ws, 0, sizeof(ws));
+
+		channel_request_start(id, "pty-req", 0);
+		packet_put_cstring(term != NULL ? term : "");
+		packet_put_int(ws.ws_col);
+		packet_put_int(ws.ws_row);
+		packet_put_int(ws.ws_xpixel);
+		packet_put_int(ws.ws_ypixel);
+		tio = get_saved_tio();
+		tty_make_modes(-1, tiop != NULL ? tiop : &tio);
+		packet_send();
+		/* XXX wait for reply */
+	}
+
+	/* Transfer any environment variables from client to server */
+	if (options.num_send_env != 0) {
+		int i, j, matched;
+		extern char **environ;
+		char *name, *val;
+
+		debug("Sending environment.");
+		for (i = 0; environ && environ[i] != NULL; i++) {
+			/* Split */
+			name = xstrdup(environ[i]);
+			if ((val = strchr(name, '=')) == NULL) {
+				free(name);
+				continue;
+			}
+			*val++ = '\0';
+
+			matched = 0;
+			for (j = 0; j < options.num_send_env; j++) {
+				if (match_pattern(name, options.send_env[j])) {
+					matched = 1;
+					break;
+				}
+			}
+			if (!matched) {
+				debug3("Ignored env %s", name);
+				free(name);
+				continue;
+			}
+
+			debug("Sending env %s = %s", name, val);
+			channel_request_start(id, "env", 0);
+			packet_put_cstring(name);
+			packet_put_cstring(val);
+			packet_send();
+			free(name);
+		}
+	}
+
+	len = buffer_len(cmd);
+	if (len > 0) {
+		if (len > 900)
+			len = 900;
+		if (want_subsystem) {
+			debug("Sending subsystem: %.*s", len, (u_char*)buffer_ptr(cmd));
+			channel_request_start(id, "subsystem", subsys_repl != NULL);
+			if (subsys_repl != NULL) {
+				/* register callback for reply */
+				/* XXX we assume that client_loop has already been called */
+				dispatch_set(SSH2_MSG_CHANNEL_FAILURE, subsys_repl);
+				dispatch_set(SSH2_MSG_CHANNEL_SUCCESS, subsys_repl);
+			}
+		} else {
+			debug("Sending command: %.*s", len, (u_char*)buffer_ptr(cmd));
+			channel_request_start(id, "exec", 0);
+		}
+		packet_put_string(buffer_ptr(cmd), buffer_len(cmd));
+		packet_send();
+	} else {
+		channel_request_start(id, "shell", 0);
+		packet_send();
+	}
+}
+
 static void
 client_init_dispatch_20(void)
 {
@@ -1503,5 +1782,7 @@
 {
 	leave_raw_mode();
 	leave_non_blocking();
+	if (options.control_path != NULL && control_fd != -1)
+		unlink(options.control_path);
 	_exit(i);
 }
diff --git a/clientloop.h b/clientloop.h
index 56af06b..f1e13ac 100644
--- a/clientloop.h
+++ b/clientloop.h
@@ -1,4 +1,4 @@
-/*	$OpenBSD: clientloop.h,v 1.8 2003/12/16 15:49:51 markus Exp $	*/
+/*	$OpenBSD: clientloop.h,v 1.9 2004/06/13 15:03:02 djm Exp $	*/
 
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -38,3 +38,5 @@
 /* Client side main loop for the interactive session. */
 int	 client_loop(int, int, int);
 void	 client_global_request_reply_fwd(int, u_int32_t, void *);
+void	 client_session2_setup(int, int, int, const char *, struct termios *, 
+	    int, Buffer *, dispatch_fn *);
diff --git a/defines.h b/defines.h
index 9b72afe..73a45fe 100644
--- a/defines.h
+++ b/defines.h
@@ -25,7 +25,7 @@
 #ifndef _DEFINES_H
 #define _DEFINES_H
 
-/* $Id: defines.h,v 1.115 2004/04/14 07:24:30 dtucker Exp $ */
+/* $Id: defines.h,v 1.116 2004/06/15 00:34:08 djm Exp $ */
 
 
 /* Constants */
@@ -462,6 +462,9 @@
 	 (struct cmsghdr *)NULL)
 #endif /* CMSG_FIRSTHDR */
 
+#ifndef offsetof
+# define offsetof(type, member) ((size_t) &((type *)0)->member)
+#endif
 
 /* Function replacement / compatibility hacks */
 
diff --git a/includes.h b/includes.h
index ca943c7..99b7050 100644
--- a/includes.h
+++ b/includes.h
@@ -1,4 +1,4 @@
-/*	$OpenBSD: includes.h,v 1.17 2002/01/26 16:44:22 stevesk Exp $	*/
+/*	$OpenBSD: includes.h,v 1.18 2004/06/13 15:03:02 djm Exp $	*/
 
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -33,6 +33,7 @@
 #include <grp.h>
 #include <time.h>
 #include <dirent.h>
+#include <stddef.h>
 
 #ifdef HAVE_LIMITS_H
 # include <limits.h> /* For PATH_MAX */
diff --git a/readconf.c b/readconf.c
index 5aa371e..2b1d7cc 100644
--- a/readconf.c
+++ b/readconf.c
@@ -12,7 +12,7 @@
  */
 
 #include "includes.h"
-RCSID("$OpenBSD: readconf.c,v 1.131 2004/05/27 00:50:13 dtucker Exp $");
+RCSID("$OpenBSD: readconf.c,v 1.132 2004/06/13 15:03:02 djm Exp $");
 
 #include "ssh.h"
 #include "xmalloc.h"
@@ -106,7 +106,7 @@
 	oEnableSSHKeysign, oRekeyLimit, oVerifyHostKeyDNS, oConnectTimeout,
 	oAddressFamily, oGssAuthentication, oGssDelegateCreds,
 	oServerAliveInterval, oServerAliveCountMax, oIdentitiesOnly,
-	oSendEnv,
+	oSendEnv, oControlPath, oControlMaster,
 	oDeprecated, oUnsupported
 } OpCodes;
 
@@ -195,6 +195,8 @@
 	{ "serveraliveinterval", oServerAliveInterval },
 	{ "serveralivecountmax", oServerAliveCountMax },
 	{ "sendenv", oSendEnv },
+	{ "controlpath", oControlPath },
+	{ "controlmaster", oControlMaster },
 	{ NULL, oBadOption }
 };
 
@@ -764,6 +766,14 @@
 		}
 		break;
 
+	case oControlPath:
+		charptr = &options->control_path;
+		goto parse_string;
+
+	case oControlMaster:
+		intptr = &options->control_master;
+		goto parse_flag;
+
 	case oDeprecated:
 		debug("%s line %d: Deprecated option \"%s\"",
 		    filename, linenum, keyword);
@@ -905,6 +915,8 @@
 	options->server_alive_interval = -1;
 	options->server_alive_count_max = -1;
 	options->num_send_env = 0;
+	options->control_path = NULL;
+	options->control_master = -1;
 }
 
 /*
@@ -1025,6 +1037,8 @@
 		options->server_alive_interval = 0;
 	if (options->server_alive_count_max == -1)
 		options->server_alive_count_max = 3;
+	if (options->control_master == -1)
+		options->control_master = 0;
 	/* options->proxy_command should not be set by default */
 	/* options->user will be set in the main program if appropriate */
 	/* options->hostname will be set in the main program if appropriate */
diff --git a/readconf.h b/readconf.h
index 6680559..5e504be 100644
--- a/readconf.h
+++ b/readconf.h
@@ -1,4 +1,4 @@
-/*	$OpenBSD: readconf.h,v 1.62 2004/04/27 09:46:37 djm Exp $	*/
+/*	$OpenBSD: readconf.h,v 1.63 2004/06/13 15:03:02 djm Exp $	*/
 
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -108,6 +108,9 @@
 
 	int     num_send_env;
 	char   *send_env[MAX_SEND_ENV];
+
+	char	*control_path;
+	int	control_master;
 }       Options;
 
 
diff --git a/scp.1 b/scp.1
index 202ebaa..f346b2a 100644
--- a/scp.1
+++ b/scp.1
@@ -9,7 +9,7 @@
 .\"
 .\" Created: Sun May  7 00:14:37 1995 ylo
 .\"
-.\" $OpenBSD: scp.1,v 1.35 2004/05/04 18:36:07 jmc Exp $
+.\" $OpenBSD: scp.1,v 1.36 2004/06/13 15:03:02 djm Exp $
 .\"
 .Dd September 25, 1999
 .Dt SCP 1
@@ -128,6 +128,8 @@
 .It CompressionLevel
 .It ConnectionAttempts
 .It ConnectTimeout
+.It ControlMaster
+.It ControlPath
 .It GlobalKnownHostsFile
 .It GSSAPIAuthentication
 .It GSSAPIDelegateCredentials
diff --git a/sftp.1 b/sftp.1
index 795a034..7f0ef11 100644
--- a/sftp.1
+++ b/sftp.1
@@ -1,4 +1,4 @@
-.\" $OpenBSD: sftp.1,v 1.54 2004/05/02 23:02:17 dtucker Exp $
+.\" $OpenBSD: sftp.1,v 1.55 2004/06/13 15:03:02 djm Exp $
 .\"
 .\" Copyright (c) 2001 Damien Miller.  All rights reserved.
 .\"
@@ -154,6 +154,8 @@
 .It CompressionLevel
 .It ConnectionAttempts
 .It ConnectTimeout
+.It ControlMaster
+.It ControlPath
 .It GlobalKnownHostsFile
 .It GSSAPIAuthentication
 .It GSSAPIDelegateCredentials
diff --git a/ssh-rand-helper.c b/ssh-rand-helper.c
index 8a320a7..471e729 100644
--- a/ssh-rand-helper.c
+++ b/ssh-rand-helper.c
@@ -39,7 +39,7 @@
 #include "pathnames.h"
 #include "log.h"
 
-RCSID("$Id: ssh-rand-helper.c,v 1.16 2003/11/21 12:56:47 djm Exp $");
+RCSID("$Id: ssh-rand-helper.c,v 1.17 2004/06/15 00:34:08 djm Exp $");
 
 /* Number of bytes we write out */
 #define OUTPUT_SEED_SIZE	48
@@ -69,10 +69,6 @@
 char *__progname;
 #endif
 
-#ifndef offsetof
-# define offsetof(type, member) ((size_t) &((type *)0)->member)
-#endif
-
 #define WHITESPACE " \t\n"
 
 #ifndef RUSAGE_SELF
diff --git a/ssh.1 b/ssh.1
index 6cef085..b70102b 100644
--- a/ssh.1
+++ b/ssh.1
@@ -34,7 +34,7 @@
 .\" (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.1,v 1.189 2004/06/13 14:01:42 dtucker Exp $
+.\" $OpenBSD: ssh.1,v 1.190 2004/06/13 15:03:02 djm Exp $
 .Dd September 25, 1999
 .Dt SSH 1
 .Os
@@ -43,7 +43,7 @@
 .Nd OpenSSH SSH client (remote login program)
 .Sh SYNOPSIS
 .Nm ssh
-.Op Fl 1246AaCfgkNnqsTtVvXxY
+.Op Fl 1246AaCfgkMNnqSsTtVvXxY
 .Op Fl b Ar bind_address
 .Op Fl c Ar cipher_spec
 .Op Fl D Ar port
@@ -605,6 +605,17 @@
 See the
 .Cm MACs
 keyword for more information.
+.It Fl M
+Places the
+.Nm
+client into
+.Dq master
+mode for connection sharing.
+Refer to the description of
+.Cm ControlMaster
+in
+.Xr ssh_config 5
+for details.
 .It Fl N
 Do not execute a remote command.
 This is useful for just forwarding ports
@@ -649,6 +660,8 @@
 .It CompressionLevel
 .It ConnectionAttempts
 .It ConnectTimeout
+.It ControlMaster
+.It ControlPath
 .It DynamicForward
 .It EscapeChar
 .It ForwardAgent
@@ -724,6 +737,15 @@
 .Ar hostport .
 .Xc
 .Sm on
+.It Fl S
+Places the
+.Nm
+client into slave mode for connection sharing.
+Refer to the description of
+.Cm ControlMaster
+in
+.Xr ssh_config 5
+for details.
 .It Fl s
 May be used to request invocation of a subsystem on the remote system.
 Subsystems are a feature of the SSH2 protocol which facilitate the use
diff --git a/ssh.c b/ssh.c
index 3c21fa3..1c6ec8b 100644
--- a/ssh.c
+++ b/ssh.c
@@ -40,7 +40,7 @@
  */
 
 #include "includes.h"
-RCSID("$OpenBSD: ssh.c,v 1.213 2004/05/08 00:01:37 deraadt Exp $");
+RCSID("$OpenBSD: ssh.c,v 1.214 2004/06/13 15:03:02 djm Exp $");
 
 #include <openssl/evp.h>
 #include <openssl/err.h>
@@ -53,21 +53,24 @@
 #include "xmalloc.h"
 #include "packet.h"
 #include "buffer.h"
+#include "bufaux.h"
 #include "channels.h"
 #include "key.h"
 #include "authfd.h"
 #include "authfile.h"
 #include "pathnames.h"
+#include "dispatch.h"
 #include "clientloop.h"
 #include "log.h"
 #include "readconf.h"
 #include "sshconnect.h"
-#include "dispatch.h"
 #include "misc.h"
 #include "kex.h"
 #include "mac.h"
 #include "sshpty.h"
 #include "match.h"
+#include "msg.h"
+#include "monitor_fdpass.h"
 
 #ifdef SMARTCARD
 #include "scard.h"
@@ -141,6 +144,13 @@
 /* pid of proxycommand child process */
 pid_t proxy_command_pid = 0;
 
+/* fd to control socket */
+int control_fd = -1;
+
+/* Only used in control client mode */
+volatile sig_atomic_t control_client_terminate = 0;
+u_int control_server_pid = 0;
+
 /* Prints a help message to the user.  This function never returns. */
 
 static void
@@ -158,6 +168,7 @@
 static int ssh_session(void);
 static int ssh_session2(void);
 static void load_public_identity_files(void);
+static void control_client(const char *path);
 
 /*
  * Main program for the ssh client.
@@ -228,7 +239,7 @@
 
 again:
 	while ((opt = getopt(ac, av,
-	    "1246ab:c:e:fgi:kl:m:no:p:qstvxACD:F:I:L:NPR:TVXY")) != -1) {
+	    "1246ab:c:e:fgi:kl:m:no:p:qstvxACD:F:I:L:MNPR:S:TVXY")) != -1) {
 		switch (opt) {
 		case '1':
 			options.protocol = SSH_PROTO_1;
@@ -364,6 +375,9 @@
 				exit(1);
 			}
 			break;
+		case 'M':
+			options.control_master = 1;
+			break;
 		case 'p':
 			options.port = a2port(optarg);
 			if (options.port == 0) {
@@ -432,6 +446,13 @@
 		case 's':
 			subsystem_flag = 1;
 			break;
+		case 'S':
+			if (options.control_path != NULL)
+				free(options.control_path);
+			options.control_path = xstrdup(optarg);
+			if (options.control_master == -1)
+				options.control_master = 0;
+			break;
 		case 'b':
 			options.bind_address = optarg;
 			break;
@@ -566,6 +587,13 @@
 	    strcmp(options.proxy_command, "none") == 0)
 		options.proxy_command = NULL;
 
+	if (options.control_path != NULL) {
+		options.control_path = tilde_expand_filename(
+		   options.control_path, original_real_uid);
+	}
+	if (options.control_path != NULL && options.control_master == 0)
+		control_client(options.control_path); /* This doesn't return */
+
 	/* Open a connection to the remote host. */
 	if (ssh_connect(host, &hostaddr, options.port,
 	    options.address_family, options.connection_attempts,
@@ -678,6 +706,9 @@
 	exit_status = compat20 ? ssh_session2() : ssh_session();
 	packet_close();
 
+	if (options.control_path != NULL && control_fd != -1)
+		unlink(options.control_path);
+
 	/*
 	 * Send SIGHUP to proxy command if used. We don't wait() in
 	 * case it hangs and instead rely on init to reap the child
@@ -974,7 +1005,7 @@
 }
 
 static void
-client_subsystem_reply(int type, u_int32_t seq, void *ctxt)
+ssh_subsystem_reply(int type, u_int32_t seq, void *ctxt)
 {
 	int id, len;
 
@@ -1006,40 +1037,50 @@
 		    options.remote_forwards[i].port);
 }
 
+static void
+ssh_control_listener(void)
+{
+	struct sockaddr_un addr;
+	mode_t old_umask;
+	
+	if (options.control_path == NULL || options.control_master != 1)
+		return;
+
+	memset(&addr, '\0', sizeof(addr));
+	addr.sun_family = AF_UNIX;
+	addr.sun_len = offsetof(struct sockaddr_un, sun_path) +
+	    strlen(options.control_path) + 1;
+
+	if (strlcpy(addr.sun_path, options.control_path,
+	    sizeof(addr.sun_path)) >= sizeof(addr.sun_path))
+		fatal("ControlPath too long");
+
+	if ((control_fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
+		fatal("%s socket(): %s\n", __func__, strerror(errno));
+
+	old_umask = umask(0177);
+	if (bind(control_fd, (struct sockaddr*)&addr, addr.sun_len) == -1) {
+		control_fd = -1;
+		if (errno == EINVAL)
+			fatal("ControlSocket %s already exists",
+			    options.control_path);
+		else
+			fatal("%s bind(): %s\n", __func__, strerror(errno));
+	}
+	umask(old_umask);
+
+	if (listen(control_fd, 64) == -1)
+		fatal("%s listen(): %s\n", __func__, strerror(errno));
+
+	set_nonblock(control_fd);
+}
+
 /* request pty/x11/agent/tcpfwd/shell for channel */
 static void
 ssh_session2_setup(int id, void *arg)
 {
-	int len;
-	int interactive = 0;
-	struct termios tio;
-
-	debug2("ssh_session2_setup: id %d", id);
-
-	if (tty_flag) {
-		struct winsize ws;
-		char *cp;
-		cp = getenv("TERM");
-		if (!cp)
-			cp = "";
-		/* Store window size in the packet. */
-		if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) < 0)
-			memset(&ws, 0, sizeof(ws));
-
-		channel_request_start(id, "pty-req", 0);
-		packet_put_cstring(cp);
-		packet_put_int(ws.ws_col);
-		packet_put_int(ws.ws_row);
-		packet_put_int(ws.ws_xpixel);
-		packet_put_int(ws.ws_ypixel);
-		tio = get_saved_tio();
-		tty_make_modes(/*ignored*/ 0, &tio);
-		packet_send();
-		interactive = 1;
-		/* XXX wait for reply */
-	}
-	if (options.forward_x11 &&
-	    getenv("DISPLAY") != NULL) {
+	int interactive = tty_flag;
+	if (options.forward_x11 && getenv("DISPLAY") != NULL) {
 		char *proto, *data;
 		/* Get reasonable local authentication information. */
 		x11_get_proto(&proto, &data);
@@ -1057,65 +1098,8 @@
 		packet_send();
 	}
 
-	/* Transfer any environment variables from client to server */
-	if (options.num_send_env != 0) {
-		int i, j, matched;
-		extern char **environ;
-		char *name, *val;
-
-		debug("Sending environment.");
-		for (i = 0; environ && environ[i] != NULL; i++) {
-			/* Split */
-			name = xstrdup(environ[i]);
-			if ((val = strchr(name, '=')) == NULL) {
-				free(name);
-				continue;
-			}
-			*val++ = '\0';
-
-			matched = 0;
-			for (j = 0; j < options.num_send_env; j++) {
-				if (match_pattern(name, options.send_env[j])) {
-					matched = 1;
-					break;
-				}
-			}
-			if (!matched) {
-				debug3("Ignored env %s", name);
-				free(name);
-				continue;
-			}
-
-			debug("Sending env %s = %s", name, val);
-			channel_request_start(id, "env", 0);
-			packet_put_cstring(name);
-			packet_put_cstring(val);
-			packet_send();
-			free(name);
-		}
-	}
-
-	len = buffer_len(&command);
-	if (len > 0) {
-		if (len > 900)
-			len = 900;
-		if (subsystem_flag) {
-			debug("Sending subsystem: %.*s", len, (u_char *)buffer_ptr(&command));
-			channel_request_start(id, "subsystem", /*want reply*/ 1);
-			/* register callback for reply */
-			/* XXX we assume that client_loop has already been called */
-			dispatch_set(SSH2_MSG_CHANNEL_FAILURE, &client_subsystem_reply);
-			dispatch_set(SSH2_MSG_CHANNEL_SUCCESS, &client_subsystem_reply);
-		} else {
-			debug("Sending command: %.*s", len, (u_char *)buffer_ptr(&command));
-			channel_request_start(id, "exec", 0);
-		}
-		packet_put_string(buffer_ptr(&command), buffer_len(&command));
-		packet_send();
-	} else {
-		channel_request_start(id, "shell", 0);
-		packet_send();
-	}
+	client_session2_setup(id, tty_flag, subsystem_flag, getenv("TERM"),
+	    NULL, fileno(stdin), &command, &ssh_subsystem_reply);
 
 	packet_set_interactive(interactive);
 }
@@ -1161,7 +1145,7 @@
 
 	channel_send_open(c->self);
 	if (!no_shell_flag)
-		channel_register_confirm(c->self, ssh_session2_setup);
+		channel_register_confirm(c->self, ssh_session2_setup, NULL);
 
 	return c->self;
 }
@@ -1173,6 +1157,7 @@
 
 	/* XXX should be pre-session */
 	ssh_init_forwarding();
+	ssh_control_listener();
 
 	if (!no_shell_flag || (datafellows & SSH_BUG_DUMMYCHAN))
 		id = ssh_session2_open();
@@ -1226,3 +1211,110 @@
 		options.identity_keys[i] = public;
 	}
 }
+
+static void
+control_client_sighandler(int signo)
+{
+	control_client_terminate = signo;
+}
+
+static void
+control_client_sigrelay(int signo)
+{
+	if (control_server_pid > 1)
+		kill(control_server_pid, signo);
+}
+
+static void
+control_client(const char *path)
+{
+	struct sockaddr_un addr;
+	int r, sock, exitval;
+	Buffer m;
+	char *cp;
+	
+	memset(&addr, '\0', sizeof(addr));
+	addr.sun_family = AF_UNIX;
+	addr.sun_len = offsetof(struct sockaddr_un, sun_path) +
+	    strlen(path) + 1;
+
+	if (strlcpy(addr.sun_path, path,
+	    sizeof(addr.sun_path)) >= sizeof(addr.sun_path))
+		fatal("ControlPath too long");
+
+	if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
+		fatal("%s socket(): %s", __func__, strerror(errno));
+
+	if (connect(sock, (struct sockaddr*)&addr, addr.sun_len) == -1)
+		fatal("Couldn't connect to %s: %s", path, strerror(errno));
+
+	if ((cp = getenv("TERM")) == NULL)
+		cp = "";
+
+	signal(SIGINT, control_client_sighandler);
+	signal(SIGTERM, control_client_sighandler);
+	signal(SIGWINCH, control_client_sigrelay);
+
+	buffer_init(&m);
+
+	/* Get PID of controlee */
+	if (ssh_msg_recv(sock, &m) == -1)
+		fatal("%s: msg_recv", __func__);
+	if (buffer_get_char(&m) != 0)
+		fatal("%s: wrong version", __func__);
+	control_server_pid = buffer_get_int(&m);
+
+	/* XXX: env passing */
+
+	buffer_clear(&m);
+	buffer_put_int(&m, tty_flag);
+	buffer_put_int(&m, subsystem_flag);
+	buffer_put_cstring(&m, cp);
+
+	buffer_append(&command, "\0", 1);
+	buffer_put_cstring(&m, buffer_ptr(&command));
+
+	if (ssh_msg_send(sock, /* version */0, &m) == -1)
+		fatal("%s: msg_send", __func__);
+
+	mm_send_fd(sock, STDIN_FILENO);
+	mm_send_fd(sock, STDOUT_FILENO);
+	mm_send_fd(sock, STDERR_FILENO);
+
+	/* Wait for reply, so master has a chance to gather ttymodes */
+	buffer_clear(&m);
+	if (ssh_msg_recv(sock, &m) == -1)
+		fatal("%s: msg_recv", __func__);
+	if (buffer_get_char(&m) != 0)
+		fatal("%s: master returned error", __func__);
+	buffer_free(&m);
+
+	if (tty_flag)
+		enter_raw_mode();
+
+	/* Stick around until the controlee closes the client_fd */
+	exitval = 0;
+	for (;!control_client_terminate;) {
+		r = read(sock, &exitval, sizeof(exitval));
+		if (r == 0) {
+			debug2("Received EOF from master");
+			break;
+		}
+		if (r > 0)
+			debug2("Received exit status from master %d", exitval);
+		if (r == -1 && errno != EINTR)
+			fatal("%s: read %s", __func__, strerror(errno));
+	}
+
+	if (control_client_terminate)
+		debug2("Exiting on signal %d", control_client_terminate);
+
+	close(sock);
+
+	leave_raw_mode();
+
+	if (tty_flag && options.log_level != SYSLOG_LEVEL_QUIET)
+		fprintf(stderr, "Connection to master closed.\r\n");
+
+	exit(exitval);
+}
diff --git a/ssh_config.5 b/ssh_config.5
index 46d3012..bab11d3 100644
--- a/ssh_config.5
+++ b/ssh_config.5
@@ -34,7 +34,7 @@
 .\" (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.35 2004/06/13 14:01:42 dtucker Exp $
+.\" $OpenBSD: ssh_config.5,v 1.36 2004/06/13 15:03:02 djm Exp $
 .Dd September 25, 1999
 .Dt SSH_CONFIG 5
 .Os
@@ -256,6 +256,28 @@
 Multiple forwardings may be specified, and
 additional forwardings can be given on the command line.
 Only the superuser can forward privileged ports.
+.It Cm ControlMaster
+Enables the sharing of multiple sessions over a single network connection.
+When set to
+.Dq yes
+.Nm ssh
+will listen for connections on a control socket specified using the
+.Cm ControlPath
+argument.
+Additional sessions can connect to this socket using the same
+.Cm ControlPath
+with
+.Cm ControlMaster
+set to
+.Dq no
+(the default.)
+These sessions will reuse the master instance's network connection rather
+than initiating new ones.
+.It Cm ControlPath
+Specify a the path to the control socket used for connection sharing.
+See
+.Cm ControlMaster
+above.
 .It Cm EnableSSHKeysign
 Setting this option to
 .Dq yes