OpenCode 更新卡住?Bun IPv6 问题分析

分析 Bun 运行时在 macOS 上的 IPv6 连接问题,涵盖 2023 年至今的 issue 与修复时间线、根因分析和当前 workaround。

最后修改于:

OpenCode 更新卡住?Bun IPv6 问题分析

问题现象

OpenCode 启动时会检查全局包的更新,触发 bun install -g opencode-ai@latest。在某次更新中,该命令在 “Resolving dependencies” 阶段卡死。开启 --verbose 后的输出:

1
2
3
4
5
bun add v1.3.14 (0d9b296a)
Resolving dependencies
> HTTP/1.1 GET https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.15.5.tgz
< 200 OK
< Content-Length: 35934948

HTTP 响应头已返回(200 OK,Content-Length 35MB),但 body 数据始终未能完整接收。同一 URL 使用 curl 下载正常,约 20 秒完成 35MB。

此问题在 OpenCode 社区中并非个例。自 2025 年 11 月起已有多位用户报告相同症状:OpenCode 启动时 bun add 卡死,程序无响应(#4682)。有用户排查后发现 bun 在收到 HTTP 304 缓存响应后陷入等待,也有用户直接将根因指向 IPv6——“disable ipv6 or add –no-cache when adding bun appears to be the simplest solution”。该 issue 有 28 条讨论,最终因 90 天无活动被 bot 关闭,而非代码修复。

排查过程

网络层

通过 lsof -i -nP 查看 bun 进程的 TCP 连接(-n 禁用 DNS 反查,-P 不转换端口名):

1
2
# lsof -i -nP | grep -i bun
bun  18653  zmz  16u  IPv6  TCP [2408:xxxx:...]:54412->[2606:4700::6810:422]:443 (ESTABLISHED)

连接走的是 IPv6,目标地址 2606:4700::6810:422 属于 Cloudflare。该 IPv6 路径质量较差:

1
2
3
4
$ traceroute6 2606:4700::6810:422
...
10  2400:cb00:447:3::  261ms
12  2400:cb00:1164:1024::ac40:d86d  334ms

RTT 约 334ms。对比 curl 的 -4-6 强制模式:

1
2
3
4
5
6
7
$ curl -4 -o /dev/null -w "speed=%{speed_download}" \
  https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.15.5.tgz
speed=1630440  # 1.6 MB/s

$ curl -6 -o /dev/null -w "speed=%{speed_download}" --max-time 30 \
  https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.15.5.tgz
speed=56491   # 56 KB/s,30 秒后超时

IPv4 路径 1.6 MB/s,IPv6 路径仅 56 KB/s,差距约 30 倍。

进程层

ps aux | grep opencode 显示有多个 OpenCode 实例和 bun install 进程共存:

1
2
3
4
5
zmz  14284  ... opencode     # 17 天前启动,CPU 时间 83 小时
zmz  95041  ... opencode     # 5 天前启动
zmz  90671  ... opencode     # 当前会话
zmz  98507  ... bun install  # opencode 子进程,卡在下载
zmz  99389  ... bun install  # 孤儿进程(PPID=1),同样卡住

两个 bun install 进程同时竞争 /Users/zmz/.bun/install/global/bun.lock,而各自的 IPv6 连接均在低速下载。即使某一进程下载完成,也无法写入 lockfile——锁被另一个进程持有。

环境层

当前网络环境(macOS, Wi-Fi):

1
2
3
$ networksetup -getinfo Wi-Fi
IPv6: Automatic
IPv6 IP address: 2408:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx  # 运营商分配的全局 IPv6,实际地址已脱敏

DNS 解析 registry.npmjs.org 同时返回 AAAA(IPv6)和 A(IPv4)记录。Bun 按 RFC 6724 地址选择规则优先走 IPv6,落入慢速路径。

关闭 Wi-Fi 的 IPv6 后,bun install 成功:

1
2
3
$ networksetup -setv6off Wi-Fi
$ bun install -g opencode-ai@latest
# 下载完成,安装成功

根因分析

问题链涉及三个层面:

  1. IPv6 路由质量差:运营商分配的 IPv6 到达 Cloudflare 的路由延迟高(334ms RTT),吞吐量低(56 KB/s vs 1.6 MB/s for IPv4)。

  2. Bun 的 IPv6 fallback 存在盲区:即使 Happy Eyeballs 已实现(v1.1.9),它只作用于 TCP 连接建立阶段——同时发起 IPv6 和 IPv4 握手,先成功者胜出。但在本例中,IPv6 连接成功建立(ESTABLISHED,HTTP 200 响应头已返回),Happy Eyeballs 判定 IPv6 “胜出"后不再干预。问题出在后续的 body 传输阶段:连接虽然建立,但吞吐量仅 56 KB/s,远低于 IPv4 的 1.6 MB/s。Happy Eyeballs 不会因传输慢而切换——它只管连接,不管传输速度。#27337 加上的 10 秒超时同样只覆盖连接握手阶段,不覆盖已建立连接的数据传输。curl 在类似场景下可通过 TCP 重传超时及时发现链路劣化并重试。

  3. 多进程锁竞争放大问题:多个 OpenCode 实例同时触发 bun install,文件锁竞争使得即使下载接近完成也无法继续。

并非所有环境都会遇到此问题。触发条件:

  • 系统启用 IPv6
  • 该 IPv6 路径到目标服务器(此处为 Cloudflare 托管的 npm registry)质量差,延迟高或吞吐低(取决于 ISP 路由)
  • Bun 的 Happy Eyeballs 未能在传输阶段切换回 IPv4

Bun IPv6 相关 Issue 与修复时间线

此问题在 Bun 项目中有长期跟踪记录。以下是主要 Issue 和 PR 的时间线:

timeline title Bun IPv6 连接问题相关 Issue 与修复 2023年8月 : Issue #4066: bun install 下载极慢,关 IPv6 可解 2023年9月 : Issue #4938/#5548: 更多用户报告相同症状 2024年1月 : PR #8295: 修复 Bun.connect(localhost) IPv6 偏好 2024年5月 : Issue #10731: Jarred-Sumner 提出双步修复计划 : PR #11206: Happy Eyeballs 实现合并 : Bun v1.1.9 发布,包含 Happy Eyeballs 2024年6月 : PR #12223: getaddrinfo 修复,优化 IPv4-only/IPv6-only 网络 2025年12月: Issue #25619: VPN(Cisco AnyConnect)场景回归 2026年1月 : PR #26012: DNS 排序修复,检测全局 IPv6 源地址 2026年2月 : PR #27337: 连接超时机制(10 秒),超时后 fallback 2026年3月 : Issue #28405: Tailscale ULA IPv6 地址导致回退失效 2026年4月 : Issue #28804: macOS 上 fetch() 返回 TLS 证书验证错误 2026年5月 : 实测 bun 1.3.14 在特定 IPv6 路径下仍会卡死

各阶段详细情况

2023.08-09: 问题首次被报告

Issue #4066 是第一个大规模报告,描述了 bun install 下载卡死。社区确认与 IPv6 相关,并提出三个 workaround:

  1. 禁用 IPv6(Linux: sysctl net.ipv6.conf.all.disable_ipv6=1,macOS: 系统设置中关闭)
  2. 修改 /etc/gai.conf(Linux),取消注释 precedence ::ffff:0:0/96 100,使 IPv4 优先
  3. 更换 DNS 服务器(部分用户反映从 Cloudflare DNS 切换到其他 DNS 有改善)

Jarred-Sumner 在 #4066 中确认:“This is likely a bug with our dns code.”

2024.01-06: Happy Eyeballs 实现

Jarred-Sumner 在 #10731 中提出了两步修复方案:

  1. 非阻塞 DNS 解析:macOS 用 libinfo,Windows 用 uv_getaddrinfo,Linux 用线程池,避免 DNS 阻塞事件循环
  2. Happy Eyeballs v2(RFC 8305:同时发起 IPv6 和 IPv4 连接,选择最先成功者

PR #11206 实现了该方案,在 Bun v1.1.9 中发布。Changelog 说明:

“If Bun receives multiple IP addresses for a single hostname, it will attempt to simultaneously connect to each address, then select the first successful connection.”

需要注意的是,Happy Eyeballs 的覆盖范围仅限 TCP 握手阶段:一旦某个地址的连接成功建立,后续数据传输不再受 Happy Eyeballs 管辖。这意味着如果 IPv6 连接成功但传输缓慢(如本文的 56 KB/s 场景),Happy Eyeballs 不会自动切换回 IPv4。

2025.12-2026.03: 持续回归

Happy Eyeballs 实现后,以下场景仍存在问题:

VPN 虚拟接口 (Issue #25619):Cisco AnyConnect 创建的虚拟接口仅分配 link-local IPv6 地址(fe80::)。link-local 地址无法路由到公网,但 Bun 仍尝试通过它连接,且连接失败后未 fallback。PR #26012 修复:DNS 排序前检测系统是否存在全局 IPv6 源地址(2000::/3),不存在则优先 IPv4。

连接超时缺失 (PR #27337):Bun 底层网络库 uSockets 的 socket timeout 默认值为 255(表示禁用)。当 IPv6 SYN 包被中间路由器丢弃(无 RST 返回)时,连接无限等待,Happy Eyeballs 的 fallback 无法触发。PR #27337 加上 10 秒连接超时,超时后 fallback 到下一个地址。

Tailscale ULA 地址 (Issue #28405):Tailscale 分配 ULA IPv6 地址(fdaa::)。ULA 地址理论可路由,但实际在很多网络环境中不可用,Bun 仍然优先尝试 IPv6。

2026.04: TLS 证书验证问题

Issue #28804(重复 #23735)报告了 macOS 上的 TLS 证书验证失败:

1
2
Error: UNKNOWN_CERTIFICATE_VERIFICATION_ERROR
path: "https://oauth2.googleapis.com/token"

同一 URL 在 Node、curl、Python 下可正常连接。

根本原因:Bun 使用内置 BoringSSL 进行 TLS 验证,不使用 macOS 的 Security Framework(SecureTransport)。BoringSSL 维护独立的 CA 信任列表,不与系统钥匙链同步。当服务端证书依赖系统钥匙链中的企业 CA 或特定中间 CA 时,Bun 验证失败。

在同一机器上,DeepSeek API(api.deepseek.com)也复现了该问题:

1
2
3
4
$ openssl s_client -connect api.deepseek.com:443 -servername api.deepseek.com
Verify return code: 0 (ok)      # 系统验证通过

# bun fetch 对该域名报 UNKNOWN_CERTIFICATE_VERIFICATION_ERROR

该 Issue 截至 2026 年 5 月仍未修复。

与 Node.js / curl 的对比

1
2
3
4
             TLS 实现              IPv6 fallback        macOS CA 信任
Node.js      OpenSSL/SecureTransport  libuv Happy Eyeballs  系统钥匙链
curl         SecureTransport/OpenSSL  Happy Eyeballs        系统钥匙链
Bun          BoringSSL(自封装)      自研,存在缺陷        内置 CA 列表

Node.js 通过 libuv 和 c-ares 处理 DNS 和连接,curl 通过 libcurl。二者均依赖经过长期验证的网络库,无需自己实现 RFC 8305 等协议细节。Bun 选择自行实现网络栈,在常规网络环境下性能更好,但在 IPv6 边界条件下暴露出更多问题。

当前状态与建议

截至 Bun 1.3.14(2026 年 5 月):

修复项 状态
Happy Eyeballs 已修复(v1.1.9)
link-local IPv6 降权 已修复(#26012)
连接超时(10 秒 fallback) 已修复(#27337)
下载 body 流卡死(本文所述场景) 未完全修复
macOS TLS 证书验证 未修复(#28804/#23735)

macOS 用户的 workaround:

1
2
3
4
5
# 临时关闭 Wi-Fi IPv6
networksetup -setv6off Wi-Fi

# 使用完毕后恢复
networksetup -setv6automatic Wi-Fi

或用 npm 替代 bun 安装全局包:

1
npm install -g opencode-ai@latest

参考链接

本文总阅读量 次 本文总访客量 人 本站总访问量 次 本站总访客数
发表了23篇文章 · 总计47.73k字
本博客已稳定运行
使用 Hugo 构建
主题 StackJimmy 设计