cdnV2

git clone https://github.com/notamitgamer/cdnV2.git
· rebuilt hourlyOpen on GitHub
commit 4d790339f8335ff76c955ac36dfba254ec7fd1ee
parent 3e4353cd7d6d37b1febd80feccef627da7e72ca4
Author: claude-bot <noreply@anthropic.com>
Date:   Tue,  6 Oct 2026 03:12:12 +0000

Fix client IP detection behind Render; add 'How the server sees you' panel

- get_client_ip ignores internal (10.x etc.) hops at the right end of X-Forwarded-For before counting
  proxy hops, so the visitor IP is used instead of Render's changing internal proxy address
- Refuse to ban non-public addresses (would never match a real visitor)
- Dashboard shows detected IP, raw forwarding headers and the proxy settings

Diffstat:
Mapp/admin.py | 21+++++++++++++++++++++
Mapp/upload_guard.py | 13+++++++++++++
2 files changed, 34 insertions(+), 0 deletions(-)

diff --git a/app/admin.py b/app/admin.py @@ -232,7 +232,15 @@ 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 + h = request.headers return { + "whoami": { + "detected": get_client_ip(request), + "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, + }, "now": int(time.time()), "error": _load_error, "log": _state["log"][-300:][::-1], "bans": _state["bans"], "gh": sorted(_state["gh"].items(), key=lambda kv: kv[1].get("last", 0), reverse=True), @@ -255,6 +263,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: + 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.", + ) _state["bans"][ip] = {"t": int(time.time()), "note": body.note[:100]} return await _mutate_and_save() @@ -347,6 +360,14 @@ function render(s){ const banned=s.bans,pending=s.gh.filter(([k,g])=>g.status==='pending'&&g.syncs>=1); let h=''; 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>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> + <tr><td>Settings</td><td class="mono">TRUST_CF_CONNECTING_IP=${w.trust_cf_header?1:0}, TRUSTED_PROXY_HOPS=${w.proxy_hops}</td></tr></table></div> + <p class="dim">The detected IP must be YOUR real public IP. If it is not, bans will hit the wrong address.</p>`; if(pending.length){ h+='<h2>Waiting for approval</h2><div class="wrap"><table><tr><th>GitHub account</th><th>Repos</th><th>Syncs</th><th>Last sync</th><th></th></tr>'; for(const [k,g] of pending)h+=`<tr><td><a href="https://github.com/${encodeURIComponent(g.owner)}" target="_blank" rel="noopener">${esc(g.owner)}</a></td><td>${esc((g.repos||[]).join(', '))}</td><td>${g.syncs}</td><td>${fmt(g.last)}</td> diff --git a/app/upload_guard.py b/app/upload_guard.py @@ -50,6 +50,13 @@ def _valid_ip(value: str | None) -> str | None: return None +def is_public_ip(value: str) -> bool: + try: + return ipaddress.ip_address(value).is_global + except ValueError: + return False + + def get_client_ip(request: Request) -> str: """Best-effort real client IP behind Cloudflare / Render's proxy. @@ -65,6 +72,12 @@ def get_client_ip(request: Request) -> str: xff = request.headers.get("x-forwarded-for") if xff: parts = [p.strip() for p in xff.split(",") if p.strip()] + # Hosts like Render append their own internal (10.x etc.) proxy addresses to + # the right-hand end. Those are never the visitor, so drop them before counting hops. + public_parts = list(parts) + while public_parts and not is_public_ip(public_parts[-1]): + public_parts.pop() + parts = public_parts or parts if parts: idx = len(parts) - TRUSTED_PROXY_HOPS ip = _valid_ip(parts[idx] if idx >= 0 else parts[0])