LabHub
学习 学习路径 课程

网络基础 — 在 Linux 虚拟机里动手

用 NAT 把私网送上互联网

在 LabHub 中继续学习

本实验在 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 设备重启后, 所有连接都会断开。“内部可以访问,但外部无法访问”这类报告的答案几乎 总能在本章中找到。

步骤

  1. 创建命名空间 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。
  2. 将从 lan 向 uplink 网关 10.0.2.1 执行 ping 失败的结果保存到 /root/nat/before.txt
  3. 在表 ip labnatpostrouting 钩子链中添加规则,使来自 10.30.0.0/24 且从 uplink 发出的流量执行 counter masquerade,从而让 ping 成功。
  4. 同时在 uplink 和 lan-r 上捕获 ICMP,并从 lan 发送 ping;将从外部看到的已变化源地址保存到 /root/nat/outside.txt,将内部的原始源地址保存到 /root/nat/inside.txt
  5. /etc/netns/lan/resolv.conf 中写入上游 DNS 服务器,使 lan 内的 getent hosts archive.ubuntu.com 能够成功,并将结果保存到 /root/nat/dns.txt
  6. 将从 lan 执行 curl -sI http://archive.ubuntu.com/ 得到的响应头保存到 /root/nat/web.txt
  7. nft list table ip labnat 保存到 /root/nat/counter.txt。地址伪装规则的 counter packets 必须大于 0。
  8. 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

参考

一台私有网络主机

创建命名空间 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 upip -n lan route add default via 10.30.0.1sysctl -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 labnatpostrouting 钩子链中添加规则,使来自 10.30.0.0/24 且从 uplink 发出的流量执行 counter masquerade,从而让 ping 成功。

执行 nft add table ip labnatnft '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