聊聊那些翻山越岭的神奇操作(中)
书接上回,上回说到可以通过域前置的手段绕开防火墙的阻断,但缺点也很显而易见:支持的网站和设备都有限(而且似乎不是很优雅),那有没有什么别的翻山越岭的方法呢?
劫持与污染
众所周知,传统的DNS查询使用的是UDP协议,DNS服务器一般监听在53端口。这里有两个问题:一是查询的流量是明文的,任何中间人都可以监听并篡改DNS服务器返回的结果,二是DNS查询固定会使用53端口,流量特征明显,非常容易被识别并加以干扰。
DNS查询采用的是“递归解析”的方法,假如需要查询blog.kenxu.top的IP,需要先查询.top这个TLD对应的解析服务器,再由.top顶级域服务器查询kenxu.top的解析服务器,最终由这个一级域服务器查询blog.kenxu.top这个二级域名的IP。但一般而言,客户端不会自行完成递归解析的全部流程,而是将DNS请求直接发给设定的DNS服务器(运营商下发的Local DNS或是公共DNS),由它们完成剩下的流程。[1]
那么,防火墙是如何利用DNS的查询过程进行审查和阻断的呢?当DNS流量经过防火墙的时候,防火墙会识别DNS查询的流量并对查询的域名进行检测,一旦匹配到黑名单中的域名,便会伪装成目标DNS服务器返回虚假的解析结果。由于各级DNS服务器都会缓存查询结果,境内的DNS服务器除了会向用户返回错误的DNS解析结果,还会将这个错误的结果缓存下来,将来有其他用户查询时便直接返回这个错误的结果。
综上所述,在查询黑名单网站时,如果你使用的是境内的DNS服务器,你收到的是来自缓存污染后的结果;如果你使用的是境外的DNS服务器,你收到的是防火墙劫持后的结果。验证的方法很简单,随意查询一个黑名单内的地址即可:
使用Cloudflare提供的公共DNS举例,作为对比,如果同城有阿里云的机房,那么阿里云DNS的查询时间一般在10ms左右,而广州-香港IEPL端内的时延一般在6ms左右。如此短的查询耗时说明除非用的是比光更快的传输介质,不然一定是被防火墙劫持了(
DoT与DoH
如何解决劫持和污染的问题?DNS over TLS (DoT) 和 DNS over HTTPS (DoH) 的相关规范于2016年和2018年制定完成,解决了传统DNS无加密和特征强的问题。
DoT的思路比较简单,即直接用TLS加密原先明文的DNS查询流量,通过TCP协议进行连接,并使用853端口作为常用端口。中间人虽然无法解密查询流量,但仍然可以通过连接目标服务器的端口判断用户正在使用DoT查询DNS,进而阻断流量。
除此之外还有一些别的特征,例如DoT流量在发送TLS ClientHello数据包时,一般不会包含ALPN (Application Layer Protocol Negotiation) 扩展,但端口号这个特征其实已经足够明显,此处按下不表。
而DoH在同样采用TCP连接与TLS加密的基础上,将DNS查询封装成了HTTP请求,并使用443端口,使其在流量特征上与一般浏览网页的HTTP流量无异,中间人除了无法监听和篡改DNS查询的结果,甚至无法得知用户正在进行DNS查询。
由于进行了HTTP封装,因此DoH的开销会略大于DoT,数据包的体积和数量会略微增大(例如ClientHello中的ALPN,数据包中的HTTP HEADER),但考虑到其相对DoT增强的安全性,以及HTTP/2带来的多路复用等特性,这点开销也是可以接受的。
DoT和DoH并非完美无缺的解决方案,其缺点也很明显,其中绕不开的一点就是查询时间。TCP协议+TLS加密的方式,使得DoT和DoH查询DNS相较传统方案增加了两个RTT的时延(如果使用的是TLS1.2,甚至是3个RTT),查询耗时相比基于UDP的方案大大增加。


DoT和DoH是否可以完美突破审查?实则不然。首先,审查者可以记录境外目标DNS服务器的IP地址,在防火墙侧直接丢弃去往这个IP的数据包;同时,无论是DoT还是DoH,都使用了TLS进行加密,在上篇中有提到,TLS存在着SNI——服务器名称指示这一扩展,其中明文记载了访问网站的信息,也就是说,审查者完全可以像屏蔽其他网站一样,根据SNI中的信息屏蔽相应的DoH流量。
事实上,根据笔者的印象,在DoH还没有推广开来的时候(约2021年以前),仍然可以在境内访问大多数海外的DoH服务器,例如Chrome和Firefox自带的OpenDNS和NextDNS,而随着这两年审查力度的逐渐加大,这些位于境外的DoT和DoH服务器也无一例外地享受到了SNI阻断的待遇。
使用OpenDNS的DoH实例测试,可以看出,ping所使用的ICMP协议可以正常通过防火墙并收到目标服务器的回应。
而使用DoH查询时,笔者同时观测到了上述的两种封锁方法:对于TCP数据包的丢弃和对于TLS流量的SNI阻断。

目前,除非自行在海外VPS上搭建DNS服务器,否则几乎所有公共DoT和DoH实例都是无法连接的。
QUIC
传统的TCP,UDP协议和TLS加密似乎都不足以解决眼下的问题。如同数十年前,HTTP/2出现之前的HTTP/1.1——与其在旧协议上缝缝补补,不如直接创造一个新的协议。早在2012年,Google就开始着手研发一种全新的协议,旨在替代传统浏览互联网所使用的HTTP协议。2015年,有关QUIC的草案交由IETF开始标准化,并逐渐转化为一种通用的传输协议。2021年,QUIC在RFC 9000中正式标准化。">[2]</span></a></sup>](/post/bypass-gfw-p2/11.png)
QUIC是一种基于UDP的传输层协议。其解决了传统基于TCP的协议面临的一个老大难问题——队头阻塞。HTTP/2虽然增加了多路复用的特性,但多个数据包跑在一个同一个TCP连接之上,一旦发生丢包,整个连接就会阻塞,直到重传丢掉的包。在2%的丢包率下,HTTP/2的表现甚至不如HTTP/1。而基于UDP的QUIC在单个连接中的流是互相独立的,意味着即使发生丢包,也不会阻塞其他流,只要重传丢失的数据帧(例如STREAM frame, ACK frame, CRYPTO frame等等)即可。
graph TD
A[QUIC Connection] --> B[Stream 1]
A --> C[Stream 2]
A --> D[Stream 3]
B --> B1[STREAM Frame 1]
B --> B2[STREAM Frame 2]
B --> B3[CRYPTO Frame]
C --> C1[STREAM Frame 1]
C --> C2[ACK Frame]
D --> D1[STREAM Frame 1]
D --> D2[STREAM Frame 2]
D --> D3[STREAM Frame 3]
style A fill:#bbf,stroke:#333,stroke-width:2px
style B fill:#bfb,stroke:#333,stroke-width:1px
style C fill:#bfb,stroke:#333,stroke-width:1px
style D fill:#bfb,stroke:#333,stroke-width:1px
style B1 fill:#ffd,stroke:#333,stroke-width:1px
style B2 fill:#ffd,stroke:#333,stroke-width:1px
style B3 fill:#ffd,stroke:#333,stroke-width:1px
style C1 fill:#ffd,stroke:#333,stroke-width:1px
style C2 fill:#ffd,stroke:#333,stroke-width:1px
style D1 fill:#ffd,stroke:#333,stroke-width:1px
style D2 fill:#ffd,stroke:#333,stroke-width:1px
style D3 fill:#ffd,stroke:#333,stroke-width:1px
此外,与TCP和UDP这类传统的传输层协议不同,QUIC的职能向上延伸到了TLS所在的层级(传输层之上,应用层之下)。QUIC协议强制要求使用TLS 1.3对流量进行加密,不仅进一步提升了安全性,更利用上了TLS 1.3带来的0-RTT特性,利用首次连接记录的预共享密钥直接向服务端发送数据,进一步降低时延。
那么,QUIC又是怎样对待在TLS握手阶段明文的Client Hello信息呢?QUIC同样对其进行了加密,但要如何在握手阶段就能够让服务端解密数据呢?其实这里使用的并非TLS加密,事实上无论协议如何变化,任何非对称加密都必须在密钥交换环节后才能进行。QUIC对于握手阶段的加密,其实是对称加密,这点在QUIC相关的RFC文档中有详细定义,这里只截取其中的一小段:
This secret is determined by using HKDF-Extract with a salt of
0x38762cf7f55934b34d179ae6a4c80cadccbb7f0aand the input keying material (IKM) of the Destination Connection ID field. This produces an intermediate pseudorandom key (PRK) that is used to derive two separate secrets for sending and receiving.
The current encryption level secret and the label “quic key” are input to the KDF to produce the AEAD key; the label “quic iv” is used to derive the Initialization Vector (IV). The header protection key uses the “quic hp” label. Using these labels provides key separation between QUIC and TLS.
Both “quic key” and “quic hp” are used to produce keys, so the Length provided to HKDF-Expand-Label along with these labels is determined by the size of keys in the AEAD or header protection algorithm.
QUIC在建立连接阶段,会使用Initial Packet和Handshake Packet进行握手和密钥交换。对于Initial Packet中的握手信息,会先使用DCID(由客户端生成)[3]和一个固定密钥(与QUIC版本相关)派生出初始密钥,分别用于客户端和服务端,再根据这个初始密钥派生出对称加密和包头混淆所需要的key,iv和hp。服务端收到Initial Packet后,读取明文的DCID信息,并按同样的流程生成初始密钥[4]并派生出解密所需要的密钥。
也就是说,加密Initial Packet的信息,都是公开可推导的。这里的加密本身不保证机密性,但可以防止中间人篡改并保证QUIC包体在连接的不同阶段的一致性。那么,防火墙目前能否识别并阻断QUIC类型的流量呢?
我们使用QUIC协议访问位于防火墙黑名单中的网站v2ex.com,并抓包分析结果,首先使用传统TCP协议:
可以看出连接立刻被防火墙阻断,接下来试试QUIC:
这里Wireshark帮我们解密了Initial Packet,可以看出在包含了黑名单内SNI的情况下,防火墙并没有阻断连接。
当然,根据我们上述分析的结果,防火墙是完全有能力识别并阻断QUIC流量的,至于为何没有阻断连接,可能是因为QUIC协议自带加密,同时Client Hello结构与传统TLS的不同,防火墙暂时没有做这方面的适配。
也有研究指出,防火墙其实已经具备了识别并阻断QUIC协议的能力(很遗憾我暂时没有体验到)。但由于对Initial Packet进行解密造成的性能开销,防火墙目前对QUIC流量的处理能力较弱,在1000kpps的混合流量下,80%左右的包都能直接“穿墙而过”,这也算是加密所起到的一个作用吧。
最后我们还是得聊聊QUIC的缺点,在境内,基于UDP的协议基本难逃QoS的命运,基于UDP带来的特性看似美好,协议本身却终究难逃高峰时期运营商的限速与阻断。事实上,本文大多写于晚间,在这个时间段,笔者使用家中的宽带始终无法与网站建立QUIC连接,在进行QUIC流量抓包的时候不得不切换到流量上网。
ECH
前文说到QUIC对Client Hello所在的Initial Packet进行了对称加密,但中间人还是可以根据公开信息解密,那么有没有什么可以彻底加密Client Hello数据的方式呢?这就要提到相关RFC标准仍在制定中的ECH(Encrypted Client Hello)了。2018年,Fastly, Cloudflare, Mozilla 等一众研究员和学者提出了ESNI草案,研究加密Client Hello中SNI扩展的可行性。2020年,协议从ESNI变为ECH,将加密范围从SNI变为整个Client Hello信息,将ALPN和Key Share等能够间接推断出应用层协议的信息也纳入了保护范围。
ECH加密Client Hello的方式并非QUIC协议Initial Packet阶段的对称加密,而是与TLS加密类似的非对称加密。如何在TLS握手阶段,服务端密钥还没有发送至客户端的情况下就使用非对称加密呢?解决方案不在TLS协议上,而是DNS。
DNS并非只能记录域名对应的IP信息,还可以记录许许多多跟域名相关的信息,例如用于将一个域名映射到另一个域名的CNAME记录,用于电子邮件服务器的MX记录等等。而ECH正是利用了其中一种DNS记录类型——SVCB (Service Binding)。
SVCB记录原先用于扩展CNAME的功能,除了可以设置映射的域名之外,还可以设置服务所在的端口,ALPN信息等等,举个例子:
1 | |
SVCB后是记录内容,3代表优先级,svc4.example.net代表可替代的主机端点(TargetName),之后的部分(SvcParams)记录了连接的细节。你可能已经发现了,这里存在一个ech设置项,实际上,这就是解决加密问题的关键:ECH将非对称加密所使用的公钥放在了DNS记录中。
事实上,对于HTTPS协议,一般不会直接使用SVCB记录类型记录连接相关的信息,而是使用一种从SVCB记录派生出类的DNS记录类型——HTTPS记录,其结构与SVCB几乎一致,只不过专用于HTTPS协议,以上文提到的被封锁的网站v2ex.com为例,它的HTTPS记录长这样:
1 | |
可以看到,在SvcParams部分记载了ECH所使用的加密公钥,除此之外还有ipv4hint和ipv6hint字段,记载了网站域名对应的IP,这是为了减少DNS查询的次数从而降低延迟。ech是一串使用Base64编码的字符串[5],试将其解码:
1 | |
一大串乱码之后是一个域名cloudflare-ech.com,这是客户端所连接的中间服务器的域名,一般是网站的CDN提供商。事实上,ECH将Client Hello分为了外层和内层(Outer ClientHello和Inner ClientHello),客户端在进行HTTPS类型解析后,首先用公钥加密连接至服务端的Client Hello信息,将其放入TLS握手包的encrypted_client_hello扩展中,再将连接至中间服务器的Client Hello信息以明文的形式放在原先的位置。中间服务器收到数据包后,先使用私钥解密encrypted_client_hello扩展中的信息,再根据其中的SNI信息将其转发至对应的后端服务器。这样中间人就只能看见用户连接中间服务器的握手信息,而看不到连接后端服务器的握手信息了。
sequenceDiagram
participant U as 用户 (Client)
participant M as 中间人 (观察者)
participant F as 前端服务器 (Gateway/CDN)
participant B as 后端服务器 (真实网站)
Note over U,M: DNS下发ECH公钥 (via HTTPS RR / SVCB)
U->>F: 发送 Outer ClientHello (包含加密的Inner ClientHello)
Note right of M: 只能看到Outer ClientHello<br/>及外层SNI (伪装用)
alt 前端服务器支持ECH
F->>F: 使用DNS提供的公钥解密Inner ClientHello
F->>B: 将解密得到的真实SNI/ALPN等转发给后端服务器
B-->>F: 返回握手响应 (证书/密钥交换等)
F-->>U: 返回加密的ServerHello等握手消息
Note over U,F: 成功建立TLS会话 (真实目标隐藏)
else 不支持ECH
F-->>U: 返回ECH失败信号 (ech_required)
U->>F: 回退到明文ClientHello
end
那么,想要使用上ECH,获取到正确的HTTPS记录就显得尤为重要。如何确保DNS记录不被中间人审查与篡改?这就需要依赖文章前半部分所说的DoT和DoH了。[6]事实上,虽然ECH相关的草案中没有明确规定,但Google Chrome和Firefox都将使用DoH作为启用ECH的前提条件之一,毕竟根据木桶理论,在使用传统UDP DNS的情况下,隐私方面最短的一块板应该是DNS查询,而非Client Hello中的明文信息。
在使用ECH的情况下,抓包看看访问v2ex.com的结果:
可以看到在Client Hello阶段,SNI被替换为了中间服务器的地址,同时真正的Client Hello信息加密后放在了encrypted_client_hello扩展中。TLS握手也顺利完成,没有被阻断。
但是,仍然存在一个小问题:虽然真实的SNI被加密了,但是“存在着被加密的SNI”这件事却是可以被检测到的,毕竟encrypted_client_hello这个扩展就摆在 (Outer) Client Hello 信息中。中间人难道不会通过检测是否存在这个扩展从而阻断所有的ECH流量吗?制定ECH草案的人也想到了这一点,这就是我认为ECH中一个绝妙的设计点:GREASE(Generate Random Extensions And Sustain Extensibility)。
GREASE并非ECH的首创,其于2016年被提出,并在2020年标准化,旨在对抗TLS面临的协议僵化 (Protocol Ossification) 问题。具体在RFC的第一节有详细介绍:
…TLS follows a model where one side, usually the client, advertises capabilities, and the peer, usually the server, selects them. The responding side must ignore unknown values so that new capabilities may be introduced to the ecosystem while maintaining interoperability.
However, bugs may cause an implementation to reject unknown values……when new values are defined, updated peers will discover that the metaphorical joint in the protocol has rusted shut[7] and the new values cannot be deployed….
To avoid this problem, this document reserves some currently unused values for TLS implementations to advertise at random. Correctly implemented peers will ignore these values and interoperate. Peers that do not tolerate unknown values will fail to interoperate, revealing the mistake before it is widespread.
GREASE 的目的,就是为了防止中间设备因为协议新特性引入的额外字段而无法正确解析流量,因此采取的一种预防性措施。通过人为增加一些无用的字段,迫使中间设备在解析流量时忽略掉这些字段(而不是抛出错误),从而为协议后续的升级留下空间。
回到ECH,GREASE在此处的作用在RFC规范的6.2小节中有说明:
The GREASE ECH mechanism allows a connection between and ECH-capable client and a non-ECH server to appear to use ECH, thus reducing the extent to which ECH connections stick out.
除了上述所说的目的之外,GREASE还可以使ECH数据包和非ECH数据包看起来完全一致,从而对抗审查。具体实现方法就是:对于不支持ECH的网站(例如未在HTTPS记录中配置ECH公钥或是干脆没有HTTPS记录),也在Client Hello数据包内添加encrypted_client_hello扩展,只不过其中的参数都是随机生成的。根据GREASE的相关规范,如果不支持ECH的服务端解析到这个扩展,应该直接将其丢弃,这样就保证了ECH既不会被中间人识别,又不会对旧服务端造成影响。[8]
最后还是说说缺点,ECH在可部署性,机密性,抗审查性和前向兼容性方面基本都做到了极致,不过目前支持ECH的网站还是少之又少,目前来看几乎只有使用Cloudflare免费计划的网站支持ECH[9](当然,是CDN服务器提供的支持),希望在不久的将来ECH能正式标准化,更多网站能支持ECH这项先进的特性。
后记
本期与上期间隔时间较远,主要还是因为笔者对其中的一些概念处于一知半解的状态,集中几天查阅了相关资料才熬出的这篇文章,可以说是很不容易了(
但即便如此,文章肯定存在着(可能很多的)错漏,敬请各位海涵,欢迎在评论区与我讨论。
虽然标题写的是中篇,但处于一些原因,下篇完成的时间仍是未知数(可能很快,也可能不会有),请各位以不抱期望的心情期待一下(
注释与参考
本文撰写过程中参考了以下资料:
https://www.ruanyifeng.com/blog/2022/08/dns-query.html
https://en.wikipedia.org/wiki/QUIC
https://en.wikipedia.org/wiki/Internet_protocol_suite
https://suntus.github.io/2019/05/09/HKDF%E7%AE%97%E6%B3%95/
https://blog.cloudflare.com/encrypted-client-hello/
https://blog.cloudflare.com/handshake-encryption-endgame-an-ech-update/
https://en.wikipedia.org/wiki/List_of_DNS_record_types
https://kb.isc.org/docs/svcb-and-https-resource-records-what-are-they
此外还有文章中已经给出的超链接
感谢ChatGPT画的mermaid图
- 可由 DNS Header 中的
recursion-desiredFlag 控制 ↩ - 图片来源:https://en.wikipedia.org/wiki/File:HTTP-1.1_vs._HTTP-2_vs._HTTP-3_Protocol_Stack.svg , CC BY-SA 4.0 ↩
DCID即 Destination Connection ID,代表连接对方的ID,初次连接时由客户端随机生成,服务端接收连接后会生成SCID(Source Connection ID)代表己方发回给客户端,之后的阶段客户端便会使用这个ID作为DCID,在截图中亦有体现 ↩- 服务端需要生成两组初始密钥,一组用于解密客户端发来的包(
client_initial_secret),另一组用于加密发给客户端的Handshake Packet(server_initial_secret) ↩ - 有关这个字段的详细介绍,请参考: https://blog.outv.im/2023/ech/#%E9%BA%BB%E7%83%A6%E7%9A%84-ech-%E9%83%A8%E7%BD%B2%EF%BC%9A%E6%89%98%E7%AE%A1%E5%9C%A8-dns-%E4%B8%8A%E7%9A%84-echconfig ↩
- 当然,你得使用没有受到污染的 DoH 服务器 ↩
- 这里使用了“生锈的关节”这个比喻,这也是为什么这个特性(或者说思想)会被特地命名成
GREASE(润滑) ↩ - 对于 ECH 的 GREASE 在浏览器端实现,支持ECH的浏览器都支持 ECH GREASE ↩
- 因为 Cloudflare 会为免费计划的用户强制启用 ECH,相当于当小白鼠了 ↩