你的 IPv6 VPN 客户端能到达工作负载,工作负载也把应答送回隧道,可是应答始终到不了。人们的第一反应是怪防火墙或隧道,而这两者通常都没错。OpenVPN 分配的地址是只存在于隧道内部的内部网络;除非你告诉 Google Cloud,否则它根本不知道 fd42:90::/64 应该位于某一台特定的虚拟机后面。

必须有两个彼此独立的系统学到这个事实,而其中只有一个是 OpenVPN。VPC 必须先有路由,才能把任何东西送回去;虚拟机则必须被允许转发到达的流量。任何一半出错,症状看起来都一模一样。

地址池是内部网络,不是子网

OpenVPN 可以从你选定的范围分配 IPv6 地址,而这个范围只是隧道两端达成一致的虚构。它不是 Google Cloud 子网,不会出现在 VPC 子网列表里,虚拟机的网络接口也不会获得这些地址。因此,回复客户端的工作负载会把数据包发往一个被网络视为不可路由的地址。

把这个地址池排除在 VPC 已经使用的所有范围之外。也要把它排除在其他隧道背后的范围之外。重叠会把干净的故障变成只在有负载时才出现的间歇性故障。

像 fd42:90::/64 这样的唯一本地地址是适合这个内部网络的选择。该前缀也提醒人们:这套设计天生就是私有的。ULA 地址池并不会让它在公共互联网上成为合法来源。

给 VPC 指定目的地和下一跳

应答需要一条 VPC 路由,其目的地是该地址池,下一跳是 VPN 虚拟机。当前的静态路由文档涵盖了 IPv6 目的范围,并列出哪些下一跳类型符合条件,因此请核对适用于你的拓扑的形式,而不是照抄旧的 IPv4 示例。

下一跳地址必须是真正分配给该实例云端网卡的地址。虚拟机的内部隧道地址并不是这个地址。硬要使用它,就会得到一条安装得很干净、却永远匹配不到任何数据包的路由。在写路由之前,请先读一遍网卡列表。

路由优先级和条目本身一样重要。子网路由比新增的自定义路由更具体,所以从既有子网范围里切出来的地址池,无论自定义路由怎么写,都会输给子网路由。请另选一个独立地址池,然后确认该路由把实例显示为下一跳,并且目的前缀正确:

gcloud compute routes create vpn6-return \
  --network=vpn-vpc \
  --destination-range=fd42:90::/64 \
  --next-hop-instance=openvpn-vm \
  --next-hop-instance-zone=europe-west1-b

转发必须启用两次

路由把数据包送到实例。它并不能让实例愿意转发并非发给自己的东西,而这项许可存在于两个层面。

云端需要启用实例的转发能力,也就是允许虚拟机接收并非自己拥有的地址的流量并把它转出去的设置。客户机端需要自己的 IPv6 转发,因为内核在数据包到达后会单独做一次判断。两者都打开之后,才可能有一个应答穿过隧道。

sysctl -w net.ipv6.conf.all.forwarding=1

接着,防火墙规则必须在两个方向上都放行这个内部源范围。Google Cloud 评估的是解封装后的数据包,所以只覆盖 VPC 子网的规则会在隧道已经成功之后把地址池的流量丢掉。请把该地址池加入入站规则的源范围,并确认没有任何自定义出站策略挡住应答。隐式放行规则覆盖默认情况,手写的出站规则则不会。

告诉 OpenVPN 谁拥有什么

只有当 OpenVPN 知道某个客户端拥有该目的地时,它才会转发给这个客户端。来自地址池的客户端地址由定义该地址池的 服务器指令处理,但客户端背后的子网是另一种安排,需要为该客户端显式写入一条所有权条目。

把两者混为一谈,就会得到一条能 ping 通服务器、却什么也到不了的隧道。先想清楚你在搭哪一种,然后写上对应的条目,而不是再加一行 push 然后指望它生效。

把互联网出口排除在这个设计之外

私有访问和互联网访问是两套计划。一旦路由和转发对齐,ULA 地址池就能通过隧道访问 VPC 工作负载;而同一个地址池并没有经由 VPC 通往公共互联网的路径,因为云端网关不会把任意的隧道来源当成自己的地址。

如果客户端还需要访问互联网,请为它单独规划受支持的地址与路由方案,让它与私有地址池相互独立,并分别测试这两条路径。一条路径能通,并不能说明另一条;把两者混在一起的设计,失败时看起来就像隧道坏了。

相关文章