Compare commits

...

3 Commits

Author SHA1 Message Date
Ona
12928c4736 Update README with smart port mapping feature
- Document the new smart port mapping functionality
- Add --test-port command example
- Fix duplicate step numbering in usage section
- Explain same-port preference and fallback behavior

Co-authored-by: Ona <no-reply@ona.com>
2025-09-26 21:30:54 +00:00
Ona
66c6ba9307 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 <no-reply@ona.com>
2025-09-26 21:30:14 +00:00
Ona
12f188de75 Update publishing status
Document current commits ready to push and authentication issue.

Co-authored-by: Ona <no-reply@ona.com>
2025-09-26 00:43:16 +00:00
5 changed files with 145 additions and 11 deletions

View File

@@ -35,6 +35,26 @@ The repository has been prepared and committed locally. To publish to https://co
- Remote configured: ✅
- Files committed: ✅
- Binary properly ignored: ✅
- Screenshot added: ✅
- SSH command rewrite completed: ✅
- Ready to push: ✅
## Current Commits Ready to Push:
```
58c10d5 Add screenshot to README
9ec67e9 Replace Go SSH library with native ssh command
02322c4 Remove debug log file
bde1529 Fix port detection and manual forwarding issues
70307c7 Document quoted include support in README
a332459 Update README with new features
5ebe20a Improve manual port input UI styling
df3c9fe Add support for SSH config includes
6e1ee6d Add .gitignore file
e39a595 Initial commit: kport - SSH Port Forwarder TUI
```
## Push Issue:
Authentication required - push manually from local machine with credentials.
The application is fully functional and ready for distribution!

View File

@@ -14,6 +14,7 @@ A terminal user interface (TUI) application for SSH port forwarding that reads f
- **Interactive Host Selection**: Choose from configured SSH hosts using arrow keys
- **Automatic Port Detection**: Scans remote host for listening ports using `netstat`, `ss`, or `lsof`
- **Manual Port Forwarding**: Option to manually specify remote ports with improved UI
- **Smart Port Mapping**: Tries to use same port locally (e.g., remote:3000 → localhost:3000)
- **Real-time Port Forwarding**: Creates SSH tunnels using `ssh -L` command
- **Clean TUI Interface**: Built with Bubble Tea for a smooth terminal experience
@@ -30,14 +31,24 @@ go build -o kport
./kport
```
2. **Select SSH Host**: Use arrow keys to navigate and press Enter to select an SSH host from your config
2. **Test port mapping** (optional):
```bash
./kport --test-port 3000
# Shows: ✅ Port 3000 available locally - using same port
# Mapping: localhost:3000 -> remote:3000
```
3. **Choose Port**:
3. **Select SSH Host**: Use arrow keys to navigate and press Enter to select an SSH host from your config
4. **Choose Port**:
- The app will automatically detect open ports on the remote host
- Select a port to forward using arrow keys and Enter
- Press 'm' for manual port entry
4. **Port Forwarding**: Once started, the app will show the local port that forwards to your remote port
5. **Port Forwarding**:
- kport tries to use the same port locally (e.g., remote:3000 → localhost:3000)
- If unavailable, it uses a random available port
- Clear feedback shows the actual mapping and access URLs
## Controls

43
main.go
View File

@@ -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 <hostname>")
fmt.Println("To test port mapping logic: ./kport --test-port <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.")
}

View File

@@ -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")

30
tui.go
View File

@@ -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")