upstream commit

ssh proxy mux mode (-O proxy; idea from Simon Tatham): - mux
client speaks the ssh-packet protocol directly over unix-domain socket. - mux
server acts as a proxy, translates channel IDs and relays to the server. - no
filedescriptor passing necessary. - combined with unix-domain forwarding it's
even possible to run mux client   and server on different machines. feedback
& ok djm@

Upstream-ID: 666a2fb79f58e5c50e246265fb2b9251e505c25b
diff --git a/channels.c b/channels.c
index 5d8c2a0..ae24841 100644
--- a/channels.c
+++ b/channels.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: channels.c,v 1.353 2016/09/19 07:52:42 natano Exp $ */
+/* $OpenBSD: channels.c,v 1.354 2016/09/30 09:19:13 markus Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -71,6 +71,7 @@
 #include "ssh.h"
 #include "ssh1.h"
 #include "ssh2.h"
+#include "ssherr.h"
 #include "packet.h"
 #include "log.h"
 #include "misc.h"
@@ -120,6 +121,7 @@
 	char *listen_host;		/* Remote side should listen address. */
 	char *listen_path;		/* Remote side should listen path. */
 	int listen_port;		/* Remote side should listen port. */
+	Channel *downstream;		/* Downstream mux*/
 } ForwardPermission;
 
 /* List of all permitted host/port pairs to connect by the user. */
@@ -183,6 +185,7 @@
 
 /* helper */
 static void port_open_helper(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 *);
@@ -207,6 +210,20 @@
 	return c;
 }
 
+Channel *
+channel_by_remote_id(int remote_id)
+{
+	Channel *c;
+	u_int i;
+
+	for (i = 0; i < channels_alloc; i++) {
+		c = channels[i];
+		if (c != NULL && c->remote_id == remote_id)
+			return c;
+	}
+	return NULL;
+}
+
 /*
  * Returns the channel if it is allowed to receive protocol messages.
  * Private channels, like listening sockets, may not receive messages.
@@ -229,6 +246,7 @@
 	case SSH_CHANNEL_INPUT_DRAINING:
 	case SSH_CHANNEL_OUTPUT_DRAINING:
 	case SSH_CHANNEL_ABANDONED:
+	case SSH_CHANNEL_MUX_PROXY:
 		return (c);
 	}
 	logit("Non-public channel %d, type %d.", id, c->type);
@@ -410,14 +428,56 @@
 {
 	char *s;
 	u_int i, n;
+	Channel *other;
 	struct channel_confirm *cc;
 
-	for (n = 0, i = 0; i < channels_alloc; i++)
-		if (channels[i])
+	for (n = 0, i = 0; i < channels_alloc; i++) {
+		if ((other = channels[i]) != NULL) {
 			n++;
+
+			/* detach from mux client and prepare for closing */
+			if (c->type == SSH_CHANNEL_MUX_CLIENT &&
+			    other->type == SSH_CHANNEL_MUX_PROXY &&
+			    other->mux_ctx == c) {
+				other->mux_ctx = NULL;
+				other->type = SSH_CHANNEL_OPEN;
+				other->istate = CHAN_INPUT_CLOSED;
+				other->ostate = CHAN_OUTPUT_CLOSED;
+			}
+		}
+	}
 	debug("channel %d: free: %s, nchannels %u", c->self,
 	    c->remote_name ? c->remote_name : "???", n);
 
+	/* XXX more MUX cleanup: remove remote forwardings */
+	if (c->type == SSH_CHANNEL_MUX_CLIENT) {
+		for (i = 0; i < (u_int)num_permitted_opens; i++) {
+			if (permitted_opens[i].downstream != c)
+				continue;
+			/* cancel on the server, since mux client is gone */
+			debug("channel %d: cleanup remote forward for %s:%u",
+			    c->self,
+			    permitted_opens[i].listen_host,
+			    permitted_opens[i].listen_port);
+			packet_start(SSH2_MSG_GLOBAL_REQUEST);
+			packet_put_cstring("cancel-tcpip-forward");
+			packet_put_char(0);
+			packet_put_cstring(channel_rfwd_bind_host(
+			    permitted_opens[i].listen_host));
+			packet_put_int(permitted_opens[i].listen_port);
+			packet_send();
+			/* unregister */
+			permitted_opens[i].listen_port = 0;
+			permitted_opens[i].port_to_connect = 0;
+			free(permitted_opens[i].host_to_connect);
+			permitted_opens[i].host_to_connect = NULL;
+			free(permitted_opens[i].listen_host);
+			permitted_opens[i].listen_host = NULL;
+			permitted_opens[i].listen_path = NULL;
+			permitted_opens[i].downstream = NULL;
+		}
+	}
+
 	s = channel_open_message();
 	debug3("channel %d: status: %s", c->self, s);
 	free(s);
