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:
TunnelProfilewith 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_pathoverride in config - Version check:
ssh_version()callsssh -V - Config parsing: Parses
~/.ssh/config, followsIncludewith glob support, skips wildcard patterns - Resolution:
ssh -Gparsing 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,-lfor 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:
KanduErrorandTunnelError - 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 persistencedirectories: Cross-platform config dirthiserror: Error typeslog: Logging frameworkrand: UUID v4 generationhome: Home directory expansionglob: Pattern matchingwindows-sys(Windows): Job Object APIlibc(Unix): Process signals
Next Steps
- UI (egis immediate-mode GUI): Poll snapshots every frame, render status + logs, buttons to start/stop
- SSH stderr capturing: Thread + channel to feed lines into log ring buffer
- Connection keep-alive: Periodic port checks while Connected to detect SSH exit early
- Profiles UI: Create, edit, delete, test profiles
- 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