"""Direct-peer IP access policy for the GPU worker's dynamic endpoints.""" import ipaddress import json from pathlib import Path import socket CONFIG_PATH = Path(__file__).resolve().parent / "ip_access.json" def _address(value): address = ipaddress.ip_address(value) return address.ipv4_mapped if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped else address def local_addresses(): addresses = {_address("127.0.0.1"), _address("::1")} try: addresses.update(_address(item[4][0].split("%", 1)[0]) for item in socket.getaddrinfo(socket.gethostname(), None)) except (OSError, ValueError): pass # Include all assigned Linux interface addresses, not entire private ranges. try: import fcntl import struct with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: for _, name in socket.if_nameindex(): try: data = fcntl.ioctl(sock.fileno(), 0x8915, struct.pack("256s", name.encode()[:15])) addresses.add(_address(socket.inet_ntoa(data[20:24]))) except OSError: continue path = Path("/proc/net/if_inet6") if path.exists(): addresses.update(ipaddress.IPv6Address(line.split()[0]) for line in path.read_text().splitlines()) except (ImportError, OSError, ValueError): pass return addresses def _networks(values): if not isinstance(values, list) or not all(isinstance(value, str) for value in values): raise ValueError("IP配置必须是字符串列表") return [ipaddress.ip_network(value, strict=False) for value in values] def _matches(address, networks): return any(address.version == network.version and address in network for network in networks) def check_access(request): """Return (allowed, reason), re-reading the whitelist for each request.""" try: config = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {} allowed = _networks(config.get("allowed_ips", [])) if request.client is None or request.client.port == 0: # Uvicorn proxy middleware has discarded the socket peer at port 0. # Reject rather than treating an unverified forwarded IP as local. return False, "无法确认直连来源,请使用 --no-proxy-headers 启动服务" peer = _address(request.client.host) # Authorize the gateway's socket IP, never its end-user forwarding headers. if peer.is_unspecified: return False, "来源地址无效" return (True, "") if peer.is_loopback or peer in local_addresses() or _matches(peer, allowed) else (False, "来源IP不在接口白名单") except (OSError, ValueError, TypeError, AttributeError): return False, "IP白名单配置或来源地址无效" def rejection(request): from fastapi.responses import JSONResponse if request.url.path == "/static" or request.url.path.startswith("/static/"): return None allowed, reason = check_access(request) if allowed: return None return JSONResponse(status_code=403, content={"code": 1010, "message": reason, "request_id": "ip-access", "data": None})