Initial commit

This commit is contained in:
Jason Thistlethwaite
2026-06-07 13:55:01 -04:00
commit 3dde79cfe2
5 changed files with 249 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
#!/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