同一位 OpenBSD 网关交到两位工程师手里,往往会得到两套不同的访问远程网络的规则,而且两套都站得住脚。源地址转换与回程路由之间的取舍根本不是语法问题。它取决于对端对 192.168.88.0/24 已知多少,以及你需要在日志里留下什么。这里判断错了,nat-to 那一行会看起来完全正确,而回应却消失在远端默认网关里。

路由与改写回答的是不同问题

路由表决定 172.20.0.0/16 可以经 tun0 到达,这个判断发生在任何 pf 转换被查询之前。随后源地址转换改动包头,让远端看到的是网关的隧道地址而不是某台 LAN 主机。两种机制不能相互替代:nat 规则变不出路由,路由也说服不了一台从未听说过你私有子网的远端路由器。对端没有回程路径时,回应会被丢弃或转发到它自己的默认网关,再怎么调整规则也补不回来。

直白地说,转发回答流量往哪里去,转换回答它看起来从哪里来。设计规则集时把两者放进各自独立的思维格子里。

决定取舍的三个条件

按顺序问三个问题,设计通常自己就定了。远端网络有没有一条指向隧道地址、覆盖 192.168.88.0/24 的静态路由,或者它的管理员能否控制那条路径上的路由器?如果是,就让 LAN 地址原样通过,完全跳过转换。你需要在远端看到每个客户端吗,无论是为了访问策略还是为了审计线索?如果需要,保住源地址值得这番路由功夫。隧道地址是否稳定到可以写进远端路由,还是会在重连之间变动?

只要有一个答案是否或不确定,nat-to 就是务实的选择,而且它还是可逆的。远端一旦补上合适的路由,转换规则一行就能撤掉。一条从未安装的回程路由,会让你在每次有人问“为什么这条隧道只能单向工作”时多开一个支持工单。

把片段放在 pf 真正会评估的位置

下面的片段假定 LAN 是 192.168.88.0/24,远端网络是 172.20.0.0/16,隧道接口是 tun0。里面没有任何东西选择下一跳。它只是改写在已经经隧道外出的包上的源地址。

match out on tun0 inet from 192.168.88.0/24 to 172.20.0.0/16 nat-to (tun0)
pass in on em0 inet from 192.168.88.0/24 to any keep state

第一行里的两个细节承担了大部分分量。tun0 外面的括号让 pf 在每次建立状态表项时重新读取接口地址,这在重连后隧道被重新编号时很关键。match 一词意味着规则改写之后继续向下走,因此仍然需要一条单独的 pass 规则放行该流。match 行里点名的接口也必须是路由表所选的那一个,否则 pf 会在与包实际所走不同的出站路径上评估这条规则。

别忘了内核一侧。必须开启 IPv4 转发,隧道与 LAN 的有状态 pass 规则也必须先存在,这一切才有意义。关键字顺序可参照 OpenBSD FAQ 中的 PF NAT 一节。

在相信流量之前先验证规则集

先用 pfctl -nf /etc/pf.conf 检查语法,它只解析文件而不加载。然后加载策略,再把内核真正持有的内容读回来,因为解析通过的规则仍可能被遮蔽或挂在错误的接口上。pfctl -sn 按加载后的形态打印转换规则,pfctl -sr 按评估顺序打印过滤规则。

接着看线上。在网关上执行 tcpdump -ni tun0 'host 172.20.0.10',可以看出离开隧道的源地址是隧道地址还是 LAN 主机。一条 netstat -rn -f inet 能确认目的地解析到哪个接口。流量跑起来后,pfctl -ss 会列出状态表项;如果某个流没有产生状态,说明这个包从未命中能建立状态的规则。

如果隧道一侧看着没问题,回应却始终不来,就不要再改 pf 了。剩下的故障在远端路由器上,回应正被发往那一侧无法到达的私有地址。

当转换的代价高于收益

转换会隐藏客户端,有时这恰恰是笔亏本买卖。重叠子网是一种情形:如果远端内部也在用 192.168.88.0/24,把 LAN 主机呈现为网关只会让冲突更糟而不是更好。按地址对选择流量的 IPsec 策略是另一种,因为安全策略库在转换之前就完成匹配,永远看不到你改写后的包。任何按用户计费、限速或审计的部署,也会在源地址压缩成一个地址的那一刻失去它所依赖的身份。

可移植的 pf 衍生品是另一个陷阱。自行生成规则文件的产品期望通过它们的界面配置 NAT,手工改过的片段会在下次重载时被覆盖。从 match 关键字出现之前的指南里抄来的规则集在现行内核上加载不了,所以要移植语法并去读报错,而不是靠猜。

与真实网络打过交道后还能站得住的决策规则很短:能装回程路由时就装上,装不了就做转换,并记下是三个条件里的哪一个让你这样选。那行笔记能替下一个人省下一下午。

相关文章