sshfwd 0.1.2

TUI-based SSH port forwarding manager with automatic port discovery
```
              __    ____             __
   __________/ /_  / __/      ______/ /
  / ___/ ___/ __ \/ /_| | /| / / __  /
 (__  |__  ) / / / __/| |/ |/ / /_/ /
/____/____/_/ /_/_/   |__/|__/\__,_/
```
# sshfwd.rs

A TUI-based SSH port forwarding management tool built with Rust. Inspired by [k9s](https://github.com/derailed/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:

```bash
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:**
```bash
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

```bash
# 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 `ForwardCommand`s
- **view()** — renders table, hotkey bar, and optional modal overlay

Event loop uses the [dua-cli pattern](https://github.com/Byron/dua-cli): 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: `Starting``Active` / `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 SSH**`russh` 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 → `mv``chmod +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

```bash
# 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](./CLAUDE.md) for development rules and workspace conventions.

## License

Licensed under the [MIT license](LICENSE-MIT).

### 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]https://github.com/ratatui-org/ratatui — TUI framework
- [russh]https://crates.io/crates/russh — Pure Rust SSH
- [k9s]https://github.com/derailed/k9s — Interface design inspiration
- [dua-cli]https://github.com/Byron/dua-cli — Event loop pattern