Skip to content

fix: align CONNECT with RFC 9110 and implement RFC 8305 Happy Eyeballs dialer #1

Description

@ssharkkky

背景与根因分析

在通过 NaiveProxy(Caddy forwardproxy)代理访问双栈(IPv4/IPv6,如 Cloudflare 等 CDN 托管的站点与 API 端点)目标时,客户端及上层应用(如 Node.js / Safari / 模型 API SDK)偶发出现以下异常:

  1. 固定卡在连接超时门限(如 10.0~10.5 秒):
    上层运行时(如 Node.js undici)默认连接超时为 10 秒(包含底层 TCP + TLS 握手协商完成)。未完成握手即抛出 Connection Timeout。
  2. 连接被直接重置 (ECONNRESET):
    客户端发出 TLS ClientHello 后,底层承载的 HTTP/3 双向流被服务端单方面 RST/Close。

经源码审查,定位到服务端 forwardproxy 继承自原版架构的两项历史技术债:


1. 违背 RFC 9110 CONNECT 规范(非标的提前 200 OK / Fast Open)

  • 代码位置:forwardproxy.go:354-365
  • 机制缺陷:
    服务端在接收到 CONNECT 请求时,尚未向目标服务器发起真实 TCP 拨号,即提前向客户端回写 200 OK 并 Flush。
  • 后果:
    客户端收到 200 OK 响应后判定代理隧道已通,立即开始上层连接超时倒计时并发送 TLS ClientHello。若服务端此时向目标拨号发生延迟或卡顿,客户端只能阻塞等待直至触发应用层超时;若服务端最终拨号失败,只能直接关闭该 HTTP/3 流,导致客户端应用层遭遇非预期的 ECONNRESET。

2. 违背 RFC 8305 Happy Eyeballs v2 规范(串行阻塞式遍历目标 IP)

  • 代码位置:forwardproxy.go:560-568
  • 机制缺陷:
    // This is net.Dial's default behavior: if the host resolves to multiple IP addresses,
    // Dial will try each IP address in order until one succeeds
    for _, address := range resolved.addresses {
        conn, err := h.dialContext(ctx, network, address)
        if err == nil {
            return conn, nil
        }
    }
    服务端为了执行 ACL 过滤,先通过 DNS 解析将域名拆散为单一 IP 列表,随后通过 for ... range 进行按序串行阻塞尝试。
  • 后果:
    单次连接超时默认高达 30 秒(DialTimeout)。当目标域名解析出多个 IPv6 和 IPv4 地址时,若列表排在首位的 IPv6 出现丢包、高延迟或路由黑洞,串行循环会阻塞在首个 IP 上死等,直接废弃了 Go 标准库原生支持的 RFC 8305(Happy Eyeballs 250ms 并发竞速回退机制)。

修复方案规划

1. 重构目标拨号逻辑(引入 RFC 8305 Happy Eyeballs 双栈竞速)

  • 实现目标:彻底消除多 IP 场景下的单线程串行等待。
  • 实施步骤:
    1. 在 ACL 检查通过后,将解析出的目标候选地址分类(IPv4 / IPv6);
    2. 实现标准 Happy Eyeballs 并发连接池:首选地址发起 SYN,若 250ms 内未成功则并发发起备选地址拨号,最先成功建立 TCP 握手者胜出,未完成的拨号立即取消;
    3. 将单个 IP 拨号超时合理约束(建议 3~5 秒),整体解析与拨号设置总 deadline;
    4. 保持 ACL 规则在建连前或建连时的合规拦截能力。

2. 规范 CONNECT 时序状态机(对齐 RFC 9110 Section 9.3.6)

  • 实现目标:真实目标 TCP 连接就绪后再确认 200 OK。
  • 实施步骤:
    1. 将 w.WriteHeader(http.StatusOK) 与 Flush 移动至 h.dialContextCheckACL 成功建立真实网络连接之后;
    2. 若拨号失败,返回符合标准的 HTTP 错误响应码(如 502 Bad Gateway、504 Gateway Timeout),替代粗暴的流 RST;
    3. 保留并验证对原有客户端 Padding 协商机制的兼容。

3. 测试与回归要求

  • 编写包含单栈路由黑洞/高延迟的 Happy Eyeballs 单元测试,验证 250ms 快速回退;
  • 保证现有 TCP/UDP 代理套件、ACL 过滤及并发基线回归全绿。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions