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.
This commit is contained in:
Asger Weirsøe
2026-06-01 19:17:15 +02:00
parent 9c6334cc60
commit 0aa56fc67c
13 changed files with 993 additions and 16 deletions
+25
View File
@@ -7,3 +7,28 @@ WEIRCON_PROXY_ADDRS=10.99.0.10:1080,10.99.0.11:1080,10.99.0.12:1080,10.99.0.13:1
# Maks tid pr. upstream-fetch (sekunder).
WEIRCON_REQUEST_TIMEOUT_SEC=30
# Admin-endpoint til rotation (drain/undrain). Kun localhost — må ALDRIG
# eksponeres via reverse-proxy. Tom streng slår admin-serveren helt fra.
WEIRCON_ADMIN_LISTEN=127.0.0.1:8081
# Rotation-drænings-tuning (sekunder):
# TIMEOUT = maks ventetid på at in-flight requests på en slot tømmes
# SETTLE = ekstra ro-periode efter in-flight = 0 ("et par sek. før")
# WARMUP = slot holdes ude af rotation efter undrain, så WG-handshaket
# kan sætte sig ("et par sek. efter")
# MAX_HOLD= sikkerhed: slot self-healer og kommer tilbage selv hvis
# rotate.sh dør mellem drain og undrain
WEIRCON_DRAIN_TIMEOUT_SEC=25
WEIRCON_DRAIN_SETTLE_SEC=2
WEIRCON_DRAIN_WARMUP_SEC=3
WEIRCON_DRAIN_MAX_HOLD_SEC=60
# Egress-IP prober: henter den aktuelle udgangs-IP gennem hver tunnel og cacher
# den, så /status kan vise hvilken IP der sidder på hver slot (og hvilken der
# snart roteres ud). INTERVAL=0 slår probing helt fra (så er egress_ip tom i
# /status, og klienten må selv probe). URL skal returnere klientens IP som ren
# tekst (ipify, ifconfig.me/ip, egen endpoint, …).
WEIRCON_PROBE_URL=https://api.ipify.org
WEIRCON_PROBE_INTERVAL_SEC=60
WEIRCON_PROBE_TIMEOUT_SEC=10
+6
View File
@@ -0,0 +1,6 @@
# Kopiér til /etc/weircon-random-proxy/rotate.env (valgfrit).
# Læses af weircon-rotate.service.
# Antal aktive tunnel-slots. SKAL matche det weircon-proxy@N-interval du har
# enablet (fx weircon-proxy@{0..9} => SLOTS=10). Default hvis usat: 10.
SLOTS=10
Executable
+133
View File
@@ -0,0 +1,133 @@
#!/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"
+20 -2
View File
@@ -54,18 +54,26 @@ chmod +x "$BIN"
# 4. Helper-scripts
install -Dm755 "$SCRIPT_DIR/netns-up.sh" /usr/local/sbin/weircon-netns-up
install -Dm755 "$SCRIPT_DIR/netns-down.sh" /usr/local/sbin/weircon-netns-down
install -Dm755 "$SCRIPT_DIR/rotate.sh" /usr/local/sbin/weircon-rotate
# 5. systemd units
install -Dm644 "$SCRIPT_DIR/weircon-proxies.target" /etc/systemd/system/weircon-proxies.target
install -Dm644 "$SCRIPT_DIR/weircon-proxy@.service" /etc/systemd/system/weircon-proxy@.service
install -Dm644 "$SCRIPT_DIR/weircon-fetch.service" /etc/systemd/system/weircon-fetch.service
install -Dm644 "$SCRIPT_DIR/weircon-rotate.service" /etc/systemd/system/weircon-rotate.service
install -Dm644 "$SCRIPT_DIR/weircon-rotate.timer" /etc/systemd/system/weircon-rotate.timer
# 6. Config-dir + default env
mkdir -p /etc/weircon-random-proxy/wg
chmod 700 /etc/weircon-random-proxy/wg
# wg/ = aktive slot-configs (proxy0.conf..proxyN.conf, evt. symlinks)
# wg-pool/ = valgfri pulje af ekstra configs som rotationen cykler igennem
mkdir -p /etc/weircon-random-proxy/wg /etc/weircon-random-proxy/wg-pool
chmod 700 /etc/weircon-random-proxy/wg /etc/weircon-random-proxy/wg-pool
if [[ ! -f /etc/weircon-random-proxy/fetch.env ]]; then
install -m640 "$SCRIPT_DIR/fetch.env.example" /etc/weircon-random-proxy/fetch.env
fi
if [[ ! -f /etc/weircon-random-proxy/rotate.env ]]; then
install -m640 "$SCRIPT_DIR/rotate.env.example" /etc/weircon-random-proxy/rotate.env
fi
systemctl daemon-reload
@@ -89,6 +97,16 @@ Start stakken:
systemctl enable --now weircon-proxy@{0..9}.service
systemctl enable --now weircon-fetch.service
(Valgfrit) Roter over en STØRRE pulje af configs end de 10 slots:
# læg fx 50-100 configs i puljen
cp proton-*.conf /etc/weircon-random-proxy/wg-pool/
# seed de 10 aktive slots som symlinks ind i puljen
weircon-rotate init
systemctl restart weircon-proxy@{0..9}.service
# roter automatisk én slot ad gangen (interval i weircon-rotate.timer)
systemctl enable --now weircon-rotate.timer
# juster intervallet: systemctl edit weircon-rotate.timer (default 5min)
Verificér:
systemctl status 'weircon-proxy@*'
curl http://127.0.0.1:8080/health
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=weircon-random-proxy: roter én tunnel-slot til næste WG-config i puljen
After=weircon-proxies.target
Wants=weircon-proxies.target
[Service]
Type=oneshot
EnvironmentFile=-/etc/weircon-random-proxy/rotate.env
ExecStart=/usr/local/sbin/weircon-rotate
+21
View File
@@ -0,0 +1,21 @@
[Unit]
Description=weircon-random-proxy: periodisk slot-rotation
PartOf=weircon-proxies.target
[Timer]
# Standard: roter én slot hvert 5. minut. Med 10 slots betyder det at hele
# slot-sættet er cyklet igennem ca. hvert 50. minut, og hver enkelt config i
# en pulje på N filer er brugt mindst én gang efter ca. (N * 5) minutter.
#
# Skift interval uden at redigere denne fil:
# systemctl edit weircon-rotate.timer
# og indsæt fx:
# [Timer]
# OnUnitActiveSec= # (tom linje nulstiller arvet værdi)
# OnUnitActiveSec=15min
OnBootSec=5min
OnUnitActiveSec=5min
AccuracySec=15s
[Install]
WantedBy=timers.target