1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-07-01 19:42:48 -05:00

Implement connection sharing between instances of PuTTY.

The basic strategy is described at the top of the new source file
sshshare.c. In very brief: an 'upstream' PuTTY opens a Unix-domain
socket or Windows named pipe, and listens for connections from other
PuTTYs wanting to run sessions on the same server. The protocol spoken
down that socket/pipe is essentially the bare ssh-connection protocol,
using a trivial binary packet protocol with no encryption, and the
upstream has to do some fiddly transformations that I've been
referring to as 'channel-number NAT' to avoid resource clashes between
the sessions it's managing.

This is quite different from OpenSSH's approach of using the Unix-
domain socket as a means of passing file descriptors around; the main
reason for that is that fd-passing is Unix-specific but this system
has to work on Windows too. However, there are additional advantages,
such as making it easy for each downstream PuTTY to run its own
independent set of port and X11 forwardings (though the method for
making the latter work is quite painful).

Sharing is off by default, but configuration is intended to be very
easy in the normal case - just tick one box in the SSH config panel
and everything else happens automatically.

[originally from svn r10083]
This commit is contained in:
Simon Tatham
2013-11-17 14:05:41 +00:00
parent f6f78f8355
commit bb78583ad2
30 changed files with 3915 additions and 216 deletions

View File

