Files

5.9 KiB
Raw Permalink Blame History

Kandu — SSH Local Port Forward Manager

Cross-platform desktop app for managing SSH local port forwards. Wraps the system SSH binary so your ~/.ssh/config, keys, OpenSSH certificates, ssh-agent, and ProxyJump all work.

Project Structure

crates/
├── kandu-core/      # Core library (this task)
│   ├── src/
│   │   ├── error.rs      # Error types
│   │   ├── profile.rs    # Profile model & persistence
│   │   ├── ssh/          # SSH utilities (config parsing, command building)
│   │   ├── supervisor.rs # Tunnel manager & supervisor threads
│   │   ├── log.rs        # Logging (ring buffer)
│   │   └── utils/        # Port checking, OS process cleanup
│   └── examples/
│       └── headless.rs   # Headless testing example (no UI)
└── kandu/           # Binary (stub, UI comes later)
    └── src/main.rs

Core Features Implemented

Profile Model

  • Struct: TunnelProfile with id, name, host, user, ssh_port, bind_address, local_port, dest_host, dest_port, auto_start, auto_reconnect
  • Validation: structured field-level errors, ports 1–65535, no whitespace in hostname/bind/dest
  • Persistence: TOML file at platform config dir, atomic saves with backup on corruption

SSH Integration

  • Finding SSH: Searches PATH, falls back to Windows default path, respects ssh_path override in config
  • Version check: ssh_version() calls ssh -V
  • Config parsing: Parses ~/.ssh/config, follows Include with glob support, skips wildcard patterns
  • Resolution: ssh -G parsing to extract hostname, user, port, identity files, certificates, ProxyJump
  • Command builder: Pure function that returns Vec with all flags set correctly:
    • -N -T (no shell, no tty)
    • -o ExitOnForwardFailure=yes, ServerAliveInterval=30, ServerAliveCountMax=3, BatchMode=yes
    • Optional -p, -l for port/user override
    • -L <bind>:<local_port>:<dest>:<dest_port> <host>
    • Display command with proper quoting for copy-paste

Port Checking

  • port_in_use() checks if port is available, already used by Kandu, or used by other process
  • Tries to bind a TcpListener

Tunnel Supervision

  • TunnelManager: Central coordinator with per-tunnel supervisor threads
  • States: Stopped, Starting, Connected, Failed, Reconnecting
  • Reconnection: Exponential backoff 1, 2, 4, 8, 16, 32, 60s (capped), reset after 60s of stable Connected
  • Permanent errors: AuthFailed, HostKey, PortInUse, SshNotFound → no retry
  • Transient errors: timeout, exit code → retry if auto_reconnect=true
  • Connection check: Polls local port for TCP connectivity every 250ms up to 15s
  • Logging: Ring buffer (500 lines max) with source (SSH/Kandu) and timestamp
  • Callbacks: set_repaint_callback() triggers only on state/log changes
  • API: start(id, profile), stop(id), start_all(profiles), stop_all(), shutdown(), snapshot(id), snapshots(), set_repaint_callback()

OS-Specific Process Cleanup

  • Windows: Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (CREATE_NO_WINDOW flag)
  • Linux: prctl(PR_SET_PDEATHSIG, SIGTERM) in pre_exec hook
  • macOS: Manual cleanup on shutdown; document that SIGKILL of Kandu can orphan children
  • Graceful stop: SIGTERM, 2s wait, then SIGKILL (Unix); TerminateProcess (Windows)

Error Handling

  • Typed errors: KanduError and TunnelError
  • Field-level validation errors for profiles
  • Backups for corrupted config files (tunnels.toml.bak-<timestamp>)
  • Error classification from stderr patterns

Testing

Unit Tests

All modules include tests for:

  • Profile validation (empty fields, invalid ports, whitespace)
  • Command building (all optional field combinations, quoting)
  • SSH config parsing (Include, globs, wildcard hosts, comments, dedup)
  • Error classification
  • Backoff math

Run tests:

cargo test -p kandu-core

Integration Tests

Headless example for manual testing:

cargo run --example headless --manifest-path crates/kandu-core/Cargo.toml

Creates ~/.config/kandu/tunnels.toml if not present, then prompts you to select a profile to start.

Building

On Linux/macOS:

cargo build --release

On Windows: Same command; Job Object API is handled in code.

Known Limitations

macOS Process Cleanup Gap

If Kandu is killed with SIGKILL (e.g., kill -9), child SSH processes will be orphaned. This is documented and accepted for MVP. Normal shutdown via Ctrl+C or app close uses SIGTERM (graceful).

Stderr Logging

Currently, SSH stderr is not captured into the log ring buffer (discarded with Stdio::null). Enhancement: spawn a reader thread with a channel to feed lines into the supervisor.

Process Alive Monitoring

After connection is established, we don't continuously poll whether the SSH process is still alive. The UI can call stop() if needed, or reconnection will be triggered on connection failure.

Dependencies

  • serde, toml: Config persistence
  • directories: Cross-platform config dir
  • thiserror: Error types
  • log: Logging framework
  • rand: UUID v4 generation
  • home: Home directory expansion
  • glob: Pattern matching
  • windows-sys (Windows): Job Object API
  • libc (Unix): Process signals

Next Steps

  1. UI (egis immediate-mode GUI): Poll snapshots every frame, render status + logs, buttons to start/stop
  2. SSH stderr capturing: Thread + channel to feed lines into log ring buffer
  3. Connection keep-alive: Periodic port checks while Connected to detect SSH exit early
  4. Profiles UI: Create, edit, delete, test profiles
  5. Tray icon / system integration: Minimize to tray, system notifications

Testing on macOS and Windows

This implementation was written on Linux. Verify on the other platforms:

  • Windows: Job Object creation, TerminateProcess call
  • macOS: SIGTERM/SIGKILL sequence, orphan handling
  • SSH config parsing and Path handling