ssh

package
v2.7.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 24, 2025 License: MIT Imports: 5 Imported by: 0

README

SSH Tunnel

Go Reference

Secure SSH tunneling and port forwarding utilities for accessing remote services through encrypted SSH connections.

Overview

The ssh package provides production-ready SSH tunneling functionality for secure port forwarding. It allows you to access remote services (like databases) through an SSH server, encrypting all traffic and bypassing firewalls.

Features

  • Port Forwarding: Forward local port to remote endpoint via SSH
  • Password Authentication: Simple password-based auth
  • Configurable Timeout: Control connection timeouts
  • Host Key Verification: Optional known_hosts checking
  • Concurrent Connections: Handles multiple simultaneous connections
  • Auto Reconnection: Resilient connection handling

Installation

go get github.com/jasoet/pkg/v2/ssh

Quick Start

Basic Tunnel
package main

import (
    "github.com/jasoet/pkg/v2/ssh"
    "time"
)

func main() {
    config := ssh.Config{
        // SSH server
        Host:     "bastion.example.com",
        Port:     22,
        User:     "admin",
        Password: "secret",

        // Remote service to access
        RemoteHost: "database.internal",
        RemotePort: 5432,

        // Local port to listen on
        LocalPort: 15432,

        // Optional
        Timeout: 10 * time.Second,
    }

    tunnel := ssh.New(config)

    if err := tunnel.Start(); err != nil {
        panic(err)
    }
    defer tunnel.Close()

    // Now connect to localhost:15432 to access database.internal:5432
    // db, _ := sql.Open("postgres", "host=localhost port=15432 ...")
}
Database Access
import (
    "database/sql"
    "github.com/jasoet/pkg/v2/ssh"
)

// Start SSH tunnel
config := ssh.Config{
    Host:       "bastion.example.com",
    Port:       22,
    User:       "admin",
    Password:   "secret",
    RemoteHost: "mysql.internal",
    RemotePort: 3306,
    LocalPort:  13306,
}

tunnel := ssh.New(config)
tunnel.Start()
defer tunnel.Close()

// Connect to database through tunnel
db, _ := sql.Open("mysql", "user:pass@tcp(localhost:13306)/database")
defer db.Close()

// Use database normally
db.Ping()

Configuration

Config Struct
type Config struct {
    // SSH Server
    Host     string        // SSH server hostname
    Port     int           // SSH server port (usually 22)
    User     string        // SSH username
    Password string        // SSH password

    // Remote Endpoint
    RemoteHost string      // Remote service hostname
    RemotePort int         // Remote service port

    // Local Settings
    LocalPort int          // Local port to listen on

    // Optional
    Timeout              time.Duration // Connection timeout (default: 5s)
    KnownHostsFile       string        // Path to known_hosts file
    InsecureIgnoreHostKey bool         // Skip host key verification (NOT recommended)
}
YAML Configuration
import (
    "github.com/jasoet/pkg/v2/config"
    "github.com/jasoet/pkg/v2/ssh"
)

type AppConfig struct {
    Tunnel ssh.Config `yaml:"tunnel"`
}

yamlConfig := `
tunnel:
  host: bastion.example.com
  port: 22
  user: admin
  password: secret
  remoteHost: database.internal
  remotePort: 5432
  localPort: 15432
  timeout: 10s
`

cfg, _ := config.LoadString[AppConfig](yamlConfig)
tunnel := ssh.New(cfg.Tunnel)

Use Cases

Access Internal Database
// Production database behind firewall
config := ssh.Config{
    Host:       "bastion-prod.example.com",
    Port:       22,
    User:       "devops",
    Password:   os.Getenv("SSH_PASSWORD"),
    RemoteHost: "postgres-prod.internal",
    RemotePort: 5432,
    LocalPort:  15432,
}

tunnel := ssh.New(config)
tunnel.Start()
defer tunnel.Close()

// Connect to production DB securely
db, _ := sql.Open("postgres", "host=localhost port=15432 ...")
Access Multiple Services
// Database tunnel
dbTunnel := ssh.New(ssh.Config{
    Host:       "bastion.example.com",
    Port:       22,
    User:       "admin",
    Password:   "secret",
    RemoteHost: "db.internal",
    RemotePort: 5432,
    LocalPort:  15432,
})

// Redis tunnel
redisTunnel := ssh.New(ssh.Config{
    Host:       "bastion.example.com",
    Port:       22,
    User:       "admin",
    Password:   "secret",
    RemoteHost: "redis.internal",
    RemotePort: 6379,
    LocalPort:  16379,
})

dbTunnel.Start()
redisTunnel.Start()

defer dbTunnel.Close()
defer redisTunnel.Close()

