commit d92e3316ba459ff523be0b29cd204591cc0849ba
parent 4d790339f8335ff76c955ac36dfba254ec7fd1ee
Author: claude-bot <noreply@anthropic.com>
Date: Tue, 6 Oct 2026 03:16:44 +0000
Detect visitor IP via CF-Connecting-IP when the request arrived from Cloudflare
Behind Render the chain is 'client, Cloudflare edge, Render internal'. The edge address changes
per request, so bans never matched. Now CF-Connecting-IP is used only when the connecting peer
(appended by the platform, unforgeable) is in Cloudflare's published ranges. Refuse to ban
Cloudflare or internal addresses; the dashboard flags a detected IP that looks wrong.
Diffstat:
2 files changed, 33 insertions(+), 5 deletions(-)
diff --git a/app/admin.py b/app/admin.py
@@ -232,11 +232,12 @@ async def admin_page():
async def admin_state(request: Request):
_require_admin(request)
await ensure_loaded()
- from .upload_guard import get_client_ip, TRUST_CF_CONNECTING_IP, TRUSTED_PROXY_HOPS
+ from .upload_guard import get_client_ip, is_cloudflare_ip, is_public_ip, TRUST_CF_CONNECTING_IP, TRUSTED_PROXY_HOPS
h = request.headers
+ me = get_client_ip(request)
return {
"whoami": {
- "detected": get_client_ip(request),
+ "detected": me, "ok": is_public_ip(me) and not is_cloudflare_ip(me),
"x_forwarded_for": h.get("x-forwarded-for"), "cf_connecting_ip": h.get("cf-connecting-ip"),
"true_client_ip": h.get("true-client-ip"), "socket_peer": request.client.host if request.client else None,
"trust_cf_header": TRUST_CF_CONNECTING_IP, "proxy_hops": TRUSTED_PROXY_HOPS,
@@ -263,10 +264,11 @@ async def admin_ban(request: Request, body: IpBody):
ip = str(ipaddress.ip_address(body.ip.strip()))
except ValueError: # also stops "unknown" (undetectable IP) from banning everyone
raise HTTPException(status_code=400, detail="Not a valid IP address.")
- if not ipaddress.ip_address(ip).is_global:
+ from .upload_guard import is_cloudflare_ip
+ if not ipaddress.ip_address(ip).is_global or is_cloudflare_ip(ip):
raise HTTPException(
status_code=400,
- detail="That is an internal address, not a real visitor. The server is not seeing visitor IPs correctly - see 'How the server sees you' at the top of the dashboard.",
+ detail="That is an internal or Cloudflare address, not a real visitor (banning it could block many people). The server is not seeing visitor IPs correctly - see 'How the server sees you' at the top of the dashboard.",
)
_state["bans"][ip] = {"t": int(time.time()), "note": body.note[:100]}
return await _mutate_and_save()
@@ -362,7 +364,7 @@ function render(s){
if(s.error)h+=`<div class="banner">${esc(s.error)}</div>`;
const w=s.whoami;
h+=`<h2>How the server sees you</h2><div class="wrap"><table>
- <tr><td>Detected IP (used for bans)</td><td class="mono ${w.detected&&w.detected!=='unknown'?'ok':'bad'}">${esc(w.detected)}</td></tr>
+ <tr><td>Detected IP (used for bans)</td><td class="mono ${w.ok?'ok':'bad'}">${esc(w.detected)}</td></tr>
<tr><td>X-Forwarded-For</td><td class="mono">${esc(w.x_forwarded_for||'-')}</td></tr>
<tr><td>CF-Connecting-IP</td><td class="mono">${esc(w.cf_connecting_ip||'-')}</td></tr>
<tr><td>Socket peer</td><td class="mono">${esc(w.socket_peer||'-')}</td></tr>
diff --git a/app/upload_guard.py b/app/upload_guard.py
@@ -50,6 +50,25 @@ def _valid_ip(value: str | None) -> str | None:
return None
+# Cloudflare's published edge ranges (https://www.cloudflare.com/ips/). A request that
+# really arrived through Cloudflare carries the visitor's address in CF-Connecting-IP.
+_CLOUDFLARE_NETS = [ipaddress.ip_network(n) for n in (
+ "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13", "104.24.0.0/14",
+ "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18", "162.158.0.0/15", "172.64.0.0/13",
+ "173.245.48.0/20", "188.114.96.0/20", "190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
+ "2400:cb00::/32", "2606:4700::/32", "2803:f800::/32", "2405:b500::/32", "2405:8100::/32",
+ "2a06:98c0::/29", "2c0f:f248::/32",
+)]
+
+
+def is_cloudflare_ip(value: str) -> bool:
+ try:
+ ip = ipaddress.ip_address(value)
+ except ValueError:
+ return False
+ return any(ip.version == n.version and ip in n for n in _CLOUDFLARE_NETS)
+
+
def is_public_ip(value: str) -> bool:
try:
return ipaddress.ip_address(value).is_global
@@ -78,6 +97,13 @@ def get_client_ip(request: Request) -> str:
while public_parts and not is_public_ip(public_parts[-1]):
public_parts.pop()
parts = public_parts or parts
+ # The last remaining entry is the address that actually connected to the platform
+ # (appended by it, so the visitor can't forge it). If that is a Cloudflare edge, the
+ # request really came through Cloudflare and CF-Connecting-IP is genuine.
+ if parts and is_cloudflare_ip(parts[-1]):
+ ip = _valid_ip(request.headers.get("cf-connecting-ip"))
+ if ip:
+ return ip
if parts:
idx = len(parts) - TRUSTED_PROXY_HOPS
ip = _valid_ip(parts[idx] if idx >= 0 else parts[0])