This commit is contained in:
@@ -7,17 +7,22 @@ RUN apk add --no-cache git ca-certificates \
|
|||||||
|
|
||||||
FROM docker:cli
|
FROM docker:cli
|
||||||
|
|
||||||
RUN apk add --no-cache bash ca-certificates tzdata jq
|
RUN apk add --no-cache bash ca-certificates tzdata jq python3 py3-pip \
|
||||||
|
&& python3 -m venv /opt/venv \
|
||||||
|
&& /opt/venv/bin/pip install --no-cache-dir 'croniter==6.2.4' \
|
||||||
|
&& apk del py3-pip
|
||||||
|
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
COPY --from=builder /go/bin/pia-wg-config /usr/local/bin/pia-wg-config
|
COPY --from=builder /go/bin/pia-wg-config /usr/local/bin/pia-wg-config
|
||||||
COPY rotate.sh entrypoint.sh /usr/local/bin/
|
COPY rotate.sh entrypoint.py /usr/local/bin/
|
||||||
|
|
||||||
RUN chmod +x /usr/local/bin/rotate.sh /usr/local/bin/entrypoint.sh
|
RUN chmod +x /usr/local/bin/rotate.sh /usr/local/bin/entrypoint.py
|
||||||
|
|
||||||
ENV TZ=Europe/Brussels
|
ENV TZ=Europe/Brussels
|
||||||
ENV WG_CONFIG_PATH=/config/wireguard/wg0.conf
|
ENV WG_CONFIG_PATH=/config/wireguard/wg0.conf
|
||||||
ENV ROTATOR_STATE_PATH=/config/rotator-state.json
|
ENV ROTATOR_STATE_PATH=/config/rotator-state.json
|
||||||
ENV GLUETUN_CONTAINER=m3u-filter-vpn
|
ENV GLUETUN_CONTAINER=m3u-filter-vpn
|
||||||
ENV ROTATE_AT=03:00
|
ENV ROTATE_CRON="0 3 * * *"
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
ENTRYPOINT ["/opt/venv/bin/python", "/usr/local/bin/entrypoint.py"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# gluetun-pia-wireguard-rotator
|
# gluetun-pia-wireguard-rotator
|
||||||
|
|
||||||
Sidecar die dagelijks (en bij container-start) een willekeurige PIA WireGuard-region kiest via [pia-wg-config](https://github.com/ccarpinteri/pia-wg-config), `wg0.conf` op het gedeelde gluetun-volume schrijft, en een configureerbare lijst containers herstart.
|
Sidecar die op een cron-schema (en bij container-start) een willekeurige PIA WireGuard-region kiest via [pia-wg-config](https://github.com/ccarpinteri/pia-wg-config), `wg0.conf` op het gedeelde gluetun-volume schrijft, en een configureerbare lijst containers herstart.
|
||||||
|
|
||||||
## Vereisten
|
## Vereisten
|
||||||
|
|
||||||
@@ -35,10 +35,12 @@ docker run --rm --entrypoint pia-wg-config bramkel/gluetun-pia-wireguard-rotator
|
|||||||
| `GLUETUN_CONTAINER` | `m3u-filter-vpn` | Gluetun-container; wordt **altijd als eerste** herstart |
|
| `GLUETUN_CONTAINER` | `m3u-filter-vpn` | Gluetun-container; wordt **altijd als eerste** herstart |
|
||||||
| `WG_CONFIG_PATH` | `/config/wireguard/wg0.conf` | Pad waar `wg0.conf` wordt geschreven |
|
| `WG_CONFIG_PATH` | `/config/wireguard/wg0.conf` | Pad waar `wg0.conf` wordt geschreven |
|
||||||
| `ROTATOR_STATE_PATH` | `/config/rotator-state.json` | Laatste rotatie-metadata |
|
| `ROTATOR_STATE_PATH` | `/config/rotator-state.json` | Laatste rotatie-metadata |
|
||||||
| `ROTATE_AT` | `03:00` | Dagelijks rotatietijdstip (`HH:MM`, in `TZ`) |
|
| `ROTATE_CRON` | `0 3 * * *` | 5-veld cron-expressie (minuut uur dag-van-maand maand dag-van-week), in `TZ`. Macros: `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly` |
|
||||||
| `RATE_LIMIT_WAIT_SECONDS` | `3600` | Wachttijd bij PIA rate-limit (`429` / `too_many_attempts`) vóór retry |
|
| `RATE_LIMIT_WAIT_SECONDS` | `3600` | Wachttijd bij PIA rate-limit (`429` / `too_many_attempts`) vóór retry |
|
||||||
| `TZ` | `Europe/Brussels` | Tijdzone voor scheduling |
|
| `TZ` | `Europe/Brussels` | Tijdzone voor scheduling |
|
||||||
|
|
||||||
|
`ROTATE_CRON` voorbeelden: `0 */6 * * *` (elke 6 uur), `0 3 * * 1-5` (weekdagen 03:00), `@hourly`. Quote de waarde in Compose (`"0 3 * * *"`) zodat YAML `*` niet speciaal interpreteert. Oude `ROTATE_AT=HH:MM` werkt nog als `ROTATE_CRON` leeg is.
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|
||||||
- `wireguard/wg0.conf` op het gedeelde volume — Gluetun leest dit als `/gluetun/wireguard/wg0.conf` en dit **overschrijft** `WIREGUARD_*` environment variables
|
- `wireguard/wg0.conf` op het gedeelde volume — Gluetun leest dit als `/gluetun/wireguard/wg0.conf` en dit **overschrijft** `WIREGUARD_*` environment variables
|
||||||
@@ -62,7 +64,7 @@ Zie [`docker-compose.example.yml`](docker-compose.example.yml) voor een volledig
|
|||||||
- PIA_REGIONS=netherlands,france,belgium
|
- PIA_REGIONS=netherlands,france,belgium
|
||||||
- GLUETUN_CONTAINER=downloaders-vpn
|
- GLUETUN_CONTAINER=downloaders-vpn
|
||||||
- RESTART_CONTAINERS=SabNZBd,qbittorrent,nzbhydra2,Spotweb
|
- RESTART_CONTAINERS=SabNZBd,qbittorrent,nzbhydra2,Spotweb
|
||||||
- ROTATE_AT=03:00
|
- 'ROTATE_CRON=0 3 * * *'
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- /var/dockers/m3u-filter-pia:/config
|
- /var/dockers/m3u-filter-pia:/config
|
||||||
@@ -100,7 +102,7 @@ Optioneel host-`.env`-keys (`WIREGUARD_*`) opruimen als die niet meer gebruikt w
|
|||||||
## Gedrag
|
## Gedrag
|
||||||
|
|
||||||
1. Bij start: direct roteren (nieuwe config + container-restarts)
|
1. Bij start: direct roteren (nieuwe config + container-restarts)
|
||||||
2. Daarna: elke dag om `ROTATE_AT` opnieuw
|
2. Daarna: volgens `ROTATE_CRON` (standaard dagelijks om 03:00)
|
||||||
3. Willekeurige region uit `PIA_REGIONS`, nooit dezelfde als de vorige (uit `rotator-state.json`; uitzondering: maar één region geconfigureerd)
|
3. Willekeurige region uit `PIA_REGIONS`, nooit dezelfde als de vorige (uit `rotator-state.json`; uitzondering: maar één region geconfigureerd)
|
||||||
4. `GLUETUN_CONTAINER` wordt altijd als eerste herstart, daarna containers uit `RESTART_CONTAINERS`
|
4. `GLUETUN_CONTAINER` wordt altijd als eerste herstart, daarna containers uit `RESTART_CONTAINERS`
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Schedule PIA WireGuard rotations via ROTATE_CRON (croniter)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
from croniter import croniter
|
||||||
|
|
||||||
|
# Matches rotate.sh EXIT_RATE_LIMITED (EX_TEMPFAIL)
|
||||||
|
EXIT_RATE_LIMITED = 75
|
||||||
|
|
||||||
|
CRON_MACROS = {
|
||||||
|
"@yearly": "0 0 1 1 *",
|
||||||
|
"@annually": "0 0 1 1 *",
|
||||||
|
"@monthly": "0 0 1 * *",
|
||||||
|
"@weekly": "0 0 * * 0",
|
||||||
|
"@daily": "0 0 * * *",
|
||||||
|
"@midnight": "0 0 * * *",
|
||||||
|
"@hourly": "0 * * * *",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
print(f"[{datetime.now().astimezone().isoformat(timespec='seconds')}] {msg}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def zone() -> ZoneInfo:
|
||||||
|
name = os.environ.get("TZ") or "UTC"
|
||||||
|
try:
|
||||||
|
return ZoneInfo(name)
|
||||||
|
except Exception as exc:
|
||||||
|
raise SystemExit(f"Invalid TZ '{name}': {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def hhmm_to_cron(value: str) -> str:
|
||||||
|
match = re.fullmatch(r"([0-9]{1,2}):([0-9]{2})", value.strip())
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Invalid ROTATE_AT '{value}' (expected HH:MM)")
|
||||||
|
hour = int(match.group(1))
|
||||||
|
minute = int(match.group(2))
|
||||||
|
if not (0 <= hour <= 23 and 0 <= minute <= 59):
|
||||||
|
raise ValueError(f"Invalid ROTATE_AT '{value}' (expected HH:MM)")
|
||||||
|
return f"{minute} {hour} * * *"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_cron_expr() -> str:
|
||||||
|
cron = os.environ.get("ROTATE_CRON", "").strip()
|
||||||
|
at = os.environ.get("ROTATE_AT", "").strip()
|
||||||
|
if cron:
|
||||||
|
expr = cron
|
||||||
|
elif at:
|
||||||
|
expr = hhmm_to_cron(at)
|
||||||
|
else:
|
||||||
|
expr = "0 3 * * *"
|
||||||
|
|
||||||
|
expr = CRON_MACROS.get(expr.lower(), expr)
|
||||||
|
if not croniter.is_valid(expr):
|
||||||
|
raise SystemExit(f"Invalid ROTATE_CRON '{expr}' (expected a 5-field cron expression)")
|
||||||
|
return expr
|
||||||
|
|
||||||
|
|
||||||
|
def next_run(expr: str, after: datetime) -> datetime:
|
||||||
|
return croniter(expr, after).get_next(datetime)
|
||||||
|
|
||||||
|
|
||||||
|
def sleep_until_next_rotate(expr: str) -> None:
|
||||||
|
tz = zone()
|
||||||
|
now = datetime.now(tz)
|
||||||
|
nxt = next_run(expr, now)
|
||||||
|
wait_s = max(0.0, (nxt - now).total_seconds())
|
||||||
|
log(f"Next rotation at {nxt.isoformat(timespec='seconds')} (cron '{expr}', TZ={tz.key}) in {int(wait_s)}s")
|
||||||
|
time.sleep(wait_s)
|
||||||
|
|
||||||
|
|
||||||
|
def run_rotation(reason: str) -> None:
|
||||||
|
wait_s = int(os.environ.get("RATE_LIMIT_WAIT_SECONDS", "3600"))
|
||||||
|
while True:
|
||||||
|
log(reason)
|
||||||
|
result = subprocess.run(["/usr/local/bin/rotate.sh"], check=False)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return
|
||||||
|
if result.returncode == EXIT_RATE_LIMITED:
|
||||||
|
log(f"PIA rate-limited (too many attempts); waiting {wait_s}s before retry")
|
||||||
|
time.sleep(wait_s)
|
||||||
|
reason = "Retrying rotation after rate-limit wait"
|
||||||
|
continue
|
||||||
|
raise SystemExit(f"Rotation failed with exit code {result.returncode}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
expr = resolve_cron_expr()
|
||||||
|
# Fail fast on bad cron / TZ before rotating.
|
||||||
|
next_run(expr, datetime.now(zone()))
|
||||||
|
log(f"Starting gluetun PIA WireGuard rotator (TZ={zone().key}, ROTATE_CRON='{expr}')")
|
||||||
|
|
||||||
|
run_rotation("Running rotation on startup")
|
||||||
|
while True:
|
||||||
|
sleep_until_next_rotate(expr)
|
||||||
|
run_rotation("Running scheduled rotation")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() { echo "[$(date -Is)] $*" >&2; }
|
|
||||||
|
|
||||||
sleep_until_next_rotate() {
|
|
||||||
local rotate_at="${ROTATE_AT:-03:00}"
|
|
||||||
local target_h target_m
|
|
||||||
IFS=: read -r target_h target_m <<<"$rotate_at"
|
|
||||||
target_h=$((10#$target_h))
|
|
||||||
target_m=$((10#$target_m))
|
|
||||||
|
|
||||||
local now_h now_m now_s target_s wait_s
|
|
||||||
now_h=$(date +%H)
|
|
||||||
now_m=$(date +%M)
|
|
||||||
now_h=$((10#$now_h))
|
|
||||||
now_m=$((10#$now_m))
|
|
||||||
now_s=$((now_h * 3600 + now_m * 60))
|
|
||||||
target_s=$((target_h * 3600 + target_m * 60))
|
|
||||||
|
|
||||||
if ((now_s < target_s)); then
|
|
||||||
wait_s=$((target_s - now_s))
|
|
||||||
else
|
|
||||||
wait_s=$((86400 - now_s + target_s))
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Next rotation at ${rotate_at} (${TZ:-UTC}) in ${wait_s}s"
|
|
||||||
sleep "$wait_s"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Matches rotate.sh EXIT_RATE_LIMITED (EX_TEMPFAIL)
|
|
||||||
readonly EXIT_RATE_LIMITED=75
|
|
||||||
|
|
||||||
run_rotation() {
|
|
||||||
local reason="$1"
|
|
||||||
local wait_s="${RATE_LIMIT_WAIT_SECONDS:-3600}"
|
|
||||||
local rc
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
log "$reason"
|
|
||||||
rc=0
|
|
||||||
/usr/local/bin/rotate.sh || rc=$?
|
|
||||||
case "$rc" in
|
|
||||||
0)
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
"$EXIT_RATE_LIMITED")
|
|
||||||
log "PIA rate-limited (too many attempts); waiting ${wait_s}s before retry"
|
|
||||||
sleep "$wait_s"
|
|
||||||
reason="Retrying rotation after rate-limit wait"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
>&2 echo "Rotation failed with exit code $rc"
|
|
||||||
return "$rc"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
log "Starting gluetun PIA WireGuard rotator (TZ=${TZ:-UTC}, ROTATE_AT=${ROTATE_AT:-03:00})"
|
|
||||||
|
|
||||||
run_rotation "Running rotation on startup"
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
sleep_until_next_rotate
|
|
||||||
run_rotation "Running scheduled daily rotation"
|
|
||||||
done
|
|
||||||
Reference in New Issue
Block a user