#!/usr/bin/env bash # Keep the Cloudflare IP ranges in sync for BOTH proxy layers: # 1) deployment/proxy/cloudflare-ips.conf (nginx: geo + set_real_ip_from) # 2) entryPoints.websecure.forwardedHeaders.trustedIPs in /docker/proxyserver/traefik.yml # # The repo file is the source of truth for nginx (installed by nginx-sync.sh); # Traefik's static config lives outside the repo and is regenerated in place. # Traefik only picks it up after a container restart (static config), which # --install performs automatically when the list actually changed. # # Usage: # scripts/cf-ips-sync.sh # regen repo + traefik files if ranges changed # scripts/cf-ips-sync.sh --check # report what would change (exit 1 if any) # sudo scripts/cf-ips-sync.sh --install # + run nginx-sync.sh and restart Traefik # # The Traefik bridge subnet is auto-detected (env TRUSTED_SUBNET overrides), # because nginx trusts it as a TLS-terminating peer. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROXY_DIR="$SCRIPT_DIR/../deployment/proxy" TARGET="$PROXY_DIR/cloudflare-ips.conf" TRAEFIK_CONFIG="${TRAEFIK_CONFIG:-/docker/proxyserver/traefik.yml}" IPV4_URL="https://www.cloudflare.com/ips-v4" IPV6_URL="https://www.cloudflare.com/ips-v6" MODE="status" for arg in "$@"; do case "$arg" in --check) MODE="check" ;; --install) MODE="install" ;; --no-traefik) TRAEFIK_CONFIG="" ;; esac done TRUSTED_SUBNET="${TRUSTED_SUBNET:-}" if [[ -z "$TRUSTED_SUBNET" ]]; then TRUSTED_SUBNET="$(docker network inspect proxyserver_traefik-proxy --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' 2>/dev/null || true)" fi if [[ -z "$TRUSTED_SUBNET" ]]; then TRUSTED_SUBNET="172.22.0.0/16" echo "warning: could not auto-detect Traefik bridge subnet, using $TRUSTED_SUBNET" >&2 fi ipv4="$(mktemp)" ipv6="$(mktemp)" trap 'rm -f "$ipv4" "$ipv6"' EXIT if ! curl -sf "$IPV4_URL" -o "$ipv4" || ! curl -sf "$IPV6_URL" -o "$ipv6"; then echo "error: could not fetch Cloudflare ranges" >&2 if [[ -f "$TARGET" ]]; then echo "keeping existing $TARGET (ranges not refreshed)" >&2 exit 0 fi exit 1 fi gen_nginx_conf() { { printf '%s\n' "# Trusted edge networks + live Cloudflare CDN ranges." printf '%s\n' "# Managed/regenerated by scripts/cf-ips-sync.sh - do not hand-edit the ranges." printf '%s\n' "" printf '%s\n' "# Topology: Cloudflare -> Traefik (:443, docker bridge proxy_traefik-proxy) ->" printf '%s\n' "# nginx (:9443) -> CMS. nginx ALSO receives direct connections on :9443 from" printf '%s\n' "# Cloudflare edges and from the game client (ws.epicnabbo.nl is not proxied)." printf '%s\n' "" printf '%s\n' "# nginx only trusts the peers listed here as a source of \$remote_addr" printf '%s\n' "# (via CF-Connecting-IP). Anyone else presenting a CF-Connecting-IP or" printf '%s\n' "# CF-ray header is spoofing and is rejected in nginx-cms.conf." printf '%s\n' "" printf '%s\n' "# 1 = peer is a trusted edge or internal network (keyed on the raw peer," printf '%s\n' "# unaffected by real_ip rewrites)." printf '%s\n' "geo \$realip_remote_addr \$cms_trusted_edge {" printf '%s\n' " default 0;" printf '%s\n' " 127.0.0.0/8 1; # localhost (health checks, admin)" printf '%s\n' " ::1 1; # localhost v6" printf '%s\n' " $TRUSTED_SUBNET 1; # Traefik (proxyserver_traefik-proxy)" printf '%s\n' " # --- Cloudflare IPv4 ranges (live from cloudflare.com/ips-v4) ---" awk '{print " "$0" 1;"}' "$ipv4" printf '%s\n' " # --- Cloudflare IPv6 ranges (live from cloudflare.com/ips-v6) ---" awk '{print " "$0" 1;"}' "$ipv6" printf '%s\n' "}" printf '%s\n' "" printf '%s\n' "# 1 when an UNTRUSTED peer still presents a CF-Connecting-IP header: that is a" printf '%s\n' "# spoof attempt (only real Cloudflare edges or Traefik may do that lawfully)." printf '%s\n' "map \"\$cms_trusted_edge:\$http_cf_connecting_ip\" \$cms_disallow_forwarding {" printf '%s\n' " default 0;" printf '%s\n' " \"~^0:.+\" 1;" printf '%s\n' "}" printf '%s\n' "" printf '%s\n' "# Rewrite \$remote_addr from CF-Connecting-IP but ONLY for the trusted peers" printf '%s\n' "# above. Direct game clients (untrusted) keep their real peer address." printf '%s\n' "set_real_ip_from 127.0.0.0/8;" printf '%s\n' "set_real_ip_from ::1;" printf '%s\n' "set_real_ip_from $TRUSTED_SUBNET;" awk '{print "set_real_ip_from "$0";"}' "$ipv4" awk '{print "set_real_ip_from "$0";"}' "$ipv6" printf '%s\n' "" printf '%s\n' "real_ip_header CF-Connecting-IP;" printf '%s\n' "real_ip_recursive off;" } > "$TARGET.tmp" } gen_nginx_conf changed=0 if cmp -s "$TARGET" "$TARGET.tmp"; then rm -f "$TARGET.tmp" echo "= $TARGET up to date (Cloudflare ranges unchanged)" else changed=1 if [[ "$MODE" == "check" ]]; then rm -f "$TARGET.tmp" echo "- Cloudflare ranges DIFFER; $TARGET would be regenerated" else mv "$TARGET.tmp" "$TARGET" echo "+ regenerated $TARGET" fi fi traefik_changed=0 if [[ -n "$TRAEFIK_CONFIG" ]]; then if [[ ! -f "$TRAEFIK_CONFIG" ]]; then echo "warning: $TRAEFIK_CONFIG not found, skipping Traefik sync" >&2 else new_traefik="$(CF_V4="$ipv4" CF_V6="$ipv6" python3 - "$TRAEFIK_CONFIG" <<'PY' import os, sys path = sys.argv[1] v4 = sorted(x.rstrip("\n") for x in open(os.environ["CF_V4"]) if x.strip()) v6 = sorted(x.rstrip("\n") for x in open(os.environ["CF_V6"]) if x.strip()) lines = open(path).read().split("\n") out, i, n = [], 0, len(lines) while i < n: line = lines[i] if line.startswith(" trustedIPs:"): out.append(line) i += 1 while i < n: s = lines[i] if not s.strip() or s.startswith(" - ") or s.startswith(" #"): i += 1 else: break out.append(" # Cloudflare IPv4 reeksen (gesynct door cf-ips-sync.sh)") out += [" - " + c for c in v4] out.append(" # Cloudflare IPv6 reeksen") out += [" - " + c for c in v6] continue out.append(line) i += 1 sys.stdout.write("\n".join(out)) PY )" if grep -q 'trustedIPs:' <<< "$new_traefik" \ && grep -cq '^ - ' <<< "$new_traefik"; then if [[ "$new_traefik" == "$(cat "$TRAEFIK_CONFIG")" ]]; then echo "= $TRAEFIK_CONFIG up to date (Cloudflare ranges unchanged)" else traefik_changed=1 if [[ "$MODE" == "check" ]]; then echo "- $TRAEFIK_CONFIG differs from live Cloudflare ranges" else cp -a "$TRAEFIK_CONFIG" "$TRAEFIK_CONFIG.bak-$(date +%Y%m%d-%H%M%S)" printf '%s\n' "$new_traefik" > "$TRAEFIK_CONFIG" echo "+ updated $TRAEFIK_CONFIG" fi fi else echo "error: generated Traefik config is missing its trustedIPs block; NOT writing" >&2 exit 1 fi fi fi [[ "$MODE" == "check" ]] && exit $((changed || traefik_changed)) if [[ "$MODE" == "install" ]]; then "$SCRIPT_DIR/nginx-sync.sh" if [[ "$traefik_changed" -eq 1 ]]; then if docker inspect traefik >/dev/null 2>&1; then echo "--- restarting traefik (static config changed) ---" docker restart traefik else echo "warning: traefik container not found; restart it manually" >&2 fi fi fi