sshfwd 0.1.2

TUI-based SSH port forwarding manager with automatic port discovery
sshfwd-0.1.2 is not a library.
              __    ____             __
   __________/ /_  / __/      ______/ /
  / ___/ ___/ __ \/ /_| | /| / / __  /
 (__  |__  ) / / / __/| |/ |/ / /_/ /
/____/____/_/ /_/_/   |__/|__/\__,_/

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/f to forward a port with matching local port, F/Shift+Enter for a custom local port
  • Smart Port Lifecycle — Automatically pauses forwards when remote port disappears (service stops) and reactivates when it returns (unlike VS Code which keeps stale forwards active)
  • 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 russh for 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:

cargo install sshfwd

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:

git clone https://github.com/gogoout/sshfwd.rs.git
cd sshfwd.rs

# Cross-compile agents for all platforms
./scripts/build-agents.sh

# Build and install the main application
cargo install --path crates/sshfwd

For development, use cargo build --release -p sshfwd to build without installing.

Usage

# Connect to a remote server
sshfwd user@hostname

# Development: override agent binary
sshfwd user@hostname --agent-path ./target/debug/sshfwd-agent

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

  1. sshfwd-common — Shared types (ScanResult, ListeningPort, AgentResponse), serialized as JSON
  2. sshfwd-agent — Remote binary deployed via SSH. Parses /proc/net/tcp{,6}, maps inodes to processes, streams JSON snapshots every 2s
  3. 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

  • ForwardManager runs on a tokio runtime alongside discovery
  • Each forward binds a local TcpListener, accepts connections, and tunnels them via russh channel_open_direct_tcpip
  • Forward state: StartingActive / Paused (port disappeared from scan) / reopened on bind error
  • Forwards persist to ~/.sshfwd/forwards.json keyed by destination; restored as Paused on 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 SSHrussh avoids spawning SSH master processes that fight with the TUI for terminal control
  • Agent-based discovery — persistent remote process streams port data; no repeated exec calls
  • Hash-based deployment — only uploads agent binary if SHA256 differs from what's already on the remote
  • Atomic upload — temp file → mvchmod +x prevents mid-upload execution
  • Stale cleanup — verifies /proc/{pid}/comm before 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
./scripts/build-agents.sh
cargo build -p sshfwd

# Verify
cargo fmt -- --check
cargo clippy --all-targets --all-features
cargo test --workspace

All checks run automatically in CI. Pull requests must pass before merging.

See CLAUDE.md for development rules and workspace conventions.

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.

Acknowledgments

  • ratatui — TUI framework
  • russh — Pure Rust SSH
  • k9s — Interface design inspiration
  • dua-cli — Event loop pattern