from __future__ import annotations import ipaddress from functools import lru_cache from django.conf import settings from django.http import HttpRequest @lru_cache(maxsize=32) def _trusted_networks( values: tuple[str, ...], ) -> tuple[ipaddress.IPv4Network | ipaddress.IPv6Network, ...]: return tuple(ipaddress.ip_network(value, strict=False) for value in values) def _parse_ip(value: str) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None: candidate = value.strip() if not candidate: return None try: return ipaddress.ip_address(candidate) except ValueError: return None def client_ip(request: HttpRequest) -> str: """Return a proxy-aware client IP without trusting arbitrary forwarded headers.""" remote = _parse_ip(request.META.get("REMOTE_ADDR", "")) if remote is None: return "unknown" networks = _trusted_networks(tuple(getattr(settings, "TRUSTED_PROXY_CIDRS", ()))) if not networks or not any(remote in network for network in networks): return str(remote) forwarded = [ parsed for item in request.META.get("HTTP_X_FORWARDED_FOR", "").split(",") if (parsed := _parse_ip(item)) is not None ] if not forwarded: return str(remote) # Walk from the nearest hop back to the client. The first address outside # the explicitly trusted proxy ranges is the client address. for candidate in reversed([*forwarded, remote]): if not any(candidate in network for network in networks): return str(candidate) return str(forwarded[0])