用 NAT 把私网送上互联网
本实验在 VM 中运行
这次,VM 本身就是路由器。命名空间 lan 是私有网络中的主机,
VM 的 uplink(enp1s0,10.0.2.2)位于互联网一侧。私有地址 10.30.0.2
没有从外部返回的路径,因此 VM 必须将其改为自己的地址再发送出去——这就是
地址伪装。此 VM 只能访问公共网络的 80/443 端口和 DNS。
目标
通过地址伪装(SNAT)让私有网络主机访问互联网,在两侧接口上捕获数据包,观察源地址确实
发生变化,再使用 DNAT 将来自外部的连接转发给内部
服务器。家用路由器和 Docker 的 -p 所做的正是这些事情。
为什么重要
私有地址无法在互联网上路由。然而,家庭、公司、云 VPC 和 Kubernetes Pod 使用的全都是私有地址。它们与外部通信的方法 只有一种——在边界处转换地址,并在响应返回时恢复原地址。让这种恢复 成为可能的是连接跟踪(conntrack),因此 NAT 设备重启后, 所有连接都会断开。“内部可以访问,但外部无法访问”这类报告的答案几乎 总能在本章中找到。
步骤
- 创建命名空间
lan和 vethlan0(lan,10.30.0.2/24)↔lan-r(根命名空间,10.30.0.1/24),将lan的默认路由设为10.30.0.1,并将根命名空间的net.ipv4.ip_forward设为 1。 - 将从
lan向 uplink 网关10.0.2.1执行 ping 失败的结果保存到/root/nat/before.txt。 - 在表
ip labnat的postrouting钩子链中添加规则,使来自10.30.0.0/24且从 uplink 发出的流量执行counter masquerade,从而让 ping 成功。 - 同时在 uplink 和
lan-r上捕获 ICMP,并从lan发送 ping;将从外部看到的已变化源地址保存到/root/nat/outside.txt,将内部的原始源地址保存到/root/nat/inside.txt。 - 在
/etc/netns/lan/resolv.conf中写入上游 DNS 服务器,使lan内的getent hosts archive.ubuntu.com能够成功,并将结果保存到/root/nat/dns.txt。 - 将从
lan执行curl -sI http://archive.ubuntu.com/得到的响应头保存到/root/nat/web.txt。 - 将
nft list table ip labnat保存到/root/nat/counter.txt。地址伪装规则的counter packets必须大于 0。 - 在
lan内启动10.30.0.2:80Web 服务器,并创建命名空间guest(vethguest010.31.0.2/24 ↔guest-r10.31.0.1/24);通过prerouting钩子链中的规则iifname "guest-r" tcp dport 8080 dnat to 10.30.0.2:80,使guest能够访问http://10.31.0.1:8080/,并将响应第一行保存到/root/nat/dnat.txt。
参考
- uplink 名称是
ip route show default中的dev(此 VM 为enp1s0)。 - 地址伪装:
nft add table ip labnat; nft 'add chain ip labnat post { type nat hook postrouting priority 100; }'; nft add rule ip labnat post ip saddr 10.30.0.0/24 oifname "enp1s0" counter masquerade。 - 命名空间也有独立的
/etc/resolv.conf。如果存在/etc/netns/<이름>/resolv.conf,ip netns exec会用它进行覆盖。上游服务器地址通过resolvectl dns enp1s0获取。 - DNAT:
nft 'add chain ip labnat pre { type nat hook prerouting priority -100; }'。 - 常见错误:遗漏
ip_forward。即使有 NAT 规则,不进行转发也毫无作用。
一台私有网络主机
创建命名空间 lan 和 veth lan0(lan,10.30.0.2/24)↔lan-r(根命名空间,10.30.0.1/24),将 lan 的默认路由设为 10.30.0.1,并将根命名空间的 net.ipv4.ip_forward 设为 1。
这次不移动 veth 的一端(lan-r),而是将其留在根命名空间。执行 ip addr add 10.30.0.1/24 dev lan-r; ip link set lan-r up、ip -n lan route add default via 10.30.0.1、sysctl -w net.ipv4.ip_forward=1。
没有 NAT 时
将从 lan 向 uplink 网关 10.0.2.1 执行 ping 失败的结果保存到 /root/nat/before.txt。
执行 ip netns exec lan ping -c 1 -W 2 10.0.2.1 > /root/nat/before.txt 2>&1。请求会经过 VM 发出,但响应没有返回 10.30.0.2 的路径,因此结果为 100% packet loss。这与没有路由时(unreachable)的症状不同。
地址伪装
在表 ip labnat 的 postrouting 钩子链中添加规则,使来自 10.30.0.0/24 且从 uplink 发出的流量执行 counter masquerade,从而让 ping 成功。
执行 nft add table ip labnat、nft 'add chain ip labnat post { type nat hook postrouting priority 100; }'、nft add rule ip labnat post ip saddr 10.30.0.0/24 oifname "enp1s0" counter masquerade。现在 lan 的 ping 可以成功。
地址发生变化的瞬间
同时在 uplink 和 lan-r 上捕获 ICMP,并从 lan 发送 ping;将从外部看到的已变化源地址保存到 /root/nat/outside.txt,将内部的原始源地址保存到 /root/nat/inside.txt。
设置两项捕获:timeout 5 tcpdump -i enp1s0 -nn -l icmp > /root/nat/outside.txt 2>&1 & 和 timeout 5 tcpdump -i lan-r -nn -l icmp > /root/nat/inside.txt 2>&1 &。然后执行 ip netns exec lan ping -c 1 10.0.2.1。外部文件应显示 10.0.2.2 > 10.0.2.1,内部文件应显示 10.30.0.2 > 10.0.2.1。
命名空间的 resolv.conf
在 /etc/netns/lan/resolv.conf 中写入上游 DNS 服务器,使 lan 内的 getent hosts archive.ubuntu.com 能够成功,并将结果保存到 /root/nat/dns.txt。
执行 mkdir -p /etc/netns/lan; echo 'nameserver <상류>' > /etc/netns/lan/resolv.conf。上游地址通过 resolvectl dns enp1s0 获取。命名空间内没有 127.0.0.53 存根,因此必须直接写入真实服务器。查询会通过地址伪装发出。
访问互联网
将从 lan 执行 curl -sI http://archive.ubuntu.com/ 得到的响应头保存到 /root/nat/web.txt。
执行 ip netns exec lan curl -sI -m 10 http://archive.ubuntu.com/ > /root/nat/web.txt。第一行必须是 HTTP/1.1 200 OK。这意味着使用私有地址的主机已经与互联网服务器建立了 TCP 连接。
进行计数
将 nft list table ip labnat 保存到 /root/nat/counter.txt。地址伪装规则的 counter packets 必须大于 0。
因为第 3 步中加入了 counter,所以 nft list table ip labnat 中会显示 counter packets N bytes M masquerade。N 是新连接的第一个数据包数——后续数据包由 conntrack 处理,不会再次经过 NAT 链。
端口转发
在 lan 内启动 10.30.0.2:80 Web 服务器,并创建命名空间 guest(veth guest0 10.31.0.2/24 ↔ guest-r 10.31.0.1/24);通过 prerouting 钩子链中的规则 iifname "guest-r" tcp dport 8080 dnat to 10.30.0.2:80,使 guest 能够访问 http://10.31.0.1:8080/,并将响应第一行保存到 /root/nat/dnat.txt。
服务器:ip netns exec lan setsid python3 -m http.server 80 --bind 10.30.0.2 --directory /root/nat > /root/nat/http.log 2>&1 < /dev/null &。guest 的创建方式与第 1 步相同(默认路由为 10.31.0.1)。链:nft 'add chain ip labnat pre { type nat hook prerouting priority -100; }'。检查:ip netns exec guest curl -sI http://10.31.0.1:8080/ | head -1。