__ ____ __
__________/ /_ / __/ ______/ /
/ ___/ ___/ __ \/ /_| | /| / / __ /
(__ |__ ) / / / __/| |/ |/ / /_/ /
/____/____/_/ /_/_/ |__/|__/\__,_/
sshfwd.rs
A TUI-based SSH port forwarding management tool built with Rust. Inspired by k9s' keyboard-driven interface.
Features
- Automatic Port Detection — Deploys a lightweight agent to the remote host that streams listening ports in real time
- One-Key Forwarding — Press
Enter/fto forward a port with matching local port,F/Shift+Enterfor a custom local port - Error Recovery — Bind failures open a modal dialog to pick a different port instead of silently falling back
- Forwarded Ports Grouped at Top — Active forwards are visually separated from unforwarded ports
- Persistence — Forwarded ports are remembered across sessions per destination (
~/.sshfwd/forwards.json) - Pure Rust SSH — Uses
russhfor in-process SSH with no system OpenSSH dependency - ProxyJump Support — Recursive tunneling through jump hosts via SSH config
Platform Support
Remote servers (agent):
- Linux x86_64 / ARM64 (aarch64) — statically linked via musl
Local machine (main app):
- macOS (Apple Silicon & Intel)
- Linux
The main app automatically detects the remote platform and deploys the correct agent binary. No manual configuration needed.
Installation
Quick Install
Install directly via cargo:
The published crate includes prebuilt agent binaries for all supported platforms (Linux x86_64/ARM64, macOS Intel/ARM64). The agent is automatically deployed to remote servers when you connect.
Platform Support
Remote servers (agent):
- Linux x86_64 / ARM64 (aarch64) — statically linked via musl
- macOS (Apple Silicon & Intel) — native binaries
Local machine (main app):
- macOS (Apple Silicon & Intel)
- Linux (x86_64 / ARM64)
- Windows via WSL (experimental)
Build from Source
For development or unsupported platforms:
Prerequisites:
- Rust 1.82.0 or later
- For Linux agent cross-compilation on macOS:
brew install filosottile/musl-cross/musl-cross
Build:
# Cross-compile agents for all platforms
# Build and install the main application
For development, use cargo build --release -p sshfwd to build without installing.
Usage
# Connect to a remote server
# Development: override agent binary
TUI
╭ ● user@host │ 5 ports │ 2 fwd ────────────────────╮
│ FWD PORT PROTO PID COMMAND │
│▶->:5432 5432 tcp 1234 postgresql/15/..│
│ ->:8080 8080 tcp6 5678 node server.js │
│ ──────── ──────── ─────── ──────── ────────────────│
│ 3000 tcp 9012 ruby bin/rails s│
│ 6379 tcp 3456 redis-server │
╰───────────────────────────────────────────────────╯
<j/k>Navigate <g/G>Top/Bottom <Enter/f>Forward <F>Custom Port <q>Quit
Forwarded ports are grouped at the top with a visual separator.
Port Input Modal
When pressing F/Shift+Enter, or when a bind error occurs:
╭─ Forward port 5432 ───────────╮
│ │
│ Address already in use │
│ Local port: 5432█ │
│ │
│ <Enter>Confirm <Esc>Cancel │
╰───────────────────────────────╯
Keyboard Shortcuts
| Key | Action |
|---|---|
j / Down |
Move selection down |
k / Up |
Move selection up |
g |
Jump to top |
G |
Jump to bottom |
Enter / f |
Toggle forwarding (same local port) |
F / Shift+Enter |
Forward with custom local port (modal) |
q / Esc / Ctrl+C |
Quit |
Architecture
Workspace Crates
- sshfwd-common — Shared types (
ScanResult,ListeningPort,AgentResponse), serialized as JSON - sshfwd-agent — Remote binary deployed via SSH. Parses
/proc/net/tcp{,6}, maps inodes to processes, streams JSON snapshots every 2s - sshfwd — Main application: SSH session, agent deployment, TUI, port forwarding
TUI Architecture (Elm / TEA)
All state flows through app.rs:
- Model — single state struct (ports, forwards, connection state, modal, selection)
- Message — enum of all events (scan data, key press, forward events, tick)
- update() — pure state transitions, returns
ForwardCommands - view() — renders table, hotkey bar, and optional modal overlay
Event loop uses the dua-cli pattern: bare crossterm::event::read() on a dedicated OS thread, crossbeam_channel::select! multiplexing keyboard + background channels.
Port Forwarding
ForwardManagerruns on a tokio runtime alongside discovery- Each forward binds a local
TcpListener, accepts connections, and tunnels them viarusshchannel_open_direct_tcpip - Forward state:
Starting→Active/Paused(port disappeared from scan) / reopened on bind error - Forwards persist to
~/.sshfwd/forwards.jsonkeyed by destination; restored asPausedon next connection
Data Flow
┌─ Main App ──────────┐ ┌──── Remote Server ────┐
│ │ │ │
│ 1. Connect (SSH) │───── russh ───────│ 2. Upload Agent │
│ 3. Deploy Agent │──── exec ch ──────│ 4. Run Agent Loop │
│ 5. Parse JSON │◄── stdout pipe ───│ (scan every 2s) │
│ 6. Display TUI │ │ │
│ 7. Forward Ports │── direct-tcpip ───│ 8. Tunnel Traffic │
│ │ │ │
└─────────────────────┘ └───────────────────────┘
Key Design Decisions
- Pure Rust SSH —
russhavoids spawning SSH master processes that fight with the TUI for terminal control - Agent-based discovery — persistent remote process streams port data; no repeated
execcalls - Hash-based deployment — only uploads agent binary if SHA256 differs from what's already on the remote
- Atomic upload — temp file →
mv→chmod +xprevents mid-upload execution - Stale cleanup — verifies
/proc/{pid}/commbefore killing to avoid hitting reused PIDs - No random port fallback — bind failures surface immediately via error modal so the user stays in control
Development
# Build agents + main app
# Verify
All checks run automatically in CI. Pull requests must pass before merging.
See CLAUDE.md for development rules and workspace conventions.
Releases
Publishing to crates.io is automated via GitHub Actions. To release a new version:
- Update version in root
Cargo.toml(under[workspace.package]) - Commit and push to main
- Create a GitHub release or push a version tag:
The release workflow automatically:
- Builds agent binaries for all 4 platforms
- Reconstructs the prebuilt-agents directory
- Publishes to crates.io using
CARGO_REGISTRY_TOKENsecret
First-time setup: Add your crates.io API token to GitHub Secrets:
- Generate token at https://crates.io/settings/tokens
- Add as
CARGO_REGISTRY_TOKENin repository settings → Secrets → Actions
License
Licensed under the MIT license.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you shall be licensed as above, without any additional terms or conditions.