From 8d0578478586e283e751ca51e7b0690631da139a Mon Sep 17 00:00:00 2001 From: "markus@openbsd.org" Date: Fri, 30 Sep 2016 09:19:13 +0000 Subject: 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 --- channels.c | 392 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 380 insertions(+), 12 deletions(-) (limited to 'channels.c') diff --git a/channels.c b/channels.c index 5d8c2a0c0..ae248414b 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 * Copyright (c) 1995 Tatu Ylonen , 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 @@ typedef struct { 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 @@ static int IPv4or6 = AF_UNSPEC; /* 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 @@ channel_by_id(int id) 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 @@ channel_lookup(int id) 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 @@ channel_free(Channel *c) { 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 @@ channel_still_open(void) 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 @@ channel_find_open(void) 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 @@ channel_find_open(void) 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 @@ channel_open_message(void) 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 @@ channel_open_message(void) 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 @@ channel_output_poll(void) } } +/* -- 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(""); + 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 @@ channel_input_data(int type, u_int32_t seq, void *ctxt) 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 @@ channel_input_extended_data(int type, u_int32_t seq, void *ctxt) 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 @@ channel_input_ieof(int type, u_int32_t seq, void *ctxt) 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 @@ channel_input_close(int type, u_int32_t seq, void *ctxt) 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 @@ channel_input_oclose(int type, u_int32_t seq, void *ctxt) 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 @@ channel_input_close_confirmation(int type, u_int32_t seq, void *ctxt) 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 @@ channel_input_open_confirmation(int type, u_int32_t seq, void *ctxt) 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 @@ channel_input_open_failure(int type, u_int32_t seq, void *ctxt) 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 @@ channel_input_window_adjust(int type, u_int32_t seq, void *ctxt) 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 @@ channel_input_status_confirm(int type, u_int32_t seq, void *ctxt) 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 @@ channel_request_remote_forwarding(struct Forward *fwd) 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 @@ channel_request_rforward_cancel_tcpip(const char *host, u_short port) 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 @@ channel_request_rforward_cancel_streamlocal(const char *path) 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 @@ channel_add_permitted_opens(char *host, int port) 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 @@ connect_to(const char *name, int port, char *ctype, char *rname) 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 @@ channel_connect_by_listen_address(const char *listen_host, 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); -- cgit v1.2.3