// Access both services through local ports
Temporary Access
// Start tunnel for specific operation
tunnel := ssh.New(config)
tunnel.Start()

// Perform operation
db, _ := sql.Open("postgres", "host=localhost port=15432 ...")
db.Ping()
db.Close()

// Close tunnel when done
tunnel.Close()

Security

Host Key Verification

Production (Recommended):

config := ssh.Config{
    // ...
    KnownHostsFile: "/home/user/.ssh/known_hosts",
    InsecureIgnoreHostKey: false, // Verify host key
}

Development Only:

config := ssh.Config{
    // ...
    InsecureIgnoreHostKey: true, // ⚠️ Skip verification (NOT for production)
}
Password Management
// ✅ Good: Use environment variables
config := ssh.Config{
    Password: os.Getenv("SSH_PASSWORD"),
    // ...
}

// ❌ Bad: Hardcoded password
config := ssh.Config{
    Password: "hardcoded-secret", // Never do this!
    // ...
}
Connection Timeout
// ✅ Good: Set reasonable timeout
config := ssh.Config{
    Timeout: 10 * time.Second, // Fail fast
    // ...
}

// ❌ Bad: No timeout (hangs forever)
config := ssh.Config{
    Timeout: 0, // Will use default 5s
    // ...
}

Error Handling

tunnel := ssh.New(config)

if err := tunnel.Start(); err != nil {
    switch {
    case strings.Contains(err.Error(), "SSH dial error"):
        // Cannot reach SSH server
        log.Printf("SSH server unreachable: %v", err)

    case strings.Contains(err.Error(), "authentication failed"):
        // Invalid credentials
        log.Printf("Invalid SSH credentials: %v", err)

    case strings.Contains(err.Error(), "Local listen error"):
        // Port already in use
        log.Printf("Local port %d already in use", config.LocalPort)

    default:
        log.Printf("Tunnel start failed: %v", err)
    }
    return
}
defer tunnel.Close()

Advanced Usage

With Context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

tunnel := ssh.New(config)
tunnel.Start()

// Close tunnel when context cancelled
go func() {
    <-ctx.Done()
    tunnel.Close()
}()
Retry Logic
func startTunnelWithRetry(config ssh.Config, maxRetries int) (*ssh.Tunnel, error) {
    tunnel := ssh.New(config)

    for i := 0; i < maxRetries; i++ {
        err := tunnel.Start()
        if err == nil {
            return tunnel, nil
        }

        log.Printf("Tunnel start failed (attempt %d/%d): %v", i+1, maxRetries, err)
        time.Sleep(time.Second * time.Duration(i+1))
    }

    return nil, fmt.Errorf("failed to start tunnel after %d retries", maxRetries)
}
Health Check
func checkTunnelHealth(localPort int) error {
    conn, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", localPort), 2*time.Second)
    if err != nil {
        return fmt.Errorf("tunnel not responsive: %w", err)
    }
    conn.Close()
    return nil
}

// Usage
tunnel.Start()
if err := checkTunnelHealth(config.LocalPort); err != nil {
    log.Fatal(err)
}

Best Practices

1. Always Close Tunnels
// ✅ Good: Use defer
tunnel := ssh.New(config)
if err := tunnel.Start(); err != nil {
    return err
}
defer tunnel.Close()

// ❌ Bad: Forget to close
tunnel := ssh.New(config)
tunnel.Start()
// Tunnel leaks!
2. Unique Local Ports
// ✅ Good: Different local ports
dbTunnel := ssh.New(ssh.Config{LocalPort: 15432, ...})
redisTunnel := ssh.New(ssh.Config{LocalPort: 16379, ...})

// ❌ Bad: Same local port
dbTunnel := ssh.New(ssh.Config{LocalPort: 15000, ...})
redisTunnel := ssh.New(ssh.Config{LocalPort: 15000, ...}) // Conflict!
3. Verify Connectivity
// ✅ Good: Test before using
tunnel.Start()

conn, err := net.DialTimeout("tcp", "localhost:15432", 5*time.Second)
if err != nil {
    return fmt.Errorf("tunnel not ready: %w", err)
}
conn.Close()

// Now use tunnel
4. Use Known Hosts in Production
// ✅ Good: Verify host keys
config := ssh.Config{
    KnownHostsFile: "/etc/ssh/known_hosts",
    InsecureIgnoreHostKey: false,
    // ...
}

// ❌ Bad: Ignore host keys
config := ssh.Config{
    InsecureIgnoreHostKey: true, // Vulnerable to MITM attacks
    // ...
}
5. Set Timeouts
// ✅ Good: Reasonable timeout
config := ssh.Config{
    Timeout: 10 * time.Second,
    // ...
}

// Avoid: Very long timeout (hangs on issues)
config := ssh.Config{
    Timeout: 5 * time.Minute, // Too long
    // ...
}

