Remove WG_CONFIG_MAX_AGE environment variable and update README to reflect changes in WireGuard configuration handling. Refactor rotation logic in rotate.py to always generate a new keypair and simplify state management.
Build and Push Docker Images / build-and-push (push) Successful in 20s

This commit is contained in:
2026-08-14 21:19:35 +02:00
parent bc9d79de02
commit 6952789722
3 changed files with 5 additions and 95 deletions
@@ -23,7 +23,6 @@ ENV SERVERLIST_CACHE_MAX_AGE=168h
ENV TOKEN_CACHE_PATH=/config/cache/pia-token.json ENV TOKEN_CACHE_PATH=/config/cache/pia-token.json
ENV TOKEN_CACHE_TTL=20h ENV TOKEN_CACHE_TTL=20h
ENV PIA_CA_PATH=/config/cache/ca.rsa.4096.crt ENV PIA_CA_PATH=/config/cache/ca.rsa.4096.crt
ENV WG_CONFIG_MAX_AGE=7d
ENV LATENCY_SWITCH_MARGIN_MS=15 ENV LATENCY_SWITCH_MARGIN_MS=15
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -49,8 +49,6 @@ docker run --rm --entrypoint /opt/venv/bin/python \
| `TOKEN_CACHE_PATH` | `/config/cache/pia-token.json` | Disk-cache voor PIA auth-token | | `TOKEN_CACHE_PATH` | `/config/cache/pia-token.json` | Disk-cache voor PIA auth-token |
| `TOKEN_CACHE_TTL` | `20h` | Hergebruik token i.p.v. opnieuw inloggen | | `TOKEN_CACHE_TTL` | `20h` | Hergebruik token i.p.v. opnieuw inloggen |
| `PIA_CA_PATH` | `/config/cache/ca.rsa.4096.crt` | Gecachete PIA CA voor `addKey` TLS | | `PIA_CA_PATH` | `/config/cache/ca.rsa.4096.crt` | Gecachete PIA CA voor `addKey` TLS |
| `WG_CONFIG_MAX_AGE` | `7d` | Geen nieuwe token/`addKey` zolang endpoint gelijk blijft en config jonger is |
| `FORCE_ROTATE` | `false` | `true` = altijd nieuwe config + restarts |
| `FORCE_TOKEN_REFRESH` | `false` | `true` = token-cache negeren | | `FORCE_TOKEN_REFRESH` | `false` | `true` = token-cache negeren |
| `RATE_LIMIT_WAIT_SECONDS` | `3600` | Wachttijd bij PIA rate-limit vóór retry | | `RATE_LIMIT_WAIT_SECONDS` | `3600` | Wachttijd bij PIA rate-limit vóór retry |
| `TZ` | `Europe/Brussels` | Tijdzone voor scheduling | | `TZ` | `Europe/Brussels` | Tijdzone voor scheduling |
@@ -113,8 +111,8 @@ Behoud minimaal:
1. Latency meten over WG-servers in `PIA_REGIONS` (of random) 1. Latency meten over WG-servers in `PIA_REGIONS` (of random)
2. **Pin** de snelste server-IP (niet alleen regio) 2. **Pin** de snelste server-IP (niet alleen regio)
3. Keypair lokaal genereren; token + `addKey` alleen bij echte refresh 3. Altijd nieuw keypair + `addKey` (reauth), daarna containers herstarten
4. Caches: serverlist, token (~20u), CA-cert 4. Caches: serverlist, token (~20u), CA-cert — alleen om overbodige API-calls te beperken
5. Skip token/`addKey`/restarts als endpoint gelijk blijft én config jonger dan `WG_CONFIG_MAX_AGE` 5. Bij rate-limit: `RATE_LIMIT_WAIT_SECONDS` (default 1 uur) wachten en opnieuw proberen
**Let op:** zet gluetun **niet** in `RESTART_CONTAINERS`; gebruik `GLUETUN_CONTAINER`. Elke echte rotatie geeft korte downtime. **Let op:** zet gluetun **niet** in `RESTART_CONTAINERS`; gebruik `GLUETUN_CONTAINER`. Elke echte rotatie geeft korte downtime.
@@ -236,83 +236,13 @@ def restart_containers(containers: list[str]) -> None:
raise SystemExit(f"docker restart failed for container={container}") raise SystemExit(f"docker restart failed for container={container}")
def config_age_seconds(wg_path: Path) -> float | None:
if not wg_path.is_file():
return None
try:
return time.time() - wg_path.stat().st_mtime
except OSError:
return None
def should_skip_regeneration(
server: pia.WgServer,
state: dict[str, Any],
wg_path: Path,
latency_results: list[dict[str, Any]],
) -> bool:
if pia.env_bool("FORCE_ROTATE", False):
log("FORCE_ROTATE=true; regenerating WireGuard config")
return False
previous_region = state.get("region")
previous_server_ip = state.get("server_ip")
if previous_region != server.region:
return False
age = config_age_seconds(wg_path)
if age is None:
return False
max_age = pia.parse_duration_seconds(os.environ.get("WG_CONFIG_MAX_AGE", "7d"), 604800)
if age > max_age:
log(f"Existing WireGuard config is stale (age {int(age)}s > max {max_age}s); regenerating")
return False
if not re.search(
r"^\[Interface\]",
wg_path.read_text(encoding="utf-8", errors="replace"),
flags=re.MULTILINE,
):
return False
# Same region, different server: only regenerate if clearly faster.
if previous_server_ip and server.ip != previous_server_ip:
margin = float(os.environ.get("LATENCY_SWITCH_MARGIN_MS", "15"))
previous_latency = state.get("latency_ms")
winner = next((item for item in latency_results if item.get("server_ip") == server.ip), None)
new_latency = winner.get("latency_ms") if winner else None
if (
isinstance(previous_latency, (int, float))
and isinstance(new_latency, (int, float))
and (previous_latency - new_latency) < margin
):
log(
f"Keeping current server {previous_server_ip}; "
f"{server.ip} only {previous_latency - new_latency:.2f}ms faster "
f"(margin {margin:g}ms)"
)
return True
log(f"Switching server within {server.region}: {previous_server_ip} -> {server.ip}")
return False
log(
f"Skipping PIA token/addKey; endpoint unchanged "
f"({server.region}/{server.ip}) and config age {int(age)}s <= {max_age}s"
)
return True
def write_state( def write_state(
state_path: Path, state_path: Path,
server: pia.WgServer, server: pia.WgServer,
mode: str, mode: str,
restarted: list[str], restarted: list[str],
latency_results: list[dict[str, Any]], latency_results: list[dict[str, Any]],
*,
skipped: bool = False,
) -> None: ) -> None:
previous = read_state(state_path)
winner = next( winner = next(
( (
item item
@@ -329,17 +259,11 @@ def write_state(
"selection": mode, "selection": mode,
"wg_config": os.environ.get("WG_CONFIG_PATH", "/config/wireguard/wg0.conf"), "wg_config": os.environ.get("WG_CONFIG_PATH", "/config/wireguard/wg0.conf"),
"restarted_containers": restarted, "restarted_containers": restarted,
"rotated_at": now,
"last_checked_at": now, "last_checked_at": now,
"skipped_regeneration": skipped,
} }
if skipped and isinstance(previous.get("rotated_at"), str):
payload["rotated_at"] = previous["rotated_at"]
else:
payload["rotated_at"] = now
if winner and winner.get("latency_ms") is not None: if winner and winner.get("latency_ms") is not None:
payload["latency_ms"] = winner["latency_ms"] payload["latency_ms"] = winner["latency_ms"]
elif skipped and previous.get("latency_ms") is not None:
payload["latency_ms"] = previous["latency_ms"]
if latency_results: if latency_results:
payload["latency_results"] = latency_results payload["latency_results"] = latency_results
@@ -355,21 +279,10 @@ def rotate_once() -> None:
state_path = Path(os.environ.get("ROTATOR_STATE_PATH", "/config/rotator-state.json")) state_path = Path(os.environ.get("ROTATOR_STATE_PATH", "/config/rotator-state.json"))
wg_path = Path(os.environ.get("WG_CONFIG_PATH", "/config/wireguard/wg0.conf")) wg_path = Path(os.environ.get("WG_CONFIG_PATH", "/config/wireguard/wg0.conf"))
state = read_state(state_path)
server, mode, latency_results = pick_server(state_path) server, mode, latency_results = pick_server(state_path)
log(f"Selected endpoint: {server.region} / {server.cn} / {server.ip} (mode={mode})") log(f"Selected endpoint: {server.region} / {server.cn} / {server.ip} (mode={mode})")
if should_skip_regeneration(server, state, wg_path, latency_results):
# If we decided to keep the previous server IP, persist that identity.
keep_ip = state.get("server_ip") if isinstance(state.get("server_ip"), str) else server.ip
keep_cn = state.get("server_cn") if isinstance(state.get("server_cn"), str) else server.cn
if keep_ip != server.ip:
server = pia.WgServer(region=server.region, ip=keep_ip, cn=str(keep_cn or server.cn))
write_state(state_path, server, mode, [], latency_results, skipped=True)
log(f"Rotation skipped for {server.region}/{server.ip}")
return
with tempfile.TemporaryDirectory(prefix="pia-rotate-") as tmp: with tempfile.TemporaryDirectory(prefix="pia-rotate-") as tmp:
tmp_conf = Path(tmp) / "wg0.conf" tmp_conf = Path(tmp) / "wg0.conf"
log("Generating WireGuard config via native PIA client") log("Generating WireGuard config via native PIA client")
@@ -391,7 +304,7 @@ def rotate_once() -> None:
restarted = parse_restart_containers() restarted = parse_restart_containers()
restart_containers(restarted) restart_containers(restarted)
write_state(state_path, server, mode, restarted, latency_results, skipped=False) write_state(state_path, server, mode, restarted, latency_results)
log(f"Rotation complete for {server.region}/{server.ip}") log(f"Rotation complete for {server.region}/{server.ip}")