Surge 中文白皮书 (草稿第一部分,接管)

Surge 从 2015 年发布至今已有 5 年,功能日益复杂,官方手册并没有系统性的阐述 Surge 的具体工作流程和架构设计,为此我开始撰写 Surge 的官方中文白皮书。阅读之前需要读者有一定的基础网络知识,但不需要很深入。

概述

Surge 是一个在 iOS 和 macOS 平台上的网络工具集,其核心能力有四项:

  • 接管:可以将设备发出的网络连接进行接管。
  • 修改:可以对被接管的网络请求和响应进行修改。
  • 转发:可以将被接管的网络请求转发给其他代理服务器。
  • 截获:可以截获网络请求和响应的具体数据,同时可对 TLS 加密流量进行 MITM 解密。

以上四项能力构成了 Surge 的核心工作流。

接管

要想使 Surge 实现后续的转发、修改和截获等功能,首先需要 Surge 对网络连接进行接管。

在 macOS 和 iOS 下,要想使程序发出的网络连接被另一个程序所接管,而不是直接将数据发送到物理网卡,有以下三种方式:

1. 配置代理:如果系统配置了代理服务器,那么程序在执行网络请求的时候,就不会直接连接目标服务器,而是产生一个发向代理服务器的连接。利用这个特性,可以在本地启动一个代理服务,并配置系统代理为 127.0.0.1 (即本机)的一个端口,这样就可以接管网络请求。

但是,这种方式必须要求程序自身支持代理机制,系统的代理设置只是告知程序应该使用代理,需要程序自己完成代理的后续逻辑。好在,对于绝大部分带用户界面的程序,由于开发时使用了系统的高层网络框架(Cocoa/Cocoa Touch),开发者不需要进行任何额外的工作就可以支持代理。

而对于命令行程序,由于使用的是 POSIX 接口进行网络请求,该接口并没有对代理服务器提供内嵌支持,所以需要开发者自己完成对代理服务器的支持,这导致各种命令行程序对代理的支持情况和具体行为并不统一。同时由于大部分命令行程序并没有为 macOS 进行特殊处理,所以不会理会系统配置里的代理服务器设置。大部分命令行程序需要通过环境变量 https_proxy 和 http_proxy 去配置代理,还有一部分需要通过修改配置文件进行配置。

还有少量程序由于完全缺乏代理服务器的支持,无法通过这种方式去接管网络连接。

2. 虚拟网卡(Virtual Network Interface,简写为 VIF):主流操作系统几乎都存在 TUN 和 TAP 两种虚拟网卡接口,原本是为了提供对 VPN 的支持。通过在系统中建立虚拟网卡并配置全局路由表,可以接管所有的网络请求。

这种方式对应程序来说是无感知的,所以并不需要程序主动提供支持,几乎所有程序都可以被这种方式接管网络请求。除非程序主动指定了物理网卡,绕过了默认的虚拟网卡。

3. Socket Filter:这是 macOS 的一项内核特性,可以通过注入一个 Kernel Extension(kext)对所有 socket 调用进行 hook,以此接管请求。

除系统自身的一些程序外,这种方式可以强制接管系统中所有程序的所有网络请求。如 Proxifier 和 Little Snitch 就使用了这种方式接管网络。

这三种方式各有优劣:

  1. 方法 1 性能最优,对系统侵入性最小,无奈有部分程序不支持。
  2. 方法 2 性能略低,因为截取到的流量是 IP 层的数据包,需要有一个 TCP 协议栈进行重组装,造成了额外的性能开销。
  3. 方式 3 最暴力,对系统侵入性高,Kernel Extension 有可能造成整个系统的不稳定,Apple 已确认在未来的 macOS 中将取消对 Socket Filter 的支持。

Surge 主要使用方法 1 接管网络请求。方法 2 作为补充,接管不支持代理的程序。

  • 对于 Surge iOS 版本,开启后会将自身注册为代理服务器,同时使用 Network Extension 接口建立了 TUN 虚拟网卡。
  • 对于 Surge Mac 版本,开启「设置为系统代理」选项会将自身注册为代理服务器(即方法 1),开启「增强模式」选项将会建立虚拟网卡(即方法 2)。

以上的说明针对的是 Surge 对本机程序的接管,当使用 Surge 接管另一个设备的网络请求时:

  • 由于 iOS 的系统限制,只能靠使用方法 1 作为代理服务器去接管另一个设备的请求。(修改目标设备的代理服务器设置)
  • Surge Mac 除了使用方法 1 外,也可以靠方法 2 接管另一个设备的请求。(修改目标设备的默认路由设置)
  1. 什么是代理协议

代理是一项从计算机网络诞生就一直存在的机制,简单来说,代理服务器就是一个传话人,将应用和目标服务器间的数据由一个代理服务器中转传递。

Image for post
Image for post
From Wikipedia: https://en.wikipedia.org/wiki/File:Proxy_concept_en.svg

当使用代理服务器时,除了发送原始的数据,还需要一些额外的工作:

  1. 告知代理服务器,目标服务器的主机名和端口号
  2. 发送鉴权信息,供代理服务器进行身份验证(可选)
  3. 对数据传输进行加密(可选)

如何进行这三项工作的规范,就是代理协议,有 RFC 规范的代理协议只有 HTTP 代理协议和 SOCKS 代理协议两种。SOCKS 代理协议有 SOCKS4、SOCKS4a、SOCKS5 三个版本。(macOS 使用的是 SOCKS5)

