mirror of
https://github.com/jthistlethwaite/ssh-pipeweasel.git
synced 2026-06-30 07:17:40 -04:00
116 lines
3.6 KiB
Bash
Executable File
116 lines
3.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# Save to: ~/.ssh/ssh-failover.sh
|
|
|
|
TARGET_HOST="$1"
|
|
CONFIG_DIR="$HOME/.ssh/hosts.d"
|
|
|
|
# Force lower-case comparison to prevent case mismatches from SSH %h token
|
|
TARGET_LOWER=$(echo "$TARGET_HOST" | tr '[:upper:]' '[:lower:]')
|
|
CONFIG_FILE="$CONFIG_DIR/proxyrule.${TARGET_LOWER}.conf"
|
|
|
|
TIMEOUT=0.5
|
|
DEBUG=${DEBUG:-false}
|
|
|
|
if [ ! -f "$CONFIG_FILE" ]; then
|
|
echo "FAILOVER ERROR: Config file missing at $CONFIG_FILE" >&2
|
|
exit 1
|
|
fi
|
|
|
|
source "$CONFIG_FILE"
|
|
|
|
if [ ${#ENDPOINTS[@]} -eq 0 ]; then
|
|
echo "FAILOVER ERROR: No ENDPOINTS array found inside $CONFIG_FILE" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Helper function to resolve hostnames to IP addresses before the timer starts
|
|
resolve_to_ip() {
|
|
local target=$1
|
|
# Check if it's already an IPv4 or IPv6 address to save process execution
|
|
if [[ "$target" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ || "$target" =~ : ]]; then
|
|
echo "$target"
|
|
else
|
|
# Resolve using standard system hosts databases (handles DNS, /etc/hosts, etc.)
|
|
local ip
|
|
ip=$(getent hosts "$target" | awk '{print $1; exit}')
|
|
# Fall back to dig if getent isn't providing a quick single target ip
|
|
[ -z "$ip" ] && ip=$(dig +short "$target" | tail -n1)
|
|
# If both fail, return original target so nc can try anyway
|
|
echo "${ip:-$target}"
|
|
fi
|
|
}
|
|
|
|
get_tcp_ms() {
|
|
local host=$1
|
|
local port=$2
|
|
local start=$EPOCHREALTIME
|
|
|
|
if timeout "$TIMEOUT" nc -z "$host" "$port" 2>/dev/null; then
|
|
local end=$EPOCHREALTIME
|
|
awk "BEGIN {print int(($end - $start) * 1000)}"
|
|
else
|
|
echo "9999"
|
|
fi
|
|
}
|
|
|
|
declare -a results
|
|
declare -a resolved_hosts
|
|
fastest_ms=9999
|
|
chosen_index=""
|
|
|
|
for ((i=0; i<${#ENDPOINTS[@]}; i++)); do
|
|
IFS="|" read -r label host port <<< "${ENDPOINTS[$i]}"
|
|
|
|
# CRITICAL BUGFIX: Strip any hidden trailing newlines (\n) or carriage returns (\r)
|
|
port=$(echo "$port" | tr -d '\r\n[:space:]')
|
|
|
|
# Pre-resolve hostnames to eliminate DNS lookup latency skew
|
|
resolved_host=$(resolve_to_ip "$host")
|
|
resolved_hosts[$i]=$resolved_host
|
|
|
|
ms=$(get_tcp_ms "$resolved_host" "$port")
|
|
results[$i]=$ms
|
|
|
|
# if [ "$DEBUG" = true ]; then
|
|
# echo "Comparing $ms to $fastest_ms"
|
|
# fi
|
|
|
|
if [ "$ms" -lt "$fastest_ms" ]; then
|
|
fastest_ms=$ms
|
|
chosen_index=$i
|
|
fi
|
|
done
|
|
|
|
# --- DYNAMIC DEBUG DISPLAY LOOP ---
|
|
if [ "$DEBUG" = true ]; then
|
|
echo "--------------------------------------------------------" >&2
|
|
printf "Testing routes for: \e[1;34m%s\e[0m\n" "$TARGET_HOST" >&2
|
|
echo "--------------------------------------------------------" >&2
|
|
printf "%-25s | %-12s | %-10s\n" "Endpoint / Host" "Status" "Latency" >&2
|
|
echo "--------------------------------------------------------" >&2
|
|
|
|
for ((i=0; i<${#ENDPOINTS[@]}; i++)); do
|
|
IFS="|" read -r label host port <<< "${ENDPOINTS[$i]}"
|
|
ms=${results[$i]}
|
|
|
|
if [ "$ms" -eq 9999 ]; then
|
|
printf "%-25s | \e[31m%-12s\e[0m | %-10s\n" "$label" "OFFLINE" "---" >&2
|
|
else
|
|
printf "%-25s | \e[32m%-12s\e[0m | %-10s\n" "$label" "ONLINE" "${ms}ms" >&2
|
|
fi
|
|
done
|
|
echo "--------------------------------------------------------" >&2
|
|
fi
|
|
|
|
# 2. Route traffic to the fastest endpoint found
|
|
if [ -n "$chosen_index" ] && [ "$fastest_ms" -lt 9999 ]; then
|
|
IFS="|" read -r label host port <<< "${ENDPOINTS[$chosen_index]}"
|
|
port=$(echo "$port" | tr -d '\r\n[:space:]')
|
|
|
|
[ "$DEBUG" = true ] && echo "-> Routing via $label ($host:$port)..." >&2
|
|
exec nc "$host" "$port"
|
|
else
|
|
echo "FAILOVER ERROR: All paths to $TARGET_HOST are dead." >&2
|
|
exit 1
|
|
fi
|