Files
Asger Weirsøe 3f170293f5
release / release (push) Successful in 1m19s
rotate egress over a larger WG pool, with graceful drain and live status
Keep the N always-on tunnel slots fixed but let each slot's WireGuard config
rotate through a larger pool, so a 10-concurrent provider cap (e.g. Proton) can
still cycle 50-100 profiles.

- lxc/rotate.sh + weircon-rotate.{service,timer}: round-robin one slot at a
  time through wg-pool/, repointing a symlink and restarting only that slot.
- service: proxyManager tracks per-slot in-flight + drain/undrain state; a
  localhost admin server (WEIRCON_ADMIN_LISTEN) lets rotate.sh drain a slot
  before teardown and warm it back in after, so no request is routed to a
  tunnel mid-rotation. Slots self-heal if undrain never arrives.
- GET /status: poll-friendly JSON of per-slot egress IP/state plus inferred
  next-rotation slot + ETA, fed by a background egress-IP prober.
- docs + env examples for all new knobs.
2026-06-01 10:58:51 +02:00

134 lines
5.0 KiB
Bash
Executable File

#!/bin/bash
# Roterer ÉN tunnel-slot ad gangen til den næste WG-config i puljen.
#
# - Slots (netns proxy0..proxy<SLOTS-1>) er faste: faste IP'er, faste
# SOCKS-porte. Fetch-servicen kender kun de faste endpoints.
# - Configs ligger i en pulje (wg-pool/) der kan være større end SLOTS.
# - Hver slot's aktive config er et symlink:
# wg/proxy<ID>.conf -> wg-pool/<navn>.conf
# - Hvert kald her flytter præcis én slot videre til næste ledige config
# i puljen og genstarter kun den slot. De øvrige 9 bliver oppe, så der
# altid er egress ud — og over tid cykler alle configs igennem.
#
# Hvorfor én ad gangen: holder os trygt inden for udbyderens samtidigheds-
# grænse (fx Proton: max 10 samtidige WG-sessioner) og dropper aldrig al
# egress på én gang. weircon-proxy@.service river desuden den gamle tunnel
# ned (ExecStopPost) FØR den nye rejses (ExecStartPre), så en slot er aldrig
# dobbelt-forbundet midt i en rotation.
#
# Brug:
# rotate.sh # roter én slot (cursor i state-filen rykker)
# rotate.sh init # seed alle slots med de første SLOTS distinkte configs
set -euo pipefail
POOL="/etc/weircon-random-proxy/wg-pool"
ACTIVE="/etc/weircon-random-proxy/wg"
STATE="/var/lib/weircon-random-proxy/rotate.state"
SLOTS="${SLOTS:-10}"
# Fetch-servicens admin-endpoint (lokal, ikke eksponeret via reverse-proxy).
# Tom => spring drain/undrain over (falder tilbage til ren restart).
ADMIN_URL="${WEIRCON_ADMIN_URL:-http://127.0.0.1:8081}"
# Pulje-configs, sorteret og stabilt ordnet (kun basenavne).
mapfile -t POOL_CONFIGS < <(find "$POOL" -maxdepth 1 -type f -name '*.conf' -printf '%f\n' 2>/dev/null | sort)
N="${#POOL_CONFIGS[@]}"
if (( N == 0 )); then
echo "ingen configs i $POOL — læg proton-*.conf filer der" >&2
exit 1
fi
if (( N < SLOTS )); then
echo "WARN: kun $N configs i puljen til $SLOTS slots — der vil være gengangere" >&2
fi
# Hvilke configs er allerede i brug af en ANDEN slot end $skip_slot?
configs_in_use() {
local skip="$1" i link
for (( i = 0; i < SLOTS; i++ )); do
(( i == skip )) && continue
link="$ACTIVE/proxy${i}.conf"
[[ -L "$link" ]] && basename "$(readlink -f "$link")"
done
}
# Find næste config i puljen fra position $1 som ikke er i $2..(resten).
# Sætter globalt $CHOSEN og $NEXT_POS.
pick_free_config() {
local start="$1" skip_slot="$2" k cand
local -a busy
mapfile -t busy < <(configs_in_use "$skip_slot")
for (( k = 0; k < N; k++ )); do
cand="${POOL_CONFIGS[(start + k) % N]}"
if ! printf '%s\n' "${busy[@]}" | grep -qxF "$cand"; then
CHOSEN="$cand"
NEXT_POS=$(( (start + k + 1) % N ))
return 0
fi
done
return 1
}
# Bed fetch-servicen om at stoppe ny trafik til en slot og vente til
# in-flight er drænet (servicen håndterer selv ventetid + settle). Stille
# no-op hvis admin-endpointet ikke svarer (fx UI/service slået fra).
drain_slot() {
local slot="$1"
[[ -z "$ADMIN_URL" ]] && return 0
curl -fsS -m 60 -X POST "${ADMIN_URL}/drain?id=${slot}" >/dev/null 2>&1 || \
echo "WARN: drain af slot $slot fejlede (fortsætter)" >&2
}
# Giv slot tilbage til servicen. Servicen holder den selv ude af rotation et
# par sekunder mere (warmup) så den friske WG-handshake kan nå at sætte sig.
undrain_slot() {
local slot="$1"
[[ -z "$ADMIN_URL" ]] && return 0
curl -fsS -m 10 -X POST "${ADMIN_URL}/undrain?id=${slot}" >/dev/null 2>&1 || \
echo "WARN: undrain af slot $slot fejlede (slot self-healer)" >&2
}
assign_slot() {
local slot="$1" cfg="$2"
drain_slot "$slot" # stop trafik FØR teardown
ln -sfn "$POOL/$cfg" "$ACTIVE/proxy${slot}.conf"
systemctl restart "weircon-proxy@${slot}.service" # river gammel ned, rejser ny
undrain_slot "$slot" # genoptag trafik EFTER (m. warmup)
}
mkdir -p "$(dirname "$STATE")"
# --- init: seed alle slots distinkt -------------------------------------
if [[ "${1:-}" == "init" ]]; then
pos=0
for (( s = 0; s < SLOTS; s++ )); do
if ! pick_free_config "$pos" "$s"; then
echo "kunne ikke finde ledig config til slot $s" >&2
exit 1
fi
ln -sfn "$POOL/$CHOSEN" "$ACTIVE/proxy${s}.conf"
pos="$NEXT_POS"
echo "slot $s -> $CHOSEN"
done
printf 'slot=0\npos=%s\n' "$pos" > "$STATE"
echo "seedet $SLOTS slots; start/genstart tunnelerne for at aktivere"
exit 0
fi
# --- normal: roter præcis én slot ---------------------------------------
slot=0
pos=0
# shellcheck disable=SC1090
[[ -f "$STATE" ]] && source "$STATE"
(( slot >= SLOTS )) && slot=0
if ! pick_free_config "$pos" "$slot"; then
echo "kunne ikke finde ledig config til slot $slot" >&2
exit 1
fi
assign_slot "$slot" "$CHOSEN"
echo "roterede slot $slot -> $CHOSEN ($((slot + 1))/$SLOTS denne runde)"
# Ryk cursors: næste slot, og pulje-position er allerede sat af pick_free_config.
slot=$(( (slot + 1) % SLOTS ))
printf 'slot=%s\npos=%s\n' "$slot" "$NEXT_POS" > "$STATE"