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:
- Seguimiento: ¿Qué puerto local mapea a qué puerto remoto?
- Estado: ¿Este túnel sigue activo o se cayó?
- Logs: SSH muestra detalles de conexión en stderr, pero están dispersos en varias pestañas
- 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:
- Binario único: Sin dependencias externas. Los usuarios descargan un archivo y funciona.
- Multi-plataforma: Compila una vez para Linux, macOS Intel y macOS Apple Silicon.
- Goroutines: Perfecto para manejar múltiples conexiones SSH concurrentes.
- 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:
-
Goroutine Principal (Navigator): Ejecuta el loop de Bubbletea TUI, maneja input de teclado/mouse, renderiza la interfaz cada segundo.
-
Goroutines Worker (uno por túnel): Cada túnel corre independientemente, lee el stderr del SSH, actualiza sus propios logs.
-
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.logsal 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:
- Ejecuta
ssh-tunnel-manager - Presiona
n - Selecciona tu servidor de base de datos (ej:
db.production) - Ingresa puerto remoto:
5432 - Ingresa puerto local:
5432 - Tag:
postgres-prod(o Enter para aleatorio) - 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:
- Valida formato de versión (semver)
- Pregunta tipo de cambio (feat, fix, chore, etc.)
- Solicita descripciones de cambios
- Actualiza
version.go - Actualiza
CHANGELOG.md - Crea commit y tag de git
make bump-version
git push && git push --tags
Consideraciones de Testing
Testear aplicaciones TUI es desafiante. Enfoque actual:
- Testing manual con múltiples túneles
- Race detector:
go run -race main.go - 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
-
Bubbletea es sólido: La arquitectura Elm (Model-Update-View) funciona bien para TUIs. La gestión de estado es predecible.
-
Mutex es necesario: Sin
sync.Mutex, el race detector falla inmediatamente cuando múltiples túneles escriben logs concurrentemente. -
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")
-
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
-
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:
- Persistencia de túneles: Guardar configuraciones y restaurar al iniciar
- Grupos: Organizar túneles por proyecto o entorno
- Auto-reconexión: Reconectar automáticamente túneles caídos
- Exportar/Importar: Compartir configuraciones con miembros del equipo
- Integración con system tray: Correr en background con ícono en bandeja
- 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.
Posts relacionados

Slog: Simplificando el Registro Estructurado en Aplicaciones Go
Slog: una solución de registro estructurado eficiente y flexible para aplicaciones en Go. Obtén información sobre el comportamiento del software y soluciona problemas fácilmente. ¡Descubre cómo mejorar tu proceso de registro hoy mismo!

Reverse Proxy
En el desarrollo de software, a menudo nos encontramos con la necesidad de manejar las solicitudes de una red de manera más controlada y segura, especialmente cuando se trata de aplicaciones web. Es aquí donde los Proxies Reversos entran en juego.