diff --git a/Recipe b/Recipe index 4ef4048e..22c6b3a1 100644 --- a/Recipe +++ b/Recipe @@ -299,9 +299,10 @@ NONSSH = telnet raw rlogin ldisc pinger SSH = ssh sshcrc sshdes sshmd5 sshrsa sshrand sshsha sshblowf + sshdh sshcrcda sshpubk sshzlib sshdss x11fwd portfwd + sshaes sshsh256 sshsh512 sshbn wildcard pinger ssharcf - + sshgssc pgssapi -WINSSH = SSH winnoise winsecur winpgntc wingss winhsock errsock -UXSSH = SSH uxnoise uxagentc uxgss + + sshgssc pgssapi sshshare +WINSSH = SSH winnoise winsecur winpgntc wingss winshare winnps winnpc + + winhsock errsock +UXSSH = SSH uxnoise uxagentc uxgss uxshare # SFTP implementation (pscp, psftp). SFTP = sftp int64 logging diff --git a/config.c b/config.c index 147f0c4e..5d019344 100644 --- a/config.c +++ b/config.c @@ -2099,6 +2099,26 @@ void setup_config_box(struct controlbox *b, int midsession, I(CONF_compression)); } + if (!midsession || protcfginfo != 1) { + s = ctrl_getset(b, "Connection/SSH", "sharing", "Sharing an SSH connection between PuTTY tools"); + + ctrl_checkbox(s, "Share SSH connections if possible", 's', + HELPCTX(ssh_share), + conf_checkbox_handler, + I(CONF_ssh_connection_sharing)); + + ctrl_text(s, "Permitted roles in a shared connection:", + HELPCTX(ssh_share)); + ctrl_checkbox(s, "Upstream (connecting to the real server)", 'u', + HELPCTX(ssh_share), + conf_checkbox_handler, + I(CONF_ssh_connection_sharing_upstream)); + ctrl_checkbox(s, "Downstream (connecting to the upstream PuTTY)", 'd', + HELPCTX(ssh_share), + conf_checkbox_handler, + I(CONF_ssh_connection_sharing_downstream)); + } + if (!midsession) { s = ctrl_getset(b, "Connection/SSH", "protocol", "Protocol options"); diff --git a/doc/config.but b/doc/config.but index 49c0ff6e..7a7c451a 100644 --- a/doc/config.but +++ b/doc/config.but @@ -2274,6 +2274,64 @@ If you select \q{1 only} or \q{2 only} here, PuTTY will only connect if the server you connect to offers the SSH protocol version you have specified. +\S{config-ssh-sharing} Sharing an SSH connection between PuTTY tools + +The controls in this box allow you to configure PuTTY to reuse an +existing SSH connection, where possible. + +The SSH-2 protocol permits you to run multiple data channels over the +same SSH connection, so that you can log in just once (and do the +expensive encryption setup just once) and then have more than one +terminal window open. + +Each instance of PuTTY can still run at most one terminal session, but +using the controls in this box, you can configure PuTTY to check if +another instance of itself has already connected to the target host, +and if so, share that instance's SSH connection instead of starting a +separate new one. + +To enable this feature, just tick the box \q{Share SSH connections if +possible}. Then, whenever you start up a PuTTY session connecting to a +particular host, it will try to reuse an existing SSH connection if +one is available. For example, selecting \q{Duplicate Session} from +the system menu will launch another session on the same host, and if +sharing is enabled then it will reuse the existing SSH connection. + +When this mode is in use, the first PuTTY that connected to a given +server becomes the \q{upstream}, which means that it is the one +managing the real SSH connection. All subsequent PuTTYs which reuse +the connection are referred to as \q{downstreams}: they do not connect +to the real server at all, but instead connect to the upstream PuTTY +via local inter-process communication methods. + +For this system to be activated, \e{both} the upstream and downstream +instances of PuTTY must have the sharing option enabled. + +The upstream PuTTY can therefore not terminate until all its +downstreams have closed. This is similar to the effect you get with +port forwarding or X11 forwarding, in which a PuTTY whose terminal +session has already finished will still remain open so as to keep +serving forwarded connections. + +In case you need to configure this system in more detail, there are +two additional checkboxes which allow you to specify whether a +particular PuTTY can act as an upstream or a downstream or both. +(These boxes only take effect if the main \q{Share SSH connections if +possible} box is also ticked.) By default both of these boxes are +ticked, so that multiple PuTTYs started from the same configuration +will designate one of themselves as the upstream and share a single +connection; but if for some reason you need a particular PuTTY +configuration \e{not} to be an upstream (e.g. because you definitely +need it to close promptly) or not to be a downstream (e.g. because it +needs to do its own authentication using a special private key) then +you can untick one or the other of these boxes. + +I have referred to \q{PuTTY} throughout the above discussion, but all +the other PuTTY tools which make SSH connections can use this +mechanism too. For example, if PSCP or PSFTP loads a configuration +with sharing enabled, then it can act as a downstream and use an +existing SSH connection set up by an instance of GUI PuTTY. The one +special case is that PSCP and PSFTP will \e{never} act as upstreams. \H{config-ssh-kex} The Kex panel diff --git a/logging.c b/logging.c index fc89db73..868ff987 100644 --- a/logging.c +++ b/logging.c @@ -235,7 +235,8 @@ void log_eventlog(void *handle, const char *event) void log_packet(void *handle, int direction, int type, char *texttype, const void *data, int len, int n_blanks, const struct logblank_t *blanks, - const unsigned long *seq) + const unsigned long *seq, + unsigned downstream_id, const char *additional_log_text) { struct LogContext *ctx = (struct LogContext *)handle; char dumpdata[80], smalldata[5]; @@ -248,15 +249,21 @@ void log_packet(void *handle, int direction, int type, /* Packet header. */ if (texttype) { - if (seq) { - logprintf(ctx, "%s packet #0x%lx, type %d / 0x%02x (%s)\r\n", - direction == PKT_INCOMING ? "Incoming" : "Outgoing", - *seq, type, type, texttype); - } else { - logprintf(ctx, "%s packet type %d / 0x%02x (%s)\r\n", - direction == PKT_INCOMING ? "Incoming" : "Outgoing", - type, type, texttype); - } + logprintf(ctx, "%s packet ", + direction == PKT_INCOMING ? "Incoming" : "Outgoing"); + + if (seq) + logprintf(ctx, "#0x%lx, ", *seq); + + logprintf(ctx, "type %d / 0x%02x (%s)", type, type, texttype); + + if (downstream_id) { + logprintf(ctx, " on behalf of downstream #%u", downstream_id); + if (additional_log_text) + logprintf(ctx, " (%s)", additional_log_text); + } + + logprintf(ctx, "\r\n"); } else { /* * Raw data is logged with a timestamp, so that it's possible diff --git a/network.h b/network.h index e3996284..675d24c7 100644 --- a/network.h +++ b/network.h @@ -100,6 +100,8 @@ Socket new_listener(char *srcaddr, int port, Plug plug, int local_host_only, Conf *conf, int addressfamily); SockAddr name_lookup(char *host, int port, char **canonicalname, Conf *conf, int addressfamily); +int proxy_for_destination (SockAddr addr, const char *hostname, int port, + Conf *conf); /* platform-dependent callback from new_connection() */ /* (same caveat about addr as new_connection()) */ @@ -116,6 +118,7 @@ void sk_cleanup(void); /* called just before program exit */ SockAddr sk_namelookup(const char *host, char **canonicalname, int address_family); SockAddr sk_nonamelookup(const char *host); void sk_getaddr(SockAddr addr, char *buf, int buflen); +int sk_addr_needs_port(SockAddr addr); int sk_hostname_is_local(const char *name); int sk_address_is_local(SockAddr addr); int sk_address_is_special_local(SockAddr addr); diff --git a/noshare.c b/noshare.c new file mode 100644 index 00000000..7412f840 --- /dev/null +++ b/noshare.c @@ -0,0 +1,24 @@ +/* + * Stub implementation of SSH connection-sharing IPC, for any + * platform which can't support it at all. + */ + +#include +#include +#include + +#include "tree234.h" +#include "putty.h" +#include "ssh.h" +#include "network.h" + +int platform_ssh_share(const char *name, Conf *conf, + Plug downplug, Plug upplug, Socket *sock, + char **logtext) +{ + return SHARE_NONE; +} + +void platform_ssh_share_cleanup(const char *name) +{ +} diff --git a/proxy.c b/proxy.c index c22f30f4..e2201c64 100644 --- a/proxy.c +++ b/proxy.c @@ -267,8 +267,8 @@ static int plug_proxy_accepting(Plug p, * This function can accept a NULL pointer as `addr', in which case * it will only check the host name. */ -static int proxy_for_destination (SockAddr addr, const char *hostname, - int port, Conf *conf) +int proxy_for_destination (SockAddr addr, const char *hostname, + int port, Conf *conf) { int s = 0, e = 0; char hostip[64]; diff --git a/pscp.c b/pscp.c index 32099323..484c348e 100644 --- a/pscp.c +++ b/pscp.c @@ -2307,6 +2307,9 @@ void cmdline_error(char *p, ...) exit(1); } +const int share_can_be_downstream = TRUE; +const int share_can_be_upstream = FALSE; + /* * Main program. (Called `psftp_main' because it gets called from * *sftp.c; bit silly, I know, but it had to be called _something_.) diff --git a/psftp.c b/psftp.c index 8ed2343a..c6ed98e9 100644 --- a/psftp.c +++ b/psftp.c @@ -2885,6 +2885,9 @@ void cmdline_error(char *p, ...) exit(1); } +const int share_can_be_downstream = TRUE; +const int share_can_be_upstream = FALSE; + /* * Main program. Parse arguments etc. */ diff --git a/putty.h b/putty.h index 89140f80..f97c723b 100644 --- a/putty.h +++ b/putty.h @@ -844,6 +844,9 @@ void cleanup_exit(int); * large window in SSH-2. \ */ \ X(INT, NONE, ssh_simple) \ + X(INT, NONE, ssh_connection_sharing) \ + X(INT, NONE, ssh_connection_sharing_upstream) \ + X(INT, NONE, ssh_connection_sharing_downstream) \ /* Options for pterm. Should split out into platform-dependent part. */ \ X(INT, NONE, stamp_utmp) \ X(INT, NONE, login_shell) \ @@ -1016,7 +1019,8 @@ struct logblank_t { void log_packet(void *logctx, int direction, int type, char *texttype, const void *data, int len, int n_blanks, const struct logblank_t *blanks, - const unsigned long *sequence); + const unsigned long *sequence, + unsigned downstream_id, const char *additional_log_text); /* * Exports from testback.c diff --git a/settings.c b/settings.c index 2aae3f1d..a82a388b 100644 --- a/settings.c +++ b/settings.c @@ -641,6 +641,9 @@ void save_open_settings(void *sesskey, Conf *conf) write_setting_i(sesskey, "SerialParity", conf_get_int(conf, CONF_serparity)); write_setting_i(sesskey, "SerialFlowControl", conf_get_int(conf, CONF_serflow)); write_setting_s(sesskey, "WindowClass", conf_get_str(conf, CONF_winclass)); + write_setting_i(sesskey, "ConnectionSharing", conf_get_int(conf, CONF_ssh_connection_sharing)); + write_setting_i(sesskey, "ConnectionSharingUpstream", conf_get_int(conf, CONF_ssh_connection_sharing_upstream)); + write_setting_i(sesskey, "ConnectionSharingDownstream", conf_get_int(conf, CONF_ssh_connection_sharing_downstream)); } void load_settings(char *section, Conf *conf) @@ -983,6 +986,9 @@ void load_open_settings(void *sesskey, Conf *conf) gppi(sesskey, "SerialParity", SER_PAR_NONE, conf, CONF_serparity); gppi(sesskey, "SerialFlowControl", SER_FLOW_XONXOFF, conf, CONF_serflow); gpps(sesskey, "WindowClass", "", conf, CONF_winclass); + gppi(sesskey, "ConnectionSharing", 0, conf, CONF_ssh_connection_sharing); + gppi(sesskey, "ConnectionSharingUpstream", 1, conf, CONF_ssh_connection_sharing_upstream); + gppi(sesskey, "ConnectionSharingDownstream", 1, conf, CONF_ssh_connection_sharing_downstream); } void do_defaults(char *session, Conf *conf) diff --git a/ssh.c b/ssh.c index ee7fb44e..25a2e24b 100644 --- a/ssh.c +++ b/ssh.c @@ -329,7 +329,6 @@ enum { #define crWaitUntil(c) do { crReturn(0); } while (!(c)) #define crWaitUntilV(c) do { crReturnV; } while (!(c)) -typedef struct ssh_tag *Ssh; struct Packet; static struct Packet *ssh1_pkt_init(int pkt_type); @@ -439,6 +438,14 @@ enum { /* channel types */ CHAN_AGENT, CHAN_SOCKDATA, CHAN_SOCKDATA_DORMANT, /* one the remote hasn't confirmed */ + /* + * CHAN_SHARING indicates a channel which is tracked here on + * behalf of a connection-sharing downstream. We do almost nothing + * with these channels ourselves: all messages relating to them + * get thrown straight to sshshare.c and passed on almost + * unmodified to downstream. + */ + CHAN_SHARING, /* * CHAN_ZOMBIE is used to indicate a channel for which we've * already destroyed the local data source: for instance, if a @@ -545,10 +552,14 @@ struct ssh_channel { } a; struct ssh_x11_channel { struct X11Connection *xconn; + int initial; } x11; struct ssh_pfd_channel { struct PortForwarding *pf; } pfd; + struct ssh_sharing_channel { + void *ctx; + } sharing; } u; }; @@ -585,6 +596,7 @@ struct ssh_rportfwd { unsigned sport, dport; char *shost, *dhost; char *sportdesc; + void *share_ctx; struct ssh_portfwd *pfrec; }; @@ -651,14 +663,25 @@ struct Packet { * pkt->savedpos stores the offset (again relative to pkt->data) * of the start of the string data field. */ + + /* Extra metadata used in SSH packet logging mode, allowing us to + * log in the packet header line that the packet came from a + * connection-sharing downstream and what if anything unusual was + * done to it. The additional_log_text field is expected to be a + * static string - it will not be freed. */ + unsigned downstream_id; + const char *additional_log_text; }; static void ssh1_protocol(Ssh ssh, void *vin, int inlen, struct Packet *pktin); static void ssh2_protocol(Ssh ssh, void *vin, int inlen, struct Packet *pktin); +static void ssh2_bare_connection_protocol(Ssh ssh, void *vin, int inlen, + struct Packet *pktin); static void ssh1_protocol_setup(Ssh ssh); static void ssh2_protocol_setup(Ssh ssh); +static void ssh2_bare_connection_protocol_setup(Ssh ssh); static void ssh_size(void *handle, int width, int height); static void ssh_special(void *handle, Telnet_Special); static int ssh2_try_send(struct ssh_channel *c); @@ -692,6 +715,14 @@ struct rdpkt2_state_tag { struct Packet *pktin; }; +struct rdpkt2_bare_state_tag { + char length[4]; + long packetlen; + int i; + unsigned long incoming_sequence; + struct Packet *pktin; +}; + struct queued_handler; struct queued_handler { int msg1, msg2; @@ -735,6 +766,10 @@ struct ssh_tag { int v2_session_id_len; void *kex_ctx; + int bare_connection; + int attempting_connshare; + void *connshare; + char *savedhost; int savedport; int send_ok; @@ -798,6 +833,7 @@ struct ssh_tag { int ssh1_rdpkt_crstate; int ssh2_rdpkt_crstate; + int ssh2_bare_rdpkt_crstate; int ssh_gotdata_crstate; int do_ssh1_connection_crstate; @@ -805,9 +841,11 @@ struct ssh_tag { void *do_ssh1_login_state; void *do_ssh2_transport_state; void *do_ssh2_authconn_state; + void *do_ssh_connection_init_state; struct rdpkt1_state_tag rdpkt1_state; struct rdpkt2_state_tag rdpkt2_state; + struct rdpkt2_bare_state_tag rdpkt2_bare_state; /* SSH-1 and SSH-2 use this for different things, but both use it */ int protocol_initial_phase_done; @@ -815,6 +853,7 @@ struct ssh_tag { void (*protocol) (Ssh ssh, void *vin, int inlen, struct Packet *pkt); struct Packet *(*s_rdpkt) (Ssh ssh, unsigned char **data, int *datalen); + int (*do_ssh_init)(Ssh ssh, unsigned char c); /* * We maintain our own copy of a Conf structure here. That way, @@ -1151,7 +1190,8 @@ static void ssh1_log_incoming_packet(Ssh ssh, struct Packet *pkt) } log_packet(ssh->logctx, PKT_INCOMING, pkt->type, ssh1_pkt_type(pkt->type), - pkt->body, pkt->length, nblanks, blanks, NULL); + pkt->body, pkt->length, nblanks, blanks, NULL, + 0, NULL); } static void ssh1_log_outgoing_packet(Ssh ssh, struct Packet *pkt) @@ -1224,7 +1264,7 @@ static void ssh1_log_outgoing_packet(Ssh ssh, struct Packet *pkt) log_packet(ssh->logctx, PKT_OUTGOING, pkt->data[12], ssh1_pkt_type(pkt->data[12]), pkt->body, pkt->length, - nblanks, blanks, NULL); + nblanks, blanks, NULL, 0, NULL); /* * Undo the above adjustment of pkt->length, to put the packet @@ -1373,7 +1413,8 @@ static void ssh2_log_incoming_packet(Ssh ssh, struct Packet *pkt) log_packet(ssh->logctx, PKT_INCOMING, pkt->type, ssh2_pkt_type(ssh->pkt_kctx, ssh->pkt_actx, pkt->type), - pkt->body, pkt->length, nblanks, blanks, &pkt->sequence); + pkt->body, pkt->length, nblanks, blanks, &pkt->sequence, + 0, NULL); } static void ssh2_log_outgoing_packet(Ssh ssh, struct Packet *pkt) @@ -1484,7 +1525,8 @@ static void ssh2_log_outgoing_packet(Ssh ssh, struct Packet *pkt) log_packet(ssh->logctx, PKT_OUTGOING, pkt->data[5], ssh2_pkt_type(ssh->pkt_kctx, ssh->pkt_actx, pkt->data[5]), pkt->body, pkt->length, nblanks, blanks, - &ssh->v2_outgoing_sequence); + &ssh->v2_outgoing_sequence, + pkt->downstream_id, pkt->additional_log_text); /* * Undo the above adjustment of pkt->length, to put the packet @@ -1708,6 +1750,65 @@ static struct Packet *ssh2_rdpkt(Ssh ssh, unsigned char **data, int *datalen) crFinish(st->pktin); } +static struct Packet *ssh2_bare_connection_rdpkt(Ssh ssh, unsigned char **data, + int *datalen) +{ + struct rdpkt2_bare_state_tag *st = &ssh->rdpkt2_bare_state; + + crBegin(ssh->ssh2_bare_rdpkt_crstate); + + /* + * Read the packet length field. + */ + for (st->i = 0; st->i < 4; st->i++) { + while ((*datalen) == 0) + crReturn(NULL); + st->length[st->i] = *(*data)++; + (*datalen)--; + } + + st->packetlen = toint(GET_32BIT_MSB_FIRST(st->length)); + if (st->packetlen <= 0 || st->packetlen >= OUR_V2_PACKETLIMIT) { + bombout(("Invalid packet length received")); + crStop(NULL); + } + + st->pktin = ssh_new_packet(); + st->pktin->data = snewn(st->packetlen, unsigned char); + + st->pktin->encrypted_len = st->packetlen; + + st->pktin->sequence = st->incoming_sequence++; + + /* + * Read the remainder of the packet. + */ + for (st->i = 0; st->i < st->packetlen; st->i++) { + while ((*datalen) == 0) + crReturn(NULL); + st->pktin->data[st->i] = *(*data)++; + (*datalen)--; + } + + /* + * pktin->body and pktin->length should identify the semantic + * content of the packet, excluding the initial type byte. + */ + st->pktin->type = st->pktin->data[0]; + st->pktin->body = st->pktin->data + 1; + st->pktin->length = st->packetlen - 1; + + /* + * Log incoming packet, possibly omitting sensitive fields. + */ + if (ssh->logctx) + ssh2_log_incoming_packet(ssh, st->pktin); + + st->pktin->savedpos = 0; + + crFinish(st->pktin); +} + static int s_wrpkt_prepare(Ssh ssh, struct Packet *pkt, int *offset_p) { int pad, biglen, i, pktoffs; @@ -1763,7 +1864,7 @@ static int s_write(Ssh ssh, void *data, int len) { if (ssh->logctx) log_packet(ssh->logctx, PKT_OUTGOING, -1, NULL, data, len, - 0, NULL, NULL); + 0, NULL, NULL, 0, NULL); if (!ssh->s) return 0; return sk_write(ssh->s, (char *)data, len); @@ -1995,6 +2096,8 @@ static struct Packet *ssh1_pkt_init(int pkt_type) ssh_pkt_addbyte(pkt, pkt_type); pkt->body = pkt->data + pkt->length; pkt->type = pkt_type; + pkt->downstream_id = 0; + pkt->additional_log_text = NULL; return pkt; } @@ -2016,6 +2119,8 @@ static struct Packet *ssh2_pkt_init(int pkt_type) pkt->type = pkt_type; ssh_pkt_addbyte(pkt, (unsigned char) pkt_type); pkt->body = pkt->data + pkt->length; /* after packet type */ + pkt->downstream_id = 0; + pkt->additional_log_text = NULL; return pkt; } @@ -2031,6 +2136,17 @@ static int ssh2_pkt_construct(Ssh ssh, struct Packet *pkt) if (ssh->logctx) ssh2_log_outgoing_packet(ssh, pkt); + if (ssh->bare_connection) { + /* + * Trivial packet construction for the bare connection + * protocol. + */ + PUT_32BIT(pkt->data + 1, pkt->length - 5); + pkt->body = pkt->data + 1; + ssh->v2_outgoing_sequence++; /* only for diagnostics, really */ + return pkt->length - 1; + } + /* * Compress packet payload. */ @@ -2080,6 +2196,7 @@ static int ssh2_pkt_construct(Ssh ssh, struct Packet *pkt) pkt->encrypted_len = pkt->length + padding; /* Ready-to-send packet starts at pkt->data. We return length. */ + pkt->body = pkt->data; return pkt->length + padding + maclen; } @@ -2137,12 +2254,13 @@ static void ssh2_pkt_send_noqueue(Ssh ssh, struct Packet *pkt) return; } len = ssh2_pkt_construct(ssh, pkt); - backlog = s_write(ssh, pkt->data, len); + backlog = s_write(ssh, pkt->body, len); if (backlog > SSH_MAX_BACKLOG) ssh_throttle_all(ssh, 1, backlog); ssh->outgoing_data_size += pkt->encrypted_len; if (!ssh->kex_in_progress && + !ssh->bare_connection && ssh->max_data_size != 0 && ssh->outgoing_data_size > ssh->max_data_size) do_ssh2_transport(ssh, "too much data sent", -1, NULL); @@ -2174,7 +2292,7 @@ static void ssh2_pkt_defer_noqueue(Ssh ssh, struct Packet *pkt, int noignore) ssh->deferred_size, unsigned char); } - memcpy(ssh->deferred_send_data + ssh->deferred_len, pkt->data, len); + memcpy(ssh->deferred_send_data + ssh->deferred_len, pkt->body, len); ssh->deferred_len += len; ssh->deferred_data_size += pkt->encrypted_len; ssh_free_packet(pkt); @@ -2244,6 +2362,7 @@ static void ssh_pkt_defersend(Ssh ssh) ssh->outgoing_data_size += ssh->deferred_data_size; if (!ssh->kex_in_progress && + !ssh->bare_connection && ssh->max_data_size != 0 && ssh->outgoing_data_size > ssh->max_data_size) do_ssh2_transport(ssh, "too much data sent", -1, NULL); @@ -2696,11 +2815,7 @@ static void ssh_detect_bugs(Ssh ssh, char *vstring) */ static void ssh_fix_verstring(char *str) { - /* Eat "SSH--". */ - assert(*str == 'S'); str++; - assert(*str == 'S'); str++; - assert(*str == 'H'); str++; - assert(*str == '-'); str++; + /* Eat "-". */ while (*str && *str != '-') str++; assert(*str == '-'); str++; @@ -2716,7 +2831,7 @@ static void ssh_fix_verstring(char *str) /* * Send an appropriate SSH version string. */ -static void ssh_send_verstring(Ssh ssh, char *svers) +static void ssh_send_verstring(Ssh ssh, const char *protoname, char *svers) { char *verstring; @@ -2724,18 +2839,19 @@ static void ssh_send_verstring(Ssh ssh, char *svers) /* * Construct a v2 version string. */ - verstring = dupprintf("SSH-2.0-%s\015\012", sshver); + verstring = dupprintf("%s2.0-%s\015\012", protoname, sshver); } else { /* * Construct a v1 version string. */ + assert(!strcmp(protoname, "SSH-")); /* no v1 bare connection protocol */ verstring = dupprintf("SSH-%s-%s\012", (ssh_versioncmp(svers, "1.5") <= 0 ? svers : "1.5"), sshver); } - ssh_fix_verstring(verstring); + ssh_fix_verstring(verstring + strlen(protoname)); if (ssh->version == 2) { size_t len; @@ -2756,6 +2872,8 @@ static void ssh_send_verstring(Ssh ssh, char *svers) static int do_ssh_init(Ssh ssh, unsigned char c) { + static const char protoname[] = "SSH-"; + struct do_ssh_init_state { int crLine; int vslen; @@ -2769,15 +2887,13 @@ static int do_ssh_init(Ssh ssh, unsigned char c) crBeginState; - /* Search for a line beginning with the string "SSH-" in the input. */ + /* Search for a line beginning with the protocol name prefix in + * the input. */ for (;;) { - if (c != 'S') goto no; - crReturn(1); - if (c != 'S') goto no; - crReturn(1); - if (c != 'H') goto no; - crReturn(1); - if (c != '-') goto no; + for (s->i = 0; protoname[s->i]; s->i++) { + if ((char)c != protoname[s->i]) goto no; + crReturn(1); + } break; no: while (c != '\012') @@ -2785,13 +2901,12 @@ static int do_ssh_init(Ssh ssh, unsigned char c) crReturn(1); } - s->vstrsize = 16; + s->vstrsize = sizeof(protoname) + 16; s->vstring = snewn(s->vstrsize, char); - strcpy(s->vstring, "SSH-"); - s->vslen = 4; + strcpy(s->vstring, protoname); + s->vslen = strlen(protoname); s->i = 0; while (1) { - crReturn(1); /* get another char */ if (s->vslen >= s->vstrsize - 1) { s->vstrsize += 16; s->vstring = sresize(s->vstring, s->vstrsize, char); @@ -2805,6 +2920,7 @@ static int do_ssh_init(Ssh ssh, unsigned char c) s->version[s->i++] = c; } else if (c == '\012') break; + crReturn(1); /* get another char */ } ssh->agentfwd_enabled = FALSE; @@ -2842,7 +2958,7 @@ static int do_ssh_init(Ssh ssh, unsigned char c) /* Send the version string, if we haven't already */ if (conf_get_int(ssh->conf, CONF_sshprot) != 3) - ssh_send_verstring(ssh, s->version); + ssh_send_verstring(ssh, protoname, s->version); if (ssh->version == 2) { size_t len; @@ -2880,6 +2996,117 @@ static int do_ssh_init(Ssh ssh, unsigned char c) crFinish(0); } +static int do_ssh_connection_init(Ssh ssh, unsigned char c) +{ + /* + * Ordinary SSH begins with the banner "SSH-x.y-...". This is just + * the ssh-connection part, extracted and given a trivial binary + * packet protocol, so we replace 'SSH-' at the start with a new + * name. In proper SSH style (though of course this part of the + * proper SSH protocol _isn't_ subject to this kind of + * DNS-domain-based extension), we define the new name in our + * extension space. + */ + static const char protoname[] = + "SSHCONNECTION@putty.projects.tartarus.org-"; + + struct do_ssh_connection_init_state { + int crLine; + int vslen; + char version[10]; + char *vstring; + int vstrsize; + int i; + }; + crState(do_ssh_connection_init_state); + + crBeginState; + + /* Search for a line beginning with the protocol name prefix in + * the input. */ + for (;;) { + for (s->i = 0; protoname[s->i]; s->i++) { + if ((char)c != protoname[s->i]) goto no; + crReturn(1); + } + break; + no: + while (c != '\012') + crReturn(1); + crReturn(1); + } + + s->vstrsize = sizeof(protoname) + 16; + s->vstring = snewn(s->vstrsize, char); + strcpy(s->vstring, protoname); + s->vslen = strlen(protoname); + s->i = 0; + while (1) { + if (s->vslen >= s->vstrsize - 1) { + s->vstrsize += 16; + s->vstring = sresize(s->vstring, s->vstrsize, char); + } + s->vstring[s->vslen++] = c; + if (s->i >= 0) { + if (c == '-') { + s->version[s->i] = '\0'; + s->i = -1; + } else if (s->i < sizeof(s->version) - 1) + s->version[s->i++] = c; + } else if (c == '\012') + break; + crReturn(1); /* get another char */ + } + + ssh->agentfwd_enabled = FALSE; + ssh->rdpkt2_bare_state.incoming_sequence = 0; + + s->vstring[s->vslen] = 0; + s->vstring[strcspn(s->vstring, "\015\012")] = '\0';/* remove EOL chars */ + logeventf(ssh, "Server version: %s", s->vstring); + ssh_detect_bugs(ssh, s->vstring); + + /* + * Decide which SSH protocol version to support. This is easy in + * bare ssh-connection mode: only 2.0 is legal. + */ + if (ssh_versioncmp(s->version, "2.0") < 0) { + bombout(("Server announces compatibility with SSH-1 in bare ssh-connection protocol")); + crStop(0); + } + if (conf_get_int(ssh->conf, CONF_sshprot) == 0) { + bombout(("Bare ssh-connection protocol cannot be run in SSH-1-only mode")); + crStop(0); + } + + ssh->version = 2; + + logeventf(ssh, "Using bare ssh-connection protocol"); + + /* Send the version string, if we haven't already */ + ssh_send_verstring(ssh, protoname, s->version); + + /* + * Initialise bare connection protocol. + */ + ssh->protocol = ssh2_bare_connection_protocol; + ssh2_bare_connection_protocol_setup(ssh); + ssh->s_rdpkt = ssh2_bare_connection_rdpkt; + + update_specials_menu(ssh->frontend); + ssh->state = SSH_STATE_BEFORE_SIZE; + ssh->pinger = pinger_new(ssh->conf, &ssh_backend, ssh); + + /* + * Get authconn (really just conn) under way. + */ + do_ssh2_authconn(ssh, NULL, 0, NULL); + + sfree(s->vstring); + + crFinish(0); +} + static void ssh_process_incoming_data(Ssh ssh, unsigned char **data, int *datalen) { @@ -2931,7 +3158,7 @@ static void ssh_gotdata(Ssh ssh, unsigned char *data, int datalen) /* Log raw data, if we're in that mode. */ if (ssh->logctx) log_packet(ssh->logctx, PKT_INCOMING, -1, NULL, data, datalen, - 0, NULL, NULL); + 0, NULL, NULL, 0, NULL); crBegin(ssh->ssh_gotdata_crstate); @@ -2945,7 +3172,7 @@ static void ssh_gotdata(Ssh ssh, unsigned char *data, int datalen) int ret; /* need not be kept across crReturn */ if (datalen == 0) crReturnV; /* more data please */ - ret = do_ssh_init(ssh, *data); + ret = ssh->do_ssh_init(ssh, *data); data++; datalen--; if (ret == 0) @@ -3042,21 +3269,71 @@ static int ssh_do_close(Ssh ssh, int notify_exit) return ret; } -static void ssh_log(Plug plug, int type, SockAddr addr, int port, - const char *error_msg, int error_code) +static void ssh_socket_log(Plug plug, int type, SockAddr addr, int port, + const char *error_msg, int error_code) { Ssh ssh = (Ssh) plug; char addrbuf[256], *msg; - sk_getaddr(addr, addrbuf, lenof(addrbuf)); + if (ssh->attempting_connshare) { + /* + * While we're attempting connection sharing, don't loudly log + * everything that happens. Real TCP connections need to be + * logged when we _start_ trying to connect, because it might + * be ages before they respond if something goes wrong; but + * connection sharing is local and quick to respond, and it's + * sufficient to simply wait and see whether it worked + * afterwards. + */ + } else { + sk_getaddr(addr, addrbuf, lenof(addrbuf)); - if (type == 0) - msg = dupprintf("Connecting to %s port %d", addrbuf, port); - else - msg = dupprintf("Failed to connect to %s: %s", addrbuf, error_msg); + if (type == 0) { + if (sk_addr_needs_port(addr)) { + msg = dupprintf("Connecting to %s port %d", addrbuf, port); + } else { + msg = dupprintf("Connecting to %s", addrbuf, port); + } + } else { + msg = dupprintf("Failed to connect to %s: %s", addrbuf, error_msg); + } - logevent(msg); - sfree(msg); + logevent(msg); + sfree(msg); + } +} + +void ssh_connshare_log(Ssh ssh, int event, const char *logtext, + const char *ds_err, const char *us_err) +{ + if (event == SHARE_NONE) { + /* In this case, 'logtext' is an error message indicating a + * reason why connection sharing couldn't be set up _at all_. + * Failing that, ds_err and us_err indicate why we couldn't be + * a downstream and an upstream respectively. */ + if (logtext) { + logeventf(ssh, "Could not set up connection sharing: %s", logtext); + } else { + if (ds_err) + logeventf(ssh, "Could not set up connection sharing" + " as downstream: %s", ds_err); + if (us_err) + logeventf(ssh, "Could not set up connection sharing" + " as upstream: %s", us_err); + } + } else if (event == SHARE_DOWNSTREAM) { + /* In this case, 'logtext' is a local endpoint address */ + logeventf(ssh, "Using existing shared connection at %s", logtext); + /* Also we should mention this in the console window to avoid + * confusing users as to why this window doesn't behave the + * usual way. */ + if ((flags & FLAG_VERBOSE) || (flags & FLAG_INTERACTIVE)) { + c_write_str(ssh,"Reusing a shared connection to this server.\r\n"); + } + } else if (event == SHARE_UPSTREAM) { + /* In this case, 'logtext' is a local endpoint address too */ + logeventf(ssh, "Sharing this connection at %s", logtext); + } } static int ssh_closing(Plug plug, const char *error_msg, int error_code, @@ -3117,7 +3394,7 @@ static const char *connect_to_host(Ssh ssh, char *host, int port, char **realhost, int nodelay, int keepalive) { static const struct plug_function_table fn_table = { - ssh_log, + ssh_socket_log, ssh_closing, ssh_receive, ssh_sent, @@ -3155,30 +3432,58 @@ static const char *connect_to_host(Ssh ssh, char *host, int port, ssh->savedport = port; } - /* - * Try to find host. - */ - addressfamily = conf_get_int(ssh->conf, CONF_addressfamily); - logeventf(ssh, "Looking up host \"%s\"%s", host, - (addressfamily == ADDRTYPE_IPV4 ? " (IPv4)" : - (addressfamily == ADDRTYPE_IPV6 ? " (IPv6)" : ""))); - addr = name_lookup(host, port, realhost, ssh->conf, addressfamily); - if ((err = sk_addr_error(addr)) != NULL) { - sk_addr_free(addr); - return err; - } - ssh->fullhostname = dupstr(*realhost); /* save in case of GSSAPI */ + ssh->fn = &fn_table; /* make 'ssh' usable as a Plug */ /* - * Open socket. + * Try connection-sharing, in case that means we don't open a + * socket after all. ssh_connection_sharing_init will connect to a + * previously established upstream if it can, and failing that, + * establish a listening socket for _us_ to be the upstream. In + * the latter case it will return NULL just as if it had done + * nothing, because here we only need to care if we're a + * downstream and need to do our connection setup differently. */ - ssh->fn = &fn_table; - ssh->s = new_connection(addr, *realhost, port, - 0, 1, nodelay, keepalive, (Plug) ssh, ssh->conf); - if ((err = sk_socket_error(ssh->s)) != NULL) { - ssh->s = NULL; - notify_remote_exit(ssh->frontend); - return err; + ssh->connshare = NULL; + ssh->attempting_connshare = TRUE; /* affects socket logging behaviour */ + ssh->s = ssh_connection_sharing_init(ssh->savedhost, ssh->savedport, + ssh->conf, ssh, &ssh->connshare); + ssh->attempting_connshare = FALSE; + if (ssh->s != NULL) { + /* + * We are a downstream. + */ + ssh->bare_connection = TRUE; + ssh->do_ssh_init = do_ssh_connection_init; + ssh->fullhostname = NULL; + *realhost = dupstr(host); /* best we can do */ + } else { + /* + * We're not a downstream, so open a normal socket. + */ + ssh->do_ssh_init = do_ssh_init; + + /* + * Try to find host. + */ + addressfamily = conf_get_int(ssh->conf, CONF_addressfamily); + logeventf(ssh, "Looking up host \"%s\"%s", host, + (addressfamily == ADDRTYPE_IPV4 ? " (IPv4)" : + (addressfamily == ADDRTYPE_IPV6 ? " (IPv6)" : ""))); + addr = name_lookup(host, port, realhost, ssh->conf, addressfamily); + if ((err = sk_addr_error(addr)) != NULL) { + sk_addr_free(addr); + return err; + } + ssh->fullhostname = dupstr(*realhost); /* save in case of GSSAPI */ + + ssh->s = new_connection(addr, *realhost, port, + 0, 1, nodelay, keepalive, + (Plug) ssh, ssh->conf); + if ((err = sk_socket_error(ssh->s)) != NULL) { + ssh->s = NULL; + notify_remote_exit(ssh->frontend); + return err; + } } /* @@ -3188,9 +3493,9 @@ static const char *connect_to_host(Ssh ssh, char *host, int port, sshprot = conf_get_int(ssh->conf, CONF_sshprot); if (sshprot == 0) ssh->version = 1; - if (sshprot == 3) { + if (sshprot == 3 && !ssh->bare_connection) { ssh->version = 2; - ssh_send_verstring(ssh, NULL); + ssh_send_verstring(ssh, "SSH-", NULL); } /* @@ -4533,6 +4838,41 @@ static void ssh_rportfwd_succfail(Ssh ssh, struct Packet *pktin, void *ctx) } } +int ssh_alloc_sharing_rportfwd(Ssh ssh, const char *shost, int sport, + void *share_ctx) +{ + struct ssh_rportfwd *pf = snew(struct ssh_rportfwd); + pf->dhost = NULL; + pf->dport = 0; + pf->share_ctx = share_ctx; + pf->shost = dupstr(shost); + pf->sport = sport; + pf->sportdesc = NULL; + if (!ssh->rportfwds) { + assert(ssh->version == 2); + ssh->rportfwds = newtree234(ssh_rportcmp_ssh2); + } + if (add234(ssh->rportfwds, pf) != pf) { + sfree(pf->shost); + sfree(pf); + return FALSE; + } + return TRUE; +} + +static void ssh_sharing_global_request_response(Ssh ssh, struct Packet *pktin, + void *ctx) +{ + share_got_pkt_from_server(ctx, pktin->type, + pktin->body, pktin->length); +} + +void ssh_sharing_queue_global_request(Ssh ssh, void *share_ctx) +{ + ssh_queue_handler(ssh, SSH2_MSG_REQUEST_SUCCESS, SSH2_MSG_REQUEST_FAILURE, + ssh_sharing_global_request_response, share_ctx); +} + static void ssh_setup_portfwd(Ssh ssh, Conf *conf) { struct ssh_portfwd *epf; @@ -4802,6 +5142,7 @@ static void ssh_setup_portfwd(Ssh ssh, Conf *conf) } pf = snew(struct ssh_rportfwd); + pf->share_ctx = NULL; pf->dhost = dupstr(epf->daddr); pf->dport = epf->dport; if (epf->saddr) { @@ -5210,6 +5551,10 @@ static void ssh1_send_ttymode(void *data, char *mode, char *val) ssh2_pkt_addbyte(pktout, arg); } +int ssh_agent_forwarding_permitted(Ssh ssh) +{ + return conf_get_int(ssh->conf, CONF_agentfwd) && agent_exists(); +} static void do_ssh1_connection(Ssh ssh, unsigned char *in, int inlen, struct Packet *pktin) @@ -5230,7 +5575,7 @@ static void do_ssh1_connection(Ssh ssh, unsigned char *in, int inlen, ssh->packet_dispatch[SSH1_MSG_CHANNEL_DATA] = ssh1_msg_channel_data; ssh->packet_dispatch[SSH1_SMSG_EXIT_STATUS] = ssh1_smsg_exit_status; - if (conf_get_int(ssh->conf, CONF_agentfwd) && agent_exists()) { + if (ssh_agent_forwarding_permitted(ssh)) { logevent("Requesting agent forwarding"); send_packet(ssh, SSH1_CMSG_AGENT_REQUEST_FORWARDING, PKT_END); do { @@ -5611,6 +5956,8 @@ static void do_ssh2_transport(Ssh ssh, void *vin, int inlen, }; crState(do_ssh2_transport_state); + assert(!ssh->bare_connection); + crBeginState; s->cscipher_tobe = s->sccipher_tobe = NULL; @@ -6688,6 +7035,19 @@ static void ssh2_try_send_and_unthrottle(Ssh ssh, struct ssh_channel *c) } } +static int ssh_is_simple(Ssh ssh) +{ + /* + * We use the 'simple' variant of the SSH protocol if we're asked + * to, except not if we're also doing connection-sharing (either + * tunnelling our packets over an upstream or expecting to be + * tunnelled over ourselves), since then the assumption that we + * have only one channel to worry about is not true after all. + */ + return (conf_get_int(ssh->conf, CONF_ssh_simple) && + !ssh->bare_connection && !ssh->connshare); +} + /* * Set up most of a new ssh_channel for SSH-2. */ @@ -6699,7 +7059,7 @@ static void ssh2_channel_init(struct ssh_channel *c) c->pending_eof = FALSE; c->throttling_conn = FALSE; c->v.v2.locwindow = c->v.v2.locmaxwin = c->v.v2.remlocwin = - conf_get_int(ssh->conf, CONF_ssh_simple) ? OUR_V2_BIGWIN : OUR_V2_WINSIZE; + ssh_is_simple(ssh) ? OUR_V2_BIGWIN : OUR_V2_WINSIZE; c->v.v2.chanreq_head = NULL; c->v.v2.throttle_state = UNTHROTTLED; bufchain_init(&c->v.v2.outbuffer); @@ -6787,6 +7147,14 @@ static void ssh2_set_window(struct ssh_channel *c, int newwin) if (c->closes & (CLOSES_RCVD_EOF | CLOSES_SENT_CLOSE)) return; + /* + * Also, never widen the window for an X11 channel when we're + * still waiting to see its initial auth and may yet hand it off + * to a downstream. + */ + if (c->type == CHAN_X11 && c->u.x11.initial) + return; + /* * If the remote end has a habit of ignoring maxpkt, limit the * window so that it has no choice (assuming it doesn't ignore the @@ -6850,7 +7218,8 @@ static struct ssh_channel *ssh2_channel_msg(Ssh ssh, struct Packet *pktin) c = find234(ssh->channels, &localid, ssh_channelfind); if (!c || - (c->halfopen && pktin->type != SSH2_MSG_CHANNEL_OPEN_CONFIRMATION && + (c->type != CHAN_SHARING && c->halfopen && + pktin->type != SSH2_MSG_CHANNEL_OPEN_CONFIRMATION && pktin->type != SSH2_MSG_CHANNEL_OPEN_FAILURE)) { char *buf = dupprintf("Received %s for %s channel %u", ssh2_pkt_type(ssh->pkt_kctx, ssh->pkt_actx, @@ -6893,6 +7262,11 @@ static void ssh2_msg_channel_response(Ssh ssh, struct Packet *pktin) struct outstanding_channel_request *ocr; if (!c) return; + if (c->type == CHAN_SHARING) { + share_got_pkt_from_server(c->u.sharing.ctx, pktin->type, + pktin->body, pktin->length); + return; + } ocr = c->v.v2.chanreq_head; if (!ocr) { ssh2_msg_unexpected(ssh, pktin); @@ -6915,6 +7289,11 @@ static void ssh2_msg_channel_window_adjust(Ssh ssh, struct Packet *pktin) c = ssh2_channel_msg(ssh, pktin); if (!c) return; + if (c->type == CHAN_SHARING) { + share_got_pkt_from_server(c->u.sharing.ctx, pktin->type, + pktin->body, pktin->length); + return; + } if (!(c->closes & CLOSES_SENT_EOF)) { c->v.v2.remwindow += ssh_pkt_getuint32(pktin); ssh2_try_send_and_unthrottle(ssh, c); @@ -6929,6 +7308,11 @@ static void ssh2_msg_channel_data(Ssh ssh, struct Packet *pktin) c = ssh2_channel_msg(ssh, pktin); if (!c) return; + if (c->type == CHAN_SHARING) { + share_got_pkt_from_server(c->u.sharing.ctx, pktin->type, + pktin->body, pktin->length); + return; + } if (pktin->type == SSH2_MSG_CHANNEL_EXTENDED_DATA && ssh_pkt_getuint32(pktin) != SSH2_EXTENDED_DATA_STDERR) return; /* extended but not stderr */ @@ -7017,15 +7401,59 @@ static void ssh2_msg_channel_data(Ssh ssh, struct Packet *pktin) * buffering anything at all and we're in "simple" mode, * throttle the whole channel. */ - if ((bufsize > c->v.v2.locmaxwin || - (conf_get_int(ssh->conf, CONF_ssh_simple) && bufsize > 0)) && - !c->throttling_conn) { + if ((bufsize > c->v.v2.locmaxwin || (ssh_is_simple(ssh) && bufsize>0)) + && !c->throttling_conn) { c->throttling_conn = 1; ssh_throttle_conn(ssh, +1); } } } +static void ssh_check_termination(Ssh ssh) +{ + if (ssh->version == 2 && + !conf_get_int(ssh->conf, CONF_ssh_no_shell) && + count234(ssh->channels) == 0 && + !(ssh->connshare && share_ndownstreams(ssh->connshare) > 0)) { + /* + * We used to send SSH_MSG_DISCONNECT here, because I'd + * believed that _every_ conforming SSH-2 connection had to + * end with a disconnect being sent by at least one side; + * apparently I was wrong and it's perfectly OK to + * unceremoniously slam the connection shut when you're done, + * and indeed OpenSSH feels this is more polite than sending a + * DISCONNECT. So now we don't. + */ + ssh_disconnect(ssh, "All channels closed", NULL, 0, TRUE); + } +} + +void ssh_sharing_downstream_connected(Ssh ssh, unsigned id) +{ + logeventf(ssh, "Connection sharing downstream #%u connected", id); +} + +void ssh_sharing_downstream_disconnected(Ssh ssh, unsigned id) +{ + logeventf(ssh, "Connection sharing downstream #%u disconnected", id); + ssh_check_termination(ssh); +} + +void ssh_sharing_logf(Ssh ssh, unsigned id, const char *logfmt, ...) +{ + va_list ap; + char *buf; + + va_start(ap, logfmt); + buf = dupvprintf(logfmt, ap); + va_end(ap); + if (id) + logeventf(ssh, "Connection sharing downstream #%u: %s", id, buf); + else + logeventf(ssh, "Connection sharing: %s", buf); + sfree(buf); +} + static void ssh_channel_destroy(struct ssh_channel *c) { Ssh ssh = c->ssh; @@ -7058,26 +7486,10 @@ static void ssh_channel_destroy(struct ssh_channel *c) sfree(c); /* - * See if that was the last channel left open. - * (This is only our termination condition if we're - * not running in -N mode.) + * If that was the last channel left open, we might need to + * terminate. */ - if (ssh->version == 2 && - !conf_get_int(ssh->conf, CONF_ssh_no_shell) && - count234(ssh->channels) == 0) { - /* - * We used to send SSH_MSG_DISCONNECT here, - * because I'd believed that _every_ conforming - * SSH-2 connection had to end with a disconnect - * being sent by at least one side; apparently - * I was wrong and it's perfectly OK to - * unceremoniously slam the connection shut - * when you're done, and indeed OpenSSH feels - * this is more polite than sending a - * DISCONNECT. So now we don't. - */ - ssh_disconnect(ssh, "All channels closed", NULL, 0, TRUE); - } + ssh_check_termination(ssh); } static void ssh2_channel_check_close(struct ssh_channel *c) @@ -7163,6 +7575,11 @@ static void ssh2_msg_channel_eof(Ssh ssh, struct Packet *pktin) c = ssh2_channel_msg(ssh, pktin); if (!c) return; + if (c->type == CHAN_SHARING) { + share_got_pkt_from_server(c->u.sharing.ctx, pktin->type, + pktin->body, pktin->length); + return; + } ssh2_channel_got_eof(c); } @@ -7173,6 +7590,11 @@ static void ssh2_msg_channel_close(Ssh ssh, struct Packet *pktin) c = ssh2_channel_msg(ssh, pktin); if (!c) return; + if (c->type == CHAN_SHARING) { + share_got_pkt_from_server(c->u.sharing.ctx, pktin->type, + pktin->body, pktin->length); + return; + } /* * When we receive CLOSE on a channel, we assume it comes with an @@ -7235,6 +7657,11 @@ static void ssh2_msg_channel_open_confirmation(Ssh ssh, struct Packet *pktin) c = ssh2_channel_msg(ssh, pktin); if (!c) return; + if (c->type == CHAN_SHARING) { + share_got_pkt_from_server(c->u.sharing.ctx, pktin->type, + pktin->body, pktin->length); + return; + } assert(c->halfopen); /* ssh2_channel_msg will have enforced this */ c->remoteid = ssh_pkt_getuint32(pktin); c->halfopen = FALSE; @@ -7290,6 +7717,11 @@ static void ssh2_msg_channel_open_failure(Ssh ssh, struct Packet *pktin) c = ssh2_channel_msg(ssh, pktin); if (!c) return; + if (c->type == CHAN_SHARING) { + share_got_pkt_from_server(c->u.sharing.ctx, pktin->type, + pktin->body, pktin->length); + return; + } assert(c->halfopen); /* ssh2_channel_msg will have enforced this */ if (c->type == CHAN_SOCKDATA_DORMANT) { @@ -7337,6 +7769,11 @@ static void ssh2_msg_channel_request(Ssh ssh, struct Packet *pktin) c = ssh2_channel_msg(ssh, pktin); if (!c) return; + if (c->type == CHAN_SHARING) { + share_got_pkt_from_server(c->u.sharing.ctx, pktin->type, + pktin->body, pktin->length); + return; + } ssh_pkt_getstring(pktin, &type, &typelen); want_reply = ssh2_pkt_getbool(pktin); @@ -7529,6 +7966,30 @@ static void ssh2_msg_global_request(Ssh ssh, struct Packet *pktin) } } +struct X11FakeAuth *ssh_sharing_add_x11_display(Ssh ssh, int authtype, + void *share_cs, + void *share_chan) +{ + struct X11FakeAuth *auth; + + /* + * Make up a new set of fake X11 auth data, and add it to the tree + * of currently valid ones with an indication of the sharing + * context that it's relevant to. + */ + auth = x11_invent_fake_auth(ssh->x11authtree, authtype); + auth->share_cs = share_cs; + auth->share_chan = share_chan; + + return auth; +} + +void ssh_sharing_remove_x11_display(Ssh ssh, struct X11FakeAuth *auth) +{ + del234(ssh->x11authtree, auth); + x11_free_fake_auth(auth); +} + static void ssh2_msg_channel_open(Ssh ssh, struct Packet *pktin) { char *type; @@ -7539,6 +8000,7 @@ static void ssh2_msg_channel_open(Ssh ssh, struct Packet *pktin) char *error = NULL; struct ssh_channel *c; unsigned remid, winsize, pktsize; + unsigned our_winsize_override = 0; struct Packet *pktout; ssh_pkt_getstring(pktin, &type, &typelen); @@ -7561,12 +8023,24 @@ static void ssh2_msg_channel_open(Ssh ssh, struct Packet *pktin) logeventf(ssh, "Received X11 connect request from %s:%d", addrstr, peerport); - if (!ssh->X11_fwd_enabled) + if (!ssh->X11_fwd_enabled && !ssh->connshare) error = "X11 forwarding is not enabled"; else { c->u.x11.xconn = x11_init(ssh->x11authtree, c, addrstr, peerport); c->type = CHAN_X11; + c->u.x11.initial = TRUE; + + /* + * If we are a connection-sharing upstream, then we should + * initially present a very small window, adequate to take + * the X11 initial authorisation packet but not much more. + * Downstream will then present us a larger window (by + * fiat of the connection-sharing protocol) and we can + * guarantee to send a positive-valued WINDOW_ADJUST. + */ + if (ssh->connshare) + our_winsize_override = 128; logevent("Opened X11 forward channel"); } @@ -7590,11 +8064,23 @@ static void ssh2_msg_channel_open(Ssh ssh, struct Packet *pktin) if (realpf == NULL) { error = "Remote port is not recognised"; } else { - char *err = pfd_connect(&c->u.pfd.pf, - realpf->dhost, - realpf->dport, c, - ssh->conf, - realpf->pfrec->addressfamily); + char *err; + + if (realpf->share_ctx) { + /* + * This port forwarding is on behalf of a + * connection-sharing downstream, so abandon our own + * channel-open procedure and just pass the message on + * to sshshare.c. + */ + share_got_pkt_from_server(realpf->share_ctx, pktin->type, + pktin->body, pktin->length); + sfree(c); + return; + } + + err = pfd_connect(&c->u.pfd.pf, realpf->dhost, realpf->dport, + c, ssh->conf, realpf->pfrec->addressfamily); logeventf(ssh, "Attempting to forward remote port to " "%s:%d", realpf->dhost, realpf->dport); if (err != NULL) { @@ -7635,6 +8121,10 @@ static void ssh2_msg_channel_open(Ssh ssh, struct Packet *pktin) ssh2_channel_init(c); c->v.v2.remwindow = winsize; c->v.v2.remmaxpkt = pktsize; + if (our_winsize_override) { + c->v.v2.locwindow = c->v.v2.locmaxwin = c->v.v2.remlocwin = + our_winsize_override; + } add234(ssh->channels, c); pktout = ssh2_pkt_init(SSH2_MSG_CHANNEL_OPEN_CONFIRMATION); ssh2_pkt_adduint32(pktout, c->remoteid); @@ -7645,6 +8135,43 @@ static void ssh2_msg_channel_open(Ssh ssh, struct Packet *pktin) } } +void sshfwd_x11_sharing_handover(struct ssh_channel *c, + void *share_cs, void *share_chan, + const char *peer_addr, int peer_port, + int endian, int protomajor, int protominor, + const void *initial_data, int initial_len) +{ + /* + * This function is called when we've just discovered that an X + * forwarding channel on which we'd been handling the initial auth + * ourselves turns out to be destined for a connection-sharing + * downstream. So we turn the channel into a CHAN_SHARING, meaning + * that we completely stop tracking windows and buffering data and + * just pass more or less unmodified SSH messages back and forth. + */ + c->type = CHAN_SHARING; + c->u.sharing.ctx = share_cs; + share_setup_x11_channel(share_cs, share_chan, + c->localid, c->remoteid, c->v.v2.remwindow, + c->v.v2.remmaxpkt, c->v.v2.locwindow, + peer_addr, peer_port, endian, + protomajor, protominor, + initial_data, initial_len); +} + +void sshfwd_x11_is_local(struct ssh_channel *c) +{ + /* + * This function is called when we've just discovered that an X + * forwarding channel is _not_ destined for a connection-sharing + * downstream but we're going to handle it ourselves. We stop + * presenting a cautiously small window and go into ordinary data + * exchange mode. + */ + c->u.x11.initial = FALSE; + ssh2_set_window(c, ssh_is_simple(c->ssh) ? OUR_V2_BIGWIN : OUR_V2_WINSIZE); +} + /* * Buffer banner messages for later display at some convenient point, * if we're going to display them. @@ -7964,35 +8491,40 @@ static void do_ssh2_authconn(Ssh ssh, unsigned char *in, int inlen, s->done_service_req = FALSE; s->we_are_in = s->userauth_success = FALSE; + s->agent_response = NULL; #ifndef NO_GSSAPI s->tried_gssapi = FALSE; #endif - if (!conf_get_int(ssh->conf, CONF_ssh_no_userauth)) { - /* - * Request userauth protocol, and await a response to it. - */ - s->pktout = ssh2_pkt_init(SSH2_MSG_SERVICE_REQUEST); - ssh2_pkt_addstring(s->pktout, "ssh-userauth"); - ssh2_pkt_send(ssh, s->pktout); - crWaitUntilV(pktin); - if (pktin->type == SSH2_MSG_SERVICE_ACCEPT) - s->done_service_req = TRUE; - } - if (!s->done_service_req) { - /* - * Request connection protocol directly, without authentication. - */ - s->pktout = ssh2_pkt_init(SSH2_MSG_SERVICE_REQUEST); - ssh2_pkt_addstring(s->pktout, "ssh-connection"); - ssh2_pkt_send(ssh, s->pktout); - crWaitUntilV(pktin); - if (pktin->type == SSH2_MSG_SERVICE_ACCEPT) { - s->we_are_in = TRUE; /* no auth required */ - } else { - bombout(("Server refused service request")); - crStopV; - } + if (!ssh->bare_connection) { + if (!conf_get_int(ssh->conf, CONF_ssh_no_userauth)) { + /* + * Request userauth protocol, and await a response to it. + */ + s->pktout = ssh2_pkt_init(SSH2_MSG_SERVICE_REQUEST); + ssh2_pkt_addstring(s->pktout, "ssh-userauth"); + ssh2_pkt_send(ssh, s->pktout); + crWaitUntilV(pktin); + if (pktin->type == SSH2_MSG_SERVICE_ACCEPT) + s->done_service_req = TRUE; + } + if (!s->done_service_req) { + /* + * Request connection protocol directly, without authentication. + */ + s->pktout = ssh2_pkt_init(SSH2_MSG_SERVICE_REQUEST); + ssh2_pkt_addstring(s->pktout, "ssh-connection"); + ssh2_pkt_send(ssh, s->pktout); + crWaitUntilV(pktin); + if (pktin->type == SSH2_MSG_SERVICE_ACCEPT) { + s->we_are_in = TRUE; /* no auth required */ + } else { + bombout(("Server refused service request")); + crStopV; + } + } + } else { + s->we_are_in = TRUE; } /* Arrange to be able to deal with any BANNERs that come in. @@ -9325,7 +9857,7 @@ static void do_ssh2_authconn(Ssh ssh, unsigned char *in, int inlen, if (s->agent_response) sfree(s->agent_response); - if (s->userauth_success) { + if (s->userauth_success && !ssh->bare_connection) { /* * We've just received USERAUTH_SUCCESS, and we haven't sent any * packets since. Signal the transport layer to consider enacting @@ -9339,10 +9871,6 @@ static void do_ssh2_authconn(Ssh ssh, unsigned char *in, int inlen, do_ssh2_transport(ssh, "enabling delayed compression", -2, NULL); } - /* - * Now the connection protocol has started, one way or another. - */ - ssh->channels = newtree234(ssh_channelcmp); /* @@ -9420,8 +9948,15 @@ static void do_ssh2_authconn(Ssh ssh, unsigned char *in, int inlen, ssh->packet_dispatch[SSH2_MSG_CHANNEL_SUCCESS] = ssh2_msg_channel_response; ssh->packet_dispatch[SSH2_MSG_CHANNEL_FAILURE] = ssh2_msg_channel_response; + /* + * Now the connection protocol is properly up and running, with + * all those dispatch table entries, so it's safe to let + * downstreams start trying to open extra channels through us. + */ + if (ssh->connshare) + share_activate(ssh->connshare, ssh->v_s); - if (ssh->mainchan && conf_get_int(ssh->conf, CONF_ssh_simple)) { + if (ssh->mainchan && ssh_is_simple(ssh)) { /* * This message indicates to the server that we promise * not to try to run any other channel in parallel with @@ -9465,7 +10000,7 @@ static void do_ssh2_authconn(Ssh ssh, unsigned char *in, int inlen, } /* Potentially enable agent forwarding. */ - if (conf_get_int(ssh->conf, CONF_agentfwd) && agent_exists()) + if (ssh_agent_forwarding_permitted(ssh)) ssh2_setup_agent(ssh->mainchan, NULL, NULL); /* Now allocate a pty for the session. */ @@ -9721,6 +10256,48 @@ static void ssh2_protocol_setup(Ssh ssh) ssh->packet_dispatch[SSH2_MSG_DEBUG] = ssh2_msg_debug; } +static void ssh2_bare_connection_protocol_setup(Ssh ssh) +{ + int i; + + /* + * Most messages cause SSH2_MSG_UNIMPLEMENTED. + */ + for (i = 0; i < 256; i++) + ssh->packet_dispatch[i] = ssh2_msg_something_unimplemented; + + /* + * Initially, we set all ssh-connection messages to 'unexpected'; + * do_ssh2_authconn will fill things in properly. We also handle a + * couple of messages from the transport protocol which aren't + * related to key exchange (UNIMPLEMENTED, IGNORE, DEBUG, + * DISCONNECT). + */ + ssh->packet_dispatch[SSH2_MSG_GLOBAL_REQUEST] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_REQUEST_SUCCESS] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_REQUEST_FAILURE] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_OPEN] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_OPEN_CONFIRMATION] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_OPEN_FAILURE] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_WINDOW_ADJUST] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_DATA] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_EXTENDED_DATA] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_EOF] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_CLOSE] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_REQUEST] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_SUCCESS] = ssh2_msg_unexpected; + ssh->packet_dispatch[SSH2_MSG_CHANNEL_FAILURE] = ssh2_msg_unexpected; + + ssh->packet_dispatch[SSH2_MSG_UNIMPLEMENTED] = ssh2_msg_unexpected; + + /* + * These messages have a special handler from the start. + */ + ssh->packet_dispatch[SSH2_MSG_DISCONNECT] = ssh2_msg_disconnect; + ssh->packet_dispatch[SSH2_MSG_IGNORE] = ssh_msg_ignore; + ssh->packet_dispatch[SSH2_MSG_DEBUG] = ssh2_msg_debug; +} + static void ssh2_timer(void *ctx, unsigned long now) { Ssh ssh = (Ssh)ctx; @@ -9728,7 +10305,8 @@ static void ssh2_timer(void *ctx, unsigned long now) if (ssh->state == SSH_STATE_CLOSED) return; - if (!ssh->kex_in_progress && conf_get_int(ssh->conf, CONF_ssh_rekey_time) != 0 && + if (!ssh->kex_in_progress && !ssh->bare_connection && + conf_get_int(ssh->conf, CONF_ssh_rekey_time) != 0 && now == ssh->next_rekey) { do_ssh2_transport(ssh, "timeout", -1, NULL); } @@ -9757,6 +10335,19 @@ static void ssh2_protocol(Ssh ssh, void *vin, int inlen, do_ssh2_authconn(ssh, in, inlen, pktin); } +static void ssh2_bare_connection_protocol(Ssh ssh, void *vin, int inlen, + struct Packet *pktin) +{ + unsigned char *in = (unsigned char *)vin; + if (ssh->state == SSH_STATE_CLOSED) + return; + + if (pktin) + ssh->packet_dispatch[pktin->type](ssh, pktin); + else + do_ssh2_authconn(ssh, in, inlen, pktin); +} + static void ssh_cache_conf_values(Ssh ssh) { ssh->logomitdata = conf_get_int(ssh->conf, CONF_logomitdata); @@ -9819,9 +10410,11 @@ static const char *ssh_init(void *frontend_handle, void **backend_handle, ssh->v2_outgoing_sequence = 0; ssh->ssh1_rdpkt_crstate = 0; ssh->ssh2_rdpkt_crstate = 0; + ssh->ssh2_bare_rdpkt_crstate = 0; ssh->ssh_gotdata_crstate = 0; ssh->do_ssh1_connection_crstate = 0; ssh->do_ssh_init_state = NULL; + ssh->do_ssh_connection_init_state = NULL; ssh->do_ssh1_login_state = NULL; ssh->do_ssh2_transport_state = NULL; ssh->do_ssh2_authconn_state = NULL; @@ -9840,6 +10433,8 @@ static const char *ssh_init(void *frontend_handle, void **backend_handle, ssh->username = NULL; ssh->sent_console_eof = FALSE; ssh->got_pty = FALSE; + ssh->bare_connection = FALSE; + ssh->attempting_connshare = FALSE; *backend_handle = ssh; @@ -9962,6 +10557,9 @@ static void ssh_free(void *handle) ssh->channels = NULL; } + if (ssh->connshare) + sharestate_free(ssh->connshare); + if (ssh->rportfwds) { while ((pf = delpos234(ssh->rportfwds, 0)) != NULL) free_rportfwd(pf); @@ -10062,7 +10660,7 @@ static void ssh_reconfig(void *handle, Conf *conf) ssh->conf = conf_copy(conf); ssh_cache_conf_values(ssh); - if (rekeying) { + if (!ssh->bare_connection && rekeying) { if (!ssh->kex_in_progress) { do_ssh2_transport(ssh, rekeying, -1, NULL); } else if (rekey_mandatory) { @@ -10217,7 +10815,7 @@ static const struct telnet_special *ssh_get_specials(void *handle) } else if (ssh->version == 2) { if (!(ssh->remote_bugs & BUG_CHOKES_ON_SSH2_IGNORE)) ADD_SPECIALS(ssh2_ignore_special); - if (!(ssh->remote_bugs & BUG_SSH2_REKEY)) + if (!(ssh->remote_bugs & BUG_SSH2_REKEY) && !ssh->bare_connection) ADD_SPECIALS(ssh2_rekey_special); if (ssh->mainchan) ADD_SPECIALS(ssh2_session_specials); @@ -10273,7 +10871,8 @@ static void ssh_special(void *handle, Telnet_Special code) } } } else if (code == TS_REKEY) { - if (!ssh->kex_in_progress && ssh->version == 2) { + if (!ssh->kex_in_progress && !ssh->bare_connection && + ssh->version == 2) { do_ssh2_transport(ssh, "at user request", -1, NULL); } } else if (code == TS_BRK) { @@ -10333,6 +10932,41 @@ void *new_sock_channel(void *handle, struct PortForwarding *pf) return c; } +unsigned ssh_alloc_sharing_channel(Ssh ssh, void *sharing_ctx) +{ + struct ssh_channel *c; + c = snew(struct ssh_channel); + + c->ssh = ssh; + ssh2_channel_init(c); + c->type = CHAN_SHARING; + c->u.sharing.ctx = sharing_ctx; + add234(ssh->channels, c); + return c->localid; +} + +void ssh_delete_sharing_channel(Ssh ssh, unsigned localid) +{ + struct ssh_channel *c; + + c = find234(ssh->channels, &localid, ssh_channelfind); + if (c) + ssh_channel_destroy(c); +} + +void ssh_send_packet_from_downstream(Ssh ssh, unsigned id, int type, + const void *data, int datalen, + const char *additional_log_text) +{ + struct Packet *pkt; + + pkt = ssh2_pkt_init(type); + pkt->downstream_id = id; + pkt->additional_log_text = additional_log_text; + ssh2_pkt_adddata(pkt, data, datalen); + ssh2_pkt_send(ssh, pkt); +} + /* * This is called when stdout/stderr (the entity to which * from_backend sends data) manages to clear some backlog. @@ -10352,7 +10986,7 @@ static void ssh_unthrottle(void *handle, int bufsize) ssh2_set_window(ssh->mainchan, bufsize < ssh->mainchan->v.v2.locmaxwin ? ssh->mainchan->v.v2.locmaxwin - bufsize : 0); - if (conf_get_int(ssh->conf, CONF_ssh_simple)) + if (ssh_is_simple(ssh)) buflimit = 0; else buflimit = ssh->mainchan->v.v2.locmaxwin; diff --git a/ssh.h b/ssh.h index 675a509e..89f772d6 100644 --- a/ssh.h +++ b/ssh.h @@ -8,12 +8,53 @@ #include "misc.h" struct ssh_channel; +typedef struct ssh_tag *Ssh; extern int sshfwd_write(struct ssh_channel *c, char *, int); extern void sshfwd_write_eof(struct ssh_channel *c); extern void sshfwd_unclean_close(struct ssh_channel *c, const char *err); extern void sshfwd_unthrottle(struct ssh_channel *c, int bufsize); Conf *sshfwd_get_conf(struct ssh_channel *c); +void sshfwd_x11_sharing_handover(struct ssh_channel *c, + void *share_cs, void *share_chan, + const char *peer_addr, int peer_port, + int endian, int protomajor, int protominor, + const void *initial_data, int initial_len); +void sshfwd_x11_is_local(struct ssh_channel *c); + +extern Socket ssh_connection_sharing_init(const char *host, int port, + Conf *conf, Ssh ssh, void **state); +void share_got_pkt_from_server(void *ctx, int type, + unsigned char *pkt, int pktlen); +void share_activate(void *state, const char *server_verstring); +void sharestate_free(void *state); +int share_ndownstreams(void *state); + +void ssh_connshare_log(Ssh ssh, int event, const char *logtext, + const char *ds_err, const char *us_err); +unsigned ssh_alloc_sharing_channel(Ssh ssh, void *sharing_ctx); +void ssh_delete_sharing_channel(Ssh ssh, unsigned localid); +int ssh_alloc_sharing_rportfwd(Ssh ssh, const char *shost, int sport, + void *share_ctx); +void ssh_sharing_queue_global_request(Ssh ssh, void *share_ctx); +struct X11FakeAuth *ssh_sharing_add_x11_display(Ssh ssh, int authtype, + void *share_cs, + void *share_chan); +void ssh_sharing_remove_x11_display(Ssh ssh, struct X11FakeAuth *auth); +void ssh_send_packet_from_downstream(Ssh ssh, unsigned id, int type, + const void *pkt, int pktlen, + const char *additional_log_text); +void ssh_sharing_downstream_connected(Ssh ssh, unsigned id); +void ssh_sharing_downstream_disconnected(Ssh ssh, unsigned id); +void ssh_sharing_logf(Ssh ssh, unsigned id, const char *logfmt, ...); +int ssh_agent_forwarding_permitted(Ssh ssh); +void share_setup_x11_channel(void *csv, void *chanv, + unsigned upstream_id, unsigned server_id, + unsigned server_currwin, unsigned server_maxpkt, + unsigned client_adjusted_window, + const char *peer_addr, int peer_port, int endian, + int protomajor, int protominor, + const void *initial_data, int initial_len); /* * Useful thing. @@ -400,6 +441,7 @@ struct X11FakeAuth { * What to do with an X connection matching this auth data. */ struct X11Display *disp; + void *share_cs, *share_chan; }; void *x11_make_greeting(int endian, int protomajor, int protominor, int auth_proto, const void *auth_data, int auth_len, @@ -450,6 +492,8 @@ char *platform_get_x_display(void); */ void x11_get_auth_from_authfile(struct X11Display *display, const char *authfilename); +int x11_identify_auth_proto(const char *proto); +void *x11_dehexify(const char *hex, int *outlen); Bignum copybn(Bignum b); Bignum bn_power_2(int n); @@ -590,6 +634,22 @@ int zlib_compress_block(void *, unsigned char *block, int len, int zlib_decompress_block(void *, unsigned char *block, int len, unsigned char **outblock, int *outlen); +/* + * Connection-sharing API provided by platforms. This function must + * either: + * - return SHARE_NONE and do nothing + * - return SHARE_DOWNSTREAM and set *sock to a Socket connected to + * downplug + * - return SHARE_UPSTREAM and set *sock to a Socket connected to + * upplug. + */ +enum { SHARE_NONE, SHARE_DOWNSTREAM, SHARE_UPSTREAM }; +int platform_ssh_share(const char *name, Conf *conf, + Plug downplug, Plug upplug, Socket *sock, + char **logtext, char **ds_err, char **us_err, + int can_upstream, int can_downstream); +void platform_ssh_share_cleanup(const char *name); + /* * SSH-1 message type codes. */ diff --git a/sshdes.c b/sshdes.c index 81aee8ba..f54990e7 100644 --- a/sshdes.c +++ b/sshdes.c @@ -1031,3 +1031,58 @@ const struct ssh_cipher ssh_des = { des_encrypt_blk, des_decrypt_blk, 8, "single-DES CBC" }; + +#ifdef TEST_XDM_AUTH + +/* + * Small standalone utility which allows encryption and decryption of + * single cipher blocks in the XDM-AUTHORIZATION-1 style. Written + * during the rework of X authorisation for connection sharing, to + * check the corner case when xa1_firstblock matches but the rest of + * the authorisation is bogus. + * + * Just compile this file on its own with the above ifdef symbol + * predefined: + +gcc -DTEST_XDM_AUTH -o sshdes sshdes.c + + */ + +#include +void *safemalloc(size_t n, size_t size) { return calloc(n, size); } +void safefree(void *p) { return free(p); } +void smemclr(void *p, size_t size) { memset(p, 0, size); } +int main(int argc, char **argv) +{ + unsigned char words[2][8]; + unsigned char out[8]; + int i, j; + + memset(words, 0, sizeof(words)); + + for (i = 0; i < 2; i++) { + for (j = 0; j < 8 && argv[i+1][2*j]; j++) { + char x[3]; + unsigned u; + x[0] = argv[i+1][2*j]; + x[1] = argv[i+1][2*j+1]; + x[2] = 0; + sscanf(x, "%02x", &u); + words[i][j] = u; + } + } + + memcpy(out, words[0], 8); + des_decrypt_xdmauth(words[1], out, 8); + printf("decrypt(%s,%s) = ", argv[1], argv[2]); + for (i = 0; i < 8; i++) printf("%02x", out[i]); + printf("\n"); + + memcpy(out, words[0], 8); + des_encrypt_xdmauth(words[1], out, 8); + printf("encrypt(%s,%s) = ", argv[1], argv[2]); + for (i = 0; i < 8; i++) printf("%02x", out[i]); + printf("\n"); +} + +#endif diff --git a/sshshare.c b/sshshare.c new file mode 100644 index 00000000..bd4602b5 --- /dev/null +++ b/sshshare.c @@ -0,0 +1,2103 @@ +/* + * Support for SSH connection sharing, i.e. permitting one PuTTY to + * open its own channels over the SSH session being run by another. + */ + +/* + * Discussion and technical documentation + * ====================================== + * + * The basic strategy for PuTTY's implementation of SSH connection + * sharing is to have a single 'upstream' PuTTY process, which manages + * the real SSH connection and all the cryptography, and then zero or + * more 'downstream' PuTTYs, which never talk to the real host but + * only talk to the upstream through local IPC (Unix-domain sockets or + * Windows named pipes). + * + * The downstreams communicate with the upstream using a protocol + * derived from SSH itself, which I'll document in detail below. In + * brief, though: the downstream->upstream protocol uses a trivial + * binary packet protocol (just length/type/data) to encapsulate + * unencrypted SSH messages, and downstreams talk to the upstream more + * or less as if it was an SSH server itself. (So downstreams can + * themselves open multiple SSH channels, for example, by sending + * multiple SSH2_MSG_CHANNEL_OPENs; they can send CHANNEL_REQUESTs of + * their choice within each channel, and they handle their own + * WINDOW_ADJUST messages.) + * + * The upstream would ideally handle these downstreams by just putting + * their messages into the queue for proper SSH-2 encapsulation and + * encryption and sending them straight on to the server. However, + * that's not quite feasible as written, because client-side channel + * IDs could easily conflict (between multiple downstreams, or between + * a downstream and the upstream). To protect against that, the + * upstream rewrites the client-side channel IDs in messages it passes + * on to the server, so that it's performing what you might describe + * as 'channel-number NAT'. Then the upstream remembers which of its + * own channel IDs are channels it's managing itself, and which are + * placeholders associated with a particular downstream, so that when + * replies come in from the server they can be sent on to the relevant + * downstream (after un-NATting the channel number, of course). + * + * Global requests from downstreams are only accepted if the upstream + * knows what to do about them; currently the only such requests are + * the ones having to do with remote-to-local port forwarding (in + * which, again, the upstream remembers that some of the forwardings + * it's asked the server to set up were on behalf of particular + * downstreams, and sends the incoming CHANNEL_OPENs to those + * downstreams when connections come in). + * + * Other fiddly pieces of this mechanism are X forwarding and + * (OpenSSH-style) agent forwarding. Both of these have a fundamental + * problem arising from the protocol design: that the CHANNEL_OPEN + * from the server introducing a forwarded connection does not carry + * any indication of which session channel gave rise to it; so if + * session channels from multiple downstreams enable those forwarding + * methods, it's hard for the upstream to know which downstream to + * send the resulting connections back to. + * + * For X forwarding, we can work around this in a really painful way + * by using the fake X11 authorisation data sent to the server as part + * of the forwarding setup: upstream ensures that every X forwarding + * request carries distinguishable fake auth data, and then when X + * connections come in it waits to see the auth data in the X11 setup + * message before it decides which downstream to pass the connection + * on to. + * + * For agent forwarding, that workaround is unavailable. As a result, + * this system (and, as far as I can think of, any other system too) + * has the fundamental constraint that it can only forward one SSH + * agent - it can't forward two agents to different session channels. + * So downstreams can request agent forwarding if they like, but if + * they do, they'll get whatever SSH agent is known to the upstream + * (if any) forwarded to their sessions. + * + * Downstream-to-upstream protocol + * ------------------------------- + * + * Here I document in detail the protocol spoken between PuTTY + * downstreams and upstreams over local IPC. The IPC mechanism can + * vary between host platforms, but the protocol is the same. + * + * The protocol commences with a version exchange which is exactly + * like the SSH-2 one, in that each side sends a single line of text + * of the form + * + * -- [comments] \r\n + * + * The only difference is that in real SSH-2, is the string + * "SSH", whereas in this protocol the string is + * "SSHCONNECTION@putty.projects.tartarus.org". + * + * (The SSH RFCs allow many protocol-level identifier namespaces to be + * extended by implementors without central standardisation as long as + * they suffix "@" and a domain name they control to their new ids. + * RFC 4253 does not define this particular name to be changeable at + * all, but I like to think this is obviously how it would have done + * so if the working group had foreseen the need :-) + * + * Thereafter, all data exchanged consists of a sequence of binary + * packets concatenated end-to-end, each of which is of the form + * + * uint32 length of packet, N + * byte[N] N bytes of packet data + * + * and, since these are SSH-2 messages, the first data byte is taken + * to be the packet type code. + * + * These messages are interpreted as those of an SSH connection, after + * userauth completes, and without any repeat key exchange. + * Specifically, any message from the SSH Connection Protocol is + * permitted, and also SSH_MSG_IGNORE, SSH_MSG_DEBUG, + * SSH_MSG_DISCONNECT and SSH_MSG_UNIMPLEMENTED from the SSH Transport + * Protocol. + * + * This protocol imposes a few additional requirements, over and above + * those of the standard SSH Connection Protocol: + * + * Message sizes are not permitted to exceed 0x4010 (16400) bytes, + * including their length header. + * + * When the server (i.e. really the PuTTY upstream) sends + * SSH_MSG_CHANNEL_OPEN with channel type "x11", and the client + * (downstream) responds with SSH_MSG_CHANNEL_OPEN_CONFIRMATION, that + * confirmation message MUST include an initial window size of at + * least 256. (Rationale: this is a bit of a fudge which makes it + * easier, by eliminating the possibility of nasty edge cases, for an + * upstream to arrange not to pass the CHANNEL_OPEN on to downstream + * until after it's seen the X11 auth data to decide which downstream + * it needs to go to.) + */ + +#include +#include +#include +#include + +#include "putty.h" +#include "tree234.h" +#include "ssh.h" + +struct ssh_sharing_state { + const struct plug_function_table *fn; + /* the above variable absolutely *must* be the first in this structure */ + + char *sockname; /* the socket name, kept for cleanup */ + Socket listensock; /* the master listening Socket */ + tree234 *connections; /* holds ssh_sharing_connstates */ + unsigned nextid; /* preferred id for next connstate */ + Ssh ssh; /* instance of the ssh backend */ + char *server_verstring; /* server version string after "SSH-" */ +}; + +struct share_globreq; + +struct ssh_sharing_connstate { + const struct plug_function_table *fn; + /* the above variable absolutely *must* be the first in this structure */ + + unsigned id; /* used to identify this downstream in log messages */ + + Socket sock; /* the Socket for this connection */ + struct ssh_sharing_state *parent; + + int crLine; /* coroutine state for share_receive */ + + int sent_verstring, got_verstring, curr_packetlen; + + unsigned char recvbuf[0x4010]; + int recvlen; + + /* + * Assorted state we have to remember about this downstream, so + * that we can clean it up appropriately when the downstream goes + * away. + */ + + /* Channels which don't have a downstream id, i.e. we've passed a + * CHANNEL_OPEN down from the server but not had an + * OPEN_CONFIRMATION or OPEN_FAILURE back. If downstream goes + * away, we respond to all of these with OPEN_FAILURE. */ + tree234 *halfchannels; /* stores 'struct share_halfchannel' */ + + /* Channels which do have a downstream id. We need to index these + * by both server id and upstream id, so we can find a channel + * when handling either an upward or a downward message referring + * to it. */ + tree234 *channels_by_us; /* stores 'struct share_channel' */ + tree234 *channels_by_server; /* stores 'struct share_channel' */ + + /* Another class of channel which doesn't have a downstream id. + * The difference between these and halfchannels is that xchannels + * do have an *upstream* id, because upstream has already accepted + * the channel request from the server. This arises in the case of + * X forwarding, where we have to accept the request and read the + * X authorisation data before we know whether the channel needs + * to be forwarded to a downstream. */ + tree234 *xchannels_by_us; /* stores 'struct share_xchannel' */ + tree234 *xchannels_by_server; /* stores 'struct share_xchannel' */ + + /* Remote port forwarding requests in force. */ + tree234 *forwardings; /* stores 'struct share_forwarding' */ + + /* Global requests we've sent on to the server, pending replies. */ + struct share_globreq *globreq_head, *globreq_tail; +}; + +struct share_halfchannel { + unsigned server_id; +}; + +/* States of a share_channel. */ +enum { + OPEN, + SENT_CLOSE, + RCVD_CLOSE, + /* Downstream has sent CHANNEL_OPEN but server hasn't replied yet. + * If downstream goes away when a channel is in this state, we + * must wait for the server's response before starting to send + * CLOSE. Channels in this state are also not held in + * channels_by_server, because their server_id field is + * meaningless. */ + UNACKNOWLEDGED +}; + +struct share_channel { + unsigned downstream_id, upstream_id, server_id; + int downstream_maxpkt; + int state; + /* + * Some channels (specifically, channels on which downstream has + * sent "x11-req") have the additional function of storing a set + * of downstream X authorisation data and a handle to an upstream + * fake set. + */ + struct X11FakeAuth *x11_auth_upstream; + int x11_auth_proto; + char *x11_auth_data; + int x11_auth_datalen; + int x11_one_shot; +}; + +struct share_forwarding { + char *host; + int port; + int active; /* has the server sent REQUEST_SUCCESS? */ +}; + +struct share_xchannel_message { + struct share_xchannel_message *next; + int type; + unsigned char *data; + int datalen; +}; + +struct share_xchannel { + unsigned upstream_id, server_id; + + /* + * xchannels come in two flavours: live and dead. Live ones are + * waiting for an OPEN_CONFIRMATION or OPEN_FAILURE from + * downstream; dead ones have had an OPEN_FAILURE, so they only + * exist as a means of letting us conveniently respond to further + * channel messages from the server until such time as the server + * sends us CHANNEL_CLOSE. + */ + int live; + + /* + * When we receive OPEN_CONFIRMATION, we will need to send a + * WINDOW_ADJUST to the server to synchronise the windows. For + * this purpose we need to know what window we have so far offered + * the server. We record this as exactly the value in the + * OPEN_CONFIRMATION that upstream sent us, adjusted by the amount + * by which the two X greetings differed in length. + */ + int window; + + /* + * Linked list of SSH messages from the server relating to this + * channel, which we queue up until downstream sends us an + * OPEN_CONFIRMATION and we can belatedly send them all on. + */ + struct share_xchannel_message *msghead, *msgtail; +}; + +enum { + GLOBREQ_TCPIP_FORWARD, + GLOBREQ_CANCEL_TCPIP_FORWARD +}; + +struct share_globreq { + struct share_globreq *next; + int type; + int want_reply; + struct share_forwarding *fwd; +}; + +static int share_connstate_cmp(void *av, void *bv) +{ + const struct ssh_sharing_connstate *a = + (const struct ssh_sharing_connstate *)av; + const struct ssh_sharing_connstate *b = + (const struct ssh_sharing_connstate *)bv; + + if (a->id < b->id) + return -1; + else if (a->id > b->id) + return +1; + else + return 0; +} + +static unsigned share_find_unused_id +(struct ssh_sharing_state *sharestate, unsigned first) +{ + int low_orig, low, mid, high, high_orig; + struct ssh_sharing_connstate *cs; + unsigned ret; + + /* + * Find the lowest unused downstream ID greater or equal to + * 'first'. + * + * Begin by seeing if 'first' itself is available. If it is, we'll + * just return it; if it's already in the tree, we'll find the + * tree index where it appears and use that for the next stage. + */ + { + struct ssh_sharing_connstate dummy; + dummy.id = first; + cs = findrelpos234(sharestate->connections, &dummy, NULL, + REL234_GE, &low_orig); + if (!cs) + return first; + } + + /* + * Now binary-search using the counted B-tree, to find the largest + * ID which is in a contiguous sequence from the beginning of that + * range. + */ + low = low_orig; + high = high_orig = count234(sharestate->connections); + while (high - low > 1) { + mid = (high + low) / 2; + cs = index234(sharestate->connections, mid); + if (cs->id == first + (mid - low_orig)) + low = mid; /* this one is still in the sequence */ + else + high = mid; /* this one is past the end */ + } + + /* + * Now low is the tree index of the largest ID in the initial + * sequence. So the return value is one more than low's id, and we + * know low's id is given by the formula in the binary search loop + * above. + * + * (If an SSH connection went on for _enormously_ long, we might + * reach a point where all ids from 'first' to UINT_MAX were in + * use. In that situation the formula below would wrap round by + * one and return zero, which is conveniently the right way to + * signal 'no id available' from this function.) + */ + ret = first + (low - low_orig) + 1; + { + struct ssh_sharing_connstate dummy; + dummy.id = ret; + assert(NULL == find234(sharestate->connections, &dummy, NULL)); + } + return ret; +} + +static int share_halfchannel_cmp(void *av, void *bv) +{ + const struct share_halfchannel *a = (const struct share_halfchannel *)av; + const struct share_halfchannel *b = (const struct share_halfchannel *)bv; + + if (a->server_id < b->server_id) + return -1; + else if (a->server_id > b->server_id) + return +1; + else + return 0; +} + +static int share_channel_us_cmp(void *av, void *bv) +{ + const struct share_channel *a = (const struct share_channel *)av; + const struct share_channel *b = (const struct share_channel *)bv; + + if (a->upstream_id < b->upstream_id) + return -1; + else if (a->upstream_id > b->upstream_id) + return +1; + else + return 0; +} + +static int share_channel_server_cmp(void *av, void *bv) +{ + const struct share_channel *a = (const struct share_channel *)av; + const struct share_channel *b = (const struct share_channel *)bv; + + if (a->server_id < b->server_id) + return -1; + else if (a->server_id > b->server_id) + return +1; + else + return 0; +} + +static int share_xchannel_us_cmp(void *av, void *bv) +{ + const struct share_xchannel *a = (const struct share_xchannel *)av; + const struct share_xchannel *b = (const struct share_xchannel *)bv; + + if (a->upstream_id < b->upstream_id) + return -1; + else if (a->upstream_id > b->upstream_id) + return +1; + else + return 0; +} + +static int share_xchannel_server_cmp(void *av, void *bv) +{ + const struct share_xchannel *a = (const struct share_xchannel *)av; + const struct share_xchannel *b = (const struct share_xchannel *)bv; + + if (a->server_id < b->server_id) + return -1; + else if (a->server_id > b->server_id) + return +1; + else + return 0; +} + +static int share_forwarding_cmp(void *av, void *bv) +{ + const struct share_forwarding *a = (const struct share_forwarding *)av; + const struct share_forwarding *b = (const struct share_forwarding *)bv; + int i; + + if ((i = strcmp(a->host, b->host)) != 0) + return i; + else if (a->port < b->port) + return -1; + else if (a->port > b->port) + return +1; + else + return 0; +} + +static void share_xchannel_free(struct share_xchannel *xc) +{ + while (xc->msghead) { + struct share_xchannel_message *tmp = xc->msghead; + xc->msghead = tmp->next; + sfree(tmp); + } + sfree(xc); +} + +static void share_connstate_free(struct ssh_sharing_connstate *cs) +{ + struct share_halfchannel *hc; + struct share_xchannel *xc; + struct share_channel *chan; + struct share_forwarding *fwd; + + while ((hc = (struct share_halfchannel *) + delpos234(cs->halfchannels, 0)) != NULL) + sfree(hc); + freetree234(cs->halfchannels); + + /* All channels live in 'channels_by_us' but only some in + * 'channels_by_server', so we use the former to find the list of + * ones to free */ + freetree234(cs->channels_by_server); + while ((chan = (struct share_channel *) + delpos234(cs->channels_by_us, 0)) != NULL) + sfree(chan); + freetree234(cs->channels_by_us); + + /* But every xchannel is in both trees, so it doesn't matter which + * we use to free them. */ + while ((xc = (struct share_xchannel *) + delpos234(cs->xchannels_by_us, 0)) != NULL) + share_xchannel_free(xc); + freetree234(cs->xchannels_by_us); + freetree234(cs->xchannels_by_server); + + while ((fwd = (struct share_forwarding *) + delpos234(cs->forwardings, 0)) != NULL) + sfree(fwd); + freetree234(cs->forwardings); + + while (cs->globreq_head) { + struct share_globreq *globreq = cs->globreq_head; + cs->globreq_head = cs->globreq_head->next; + sfree(globreq); + } + + sfree(cs); +} + +void sharestate_free(void *v) +{ + struct ssh_sharing_state *sharestate = (struct ssh_sharing_state *)v; + struct ssh_sharing_connstate *cs; + + platform_ssh_share_cleanup(sharestate->sockname); + + while ((cs = (struct ssh_sharing_connstate *) + delpos234(sharestate->connections, 0)) != NULL) { + share_connstate_free(cs); + } + freetree234(sharestate->connections); + sfree(sharestate->server_verstring); + sfree(sharestate->sockname); + sfree(sharestate); +} + +static struct share_halfchannel *share_add_halfchannel + (struct ssh_sharing_connstate *cs, unsigned server_id) +{ + struct share_halfchannel *hc = snew(struct share_halfchannel); + hc->server_id = server_id; + if (add234(cs->halfchannels, hc) != hc) { + /* Duplicate?! */ + sfree(hc); + return NULL; + } else { + return hc; + } +} + +static struct share_halfchannel *share_find_halfchannel + (struct ssh_sharing_connstate *cs, unsigned server_id) +{ + struct share_halfchannel dummyhc; + dummyhc.server_id = server_id; + return find234(cs->halfchannels, &dummyhc, NULL); +} + +static void share_remove_halfchannel(struct ssh_sharing_connstate *cs, + struct share_halfchannel *hc) +{ + del234(cs->halfchannels, hc); + sfree(hc); +} + +static struct share_channel *share_add_channel + (struct ssh_sharing_connstate *cs, unsigned downstream_id, + unsigned upstream_id, unsigned server_id, int state, int maxpkt) +{ + struct share_channel *chan = snew(struct share_channel); + chan->downstream_id = downstream_id; + chan->upstream_id = upstream_id; + chan->server_id = server_id; + chan->state = state; + chan->downstream_maxpkt = maxpkt; + chan->x11_auth_upstream = NULL; + chan->x11_auth_data = NULL; + chan->x11_auth_proto = -1; + chan->x11_auth_datalen = 0; + chan->x11_one_shot = 0; + if (add234(cs->channels_by_us, chan) != chan) { + sfree(chan); + return NULL; + } + if (chan->state != UNACKNOWLEDGED) { + if (add234(cs->channels_by_server, chan) != chan) { + del234(cs->channels_by_us, chan); + sfree(chan); + return NULL; + } + } + return chan; +} + +static void share_channel_set_server_id(struct ssh_sharing_connstate *cs, + struct share_channel *chan, + unsigned server_id, int newstate) +{ + chan->server_id = server_id; + chan->state = newstate; + assert(newstate != UNACKNOWLEDGED); + add234(cs->channels_by_server, chan); +} + +static struct share_channel *share_find_channel_by_upstream + (struct ssh_sharing_connstate *cs, unsigned upstream_id) +{ + struct share_channel dummychan; + dummychan.upstream_id = upstream_id; + return find234(cs->channels_by_us, &dummychan, NULL); +} + +static struct share_channel *share_find_channel_by_server + (struct ssh_sharing_connstate *cs, unsigned server_id) +{ + struct share_channel dummychan; + dummychan.server_id = server_id; + return find234(cs->channels_by_server, &dummychan, NULL); +} + +static void share_remove_channel(struct ssh_sharing_connstate *cs, + struct share_channel *chan) +{ + del234(cs->channels_by_us, chan); + del234(cs->channels_by_server, chan); + if (chan->x11_auth_upstream) + ssh_sharing_remove_x11_display(cs->parent->ssh, + chan->x11_auth_upstream); + sfree(chan->x11_auth_data); + sfree(chan); +} + +static struct share_xchannel *share_add_xchannel + (struct ssh_sharing_connstate *cs, + unsigned upstream_id, unsigned server_id) +{ + struct share_xchannel *xc = snew(struct share_xchannel); + xc->upstream_id = upstream_id; + xc->server_id = server_id; + xc->live = TRUE; + xc->msghead = xc->msgtail = NULL; + if (add234(cs->xchannels_by_us, xc) != xc) { + sfree(xc); + return NULL; + } + if (add234(cs->xchannels_by_server, xc) != xc) { + del234(cs->xchannels_by_us, xc); + sfree(xc); + return NULL; + } + return xc; +} + +static struct share_xchannel *share_find_xchannel_by_upstream + (struct ssh_sharing_connstate *cs, unsigned upstream_id) +{ + struct share_xchannel dummyxc; + dummyxc.upstream_id = upstream_id; + return find234(cs->xchannels_by_us, &dummyxc, NULL); +} + +static struct share_xchannel *share_find_xchannel_by_server + (struct ssh_sharing_connstate *cs, unsigned server_id) +{ + struct share_xchannel dummyxc; + dummyxc.server_id = server_id; + return find234(cs->xchannels_by_server, &dummyxc, NULL); +} + +static void share_remove_xchannel(struct ssh_sharing_connstate *cs, + struct share_xchannel *xc) +{ + del234(cs->xchannels_by_us, xc); + del234(cs->xchannels_by_server, xc); + share_xchannel_free(xc); +} + +static struct share_forwarding *share_add_forwarding + (struct ssh_sharing_connstate *cs, + const char *host, int port) +{ + struct share_forwarding *fwd = snew(struct share_forwarding); + fwd->host = dupstr(host); + fwd->port = port; + fwd->active = FALSE; + if (add234(cs->forwardings, fwd) != fwd) { + /* Duplicate?! */ + sfree(fwd); + return NULL; + } + return fwd; +} + +static struct share_forwarding *share_find_forwarding + (struct ssh_sharing_connstate *cs, const char *host, int port) +{ + struct share_forwarding dummyfwd, *ret; + dummyfwd.host = dupstr(host); + dummyfwd.port = port; + ret = find234(cs->forwardings, &dummyfwd, NULL); + sfree(dummyfwd.host); + return ret; +} + +static void share_remove_forwarding(struct ssh_sharing_connstate *cs, + struct share_forwarding *fwd) +{ + del234(cs->forwardings, fwd); + sfree(fwd); +} + +static void send_packet_to_downstream(struct ssh_sharing_connstate *cs, + int type, const void *pkt, int pktlen, + struct share_channel *chan) +{ + if (!cs->sock) /* throw away all packets destined for a dead downstream */ + return; + + if (type == SSH2_MSG_CHANNEL_DATA) { + /* + * Special case which we take care of at a low level, so as to + * be sure to apply it in all cases. On rare occasions we + * might find that we have a channel for which the + * downstream's maximum packet size exceeds the max packet + * size we presented to the server on its behalf. (This can + * occur in X11 forwarding, where we have to send _our_ + * CHANNEL_OPEN_CONFIRMATION before we discover which if any + * downstream the channel is destined for, so if that + * downstream turns out to present a smaller max packet size + * then we're in this situation.) + * + * If that happens, we just chop up the packet into pieces and + * send them as separate CHANNEL_DATA packets. + */ + const char *upkt = (const char *)pkt; + char header[13]; /* 4 length + 1 type + 4 channel id + 4 string len */ + + int len = toint(GET_32BIT(upkt + 4)); + upkt += 8; /* skip channel id + length field */ + + if (len < 0 || len > pktlen - 8) + len = pktlen - 8; + + do { + int this_len = (len > chan->downstream_maxpkt ? + chan->downstream_maxpkt : len); + PUT_32BIT(header, this_len + 9); + header[4] = type; + PUT_32BIT(header + 5, chan->downstream_id); + PUT_32BIT(header + 9, this_len); + sk_write(cs->sock, header, 13); + sk_write(cs->sock, upkt, this_len); + len -= this_len; + upkt += this_len; + } while (len > 0); + } else { + /* + * Just do the obvious thing. + */ + char header[9]; + + PUT_32BIT(header, pktlen + 1); + header[4] = type; + sk_write(cs->sock, header, 5); + sk_write(cs->sock, pkt, pktlen); + } +} + +static void share_try_cleanup(struct ssh_sharing_connstate *cs) +{ + int i; + struct share_halfchannel *hc; + struct share_channel *chan; + struct share_forwarding *fwd; + + /* + * Any half-open channels, i.e. those for which we'd received + * CHANNEL_OPEN from the server but not passed back a response + * from downstream, should be responded to with OPEN_FAILURE. + */ + while ((hc = (struct share_halfchannel *) + index234(cs->halfchannels, 0)) != NULL) { + static const char reason[] = "PuTTY downstream no longer available"; + static const char lang[] = "en"; + unsigned char packet[256]; + int pos = 0; + + PUT_32BIT(packet + pos, hc->server_id); pos += 4; + PUT_32BIT(packet + pos, SSH2_OPEN_CONNECT_FAILED); pos += 4; + PUT_32BIT(packet + pos, strlen(reason)); pos += 4; + memcpy(packet + pos, reason, strlen(reason)); pos += strlen(reason); + PUT_32BIT(packet + pos, strlen(lang)); pos += 4; + memcpy(packet + pos, lang, strlen(lang)); pos += strlen(lang); + ssh_send_packet_from_downstream(cs->parent->ssh, cs->id, + SSH2_MSG_CHANNEL_OPEN_FAILURE, + packet, pos, "cleanup after" + " downstream went away"); + + share_remove_halfchannel(cs, hc); + } + + /* + * Any actually open channels should have a CHANNEL_CLOSE sent for + * them, unless we've already done so. We won't be able to + * actually clean them up until CHANNEL_CLOSE comes back from the + * server, though (unless the server happens to have sent a CLOSE + * already). + * + * Another annoying exception is UNACKNOWLEDGED channels, i.e. + * we've _sent_ a CHANNEL_OPEN to the server but not received an + * OPEN_CONFIRMATION or OPEN_FAILURE. We must wait for a reply + * before closing the channel, because until we see that reply we + * won't have the server's channel id to put in the close message. + */ + for (i = 0; (chan = (struct share_channel *) + index234(cs->channels_by_us, i)) != NULL; i++) { + unsigned char packet[256]; + int pos = 0; + + if (chan->state != SENT_CLOSE && chan->state != UNACKNOWLEDGED) { + PUT_32BIT(packet + pos, chan->server_id); pos += 4; + ssh_send_packet_from_downstream(cs->parent->ssh, cs->id, + SSH2_MSG_CHANNEL_CLOSE, + packet, pos, "cleanup after" + " downstream went away"); + if (chan->state != RCVD_CLOSE) { + chan->state = SENT_CLOSE; + } else { + /* In this case, we _can_ clear up the channel now. */ + ssh_delete_sharing_channel(cs->parent->ssh, chan->upstream_id); + share_remove_channel(cs, chan); + i--; /* don't accidentally skip one as a result */ + } + } + } + + /* + * Any remote port forwardings we're managing on behalf of this + * downstream should be cancelled. Again, we must defer those for + * which we haven't yet seen REQUEST_SUCCESS/FAILURE. + * + * We take a fire-and-forget approach during cleanup, not + * bothering to set want_reply. + */ + for (i = 0; (fwd = (struct share_forwarding *) + index234(cs->forwardings, i)) != NULL; i++) { + if (fwd->active) { + static const char request[] = "cancel-tcpip-forward"; + char *packet = snewn(256 + strlen(fwd->host), char); + int pos = 0; + + PUT_32BIT(packet + pos, strlen(request)); pos += 4; + memcpy(packet + pos, request, strlen(request)); + pos += strlen(request); + + packet[pos++] = 0; /* !want_reply */ + + PUT_32BIT(packet + pos, strlen(fwd->host)); pos += 4; + memcpy(packet + pos, fwd->host, strlen(fwd->host)); + pos += strlen(fwd->host); + + PUT_32BIT(packet + pos, fwd->port); pos += 4; + + ssh_send_packet_from_downstream(cs->parent->ssh, cs->id, + SSH2_MSG_GLOBAL_REQUEST, + packet, pos, "cleanup after" + " downstream went away"); + + share_remove_forwarding(cs, fwd); + i--; /* don't accidentally skip one as a result */ + } + } + + if (count234(cs->halfchannels) == 0 && + count234(cs->channels_by_us) == 0 && + count234(cs->forwardings) == 0) { + /* + * Now we're _really_ done, so we can get rid of cs completely. + */ + del234(cs->parent->connections, cs); + ssh_sharing_downstream_disconnected(cs->parent->ssh, cs->id); + share_connstate_free(cs); + } +} + +static void share_begin_cleanup(struct ssh_sharing_connstate *cs) +{ + + sk_close(cs->sock); + cs->sock = NULL; + + share_try_cleanup(cs); +} + +static void share_disconnect(struct ssh_sharing_connstate *cs, + const char *message) +{ + static const char lang[] = "en"; + int msglen = strlen(message); + char *packet = snewn(msglen + 256, char); + int pos = 0; + + PUT_32BIT(packet + pos, SSH2_DISCONNECT_PROTOCOL_ERROR); pos += 4; + + PUT_32BIT(packet + pos, msglen); pos += 4; + memcpy(packet + pos, message, msglen); + pos += msglen; + + PUT_32BIT(packet + pos, strlen(lang)); pos += 4; + memcpy(packet + pos, lang, strlen(lang)); pos += strlen(lang); + + send_packet_to_downstream(cs, SSH2_MSG_DISCONNECT, packet, pos, NULL); + + share_begin_cleanup(cs); +} + +static int share_closing(Plug plug, const char *error_msg, int error_code, + int calling_back) +{ + struct ssh_sharing_connstate *cs = (struct ssh_sharing_connstate *)plug; + if (error_msg) + ssh_sharing_logf(cs->parent->ssh, cs->id, "%s", error_msg); + share_begin_cleanup(cs); + return 1; +} + +static int getstring_inner(const void *vdata, int datalen, + char **out, int *outlen) +{ + const unsigned char *data = (const unsigned char *)vdata; + int len; + + if (datalen < 4) + return FALSE; + + len = toint(GET_32BIT(data)); + if (len < 0 || len > datalen - 4) + return FALSE; + + if (outlen) + *outlen = len + 4; /* total size including length field */ + if (out) + *out = dupprintf("%.*s", len, (char *)data + 4); + return TRUE; +} + +static char *getstring(const void *data, int datalen) +{ + char *ret; + if (getstring_inner(data, datalen, &ret, NULL)) + return ret; + else + return NULL; +} + +static int getstring_size(const void *data, int datalen) +{ + int ret; + if (getstring_inner(data, datalen, NULL, &ret)) + return ret; + else + return -1; +} + +/* + * Append a message to the end of an xchannel's queue, with the length + * and type code filled in and the data block allocated but + * uninitialised. + */ +struct share_xchannel_message *share_xchannel_add_message +(struct share_xchannel *xc, int type, int len) +{ + unsigned char *block; + struct share_xchannel_message *msg; + + /* + * Be a little tricksy here by allocating a single memory block + * containing both the 'struct share_xchannel_message' and the + * actual data. Simplifies freeing it later. + */ + block = smalloc(sizeof(struct share_xchannel_message) + len); + msg = (struct share_xchannel_message *)block; + msg->data = block + sizeof(struct share_xchannel_message); + msg->datalen = len; + msg->type = type; + + /* + * Queue it in the xchannel. + */ + if (xc->msgtail) + xc->msgtail->next = msg; + else + xc->msghead = msg; + msg->next = NULL; + xc->msgtail = msg; + + return msg; +} + +void share_dead_xchannel_respond(struct ssh_sharing_connstate *cs, + struct share_xchannel *xc) +{ + /* + * Handle queued incoming messages from the server destined for an + * xchannel which is dead (i.e. downstream sent OPEN_FAILURE). + */ + int delete = FALSE; + while (xc->msghead) { + struct share_xchannel_message *msg = xc->msghead; + xc->msghead = msg->next; + + if (msg->type == SSH2_MSG_CHANNEL_REQUEST && msg->datalen > 4) { + /* + * A CHANNEL_REQUEST is responded to by sending + * CHANNEL_FAILURE, if it has want_reply set. + */ + int wantreplypos = getstring_size(msg->data, msg->datalen); + if (wantreplypos > 0 && wantreplypos < msg->datalen && + msg->data[wantreplypos] != 0) { + unsigned char id[4]; + PUT_32BIT(id, xc->server_id); + ssh_send_packet_from_downstream + (cs->parent->ssh, cs->id, SSH2_MSG_CHANNEL_FAILURE, id, 4, + "downstream refused X channel open"); + } + } else if (msg->type == SSH2_MSG_CHANNEL_CLOSE) { + /* + * On CHANNEL_CLOSE we can discard the channel completely. + */ + delete = TRUE; + } + + sfree(msg); + } + xc->msgtail = NULL; + if (delete) { + ssh_delete_sharing_channel(cs->parent->ssh, xc->upstream_id); + share_remove_xchannel(cs, xc); + } +} + +void share_xchannel_confirmation(struct ssh_sharing_connstate *cs, + struct share_xchannel *xc, + struct share_channel *chan, + unsigned downstream_window) +{ + unsigned char window_adjust[8]; + + /* + * Send all the queued messages downstream. + */ + while (xc->msghead) { + struct share_xchannel_message *msg = xc->msghead; + xc->msghead = msg->next; + + if (msg->datalen >= 4) + PUT_32BIT(msg->data, chan->downstream_id); + send_packet_to_downstream(cs, msg->type, + msg->data, msg->datalen, chan); + + sfree(msg); + } + + /* + * Send a WINDOW_ADJUST back upstream, to synchronise the window + * size downstream thinks it's presented with the one we've + * actually presented. + */ + PUT_32BIT(window_adjust, xc->server_id); + PUT_32BIT(window_adjust + 4, downstream_window - xc->window); + ssh_send_packet_from_downstream(cs->parent->ssh, cs->id, + SSH2_MSG_CHANNEL_WINDOW_ADJUST, + window_adjust, 8, "window adjustment after" + " downstream accepted X channel"); +} + +void share_xchannel_failure(struct ssh_sharing_connstate *cs, + struct share_xchannel *xc) +{ + /* + * If downstream refuses to open our X channel at all for some + * reason, we must respond by sending an emergency CLOSE upstream. + */ + unsigned char id[4]; + PUT_32BIT(id, xc->server_id); + ssh_send_packet_from_downstream + (cs->parent->ssh, cs->id, SSH2_MSG_CHANNEL_CLOSE, id, 4, + "downstream refused X channel open"); + + /* + * Now mark the xchannel as dead, and respond to anything sent on + * it until we see CLOSE for it in turn. + */ + xc->live = FALSE; + share_dead_xchannel_respond(cs, xc); +} + +void share_setup_x11_channel(void *csv, void *chanv, + unsigned upstream_id, unsigned server_id, + unsigned server_currwin, unsigned server_maxpkt, + unsigned client_adjusted_window, + const char *peer_addr, int peer_port, int endian, + int protomajor, int protominor, + const void *initial_data, int initial_len) +{ + struct ssh_sharing_connstate *cs = (struct ssh_sharing_connstate *)csv; + struct share_channel *chan = (struct share_channel *)chanv; + struct share_xchannel *xc; + struct share_xchannel_message *msg; + void *greeting; + int greeting_len; + unsigned char *pkt; + int pktlen; + + /* + * Create an xchannel containing data we've already received from + * the X client, and preload it with a CHANNEL_DATA message + * containing our own made-up authorisation greeting and any + * additional data sent from the server so far. + */ + xc = share_add_xchannel(cs, upstream_id, server_id); + greeting = x11_make_greeting(endian, protomajor, protominor, + chan->x11_auth_proto, + chan->x11_auth_data, chan->x11_auth_datalen, + peer_addr, peer_port, &greeting_len); + msg = share_xchannel_add_message(xc, SSH2_MSG_CHANNEL_DATA, + 8 + greeting_len + initial_len); + /* leave the channel id field unfilled - we don't know the + * downstream id yet, of course */ + PUT_32BIT(msg->data + 4, greeting_len + initial_len); + memcpy(msg->data + 8, greeting, greeting_len); + memcpy(msg->data + 8 + greeting_len, initial_data, initial_len); + sfree(greeting); + + xc->window = client_adjusted_window + greeting_len; + + /* + * Send on a CHANNEL_OPEN to downstream. + */ + pktlen = 27 + strlen(peer_addr); + pkt = snewn(pktlen, unsigned char); + PUT_32BIT(pkt, 3); /* strlen("x11") */ + memcpy(pkt+4, "x11", 3); + PUT_32BIT(pkt+7, server_id); + PUT_32BIT(pkt+11, server_currwin); + PUT_32BIT(pkt+15, server_maxpkt); + PUT_32BIT(pkt+19, strlen(peer_addr)); + memcpy(pkt+23, peer_addr, strlen(peer_addr)); + PUT_32BIT(pkt+23+strlen(peer_addr), peer_port); + send_packet_to_downstream(cs, SSH2_MSG_CHANNEL_OPEN, pkt, pktlen, NULL); + sfree(pkt); + + /* + * If this was a once-only X forwarding, clean it up now. + */ + if (chan->x11_one_shot) { + ssh_sharing_remove_x11_display(cs->parent->ssh, + chan->x11_auth_upstream); + chan->x11_auth_upstream = NULL; + sfree(chan->x11_auth_data); + chan->x11_auth_proto = -1; + chan->x11_auth_datalen = 0; + chan->x11_one_shot = 0; + } +} + +void share_got_pkt_from_server(void *csv, int type, + unsigned char *pkt, int pktlen) +{ + struct ssh_sharing_connstate *cs = (struct ssh_sharing_connstate *)csv; + struct share_globreq *globreq; + int id_pos; + unsigned upstream_id, server_id; + struct share_channel *chan; + struct share_xchannel *xc; + + switch (type) { + case SSH2_MSG_REQUEST_SUCCESS: + case SSH2_MSG_REQUEST_FAILURE: + globreq = cs->globreq_head; + if (globreq->type == GLOBREQ_TCPIP_FORWARD) { + if (type == SSH2_MSG_REQUEST_FAILURE) { + share_remove_forwarding(cs, globreq->fwd); + } else { + globreq->fwd->active = TRUE; + } + } else if (globreq->type == GLOBREQ_CANCEL_TCPIP_FORWARD) { + if (type == SSH2_MSG_REQUEST_SUCCESS) { + share_remove_forwarding(cs, globreq->fwd); + } + } + if (globreq->want_reply) { + send_packet_to_downstream(cs, type, pkt, pktlen, NULL); + } + cs->globreq_head = globreq->next; + sfree(globreq); + if (cs->globreq_head == NULL) + cs->globreq_tail = NULL; + + if (!cs->sock) { + /* Retry cleaning up this connection, in case that reply + * was the last thing we were waiting for. */ + share_try_cleanup(cs); + } + + break; + + case SSH2_MSG_CHANNEL_OPEN: + id_pos = getstring_size(pkt, pktlen); + assert(id_pos >= 0); + server_id = GET_32BIT(pkt + id_pos); + share_add_halfchannel(cs, server_id); + + send_packet_to_downstream(cs, type, pkt, pktlen, NULL); + break; + + case SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: + case SSH2_MSG_CHANNEL_OPEN_FAILURE: + case SSH2_MSG_CHANNEL_CLOSE: + case SSH2_MSG_CHANNEL_WINDOW_ADJUST: + case SSH2_MSG_CHANNEL_DATA: + case SSH2_MSG_CHANNEL_EXTENDED_DATA: + case SSH2_MSG_CHANNEL_EOF: + case SSH2_MSG_CHANNEL_REQUEST: + case SSH2_MSG_CHANNEL_SUCCESS: + case SSH2_MSG_CHANNEL_FAILURE: + /* + * All these messages have the recipient channel id as the + * first uint32 field in the packet. Substitute the downstream + * channel id for our one and pass the packet downstream. + */ + assert(pktlen >= 4); + upstream_id = GET_32BIT(pkt); + if ((chan = share_find_channel_by_upstream(cs, upstream_id)) != NULL) { + /* + * The normal case: this id refers to an open channel. + */ + PUT_32BIT(pkt, chan->downstream_id); + send_packet_to_downstream(cs, type, pkt, pktlen, chan); + + /* + * Update the channel state, for messages that need it. + */ + if (type == SSH2_MSG_CHANNEL_OPEN_CONFIRMATION) { + if (chan->state == UNACKNOWLEDGED && pktlen >= 8) { + share_channel_set_server_id(cs, chan, GET_32BIT(pkt+4), + OPEN); + if (!cs->sock) { + /* Retry cleaning up this connection, so that we + * can send an immediate CLOSE on this channel for + * which we now know the server id. */ + share_try_cleanup(cs); + } + } + } else if (type == SSH2_MSG_CHANNEL_OPEN_FAILURE) { + ssh_delete_sharing_channel(cs->parent->ssh, chan->upstream_id); + share_remove_channel(cs, chan); + } else if (type == SSH2_MSG_CHANNEL_CLOSE) { + if (chan->state == SENT_CLOSE) { + ssh_delete_sharing_channel(cs->parent->ssh, + chan->upstream_id); + share_remove_channel(cs, chan); + if (!cs->sock) { + /* Retry cleaning up this connection, in case this + * channel closure was the last thing we were + * waiting for. */ + share_try_cleanup(cs); + } + } else { + chan->state = RCVD_CLOSE; + } + } + } else if ((xc = share_find_xchannel_by_upstream(cs, upstream_id)) + != NULL) { + /* + * The unusual case: this id refers to an xchannel. Add it + * to the xchannel's queue. + */ + struct share_xchannel_message *msg; + + msg = share_xchannel_add_message(xc, type, pktlen); + memcpy(msg->data, pkt, pktlen); + + /* If the xchannel is dead, then also respond to it (which + * may involve deleting the channel). */ + if (!xc->live) + share_dead_xchannel_respond(cs, xc); + } + break; + + default: + assert(!"This packet type should never have come from ssh.c"); + break; + } +} + +static void share_got_pkt_from_downstream(struct ssh_sharing_connstate *cs, + int type, + unsigned char *pkt, int pktlen) +{ + char *request_name; + struct share_forwarding *fwd; + int id_pos; + unsigned old_id, new_id, server_id; + struct share_globreq *globreq; + struct share_channel *chan; + struct share_halfchannel *hc; + struct share_xchannel *xc; + char *err = NULL; + + switch (type) { + case SSH2_MSG_DISCONNECT: + /* + * This message stops here: if downstream is disconnecting + * from us, that doesn't mean we want to disconnect from the + * SSH server. Close the downstream connection and start + * cleanup. + */ + share_begin_cleanup(cs); + break; + + case SSH2_MSG_GLOBAL_REQUEST: + /* + * The only global requests we understand are "tcpip-forward" + * and "cancel-tcpip-forward". Since those require us to + * maintain state, we must assume that other global requests + * will probably require that too, and so we don't forward on + * any request we don't understand. + */ + request_name = getstring(pkt, pktlen); + if (request_name == NULL) { + err = dupprintf("Truncated GLOBAL_REQUEST packet"); + goto confused; + } + + if (!strcmp(request_name, "tcpip-forward")) { + int wantreplypos, orig_wantreply, port, ret; + char *host; + + sfree(request_name); + + /* + * Pick the packet apart to find the want_reply field and + * the host/port we're going to ask to listen on. + */ + wantreplypos = getstring_size(pkt, pktlen); + if (wantreplypos < 0 || wantreplypos >= pktlen) { + err = dupprintf("Truncated GLOBAL_REQUEST packet"); + goto confused; + } + orig_wantreply = pkt[wantreplypos]; + port = getstring_size(pkt + (wantreplypos + 1), + pktlen - (wantreplypos + 1)); + port += (wantreplypos + 1); + if (port < 0 || port > pktlen - 4) { + err = dupprintf("Truncated GLOBAL_REQUEST packet"); + goto confused; + } + host = getstring(pkt + (wantreplypos + 1), + pktlen - (wantreplypos + 1)); + assert(host != NULL); + port = GET_32BIT(pkt + port); + + /* + * See if we can allocate space in ssh.c's tree of remote + * port forwardings. If we can't, it's because another + * client sharing this connection has already allocated + * the identical port forwarding, so we take it on + * ourselves to manufacture a failure packet and send it + * back to downstream. + */ + ret = ssh_alloc_sharing_rportfwd(cs->parent->ssh, host, port, cs); + if (!ret) { + if (orig_wantreply) { + send_packet_to_downstream(cs, SSH2_MSG_REQUEST_FAILURE, + "", 0, NULL); + } + } else { + /* + * We've managed to make space for this forwarding + * locally. Pass the request on to the SSH server, but + * set want_reply even if it wasn't originally set, so + * that we know whether this forwarding needs to be + * cleaned up if downstream goes away. + */ + int old_wantreply = pkt[wantreplypos]; + pkt[wantreplypos] = 1; + ssh_send_packet_from_downstream + (cs->parent->ssh, cs->id, type, pkt, pktlen, + old_wantreply ? NULL : "upstream added want_reply flag"); + fwd = share_add_forwarding(cs, host, port); + ssh_sharing_queue_global_request(cs->parent->ssh, cs); + + if (fwd) { + globreq = snew(struct share_globreq); + globreq->next = NULL; + if (cs->globreq_tail) + cs->globreq_tail->next = globreq; + else + cs->globreq_head = globreq; + globreq->fwd = fwd; + globreq->want_reply = orig_wantreply; + globreq->type = GLOBREQ_TCPIP_FORWARD; + } + } + + sfree(host); + } else if (!strcmp(request_name, "cancel-tcpip-forward")) { + int wantreplypos, orig_wantreply, port; + char *host; + struct share_forwarding *fwd; + + sfree(request_name); + + /* + * Pick the packet apart to find the want_reply field and + * the host/port we're going to ask to listen on. + */ + wantreplypos = getstring_size(pkt, pktlen); + if (wantreplypos < 0 || wantreplypos >= pktlen) { + err = dupprintf("Truncated GLOBAL_REQUEST packet"); + goto confused; + } + orig_wantreply = pkt[wantreplypos]; + port = getstring_size(pkt + (wantreplypos + 1), + pktlen - (wantreplypos + 1)); + port += (wantreplypos + 1); + if (port < 0 || port > pktlen - 4) { + err = dupprintf("Truncated GLOBAL_REQUEST packet"); + goto confused; + } + host = getstring(pkt + (wantreplypos + 1), + pktlen - (wantreplypos + 1)); + assert(host != NULL); + port = GET_32BIT(pkt + port); + + /* + * Look up the existing forwarding with these details. + */ + fwd = share_find_forwarding(cs, host, port); + if (!fwd) { + if (orig_wantreply) { + send_packet_to_downstream(cs, SSH2_MSG_REQUEST_FAILURE, + "", 0, NULL); + } + } else { + /* + * Pass the cancel request on to the SSH server, but + * set want_reply even if it wasn't originally set, so + * that _we_ know whether the forwarding has been + * deleted even if downstream doesn't want to know. + */ + int old_wantreply = pkt[wantreplypos]; + pkt[wantreplypos] = 1; + ssh_send_packet_from_downstream + (cs->parent->ssh, cs->id, type, pkt, pktlen, + old_wantreply ? NULL : "upstream added want_reply flag"); + ssh_sharing_queue_global_request(cs->parent->ssh, cs); + } + + sfree(host); + } else { + /* + * Request we don't understand. Manufacture a failure + * message if an answer was required. + */ + int wantreplypos; + + sfree(request_name); + + wantreplypos = getstring_size(pkt, pktlen); + if (wantreplypos < 0 || wantreplypos >= pktlen) { + err = dupprintf("Truncated GLOBAL_REQUEST packet"); + goto confused; + } + if (pkt[wantreplypos]) + send_packet_to_downstream(cs, SSH2_MSG_REQUEST_FAILURE, + "", 0, NULL); + } + break; + + case SSH2_MSG_CHANNEL_OPEN: + /* Sender channel id comes after the channel type string */ + id_pos = getstring_size(pkt, pktlen); + if (id_pos < 0 || id_pos > pktlen - 12) { + err = dupprintf("Truncated CHANNEL_OPEN packet"); + goto confused; + } + + old_id = GET_32BIT(pkt + id_pos); + new_id = ssh_alloc_sharing_channel(cs->parent->ssh, cs); + share_add_channel(cs, old_id, new_id, 0, UNACKNOWLEDGED, + GET_32BIT(pkt + id_pos + 8)); + PUT_32BIT(pkt + id_pos, new_id); + ssh_send_packet_from_downstream(cs->parent->ssh, cs->id, + type, pkt, pktlen, NULL); + break; + + case SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: + if (pktlen < 16) { + err = dupprintf("Truncated CHANNEL_OPEN_CONFIRMATION packet"); + goto confused; + } + + id_pos = 4; /* sender channel id is 2nd uint32 field in packet */ + old_id = GET_32BIT(pkt + id_pos); + + server_id = GET_32BIT(pkt); + /* This server id may refer to either a halfchannel or an xchannel. */ + hc = NULL, xc = NULL; /* placate optimiser */ + if ((hc = share_find_halfchannel(cs, server_id)) != NULL) { + new_id = ssh_alloc_sharing_channel(cs->parent->ssh, cs); + } else if ((xc = share_find_xchannel_by_server(cs, server_id)) + != NULL) { + new_id = xc->upstream_id; + } else { + err = dupprintf("CHANNEL_OPEN_CONFIRMATION packet cited unknown channel %u", (unsigned)server_id); + goto confused; + } + + PUT_32BIT(pkt + id_pos, new_id); + + chan = share_add_channel(cs, old_id, new_id, server_id, OPEN, + GET_32BIT(pkt + 12)); + + if (hc) { + ssh_send_packet_from_downstream(cs->parent->ssh, cs->id, + type, pkt, pktlen, NULL); + share_remove_halfchannel(cs, hc); + } else if (xc) { + unsigned downstream_window = GET_32BIT(pkt + 8); + if (downstream_window < 256) { + err = dupprintf("Initial window size for x11 channel must be at least 256 (got %u)", downstream_window); + goto confused; + } + share_xchannel_confirmation(cs, xc, chan, downstream_window); + share_remove_xchannel(cs, xc); + } + + break; + + case SSH2_MSG_CHANNEL_OPEN_FAILURE: + if (pktlen < 4) { + err = dupprintf("Truncated CHANNEL_OPEN_FAILURE packet"); + goto confused; + } + + server_id = GET_32BIT(pkt); + /* This server id may refer to either a halfchannel or an xchannel. */ + if ((hc = share_find_halfchannel(cs, server_id)) != NULL) { + ssh_send_packet_from_downstream(cs->parent->ssh, cs->id, + type, pkt, pktlen, NULL); + share_remove_halfchannel(cs, hc); + } else if ((xc = share_find_xchannel_by_server(cs, server_id)) + != NULL) { + share_xchannel_failure(cs, xc); + } else { + err = dupprintf("CHANNEL_OPEN_FAILURE packet cited unknown channel %u", (unsigned)server_id); + goto confused; + } + + break; + + case SSH2_MSG_CHANNEL_WINDOW_ADJUST: + case SSH2_MSG_CHANNEL_DATA: + case SSH2_MSG_CHANNEL_EXTENDED_DATA: + case SSH2_MSG_CHANNEL_EOF: + case SSH2_MSG_CHANNEL_CLOSE: + case SSH2_MSG_CHANNEL_REQUEST: + case SSH2_MSG_CHANNEL_SUCCESS: + case SSH2_MSG_CHANNEL_FAILURE: + case SSH2_MSG_IGNORE: + case SSH2_MSG_DEBUG: + if (type == SSH2_MSG_CHANNEL_REQUEST && + (request_name = getstring(pkt + 4, pktlen - 4)) != NULL) { + /* + * Agent forwarding requests from downstream are treated + * specially. Because OpenSSHD doesn't let us enable agent + * forwarding independently per session channel, and in + * particular because the OpenSSH-defined agent forwarding + * protocol does not mark agent-channel requests with the + * id of the session channel they originate from, the only + * way we can implement agent forwarding in a + * connection-shared PuTTY is to forward the _upstream_ + * agent. Hence, we unilaterally deny agent forwarding + * requests from downstreams if we aren't prepared to + * forward an agent ourselves. + * + * (If we are, then we dutifully pass agent forwarding + * requests upstream. OpenSSHD has the curious behaviour + * that all but the first such request will be rejected, + * but all session channels opened after the first request + * get agent forwarding enabled whether they ask for it or + * not; but that's not our concern, since other SSH + * servers supporting the same piece of protocol might in + * principle at least manage to enable agent forwarding on + * precisely the channels that requested it, even if the + * subsequent CHANNEL_OPENs still can't be associated with + * a parent session channel.) + */ + if (!strcmp(request_name, "auth-agent-req@openssh.com") && + !ssh_agent_forwarding_permitted(cs->parent->ssh)) { + unsigned server_id = GET_32BIT(pkt); + unsigned char recipient_id[4]; + chan = share_find_channel_by_server(cs, server_id); + if (chan) { + PUT_32BIT(recipient_id, chan->downstream_id); + send_packet_to_downstream(cs, SSH2_MSG_CHANNEL_FAILURE, + recipient_id, 4, NULL); + } else { + char *buf = dupprintf("Agent forwarding request for " + "unrecognised channel %u", server_id); + share_disconnect(cs, buf); + sfree(buf); + return; + } + break; + } + + /* + * Another thing we treat specially is X11 forwarding + * requests. For these, we have to make up another set of + * X11 auth data, and enter it into our SSH connection's + * list of possible X11 authorisation credentials so that + * when we see an X11 channel open request we can know + * whether it's one to handle locally or one to pass on to + * a downstream, and if the latter, which one. + */ + if (!strcmp(request_name, "x11-req")) { + unsigned server_id = GET_32BIT(pkt); + int want_reply, single_connection, screen; + char *auth_proto_str, *auth_data; + int auth_proto, protolen, datalen; + int pos; + + chan = share_find_channel_by_server(cs, server_id); + if (!chan) { + char *buf = dupprintf("X11 forwarding request for " + "unrecognised channel %u", server_id); + share_disconnect(cs, buf); + sfree(buf); + return; + } + + /* + * Pick apart the whole message to find the downstream + * auth details. + */ + /* we have already seen: 4 bytes channel id, 4+7 request name */ + if (pktlen < 17) { + err = dupprintf("Truncated CHANNEL_REQUEST(\"x11\") packet"); + goto confused; + } + want_reply = pkt[15] != 0; + single_connection = pkt[16] != 0; + auth_proto_str = getstring(pkt+17, pktlen-17); + pos = 17 + getstring_size(pkt+17, pktlen-17); + auth_data = getstring(pkt+pos, pktlen-pos); + pos += getstring_size(pkt+pos, pktlen-pos); + if (pktlen < pos+4) { + err = dupprintf("Truncated CHANNEL_REQUEST(\"x11\") packet"); + goto confused; + } + screen = GET_32BIT(pkt+pos); + + auth_proto = x11_identify_auth_proto(auth_proto_str); + if (auth_proto < 0) { + /* Reject due to not understanding downstream's + * requested authorisation method. */ + unsigned char recipient_id[4]; + PUT_32BIT(recipient_id, chan->downstream_id); + send_packet_to_downstream(cs, SSH2_MSG_CHANNEL_FAILURE, + recipient_id, 4, NULL); + } + + chan->x11_auth_proto = auth_proto; + chan->x11_auth_data = x11_dehexify(auth_data, + &chan->x11_auth_datalen); + chan->x11_auth_upstream = + ssh_sharing_add_x11_display(cs->parent->ssh, auth_proto, + cs, chan); + chan->x11_one_shot = single_connection; + + /* + * Now construct a replacement X forwarding request, + * containing our own auth data, and send that to the + * server. + */ + protolen = strlen(chan->x11_auth_upstream->protoname); + datalen = strlen(chan->x11_auth_upstream->datastring); + pktlen = 29+protolen+datalen; + pkt = snewn(pktlen, unsigned char); + PUT_32BIT(pkt, server_id); + PUT_32BIT(pkt+4, 7); /* strlen("x11-req") */ + memcpy(pkt+8, "x11-req", 7); + pkt[15] = want_reply; + pkt[16] = single_connection; + PUT_32BIT(pkt+17, protolen); + memcpy(pkt+21, chan->x11_auth_upstream->protoname, protolen); + PUT_32BIT(pkt+21+protolen, datalen); + memcpy(pkt+25+protolen, chan->x11_auth_upstream->datastring, + datalen); + PUT_32BIT(pkt+25+protolen+datalen, screen); + ssh_send_packet_from_downstream(cs->parent->ssh, cs->id, + SSH2_MSG_CHANNEL_REQUEST, + pkt, pktlen, NULL); + sfree(pkt); + + break; + } + } + + ssh_send_packet_from_downstream(cs->parent->ssh, cs->id, + type, pkt, pktlen, NULL); + if (type == SSH2_MSG_CHANNEL_CLOSE && pktlen >= 4) { + server_id = GET_32BIT(pkt); + chan = share_find_channel_by_server(cs, server_id); + if (chan) { + if (chan->state == RCVD_CLOSE) { + ssh_delete_sharing_channel(cs->parent->ssh, + chan->upstream_id); + share_remove_channel(cs, chan); + } else { + chan->state = SENT_CLOSE; + } + } + } + break; + + default: + err = dupprintf("Unexpected packet type %d\n", type); + goto confused; + + /* + * Any other packet type is unexpected. In particular, we + * never pass GLOBAL_REQUESTs downstream, so we never expect + * to see SSH2_MSG_REQUEST_{SUCCESS,FAILURE}. + */ + confused: + assert(err != NULL); + share_disconnect(cs, err); + sfree(err); + break; + } +} + +/* + * Coroutine macros similar to, but simplified from, those in ssh.c. + */ +#define crBegin(v) { int *crLine = &v; switch(v) { case 0:; +#define crFinish(z) } *crLine = 0; return (z); } +#define crGetChar(c) do \ + { \ + while (len == 0) { \ + *crLine =__LINE__; return 1; case __LINE__:; \ + } \ + len--; \ + (c) = (unsigned char)*data++; \ + } while (0) + +static int share_receive(Plug plug, int urgent, char *data, int len) +{ + struct ssh_sharing_connstate *cs = (struct ssh_sharing_connstate *)plug; + static const char expected_verstring_prefix[] = + "SSHCONNECTION@putty.projects.tartarus.org-2.0-"; + unsigned char c; + + crBegin(cs->crLine); + + /* + * First read the version string from downstream. + */ + cs->recvlen = 0; + while (1) { + crGetChar(c); + if (c == '\012') + break; + if (cs->recvlen > sizeof(cs->recvbuf)) { + char *buf = dupprintf("Version string far too long\n"); + share_disconnect(cs, buf); + sfree(buf); + goto dead; + } + cs->recvbuf[cs->recvlen++] = c; + } + + /* + * Now parse the version string to make sure it's at least vaguely + * sensible, and log it. + */ + if (cs->recvlen < sizeof(expected_verstring_prefix)-1 || + memcmp(cs->recvbuf, expected_verstring_prefix, + sizeof(expected_verstring_prefix) - 1)) { + char *buf = dupprintf("Version string did not have expected prefix\n"); + share_disconnect(cs, buf); + sfree(buf); + goto dead; + } + if (cs->recvlen > 0 && cs->recvbuf[cs->recvlen-1] == '\015') + cs->recvlen--; /* trim off \r before \n */ + ssh_sharing_logf(cs->parent->ssh, cs->id, + "Downstream version string: %.*s", + cs->recvlen, cs->recvbuf); + + /* + * Loop round reading packets. + */ + while (1) { + cs->recvlen = 0; + while (cs->recvlen < 4) { + crGetChar(c); + cs->recvbuf[cs->recvlen++] = c; + } + cs->curr_packetlen = toint(GET_32BIT(cs->recvbuf) + 4); + if (cs->curr_packetlen < 5 || + cs->curr_packetlen > sizeof(cs->recvbuf)) { + char *buf = dupprintf("Bad packet length %u\n", + (unsigned)cs->curr_packetlen); + share_disconnect(cs, buf); + sfree(buf); + goto dead; + } + while (cs->recvlen < cs->curr_packetlen) { + crGetChar(c); + cs->recvbuf[cs->recvlen++] = c; + } + + share_got_pkt_from_downstream(cs, cs->recvbuf[4], + cs->recvbuf + 5, cs->recvlen - 5); + } + + dead:; + crFinish(1); +} + +static void share_sent(Plug plug, int bufsize) +{ + /* struct ssh_sharing_connstate *cs = (struct ssh_sharing_connstate *)plug; */ + + /* + * We do nothing here, because we expect that there won't be a + * need to throttle and unthrottle the connection to a downstream. + * It should automatically throttle itself: if the SSH server + * sends huge amounts of data on all channels then it'll run out + * of window until our downstream sends it back some + * WINDOW_ADJUSTs. + */ +} + +static int share_listen_closing(Plug plug, const char *error_msg, + int error_code, int calling_back) +{ + struct ssh_sharing_state *sharestate = (struct ssh_sharing_state *)plug; + if (error_msg) + ssh_sharing_logf(sharestate->ssh, 0, + "listening socket: %s", error_msg); + sk_close(sharestate->listensock); + return 1; +} + +static void share_send_verstring(struct ssh_sharing_connstate *cs) +{ + char *fullstring = dupcat("SSHCONNECTION@putty.projects.tartarus.org-2.0-", + cs->parent->server_verstring, "\015\012", NULL); + sk_write(cs->sock, fullstring, strlen(fullstring)); + sfree(fullstring); + + cs->sent_verstring = TRUE; +} + +int share_ndownstreams(void *state) +{ + struct ssh_sharing_state *sharestate = (struct ssh_sharing_state *)state; + return count234(sharestate->connections); +} + +void share_activate(void *state, const char *server_verstring) +{ + /* + * Indication from ssh.c that we are now ready to begin serving + * any downstreams that have already connected to us. + */ + struct ssh_sharing_state *sharestate = (struct ssh_sharing_state *)state; + struct ssh_sharing_connstate *cs; + int i; + + /* + * Trim the server's version string down to just the software + * version component, removing "SSH-2.0-" or whatever at the + * front. + */ + for (i = 0; i < 2; i++) { + server_verstring += strcspn(server_verstring, "-"); + if (*server_verstring) + server_verstring++; + } + + sharestate->server_verstring = dupstr(server_verstring); + + for (i = 0; (cs = (struct ssh_sharing_connstate *) + index234(sharestate->connections, i)) != NULL; i++) { + assert(!cs->sent_verstring); + share_send_verstring(cs); + } +} + +static int share_listen_accepting(Plug plug, + accept_fn_t constructor, accept_ctx_t ctx) +{ + static const struct plug_function_table connection_fn_table = { + NULL, /* no log function, because that's for outgoing connections */ + share_closing, + share_receive, + share_sent, + NULL /* no accepting function, because we've already done it */ + }; + struct ssh_sharing_state *sharestate = (struct ssh_sharing_state *)plug; + struct ssh_sharing_connstate *cs; + const char *err; + + /* + * A new downstream has connected to us. + */ + cs = snew(struct ssh_sharing_connstate); + cs->fn = &connection_fn_table; + cs->parent = sharestate; + + if ((cs->id = share_find_unused_id(sharestate, sharestate->nextid)) == 0 && + (cs->id = share_find_unused_id(sharestate, 1)) == 0) { + sfree(cs); + return 1; + } + sharestate->nextid = cs->id + 1; + if (sharestate->nextid == 0) + sharestate->nextid++; /* only happens in VERY long-running upstreams */ + + cs->sock = constructor(ctx, (Plug) cs); + if ((err = sk_socket_error(cs->sock)) != NULL) { + sfree(cs); + return err != NULL; + } + + sk_set_frozen(cs->sock, 0); + + add234(cs->parent->connections, cs); + + cs->sent_verstring = FALSE; + if (sharestate->server_verstring) + share_send_verstring(cs); + + cs->got_verstring = FALSE; + cs->recvlen = 0; + cs->crLine = 0; + cs->halfchannels = newtree234(share_halfchannel_cmp); + cs->channels_by_us = newtree234(share_channel_us_cmp); + cs->channels_by_server = newtree234(share_channel_server_cmp); + cs->xchannels_by_us = newtree234(share_xchannel_us_cmp); + cs->xchannels_by_server = newtree234(share_xchannel_server_cmp); + cs->forwardings = newtree234(share_forwarding_cmp); + cs->globreq_head = cs->globreq_tail = NULL; + + ssh_sharing_downstream_connected(sharestate->ssh, cs->id); + + return 0; +} + +/* Per-application overrides for what roles we can take (e.g. pscp + * will never be an upstream) */ +extern const int share_can_be_downstream; +extern const int share_can_be_upstream; + +/* + * Init function for connection sharing. We either open a listening + * socket and become an upstream, or connect to an existing one and + * become a downstream, or do neither. We are responsible for deciding + * which of these to do (including checking the Conf to see if + * connection sharing is even enabled in the first place). If we + * become a downstream, we return the Socket with which we connected + * to the upstream; otherwise (whether or not we have established an + * upstream) we return NULL. + */ +Socket ssh_connection_sharing_init(const char *host, int port, + Conf *conf, Ssh ssh, void **state) +{ + static const struct plug_function_table listen_fn_table = { + NULL, /* no log function, because that's for outgoing connections */ + share_listen_closing, + NULL, /* no receive function on a listening socket */ + NULL, /* no sent function on a listening socket */ + share_listen_accepting + }; + + int result, can_upstream, can_downstream; + char *logtext, *ds_err, *us_err; + char *sockname; + Socket sock; + struct ssh_sharing_state *sharestate; + + if (!conf_get_int(conf, CONF_ssh_connection_sharing)) + return NULL; /* do not share anything */ + can_upstream = share_can_be_upstream && + conf_get_int(conf, CONF_ssh_connection_sharing_upstream); + can_downstream = share_can_be_downstream && + conf_get_int(conf, CONF_ssh_connection_sharing_downstream); + if (!can_upstream && !can_downstream) + return NULL; + + /* + * Decide on the string used to identify the connection point + * between upstream and downstream (be it a Windows named pipe or + * a Unix-domain socket or whatever else). + * + * I wondered about making this a SHA hash of all sorts of pieces + * of the PuTTY configuration - essentially everything PuTTY uses + * to know where and how to make a connection, including all the + * proxy details (or rather, all the _relevant_ ones - only + * including settings that other settings didn't prevent from + * having any effect), plus the username. However, I think it's + * better to keep it really simple: the connection point + * identifier is derived from the hostname and port used to index + * the host-key cache (not necessarily where we _physically_ + * connected to, in cases involving proxies or CONF_loghost), plus + * the username if one is specified. + */ + { + char *username = get_remote_username(conf); + + if (port == 22) { + if (username) + sockname = dupprintf("%s@%s", username, host); + else + sockname = dupprintf("%s", host); + } else { + if (username) + sockname = dupprintf("%s@%s:%d", username, host, port); + else + sockname = dupprintf("%s:%d", host, port); + } + + sfree(username); + + /* + * The platform-specific code may transform this further in + * order to conform to local namespace conventions (e.g. not + * using slashes in filenames), but that's its job and not + * ours. + */ + } + + /* + * Create a data structure for the listening plug if we turn out + * to be an upstream. + */ + sharestate = snew(struct ssh_sharing_state); + sharestate->fn = &listen_fn_table; + sharestate->listensock = NULL; + + /* + * Now hand off to a per-platform routine that either connects to + * an existing upstream (using 'ssh' as the plug), establishes our + * own upstream (using 'sharestate' as the plug), or forks off a + * separate upstream and then connects to that. It will return a + * code telling us which kind of socket it put in 'sock'. + */ + sock = NULL; + logtext = ds_err = us_err = NULL; + result = platform_ssh_share(sockname, conf, (Plug)ssh, + (Plug)sharestate, &sock, &logtext, &ds_err, + &us_err, can_upstream, can_downstream); + ssh_connshare_log(ssh, result, logtext, ds_err, us_err); + sfree(logtext); + sfree(ds_err); + sfree(us_err); + switch (result) { + case SHARE_NONE: + /* + * We aren't sharing our connection at all (e.g. something + * went wrong setting the socket up). Free the upstream + * structure and return NULL. + */ + assert(sock == NULL); + *state = NULL; + sfree(sharestate); + sfree(sockname); + return NULL; + + case SHARE_DOWNSTREAM: + /* + * We are downstream, so free sharestate which it turns out we + * don't need after all, and return the downstream socket as a + * replacement for an ordinary SSH connection. + */ + *state = NULL; + sfree(sharestate); + sfree(sockname); + return sock; + + case SHARE_UPSTREAM: + /* + * We are upstream. Set up sharestate properly and pass a copy + * to the caller; return NULL, to tell ssh.c that it has to + * make an ordinary connection after all. + */ + *state = sharestate; + sharestate->listensock = sock; + sharestate->connections = newtree234(share_connstate_cmp); + sharestate->ssh = ssh; + sharestate->server_verstring = NULL; + sharestate->sockname = dupstr(sockname); + sharestate->nextid = 1; + return NULL; + } + + return NULL; +} diff --git a/unix/uxnet.c b/unix/uxnet.c index 3ceb9858..868f9d0a 100644 --- a/unix/uxnet.c +++ b/unix/uxnet.c @@ -337,6 +337,15 @@ void sk_getaddr(SockAddr addr, char *buf, int buflen) } } +int sk_addr_needs_port(SockAddr addr) +{ + if (addr->superfamily == UNRESOLVED || addr->superfamily == UNIX) { + return FALSE; + } else { + return TRUE; + } +} + int sk_hostname_is_local(const char *name) { return !strcmp(name, "localhost") || diff --git a/unix/uxplink.c b/unix/uxplink.c index 87efe78a..81356d36 100644 --- a/unix/uxplink.c +++ b/unix/uxplink.c @@ -591,6 +591,9 @@ static void version(void) void frontend_net_error_pending(void) {} +const int share_can_be_downstream = TRUE; +const int share_can_be_upstream = TRUE; + int main(int argc, char **argv) { int sending; diff --git a/unix/uxputty.c b/unix/uxputty.c index fbaf029d..1e3604ba 100644 --- a/unix/uxputty.c +++ b/unix/uxputty.c @@ -122,6 +122,9 @@ char *platform_get_x_display(void) { return dupstr(display); } +const int share_can_be_downstream = TRUE; +const int share_can_be_upstream = TRUE; + int main(int argc, char **argv) { extern int pt_main(int argc, char **argv); diff --git a/unix/uxshare.c b/unix/uxshare.c new file mode 100644 index 00000000..89603726 --- /dev/null +++ b/unix/uxshare.c @@ -0,0 +1,228 @@ +/* + * Unix implementation of SSH connection-sharing IPC setup. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#define DEFINE_PLUG_METHOD_MACROS +#include "tree234.h" +#include "putty.h" +#include "network.h" +#include "proxy.h" +#include "ssh.h" + +#define CONNSHARE_SOCKETDIR_PREFIX "/tmp/putty-connshare" + +/* + * Functions provided by uxnet.c to help connection sharing. + */ +SockAddr unix_sock_addr(const char *path); +Socket new_unix_listener(SockAddr listenaddr, Plug plug); + +static char *make_dirname(const char *name, char **parent_out) +{ + char *username, *dirname, *parent; + + username = get_username(); + parent = dupprintf("%s.%s", CONNSHARE_SOCKETDIR_PREFIX, username); + sfree(username); + assert(*parent == '/'); + + dirname = dupprintf("%s/%s", parent, name); + + if (parent_out) + *parent_out = parent; + else + sfree(parent); + + return dirname; +} + +static char *make_dir_and_check_ours(const char *dirname) +{ + struct stat st; + + /* + * Create the directory. We might have created it before, so + * EEXIST is an OK error; but anything else is doom. + */ + if (mkdir(dirname, 0700) < 0 && errno != EEXIST) + return dupprintf("%s: mkdir: %s", dirname, strerror(errno)); + + /* + * Now check that that directory is _owned by us_ and not writable + * by anybody else. This protects us against somebody else + * previously having created the directory in a way that's + * writable to us, and thus manipulating us into creating the + * actual socket in a directory they can see so that they can + * connect to it and use our authenticated SSH sessions. + */ + if (stat(dirname, &st) < 0) + return dupprintf("%s: stat: %s", dirname, strerror(errno)); + if (st.st_uid != getuid()) + return dupprintf("%s: directory owned by uid %d, not by us", + dirname, st.st_uid); + if ((st.st_mode & 077) != 0) + return dupprintf("%s: directory has overgenerous permissions %03o" + " (expected 700)", dirname, st.st_mode & 0777); + + return NULL; +} + +int platform_ssh_share(const char *pi_name, Conf *conf, + Plug downplug, Plug upplug, Socket *sock, + char **logtext, char **ds_err, char **us_err, + int can_upstream, int can_downstream) +{ + char *name, *parentdirname, *dirname, *lockname, *sockname, *err; + int lockfd; + Socket retsock; + + /* + * Transform the platform-independent version of the connection + * identifier into something valid for a Unix socket, by escaping + * slashes (and, while we're here, any control characters). + */ + { + const char *p; + char *q; + + name = snewn(1+3*strlen(pi_name), char); + + for (p = pi_name, q = name; *p; p++) { + if (*p == '/' || *p == '%' || + (unsigned char)*p < 0x20 || *p == 0x7f) { + q += sprintf(q, "%%%02x", (unsigned char)*p); + } else { + *q++ = *p; + } + } + *q = '\0'; + } + + /* + * First, make sure our subdirectory exists. We must create two + * levels of directory - the one for this particular connection, + * and the containing one for our username. + */ + dirname = make_dirname(name, &parentdirname); + if ((err = make_dir_and_check_ours(parentdirname)) != NULL) { + *logtext = err; + sfree(dirname); + sfree(parentdirname); + sfree(name); + return SHARE_NONE; + } + sfree(parentdirname); + if ((err = make_dir_and_check_ours(dirname)) != NULL) { + *logtext = err; + sfree(dirname); + sfree(name); + return SHARE_NONE; + } + + /* + * Acquire a lock on a file in that directory. + */ + lockname = dupcat(dirname, "/lock", (char *)NULL); + lockfd = open(lockname, O_CREAT | O_RDWR | O_TRUNC, 0600); + if (lockfd < 0) { + *logtext = dupprintf("%s: open: %s", lockname, strerror(errno)); + sfree(dirname); + sfree(lockname); + sfree(name); + return SHARE_NONE; + } + if (flock(lockfd, LOCK_EX) < 0) { + *logtext = dupprintf("%s: flock(LOCK_EX): %s", + lockname, strerror(errno)); + sfree(dirname); + sfree(lockname); + close(lockfd); + sfree(name); + return SHARE_NONE; + } + + sockname = dupprintf("%s/socket", dirname); + + *logtext = NULL; + + if (can_downstream) { + retsock = new_connection(unix_sock_addr(sockname), + "", 0, 0, 1, 0, 0, downplug, conf); + if (sk_socket_error(retsock) == NULL) { + sfree(*logtext); + *logtext = sockname; + *sock = retsock; + sfree(dirname); + sfree(lockname); + close(lockfd); + sfree(name); + return SHARE_DOWNSTREAM; + } + sfree(*ds_err); + *ds_err = dupprintf("%s: %s", sockname, sk_socket_error(retsock)); + sk_close(retsock); + } + + if (can_upstream) { + retsock = new_unix_listener(unix_sock_addr(sockname), upplug); + if (sk_socket_error(retsock) == NULL) { + sfree(*logtext); + *logtext = sockname; + *sock = retsock; + sfree(dirname); + sfree(lockname); + close(lockfd); + sfree(name); + return SHARE_UPSTREAM; + } + sfree(*us_err); + *us_err = dupprintf("%s: %s", sockname, sk_socket_error(retsock)); + sk_close(retsock); + } + + /* One of the above clauses ought to have happened. */ + assert(*logtext || *ds_err || *us_err); + + sfree(dirname); + sfree(lockname); + sfree(sockname); + close(lockfd); + sfree(name); + return SHARE_NONE; +} + +void platform_ssh_share_cleanup(const char *name) +{ + char *dirname, *filename; + + dirname = make_dirname(name, NULL); + + filename = dupcat(dirname, "/socket", (char *)NULL); + remove(filename); + sfree(filename); + + filename = dupcat(dirname, "/lock", (char *)NULL); + remove(filename); + sfree(filename); + + rmdir(dirname); + + /* + * We deliberately _don't_ clean up the parent directory + * /tmp/putty-connshare., because if we leave it around + * then it reduces the ability for other users to be a nuisance by + * putting their own directory in the way of it. + */ + + sfree(dirname); +} diff --git a/windows/window.c b/windows/window.c index bcf1ab81..bf212b9a 100644 --- a/windows/window.c +++ b/windows/window.c @@ -215,6 +215,9 @@ static UINT wm_mousewheel = WM_MOUSEWHEEL; (((wch) >= 0x180B && (wch) <= 0x180D) || /* MONGOLIAN FREE VARIATION SELECTOR */ \ ((wch) >= 0xFE00 && (wch) <= 0xFE0F)) /* VARIATION SELECTOR 1-16 */ +const int share_can_be_downstream = TRUE; +const int share_can_be_upstream = TRUE; + /* Dummy routine, only required in plink. */ void ldisc_update(void *frontend, int echo, int edit) { diff --git a/windows/winhelp.h b/windows/winhelp.h index ca9c721a..d81af0bc 100644 --- a/windows/winhelp.h +++ b/windows/winhelp.h @@ -99,6 +99,7 @@ #define WINHELP_CTX_ssh_protocol "ssh.protocol:config-ssh-prot" #define WINHELP_CTX_ssh_command "ssh.command:config-command" #define WINHELP_CTX_ssh_compress "ssh.compress:config-ssh-comp" +#define WINHELP_CTX_ssh_share "ssh.auth.gssapi:config-ssh-share" #define WINHELP_CTX_ssh_kexlist "ssh.kex.order:config-ssh-kex-order" #define WINHELP_CTX_ssh_kex_repeat "ssh.kex.repeat:config-ssh-kex-rekey" #define WINHELP_CTX_ssh_auth_bypass "ssh.auth.bypass:config-ssh-noauth" diff --git a/windows/winnet.c b/windows/winnet.c index aca37af7..f4507240 100644 --- a/windows/winnet.c +++ b/windows/winnet.c @@ -81,6 +81,8 @@ struct SockAddr_tag { int refcount; char *error; int resolved; + int namedpipe; /* indicates that this SockAddr is phony, holding a Windows + * named pipe pathname instead of a network address */ #ifndef NO_IPV6 struct addrinfo *ais; /* Addresses IPv6 style. */ #endif @@ -504,6 +506,7 @@ SockAddr sk_namelookup(const char *host, char **canonicalname, #ifndef NO_IPV6 ret->ais = NULL; #endif + ret->namedpipe = FALSE; ret->addresses = NULL; ret->resolved = FALSE; ret->refcount = 1; @@ -610,6 +613,7 @@ SockAddr sk_nonamelookup(const char *host) #ifndef NO_IPV6 ret->ais = NULL; #endif + ret->namedpipe = FALSE; ret->addresses = NULL; ret->naddresses = 0; ret->refcount = 1; @@ -618,6 +622,23 @@ SockAddr sk_nonamelookup(const char *host) return ret; } +SockAddr sk_namedpipe_addr(const char *pipename) +{ + SockAddr ret = snew(struct SockAddr_tag); + ret->error = NULL; + ret->resolved = FALSE; +#ifndef NO_IPV6 + ret->ais = NULL; +#endif + ret->namedpipe = TRUE; + ret->addresses = NULL; + ret->naddresses = 0; + ret->refcount = 1; + strncpy(ret->hostname, pipename, lenof(ret->hostname)); + ret->hostname[lenof(ret->hostname)-1] = '\0'; + return ret; +} + int sk_nextaddr(SockAddr addr, SockAddrStep *step) { #ifndef NO_IPV6 @@ -671,6 +692,11 @@ void sk_getaddr(SockAddr addr, char *buf, int buflen) } } +int sk_addr_needs_port(SockAddr addr) +{ + return addr->namedpipe ? FALSE : TRUE; +} + int sk_hostname_is_local(const char *name) { return !strcmp(name, "localhost") || diff --git a/windows/winnpc.c b/windows/winnpc.c index 90e2cbda..0e8ac699 100644 --- a/windows/winnpc.c +++ b/windows/winnpc.c @@ -30,15 +30,36 @@ Socket new_named_pipe_client(const char *pipename, Plug plug) assert(strncmp(pipename, "\\\\.\\pipe\\", 9) == 0); assert(strchr(pipename + 9, '\\') == NULL); - pipehandle = CreateFile(pipename, GENERIC_READ | GENERIC_WRITE, 0, NULL, - OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); + while (1) { + pipehandle = CreateFile(pipename, GENERIC_READ | GENERIC_WRITE, + 0, NULL, OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, NULL); - if (pipehandle == INVALID_HANDLE_VALUE) { - err = dupprintf("Unable to open named pipe '%s': %s", - pipename, win_strerror(GetLastError())); - ret = new_error_socket(err, plug); - sfree(err); - return ret; + if (pipehandle != INVALID_HANDLE_VALUE) + break; + + if (GetLastError() != ERROR_PIPE_BUSY) { + err = dupprintf("Unable to open named pipe '%s': %s", + pipename, win_strerror(GetLastError())); + ret = new_error_socket(err, plug); + sfree(err); + return ret; + } + + /* + * If we got ERROR_PIPE_BUSY, wait for the server to + * create a new pipe instance. (Since the server is + * expected to be winnps.c, which will do that immediately + * after a previous connection is accepted, that shouldn't + * take excessively long.) + */ + if (!WaitNamedPipe(pipename, NMPWAIT_USE_DEFAULT_WAIT)) { + err = dupprintf("Error waiting for named pipe '%s': %s", + pipename, win_strerror(GetLastError())); + ret = new_error_socket(err, plug); + sfree(err); + return ret; + } } if ((usersid = get_user_sid()) == NULL) { @@ -49,10 +70,10 @@ Socket new_named_pipe_client(const char *pipename, Plug plug) return ret; } - if (GetSecurityInfo(pipehandle, SE_KERNEL_OBJECT, - OWNER_SECURITY_INFORMATION, - &pipeowner, NULL, NULL, NULL, - &psd) != ERROR_SUCCESS) { + if (p_GetSecurityInfo(pipehandle, SE_KERNEL_OBJECT, + OWNER_SECURITY_INFORMATION, + &pipeowner, NULL, NULL, NULL, + &psd) != ERROR_SUCCESS) { err = dupprintf("Unable to get named pipe security information: %s", win_strerror(GetLastError())); ret = new_error_socket(err, plug); diff --git a/windows/winnps.c b/windows/winnps.c index a0137367..9cc8176f 100644 --- a/windows/winnps.c +++ b/windows/winnps.c @@ -14,7 +14,7 @@ #if !defined NO_SECURITY -#include +#include "winsecur.h" Socket make_handle_socket(HANDLE send_H, HANDLE recv_H, Plug plug, int overlapped); @@ -118,6 +118,12 @@ static Socket named_pipe_accept(accept_ctx_t ctx, Plug plug) return make_handle_socket(conn, conn, plug, TRUE); } +/* + * Dummy SockAddr type which just holds a named pipe address. Only + * used for calling plug_log from named_pipe_accept_loop() here. + */ +SockAddr sk_namedpipe_addr(const char *pipename); + static void named_pipe_accept_loop(Named_Pipe_Server_Socket ps, int got_one_already) { @@ -178,7 +184,7 @@ static void named_pipe_accept_loop(Named_Pipe_Server_Socket ps, errmsg = dupprintf("Error while listening to named pipe: %s", win_strerror(error)); - plug_log(ps->plug, 1, NULL /* FIXME: appropriate kind of sockaddr */, 0, + plug_log(ps->plug, 1, sk_namedpipe_addr(ps->pipename), 0, errmsg, error); sfree(errmsg); break; @@ -209,8 +215,6 @@ Socket new_named_pipe_listener(const char *pipename, Plug plug) }; Named_Pipe_Server_Socket ret; - SID_IDENTIFIER_AUTHORITY nt_auth = SECURITY_NT_AUTHORITY; - EXPLICIT_ACCESS ea[2]; ret = snew(struct Socket_named_pipe_server_tag); ret->fn = &socket_fn_table; @@ -224,49 +228,9 @@ Socket new_named_pipe_listener(const char *pipename, Plug plug) assert(strncmp(pipename, "\\\\.\\pipe\\", 9) == 0); assert(strchr(pipename + 9, '\\') == NULL); - if (!AllocateAndInitializeSid(&nt_auth, 1, SECURITY_NETWORK_RID, - 0, 0, 0, 0, 0, 0, 0, &ret->networksid)) { - ret->error = dupprintf("unable to construct SID for rejecting " - "remote pipe connections: %s", - win_strerror(GetLastError())); - goto cleanup; - } - - memset(ea, 0, sizeof(ea)); - ea[0].grfAccessPermissions = GENERIC_READ | GENERIC_WRITE; - ea[0].grfAccessMode = GRANT_ACCESS; - ea[0].grfInheritance = NO_INHERITANCE; - ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME; - ea[0].Trustee.ptstrName = "CURRENT_USER"; - ea[1].grfAccessPermissions = GENERIC_READ | GENERIC_WRITE; - ea[1].grfAccessMode = REVOKE_ACCESS; - ea[1].grfInheritance = NO_INHERITANCE; - ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; - ea[1].Trustee.ptstrName = (LPTSTR)ret->networksid; - - if (SetEntriesInAcl(2, ea, NULL, &ret->acl) != ERROR_SUCCESS) { - ret->error = dupprintf("unable to construct ACL: %s", - win_strerror(GetLastError())); - goto cleanup; - } - - ret->psd = (PSECURITY_DESCRIPTOR) - LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH); - if (!ret->psd) { - ret->error = dupprintf("unable to allocate security descriptor: %s", - win_strerror(GetLastError())); - goto cleanup; - } - - if (!InitializeSecurityDescriptor(ret->psd,SECURITY_DESCRIPTOR_REVISION)) { - ret->error = dupprintf("unable to initialise security descriptor: %s", - win_strerror(GetLastError())); - goto cleanup; - } - - if (!SetSecurityDescriptorDacl(ret->psd, TRUE, ret->acl, FALSE)) { - ret->error = dupprintf("unable to set DACL in security descriptor: %s", - win_strerror(GetLastError())); + if (!make_private_security_descriptor(GENERIC_READ | GENERIC_WRITE, + &ret->psd, &ret->networksid, + &ret->acl, &ret->error)) { goto cleanup; } diff --git a/windows/winpgnt.c b/windows/winpgnt.c index 7e6a12fa..22b60788 100644 --- a/windows/winpgnt.c +++ b/windows/winpgnt.c @@ -117,12 +117,6 @@ static void unmungestr(char *in, char *out, int outlen) static tree234 *rsakeys, *ssh2keys; static int has_security; -#ifndef NO_SECURITY -DECL_WINDOWS_FUNCTION(extern, DWORD, GetSecurityInfo, - (HANDLE, SE_OBJECT_TYPE, SECURITY_INFORMATION, - PSID *, PSID *, PACL *, PACL *, - PSECURITY_DESCRIPTOR *)); -#endif /* * Forward references diff --git a/windows/winplink.c b/windows/winplink.c index 5849e0d4..ac7069a2 100644 --- a/windows/winplink.c +++ b/windows/winplink.c @@ -288,6 +288,9 @@ void stdouterr_sent(struct handle *h, int new_backlog) } } +const int share_can_be_downstream = TRUE; +const int share_can_be_upstream = TRUE; + int main(int argc, char **argv) { int sending; diff --git a/windows/winsecur.c b/windows/winsecur.c index 9271af5f..05feafd2 100644 --- a/windows/winsecur.c +++ b/windows/winsecur.c @@ -26,7 +26,23 @@ int got_advapi(void) GET_WINDOWS_FUNCTION(advapi, OpenProcessToken) && GET_WINDOWS_FUNCTION(advapi, GetTokenInformation) && GET_WINDOWS_FUNCTION(advapi, InitializeSecurityDescriptor) && - GET_WINDOWS_FUNCTION(advapi, SetSecurityDescriptorOwner); + GET_WINDOWS_FUNCTION(advapi, SetSecurityDescriptorOwner) && + GET_WINDOWS_FUNCTION(advapi, SetEntriesInAclA); + } + return successful; +} + +int got_crypt(void) +{ + static int attempted = FALSE; + static int successful; + static HMODULE crypt; + + if (!attempted) { + attempted = TRUE; + crypt = load_system32_dll("crypt32.dll"); + successful = crypt && + GET_WINDOWS_FUNCTION(crypt, CryptProtectMemory); } return successful; } @@ -83,4 +99,98 @@ PSID get_user_sid(void) return ret; } +int make_private_security_descriptor(DWORD permissions, + PSECURITY_DESCRIPTOR *psd, + PSID *networksid, + PACL *acl, + char **error) +{ + SID_IDENTIFIER_AUTHORITY nt_auth = SECURITY_NT_AUTHORITY; + EXPLICIT_ACCESS ea[3]; + int ret = FALSE; + + *psd = NULL; + *networksid = NULL; + *acl = NULL; + *error = NULL; + + if (!got_advapi()) { + *error = dupprintf("unable to load advapi32.dll"); + goto cleanup; + } + + if (!AllocateAndInitializeSid(&nt_auth, 1, SECURITY_NETWORK_RID, + 0, 0, 0, 0, 0, 0, 0, networksid)) { + *error = dupprintf("unable to construct SID for " + "local same-user access only: %s", + win_strerror(GetLastError())); + goto cleanup; + } + + memset(ea, 0, sizeof(ea)); + ea[0].grfAccessPermissions = permissions; + ea[0].grfAccessMode = REVOKE_ACCESS; + ea[0].grfInheritance = NO_INHERITANCE; + ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME; + ea[0].Trustee.ptstrName = "EVERYONE"; + ea[1].grfAccessPermissions = permissions; + ea[1].grfAccessMode = GRANT_ACCESS; + ea[1].grfInheritance = NO_INHERITANCE; + ea[1].Trustee.TrusteeForm = TRUSTEE_IS_NAME; + ea[1].Trustee.ptstrName = "CURRENT_USER"; + ea[2].grfAccessPermissions = permissions; + ea[2].grfAccessMode = REVOKE_ACCESS; + ea[2].grfInheritance = NO_INHERITANCE; + ea[2].Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea[2].Trustee.ptstrName = (LPTSTR)*networksid; + + if (p_SetEntriesInAclA(2, ea, NULL, acl) != ERROR_SUCCESS || *acl == NULL) { + *error = dupprintf("unable to construct ACL: %s", + win_strerror(GetLastError())); + goto cleanup; + } + + *psd = (PSECURITY_DESCRIPTOR) + LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH); + if (!*psd) { + *error = dupprintf("unable to allocate security descriptor: %s", + win_strerror(GetLastError())); + goto cleanup; + } + + if (!InitializeSecurityDescriptor(*psd, SECURITY_DESCRIPTOR_REVISION)) { + *error = dupprintf("unable to initialise security descriptor: %s", + win_strerror(GetLastError())); + goto cleanup; + } + + if (!SetSecurityDescriptorDacl(*psd, TRUE, *acl, FALSE)) { + *error = dupprintf("unable to set DACL in security descriptor: %s", + win_strerror(GetLastError())); + goto cleanup; + } + + ret = TRUE; + + cleanup: + if (!ret) { + if (*psd) { + LocalFree(*psd); + *psd = NULL; + } + if (*networksid) { + LocalFree(*networksid); + *networksid = NULL; + } + if (*acl) { + LocalFree(*acl); + *acl = NULL; + } + } else { + sfree(*error); + *error = NULL; + } + return ret; +} + #endif /* !defined NO_SECURITY */ diff --git a/windows/winsecur.h b/windows/winsecur.h index 9844afc8..57de5d1d 100644 --- a/windows/winsecur.h +++ b/windows/winsecur.h @@ -12,6 +12,9 @@ #define WINSECUR_GLOBAL extern #endif +/* + * Functions loaded from advapi32.dll. + */ DECL_WINDOWS_FUNCTION(WINSECUR_GLOBAL, BOOL, OpenProcessToken, (HANDLE, DWORD, PHANDLE)); DECL_WINDOWS_FUNCTION(WINSECUR_GLOBAL, BOOL, GetTokenInformation, @@ -25,8 +28,38 @@ DECL_WINDOWS_FUNCTION(WINSECUR_GLOBAL, DWORD, GetSecurityInfo, (HANDLE, SE_OBJECT_TYPE, SECURITY_INFORMATION, PSID *, PSID *, PACL *, PACL *, PSECURITY_DESCRIPTOR *)); - +DECL_WINDOWS_FUNCTION(WINSECUR_GLOBAL, DWORD, SetEntriesInAclA, + (ULONG, PEXPLICIT_ACCESS, PACL, PACL *)); int got_advapi(void); + +/* + * Functions loaded from crypt32.dll. + */ +DECL_WINDOWS_FUNCTION(WINSECUR_GLOBAL, BOOL, CryptProtectMemory, + (LPVOID, DWORD, DWORD)); +int got_crypt(void); + +/* + * Find the SID describing the current user. The return value (if not + * NULL for some error-related reason) is smalloced. + */ PSID get_user_sid(void); +/* + * Construct a PSECURITY_DESCRIPTOR of the type used for named pipe + * servers, i.e. allowing access only to the current user id and also + * only local (i.e. not over SMB) connections. + * + * If this function returns TRUE, then 'psd', 'networksid' and 'acl' + * will all have been filled in with memory allocated using LocalAlloc + * (and hence must be freed later using LocalFree). If it returns + * FALSE, then instead 'error' has been filled with a dynamically + * allocated error message. + */ +int make_private_security_descriptor(DWORD permissions, + PSECURITY_DESCRIPTOR *psd, + PSID *networksid, + PACL *acl, + char **error); + #endif diff --git a/windows/winshare.c b/windows/winshare.c new file mode 100644 index 00000000..ad1cea4d --- /dev/null +++ b/windows/winshare.c @@ -0,0 +1,227 @@ +/* + * Windows implementation of SSH connection-sharing IPC setup. + */ + +#include +#include + +#define DEFINE_PLUG_METHOD_MACROS +#include "tree234.h" +#include "putty.h" +#include "network.h" +#include "proxy.h" +#include "ssh.h" + +#if !defined NO_SECURITY + +#include "winsecur.h" + +#define CONNSHARE_PIPE_PREFIX "\\\\.\\pipe\\putty-connshare" +#define CONNSHARE_MUTEX_PREFIX "Local\\putty-connshare-mutex" + +static char *obfuscate_name(const char *realname) +{ + /* + * Windows's named pipes all live in the same namespace, so one + * user can see what pipes another user has open. This is an + * undesirable privacy leak and in particular permits one user to + * know what username@host another user is SSHing to, so we + * protect that information by using CryptProtectMemory (which + * uses a key built in to each user's account). + */ + char *cryptdata; + int cryptlen; + SHA256_State sha; + unsigned char lenbuf[4]; + unsigned char digest[32]; + char retbuf[65]; + int i; + + cryptlen = strlen(realname) + 1; + cryptlen += CRYPTPROTECTMEMORY_BLOCK_SIZE - 1; + cryptlen /= CRYPTPROTECTMEMORY_BLOCK_SIZE; + cryptlen *= CRYPTPROTECTMEMORY_BLOCK_SIZE; + + cryptdata = snewn(cryptlen, char); + memset(cryptdata, 0, cryptlen); + strcpy(cryptdata, realname); + + /* + * CRYPTPROTECTMEMORY_CROSS_PROCESS causes CryptProtectMemory to + * use the same key in all processes with this user id, meaning + * that the next PuTTY process calling this function with the same + * input will get the same data. + * + * (Contrast with CryptProtectData, which invents a new session + * key every time since its API permits returning more data than + * was input, so calling _that_ and hashing the output would not + * be stable.) + */ + if (!p_CryptProtectMemory(cryptdata, cryptlen, + CRYPTPROTECTMEMORY_CROSS_PROCESS)) { + return NULL; + } + + /* + * We don't want to give away the length of the hostname either, + * so having got it back out of CryptProtectMemory we now hash it. + */ + SHA256_Init(&sha); + PUT_32BIT_MSB_FIRST(lenbuf, cryptlen); + SHA256_Bytes(&sha, lenbuf, 4); + SHA256_Bytes(&sha, cryptdata, cryptlen); + SHA256_Final(&sha, digest); + + sfree(cryptdata); + + /* + * Finally, make printable. + */ + for (i = 0; i < 32; i++) { + sprintf(retbuf + 2*i, "%02x", digest[i]); + /* the last of those will also write the trailing NUL */ + } + + return dupstr(retbuf); +} + +static char *make_name(const char *prefix, const char *name) +{ + char *username, *retname; + + username = get_username(); + retname = dupprintf("%s.%s.%s", prefix, username, name); + sfree(username); + + return retname; +} + +Socket new_named_pipe_client(const char *pipename, Plug plug); +Socket new_named_pipe_listener(const char *pipename, Plug plug); + +int platform_ssh_share(const char *pi_name, Conf *conf, + Plug downplug, Plug upplug, Socket *sock, + char **logtext, char **ds_err, char **us_err, + int can_upstream, int can_downstream) +{ + char *name, *mutexname, *pipename; + HANDLE mutex; + Socket retsock; + PSECURITY_DESCRIPTOR psd; + PACL acl; + PSID networksid; + + if (!got_crypt()) { + *logtext = dupprintf("Unable to load crypt32.dll"); + return SHARE_NONE; + } + + /* + * Transform the platform-independent version of the connection + * identifier into the obfuscated version we'll use for our + * Windows named pipe and mutex. A side effect of doing this is + * that it also eliminates any characters illegal in Windows pipe + * names. + */ + name = obfuscate_name(pi_name); + if (!name) { + *logtext = dupprintf("Unable to call CryptProtectMemory: %s", + win_strerror(GetLastError())); + return SHARE_NONE; + } + + /* + * Make a mutex name out of the connection identifier, and lock it + * while we decide whether to be upstream or downstream. + */ + { + SECURITY_ATTRIBUTES sa; + + mutexname = make_name(CONNSHARE_MUTEX_PREFIX, name); + if (!make_private_security_descriptor(MUTEX_ALL_ACCESS, + &psd, &networksid, + &acl, logtext)) { + sfree(mutexname); + return SHARE_NONE; + } + + memset(&sa, 0, sizeof(sa)); + sa.nLength = sizeof(sa); + sa.lpSecurityDescriptor = psd; + sa.bInheritHandle = FALSE; + + mutex = CreateMutex(&sa, FALSE, mutexname); + + if (!mutex) { + *logtext = dupprintf("CreateMutex(\"%s\") failed: %s", + mutexname, win_strerror(GetLastError())); + sfree(mutexname); + LocalFree(psd); + LocalFree(networksid); + LocalFree(acl); + return SHARE_NONE; + } + + sfree(mutexname); + LocalFree(psd); + LocalFree(networksid); + LocalFree(acl); + + WaitForSingleObject(mutex, INFINITE); + } + + pipename = make_name(CONNSHARE_PIPE_PREFIX, name); + + *logtext = NULL; + + if (can_downstream) { + retsock = new_named_pipe_client(pipename, downplug); + if (sk_socket_error(retsock) == NULL) { + sfree(*logtext); + *logtext = pipename; + *sock = retsock; + sfree(name); + ReleaseMutex(mutex); + CloseHandle(mutex); + return SHARE_DOWNSTREAM; + } + sfree(*ds_err); + *ds_err = dupprintf("%s: %s", pipename, sk_socket_error(retsock)); + sk_close(retsock); + } + + if (can_upstream) { + retsock = new_named_pipe_listener(pipename, upplug); + if (sk_socket_error(retsock) == NULL) { + sfree(*logtext); + *logtext = pipename; + *sock = retsock; + sfree(name); + ReleaseMutex(mutex); + CloseHandle(mutex); + return SHARE_UPSTREAM; + } + sfree(*us_err); + *us_err = dupprintf("%s: %s", pipename, sk_socket_error(retsock)); + sk_close(retsock); + } + + /* One of the above clauses ought to have happened. */ + assert(*logtext || *ds_err || *us_err); + + sfree(pipename); + sfree(name); + ReleaseMutex(mutex); + CloseHandle(mutex); + return SHARE_NONE; +} + +void platform_ssh_share_cleanup(const char *name) +{ +} + +#else /* !defined NO_SECURITY */ + +#include "noshare.c" + +#endif /* !defined NO_SECURITY */ diff --git a/x11fwd.c b/x11fwd.c index 54656f3a..ce572f69 100644 --- a/x11fwd.c +++ b/x11fwd.c @@ -75,6 +75,31 @@ struct X11FakeAuth *x11_invent_fake_auth(tree234 *authtree, int authtype) struct X11FakeAuth *auth = snew(struct X11FakeAuth); int i; + /* + * This function has the job of inventing a set of X11 fake auth + * data, and adding it to 'authtree'. We must preserve the + * property that for any given actual authorisation attempt, _at + * most one_ thing in the tree can possibly match it. + * + * For MIT-MAGIC-COOKIE-1, that's not too difficult: the match + * criterion is simply that the entire cookie is correct, so we + * just have to make sure we don't make up two cookies the same. + * (Vanishingly unlikely, but we check anyway to be sure, and go + * round again inventing a new cookie if add234 tells us the one + * we thought of is already in use.) + * + * For XDM-AUTHORIZATION-1, it's a little more fiddly. The setup + * with XA1 is that half the cookie is used as a DES key with + * which to CBC-encrypt an assortment of stuff. Happily, the stuff + * encrypted _begins_ with the other half of the cookie, and the + * IV is always zero, which means that any valid XA1 authorisation + * attempt for a given cookie must begin with the same cipher + * block, consisting of the DES ECB encryption of the first half + * of the cookie using the second half as a key. So we compute + * that cipher block here and now, and use it as the sorting key + * for distinguishing XA1 entries in the tree. + */ + if (authtype == X11_MIT) { auth->proto = X11_MIT; @@ -118,6 +143,9 @@ struct X11FakeAuth *x11_invent_fake_auth(tree234 *authtree, int authtype) sprintf(auth->datastring + i*2, "%02x", auth->data[i]); + auth->disp = NULL; + auth->share_cs = auth->share_chan = NULL; + return auth; } @@ -342,10 +370,20 @@ static char *x11_verify(unsigned long peer_ip, int peer_port, * record that _might_ match. */ if (!strcmp(proto, x11_authnames[X11_MIT])) { + /* + * Just look up the whole cookie that was presented to us, + * which x11_authcmp will compare against the cookies we + * currently believe in. + */ match_dummy.proto = X11_MIT; match_dummy.datalen = dlen; match_dummy.data = data; } else if (!strcmp(proto, x11_authnames[X11_XDM])) { + /* + * Look up the first cipher block, against the stored first + * cipher blocks for the XDM-AUTHORIZATION-1 cookies we + * currently know. (See comment in x11_invent_fake_auth.) + */ match_dummy.proto = X11_XDM; match_dummy.xa1_firstblock = data; } else { @@ -843,6 +881,7 @@ int x11_send(struct X11Connection *xconn, char *data, int len) xconn->auth_protocol[xconn->auth_plen] = '\0'; /* ASCIZ */ + peer_ip = 0; /* placate optimiser */ if (x11_parse_ip(xconn->peer_addr, &peer_ip)) peer_port = xconn->peer_port; else @@ -857,10 +896,25 @@ int x11_send(struct X11Connection *xconn, char *data, int len) } assert(auth_matched); + /* + * If this auth points to a connection-sharing downstream + * rather than an X display we know how to connect to + * directly, pass it off to the sharing module now. + */ + if (auth_matched->share_cs) { + sshfwd_x11_sharing_handover(xconn->c, auth_matched->share_cs, + auth_matched->share_chan, + xconn->peer_addr, xconn->peer_port, + xconn->firstpkt[0], + protomajor, protominor, data, len); + return 0; + } + /* * Now we know we're going to accept the connection, and what * X display to connect to. Actually connect to it. */ + sshfwd_x11_is_local(xconn->c); xconn->disp = auth_matched->disp; xconn->s = new_connection(sk_addr_dup(xconn->disp->addr), xconn->disp->realhost, xconn->disp->port, @@ -930,6 +984,44 @@ void x11_send_eof(struct X11Connection *xconn) sshfwd_write_eof(xconn->c); } } + +/* + * Utility functions used by connection sharing to convert textual + * representations of an X11 auth protocol name + hex cookie into our + * usual integer protocol id and binary auth data. + */ +int x11_identify_auth_proto(const char *protoname) +{ + int protocol; + + for (protocol = 1; protocol < lenof(x11_authnames); protocol++) + if (!strcmp(protoname, x11_authnames[protocol])) + return protocol; + return -1; +} + +void *x11_dehexify(const char *hex, int *outlen) +{ + int len, i; + unsigned char *ret; + + len = strlen(hex) / 2; + ret = snewn(len, unsigned char); + + for (i = 0; i < len; i++) { + char bytestr[3]; + unsigned val = 0; + bytestr[0] = hex[2*i]; + bytestr[1] = hex[2*i+1]; + bytestr[2] = '\0'; + sscanf(bytestr, "%x", &val); + ret[i] = val; + } + + *outlen = len; + return ret; +} + /* * Construct an X11 greeting packet, including making up the right * authorisation data. @@ -969,7 +1061,8 @@ void *x11_make_greeting(int endian, int protomajor, int protominor, t = time(NULL); PUT_32BIT_MSB_FIRST(realauthdata+14, t); - des_encrypt_xdmauth(auth_data + 9, realauthdata, authdatalen); + des_encrypt_xdmauth((const unsigned char *)auth_data + 9, + realauthdata, authdatalen); } else { authdata = realauthdata; authdatalen = 0;