Volver al blog

SSH Tunnel Manager: Creando una Herramienta CLI para Gestionar Múltiples Túneles SSH

9 min
SSH Tunnel Manager: Creando una Herramienta CLI para Gestionar Múltiples Túneles SSH

Introducción

Si trabajas con servidores remotos, bases de datos en la nube o servicios detrás de firewalls, probablemente usas túneles SSH regularmente. El comando ssh -L funciona bien para un túnel ocasional, pero cuando necesitas mantener múltiples conexiones simultáneas, la gestión se complica rápidamente.

Después de meses de abrir múltiples pestañas de terminal, anotar puertos en archivos de texto y eliminar accidentalmente el túnel equivocado, decidí construir una solución: SSH Tunnel Manager, una aplicación con interfaz de terminal (TUI) que centraliza la administración de túneles SSH en una sola interfaz.

Este artículo cubre las decisiones técnicas, arquitectura y lecciones aprendidas durante el desarrollo.

El Problema

Los túneles SSH son esenciales para:

  • Acceder a bases de datos remotas de forma segura
  • Exponer servicios locales durante el desarrollo
  • Saltar restricciones de firewalls
  • Probar servicios en entornos de staging

Sin embargo, gestionar múltiples túneles simultáneamente presenta desafíos:

  1. Seguimiento: ¿Qué puerto local mapea a qué puerto remoto?
  2. Estado: ¿Este túnel sigue activo o se cayó?
  3. Logs: SSH muestra detalles de conexión en stderr, pero están dispersos en varias pestañas
  4. Limpieza: Matar túneles específicos sin afectar a otros

Mi workflow involucraba 4-5 pestañas de terminal con comandos como:

ssh -N -L 8080:localhost:80 usuario@servidor1.com
ssh -N -L 5432:localhost:5432 usuario@db.servidor.com
ssh -N -L 3000:localhost:3000 usuario@staging.servidor.com

Sin visibilidad, sin centralización, fácil de cometer errores.

La Solución

SSH Tunnel Manager proporciona:

  • Múltiples túneles simultáneos en una sola interfaz
  • Logs en tiempo real por túnel
  • Nombres automáticos (estilo Docker: brave-tesla, happy-curie)
  • Navegación completa con teclado y mouse
  • Modales de confirmación para acciones destructivas
  • Integración con SSH config

Arquitectura Técnica

Stack Tecnológico

- Go 1.21+
- Bubbletea (framework TUI de Charm.sh)
- Lipgloss (estilos tipo CSS para terminal)
- Bubbles (componentes de UI)
- Moby names (generador de nombres de Docker)

¿Por qué Go?

Elegí Go por varias razones:

  1. Binario único: Sin dependencias externas. Los usuarios descargan un archivo y funciona.
  2. Multi-plataforma: Compila una vez para Linux, macOS Intel y macOS Apple Silicon.
  3. Goroutines: Perfecto para manejar múltiples conexiones SSH concurrentes.
  4. Rendimiento: Bajo uso de memoria, inicio rápido.

Alternativas consideradas:

  • Python: Requeriría virtualenv o instalación con pip
  • Node.js: Necesita instalación global con npm
  • Rust: Gran opción, pero tiempos de compilación más largos y curva de aprendizaje más pronunciada para contribuidores

Arquitectura: Navigator + Workers

El diseño es intencionalmente simple:

  1. Goroutine Principal (Navigator): Ejecuta el loop de Bubbletea TUI, maneja input de teclado/mouse, renderiza la interfaz cada segundo.

  2. Goroutines Worker (uno por túnel): Cada túnel corre independientemente, lee el stderr del SSH, actualiza sus propios logs.

  3. Comunicación: Sin channels complejos ni paso de mensajes. Los workers escriben logs con protección mutex, el navigator lee al renderizar.

// Cada túnel tiene su propio goroutine para leer logs
go m.streamTunnelLogs(&m.tunnels[len(m.tunnels)-1], stderr)

// La función stream corre independientemente
func (m *model) streamTunnelLogs(tun *tunnel, stderr io.ReadCloser) {
    scanner := bufio.NewScanner(stderr)
    for scanner.Scan() {
        tun.logMutex.Lock()
        tun.logs = append(tun.logs, fmt.Sprintf("[%s] %s", time.Now().Format("15:04:05"), scanner.Text()))
        if len(tun.logs) > 100 {
            tun.logs = tun.logs[1:] // Mantiene solo últimas 100 líneas
        }
        tun.logMutex.Unlock()
    }
}

