VPN 背后,PHP 的 REMOTE_ADDR 究竟告诉了你什么
它是 Web 服务器给你的一个值,而不是对隧道的查询。顺着请求路径走一遍,弄清谁能改写它,并把身份判断留在应用里。
有人问:为什么 VPN 用户在应用日志里出现的地址属于网关,或者午饭之后换了一个地址。答案要从这个值是什么讲起:它是 Web 服务器放进请求环境里的一条记录。它不是对隧道的查询,不是证书身份,也不是“这个数字属于同一个人”的承诺。把它当成这三者中的任何一个,都会得出自信却以不同方式出错的结论。
值来自服务器,而不是隧道
PHP 从执行脚本的那一方获得请求环境。PHP 服务器变量参考把这些条目描述为由 Web 服务器创建,其可用性取决于请求是如何到达的。
作为 VPN 命令行钩子运行的脚本,是完全不同的执行环境。它没有 Web 请求,没有客户端套接字,也没有 REMOTE_ADDR,所以把 Web 代码复制进钩子,得到的是空值而不是错值。钩子有自己的环境,由守护进程填充,并且单独文档化。在那里去找 REMOTE_ADDR,等于去找一个从未设置过的变量。
在比较两份日志之前,请记住这个区别。你比较的可能是一次页面请求和一个钩子,那根本算不上比较。先检查执行环境,再看地址。
在相信这个数字之前,先走一遍请求路径
报告的地址是 Web 服务器所看到的那条连接的通信对端。只要在客户端和那台服务器之间插入任何东西,对端就变了。地址转换、连接终止和负载均衡,各自都把观测点向远离用户的方向推进一步。
四种常见路径会带来四种不同的观测结果。
| Path to the listener | What can be reported |
|---|---|
| Client routed straight to the private listener, no translation | The client's tunnel address |
| Gateway translates the source before forwarding | The gateway's translated address |
| Reverse proxy terminates the client and opens its own backend connection | The proxy's backend address |
| Load balancer in front of two proxies | Whichever hop the server treats as its peer |
把路径连同目标名称、地址族和每一个转换点一起写下来。没有这张图,正确值和错误值在日志行里看起来完全一样。两行示意图能省下一整个下午的争论。
只有配置了的地方才会发生替换
服务器或框架可以有意替换客户端地址。Apache 的 remote-IP 模块和 NGINX 的 real-IP 模块都会这样做,PHP 收到的值取决于该配置以及传给脚本处理器的参数。
所以 REMOTE_ADDR 并不总是未被触碰的 TCP 对端。它是服务器在应用自身策略之后选择报告的产物。这个区别决定了该值是证据还是装饰。
问清服务器所有者:这套部署里哪个字段才是权威的,发生替换时原始对端如何保留。如果没人答得上来,日志暂时还证明不了任何事。
不要凭信任去读转发头
调用方可以自己发送转发头。把第一个逗号分隔的条目当成已验证的用户地址,等于决定相信任何连上来的人,这正好与这个头的用途相反。头描述的是一条链,只有可信的一层才能为这条链背书。
使用服务器或框架的可信代理支持,并配置真实的代理链,让它丢弃来自链外来源的值。手写的解析正是大多数有意思的故障所在之处。
代理报告的地址也不会改变上游防火墙看到的报文来源。请把 HTTP 元数据和网络过滤当成两件独立的观测,因为它们回答的是不同的问题。
用应用身份来做决定
要访问某个用户的记录,请依赖已认证的主体和应用的授权规则。内部地址可能被重新分配,而一个被转换过的地址可能代表数百个并发用户。这两点都不是地址校验能修补的。
如果运维需要把一次请求关联到某个 VPN 会话,就按你记录过的转换模型,把观测到的地址和时间戳与权威会话记录关联起来。这样才能得到站得住脚的关联;单靠一个地址不行。
做诊断时,只记录你需要的那几个字段,而不是整个服务器数组,因为它可能带着请求头和认证材料。有用的结论说明的是这个值在这套部署里意味着什么,而不是哪个值最像客户端。