@ -215,6 +215,9 @@ static UINT wm_mousewheel = WM_MOUSEWHEEL;
(((wch) >= 0x180B && (wch) <= 0x180D) || /* MONGOLIAN FREE VARIATION SELECTOR */ \
((wch) >= 0xFE00 && (wch) <= 0xFE0F)) /* VARIATION SELECTOR 1-16 */
const int share_can_be_downstream = TRUE;
const int share_can_be_upstream = TRUE;
/* Dummy routine, only required in plink. */
void ldisc_update(void *frontend, int echo, int edit)
{

View File

@ -99,6 +99,7 @@
#define WINHELP_CTX_ssh_protocol "ssh.protocol:config-ssh-prot"
#define WINHELP_CTX_ssh_command "ssh.command:config-command"
#define WINHELP_CTX_ssh_compress "ssh.compress:config-ssh-comp"
#define WINHELP_CTX_ssh_share "ssh.auth.gssapi:config-ssh-share"
#define WINHELP_CTX_ssh_kexlist "ssh.kex.order:config-ssh-kex-order"
#define WINHELP_CTX_ssh_kex_repeat "ssh.kex.repeat:config-ssh-kex-rekey"
#define WINHELP_CTX_ssh_auth_bypass "ssh.auth.bypass:config-ssh-noauth"

View File

@ -81,6 +81,8 @@ struct SockAddr_tag {
int refcount;
char *error;
int resolved;
int namedpipe; /* indicates that this SockAddr is phony, holding a Windows
* named pipe pathname instead of a network address */
#ifndef NO_IPV6
struct addrinfo *ais; /* Addresses IPv6 style. */
#endif
@ -504,6 +506,7 @@ SockAddr sk_namelookup(const char *host, char **canonicalname,
#ifndef NO_IPV6
ret->ais = NULL;
#endif
ret->namedpipe = FALSE;
ret->addresses = NULL;
ret->resolved = FALSE;
ret->refcount = 1;
@ -610,6 +613,7 @@ SockAddr sk_nonamelookup(const char *host)
#ifndef NO_IPV6
ret->ais = NULL;
#endif
ret->namedpipe = FALSE;
ret->addresses = NULL;
ret->naddresses = 0;
ret->refcount = 1;
@ -618,6 +622,23 @@ SockAddr sk_nonamelookup(const char *host)
return ret;
}
SockAddr sk_namedpipe_addr(const char *pipename)
{
SockAddr ret = snew(struct SockAddr_tag);
ret->error = NULL;
ret->resolved = FALSE;
#ifndef NO_IPV6
ret->ais = NULL;
#endif
ret->namedpipe = TRUE;
ret->addresses = NULL;
ret->naddresses = 0;
ret->refcount = 1;
strncpy(ret->hostname, pipename, lenof(ret->hostname));
ret->hostname[lenof(ret->hostname)-1] = '\0';
return ret;
}
int sk_nextaddr(SockAddr addr, SockAddrStep *step)
{
#ifndef NO_IPV6
@ -671,6 +692,11 @@ void sk_getaddr(SockAddr addr, char *buf, int buflen)
}
}
int sk_addr_needs_port(SockAddr addr)
{
return addr->namedpipe ? FALSE : TRUE;
}
int sk_hostname_is_local(const char *name)
{
return !strcmp(name, "localhost") ||

View File

@ -30,15 +30,36 @@ Socket new_named_pipe_client(const char *pipename, Plug plug)
assert(strncmp(pipename, "\\\\.\\pipe\\", 9) == 0);
assert(strchr(pipename + 9, '\\') == NULL);
pipehandle = CreateFile(pipename, GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
while (1) {
pipehandle = CreateFile(pipename, GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, NULL);
if (pipehandle == INVALID_HANDLE_VALUE) {
err = dupprintf("Unable to open named pipe '%s': %s",
pipename, win_strerror(GetLastError()));
ret = new_error_socket(err, plug);
sfree(err);
return ret;
if (pipehandle != INVALID_HANDLE_VALUE)
break;
if (GetLastError() != ERROR_PIPE_BUSY) {
err = dupprintf("Unable to open named pipe '%s': %s",
pipename, win_strerror(GetLastError()));
ret = new_error_socket(err, plug);
sfree(err);
return ret;
}
/*
* If we got ERROR_PIPE_BUSY, wait for the server to
* create a new pipe instance. (Since the server is
* expected to be winnps.c, which will do that immediately
* after a previous connection is accepted, that shouldn't
* take excessively long.)
*/
if (!WaitNamedPipe(pipename, NMPWAIT_USE_DEFAULT_WAIT)) {
err = dupprintf("Error waiting for named pipe '%s': %s",
pipename, win_strerror(GetLastError()));
ret = new_error_socket(err, plug);
sfree(err);
return ret;
}
}
if ((usersid = get_user_sid()) == NULL) {
@ -49,10 +70,10 @@ Socket new_named_pipe_client(const char *pipename, Plug plug)
return ret;
}
if (GetSecurityInfo(pipehandle, SE_KERNEL_OBJECT,
OWNER_SECURITY_INFORMATION,
&pipeowner, NULL, NULL, NULL,
&psd) != ERROR_SUCCESS) {
if (p_GetSecurityInfo(pipehandle, SE_KERNEL_OBJECT,
OWNER_SECURITY_INFORMATION,
&pipeowner, NULL, NULL, NULL,
&psd) != ERROR_SUCCESS) {
err = dupprintf("Unable to get named pipe security information: %s",
win_strerror(GetLastError()));
ret = new_error_socket(err, plug);

View File

@ -14,7 +14,7 @@
#if !defined NO_SECURITY
#include <aclapi.h>
#include "winsecur.h"
Socket make_handle_socket(HANDLE send_H, HANDLE recv_H, Plug plug,
int overlapped);
@ -118,6 +118,12 @@ static Socket named_pipe_accept(accept_ctx_t ctx, Plug plug)
return make_handle_socket(conn, conn, plug, TRUE);
}
/*
* Dummy SockAddr type which just holds a named pipe address. Only
* used for calling plug_log from named_pipe_accept_loop() here.
*/
SockAddr sk_namedpipe_addr(const char *pipename);
static void named_pipe_accept_loop(Named_Pipe_Server_Socket ps,
int got_one_already)
{
@ -178,7 +184,7 @@ static void named_pipe_accept_loop(Named_Pipe_Server_Socket ps,
errmsg = dupprintf("Error while listening to named pipe: %s",
win_strerror(error));
plug_log(ps->plug, 1, NULL /* FIXME: appropriate kind of sockaddr */, 0,
plug_log(ps->plug, 1, sk_namedpipe_addr(ps->pipename), 0,
errmsg, error);
sfree(errmsg);
break;
@ -209,8 +215,6 @@ Socket new_named_pipe_listener(const char *pipename, Plug plug)
};
Named_Pipe_Server_Socket ret;
SID_IDENTIFIER_AUTHORITY nt_auth = SECURITY_NT_AUTHORITY;
EXPLICIT_ACCESS ea[2];
ret = snew(struct Socket_named_pipe_server_tag);
ret->fn = &socket_fn_table;
@ -224,49 +228,9 @@ Socket new_named_pipe_listener(const char *pipename, Plug plug)
assert(strncmp(pipename, "\\\\.\\pipe\\", 9) == 0);
assert(strchr(pipename + 9, '\\') == NULL);
if (!AllocateAndInitializeSid(&nt_auth, 1, SECURITY_NETWORK_RID,
0, 0, 0, 0, 0, 0, 0, &ret->networksid)) {
ret->error = dupprintf("unable to construct SID for rejecting "
"remote pipe connections: %s",
win_strerror(GetLastError()));
goto cleanup;
}
memset(ea, 0, sizeof(ea));
ea[0].grfAccessPermissions = GENERIC_READ | GENERIC_WRITE;
ea[0].grfAccessMode = GRANT_ACCESS;
ea[0].grfInheritance = NO_INHERITANCE;
ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
ea[0].Trustee.ptstrName = "CURRENT_USER";
ea[1].grfAccessPermissions = GENERIC_READ | GENERIC_WRITE;
ea[1].grfAccessMode = REVOKE_ACCESS;
ea[1].grfInheritance = NO_INHERITANCE;
ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[1].Trustee.ptstrName = (LPTSTR)ret->networksid;
if (SetEntriesInAcl(2, ea, NULL, &ret->acl) != ERROR_SUCCESS) {
ret->error = dupprintf("unable to construct ACL: %s",
win_strerror(GetLastError()));
goto cleanup;
}
ret->psd = (PSECURITY_DESCRIPTOR)
LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH);
if (!ret->psd) {
ret->error = dupprintf("unable to allocate security descriptor: %s",
win_strerror(GetLastError()));
goto cleanup;
}
if (!InitializeSecurityDescriptor(ret->psd,SECURITY_DESCRIPTOR_REVISION)) {
ret->error = dupprintf("unable to initialise security descriptor: %s",
win_strerror(GetLastError()));
goto cleanup;
}
if (!SetSecurityDescriptorDacl(ret->psd, TRUE, ret->acl, FALSE)) {
ret->error = dupprintf("unable to set DACL in security descriptor: %s",
win_strerror(GetLastError()));
if (!make_private_security_descriptor(GENERIC_READ | GENERIC_WRITE,
&ret->psd, &ret->networksid,
&ret->acl, &ret->error)) {
goto cleanup;
}

View File

@ -117,12 +117,6 @@ static void unmungestr(char *in, char *out, int outlen)
static tree234 *rsakeys, *ssh2keys;
static int has_security;
#ifndef NO_SECURITY
DECL_WINDOWS_FUNCTION(extern, DWORD, GetSecurityInfo,
(HANDLE, SE_OBJECT_TYPE, SECURITY_INFORMATION,
PSID *, PSID *, PACL *, PACL *,
PSECURITY_DESCRIPTOR *));
#endif
/*
* Forward references

View File

@ -288,6 +288,9 @@ void stdouterr_sent(struct handle *h, int new_backlog)
}
}
const int share_can_be_downstream = TRUE;
const int share_can_be_upstream = TRUE;
int main(int argc, char **argv)
{
int sending;

View File

@ -26,7 +26,23 @@ int got_advapi(void)
GET_WINDOWS_FUNCTION(advapi, OpenProcessToken) &&
GET_WINDOWS_FUNCTION(advapi, GetTokenInformation) &&
GET_WINDOWS_FUNCTION(advapi, InitializeSecurityDescriptor) &&
GET_WINDOWS_FUNCTION(advapi, SetSecurityDescriptorOwner);
GET_WINDOWS_FUNCTION(advapi, SetSecurityDescriptorOwner) &&
GET_WINDOWS_FUNCTION(advapi, SetEntriesInAclA);
}
return successful;
}
int got_crypt(void)
{
static int attempted = FALSE;
static int successful;
static HMODULE crypt;
if (!attempted) {
attempted = TRUE;
crypt = load_system32_dll("crypt32.dll");
successful = crypt &&
GET_WINDOWS_FUNCTION(crypt, CryptProtectMemory);
}
return successful;
}
@ -83,4 +99,98 @@ PSID get_user_sid(void)
return ret;
}
int make_private_security_descriptor(DWORD permissions,
PSECURITY_DESCRIPTOR *psd,
PSID *networksid,
PACL *acl,
char **error)
{
SID_IDENTIFIER_AUTHORITY nt_auth = SECURITY_NT_AUTHORITY;
EXPLICIT_ACCESS ea[3];
int ret = FALSE;
*psd = NULL;
*networksid = NULL;
*acl = NULL;
*error = NULL;
if (!got_advapi()) {
*error = dupprintf("unable to load advapi32.dll");
goto cleanup;
}
if (!AllocateAndInitializeSid(&nt_auth, 1, SECURITY_NETWORK_RID,
0, 0, 0, 0, 0, 0, 0, networksid)) {
*error = dupprintf("unable to construct SID for "
"local same-user access only: %s",
win_strerror(GetLastError()));
goto cleanup;
}
memset(ea, 0, sizeof(ea));
ea[0].grfAccessPermissions = permissions;
ea[0].grfAccessMode = REVOKE_ACCESS;
ea[0].grfInheritance = NO_INHERITANCE;
ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
ea[0].Trustee.ptstrName = "EVERYONE";
ea[1].grfAccessPermissions = permissions;
ea[1].grfAccessMode = GRANT_ACCESS;
ea[1].grfInheritance = NO_INHERITANCE;
ea[1].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
ea[1].Trustee.ptstrName = "CURRENT_USER";
ea[2].grfAccessPermissions = permissions;
ea[2].grfAccessMode = REVOKE_ACCESS;
ea[2].grfInheritance = NO_INHERITANCE;
ea[2].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[2].Trustee.ptstrName = (LPTSTR)*networksid;
if (p_SetEntriesInAclA(2, ea, NULL, acl) != ERROR_SUCCESS || *acl == NULL) {
*error = dupprintf("unable to construct ACL: %s",
win_strerror(GetLastError()));
goto cleanup;
}
*psd = (PSECURITY_DESCRIPTOR)
LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH);
if (!*psd) {
*error = dupprintf("unable to allocate security descriptor: %s",
win_strerror(GetLastError()));
goto cleanup;
}
if (!InitializeSecurityDescriptor(*psd, SECURITY_DESCRIPTOR_REVISION)) {
*error = dupprintf("unable to initialise security descriptor: %s",
win_strerror(GetLastError()));
goto cleanup;
}
if (!SetSecurityDescriptorDacl(*psd, TRUE, *acl, FALSE)) {
*error = dupprintf("unable to set DACL in security descriptor: %s",
win_strerror(GetLastError()));
goto cleanup;
}
ret = TRUE;
cleanup:
if (!ret) {
if (*psd) {
LocalFree(*psd);
*psd = NULL;
}
if (*networksid) {
LocalFree(*networksid);
*networksid = NULL;
}
if (*acl) {
LocalFree(*acl);
*acl = NULL;
}
} else {
sfree(*error);
*error = NULL;
}
return ret;
}
#endif /* !defined NO_SECURITY */

View File

@ -12,6 +12,9 @@
#define WINSECUR_GLOBAL extern
#endif
/*
* Functions loaded from advapi32.dll.
*/
DECL_WINDOWS_FUNCTION(WINSECUR_GLOBAL, BOOL, OpenProcessToken,
(HANDLE, DWORD, PHANDLE));
DECL_WINDOWS_FUNCTION(WINSECUR_GLOBAL, BOOL, GetTokenInformation,
@ -25,8 +28,38 @@ DECL_WINDOWS_FUNCTION(WINSECUR_GLOBAL, DWORD, GetSecurityInfo,
(HANDLE, SE_OBJECT_TYPE, SECURITY_INFORMATION,
PSID *, PSID *, PACL *, PACL *,
PSECURITY_DESCRIPTOR *));
DECL_WINDOWS_FUNCTION(WINSECUR_GLOBAL, DWORD, SetEntriesInAclA,
(ULONG, PEXPLICIT_ACCESS, PACL, PACL *));
int got_advapi(void);
/*
* Functions loaded from crypt32.dll.
*/
DECL_WINDOWS_FUNCTION(WINSECUR_GLOBAL, BOOL, CryptProtectMemory,
(LPVOID, DWORD, DWORD));
int got_crypt(void);
/*
* Find the SID describing the current user. The return value (if not
* NULL for some error-related reason) is smalloced.
*/
PSID get_user_sid(void);
/*
* Construct a PSECURITY_DESCRIPTOR of the type used for named pipe
* servers, i.e. allowing access only to the current user id and also
* only local (i.e. not over SMB) connections.
*
* If this function returns TRUE, then 'psd', 'networksid' and 'acl'
* will all have been filled in with memory allocated using LocalAlloc
* (and hence must be freed later using LocalFree). If it returns
* FALSE, then instead 'error' has been filled with a dynamically
* allocated error message.
*/
int make_private_security_descriptor(DWORD permissions,
PSECURITY_DESCRIPTOR *psd,
PSID *networksid,
PACL *acl,
char **error);
#endif

227
windows/winshare.c Normal file
View File

@ -0,0 +1,227 @@
/*
* 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 */