Skip to content

Repository files navigation

SSH Client Relay

ssh-client-relay lets a client delegate selected SSH connections to another SSH-reachable machine. The relay machine runs the final SSH client and can reuse credentials or a ControlMaster socket that exists only on that machine.

client application -> local ssh-client-relay -> SSH to relay
                   -> SSH client on relay -> final SSH server

All other destinations continue to use the client's normal OpenSSH executable. This makes the wrapper suitable as a global SSH runtime for applications such as VS Code Remote SSH while changing the route for only one configured target.

How it works

The client wrapper sends a protocol marker, argument count, and NUL-delimited SSH arguments to a fixed helper on the relay. The helper reconstructs the exact argument array without evaluating it as shell source, then executes the relay's /usr/bin/ssh. After the argument frame, the same channel transports the inner SSH process's stdin and stdout.

Dynamic forwarding needs one additional bridge. An inner -D PORT listener is created on the relay, while applications such as VS Code expect it on the client. The wrapper therefore adds an outer -L PORT:127.0.0.1:PORT forward so the client-local port reaches the relay-local SOCKS listener.

This requires two independent connections:

  1. The client must be able to SSH to the relay.
  2. The relay must be able to SSH to the final server.

The second connection may reuse a relay-owned ControlMaster, but that is not a requirement.

Measured performance and methodology are documented in BENCHMARKS.md.

Before deployment, review the security model and known limitations. The relay is a fully trusted machine, not an end-to-end opaque transport.

Requirements

  • Bash and OpenSSH on the relay
  • A working, preferably non-interactive SSH login from client to relay
  • A working SSH configuration or route from relay to the final server
  • On a Linux client: Bash, Perl, OpenSSH ssh and scp, and install
  • On a Windows client: Windows PowerShell 5.1 or later and Windows OpenSSH

The relay must run Linux because the helper uses Bash and the intended authentication reuse mechanism is an OpenSSH ControlMaster Unix socket. The client may run Linux or Windows; WSL is not required for the Windows client.

Install on Linux

Supply the relay's SSH name, the client-facing target alias, and the target name understood by the relay:

./install.sh RELAY_HOST TARGET_ALIAS TARGET_HOST

For example:

./install.sh ssh-relay compute compute.example.org

Environment variables are also supported:

RELAY_HOST=ssh-relay \
TARGET_ALIAS=compute \
TARGET_HOST=compute.example.org \
./install.sh

The installer creates:

  • ~/.local/bin/ssh-client-relay on the client
  • ~/.config/ssh-client-relay/config on the client
  • ~/.local/bin/ssh-client-relay-helper on the relay

RELAY_HOST must resolve through DNS or the client's SSH configuration. If REMOTE_HELPER is overridden, it must be a simple path relative to the relay user's home directory, such as .local/bin/ssh-client-relay-helper. Absolute paths, traversal components, whitespace, and shell metacharacters are rejected.

Install on Windows

Run Windows PowerShell from the repository directory. If script execution is restricted, use a process-scoped bypass:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install-windows.ps1 `
    ssh-relay compute compute.example.org

The Windows installer:

  • Compiles windows\SshClientRelay.cs into a native Windows-launchable .NET console executable using PowerShell's built-in C# compiler.
  • Writes %USERPROFILE%\.config\ssh-client-relay\config.windows.
  • Copies the shared Bash helper to the Linux relay.

It installs the client executable at:

%USERPROFILE%\bin\ssh-client-relay.exe

No WSL, Visual Studio, separate .NET SDK, Python, or third-party package is required. The Windows client uses ssh.exe and scp.exe from Windows OpenSSH.

During an upgrade, the installer validates a staged executable and waits up to 30 seconds for active relay processes to exit before replacing the installed file atomically. It never kills VS Code or removes the working executable on a failed upgrade. Close active remote windows before upgrading, or adjust the wait:

.\install-windows.ps1 ssh-relay compute compute.example.org `
    -WaitForExitSeconds 120

Verify

Using the example configuration:

~/.local/bin/ssh-client-relay -O check compute
~/.local/bin/ssh-client-relay compute hostname
printf 'stream-test\n' | ~/.local/bin/ssh-client-relay compute \
    'read line; printf "%s\n" "$line"'

A non-target host is dispatched directly to /usr/bin/ssh:

~/.local/bin/ssh-client-relay -G ssh-relay

VS Code on Linux

Complete the following setup before changing remote.SSH.path.

1. Confirm where VS Code runs

This configuration is for a VS Code client process running on Linux. The path in remote.SSH.path is resolved by the client process. A Windows VS Code client cannot execute /home/your-user/.local/bin/ssh-client-relay; it needs the Windows client executable instead.

