Some checks failed
Kernel-level (`ip tunnel change`), not VyOS config: no commit churn on a flapping line, no drift against the Pulumi model, and a reboot restores config.boot — which pins the 10 gig — so a wrong source cannot survive a restart. Sends `myip` explicitly, because mid-failover the update request may egress either line and letting HE infer the address would point the tunnel at the WAN we just left. Requires two consecutive agreeing runs before acting, since HE rate-limits updates and a flapping WAN would hammer the API precisely when it matters. MTU moves with the WAN: 1480 on the 10 gig (1500-20), 1472 on PPPoE (1492-20). Fixed at 1480, the backup path gives the signature people lose a day to — small packets fine, large transfers hang. Inert without /config/he-secrets, and a no-op when already in sync.
122 lines
5.4 KiB
Bash
Executable File
122 lines
5.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# Keep the Hurricane Electric 6in4 tunnel pointed at whichever WAN is live.
|
|
#
|
|
# The tunnel is anchored to a source IPv4. When failover moves the default route
|
|
# from the 10 gig to PPPoE, 6in4 packets keep leaving with the old source, HE
|
|
# drops them, and IPv6 goes dark while IPv4 keeps working -- a partial outage
|
|
# that presents as "some sites are broken", which is far worse to diagnose than
|
|
# a clean one.
|
|
#
|
|
# Changes are made at KERNEL level (`ip tunnel change`), not in VyOS config, on
|
|
# purpose:
|
|
# - no commit per WAN flip, so a flapping line cannot churn the config;
|
|
# - no drift against the Pulumi model, so `vyos-verify` stays meaningful;
|
|
# - a reboot restores config.boot, which pins the 10 gig -- the correct
|
|
# default -- so the wrong state cannot survive a restart.
|
|
#
|
|
# he-tunnel-follow status what is live vs what should be (read-only)
|
|
# he-tunnel-follow run reconcile, updating HE if the source changed
|
|
# he-tunnel-follow run --dry say what it would do, change nothing
|
|
#
|
|
# Credentials in /config/he-secrets (0600), NOT in git:
|
|
# HE_USER=<tunnelbroker username>
|
|
# HE_UPDATE_KEY=<from the tunnel's Advanced tab -- replaces the account password>
|
|
# HE_TUNNEL_ID=<numeric tunnel id>
|
|
set -uo pipefail
|
|
|
|
TUNNEL="${TUNNEL:-tun0}"
|
|
SECRETS="${SECRETS:-/config/he-secrets}"
|
|
STATE="${STATE:-/run/he-tunnel-follow.state}"
|
|
# 6in4 costs 20 bytes. The 10 gig path is 1500 -> 1480; PPPoE is 1492 -> 1472.
|
|
# Getting this wrong is the classic "IPv6 works until something large" failure.
|
|
declare -A WAN_MTU=( ["bond0.53"]=1480 ["pppoe0"]=1472 )
|
|
# Require the same answer twice before acting. HE rate-limits updates, and a
|
|
# flapping WAN would otherwise hammer the API exactly when it is needed most.
|
|
HYSTERESIS="${HYSTERESIS:-2}"
|
|
|
|
log() { logger -t he-tunnel-follow -- "$*"; printf ' %s\n' "$*"; }
|
|
die() { logger -t he-tunnel-follow -p user.err -- "$*"; printf ' ERROR: %s\n' "$*" >&2; exit 1; }
|
|
|
|
active_wan() { ip -4 route show default 2>/dev/null | awk '/^default/{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1); exit}'; }
|
|
addr_of() { ip -4 -br addr show "$1" 2>/dev/null | awk '{print $3}' | cut -d/ -f1; }
|
|
tunnel_src() { ip tunnel show "$TUNNEL" 2>/dev/null | sed -nE 's/.* local ([0-9.]+).*/\1/p'; }
|
|
tunnel_mtu() { cat "/sys/class/net/$TUNNEL/mtu" 2>/dev/null; }
|
|
|
|
# HE's dyndns-style endpoint. `myip` is passed EXPLICITLY rather than letting HE
|
|
# infer it from the request source: mid-failover the request itself may egress
|
|
# either line, and inferring would happily point the tunnel at the WAN we just
|
|
# left.
|
|
he_update() {
|
|
local ip="$1"
|
|
[ -r "$SECRETS" ] || die "no $SECRETS -- create it with HE_USER / HE_UPDATE_KEY / HE_TUNNEL_ID (0600)"
|
|
# shellcheck disable=SC1090
|
|
. "$SECRETS"
|
|
[ -n "${HE_USER:-}" ] && [ -n "${HE_UPDATE_KEY:-}" ] && [ -n "${HE_TUNNEL_ID:-}" ] \
|
|
|| die "$SECRETS is missing HE_USER, HE_UPDATE_KEY or HE_TUNNEL_ID"
|
|
|
|
local out
|
|
out="$(curl -sS --max-time 25 \
|
|
--data-urlencode "username=$HE_USER" \
|
|
--data-urlencode "password=$HE_UPDATE_KEY" \
|
|
--data-urlencode "hostname=$HE_TUNNEL_ID" \
|
|
--data-urlencode "myip=$ip" \
|
|
"https://ipv4.tunnelbroker.net/nic/update" 2>&1)"
|
|
# dyndns protocol: "good <ip>" or "nochg <ip>" are both success.
|
|
case "$out" in
|
|
good*|nochg*) log "HE endpoint set to $ip ($out)"; return 0 ;;
|
|
*) die "HE update refused: $out" ;;
|
|
esac
|
|
}
|
|
|
|
reconcile() {
|
|
local dry="${1:-}"
|
|
local wan src want_mtu cur_src cur_mtu
|
|
wan="$(active_wan)"; [ -n "$wan" ] || die "no default route; refusing to guess"
|
|
src="$(addr_of "$wan")"; [ -n "$src" ] || die "no IPv4 address on $wan"
|
|
want_mtu="${WAN_MTU[$wan]:-}"
|
|
[ -n "$want_mtu" ] || die "unknown WAN '$wan' -- add it to WAN_MTU rather than guessing an MTU"
|
|
cur_src="$(tunnel_src)"; cur_mtu="$(tunnel_mtu)"
|
|
|
|
if [ "$cur_src" = "$src" ] && [ "$cur_mtu" = "$want_mtu" ]; then
|
|
rm -f "$STATE"
|
|
log "in sync: $TUNNEL via $wan src $src mtu $cur_mtu"
|
|
return 0
|
|
fi
|
|
|
|
# Hysteresis: count consecutive runs agreeing on the same target.
|
|
local seen=0 last=""
|
|
[ -r "$STATE" ] && { read -r last seen < "$STATE"; }
|
|
if [ "$last" = "$src" ]; then seen=$((seen + 1)); else seen=1; fi
|
|
echo "$src $seen" > "$STATE"
|
|
if [ "$seen" -lt "$HYSTERESIS" ]; then
|
|
log "change seen ($cur_src -> $src) but waiting for stability ($seen/$HYSTERESIS)"
|
|
return 0
|
|
fi
|
|
|
|
if [ "$dry" = "--dry" ]; then
|
|
log "DRY RUN: would set HE endpoint to $src, then $TUNNEL local $src mtu $want_mtu"
|
|
return 0
|
|
fi
|
|
|
|
# HE first, then local. Either order costs a brief drop, but changing locally
|
|
# first guarantees HE discards our packets for the whole window.
|
|
he_update "$src" || return 1
|
|
sudo ip tunnel change "$TUNNEL" mode sit local "$src" || die "failed to set tunnel local address"
|
|
sudo ip link set "$TUNNEL" mtu "$want_mtu" || die "failed to set tunnel MTU"
|
|
rm -f "$STATE"
|
|
log "moved $TUNNEL to $wan: src $cur_src -> $src, mtu $cur_mtu -> $want_mtu"
|
|
}
|
|
|
|
case "${1:-status}" in
|
|
status)
|
|
wan="$(active_wan)"
|
|
printf ' active WAN : %s\n' "${wan:-<none>}"
|
|
printf ' wan addr : %s\n' "$(addr_of "${wan:-lo}")"
|
|
printf ' tunnel src : %s\n' "$(tunnel_src)"
|
|
printf ' tunnel mtu : %s (want %s)\n' "$(tunnel_mtu)" "${WAN_MTU[${wan:-}]:-?}"
|
|
[ -r "$SECRETS" ] && printf ' credentials: present\n' || printf ' credentials: MISSING (%s)\n' "$SECRETS"
|
|
;;
|
|
run) reconcile "${2:-}" ;;
|
|
*) die "usage: he-tunnel-follow {status|run [--dry]}" ;;
|
|
esac
|