Surge Ponte 研发手记

Yachen Liu
7 min readMar 19

--

一直以来,都有很多用户希望 Surge 加入 tailscale 的支持,tailscale 是一套基于 WireGuard 协议的私有网络解决方案,大部分用户的使用场景是希望在外时能访问家庭或者办公室的内网。然而,虽然 tailscale 大部分代码是开源的,但是其并未完全公开其 API 稳定和提供响应的协议控制流说明,从其细节和其商业发展角度也能看出它们并不希望有第三方 App 接入。所以想要接入 tailscale 非常麻烦,除非直接将 tailscale 的代码整个嵌入 Surge,但是由于 Surge 已拥有极其复杂的网络栈,这种操作几乎不可能,也不值得。(举个例子,就像让 iOS 把 Android 源码嵌进去一样)

另外,由于 tailscale 没有在中国设置公共 DERP 服务器,所以为了取得较好的效果,需要自建 DERP 转发服务器,这样大幅提高了门槛。

所以,我们决定自己研发一套私有网络访问解决方案。

内网穿透

由于绝大部分用户的网络都处于 NAT 之后,特别是数据网络下,且 IPv6 也并未做到随处可用,所以最优的解决方案依然是尝试通过内网穿透技术在设备间建立连接。

之前我只对内网穿透的原理有过大概了解,并不清楚完整的细节,在动手实践后变遇到了第一个障碍:如果有一端的 NAT 类型是 Symmetric NAT,那另一端至少需要是 Address Restricted Cone NAT 方可进行穿透。

但是,即使拥有公网 IP,绝大部分路由器的 NAT 类型都是 Port Restricted Cone NAT,而数据网络下几乎全是 Symmetric NAT。这使得穿透最常见的使用场景,从手机访问家庭内网,无法被实现。

一筹莫展之际我开始研究各种替代方案,在一些 patent 和 paper 里发现了一种可在双端均为 Symmetric NAT 进行穿透的“方案”:两端不断随机变换端口号向对方外网 IP 发送 UDP 数据包,直到正好让外部端口撞上为止……这种暴力解决的方案是我没想到的,但是理论上确实可行,甚至 paper 里还有预估成功率与尝试次数的概率分析。

然而我实现后尝试了一下,发现不等待多久都无法成功,于是我又去观察了一些其他需要进行内网穿透的 App(如米家连接摄像头),也是使用了这样的方案去进行穿透(这也就是为什么打开各种摄像头 App 可能导致 Surge 报警连接数过多…),但同样未能成功,回退至了服务器中转。我猜测可能是现在数据网络下,对于单设备的 NAT 表容量很小,导致大量开启新端口后旧端口很快就被关闭了。

再次陷入困境,难道只能靠建立中转服务器实现了吗?如果 Surge 提供服务器会有很多问题,包括带宽、计费、合规、隐私等等,如果让用户自建服务器,那好像也没必要专门做一套解决方案了,用现成的各种工具折腾下即可。

这时正好看到了吃灰很久的 Nintendo Switch,既然 Surge 通过 UDP 代理让小动物们互相串门,那同样也可以借助代理穿透回家啊,毕竟哪个 Surge 用户没有一箩筐代理呢:)

于是,Surge 成功实现了利用代理协议的 UDP 转发功能进行内网穿透,这种穿透下可以无视任何 NAT 阻碍,甚至 UDP 封锁。Surge Ponte 应该是第一个使用这样的技术实现端到端互联的解决方案,对比自建如 DERP 那样的转发服务器,这种方案的最大优势是只需要一支持 UDP 转发的标准代理服务器即可,不需要在服务端再进行任何额外配置和安装软件。

Vector 协议

已经在端到端之间打通了 UDP 信道,下一步则是要考虑怎样利用这个信道进行联通。绝大多数私有网络解决方案都使用了 VPN 协议,工作在 Layer 3,一方面是因为 3 层网络没有状态,实现起来很简单,另一方面因为 3 层可以承载几乎所有上层协议,兼容性最好。

