Linux 上的 EHOSTUNREACH 与 OpenVPN:哪一跳失败了?
EHOSTUNREACH 可能来自显式的 unreachable 路由、已死的邻居或远端回报。先读所选路由与下一跳,再动加密。
EHOSTUNREACH 往往和远端故障一样,是本地做出的判决。内核可能因为路由声明目的地不可达、因为以太网下一跳从未应答,或因为路径更远处送来 ICMP host-unreachable 回报而拒收报文。每种来源位于不同的表或缓存,也各有不同的修复位置。在触碰密码套件或重装隧道之前,先读所选路由与下一跳状态。
先选对客户端真正使用的地址与命名空间
先把目的地和产生错误的网络命名空间固定下来。VPN 客户端可能运行在自己的命名空间里,而从默认命名空间查询会看到另一张路由表,策略规则也不同。
查询必须使用与出错应用相同的源地址、mark 和表上下文,否则它回答的可能是一个没人问过的问题。以一个示例 IPv4 对端为例,只读检查如下:
ip -4 route get 198.51.100.25
ip -4 neigh show 198.51.100.1
ip -4 route show table all | grep -w unreachable
来自管理 shell 的结果只能证明那个 shell 会怎么做。如果客户端设置了防火墙 mark 或绑定到 VRF,就带上对应的 mark 或在 VRF 内重跑查询,并比较所选路由与下一跳。首先要确认的地址是配置文件里的 peer 地址,而不是用户所说的集群或服务。
显式的 unreachable 路由会在本地拒收报文
类型为 unreachable 的路由是刻意的本地答复。它让内核丢弃报文并向发送方返回 EHOSTUNREACH,所以错误瞬间出现,且没有任何流量离开本机。有人或某段自动化是故意把这条路由放在那里的。
路由类型不可互换。blackhole 丢弃报文而不向发送方报错,prohibit 返回 EACCES,unreachable 返回 EHOSTUNREACH。因此,在重连期间安装 sink 路由的 VPN 辅助程序会造成一阵 host-unreachable 错误,隧道建立后这些错误就消失;而 blackhole 路由则会改为制造静默超时。
在怪罪远端之前,先把所有表按类型搜一遍。选择表的规则与表内的路由同样重要,而加进主表的路由,帮不上策略规则把流量送去别处的客户端。
邻居已死会在以太网上产生同样的错误
当所选路由使用 on-link 网关时,投递需要一个已解析的链路层地址。内核发出 ARP 或 IPv6 邻居请求,若没有回应,报文就无法成帧。此时即使路由本身有效,发送也会以 EHOSTUNREACH 失败。
邻居表说明了阶段。INCOMPLETE 表示解析仍在进行,FAILED 表示最后一次尝试没有得到回应,REACHABLE 或 STALE 表示内核已有可用地址。VPN 网关的 FAILED 条目是本地投递问题,不是隧道另一侧的证据。
要读的是真正下一跳的条目,而不是远端互联网地址的条目。远端地址通常经路由器到达,所以不会有自己的 ARP 记录;去要这样一条记录属于类别错误,会让人去找一条本就不该存在的缺失条目。
远端回报与异步 UDP 错误
EHOSTUNREACH 也可能来自外部。无法转发报文的路由器可能发出 ICMP host unreachable,内核把该回报交给拥有该流的 socket。这时路由和邻居都没问题,故障属于本地网络之外的一跳。
UDP 让归因更难。后续 send 或 receive 调用返回的错误,可能对应更早的数据报,因此失败调用的地址和时间未必与触发回报的报文一致。已连接的 UDP socket 只会收到一次该错误,而未连接的 socket 可能永远看不到。
所以时间也属于证据。记下错误出现的时刻、socket 用的是哪个 peer 地址,以及隧道是否刚刚重连。路由变更后立刻出现的一阵错误指向本地表;稳定流跑了几分钟后才出现的单个错误,则指向路径回报。
改动加密之前,先把层次分开
三个问题可以给错误分类。内核是不是选了一条拒收目的地的路由?是不是选了有效路由却无法解析下一跳?还是回报来自本地网络之外?只有第三种情况才可能牵涉隧道远端,即便那时也不牵涉加密。
认证与协商问题会在传输抵达 peer 之后以各自的错误出现。更换密码套件或重装客户端都修不好本地的 unreachable 路由,还会毁掉你查找真正原因所需的路径与邻居证据。
在第一次改动之前,记录目的地、命名空间、所选表、下一跳、邻居状态和错误时间。修复后用同样的查询重跑;如今指向隧道的路由和如今能解析的邻居条目,比对着无关地址 ping 成功更有说服力。