4 Commits

8 changed files with 195 additions and 119 deletions

View File

@@ -10,13 +10,13 @@ A Go-based command-line tool to manage SSH tunnels. This tool allows you to:
## Installation ## Installation
``` ```
go install github.com/yourusername/sshtunnel/cmd@latest go install git.mvl.sh/vleeuwenmenno/sshtunnel@latest
``` ```
Or clone this repository and build it yourself: Or clone this repository and build it yourself:
``` ```
git clone https://github.com/yourusername/sshtunnel.git git clone https://git.mvl.sh/vleeuwenmenno/sshtunnel.git
cd sshtunnel cd sshtunnel
go build -o sshtunnel ./cmd go build -o sshtunnel ./cmd
``` ```
@@ -34,31 +34,31 @@ This will display all active SSH tunnels with their IDs, local ports, remote end
### Starting a new tunnel ### Starting a new tunnel
``` ```
sshtunnel start -local 8080 -remote 80 -host example.com -server user@ssh-server.com sshtunnel start -l 8080 -r 80 -H example.com -s user@ssh-server.com
``` ```
Options: Options:
- `-local`: Local port to forward (required) - `-l`: Local port to forward (required)
- `-remote`: Remote port to forward to (required) - `-r`: Remote port to forward to (required)
- `-host`: Remote host to forward to (default: "localhost") - `-H`: Remote host to forward to (default: "localhost")
- `-server`: SSH server address in the format user@host (required) - `-s`: SSH server address in the format user@host (required)
- `-identity`: Path to SSH identity file (optional) - `-i`: Path to SSH identity file (optional)
### Stopping tunnels ### Stopping tunnels
Stop a specific tunnel by ID: Stop a specific tunnel by ID:
``` ```
sshtunnel stop -id 1 sshtunnel stop -i 1
``` ```
Stop all active tunnels: Stop all active tunnels:
``` ```
sshtunnel stop -all sshtunnel stop --all
``` ```
Options: Options:
- `-id`: ID of the tunnel to stop - `-i`: ID of the tunnel to stop
- `-all`: Stop all tunnels - `--all`: Stop all tunnels
### Viewing traffic statistics ### Viewing traffic statistics
@@ -69,12 +69,12 @@ sshtunnel stats --all
View statistics for a specific tunnel: View statistics for a specific tunnel:
``` ```
sshtunnel stats --id 1 sshtunnel stats -i 1
``` ```
Monitor tunnel traffic in real-time: Monitor tunnel traffic in real-time:
``` ```
sshtunnel stats --id 1 --watch sshtunnel stats -i 1 --watch
``` ```
Options: Options:
@@ -107,4 +107,4 @@ The tool creates SSH tunnels using the system's SSH client and manages them by t
## License ## License
MIT MIT

