Fix port detection and manual forwarding issues
Major improvements to error handling and debugging: - Fix program quitting on manual port forwarding errors - Add comprehensive debug logging for SSH connections - Improve error handling to show messages instead of quitting - Add StateStartingForward for better user feedback - Enhanced SSH client creation with default key loading - Add --test-connect mode for debugging specific hosts - Better timeout handling and connection diagnostics The application now gracefully handles connection failures and provides helpful error messages instead of crashing. Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
56
main.go
56
main.go
@@ -12,6 +12,12 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for connection test mode
|
||||||
|
if len(os.Args) > 2 && os.Args[1] == "--test-connect" {
|
||||||
|
testConnection(os.Args[2])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the application
|
// Initialize the application
|
||||||
app := NewApp()
|
app := NewApp()
|
||||||
if err := app.Run(); err != nil {
|
if err := app.Run(); err != nil {
|
||||||
@@ -51,4 +57,54 @@ func testMode() {
|
|||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("To run the interactive TUI, use: ./kport")
|
fmt.Println("To run the interactive TUI, use: ./kport")
|
||||||
fmt.Println("Note: TUI requires a proper terminal environment")
|
fmt.Println("Note: TUI requires a proper terminal environment")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("To test connection to a specific host: ./kport --test-connect <hostname>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// testConnection tests connecting to a specific host
|
||||||
|
func testConnection(hostName string) {
|
||||||
|
fmt.Printf("Testing connection to host: %s\n", hostName)
|
||||||
|
fmt.Println("=====================================")
|
||||||
|
|
||||||
|
// Load SSH config
|
||||||
|
config := NewSSHConfig()
|
||||||
|
if err := config.LoadConfig(); err != nil {
|
||||||
|
fmt.Printf("❌ Failed to load SSH config: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the host
|
||||||
|
host, err := config.GetHostByName(hostName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("❌ Host not found: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found host configuration:\n")
|
||||||
|
fmt.Printf(" Name: %s\n", host.Name)
|
||||||
|
fmt.Printf(" Hostname: %s\n", host.Hostname)
|
||||||
|
fmt.Printf(" User: %s\n", host.User)
|
||||||
|
fmt.Printf(" Port: %s\n", host.Port)
|
||||||
|
if host.Identity != "" {
|
||||||
|
fmt.Printf(" Identity: %s\n", host.Identity)
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
|
||||||
|
// Test port detection
|
||||||
|
fmt.Println("Testing port detection...")
|
||||||
|
ports, err := detectRemotePorts(*host)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("❌ Port detection failed: %v\n", err)
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("This is expected if:")
|
||||||
|
fmt.Println("- The host is not reachable")
|
||||||
|
fmt.Println("- SSH keys are not set up")
|
||||||
|
fmt.Println("- SSH agent is not running")
|
||||||
|
fmt.Println("- The host doesn't exist")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ Port detection successful! Found %d ports: %v\n", len(ports), ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("You can still use manual port forwarding in the TUI even if port detection fails.")
|
||||||
}
|
}
|
@@ -28,10 +28,12 @@ func DetectPorts(host SSHHost) tea.Cmd {
|
|||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
ports, err := detectRemotePorts(host)
|
ports, err := detectRemotePorts(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Instead of returning an error that quits the app,
|
// Log the error for debugging but don't quit the app
|
||||||
// return an empty ports list with a message
|
fmt.Fprintf(os.Stderr, "Debug: Port detection failed for %s: %v\n", host.Name, err)
|
||||||
|
// Return empty ports list so user can still use manual port forwarding
|
||||||
return PortsDetectedMsg{Ports: []int{}}
|
return PortsDetectedMsg{Ports: []int{}}
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Detected %d ports on %s: %v\n", len(ports), host.Name, ports)
|
||||||
return PortsDetectedMsg{Ports: ports}
|
return PortsDetectedMsg{Ports: ports}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -154,24 +155,32 @@ func (pf *PortForwarder) handleConnection(localConn net.Conn) {
|
|||||||
// StartPortForwarding starts port forwarding for a specific port
|
// StartPortForwarding starts port forwarding for a specific port
|
||||||
func StartPortForwarding(host SSHHost, remotePort int) tea.Cmd {
|
func StartPortForwarding(host SSHHost, remotePort int) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Starting port forwarding for %s:%d\n", host.Name, remotePort)
|
||||||
|
|
||||||
// Find an available local port
|
// Find an available local port
|
||||||
localPort, err := findAvailablePort()
|
localPort, err := findAvailablePort()
|
||||||
if err != nil {
|
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)}
|
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 SSH client
|
// Create SSH client
|
||||||
client, err := createSSHClient(host)
|
client, err := createSSHClient(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrorMsg{Error: fmt.Errorf("failed to create SSH client: %w", err)}
|
fmt.Fprintf(os.Stderr, "Debug: Failed to create SSH client: %v\n", err)
|
||||||
|
return ErrorMsg{Error: fmt.Errorf("failed to connect to %s: %w", host.Name, err)}
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: SSH client created successfully\n")
|
||||||
|
|
||||||
// Create and start port forwarder
|
// Create and start port forwarder
|
||||||
forwarder := NewPortForwarder(client, localPort, remotePort)
|
forwarder := NewPortForwarder(client, localPort, remotePort)
|
||||||
if err := forwarder.Start(); err != nil {
|
if err := forwarder.Start(); err != nil {
|
||||||
client.Close()
|
client.Close()
|
||||||
|
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)}
|
return ErrorMsg{Error: fmt.Errorf("failed to start port forwarding: %w", err)}
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Port forwarder started successfully\n")
|
||||||
|
|
||||||
return ForwardingStartedMsg{
|
return ForwardingStartedMsg{
|
||||||
LocalPort: localPort,
|
LocalPort: localPort,
|
||||||
@@ -183,33 +192,43 @@ func StartPortForwarding(host SSHHost, remotePort int) tea.Cmd {
|
|||||||
// StartManualPortForwarding starts port forwarding for a manually entered port
|
// StartManualPortForwarding starts port forwarding for a manually entered port
|
||||||
func StartManualPortForwarding(host SSHHost, portStr string) tea.Cmd {
|
func StartManualPortForwarding(host SSHHost, portStr string) tea.Cmd {
|
||||||
return func() tea.Msg {
|
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)
|
remotePort, err := strconv.Atoi(portStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Invalid port number: %s\n", portStr)
|
||||||
return ErrorMsg{Error: fmt.Errorf("invalid port number: %s", portStr)}
|
return ErrorMsg{Error: fmt.Errorf("invalid port number: %s", portStr)}
|
||||||
}
|
}
|
||||||
|
|
||||||
if remotePort <= 0 || remotePort > 65535 {
|
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")}
|
return ErrorMsg{Error: fmt.Errorf("port number must be between 1 and 65535")}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find an available local port
|
// Find an available local port
|
||||||
localPort, err := findAvailablePort()
|
localPort, err := findAvailablePort()
|
||||||
if err != nil {
|
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)}
|
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 SSH client
|
// Create SSH client
|
||||||
client, err := createSSHClient(host)
|
client, err := createSSHClient(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrorMsg{Error: fmt.Errorf("failed to create SSH client: %w", err)}
|
fmt.Fprintf(os.Stderr, "Debug: Failed to create SSH client: %v\n", err)
|
||||||
|
return ErrorMsg{Error: fmt.Errorf("failed to connect to %s: %w", host.Name, err)}
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: SSH client created successfully\n")
|
||||||
|
|
||||||
// Create and start port forwarder
|
// Create and start port forwarder
|
||||||
forwarder := NewPortForwarder(client, localPort, remotePort)
|
forwarder := NewPortForwarder(client, localPort, remotePort)
|
||||||
if err := forwarder.Start(); err != nil {
|
if err := forwarder.Start(); err != nil {
|
||||||
client.Close()
|
client.Close()
|
||||||
|
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)}
|
return ErrorMsg{Error: fmt.Errorf("failed to start port forwarding: %w", err)}
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Port forwarder started successfully\n")
|
||||||
|
|
||||||
return ForwardingStartedMsg{
|
return ForwardingStartedMsg{
|
||||||
LocalPort: localPort,
|
LocalPort: localPort,
|
||||||
@@ -224,29 +243,58 @@ func createSSHClient(host SSHHost) (*ssh.Client, error) {
|
|||||||
User: host.User,
|
User: host.User,
|
||||||
Auth: []ssh.AuthMethod{},
|
Auth: []ssh.AuthMethod{},
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // In production, use proper host key verification
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // In production, use proper host key verification
|
||||||
Timeout: 5 * time.Second, // Shorter timeout
|
Timeout: 10 * time.Second, // Longer timeout for better reliability
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add key-based authentication if identity file is specified
|
// Add key-based authentication if identity file is specified
|
||||||
if host.Identity != "" {
|
if host.Identity != "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Trying identity file: %s\n", host.Identity)
|
||||||
key, err := loadPrivateKey(host.Identity)
|
key, err := loadPrivateKey(host.Identity)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
config.Auth = append(config.Auth, ssh.PublicKeys(key))
|
config.Auth = append(config.Auth, ssh.PublicKeys(key))
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Added key-based auth\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Failed to load identity file: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add SSH agent authentication
|
// Add SSH agent authentication
|
||||||
if agentAuth, err := sshAgentAuth(); err == nil {
|
if agentAuth, err := sshAgentAuth(); err == nil {
|
||||||
config.Auth = append(config.Auth, agentAuth)
|
config.Auth = append(config.Auth, agentAuth)
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Added SSH agent auth\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: SSH agent not available: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load default SSH keys if no specific identity is set
|
||||||
|
if host.Identity == "" {
|
||||||
|
defaultKeys := []string{"id_rsa", "id_ecdsa", "id_ed25519"}
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err == nil {
|
||||||
|
for _, keyName := range defaultKeys {
|
||||||
|
keyPath := filepath.Join(homeDir, ".ssh", keyName)
|
||||||
|
if key, err := loadPrivateKey(keyPath); err == nil {
|
||||||
|
config.Auth = append(config.Auth, ssh.PublicKeys(key))
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Added default key: %s\n", keyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no auth methods available, provide helpful error
|
||||||
|
if len(config.Auth) == 0 {
|
||||||
|
return nil, fmt.Errorf("no SSH authentication methods available - please set up SSH keys or SSH agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to the remote host
|
// Connect to the remote host
|
||||||
addr := net.JoinHostPort(host.Hostname, host.Port)
|
addr := net.JoinHostPort(host.Hostname, host.Port)
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Connecting to %s\n", addr)
|
||||||
client, err := ssh.Dial("tcp", addr, config)
|
client, err := ssh.Dial("tcp", addr, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to %s: %w", host.Name, err)
|
return nil, fmt.Errorf("failed to connect to %s (%s): %w", host.Name, addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: Successfully connected to %s\n", host.Name)
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
58
tui.go
58
tui.go
@@ -16,6 +16,7 @@ const (
|
|||||||
StateConnecting
|
StateConnecting
|
||||||
StateSelectPort
|
StateSelectPort
|
||||||
StateManualPort
|
StateManualPort
|
||||||
|
StateStartingForward
|
||||||
StateForwarding
|
StateForwarding
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,6 +76,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m.updatePortSelection(msg)
|
return m.updatePortSelection(msg)
|
||||||
case StateManualPort:
|
case StateManualPort:
|
||||||
return m.updateManualPort(msg)
|
return m.updateManualPort(msg)
|
||||||
|
case StateStartingForward:
|
||||||
|
return m.updateStartingForward(msg)
|
||||||
case StateForwarding:
|
case StateForwarding:
|
||||||
return m.updateForwarding(msg)
|
return m.updateForwarding(msg)
|
||||||
}
|
}
|
||||||
@@ -95,8 +98,22 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.state = StateForwarding
|
m.state = StateForwarding
|
||||||
return m, nil
|
return m, nil
|
||||||
case ErrorMsg:
|
case ErrorMsg:
|
||||||
m.err = msg.Error
|
// Don't quit on errors, just show them and let user continue
|
||||||
return m, tea.Quit
|
m.message = fmt.Sprintf("Error: %v", msg.Error)
|
||||||
|
// Go back to appropriate state depending on current state
|
||||||
|
switch m.state {
|
||||||
|
case StateConnecting:
|
||||||
|
m.state = StateSelectPort
|
||||||
|
m.ports = []int{} // Show empty ports list
|
||||||
|
case StateStartingForward:
|
||||||
|
// Go back to port selection or manual port depending on where we came from
|
||||||
|
if len(m.ports) > 0 {
|
||||||
|
m.state = StateSelectPort
|
||||||
|
} else {
|
||||||
|
m.state = StateManualPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -163,6 +180,8 @@ func (m *Model) updatePortSelection(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case "enter", " ":
|
case "enter", " ":
|
||||||
m.selectedPort = m.cursor
|
m.selectedPort = m.cursor
|
||||||
|
m.state = StateStartingForward
|
||||||
|
m.message = "Starting port forwarding..."
|
||||||
// Start port forwarding
|
// Start port forwarding
|
||||||
return m, StartPortForwarding(m.hosts[m.selectedHost], m.ports[m.selectedPort])
|
return m, StartPortForwarding(m.hosts[m.selectedHost], m.ports[m.selectedPort])
|
||||||
case "m":
|
case "m":
|
||||||
@@ -188,6 +207,8 @@ func (m *Model) updateManualPort(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
case "enter":
|
case "enter":
|
||||||
if m.manualPort != "" {
|
if m.manualPort != "" {
|
||||||
|
m.state = StateStartingForward
|
||||||
|
m.message = "Starting port forwarding..."
|
||||||
// Parse and start manual port forwarding
|
// Parse and start manual port forwarding
|
||||||
return m, StartManualPortForwarding(m.hosts[m.selectedHost], m.manualPort)
|
return m, StartManualPortForwarding(m.hosts[m.selectedHost], m.manualPort)
|
||||||
}
|
}
|
||||||
@@ -204,6 +225,20 @@ func (m *Model) updateManualPort(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateStartingForward handles the starting forward state
|
||||||
|
func (m *Model) updateStartingForward(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "q":
|
||||||
|
return m, tea.Quit
|
||||||
|
case "esc":
|
||||||
|
// Cancel the forwarding attempt
|
||||||
|
m.state = StateSelectPort
|
||||||
|
m.message = ""
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
// updateForwarding handles forwarding state
|
// updateForwarding handles forwarding state
|
||||||
func (m *Model) updateForwarding(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *Model) updateForwarding(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
@@ -256,6 +291,8 @@ func (m *Model) View() string {
|
|||||||
s.WriteString(m.renderPortSelection())
|
s.WriteString(m.renderPortSelection())
|
||||||
case StateManualPort:
|
case StateManualPort:
|
||||||
s.WriteString(m.renderManualPort())
|
s.WriteString(m.renderManualPort())
|
||||||
|
case StateStartingForward:
|
||||||
|
s.WriteString(m.renderStartingForward())
|
||||||
case StateForwarding:
|
case StateForwarding:
|
||||||
s.WriteString(m.renderForwarding())
|
s.WriteString(m.renderForwarding())
|
||||||
}
|
}
|
||||||
@@ -408,6 +445,23 @@ func (m *Model) renderManualPort() string {
|
|||||||
return s.String()
|
return s.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderStartingForward renders the starting forward view
|
||||||
|
func (m *Model) renderStartingForward() string {
|
||||||
|
var s strings.Builder
|
||||||
|
|
||||||
|
startingStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#00BFFF")).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
s.WriteString(startingStyle.Render("🚀 " + m.message))
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
s.WriteString("Setting up SSH tunnel and port forwarding...\n\n")
|
||||||
|
s.WriteString("Controls:\n")
|
||||||
|
s.WriteString(" Esc: Cancel q: Quit\n")
|
||||||
|
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
// renderForwarding renders the forwarding status view
|
// renderForwarding renders the forwarding status view
|
||||||
func (m *Model) renderForwarding() string {
|
func (m *Model) renderForwarding() string {
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
Reference in New Issue
Block a user