A modern Rust implementation of SSH port forwarding and tunneling, inspired by Python automation scripts but built with Rust's performance and safety guarantees.
- Local and Remote Port Forwarding: Support for both
-L(local) and-R(remote) SSH forwarding modes - Connection Health Monitoring: Automatic detection of failed connections with configurable health checks
- Automatic Reconnection: Failed tunnels are automatically restarted with exponential backoff
- JSON Configuration: Easy-to-read configuration files with validation
- Structured Logging: Comprehensive logging with
tracingfor debugging and monitoring - Library and CLI: Use as a Rust library in your projects or as a standalone CLI tool
- Cross-platform: Works on Linux, macOS, and Windows (where SSH is available)
git clone https://github.com/akjong/stun
cd stun
cargo build --releaseThe binary will be available at target/release/stun.
Add this to your Cargo.toml:
[dependencies]
stun = { git = "https://github.com/akjong/stun" }- Create a configuration file:
{
"mode": "local",
"remote": {
"host": "192.168.1.100",
"port": 22,
"user": "username"
},
"forwarding_list": [
"8080:127.0.0.1:8080",
"9000:127.0.0.1:9000",
"3306:127.0.0.1:3306"
],
"timeout": 5,
"backoff_base_secs": 1,
"backoff_max_secs": 30
}- Run the CLI:
stun -c config.jsonuse stun::{Config, TunnelManager};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load configuration from file
let config = Config::from_file("config.json")?;
// Create and start tunnel manager in background; stop on Ctrl+C or your own signal
let mut manager = TunnelManager::new(config)?;
let handle = manager.start_background().await?;
// ... do other async work ... then stop gracefully
manager.stop().await?;
handle.await.ok();
Ok(())
}The configuration file is in JSON format with the following structure:
{
"mode": "local|remote",
"remote": {
"host": "hostname or IP",
"port": 22,
"user": "username",
"key": "/path/to/private/key"
},
"forwarding_list": [
"local_port:remote_host:remote_port",
"bind_addr:local_port:remote_host:remote_port"
],
"timeout": 5,
"backoff_base_secs": 1,
"backoff_max_secs": 30,
"remote_probes": {
"spec": "host:port"
}
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
mode |
string | Yes | - | Forwarding mode: "local" or "remote" |
remote.host |
string | Yes | - | SSH server hostname or IP address |
remote.port |
number | No | 22 | SSH server port |
remote.user |
string | Yes | - | SSH username |
remote.key |
string | No | - | Path to SSH private key file |
forwarding_list |
array | Yes | - | List of port forwarding specifications |
timeout |
number | No | 2 | Connection timeout in seconds |
backoff_base_secs |
number | No | 1 | Initial backoff for restarts (seconds) |
backoff_max_secs |
number | No | 30 | Maximum backoff cap (seconds) |
remote_probes |
object | No | - | Remote mode health probes: map forwarding spec string → "host:port" to test on the remote host via SSH |
Two formats are supported:
-
Three-part format:
"local_port:remote_host:remote_port"- Example:
"8080:127.0.0.1:8080" - Binds to
127.0.0.1:8080locally
- Example:
-
Four-part format:
"bind_address:local_port:remote_host:remote_port"- Example:
"0.0.0.0:8080:127.0.0.1:8080" - Binds to
0.0.0.0:8080locally
- Example:
stun [OPTIONS] --config <FILE>
OPTIONS:
-c, --config <FILE> Configuration file path
-v, --verbose Increase logging verbosity (can be used multiple times)
-h, --help Print help information
-V, --version Print version information
The application uses structured logging with different levels:
ERROR: Critical errors that prevent operationWARN: Warning conditions, such as connection failuresINFO: General information about tunnel statusDEBUG: Detailed debugging information
Control logging with the RUST_LOG environment variable:
# Show all logs
RUST_LOG=debug stun -c config.json
# Show only error and warning logs
RUST_LOG=warn stun -c config.json
# Show only stun library logs
RUST_LOG=stun=debug stun -c config.json- Initialization: The tunnel manager parses the configuration and validates all forwarding specifications
- SSH Process Management: For each forwarding specification, an SSH process is spawned with appropriate flags
- Health Monitoring: Every ~5 seconds, the manager checks:
- SSH process status
- Port connectivity:
- Local mode (
-L): TCP probe to the local bind address/port - Remote mode (
-R): process liveness only, unlessremote_probesare configured, in which case a TCP probe is executed on the remote host via SSH (usingncor/dev/tcpfallback)
- Local mode (
- Automatic Recovery: If a tunnel fails health checks 3 times consecutively, it's automatically restarted
- Graceful Shutdown: On SIGINT (Ctrl+C), all SSH processes are terminated gracefully
Forward local ports to remote services through SSH:
{
"mode": "local",
"remote": {
"host": "bastion.example.com",
"user": "admin",
"key": "~/.ssh/id_rsa"
},
"forwarding_list": [
"3306:database.internal:3306",
"5432:postgres.internal:5432",
"6379:redis.internal:6379"
]
}This creates local ports:
localhost:3306→database.internal:3306(through bastion)localhost:5432→postgres.internal:5432(through bastion)localhost:6379→redis.internal:6379(through bastion)
Expose local services to remote networks:
{
"mode": "remote",
"remote": {
"host": "public-server.example.com",
"user": "deploy"
},
"forwarding_list": [
"8080:127.0.0.1:3000",
"8443:127.0.0.1:8443"
],
"remote_probes": {
"8080:127.0.0.1:3000": "127.0.0.1:8080",
"8443:127.0.0.1:8443": "127.0.0.1:8443"
}
}This makes local services available on the remote server:
public-server.example.com:8080→localhost:3000public-server.example.com:8443→localhost:8443
Remote probes (optional) allow STUN to verify in remote mode that the remote-side listening ports are reachable by attempting a TCP connection from the remote host to the specified host:port. If omitted, health checks in remote mode rely on SSH process liveness only.
Connect to development databases and services:
{
"mode": "local",
"remote": {
"host": "dev.example.com",
"port": 2222,
"user": "developer",
"key": "~/.ssh/dev_key"
},
"forwarding_list": [
"5432:localhost:5432",
"3306:localhost:3306",
"6379:localhost:6379",
"9200:elasticsearch:9200",
"5672:rabbitmq:5672"
],
"timeout": 10
}use stun::{Config, TunnelManager, ForwardingMode, RemoteConfig};
// Configuration
let config = Config {
mode: ForwardingMode::Local,
remote: RemoteConfig {
host: "example.com".to_string(),
port: 22,
user: "user".to_string(),
key: Some("~/.ssh/id_rsa".to_string()),
},
forwarding_list: vec![
"8080:127.0.0.1:8080".to_string(),
],
timeout: Some(5),
backoff_base_secs: Some(1),
backoff_max_secs: Some(30),
remote_probes: None,
};
// Create manager
let mut manager = TunnelManager::new(config)?;
// Start tunneling (background) and stop gracefully
let handle = manager.start_background().await?;
manager.stop().await?;
handle.await.ok();use stun::{StunError, StunResult};
match TunnelManager::new(config) {
Ok(manager) => { /* handle success */ }
Err(StunError::Config(msg)) => { /* configuration error */ }
Err(StunError::Ssh(msg)) => { /* SSH error */ }
Err(e) => { /* other errors */ }
}| Feature | Python Script | Rust STUN |
|---|---|---|
| Performance | Interpreted, slower startup | Compiled, fast startup |
| Memory Usage | Higher baseline memory | Lower memory footprint |
| Error Handling | Basic exception handling | Comprehensive error types |
| Configuration | JSON + manual parsing | Structured config with validation |
| Logging | Print statements | Structured logging with levels |
| Testing | Manual testing | Unit tests and integration tests |
| Type Safety | Runtime type errors | Compile-time type checking |
| Concurrency | Threading with GIL | Async/await with Tokio |
| Distribution | Requires Python interpreter | Single binary executable |
- Rust 1.70+ (for building from source)
- SSH client installed and available in PATH
- Network access to SSH server
- Appropriate SSH keys or password authentication configured
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass:
cargo test - Submit a pull request
This project is licensed under the MIT License. See LICENSE for details.