聊聊那些翻山越岭的神奇操作(上)
前置知识
Deepseek生成,比较简略,详细内容可以查看维基百科相应词条
TCP: 传输控制协议,提供可靠、面向连接的端到端数据传输服务,通过三次握手建立连接,具有流量控制和拥塞控制机制
TLS: 安全传输层协议,为网络通信提供加密传输、身份认证和数据完整性保护,HTTPS 的核心安全基础
TCP和TLS的关系相当于:TCP是快递员,TLS是包裹,封装(加密)了其中的数据
DNS: 域名解析系统,通过分层分布式架构实现域名与IP地址的映射,传输内容为明文
AnyCast: 任播路由技术,将相同IP地址分配给多个节点,基于BGP路由策略将用户请求智能引导至最优服务节点,提升网络服务响应速度与可用性
引入
一般我们聊到翻墙,想到的肯定是通过代理服务器的方式连接被封锁的境外网站,这确实是最常见和最直接的想法。但是,各位有没有想过通过防火长城本身的设计缺陷来完成翻墙的目的呢?相比起前者,这种方法难度更大,而且随时面临这失效的风险(防火长城经过近二十年的发展,许多漏洞已经被堵的死死的了)。但它们真的挺有意思的,(硬说优点的话,或许可以是:不需要额外花钱?)。本文将会探讨其中的一种可行思路,其他思路将会放到下一篇(或者是两篇?)文章中。
历史
这里写的比较简略,想详细了解可自行前往维基百科查询
互联网刚引入中国不久时,人们可以自由地访问境内外的网站。(毕竟当时能上网的人是少数)但随着技术的不断发展,有关部门逐渐意识到,需要通过某种手段对互联网进行管控,过滤掉互联网上不合适的内容。
防火墙刚刚建立的时候,互联网还没有“加密”这个概念,所有信息会通过明文HTTP协议发送,因此审查者只需要在适当的位置(对于GFW,通常是物理链路国内转国际的地方,一般位于北京,上海,广州,下称跨境段)布置审查设备,即可获取到所有流量的详细内容。大约20年前,人们发现维基百科遭到了干扰,具体而言,当访问我国历任领导人相关页面的时候,网页便无法加载,而与此同时其它页面又完全不受影响。这就是因为审查设备在探测到流量中的一些词条时,发送TCP RST数据包强行终止了数据包的传输。
与此同时,审查者还会通过DNS污染和DNS劫持的方式返回某些网站错误的DNS,进而导致客户端获取错误的IP地址,导致无法连接这些网站。
对于一些“行为恶劣”的网站,防火墙还会直接封锁对应的IP,当你访问这些IP时,其数据包会在跨境段被丢弃。
前两种方法十分初级,因此早有成熟的解决方法。对于第一种,随着互联网的不断发展,人们逐渐认识到加密的重要性。随着TLS的广泛应用,明文检测这种审查方法逐渐式微。
而第二种则更加简单,电脑本地存在一个叫做hosts文件的东西,其优先级高于DNS,只需要把被封锁网站的IP地址放入这个文件中,就能绕过污染和劫持访问网站。
对于第三种方法,虽然对封锁的IP并没有任何解决方法,但对于类似于谷歌这种拥有数百万IP的互联网企业,审查者不可能确保封锁每一个IP,总会有漏网之鱼。同时,广泛使用的AnyCast技术也使得审查者在使用这种方法时更加小心谨慎。
随着时代的发展,防火墙也在不断地自我迭代。SNI阻断是相对新兴的审查方式。在建立TLS加密连接之前,客户端会先发送一个Client Hello数据包与服务端建立连接(TLS三次握手的第一次),其中包含了一个明文SNI(Server Name Indicator)字段,内容为访问网站的域名。
也就是说,虽然审查者无法解密后续数据包中的数据,但可以通过这个字段判断用户访问的网站,进而针对被封锁的域名向用户发送RST数据包中断连接。
18年夏,通过修改 hosts 以连接被 GFW 屏蔽的维基百科、Pixiv等网站的方法突然失效。很快人们就反应过来问题的所在:SNI阻断。 在这之前,修改 hosts 是一个几近于零成本的翻墙方法。突然的变化意味着翻墙成本的急剧上升。眼下易于使用的翻墙手段已悉数失效,难度只有使用代理这一条路了吗?
RST
防火墙是怎样强制终止我们与服务器的连接的呢?想知道这一点,我们得简单了解一下TCP报文的组成结构。
在TCP报文中,有数个标识符可以设置,以向客户端或服务端表明连接状态。如下图:
其中SYN和ACK(也就是图中的Acknowledgement)在TCP握手中使用,这张图中的ACK位设置为1,说明这是TCP的第三次握手,连接已经建立,可以开始发送数据了。
先不管TCP握手的详细知识,有没有注意到这张图中有一个名为Reset的标识符,其值为0?
这就是RST标识符,其作用是在服务端接收到错误或非法的数据包时,强制终止连接。与SYN,ACK类似,只要将其值置为1即可。
遗憾的是,TCP握手的流程并没有任何形式的加密(连接都没有建立怎么加密呢?),所以只要监听到了TCP连接的源IP、源端口、目的IP、目的端口(这些信息已经包含在三次握手中),任何人都可以伪装成服务端向我们发送伪造的RST数据包。这也正是防火墙阻断我们连接的技术:在建立TCP连接后,通过识别TLS Client Hello中的SNI信息,判断是否是黑名单网站,若是,则伪装成服务端向我们发送RST数据包,强行中断我们与真正服务端的连接。
这项技术不仅用于防火墙,也广泛用于国内各大云服务厂商,用于阻断未经过ICP备案的域名连接
SNI
我们不妨详细看看SNI这个字段,首先,SNI的作用是什么?
很多时候,一台服务器上可能同时部署有多个网站,同时,对于AnyCast技术,单IP可能对应着许多网站。在这些情况下,服务端需要知道用户究竟想访问哪个网站以便发送正确的证书进行后续的加密。SNI便应运而生,由于这个字段是Client Hello中的明文字段,因此不需要解密数据包即可判断出用户想要访问的网站,进而正确地路由流量。
这里存在着两个有意思的点:
- 从这段文字可以看出来,在单IP对应多网站的情况下才需要SNI字段,也就是说,(理论上)如果一个IP只对应一个网站,SNI字段就不是必须的了。
- 在IETF发布的有关TLS扩展的RFC中,对于SNI的描述是这样的
也就是说,这个扩展其实是可选的,去除这个字段也不会影响TLS协议的完整性(虽然目前所有的客户端都会发送这个扩展)In order to provide any of the server names, clients MAY include an
extension of type “server_name” in the (extended) client hello.
那落实到实践上,可以怎么办呢?
首先,利用某些服务端不检查SNI的特性,我们完全可以人为去除Client Hello信息中的SNI字段,进而绕过审查。最著名的一个网站就是pixiv.net,Pixiv虽然使用了Cloudflare的CDN,这些CDN的IP也确实要求SNI字段,但Pixiv的源服务器可以直接访问且不检查SNI字段。因此只要手动指定Pixiv网站的IP为源服务器的IP并去除SNI字段即可做到“直连”P站,目前一些宣传可以直连Pixiv的应用使用的就是这种技术。
那对于单IP对应多网站的场景,这种方法就完全不可用了吗?也不尽然。SNI只在TLS握手阶段使用以选择正确的TLS证书,建立加密连接后,服务器和CDN提供商会解密并根据使用请求头中的Host字段来路由流量。假如我们将SNI字段设置成该服务器或CDN供应商托管的,未被封锁的网站,而保持HTTP请求头中Host字段为我们想要访问的网站,我们便可以在TLS握手流程中向GFW展示这个正常网站的连接过程,而在结束握手后的加密连接中,通过Host字段使服务器或CDN提供商将流量路由到我们真正想访问的网站。
用一张图表示这个略显复杂的流程:
举个例子,对于Steam社区,其网址steamcommunity.com在GFW的黑名单中,但与其共享同一IP的www.valvesoftware.com的V社官网则不在黑名单中。
因此,当我们想要连接Steam社区的时候,只要把SNI字段的值修改为V社官网的域名,就可以绕过GFW的封锁。
这种修改SNI方法其实有个正式的名称,叫做域前置,以前我一直没理解这个“前置”是什么意思,现在终于明白了,所谓域前置,就是在Client Hello包的SNI字段中,使用一个正常的域名来“前置”(替代)被封锁网站的域名,从而达成混淆审查者的目的。
使用这种方法,我们便能访问一系列被封锁但支持域前置的网站了,例如:
- Twitch
- Discord
- Pixiv
- Epic
- Uplay
- Onedrive
- …
由于修改了SNI字段,GFW针对部分网站的随机丢包策略也随之失效,具体表现为一些连接不稳定的网站也能随时随地稳定连接了,例如Github和Steam。
讲句题外话,各位应该听说过Watt Toolkit(原名Steam++)吧,该软件使用的加速方法(其称之为“本地代理”)就是上文所说的域前置。
抓包
这里还是以steamcommunity.com为例子,使用Wireshark进行抓包分析
我们先模拟一个正常用户的行为,直接在网址栏中输入域名,回车,看看会发生什么
可以看到,这里DNS首先返回了steamcommunity.com的地址,是199.59.148.147,查询可知,这个IP地址归属于Twitter,很明显不是V社的IP,这说明我们遭到了DNS污染
接下来客户端开始TCP握手,向这个错误的IP发送包含SYN的报文以建立连接,不过我们并没有看到来自服务器的ACK确认报文。使用Traceroute工具可知,我们对这个IP发送的数据包在跨境段被丢弃了,因此服务器(无论是真是假)永远都不可能收到我们的连接请求
随后,我们从其他途径获取到steamcommunity.com对应的正确IP,再次进行测试。
可以发现,首先我们顺利完成了TCP三次握手的流程,开始了TLS握手。在发送了包含steamcommunity.comSNI的Client Hello数据包(深蓝色)后,立刻就收到了来自GFW的RST数据包(红色),连接终止,网页显示“连接已重置”。
接下来,我们通过工具修改SNI为人畜无害的www.valvesoftware.com,再加载一遍网页试试,见证奇迹的时刻:
果不其然,在修改完SNI字段之后,我们成功收到了来自服务端的Server Hello数据包,随后顺利完成了TLS握手并建立了加密连接,说明我们确实绕过了GFW的封锁。
缺点
听上去域前置是不是挺厉害?那为什么没有被广泛采用呢?
首先,并不是所有网站都支持域前置,甚至可以说,支持域前置的网站是少数。因为许多大型的CDN提供商,如Cloudflare, Cloudfront, Azure, 都主动拒绝了使用域前置进行的连接。考虑到这几家CDN公司的巨大市场份额,这意味着大部分被封锁的网站都是无法通过域前置进行访问的。
另一方面,域前置涉及到对SNI字段的修改。虽然SNI字段并未加密,但TLS握手结束时,客户端会发送一个Finished消息,其中包含对先前握手信息的校验值。因此,如果直接修改SNI字段信息,服务端会在校验完Finished信息后终止连接。也就是说,在不对浏览器或应用程序源代码进行修改的情况下,需要对数据包进行劫持以达成修改SNI的目的(也就是俗称的MitM)。了解过非对称加密的小伙伴们应该清楚,为了达成这一点,需要让代理软件充当中间人的角色,劫持并对流量进行加解密操作。暂且不提对软件和开发者的信任问题,这需要你的设备信任代理软件生成的根证书,否则现代浏览器都会认为遭到了网络攻击从而拒绝连接。
这不仅提升了操作的复杂度,在部分系统中(如Android),操作系统的受信任根证书存储区也是不被允许的,更不用提部分网站启用的证书透明度和证书锁定技术,都从根本上阻止了通过域前置进行连接。
后记
Watt Toolkit,或者说Steam++的老用户不知道记不记得,曾经这个工具是可以加速Pixiv和Discord的。大约一年前的某次更新中,这两个网站被移除了。
为什么?请看作者的回复:
参考资料
几乎都以超链接形式给出,除此之外还有一些关于TCP报文的相关知识:
https://blog.csdn.net/Chaman1378/article/details/107160327
https://www.cnblogs.com/n0rmally/p/18789097
感谢Grok绘制的两张Mermaid图