| CI | Code | OpenSpec | Security |
|---|---|---|---|
|
|
|
|
|
Network-aware SSH router. Detects your active network or VPN and automatically selects the right host, port, identity file, and jump host for each SSH connection — without touching ~/.ssh/config.
Define each logical host once with a default profile and optional per-network overrides. On every connection, sshroute detects which network you're on (VPN, office LAN, WireGuard peer, etc.) and resolves the correct SSH parameters before handing off to the real /usr/bin/ssh.
ssh myserver
→ sshroute detects: corp-vpn is active
→ resolves: 10.100.0.50:2222 via bastion.corp.internal
→ exec /usr/bin/ssh -p 2222 -i ~/.ssh/corp_key -J bastion.corp.internal 10.100.0.50
Your lab probably has at least two realities: you're either sitting at home on the LAN, or you're away and coming in over WireGuard or another VPN. The problem is ~/.ssh/config doesn't know which one you're in — so you end up with separate aliases (server-lan, server-vpn), or a jump host that only works half the time, or you just memorize IPs.
sshroute solves this by detecting your current network before every connection. When the WireGuard interface is up and the peer route exists, it connects directly to the tunnel IP. When you're on the LAN, it uses the local address. When neither is reachable, it falls back to the public hostname. One alias, three realities, zero manual switching.
It also intercepts SSH transparently — git push, rsync, scp all go through it automatically once you set up shadow mode. No wrappers, no shell functions, no thinking.
Enterprise networks are worse. You have the public internet, maybe a site-to-site VPN, maybe a personal VPN split-tunnel, and inside that you have different jump hosts depending on which environment you're targeting — dev, staging, prod, each with their own bastion and key. Keeping this straight in ~/.ssh/config means either one enormous config that breaks whenever infra changes, or you write a script that everyone on the team maintains differently.
sshroute lets you define the routing logic declaratively, keep it in a versioned YAML file, and share it across the team. The same config works for everyone — the right network is detected automatically based on what interfaces or routes are active on each machine. Keys, ports, users, and jump hosts resolve without the user having to think about it.
| Feature | ~/.ssh/config |
WireGuard-only | Teleport / Boundary | sshroute |
|---|---|---|---|---|
| Detects your current network | ❌ | ❌ | ❌ | ✅ |
| Picks the best path automatically | ❌ | ❌ | ❌ | ✅ |
| Falls back on connection failure | ❌ | ❌ | ✅ | ✅ |
| One command per host, any location | ❌ | ✅ | ✅ | |
| Config size for 10 hosts × 4 paths | 📄 ~600 lines | 📄 ~600 lines + VPN config | 📄 server-side config | 📄 ~60 lines |
| Roaming mobile devices | ✅ | ✅ | ||
| Jump host auto-chaining | -J |
➖ n/a | ✅ | ✅ |
| Works with scp / rsync / git / Ansible | ✅ | ✅ | ✅ | |
| No server-side install on targets | ✅ | ❌ | ❌ | ✅ |
| No auth server or daemon to run | ✅ | ❌ | ❌ | ✅ |
| No client agent | ✅ | ❌ | ❌ | ✅ |
| Open source, fully self-hosted | ✅ | ✅ | ✅ |
Teleport and Boundary are a different category — they add access control, audit logs, and certificate-based auth on top of routing. If that's what you need, use them. sshroute is for when you want the routing intelligence without the operational overhead of running a central auth server.
Download the latest release from GitHub Releases. Binaries are available for Linux, macOS, and Android on AMD64 and ARM64.
go install github.com/thereisnotime/sshroute@latestDownload the android_arm64 tarball from GitHub Releases, extract, and place the binary in ~/.local/bin:
mkdir -p ~/.local/bin
curl -Lo "$TMPDIR/sshroute.tar.gz" \
https://github.com/thereisnotime/sshroute/releases/latest/download/sshroute_android_arm64.tar.gz
tar -xzf "$TMPDIR/sshroute.tar.gz" -C ~/.local/bin sshroute
chmod +x ~/.local/bin/sshrouteAdd ~/.local/bin to your PATH in ~/.bashrc or ~/.profile if it isn't already:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrcAlternatively, compile from source with Termux's Go. Because the official Go toolchain doesn't publish android/arm64 binaries, set GOTOOLCHAIN=local to use what Termux ships:
GOTOOLCHAIN=local go install github.com/thereisnotime/sshroute@latestAfter installing, set the SSH binary path since Termux doesn't have /usr/bin/ssh:
# ~/.config/sshroute/config.yaml
ssh_binary: /data/data/com.termux/files/usr/bin/sshOr via environment variable: export SSHROUTE_SSH=$(which ssh)
docker run --rm -v ~/.config/sshroute:/root/.config/sshroute \
ghcr.io/thereisnotime/sshroute networkpodman run --rm -v ~/.config/sshroute:/root/.config/sshroute \
ghcr.io/thereisnotime/sshroute networkOn SELinux-enabled systems (Fedora, RHEL, etc.) add :Z to the volume flag:
podman run --rm -v ~/.config/sshroute:/root/.config/sshroute:Z \
ghcr.io/thereisnotime/sshroute networkInstall sshroute as ssh earlier in your $PATH. All SSH calls — from your terminal, git, rsync, scp — are intercepted automatically. Hosts not in your config pass through to /usr/bin/ssh unchanged.
mkdir -p ~/.local/bin
ln -s $(which sshroute) ~/.local/bin/ssh
# Add to ~/.bashrc or ~/.zshrc if not already present:
export PATH="$HOME/.local/bin:$PATH"# Add a host with a default profile
sshroute add myserver --host myserver.example.com --user alice --key ~/.ssh/id_ed25519
# Add a VPN-specific override
sshroute add myserver --network vpn --host 10.8.0.50 --port 2222 --jump bastion.vpn
# Connect — network is detected automatically
sshroute connect myserver
# Preview the resolved command without running it
sshroute connect myserver --dry-run
# See what network is currently active
sshroute networkThese flags apply to every command:
| Flag | Env var | Default | Description |
|---|---|---|---|
--config |
SSHROUTE_CONFIG |
~/.config/sshroute/config.yaml |
Config file path |
-o, --output |
table |
Output format: table, json, yaml |
|
-v, --verbose |
SSHROUTE_VERBOSE=1 |
false |
Debug logging to stderr |
--dry-run |
false |
Print resolved SSH command without executing |
Create a starter config file with commented examples. Fails if the file already exists.
| Flag | Default | Description |
|---|---|---|
--force |
false |
Overwrite an existing config file |
Detect the active network, resolve SSH parameters for alias, and exec the real SSH binary. Any extra arguments after the alias are passed through to SSH unchanged.
List all configured hosts and the SSH parameters that would be used on the current network. Supports -o table|json|yaml.
Add a host or update an existing one. Omitted flags keep their current value. Run multiple times with different --network values to build per-network overrides.
| Flag | Default | Description |
|---|---|---|
--host |
Hostname or IP address | |
--port |
22 |
SSH port |
--user |
SSH username | |
--key |
Path to identity file (supports ~) |
|
--jump |
Jump host — passed as -J to SSH |
|
--network |
default |
Network profile to write the params into |
Remove all profiles for alias from the config.
Print the name of the currently detected network (or default if none match).
List all configured networks with their priority, check rules, and current active state. Supports -o table|json|yaml.
Run every check for network name and print pass/fail per rule. Useful for debugging detection logic.
Print the resolved path to the config file.
Open the config file in $EDITOR (falls back to nano). Creates the file and its parent directory if they do not exist.
Print the SSH parameters that would be used for alias on the current network. Useful for debugging and scripting. Use --network <name> to override the detected network. Supports -o table|json|yaml.
| Flag | Default | Description |
|---|---|---|
--network |
auto-detect | Network profile to resolve against |
Copy files to or from a configured host using scp with the same resolved parameters (key, port, jump) as connect. Use <alias>:<path> syntax for remote paths:
sshroute copy myserver ./local.txt myserver:/remote/path/
sshroute copy myserver myserver:/remote/file.txt ./local/The SSHROUTE_SCP environment variable overrides the scp binary used.
Print the version, git commit, build date, and Go runtime info.
Default location: ~/.config/sshroute/config.yaml
networks:
corp-vpn:
priority: 10 # lower = checked first
checks:
- type: interface
match: wg0
- type: route
match: 10.100.0.0
office:
priority: 20
checks:
- type: ping
host: 192.168.1.1
timeout: 500ms
hosts:
myserver:
default: # required — used when no network matches
host: myserver.example.com
port: 22
user: alice
key: ~/.ssh/id_ed25519
corp-vpn:
host: 10.100.0.50
port: 2222
key: ~/.ssh/corp_key
jump: bastion.corp.internal
office:
host: 192.168.1.50Every host must have a default profile. Network profiles only need to specify fields that differ from the default — unset fields inherit from default.
Networks are evaluated in priority order (lowest value first). Alphabetical order breaks ties. The first network whose checks all pass is used; if none match, default applies.
| Check type | Passes when | Required fields |
|---|---|---|
route |
Subnet/IP appears in the kernel routing table | match |
interface |
Named interface exists and is operationally up | match |
ping |
Host responds to ICMP echo within timeout | host, timeout (optional, default 2s) |
exec |
Shell command exits with code 0 | command |
Multiple checks within one network definition use AND logic — all must pass.
Ready-to-use config files are in examples/:
| File | Use case |
|---|---|
basic.yaml |
Single host, VPN vs public fallback |
multi-network.yaml |
Office LAN, corp VPN, remote VPN, public |
wireguard-backconnect.yaml |
WireGuard peer that backconnects to you |
jump-hosts.yaml |
Different bastions per network |
multi-zone-roaming.yaml |
Multi-zone homelab with WireGuard gateway and roaming mobile devices |
In-depth guides are in docs/:
| Guide | Description |
|---|---|
| Homelab setup | Multi-zone homelab with WireGuard, jump hosts, NAS, k3s nodes |
| Multi-zone roaming | Multiple LANs, WireGuard gateway, mobile devices that roam between networks |
| Corporate / multi-environment | Dev/staging/prod with per-environment bastions and VPN detection |
| Shadow mode | Transparent SSH replacement — git, rsync, scp, Ansible |
| Shell completion | Dynamic alias completion for bash, zsh, fish |
| Scripting and automation | Using resolve and copy in scripts and CI pipelines |
All list commands support multiple output formats:
sshroute list # table (default)
sshroute list -o json # JSON — for scripting
sshroute list -o yaml # YAML
sshroute network list -o jsonGet the software — download a pre-built binary from Releases, install with go install github.com/thereisnotime/sshroute@latest, or build from source.
Feedback and bug reports — open an issue on GitHub Issues. Use the bug report template for unexpected behaviour and the feature request template for ideas.
Contributing — see CONTRIBUTING.md for how to set up the project, run tests, and open a pull request. Security vulnerabilities should be reported privately via GitHub Security Advisories.
git clone git@github.com:thereisnotime/sshroute.git
cd sshroute
just build # outputs bin/sshroute
just build-all # cross-compile linux/darwin × amd64/arm64
just test # run tests with race detector
just install # go install with version ldflags injected