有 RFC 规范的代理协议,还有很多自定义的代理协议,如 shadowsocks、Snell 等。但是由于系统和程序没有内嵌对这些协议的支持,需要将他们通过一个客户端程序转换为标准的 HTTP 或 SOCKS5 代理服务供程序和系统使用。Surge 也可以充当这样的转换器。

HTTPS 和 SOCKS-TLS 代理并没有 RFC 规范,只是在原协议套上了 TLS 层进行加密。

2. HTTP 代理和 TCP 代理

HTTP 代理仅可以转发 HTTP 协议的请求(除非该 HTTP 代理额外支持了 CONNECT 方法)。当使用一个 HTTP 代理时,向 HTTP 代理发出是一个完整的 HTTP 请求体,代理服务器收到该请求后进行转发,拿到完整的 HTTP 响应,再将该响应转发给客户端。所以 HTTP 代理是会话制,单个 HTTP 代理连接上可以不断地转发不同的 HTTP 请求,这些 HTTP 请求甚至可以不是同一个目标主机。

其他的代理协议都属于 TCP 数据流代理,仅仅是对 TCP 数据流进行了中转,也就是说代理并不关心和理解具体传递的是什么内容,只要是一个基于 TCP 协议的数据流,就可以被代理服务器所转发。

需要注意的是,当我们和目标服务器间使用的是 HTTPS 协议进行连接时,并不可以使用传统的 HTTP 代理协议,我们并不希望代理能够获知转发的内容,所以传递的内容的明文对代理并不可见。为此 HTTP 代理协议增加了 CONNECT 方法,可以将一个 HTTP 代理转变为一个 TCP 数据流代理,用于处理 HTTPS 请求。所以现在 HTTP 代理也可以被用来对任意 TCP 协议进行转发。

3. 不包括简单主机名和忽略这些主机的代理设置

在 macOS 的网络设置中,有「不包括简单主机名」和「忽略这些主机和域的代理设置」两个选项,和代理设置相同,这两个选项也只是「告知」程序应该按照这样的行为去工作,所以需要应用自己提供对这两个选项的支持。

和代理本身的实现一样,绝大部分带用户界面的程序,由于开发时使用了系统的高层网络框架(Cocoa/Cocoa Touch),也自动就获得了对这两个选项的支持。而命令行程序几乎全都不支持这两个设置。

Surge 的 [General] 配置中的 exclude-simple-hostnames 和 skip-proxy 两个设置对应的就是这两个选项,在勾选「设置为系统代理」时会同时应用到系统中。

需要注意的是,如果方法 1 和方法 2 同时启动,那么被这两个设置排除的网络连接,同样会被方法 2 接管。所以该选项一般只用于处理某些特殊的兼容性问题,并不能使请求绕过 Surge。(对于 Surge Mac,如果未开启「增强模式」,确实可以绕过)

  1. Fake IP

在 POSIX 规范下,执行网络请求需要先通过 gethostbyname 等类似方法进行 DNS 解析,再通过 connect 去连接获取到的 IP 地址,导致使用方法 2 接管请求时会遇到一个问题:必须要先进行 DNS 解析。

但是,如果该网络请求 Surge 决定交给代理服务器去转发,那么在本地进行的 DNS 查询就是无意义的,而且在某些情况下,该域名可能根本无法在本地进行解析。

为解决这个问题,Surge 的 VIF 在收到一个 DNS 查询时,并不会进行真正的 DNS 查询,而是直接返回一个 Fake IP 地址(通常为 198.18.x.x,该地址段并不会在公网上被使用)。后续收到发往 Fake IP 的 TCP 或 UDP 数据包时,将该虚假 IP 翻译回原始域名进行后续处理。

Surge 返回的 DNS 应答的 TTL 值(Time to live,可粗略理解为有效期)仅为 1 秒,所以该结果即用即抛,不必担心因 Fake IP 造成 Surge 关闭后网络异常。(不过的确观察到一些智能硬件未正确遵循 TTL 重新进行 DNS 查询,一般重启设备即可解决。)

在 Surge 早期的版本中,谨慎起见只会对规则中标记为 force-remote-dns 的主机名返回 fake IP。由于该选项经常对用户产生困扰,现在版本已取消了这个选项,对所有主机名都会返回 Fake IP 地址。配置 [General] 中的 always-real-ip 选项用于覆盖该行为,对于出现在该选项中的主机名,Surge 不会返回 Fake IP,会将该 DNS 查询转发给 DNS 服务器获得真实 IP 地址。

2. tun-excluded-routes 和 tun-included-routes 选项

在建立虚拟网卡时,Surge 会根据这两个选项加入额外的路由表,tun-excluded-routes 比较好理解,有些用户可能会问为什么会有 tun-included-routes 选项,不应该默认就包含了所有路由吗?

这里需要补充一些网络知识,对于主流操作系统,路由表条目的优先级是按照条目的子网覆盖域决定的,覆盖越小的路由表条目优先级越高,而非按照先后或者上下的顺序。

所以,即使 Surge 的 VIF 配置了 0.0.0.0/0 的默认路由表条目,物理网卡本身存在当前子网的路由表条目(如 192.168.1.0/24),该条目覆盖域小优先级更高。所以所有发往 192.168.1.x 的网络连接依然不会被 Surge 接管。如果配置了 tun-included-routes = 192.168.1.100/32,那么这条路由表条目覆盖域最小优先级最高,使得发往该 IP 的网络连接也能被 Surge 接管。

iOS Freelance Developer, Technical Advisor

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store