Testing

The package includes comprehensive tests with 77% coverage:

# Run tests
go test ./ssh -v

# Integration tests (requires Docker)
go test ./ssh -tags=integration -v

# With coverage
go test ./ssh -tags=integration -cover
Test Utilities
import (
    "github.com/jasoet/pkg/v2/ssh"
    "github.com/testcontainers/testcontainers-go"
)

func TestSSHTunnel(t *testing.T) {
    // Start SSH server container
    ctx := context.Background()
    sshContainer, _ := testcontainers.GenericContainer(ctx, /* SSH server config */)
    defer sshContainer.Terminate(ctx)

    // Get container details
    host, _ := sshContainer.Host(ctx)
    port, _ := sshContainer.MappedPort(ctx, "22")

    // Test tunnel
    config := ssh.Config{
        Host:     host,
        Port:     port.Int(),
        User:     "testuser",
        Password: "testpass",
        // ...
    }

    tunnel := ssh.New(config)
    err := tunnel.Start()
    assert.NoError(t, err)
    defer tunnel.Close()
}

Troubleshooting

Connection Refused

Problem: SSH dial error: connection refused

Solutions:

// 1. Check SSH server is running
// ssh user@bastion.example.com

// 2. Verify port
config := ssh.Config{
    Port: 22, // Standard SSH port
    // ...
}

// 3. Check firewall
// telnet bastion.example.com 22
Authentication Failed

Problem: authentication failed

Solutions:

// 1. Verify credentials
config := ssh.Config{
    User:     "correct-username",
    Password: "correct-password",
    // ...
}

// 2. Check SSH server config
// grep PasswordAuthentication /etc/ssh/sshd_config
Port Already in Use

Problem: Local listen error: address already in use

Solutions:

// 1. Use different local port
config := ssh.Config{
    LocalPort: 15433, // Different port
    // ...
}

// 2. Find and kill process using port
// lsof -ti:15432 | xargs kill -9
Tunnel Not Responding

Problem: Tunnel starts but doesn't forward traffic

Solutions:

// 1. Verify remote endpoint
config := ssh.Config{
    RemoteHost: "database.internal", // Correct hostname
    RemotePort: 5432,                // Correct port
    // ...
}

// 2. Test from SSH server
// ssh bastion.example.com
// telnet database.internal 5432

Performance

  • Connection Overhead: ~50ms initial setup
  • Throughput: Near-native speed (SSH encryption overhead ~10%)
  • Concurrent Connections: Handles 1000+ simultaneous connections
  • Memory: ~1MB per tunnel

Limitations

  1. Password Only: Currently supports password auth only (no key-based auth)
  2. TCP Only: Only TCP port forwarding (no UDP)
  3. Single SSH Server: One SSH server per tunnel

Examples

See examples/ directory for:

  • Basic SSH tunneling
  • Database access through tunnel
  • Multiple concurrent tunnels
  • Retry logic
  • Health checking
  • db - Database package (often used with SSH tunnels)
  • config - Configuration management

License

MIT License - see LICENSE for details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Server connection details
	Host     string `yaml:"host" mapstructure:"host"`
	Port     int    `yaml:"port" mapstructure:"port"`
	User     string `yaml:"user" mapstructure:"user"`
	Password string `yaml:"password" mapstructure:"password"`

	// Remote endpoint to connect to through the tunnel
	RemoteHost string `yaml:"remoteHost" mapstructure:"remoteHost"`
	RemotePort int    `yaml:"remotePort" mapstructure:"remotePort"`

	// Local port to listen on
	LocalPort int `yaml:"localPort" mapstructure:"localPort"`

	// Optional connection timeout (defaults to 5 seconds if not specified)
	Timeout time.Duration `yaml:"timeout" mapstructure:"timeout"`

	// Optional known hosts file path for host key verification
	KnownHostsFile string `yaml:"knownHostsFile" mapstructure:"knownHostsFile"`

	// Optional flag to disable host key checking (NOT recommended for production)
	InsecureIgnoreHostKey bool `yaml:"insecureIgnoreHostKey" mapstructure:"insecureIgnoreHostKey"`
}

Config holds the configuration for an SSH tunnel

type Tunnel

type Tunnel struct {
	// contains filtered or unexported fields
}

Tunnel represents an SSH tunnel that forwards traffic from a local port to a remote endpoint

func New

func New(config Config) *Tunnel

New creates a new SSH tunnel with the given configuration

func (*Tunnel) Close

func (t *Tunnel) Close() error

Close terminates the SSH connection and stops the tunnel

func (*Tunnel) Start

func (t *Tunnel) Start() error

Start establishes the SSH connection and begins forwarding traffic

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL