Trouble with port forwarding over proxy control socket

ding ding at diinngg.com
Sun Dec 28 00:24:50 AEDT 2025


On 22/12/2025 04:21, Damien Miller wrote:
> On Sun, 21 Dec 2025, ding wrote:
>
>> Hi,
>>
>> This is attempt 3 at sending this email, apologies to the admins.
>>
>> I'm working on support for connecting to a control socket in the Go SSH
>> package
>> (a continuation of https://github.com/golang/crypto/pull/205), but I'm having
>> trouble getting port forwarding to work correctly.
>>
>> As far as I can tell the comment at
>> https://github.com/openssh/openssh-portable/blob/b652322cdc5e94f059b37a8fb87e44ccb1cdff33/channels.c#L3169
>> suggests that it *should* work, but looking through the code I can't see how.
>> The problem I'm hitting is that the response to the global "tcpip-forward"
>> request never gets forwarded downstream back over the control socket.
>>
>> I can only assume that anyone currently using this is just not waiting for the
>> response? If the port is known ahead of time and you don't care about handling
>> a failure response then I guess you can just ignore it? It seems like it
>> definitely breaks the ability to use port 0 for remote forwarding though as
>> there's no way to find out which port was chosen on the server.
>>
>> This seems to break with the OpenSSH client with `-O proxy` as well. Running
>> `ssh -R 0:127.0.0.1:<localport> -N <host>` allows connecting from a random
>> port
>> on the server to the local port (and the log shows "Allocated port <...> for
>> remote forward to 127.0.0.1:<localport>"). Doing the same through a control
>> socket with `ssh -O proxy -R 0:127.0.0.1:<localport> -N <host>` doesn't output
>> the port to log, finding the port on the server with `lsof` and writing to it
>> results in the control master process logging "WARNING: Server requests
>> forwarding for unknown listen_port <...>".
>>
>> Building "ssh" with the attached patch fixes the issue for me (at least for
>> the
>> Go implementation), but including "clientloop.h" in "channels.c" seems wrong,
>> and it breaks a bunch of build targets. There's a comment above
>> `struct global_confirm` suggesting that it should be moved elsewhere, so I
>> assume that problem can be solved by moving it and the related functions to a
>> more accessible place. There could also be bigger technical problems with this
>> approach that I'm not aware of.
>>
>> Before putting more effort into this I wanted to check if anyone had any
>> thoughts or better ideas on what the problem might be, or how to fix it?
> AFAIK your analysis is 100% correct, and thanks for looking so deeply
> into this.
>
> I don't have a great idea on how to fix this yet either, though perhaps
> the queue of pending global confirms could move from clientloop.c to
> channels.c, with the actual TAILQ anchor in struct ssh_channels.
>
> This wouldn't be a layering violation because 1) global confirms *are*
> channels-related, 2) both ssh and sshd could conceivably issue and
> therefore want to track them (though sshd doesn't ATM) and 3) I think
> you've demonstrated that the mux proxy code needs this facility to
> function properly.
>
> Anyway that's one possible approach...
>
> -d
> _______________________________________________
> openssh-unix-dev mailing list
> openssh-unix-dev at mindrot.org
> https://lists.mindrot.org/mailman/listinfo/openssh-unix-dev

I've had some more time to work on this, and I think I've gotten it to a 
working state.

The first attached patch moves the global requests confirm list into 
`struct ssh_channels` as suggested, the second patch then adds support 
for both TCP and unix socket remote forwarding. I've added some tests in 
`regress/forwarding-proxy.sh`, essentially just a copy of 
`regress/forwarding.sh` (the file header is a bit of a guess).

I'll hopefully look at submitting a bug report for this later on today.

One outstanding question: it looks like there's a memory leak of the 
passed-in context in `mux_confirm_remote_forward()` in the first `if` 
statement, if so then I guess that should have its own report?

Thanks!
-------------- next part --------------
From 543f62efa3f9d289d8913dbcaa7b8f1b15814f36 Mon Sep 17 00:00:00 2001
From: ding <ding at diinngg.com>
Date: Wed, 24 Dec 2025 22:41:12 +0000
Subject: [PATCH] Move global requests callback list to struct ssh_channels

---
 channels.c   | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 channels.h   |  5 +++++
 clientloop.c | 59 ++++--------------------------------------------
 clientloop.h |  4 ----
 mux.c        |  2 +-
 ssh.c        |  3 ++-
 6 files changed, 75 insertions(+), 61 deletions(-)

diff --git a/channels.c b/channels.c
index 80014ff34..7a3a22979 100644
--- a/channels.c
+++ b/channels.c
@@ -96,6 +96,15 @@
 /* Per-channel callback for pre/post IO actions */
 typedef void chan_fn(struct ssh *, Channel *c);
 
+/* Global request success/failure callbacks */
+struct global_confirm {
+	TAILQ_ENTRY(global_confirm) entry;
+	global_confirm_cb *cb;
+	void *ctx;
+	int ref_count;
+};
+TAILQ_HEAD(global_confirms, global_confirm);
+
 /*
  * Data structure for storing which hosts are permitted for forward requests.
  * The local sides of any remote forwards are stored in this array to prevent
@@ -170,6 +179,9 @@ struct ssh_channels {
 	chan_fn **channel_pre;
 	chan_fn **channel_post;
 
+	/* Global confirm callbacks */
+	struct global_confirms global_confirms;
+
 	/* -- tcp forwarding */
 	struct permission_set local_perms;
 	struct permission_set remote_perms;
@@ -236,6 +248,8 @@ channel_init_channels(struct ssh *ssh)
 		fatal_f("allocation failed");
 	sc->channels_alloc = 10;
 	sc->channels = xcalloc(sc->channels_alloc, sizeof(*sc->channels));
+	sc->global_confirms =
+		(struct global_confirms)TAILQ_HEAD_INITIALIZER(sc->global_confirms);
 	sc->IPv4or6 = AF_UNSPEC;
 	sc->bulk_classifier_tty = xstrdup(CHANNEL_BULK_TTY);
 	sc->bulk_classifier_notty = xstrdup(CHANNEL_BULK_NOTTY);
@@ -859,6 +873,8 @@ void
 channel_free_channels(struct ssh *ssh)
 {
 	struct ssh_channels *sc;
+	struct global_confirms *gcs;
+	struct global_confirm *gc;
 
 	if (ssh == NULL || ssh->chanctxt == NULL)
 		return;
@@ -868,6 +884,11 @@ channel_free_channels(struct ssh *ssh)
 	channel_clear_permission(ssh, FORWARD_ADM, FORWARD_LOCAL);
 	channel_clear_permission(ssh, FORWARD_ADM, FORWARD_REMOTE);
 	sc = ssh->chanctxt;
+	gcs = &sc->global_confirms;
+	while ((gc = TAILQ_FIRST(gcs)) != NULL) {
+		TAILQ_REMOVE(gcs, gc, entry);
+		freezero(gc, sizeof(*gc));
+	}
 	free(sc->bulk_classifier_tty);
 	free(sc->bulk_classifier_notty);
 	free(sc->channel_pre);
@@ -1312,6 +1333,48 @@ channel_set_fds(struct ssh *ssh, int id, int rfd, int wfd, int efd,
 		fatal_fr(r, "channel %i", c->self);
 }
 
+void
+channel_register_global_confirm(struct ssh *ssh, global_confirm_cb *cb,
+	void *ctx)
+{
+	struct global_confirms *gcs = &ssh->chanctxt->global_confirms;
+	struct global_confirm *gc, *last_gc;
+
+	/* Coalesce identical callbacks */
+	last_gc = TAILQ_LAST(gcs, global_confirms);
+	if (last_gc && last_gc->cb == cb && last_gc->ctx == ctx) {
+		if (++last_gc->ref_count >= INT_MAX)
+			fatal_f("last_gc->ref_count = %d",
+				last_gc->ref_count);
+		return;
+	}
+
+	gc = xcalloc(1, sizeof(*gc));
+	gc->cb = cb;
+	gc->ctx = ctx;
+	gc->ref_count = 1;
+	TAILQ_INSERT_TAIL(gcs, gc, entry);
+}
+
+int
+channel_global_request_reply(int type, u_int32_t seq, struct ssh *ssh)
+{
+	struct global_confirms *gcs = &ssh->chanctxt->global_confirms;
+	struct global_confirm *gc;
+
+	if ((gc = TAILQ_FIRST(gcs)) == NULL)
+		return 0;
+	if (gc->cb != NULL)
+		gc->cb(ssh, type, seq, gc->ctx);
+	if (--gc->ref_count <= 0) {
+		TAILQ_REMOVE(gcs, gc, entry);
+		freezero(gc, sizeof(*gc));
+	}
+
+	ssh_packet_set_alive_timeouts(ssh, 0);
+	return 0;
+}
+
 static void
 channel_pre_listener(struct ssh *ssh, Channel *c)
 {
diff --git a/channels.h b/channels.h
index 7456541f8..edfa50dc1 100644
--- a/channels.h
+++ b/channels.h
@@ -318,6 +318,11 @@ int	 channel_close_fd(struct ssh *, Channel *, int *);
 void	 channel_send_window_changes(struct ssh *);
 int	 channel_has_bulk(struct ssh *);
 
+/* global request confirmation callbacks */
+typedef void global_confirm_cb(struct ssh *, int, u_int32_t, void *);
+void channel_register_global_confirm(struct ssh *, global_confirm_cb *, void *);
+int channel_global_request_reply(int, u_int32_t, struct ssh *);
+
 /* channel inactivity timeouts */
 void channel_add_timeout(struct ssh *, const char *, int);
 void channel_clear_timeouts(struct ssh *);
diff --git a/clientloop.c b/clientloop.c
index a78dfa6e0..fe3b98d63 100644
--- a/clientloop.c
+++ b/clientloop.c
@@ -174,18 +174,6 @@ struct channel_reply_ctx {
 	enum confirm_action action;
 };
 
-/* Global request success/failure callbacks */
-/* XXX move to struct ssh? */
-struct global_confirm {
-	TAILQ_ENTRY(global_confirm) entry;
-	global_confirm_cb *cb;
-	void *ctx;
-	int ref_count;
-};
-TAILQ_HEAD(global_confirms, global_confirm);
-static struct global_confirms global_confirms =
-    TAILQ_HEAD_INITIALIZER(global_confirms);
-
 static void quit_message(const char *fmt, ...)
     __attribute__((__format__ (printf, 1, 2)));
 
@@ -468,24 +456,6 @@ client_check_window_change(struct ssh *ssh)
 	channel_send_window_changes(ssh);
 }
 
-static int
-client_global_request_reply(int type, u_int32_t seq, struct ssh *ssh)
-{
-	struct global_confirm *gc;
-
-	if ((gc = TAILQ_FIRST(&global_confirms)) == NULL)
-		return 0;
-	if (gc->cb != NULL)
-		gc->cb(ssh, type, seq, gc->ctx);
-	if (--gc->ref_count <= 0) {
-		TAILQ_REMOVE(&global_confirms, gc, entry);
-		freezero(gc, sizeof(*gc));
-	}
-
-	ssh_packet_set_alive_timeouts(ssh, 0);
-	return 0;
-}
-
 static void
 schedule_server_alive_check(void)
 {
@@ -508,7 +478,7 @@ server_alive_check(struct ssh *ssh)
 	    (r = sshpkt_send(ssh)) != 0)
 		fatal_fr(r, "send packet");
 	/* Insert an empty placeholder to maintain ordering */
-	client_register_global_confirm(NULL, NULL);
+	channel_register_global_confirm(ssh, NULL, NULL);
 	schedule_server_alive_check();
 }
 
@@ -896,27 +866,6 @@ client_expect_confirm(struct ssh *ssh, int id, const char *request,
 	    client_abandon_status_confirm, cr);
 }
 
-void
-client_register_global_confirm(global_confirm_cb *cb, void *ctx)
-{
-	struct global_confirm *gc, *last_gc;
-
-	/* Coalesce identical callbacks */
-	last_gc = TAILQ_LAST(&global_confirms, global_confirms);
-	if (last_gc && last_gc->cb == cb && last_gc->ctx == ctx) {
-		if (++last_gc->ref_count >= INT_MAX)
-			fatal_f("last_gc->ref_count = %d",
-			    last_gc->ref_count);
-		return;
-	}
-
-	gc = xcalloc(1, sizeof(*gc));
-	gc->cb = cb;
-	gc->ctx = ctx;
-	gc->ref_count = 1;
-	TAILQ_INSERT_TAIL(&global_confirms, gc, entry);
-}
-
 /*
  * Returns non-zero if the client is able to handle a hostkeys-00 at openssh.com
  * hostkey update request.
@@ -2647,7 +2596,7 @@ client_input_hostkeys(struct ssh *ssh)
 	}
 	if ((r = sshpkt_send(ssh)) != 0)
 		fatal_fr(r, "send hostkeys-prove");
-	client_register_global_confirm(
+	channel_register_global_confirm(ssh,
 	    client_global_hostkeys_prove_confirm, ctx);
 	ctx = NULL;  /* will be freed in callback */
 	prove_sent = 1;
@@ -2841,8 +2790,8 @@ client_init_dispatch(struct ssh *ssh)
 	ssh_dispatch_set(ssh, SSH2_MSG_KEXINIT, &kex_input_kexinit);
 
 	/* global request reply messages */
-	ssh_dispatch_set(ssh, SSH2_MSG_REQUEST_FAILURE, &client_global_request_reply);
-	ssh_dispatch_set(ssh, SSH2_MSG_REQUEST_SUCCESS, &client_global_request_reply);
+	ssh_dispatch_set(ssh, SSH2_MSG_REQUEST_FAILURE, &channel_global_request_reply);
+	ssh_dispatch_set(ssh, SSH2_MSG_REQUEST_SUCCESS, &channel_global_request_reply);
 }
 
 void
