Skip to content

Add client-side ControlMaster (connection multiplexing) support#863

Open
duncm wants to merge 10 commits into
PowerShell:latestw_allfrom
duncm:controlmaster-mux
Open

Add client-side ControlMaster (connection multiplexing) support#863
duncm wants to merge 10 commits into
PowerShell:latestw_allfrom
duncm:controlmaster-mux

Conversation

@duncm

@duncm duncm commented Jul 14, 2026

Copy link
Copy Markdown

Preface

Except for this preface (which I'm writing myself, hi everyone!) this pull request and diff was written entirely by Claude. I don't expect it to be merged without proper review (if at all) and I recommend appropriate caution for anyone wanting to test it.

Why did I do this? I was starting to get impatient at the need to reauthenticate. But it's working well enough that it seemed appropriate to submit a PR. The text below is a tad verbose but I figured this was preferable given that the implementation differs from POSIX in multiple ways.

Summary

This PR implements ControlMaster / ControlPath connection multiplexing in ssh.exe, which the project scope has historically excluded. With it, the following works today on Windows:

# terminal 1: establish the master (stays in foreground)
ssh -M -N -S C:\Users\me\.ssh\ctl-%C user@host

# terminal 2+: sessions reuse the master's TCP connection & authentication
ssh -S C:\Users\me\.ssh\ctl-%C user@host hostname   # command
ssh -S C:\Users\me\.ssh\ctl-%C user@host            # interactive shell (tty)
ssh -S C:\Users\me\.ssh\ctl-%C -O check user@host
ssh -S C:\Users\me\.ssh\ctl-%C -O forward -L 8080:localhost:80 user@host
ssh -S C:\Users\me\.ssh\ctl-%C -O exit user@host

Interactive tty sessions multiplex too (via a client-side console relay — see the dedicated section below), so a shared master gives you real interactive shells, not just command execution. ControlPersist works as well — an auto-started background master keeps the connection warm:

# with ControlMaster auto + ControlPersist in ssh_config, the first
# connection transparently starts a persistent master; later ones reuse it
ssh user@host          # no visible master to manage
ssh user@host          # reuses the same connection

ControlMaster auto in ssh_config also works for scp/sftp invocations that shell out to ssh.exe.

Why this was hard, and the approach

Upstream mux relies on two POSIX primitives with no direct Windows equivalent:

  1. Unix domain socket servers. The compat layer only ever implemented the client side of AF_UNIX (a CreateFileW on a pipe name, used for ssh-agent); bind()/listen()/accept() returned ENOTSUP.
  2. SCM_RIGHTS fd passing. The mux client hands its stdin/stdout/stderr to the master over the control socket. Windows AF_UNIX (afunix.sys) has no ancillary data at all, so even a "real" Unix-socket implementation would not solve this.

Because SCM_RIGHTS is unavailable either way, this PR uses named pipes as the control-channel transport rather than afunix.sys — consistent with how this port already handles ssh-agent, and it brings two things Unix sockets on Windows don't have: security descriptors on the listener and GetNamedPipeClientProcessId() for peer verification.

What the change consists of

Piece Where Description
AF_UNIX server emulation fileio.c, w32fd.c bind() creates the first pipe instance (FILE_FLAG_FIRST_PIPE_INSTANCE gives EADDRINUSE semantics), listen() arms an overlapped ConnectNamedPipe, accept() hands out the connected instance and re-arms. The listener integrates with w32_select() through the same event mechanism as TCP listeners.
ControlPath → pipe name fileio.c Deterministic mapping to \\.\pipe\openssh-uds-<mangled path> (both sides derive it independently); paths already of the form \\.\pipe\... are used verbatim.
fd passing w32fd.c, monitor_fdpass.c mm_send_fd() sends {magic, pid, handle value, io type} in-band. mm_receive_fd() validates the message, cross-checks the claimed pid against GetNamedPipeClientProcessId(), requires the peer process token to be the same Windows user, then OpenProcess(PROCESS_DUP_HANDLE) + DuplicateHandle(). Received handles enter the fd table on the synchronous-io path (same classification as inherited stdio).
getpeereid() misc.c Replaces the ENOTSUP stub: succeeds iff the pipe peer runs as the same Windows user, so channel_post_mux_listener()'s uid check works unchanged.
mux.c on Windows mux.c, no-ops.c, ssh.vcxproj mux.c now compiles into ssh.exe (stubs removed). Windows-specific #ifdefs: no temp-path/link()/unlink()/umask() dance in muxserver_listen() (pipe creation is atomic), no tcgetattr().
tty session relay mux.c, channels.c, clientloop.c, ssh.c Client-side console relay so interactive sessions multiplex: pipe substitution for console fds, a relay pump loop, and a MUX_C_WINSIZE mux message negotiated via a hello extension. See the tty section below.
ControlPersist ssh.c, misc.c Auto-spawns a separate persistent master process (no fork()), which authenticates itself and detaches. See the ControlPersist section below.
Misc inc/sys/un.h, ssh.c sun_path raised to 260 (profile paths are deep).

Security model

The control pipe grants full use of the authenticated SSH connection, so access control matters:

  • Listener DACL: D:P(A;;GA;;;SY)(A;;GA;;;<owner SID>) — only SYSTEM and the creating user can connect. PIPE_REJECT_REMOTE_CLIENTS is set.
  • getpeereid() independently verifies the connecting process token's user SID (defense in depth, mirrors the Unix uid check in channels.c).
  • Before DuplicateHandle, the receiver requires (a) the fd message's claimed pid to match the pipe peer pid, and (b) that process to be the same Windows user. An elevated master will not accept handles from processes it can't verify, and DuplicateHandle across an elevation boundary fails closed.

Points I'd particularly welcome maintainer scrutiny on: the DACL string, the pid/SID verification ordering in w32_fdpass_recv(), and whether SECURITY_IDENTIFICATION on the client's CreateFileW is sufficiently restrictive.

tty sessions (client-side console relay)

Interactive (ssh -t) sessions do multiplex. This was the hard part, because Windows console handles cannot be driven across a process boundary — the master cannot read a DuplicateHandle'd CONIN (ERROR_OPERATION_ABORTED), which is why passing console handles to the master (as POSIX does via SCM_RIGHTS) fundamentally can't work.

The solution keeps the console on the client, where it already works exactly as a non-mux ssh -t drives it, and never hands it to the master:

  • For each std fd that is a console, the client substitutes a pipe and hands the master-facing pipe end to mm_send_fd() in place of the real fd. The master drives that pipe identically to redirected stdio — its data path is completely unchanged and can't tell the difference.
  • The client runs a small relay pump (mux_relay_pump) that moves bytes between its own console and the pipe ends using the port's existing in-process terminal emulation (raw mode, VT translation), while still servicing the control channel (MUX_S_TTY_ALLOC_FAIL, MUX_S_EXIT_MESSAGE) and draining remote output before exit.
  • Window size can no longer be queried by the master, so it travels over the control channel as a new MUX_C_WINSIZE message — an initial size before the session request (seeding the pty-req) and one per local resize. SIGWINCH is handled locally rather than relayed with kill() (which w32_kill() would turn into TerminateProcess of a tracked child).

This is negotiated by a hello extension (tty-relay@win32.openssh.com, provisional): a client that wants a tty only multiplexes if the master advertised it, otherwise it falls back to a separate direct connection. Unrecognized extensions are ignored on both sides per PROTOCOL.mux, so every old/new client/master pairing interoperates (verified — see below). The protocol additions are documented in a block comment atop the relay code in mux.c.

Known tty caveats (parity with a direct Windows connection, not relay-specific): exit-status of a -t session is not reliably propagated through Windows conpty (a direct ssh -t exit 42 behaves the same); resize latency is bounded by the pump's poll interval (~200 ms).

ControlPersist (auto-spawned master)

ControlPersist works. POSIX implements it by fork()-ing an already-authenticated connection into the background — impossible on Windows (no fork(), and a live SSH transport can't be handed to another process). Instead, when the user wants a persistent master and none is running, ssh auto-starts a separate master process:

  • ssh_controlpersist_spawn() launches a fresh ssh (from saved_av plus -oControlMaster=yes -oSessionType=none, marked by an env var) that shares the current console so it can prompt for authentication. The foreground process waits for the new master's control socket, then connects to it as an ordinary mux client — interactive tty included.
  • The spawned master authenticates itself, sets up the control socket, then detaches from the shared console (FreeConsole) so it survives the terminal and doesn't contend with the foreground client. Its lifetime is governed by the existing ControlPersist idle timeout (set_control_persist_exit_time), which needs no fork().
  • If the master can't be established, ssh falls back cleanly to a direct, non-persistent connection.

This means the console-sharing model handles interactive auth (password/passphrase/2FA prompts appear on your terminal before the master detaches), not just agent/key auth. The POSIX fork-based backgrounding path is compiled out on Windows.

Limitations (all fail safe, documented in code)

  • Passing socket-type fds (OPENSSH_STDIO_MODE=sock) is rejected with a clear error.
  • ssh -f (background-after-auth) doesn't return control to the invoking shell — with no fork(), daemon() keeps the master in the same process. The master itself functions (verified via -O check / -O forward / exec through an ssh -f -M master); it's a backgrounding-semantics gap, and ControlPersist (above) is the better way to get a persistent shared master anyway.
  • ssh -O stop, -O cancel, -O proxy compile and follow upstream code paths but have had lighter testing than the flows above.

Testing done

Windows 11 x64, binaries from this branch, against sshd.exe (also from this branch) on 127.0.0.1:2222:

  • -M -N -S <path> master establishes; control pipe appears with the expected name and DACL.
  • -O check returns the master pid.
  • Multiple sequential/concurrent exec sessions through the master: stdout, stderr, and exit status all propagate correctly; stdin round-trips ('data' | ssh -S ... "$input").
  • -O forward -L 12345:127.0.0.1:2222 adds a live forwarding to the running master; the tunnel serves traffic (verified by reading the SSH banner through it).
  • Exactly one established TCP connection to the server while multiple sessions run.
  • -O exit shuts the master down and the pipe name disappears.
  • Long ControlPath (>108 chars, the old sun_path limit) works; over-long paths fail with ENAMETOOLONG rather than truncating.

tty sessions over mux (manual, nested-conpty harness)

  • Interactive session multiplexes: -v shows a master session id (not a fallback); keystroke round-trip through Read-Host returns the typed value.
  • Initial window size: remote mode con inside the mux session matches the outer pty (120×30) — proves MUX_C_WINSIZE → pty-req.
  • Ctrl+C through the relay interrupts a remote ping -t without killing the client; the session continues.
  • Killing the master mid-session drains remote output, prints "Shared connection … closed.", and exits 255 with the console restored.

ControlPersist

  • First ControlMaster=auto + ControlPersist connection auto-starts a master; the foreground returns promptly while the master persists.
  • Subsequent connections reuse it over the one shared TCP connection; -O check reports it; an interactive tty session multiplexes through it.
  • The master self-exits after the idle timeout (verified with ControlPersist=5), and -O exit tears it down immediately.
  • An unreachable target falls back to a direct connection in a few seconds — no hang.

Interop matrix (with a stashed pre-change ssh.exe)

Client Master tty non-tty
new new multiplexes via relay multiplexes
new old falls back to a direct connection (post-hello gate) multiplexes
old new falls back (old client's own gate; ignores the extension) multiplexes

All four cells verified; masters stay healthy after a peer falls back.

Automated tests

regress/pesterTests/Multiplex.Tests.ps1 (new, tagged CI like its siblings) covers the matrix end-to-end against the standard test_target environment: master startup + -O check, command output and exit-code propagation, stdin pass-through, the single-shared-TCP-connection property, -O forward with a live tunnel (banner read through the forwarded port), tty session multiplexing (a -tt session gets a master session id and does not fall back), second-master graceful degradation on a busy ControlPath, -O exit, direct-connection fallback when no master exists (explicit ControlPath and ControlMaster=auto), and a ControlPersist context (auto-start a persistent master, reuse it, -O exit). 14/14 passing under Pester 3.4.0 — the version Run-OpenSSHE2ETest uses. CI runs with pipe stdio, so the console-relay data path and interactive-auth prompting are exercised by the manual tests above; happy to wire up upstream's regress/multiplex.sh too if maintainers prefer.

Follow-up work (out of scope here)

  1. Socket-fd passing with WSADuplicateSocket (needed for -O stdio-forward from socket stdio).
  2. Finalizing the tty-relay@win32.openssh.com extension name with maintainers.

Appendix: why the console relay lives on the client

The obvious approach — pass the console handles to the master and let it drive the terminal, as POSIX does — was prototyped and rejected. Isolation pinned down why: a direct ssh -t works and a mux non-tty session works, but mux + tty hung, because pipes/files are real cross-process kernel objects while console buffers are not (a ReadConsoleInputW on a DuplicateHandle'd CONIN in the un-attached master returns 0 with ERROR_OPERATION_ABORTED). AttachConsole on the master is also a dead end — one console per process, so it can't serve concurrent tty sessions. Hence the client keeps its console and relays over pipes.


🤖 Generated with Claude Code

duncm and others added 9 commits July 12, 2026 10:21
Implements ssh ControlMaster/ControlPath/ControlSlave multiplexing on
Windows, which has historically been out of scope for this port. The
POSIX mux design depends on two primitives Windows lacks: Unix domain
socket servers and SCM_RIGHTS file descriptor passing. This change
provides Windows-native equivalents in the compat layer and compiles
mux.c into ssh.exe:

- AF_UNIX server emulation over named pipes (fileio.c, w32fd.c):
  bind()/listen()/accept() on AF_UNIX stream sockets are implemented
  with CreateNamedPipe + overlapped ConnectNamedPipe, integrated with
  the w32_select event loop the same way TCP listeners are. ControlPath
  values are mapped deterministically onto pipe names
  (\\.\pipe\openssh-uds-<mangled path>); explicit \\.\pipe\ paths are
  used verbatim. The listener pipe is created with a DACL restricted to
  SYSTEM and the current user, with PIPE_REJECT_REMOTE_CLIENTS.

- File descriptor passing (w32fd.c, monitor_fdpass.c): mm_send_fd()
  transmits the sender's pid and raw handle value in-band;
  mm_receive_fd() verifies the claimed pid against
  GetNamedPipeClientProcessId, verifies the peer process token belongs
  to the same Windows user, and then duplicates the handle with
  OpenProcess(PROCESS_DUP_HANDLE) + DuplicateHandle. Received handles
  default to the synchronous io path, matching how inherited stdio
  handles are classified.

- getpeereid() (misc.c): now succeeds iff the pipe peer process runs as
  the same Windows user, so the mux listener's peer check in channels.c
  behaves as intended (defense in depth on top of the pipe DACL).

- mux.c is compiled into ssh.exe; the temp-path/link/unlink/umask
  socket setup and tcgetattr are #ifdef'd for Windows. sun_path is
  raised to 260 since ControlPath commonly lives under deep profile
  paths.

Limitations (documented, fail safe):
- tty sessions are not multiplexed; ssh transparently falls back to a
  separate connection (the master would have to drive the client's
  console - raw mode, VT input, resize - which is future work).
- ControlPersist is disabled on Windows (requires fork()); the master
  must remain in the foreground, e.g. ssh -M -N.
- Passing socket-type fds (OPENSSH_STDIO_MODE=sock) is not supported.
- Client and master must run un-elevated or both elevated;
  DuplicateHandle across an elevation boundary fails (by design).

Tested on Windows 11 x64 against sshd from this branch: master
(-M -N -S), -O check, multiple concurrent exec sessions with stdout/
stderr/exit-status propagation, stdin round-trip, -O forward with a
live tunnel, -O exit, and single shared TCP connection verified
throughout. Stale/foreign ControlPath and same-user enforcement
exercised manually.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers: master startup and -O check, remote command output and exit
code propagation through the master, stdin pass-through, single shared
TCP connection, -O forward with a live tunnel, tty fallback to a
separate connection, second-master graceful degradation on a busy
ControlPath, -O exit shutdown, and direct-connection fallback when no
master is present (explicit ControlPath and ControlMaster=auto).

Verified against Pester 3.4.0 (the version the E2E framework uses):
11/11 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the master-side half of client-side console relay support for
multiplexed tty sessions on Windows (client half follows). Because
Windows console handles are not usable across processes, the master
cannot query a mux client's terminal size, so the client will send it
explicitly; and old peers must not be disrupted.

- Hello extension "tty-relay@win32.openssh.com" (provisional): the
  Windows master advertises it in its hello; a Windows client detects
  it. Unknown extensions are already ignored with a debug log on both
  sides, so this is non-breaking in every direction (verified against
  old/new client/master pairings).

- MUX_C_WINSIZE (0x1000000e): client->master, {col,row,xpix,ypix},
  no reply. mux_master_process_winsize() stashes the size in
  struct mux_master_state and, if a tty session channel is already
  open, forwards it as a "window-change" channel request. An unknown
  type on an old master yields MUX_S_FAILURE rather than a disconnect,
  and the client only sends this once the extension is negotiated.

- client_session2_setup() gains a winsize parameter (NULL preserves the
  existing ioctl() behavior; POSIX callers pass NULL). On Windows the
  mux master passes the client-reported size, or zeros when none was
  received -- never falling back to ioctl(), which on Windows would
  return the master's own console size (w32_ioctl ignores the fd).

- channel_send_window_changes() skips mux-owned channels (ctl_chan
  != -1) on Windows for the same reason: their rfd is a relay pipe and
  ioctl() would stamp the master's console size onto them. Their
  resizes arrive via MUX_C_WINSIZE instead.

All additions are #ifdef WINDOWS or NULL-preserving; POSIX behavior is
unchanged. Existing 11 mux Pester scenarios still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ndows)

Implements the client half of multiplexed tty sessions on Windows.
Because a Windows console handle is bound to its owning process's
console and cannot be driven by the master across the process
boundary, the mux client no longer passes console handles to the
master. Instead, for each std fd that is a console, it substitutes a
pipe (which the master drives exactly 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.

- mux_relay_prepare/pump/close: for each console std fd, create a pipe,
  hand the master-facing end to mm_send_fd() in place of the real fd,
  and run a poll() loop (finite timeout, since SIGWINCH does not wake
  poll on Windows) moving console<->pipe bytes while still handling the
  control-channel packets (MUX_S_TTY_ALLOC_FAIL / MUX_S_EXIT_MESSAGE)
  and draining remote output before exit. Applies to both session and
  stdio-forward requests, and to non-tty sessions whose stdio is a
  console (the master's cross-process console I/O is equally broken
  there). With redirected stdio (e.g. CI) no fd is a console, the relay
  stays inactive, and behavior is byte-identical to before.

- The passed pipe ends are held open until MUX_S_SESSION_OPENED, after
  which the master's duplicated handles keep them alive; failure paths
  close everything and leave the real std fds untouched so the caller
  can still fall back to a direct connection.

- SIGWINCH is caught locally (never relayed via kill(), which
  w32_kill() would turn into TerminateProcess of a tracked child) and
  sent to the master as MUX_C_WINSIZE; an initial size is sent before
  the session request to seed the pty-req.

- The pre-connect Windows tty fallback moves to after the hello
  exchange: tty sessions multiplex when the master advertised the
  tty-relay extension, and fall back to a separate connection
  otherwise, so old masters keep working.

Verified on Windows 11 x64 (nested conpty): keystroke round-trip,
initial window size matching the outer pty, Ctrl+C interrupting a
remote command, and clean drain + "Shared connection closed" when the
master is killed mid-session. POSIX and the existing 11 mux Pester
scenarios are unaffected (relay is #ifdef WINDOWS and inactive without
a console).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mux client now runs tty sessions over the master via the console
relay, so the Pester scenario that asserted a fallback to a separate
connection is updated to assert multiplexing: the session gets a master
session id and no fallback message is emitted. Exit-code propagation
through a tty is a pre-existing Windows conpty limitation (a direct
-tt connection behaves the same), so it stays covered by the existing
non-tty exit-code scenario. Console-relay keystroke/resize behavior is
verified manually (nested conpty) and documented in the PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Windows has no fork(), so ssh cannot background an already-authenticated
connection the way POSIX ControlPersist does (control_persist_detach()).
A live SSH transport also cannot be handed to another process. So on
Windows ControlPersist is implemented by auto-starting a *separate*
master process:

- When the user wants a persistent master (ControlMaster auto +
  ControlPersist) and none is running, ssh_controlpersist_spawn()
  launches a fresh ssh process from saved_av plus overrides
  (-oControlMaster=yes -oSessionType=none) marked with the environment
  variable SSH_CONTROLPERSIST_MASTER. The child shares this process's
  console so it can prompt for authentication. The foreground process
  then waits for the child's control socket to appear and connects to it
  as an ordinary mux client (interactive tty sessions included, via the
  console relay). If the child cannot be established, ssh falls back to a
  direct, non-persistent connection.

- The spawned master authenticates itself, sets up the control socket
  in ssh_session2(), then detaches from the shared console
  (w32_detach_console -> FreeConsole) so it survives the terminal and
  does not contend with the foreground client. Its lifetime is governed
  by the existing ControlPersist idle timeout (set_control_persist_exit_
  time), which needs no fork and works unchanged.

- The POSIX fork-based backgrounding block in ssh_session2() is compiled
  out on Windows; the blanket "ControlPersist is not supported" disable
  is removed.

New Windows compat helpers (misc.c): w32_spawn_control_master (spawn
sharing the console, not tracked as a child so it outlives us),
w32_process_alive, w32_close_handle, w32_is_controlpersist_master,
w32_detach_console.

Verified on Windows 11 x64 (key auth): first connection auto-starts a
master and returns promptly while the master persists; later
connections reuse it over a single TCP connection; -O check / -O exit
work; interactive tty multiplexes through the persistent master; the
master self-exits after the ControlPersist idle timeout; and an
unreachable target falls back cleanly to a direct connection without
hanging. POSIX is unaffected (all additions are #ifdef WINDOWS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Pester context covering Windows ControlPersist: a first
connection auto-starts a persistent master and returns while it
persists, a subsequent connection reuses it, and -O exit stops it. Uses
Start-Process for the auto-spawning invocations so the spawned master's
console does not block the test runner. Interactive-auth prompting and
the console-relay data path remain manual-only (CI has pipe stdio); the
existing 11 scenarios are unchanged (14 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tty scenario forced a pty (-tt) and asserted the remote command's
output in the redirected stdout. Capturing a forced pty's stdout under
redirection is unreliable in the headless CI agent (the interactive
Terminal test is skipped there for the same reason), and the assertion
flaked: it passed on the tty commit's build and failed on the next.

Assert multiplexing from the client's own debug output instead -- a
master session id is assigned and no fallback message is emitted -- which
does not depend on pty output flushing. This still fails if a tty session
wrongly falls back to a separate connection, so coverage is preserved.
Console-relay keystroke/output behavior remains manually verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No functional change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 14, 2026 06:27
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@duncm

duncm commented Jul 14, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Windows client support for SSH connection multiplexing (ControlMaster/ControlPath/ControlPersist) by emulating AF_UNIX server-side semantics over Windows named pipes, implementing Windows handle “fd passing” for mux stdio, and adding a client-side console relay to support multiplexed interactive tty sessions.

Changes:

  • Implement AF_UNIX listener semantics over named pipes (bind/listen/accept) and deterministic ControlPath → pipe name mapping for Windows.
  • Add Windows fd-passing for mux stdio using in-band {pid, handle} + DuplicateHandle, plus getpeereid() emulation for same-user verification.
  • Enable mux.c in the Windows build, add tty relay + MUX_C_WINSIZE, and add new Pester E2E tests for multiplexing and ControlPersist.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ssh.c Adds Windows ControlPersist auto-spawn logic and detaches the spawned master from the console after mux socket setup.
regress/pesterTests/Multiplex.Tests.ps1 Adds new Pester E2E tests covering mux lifecycle, sessions, tty muxing, and ControlPersist.
mux.c Enables Windows mux support, adds hello extension + MUX_C_WINSIZE, and implements client-side tty console relay for multiplexed sessions.
monitor_fdpass.c Switches mm_send_fd/mm_receive_fd to Windows implementations using DuplicateHandle.
contrib/win32/win32compat/w32fd.h Declares AF_UNIX bind/listen/accept emulation APIs.
contrib/win32/win32compat/w32fd.c Adds AF_UNIX accept/listen/bind support and implements Windows fd passing send/recv routines.
contrib/win32/win32compat/no-ops.c Removes Windows mux stubs now that mux.c is compiled.
contrib/win32/win32compat/misc.c Adds ControlPersist spawn/detach helpers and implements getpeereid() via named-pipe peer validation.
contrib/win32/win32compat/misc_internal.h Exposes w32_is_pid_same_user() for cross-module use.
contrib/win32/win32compat/inc/sys/un.h Increases sockaddr_un.sun_path capacity for Windows path depth.
contrib/win32/win32compat/fileio.c Implements AF_UNIX server emulation over named pipes and updates connect to map ControlPath to pipe names.
contrib/win32/openssh/ssh.vcxproj Builds mux.c into the Windows ssh.exe project.
clientloop.h Extends client_session2_setup signature to optionally accept a winsize override.
clientloop.c Uses provided winsize (when present) instead of always ioctl’ing in_fd for pty-req.
channels.c Skips ioctl-based resize handling for mux-owned sessions on Windows (resized via MUX_C_WINSIZE).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mux.c Outdated
Comment thread regress/pesterTests/Multiplex.Tests.ps1 Outdated
Copilot flagged two issues on the upstream PR:

- mux_client_send_winsize() only queried TIOCGWINSZ on stdout, so a tty
  session with stdout redirected (stdin the only tty) would skip sending
  MUX_C_WINSIZE and seed the pty-req with a 0x0 size. Fall back to stdin
  when stdout is not a tty.

- Fix "becase" -> "because" in a Multiplex.Tests.ps1 comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@lhecker lhecker left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes from testing this PR. I really hope we can get support for this sometime soon.

(You may notice that I'm a MSFT employee, but I'm not affiliated with this project. I'm testing it in a personal capacity.)

Comment thread ssh.c
debug_f("persistent master exited before it was ready");
break;
}
usleep(200000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sleep is a bit hacky. You can pass an event HANDLE via PROC_THREAD_ATTRIBUTE_HANDLE_LIST to the child process, tell the master about the HANDLE value via your W32_CONTROLPERSIST_ENV and make it such that this can wait on the child process immediately with WaitForMultipleObjects.

memset(&pi, 0, sizeof(pi));

/* the child reads this marker to learn it is the persistent master */
SetEnvironmentVariableW(L"" W32_CONTROLPERSIST_ENV, L"1");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally you should modify the child process environment block here. But if I search for setenv(, there are lots of other places, so this is probably fine.

debug4("fileclose - pio:%p", pio);

/* bound/listening AF_UNIX socket emulated over named pipes */
if (pio->internal.context) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing for non-null here is also a bit hacky since that just so happens to work today. A proper tag would be cleaner, but idk if modifying the struct is a good idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants