package main import ( "fmt" "net" "os" "os/exec" "strconv" "sync" tea "github.com/charmbracelet/bubbletea" ) // ForwardingStartedMsg is sent when port forwarding starts type ForwardingStartedMsg struct { LocalPort int RemotePort int } // PortForwarder manages SSH port forwarding using ssh command type PortForwarder struct { hostName string localPort int remotePort int sshCmd *exec.Cmd stopChan chan struct{} wg sync.WaitGroup isRunning bool mu sync.Mutex } // NewPortForwarder creates a new port forwarder using ssh command func NewPortForwarder(hostName string, localPort, remotePort int) *PortForwarder { return &PortForwarder{ hostName: hostName, localPort: localPort, remotePort: remotePort, stopChan: make(chan struct{}), } } // Start starts the port forwarding using ssh command func (pf *PortForwarder) Start() error { pf.mu.Lock() defer pf.mu.Unlock() if pf.isRunning { return fmt.Errorf("port forwarding already running") } // Use ssh command with -L flag for local port forwarding // Format: ssh -L localport:localhost:remoteport hostname pf.sshCmd = exec.Command("ssh", "-L", fmt.Sprintf("%d:localhost:%d", pf.localPort, pf.remotePort), "-N", // Don't execute remote command, just forward ports "-o", "ExitOnForwardFailure=yes", // Exit if port forwarding fails "-o", "ServerAliveInterval=30", // Keep connection alive "-o", "ServerAliveCountMax=3", pf.hostName) fmt.Fprintf(os.Stderr, "Debug: Starting SSH command: %s\n", pf.sshCmd.String()) // Start the SSH command if err := pf.sshCmd.Start(); err != nil { return fmt.Errorf("failed to start SSH port forwarding: %w", err) } pf.isRunning = true // Monitor the SSH process pf.wg.Add(1) go pf.monitorSSH() return nil } // Stop stops the port forwarding func (pf *PortForwarder) Stop() { pf.mu.Lock() defer pf.mu.Unlock() if !pf.isRunning { return } pf.isRunning = false close(pf.stopChan) // Kill the SSH process if pf.sshCmd != nil && pf.sshCmd.Process != nil { fmt.Fprintf(os.Stderr, "Debug: Stopping SSH port forwarding\n") pf.sshCmd.Process.Kill() } pf.wg.Wait() } // monitorSSH monitors the SSH process func (pf *PortForwarder) monitorSSH() { defer pf.wg.Done() // Wait for the SSH command to finish or be stopped select { case <-pf.stopChan: // We were asked to stop return default: // Wait for SSH command to finish if err := pf.sshCmd.Wait(); err != nil { fmt.Fprintf(os.Stderr, "Debug: SSH command finished with error: %v\n", err) } else { fmt.Fprintf(os.Stderr, "Debug: SSH command finished successfully\n") } } } // StartPortForwarding starts port forwarding for a specific port func StartPortForwarding(host SSHHost, remotePort int) tea.Cmd { return func() tea.Msg { fmt.Fprintf(os.Stderr, "Debug: Starting port forwarding for %s:%d\n", host.Name, remotePort) // Find an available local port localPort, err := findAvailablePort() if err != nil { fmt.Fprintf(os.Stderr, "Debug: Failed to find available port: %v\n", err) return ErrorMsg{Error: fmt.Errorf("failed to find available local port: %w", err)} } fmt.Fprintf(os.Stderr, "Debug: Found available local port: %d\n", localPort) // Create and start port forwarder using ssh command forwarder := NewPortForwarder(host.Name, localPort, remotePort) if err := forwarder.Start(); err != nil { fmt.Fprintf(os.Stderr, "Debug: Failed to start port forwarder: %v\n", err) return ErrorMsg{Error: fmt.Errorf("failed to start port forwarding: %w", err)} } fmt.Fprintf(os.Stderr, "Debug: Port forwarder started successfully\n") return ForwardingStartedMsg{ LocalPort: localPort, RemotePort: remotePort, } } } // StartManualPortForwarding starts port forwarding for a manually entered port func StartManualPortForwarding(host SSHHost, portStr string) tea.Cmd { return func() tea.Msg { fmt.Fprintf(os.Stderr, "Debug: Manual port forwarding requested for %s:%s\n", host.Name, portStr) remotePort, err := strconv.Atoi(portStr) if err != nil { fmt.Fprintf(os.Stderr, "Debug: Invalid port number: %s\n", portStr) return ErrorMsg{Error: fmt.Errorf("invalid port number: %s", portStr)} } if remotePort <= 0 || remotePort > 65535 { fmt.Fprintf(os.Stderr, "Debug: Port number out of range: %d\n", remotePort) return ErrorMsg{Error: fmt.Errorf("port number must be between 1 and 65535")} } // Find an available local port localPort, err := findAvailablePort() if err != nil { fmt.Fprintf(os.Stderr, "Debug: Failed to find available port: %v\n", err) return ErrorMsg{Error: fmt.Errorf("failed to find available local port: %w", err)} } fmt.Fprintf(os.Stderr, "Debug: Found available local port: %d\n", localPort) // Create and start port forwarder using ssh command forwarder := NewPortForwarder(host.Name, localPort, remotePort) if err := forwarder.Start(); err != nil { fmt.Fprintf(os.Stderr, "Debug: Failed to start port forwarder: %v\n", err) return ErrorMsg{Error: fmt.Errorf("failed to start port forwarding: %w", err)} } fmt.Fprintf(os.Stderr, "Debug: Port forwarder started successfully\n") return ForwardingStartedMsg{ LocalPort: localPort, RemotePort: remotePort, } } } // findAvailablePort finds an available local port func findAvailablePort() (int, error) { listener, err := net.Listen("tcp", ":0") if err != nil { return 0, err } defer listener.Close() addr := listener.Addr().(*net.TCPAddr) return addr.Port, nil }