把 VPN 客户端的地址一路保留到应用,可以让每条日志和每条允许列表记录表达同一件事。在云边界重写地址会让路由表更简单,也让日志变得不那么有用。两种做法本身都谈不上错,而决定取舍的论据很少是性能。真正起决定作用的是三个条件:应用是否必须知道是哪个具体的人连了进来、云平台是否会放行你打算发出的源地址,以及事后谁愿意承担回程路由的责任。

三个条件决定取舍

先看应用的需求,而不是网络拓扑图。如果授权或审计依赖单个客户端地址,那么保留源地址就是功能需求,做转换则是功能退化。如果应用只需要知道流量是从远程接入路径过来的,那么转换后的地址就够了,运维成本也低得多。

第二个条件是机制层面的。虚拟网络不会自动相信一个声称 并非发送接口所分配地址 的数据包。多数云平台都会以不同方式、不同名义强制执行这一点,无视它的设计在图上看是对的,上线后却会静默丢包。

第三个条件是组织层面的。保留源地址意味着应用子网的路由表必须为客户端网段指回网关,而此后每新增一个子网的团队都要维护这条路由的一致性。这是一次性的小成本,却会在几十个账户里反复出现。在承诺这种行为之前,先决定由谁承担。

有路由不等于可以伪造源地址

加一条回程路由只是告诉子网应答该往哪走,并没有告诉云平台:转发这些包的实例有权使用别人的源地址发包。在 AWS 上,对应的控制项是源和目标检查,它可以按实例查看。

aws ec2 describe-instances --instance-ids i-0a1b2c3d4e5f60718 \
  --query "Reservations[].Instances[].SourceDestCheck"
aws ec2 modify-instance-attribute --instance-id i-0a1b2c3d4e5f60718 \
  --no-source-dest-check

只对文档中职责就是转发的实例关闭该检查,别的机器一律不动。每多保留一台开启检查的应用虚拟机,就少一台可能被用来以借用源地址发包的机器。如果你的平台不是 AWS,请去找对应的防伪造或允许地址设置,而不要想当然地认为行为会平移过来。

抓一次包比读控制台更快得出结论。如果应答发出去了而客户端从未收到,说明是云平台丢弃了伪造的源地址,而不是应用出了问题。

转换买到了什么,又付出了什么

在网关上重写源地址,可以彻底消除回程路径问题。应用只看到一个它本来就知道怎么访问的地址,客户端网段也永远不必出现在云路由表里。在 Linux 上规则只有一行,值得写成你能写出的最窄版本:

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 172.20.8.0/24 -o ens5 -j SNAT --to-source 10.40.3.10

注意应用从此记录的是什么:每个用户、每条连接、永远都是 10.40.3.10。任何基于这个值建立的地址允许列表,等于对所有远程接入人员放行;任何事故复盘都得回到网关去回答这是谁。

问题不在于放弃转换,而在于别再假装地址就是身份。把客户端身份放进经过认证的字段,在网关上连同转换后的元组一起记录,让应用按这个字段去关联。没有这个关联键,转换就把网络问题变成了永久的取证问题。

中继是另一种接口

放在应用前面的反向代理可以完全绕开路径是否可路由这个问题。代理会自己建立到后端的连接,因此客户端网段不必在虚拟网络内可路由,应用还能从代理设置的请求头里拿到身份。

只有当后端只接受来自该代理的这个请求头、而不信任其他来源时它才可靠。用 nginx 的说法,就是明确写出可信对端,而不是来什么都信:

set_real_ip_from 10.40.3.10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

任何能直接访问后端的客户端都能伪造同样的请求头,所以后端必须除代理之外不可达。这是防火墙层面的事实,不是配置偏好;很多人复制那几行配置时,恰恰跳过了这个前提。

中继也改变了你买到的东西。你得到了应用层身份和一个记录它的自然位置,却放弃了通用性,因为每种新协议都需要自己的代理支持。对 HTTP 服务来说这笔交换通常划算,而对非 HTTP 的东西往往根本做不到。

把每个边界看到的东西写下来

无论选哪种,设计评审最后都该以一张表收尾,写清每一跳的源地址和可证明的身份。没有这张表,第一次事故就会出现三个团队各描述一个不同的客户端。

BoundarySource the next hop seesWhat is actually proven
Client to gateway172.20.8.14A certificate or credential was presented
Gateway to app subnet, preserved172.20.8.14The same session, if the return route exists
Gateway to app subnet, translated10.40.3.10Only that traffic came through the gateway
Proxy to backend10.40.3.10 plus a headerThe proxy asserted an identity it authenticated

然后再选。当应用的授权确实依赖客户端地址、而你又承担得起那条路由时,就保留源地址。做不到、但你能把身份带外传递时,就做转换,平台相关的转发设置可以参考 AWS 的转发实例指南。当服务说的是 HTTP、而你想让身份彻底不再是地址时,就上中继。

相关文章