Thread Safety con Mutex

Cada túnel tiene su propio mutex para proteger acceso concurrente a los logs:

type tunnel struct {
    id         int
    tag        string
    host       string
    localPort  string
    remotePort string
    verbose    bool
    cmd        *exec.Cmd
    logs       []string
    active     bool
    logMutex   sync.Mutex  // ← Protege acceso concurrente
}

Sin el mutex, el race detector fallaría porque:

  • El goroutine del túnel escribe en tun.logs
  • El goroutine de la UI lee de tun.logs al renderizar

Parseo del SSH Config

La herramienta lee automáticamente los hosts desde ~/.ssh/config:

func getSSHHosts() []string {
    file, _ := os.Open(os.Getenv("HOME") + "/.ssh/config")
    defer file.Close()
    
    var hosts []string
    scanner := bufio.NewScanner(file)
    re := regexp.MustCompile(`^Host\s+(.+)`)
    
    for scanner.Scan() {
        line := scanner.Text()
        if matches := re.FindStringSubmatch(line); matches != nil {
            host := strings.TrimSpace(matches[1])
            if host != "*" {
                hosts = append(hosts, host)
            }
        }
    }
    
    return expandSSHHosts(hosts)
}

La función expandSSHHosts también extrae el campo Hostname, así que los usuarios pueden seleccionar tanto el alias del host como el hostname real.

Validación de Puertos

Antes de crear un túnel, la herramienta verifica si el puerto local ya está en uso:

func isPortInUse(port string) bool {
    cmd := exec.Command("ss", "-tuln")
    output, err := cmd.Output()
    if err != nil {
        return false
    }
    return strings.Contains(string(output), ":"+port+" ")
}

Esto previene el error común de intentar hacer bind a un puerto ya ocupado.

Características Principales

Múltiples IPs por Host

Algunos hosts resuelven a múltiples IPs (DNS round-robin, load balancers). La herramienta detecta esto y permite elegir:

func extractAllHostnames(hostWithIP string) []string {
    parts := strings.Fields(hostWithIP)
    if len(parts) <= 1 {
        return []string{}
    }
    result := []string{parts[0]}  // Alias del host
    result = append(result, parts[1:]...)  // Todas las IPs
    return result
}

Nombres Automáticos

Inspirado en los nombres de contenedores de Docker, la herramienta usa moby/moby/pkg/namesgenerator:

import "github.com/moby/moby/pkg/namesgenerator"

name := namesgenerator.GetRandomName(0)
// Retorna nombres como: "peaceful-einstein", "brave-tesla"

Los usuarios aún pueden proveer nombres personalizados, pero los auto-generados ahorran tiempo y agregan personalidad.

Build Multi-plataforma

El Makefile compila para todas las plataformas objetivo:

build:
	GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o build/ssh-tunnel-manager-linux-amd64
	GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o build/ssh-tunnel-manager-darwin-amd64
	GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o build/ssh-tunnel-manager-darwin-arm64

El flag -ldflags="-s -w" elimina símbolos de debug, reduciendo el tamaño del binario en ~30%.

Script de Instalación

El script install.sh maneja:

  • Detección de SO y arquitectura
  • Descarga del binario correcto desde GitHub Releases
  • Configuración de permisos de ejecución
  • Movimiento a /usr/local/bin
curl -sSL https://raw.githubusercontent.com/gouh/ssh-tunnel-manager/main/install.sh | bash

Ejemplos de Uso

Uso Básico

# Iniciar la aplicación
ssh-tunnel-manager

# Atajos de teclado
n       - Crear nuevo túnel
d       - Eliminar túnel seleccionado (con confirmación)
Tab     - Cambiar entre lista de túneles y panel de logs
↑/↓     - Navegar lista / scroll logs
j/k     - Navegación estilo Vim (alternativa a flechas)
?       - Mostrar ayuda
q       - Salir (con confirmación)

Caso de Uso 1: Acceso a Base de Datos Remota

Necesitas trabajar con una base de datos PostgreSQL en un servidor remoto:

  1. Ejecuta ssh-tunnel-manager
  2. Presiona n
  3. Selecciona tu servidor de base de datos (ej: db.production)
  4. Ingresa puerto remoto: 5432
  5. Ingresa puerto local: 5432
  6. Tag: postgres-prod (o Enter para aleatorio)
  7. Verbose: n

Ahora puedes conectar tu cliente local de PostgreSQL a localhost:5432 y estás conectado a la base de datos remota.

Caso de Uso 2: Múltiples Microservicios

