客户端拨号连接到 Compute Engine VM 上的OpenVPN服务器,状态窗口显示已连接,但对 VPC 内某台主机的 ping 仍然不通。直觉反应是再加一条 push 指令,而这从来解决不了问题。Compute Engine 依据自己的路由表和 VM 网卡实际持有的地址来决定数据包的去向,因此 VM 并不拥有的隧道地址池,就是一个谁也投递不到的地址池。

顺着失败的数据包去查,而不是盯着配置文件,缺失的部分就会依次显现。

从那个夭折的数据包查起

SYN 离开客户端、进入隧道,作为发往 VPC 内某个目的地的封装包到达 VM。OpenVPN 将其解封装,并尝试从 eth0 转发出去,在这里网络结构首先会问一个问题:这个实例是否拥有该目的地址。从别人的子网里切出来的隧道地址池无法通过这个问题,数据包在查询任何路由或防火墙规则之前就被丢弃了。

所以从客户端到 VPC 这个方向有两个彼此独立的要求,而它们都不是 push 指令。VM 需要一个覆盖隧道地址池的别名网段,正是这一点让它成为这些地址的合法持有者。VPC 需要一条路由,为每个受保护的网段把该 VM 指定为下一跳。缺任何一个,数据包都会死在不同的地方,而客户端看到的是一样的沉默。

按这个顺序去读故障,因为从客户端一侧看,这两种情况一模一样。隧道显示已连接却什么都应答不了,这是拓扑问题的特征,而不是加密算法有问题。

实例是否拥有隧道地址池

给 VM 一个位于其自身子网内的辅助网段,然后让 OpenVPN 恰好从该网段分配地址。对于主地址为 10.156.0.10/20 的实例,一个 /28 别名能让地址池与其他实例已占用的地址分开:

gcloud compute instances network-interfaces update openvpn-vm \
  --zone=europe-west1-b \
  --aliases=eth0:10.156.1.0/28

随后 server 指令必须描述同一个地址块。它的第一个地址成为隧道网关,其余地址分配给客户端。这里用两个数字描述一个地址块,一个网络号和一个子网掩码,这就是 OpenVPN 仍然期望的地址池语法。

server 10.156.1.0 255.255.255.240

确认该地址真的落到了网卡上,而不只是写在 OpenVPN 配置里。网络结构转发的是 NIC 报告的地址,而不是守护进程以为的地址。一条命令就能确认:

ip -4 addr show dev eth0

一个从未到达 eth0 的辅助地址,或者一个与其它子网重叠的地址池,后来会表现为间歇性丢包,而不是启动报错。守护进程正常启动,客户端也能连上,只有部分目的地不正常。让别名网段保持较小,这样地址池在路由表里容易辨认。绝不要从另一个实例已在使用的子网里切出它。

给返回流量一个下一跳

来自 VPC 主机的回复发往隧道地址池中的某个客户端地址,而该地址不属于 VPC 所知的任何子网。没有针对该地址池的路由,回复就会在第一个无匹配的跳点停下,而请求在远端很可能已经成功了。

gcloud compute routes create openvpn-return \
  --network=prod-vpc \
  --destination-range=192.168.127.0/24 \
  --next-hop-instance=openvpn-vm \
  --next-hop-instance-zone=europe-west1-b \
  --priority=800

任何位于客户端之后而不是 VPC 内部的网段,都需要同一条路由。这些网段还需要在该客户端的 ccd 文件里有一条 iroute 条目。该条目是服务器端的记录:它告诉服务器哪个客户端拥有该网段,它本身不会在客户端上安装任何东西。

push 列表留给通信的客户端一侧,因为它只会给发起连接的机器添加路由。为 VPC 子网写一条 push 行、为回地址池写一条 VPC 路由,是同一条路径的两半,任何一半单独存在都只会造出一条能发出请求却收不到应答的隧道。

防火墙、转发以及中间的那些跳

VPC 防火墙检查的是解封装之后的包,因此入站规则必须放行隧道地址池,以及任何以这种方式到达 VM 的站点网段。默认拒绝的策略在这里会静默失败:数据包在解封装之后被丢弃,而 OpenVPN 日志显示的却是一个健康的会话。

在任何做转发而非终结的 Linux 主机上,检查 net.ipv4.ip_forward 是否启用。归属规则在那里同样适用:一台为它并不持有的子网做转发的机器,会像那台 VM 一样丢弃数据包。先读这个值,再去怪隧道。

当恰好只有一个网段失败时

部分失败是最容易判读的情况,因为路径上其它环节都已经被证明过了。先看路由列表,读出本应覆盖该网段那条条目的目的范围和下一跳,然后看实例描述,把它的别名网段与服务器下发的地址池做比较。

gcloud compute routes list --filter=destination-range~10.164.0.0
 gcloud compute instances describe openvpn-vm --format=get(networkInterfaces[0].aliasIpRanges)

缺少路由表现为该网段没有下一跳,而缺少别名表现为数据包在网卡处被丢弃;从客户端看两者很像,在这两份输出里却完全不同。如果两者都在,剩下的嫌疑就是声明该网段的那个客户端的 ccd iroute。

把四个值写在隧道旁边:别名网段、服务器地址池、推送的网段、VPC 路由。别名过期会让地址池里每个地址都失效,路由过期会断掉一个方向,缺少 push 会让客户端看不到该网段,iroute 写错则会把该网段发给错误的对端。下一个要添加子网的人需要这四样东西集中在一处,而这张便条比熬一个晚上便宜。

相关文章