Compare commits
3 Commits
58c10d5a8e
...
12928c4736
Author | SHA1 | Date | |
---|---|---|---|
12928c4736 | |||
66c6ba9307 | |||
12f188de75 |
20
PUBLISH.md
20
PUBLISH.md
@@ -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!
|
17
README.md
17
README.md
@@ -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
43
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 <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.")
|
||||
}
|
@@ -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
30
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")
|
||||
|
||||
|
Reference in New Issue
Block a user