@@ -563,6 +623,7 @@
 		case SSH_CHANNEL_OPEN:
 		case SSH_CHANNEL_X11_OPEN:
 		case SSH_CHANNEL_MUX_CLIENT:
+		case SSH_CHANNEL_MUX_PROXY:
 			return 1;
 		case SSH_CHANNEL_INPUT_DRAINING:
 		case SSH_CHANNEL_OUTPUT_DRAINING:
@@ -596,6 +657,7 @@
 		case SSH_CHANNEL_RPORT_LISTENER:
 		case SSH_CHANNEL_MUX_LISTENER:
 		case SSH_CHANNEL_MUX_CLIENT:
+		case SSH_CHANNEL_MUX_PROXY:
 		case SSH_CHANNEL_OPENING:
 		case SSH_CHANNEL_CONNECTING:
 		case SSH_CHANNEL_ZOMBIE:
@@ -621,7 +683,6 @@
 	return -1;
 }
 
-
 /*
  * Returns a message describing the currently open forwarded connections,
  * suitable for sending to the client.  The message contains crlf pairs for
@@ -650,7 +711,6 @@
 		case SSH_CHANNEL_AUTH_SOCKET:
 		case SSH_CHANNEL_ZOMBIE:
 		case SSH_CHANNEL_ABANDONED:
-		case SSH_CHANNEL_MUX_CLIENT:
 		case SSH_CHANNEL_MUX_LISTENER:
 		case SSH_CHANNEL_UNIX_LISTENER:
 		case SSH_CHANNEL_RUNIX_LISTENER:
@@ -663,6 +723,8 @@
 		case SSH_CHANNEL_X11_OPEN:
 		case SSH_CHANNEL_INPUT_DRAINING:
 		case SSH_CHANNEL_OUTPUT_DRAINING:
+		case SSH_CHANNEL_MUX_PROXY:
+		case SSH_CHANNEL_MUX_CLIENT:
 			snprintf(buf, sizeof buf,
 			    "  #%d %.300s (t%d r%d i%u/%d o%u/%d fd %d/%d cc %d)\r\n",
 			    c->self, c->remote_name,
@@ -2360,6 +2422,278 @@
 	}
 }
 
+/* -- mux proxy support  */
+
+/*
+ * When multiplexing channel messages for mux clients we have to deal
+ * with downstream messages from the mux client and upstream messages
+ * from the ssh server:
+ * 1) Handling downstream messages is straightforward and happens
+ *    in channel_proxy_downstream():
+ *    - We forward all messages (mostly) unmodified to the server.
+ *    - However, in order to route messages from upstream to the correct
+ *      downstream client, we have to replace the channel IDs used by the
+ *      mux clients with a unique channel ID because the mux clients might
+ *      use conflicting channel IDs.
+ *    - so we inspect and change both SSH2_MSG_CHANNEL_OPEN and
+ *      SSH2_MSG_CHANNEL_OPEN_CONFIRMATION messages, create a local
+ *      SSH_CHANNEL_MUX_PROXY channel and replace the mux clients ID
+ *      with the newly allocated channel ID.
+ * 2) Upstream messages are received by matching SSH_CHANNEL_MUX_PROXY
+ *    channels and procesed by channel_proxy_upstream(). The local channel ID
+ *    is then translated back to the original mux client ID.
+ * 3) In both cases we need to keep track of matching SSH2_MSG_CHANNEL_CLOSE
+ *    messages so we can clean up SSH_CHANNEL_MUX_PROXY channels.
+ * 4) The SSH_CHANNEL_MUX_PROXY channels also need to closed when the
+ *    downstream mux client are removed.
+ * 5) Handling SSH2_MSG_CHANNEL_OPEN messages from the upstream server
+ *    requires more work, because they are not addressed to a specific
+ *    channel. E.g. client_request_forwarded_tcpip() needs to figure
+ *    out whether the request is addressed to the local client or a
+ *    specific downstream client based on the listen-address/port.
+ * 6) Agent and X11-Forwarding have a similar problem and are currenly
+ *    not supported as the matching session/channel cannot be identified
+ *    easily.
+ */
+
+/*
+ * receive packets from downstream mux clients:
+ * channel callback fired on read from mux client, creates
+ * SSH_CHANNEL_MUX_PROXY channels and translates channel IDs
+ * on channel creation.
+ */
+int
+channel_proxy_downstream(Channel *downstream)
+{
+	Channel *c = NULL;
+	struct ssh *ssh = active_state;
+	struct sshbuf *original = NULL, *modified = NULL;
+	const u_char *cp;
+	char *ctype = NULL, *listen_host = NULL;
+	u_char type;
+	size_t have;
+	int ret = -1, r, id, remote_id, listen_port, idx;
+
+	/* sshbuf_dump(&downstream->input, stderr); */
+	if ((r = sshbuf_get_string_direct(&downstream->input, &cp, &have))
+	    != 0) {
+		error("%s: malformed message: %s", __func__, ssh_err(r));
+		return -1;
+	}
+	if (have < 2) {
+		error("%s: short message", __func__);
+		return -1;
+	}
+	type = cp[1];
+	/* skip padlen + type */
+	cp += 2;
+	have -= 2;
+	if (ssh_packet_log_type(type))
+		debug3("%s: channel %u: down->up: type %u", __func__,
+		    downstream->self, type);
+
+	switch (type) {
+	case SSH2_MSG_CHANNEL_OPEN:
+		if ((original = sshbuf_from(cp, have)) == NULL ||
+		    (modified = sshbuf_new()) == NULL) {
+			error("%s: alloc", __func__);
+			goto out;
+		}
+		if ((r = sshbuf_get_cstring(original, &ctype, NULL)) != 0 ||
+		    (r = sshbuf_get_u32(original, &id)) != 0) {
+			error("%s: parse error %s", __func__, ssh_err(r));
+			goto out;
+		}
+		c = channel_new("mux proxy", SSH_CHANNEL_MUX_PROXY,
+		   -1, -1, -1, 0, 0, 0, ctype, 1);
+		c->mux_ctx = downstream;	/* point to mux client */
+		c->mux_downstream_id = id;	/* original downstream id */
+		if ((r = sshbuf_put_cstring(modified, ctype)) != 0 ||
+		    (r = sshbuf_put_u32(modified, c->self)) != 0 ||
+		    (r = sshbuf_putb(modified, original)) != 0) {
+			error("%s: compose error %s", __func__, ssh_err(r));
+			channel_free(c);
+			goto out;
+		}
+		break;
+	case SSH2_MSG_CHANNEL_OPEN_CONFIRMATION:
+		/*
+		 * Almost the same as SSH2_MSG_CHANNEL_OPEN, except then we
+		 * need to parse 'remote_id' instead of 'ctype'.
+		 */
+		if ((original = sshbuf_from(cp, have)) == NULL ||
+		    (modified = sshbuf_new()) == NULL) {
+			error("%s: alloc", __func__);
+			goto out;
+		}
+		if ((r = sshbuf_get_u32(original, &remote_id)) != 0 ||
+		    (r = sshbuf_get_u32(original, &id)) != 0) {
+			error("%s: parse error %s", __func__, ssh_err(r));
+			goto out;
+		}
+		c = channel_new("mux proxy", SSH_CHANNEL_MUX_PROXY,
+		   -1, -1, -1, 0, 0, 0, "mux-down-connect", 1);
+		c->mux_ctx = downstream;	/* point to mux client */
+		c->mux_downstream_id = id;
+		c->remote_id = remote_id;
+		if ((r = sshbuf_put_u32(modified, remote_id)) != 0 ||
+		    (r = sshbuf_put_u32(modified, c->self)) != 0 ||
+		    (r = sshbuf_putb(modified, original)) != 0) {
+			error("%s: compose error %s", __func__, ssh_err(r));
+			channel_free(c);
+			goto out;
+		}
+		break;
+	case SSH2_MSG_GLOBAL_REQUEST:
+		if ((original = sshbuf_from(cp, have)) == NULL) {
+			error("%s: alloc", __func__);
+			goto out;
+		}
+		if ((r = sshbuf_get_cstring(original, &ctype, NULL)) != 0) {
+			error("%s: parse error %s", __func__, ssh_err(r));
+			goto out;
+		}
+		if (strcmp(ctype, "tcpip-forward") != 0) {
+			error("%s: unsupported request %s", __func__, ctype);
+			goto out;
+		}
+		if ((r = sshbuf_get_u8(original, NULL)) != 0 ||
+		    (r = sshbuf_get_cstring(original, &listen_host, NULL)) != 0 ||
+		    (r = sshbuf_get_u32(original, &listen_port)) != 0) {
+			error("%s: parse error %s", __func__, ssh_err(r));
+			goto out;
+		}
+		/* Record that connection to this host/port is permitted. */
+		permitted_opens = xreallocarray(permitted_opens,
+		    num_permitted_opens + 1, sizeof(*permitted_opens));
+		idx = num_permitted_opens++;
+		permitted_opens[idx].host_to_connect = xstrdup("<mux>");
+		permitted_opens[idx].port_to_connect = -1;
+		permitted_opens[idx].listen_host = listen_host;
+		permitted_opens[idx].listen_port = listen_port;
+		permitted_opens[idx].downstream = downstream;
+		listen_host = NULL;
+		break;
+	case SSH2_MSG_CHANNEL_CLOSE:
+		if (have < 4)
+			break;
+		remote_id = PEEK_U32(cp);
+		if ((c = channel_by_remote_id(remote_id)) != NULL) {
+			if (c->flags & CHAN_CLOSE_RCVD)
+				channel_free(c);
+			else
+				c->flags |= CHAN_CLOSE_SENT;
+		}
+		break;
+	}
+	if (modified) {
+		if ((r = sshpkt_start(ssh, type)) != 0 ||
+		    (r = sshpkt_putb(ssh, modified)) != 0 ||
+		    (r = sshpkt_send(ssh)) != 0) {
+			error("%s: send %s", __func__, ssh_err(r));
+			goto out;
+		}
+	} else {
+		if ((r = sshpkt_start(ssh, type)) != 0 ||
+		    (r = sshpkt_put(ssh, cp, have)) != 0 ||
+		    (r = sshpkt_send(ssh)) != 0) {
+			error("%s: send %s", __func__, ssh_err(r));
+			goto out;
+		}
+	}
+	ret = 0;
+ out:
+	free(ctype);
+	free(listen_host);
+	sshbuf_free(original);
+	sshbuf_free(modified);
+	return ret;
+}
+
+/*
+ * receive packets from upstream server and de-multiplex packets
+ * to correct downstream:
+ * implemented as a helper for channel input handlers,
+ * replaces local (proxy) channel ID with downstream channel ID.
+ */
+int
+channel_proxy_upstream(Channel *c, int type, u_int32_t seq, void *ctxt)
+{
+	struct ssh *ssh = active_state;
+	struct sshbuf *b = NULL;
+	Channel *downstream;
+	const u_char *cp = NULL;
+	size_t len;
+	int r;
+
+	/*
+	 * When receiving packets from the peer we need to check whether we
+	 * need to forward the packets to the mux client. In this case we
+	 * restore the orignal channel id and keep track of CLOSE messages,
+	 * so we can cleanup the channel.
+	 */
+	if (c == NULL || c->type != SSH_CHANNEL_MUX_PROXY)
+		return 0;
+	if ((downstream = c->mux_ctx) == NULL)
+		return 0;
+	switch (type) {
+	case SSH2_MSG_CHANNEL_CLOSE:
+	case SSH2_MSG_CHANNEL_DATA:
+	case SSH2_MSG_CHANNEL_EOF:
+	case SSH2_MSG_CHANNEL_EXTENDED_DATA:
+	case SSH2_MSG_CHANNEL_OPEN_CONFIRMATION:
+	case SSH2_MSG_CHANNEL_OPEN_FAILURE:
+	case SSH2_MSG_CHANNEL_WINDOW_ADJUST:
+	case SSH2_MSG_CHANNEL_SUCCESS:
+	case SSH2_MSG_CHANNEL_FAILURE:
+	case SSH2_MSG_CHANNEL_REQUEST:
+		break;
+	default:
+		debug2("%s: channel %u: unsupported type %u", __func__,
+		    c->self, type);
+		return 0;
+	}
+	if ((b = sshbuf_new()) == NULL) {
+		error("%s: alloc reply", __func__);
+		goto out;
+	}
+	/* get remaining payload (after id) */
+	cp = sshpkt_ptr(ssh, &len);
+	if (cp == NULL) {
+		error("%s: no packet", __func__);
+		goto out;
+	}
+	/* translate id and send to muxclient */
+	if ((r = sshbuf_put_u8(b, 0)) != 0 ||	/* padlen */
+	    (r = sshbuf_put_u8(b, type)) != 0 ||
+	    (r = sshbuf_put_u32(b, c->mux_downstream_id)) != 0 ||
+	    (r = sshbuf_put(b, cp, len)) != 0 ||
+	    (r = sshbuf_put_stringb(&downstream->output, b)) != 0) {
+		error("%s: compose for muxclient %s", __func__, ssh_err(r));
+		goto out;
+	}
+	/* sshbuf_dump(b, stderr); */
+	if (ssh_packet_log_type(type))
+		debug3("%s: channel %u: up->down: type %u", __func__, c->self,
+		    type);
+ out:
+	/* update state */
+	switch (type) {
+	case SSH2_MSG_CHANNEL_OPEN_CONFIRMATION:
+		/* record remote_id for SSH2_MSG_CHANNEL_CLOSE */
+		if (cp && len > 4)
+			c->remote_id = PEEK_U32(cp);
+		break;
+	case SSH2_MSG_CHANNEL_CLOSE:
+		if (c->flags & CHAN_CLOSE_SENT)
+			channel_free(c);
+		else
+			c->flags |= CHAN_CLOSE_RCVD;
+		break;
+	}
+	sshbuf_free(b);
+	return 1;
+}
 
 /* -- protocol input */
 
