diff --git a/channels.c b/channels.c index 6caab482710e..ddc3b3084f02 100644 --- a/channels.c +++ b/channels.c @@ -4919,6 +4919,15 @@ channel_send_window_changes(struct ssh *ssh) if (sc->channels[i] == NULL || !sc->channels[i]->client_tty || sc->channels[i]->type != SSH_CHANNEL_OPEN) continue; +#ifdef WINDOWS + /* + * mux-owned sessions are resized via MUX_C_WINSIZE from the + * mux client; w32_ioctl() would report this process's own + * console size for their relay-pipe rfds, which is wrong. + */ + if (sc->channels[i]->ctl_chan != -1) + continue; +#endif if (ioctl(sc->channels[i]->rfd, TIOCGWINSZ, &ws) == -1) continue; channel_request_start(ssh, i, "window-change", 0); diff --git a/clientloop.c b/clientloop.c index f3350a83b1fc..fccc46ed4d94 100644 --- a/clientloop.c +++ b/clientloop.c @@ -2687,7 +2687,7 @@ client_send_env(struct ssh *ssh, int id, const char *name, const char *val) void client_session2_setup(struct ssh *ssh, int id, int want_tty, int want_subsystem, const char *term, struct termios *tiop, int in_fd, struct sshbuf *cmd, - char **env) + char **env, const struct winsize *wsp) { size_t i, j, len; int matched, r; @@ -2706,7 +2706,9 @@ client_session2_setup(struct ssh *ssh, int id, int want_tty, int want_subsystem, struct winsize ws; /* Store window size in the packet. */ - if (ioctl(in_fd, TIOCGWINSZ, &ws) == -1) + if (wsp != NULL) + ws = *wsp; + else if (ioctl(in_fd, TIOCGWINSZ, &ws) == -1) memset(&ws, 0, sizeof(ws)); channel_request_start(ssh, id, "pty-req", 1); diff --git a/clientloop.h b/clientloop.h index 4bc7bcd7c4f2..45a3b1cbf0fd 100644 --- a/clientloop.h +++ b/clientloop.h @@ -38,13 +38,15 @@ #include struct ssh; +struct winsize; /* Client side main loop for the interactive session. */ int client_loop(struct ssh *, int, int, int); int client_x11_get_proto(struct ssh *, const char *, const char *, u_int, u_int, char **, char **); void client_session2_setup(struct ssh *, int, int, int, - const char *, struct termios *, int, struct sshbuf *, char **); + const char *, struct termios *, int, struct sshbuf *, char **, + const struct winsize *); char *client_request_tun_fwd(struct ssh *, int, int, int, channel_open_fn *, void *); void client_stop_mux(void); diff --git a/contrib/win32/openssh/ssh.vcxproj b/contrib/win32/openssh/ssh.vcxproj index 911460869af3..272c5badc9c0 100644 --- a/contrib/win32/openssh/ssh.vcxproj +++ b/contrib/win32/openssh/ssh.vcxproj @@ -515,6 +515,7 @@ + diff --git a/contrib/win32/win32compat/fileio.c b/contrib/win32/win32compat/fileio.c index 2f62fa855478..5078b9c8d25e 100644 --- a/contrib/win32/win32compat/fileio.c +++ b/contrib/win32/win32compat/fileio.c @@ -81,6 +81,7 @@ struct createFile_flags { int syncio_initiate_read(struct w32_io* pio); int syncio_initiate_write(struct w32_io* pio, DWORD num_bytes); int syncio_close(struct w32_io* pio); +static wchar_t *afunix_pipe_name(const char *sun_path); /* maps Win32 error to errno */ int @@ -133,11 +134,11 @@ fileio_connect(struct w32_io* pio, char* name) goto cleanup; } - if ((name_w = utf8_to_utf16(name)) == NULL) { + if ((name_w = afunix_pipe_name(name)) == NULL) { errno = ENOMEM; return -1; } - + do { h = CreateFileW(name_w, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, NULL); @@ -178,6 +179,280 @@ fileio_connect(struct w32_io* pio, char* name) return ret; } +/* + * AF_UNIX stream sockets are emulated over named pipes. + * A path that is not already a pipe name (\\.\pipe\...) is mapped onto + * one deterministically so that server (bind) and client (connect) + * arrive at the same pipe name from the same sun_path. + */ +#define AFUNIX_PIPE_PREFIX L"\\\\.\\pipe\\" +#define AFUNIX_PIPE_PREFIX_LEN 9 +/* pipe name component (after \\.\pipe\) is limited to 256 chars */ +#define AFUNIX_PIPE_NAME_MAX 256 + +/* per-listener state, hangs off pio->internal.context */ +struct afunix_listener_state { + wchar_t *pipe_name; + PSECURITY_DESCRIPTOR sd; /* owner + SYSTEM only */ + BOOL connect_pending; /* overlapped ConnectNamedPipe outstanding */ + BOOL client_connected; /* connection completed, awaiting accept() */ +}; + +static wchar_t * +afunix_pipe_name(const char *sun_path) +{ + wchar_t *path_w = NULL, *ret = NULL, *p; + size_t len; + + if ((path_w = utf8_to_utf16(sun_path)) == NULL) { + errno = ENOMEM; + return NULL; + } + + for (p = path_w; *p; p++) + if (*p == L'/') + *p = L'\\'; + + if (_wcsnicmp(path_w, AFUNIX_PIPE_PREFIX, AFUNIX_PIPE_PREFIX_LEN) == 0) + return path_w; + + /* map filesystem-style path to \\.\pipe\openssh-uds- */ + for (p = path_w; *p; p++) + if (*p == L'\\' || *p == L':') + *p = L'-'; + + if (wcslen(path_w) > AFUNIX_PIPE_NAME_MAX - wcslen(L"openssh-uds-")) { + free(path_w); + errno = ENAMETOOLONG; + return NULL; + } + + len = AFUNIX_PIPE_PREFIX_LEN + wcslen(L"openssh-uds-") + wcslen(path_w) + 1; + if ((ret = malloc(len * sizeof(wchar_t))) == NULL) { + free(path_w); + errno = ENOMEM; + return NULL; + } + swprintf_s(ret, len, L"%s%s%s", AFUNIX_PIPE_PREFIX, L"openssh-uds-", path_w); + free(path_w); + return ret; +} + +/* issue an overlapped ConnectNamedPipe on the current listener instance */ +static int +afunix_listener_arm(struct w32_io* pio) +{ + struct afunix_listener_state* state = (struct afunix_listener_state*)pio->internal.context; + + ResetEvent(pio->read_overlapped.hEvent); + if (ConnectNamedPipe(WINHANDLE(pio), &pio->read_overlapped)) { + state->client_connected = TRUE; + SetEvent(pio->read_overlapped.hEvent); + return 0; + } + switch (GetLastError()) { + case ERROR_IO_PENDING: + state->connect_pending = TRUE; + return 0; + case ERROR_PIPE_CONNECTED: + state->client_connected = TRUE; + SetEvent(pio->read_overlapped.hEvent); + return 0; + default: + errno = errno_from_Win32LastError(); + debug3("afunix listener - ConnectNamedPipe() ERROR:%d, io:%p", GetLastError(), pio); + return -1; + } +} + +static HANDLE +afunix_create_instance(struct afunix_listener_state* state, BOOL first) +{ + SECURITY_ATTRIBUTES sa; + HANDLE h; + DWORD open_mode = PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED; + + if (first) + open_mode |= FILE_FLAG_FIRST_PIPE_INSTANCE; + + memset(&sa, 0, sizeof(sa)); + sa.nLength = sizeof(sa); + sa.lpSecurityDescriptor = state->sd; + sa.bInheritHandle = FALSE; + + h = CreateNamedPipeW(state->pipe_name, open_mode, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_REJECT_REMOTE_CLIENTS | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, 4096, 4096, 0, &sa); + + if (h == INVALID_HANDLE_VALUE) { + DWORD win32_error = GetLastError(); + debug3("afunix - CreateNamedPipe(%ls) ERROR:%d", state->pipe_name, win32_error); + /* pipe already owned by another process */ + if (win32_error == ERROR_ACCESS_DENIED || win32_error == ERROR_PIPE_BUSY) + errno = EADDRINUSE; + else + errno = errno_from_Win32Error(win32_error); + } + return h; +} + +/* bind() on an AF_UNIX socket - creates the first pipe instance */ +int +fileio_afunix_bind(struct w32_io* pio, const char* sun_path) +{ + struct afunix_listener_state* state = NULL; + wchar_t *sid_utf16 = NULL, sddl[SDDL_LENGTH]; + PSID user_sid = NULL; + HANDLE h = INVALID_HANDLE_VALUE; + int ret = -1; + + if (WINHANDLE(pio) != 0 && WINHANDLE(pio) != INVALID_HANDLE_VALUE) { + errno = EINVAL; + return -1; + } + + if ((state = calloc(1, sizeof(*state))) == NULL) { + errno = ENOMEM; + return -1; + } + + if ((state->pipe_name = afunix_pipe_name(sun_path)) == NULL) + goto cleanup; + + /* restrict the control pipe to SYSTEM and the current user */ + if ((user_sid = get_sid(NULL)) == NULL || + ConvertSidToStringSidW(user_sid, &sid_utf16) == FALSE) { + debug3("afunix bind - cannot retrieve current user's SID"); + errno = EOTHER; + goto cleanup; + } + swprintf_s(sddl, SDDL_LENGTH, L"D:P(A;;GA;;;SY)(A;;GA;;;%s)", sid_utf16); + if (ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl, + SDDL_REVISION_1, &state->sd, NULL) == FALSE) { + debug3("afunix bind - cannot convert sddl ERROR:%d", GetLastError()); + errno = EOTHER; + goto cleanup; + } + + if ((h = afunix_create_instance(state, TRUE)) == INVALID_HANDLE_VALUE) + goto cleanup; + + pio->handle = h; + pio->internal.context = state; + pio->internal.state = SOCK_BOUND; /* tag checked by fileio_close */ + ret = 0; + +cleanup: + if (user_sid) + free(user_sid); + if (sid_utf16) + LocalFree(sid_utf16); + if (ret != 0 && state) { + if (state->pipe_name) + free(state->pipe_name); + if (state->sd) + LocalFree(state->sd); + free(state); + } + return ret; +} + +/* listen() on a bound AF_UNIX socket - starts accepting connections */ +int +fileio_afunix_listen(struct w32_io* pio, int backlog) +{ + struct afunix_listener_state* state = (struct afunix_listener_state*)pio->internal.context; + + if (state == NULL || WINHANDLE(pio) == 0 || WINHANDLE(pio) == INVALID_HANDLE_VALUE) { + errno = EINVAL; + return -1; + } + + if ((pio->read_overlapped.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL)) == NULL) { + errno = ENOMEM; + return -1; + } + + if (afunix_listener_arm(pio) != 0) { + CloseHandle(pio->read_overlapped.hEvent); + pio->read_overlapped.hEvent = NULL; + return -1; + } + + pio->internal.state = SOCK_LISTENING; + return 0; +} + +/* select() readiness check for a listening AF_UNIX socket */ +BOOL +fileio_afunix_listener_ready(struct w32_io* pio) +{ + struct afunix_listener_state* state = (struct afunix_listener_state*)pio->internal.context; + DWORD bytes; + + if (state == NULL) + return FALSE; + if (state->client_connected) + return TRUE; + if (!state->connect_pending) + return FALSE; + + if (GetOverlappedResult(WINHANDLE(pio), &pio->read_overlapped, &bytes, FALSE)) { + state->connect_pending = FALSE; + state->client_connected = TRUE; + return TRUE; + } + if (GetLastError() == ERROR_IO_INCOMPLETE) + return FALSE; + + /* + * connection attempt failed (client vanished). Report ready anyway; + * accept() hands out the dead pipe and reads on it return EOF, matching + * socket semantics for a connection that was closed right after accept. + */ + state->connect_pending = FALSE; + state->client_connected = TRUE; + return TRUE; +} + +/* accept() on a listening AF_UNIX socket. Caller ensures a client is connected */ +struct w32_io* +fileio_afunix_accept(struct w32_io* pio) +{ + struct afunix_listener_state* state = (struct afunix_listener_state*)pio->internal.context; + struct w32_io* accepted = NULL; + HANDLE connected, next; + + if (state == NULL || !fileio_afunix_listener_ready(pio)) { + errno = EAGAIN; + return NULL; + } + + if ((accepted = malloc(sizeof(struct w32_io))) == NULL) { + errno = ENOMEM; + return NULL; + } + memset(accepted, 0, sizeof(struct w32_io)); + + connected = WINHANDLE(pio); + state->client_connected = FALSE; + + /* stand up the next instance before handing out the connected one */ + if ((next = afunix_create_instance(state, FALSE)) == INVALID_HANDLE_VALUE) { + /* listener is degraded; subsequent accepts will fail */ + error("afunix accept - failed to create next pipe instance, errno:%d", errno); + pio->handle = 0; + } else { + pio->handle = next; + if (afunix_listener_arm(pio) != 0) + error("afunix accept - failed to arm next pipe instance"); + } + + accepted->handle = connected; + accepted->type = NONSOCK_FD; + return accepted; +} + /* used to name named pipes used to implement pipe() */ static int pipe_counter = 0; @@ -1083,6 +1358,26 @@ fileio_close(struct w32_io* pio) { debug4("fileclose - pio:%p", pio); + /* bound/listening AF_UNIX socket emulated over named pipes */ + if (pio->internal.state == SOCK_BOUND || + pio->internal.state == SOCK_LISTENING) { + struct afunix_listener_state* state = + (struct afunix_listener_state*)pio->internal.context; + if (WINHANDLE(pio) != 0 && WINHANDLE(pio) != INVALID_HANDLE_VALUE) { + CancelIo(WINHANDLE(pio)); + CloseHandle(WINHANDLE(pio)); + } + if (pio->read_overlapped.hEvent) + CloseHandle(pio->read_overlapped.hEvent); + if (state->pipe_name) + free(state->pipe_name); + if (state->sd) + LocalFree(state->sd); + free(state); + free(pio); + return 0; + } + if (pio->type == NONSOCK_SYNC_FD || FILETYPE(pio) == FILE_TYPE_CHAR) return syncio_close(pio); diff --git a/contrib/win32/win32compat/inc/sys/un.h b/contrib/win32/win32compat/inc/sys/un.h index 42f09b8cebd7..2ff0b531e83f 100644 --- a/contrib/win32/win32compat/inc/sys/un.h +++ b/contrib/win32/win32compat/inc/sys/un.h @@ -2,6 +2,10 @@ struct sockaddr_un { short sun_family; /* AF_UNIX */ - char sun_path[108]; /* path name (gag) */ + /* + * larger than the traditional 108 - these paths are mapped onto + * named pipe names on Windows and deep profile paths are common + */ + char sun_path[260]; /* path name (gag) */ }; diff --git a/contrib/win32/win32compat/misc.c b/contrib/win32/win32compat/misc.c index 861ee2d585e4..193666c11ed4 100644 --- a/contrib/win32/win32compat/misc.c +++ b/contrib/win32/win32compat/misc.c @@ -55,6 +55,7 @@ #include "inc\sys\types.h" #include "inc\sys\ioctl.h" #include "inc\fcntl.h" +#include "inc\pwd.h" #include "inc\utf.h" #include "debug.h" #include "w32fd.h" @@ -480,6 +481,221 @@ daemon(int nochdir, int noclose) return 0; } +/* + * ssh ControlPersist support (Windows). Windows has no fork(), so a + * persistent mux master cannot be produced by backgrounding an + * already-authenticated connection the way POSIX does. Instead the + * foreground process spawns a fresh ssh process to act as the master; that + * process authenticates itself (sharing this console so it can prompt) and + * then detaches. These helpers implement the spawn / liveness / detach + * primitives; the policy lives in ssh.c. + */ + +/* environment marker identifying the spawned persistent master process; + * its value names the ready event the master signals (see below) */ +#define W32_CONTROLPERSIST_ENV "SSH_CONTROLPERSIST_MASTER" + +/* copy of this process's environment block with one extra "VAR=value" entry */ +static wchar_t * +env_block_append(const wchar_t *entry) +{ + wchar_t *parent, *block = NULL, *p; + size_t plen, elen; + + if ((parent = GetEnvironmentStringsW()) == NULL) + return NULL; + for (p = parent; *p != L'\0'; p += wcslen(p) + 1) + ; + plen = p - parent; + elen = wcslen(entry) + 1; + if ((block = malloc((plen + elen + 1) * sizeof(wchar_t))) != NULL) { + memcpy(block, parent, plen * sizeof(wchar_t)); + memcpy(block + plen, entry, elen * sizeof(wchar_t)); + block[plen + elen] = L'\0'; + } + FreeEnvironmentStringsW(parent); + return block; +} + +/* + * Spawn a detached background ssh process (this same executable) with the + * given arguments to act as a persistent mux master. The child shares this + * process's console so it can prompt for authentication, and is deliberately + * NOT registered as a tracked child, so it survives after this process + * exits. *ready_event receives an event HANDLE (as intptr_t) that the child + * signals once its control socket is up; the child opens it by the name + * carried in the environment marker (see w32_signal_controlpersist_ready). + * A named event is used rather than handle inheritance: for a console child + * CreateProcess propagates the parent's standard handles regardless of any + * PROC_THREAD_ATTRIBUTE_HANDLE_LIST restriction, and the long-lived master + * must not hold the client's redirected stdio open. Returns the child + * process HANDLE as intptr_t (caller w32_close_handle's both), or -1 on + * failure. + */ +intptr_t +w32_spawn_control_master(char *const args[], intptr_t *ready_event) +{ + wchar_t exe_w[MAX_PATH], event_name[64], env_entry[96]; + char *exe = NULL, *cmdline = NULL; + wchar_t *cmdline_w = NULL, *env = NULL; + LARGE_INTEGER qpc; + HANDLE event = NULL; + STARTUPINFOW si; + PROCESS_INFORMATION pi; + intptr_t ret = -1; + + *ready_event = -1; + + if (GetModuleFileNameW(NULL, exe_w, MAX_PATH) == 0) { + error("%s: GetModuleFileName failed: %d", __func__, GetLastError()); + return -1; + } + if ((exe = utf16_to_utf8(exe_w)) == NULL) { + errno = ENOMEM; + return -1; + } + /* build "" ; exe path is absolute so no module prepend */ + if ((cmdline = build_commandline_string(exe, args, FALSE)) == NULL) + goto done; + if ((cmdline_w = utf8_to_utf16(cmdline)) == NULL) { + errno = ENOMEM; + goto done; + } + + /* named event (session-local) the child signals once its control + * socket is up; pid + perf counter makes the name unique */ + QueryPerformanceCounter(&qpc); + swprintf_s(event_name, _countof(event_name), + L"Local\\ssh-controlpersist-%u-%08x%08x", GetCurrentProcessId(), + (unsigned int)qpc.HighPart, (unsigned int)qpc.LowPart); + if ((event = CreateEventW(NULL, TRUE, FALSE, event_name)) == NULL) { + error("%s: CreateEvent failed: %d", __func__, GetLastError()); + goto done; + } + + /* the child reads this marker to learn it is the persistent master + * and which event to signal when its control socket is up */ + swprintf_s(env_entry, _countof(env_entry), + L"" W32_CONTROLPERSIST_ENV L"=%s", event_name); + if ((env = env_block_append(env_entry)) == NULL) { + errno = ENOMEM; + goto done; + } + + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + /* + * Explicit null std handles: without STARTF_USESTDHANDLES, Windows + * duplicates a console parent's std handles into a console child even + * with bInheritHandles=FALSE, and the long-lived master must not keep + * the client's redirected stdio open. Auth prompts are unaffected: + * they use the shared console directly (conio), not the std handles. + */ + si.dwFlags = STARTF_USESTDHANDLES; + memset(&pi, 0, sizeof(pi)); + + /* + * No CREATE_NEW_CONSOLE/DETACHED_PROCESS: the child shares our console + * so it can prompt for authentication. It calls FreeConsole() once its + * control socket is up (see w32_detach_console). Handles are not + * inherited; the master opens its own connection. + */ + if (!CreateProcessW(NULL, cmdline_w, NULL, NULL, FALSE, + CREATE_UNICODE_ENVIRONMENT, env, NULL, &si, &pi)) { + error("%s: CreateProcess failed: %d", __func__, GetLastError()); + goto done; + } + CloseHandle(pi.hThread); + *ready_event = (intptr_t)event; + ret = (intptr_t)pi.hProcess; + +done: + if (ret == -1 && event != NULL) + CloseHandle(event); + free(env); + free(exe); + free(cmdline); + free(cmdline_w); + return ret; +} + +/* + * Wait for the spawned master to become ready or fail. Returns 1 when the + * ready event was signaled (the control socket is up), 0 when the process + * exited without signaling it, -1 on timeout or error. + */ +int +w32_wait_controlpersist_ready(intptr_t proc, intptr_t ready_event, + int timeout_ms) +{ + HANDLE handles[2] = { (HANDLE)ready_event, (HANDLE)proc }; + + switch (WaitForMultipleObjects(2, handles, FALSE, (DWORD)timeout_ms)) { + case WAIT_OBJECT_0: + return 1; + case WAIT_OBJECT_0 + 1: + return 0; + default: + return -1; + } +} + +/* + * In a spawned persistent master: signal the ready event named by the + * environment marker to unblock the spawning process's wait. No-op if the + * event cannot be opened (e.g. the spawning process already gave up). + */ +void +w32_signal_controlpersist_ready(void) +{ + char *val = NULL; + wchar_t *name_w = NULL; + size_t len = 0; + HANDLE event; + + _dupenv_s(&val, &len, W32_CONTROLPERSIST_ENV); + if (val == NULL) + return; + if ((name_w = utf8_to_utf16(val)) != NULL && + (event = OpenEventW(EVENT_MODIFY_STATE, FALSE, name_w)) != NULL) { + SetEvent(event); + CloseHandle(event); + } else + debug3("%s: cannot signal ready event: %d", __func__, + GetLastError()); + free(name_w); + free(val); +} + +/* close a process handle returned by w32_spawn_control_master */ +void +w32_close_handle(intptr_t h) +{ + if (h != -1 && h != 0) + CloseHandle((HANDLE)h); +} + +/* TRUE if this process was spawned as a persistent master (see above) */ +int +w32_is_controlpersist_master(void) +{ + char *val = NULL; + size_t len = 0; + int ret; + + _dupenv_s(&val, &len, W32_CONTROLPERSIST_ENV); + ret = (val != NULL); + free(val); + return ret; +} + +/* detach the persistent master from the shared console once auth is done */ +void +w32_detach_console(void) +{ + FreeConsole(); +} + int w32_ioctl(int d, int request, ...) { @@ -2046,12 +2262,39 @@ bash_to_win_path(const char *in, char *out, const size_t out_len) return retVal; } +/* + * getpeereid() emulation for AF_UNIX sockets emulated over named pipes. + * There are no numeric uids on Windows; the contract provided is: succeed + * with euid == geteuid() iff the pipe peer process runs as the same Windows + * user, so that callers comparing against getuid()/geteuid() get the right + * answer. Note that the pipe's DACL (owner + SYSTEM) enforces this too. + */ int getpeereid(int s, uid_t *euid, gid_t *egid) { - verbose("%s is not supported", __func__); - errno = ENOTSUP; - return -1; + HANDLE h; + DWORD peer_pid = 0; + + if ((h = w32_fd_to_handle(s)) == NULL || h == INVALID_HANDLE_VALUE) { + errno = EBADF; + return -1; + } + + if (!GetNamedPipeClientProcessId(h, &peer_pid) && + !GetNamedPipeServerProcessId(h, &peer_pid)) { + debug3("%s - cannot determine pipe peer, error: %d", __func__, GetLastError()); + errno = ENOTSUP; + return -1; + } + + if (peer_pid != GetCurrentProcessId() && !w32_is_pid_same_user(peer_pid)) { + errno = EPERM; + return -1; + } + + *euid = geteuid(); + *egid = getegid(); + return 0; } int diff --git a/contrib/win32/win32compat/misc_internal.h b/contrib/win32/win32compat/misc_internal.h index 5a43a992e820..ad6d0fd2389f 100644 --- a/contrib/win32/win32compat/misc_internal.h +++ b/contrib/win32/win32compat/misc_internal.h @@ -73,6 +73,7 @@ int file_in_chroot_jail(HANDLE); int file_in_chroot_jail_helper(wchar_t*); PSID lookup_sid(const wchar_t* name_utf16, PSID psid, DWORD * psid_len); PSID get_sid(const char*); +BOOL w32_is_pid_same_user(DWORD pid); int am_system(); int is_conpty_supported(); int exec_command_with_pty(int * pid, char* cmd, int in, int out, int err, unsigned int col, unsigned int row, int ttyfd); diff --git a/contrib/win32/win32compat/no-ops.c b/contrib/win32/win32compat/no-ops.c index a179d0ddfaae..b604fa9da1ce 100644 --- a/contrib/win32/win32compat/no-ops.c +++ b/contrib/win32/win32compat/no-ops.c @@ -56,33 +56,7 @@ permanently_set_uid(struct passwd *pw) } -/* mux.c defs */ -int muxserver_sock = -1; -typedef struct Channel Channel; -unsigned int muxclient_command = 0; -void -muxserver_listen(void) -{ - return; -} - -void -mux_exit_message(Channel *c, int exitval) -{ - return; -} - -void -mux_tty_alloc_failed(Channel *c) -{ - return; -} - -void -muxclient(const char *path) -{ - return; -} +/* mux.c is now compiled on Windows (ControlMaster support) */ int innetgr(const char *netgroup, const char *host, const char *user, const char *domain) diff --git a/contrib/win32/win32compat/w32fd.c b/contrib/win32/win32compat/w32fd.c index b4c436864dc4..fb1d37b9162d 100644 --- a/contrib/win32/win32compat/w32fd.c +++ b/contrib/win32/win32compat/w32fd.c @@ -275,6 +275,9 @@ w32_io_is_io_available(struct w32_io* pio, BOOL rd) { if (pio->type == SOCK_FD) return socketio_is_io_available(pio, rd); + else if (pio->internal.state == SOCK_LISTENING) + /* listening AF_UNIX socket emulated over named pipes */ + return rd ? fileio_afunix_listener_ready(pio) : FALSE; else return fileio_is_io_available(pio, rd); } @@ -284,6 +287,9 @@ w32_io_on_select(struct w32_io* pio, BOOL rd) { if ((pio->type == SOCK_FD)) socketio_on_select(pio, rd); + else if (pio->internal.state == SOCK_LISTENING) + /* ConnectNamedPipe is already pending; nothing to initiate */ + return; else fileio_on_select(pio, rd); } @@ -337,7 +343,6 @@ int w32_accept(int fd, struct sockaddr* addr, int* addrlen) { CHECK_FD(fd); - CHECK_SOCK_IO(fd_table.w32_ios[fd]); int min_index = fd_table_get_min_index(); struct w32_io* pio = NULL; @@ -345,11 +350,35 @@ w32_accept(int fd, struct sockaddr* addr, int* addrlen) return -1; if (fd_table.w32_ios[fd]->type == NONSOCK_FD) { - errno = ENOTSUP; - verbose("Unix domain server sockets are not supported"); - return -1; + struct w32_io* listener = fd_table.w32_ios[fd]; + + if (listener->internal.state != SOCK_LISTENING || + WINHANDLE(listener) == 0) { + errno = EINVAL; + return -1; + } + + while (!fileio_afunix_listener_ready(listener)) { + if (!w32_io_is_blocking(listener)) { + errno = EAGAIN; + return -1; + } + if (wait_for_any_event(&listener->read_overlapped.hEvent, + 1, INFINITE) == -1) + return -1; + } + + if ((pio = fileio_afunix_accept(listener)) == NULL) + return -1; + + fd_table_set(pio, min_index); + if (addr && addrlen) + memset(addr, 0, *addrlen); + debug4("afunix accept - handle:%p, io:%p, fd:%d", pio->handle, pio, min_index); + return min_index; } + CHECK_SOCK_IO(fd_table.w32_ios[fd]); pio = socketio_accept(fd_table.w32_ios[fd], addr, addrlen); if (!pio) return -1; @@ -396,11 +425,8 @@ int w32_listen(int fd, int backlog) { CHECK_FD(fd); - if (fd_table.w32_ios[fd]->type == NONSOCK_FD) { - errno = ENOTSUP; - verbose("Unix domain server sockets are not supported"); - return -1; - } + if (fd_table.w32_ios[fd]->type == NONSOCK_FD) + return fileio_afunix_listen(fd_table.w32_ios[fd], backlog); CHECK_SOCK_IO(fd_table.w32_ios[fd]); return socketio_listen(fd_table.w32_ios[fd], backlog); @@ -411,9 +437,8 @@ w32_bind(int fd, const struct sockaddr *name, int namelen) { CHECK_FD(fd); if (fd_table.w32_ios[fd]->type == NONSOCK_FD) { - errno = ENOTSUP; - verbose("Unix domain server sockets are not supported"); - return -1; + struct sockaddr_un* addr = (struct sockaddr_un*)name; + return fileio_afunix_bind(fd_table.w32_ios[fd], addr->sun_path); } CHECK_SOCK_IO(fd_table.w32_ios[fd]); @@ -795,8 +820,9 @@ w32_select(int fds, w32_fd_set* readfds, w32_fd_set* writefds, w32_fd_set* excep for (int i = 0; i < fds; i++) { if (readfds && FD_ISSET(i, readfds)) { w32_io_on_select(fd_table.w32_ios[i], TRUE); - if ((fd_table.w32_ios[i]->type == SOCK_FD) && - (fd_table.w32_ios[i]->internal.state == SOCK_LISTENING)) { + /* listening sockets (TCP or AF_UNIX pipe) signal via event */ + if (fd_table.w32_ios[i]->internal.state == SOCK_LISTENING && + fd_table.w32_ios[i]->read_overlapped.hEvent != NULL) { if (num_events == SELECT_EVENT_LIMIT) { debug3("select - ERROR: max #events breach"); errno = ENOMEM; @@ -1004,6 +1030,222 @@ w32_fd_to_handle(int fd) return fd_table.w32_ios[fd]->handle; } +/* wraps a raw win32 handle in a new fd table entry */ +static int +w32_allocate_fd_for_handle(HANDLE h, int type) +{ + int min_index = fd_table_get_min_index(); + struct w32_io* pio; + + if (min_index == -1) + return -1; + + if ((pio = malloc(sizeof(struct w32_io))) == NULL) { + errno = ENOMEM; + return -1; + } + memset(pio, 0, sizeof(struct w32_io)); + pio->type = type; + pio->handle = h; + fd_table_set(pio, min_index); + return min_index; +} + +/* + * File descriptor passing over AF_UNIX (named pipe) sockets. + * Windows has no SCM_RIGHTS: the sender transmits its pid and raw handle + * value in-band; the receiver verifies the peer is the same Windows user + * and pulls the handle across with DuplicateHandle. Used by ssh mux + * (ControlMaster) to pass the mux client's stdio to the mux master. + */ +#define W32_FDPASS_MAGIC 0x77465044 /* "wFPD" */ +#define W32_FDPASS_TIMEOUT_MS 15000 + +#pragma pack(push, 1) +struct w32_fdpass_msg { + unsigned __int32 magic; + unsigned __int32 pid; + unsigned __int64 handle; + unsigned __int32 type; /* enum w32_io_type of sender's fd */ +}; +#pragma pack(pop) + +/* TRUE if process pid runs as the same Windows user as us */ +BOOL +w32_is_pid_same_user(DWORD pid) +{ + BOOL ret = FALSE; + HANDLE proc = NULL, token = NULL; + TOKEN_USER *peer_info = NULL; + PSID my_sid = NULL; + DWORD info_len = 0; + + if ((my_sid = get_sid(NULL)) == NULL) { + error("fdpass - cannot retrieve own SID"); + goto done; + } + if ((proc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid)) == NULL) { + error("fdpass - OpenProcess(%d) failed, error: %d", pid, GetLastError()); + goto done; + } + if (!OpenProcessToken(proc, TOKEN_QUERY, &token)) { + error("fdpass - OpenProcessToken failed, error: %d", GetLastError()); + goto done; + } + if (GetTokenInformation(token, TokenUser, NULL, 0, &info_len) == TRUE || + (peer_info = (TOKEN_USER*)malloc(info_len)) == NULL) + goto done; + if (GetTokenInformation(token, TokenUser, peer_info, info_len, &info_len) == FALSE) + goto done; + ret = EqualSid(my_sid, peer_info->User.Sid); + if (!ret) + error("fdpass - peer process %d is a different user", pid); + +done: + if (peer_info) + free(peer_info); + if (my_sid) + free(my_sid); + if (token) + CloseHandle(token); + if (proc) + CloseHandle(proc); + return ret; +} + +/* read/write full buffer on possibly nonblocking fd, pumping APCs */ +static int +fdpass_io(int sock, char* buf, int len, BOOL do_write) +{ + int done = 0, r; + ULONGLONG deadline = GetTickCount64() + W32_FDPASS_TIMEOUT_MS; + + while (done < len) { + r = do_write ? w32_write(sock, buf + done, len - done) : + w32_read(sock, buf + done, len - done); + if (r > 0) { + done += r; + continue; + } + if (r == 0 && !do_write) { + errno = EPIPE; + return -1; + } + if (r < 0 && errno != EAGAIN && errno != EINTR) + return -1; + if (GetTickCount64() > deadline) { + errno = ETIMEDOUT; + return -1; + } + /* pump APCs so pending async io on sock can complete */ + if (wait_for_any_event(NULL, 0, 100) == -1 && errno != EINTR) + return -1; + errno = 0; + } + return 0; +} + +int +w32_fdpass_send(int sock, int fd) +{ + struct w32_fdpass_msg msg; + struct w32_io* pio; + + CHECK_FD(sock); + CHECK_FD(fd); + + pio = fd_table.w32_ios[fd]; + if (pio->type == SOCK_FD) { + /* would need WSADuplicateSocket with receiver pid */ + errno = ENOTSUP; + error("fdpass - passing socket fds is not supported"); + return -1; + } + + msg.magic = W32_FDPASS_MAGIC; + msg.pid = GetCurrentProcessId(); + msg.handle = (unsigned __int64)(uintptr_t)pio->handle; + msg.type = pio->type; + + if (fdpass_io(sock, (char*)&msg, sizeof(msg), TRUE) != 0) { + error("fdpass - failed to send fd %d over fd %d, errno: %d", fd, sock, errno); + return -1; + } + debug3("fdpass - sent fd:%d handle:%p over fd:%d", fd, pio->handle, sock); + return 0; +} + +int +w32_fdpass_recv(int sock) +{ + struct w32_fdpass_msg msg; + HANDLE src_proc = NULL, dup = NULL; + DWORD pipe_client_pid = 0; + int fd = -1, type; + + CHECK_FD(sock); + + if (fdpass_io(sock, (char*)&msg, sizeof(msg), FALSE) != 0) { + error("fdpass - failed to read fd message from fd %d, errno: %d", sock, errno); + return -1; + } + + if (msg.magic != W32_FDPASS_MAGIC) { + error("fdpass - bad magic 0x%08x from fd %d", msg.magic, sock); + errno = EINVAL; + return -1; + } + + /* when the transport is a named pipe, the claimed pid must match the peer */ + if (fd_table.w32_ios[sock]->type == NONSOCK_FD && + GetNamedPipeClientProcessId(fd_table.w32_ios[sock]->handle, &pipe_client_pid) && + pipe_client_pid != 0 && pipe_client_pid != GetCurrentProcessId() && + pipe_client_pid != msg.pid) { + error("fdpass - claimed pid %d does not match pipe peer %d", + msg.pid, pipe_client_pid); + errno = EPERM; + return -1; + } + + if (!w32_is_pid_same_user(msg.pid)) { + errno = EPERM; + return -1; + } + + if ((src_proc = OpenProcess(PROCESS_DUP_HANDLE, FALSE, msg.pid)) == NULL) { + error("fdpass - OpenProcess(%d) for dup failed, error: %d", msg.pid, GetLastError()); + errno = EPERM; + return -1; + } + + if (!DuplicateHandle(src_proc, (HANDLE)(uintptr_t)msg.handle, + GetCurrentProcess(), &dup, 0, FALSE, DUPLICATE_SAME_ACCESS)) { + error("fdpass - DuplicateHandle failed, error: %d", GetLastError()); + CloseHandle(src_proc); + errno = EPERM; + return -1; + } + CloseHandle(src_proc); + + /* + * sender's fd classification decides sync vs async io; inherited stdio + * handles (console, redirected pipes/files) are typically opened + * non-overlapped, so default to synchronous io unless the sender was + * using async io on its end. + */ + type = (msg.type == NONSOCK_FD) ? NONSOCK_FD : NONSOCK_SYNC_FD; + if (GetFileType(dup) == FILE_TYPE_CHAR) + type = NONSOCK_SYNC_FD; + + if ((fd = w32_allocate_fd_for_handle(dup, type)) == -1) { + CloseHandle(dup); + return -1; + } + debug3("fdpass - received handle:%p from pid:%d as fd:%d type:%d", + dup, msg.pid, fd, type); + return fd; +} + int w32_ftruncate(int fd, off_t length) { diff --git a/contrib/win32/win32compat/w32fd.h b/contrib/win32/win32compat/w32fd.h index 0f1ee3a08f62..d5b134e33675 100644 --- a/contrib/win32/win32compat/w32fd.h +++ b/contrib/win32/win32compat/w32fd.h @@ -62,7 +62,8 @@ enum w32_io_sock_state { SOCK_INITIALIZED = 0, SOCK_LISTENING = 1, /*listen called on socket*/ SOCK_CONNECTING = 2, /*connect called on socket, connect is in progress*/ - SOCK_READY = 3 /*recv and send can be done*/ + SOCK_READY = 3, /*recv and send can be done*/ + SOCK_BOUND = 4 /*bind called on AF_UNIX (named pipe) socket*/ }; /* @@ -155,6 +156,10 @@ void fileio_on_select(struct w32_io* pio, BOOL rd); int fileio_close(struct w32_io* pio); int fileio_pipe(struct w32_io* pio[2], int); struct w32_io* fileio_afunix_socket(); +int fileio_afunix_bind(struct w32_io* pio, const char* sun_path); +int fileio_afunix_listen(struct w32_io* pio, int backlog); +BOOL fileio_afunix_listener_ready(struct w32_io* pio); +struct w32_io* fileio_afunix_accept(struct w32_io* pio); int fileio_connect(struct w32_io*, char*); struct w32_io* fileio_open(const char *pathname, int flags, mode_t mode); int fileio_read(struct w32_io* pio, void *dst, size_t max); diff --git a/monitor_fdpass.c b/monitor_fdpass.c index a07727a8e743..d93f498fe737 100644 --- a/monitor_fdpass.c +++ b/monitor_fdpass.c @@ -48,6 +48,23 @@ #include "log.h" #include "monitor_fdpass.h" +#ifdef WINDOWS +/* implemented in contrib/win32/win32compat/w32fd.c over DuplicateHandle */ +int w32_fdpass_send(int sock, int fd); +int w32_fdpass_recv(int sock); + +int +mm_send_fd(int sock, int fd) +{ + return w32_fdpass_send(sock, fd); +} + +int +mm_receive_fd(int sock) +{ + return w32_fdpass_recv(sock); +} +#else /* !WINDOWS */ int mm_send_fd(int sock, int fd) { @@ -183,3 +200,4 @@ mm_receive_fd(int sock) return -1; #endif } +#endif /* !WINDOWS */ diff --git a/mux.c b/mux.c index 415024f74e8f..4a210efaf21c 100644 --- a/mux.c +++ b/mux.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -116,10 +117,18 @@ static volatile sig_atomic_t muxclient_terminate = 0; /* PID of multiplex server */ static u_int muxserver_pid = 0; +#ifdef WINDOWS +/* master advertised MUX_EXT_TTY_RELAY in its hello */ +static int muxclient_tty_relay = 0; +#endif + static Channel *mux_listener_channel = NULL; struct mux_master_state { int hello_rcvd; + /* last window size received from the mux client (MUX_C_WINSIZE) */ + struct winsize ws; + int ws_valid; }; /* mux protocol messages */ @@ -131,6 +140,7 @@ struct mux_master_state { #define MUX_C_CLOSE_FWD 0x10000007 #define MUX_C_NEW_STDIO_FWD 0x10000008 #define MUX_C_STOP_LISTENING 0x10000009 +#define MUX_C_WINSIZE 0x1000000e #define MUX_C_PROXY 0x1000000f #define MUX_S_OK 0x80000001 #define MUX_S_PERMISSION_DENIED 0x80000002 @@ -142,6 +152,14 @@ struct mux_master_state { #define MUX_S_TTY_ALLOC_FAIL 0x80000008 #define MUX_S_PROXY 0x8000000f +/* + * Windows: hello extension advertised by masters that support tty sessions + * over multiplexed connections (client-side console relay + MUX_C_WINSIZE). + * Name is provisional pending maintainer review; the empty value is reserved + * for future versioning. + */ +#define MUX_EXT_TTY_RELAY "tty-relay@win32.openssh.com" + /* type codes for MUX_C_OPEN_FWD and MUX_C_CLOSE_FWD */ #define MUX_FWD_LOCAL 1 #define MUX_FWD_REMOTE 2 @@ -168,6 +186,10 @@ static int mux_master_process_stop_listening(struct ssh *, u_int, Channel *, struct sshbuf *, struct sshbuf *); static int mux_master_process_proxy(struct ssh *, u_int, Channel *, struct sshbuf *, struct sshbuf *); +#ifdef WINDOWS +static int mux_master_process_winsize(struct ssh *, u_int, + Channel *, struct sshbuf *, struct sshbuf *); +#endif static const struct { u_int type; @@ -183,6 +205,9 @@ static const struct { { MUX_C_NEW_STDIO_FWD, mux_master_process_stdio_fwd }, { MUX_C_STOP_LISTENING, mux_master_process_stop_listening }, { MUX_C_PROXY, mux_master_process_proxy }, +#ifdef WINDOWS + { MUX_C_WINSIZE, mux_master_process_winsize }, +#endif { 0, NULL } }; @@ -447,8 +472,13 @@ mux_master_process_new_session(struct ssh *ssh, u_int rid, } /* Try to pick up ttymodes from client before it goes raw */ +#ifdef WINDOWS + /* no tcgetattr; tty sessions get default modes */ + memset(&cctx->tio, 0, sizeof(cctx->tio)); +#else if (cctx->want_tty && tcgetattr(new_fd[0], &cctx->tio) == -1) error_f("tcgetattr: %s", strerror(errno)); +#endif window = CHAN_SES_WINDOW_DEFAULT; packetmax = CHAN_SES_PACKET_DEFAULT; @@ -502,6 +532,56 @@ mux_master_process_alive_check(struct ssh *ssh, u_int rid, return 0; } +#ifdef WINDOWS +/* + * MUX_C_WINSIZE: the mux client reports its terminal size. Windows masters + * cannot query the client's console themselves (console handles are not + * usable across processes), so the client sends the size explicitly: once + * before MUX_C_NEW_SESSION (the control channel is ordered, so this seeds + * the pty-req dimensions) and again on every local resize. No reply is sent. + */ +static int +mux_master_process_winsize(struct ssh *ssh, u_int rid, + Channel *c, struct sshbuf *m, struct sshbuf *reply) +{ + struct mux_master_state *state = (struct mux_master_state *)c->mux_ctx; + Channel *sc; + u_int col, row, xpix, ypix; + int r; + + if ((r = sshbuf_get_u32(m, &col)) != 0 || + (r = sshbuf_get_u32(m, &row)) != 0 || + (r = sshbuf_get_u32(m, &xpix)) != 0 || + (r = sshbuf_get_u32(m, &ypix)) != 0) { + error_f("malformed message"); + return -1; + } + + debug2_f("channel %d: winsize %ux%u", c->self, col, row); + + state->ws.ws_col = col; + state->ws.ws_row = row; + state->ws.ws_xpixel = xpix; + state->ws.ws_ypixel = ypix; + state->ws_valid = 1; + + /* forward to an established session as a window-change request */ + if (c->have_ctl_child_id && + (sc = channel_by_id(ssh, c->ctl_child_id)) != NULL && + sc->client_tty && sc->type == SSH_CHANNEL_OPEN) { + channel_request_start(ssh, sc->self, "window-change", 0); + if ((r = sshpkt_put_u32(ssh, col)) != 0 || + (r = sshpkt_put_u32(ssh, row)) != 0 || + (r = sshpkt_put_u32(ssh, xpix)) != 0 || + (r = sshpkt_put_u32(ssh, ypix)) != 0 || + (r = sshpkt_send(ssh)) != 0) + fatal_fr(r, "channel %u: send window-change", sc->self); + } + + return 0; +} +#endif /* WINDOWS */ + static int mux_master_process_terminate(struct ssh *ssh, u_int rid, Channel *c, struct sshbuf *m, struct sshbuf *reply) @@ -1167,7 +1247,12 @@ mux_master_read_cb(struct ssh *ssh, Channel *c) if ((r = sshbuf_put_u32(out, MUX_MSG_HELLO)) != 0 || (r = sshbuf_put_u32(out, SSHMUX_VER)) != 0) fatal_fr(r, "reply"); - /* no extensions */ +#ifdef WINDOWS + /* advertise tty session support (client-side console relay) */ + if ((r = sshbuf_put_cstring(out, MUX_EXT_TTY_RELAY)) != 0 || + (r = sshbuf_put_string(out, NULL, 0)) != 0) + fatal_fr(r, "reply extension"); +#endif if ((r = sshbuf_put_stringb(c->output, out)) != 0) fatal_fr(r, "enqueue"); debug3_f("channel %d: hello sent", c->self); @@ -1270,10 +1355,12 @@ mux_tty_alloc_failed(struct ssh *ssh, Channel *c) void muxserver_listen(struct ssh *ssh) { +#ifndef WINDOWS mode_t old_umask; char *orig_control_path = options.control_path; char rbuf[16+1]; u_int i, r; +#endif int oerrno; if (options.control_path == NULL || @@ -1282,6 +1369,28 @@ muxserver_listen(struct ssh *ssh) debug("setting up multiplex master socket"); +#ifdef WINDOWS + /* + * ControlPath maps to a named pipe: there is no filesystem entry, so + * the umask/temp-path/link tricks below do not apply. Pipe name + * creation is atomic (FILE_FLAG_FIRST_PIPE_INSTANCE) and reports + * EADDRINUSE if the name is owned by another process. + */ + muxserver_sock = unix_listener(options.control_path, 64, 0); + if (muxserver_sock < 0) { + oerrno = errno; + if (oerrno == EINVAL || oerrno == EADDRINUSE) { + error("ControlSocket %s already exists, " + "disabling multiplexing", options.control_path); + free(options.control_path); + options.control_path = NULL; + options.control_master = SSHCTL_MASTER_NO; + return; + } + /* unix_listener() logs the error */ + cleanup_exit(255); + } +#else /* !WINDOWS */ /* * Use a temporary path before listen so we can pseudo-atomically * establish the listening socket in its final location to avoid @@ -1338,6 +1447,7 @@ muxserver_listen(struct ssh *ssh) unlink(options.control_path); free(options.control_path); options.control_path = orig_control_path; +#endif /* !WINDOWS */ set_nonblock(muxserver_sock); @@ -1403,8 +1513,27 @@ mux_session_confirm(struct ssh *ssh, int id, int success, void *arg) fatal_fr(r, "send"); } +#ifdef WINDOWS + { + /* + * The client's console size arrives via MUX_C_WINSIZE (sent + * before the session request); rfd is a relay pipe here, and + * falling back to ioctl() would leak the master's own console + * size into the pty-req, so pass zeros when no size was seen. + */ + static const struct winsize zws; + struct mux_master_state *state = + (struct mux_master_state *)cc->mux_ctx; + + client_session2_setup(ssh, id, cctx->want_tty, + cctx->want_subsys, cctx->term, &cctx->tio, c->rfd, + cctx->cmd, cctx->env, + (state != NULL && state->ws_valid) ? &state->ws : &zws); + } +#else client_session2_setup(ssh, id, cctx->want_tty, cctx->want_subsys, - cctx->term, &cctx->tio, c->rfd, cctx->cmd, cctx->env); + cctx->term, &cctx->tio, c->rfd, cctx->cmd, cctx->env, NULL); +#endif debug3_f("sending success reply"); /* prepare reply */ @@ -1592,6 +1721,394 @@ mux_client_read_packet(int fd, struct sshbuf *m) return mux_client_read_packet_timeout(fd, m, -1); } +#ifdef WINDOWS +/* + * Windows console relay for multiplexed passenger sessions. + * + * On POSIX the mux client passes its stdio file descriptors to the master + * via SCM_RIGHTS and the master does all the terminal I/O. On Windows, + * console handles are bound to the owning process's console and cannot be + * driven by the master across the process boundary. So when a std fd is a + * console, the client substitutes a pipe (which the master CAN drive, just + * like redirected stdio) and pumps bytes between its own console and that + * pipe itself, reusing the same in-process terminal emulation a non-mux + * ssh already uses. Window-size changes travel over the control channel as + * MUX_C_WINSIZE (see mux_master_process_winsize) instead of via ioctl on + * the master, and instead of relaying SIGWINCH with kill() (which on + * Windows would terminate the master process). + * + * Protocol additions (Windows only; see PROTOCOL.mux for the base + * protocol): + * + * 1. Masters advertise the hello extension "tty-relay@win32.openssh.com" + * with an empty value (reserved for future versioning). Peers that do + * not recognise the extension ignore it, per PROTOCOL.mux. + * + * 2. A client that saw the extension may send, at any time after the + * hello: + * uint32 MUX_C_WINSIZE + * uint32 request id + * uint32 columns + * uint32 rows + * uint32 x pixels + * uint32 y pixels + * No reply is sent and the request id is not consumed. Because the + * control channel is ordered, a MUX_C_WINSIZE sent before + * MUX_C_NEW_SESSION seeds the dimensions used in the session's pty-req; + * later messages become "window-change" channel requests. + * + * 3. A client that wants a tty but did not see the extension MUST NOT + * open a session over the multiplexed connection; it falls back to a + * separate direct connection instead (see muxclient()). + */ + +#define MUX_RELAY_BUF 8192 + +struct mux_relay_ent { + int is_console; /* this std fd was a console -> relayed */ + int is_input; /* 1 = console->master (stdin); 0 = ->console */ + int console_fd; /* the real console std fd (0/1/2), not owned */ + int local_pipe; /* our end of the substitute pipe (owned) */ + int passed; /* pipe end handed to the master (-1 if none) */ + int rd_done; /* source (console or pipe) hit EOF */ + int pipe_closed; /* local_pipe has been closed */ + char buf[MUX_RELAY_BUF]; + size_t buf_len; +}; + +struct mux_relay { + int active; /* at least one console fd is being relayed */ + struct mux_relay_ent ent[3]; /* indexed by std fd (0,1,2) */ + int nent; +}; + +#define MUX_RELAY_RD(e) ((e)->is_input ? (e)->console_fd : (e)->local_pipe) +#define MUX_RELAY_WR(e) ((e)->is_input ? (e)->local_pipe : (e)->console_fd) + +static void mux_relay_close_all(struct mux_relay *r); + +/* Set when a SIGWINCH is caught; drained by the pump loop. */ +static volatile sig_atomic_t muxclient_winch = 0; + +static void +control_client_sigwinch(int signo) +{ + (void)signo; + muxclient_winch = 1; +} + +/* Send the local console size to the master. Silent no-op without a tty. */ +static void +mux_client_send_winsize(int fd) +{ + struct sshbuf *m; + struct winsize ws; + int r; + + /* prefer stdout, but fall back to stdin if only that is a tty */ + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 && + ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == -1) + return; + if ((m = sshbuf_new()) == NULL) + fatal_f("sshbuf_new"); + /* no reply is sent for WINSIZE, so the request id is not consumed */ + if ((r = sshbuf_put_u32(m, MUX_C_WINSIZE)) != 0 || + (r = sshbuf_put_u32(m, muxclient_request_id)) != 0 || + (r = sshbuf_put_u32(m, (u_int)ws.ws_col)) != 0 || + (r = sshbuf_put_u32(m, (u_int)ws.ws_row)) != 0 || + (r = sshbuf_put_u32(m, (u_int)ws.ws_xpixel)) != 0 || + (r = sshbuf_put_u32(m, (u_int)ws.ws_ypixel)) != 0) + fatal_fr(r, "assemble winsize"); + if (mux_client_write_packet(fd, m) != 0) + debug_f("write winsize: %s", strerror(errno)); + sshbuf_free(m); +} + +/* + * Prepare the relay for a passenger session with nfds std fds (3 for a + * normal session, 2 for stdio forwarding). For each fd that is a console, + * a pipe is created and its master-facing end is recorded in passed_fd[i] + * so the caller sends that instead of the real fd. Returns 0 on success + * (relay may be inactive if no fd was a console), -1 on error with all + * pipes closed. Real std fds are never modified, so callers can still fall + * back to a direct connection. + */ +static int +mux_relay_prepare(struct mux_relay *r, int nfds, int passed_fd[3]) +{ + int i, p[2]; + + memset(r, 0, sizeof(*r)); + r->nent = nfds; + for (i = 0; i < nfds; i++) { + r->ent[i].passed = -1; + passed_fd[i] = i; /* default: pass the real fd */ + } + + if (!muxclient_tty_relay) + return 0; + + for (i = 0; i < nfds; i++) { + struct mux_relay_ent *e = &r->ent[i]; + + if (!isatty(i)) + continue; + if (pipe(p) == -1) { + error_f("pipe: %s", strerror(errno)); + mux_relay_close_all(r); + return -1; + } + e->is_console = 1; + e->console_fd = i; + if (i == STDIN_FILENO) { + /* console -> master: master reads the pipe read end */ + e->is_input = 1; + e->local_pipe = p[1]; /* we write */ + e->passed = p[0]; /* master reads */ + } else { + /* master -> console: master writes the pipe write end */ + e->is_input = 0; + e->local_pipe = p[0]; /* we read */ + e->passed = p[1]; /* master writes */ + } + passed_fd[i] = e->passed; + r->active = 1; + (void)fcntl(e->local_pipe, F_SETFL, O_NONBLOCK); + (void)fcntl(e->console_fd, F_SETFL, O_NONBLOCK); + } + return 0; +} + +/* Close the pipe ends handed to the master (call after the reply arrives). */ +static void +mux_relay_close_passed(struct mux_relay *r) +{ + int i; + + for (i = 0; i < r->nent; i++) { + if (r->ent[i].is_console && r->ent[i].passed != -1) { + close(r->ent[i].passed); + r->ent[i].passed = -1; + } + } +} + +/* Close every fd the relay still owns (pipe ends only; never the console). */ +static void +mux_relay_close_all(struct mux_relay *r) +{ + int i; + + for (i = 0; i < r->nent; i++) { + struct mux_relay_ent *e = &r->ent[i]; + if (!e->is_console) + continue; + if (e->passed != -1) { + close(e->passed); + e->passed = -1; + } + if (!e->pipe_closed) { + close(e->local_pipe); + e->pipe_closed = 1; + } + } + memset(r, 0, sizeof(*r)); +} + +/* Read from the source fd into the buffer if there is room. */ +static void +mux_relay_fill(struct mux_relay_ent *e) +{ + ssize_t n; + + if (e->rd_done || e->buf_len == sizeof(e->buf)) + return; + n = read(MUX_RELAY_RD(e), e->buf + e->buf_len, + sizeof(e->buf) - e->buf_len); + if (n > 0) + e->buf_len += n; + else if (n == 0) + e->rd_done = 1; /* EOF */ + else if (errno != EAGAIN && errno != EINTR) + e->rd_done = 1; /* broken pipe counts as EOF */ +} + +/* Write buffered bytes out to the destination fd. */ +static void +mux_relay_flush(struct mux_relay_ent *e) +{ + ssize_t n; + + while (e->buf_len > 0) { + n = write(MUX_RELAY_WR(e), e->buf, e->buf_len); + if (n > 0) { + memmove(e->buf, e->buf + n, e->buf_len - n); + e->buf_len -= n; + continue; + } + if (n == -1 && (errno == EAGAIN || errno == EINTR)) + return; /* retry next poll */ + /* destination gone: drop this direction */ + e->rd_done = 1; + e->buf_len = 0; + return; + } +} + +/* + * Run the passenger session, moving console<->pipe bytes while watching the + * control fd for tty-alloc-fail / exit-message / EOF. Returns when the + * master closes the control fd (session over). Mirrors the control-packet + * handling of the POSIX SCM_RIGHTS wait-loop. + */ +static void +mux_relay_pump(int fd, struct mux_relay *r, u_int sid, + u_int *exitval, int *exitval_seen, int *rawmode) +{ + struct sshbuf *m; + struct pollfd pfd[7]; + u_int type, esid; + int i, r2, ctl_idx, draining = 0; + int rd_idx[3], wr_idx[3]; + char *e; + time_t drain_deadline = 0; + + if ((m = sshbuf_new()) == NULL) + fatal_f("sshbuf_new"); + + for (;;) { + int nfds = 0; + + if (muxclient_terminate) + break; + if (muxclient_winch) { + muxclient_winch = 0; + if (tty_flag) + mux_client_send_winsize(fd); + } + + ctl_idx = -1; + if (!draining) { + ctl_idx = nfds; + pfd[nfds].fd = fd; + pfd[nfds].events = POLLIN; + pfd[nfds].revents = 0; + nfds++; + } + + for (i = 0; i < r->nent; i++) { + struct mux_relay_ent *ent = &r->ent[i]; + + rd_idx[i] = wr_idx[i] = -1; + if (!ent->is_console) + continue; + /* in the drain phase stop reading local console input */ + if (!(draining && ent->is_input) && !ent->rd_done && + ent->buf_len < sizeof(ent->buf)) { + rd_idx[i] = nfds; + pfd[nfds].fd = MUX_RELAY_RD(ent); + pfd[nfds].events = POLLIN; + pfd[nfds].revents = 0; + nfds++; + } + if (ent->buf_len > 0) { + wr_idx[i] = nfds; + pfd[nfds].fd = MUX_RELAY_WR(ent); + pfd[nfds].events = POLLOUT; + pfd[nfds].revents = 0; + nfds++; + } + } + + (void)poll(pfd, nfds, 200); /* finite: SIGWINCH won't wake it */ + + /* move data in both directions */ + for (i = 0; i < r->nent; i++) { + struct mux_relay_ent *ent = &r->ent[i]; + + if (!ent->is_console) + continue; + if (rd_idx[i] != -1 && + (pfd[rd_idx[i]].revents & (POLLIN | POLLHUP))) + mux_relay_fill(ent); + if (ent->buf_len > 0) + mux_relay_flush(ent); + /* propagate console EOF to the master by closing the pipe */ + if (ent->is_input && ent->rd_done && ent->buf_len == 0 && + !ent->pipe_closed) { + close(ent->local_pipe); + ent->pipe_closed = 1; + } + } + + /* control channel */ + if (ctl_idx != -1 && + (pfd[ctl_idx].revents & (POLLIN | POLLHUP))) { + sshbuf_reset(m); + if (mux_client_read_packet(fd, m) != 0) { + /* master closed the control fd: drain output */ + draining = 1; + drain_deadline = monotime() + 5; + } else { + if ((r2 = sshbuf_get_u32(m, &type)) != 0) + fatal_fr(r2, "parse type"); + switch (type) { + case MUX_S_TTY_ALLOC_FAIL: + if ((r2 = sshbuf_get_u32(m, &esid)) != 0) + fatal_fr(r2, "parse session ID"); + if (esid != sid) + fatal_f("tty alloc fail on unknown " + "session: my id %u theirs %u", + sid, esid); + leave_raw_mode(options.request_tty == + REQUEST_TTY_FORCE); + *rawmode = 0; + break; + case MUX_S_EXIT_MESSAGE: + if ((r2 = sshbuf_get_u32(m, &esid)) != 0) + fatal_fr(r2, "parse session ID"); + if (esid != sid) + fatal_f("exit on unknown session: " + "my id %u theirs %u", sid, esid); + if (*exitval_seen) + fatal_f("exitval sent twice"); + if ((r2 = sshbuf_get_u32(m, exitval)) != 0) + fatal_fr(r2, "parse exitval"); + *exitval_seen = 1; + break; + default: + if ((r2 = sshbuf_get_cstring(m, &e, + NULL)) != 0) + fatal_fr(r2, "parse error message"); + if (*rawmode) + leave_raw_mode(options.request_tty + == REQUEST_TTY_FORCE); + fatal_f("master returned error: %s", e); + } + } + } + + /* drain phase: leave once output is flushed or the cap is hit */ + if (draining) { + int pending = 0; + + for (i = STDOUT_FILENO; + i <= STDERR_FILENO && i < r->nent; i++) { + struct mux_relay_ent *ent = &r->ent[i]; + if (ent->is_console && + (!ent->rd_done || ent->buf_len > 0)) + pending = 1; + } + if (!pending || monotime() >= drain_deadline) + break; + } + } + + sshbuf_free(m); +} + +#endif /* WINDOWS */ + static int mux_client_hello_exchange(int fd, int timeout_ms) { @@ -1642,6 +2159,14 @@ mux_client_hello_exchange(int fd, int timeout_ms) error_fr(r, "parse extension"); goto out; } +#ifdef WINDOWS + if (strcmp(name, MUX_EXT_TTY_RELAY) == 0) { + debug2("master supports tty sessions over mux"); + muxclient_tty_relay = 1; + free(name); + continue; + } +#endif debug2("Unrecognised master extension \"%s\"", name); free(name); } @@ -1888,6 +2413,10 @@ mux_client_request_session(int fd) u_int i, echar, rid, sid, esid, exitval, type, exitval_seen; extern char **environ; int r, rawmode = 0; +#ifdef WINDOWS + struct mux_relay relay; + int passed_fd[3]; +#endif debug3_f("entering"); @@ -1909,6 +2438,21 @@ mux_client_request_session(int fd) if (options.escape_char != SSH_ESCAPECHAR_NONE) echar = (u_int)options.escape_char; +#ifdef WINDOWS + /* substitute pipes for console std fds (see relay comment above) */ + if (mux_relay_prepare(&relay, 3, passed_fd) == -1) { + error_f("cannot set up console relay"); + return -1; + } + /* + * Seed the master with our terminal size before the session request: + * the control channel is ordered, and the master cannot query our + * console itself. + */ + if (tty_flag && muxclient_tty_relay) + mux_client_send_winsize(fd); +#endif + if ((m = sshbuf_new()) == NULL) fatal_f("sshbuf_new"); if ((r = sshbuf_put_u32(m, MUX_C_NEW_SESSION)) != 0 || @@ -1941,10 +2485,18 @@ mux_client_request_session(int fd) fatal_f("write packet: %s", strerror(errno)); /* Send the stdio file descriptors */ +#ifdef WINDOWS + /* console fds were substituted with relay pipe ends */ + if (mm_send_fd(fd, passed_fd[0]) == -1 || + mm_send_fd(fd, passed_fd[1]) == -1 || + mm_send_fd(fd, passed_fd[2]) == -1) + fatal_f("send fds failed"); +#else if (mm_send_fd(fd, STDIN_FILENO) == -1 || mm_send_fd(fd, STDOUT_FILENO) == -1 || mm_send_fd(fd, STDERR_FILENO) == -1) fatal_f("send fds failed"); +#endif debug3_f("session request sent"); @@ -1953,7 +2505,7 @@ mux_client_request_session(int fd) if (mux_client_read_packet(fd, m) != 0) { error_f("read from master failed: %s", strerror(errno)); sshbuf_free(m); - return -1; + goto fail; } if ((r = sshbuf_get_u32(m, &type)) != 0 || @@ -1974,20 +2526,29 @@ mux_client_request_session(int fd) fatal_fr(r, "parse error message"); error("Master refused session request: %s", e); sshbuf_free(m); - return -1; + goto fail; case MUX_S_FAILURE: if ((r = sshbuf_get_cstring(m, &e, NULL)) != 0) fatal_fr(r, "parse error message"); error_f("session request failed: %s", e); sshbuf_free(m); - return -1; + goto fail; default: sshbuf_free(m); error_f("unexpected response from master 0x%08x", type); - return -1; + goto fail; } muxclient_request_id++; +#ifdef WINDOWS + /* + * The master duplicated the passed handles while processing the + * request, strictly before its reply, so our copies of the passed + * pipe ends can (and must) be dropped now. + */ + mux_relay_close_passed(&relay); +#endif + if (pledge("stdio proc tty", NULL) == -1) fatal_f("pledge(): %s", strerror(errno)); platform_pledge_mux(); @@ -1995,7 +2556,16 @@ mux_client_request_session(int fd) ssh_signal(SIGHUP, control_client_sighandler); ssh_signal(SIGINT, control_client_sighandler); ssh_signal(SIGTERM, control_client_sighandler); +#ifdef WINDOWS + /* + * Do not relay SIGWINCH via kill(): w32_kill() terminates a tracked + * child process for any signal. Resizes are detected locally and + * sent to the master as MUX_C_WINSIZE by the relay pump instead. + */ + ssh_signal(SIGWINCH, control_client_sigwinch); +#else ssh_signal(SIGWINCH, control_client_sigrelay); +#endif if (options.fork_after_authentication) daemon(1, 1); @@ -2014,7 +2584,18 @@ mux_client_request_session(int fd) * the client_fd; if this one closes early, the multiplex master will * terminate early too (possibly losing data). */ - for (exitval = 255, exitval_seen = 0;;) { + exitval = 255; + exitval_seen = 0; +#ifdef WINDOWS + if (relay.active) { + int eseen = 0; + + mux_relay_pump(fd, &relay, sid, &exitval, &eseen, &rawmode); + exitval_seen = (u_int)eseen; + mux_relay_close_all(&relay); + } else +#endif + for (;;) { sshbuf_reset(m); if (mux_client_read_packet(fd, m) != 0) break; @@ -2067,6 +2648,12 @@ mux_client_request_session(int fd) fprintf(stderr, "Shared connection to %s closed.\r\n", host); exit(exitval); + + fail: +#ifdef WINDOWS + mux_relay_close_all(&relay); +#endif + return -1; } static int @@ -2117,6 +2704,10 @@ mux_client_request_stdio_fwd(int fd) char *e; u_int type, rid, sid; int r; +#ifdef WINDOWS + struct mux_relay relay; + int passed_fd[3]; +#endif debug3_f("entering"); @@ -2130,6 +2721,14 @@ mux_client_request_stdio_fwd(int fd) if (options.stdin_null && stdfd_devnull(1, 0, 0) == -1) fatal_f("stdfd_devnull failed"); +#ifdef WINDOWS + /* substitute pipes for console std fds (see relay comment above) */ + if (mux_relay_prepare(&relay, 2, passed_fd) == -1) { + error_f("cannot set up console relay"); + return -1; + } +#endif + if ((m = sshbuf_new()) == NULL) fatal_f("sshbuf_new"); if ((r = sshbuf_put_u32(m, MUX_C_NEW_STDIO_FWD)) != 0 || @@ -2143,9 +2742,15 @@ mux_client_request_stdio_fwd(int fd) fatal_f("write packet: %s", strerror(errno)); /* Send the stdio file descriptors */ +#ifdef WINDOWS + if (mm_send_fd(fd, passed_fd[0]) == -1 || + mm_send_fd(fd, passed_fd[1]) == -1) + fatal_f("send fds failed"); +#else if (mm_send_fd(fd, STDIN_FILENO) == -1 || mm_send_fd(fd, STDOUT_FILENO) == -1) fatal_f("send fds failed"); +#endif if (pledge("stdio proc tty", NULL) == -1) fatal_f("pledge(): %s", strerror(errno)); @@ -2159,7 +2764,7 @@ mux_client_request_stdio_fwd(int fd) if (mux_client_read_packet(fd, m) != 0) { error_f("read from master failed: %s", strerror(errno)); sshbuf_free(m); - return -1; + goto fail; } if ((r = sshbuf_get_u32(m, &type)) != 0 || @@ -2187,18 +2792,37 @@ mux_client_request_stdio_fwd(int fd) default: sshbuf_free(m); error_f("unexpected response from master 0x%08x", type); - return -1; + goto fail; } muxclient_request_id++; +#ifdef WINDOWS + /* the master duplicated the handles before replying; drop our copies */ + mux_relay_close_passed(&relay); +#endif + ssh_signal(SIGHUP, control_client_sighandler); ssh_signal(SIGINT, control_client_sighandler); ssh_signal(SIGTERM, control_client_sighandler); +#ifndef WINDOWS + /* not on Windows: w32_kill() would terminate a tracked child */ ssh_signal(SIGWINCH, control_client_sigrelay); +#endif /* * Stick around until the controlee closes the client_fd. */ +#ifdef WINDOWS + if (relay.active) { + u_int exitval = 0; + int exitval_seen = 0, rawmode = 0; + + mux_relay_pump(fd, &relay, sid, &exitval, &exitval_seen, + &rawmode); + mux_relay_close_all(&relay); + return 0; + } +#endif sshbuf_reset(m); if (mux_client_read_packet(fd, m) != 0) { if (errno == EPIPE || @@ -2207,6 +2831,12 @@ mux_client_request_stdio_fwd(int fd) fatal_f("mux_client_read_packet: %s", strerror(errno)); } fatal_f("master returned unexpected message %u", type); + + fail: +#ifdef WINDOWS + mux_relay_close_all(&relay); +#endif + return -1; } static void @@ -2330,6 +2960,21 @@ muxclient(const char *path) return -1; } +#ifdef WINDOWS + /* + * tty sessions need the console relay (see above), which requires a + * master that understands MUX_C_WINSIZE. Fall back to a separate + * connection when talking to a master that doesn't advertise it. + */ + if (muxclient_command == SSHMUX_COMMAND_OPEN && tty_flag && + !muxclient_tty_relay) { + debug("master does not support tty sessions over multiplexed " + "connections; opening a separate connection"); + close(sock); + return -1; + } +#endif + switch (muxclient_command) { case SSHMUX_COMMAND_ALIVE_CHECK: if ((pid = mux_client_request_alive(sock)) == 0) diff --git a/regress/pesterTests/Multiplex.Tests.ps1 b/regress/pesterTests/Multiplex.Tests.ps1 new file mode 100644 index 000000000000..dee153151d55 --- /dev/null +++ b/regress/pesterTests/Multiplex.Tests.ps1 @@ -0,0 +1,217 @@ +If ($PSVersiontable.PSVersion.Major -le 2) {$PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path} +Import-Module $PSScriptRoot\CommonUtils.psm1 -Force +$tC = 1 +$tI = 0 +$suite = "multiplex" + +Describe "E2E scenarios for connection multiplexing (ControlMaster)" -Tags "CI" { + BeforeAll { + if($OpenSSHTestInfo -eq $null) + { + Throw "`$OpenSSHTestInfo is null. Please run Set-OpenSSHTestEnvironment to set test environments." + } + + $port = $OpenSSHTestInfo["Port"] + + $testDir = Join-Path $OpenSSHTestInfo["TestDataPath"] $suite + if(-not (Test-Path $testDir)) + { + $null = New-Item $testDir -ItemType directory -Force -ErrorAction SilentlyContinue + } + #skip on ps 2 because non-interactive cmd require a ENTER before it returns on ps2 + $skip = $IsWindows -and ($PSVersionTable.PSVersion.Major -le 2) + + $controlPath = Join-Path $testDir "mux_ctl" + $sshExe = (Get-Command ssh).Source + $script:masterProc = $null + + function Start-MuxMaster + { + param([string]$MasterLog) + + $script:masterProc = Start-Process -FilePath $sshExe ` + -ArgumentList "-M", "-N", "-S", "`"$controlPath`"", "test_target" ` + -WindowStyle Hidden -RedirectStandardError $MasterLog -PassThru + + # wait until the master answers control requests + $deadline = (Get-Date).AddSeconds(30) + while ((Get-Date) -lt $deadline) + { + ssh -S $controlPath -O check test_target 2>$null + if ($LASTEXITCODE -eq 0) { return $true } + if ($script:masterProc.HasExited) { return $false } + Start-Sleep -Milliseconds 500 + } + return $false + } + + function Stop-MuxMaster + { + if ($script:masterProc -eq $null) { return } + ssh -S $controlPath -O exit test_target 2>$null + if (-not $script:masterProc.WaitForExit(10000)) + { + Stop-Process -Id $script:masterProc.Id -Force -ErrorAction SilentlyContinue + } + $script:masterProc = $null + } + } + + AfterAll { + Stop-MuxMaster + } + + BeforeEach { + $stderrFile=Join-Path $testDir "$tC.$tI.stderr.txt" + $stdoutFile=Join-Path $testDir "$tC.$tI.stdout.txt" + $logFile = Join-Path $testDir "$tC.$tI.log.txt" + } + AfterEach {$tI++;} + + Context "$tC - mux master lifecycle and sessions" { + BeforeAll {$tI=1} + AfterAll{$tC++} + + It "$tC.$tI - master starts and answers -O check" -skip:$skip { + Start-MuxMaster -MasterLog $logFile | Should Be $true + iex "cmd /c `"ssh -S $controlPath -O check test_target 2> $stderrFile`"" + $LASTEXITCODE | Should Be 0 + $stderrFile | Should Contain "Master running" + } + + It "$tC.$tI - remote command through master returns output" -skip:$skip { + ssh -S $controlPath test_target echo mux-session-1234 | Set-Content $stdoutFile + $stdoutFile | Should Contain "mux-session-1234" + } + + It "$tC.$tI - exit codes propagate through master" -skip:$skip { + foreach ($i in (0,1,4,5,44)) { + ssh -S $controlPath test_target exit $i + $LASTEXITCODE | Should Be $i + } + } + + It "$tC.$tI - stdin passes through master" -skip:$skip { + iex "cmd /c `"echo mux-stdin-data | ssh -S $controlPath test_target findstr mux-stdin > $stdoutFile`"" + $stdoutFile | Should Contain "mux-stdin-data" + } + + It "$tC.$tI - sessions share the master's single TCP connection" -skip:$skip { + ssh -S $controlPath test_target echo again | Set-Content $stdoutFile + $stdoutFile | Should Contain "again" + $conns = @(Get-NetTCPConnection -OwningProcess $script:masterProc.Id -State Established -ErrorAction SilentlyContinue | Where-Object { $_.RemotePort -eq $port }) + $conns.Count | Should Be 1 + } + + It "$tC.$tI - -O forward adds a working local forwarding" -skip:$skip { + $fwdPort = 5433 + iex "cmd /c `"ssh -S $controlPath -O forward -L $($fwdPort):127.0.0.1:$port test_target 2> $stderrFile`"" + $LASTEXITCODE | Should Be 0 + # the tunnel targets the test sshd; reading its banner proves end-to-end flow + $client = New-Object System.Net.Sockets.TcpClient("127.0.0.1", $fwdPort) + $stream = $client.GetStream() + $stream.ReadTimeout = 10000 + $buf = New-Object byte[] 64 + $read = $stream.Read($buf, 0, 64) + $client.Close() + $banner = [System.Text.Encoding]::ASCII.GetString($buf, 0, $read) + $banner | Should Match "^SSH-2.0-" + } + + It "$tC.$tI - tty session multiplexes through the master" -skip:$skip { + # -tt forces a pty; on Windows this now runs over the master + # instead of falling back to a separate connection. The proof of + # multiplexing is the client's own debug output (a master session + # id is assigned and no fallback message is emitted). The remote + # command's pty output is NOT asserted here: capturing a forced + # pty's stdout under redirection is unreliable headless (the + # interactive Terminal test is skipped in CI for the same reason). + iex "cmd /c `"ssh -v -tt -S $controlPath test_target echo tty-mux-ok 2> $stderrFile`"" + $stderrFile | Should Contain "master session id" + $stderrFile | Should Not Contain "opening a separate connection" + } + + It "$tC.$tI - second master on the same ControlPath degrades gracefully" -skip:$skip { + # muxserver_listen() must detect the busy pipe, disable multiplexing + # for the second client and run its session over its own connection + iex "cmd /c `"ssh -M -S $controlPath test_target echo second-master-ok > $stdoutFile 2> $stderrFile`"" + $stdoutFile | Should Contain "second-master-ok" + $stderrFile | Should Contain "already exists, disabling multiplexing" + # first master is unaffected + ssh -S $controlPath -O check test_target 2>$null + $LASTEXITCODE | Should Be 0 + } + + It "$tC.$tI - -O exit shuts the master down" -skip:$skip { + iex "cmd /c `"ssh -S $controlPath -O exit test_target 2> $stderrFile`"" + $stderrFile | Should Contain "Exit request sent" + $script:masterProc.WaitForExit(10000) | Should Be $true + $script:masterProc = $null + # control requests must now fail + ssh -S $controlPath -O check test_target 2>$null + $LASTEXITCODE | Should Not Be 0 + } + } + + Context "$tC - graceful degradation without a master" { + BeforeAll {$tI=1} + AfterAll{$tC++} + + It "$tC.$tI - ControlPath with no master falls back to a direct connection" -skip:$skip { + ssh -S $controlPath test_target echo no-master-fallback | Set-Content $stdoutFile + $stdoutFile | Should Contain "no-master-fallback" + } + + It "$tC.$tI - ControlMaster auto without a master connects directly" -skip:$skip { + ssh -o ControlMaster=auto -o ControlPersist=no -S $controlPath test_target echo auto-ok | Set-Content $stdoutFile + $stdoutFile | Should Contain "auto-ok" + # ControlPersist=no: no master may linger + Stop-MuxMaster + } + } + + Context "$tC - ControlPersist auto-spawned master" { + BeforeAll { + $tI=1 + $cpPath = Join-Path $testDir "cp_ctl" + # ControlPersist=yes: the auto-started master persists until -O exit + # (a numeric timeout would risk expiring mid-test) + $cpOpts = "-o", "ControlMaster=auto", "-o", "ControlPersist=yes", + "-o", "ControlPath=`"$cpPath`"" + } + AfterAll { + # make sure the persistent master does not leak between runs + ssh -o ControlPath="$cpPath" -O exit test_target 2>$null + $tC++ + } + + # Windows has no fork(), so ControlPersist auto-starts a separate master + # process (which authenticates itself) and connects to it as a client. + It "$tC.$tI - first connection auto-starts a persistent master" -skip:$skip { + # Start-Process (own console) so the spawned master does not block us + $p = Start-Process -FilePath $sshExe ` + -ArgumentList ($cpOpts + @("test_target", "echo cp-first")) ` + -WindowStyle Hidden -RedirectStandardOutput $stdoutFile -PassThru + $p.WaitForExit(30000) | Should Be $true + $stdoutFile | Should Contain "cp-first" + # the master should have persisted and answer control requests + ssh -o ControlPath="$cpPath" -O check test_target 2>$null + $LASTEXITCODE | Should Be 0 + } + + It "$tC.$tI - subsequent connection reuses the persistent master" -skip:$skip { + $p = Start-Process -FilePath $sshExe ` + -ArgumentList @("-o", "ControlPath=`"$cpPath`"", "test_target", "echo cp-reuse") ` + -WindowStyle Hidden -RedirectStandardOutput $stdoutFile -PassThru + $p.WaitForExit(20000) | Should Be $true + $stdoutFile | Should Contain "cp-reuse" + } + + It "$tC.$tI - -O exit stops the persistent master" -skip:$skip { + iex "cmd /c `"ssh -o ControlPath=$cpPath -O exit test_target 2> $stderrFile`"" + $stderrFile | Should Contain "Exit request sent" + ssh -o ControlPath="$cpPath" -O check test_target 2>$null + $LASTEXITCODE | Should Not Be 0 + } + } +} diff --git a/ssh.c b/ssh.c index ea94d25106cf..bb2378ba3dd0 100644 --- a/ssh.c +++ b/ssh.c @@ -198,6 +198,17 @@ usage(void) static int ssh_session2(struct ssh *, const struct ssh_conn_info *); static void load_public_identity_files(const struct ssh_conn_info *); static void main_sigchld_handler(int); +#ifdef WINDOWS +static int ssh_controlpersist_spawn(struct ssh *); +/* ControlPersist spawn/detach primitives, in contrib/win32/win32compat/misc.c */ +intptr_t w32_spawn_control_master(char *const args[], intptr_t *ready_event); +int w32_wait_controlpersist_ready(intptr_t proc, intptr_t ready_event, + int timeout_ms); +void w32_close_handle(intptr_t h); +int w32_is_controlpersist_master(void); +void w32_signal_controlpersist_ready(void); +void w32_detach_console(void); +#endif /* ~/ expand a list of paths. NB. assumes path[n] is heap-allocated. */ static void @@ -1680,6 +1691,20 @@ main(int ac, char **av) ssh_packet_set_mux(ssh); goto skip_connect; } +#ifdef WINDOWS + /* + * No master is running. If ControlPersist is requested and we + * are not ourselves the spawned master, start one (a separate + * process, since we cannot fork) and connect to it. On failure, + * fall back to a direct, non-persistent connection. The master + * process keeps control_persist so its idle timeout applies. + */ + if (options.control_persist && !w32_is_controlpersist_master()) { + if (ssh_controlpersist_spawn(ssh)) + goto skip_connect; + options.control_persist = 0; + } +#endif } /* @@ -1883,6 +1908,72 @@ main(int ac, char **av) return exit_status; } +#ifdef WINDOWS +/* + * Windows ControlPersist. There is no fork(), so we cannot background an + * already-authenticated connection like POSIX does. Instead, when the user + * wants a persistent master and none is running yet, spawn a fresh ssh + * process to be that master: it authenticates itself (sharing our console so + * it can prompt), sets up the control socket, and detaches. We then connect + * to it as an ordinary mux client. Returns 1 if a session ran against the + * spawned master (does not return in the common case, since muxclient() + * exits), 0 if the master could not be established (caller falls back to a + * direct, non-persistent connection). + */ +static int +ssh_controlpersist_spawn(struct ssh *ssh) +{ + char **args = NULL; + intptr_t proc, ready_event; + int i, n, r, ret = 0, sock; + + /* only the auto-master, no-existing-master, session case applies */ + if (!options.control_persist || options.control_path == NULL || + w32_is_controlpersist_master() || + (options.control_master != SSHCTL_MASTER_AUTO && + options.control_master != SSHCTL_MASTER_AUTO_ASK)) + return 0; + + /* args = forced master overrides + this invocation's args (saved_av) */ + for (n = 0; saved_av[n] != NULL; n++) + ; + args = xcalloc(n + 3, sizeof(*args)); + args[0] = "-oControlMaster=yes"; + args[1] = "-oSessionType=none"; + for (i = 1; i < n; i++) /* skip saved_av[0] (argv0) */ + args[i + 1] = saved_av[i]; + args[n + 1] = NULL; + + debug_f("starting persistent mux master for %s", options.control_path); + proc = w32_spawn_control_master(args, &ready_event); + free(args); + if (proc == -1) { + error("could not start persistent control master"); + return 0; + } + + /* + * Wait for the master to signal (via the named ready event) that it + * has authenticated and its control socket is up, then connect. + * Generous timeout: interactive auth may prompt. + */ + if ((r = w32_wait_controlpersist_ready(proc, ready_event, + 120 * 1000)) == 1) { + if ((sock = muxclient(options.control_path)) >= 0) { + /* SSHMUX_COMMAND_PROXY returns the socket */ + ssh_packet_set_connection(ssh, sock, sock); + ssh_packet_set_mux(ssh); + ret = 1; + } + } else + debug_f("persistent master %s", r == 0 ? + "exited before it was ready" : "was not ready in time"); + w32_close_handle(ready_event); + w32_close_handle(proc); + return ret; +} +#endif /* WINDOWS */ + static void control_persist_detach(void) { @@ -2238,7 +2329,7 @@ ssh_session2_setup(struct ssh *ssh, int id, int success, void *arg) term = getenv("TERM"); client_session2_setup(ssh, id, tty_flag, options.session_type == SESSION_TYPE_SUBSYSTEM, term, - NULL, fileno(stdin), command, environ); + NULL, fileno(stdin), command, environ, NULL); } /* open new channel for a session */ @@ -2307,6 +2398,22 @@ ssh_session2(struct ssh *ssh, const struct ssh_conn_info *cinfo) if (!ssh_packet_get_mux(ssh)) muxserver_listen(ssh); +#ifdef WINDOWS + /* + * A persistent master spawned by ssh_controlpersist_spawn() shares the + * console with the foreground process so it can prompt for auth. Now + * that authentication is done and the control socket is up, signal + * readiness to the waiting foreground client and detach from the + * console so we survive the terminal and don't contend with that + * client. The idle timeout (set_control_persist_exit_time) then + * governs our lifetime. Windows has no fork(), so the POSIX + * backgrounding below does not apply. + */ + if (w32_is_controlpersist_master() && muxserver_sock != -1) { + w32_signal_controlpersist_ready(); + w32_detach_console(); + } +#else /* * If we are in control persist mode and have a working mux listen * socket, then prepare to background ourselves and have a foreground @@ -2330,6 +2437,7 @@ ssh_session2(struct ssh *ssh, const struct ssh_conn_info *cinfo) need_controlpersist_detach = 1; options.fork_after_authentication = 1; } +#endif /* * ControlPersist mux listen socket setup failed, attempt the * stdio forward setup that we skipped earlier.