SSH Channels Hub
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.
Why
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— alias-only references, publickey auth, 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.
Quickstart
1. Run or install
Run directly with uvx (recommended, no installation required):
uvx ssh-channels-hub --help
Or install it with pip inside an activated virtual environment:
pip install ssh-channels-hub
ssh-channels-hub --help
The wheel installs the same ssh-channels-hub binary on Linux x86_64,
macOS arm64, and Windows x86_64; it does not run through Python.
For 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:3306
4. Run
uvx ssh-channels-hub start # no installation
# or, after pip install:
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 PowerShell
Now 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.
Configuration
config.toml is looked up in this order (first existing wins):
| Platform | Path |
|---|---|
| Current directory (always tried first) | ./config.toml |
| Linux / macOS | ~/.config/ssh-channels-hub/config.toml |
| Windows | %APPDATA%\ssh-channels-hub\config.toml |
--config /path/to/file overrides the lookup.
Channel schema
[[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
Web status page
Starting the service also serves a live channel dashboard on loopback. It shows the service summary, each channel's direction, local and remote endpoints, health, retry attempt, and latest error. The actual URL is printed for both foreground and daemon startup.
Every channel has an Open local link built from its local endpoint. This
also applies to remote->local channels: the link opens the local service being
exposed, never the remote bind address.
[web]
enabled = true # default: true; set false to disable
port = 9090 # default: 9090; preferred port
strict = false # default: false; if occupied, try 9091, 9092, ...
Credentials
~/.ssh/config can't hold passwords or key passphrases. When SSH config alone can't authenticate the host, add an [auth.<alias>] block keyed by the SSH config alias:
[auth.my-db]
password = "..." # for password-auth hosts (no IdentityFile in SSH config)
# or
passphrase = "..." # for encrypted IdentityFile
password overrides any IdentityFile. Hosts that authenticate cleanly via SSH config alone don't need an [auth.*] block at all.
Reconnection (global)
[reconnection]
max_retries = 0 # 0 = unlimited
initial_delay_secs = 1
max_delay_secs = 30
use_exponential_backoff = true
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.
More examples
Listen on every interface so other LAN machines can use the tunnel (mind your firewall):
[[channels]]
name = "shared-db"
hostname = "db-server"
direction = "local->remote"
local = "0.0.0.0:3306"
remote = "3306"
Expose a local service to the SSH server (ssh -R):
[[channels]]
name = "expose-local-web"
hostname = "jumpbox"
direction = "remote->local"
remote = "8022" # server binds 127.0.0.1:8022
local = "80" # incoming traffic bridges to 127.0.0.1:80 here
(For the server to bind 0.0.0.0:8022, set remote = "0.0.0.0:8022" and enable GatewayPorts in the server's sshd_config.)
Full field reference: docs/configuration.md.
Commands
| Command | What it does |
|---|---|
start |
Run in the foreground (Ctrl+C to stop). |
start -D / --daemon |
Spawn a detached background process. |
stop |
Tell the running process to exit gracefully (via IPC). |
restart |
Stop the running service, then re-start as daemon. |
status |
Show service state, per-channel health (Connected / Reconnecting / Failed / Stopped), PID, and endpoints. Add --watch / -w to refresh every --interval / -n seconds (default 2). |
test |
Probe each configured local->remote listener to confirm the tunnel is alive. remote->local channels are skipped — verify those server-side. |
validate |
Resolve every channel against ~/.ssh/config and report any problems. |
generate -o config.toml |
Scaffold a config.toml from existing SSH config aliases. |
hosts |
Scan SSH config aliases and show whether each host is supported. Use --format json for script-friendly output. |
All commands accept --config /path/to/config.toml to point at a non-default file, and --debug for verbose logging.
Troubleshooting
Channel '...' references host alias '...', but no Host ... block exists— typo inhostname, or the alias is missing from~/.ssh/config.Address(es) already in use— something else is bound to yourlocaladdress. Change the port or stop the other process. Find the culprit withlsof -i :PORT(Linux/macOS) ornetstat -ano | findstr :PORT(Windows).- Bind ports < 1024 — needs root (Linux/macOS) or Administrator (Windows).
- Connection fails —
ssh <alias>manually first to isolate SSH config / network / key permission issues. - Encrypted key not unlocking — set
[auth.<alias>] passphrase = "...". - Full debug output —
ssh-channels-hub start --debuglogs each channel's SSH handshake, channel open, and reconnection attempts.
Further reading
- Configuration reference — every field, every edge case.
- How to use — task-oriented walkthroughs.
- Architecture — how channels, sessions, and reconnection fit together.
License
MIT — see LICENSE.
Metadata
Release files for ssh-channels-hub 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ssh_channels_hub-0.5.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details for ssh_channels_hub-0.5.0-py3-none-win_amd64.whl |
| ssh_channels_hub-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details for ssh_channels_hub-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
| ssh_channels_hub-0.5.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details for ssh_channels_hub-0.5.0-py3-none-macosx_11_0_arm64.whl |
Total release size: 7.6 MB
Release files / ssh_channels_hub-0.5.0-py3-none-win_amd64.whl
| Download URL | ssh_channels_hub-0.5.0-py3-none-win_amd64.whl |
|---|---|
| Size | 2.5 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
ca52449b5d21e664afc3cfcb6680935799afcf00b5c0194d61b87cecd3b7bd49
|
|
BLAKE2b-256 checksum How to use checksums |
16041fcff826e18c86cac9b4a3137877e1a0cbd882ecabe92b1a97156c9b986b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logRelease files / ssh_channels_hub-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | ssh_channels_hub-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 2.6 MB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
7f790fe04a3ad99cb8fb8b71a9f2bf272787e9940a58c7d575ace241f5b67326
|
|
BLAKE2b-256 checksum How to use checksums |
d25bf5fda7a008506de129d9a166c711d71e51e4830d9efff71c30300055a4a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logRelease files / ssh_channels_hub-0.5.0-py3-none-macosx_11_0_arm64.whl
| Download URL | ssh_channels_hub-0.5.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 2.4 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
320ffece6a5aa71e49c7bce44db3a2a67e1e555f47e38756aa3ec5e05716f713
|
|
BLAKE2b-256 checksum How to use checksums |
4b396e1a50c1b3d561ed51778aa3d681faf7c049365ee86307bf661e6272f24d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logSecurity for ssh-channels-hub 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 3 files in this release have been uploaded with provenance metadata.
Attestation for ssh_channels_hub-0.5.0-py3-none-win_amd64.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.