在云边界保留 VPN 客户端 IP 还是做 SNAT
保留源地址能让每条日志都指向同一个客户端,SNAT 则让路由表保持简单。三个条件决定应用需要哪一种。
把 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 的东西往往根本做不到。
把每个边界看到的东西写下来
无论选哪种,设计评审最后都该以一张表收尾,写清每一跳的源地址和可证明的身份。没有这张表,第一次事故就会出现三个团队各描述一个不同的客户端。
| Boundary | Source the next hop sees | What is actually proven |
|---|---|---|
| Client to gateway | 172.20.8.14 | A certificate or credential was presented |
| Gateway to app subnet, preserved | 172.20.8.14 | The same session, if the return route exists |
| Gateway to app subnet, translated | 10.40.3.10 | Only that traffic came through the gateway |
| Proxy to backend | 10.40.3.10 plus a header | The proxy asserted an identity it authenticated |
然后再选。当应用的授权确实依赖客户端地址、而你又承担得起那条路由时,就保留源地址。做不到、但你能把身份带外传递时,就做转换,平台相关的转发设置可以参考 AWS 的转发实例指南。当服务说的是 HTTP、而你想让身份彻底不再是地址时,就上中继。