但是 3 层也存在它的问题:

  • 效率低下,原因是服务端没有 buffer、DNS 请求需由客户端完成等等。
  • 现在互联网上 L4 已经只剩 TCP、UDP、ICMP 三种协议,L3 的兼容性优势几乎没有用武之地。
  • 需要给每个设备都配置 IP 和密钥,虽然 tailscale 已经将这些工作大幅简化,但有时仍需考虑。
  • Surge 本身就工作在 L4,若使用 L3 的协议需要再产生额外开销,没有必要。

所以 Surge Ponte 使用基于连接的代理协议工作而非 VPN 协议。目前基于 UDP 的代理协议屈指可数,在之前版本中,Surge 已经支持了一个基于 QUIC 的代理协议:TUIC,但是在其存在一些问题,如:

  1. 服务端和客户端在连接建立阶段未相互鉴别,易遭受攻击或被探测。
  2. 服务端必须要配置证书方可使用,且需使用有效证书,若使用自签名证书需在客户端绑定指纹,很麻烦。(由于客户端和服务端存在 pre-shared key,其实没有必要使用标准的 X509 验证)
  3. TUIC 协议设计是在握手后立刻新建一个 uni-stream 写入 token 进行鉴权,但是由于 QUIC 协议中各数据流间的流控是独立的,所以有可能出现鉴权数据流丢包未完成,但是代理数据流请求先送达的请,这时会导致服务器认为未授权直接断开连接(ERR_DRAINING)。要避免这种情况需要等待鉴权数据流 ack 完毕再请求,这导致额外引入了一个 RTT。
  4. TUIC 的两种 UDP 转发方式都很不理想,native 方式下 UDP payload 尺寸过小且不支持分包,无法承载部分协议。quic 方式下开销巨大且有带宽上限。

所以我们自行研发了基于 QUIC/UDP 的新型代理协议:Vector。

  • Vector 协议无需配置证书,服务端将在启动时自动生成随机的自签名证书。(当然也可以手动指定)
  • Vector 协议在 QUIC 连接建立阶段便加入了双向身份验证,具体来说:
    1. 客户端和服务端根据 psk,使用 Argon2id 和 BLAKE2b 算法生成 ckey 和 skey。
    2. 客户端随机生成 16 bytes 的二进制,使用 BLAKE2b 算法用 ckey 生成 MAC(类似于 HMAC-SHA),取其前 16 bytes。将这两个 16 bytes 的二进制拼接为 32 bytes,用作 QUIC 握手时的 SCID。
    3. 服务端收到新的握手请求后,使用 ckey 计算数据包中的 SCID 是否合法且非重复,若非法,丢弃该数据包或转发给一标准 H3 服务器。
    4. 服务端以该 SCID 作为输入,使用 skey 进行 BLAKE2b 算法生成 MAC(32 bytes),将该结果作为新的 SCID 向客户端完成握手。
    5. 客户端使用 skey 验证新的 SCID 以确认服务器合法性,并采取后续措施,如回退至标准 H3 客户端行为。
  • Vector 协议提供了更完善的 metadata 机制,如允许客户端上报自己的设备名(目前用于 Dashboard 显示和 DEVICE-NAME 规则匹配),允许服务端出现错误时回报客户端错误信息。
  • Vector 协议提供了完善的 UDP 转发支持,支持任意大小的 UDP payload,且通过与 QUIC 协议的流控算法结合,在出现拥塞时主动丢弃 UDP 数据包,避免出现类似于 TCP over TCP 的问题。

目前 Vector 协议仅可被用于 Surge Ponte 中,可能于未来推出其他平台的服务端二进制,类似于 Snell。

细节

这两项大工程完成后,就是一些细节问题了,比如:

  • 在各个设备间使用 iCloud 同步加密密钥和访问信息(主要是对各种 iCloud 问题进行 workaround…)
  • Surge Ponte 会自动检查是否与目标设备在同一个子网,如果在的话则直接进行连接,不会再进行穿透。且当 DEIVCE:NAME 规则使用的名字即为自身设备名,则将该策略转变为 DIRECT 策略。这在多个设备间共享同一份规则时很有用。
  • 实现了对当前网络和代理的 NAT 类型进行探测的能力,而且速度比其他工具快得多,方便进行 Surge Ponte 配置,也可以手动使用。(菜单,窗口,代理诊断)

目前 Surge Mac 5 已经开放测试,详见这里,祝各位 have fun。

--

--