|
| 1 | +# 11 IPv6 反向代理 VIP on-link 误判修复 |
| 2 | + |
| 3 | +> 角色:spec-writer(编码完成后的文档同步)。本报告记录「f-stack 版 nginx 做 IPv6 反向代理时,主动 connect 上游后端连不通(504)」问题的**根因定位与修复落地**。 |
| 4 | +> 所有行号经读码核实(以代码为准);运行时诊断以 leader/多 agent 交叉验证的 pcap 铁证为准;真机端到端验证情况见 §7。 |
| 5 | +> 涉及文件:`lib/ff_veth.c`(唯一改动文件)。参考代码:`freebsd/netinet6/in6.c`(未改)。 |
| 6 | +
|
| 7 | +--- |
| 8 | + |
| 9 | +## 1. 问题现象 |
| 10 | + |
| 11 | +f-stack 版 nginx(`nginx_fstack`)配置为 IPv6 反向代理: |
| 12 | + |
| 13 | +- 反代服务端 `listen [::]:80`:客户端能正常访问反代、能收到完整请求; |
| 14 | +- 但反代主动 `connect` 上游后端时,**后端抓不到任何 v6 反代请求**,nginx 报 `504 Gateway Timeout`。 |
| 15 | + |
| 16 | +复现环境:CVM(virtio 网卡)与 mlx5 100G 物理机(无安全组)**均可复现**,与网卡型号无关。 |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## 2. 运行时诊断(pcap 铁证) |
| 21 | + |
| 22 | +抓包文件:`/data/workspace/ipv6_proxy.pcap`(mlx5 物理机现场)。 |
| 23 | + |
| 24 | +| 角色 | 地址 | |
| 25 | +|------|------| |
| 26 | +| 反代服务端 VIP | `2402:4e00:1840::17` | |
| 27 | +| 上游后端 | `2402:4e00:1840::18` | |
| 28 | +| 客户端 | `2402:4e00:1701:c000::1` | |
| 29 | + |
| 30 | +`f-stack.conf` 关键配置: |
| 31 | + |
| 32 | +| 项 | 值 | 网段 | |
| 33 | +|----|----|------| |
| 34 | +| `addr6`(主地址) | `2402:4e00:2e80:1030::b` | 主网段 `2e80:1030` | |
| 35 | +| `prefix_len` | `64` | — | |
| 36 | +| `gateway6` | `2402:4e00:2e80:1030::1` | 主网段 `2e80:1030` | |
| 37 | +| `vip_addr6` | `2402:4e00:1840::17` | VIP 网段 `1840`(与主地址**不同网段**) | |
| 38 | +| `vip_prefix_len` | `64` | — | |
| 39 | + |
| 40 | +pcap 关键观测: |
| 41 | + |
| 42 | +1. **入向完整正常**:客户端连 VIP `1840::17`,SYN / SYN-ACK / GET / `504` 一应俱全,说明监听与入向收发无问题。 |
| 43 | +2. **出向异常**:f-stack 对后端 `1840::18` 只发了 **1 个直连 NS**(`who has 1840::18`,solicited-node 组播),**无任何 NA 响应**;此后**零 TCP SYN** 发往后端 → connect 超时 → `504`。 |
| 44 | +3. **对照**:同一 pcap 中,NS 询问网关 `2e80:1030::1` 能得到 NA(正常)。 |
| 45 | + |
| 46 | +**结论**:f-stack 把「去往后端 `1840::18`」误判为**直连(on-link)**,于是对目的地址本身发直连 NS 做地址解析;而后端实际在网关后面、不在本地 L2 → 收不到 NA → 邻居表项停在 `INCOMPLETE` → SYN 排队发不出。 |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## 3. 根因分析 |
| 51 | + |
| 52 | +### 3.1 调用链 |
| 53 | + |
| 54 | +1. `lib/ff_veth.c` → `ff_veth_setvaddr6()` 给 VIP 添加地址时,`ifra_prefixmask` 使用了 `sc->prefix_length`(**主前缀 = 64**); |
| 55 | +2. → `freebsd/netinet6/in6.c` → `in6_addifaddr()` 处理 `SIOCAIFADDR_IN6`; |
| 56 | +3. → 由于前缀长度非 128,走入前缀路由安装分支,`ndpr_raf_onlink = 1`,为 VIP 网段安装 `1840::/64` **on-link 前缀路由**; |
| 57 | +4. → 此后去往同网段的后端 `1840::18`,最长前缀匹配命中 `1840::/64` on-link 前缀 → 被判为直连 → 发直连 NS → 无 NA → 连不通。 |
| 58 | + |
| 59 | +### 3.2 in6.c 关键行号(未改,仅引用核实) |
| 60 | + |
| 61 | +`freebsd/netinet6/in6.c` `in6_addifaddr()`(L1230 起): |
| 62 | + |
| 63 | +| 落点 | 行号 | 说明 | |
| 64 | +|------|------|------| |
| 65 | +| `pr0.ndpr_plen = in6_mask2len(&ifra->ifra_prefixmask...)` | L1298-1299 | 由传入的 prefixmask 换算前缀长度 | |
| 66 | +| `if (pr0.ndpr_plen == 128) { goto aifaddr_out; }` | **L1300-1303** | **仅当 /128 时跳过前缀路由安装**("we don't need to install a host route") | |
| 67 | +| `pr0.ndpr_raf_onlink = 1;` | **L1316** | 非 /128 分支:把该前缀标记为 on-link | |
| 68 | +| `pfxlist_onlink_check();` | L1361 | 前缀 on-link 生效 | |
| 69 | +| `aifaddr_out:` | L1363 | /128 直接跳到此,**不安装 on-link 前缀路由** | |
| 70 | + |
| 71 | +即:只有 `/128` 才会跳过 on-link 前缀路由安装;`/64` 的 VIP 必然装上 `1840::/64` on-link 前缀路由。 |
| 72 | + |
| 73 | +### 3.3 修复前的代码(错误) |
| 74 | + |
| 75 | +修复前 `ff_veth_setvaddr6()` 用主前缀 `sc->prefix_length` 构造 prefixmask(与 13.0-baseline 同款逻辑): |
| 76 | + |
| 77 | +``` |
| 78 | +ifr6.ifra_prefixmask.sin6_len = sizeof ifr6.ifra_prefixmask; |
| 79 | +memset(&ifr6.ifra_prefixmask.sin6_addr, 0xff, sc->prefix_length / 8); |
| 80 | +uint8_t mask_size_mod = sc->prefix_length % 8; |
| 81 | +if (mask_size_mod) { |
| 82 | + ifr6.ifra_prefixmask.sin6_addr.__u6_addr.__u6_addr8[sc->prefix_length / 8] = |
| 83 | + ((1 << mask_size_mod) - 1) << (8 - mask_size_mod); |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +两处缺陷: |
| 88 | + |
| 89 | +1. **主缺陷**:VIP 以 `/64` 加入 → 装 `1840::/64` on-link 前缀路由 → 同网段后端被误判直连; |
| 90 | +2. **次缺陷**:即便要用配置前缀,这里也误用了主地址前缀 `sc->prefix_length`,而非结构体中本就为 VIP 预留的 `sc->vip_prefix_length`(见 `ff_veth.c` L88 字段定义、L145/L201 赋值)。 |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## 4. 修复方案 |
| 95 | + |
| 96 | +将 VIP 固定以 **/128 host 地址**方式添加:prefixmask 固定全 1(`memset 0xff, 16`),删除 `mask_size_mod` 取模分支。 |
| 97 | + |
| 98 | +`lib/ff_veth.c` `ff_veth_setvaddr6()`(L860-898)修复后: |
| 99 | + |
| 100 | +``` |
| 101 | +/* VIP as /128 host addr: avoid installing an on-link prefix route, |
| 102 | + * so traffic to other addrs in the VIP subnet goes via the gateway. */ |
| 103 | +ifr6.ifra_prefixmask.sin6_len = sizeof ifr6.ifra_prefixmask; |
| 104 | +memset(&ifr6.ifra_prefixmask.sin6_addr, 0xff, 16); /* L879-880 */ |
| 105 | +``` |
| 106 | + |
| 107 | +效果: |
| 108 | + |
| 109 | +- VIP 以 `/128` 加入 → 命中 `in6.c` L1300-1303 `/128` 特判 → **不安装 on-link 前缀路由**; |
| 110 | +- 去往 VIP 同网段的其它地址(含后端 `1840::18`)在 Prefix List 上**无匹配 on-link 前缀** → 走**默认网关** → 对网关做邻居发现(网关在本地 L2、能应答 NA)→ SYN 正常送出。 |
| 111 | + |
| 112 | +此做法与 FreeBSD 原生 `ifconfig <if> inet6 <addr> prefixlen 128 alias` 惯例一致:给接口挂一个 host 地址而不引入该网段的 on-link 语义。 |
| 113 | + |
| 114 | +--- |
| 115 | + |
| 116 | +## 5. diff 摘要(`lib/ff_veth.c` `ff_veth_setvaddr6`) |
| 117 | + |
| 118 | +```diff |
| 119 | +- ifr6.ifra_prefixmask.sin6_len = sizeof ifr6.ifra_prefixmask; |
| 120 | +- memset(&ifr6.ifra_prefixmask.sin6_addr, 0xff, sc->prefix_length / 8); |
| 121 | +- uint8_t mask_size_mod = sc->prefix_length % 8; |
| 122 | +- if (mask_size_mod) |
| 123 | +- { |
| 124 | +- ifr6.ifra_prefixmask.sin6_addr.__u6_addr.__u6_addr8[sc->prefix_length / 8] = \ |
| 125 | +- ((1 << mask_size_mod) - 1) << (8 - mask_size_mod); |
| 126 | +- } |
| 127 | ++ /* VIP as /128 host addr: avoid installing an on-link prefix route, |
| 128 | ++ * so traffic to other addrs in the VIP subnet goes via the gateway. */ |
| 129 | ++ ifr6.ifra_prefixmask.sin6_len = sizeof ifr6.ifra_prefixmask; |
| 130 | ++ memset(&ifr6.ifra_prefixmask.sin6_addr, 0xff, 16); |
| 131 | +``` |
| 132 | + |
| 133 | +改动范围:仅 `ff_veth_setvaddr6()` 内 prefixmask 构造段;未触碰主地址 `ff_veth_setaddr6()`(其行为不变,主地址仍按配置前缀加,属预期)。 |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## 6. 13.0 对照(非 13→15 回归) |
| 138 | + |
| 139 | +`f-stack-13.0-baseline/lib/ff_veth.c` 的 `ff_veth_setvaddr6()`(对应 L789-796 附近)同样使用 `sc->prefix_length` + `mask_size_mod` 构造 VIP 的 prefixmask,**与 15.0 修复前完全一致**。 |
| 140 | + |
| 141 | +因此该问题**不是 13.0→15.0 升级引入的回归**,而是 f-stack **长期存在的设计缺陷**:仅在「VIP 与主地址不同网段」这一特定拓扑下才会暴露(同网段时后端本就 on-link,误判恰好不产生错误后果,故长期未被发现)。 |
| 142 | + |
| 143 | +--- |
| 144 | + |
| 145 | +## 7. 入向不受影响分析 |
| 146 | + |
| 147 | +有人可能担心 VIP 改 `/128` 后入向(客户端 → VIP)受影响。分析确认**不受影响**: |
| 148 | + |
| 149 | +- 入向依赖的是 VIP 地址本身在本机可达(loopback host route),该 host route 由 `in6.c` 的 `in6_notify_ifa()` 在地址生效时**无条件**为该地址安装 `/128` loopback 路由,**不依赖 prefixlen**; |
| 150 | +- `/128` 与 `/64` 两种加法都会生成该 loopback host route,故 VIP 仍正常接收发往自己的报文; |
| 151 | +- pcap 中入向 SYN/SYN-ACK/GET/504 全程正常,运行时亦印证入向不受影响。 |
| 152 | + |
| 153 | +唯一改变的是「VIP 网段是否 on-link」这一出向语义,正是本次要修的点。 |
| 154 | + |
| 155 | +--- |
| 156 | + |
| 157 | +## 8. 验证情况 |
| 158 | + |
| 159 | +| 环节 | 结果 | |
| 160 | +|------|------| |
| 161 | +| 编译 `libfstack.a` | OK | |
| 162 | +| `make install` | OK | |
| 163 | +| nginx 重链 | OK(二进制 md5 变化,确认含本修复) | |
| 164 | +| 代码正确性 | 独立 gatekeeper 门禁 + fix-verifier 交叉核实 + leader 独立读 `in6.c` 坐实(6 条核查全过) | |
| 165 | +| 本机 CVM 真机端到端 | **受阻**:(1) CVM 有安全组丢主动外连(需用户自查);(2) 重启过程中 virtio 网卡退回内核驱动,本环境无法干净复现 VIP6 场景 | |
| 166 | +| mlx5 物理机真机端到端 | **待用户执行**(无安全组、可干净复现) | |
| 167 | + |
| 168 | +真机验证建议步骤(mlx5 物理机): |
| 169 | + |
| 170 | +1. 部署含修复的 `nginx_fstack`,配置同 §2; |
| 171 | +2. 客户端访问反代 VIP,观察 nginx 是否正常回上游响应(不再 504); |
| 172 | +3. 抓包确认:出向对后端 `1840::18` **不再发直连 NS**,而是对**网关** `2e80:1030::1` 做邻居发现并将 SYN 送出; |
| 173 | +4. `netstat -rn -f inet6` 确认**不再存在** `1840::/64` 的 on-link 前缀路由(仅 VIP `/128` host 路由 + 默认路由)。 |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## 9. 外网印证 |
| 178 | + |
| 179 | +真实可查来源,逐条印证「/128 host 地址不建 on-link 前缀路由、IPv6 用 on-link 前缀列表而非纯前缀比较判定下一跳、无匹配 on-link 前缀时走默认网关」: |
| 180 | + |
| 181 | +| # | 来源 | URL | 结论与本修复的关系 | |
| 182 | +|---|------|-----|------| |
| 183 | +| 1 | FreeBSD `ifconfig(8)` man page | https://man.freebsd.org/cgi/man.cgi?query=ifconfig | inet6 支持 `::1/128` 斜杠记法/`prefixlen` 指定前缀长度;`/128` 是原生用来给接口挂单个 host 地址的标准写法,印证本修复用 `/128 alias` 属官方惯例。 | |
| 184 | +| 2 | Packet Pushers《On Link in IPv6》(据 RFC 5942) | https://packetpushers.net/blog/on-link-in-ipv6/ | IPv6 不靠「前缀+前缀长度相同就算同链路」判定,而是维护 on-link 前缀列表;**一个 /128 地址/主机路由本身不会自动创建 on-link 前缀**,无匹配 on-link 前缀时流量发往默认网关。直接支持「VIP 改 /128 后同网段后端走网关」。 | |
| 185 | +| 3 | RFC 4861 §5.2 / §2.1(IPv6 邻居发现) | https://www.rfc-editor.org/rfc/rfc4861 | 发送方对 Prefix List 做**最长前缀匹配**判定 on/off-link:命中前缀→下一跳=目的地址本身(对目的发 NS 解析);未命中→下一跳=默认路由器(对网关发 NS)。印证修复前因 `1840::/64` on-link 前缀命中而误对后端发直连 NS 的机理。 | |
| 186 | + |
| 187 | +--- |
| 188 | + |
| 189 | +## 10. 结论 |
| 190 | + |
| 191 | +- **根因**:`ff_veth_setvaddr6()` 用主前缀 `/64` 加 VIP,导致 `in6_addifaddr()` 安装 `1840::/64` on-link 前缀路由,使跨网段后端被误判直连、发直连 NS 无 NA、SYN 发不出。 |
| 192 | +- **修复**:VIP 固定 `/128` host 加入,避免 on-link 前缀路由,跨网段流量走默认网关,与 FreeBSD 原生 `prefixlen 128 alias` 惯例一致。 |
| 193 | +- **影响面**:仅出向 VIP 网段的 on-link 语义;入向依赖 loopback `/128` host route 不受影响。 |
| 194 | +- **性质**:13.0/15.0 同款长期设计缺陷,非升级回归。 |
| 195 | +- **状态**:代码修复已落地、编译/门禁/交叉核实通过;mlx5 物理机真机端到端验证待用户执行。 |
0 commit comments