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
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");
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
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];
/* 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
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()) */
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);
--- /dev/null
+/*
+ * Stub implementation of SSH connection-sharing IPC, for any
+ * platform which can't support it at all.
+ */
+
+#include <stdio.h>
+#include <assert.h>
+#include <errno.h>
+
+#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)
+{
+}
* 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];
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_.)
exit(1);
}
+const int share_can_be_downstream = TRUE;
+const int share_can_be_upstream = FALSE;
+
/*
* Main program. Parse arguments etc.
*/
* 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) \
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
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)
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)
#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);
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
} 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;
};
unsigned sport, dport;
char *shost, *dhost;
char *sportdesc;
+ void *share_ctx;
struct ssh_portfwd *pfrec;
};
* 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);
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;
int v2_session_id_len;
void *kex_ctx;
+ int bare_connection;
+ int attempting_connshare;
+ void *connshare;
+
char *savedhost;
int savedport;
int send_ok;
int ssh1_rdpkt_crstate;
int ssh2_rdpkt_crstate;
+ int ssh2_bare_rdpkt_crstate;
int ssh_gotdata_crstate;
int do_ssh1_connection_crstate;
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;
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,
}
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)
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
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)
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
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;
{
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);
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;
}
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;
}
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.
*/
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;
}
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);
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);
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);
*/
static void ssh_fix_verstring(char *str)
{
- /* Eat "SSH-<protoversion>-". */
- assert(*str == 'S'); str++;
- assert(*str == 'S'); str++;
- assert(*str == 'H'); str++;
- assert(*str == '-'); str++;
+ /* Eat "<protoversion>-". */
while (*str && *str != '-') str++;
assert(*str == '-'); 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;
/*
* 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;
static int do_ssh_init(Ssh ssh, unsigned char c)
{
+ static const char protoname[] = "SSH-";
+
struct do_ssh_init_state {
int crLine;
int vslen;
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')
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);
s->version[s->i++] = c;
} else if (c == '\012')
break;
+ crReturn(1); /* get another char */
}
ssh->agentfwd_enabled = FALSE;
/* 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;
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)
{
/* 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);
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)
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,
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,
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;
+ }
}
/*
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);
}
/*
}
}
+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;
}
pf = snew(struct ssh_rportfwd);
+ pf->share_ctx = NULL;
pf->dhost = dupstr(epf->daddr);
pf->dport = epf->dport;
if (epf->saddr) {
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)
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 {
};
crState(do_ssh2_transport_state);
+ assert(!ssh->bare_connection);
+
crBeginState;
s->cscipher_tobe = s->sccipher_tobe = NULL;
}
}
+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.
*/
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);
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
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,
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);
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);
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 */
* 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;
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)
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);
}
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
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;
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) {
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);
}
}
+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;
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);
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");
}
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) {
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);
}
}
+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.
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.
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
do_ssh2_transport(ssh, "enabling delayed compression", -2, NULL);
}
- /*
- * Now the connection protocol has started, one way or another.
- */
-
ssh->channels = newtree234(ssh_channelcmp);
/*
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
}
/* 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. */
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;
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);
}
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);
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;
ssh->username = NULL;
ssh->sent_console_eof = FALSE;
ssh->got_pty = FALSE;
+ ssh->bare_connection = FALSE;
+ ssh->attempting_connshare = FALSE;
*backend_handle = ssh;
ssh->channels = NULL;
}
+ if (ssh->connshare)
+ sharestate_free(ssh->connshare);
+
if (ssh->rportfwds) {
while ((pf = delpos234(ssh->rportfwds, 0)) != NULL)
free_rportfwd(pf);
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) {
} 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);
}
}
} 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) {
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.
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;
#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.
* 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,
*/
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);
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.
*/
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 <stdlib.h>
+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
--- /dev/null
+/*
+ * 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
+ *
+ * <protocol>-<version>-<softwareversion> [comments] \r\n
+ *
+ * The only difference is that in real SSH-2, <protocol> 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 <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <limits.h>
+
+#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;
+}
}
}
+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") ||
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;
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);
--- /dev/null
+/*
+ * Unix implementation of SSH connection-sharing IPC setup.
+ */
+
+#include <stdio.h>
+#include <assert.h>
+#include <errno.h>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/file.h>
+
+#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.<username>, 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);
+}
(((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)
{
#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"
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
#ifndef NO_IPV6
ret->ais = NULL;
#endif
+ ret->namedpipe = FALSE;
ret->addresses = NULL;
ret->resolved = FALSE;
ret->refcount = 1;
#ifndef NO_IPV6
ret->ais = NULL;
#endif
+ ret->namedpipe = FALSE;
ret->addresses = NULL;
ret->naddresses = 0;
ret->refcount = 1;
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
}
}
+int sk_addr_needs_port(SockAddr addr)
+{
+ return addr->namedpipe ? FALSE : TRUE;
+}
+
int sk_hostname_is_local(const char *name)
{
return !strcmp(name, "localhost") ||
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) {
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);
#if !defined NO_SECURITY
-#include <aclapi.h>
+#include "winsecur.h"
Socket make_handle_socket(HANDLE send_H, HANDLE recv_H, Plug plug,
int overlapped);
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)
{
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;
};
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;
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;
}
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
}
}
+const int share_can_be_downstream = TRUE;
+const int share_can_be_upstream = TRUE;
+
int main(int argc, char **argv)
{
int sending;
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;
}
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 */
#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,
(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
--- /dev/null
+/*
+ * Windows implementation of SSH connection-sharing IPC setup.
+ */
+
+#include <stdio.h>
+#include <assert.h>
+
+#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 */
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;
sprintf(auth->datastring + i*2, "%02x",
auth->data[i]);
+ auth->disp = NULL;
+ auth->share_cs = auth->share_chan = NULL;
+
return auth;
}
* 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 {
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
}
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,
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.
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;