几天前,我的 ISP(像常见的电信、联通、移动等公司,在为用户提供网络接入服务时,都属于 ISP) 断电后切断了我路由器的 IPv4 连接。幸好 IPv6 连接还能正常使用,但只能访问一小部分网站。
在这篇文章中,我将介绍 Linux、WireGuard 和 Hetzner 如何提供帮助 - 仅通过 IPv6 连接即可保持整个互联网可用。
背景
一天早上,我醒来发现停电了,发现断路器跳闸了。重置后,一切恢复正常——只是无法连接到 GitHub 和许多其他网站。
在联系我的 ISP 的过程中,我意识到问题只出在 IPv4 服务器上。我可以正常连接到 IPv6 服务器——这也解释了为什么 Google 和 Meta 可以正常工作,而很多网站却不行。我的电脑和路由器的诊断页面ping -6都立即证实了这一点。这似乎是运营商级 NAT (CG-NAT)traceroute的问题,这就是为什么只有 IPv4 受到影响的原因。
不幸的是,互联网服务提供商说他们可能需要派人来,而且需要几天时间,周末过后也一样。与此同时,我需要上网,妻子也需要完成她的论文,所以不能一直让它断网。
幸好我记得我有一台 Hetzner VPS 服务器(VPS 即虚拟专用服务器(Virtual Private Server),是利用特殊的虚拟化技术,将一台物理服务器分割成多个相互隔离的小服务器。),同时拥有静态 IPv4 和 IPv6 地址。而且 Hetzner 网站也支持 IPv6,所以我可以访问那里的控制台进行设置。
但首先我们需要了解什么是网络地址转换(NAT)。
网络地址转换 (NAT)
互联网协议 (IP) 地址用于指定 IP 流量的来源和目的地。就像一封带有回信地址的信件一样,您将数据包发送到带有 IP 地址(可能是通过域名解析的)的服务器,服务器会从收到的数据包上的源地址将回复发送给您。
然而,IPv4 地址只有 32 位,扣除各种保留地址块后,我们只能得到大约 37 亿个可能的公共 IPv4 地址。如今,几乎每个人都至少拥有一部联网的手机,并且可能同时连接多台电脑、智能手表、智能电视等——可用的地址根本不够直接寻址互联网上的所有设备。
NAT 通过让多台设备共享一个 IP 地址来缓解这种情况。例如,您的家用路由器可能只分配了一个公网 IPv4 地址,供所有设备共享。当路由器收到来自您某台设备的数据包时,它会将源 IP 地址(例如,您设备的本地 IP 地址 - 192.168.1.xxx)替换为其公网 IP 地址。
路由器的连接跟踪 (conntrack) 系统会记录原始源 IP 和端口。当路由器转发数据包时,它会将源 IP 替换为自己的公网 IP 和唯一端口,并存储此映射。当回复到达该唯一端口时,conntrack 会使用该映射将目标 IP 重写回原始设备的内部 IP 和端口。在 Linux 上,您可以使用conntrack -Lfrom 查看存储的映射conntrack-tools。
这有点像往办公室寄信,只需填写收件人的姓名和办公楼即可。秘书 (NAT) 负责将信寄给员工并收到回复,而您无需知道收件人的具体办公桌和位置。您只会看到办公楼(NAT 路由器)的地址。
顺便说一下,这也充当了隐式防火墙的作用,因为路由器后面的本地设备上的任何服务都需要明确地进行端口转发 - 即路由器将在给定端口上接收的所有流量转发到特定的设备和端口,反之亦然 - 像前面的例子一样修改数据包。
但鉴于 IPv4 地址的稀缺,仅在家庭路由器层面,这仍然不够。因此,许多互联网服务提供商 (ISP) 会在内部再次应用这种技术——这被称为运营商级 NAT (CG-NAT)。其概念相同,只是它不是由家庭路由器对许多本地设备进行 NAT,而是由 ISP 路由器对许多家庭路由器进行 NAT(这些家庭路由器本身仍然具有对本地设备的 NAT)。
根据 ISP 拥有的 IPv4 地址数量以及愿意分配的数量,该过程可能会在多个层次上重复 - 例如针对 ISP 覆盖的不同区域。
正是这个过程导致中断仅影响 IPv4 - 在运营商级 NAT 层次结构中的某个地方,数据包没有被正确 NAT,导致数据包被丢弃并完全丢失 IPv4 流量。
请注意,如果您想将本地设备(例如视频游戏服务器)的服务转发到互联网,这也会很麻烦——您可能需要联系您的 ISP,在 CG-NAT 中传播您的端口转发,而许多 ISP 会完全拒绝这种做法。我强烈建议您阅读 Tailscale 的文章“NAT 穿越的工作原理”, 了解解决此问题的不同方法——这是我读过的最好的文章。
IPv6
IPv6 有如此多的可用地址,我们不需要使用 NAT(但如果您愿意,仍然可以使用 - 例如简化防火墙规则,尽管这并不常见,因为直接路由的性能更高)。
IPv6 地址为 128 位,考虑到保留块后,我们总共可以获得约 3.4E38 个地址。
由于地址空间如此充裕,/64 在家用路由器上接收子网是很常见的。这样你就可以获得 1.84E19 个地址——对于你所有智能手表、智能电视、冰箱、镜子和灯泡来说,这些地址已经足够了。
这意味着使用 IPv6,您甚至不需要在家用路由器上进行 NAT,您的所有设备都可以直接从互联网寻址,而无需担心端口转发问题。然而,这也意味着您的路由器和/或设备本身必须设置适当的防火墙规则,以禁止从互联网上任何地方随机连接到其开放端口的新连接。
这就是为什么没有将 CG-NAT 应用于 IPv6,以及为什么它幸运地没有受到该事件的影响。
遗憾的是,许多 Web 服务器仍然无法通过 IPv6 访问(在撰写本文时,最突出的是 GitHub!)。这意味着必须通过 IPv6 隧道传输流量才能恢复 IPv4 功能——因为只有 IPv6 地址,我们只能与同样拥有 IPv6 地址的服务器直接通信。
WireGuard 隧道
计划很简单:在 VPS (上文提到的Hetzner)上设置 WireGuard(安装 wireguard-tools),然后在我的机器上使用 IPv6 地址作为客户端的端点。隧道建立后,IPv4 流量应该可以正常工作(尽管通过 VPS 的延迟会更高)——有点像运行我们自己的 Dual-Stack Lite (DS Lite——但不是任天堂主机!)。
我之前在我的服务器上使用 vps2arch安装了 Arch Linux,效果非常好——我很高兴我这么做了,因为这意味着我拥有了一个熟悉的环境。我使用 Hetzner 上最新的 Debian 镜像作为基础。注意,你也可以之后使用 Hetzner 的 ISO 镜像挂载功能手动安装 Arch Linux(他们也托管 Arch Linux 的 ISO,你无需设置自定义镜像)。
起初,我在隧道中运行 IPv6 流量时遇到了一些困难(讽刺的是,考虑到最初的问题),但最终还是成功了。以下是我的完整配置供参考(改编自 ArchWiki 上的示例,但添加了 IPv6 流量):
服务器端
服务器端配置(演示 NATed 和直接 IPv6 对等体):
# This is the server config
# Place in /etc/wireguard/wg0.conf
# Install wireguard-tools
# Then start with:
# sudo wg-quick up wg0
# You can also set up a service with systemd
# systemctl enable wg-quick@wg0.service
# systemctl start wg-quick@wg0.service
# Generate WireGuard keypairs with:
# wg genkey | (umask 0077 && tee peer_A.key) | wg pubkey > peer_A.pub
# Do this once for the server pair, and once for each client pair
[Interface]
Address = 10.200.200.1/24, fd42:42:42::1/64, 2001:db8:abcd:1234::1/128
ListenPort = 51820
PrivateKey = serverprivatekey # CHANGEME: Set server private key here
# Note here we assume the network device interface is eth0 - remember to check this!
# IPv4 forwarding with NAT - note it'd be better to use SNAT here
# if the public IPv4 is static, but this is left as an example of MASQUERADE
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# IPv6 NAT for ULAs only (not GUAs)
# Here we use SNAT instead of MASQUERADE as an example (assume the server IPv6 GUA is static)
PostUp = ip6tables -t nat -A POSTROUTING -s fd42:42:42::/64 -o eth0 -j SNAT --to-source 2001:db8:abcd:1234::1
# IPv6 forwarding (for NATed ULAs and GUAs too)
PostUp = ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -A FORWARD -o %i -j ACCEPT
PostUp = echo 1 > /proc/sys/net/ipv6/conf/all/forwarding
PostUp = echo 1 > /proc/sys/net/ipv4/conf/all/forwarding
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
PostDown = ip6tables -t nat -D POSTROUTING -s fd42:42:42::/64 -o eth0 -j SNAT --to-source 2001:db8:abcd:1234::1
PostDown = ip6tables -D FORWARD -i %i -j ACCEPT; ip6tables -D FORWARD -o %i -j ACCEPT
# This peer has a direct IPv6 Global Unicast Address (no IPv6 NAT)
[Peer]
# foo
PublicKey = clientpublickey # CHANGEME: Set client public key here
AllowedIPs = 10.200.200.2/32, 2001:db8:abcd:1234::2/128
# This peer uses NATed IPv6 and is assigned a Unique Local Address
[Peer]
# bar
PublicKey = client2publickey # CHANGEME: Set client2 public key here
AllowedIPs = 10.200.200.3/32, fd42:42:42::3/128
请注意,与许多 .ini 风格的配置文件不同,wg-quick 允许多次指定 PostUp 和 PostDown 命令,并且会按顺序执行每个命令。bash脚本的源代码请见此处。
通过 MASQUERADE 进行 IPv6 NAT
另请注意,我最初使用的是 IPv6 NAT(并非仅限于 IPv4,在必要时),就像上面的“bar”配置一样。但是,如果您的 VPS 有地址块(例如,Hetzner 为您提供了 /64 的 IPv6 地址块),则可以避免使用 NAT,而是让 WireGuard 对等体直接通过 IPv6 全局单播地址 (GUA) 进行寻址,如上文 NAT 部分所述——这在“foo”配置中有所体现。
我们只需将对等体的唯一本地地址 (ULA) 直接更改为公共 IPv6 地址,然后删除 ip6tables MASQUERADE 规则即可。现在,每个对等体都可以通过其分配的 IPv6 地址从互联网直接寻址。
如果您想转发具有自己的服务的多台设备,这绝对是可行的方法(但您还需要确保 VPS 上的防火墙规则正确处理传入流量)。
SNAT 规则
最后需要注意的是,如果您的 VPS 上有一个静态 IP 地址,并且确定它不会更改,您也可以使用 SNAT 而不是 MASQUERADE。这样做效率会更高一些,因为 MASQUERADE 规则需要在运行时查找接口的 IP 地址,而 SNAT 规则则会直接设置。
这在 IPv6 NATing 的配置中得到了证明,使用 SNAT 而 --to-source不是 MASQUERADE。
客户端
客户端配置:
具有直接 IPv6 的对等点“foo”:
# This is the client config, run on the client machine with:
# sudo wg-quick up ./foo.conf
# from wireguard-tools
[Interface]
Address = 10.200.200.2/32, 2001:db8:abcd:1234::2/128
PrivateKey = clientprivatekey # CHANGEME: Set client private key here
# Google DNS
DNS = 8.8.8.8
DNS = 2001:4860:4860::8888
MTU = 1280 # This was not in the initial config - see later in the post
[Peer]
PublicKey = serverpublickey # CHANGEME: Set server public key here
# Note the square brackets needed for IPv6
Endpoint = [2001:db8:abcd:1234::1]:51820 # CHANGEME: Change serveripv6 here! If IPv4 do not need square brackets
AllowedIPs = 0.0.0.0/0, ::/0
使用 NATed IPv6 的对等“栏”:
# This is the client config, run on the client machine with:
# sudo wg-quick up ./bar.conf
# from wireguard-tools
[Interface]
Address = 10.200.200.3/32, fd42:42:42::3/128
PrivateKey = clientprivatekey # CHANGEME: Set client private key here
# Google DNS
DNS = 8.8.8.8
DNS = 2001:4860:4860::8888
MTU = 1280 # This was not in the initial config - see later in the post
[Peer]
PublicKey = serverpublickey # CHANGEME: Set server public key here
# Note the square brackets needed for IPv6
Endpoint = [2001:db8:abcd:1234::1]:51820 # CHANGEME: Change serveripv6 here! If IPv4 do not need square brackets
AllowedIPs = 0.0.0.0/0, ::/0
然后,在两端运行后,一切顺利。我甚至可以直接通过 SSH 连接到隧道本地的 IPv4 和 IPv6 地址来访问服务器。
这解决了常规浏览的问题,并且为我的妻子在 Linux 上安装 WireGuard 客户端也变得非常简单。
但是,我仍然无法以这种方式连接到我的工作 VPN,因为它会干扰 WireGuard 连接。
网络命名空间
作为vopono的创建者,我的计划是在网络命名空间中运行工作 VPN 和所有必要的应用程序。诀窍在于设置 MASQUERADE 规则,使其将流量转发到 WireGuard 接口(上文中的“foo”或“bar”),而不是直接转发到实际的网络接口 (enpXsY)。
这样,网络命名空间内的流量就不会受到主机外部 WireGuard 隧道 nftables 规则(来自 wg-quick)的影响,但流量将通过 WireGuard 隧道路由。
还值得注意的是,尽管 wg-quick 在可用时更喜欢使用 nftables 而不是 iptables,但它设法避免与 Docker 的标准 iptables 规则发生冲突。
下面是具有网络命名空间(vo_none_none)的 NATed 情况(上面的“bar”配置)的图表:
添加图片注释,不超过 140 字(可选)
我们可以使用 vopono 来实现这一点,只需在参数中指定正在运行的 WireGuard 接口(本例中为“bar”)-i。整体如下所示:
$ vopono -v exec --create-netns-only --provider None --protocol None -i bar bash
$ sudo ip netns exec vo_none_none bash
$ (inside netns) ./vpn.sh # Script to run the work VPN
请注意,/etc/netns/vo_none_none/将被挂载到 的/etc挂载命名空间中ip netns exec。这意味着我们可以将特定的 DNS 服务器放入其中resolv.conf。请注意,如果您想在网络命名空间中优先使用 IPv4 DNS 解析,也可以执行相同的操作进行编辑 gai.conf。
当我仍在努力让 IPv6 流量通过隧道时,我使用了后者,这里gai.conf供参考(改编自这个 AskUbuntu 答案):
precedence ::ffff:0:0/96 100
# For sites which use site-local IPv4 addresses behind NAT there is
# the problem that even if IPv4 addresses are preferred they do not
# have the same scope and are therefore not sorted first. To change
# this use only these rules:
#
scopev4 ::ffff:169.254.0.0/112 2
scopev4 ::ffff:127.0.0.0/104 2
scopev4 ::ffff:0.0.0.0/96 14
因此,在如上所述连接到 VPN 之后,我们可以将内部 DNS 服务器放入其中,
/etc/netns/vo_none_none/resolv.conf以便所有在网络命名空间中启动的应用程序都能正常工作。连接后再执行此操作有点尴尬(因为之前我们无法访问这些服务器),但由于挂载命名空间是特定于每次ip netns exec调用的,因此脚本无法为我们执行此操作(除非我们在同一个 bash 会话中运行所有内容,并且不再使用ip netns exec)。
然后,我们可以在网络命名空间中以普通用户身份运行应用程序(现在通过工作 VPN,脚本vpn.sh在另一个会话中运行):
$ vopono -v exec -i bar --provider None --protocol None google-chrome-stable
但是,要想通过 VPN 运行所需的一切,还剩下一个问题:Docker。
Docker
像上面的其他应用程序一样简单地运行 Docker 是行不通的 - Docker 套接字是在网络命名空间之外创建的(使用 systemd 启用时),因此我们不会有任何内部连接。
但是,从外部停止 Docker 并尝试简单地运行 Docker 守护程序并在网络命名空间内创建套接字也行不通 - 这是因为ip netns exec创建了挂载命名空间并重新挂载/sys- 所以我们的主机/sys/fs/cgroup将不可见。
这将产生如下错误:
Error: OCI runtime error: runc: runc create failed: no cgroup mount found in mountinfo
(请注意,我认为该错误实际上是由于使用 podman-docker 进行测试而导致的)
我们可以使用以下命令来解决这个问题:
$ (on host) sudo systemctl stop docker && sudo systemctl stop docker.socket
$ (on host) sudo -E unshare -m sh -c 'mount --bind /sys /sys; exec ip netns exec vo_none_none sudo --user youruser --preserve-env bash'
$ (in netns) sudo umount /sys
$ (in netns) sudo dockerd --host=unix:///var/run/docker-netns.sock --data-root=/var/lib/docker-netns
$ (in netns) DOCKER_OPTS="--dns=YOURDNSHERE" DOCKER_HOST=unix:///var/run/docker-netns.sock sudo --user youruser --preserve-env docker ... # your docker command here
这是根据这篇 Unix StackExchange 帖子改编的。诀窍在于使用unshare强制/sys成为 创建的挂载命名空间中的绑定挂载ip netns exec,然后卸载/sys由 创建的内部挂载 ip netns exec- 这样,/sys挂载命名空间内就变成了主机的 绑定挂载/sys。
这只会影响 的这次调用ip netns exec。但是我们可以dockerd在网络命名空间内的同一会话中启动我们的 Docker 命令,因此它们的挂载命名空间是相同的。
请注意,您还可以在中设置 Docker DNS 设置
/etc/netns/vo_none_none/docker/daemon.json。
还要注意,这可以满足我的需求(包括一个使用 Docker 网络连接到辅助容器的容器),但它可能不适用于需要网桥等的更复杂的 Docker 设置(尽管我确信这在技术上是可行的)。
WireGuard MTU 问题
重新启动机器后,我遇到了 WireGuard 连接的一些问题,只有几个页面可以加载,而有些页面根本无法加载 - 例如 GitHub,同时ping运行ping -6完美。
这非常难调试——因为即使wg show显示WireGuard连接运行正常,它仍然无法正常工作。最后,我通过执行不同大小的ping命令找到了问题所在:
$ ping6 -s 1400 fd42:42:42::1
$ ping6 -s 1200 fd42:42:42::1
$ ping6 -s 800 fd42:42:42::1
在这种情况下,第一个失败了,但其他的都成功了。这表明连接问题是由于 WireGuard MTU 设置造成的——较大的数据包被丢弃了。设置较低的 MTU 可以立即解决问题(并在上面的配置中反映出来)。
最大传输单元 (MTU) 是接口能够处理的最大数据包大小。通过在本地 WireGuard 接口上设置较低的 MTU,我们可以指示内核的 IP 堆栈不要创建大于此大小的数据包。这确保了在 WireGuard 加上自身的封装开销后,最终通过互联网发送的 UDP 数据包足够小,从而避免被路径上任何具有较小 MTU 的链路丢弃。
当我们将数据包发送到远程服务器时,它会在途中经过许多路由器(以及海底电缆!)。就像你向国外寄信一样,它不会立即到达收件人,而是必须经过多个处理中心,每个处理中心都会将其发送到下一个合适的中心。每个路由器都有自己的 MTU,并且会丢弃大于该 MTU 的数据包。
你可以把它想象成邮政服务的重量限制。如果你想给国外的朋友寄一个20公斤的包裹,仅仅你当地的邮政服务接受这么重的包裹是不够的,国际航空邮件服务和你朋友当地的邮政服务也必须接受——否则包裹根本寄不上。
因此,如果我们的 MTU 设置得过高,那么当数据包超过路由的最小 MTU(即路由中所有跃点的最小 MTU)时,它们就会被丢弃。这会导致令人困惑的间歇性行为,因为某些数据包(例如 ping 数据包)本身足够小,不会受到 MTU 的影响,但较大的数据包被丢弃会导致连接失败,例如当您尝试使用 HTTPS 连接时。
请注意,对于像 WireGuard 这样的隧道流量来说,这是一个更大的问题,因为我们从额外的封装中添加了约 32 字节的开销(在上面的类比中为大量双重打包),并且也不太可能从中间路由器接收任何路径 MTU 发现 (PMTUD) 消息,这会通知我们 MTU 问题并让我们自动调整它,因为这些互联网控制消息协议 (ICMP) 消息经常被防火墙丢弃。
IPv6 规范中固定的最小 MTU 为 1280,因此这对于 IPv6 上的隧道始终有效。
结论
总而言之,我们讨论了:
- 在具有 IPv4 和 IPv6(直接和 NATed)流量的 VPS 上创建 WireGuard VPN 服务器。
- 使用网络命名空间通过此 WireGuard 接口运行另一个 VPN。
- 使用unshare技巧在该网络命名空间内运行 Docker。
- 使用 WireGuard 时调试 MTU 问题。
远程工作时,互联网连接问题始终是一个风险,幸运的是,在这种情况下,Linux 能够挽救局面,让我免于完全受制于 ISP 修复其配置。
我希望这篇文章对其他人有用(也许将来对我自己也有用!)。它真正展现了 Linux “自己动手修复” 方法的优势。虽然 Mac 凭借其强大的 M4 处理器很吸引人,但我完全不知道如何在 macOS 上管理上述所有问题(而且我上次使用 macOS 的体验也不太好!)。
我强烈推荐 Hetzner 的 VPS,它们价格实惠,全面支持运行 WireGuard 隧道以及几乎所有合法用途(只是不支持端口扫描、流量欺骗或加密货币挖矿)。你永远不知道什么时候会需要它。(请注意,另一个有用的选择是像 AirVPN、ProtonVPN 或 AzireVPN 这样的支持端口转发的 VPN,这样你就可以从自己的家庭服务器转发端口,而无需依赖你的 ISP)。
出于同样的原因,我也考虑入手一台 OpenWRT 路由器。我以前觉得管理自己的路由器是不必要的额外工作(讽刺的是,很多人可能也是这么想的 GNU/Linux),但如果能在路由器端进行更多调试,就能很好地解决这类问题,甚至可以直接在路由器上运行 WireGuard,而无需在每个设备上单独配置。
本文为译文,英文原文地址(可能需要使用魔法访问或付费会员才可观看):
https://jamesmcm.github.io/blog/no-ipv4/