From 66c6ba9307d80d3d7388934e63cfce9f576e36d7 Mon Sep 17 00:00:00 2001 From: Ona Date: Fri, 26 Sep 2025 21:30:14 +0000 Subject: [PATCH] Improve port mapping to prefer same local port Enhanced port forwarding logic to be more intuitive: - Try to map remote port to same local port when possible - Fallback to random available port if same port unavailable - Clear user feedback showing port mapping (same vs different) - Enhanced forwarding view with access URLs and instructions - Added --test-port command to test port mapping logic Examples: - Remote port 3000 -> localhost:3000 (if available) - Remote port 80 -> localhost:random (if 80 unavailable) - Shows 'same port' or 'port X was unavailable' messages This makes port forwarding much more intuitive - users can access localhost:3000 when forwarding remote port 3000. Co-authored-by: Ona --- main.go | 43 +++++++++++++++++++++++++++++++++++++++++++ port_forwarder.go | 46 ++++++++++++++++++++++++++++++++++++++++------ tui.go | 30 ++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index b07eab3..f63963c 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "os/user" + "strconv" "strings" ) @@ -21,6 +22,12 @@ func main() { return } + // Check for port mapping test mode + if len(os.Args) > 2 && os.Args[1] == "--test-port" { + testPortMapping(os.Args[2]) + return + } + // Initialize the application app := NewApp() if err := app.Run(); err != nil { @@ -62,6 +69,7 @@ func testMode() { fmt.Println("Note: TUI requires a proper terminal environment") fmt.Println("") fmt.Println("To test connection to a specific host: ./kport --test-connect ") + fmt.Println("To test port mapping logic: ./kport --test-port ") } // testConnection tests connecting to a specific host @@ -182,4 +190,39 @@ func expandShellVars(value string) string { } return value +} + +// testPortMapping tests the port mapping logic +func testPortMapping(portStr string) { + fmt.Printf("Testing port mapping for port: %s\n", portStr) + fmt.Println("=====================================") + + remotePort, err := strconv.Atoi(portStr) + if err != nil { + fmt.Printf("❌ Invalid port number: %s\n", portStr) + return + } + + if remotePort <= 0 || remotePort > 65535 { + fmt.Printf("❌ Port number must be between 1 and 65535\n") + return + } + + // Test the port mapping logic + localPort, samePort, err := findPreferredLocalPort(remotePort) + if err != nil { + fmt.Printf("❌ Failed to find available port: %v\n", err) + return + } + + if samePort { + fmt.Printf("✅ Port %d is available locally - using same port\n", localPort) + fmt.Printf(" Mapping: localhost:%d -> remote:%d\n", localPort, remotePort) + } else { + fmt.Printf("⚠️ Port %d is unavailable locally - using alternative port %d\n", remotePort, localPort) + fmt.Printf(" Mapping: localhost:%d -> remote:%d\n", localPort, remotePort) + } + + fmt.Println("") + fmt.Println("This is how kport will map the ports when forwarding.") } \ No newline at end of file diff --git a/port_forwarder.go b/port_forwarder.go index e0d4c7c..3191866 100644 --- a/port_forwarder.go +++ b/port_forwarder.go @@ -119,13 +119,17 @@ 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() + // Try to use the same port locally, fallback to random if unavailable + localPort, samePort, err := findPreferredLocalPort(remotePort) 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) + if samePort { + fmt.Fprintf(os.Stderr, "Debug: Using same port locally: %d\n", localPort) + } else { + fmt.Fprintf(os.Stderr, "Debug: Port %d unavailable, using alternative: %d\n", remotePort, localPort) + } // Create and start port forwarder using ssh command forwarder := NewPortForwarder(host.Name, localPort, remotePort) @@ -158,13 +162,17 @@ func StartManualPortForwarding(host SSHHost, portStr string) tea.Cmd { return ErrorMsg{Error: fmt.Errorf("port number must be between 1 and 65535")} } - // Find an available local port - localPort, err := findAvailablePort() + // Try to use the same port locally, fallback to random if unavailable + localPort, samePort, err := findPreferredLocalPort(remotePort) 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) + if samePort { + fmt.Fprintf(os.Stderr, "Debug: Using same port locally: %d\n", localPort) + } else { + fmt.Fprintf(os.Stderr, "Debug: Port %d unavailable, using alternative: %d\n", remotePort, localPort) + } // Create and start port forwarder using ssh command forwarder := NewPortForwarder(host.Name, localPort, remotePort) @@ -183,6 +191,32 @@ func StartManualPortForwarding(host SSHHost, portStr string) tea.Cmd { +// findPreferredLocalPort tries to use the same port as remote, fallback to random +func findPreferredLocalPort(remotePort int) (localPort int, samePort bool, err error) { + // First try to use the same port as the remote port + if isPortAvailable(remotePort) { + return remotePort, true, nil + } + + // If same port is not available, find any available port + availablePort, err := findAvailablePort() + if err != nil { + return 0, false, err + } + + return availablePort, false, nil +} + +// isPortAvailable checks if a specific port is available locally +func isPortAvailable(port int) bool { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return false + } + listener.Close() + return true +} + // findAvailablePort finds an available local port func findAvailablePort() (int, error) { listener, err := net.Listen("tcp", ":0") diff --git a/tui.go b/tui.go index 8716b9d..bea34f0 100644 --- a/tui.go +++ b/tui.go @@ -93,8 +93,13 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, nil case ForwardingStartedMsg: - m.message = fmt.Sprintf("Port forwarding started: localhost:%d -> %s:%d", - msg.LocalPort, m.hosts[m.selectedHost].Name, msg.RemotePort) + if msg.LocalPort == msg.RemotePort { + m.message = fmt.Sprintf("Port forwarding started: localhost:%d -> %s:%d (same port)", + msg.LocalPort, m.hosts[m.selectedHost].Name, msg.RemotePort) + } else { + m.message = fmt.Sprintf("Port forwarding started: localhost:%d -> %s:%d (port %d was unavailable)", + msg.LocalPort, m.hosts[m.selectedHost].Name, msg.RemotePort, msg.RemotePort) + } m.state = StateForwarding return m, nil case ErrorMsg: @@ -474,6 +479,27 @@ func (m *Model) renderForwarding() string { s.WriteString("\n\n") s.WriteString(m.message) s.WriteString("\n\n") + + // Add helpful access information + accessStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#7D56F4")). + Bold(true) + + s.WriteString(accessStyle.Render("Access your service:")) + s.WriteString("\n") + + // Extract local port from message for display + if strings.Contains(m.message, "localhost:") { + parts := strings.Split(m.message, "localhost:") + if len(parts) > 1 { + portPart := strings.Split(parts[1], " ")[0] + s.WriteString(fmt.Sprintf(" • http://localhost:%s\n", portPart)) + s.WriteString(fmt.Sprintf(" • https://localhost:%s\n", portPart)) + s.WriteString(fmt.Sprintf(" • Or connect to localhost:%s with any client\n", portPart)) + } + } + + s.WriteString("\n") s.WriteString("Controls:\n") s.WriteString(" Esc: Stop forwarding and return q: Quit\n")