English | 中文
Declarative SSH tunnels with auto-reconnect. Define your port forwards once in TOML, start one service, and they all come up — reconnecting automatically when the link drops.
Cross-platform (Linux, macOS, Windows). Written in Rust on top of russh.
Reach for this when ssh -L 3306:127.0.0.1:3306 db.example.com has grown into
"I have five of those, my laptop sleeps, my Wi-Fi flakes, and I want them all
back when I open the lid."
- Declarative: tunnels live in
config.toml, not in shell history or terminal panes. - No host config duplication: host info (
HostName/User/Port/IdentityFile) is read straight from~/.ssh/config— you reference aliases. - ProxyJump aware: chain through bastions defined in
~/.ssh/config— using alias-only references, public-key authentication, and strictknown_hostschecks for targets and jumps. See docs/configuration.md §3.4. - Auto-reconnect: compatible tunnels share one SSH session; a dropped route reconnects with jittered backoff without disturbing other routes.
- Both directions in one schema: local-to-remote (
ssh -L) and remote-to-local (ssh -R). - Foreground or daemon:
startattaches to the terminal,start -Ddetaches;stop/restart/statustalk to the running process via IPC.
1. Run or install
Run directly with uvx (recommended, no installation required):
uvx ssh-channels-hub --helpOr install it with pip inside an activated virtual environment:
pip install ssh-channels-hub
ssh-channels-hub --helpThe wheel installs the same ssh-channels-hub binary on Linux x86_64, macOS ARM64, and Windows x86_64; it does not run through Python.
If you already have a Rust/Cargo toolchain, you can also install with:
cargo binstall ssh-channels-hub # requires cargo-binstall; installs a prebuilt binary
cargo install ssh-channels-hub --locked # build and install from sourceFor development, clone and build the source:
git clone https://github.com/maoXyzt/ssh-channels-hub.git
cd ssh-channels-hub
cargo build --release # binary at target/release/ssh-channels-hub (or .exe on Windows)2. Have the host in ~/.ssh/config
Host my-db
HostName db.example.com
User myuser
IdentityFile ~/.ssh/id_rsa
3. Write config.toml in the current directory:
[[channels]]
name = "db"
hostname = "my-db" # alias from ~/.ssh/config
direction = "local->remote" # ssh -L
local = "3306" # listen on 127.0.0.1:3306
remote = "3306" # server connects to 127.0.0.1:33064. Run
uvx ssh-channels-hub start # no installation
# or, after pip or Cargo installation:
ssh-channels-hub start # Ctrl+C to stop
# or, after cargo build:
./target/release/ssh-channels-hub start # Linux/macOS
.\target\release\ssh-channels-hub.exe start # Windows PowerShellNow mysql -h 127.0.0.1 -P 3306 goes through the tunnel.
Tip:
ssh-channels-hub generate -o config.tomlscaffolds one commented-out[[channels]]block per alias in your SSH config — uncomment and fill in ports. Orcp config.example.toml config.tomlfor an annotated template.
config.toml is looked up in this order (first existing wins):
| Platform | Path |
|---|---|
| Current directory (always tried first) | ./config.toml |
| Linux / macOS | $XDG_CONFIG_HOME/ssh-channels-hub/config.toml (~/.config/... when unset) |
| Windows | %APPDATA%\ssh-channels-hub\config.toml |
--config /path/to/file overrides the lookup.
[[channels]]
name = "string" # required, unique identifier
hostname = "ssh-config-alias" # required; resolves via ~/.ssh/config
direction = "local->remote" | "remote->local" # required
local = "port" | "host:port" # required, this machine's side
remote = "port" | "host:port" # required, the SSH server's side
local and remote always name the address on their respective side regardless of direction. Direction decides who listens:
local->remote(≈ssh -L): this machine listens onlocal; the server dialsremotefor each connection.remote->local(≈ssh -R): the server bindsremote; incoming traffic is bridged tolocalon this side.
Endpoints accept:
"3306"→127.0.0.1:3306(bare port, host defaults to loopback)"127.0.0.1:3306"→ explicit form"0.0.0.0:8080"→ bind on every interface"[::1]:3306"→ IPv6
The Web status page is enabled by default, and its loopback URL is printed at startup. It shows channel endpoints, health, retries, latest errors, and host-key remediation commands.
Open local opens the channel's local endpoint. Set [web].enabled = false to disable the page; see the configuration reference for other options.
~/.ssh/config can't hold passwords or key passphrases. If SSH config alone can't authenticate to a host, add an [auth.<alias>] block keyed by its alias:
[auth.my-db]
password = "..." # for password-auth hosts (no IdentityFile in SSH config)
# or
passphrase = "..." # for encrypted IdentityFilepassword overrides any IdentityFile. Hosts that authenticate cleanly via SSH config alone don't need an [auth.*] block at all.
[reconnection]
# max_retries = 0 # default: 0 = unlimited
# initial_delay_secs = 1 # default: 1 second
# max_delay_secs = 30 # default: 30 seconds
# use_exponential_backoff = true # default: trueOmit this section to use all defaults.
Each retry delay includes jitter. After a finite retry cycle is exhausted, automatic recovery continues with a second exponential backoff capped at 60 seconds; a successful session resets both counters. SSH handshakes are serialized to avoid reconnect storms.
Scenario: Let other LAN machines use a database tunnel on this machine.
Configuration:
[[channels]]
name = "shared-db"
hostname = "db-server"
direction = "local->remote"
local = "0.0.0.0:3306"
remote = "3306"Explanation: The service listens on 0.0.0.0:3306 and forwards connections to 127.0.0.1:3306 on db-server. Restrict access with a firewall.
Scenario: Expose a service on the local network to edge-server with remote->local (ssh -R).
Configuration:
[[channels]]
name = "lan-api"
hostname = "edge-server"
direction = "remote->local"
local = "192.168.1.50:3000" # bare "3000" means 127.0.0.1:3000
remote = "8080" # edge-server binds 127.0.0.1:8080Explanation: Connections to 127.0.0.1:8080 on edge-server are forwarded to 192.168.1.50:3000. To accept external connections on the server, set remote = "0.0.0.0:8080" and configure GatewayPorts clientspecified in sshd_config.
Full field reference: docs/configuration.md.
All commands accept --config /path/to/config.toml to point at a non-default file, and --debug for verbose logging.
- Troubleshooting — common connection, host-key, and port issues.
- Configuration reference — every field, every edge case.
- How to use — task-oriented walkthroughs.
- Connection testing — verify configured channels.
- Architecture — how channels, sessions, and reconnection fit together.
MIT — see LICENSE.