Estás desarrollando localmente y necesitas conectarte a múltiples servicios en staging:

Túnel 1: localhost:8080 → staging-api.empresa.com:8080
Túnel 2: localhost:3000 → staging-web.empresa.com:3000
Túnel 3: localhost:5432 → staging-db.empresa.com:5432
Túnel 4: localhost:6379 → staging-redis.empresa.com:6379

Todos visibles y gestionables en una sola ventana de terminal.

Caso de Uso 3: Alternativa a Kubernetes Port Forwarding

Mientras que kubectl port-forward funciona para Kubernetes, a veces necesitas túneles SSH para VMs o servidores bare metal. Esta herramienta cubre ese gap.

Workflow de Desarrollo

Gestión de Versiones

El proyecto usa versionamiento semántico. El script scripts/bump-version.sh:

  1. Valida formato de versión (semver)
  2. Pregunta tipo de cambio (feat, fix, chore, etc.)
  3. Solicita descripciones de cambios
  4. Actualiza version.go
  5. Actualiza CHANGELOG.md
  6. Crea commit y tag de git
make bump-version
git push && git push --tags

Consideraciones de Testing

Testear aplicaciones TUI es desafiante. Enfoque actual:

  1. Testing manual con múltiples túneles
  2. Race detector: go run -race main.go
  3. Build para todas las plataformas antes de release

Mejoras futuras podrían incluir:

  • Tests unitarios para parseo de SSH config
  • Tests de integración con servidores SSH mock
  • Snapshot testing para renderizado de UI

Lecciones Aprendidas

  1. Bubbletea es sólido: La arquitectura Elm (Model-Update-View) funciona bien para TUIs. La gestión de estado es predecible.

  2. Mutex es necesario: Sin sync.Mutex, el race detector falla inmediatamente cuando múltiples túneles escriben logs concurrentemente.

  3. La UX importa incluso en CLI:

    • Los modales de confirmación previenen eliminaciones accidentales
    • El overlay de ayuda (?) reduce la necesidad de revisar el README
    • Los mensajes de status proveen feedback ("Túnel creado", "Puerto en uso")
  4. Publicar es iterar: La versión 1.0 era funcional. Las versiones 1.1 y 1.2 agregaron:

    • Soporte para mouse
    • Modales de confirmación
    • Overlay de ayuda
    • Selección de IP para hosts con múltiples IPs
    • Mejores mensajes de error
  5. La documentación es parte del producto: Un buen README con screenshots e instrucciones claras de instalación reduce preguntas de soporte.

Consideraciones de Rendimiento

  • Memoria: Cada túnel consume ~2-3 MB (mayormente para buffers de logs)
  • CPU: Mínimo cuando está idle, ligero spike durante refresh de UI (cada segundo)
  • Tamaño del binario: ~8 MB comprimido, ~20 MB sin comprimir

Optimizaciones implementadas:

  • Mantener solo últimas 100 líneas de log por túnel
  • Eliminar símbolos de debug en builds de release
  • Reducir frecuencia de refresh de UI de 250ms a 1 segundo

Troubleshooting

Problemas Comunes

"Port already in use"

# Verificar qué está usando el puerto
lsof -i :8080
# o
ss -tuln | grep 8080

"Connection refused"

  • Verificar acceso SSH: ssh usuario@host
  • Checar si el servicio remoto está corriendo
  • Asegurarse que el firewall permite la conexión

Los túneles no aparecen

  • Verificar sintaxis de ~/.ssh/config
  • Checar permisos del archivo: chmod 600 ~/.ssh/config
  • Reiniciar la aplicación después de cambios en el config

Mejoras Futuras

Características potenciales para versiones futuras:

  1. Persistencia de túneles: Guardar configuraciones y restaurar al iniciar
  2. Grupos: Organizar túneles por proyecto o entorno
  3. Auto-reconexión: Reconectar automáticamente túneles caídos
  4. Exportar/Importar: Compartir configuraciones con miembros del equipo
  5. Integración con system tray: Correr en background con ícono en bandeja
  6. Métricas: Trackear uptime de túneles, datos transferidos

Conclusiones

SSH Tunnel Manager centraliza la gestión de túneles SSH en una sola interfaz. No reemplaza ssh -L para casos simples, pero mejora significativamente el workflow cuando se trabaja con múltiples conexiones simultáneas.

La herramienta está lista para producción y la uso diariamente en mi trabajo. El código es open source en github.com/gouh/ssh-tunnel-manager. Issues y PRs son bienvenidos.

Compartir

Posts relacionados