2. Configure the client-to-relay connection

The relay name passed to install.sh must work with the client's normal SSH. For example, the Linux client's ~/.ssh/config can contain:

Host ssh-relay
    HostName relay.example.com
    User relay-user
    Port 22
    IdentityFile ~/.ssh/id_ed25519

Verify the outer connection independently:

/usr/bin/ssh ssh-relay true
/usr/bin/ssh -o BatchMode=yes ssh-relay true

The second command should succeed without a password or interactive MFA prompt. If it does not, configure a key or a separate ControlMaster for the relay. Any authentication prompt on this outer connection will otherwise occur whenever VS Code starts a relayed connection.

3. Configure the relay-to-target connection

On the relay machine, configure the final server in ~/.ssh/config. For example, ssh-relay:~/.ssh/config needs an entry equivalent to:

Host compute.example.org compute
    HostName compute.example.org
    User remote-user
    ControlMaster auto
    ControlPath ~/.ssh/controlmasters/%r@%h:%p
    ControlPersist 48h

Create the socket directory once:

ssh ssh-relay 'mkdir -p ~/.ssh/controlmasters && chmod 700 ~/.ssh/controlmasters'

Then log in from the relay and complete any password or MFA authentication. An interactive session, service, or command such as the following can establish the master:

ssh -t ssh-relay 'ssh compute'

After disconnecting that interactive session, confirm that the persistent master remains available:

ssh ssh-relay 'ssh -O check compute'

The expected result contains Master running. The relay can connect without a ControlMaster, but then every new VS Code connection may require the target's normal authentication.

4. Install and verify the relay wrapper

Run the installer on the Linux client:

./install.sh ssh-relay compute compute.example.org

Before involving VS Code, all of these commands must work:

~/.local/bin/ssh-client-relay -O check compute
~/.local/bin/ssh-client-relay compute hostname
~/.local/bin/ssh-client-relay -G compute | grep -E '^(hostname|user|port) '

The first command should report the relay's master as running. The second should print the final server's hostname. The third should show the final server's effective SSH configuration as evaluated on the relay.

Also verify that an unrelated host still uses local SSH:

~/.local/bin/ssh-client-relay -G ssh-relay

5. Make the target discoverable in VS Code

VS Code reads the client's SSH config to populate Remote-SSH: Connect to Host. Add at least a host declaration to the Linux client's ~/.ssh/config:

Host compute
    HostName compute.example.org

The connection details used for the final hop still come from the relay's SSH configuration. This local entry primarily makes the target visible to VS Code and ensures that either the alias or canonical hostname triggers dispatch.

6. Set the VS Code SSH runtime

In the Linux VS Code user settings.json, set the absolute path:

"remote.SSH.path": "/home/your-user/.local/bin/ssh-client-relay",
"remote.SSH.useExecServer": true

Although remote.SSH.path is global, the wrapper relays only the configured target alias or canonical hostname. Other hosts continue through /usr/bin/ssh on the client.

If VS Code Settings Sync is shared with a Windows client, keep the executable path local to each platform:

"settingsSync.ignoredSettings": [
    "remote.SSH.path"
]

Merge remote.SSH.path into an existing settingsSync.ignoredSettings array rather than creating a second array. The remote.SSH.useExecServer value can be synced because true is appropriate on both platforms.

Reload VS Code, run Remote-SSH: Connect to Host, and select the configured target alias. If connection fails, inspect View: Output and select Remote - SSH; first rerun the commands in steps 2 through 4 outside VS Code to identify whether the failed hop is client-to-relay or relay-to-target.

VS Code on Windows

Complete these steps before changing remote.SSH.path.

1. Configure Windows-to-relay SSH

Add the Linux relay to %USERPROFILE%\.ssh\config:

Host ssh-relay
    HostName relay.example.com
    User relay-user
    Port 22
    IdentityFile ~/.ssh/id_ed25519

From PowerShell, verify both interactive and non-interactive access:

ssh.exe ssh-relay true
ssh.exe -o BatchMode=yes ssh-relay true

The second command should complete without a password or MFA prompt. This is the outer connection. It is independent of the relay's ControlMaster for the final server.

2. Prepare relay-to-target SSH

Configure and establish the target ControlMaster on the Linux relay exactly as described in steps 3 and 4 of the Linux VS Code section. From PowerShell, the key verification is:

ssh.exe ssh-relay "ssh -O check compute"

It should report Master running before VS Code is started.

3. Run the Windows installer

In Windows PowerShell, from this repository:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install-windows.ps1 `
    ssh-relay compute compute.example.org

The installer itself connects to the relay twice to deploy the helper. Complete any outer-hop authentication prompts during installation.

4. Verify the executable outside VS Code

Run all checks from PowerShell:

& "$env:USERPROFILE\bin\ssh-client-relay.exe" -O check compute
& "$env:USERPROFILE\bin\ssh-client-relay.exe" compute hostname
& "$env:USERPROFILE\bin\ssh-client-relay.exe" -G compute
& "$env:USERPROFILE\bin\ssh-client-relay.exe" -G ssh-relay

The first three commands use the Linux relay. The last command demonstrates that an unrelated destination still goes directly through Windows OpenSSH.

5. Make the target visible to VS Code

Add the target alias to %USERPROFILE%\.ssh\config if it is not already present:

Host compute
    HostName compute.example.org

This entry makes compute appear in Remote-SSH: Connect to Host. The final connection settings and credentials still come from the Linux relay.

6. Set the Windows VS Code SSH runtime

In the Windows VS Code user settings.json, set:

"remote.SSH.path": "C:\\Users\\your-user\\bin\\ssh-client-relay.exe",
"remote.SSH.useExecServer": true

Reload VS Code and select compute from Remote-SSH: Connect to Host. The path must point to the .exe, not the PowerShell installer or C# source file.

When the same Settings Sync account is used on Linux and Windows, also add remote.SSH.path to settingsSync.ignoredSettings as shown in the Linux section. Otherwise one platform can overwrite the other platform's executable path. On failure, Remote SSH may silently fall back to ordinary ssh, which causes the target to authenticate directly instead of using the relay.

Troubleshooting VS Code

Start by testing each layer outside VS Code:

/usr/bin/ssh -o BatchMode=yes ssh-relay true
ssh ssh-relay 'ssh -O check compute'
~/.local/bin/ssh-client-relay -O check compute
~/.local/bin/ssh-client-relay compute true

For a Windows client, run the equivalent checks in PowerShell with ssh.exe and %USERPROFILE%\bin\ssh-client-relay.exe.

Common failure signatures:

  • A password or MFA prompt for the relay account means the outer client-to-relay connection is not non-interactive.
  • A password or MFA prompt for the target means the relay-to-target ControlMaster is absent, expired, or unable to open another channel.
  • A log saying the configured SSH path is not a valid binary, followed by a plain ssh command, means remote.SSH.path is wrong for the client OS.
  • Repeated Installation already in progress, followed by ExhaustedRetries, can come from the legacy VS Code bootstrap colliding with a legitimately running server. Keep remote.SSH.useExecServer set to true. Do not delete a remote lock until confirming no live VS Code server owns it.

The relay's own version is printed with --version. VS Code probes SSH runtimes with -V; the wrapper intentionally passes that non-target invocation to the underlying OpenSSH client, so seeing an OpenSSH version there is normal.

SSHFS

The Linux installer also provides ~/.local/bin/ssh-client-relay-sshfs. It runs SSHFS with ssh-client-relay as the SSH transport, so the SFTP process is created on the Linux relay and can reuse the relay's target ControlMaster.

client FUSE mount -> relayed SSH stream -> relay ControlMaster -> target SFTP

After installing and verifying the normal relay connection, mount a remote directory with:

mkdir -p ~/remote-mount
ssh-client-relay-sshfs \
    -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 \
    compute.example.org:/remote/path ~/remote-mount

Run it in the foreground when a service manager owns the process:

ssh-client-relay-sshfs -f \
    -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 \
    compute.example.org:/remote/path ~/remote-mount

Unmount with the platform's normal FUSE command:

fusermount3 -u ~/remote-mount

The helper accepts normal SSHFS arguments and adds only the ssh_command option. Set SSH_CLIENT_RELAY_BIN to override the installed relay executable. The target alias or hostname must match the target configured by install.sh.

Before mounting, test a real target session rather than only checking the ControlMaster process:

ssh-client-relay compute.example.org true

Each active SSHFS mount consumes a target SFTP subsystem session on the relay's ControlMaster. A mount fails if the relay master is absent, saturated, or no longer accepts new channels.

License

Licensed under the Apache License 2.0.

Development

Run the local Linux tests with:

tests/test-linux.sh

Run the Windows tests from PowerShell with:

tests\test-windows.ps1

GitHub Actions runs both suites on every push and pull request. The tests use fake local SSH transports and require no credentials or network hosts.

Show the installed client version with:

ssh-client-relay --version

See CHANGELOG.md and SECURITY.md for release and vulnerability-reporting information. Contributions are described in CONTRIBUTING.md.

About

Delegate selected SSH connections to a trusted Linux relay and reuse relay-owned OpenSSH ControlMaster sessions.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages