Created
September 10, 2025 14:37
-
-
Save tomterragni/d7fccd6cf897098f03684cf414618c75 to your computer and use it in GitHub Desktop.
Script to automate ssh connection and allow tunnelling of remote port 3306 to a local port
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| # ---------- Defaults ---------- | |
| CERT="" | |
| USER="" | |
| HOST="" | |
| DO_TUN=false | |
| DO_STOP=false | |
| DO_STATUS=false | |
| DO_LIST=false | |
| LOCAL_PORT="" | |
| # Directory to store SSH control sockets | |
| CTL_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/ssh-tunnels" | |
| mkdir -p "$CTL_DIR" | |
| helpFunction(){ | |
| cat <<EOF | |
| ================================================================ | |
| Connects to a specific host with optional MySQL tunnel. | |
| ================================================================ | |
| Usage: $(basename "$0") --ip <ip_address> --user <username> --cert <cert_path> [--port <port>] [--tun] [--stop] [--status] [--list] [-h] | |
| --ip <ip> Target IP address (required) | |
| --user <user> SSH username (required) | |
| --cert <path> Path to SSH certificate/key file (required) | |
| --port <port> Local port for tunnel (default: auto-generated from IP) | |
| --tun Start a local tunnel: 127.0.0.1:<port> -> remote 127.0.0.1:3306 | |
| --stop Stop the tunnel for this host (if running) | |
| --status Check tunnel status for this host | |
| --list List status of all tunnels started by $(basename "$0") | |
| -h Print this help and exit | |
| Examples: | |
| # SSH shell | |
| $(basename "$0") --ip <ip_of_remote_server> --user root --cert /path/to/cert.pem | |
| # Start tunnel only (no shell), auto-generated local port | |
| $(basename "$0") --ip <ip_of_remote_server> --user root --cert /path/to/cert.pem --tun | |
| # Start tunnel with specific local port | |
| $(basename "$0") --ip <ip_of_remote_server> --user root --cert /path/to/cert.pem --port 3342 --tun | |
| # Stop tunnel | |
| $(basename "$0") --ip <ip_of_remote_server> --user root --cert /path/to/cert.pem --stop | |
| # Check status | |
| $(basename "$0") --ip <ip_of_remote_server> --user root --cert /path/to/cert.pem --status | |
| # List all tunnels | |
| $(basename "$0") --list | |
| EOF | |
| exit 0 | |
| } | |
| # Function to generate a local port from IP address | |
| generate_port_from_ip() { | |
| local ip="$1" | |
| # Extract the last octet and add 3300 to it | |
| local last_octet="${ip##*.}" | |
| echo "$((3300 + last_octet))" | |
| } | |
| # Function to validate IP address | |
| validate_ip() { | |
| local ip="$1" | |
| if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then | |
| IFS='.' read -ra ADDR <<< "$ip" | |
| for i in "${ADDR[@]}"; do | |
| if [[ $((10#$i)) -gt 255 ]]; then | |
| echo "Invalid IP address: $ip" | |
| exit 1 | |
| fi | |
| done | |
| else | |
| echo "Invalid IP address format: $ip" | |
| exit 1 | |
| fi | |
| } | |
| # Function to validate certificate file | |
| validate_cert() { | |
| local cert="$1" | |
| if [[ ! -f "$cert" ]]; then | |
| echo "Certificate file does not exist: $cert" | |
| exit 1 | |
| fi | |
| } | |
| # ---------- Parse args ---------- | |
| if (( $# == 0 )); then echo "No arguments provided"; helpFunction; fi | |
| while (( "$#" )); do | |
| case "$1" in | |
| --ip) | |
| if [[ -n "${2:-}" ]] && [[ ${2:0:1} != "-" ]]; then | |
| HOST="$2" | |
| validate_ip "$HOST" | |
| shift 2 | |
| else | |
| echo "Error: --ip requires an IP address" >&2 | |
| exit 1 | |
| fi | |
| ;; | |
| --user) | |
| if [[ -n "${2:-}" ]] && [[ ${2:0:1} != "-" ]]; then | |
| USER="$2" | |
| shift 2 | |
| else | |
| echo "Error: --user requires a username" >&2 | |
| exit 1 | |
| fi | |
| ;; | |
| --cert) | |
| if [[ -n "${2:-}" ]] && [[ ${2:0:1} != "-" ]]; then | |
| CERT="$2" | |
| validate_cert "$CERT" | |
| shift 2 | |
| else | |
| echo "Error: --cert requires a certificate file path" >&2 | |
| exit 1 | |
| fi | |
| ;; | |
| --port) | |
| if [[ -n "${2:-}" ]] && [[ ${2:0:1} != "-" ]]; then | |
| if [[ "$2" =~ ^[0-9]+$ ]] && (( $2 >= 1024 && $2 <= 65535 )); then | |
| LOCAL_PORT="$2" | |
| shift 2 | |
| else | |
| echo "Error: --port requires a valid port number (1024-65535)" >&2 | |
| exit 1 | |
| fi | |
| else | |
| echo "Error: --port requires a port number" >&2 | |
| exit 1 | |
| fi | |
| ;; | |
| --tun) DO_TUN=true; shift ;; | |
| --stop) DO_STOP=true; shift ;; | |
| --status) DO_STATUS=true; shift ;; | |
| --list) DO_LIST=true; shift ;; | |
| -h) helpFunction ;; | |
| -* ) echo "Unknown flag $1"; helpFunction ;; | |
| * ) echo "Unknown argument '$1'. Use flags to specify parameters."; helpFunction ;; | |
| esac | |
| done | |
| # If just listing, no other parameters required | |
| if $DO_LIST; then | |
| : | |
| else | |
| # Validate required parameters | |
| if [[ -z "$HOST" ]]; then | |
| echo "Error: --ip is required" | |
| helpFunction | |
| fi | |
| if [[ -z "$USER" ]]; then | |
| echo "Error: --user is required" | |
| helpFunction | |
| fi | |
| if [[ -z "$CERT" ]]; then | |
| echo "Error: --cert is required" | |
| helpFunction | |
| fi | |
| # Generate local port if not specified | |
| if [[ -z "$LOCAL_PORT" ]]; then | |
| LOCAL_PORT=$(generate_port_from_ip "$HOST") | |
| fi | |
| fi | |
| CTL="${CTL_DIR}/ssh-${USER}@${HOST}-${LOCAL_PORT}.ctl" | |
| # ---------- Helpers ---------- | |
| tunnel_start() { | |
| ssh -i "$CERT" \ | |
| -M -S "$CTL" \ | |
| -fNT \ | |
| -o ExitOnForwardFailure=yes \ | |
| -o ServerAliveInterval=60 -o ServerAliveCountMax=3 \ | |
| -L "127.0.0.1:${LOCAL_PORT}:127.0.0.1:3306" \ | |
| "${USER}@${HOST}" | |
| echo "Tunnel UP: 127.0.0.1:${LOCAL_PORT} → ${HOST}:3306" | |
| echo "Control socket: $CTL" | |
| } | |
| tunnel_stop() { | |
| if ssh -S "$CTL" -O check "${USER}@${HOST}" >/dev/null 2>&1; then | |
| ssh -S "$CTL" -O exit "${USER}@${HOST}" >/dev/null 2>&1 || true | |
| echo "Tunnel STOPPED for ${HOST}" | |
| else | |
| echo "No active tunnel for ${HOST}" | |
| fi | |
| } | |
| tunnel_status() { | |
| if ssh -S "$CTL" -O check "${USER}@${HOST}" >/dev/null 2>&1; then | |
| echo "Tunnel is RUNNING for ${HOST} (local: ${LOCAL_PORT})" | |
| else | |
| echo "Tunnel is NOT running for ${HOST}" | |
| fi | |
| } | |
| list_all() { | |
| shopt -s nullglob | |
| local any=0 | |
| printf "%-22s %-22s %-10s %s\n" "Local endpoint" "Remote endpoint" "Status" "Socket" | |
| printf "%-22s %-22s %-10s %s\n" "----------------------" "----------------------" "----------" "-----" | |
| for sock in "$CTL_DIR"/ssh-*@*-*.ctl; do | |
| any=1 | |
| # sock name pattern: ssh-<user>@<host>-<localport>.ctl | |
| local base; base="$(basename "$sock")" | |
| local u_at_h="${base#ssh-}"; u_at_h="${u_at_h%-*.ctl}" | |
| local lp="${base##*-}"; lp="${lp%.ctl}" | |
| local user_part="${u_at_h%@*}" | |
| local host_part="${u_at_h#*@}" | |
| local status="DOWN" | |
| if ssh -S "$sock" -O check "${user_part}@${host_part}" >/dev/null 2>&1; then | |
| status="RUNNING" | |
| fi | |
| printf "%-22s %-22s %-10s %s\n" "127.0.0.1:${lp}" "${host_part}:3306" "$status" "$sock" | |
| done | |
| shopt -u nullglob | |
| if (( any == 0 )); then | |
| echo "No tunnels found in $CTL_DIR" | |
| fi | |
| } | |
| connect_shell() { | |
| ssh -i "$CERT" "${USER}@${HOST}" | |
| } | |
| # ---------- Main ---------- | |
| if $DO_LIST; then | |
| list_all | |
| exit 0 | |
| fi | |
| if $DO_STATUS; then | |
| tunnel_status | |
| exit 0 | |
| fi | |
| if $DO_STOP; then | |
| tunnel_stop | |
| exit 0 | |
| fi | |
| if $DO_TUN; then | |
| tunnel_start | |
| exit 0 | |
| fi | |
| # default: interactive SSH (no tunnel) | |
| connect_shell |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment