GCE 上的 OpenVPN:推送路由之前先添加别名 IP
推送一条路由并不会让 VM 成为下一跳。先添加别名 IP 网段、与服务器地址池保持一致,并创建 VPC 返回路由,然后再测试客户端。
客户端拨号连接到 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 写错则会把该网段发给错误的对端。下一个要添加子网的人需要这四样东西集中在一处,而这张便条比熬一个晚上便宜。