#!/usr/bin/env bash set -euo pipefail # Script Name: tunnel # Description: SSH tunnel manager with saved configurations # Usage: tunnel list # List saved tunnels # tunnel add name user@host:port ... # Add new tunnel config # tunnel start name # Start a saved tunnel # tunnel stop name # Stop a running tunnel # tunnel status # Show active tunnels VERSION="1.0.0" TUNNEL_DIR="$HOME/.tunnels" TUNNEL_CONF="$TUNNEL_DIR/tunnels.conf" PID_DIR="$TUNNEL_DIR/pids" # Colors readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly CYAN='\033[0;36m' readonly MAGENTA='\033[0;35m' readonly BOLD='\033[1m' readonly NC='\033[0m' # Initialize tunnel directory init_tunnel() { if [[ ! -d "$TUNNEL_DIR" ]]; then mkdir -p "$TUNNEL_DIR" "$PID_DIR" touch "$TUNNEL_CONF" fi } show_help() { echo -e "${BOLD}tunnel${NC} - SSH Tunnel Manager v${VERSION}" echo echo -e "${BOLD}USAGE:${NC}" echo " tunnel [OPTIONS]" echo echo -e "${BOLD}COMMANDS:${NC}" echo -e " ${CYAN}list${NC} List all saved tunnels" echo -e " ${CYAN}add NAME SPEC${NC} Add new tunnel configuration" echo -e " ${CYAN}start NAME${NC} Start a saved tunnel" echo -e " ${CYAN}stop NAME${NC} Stop a running tunnel" echo -e " ${CYAN}restart NAME${NC} Restart a tunnel" echo -e " ${CYAN}status${NC} Show active tunnels" echo -e " ${CYAN}delete NAME${NC} Delete saved tunnel" echo -e " ${CYAN}edit NAME${NC} Edit tunnel configuration" echo echo -e "${BOLD}TUNNEL TYPES:${NC}" echo -e " ${YELLOW}Local forward:${NC} -L local_port:remote_host:remote_port" echo -e " ${YELLOW}Remote forward:${NC} -R remote_port:local_host:local_port" echo -e " ${YELLOW}Dynamic (SOCKS):${NC} -D local_port" echo echo -e "${BOLD}EXAMPLES:${NC}" echo " # Forward local port 3000 to remote port 80" echo " tunnel add web user@server.com -L 3000:localhost:80" echo echo " # Expose local port 8080 on remote port 9000" echo " tunnel add reverse user@server.com -R 9000:localhost:8080" echo echo " # SOCKS proxy on local port 1080" echo " tunnel add socks user@server.com -D 1080" echo echo " # Multiple forwards" echo " tunnel add multi user@server.com -L 3000:localhost:80 -L 5432:localhost:5432" echo echo " # Start/stop tunnels" echo " tunnel start web" echo " tunnel stop web" echo " tunnel status" } # Add tunnel configuration add_tunnel() { local name="$1" shift local ssh_target="$1" shift local ssh_args="$*" if grep -q "^$name|" "$TUNNEL_CONF" 2>/dev/null; then echo -e "${RED}Error:${NC} Tunnel '$name' already exists" echo "Use: tunnel delete $name (then re-add)" return 1 fi echo "$name|$ssh_target|$ssh_args" >> "$TUNNEL_CONF" echo -e "${GREEN}✓${NC} Added tunnel: ${BOLD}$name${NC}" echo -e " Target: $ssh_target" echo -e " Args: $ssh_args" } # List tunnels list_tunnels() { if [[ ! -f "$TUNNEL_CONF" ]] || [[ ! -s "$TUNNEL_CONF" ]]; then echo -e "${YELLOW}No tunnels configured${NC}" echo "Add one with: tunnel add " return 0 fi echo -e "${BOLD}${CYAN}Saved Tunnels:${NC}" echo while IFS='|' read -r name target args; do # Check if running pid_file="$PID_DIR/$name.pid" if [[ -f "$pid_file" ]]; then pid=$(cat "$pid_file") if ps -p "$pid" &>/dev/null; then status="${GREEN}●${NC} running" else status="${RED}●${NC} dead" rm -f "$pid_file" fi else status="${YELLOW}○${NC} stopped" fi echo -e " [$status] ${BOLD}$name${NC}" echo -e " Target: $target" echo -e " Args: $args" echo done < "$TUNNEL_CONF" } # Get tunnel config get_tunnel() { local name="$1" if [[ ! -f "$TUNNEL_CONF" ]]; then echo -e "${RED}Error:${NC} No tunnels configured" >&2 return 1 fi local line=$(grep "^$name|" "$TUNNEL_CONF") if [[ -z "$line" ]]; then echo -e "${RED}Error:${NC} Tunnel '$name' not found" >&2 return 1 fi echo "$line" } # Start tunnel start_tunnel() { local name="$1" local config=$(get_tunnel "$name") IFS='|' read -r _ target args <<< "$config" pid_file="$PID_DIR/$name.pid" # Check if already running if [[ -f "$pid_file" ]]; then pid=$(cat "$pid_file") if ps -p "$pid" &>/dev/null; then echo -e "${YELLOW}⚠${NC} Tunnel '$name' is already running (PID: $pid)" return 0 else rm -f "$pid_file" fi fi echo -e "${CYAN}[*]${NC} Starting tunnel: ${BOLD}$name${NC}" echo -e " Target: $target" echo -e " Args: $args" # Start SSH tunnel in background ssh -f -N $args "$target" & local pid=$! echo "$pid" > "$pid_file" echo -e "${GREEN}✓${NC} Tunnel started (PID: $pid)" } # Stop tunnel stop_tunnel() { local name="$1" pid_file="$PID_DIR/$name.pid" if [[ ! -f "$pid_file" ]]; then echo -e "${YELLOW}⚠${NC} Tunnel '$name' is not running" return 0 fi pid=$(cat "$pid_file") if ! ps -p "$pid" &>/dev/null; then echo -e "${YELLOW}⚠${NC} Tunnel process not found (stale PID file)" rm -f "$pid_file" return 0 fi echo -e "${CYAN}[*]${NC} Stopping tunnel: ${BOLD}$name${NC} (PID: $pid)" kill "$pid" 2>/dev/null || true # Wait a moment and verify sleep 1 if ps -p "$pid" &>/dev/null; then echo -e "${YELLOW}⚠${NC} Process didn't stop gracefully, using SIGKILL" kill -9 "$pid" 2>/dev/null || true fi rm -f "$pid_file" echo -e "${GREEN}✓${NC} Tunnel stopped" } # Restart tunnel restart_tunnel() { local name="$1" echo -e "${CYAN}[*]${NC} Restarting tunnel: ${BOLD}$name${NC}" stop_tunnel "$name" sleep 1 start_tunnel "$name" } # Show status of all tunnels show_status() { echo -e "${BOLD}${CYAN}Active SSH Tunnels:${NC}" echo local found_any=false if [[ -f "$TUNNEL_CONF" ]]; then while IFS='|' read -r name target args; do pid_file="$PID_DIR/$name.pid" if [[ -f "$pid_file" ]]; then pid=$(cat "$pid_file") if ps -p "$pid" &>/dev/null; then found_any=true echo -e " ${GREEN}●${NC} ${BOLD}$name${NC} (PID: $pid)" echo -e " $target $args" echo fi fi done < "$TUNNEL_CONF" fi if [[ "$found_any" == "false" ]]; then echo -e " ${YELLOW}No active tunnels${NC}" fi } # Delete tunnel delete_tunnel() { local name="$1" if ! grep -q "^$name|" "$TUNNEL_CONF" 2>/dev/null; then echo -e "${RED}Error:${NC} Tunnel '$name' not found" return 1 fi # Stop if running pid_file="$PID_DIR/$name.pid" if [[ -f "$pid_file" ]]; then echo -e "${YELLOW}⚠${NC} Stopping running tunnel first..." stop_tunnel "$name" fi # Remove from config sed -i "/^$name|/d" "$TUNNEL_CONF" echo -e "${GREEN}✓${NC} Deleted tunnel: ${BOLD}$name${NC}" } # Edit tunnel edit_tunnel() { local name="$1" if ! grep -q "^$name|" "$TUNNEL_CONF" 2>/dev/null; then echo -e "${RED}Error:${NC} Tunnel '$name' not found" return 1 fi "${EDITOR:-vim}" "$TUNNEL_CONF" echo -e "${GREEN}✓${NC} Configuration updated" } # Initialize init_tunnel # Parse command if [[ $# -eq 0 ]] || [[ "$1" =~ ^(-h|--help|help)$ ]]; then show_help exit 0 fi command="$1" shift case "$command" in list|ls) list_tunnels ;; add|new) if [[ $# -lt 2 ]]; then echo -e "${RED}Error:${NC} Usage: tunnel add " exit 1 fi add_tunnel "$@" ;; start|up) if [[ $# -lt 1 ]]; then echo -e "${RED}Error:${NC} Usage: tunnel start " exit 1 fi start_tunnel "$1" ;; stop|down) if [[ $# -lt 1 ]]; then echo -e "${RED}Error:${NC} Usage: tunnel stop " exit 1 fi stop_tunnel "$1" ;; restart) if [[ $# -lt 1 ]]; then echo -e "${RED}Error:${NC} Usage: tunnel restart " exit 1 fi restart_tunnel "$1" ;; status|ps) show_status ;; delete|del|rm) if [[ $# -lt 1 ]]; then echo -e "${RED}Error:${NC} Usage: tunnel delete " exit 1 fi delete_tunnel "$1" ;; edit) if [[ $# -lt 1 ]]; then echo -e "${RED}Error:${NC} Usage: tunnel edit " exit 1 fi edit_tunnel "$1" ;; *) echo -e "${RED}Error:${NC} Unknown command: $command" echo "Run 'tunnel --help' for usage" exit 1 ;; esac