@@ -2377,6 +2711,8 @@
 	c = channel_lookup(id);
 	if (c == NULL)
 		packet_disconnect("Received data for nonexistent channel %d.", id);
+	if (channel_proxy_upstream(c, type, seq, ctxt))
+		return 0;
 
 	/* Ignore any data for non-open channels (might happen on close) */
 	if (c->type != SSH_CHANNEL_OPEN &&
@@ -2439,6 +2775,8 @@
 
 	if (c == NULL)
 		packet_disconnect("Received extended_data for bad channel %d.", id);
+	if (channel_proxy_upstream(c, type, seq, ctxt))
+		return 0;
 	if (c->type != SSH_CHANNEL_OPEN) {
 		logit("channel %d: ext data for non open", id);
 		return 0;
@@ -2484,6 +2822,8 @@
 	c = channel_lookup(id);
 	if (c == NULL)
 		packet_disconnect("Received ieof for nonexistent channel %d.", id);
+	if (channel_proxy_upstream(c, type, seq, ctxt))
+		return 0;
 	chan_rcvd_ieof(c);
 
 	/* XXX force input close */
@@ -2508,7 +2848,8 @@
 	c = channel_lookup(id);
 	if (c == NULL)
 		packet_disconnect("Received close for nonexistent channel %d.", id);
-
+	if (channel_proxy_upstream(c, type, seq, ctxt))
+		return 0;
 	/*
 	 * Send a confirmation that we have closed the channel and no more
 	 * data is coming for it.
@@ -2543,9 +2884,11 @@
 	int id = packet_get_int();
 	Channel *c = channel_lookup(id);
 
-	packet_check_eom();
 	if (c == NULL)
 		packet_disconnect("Received oclose for nonexistent channel %d.", id);
+	if (channel_proxy_upstream(c, type, seq, ctxt))
+		return 0;
+	packet_check_eom();
 	chan_rcvd_oclose(c);
 	return 0;
 }
@@ -2557,10 +2900,12 @@
 	int id = packet_get_int();
 	Channel *c = channel_lookup(id);
 
-	packet_check_eom();
 	if (c == NULL)
 		packet_disconnect("Received close confirmation for "
 		    "out-of-range channel %d.", id);
+	if (channel_proxy_upstream(c, type, seq, ctxt))
+		return 0;
+	packet_check_eom();
 	if (c->type != SSH_CHANNEL_CLOSED && c->type != SSH_CHANNEL_ABANDONED)
 		packet_disconnect("Received close confirmation for "
 		    "non-closed channel %d (type %d).", id, c->type);
@@ -2578,7 +2923,12 @@
 	id = packet_get_int();
 	c = channel_lookup(id);
 
-	if (c==NULL || c->type != SSH_CHANNEL_OPENING)
+	if (c==NULL)
+		packet_disconnect("Received open confirmation for "
+		    "unknown channel %d.", id);
+	if (channel_proxy_upstream(c, type, seq, ctxt))
+		return 0;
+	if (c->type != SSH_CHANNEL_OPENING)
 		packet_disconnect("Received open confirmation for "
 		    "non-opening channel %d.", id);
 	remote_id = packet_get_int();
@@ -2628,7 +2978,12 @@
 	id = packet_get_int();
 	c = channel_lookup(id);
 
-	if (c==NULL || c->type != SSH_CHANNEL_OPENING)
+	if (c==NULL)
+		packet_disconnect("Received open failure for "
+		    "unknown channel %d.", id);
+	if (channel_proxy_upstream(c, type, seq, ctxt))
+		return 0;
+	if (c->type != SSH_CHANNEL_OPENING)
 		packet_disconnect("Received open failure for "
 		    "non-opening channel %d.", id);
 	if (compat20) {
@@ -2672,6 +3027,8 @@
 		logit("Received window adjust for non-open channel %d.", id);
 		return 0;
 	}
+	if (channel_proxy_upstream(c, type, seq, ctxt))
+		return 0;
 	adjust = packet_get_int();
 	packet_check_eom();
 	debug2("channel %d: rcvd adjust %u", id, adjust);
@@ -2726,14 +3083,15 @@
 	packet_set_alive_timeouts(0);
 
 	id = packet_get_int();
-	packet_check_eom();
-
 	debug2("channel_input_status_confirm: type %d id %d", type, id);
 
 	if ((c = channel_lookup(id)) == NULL) {
 		logit("channel_input_status_confirm: %d: unknown", id);
 		return 0;
 	}	
+	if (channel_proxy_upstream(c, type, seq, ctxt))
+		return 0;
+	packet_check_eom();
 	if ((cc = TAILQ_FIRST(&c->status_confirms)) == NULL)
 		return 0;
 	cc->cb(type, c, cc->ctx);
@@ -3287,6 +3645,7 @@
 			permitted_opens[idx].listen_path = NULL;
 			permitted_opens[idx].listen_port = fwd->listen_port;
 		}
+		permitted_opens[idx].downstream = NULL;
 	}
 	return (idx);
 }
@@ -3382,6 +3741,7 @@
 	free(permitted_opens[i].listen_host);
 	permitted_opens[i].listen_host = NULL;
 	permitted_opens[i].listen_path = NULL;
+	permitted_opens[i].downstream = NULL;
 
 	return 0;
 }
@@ -3419,6 +3779,7 @@
 	permitted_opens[i].listen_host = NULL;
 	free(permitted_opens[i].listen_path);
 	permitted_opens[i].listen_path = NULL;
+	permitted_opens[i].downstream = NULL;
 
 	return 0;
 }
@@ -3501,6 +3862,7 @@
 	permitted_opens[num_permitted_opens].listen_host = NULL;
 	permitted_opens[num_permitted_opens].listen_path = NULL;
 	permitted_opens[num_permitted_opens].listen_port = 0;
+	permitted_opens[num_permitted_opens].downstream = NULL;
 	num_permitted_opens++;
 
 	all_opens_permitted = 0;
@@ -3763,6 +4125,10 @@
 	return c;
 }
 
+/*
+ * returns either the newly connected channel or the downstream channel
+ * that needs to deal with this connection.
+ */
 Channel *
 channel_connect_by_listen_address(const char *listen_host,
     u_short listen_port, char *ctype, char *rname)
@@ -3772,6 +4138,8 @@
 	for (i = 0; i < num_permitted_opens; i++) {
 		if (open_listen_match_tcpip(&permitted_opens[i], listen_host,
 		    listen_port, 1)) {
+			if (permitted_opens[i].downstream)
+				return permitted_opens[i].downstream;
 			return connect_to(
 			    permitted_opens[i].host_to_connect,
 			    permitted_opens[i].port_to_connect, ctype, rname);