Compare commits

...

2 Commits

Author SHA1 Message Date
Ona
02322c4a61 Remove debug log file
Co-authored-by: Ona <no-reply@ona.com>
2025-09-26 00:24:08 +00:00
Ona
bde1529248 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>
2025-09-26 00:24:04 +00:00
4 changed files with 168 additions and 8 deletions

56
main.go
View File

@@ -12,6 +12,12 @@ func main() {
return
}
// Check for connection test mode
if len(os.Args) > 2 && os.Args[1] == "--test-connect" {
testConnection(os.Args[2])
return
}
// Initialize the application
app := NewApp()
if err := app.Run(); err != nil {
@@ -51,4 +57,54 @@ func testMode() {
fmt.Println("")
fmt.Println("To run the interactive TUI, use: ./kport")
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.")
}

View File

@@ -28,10 +28,12 @@ func DetectPorts(host SSHHost) tea.Cmd {
return func() tea.Msg {
ports, err := detectRemotePorts(host)
if err != nil {
// Instead of returning an error that quits the app,
// return an empty ports list with a message
// Log the error for debugging but don't quit the app
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{}}
}
fmt.Fprintf(os.Stderr, "Debug: Detected %d ports on %s: %v\n", len(ports), host.Name, ports)
return PortsDetectedMsg{Ports: ports}
}
}

View File

@@ -5,6 +5,7 @@ import (
"io"
"net"
"os"
"path/filepath"
"strconv"
"sync"
"time"
@@ -154,24 +155,32 @@ func (pf *PortForwarder) handleConnection(localConn net.Conn) {
// 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 SSH client
client, err := createSSHClient(host)
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
forwarder := NewPortForwarder(client, localPort, remotePort)
if err := forwarder.Start(); err != nil {
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)}
}
fmt.Fprintf(os.Stderr, "Debug: Port forwarder started successfully\n")
return ForwardingStartedMsg{
LocalPort: localPort,
@@ -183,33 +192,43 @@ func StartPortForwarding(host SSHHost, remotePort int) tea.Cmd {
// 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 SSH client
client, err := createSSHClient(host)
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
forwarder := NewPortForwarder(client, localPort, remotePort)
if err := forwarder.Start(); err != nil {
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)}
}
fmt.Fprintf(os.Stderr, "Debug: Port forwarder started successfully\n")
return ForwardingStartedMsg{
LocalPort: localPort,
@@ -224,29 +243,58 @@ func createSSHClient(host SSHHost) (*ssh.Client, error) {
User: host.User,
Auth: []ssh.AuthMethod{},
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
if host.Identity != "" {
fmt.Fprintf(os.Stderr, "Debug: Trying identity file: %s\n", host.Identity)
key, err := loadPrivateKey(host.Identity)
if err == nil {
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
if agentAuth, err := sshAgentAuth(); err == nil {
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
addr := net.JoinHostPort(host.Hostname, host.Port)
fmt.Fprintf(os.Stderr, "Debug: Connecting to %s\n", addr)
client, err := ssh.Dial("tcp", addr, config)
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
}

58
tui.go
View File

@@ -16,6 +16,7 @@ const (
StateConnecting
StateSelectPort
StateManualPort
StateStartingForward
StateForwarding
)
@@ -75,6 +76,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updatePortSelection(msg)
case StateManualPort:
return m.updateManualPort(msg)
case StateStartingForward:
return m.updateStartingForward(msg)
case StateForwarding:
return m.updateForwarding(msg)
}
@@ -95,8 +98,22 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.state = StateForwarding
return m, nil
case ErrorMsg:
m.err = msg.Error
return m, tea.Quit
// Don't quit on errors, just show them and let user continue
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
}
@@ -163,6 +180,8 @@ func (m *Model) updatePortSelection(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
case "enter", " ":
m.selectedPort = m.cursor
m.state = StateStartingForward
m.message = "Starting port forwarding..."
// Start port forwarding
return m, StartPortForwarding(m.hosts[m.selectedHost], m.ports[m.selectedPort])
case "m":
@@ -188,6 +207,8 @@ func (m *Model) updateManualPort(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
case "enter":
if m.manualPort != "" {
m.state = StateStartingForward
m.message = "Starting port forwarding..."
// Parse and start manual port forwarding
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
}
// 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
func (m *Model) updateForwarding(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
@@ -256,6 +291,8 @@ func (m *Model) View() string {
s.WriteString(m.renderPortSelection())
case StateManualPort:
s.WriteString(m.renderManualPort())
case StateStartingForward:
s.WriteString(m.renderStartingForward())
case StateForwarding:
s.WriteString(m.renderForwarding())
}
@@ -408,6 +445,23 @@ func (m *Model) renderManualPort() 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
func (m *Model) renderForwarding() string {
var s strings.Builder