76
bin/scripts/release.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
source bin/helpers/func.sh
printfe "%s\n" "green" "SSH Tunnel Manager Release Script"
printfe "%s\n" "normal" "==============================="
# Check if git is installed
if ! command -v git &> /dev/null; then
printfe "%s\n" "red" "Error: git is not installed"
exit 1
fi
# Check if we're in a git repository
if ! git rev-parse --is-inside-work-tree &> /dev/null; then
printfe "%s\n" "red" "Error: Not in a git repository"
exit 1
fi
# Get the latest tag or default to v0.0.0 if no tags exist
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
# Parse the version number
if [[ $LATEST_TAG =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
else
printfe "%s\n" "red" "Error: Could not parse latest tag: $LATEST_TAG"
exit 1
fi
# Calculate the next patch version
NEXT_PATCH=$((PATCH + 1))
NEXT_VERSION="v$MAJOR.$MINOR.$NEXT_PATCH"
# Display current version and suggested next version
printfe "%s" "cyan" "Current version: "
printfe "%s\n" "yellow" "$LATEST_TAG" false
printfe "%s" "cyan" "Suggested next version: "
printfe "%s\n" "yellow" "$NEXT_VERSION" false
# Ask for confirmation or custom version
read -p "Accept suggested version? (y/n) " ACCEPT
if [[ $ACCEPT != "y" && $ACCEPT != "Y" ]]; then
read -p "Enter custom version (format vX.Y.Z): " CUSTOM_VERSION
if [[ $CUSTOM_VERSION =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
NEXT_VERSION=$CUSTOM_VERSION
else
printfe "%s\n" "red" "Error: Invalid version format. Must be vX.Y.Z where X, Y, Z are numbers."
exit 1
fi
fi
printfe "%s" "cyan" "Creating and pushing tag: "
printfe "%s\n" "yellow" "$NEXT_VERSION" false
# Make sure we have the latest changes
printfe "%s\n" "cyan" "Fetching latest tags..."
log_and_run git fetch --tags
# Create the new tag
printfe "%s\n" "cyan" "Creating new tag..."
log_and_run git tag "$NEXT_VERSION"
# Push the new tag
printfe "%s\n" "cyan" "Pushing new tag..."
log_and_run git push origin "$NEXT_VERSION"
# Update the latest tag
printfe "%s\n" "cyan" "Updating 'latest' tag..."
log_and_run git tag -f latest "$NEXT_VERSION"
log_and_run git push -f origin latest
printfe "%s\n" "green" "Successfully tagged and pushed version $NEXT_VERSION"
printfe "%s\n" "cyan" "Also updated 'latest' tag to point to $NEXT_VERSION"

View File

@@ -8,8 +8,8 @@ import (
"strings" "strings"
"time" "time"
"git.mvl.sh/vleeuwenmenno/sshtunnel/pkg/stats"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sshtunnel/pkg/stats"
) )
var ( var (
@@ -28,106 +28,106 @@ var startCmd = &cobra.Command{
The tunnel will run in the background and can be managed using the list and stop commands.`, The tunnel will run in the background and can be managed using the list and stop commands.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// Check required flags // Check required flags
// Generate the SSH command with appropriate flags for reliable background operation // Generate the SSH command with appropriate flags for reliable background operation
sshArgs := []string{ sshArgs := []string{
"-N", // Don't execute remote command "-N", // Don't execute remote command
"-f", // Run in background "-f", // Run in background
"-L", fmt.Sprintf("%d:%s:%d", localPort, remoteHost, remotePort), "-L", fmt.Sprintf("%d:%s:%d", localPort, remoteHost, remotePort),
}
if identity != "" {
sshArgs = append(sshArgs, "-i", identity)
}
sshArgs = append(sshArgs, sshServer)
sshCmd := exec.Command("ssh", sshArgs...)
// Capture output for debugging
var outputBuffer strings.Builder
sshCmd.Stdout = &outputBuffer
sshCmd.Stderr = &outputBuffer
// Run the command (not just Start) - the -f flag means it will return immediately
// after going to the background
if err := sshCmd.Run(); err != nil {
fmt.Printf("Error starting SSH tunnel: %v\n", err)
fmt.Printf("SSH output: %s\n", outputBuffer.String())
os.Exit(1)
}
// The PID from cmd.Process is no longer valid since ssh -f forks
// We need to find the actual SSH process PID
actualPID, err := findSSHTunnelPID(localPort)
if err != nil {
fmt.Printf("Warning: Could not determine tunnel PID: %v\n", err)
}
// Store tunnel information
id := generateTunnelID()
pid := 0
if actualPID > 0 {
pid = actualPID
}
tunnel := Tunnel{
ID: id,
LocalPort: localPort,
RemotePort: remotePort,
RemoteHost: remoteHost,
SSHServer: sshServer,
PID: pid,
}
if err := saveTunnel(tunnel); err != nil {
fmt.Printf("Error saving tunnel information: %v\n", err)
if pid > 0 {
// Try to kill the process
process, _ := os.FindProcess(pid)
if process != nil {
process.Kill()
}
} }
os.Exit(1)
}
if identity != "" { // Initialize statistics for this tunnel
sshArgs = append(sshArgs, "-i", identity) statsManager, err := stats.NewStatsManager()
} if err == nil {
err = statsManager.InitStats(id, localPort)
sshArgs = append(sshArgs, sshServer)
sshCmd := exec.Command("ssh", sshArgs...)
// Capture output for debugging
var outputBuffer strings.Builder
sshCmd.Stdout = &outputBuffer
sshCmd.Stderr = &outputBuffer
// Run the command (not just Start) - the -f flag means it will return immediately
// after going to the background
if err := sshCmd.Run(); err != nil {
fmt.Printf("Error starting SSH tunnel: %v\n", err)
fmt.Printf("SSH output: %s\n", outputBuffer.String())
os.Exit(1)
}
// The PID from cmd.Process is no longer valid since ssh -f forks
// We need to find the actual SSH process PID
actualPID, err := findSSHTunnelPID(localPort)
if err != nil { if err != nil {
fmt.Printf("Warning: Could not determine tunnel PID: %v\n", err) fmt.Printf("Warning: Failed to initialize statistics: %v\n", err)
} }
}
// Store tunnel information // Verify tunnel is actually working
id := generateTunnelID() time.Sleep(500 * time.Millisecond)
pid := 0 active := verifyTunnelActive(localPort)
status := "ACTIVE"
if actualPID > 0 { if !active {
pid = actualPID status = "UNKNOWN"
} }
tunnel := Tunnel{
ID: id,
LocalPort: localPort,
RemotePort: remotePort,
RemoteHost: remoteHost,
SSHServer: sshServer,
PID: pid,
}
if err := saveTunnel(tunnel); err != nil { fmt.Printf("Started SSH tunnel (ID: %d): localhost:%d -> %s:%d (%s) [PID: %d] [Status: %s]\n",
fmt.Printf("Error saving tunnel information: %v\n", err) id, localPort, remoteHost, remotePort, sshServer, pid, status)
if pid > 0 { },
// Try to kill the process }
process, _ := os.FindProcess(pid)
if process != nil {
process.Kill()
}
}
os.Exit(1)
}
// Initialize statistics for this tunnel
statsManager, err := stats.NewStatsManager()
if err == nil {
err = statsManager.InitStats(id, localPort)
if err != nil {
fmt.Printf("Warning: Failed to initialize statistics: %v\n", err)
}
}
// Verify tunnel is actually working func init() {
time.Sleep(500 * time.Millisecond) rootCmd.AddCommand(startCmd)
active := verifyTunnelActive(localPort)
status := "ACTIVE"
if !active {
status = "UNKNOWN"
}
fmt.Printf("Started SSH tunnel (ID: %d): localhost:%d -> %s:%d (%s) [PID: %d] [Status: %s]\n", // Add flags for the start command
id, localPort, remoteHost, remotePort, sshServer, pid, status) startCmd.Flags().IntVarP(&localPort, "local", "l", 0, "Local port to forward")
}, startCmd.Flags().IntVarP(&remotePort, "remote", "r", 0, "Remote port to forward to")
} startCmd.Flags().StringVarP(&remoteHost, "host", "H", "localhost", "Remote host to forward to")
startCmd.Flags().StringVarP(&sshServer, "server", "s", "", "SSH server address (user@host)")
startCmd.Flags().StringVarP(&identity, "identity", "i", "", "Path to SSH identity file")
func init() { // Mark required flags
rootCmd.AddCommand(startCmd) startCmd.MarkFlagRequired("local")
startCmd.MarkFlagRequired("remote")
// Add flags for the start command startCmd.MarkFlagRequired("server")
startCmd.Flags().IntVarP(&localPort, "local", "l", 0, "Local port to forward") }
startCmd.Flags().IntVarP(&remotePort, "remote", "r", 0, "Remote port to forward to")
startCmd.Flags().StringVarP(&remoteHost, "host", "H", "localhost", "Remote host to forward to")
startCmd.Flags().StringVarP(&sshServer, "server", "s", "", "SSH server address (user@host)")
startCmd.Flags().StringVarP(&identity, "identity", "i", "", "Path to SSH identity file")
// Mark required flags
startCmd.MarkFlagRequired("local")
startCmd.MarkFlagRequired("remote")
startCmd.MarkFlagRequired("server")
}
// verifyTunnelActive checks if the tunnel is actually working // verifyTunnelActive checks if the tunnel is actually working
func verifyTunnelActive(port int) bool { func verifyTunnelActive(port int) bool {
@@ -138,4 +138,4 @@ func verifyTunnelActive(port int) bool {
} }
conn.Close() conn.Close()
return true return true
} }

View File

@@ -5,8 +5,8 @@ import (
"os" "os"
"time" "time"
"sshtunnel/pkg/monitor" "git.mvl.sh/vleeuwenmenno/sshtunnel/pkg/monitor"
"sshtunnel/pkg/stats" "git.mvl.sh/vleeuwenmenno/sshtunnel/pkg/stats"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -181,7 +181,7 @@ func watchTunnelStats(tunnelID int, interval int) {
if err != nil { if err != nil {
fmt.Printf("Warning: Tunnel #%d may not be active: %v\n", tunnelID, err) fmt.Printf("Warning: Tunnel #%d may not be active: %v\n", tunnelID, err)
fmt.Println("Attempting to monitor anyway...") fmt.Println("Attempting to monitor anyway...")
// Try to get port from stats // Try to get port from stats
statsManager, _ := stats.NewStatsManager() statsManager, _ := stats.NewStatsManager()
s, err := statsManager.GetStats(tunnelID) s, err := statsManager.GetStats(tunnelID)
@@ -226,7 +226,7 @@ func watchTunnelStats(tunnelID int, interval int) {
if err != nil { if err != nil {
fmt.Printf("Warning: %v\n", err) fmt.Printf("Warning: %v\n", err)
fmt.Println("Displaying minimal statistics...") fmt.Println("Displaying minimal statistics...")
statsStr = fmt.Sprintf("Monitoring tunnel #%d on port %d...", statsStr = fmt.Sprintf("Monitoring tunnel #%d on port %d...",
tunnelID, tunnel.LocalPort) tunnelID, tunnel.LocalPort)
} }
fmt.Println(statsStr) fmt.Println(statsStr)
@@ -241,7 +241,7 @@ func watchTunnelStats(tunnelID int, interval int) {
statsStr, err := mon.FormatStats() statsStr, err := mon.FormatStats()
if err != nil { if err != nil {
statsStr = fmt.Sprintf("Warning: %v\n\nStill monitoring tunnel #%d on port %d...", statsStr = fmt.Sprintf("Warning: %v\n\nStill monitoring tunnel #%d on port %d...",
err, tunnelID, tunnel.LocalPort) err, tunnelID, tunnel.LocalPort)
} }

View File

@@ -5,8 +5,8 @@ import (
"os" "os"
"syscall" "syscall"
"git.mvl.sh/vleeuwenmenno/sshtunnel/pkg/stats"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sshtunnel/pkg/stats"
) )
var ( var (
@@ -86,7 +86,7 @@ func killTunnel(t Tunnel) {
removeFile(t.ID) removeFile(t.ID)
cleanupStats(t.ID) cleanupStats(t.ID)
fmt.Printf("Stopped SSH tunnel (ID: %d): localhost:%d -> %s:%d\n", fmt.Printf("Stopped SSH tunnel (ID: %d): localhost:%d -> %s:%d\n",
t.ID, t.LocalPort, t.RemoteHost, t.RemotePort) t.ID, t.LocalPort, t.RemoteHost, t.RemotePort)
} }
@@ -102,4 +102,4 @@ func cleanupStats(tunnelID int) {
if err != nil { if err != nil {
fmt.Printf("Warning: Failed to delete statistics for tunnel %d: %v\n", tunnelID, err) fmt.Printf("Warning: Failed to delete statistics for tunnel %d: %v\n", tunnelID, err)
} }
} }

2
go.mod
View File

@@ -1,4 +1,4 @@
module sshtunnel module git.mvl.sh/vleeuwenmenno/sshtunnel
go 1.22.2 go 1.22.2

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"sshtunnel/cmd" "git.mvl.sh/vleeuwenmenno/sshtunnel/cmd"
) )
func main() { func main() {

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"sshtunnel/pkg/stats" "git.mvl.sh/vleeuwenmenno/sshtunnel/pkg/stats"
) )
// Monitor represents a SSH tunnel traffic monitor // Monitor represents a SSH tunnel traffic monitor