"""API Key 认证依赖 用于保护写操作(POST/PUT/DELETE)。 - 来自可信内网(Tailscale 100.64.0.0/10、本机回环)的请求直接放行; - 其余来源:API_KEY 已配置则校验 X-API-Key,未配置则拒绝(防止外部裸奔)。 """ import hmac import ipaddress from typing import Optional from fastapi import Header, HTTPException, Request, status from app.core.config import settings # 可信来源网段:Tailscale 使用 CGNAT 100.64.0.0/10 分配内网 IP(如 100.89.x.x); # 回环地址覆盖 tailscale serve 代理转发与本地开发场景。 TRUSTED_NETWORKS = [ ipaddress.ip_network("100.64.0.0/10"), ipaddress.ip_network("127.0.0.0/8"), ipaddress.ip_network("::1/128"), ] def _key_matches(provided: Optional[str], expected: str) -> bool: """常量时间比较密钥,避免时序旁路泄露密钥长度/前缀信息""" if not provided: return False return hmac.compare_digest(provided.encode(), expected.encode()) def _client_ip(request: Request) -> str: """获取客户端真实 IP tailscale serve 转发到本机时 request.client 为回环地址,真实来源在 X-Forwarded-For 头中;仅信任来自回环的 XFF,防止外部直连时伪造 X-Forwarded-For 绕过鉴权。 """ host = request.client.host if request.client else "" if host in ("127.0.0.1", "::1"): xff = request.headers.get("x-forwarded-for") if xff: return xff.split(",")[0].strip() return host def _is_trusted(ip: str) -> bool: """判断来源 IP 是否属于可信内网(Tailscale 网段 / 本机回环)""" if not ip: return False try: addr = ipaddress.ip_address(ip) except ValueError: return False return any(addr in net for net in TRUSTED_NETWORKS) def _is_funnel_request(request: Request) -> bool: """判断是否 tailscale funnel 公网流量 tailscale serve 转发时会把客户端伪造的 Tailscale-* 头清空后按真实 情况设置:tailnet 内无此头,funnel 公网流量为 "?1"(不可伪造)。 """ return request.headers.get("tailscale-funnel-request") == "?1" async def _require_key(x_api_key: Optional[str]) -> None: """按外部来源校验 API Key(可信内网已在调用方排除)""" if not settings.API_KEY: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="外部访问需要 API Key,请先在 .env 配置 API_KEY 并设置到前端", ) if not _key_matches(x_api_key, settings.API_KEY): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="API Key 无效或缺失", ) async def require_api_key( request: Request, x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"), ) -> None: """校验 API Key:Tailscale 内网/本机放行,外部来源必须携带有效 Key""" # funnel 公网流量:即使来源 IP 恰在 tailnet 网段也强制校验(双保险) if _is_funnel_request(request): await _require_key(x_api_key) return if _is_trusted(_client_ip(request)): return await _require_key(x_api_key) async def require_agent_key( x_agent_key: Optional[str] = Header(default=None, alias="X-Agent-Key"), ) -> None: """校验 Agent 上报 Key(可选启用)""" if not settings.AGENT_KEY: return if not _key_matches(x_agent_key, settings.AGENT_KEY): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Agent Key 无效或缺失", )