diff --git a/clientloop.h b/clientloop.h
index 1f550b35c..7997cee32 100644
--- a/clientloop.h
+++ b/clientloop.h
@@ -54,10 +54,6 @@ void	*client_new_escape_filter_ctx(int);
 void	 client_filter_cleanup(struct ssh *, int, void *);
 int	 client_simple_escape_filter(struct ssh *, Channel *, char *, int);
 
-/* Global request confirmation callbacks */
-typedef void global_confirm_cb(struct ssh *, int, u_int32_t, void *);
-void	 client_register_global_confirm(global_confirm_cb *, void *);
-
 /* Channel request confirmation callbacks */
 enum confirm_action { CONFIRM_WARN = 0, CONFIRM_CLOSE, CONFIRM_TTY };
 void client_expect_confirm(struct ssh *, int, const char *,
diff --git a/mux.c b/mux.c
index 53cbab0fc..9654b3d96 100644
--- a/mux.c
+++ b/mux.c
@@ -865,7 +865,7 @@ mux_master_process_open_fwd(struct ssh *ssh, u_int rid,
 		fctx->cid = c->self;
 		fctx->rid = rid;
 		fctx->fid = options.num_remote_forwards - 1;
-		client_register_global_confirm(mux_confirm_remote_forward,
+		channel_register_global_confirm(ssh, mux_confirm_remote_forward,
 		    fctx);
 		freefwd = 0;
 		c->mux_pause = 1; /* wait for mux_confirm_remote_forward */
diff --git a/ssh.c b/ssh.c
index 461b60975..38a812a40 100644
--- a/ssh.c
+++ b/ssh.c
@@ -2153,7 +2153,8 @@ ssh_init_forwarding(struct ssh *ssh, char **ifname)
 		if ((options.remote_forwards[i].handle =
 		    channel_request_remote_forwarding(ssh,
 		    &options.remote_forwards[i])) >= 0) {
-			client_register_global_confirm(
+			channel_register_global_confirm(
+				ssh,
 			    ssh_confirm_remote_forward,
 			    &options.remote_forwards[i]);
 			forward_confirms_pending++;
-- 
2.51.0

-------------- next part --------------
From b6d51aff533d9050c76e41938536d963181cee63 Mon Sep 17 00:00:00 2001
From: ding <ding at diinngg.com>
Date: Sat, 27 Dec 2025 12:41:01 +0000
Subject: [PATCH] Add support for remote forwarding in proxy mode

---
 channels.c                  | 242 +++++++++++++++++++++++++++++++++---
 clientloop.c                |  36 +++++-
 packet.c                    |   4 +-
 regress/Makefile            |   1 +
 regress/forwarding-proxy.sh | 124 ++++++++++++++++++
 5 files changed, 383 insertions(+), 24 deletions(-)
 create mode 100644 regress/forwarding-proxy.sh

diff --git a/channels.c b/channels.c
index 7a3a22979..5e642d51f 100644
--- a/channels.c
+++ b/channels.c
@@ -237,6 +237,12 @@ static int rdynamic_connect_finish(struct ssh *, Channel *);
 /* Setup helper */
 static void channel_handler_init(struct ssh_channels *sc);
 
+/* Remote forwarding permission check helpers */
+static int open_listen_match_tcpip(struct permission *allowed_open,
+	const char *requestedhost, u_short requestedport, int translate);
+static int open_listen_match_streamlocal(struct permission *allowed_open,
+	const char *requestedpath);
+
 /* -- channel core */
 
 void
@@ -3260,6 +3266,131 @@ channel_output_poll(struct ssh *ssh)
  *    easily.
  */
 
+static void
+channel_proxy_remote_forward(struct ssh *ssh, Channel *downstream,
+	const char *listen_host, const char *listen_path, int listen_port)
+{
+	permission_set_add(ssh, FORWARD_USER, FORWARD_LOCAL,
+		"<mux>", -1, listen_host, listen_path, listen_port, downstream);
+}
+
+static int
+channel_proxy_cancel_remote_forward_tcpip(struct ssh *ssh,
+	const char *listen_host, int listen_port)
+{
+	struct permission *perm = NULL, **permp;
+	u_int i, *npermp;
+
+	permission_set_get_array(ssh, FORWARD_USER, FORWARD_LOCAL, &permp, &npermp);
+	for (i = 0; i < *npermp; i++) {
+		perm = (*permp + i);
+		if (open_listen_match_tcpip(perm, listen_host, listen_port, 0)) {
+			fwd_perm_clear(perm);
+			return 1;
+		}
+	}
+	return 0;
+}
+
+static int
+channel_proxy_cancel_remote_forward_streamlocal(struct ssh *ssh,
+	const char *listen_path)
+{
+	struct permission *perm = NULL, **permp;
+	u_int i, *npermp;
+
+	permission_set_get_array(ssh, FORWARD_USER, FORWARD_LOCAL, &permp, &npermp);
+	for (i = 0; i < *npermp; i++) {
+		perm = (*permp + i);
+		if (open_listen_match_streamlocal(perm, listen_path)) {
+			fwd_perm_clear(perm);
+			return 1;
+		}
+	}
+	return 0;
+}
+
+static void
+channel_proxy_cancel_remote_forward(struct ssh *ssh,
+	const char *listen_host, const char *listen_path, int listen_port)
+{
+	if (listen_path != NULL) {
+		if (channel_proxy_cancel_remote_forward_streamlocal(ssh, listen_path))
+			return;
+	} else {
+		if (channel_proxy_cancel_remote_forward_tcpip(ssh,
+			listen_host, listen_port))
+			return;
+	}
+	debug_f("requested forward not found");
+}
+
+struct channel_proxy_remote_forward_ctx {
+	int cid;		/* Channel id. */
+	int cancel;		/* If this is a cancel request. */
+	char *host;		/* Remote forward host. */
+	char *path;		/* Remote forward socket path. */
+	int port;		/* Remote forward port. */
+};
+
+static void
+channel_proxy_confirm_remote_forward(struct ssh *ssh, int type,
+	u_int32_t seq, void *ctxt)
+{
+	struct channel_proxy_remote_forward_ctx *ctx = ctxt;
+	Channel *downstream;
+	struct sshbuf *b = NULL;
+	const u_char *cp = NULL;
+	size_t len;
+	u_int port;
+	int r;
+
+	if ((downstream = channel_by_id(ssh, ctx->cid)) == NULL) {
+		/* No channel for reply. */
+		error_f("unknown channel");
+		goto free_ctx;
+	}
+	if ((b = sshbuf_new()) == NULL)
+		fatal_f("sshbuf_new");
+	/* Forward the packet to muxclient. */
+	cp = sshpkt_ptr(ssh, &len);
+	if (cp == NULL) {
+		error_f("no packet");
+		goto out;
+	}
+	if ((r = sshbuf_put_u8(b, 0)) != 0 ||	/* Padlen. */
+		(r = sshbuf_put_u8(b, type)) != 0 ||
+		(r = sshbuf_put(b, cp, len)) != 0 ||
+		(r = sshbuf_put_stringb(downstream->output, b)) != 0) {
+		error_fr(r, "forward muxclient");
+		goto out;
+	}
+
+	/* Update permissions. */
+	if (!ctx->cancel) {
+		if (type == SSH2_MSG_REQUEST_SUCCESS) {
+			if (ctx->port == 0) {
+				/* Update the remote port. */
+				if ((r = sshpkt_get_u32(ssh, &port)) != 0)
+					fatal_fr(r, "parse packet");
+				ctx->port = (int)port;
+			}
+			channel_proxy_remote_forward(ssh, downstream,
+				ctx->host, ctx->path, ctx->port);
+		}
+	} else {
+		channel_proxy_cancel_remote_forward(ssh,
+			ctx->host, ctx->path, ctx->port);
+	}
+
+out:
+	sshbuf_free(b);
+free_ctx:
+	free(ctx->host);
+	free(ctx->path);
+	free(ctx);
+}
+
 /*
  * receive packets from downstream mux clients:
  * channel callback fired on read from mux client, creates
@@ -3272,11 +3403,13 @@ channel_proxy_downstream(struct ssh *ssh, Channel *downstream)
 	Channel *c = NULL;
 	struct sshbuf *original = NULL, *modified = NULL;
 	const u_char *cp;
-	char *ctype = NULL, *listen_host = NULL;
+	char *ctype = NULL, *listen_host = NULL, *listen_path = NULL;
+	u_char want_reply;
 	u_char type;
 	size_t have;
 	int ret = -1, r;
 	u_int id, remote_id, listen_port;
+	struct channel_proxy_remote_forward_ctx *rfwd_ctx = NULL;
 
 	/* sshbuf_dump(downstream->input, stderr); */
 	if ((r = sshbuf_get_string_direct(downstream->input, &cp, &have))
@@ -3354,28 +3487,100 @@ channel_proxy_downstream(struct ssh *ssh, Channel *downstream)
 			error_f("alloc");
 			goto out;
 		}
-		if ((r = sshbuf_get_cstring(original, &ctype, NULL)) != 0) {
+		if ((r = sshbuf_get_cstring(original, &ctype, NULL)) != 0 ||
+			(r = sshbuf_get_u8(original, &want_reply)) != 0) {
 			error_fr(r, "parse");
 			goto out;
 		}
-		if (strcmp(ctype, "tcpip-forward") != 0) {
+		if (strcmp(ctype, "tcpip-forward") == 0) {
+			if ((r = sshbuf_get_cstring(original, &listen_host, NULL)) != 0 ||
+				(r = sshbuf_get_u32(original, &listen_port)) != 0) {
+				error_fr(r, "parse");
+				goto out;
+			}
+			if (listen_port > 65535) {
+				error_f("tcpip-forward for %s: bad port %u",
+				listen_host, listen_port);
+				goto out;
+			}
+			if (want_reply) {
+				rfwd_ctx = xcalloc(1, sizeof(*rfwd_ctx));
+				rfwd_ctx->cid = downstream->self;
+				rfwd_ctx->cancel = 0;
+				rfwd_ctx->host = xstrdup(listen_host);
+				rfwd_ctx->path = NULL;
+				rfwd_ctx->port = (int)listen_port;
+				channel_register_global_confirm(ssh,
+					channel_proxy_confirm_remote_forward, rfwd_ctx);
+			} else {
+				channel_proxy_remote_forward(ssh, downstream,
+					listen_host, NULL, (int)listen_port);
+			}
+		} else if (strcmp(ctype, "cancel-tcpip-forward") == 0) {
+			if ((r = sshbuf_get_cstring(original, &listen_host, NULL)) != 0 ||
+				(r = sshbuf_get_u32(original, &listen_port)) != 0) {
+				error_fr(r, "parse");
+				goto out;
+			}
+			if (listen_port > 65535) {
+				error_f("cancel-tcpip-forward for %s: bad port %u",
+				listen_host, listen_port);
+				goto out;
+			}
+			if (want_reply) {
+				rfwd_ctx = xcalloc(1, sizeof(*rfwd_ctx));
+				rfwd_ctx->cid = downstream->self;
+				rfwd_ctx->cancel = 1;
+				rfwd_ctx->host = xstrdup(listen_host);
+				rfwd_ctx->path = NULL;
+				rfwd_ctx->port = (int)listen_port;
+				channel_register_global_confirm(ssh,
+					channel_proxy_confirm_remote_forward, rfwd_ctx);
+			} else {
+				channel_proxy_cancel_remote_forward(ssh,
+					listen_host, NULL, (int)listen_port);
+			}
+		} else if (strcmp(ctype, "streamlocal-forward at openssh.com") == 0) {
+			if ((r = sshbuf_get_cstring(original, &listen_path, NULL)) != 0) {
+				error_fr(r, "parse");
+				goto out;
+			}
+			if (want_reply) {
+				rfwd_ctx = xcalloc(1, sizeof(*rfwd_ctx));
+				rfwd_ctx->cid = downstream->self;
+				rfwd_ctx->cancel = 0;
+				rfwd_ctx->host = NULL;
+				rfwd_ctx->path = xstrdup(listen_path);
+				rfwd_ctx->port = PORT_STREAMLOCAL;
+				channel_register_global_confirm(ssh,
+					channel_proxy_confirm_remote_forward, rfwd_ctx);
+			} else {
+				channel_proxy_remote_forward(ssh, downstream,
+					NULL, listen_path, 0);
+			}
+		} else if (strcmp(ctype,
+				"cancel-streamlocal-forward at openssh.com") == 0) {
+			if ((r = sshbuf_get_cstring(original, &listen_path, NULL)) != 0) {
+				error_fr(r, "parse");
+				goto out;
+			}
+			if (want_reply) {
+				rfwd_ctx = xcalloc(1, sizeof(*rfwd_ctx));
+				rfwd_ctx->cid = downstream->self;
+				rfwd_ctx->cancel = 1;
+				rfwd_ctx->host = NULL;
+				rfwd_ctx->path = xstrdup(listen_path);
+				rfwd_ctx->port = PORT_STREAMLOCAL;
+				channel_register_global_confirm(ssh,
+					channel_proxy_confirm_remote_forward, rfwd_ctx);
+			} else {
+				channel_proxy_cancel_remote_forward(ssh,
+					NULL, listen_path, 0);
+			}
+		} else {
 			error_f("unsupported request %s", 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_fr(r, "parse");
-			goto out;
-		}
-		if (listen_port > 65535) {
-			error_f("tcpip-forward for %s: bad port %u",
-			    listen_host, listen_port);
-			goto out;
-		}
-		/* Record that connection to this host/port is permitted. */
-		permission_set_add(ssh, FORWARD_USER, FORWARD_LOCAL, "<mux>",
-		    -1, listen_host, NULL, (int)listen_port, downstream);
 		break;
 	case SSH2_MSG_CHANNEL_CLOSE:
 		if (have < 4)
@@ -3408,6 +3613,7 @@ channel_proxy_downstream(struct ssh *ssh, Channel *downstream)
  out:
 	free(ctype);
 	free(listen_host);
+	free(listen_path);
 	sshbuf_free(original);
 	sshbuf_free(modified);
 	return ret;
@@ -4920,6 +5126,8 @@ channel_connect_by_listen_path(struct ssh *ssh, const char *path,
 
 	for (i = 0; i < pset->num_permitted_user; i++) {
 		perm = &pset->permitted_user[i];
+		if (perm->downstream)
+			return perm->downstream;
 		if (open_listen_match_streamlocal(perm, path)) {
 			return connect_to(ssh,
 			    perm->host_to_connect, perm->port_to_connect,
diff --git a/clientloop.c b/clientloop.c
index fe3b98d63..1c39e6550 100644
--- a/clientloop.c
+++ b/clientloop.c
@@ -1726,16 +1726,17 @@ client_request_forwarded_tcpip(struct ssh *ssh, const char *request_type,
 }
 
 static Channel *
-client_request_forwarded_streamlocal(struct ssh *ssh,
-    const char *request_type, int rchan)
+client_request_forwarded_streamlocal(struct ssh *ssh, const char *request_type,
+	int rchan, u_int rwindow, u_int rmaxpack)
 {
 	Channel *c = NULL;
-	char *listen_path;
+	struct sshbuf *b = NULL;
+	char *listen_path, *reserved;
 	int r;
 
 	/* Get the remote path. */
 	if ((r = sshpkt_get_cstring(ssh, &listen_path, NULL)) != 0 ||
-	    (r = sshpkt_get_string(ssh, NULL, NULL)) != 0 ||	/* reserved */
+	    (r = sshpkt_get_cstring(ssh, &reserved, NULL)) != 0 ||
 	    (r = sshpkt_get_end(ssh)) != 0)
 		fatal_fr(r, "parse packet");
 
@@ -1743,7 +1744,31 @@ client_request_forwarded_streamlocal(struct ssh *ssh,
 
 	c = channel_connect_by_listen_path(ssh, listen_path,
 	    "forwarded-streamlocal at openssh.com", "forwarded-streamlocal");
+
+	if (c != NULL && c->type == SSH_CHANNEL_MUX_CLIENT) {
+		if ((b = sshbuf_new()) == NULL) {
+			error_f("alloc reply");
+			goto out;
+		}
+		/* reconstruct and send to muxclient */
+		if ((r = sshbuf_put_u8(b, 0)) != 0 ||	/* padlen */
+			(r = sshbuf_put_u8(b, SSH2_MSG_CHANNEL_OPEN)) != 0 ||
+			(r = sshbuf_put_cstring(b, request_type)) != 0 ||
+			(r = sshbuf_put_u32(b, rchan)) != 0 ||
+			(r = sshbuf_put_u32(b, rwindow)) != 0 ||
+			(r = sshbuf_put_u32(b, rmaxpack)) != 0 ||
+			(r = sshbuf_put_cstring(b, listen_path)) != 0 ||
+			(r = sshbuf_put_cstring(b, reserved)) != 0 ||
+			(r = sshbuf_put_stringb(c->output, b)) != 0) {
+			error_fr(r, "compose for muxclient");
+			goto out;
+		}
+	}
+
+out:
+	sshbuf_free(b);
 	free(listen_path);
+	free(reserved);
 	return c;
 }
 
@@ -1891,7 +1916,8 @@ client_input_channel_open(int type, u_int32_t seq, struct ssh *ssh)
 		c = client_request_forwarded_tcpip(ssh, ctype, rchan, rwindow,
 		    rmaxpack);
 	} else if (strcmp(ctype, "forwarded-streamlocal at openssh.com") == 0) {
-		c = client_request_forwarded_streamlocal(ssh, ctype, rchan);
+		c = client_request_forwarded_streamlocal(ssh, ctype, rchan, rwindow,
+			rmaxpack);
 	} else if (strcmp(ctype, "x11") == 0) {
 		c = client_request_x11(ssh, ctype, rchan);
 	} else if (strcmp(ctype, "auth-agent at openssh.com") == 0) {
diff --git a/packet.c b/packet.c
index 2a5a56a88..7d0c289bc 100644
--- a/packet.c
+++ b/packet.c
@@ -1550,10 +1550,10 @@ ssh_packet_read_poll2_mux(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p)
 	*typep = SSH_MSG_NONE;
 	cp = sshbuf_ptr(state->input);
 	if (state->packlen == 0) {
-		if (sshbuf_len(state->input) < 4 + 1)
+		if (sshbuf_len(state->input) < 4 + 1 + 1) /* packlen + padlen + type */
 			return 0; /* packet is incomplete */
 		state->packlen = PEEK_U32(cp);
-		if (state->packlen < 4 + 1 ||
+		if (state->packlen < 1 + 1 || /* padlen + type */
 		    state->packlen > PACKET_MAX_SIZE)
 			return SSH_ERR_MESSAGE_INCOMPLETE;
 	}
diff --git a/regress/Makefile b/regress/Makefile
index bd44b0489..c5cf6dd9b 100644
--- a/regress/Makefile
+++ b/regress/Makefile
@@ -71,6 +71,7 @@ LTESTS= 	connect \
 		reconfigure \
 		dynamic-forward \
 		forwarding \
+		forwarding-proxy \
 		multiplex \
 		reexec \
 		brokenkeys \
diff --git a/regress/forwarding-proxy.sh b/regress/forwarding-proxy.sh
new file mode 100644
index 000000000..b90e5916d
--- /dev/null
+++ b/regress/forwarding-proxy.sh
@@ -0,0 +1,124 @@
+#	$OpenBSD: forwarding-proxy.sh,v 1.00 2025/12/27 11:51:45 ding Exp $
+#	Placed in the Public Domain.
+
+tid="local and remote forwarding (proxy mode)"
+
+DATA=/bin/ls${EXEEXT}
+
+start_sshd
+
+base=33
+last=$PORT
+fwd=""
+make_tmpdir
+CTL=${SSH_REGRESS_TMP}/ctl-sock
+CTL=/Users/ding/Documents/dev/cpp/openssh-portable/sshcs
+SSH_PROXY="${SSH} -S $CTL -O proxy"
+
+start_proxy() {
+    rm -f $CTL
+    ${SSH} -F $OBJ/ssh_config -S $CTL -M -N -f somehost
+}
+
+stop_proxy() {
+    ${SSH} -F $OBJ/ssh_config -S $CTL -O exit somehost 2>/dev/null
+    rm -f $CTL
+}
+
+for j in 0 1 2; do
+	for i in 0 1 2; do
+		a=$base$j$i
+		b=`expr $a + 50`
+		c=$last
+		# fwd chain: $a -> $b -> $c
+		fwd="$fwd -L$a:127.0.0.1:$b -R$b:127.0.0.1:$c"
+		last=$a
+	done
+done
+
+start_proxy
+trace "start forwarding, fork to background"
+${SSH_PROXY} -N -F $OBJ/ssh_config -f $fwd somehost
+
+trace "transfer over forwarded channels and check result"
+${SSH} -F $OBJ/ssh_config -p$last -o 'ConnectionAttempts=10' \
+	somehost cat ${DATA} > ${COPY}
+test -s ${COPY}		|| fail "failed copy of ${DATA}"
+cmp ${DATA} ${COPY}	|| fail "corrupted copy of ${DATA}"
+stop_proxy
+
+start_proxy
+for d in L R; do
+	trace "exit on -$d forward failure"
+
+	# this one should succeed
+	${SSH_PROXY} -F $OBJ/ssh_config \
+	    -$d ${base}01:127.0.0.1:$PORT \
+	    -$d ${base}02:127.0.0.1:$PORT \
+	    -$d ${base}03:127.0.0.1:$PORT \
+	    -$d ${base}04:127.0.0.1:$PORT \
+	    -oExitOnForwardFailure=yes somehost true
+	if [ $? != 0 ]; then
+		fatal "connection failed, should not"
+	else
+		# this one should fail
+		${SSH_PROXY} -q -F $OBJ/ssh_config \
+		    -$d ${base}01:127.0.0.1:$PORT \
+		    -$d ${base}02:127.0.0.1:$PORT \
+		    -$d ${base}03:127.0.0.1:$PORT \
+		    -$d ${base}01:localhost:$PORT \
+		    -$d ${base}04:127.0.0.1:$PORT \
+		    -oExitOnForwardFailure=yes somehost true
+		r=$?
+		if [ $r != 255 ]; then
+			fail "connection not terminated, but should ($r)"
+		fi
+	fi
+done
+stop_proxy
+
+start_proxy
+trace "simple clear forwarding"
+${SSH_PROXY} -F $OBJ/ssh_config -oClearAllForwardings=yes somehost true
+stop_proxy
+
+start_proxy
+trace "clear local forward"
+${SSH_PROXY} -N -f -F $OBJ/ssh_config -L ${base}01:127.0.0.1:$PORT \
+    -oClearAllForwardings=yes somehost
+if [ $? != 0 ]; then
+	fail "connection failed with cleared local forwarding"
+else
+	# this one should fail
+	${SSH} -F $OBJ/ssh_config -p ${base}01 somehost true \
+	     >>$TEST_REGRESS_LOGFILE 2>&1 && \
+		fail "local forwarding not cleared"
+fi
+stop_proxy
+
+start_proxy
+trace "clear remote forward"
+${SSH_PROXY} -N -f -F $OBJ/ssh_config -R ${base}01:127.0.0.1:$PORT \
+    -oClearAllForwardings=yes somehost
+if [ $? != 0 ]; then
+	fail "connection failed with cleared remote forwarding"
+else
+	# this one should fail
+	${SSH} -F $OBJ/ssh_config -p ${base}01 somehost true \
+	     >>$TEST_REGRESS_LOGFILE 2>&1 && \
+		fail "remote forwarding not cleared"
+fi
+stop_proxy
+
+start_proxy
+trace "transfer over chained unix domain socket forwards and check result"
+rm -f $OBJ/unix-[123].fwd
+${SSH_PROXY} -N -f -F $OBJ/ssh_config -L[$OBJ/unix-3.fwd]:127.0.0.1:$PORT somehost
+${SSH_PROXY} -N -f -F $OBJ/ssh_config -R[$OBJ/unix-2.fwd]:[$OBJ/unix-3.fwd] somehost
+${SSH_PROXY} -N -f -F $OBJ/ssh_config -L[$OBJ/unix-1.fwd]:[$OBJ/unix-2.fwd] somehost
+${SSH_PROXY} -N -f -F $OBJ/ssh_config -R${base}01:[$OBJ/unix-1.fwd] somehost
+${SSH} -F $OBJ/ssh_config -p${base}01 -o 'ConnectionAttempts=10' \
+	somehost cat ${DATA} > ${COPY}
+test -s ${COPY}			|| fail "failed copy ${DATA}"
+cmp ${DATA} ${COPY}		|| fail "corrupted copy of ${DATA}"
+stop_proxy
-- 
2.51.0



More information about the openssh-unix-dev mailing list