要访问隧道端点后的一个子网,你有三种办法,而花一分钟收集事实通常就能排除其中两种。事实是:那边服务器是否必须区分用户、那边是否有人会主动拨向你的网段、以及谁能改远端路由器。回答这三问,方案自然浮现。其余内容假设端点由你运营,且你能修改它的防火墙。

三种方案,以及各自实际改变了什么

前缀映射、逐主机 DNAT 和路由化前缀都在同样的两个网段之间搬运数据包,但改变的东西不同。映射凭空造出一个别名网段,并在传输途中按算术改写地址。逐主机规则也改写,只不过每个目标都要手工点名。路由化前缀什么都不改写:数据包保留真实地址,两端对地址归属达成一致。

What the far side needsPrefix mappingPer-host DNATRouted prefix
Users visible to its logsOnly without source NATSame conditionYes, unchanged
To start a connection toward youNo, alias is localNo, same reasonYes, both ends route
To absorb a new hostYes, arithmetic onlyNo, one rule per addressYes, it simply routes
To require no config changeYes, with source NATYes, same conditionNo, it needs a route back

先从远端开始排除,因为那里的约束最难改。如果那边的服务器把客户端地址写进日志、拿它去比对白名单,或交给别的系统,那么转换源地址就毁掉了它唯一的标识。如果远端还必须主动连进来,转换就完全不可行,因为那边根本不知道别名的存在。只有当远端只是被动应答请求时,便宜的做法才安全。

前缀对,以及为什么两端长度必须一致

NETMAP 是 nat 表的目标,作用于一对长度相同的前缀。它内部没有逐地址表:它剥掉前缀、保留主机位,再挂到新前缀上,所以 /24 只能映射到 /24,绝不可能映射到 /25。位置决定方向:PREROUTING 里的目标匹配改写包去哪儿,POSTROUTING 里的源匹配改写包从哪儿来。

192.168.2.17 的主机在你的客户端眼里是 10.50.1.17,最后一个八位组不是装饰——两侧是同一个数字。明天再加一台 192.168.2.40,它的别名就是 10.50.1.40,没有规则要改。

客户端仍然需要一条指向你这台机器的别名网段路由,除非别名落在它们本就可达的子网里。网关不需要别名的路由,因为目标转换发生在 PREROUTING,早于路由查找。转发也必须打开,否则内核在改写之后会把包丢掉。下面三条命令就是 Debian 风格网关上的全部配置:客户端在 br0、别名在 10.50.1.0/24、隧道在 tun0。

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A PREROUTING -i br0 -d 10.50.1.0/24 -j NETMAP --to 192.168.2.0/24
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE

回程必须自己找到路

改写目标只决定包被送到哪里。远端服务器仍然必须应答一个它能到达的地址,而这有两种安排方式。转换源地址,让应答回到你这台机器;或者不动源地址,给远端一条客户端网段的路由。

出方向做 masquerade 是更短的路:POSTROUTING 里一条规则,远端看到你的隧道地址,因此它不需要自己的路由。代价是所有用户都塌缩成一个地址,按用户的日志也就失去了意义。保留源地址能保住这些信息,但那样远端站点必须把客户端网段的应答从隧道送回来。OpenVPN 服务器上,那就是一条 push 下发的 push "route 10.50.9.0 255.255.255.0",外加一条 route 192.168.2.0 255.255.255.0 行。

代价高昂的错误是两边各做一半。如果你伪装了一部分流量,而远端同时也为其他流量带了一条回程路由,应答就会到达一个从未见过连接跟踪条目的接口。客户端报告超时,而服务器日志却记为已服务。一条流的两个方向必须穿过同一个 netfilter 实例:一个网关做转换,而不是两个防火墙串联。

conntrack -L -d 10.50.1.17
iptables -t nat -L PREROUTING -v -n

两个方向都要读条目:原始元组在目标上保留你的别名,而应答元组携带真实地址。如果应答元组缺失,问题在回程而不是规则。如果 PREROUTING 计数器从未动过,包根本没到那个钩子。

冲突,以及先执行的那次查找

两个不同的网段可能冲突,而两种故障看起来都像隧道坏了,而不是规则写错了。你造的别名必须整条路径上都没人用,真实远端网段也不能已经存在于你的网络里。重复会造成间歇性的错主机流量,比连接被拒更难诊断。加规则之前,先问路由表。

在网关上,ip route get 192.168.2.17 应当指出隧道设备;返回本地网桥意味着远端网段被遮蔽,任何 nat 规则都修不好。如果你的网络已经在用 192.168.2.0/24,就重新给远端编址,而不是在冲突上再叠一层映射。

用真实客户端测试,然后再保存

要从真正会使用别名的机器上测,而不是从网关上测,因为本机生成的流量从来不会以同样的方式穿过 PREROUTING。开一条到某台别名主机的连接,观察计数器和 conntrack 条目一起出现,然后关掉,确认条目消失。残留的条目指向半开连接,通常意味着应答从未回来。

curl -sv http://10.50.1.17/ -o /dev/null
ss -tn dst 10.50.1.17
conntrack -L -d 10.50.1.17

两三次成功的连接能证明算术没错,但对持久化和下一位工程师毫无说明。把规则保存在启动流程会读取的地方,在 Debian 和 Ubuntu 上就是 iptables-save > /etc/iptables/rules.v4 并启用 netfilter-persistent。重启就消失的映射比没有更糟,因为故障来临时,找不到最近的改动来背锅。

在规则旁边留一条注释:别名网段、真实网段、成对的原因,以及哪一侧可以发起。这条注释把设计从一堆没人敢删的规则里区分出来。它也回答了后来的问题:远端加了一台主机,而一个没人配置过的地址已经在应答。如果注释说远端需要回程路由,下一个人就知道删掉 masquerade 规则会断掉连通性。这里的文档很便宜;凌晨三点重新发现 NAT 依赖可不便宜。

相关文章