互联网档案馆 存档于 2017 年 4 月 21 日:https://web.archive.org/web/20170421092911/http://blog.renren.com/share/200487056/5148854419
时间表
1998 年 9 月 22 日,公安部部长办公会议通过研究,决定在全国公安机关开展全国公安工作信息化工程――” 金盾工程” 建设。
1999 年 4 月 20 日,公安部向国家计委送交金盾工程立项报告和金盾工程项目建议书。
1999 年 6 月,国家计算机网络与信息安全管理中心成立,局级事业单位。
1999-2000 年,在哈尔滨工业大学任教多年的方滨兴调任国家计算机网络与信息安全管理中心副总工程师。
1999 年 12 月 23 日,国务院发文成立国家信息化工作领导小组,国务院副总理吴邦国任组长。其第一下属机构计算机网络与信息安全管理工作办公室设在已经成立的国家计算机网络与信息安全管理中心,取代计算机网络与信息安全管理部际协调小组,对” 公安部、安全部、保密局、商用密码管理办公室以及信息产业部” 等部门的网络安全管理进行组织协调。
2000-2002 年,方滨兴在国家计算机网络与信息安全管理中心任总工程师、副主任、教授级高级工程师。
2000 年 4 月 20 日,公安部成立金盾工程领导小组及办公室。
2000 年 5 月,005 工程开始实施。
2000 年 10 月,信息产业部组建计算机网络应急处理协调中心。
2000 年 12 月 28 日,第九届全国人民代表大会常务委员会第十九次会议通过《关于维护互联网安全的决定》。
2001 年,方滨兴” 计算机病毒及其预防技术” 获国防科学技术三等奖,排名第一。
2001 年,方滨兴获国务院政府特殊津贴、信息产业部” 在信息产业部重点工程中出突出贡献特等奖先进个人” 称号,中组部、中宣部、中央政法委、公安部、民部、人事部等联合授予” 先进个人” 称号。
2001 年 1 月 19 日,国家计算机网络与信息安全管理中心上海分中心成立,位于上海市黄浦区中山南路 508 号 6 楼。国家计算机网络应急技术处理协调中心上海分中心是工业和信息化部直属的中央财政全额拨款事业单位。
2001 年 4 月 25 日,” 金盾工程” 经国务院批准立项。
2001 年 7 月,计算机网络与信息安全管理工作办公室批准哈尔滨工业大学建立国家计算机信息内容安全重点实验室,胡铭曾、方滨兴牵头。
2001 年 7 月 24 日,国家计算机网络与信息安全管理中心广州分中心成立,位于广州市越秀区建中路 2、4 号。
2001 年 8 月 8 日,国家计算机网络与信息安全管理中心组建国家计算机网络应急处理协调中心,缩写 CNCERT/CC。
2001 年 8 月 23 日,国家信息化领导小组重新组建,中央政治局常委、国务院总理朱镕基任组长。
2001 年 11 月 28 日,国家计算机网络与信息安全管理中心上海互联网交换中心成立。提供” 互联网交换服务,互联网骨干网华东地区数据交换,数据流量监测与统计,网间通信质量监督,交换中心设备维护与运行,网间互联费用计算,网间互联争议协调”,位于上海市黄浦区中山南路 508 号。
2001 年 11 月 28 日,国家计算机网络与信息安全管理中心广州互联网交换中心成立,位于广州市越秀区建中路 204 号。
2001 年 12 月,在北京的国家计算机网络与信息安全管理中心综合楼开始兴建。
2001 年 12 月 17 日,国家计算机网络与信息安全管理中心湖北分中心成立。
2002 年,方滨兴任中国科学院计算技术研究所客座研究员、博士生导师、信息安全首席科学家。2002-2006 年,方滨兴在国家计算机网络与信息安全管理中心任主任、总工程师、教授级高级工程师,升迁后任其名誉主任。
2002 年 1 月 25 日,报道称:” 国家计算机网络与信息安全管理中心上海互联网交换中心日前开通并投入试运行,中国电信、中国网通、中国联通、中国吉通等 4 家国家级互联单位首批接入。中国移动互联网的接入正在进行之中,近期可望成为第五家接入单位。”
2002 年 2 月 1 日,国家计算机网络与信息安全管理中心新疆分中心成立。
2002 年 2 月 25 日,国家计算机网络与信息安全管理中心贵州分中心成立。
2002 年 3 月 20 日,多个国家计算机网络与信息安全管理中心省级分中心同时成立。
2002 年 9 月 3 日,Google.com 被封锁,主要手段为 DNS 劫持。
2002 年 9 月 12 日,Google.com 封锁解除,之后网页快照等功能被封锁,手段为 TCP 会话阻断。
2002 年 11 月,经费 6600 万的国家信息安全重大项目” 大范围宽带网络动态阻断系统”(大范围宽带网络动态处置系统)项目获国防科学技术二等奖。云晓春排名第一,方滨兴排名第二。哈尔滨工业大学计算机网络与信息内容安全重点实验室李斌、清华大学计算机系网络技术研究所、清华大学网格计算研究部杨广文有参与。
2003-2007 年,方滨兴任信息产业部互联网应急处理协调办公室主任。
2003 年 1 月 31 日,经费 4.9 亿的国家信息安全重大项目” 国家信息安全管理系统”(005 工程)获 2002 年度国家科技进步一等奖,方滨兴排名第一,胡铭曾排名第二,清华大学排名第三,哈尔滨工业大学排名第四,云晓春排名第四,北京大学排名第五,郑纬民排名第七,中国科学院计算技术研究所有参与。
2003 年 2 月,在北京的国家计算机网络与信息安全管理中心综合楼工程竣工。
2003 年 7 月,国家计算机网络应急处理协调中心更名为国家计算机网络应急技术处理协调中心。
2003 年 9 月 2 日,全国” 金盾工程” 会议在北京召开,” 金盾工程” 全面启动。
2004 年,国家信息安全重大项目” 大规模网络特定信息获取系统”,经费 7000 万,获国家科技进步二等奖。
2005 年,方滨兴任国防科学技术大学兼职教授、特聘教授、博士生导师。
2005 年,方滨兴被遴选为中国工程院院士。
2005 年,” 该系统” 已经在北京、上海、广州、长沙建立了互相镜像的 4 套主系统,之间用万兆网互联。每套系统由 8CPU 的多节点集群构成,操作系统是红旗 Linux,数据库用的是 OracleRAC。2005 年国家计算机网络与信息安全管理中心(北京)就已经建立了一套 384*16 节点的集群用于网络内容过滤(005 工程)和短信过滤(016 工程)。该系统在广州、上海都有镜像,互相以十万兆网链接,可以协同工作,也可以独立接管工作。
2006 年 11 月 16 日,” 金盾工程” 一期在北京正式通过国家验收,其为” 为中华人民共和国公安部设计,处理中国公安管理的业务,涉外饭店管理,出入境管理,治安管理等的工程”。
2007 年 4 月 6 日,国家计算机网络与信息安全管理中心上海分中心机房楼奠基,位于康桥镇杨高南路 5788 号,投资 9047 万元,”…… 是国家发改委批准实施的国家级重大项目,目前全国只有北京和上海建立了分中心,它是全国互联网信息海关,对保障国家信息安全担负着重要作用。”
2007 年 7 月 17 日,大量使用中国国内邮件服务商的用户与国外通信出现了退信、丢信等普遍现象。
2007 年 12 月,方滨兴任北京邮电大学校长。
2008 年 1 月 18 日,信息产业部决定免去方滨兴的国家计算机网络与信息安全管理中心名誉主任、信息产业部互联网应急处理协调办公室主任职务,” 另有职用”。
2008 年 2 月 29 日,方滨兴当选第十一届全国人民代表大会安徽省代表。
2009 年 8 月 10 日,方滨兴在” 第一届中国互联网治理与法律论坛” 上大力鼓吹网络实名制。
机构关系
国家计算机网络与信息安全管理中心(安管中心)是原信产部现工信部的直属部门。
安管中心与国家信息化工作领导小组计算机网络与信息安全管理工作办公室与国家计算机网络应急技术处理协调中心(CNCERT/CC,互联网应急中心)是一个机构几块牌子的关系。比如方滨兴简历中”1999-2000 年在国家计算机网络应急技术处理协调中心任副总工” 与” 计算机网络应急处理协调中心” 的成立时间两种说法就有着微妙的矛盾。实际上几个机构的人员基本一致。安管中心下属互联网交换中心与国家互联网络交换中心是不同的机构。各安管中心省级分中心一般挂靠当地的通信管理局。
安管中心的主要科研力量来自” 哈尔滨工业大学一定会兴盛” 方滨兴当博导有一批学生的哈工大以及关系良好的中科院计算所,这两个机构是那三个国家信息 安全重大项目的主要参与者,之后还在不断吸引人才并为安管中心输送人才和技术。在方滨兴空降北邮之后,往安管中心输血的成分中哈工大的逐渐减少,北邮的逐渐增多。
CNCERT/CC 的国内” 合作伙伴” 有中国互联网协会主办北京光芒在线网络科技有限公司承办的中国互联网用户反垃圾邮件中心,是个没有实权的空壳;国家反计算机入侵及防病毒研究中心、国家计算机病毒应急处理中心,是公安部、科技部麾下;违法和不良信息举报中心是国新办势力范围;国家计算机网络入侵防范中心是中科院研究生院的机构,同样直接支撑 CNCERT/CC。
CNCERT/CC 的应急支撑单位中民营企业最初领跑者是绿盟,后来绿盟因其台谍案被罢黜,启明星辰取而代之。而安管中心具有一些资质认证、准入审 批的行政权力,这可能是民间安全企业趋之若骛的原因。不过,民营企业并未参与到国家信息安全的核心项目建设中,安管中心许多外围项目交给民企外企做,比如 像隔离器之类的访问限制设备外包给启明星辰以作为辅助、备用,或者在与他们在网络安全监测上有所交流。
GFW 与金盾没有关系
敏锐的读者从时间表应该已经看出这样的感觉了。实际上,GFW 与金盾就是没有关系,两者泾渭分明,有很多区别。
公安系统搞网络监控的是公安部十一局
GFW 是 “国家信息关防工程” 的一个子工程,直接上级是国家信息化工作领导小组和信息产业部是政治局亲自抓的国防工程。这个工程主要监测发现有害网站和信息,IP 地址定位,网上对抗信息的上报,跟踪有害短信息和及时进行封堵。 江泽民,朱镕基,胡锦涛,李岚清,吴邦国等多次视察该工程
“国家信息关防工程” 包括 “国家信息安全管理系统 工程代号为 005。还有国家信息安全 016 工程等等
GFW 主要是舆情情报系统的工具,而金盾主要是公安系统的工具。GFW 的总支持者是负责宣传工作的李长春,和张春江 江绵恒 最初的主要需求来自政治局 政法委 安全部 610 办 ;而金盾的总支持者是公安系统的高层人士,主要需求来自公安部门。GFW 主外,作网络海关用;而金盾主内,作侦查取证用。GFW 建设时间短,花费少,成效好;而金盾 建设时间长,花费巨大(GFW 的十倍以上),成效不显著。GFW 依附于三个国家级国际出入口骨干网交换中心从 CRS GSR 流量分光镜像到自己的交换中心搞入侵检测,再扩散到一些放在 ISP 那里的路由封 IP,位置集中,设备数量少;而金盾则是公安内部信息网络,无处不在,数量巨大。GFW 的科研实力雄厚,国内研 究信息安全的顶尖人才和实验室有不少在为其服务,比如哈工大信息安全重点实验室、中科院计算所 软件所 高能所 国防科大总参三部 安全部 9 局 北邮 西电 、 上海交大 北方交大 北京电子科技学院 解放军信息工程学院 解放军装甲兵工程学院 信产部中电 30 所 总参 56 所等等;另外几乎所有 985 211 高校都参与此工程 一些公司商业机构也参与某些外围工程项目如 Websense packeteer BlueCoat 华为 北大方正 港湾 启明星辰 神州数码也提供了一些辅助设备 中搜 奇虎 北京大正 雅虎等等参与了搜索引擎安全管理系统 在某些省市级的网络机房里,接入监控的部门就五花八门了,有安全、公安、纪检、部队,等等部署的设备也是五花八门 正规军 杂牌军 洋外援各自为战.
而金盾的科研实力较弱,公安系统的公安部第三研究所信息网络安全研发中心、国家反计算机入侵与防病毒研究中心都缺乏科研力量和科研成果,2008 年 8 月成立信息网络安全公安部重点实验室想 与哈工大的重点实验室抗衡,还特意邀请方滨兴来实验室学术委员会,不过这个实验室光是电子数据取证的研究方向就没什么前景,而且也没什么研究成果。GFW 之父方滨兴没有参与金盾工程,而工程院里在支持金盾工程的是沈昌祥;实际上那个公安部重点实验室的学术委员会名单很是有趣,沈昌祥自然排第一,方滨兴因为最近声名太显赫也不好意思不邀请他,方滨兴可能也有屈尊与公安系统打好关系的用意。
GFW 发展和状况
GFW 主要使用的硬件来自曙光和华为,没有思科、Juniper,软件大部为自主开发。原因很简单,对国家信息安全基础设施建设,方滨兴在他最近的 讲话《五个层面解读国家信息安全保障体系》中也一直强调” 信息安全应该以自主知识产权为主”。何况 GFW 属于保密的国防工程而且 GFW 没有闲钱去养洋老爷,肥水不流外人田。李国杰是工程院信息工程部主任、曙光公司董事长、中科院计算所所长,GFW 的大量服务器设备订单都给了曙光。方滨兴还将安管中心所需的大型机大订单给李国杰、国防科大卢锡城、总参 56 所陈左宁三位院士所在单位各一份。所以 GFW 为什么那么多曙光的设备,GFW 为什么那么多中科院计算所的科研力量,为什么方滨兴成为 中科院计算所和国防科大都有显赫的兼职,为什么方滨兴从老家哈尔滨出来打拼短短 7 年时间就入选工程院卢浮宫?就是因为方滨兴头脑灵活,做事皆大欢喜。
网上有人讽刺 GFW 夜郎自大,事实上这是盲目乐观,无知者无畏。GFW 的技术是世界顶尖的,GFW 集中了哈工大、中科院、北邮货真价实的顶尖人才, 科研力量也是实打实地雄厚,什么动态 SSL Freenet VPN SSH TOR GNUnet JAP I2P Psiphon 什么 Feed Over Email 算什么葱。所有的翻墙方法,只要有人想得到,GFW 都有研究并且有反制措施的实验室方案储备。
比如说:串接式封堵 采用中间人攻击手段来替换加密通信双方所用的没有经过可信赖 CA 签名保护的数字证书网关 / 代理间的证书协调,在出口网关上进行解密检测也就是所谓深度内容检测 七层过滤 HTTPS 是需要认证的。客户端访问服务器时,服务器端提供 CA 证书,但有些实现也可以不提供 CA 证书那么对于不提供 CA 证书的服务器,防火墙处理很简单,一律屏蔽掉另外检测默认的 CA 发证机构,如果证书不是这些机构(Verisign、Thawte Geotrust)发的,杀无赦就是在客户端与服务器端进行 https 握手的阶段,过滤掉一切无 CA 证书或使用不合法 CA 证书的 https 请求。这一步是广谱过滤,与服务器的 IP 地址无关。
GFW 主要是入侵防御系统,检测 - 攻击两相模型。#
所有传输层明文的翻墙方案,检测然后立即进行攻击是很容易的事情;即使传输层用 TLS 之类的加密无法实时检测,那种方案面向最终用户肯定是透明的,谁也不能阻止 GFW 也作为最终用户来静态分析其网络层可检测特征。
入侵检测然后 TCP 会话重置攻击算是干净利落的手段了,最不济也能通过人工的方式来查出翻墙方 法的网络层特征(仅仅目标 IP 地址就已经足够)然后进行定点清除。
如果是一两个国家的敌人,GFW 也能找到集群来算密钥。GFW 是难得能有中央财政喂奶的科研项目。那些在哈工大地下室、中科院破楼里的穷研究生即使没有钱也能搞出东西来,现在中央财政喂奶,更是干劲十足了。
GFW 什么都行,就是 P2P 没办法,因为匿名性太好了,既不能实时检测出来,也无法通过静态分析找到固定的、或者变化而可跟踪的网络层特征。就这样也能建两个陷阱节点搞点小破坏,而且中科院的 242 项目”P2P 协议分析与测量” 一直都没停。什么时候国外开学术会议还是 Defcon 谁谁发一篇讲 Tor 安全性的 paper,立即拿回来研究一番实现一下,已然紧跟学术技术最前沿了。不过实际上,即使 GFW 这样一个中国最顶尖的技术项目也摆脱不了山寨的本性,就是做一个东西出来很容易,但是要把东西做细致就不行了。
不过可能有人就疑问,为什么 GFW 什么都能封但又不真的封呢?我的这个翻墙方法一直还是好好的嘛。其实 GFW 有它自己的运作方式。GFW 从性质上讲 是纯粹的科研技术部门,对政治势力来说是一个完全没有主观能动性的工具。GFW 内部有很严格权限管理,技术与政治封装隔离得非常彻底。封什么还是解封什 么,都是完全由上峰决定,党指挥枪,授权专门人员操作关键词列表,与技术实现者隔离得很彻底,互相都不知道在做什么。所以很多时候一些莫名其妙的封禁比如 封 freebsd.org 封 freepascal.org(可能都联想到 freetibet.org),或者把跟轮子的 GPass 八杆子打不着的”package.debian.org/zh-cn/lenny/gpass” 列为关键词,都是那些摆弄着 IE6 的官僚们的颐指气使,技术人员要是知道了都得气死。
方滨兴在他最近的讲话《五个层面解读国家信息安全保障体系》中讲一个立足国情的原则,说:” 主要是强调综合平衡安全成本与风险,如果风险不大就没有必要花太大的安全成本来做。在这里面需要强调一点就是确保重点的,如等级保护就是根据信息系统的重要性来定级,从而施加适当强度的保护。”
所以对于小众的翻墙方式,GFW 按照它的职能发现了也就只能过一下目心里有个底,上峰根本都不知道有这么一种方式所以也根本不会去封、GFW 自己也没权限封,或者知道了也懒得再花钱花精力去布置。枪打出头鸟,什么时候都是这样。
目前的状况是对于敏感数据能通过封锁基本上就是安全的,否则就被过滤掉了,对于庞大的网络数据用人来分析是不可能的,敏感数据只能基于过滤技术根据数据流里面的一些特征来发现,目前的解密技术对于庞大数据流量和加密技术想使用解密的方法是不可能实现的,只要加密数据流没有可识别的特征,过滤技术就不会有任何记录和反映,因此过滤技术是无法真正实现网络封锁的,因此必需加入新的参数,它们选择了量,即保存你的一段时间的数据。
现在的破网方法用的比较多得是动态网,无界,花园,等等,由于接点相对来说是有限的和可知的,因此保存一段时间的数据就有了意义,由于使用破网软件的人很多,不可能人人都抓,可以根据量来区分出重点,和经常使用破网软件的人,当然你可已通过代理来连接这些可知接点来解决这个问题,破网软件也提供了这样的方法,但是通过代理联接可知的接点的请求还是可能被截获的方滨兴一个人把 GFW 崛起过程中的政治势能全部转化为他的动能之后就把 GFW 扔掉了。
现在 GFW 是平稳期,完全是清水衙门,既没有什么后台,也无法 再有什么政治、资金上的利益可以攫取,也无法再搞什么新的大型项目,连 IPv6 对 GFW 来说都成了一件麻烦事情。方滨兴在他最近的讲话《五个层面解读国家信息安全保障体系》中也感慨道:” 比如说 Web 2.0 概念出现后,甚至包括病毒等等这些问题就比较容易扩散,再比如说 IPv6 出来之后,入侵检测就没有意义了,因为协议都看不懂还检测什么……”
GFW 一直就没有地位,一直就是一个没人管的萝莉,国新办、网监、广电、版权、通管局之类的怪蜀黍都压在上面要做这做那。所以方滨兴在他最近的讲话《五个层面解读国家信息安全保障体系》中也首先强调一个机制,” 需要宏观层面,包括主管部门予以支持。” 所以,想解封网站,不要去找 GFW 本体,那没用,要去找 GFW 的上峰,随便哪个都行。而 ISP 就根本跟 GFW 没关系了,都不知道 GFW 具体搞些什么,起诉 ISP 完全属于没找到脉门。
不过 GFW 现在还是运行得很好,工作能力还有很大潜力可挖,唯一害怕的就是 DDoS 死撞墙。GFW 的规模在前面的时间表里也有数字可以估计,而且 GFW 现在的网站封禁列表也有几十万条之多。网络监控和对 MSN YMSG ICQ 等 IM 短信监控也都尽善尽美。GFW 在数据挖掘和协议分析上做的还比较成功多媒体数据如音频 视频 图形图像的智能识别分析 自然语言语义判断识别模式匹配 p2p VoIP IM 流媒体 加密内容识别过滤 串接式封堵 等等是将来的重点不过 GFW 也没有像机器学习之类的自组织反馈机制来自动生成关键词,因为它 本身没有修改关键词的权限,所以这种技术也没必要,况且国内这种技术也是概念吹得多论文发得多实践不成熟。现在 GFW 和金盾最想要的就是能够从万草从中揪出一小撮毒草的数据挖掘之类的人工智能技术。
方滨兴在他最近的讲话《五个层面解读国家信息安全保障体系》中提到” 舆情驾驭核心能力”,” 首先要能够发现和获取,然后要有分析和引导的能力”。怎么发现?就靠中科院在研的 973 课题” 文本识别及信息过滤” 和 863 重点项目” 大规模网络安全事件监控” 这种项目。
金盾工程花大钱搞出来,好评反而不如 GFW,十一局的干警们脸上无光无法跟老一辈交代啊。公安系统的技术力量跟 GFW 没法比,不过公安系统有的是钱,随便买个几十万个摄像头几万台刀片几十 PB 硬盘接到省市级网络中心,把什么东西都记录下来。问题是记下来不能用,只能靠公安干警一页一页地翻 Excel。所以说,虽然看起来 GFW 千疮百孔,金盾深不可测,只是因为公安部门比起 GFW 来比较有攻击性,看到毒草不是给你一个 RST 而是给你一张拘留证。反而是 GFW 大多数时候都把毒草给挡住了,而大多数毒草金盾都是没发现的。
国家信息安全话语范式
境外网上而来的大量网络宣传让从未有过网络化经验的中央无所适从、毫无办法、十分着急。这些东西对中央来说都是难以忍受的安全威胁,为这些威胁又发生在网上,自然国家网络安全就被提上了首要议程。适逢信息化大潮,电子政务概念兴起,中央下决心好好应对信息化的问题,于是就成立了国家信息化工作领导小 组。我们可以看到,首批组成名单中,安全部门和宣传部门占了大多数席位,而且其第一下属机构就是处理安全问题,第二下属才是处理信息化改革,安全需求之强 烈,可见一斑。正是这个时候,一贯对信息安全充满独到见解的方滨兴被信产部的张春江调入了安管中心练级。方滨兴对信息安全的见解与高层对网络安全的需求不谋而合。
方滨兴在他最近的讲话《五个层面解读国家信息安全保障体系》中说:” 一定要有一个信息安全法,有了这个核心法你才能做一系列的工作。” 国家信息安全体系的首要核心就是以信息安全为纲的法律保障体系,通过国家意志――法律来定义何谓” 信息安全”。信息安全本来是纯技术、完全中性的词语,通过国家意志的定义,将” 煽动… 煽动… 煽动… 煽动… 捏造… 宣扬… 侮辱… 损害… 其他…” 定义为所谓的网络攻击、网络垃圾、网络有害信息、网络安全威胁,却在实现层面完全技术性、中立性地看待安全,丝毫不考虑现实政治问题。这样既在技术上实现完备的封装,也给了用户以高可扩展性的安全事件定义界面。对国家安全与技术安全实现充满隐喻的捆绑,对意识形态与信息科学进行牢不可破的焊接,这就是方滨兴带给高层的开拓性思维,这就是方滨兴提出的国家信息安全话语范式。
这个话语范式是如此自然、封装得如此彻底,以至于几乎所有人都没有意识到中国的网络化发展出现了怎样严重的问题。几乎所有网民都没有意识到,给他们带来巨大麻烦和沮丧的 GFW 竟然是本来应该为网民打黑除恶的国家互联网应急响应中心;几乎所有网民都没有意识到,自己在网上某处的一亩三分地修剪花草对于国家来说竟然是网络安全攻击事件;几乎所有决策者都没有意识到,那个看似立竿见影的防火墙实际上具有怎样强大的副作用、会给互联网发展带来怎样大的伤害;几乎所有决策者都没有意识到,使用 GFW 这样专业的安全工具来进行网络封锁意味着什么。意识形态面对网络化这样变幻莫测的景色无法忍受,就只能用眼罩封闭住眼睛。
在讨论网络化的中文理论文本中,摆到首要位置占据最多篇幅的便是网络安全和网络威胁。国家信息化工作领导小组第一下属机构便是处理安全问题。这样,在网络本身都没有发展起来的时候,就在理论上对网络进行种种限制和控制;在网络仍然自发地成长起来以后,便在文化上对网络进行系统性妖魔化,在地理上 对网络中国进行闭关锁国。更严重的是,在根本不了解技术本质和副作用的情况下使用国家信息安全工具,就像一个不懂事的小孩把玩枪械。在维护安全的话语之下,决策者根本不知道使用 GFW 进行网络封锁就是在自己的网络国土上使用军队进行镇压,切断网线就是在自己的网络国土上使用核武器。
更悲哀的是,GFW 的建设者们大多都没有意识到他们在做的究竟是什么事情,在签订保密协议之后就无意识中投身党国事业滚滚长江东逝水。像云晓春这种 跟着方滨兴出来打江山的,方滨兴倒是高飞了,云晓春们就只能鞠躬尽瘁干死技术,在安管中心反而被王秀军、黄澄清之辈后来居上。而当初在哈工大跟着方滨兴的穷研究生们,最后也陆陆续续去了百度之类的公司。GFW 面临与曼哈顿工程一样的伦理困局。科学本是中立的,但科学家却被政治摆弄。技术工作者们只关心也只被允许关心如何实现安全,并不能关心安全的定义到底如何。他们缺乏学术伦理精神,不能实践” 对自己工作的一切可能后果进行检验和评估;一旦发现弊端或危险,应改变甚至中断自己的工作;如果不能独自做出抉择,应暂缓或中止相关研究,及时向社会报警” 的准则。结果就算他们辛辛苦苦做研究却也不能造福民生,反而被扣上 “扼杀中国人权”,“纳粹帮凶” 的帽子,不可谓不是历史的悲哀。
这种话语范式浸透了社会的方方面面。在这种话语之下,中国有了世界上最强大的防火墙,但中国的网络建设却远远落后于世界先进水平;中国有了世界上最庞大的网瘾治疗产业链,但中国的网络产业却只会山寨技;中国有了世界上最多的网民,但在互联网上却听不见中国的声音。GFW 已经实现了人们的自我审查,让人们即使重获自由也无法飞翔,完成了其根本目的。现在即使对 GFW 的 DDoS 的技术已经成熟,然而推倒墙却也变得没有意义,只能让公安系统的金盾得势,更 多的网民被捕,最终新墙竖起。这一切都出自意识形态化现代性与网络化后现代性之间巨大断裂,以及 “国家信息安全话语” 这种致命的讳疾忌医。
GFW 实体是安管中心(CNCERT/CC),是事业单位。一个事业单位的政治地位可以想是很低的,凡是它的上级都可以向它下指令进行网络封锁,而它本身则没有任何主观能动性,专于业务,勤勤恳恳抓好国家安全基础设施的建设。如果转换角度,从 GFW 的视角来考虑,那么 GFW 所做的事情其实并不神秘,其实恰恰符合了它自己的名义。我们来看看精美的 “宽带网络环境下恶意代码监测系统”,你们访问一下 blogger、wordpress 那就是 URL 攻击啊,而且要比一般的钓鱼攻击和病毒更严重,因为危害的是党和国家的安全啊。GFW 的研发也与一般的网络安全公司所做别无二致,监视分析网络流量,逆向工程有害软件,阻断恶意攻击,区别只在于:GFW 为了国家安全也要处理一下你们这些反革命的攻击;另外 GFW 还开了金钱无限、人口无限和无敌秘笈。
GFW 与网民的生态系统
政府、GFW 与网民构成了一个生态系统,政府与 GFW 共生于食物链的上端,网民处于食物链的下端。有人称之为 “猫和老鼠的游戏”。观 GFW 与网民的互动历史,可以归结为相互提升技术水准的军备竞赛,但尽管如此两者的关系也从来没有打破 GFW 越来越善于主动追捕网民越来越善于被动逃避的模式。从最开始的普通 HTTP 代理、SOCKS 代理,到加密代理软件,到种类繁多的网页代理,到 VPN、SSH 代理,到 p2p 网络,以及混合方法。然而这些方法都未曾彻底免于 GFW 的封锁,这是因为 GFW 很善于进攻,而网民们迄今为止只会不断地四处寻找新的逃避办法。
这种互动模式的问题在于,随着军备竞赛的继续,GFW 越来越完善越来越强大,而网民不断地失去手中的牌,翻墙的难度和成本越来越高。GFW 是这个领域(网络安全)的专业人士,而网民虽然富有群体智慧,但是其技术能力缺乏有效组织不能与 GFW 对等。因此如果稍微看得远一些就会了解到,这种模式对于网民来说是不可持续的,总有一天 GFW 会超过绝大多数网民的技术基准线。所以,唯一的出路便是改变方式,突破这种模式。
应对 GFW#
网民突破当前被动态势方法的基本原理,在后面的章节中我们会看到,在于利用 GFW 在善于进攻的同时不善于防守的特点。与其把 GFW 看作国家网络暴力机关,不如把 GFW 看作一个网络安全机构,事实上它也是一个网络安全机构(CNCERT/CC)。任何安全系统必然都有漏洞和弱点,它所提供的这个 GFW 安全解决方案(国家信息安全管理系统)也不例外。网民并非缺乏技术,而是技术没有得到有效组织,没有往这个方向进行有效投射。实际上 GFW 漏洞和弱点并不少,有一些甚至是理论上无法解决的,这在以后会详细论述。正如《阅后即焚》一文所言,GFW 尽管是中国少有的顶尖科研力量与国家强力支持结合的产物,“但也无法摆脱山寨的本性 —— 做一个东西出来很容易,但是要把这个东西做得细致严格就不行了”。
更进一步,除了利用 GFW 本身的问题以外,网民甚至还可以考虑采取网络正当防卫的方式阻止 GFW 的非法行为。像 GFW 这种机构,无行政立法,无舆论监督,无申诉渠道,被少数别有用心的人利用,滥用国家安全之名义,封锁与国家安全毫无关系的网站,对合法的网络通信进行干扰和攻击,“对计算机信息系统正在进行传输的合法数据进行篡改,向计算机信息系统进行攻击令其无法正常运行”,甚至曾多次造成全国性网络故障,已触犯《中华人民共和国刑法》第二百八十六条,情节特别严重,后果特别恶劣,等等。
然而另一方面,GFW 与网民之间已经或者即将形成某种稳态,这种稳态是双方斗争状况下的动态平衡,是需要有意识维护的。一个无法控制的网络是无法被政府所容忍的,当网络无法控制时政府是不吝于切断一切网络的(你一定知道我在说什么),稳态的破坏也就意味着环境的毁灭。一个理想的稳态就是网络处于 “看起来” 可以控制的状态,让 GFW 处于不断取得小型封锁成功的虚幻胜利感之中,网民个人各自掌握非中心化的翻墙方法。一个中心化的大众翻墙方法(最典型的例子就是设置 hosts 静态解析)必定无法避免被当局发现并被 GFW 封锁。下一代的翻墙方法应该是去中心化的(p2p)、小众的、多样化的、混合型的、动态更新的。
参考文献#
有两篇重要文献在这里要推荐。
首先是 Thomas Ptacek 等在 98 年发表的Insertion, Evasion, and Denial of Service: Eluding Network Intrusion Detection
初识 DNS 污染#
DNS(Domain Name System)污染是 GFW 的一种让一般用户由于得到虚假目标主机 IP 而不能与其通信的方法,是一种 DNS 缓存投毒攻击(DNS cache poisoning)。其工作方式是:对经过 GFW 的在 UDP 端口 53 上的 DNS 查询进行入侵检测,一经发现与关键词相匹配的请求则立即伪装成目标域名的解析服务器(NS,Name Server)给查询者返回虚假结果。由于通常的 DNS 查询没有任何认证机制,而且 DNS 查询通常基于的 UDP 是无连接不可靠的协议,查询者只能接受最先到达的格式正确结果,并丢弃之后的结果。对于不了解相关知识的网民来说也就是,由于系统默认使用的 ISP 提供的 NS 查询国外的权威服务器时被劫持,其缓存受到污染,因而默认情况下查询 ISP 的服务器就会获得虚假 IP;而用户直接查询境外 NS(比如 OpenDNS)又可能被 GFW 劫持,从而在没有防范机制的情况下仍然不能获得正确 IP。然而对这种攻击有着十分简单有效的应对方法:修改 Hosts 文件。但是 Hosts 文件的条目一般不能使用通配符(例如 *.blogspot.com),而 GFW 的 DNS 污染对域名匹配进行的是部分匹配不是精确匹配,因此 Hosts 文件也有一定的局限性,网民试图访问这类域名仍会遇到很大麻烦。
观测 DNS 污染
“知己知彼,百战不殆”。这一节我们需要用到前面提到的报文监听工具,以及参考其 DNS 劫持诊断一节。在 Wireshark 的 filter 一栏输入 udp.port eq 53 可以方便地过滤掉其他无关报文。为了进一步减少干扰,我们选择一个并没有提供域名解析服务的国外 IP 作为目标域名解析服务器,例如 129.42.17.103。运行命令 nslookup -type=A www.youtube.com 129.42.17.103。如果有回答,只能说明这是 GFW 的伪造回答,也就是我们要观测和研究的对象。
伪包特征
经过一番紧密的查询,我们可以发现 GFW 返回的 IP 取自如下列表:
4.36.66.178
203.161.230.171
211.94.66.147
202.181.7.85
202.106.1.2
209.145.54.50
216.234.179.13
64.33.88.161
关于这八个特殊 IP,鼓励读者对这样两个问题进行探究:1、为什么是特定的 IP 而不是随机 IP,固定 IP 和随机 IP 各自有什么坏处;
2、为什么就是这 8 个 IP 不是别的 IP,这 8 个 IP 为什么倒了 GFW 的霉?关于搜索这类信息,
除了 www.google.com 之外,www.bing.com 有专门的搜索 IP 对应网站的功能,使用方法是输入 ip地址搜索。
www.robtex.com 则是一个专门收集域名解析信息的网站。欢迎读者留下自己的想法和发现。
从 Wireshark 收集到的结果分析(实际上更好的办法是,将结果保存为 pcap 文件,或者直接使用 tcpdump,由 tcpdump 显示成文本再自行提取数据得到统计),我们将 GFW 发送的 DNS 污染包在 IP 头部的指纹特征分为两类:
一型:
ip_id == ____(是一个固定的数,具体数值的查找留作习题)。
没有设置 “不分片” 选项。
没有设置服务类型。
对同一对源 IP、目标 IP,GFW 返回的污染 IP 在上述 8 个中按照给出的顺序循环。与源端口无关、与源 IP 目标 IP 对相关。
TTL 返回值比较固定。TTL 为 IP 头部的 “Time to Live” 值,每经过一层路由器这个值会减 1,TTL 为 1 的 IP 包路由器将不再转发,多数路由器会返回源 IP 一条 “ICMP time to live exceed in transit” 消息。
二型:
每个包重复发送 3 次。
没有设置 “不分片” 选项。
设置了 “保障高流量” 服务类型。
(ip_id + ? * 13 + 1) % 65536 == 0,其中?为一个有趣的未知数。ip_id 在同一个源 IP、目标 IP 对的连续查询之间以 13 为单位递减、观测到的 ip_id 的最小值和最大值分别为 65525(即 - 11,溢出了!)和 65535。
对同一对源 IP、目标 IP,GFW 返回的污染 IP 在上述 8 个中按照给出的顺序循环。与源端口无关、与源 IP 目标 IP 对相关。
对同一对源 IP、目标 IP,TTL 返回值时序以 1 为单位递增。TTL 在 GFW 发送时的取值有 64 种。注:源 IP 接收到的包的 TTL 被路由修改过,所以用户观测到的 TTL 不一定只有 64 种取值,这是由于网络拓扑变化的原因导致的。一型中的 “比较固定” 的 “比较” 二字也是考虑到网络拓扑偶尔的变化而添加的,也许可以认为 GFW 发送时的初始值是恒定的。
(以上结果仅保证真实性,不保证时效性,GFW 的特征随时有可能改变,尤其是时序特征与传输层特征相关性方面。最近半年 GFW 的特征在很多方面的变化越来越频繁,在将来介绍 TCP 阻断时我们会提到。)
还可以进行的实验有:由于当前二型的 TTL 变化范围是 IP 个数的整数倍,通过控制 DNS 查询的 TTL 使得恰好有 GFW 的返回(避免动态路由造成的接收者观察到的 TTL 不规律变化),观察 IP 和 TTL 除以 8 的余数是否有对应关系,在更改源 IP、目标 IP 对之后这个关系是否仍然成立。这关系到的 GFW 负载平衡算法及响应计数器(hit counter)的独立性和一致性。事实上对 GFW 进行穷举给出所有关于 GFW 的结果也缺乏意义,这里只是提出这样的研究方法,如果读者感兴趣可以继续探究。
每次查询通常会得到一个一型包和三个完全相同的二型包。更换查询命令中 type=A 为 type=MX 或者 type=AAAA 或者其它类型,可以看到 nslookup 提示收到了损坏的回复包。这是因为 GFW 的 DNS 污染模块做得十分粗制滥造。GFW 伪造的 DNS 应答的 ANSWER 部分通常只有一个 RR 组成(即一条记录),这个记录的 RDATA 部分为那 8 个污染 IP 之一。对于二型,RR 记录的 TYPE 值是从用户查询之中直接复制的。于是用户就收到了如此奇特的损坏包。DNS 响应包的 UDP 荷载内容特征:
一型
DNS 应答包的 ANSWER 部分的 RR 记录中的域名部分由 0xc00c 指代被查询域名。
RR 记录中的 TTL 设置为 5 分钟。
无论用户查询的 TYPE 是什么,应答包的 TYPE 总是设置为 A(IPv4 地址的意思)、CLASS 总是设置为 IN。
二型
DNS 应答包的 ANSWER 部分的 RR 记录中的域名部分是被查询域名的全文。
RR 记录中的 TTL 设置为 1 天。
RR 记录中的 TYPE 和 CLASS 值是从源 IP 发送的查询复制的。
其中的术语解释:RR = Resource Record:dns 数据包中的一条记录;RDATA = Resource Data:一条记录的数据部分;TYPE:查询的类型,有 A、AAAA、MX、NS 等;CLASS:一般为 IN [ternet]。
触发条件
实际上 DNS 还有 TCP 协议部分,实验发现,GFW 还没有对 TCP 协议上的 DNS 查询进行劫持和污染。匹配规则方面,GFW 进行的是子串匹配而不是精确匹配,并且 GFW 实际上是先将域名转换为字符串进行匹配的。这一点值得特殊说明的原因是,DNS 中域名是这样表示的:一个整数 n1 代表以 “.” 作分割最前面的部分的长度,之后 n1 个字母,之后又是一个数字,若干字母,直到某次的数字为 0 结束。例如 www.youtube.com 则是 "\x03www\x07youtube\x03com\x00"。因此,事实上就可以观察到,对 www.youtube.coma 的查询也被劫持了。
现状分析
4.36.66.178,关键词。whois:Level 3 Communications, Inc. 位于 Broomfield, CO, U.S.
203.161.230.171,关键词。whois:POWERBASE-HK 位于 Hong Kong, HK.
211.94.66.147,whois:China United Network Communications Corporation Limited 位于 Beijing, P.R. China.
202.181.7.85,关键词。whois:First Link Internet Services Pty Ltd. 位于 North Rocks, AU.
202.106.1.2,whois:China Unicom Beijing province network 位于 Beijing, CN.
209.145.54.50,反向解析为 dns1.gapp.gov.cn,新闻出版总署的域名解析服务器?目前 dns1.gapp.gov.cn 现在是 219.141.187.13 在 bjtelecom。whois:World Internet Services 位于 San Marcos, CA, US.
216.234.179.13,关键词。反向解析为 IP-216-234-179-13.tera-byte.com。whois:Tera-byte Dot Com Inc. 位于 Edmonton, AB, CA.
64.33.88.161,反向解析为 tonycastro.org.ez-site.net, tonycastro.com, tonycastro.net, thepetclubfl.net。whois:OLM,LLC 位于 Lisle, IL, U.S.
可见上面的 IP 大多数并不是中国的。如果有网站架设到了这个 IP 上,全中国的 Twitter、Facebook 请求都会被定向到这里 —— 好在 GFW 还有 HTTP URL 关键词的 TCP 阻断 ——HTTPS 的请求才构成对目标 IP 的实际压力,相当于中国网民对这个 IP 发起 DDoS 攻击,不知道受害网站、ISP 是否有索赔的打算?
我们尝试用 bing.com 的 ip 反向搜索功能搜索上面那些 DNS 污染专用 IP,发现了一些有趣的域名。显然,这些域名都是 DNS 污染的受害域名。
例如倒霉的 edoors.cn.china.cn,宁波中国门业网,其实是因为 edoors.cn 被 dns 污染。一起受害的 * 还有 chasedoors.cn.china.cn,美国蔡斯门业(深圳)有限公司。
还有 *.sf520.com,似乎是一个国内的游戏私服网站。www.sf520.com 也是一个私服网站。可见国内行政体系官商勾结之严重,一个 “国家信息安全基础设施” 竟然还会用来保护一些网游公司的利益。
此外还有一些个人 blog。www.99tw.net 也是一个游戏网站。
还有 www.why.com.cn,名字起得好。
还有 www.999sw.com 广东上九生物降解塑料有限公司生物降解树脂 | 增粘母料 | 高效保水济 | 防洪 邮编:523128…… 这又是怎么一回事呢?不像是被什么反动网站连坐的。还有人问怎么回事怎么会有那么多 IP 结果。
www.facebook.comwww.xiaonei.com,怎么回事呢?其实是因为有人不小心把两个地址连起来了,搜索引擎以为这是一个链接,其实这个域名不存在,但是解析的时候遭到了污染,就以为存在这个域名了。
倒霉的 www.xinsheng.net.cn—— 武汉市新胜电脑有限公司,因为 www.xinsheng.net 被连坐。
DNS 劫持的防范和利用
之前我们已经谈到,GFW 是一套入侵检测系统,仅对流量进行监控,暂没有能力切断网络传输,其 “阻断” 也只是利用网络协议容易被会话劫持(Session hijacking)的弱点来进行的。使用无连接 UDP 的 DNS 查询只是被 GFW 抢答了,真正的答案就跟在后面。于是应对 GFW 这种攻击很自然的想法就是:
根据时序特性判断真伪,忽略过早的回复。
通常情况对于分别处于 GFW 两端的 IP,其 RTT(Round-trip time,往返延迟)要大于源 IP 到 GFW 的 RTT,可以设法统计出这两个 RTT 的合适的均值作为判断真伪的标准。另外由于 GFW 对基于 TCP 的 DNS 请求没有作处理,于是可以指定使用 TCP 而不是 UDP 解析域名。也可以通过没有部署 GFW 的线路到没有被 DNS 污染的 NS 进行查询,例如文章一开始提到的 “远程解析”。但黑体字标出的两个条件缺一不可,例如网上广为流传的 OpenDNS 可以反 DNS 劫持的说法是以讹传讹,因为到 OpenDNS 服务器的线路上是经由 GFW 的。
本质的解决办法是给 DNS 协议增加验证机制,例如 DNSSEC(Domain Name System Security Extensions),客户端进行递归查询(Recursive Query)而不查询已经被污染了的递归解析服务器(Recursive/caching name server)。然而缺点是目前并非所有的权威域名解析服务器(Authoritative name server)都支持了 DNSSEC。Unbound 提供了一个这样的带 DNSSEC 验证机制的递归解析程序。
另外 GFW 的 DNS 劫持还可能被黑客利用、带来对国际国内互联网的严重破坏。一方面,GFW 可能在一些紧急时刻按照 “国家安全” 的需要对所有 DNS 查询都进行污染,且可能指定污染后的 IP 为某个特定 IP,使得全球网络流量的一部分直接转移到目标网络,使得目标网络立刻瘫痪。当然我们伟大的祖国郑重承诺 “不率先使用核武器”… 另一方面,GFW 将伪造的 DNS 返回包要发送给源 IP 地址的源端口,如果攻击者伪造源 IP,会怎样呢?将会导致著名的增幅攻击:十倍于攻击者发送 DNS 查询的流量将会返回给伪源 IP,如果伪源 IP 的端口上没有开启任何服务,很多安全配置不严的系统就需要返回一条 ICMP Port Unreachable 消息,并且将收到的信息附加到这条 ICMP 信息之后;如果伪源 IP 的端口上开启了服务,大量的非法 UDP 数据涌入将使得伪源 IP 该端口提供的服务瘫痪。如果攻击者以 1Gbps 的速度进行查询,一个小型 IDC(DNSpod 被攻击事件)甚至一个地域的 ISP 也会因此瘫痪(暴风影音事件)。攻击者还可能设置 TTL 使得这些流量恰好通过 GFW 产生劫持响应,并在到达实际目标之前被路由丢弃,实现流量 “空对空不落地”。攻击者还可能将攻击流量的目标 IP 设置伪造成与伪源 IP 有正常通信或者其他关联的 IP,更难以识别。这样实际上就将一个国家级防火墙变成了一个国家级反射放大式拒绝服务攻击跳板。
最为严重的是,这种攻击入门难度极低,任何一个会使用 C 语言编程的人只要稍微阅读 libnet 或者 libpcap 的文档,就可能在几天之内写出这样的程序。而 GFW 作为一套入侵防御系统,注定缺乏专门防范这种攻击的能力,因为如果 GFW 选择性忽略一些 DNS 查询不进行劫持,网民就有机可乘利用流量掩护来保证真正的 DNS 通信不被 GFW 污染。尤其是 UDP 这样一种无连接的协议,GFW 更加难以分析应对。“反者道之动,弱者道之用。”
参考文献#
闫伯儒,方滨兴,李斌,王垚. "DNS 欺骗攻击的检测和防范". 计算机工程,32 (21):130-132,135. 2006-11.
Graham Lowe, Patrick Winters, Michael L. MarcusThe Great DNS Wall of China
KLZ 毕业. 入侵防御系统的评测和问题
FW 的重要工作方式之一是在网络层的针对 IP 的封锁。事实上,GFW 采用的是一种比传统的访问控制列表(Access Control List,ACL)高效得多的控制访问方式 —— 路由扩散技术。分析这种新的技术之前先看看传统的技术,并介绍几个概念。
访问控制列表(ACL)#
ACL 可以工作在网络的二层(链路层)或是三层(网络层),以工作在三层的 ACL 为例,基本原理如下:想在某个路由器上用 ACL 控制(比如说是切断)对某个 IP 地址的访问,那么只要把这个 IP 地址通过配置加入到 ACL 中,并且针对这个 IP 地址规定一个控制动作,比如说最简单的丢弃。当有报文经过这个路由器的时候,在转发报文之前首先对 ACL 进行匹配,若这个报文的目的 IP 地址存在于 ACL 中,那么根据之前 ACL 中针对该 IP 地址定义的控制动作进行操作,比如丢弃掉这个报文。这样通过 ACL 就可以切断对于这个 IP 的访问。ACL 同样也可以针对报文的源地址进行控制。如果 ACL 工作在二层的话,那么 ACL 控制的对象就从三层的 IP 地址变成二层的 MAC 地址。从 ACL 的工作原理可以看出来,ACL 是在正常报文转发的流程中插入了一个匹配 ACL 的操作,这肯定会影响到报文转发的效率,如果需要控制的 IP 地址比较多,则 ACL 列表会更长,匹配 ACL 的时间也更长,那么报文的转发效率会更低,这对于一些骨干路由器来讲是不可忍受的。
路由协议与路由重分发#
而 GFW 的网络管控方法是利用了 OSPF 等路由协议的路由重分发(redistribution)功能,可以说是 “歪用” 了这个本来是正常的功能。
动态路由协议
说路由重分发之前先简单介绍下动态路由协议。正常情况下路由器上各种路由协议如 OSPF、IS-IS、BGP 等,各自计算并维护自己的路由表,所有的协议生成的路由条目最终汇总到一个路由管理模块。对于某一个目的 IP 地址,各种路由协议都可以计算出一条路由。但是具体报文转发的时候使用哪个协议计算出来的路由,则由路由管理模块根据一定的算法和原则进行选择,最终选择出来一条路由,作为实际使用的路由条目。
静态路由#
相对于由动态路由协议计算出来的动态路由条目,还有一种路由不是由路由协议计算出来的,而是由管理员手工配置下去的,这就是所谓的静态路由。这种路由条目优先级最高,存在静态路由的情况下路由管理模块会优先选择静态路由,而不是路由协议计算出来的动态路由。
路由重分发#
刚才说到正常情况下各个路由协议是只维护自己的路由。但是在某些情况下比如有两个 AS(自治系统),AS 内使用的都是 OSPF 协议,而 AS 之间的 OSPF 不能互通,那么两个 AS 之间的路由也就无法互通。为了让两个 AS 之间互通,那么要在两个 AS 之间运行一个域间路由协议 BGP,通过配置,使得两个 AS 内由 OSPF 计算出来的路由,能通过 BGP 在两者之间重分发。BGP 会把两个 AS 内部的路由互相通告给对方 AS,两个 AS 就实现了路由互通。这种情况就是通过 BGP 协议重分发 OSPF 协议的路由条目。
另外一种情况,管理员在某个路由器上配置了一条静态路由,但是这条静态路由只能在这台路由器上起作用。如果也想让它在其他的路由器上起作用,最笨的办法是在每个路由器上都手动配置一条静态路由,这很麻烦。更好的方式是让 OSPF 或是 IS-IS 等动态路由协议来重分发这条静态路由,这样通过动态路由协议就把这条静态路由重分发到了其他路由器上,省去了逐个路由器手工配置的麻烦。
GFW 路由扩散技术的工作原理#
前面说了是 “歪用”,正常的情况下静态路由是由管理员根据网络拓扑或是基于其他目的而给出的一条路由,这条路由最起码要是正确的,可以引导路由器把报文转发到正确的目的地。而 GFW 的路由扩散技术中使用的静态路由其实是一条错误的路由,而且是有意配置错误的。其目的就是为了把本来是发往某个 IP 地址的报文统统引导到一个 “黑洞服务器” 上,而不是把它们转发到正确目的地。这个黑洞服务器上可以什么也不做,这样报文就被无声无息地丢掉了。更多地,可以在服务器上对这些报文进行分析和统计,获取更多的信息,甚至可以做一个虚假的回应。
评价
有了这种新的方法,以前配置在 ACL 里的每条 IP 地址就可以转换成一条故意配置错误的静态路由信息。这条静态路由信息会把相应的 IP 报文引导到黑洞服务器上,通过动态路由协议的路由重分发功能,这些错误的路由信息可以发布到整个网络。这样对于路由器来讲现在只是在根据这条路由条目做一个常规报文转发动作,无需再进行 ACL 匹配,与以前的老方法相比,大大提高了报文的转发效率。而路由器的这个常规转发动作,却是把报文转发到了黑洞路由器上,这样既提高了效率,又达到了控制报文之目的,手段更为高明。
这种技术在正常的网络运营当中是不会采用的,错误的路由信息会扰乱网络。正常的网络运营和管控体系的需求差别很大,管控体系需要屏蔽的 IP 地址会越来越多。正常的网络运营中的 ACL 条目一般是固定的,变动不大、数量少,不会对转发造成太大的影响。而这种技术直接频繁修改骨干路由表,一旦出现问题,将会造成骨干网络故障。
所以说 GFW 是歪用了路由扩散技术,正常情况下没有那个运营商会把一条错误的路由信息到处扩散,这完全是歪脑筋。或者相对于正常的网络运营来说,GFW 对路由扩散技术的应用是一种小聪明的做法。正常的路由协议功能被滥用至此,而且非常之实用与高效,兲朝在这方面真是人才济济。
测量#
GFW 动态路由系统概括起来就是:人工配置 (c) 样本路由器 (sr) 的静态路由 (r),向各 ISP 的出入口路由器 (or) 扩散此路由 (r),将特定网络流量转到黑洞服务器 (fs) 进行记录。因此可以进行测量的项目有:
被封锁的 IP 列表:可以通过协作报告机制收集用户报告,也可通过扫描著名站点获得;(传言:GFW 动态路由系统的容量是几十万条规则)
受到 GFW 影响的 ISP 出入口路由器:通过在广域多 ISP 内的节点协作 traceroute 可以测得;
从关键词生效到动态路由生效的延迟:通过建立蜜罐并提交给 GFW 然后观察其响应;
黑洞服务器的健壮性:用伪源噪音流量对黑洞服务器进行填充,观测其响应。
参考文献
刘刚,云晓春,方滨兴,胡铭曾. "一种基于路由扩散的大规模网络控管方法". 通信学报,24 (10): 159-164. 2003.
李蕾,乔佩利,陈训逊. "一种 IP 访问控制技术的实现". 信息技术,(6). 2001.
深入理解 GFW:内部结构#
之前我们对 GFW 进行了大量的黑箱测试,尽管大多数实验数据都得到了良好的解释,但是还是有些数据或者体现出的规律性(不规律性)没有得到合理的解释。比如 TCP 连接的各项超时时间,比如 Google 的 443 端口被无状态阻断时,继发状态的持续跟源 IP 相关的问题。比如一般 TCP 连接的继发阻断时,窗口尺寸和 TTL 的连续变化特性。这些问题已经超出纯协议的范畴,需要对 GFW 的内部结构进行进一步了解才能明白其原因。所以在这一章介绍 GFW 的实现和内部结构。
总的来说,GFW 是一个建立在高性能计算集群上规模庞大的分布式入侵检测系统。其分布式架构带来了很高的可伸缩性,对骨干网一点上庞大流量的处理问题被成功转换成购买超级计算机堆砌处理能力的问题。它目前有能力对中国大陆全部国际网络流量进行复杂和深度的检测,而且处理能力 “还有很大潜力”。
线路接入#
对于 GFW 在网络上的位置,有很模糊的认知:“在三个国际出口作旁路监听”。然而还希望对在出国之前最后一跳之前发生了什么有详细了解。
GFW 希望对不同线路的链路异构性进行耦合,并研究了快速以太网、低速 WAN、光纤、专用信号多种类型链路的耦合技术。而根据《国际通信出入口局管理办法》,几大 ISP 有自己的国际出入口局,最后在公用国际光缆处汇合,比如在海缆登陆站之前汇合。据已有的资料,安管中心(CNNISC)有独立的交换中心,而且有报道说各个 ISP 是分别接入其交换中心。这样几个材料就可以形成一致的解释:为了适应不同 ISP 不同的链路规格,GFW 自己的交换中心需要对不同的链路进行整合,不同的 ISP 分别引出旁路接入 GFW。而没有接入 GFW 的线路则被称为 “防外线”[来源不可靠],不受 GFW 影响。接入的线路类型应该主要是光纤线路,因此通常称此接入方式为分光。这就是 “旁路分光”。另外实验发现,GFW 的接入地点并不一定紧靠最后一跳,因此图中以虚线表示。需要注意 GFW 的响应流量重新接回网络的地点难以确认,这里只是假设是与接出的地点相同。
负载平衡#
面对多条骨干监测线路接入产生的巨大不均匀流量,不能直接接到处理集群,而是要先进行汇聚然后再负载均衡分流成均匀的小流量,分别送给处理集群并行处理。首先需要将网络设备通信接口(Pos、ATM、E1 等)转换成节点可用的主机通信接口(FE、GE 等)。处理负载均衡的算法经过仔细考虑,希望实现:流量均匀分布、对于有连接协议保持连接约束、算法简单。连接约束是指:一对地址端口对之间的一个连接全部通信都要保证调度到同一个节点。
GFW 关于负载平衡的文章中主要提出两种算法。一种是轮转调度,对于 TCP,当 SYN 到达时,以最近分配的节点号取模再加 1,并将连接存入 hash 表,当后继流量到达时就能查询 hash 表获得目标节点号。[03a] 另一种是基于连接参数的散列,对于 N 个输出端口调度输出端口号是 H (源地址,目标地址,源端口,目标端口) mod N,这个 H 函数可以是 xor。
而之前的某个实验中我们碰到一种特殊的模式,负载平衡在解释其现象中起到了重要作用,下面专门分出一节详细说明。
一个关于窗口值的实验
实验步骤:发送含有关键词的特制包通过 GFW,并接收 GFW 返回的阻断响应包。因为触发阻断之后,同地址对和同目标端口的连接都会受到继发阻断,为了消除这种干扰,一般采取顺序改变目标端口的扫描式方法。通过前期一些实验,我们已经发现和确认某类(二型)阻断响应包中的 TTL 和 id 都跟窗口大小有线性关系,我们认为窗口是基本量(二型窗口为 5042 时 id 发生了溢出,只有在 id 根据窗口算出时才会发生此种情况)。
然而在顺序扫描中有一种特殊的模式无法用现有证据解释。进一步的实验步骤是:在源、目标地址不变的情况下,顺序扫描目标端口,记录返回的阻断响应包的窗口。数据如下图,横轴是时间(秒),纵轴是端口号,每个点代表一次阻断触发事件中观测者收到的阻断包的窗口值。
可以明显看出一种线性增加的趋势。图像取局部放大看:
可以看出,在同一时间有 13 根较连续的线。这样产生了几个问题:为什么有独立可区分的不同的线?这些线表示了什么?为什么有 13 根?为什么每根线是递增的?
为什么有独立可区分的不同的线?现象具有明显的可以继续划分的子模式,而不是一个整体的随机量,并且每个子模式都有良好的连续增加的性质。因此可以推测产生此现象的内在机制不是一块铁板,而是多个独立的实体。进一步的实验事实是,如果顺序扫描端口每次增加 13,那么只会产生一条较连续的线而排除其他的线。这直接证明了模 13 同余端口产生结果的不可分性、实体性,以及同余类间的独立性。
这些线表示了什么?我们猜想,这 13 根线就表征了背后有 13 个独立实体分别根据某个内在的状态产生阻断响应,窗口值就是其内在状态的直接表现。
为什么有 13 个?而不是 1 个 2 个?这个时候,负载平衡就是对此事实的一种解释良好的模型。如果 GFW 有 13 个节点在线,由于希望将流量平均分配到每个节点,那么根据前面论文所述,便采用模的方式,在源、目标地址不变时,根据目标端口模 13 分配流量,目标端口模 13 同余的包会进入同一个节点。实际上更早的时候的一次实验是发现有 15 根线,同理可以猜测有 15 个节点在线。
为什么每根线是递增的?实验中发现,每次阻断 GFW 会分别向连接双方发送窗口值依次增加的两组阻断包,这样对于每方来说,每次阻断就会使窗口值增加 2。每根线会递增正是说明节点在不断产生阻断包增加窗口值,一部分是实验观察者的观测行为触发的,另一部分则是普通网络流量造成的。如果对数据做差分并扣除观测造成的影响,甚至还可以对每节点产生阻断的速率有所估计。
但是为什么要让窗口递增?这背后的动机难以找到很合理的解释,可能这个窗口值有计数器的作用,也可能是为了在 ip.id 上对不同节点产生的包进行区分。事实上,一型的窗口值就是几乎随机但 ip.id 固定,窗口递增并非是必须的。
然而进一步的实验发现,如果目标端口、源地址不变,而目标地址顺序变化,图像就显得比较紊乱,找不出规律。虽然如此,仍然在局部可以识别出同时存在 13 根线的情况,进一步证实 “13 个节点在线” 的猜测。这个实验的意义在于,通过对现象的分解约化,分离出 GFW 内部的某种独立实体结构,对论文中主张的负载平衡算法有进一步的实践证实,对 GFW 的内部结构得到进一步的认识。
数据处理#
当数据流通过当数据总线到达终端节点之后,需要将其从物理层提取出来供上层进一步分析,这个部分称为报文捕获。普通的做法,先网卡中断一次通知内核来取,然后控制 DMA 传到内核空间,然后用户用 read (),让内核 copy_to_user () 将 sk_buff 的数据复制到用户空间,但是这样复制一次就带来了无谓开销。因此 GFW 设计环状队列缓存,以半轮询半中断机制减少频繁中断的系统调用开销,用 mmap 实现 zero-copy,把数据直接从网卡 DMA 到用户空间。这样性能提高很多(耦合也提高很多)。
链路层数据到怀里了,接下来要将数据上交给 TCP/IP 栈。论文中多次提到 libnids(这个库我们也是第一眼就瞟到了,后来发现对诊断没什么用),将其作为基准,(甚至可能符合国情地)以其为蓝本改进,开发出了一种多线程的 TCP/IP(自动机)。后面又在考虑对其进一步做自动机分解优化。后来又再次提出一种两级连接状态记录表,一级轻量级环状 hash 表可以缓解大量无效连接和 SYN Flood 的情况,二级表才真正存储连接的信息。实验结果与此相符:发送 SYN 之后的超时时间要比发送第一个 ACK 之后的超时时间短得多。文献中还提到 libnids 的 half_stream,从实际的情况上看,GFW 的 TCP 栈的确具有鲜明的半连接特性,也就是说:一个方向的 TCP 栈只检测客户端到服务端的数据,或者反之。这样一个直接的后果就是,即使服务端根本不在线没响应,客户端照样可以假装三次握手然后触发一堆 RST。往好的方向看,也许是因为多线程 TCP 栈还原全连接时不想处理数据共享控制的问题。总而言之,GFW 有一种非常轻量级的 TCP/IP 栈,刚好能够处理大多数遵守 RFC 的连接。如果用户稍微精明一点就能穿过去,GFW 要么坐视不管,要么重写 TCP 栈。
TCP/IP 栈将数据分片重组,流重组之后交给应用层解析。应用层由很多插件模块组成,耦合松,部署易。其应用层插件包括 “HTTP、TELNET、FTP、SMTP、POP3、FREENET、IMAP、FREEGATE、TRIBOY”。
有意思的是,这是首次官方确认 GFW 与 Freegate、Freenet、Triboy 的敌对关系。应用层的协议大家都很熟悉不用多解释,不过应用层问题比传输层更多了。好几个模块都有一些小毛病,比如某类 HTTP 模块只认得 CRLF 作为 EOL,换作 LF 便呆了。再比如某类 DNS 模块,发的 DNS 干扰包,十有五六都校验和错误,查询 AAAA 也返回 A,还不如关掉。多数模块都是得过且过,刚好可以工作,一点都不完善。这里列出的、发现的问题按照软件设计一般规律也只是冰山一角。由此推断,GFW 的设计哲学是:better is worse。
不过在可以生产论文的话题上,GFW 绝不含糊,就是模式匹配。应用层模块把应用层协议解析好了,然后就要看是不是哪里有关键词,字符串匹配。搞了一堆论文出来,改进 AC 算法和 BM 算法,就差汇编的干活了,得出某种基于有限状态自动机的多模式匹配算法,特别适合 GFW 这种预定义关键词的需求。总之复杂度是线性的,攻击匹配算法消耗 CPU 什么的就不要想了。
响应机制
如果匹配到一个关键词了,要积极响应阻断之。响应的手段其他地方已经说得太多,手懒,特此剽窃一段,欢迎举报学术不正之风:
响应机制的发展已经经历 IP 包过滤(静态 IP 包过滤、动态 IP 包过滤)、连接欺骗(传输层连接欺骗、应用层连接欺骗)两个阶段,并且形成了针对不同的应用多种方式共存的现状。
静态 IP 包过滤是 IDS 通过和被保护网络与外部网络之间的连通边的端点网络层设备(路由器、三层交换机等)进行联动,在其上设置访问控制列表(ACL)或静态路由表来实现对指定 IP 地址的过滤。由于需要过滤的 IP 地址数量很大,大多数的网络层设备上对 ACL 大小和性能的支持不能满足要求,因此,实际工作中大多采用静态路由的方式。使用该种方式,信息入侵检测系统只能通过专用客户端程序静态写入的方式进行访问控制,粒度大(IP 地址级),响应时间慢,容量较小,但是可以静态写入路由设备的配置文件中,是非易失的。
动态 IP 包过滤是指入侵检测系统采用动态路由协议(BGP,OSPF 等)和关键路由设备进行路由扩散,将需要过滤的 IP 地址扩散到路由设备中的路由表中,特点是响应时间快、容量大,但是只能动态地写入路由设备内存(RAM)中的路由表中,是易失的,同样粒度大。
连接欺骗指信息入侵检测系统在敏感连接传输过程中伪造连接结束信令(RST,FIN)发送给连接的源和目的地址,以中断该连接。特点是实时性强、粒度小(连接级),可以针对某一次敏感连接进行阻断。缺点是对分析系统工作状态依赖较强,需要向业务网上发送数据包,易受 DoS 攻击。
通过和连接级防火墙设备进行联动,可以针对连接五元组(传输协议类型、源地址、源端口、目的地址、目的端口)对数据流进行过滤。可以针对指定的任意五元以内的组合条件进行过滤,实时性强、粒度小。
后来又加上了 DNS 劫持 / 污染,不过这个手动设置的机制已经不能算一个入侵检测系统的响应了。
日志记录
GFW 有日志。这意味着什么?这就意味着当你翻墙的时候,你的所作所为都记录在案。不光是你一个人,其他所有人都经常翻墙。但据统计 87.53% 的人(361 之 316)都是无意之中翻墙,从统计理论上看,记录在案的无效信息过多会造成信息难以利用。因此 GFW 后期一直在做 “数据融合、聚类、分类的研究”,鸭子硬上弓,各种神经网络、概率模型、人工智能的论文整了一大堆,效果如何呢?
GFW 的日志应该会记录这样一些事件信息:起始时间、结束时间、源地址、目标地址、目标端口、服务类型、敏感类型。信息难以利用不等于不能利用,如果日志被翻出来了而且用户没有用代理,那么根据常识,从 IP 地址对应到人也只是时间问题。这就是说,GFW 即使不能阻断,最差也是一个巨型监听设备。
规模估计
问题:GFW 的软硬件配置?
事实:
“虚拟计算环境实验床” 是由国家计算机网络应急技术处理协调中心(CNCERT/CC)和哈尔滨工业大学(HIT)协作建设,以国家计算机网络应急技术处理协调中心遍布全国 31 个省份的网络基础设施及计算资源为基础,对分布自治资源进行集成和综合利用,构建起的一个开放、安全、动态、可控的大规模虚拟计算环境实验平台,研究并验证虚拟计算环境聚合与协同机理。2005 年此平台配置如下:
CNCERT/CC 北京 曙光 4000L 128 节点 2Xeon 2.4G RAM2G
HIT 哈尔滨 曙光服务器 32 节点 2Xeon 2.4G RAM2G
CNCERT/CC 上海 Beowulf 集群 64 节点 2*AMD Athlon 1.5G RAM2G
结论:
GFW(北京)使用曙光 4000L 机群,操作系统 Red Hat 系列(从 7.2 到 7.3 到 AS 4),周边软件见曙光 4000L 一般配置;GFW 实验室(哈工大)使用曙光服务器],Red Hat 系列;GFW(上海)使用 Beowulf 集群(攒的?)。
问题:GFW 与曙光是什么关系?
事实:
换句话说,是先有了用户的应用需求,才有了曙光 4000L 的研制。这其实不难想像,一套价值几千万元的系统,如果纯是为了填补科学空白,将会延长产品市场化的时间。曙光 4000L 充分体现了中科院计算所在科研成果市场化方面的运作能力,而曙光 4000L 这套系统就是针对国家信息化的实际应用而设计的。 在曙光 4000L 的研制中,曙光公司从事了工程任务和产品化工作,国防科技大学从事了机群数据库中间件的开发工作,哈尔滨工业大学开发了应用软件。
结论:
GFW 是曙光 4000L 的主要需求来源、研究发起者、客户、股东、共同开发者。是不是应该打一点折?(曙光公司 = 中科院计算所)
问题:GFW 计算规模有多大?
事实:
2007 年机群规模进一步扩大,北京增至 360 节点,上海增至 128 节点,哈尔滨增至 64 节点,共计 552 节点。机群间星型千兆互联。[null] 计划节点数上千。[null] 曙光 4000L…… 系统节点数为 322 节点,可扩展到 640 节点。根据功能的不同,曙光 4000L 可以分为服务节点、计算节点和数据库节点三类。每个计算节点 2 个 2.4GHZ 的 Intel Xeon CPU,内存 2GB。 2005 年国家计算机网络与信息安全管理中心(北京)就已经建立了一套 384*16 节点的集群用于网络内容过滤(005 工程)和短信过滤(016 工程)。[来源不可靠] 64 个节点、128 个处理器(主频为 2.8GHz)的曙光 4000L…… 包括系统软件、管理软件、输入输出设备和存储设备,采购金额近千万。 才有了曙光 4000L 的研制…… 一套价值几千万元的系统。 国家信息安全重大项目 “国家信息安全管理系统”(005 工程)经费 4.9 亿。
猜测:
GFW(北京)拥有 16 套曙光 4000L,每套 384 节点,其中 24 个服务和数据库节点,360 个计算节点。每套价格约两千万到三千万,占 005 工程经费的主要部分。有 3 套(将)用于虚拟计算环境实验床,计千余节点。13 套用于骨干网络过滤。总计 6144 节点,12288CPU,12288GB 内存,峰值计算速度 48 万亿次(定义不明,GFW 不做浮点运算,2003 年 top500 排名榜首地球模拟器 5120 个 CPU)。
问题:GFW 吞吐量有多大?
事实:
2GHz CPU 的主机 Linux 操作系统下可达到 600Kpps 以上的捕包率。通过骨干网实验,配置 16 个数据流总线即可以线速处理八路 OC48 接口网络数据。曙光 4000L 单结点的接入能力为每秒 65 万数据包,整个系统能够满足 32Gbp 的实时数据流的并发接入要求。
猜测:
512Gbps(北京)。
从 GFW 原理到翻墙实践#
引言
GFW 具有重大的社会意义。无论是正面的社会意义,还是负面的意义。无论你是讨厌,还是憎恨。它都在那里。在可以预见的将来,墙还会继续存在。我们要学会如何与其共存。
我们把翻墙看成一场我们与 GFW 之间的博弈,是一个不断对抗升级的动态过程。目前整体的博弈态势来讲是 GFW 占了绝对的上风。我们花费了大量的金钱(买 VPS 买 VPN),花费大量时间(学习各种翻墙技术),而 GFW 只需要简单发几个包,配几个路由规则就可以让你的心血都白费。
GFW 并不需要检查所有的上下行流量中是不是有不和谐的内容,很多时候只需要检查连接的前几个包就可以判断出是否要阻断这个连接。为了规避这种检查,我们就需要把所有的流量都通过第三方代理,还要忍受不稳定,速度慢等各种各样的问题。花费的是大量的研究的时间,切换线路的时间,找出是什么导致不能用的时间,当然还有服务器的租用费用和带宽费用。我的感觉是,这就像太极里的四两拨千斤。GFW 只需要付出很小的成本,就迫使了我们去付出很大的反封锁成本,而且这种成本好像是越来越高了。
这场博弈的不公平之处在于,GFW 拥有国家的资源和专业的团队。而我们做为个体,愿意花费在翻墙上的时间与金钱是非常有限的。在竞争激烈的北上广深,每天辛苦忙碌的白领们。翻墙无非是为了方便自己的工作而已。不可能在每天上下班从拥挤的地铁中挤出来之后再去花费已经少得可怜的业余时间去学习自己不是翻墙根本不需要知道的名词到底是什么意思。于是乎,我们得过且过。不用 Google 也不会死,对不对?。博弈的天平远远不是平衡的,而是一边倒。
全面学习 GFW
GFW 会是一个长期的存在。要学会与之共存,必须先了解 GFW 是什么。做为局外人,学习 GFW 有六个角度。渐进的来看分别是:
首先我们学习到的是 what 和 when。比如说,你经常听到人的议论是 “昨天”,“github” 被封了。其中的昨天就是 when,github 就是 what。这是学习 GFW 的最天然,最朴素的角度。在这个方面做得非常极致的是一个叫做 greatfire 的网站。这个网站长期监控成千上万个网站和关键词。通过长期监控,不但可以掌握为什么被封锁了,还可以知道什么时候被封的,什么时候被解封的。
接下来的角度是 who。比如说,“方校长” 这个人名就经常和 GFW 同时出现。但是如果仅仅是掌握一个两个人名,然后天天在 twitter 上骂一遍,除了把这个人名骂成名人之外,没有什么特别的积极意义。我们可以通过网络上的公开信息,掌握 GFW 的哪些方面与哪些人有关系,这些合作者之间又有什么联系。除了大家猜测的将来可以鞭尸之外,对现在也是有积极的意义的。比如关注这些人的研究动态和思想发展,可以猜测 GFW 的下一步发展方向。比如阅读过去发表的论文,可以了解 GFW 的技术演进历史,可以从历史中找到一些技术或者管理体制上的缺陷。
再接下来就是 why 了。github 被封之后就常听人说,github 这样的技术网站你封它干啥?是什么原因促成了一个网站的被封与解封的?我们做为局外人,真正的原因当然是无从得知的。但是我们可以猜测。基于猜测,可以把不同网站被封,与网络上的舆情时间做关联和分类。我们知道,方校长对于网路舆情监控是有很深入研究的。有一篇论文(Whiskey, Weed, and Wukan on the World Wide Web: On Measuring Censors’ Resources and Motivations)专门讨论监管者的动机的。观测触发被封的事件与实际被封之间的时间关系,也可以推测出一些有趣的现象。比如有人报告,翻墙触发的封端口和封 IP 这样的事情一般都发生在中国的白天。也就是说,GFW 背后不光是机器,有一些组件是血肉构成的。
剩下的两个角度就是对如何翻墙穿墙最有价值的两个角度了:how 和 where。how 是非常好理解的,就是在服务器和客户端两边抓包,看看一个正常的网络通信,GFW 做为中间人,分别给两端在什么时候发了什么包或者过滤掉了什么包。而这些 GFW 做的动作,无论是过滤还是发伪包又是如何干扰客户端与服务器之间的正常通信的。where 是在知道了 how 之后的进一步发展,不但要了解客户端与服务器这两端的情况,更要了解 GFW 是挂在两端中间的哪一级路由器上做干扰的。在了解到 GFW 的关联路由器的 IP 的基础上,可以根据不同的干扰行为,不同的运营商归属做分组,进一步了解 GFW 的整体部署情况。
整体上来说,对 GFW 的研究都是从 what 和 when 开始,让偏人文的就去研究 who 和 why,像我们这样偏工程的就会去研究 how 和 where。以上就是全面了解 GFW 的主体脉络。接下来,我们就要以 how 和 where 这两个角度去看一看 GFW 的原理。
GFW 的原理
要与 GFW 对抗不能仅仅停留在什么不能访问了,什么可以访问之类的表面现象上。知道 youtube 不能访问了,对于翻墙来说并无帮助。但是知道 GFW 是如何让我们不能访问 youtube 的,则对下一步的翻墙方案的选择和实施具有重大意义。所以在讨论如何翻之前,先要深入原理了解 GFW 是如何封的。
总的来说,GFW 是一个分布式的入侵检测系统,并不是一个严格意义上的防火墙。不是说每个出入国境的 IP 包都需要先经过 GFW 的首可。做为一个入侵检测系统,GFW 把你每一次访问 facebook 都看做一次入侵,然后在检测到入侵之后采取应对措施,也就是常见的连接重置。整个过程一般话来说就是:
检测有两种方式。一种是人工检测,一种是机器检测。你去国新办网站举报,就是参与了人工检测。在人工检测到不和谐的网站之后,就会采取一些应对方式来防止国内的网民访问该网站。对于这类的封锁,规避检测就不是技术问题了,只能从 GFW 采取的应对方式上采取反制措施。另外一类检测是机器检测,其检测过程又可以再进一步细分:
重建
重建是指 GFW 从网络上监听过往的 IP 包,然后分析其中的 TCP 协议,最后重建出一个完整的字节流。分析是在这个重建的字节流上分析具体的应用协议,比如 HTTP 协议。然后在应用协议中查找是不是有不和谐的内容,然后决定采用何种应对方式。
所以,GFW 机器检测的第一步就是重建出一个字节流。那么 GFW 是如何拿到原始的 IP 包的呢?真正的 GFW 部署方式,外人根本无从得知。据猜测,GFW 是部署在国家的出口路由器的旁路上,用 “分光” 的方式把 IP 包复制一份到另外一根光纤上,从而拿到所有进出国境的 IP 包。
GFW 通过配置骨干网的 BGP 路由规则,是可以让国内机房的流量经过它。一个例子是当我们访问被封的网站触发连接重置的时候,往往收到两个 RST 包,但是 TTL 不同。还有一个例子是对于被封的 IP,访问的 IP 包还没有到达国际出口就已经被丢弃。所以 GFW 应该在其他地方也部署有设备,据推测是在省级骨干路由的位置。
对于 GFW 到底在哪这个话题,最近又有国外友人表达了兴趣( https://github.com/mothran/mongol)。其原理是基于一个 IP 协议的特性叫 TTL。TTL 是 Time to Live 的简写。IP 包在没经过一次路由的时候,路由器都会把 IP 包的 TTL 减去 1。如果 TTL 到零了,路由器就不会再把 IP 包发给下一级路由。然后我们知道 GFW 会在监听到不和谐的 IP 包之后发回 RST 包来重置 TCP 连接。那么通过设置不同的 TTL 就可以知道从你的电脑,到 GFW 之间经过了几个路由器。比如说 TTL 设置成 9 不触发 RST,但是 10 就触发 RST,那么到 GFW 就是经过了 10 个路由器。另外一个 IP 协议的特性是当 TTL 耗尽的时候,路由器应该发回一个 TTL EXCEEDED 的 ICMP 包,并把自己的 IP 地址设置成 SRC(来源)。结合这两点,就可以探测出 IP 包是到了 IP 地址为什么的路由器之后才被 GFW 检测到。有了 IP 地址之后,再结合 IP 地址地理位置的数据库就可以知道其地理位置。据说,得出的位置大概是这样的:
但是这里检测出来的 IP 到底是 GFW 的还是骨干路由器的?更有可能的是骨干路由器的 IP。GFW 做为一个设备用 “分光” 的方式挂在主干路由器旁边做入侵检测。无论如何,GFW 通过某种神奇的方式,可以拿到你和国外服务器之间来往的所有的 IP 包,这点是肯定的。更严谨的理论研究有: Internet Censorship in China: Where Does the Filtering Occur?
GFW 在拥有了这些 IP 包之后,要做一个艰难的决定,那就是到底要不要让你和服务器之间的通信继续下去。GFW 不能太过于激进,毕竟全国性的不能访问国外的网站是违反 GFW 自身存在价值的。GFW 就需要在理解了 IP 包背后代表的含义之后,再来决定是不是可以安全的阻断你和国外服务器之间的连接。这种理解就要建立了前面说的 “重建” 这一步的基础上。大概用图表达一下重建是在怎么一回事:
重建这样的字节流有一个难点是如何处理巨大的流量?其原理与网站的负载均衡器一样。对于给定的来源和目标,使用一个 HASH 算法取得一个节点值,然后把所有符合这个来源和目标的流量都往这个节点发。所以在一个节点上就可以重建一个 TCP 会话的单向字节流。
最后为了讨论完整,再提两点。虽然 GFW 的重建发生在旁路上是基于分光来实现的,但并不代表整个 GFW 的所有设备都在旁路。后面会提到有一些 GFW 应对形式必须是把一些 GFW 的设备部署在了主干路由上,也就是 GFW 是要参与部分 IP 的路由工作的。另外一点是,重建是单向的 TCP 流,也就是 GFW 根本不在乎双向的对话内容,它只根据监听到的一个方向的内容然后做判断。但是监听本身是双向的,也就是无论是从国内发到国外,还是从国外发到国内,都会被重建然后加以分析。所以一个 TCP 连接对于 GFW 来说会被重建成两个字节流。具体的证据会在后面谈如何直穿 GFW 中详细讲解。
分析
分析是 GFW 在重建出字节流之后要做的第二步。对于重建来说,GFW 主要处理 IP 协议,以及上一层的 TCP 和 UDP 协议就可以了。但是对于分析来说,GFW 就需要理解各种各样的应用层的稀奇古怪的协议了。甚至,我们也可以自己发明新的协议。
总的来说,GFW 做协议分析有两个相似,但是不同的目的。第一个目的是防止不和谐内容的传播,第二个目的是防止使用翻墙工具绕过 GFW 的审查。下面列举一些已知的 GFW 能够处理的协议。
对于 GFW 具体是怎么达到目的一,也就是防止不和谐内容传播的就牵涉到对 HTTP 协议和 DNS 协议等几个协议的明文审查。大体的做法是这样的:
像 HTTP 这样的协议会有非常明显的特征供检测,所以第一步就没什么好说的了。当 GFW 发现了包是 HTTP 的包之后就会按照 HTTP 的协议规则拆包。这个拆包过程是 GFW 按照它对于协议的理解来做的。比如说,从 HTTP 的 GET 请求中取得请求的 URL。然后 GFW 拿到这个请求的 URL 去与关键字做匹配,比如查找 Twitter 是否在请求的 URL 中。为什么有拆包这个过程?首先,拆包之后可以更精确的打击,防止误杀。另外可能预先做拆包,比全文匹配更节省资源。其次,GFW 还是先去理解协议,然后才做关键字匹配的。关键字匹配应该就是使用了一些高效的正则表达式算法,没有什么可以讨论的。
HTTP 代理和 SOCKS 代理,这两种明文的代理都可以被 GFW 识别。之前笔者认为 GFW 可以在识别到 HTTP 代理和 SOCKS 代理之后,再拆解其内部的 HTTP 协议的正文。也就是做两次拆包。但是分析发现,HTTP 代理的关键字列表和 HTTP 的关键字列表是不一样的,所以笔者现在认为 HTTP 代理协议和 SOCKS 代理协议是当作单独的协议来处理的,并不是拆出载荷的 HTTP 请求再进行分析的。
目前已知的 GFW 会做的协议分析如下:
DNS 查询
GFW 可以分析 53 端口的 UDP 协议的 DNS 查询。如果查询的域名匹配关键字则会被 DNS 劫持。可以肯定的是,这个匹配过程使用的是类似正则的机制,而不仅仅是一个黑名单,因为子域名实在太多了。证据是:2012 年 11 月 9 日下午 3 点半开始,防火长城对 Google 的泛域名 .google.com 进行了大面积的污染,所有以 google.com 结尾的域名均遭到污染而解析错误不能正常访问,其中甚至包括不存在的域名。
目前为止 53 端口之外的查询也没有被劫持。但是 TCP 的 DNS 查询已经可以被 TCP RST 切断了,表明了 GFW 具有这样的能力,只是不屑于大规模部署。而且 TCP 查询的关键字比 UDP 劫持的域名要少的多。
HTTP 请求
GFW 可以识别出 HTTP 协议,并且检查 GET 的 URL 与 HOST。如果匹配了关键字则会触发 TCP RST 阻断。
HTTP 响应
GFW 除了会分析上行的 HTTP GET 请求,对于 HTTP 返回的内容也会做全文关键字检查。这种检查与对请求的关键字检查不是由同一设备完成的,而且对 GFW 的资源消耗也更大。
HTTP 代理协议
略
SOCKS4/5 代理协议
略
SMTP 协议
因为有很多翻墙软件都是以邮件索取下载地址的方式发布的,所以 GFW 有针对性的封锁了 SMTP 协议,阻止这样的邮件往来。
封锁有三种表现方式,简单概要的说就是看邮件是不是发往上了黑名单的邮件地址的,如果发现了就立马用 TCP RST 包切断连接。
发现发件人在黑名单中,立即重置 TCP 链接
from scapy.all import *
send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='S', seq=0))
send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='A', seq=1))
send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='A', seq=1) / 'MAIL FROM: [email protected]\r\n')
看似普通的三个包其实暗藏玄机。首先,目标地址是 1.2.3.4,这显然是我胡写的一个地址,而且 TTL 设置为 9。所以这个包发出去就没有打算让最终的目标机器接到,而只是发给 GFW 看的。这个 TTL 值要大于你的机器到 GFW 的跳数,一般 11 是一个保险的值。
然后要触发 GFW 的响应,有以下几个缺一不可的条件:
目标端口是 25,我尝试了其他几个端口没有发现触发响应。
第二个包虽然内容是空的,但是必须存在。而且必须是 ACK。内容也可以不为空,GFW 似乎不 care 内容是什么,只要有这个包就可以。
第三个包的 seq 必须为 1,哪怕第二包有内容了,这个包的 seq 也必须为 1。而且 MAIL FROM: \r\n 这个格式必须对,不能替换成 FROM MAIL 啥的。还有一个条件就是邮件地址必须上了黑名单。这里举的例子是一个翻墙软件的索取地址,所以上了黑名单。
观测到的响应是 GFW 发一个 TCP RST 包。而且每次都是一个,从来不多发。如果少了中间那个空的 ACK,则是连做两次探测触发一个 TCP RST。貌似 GFW 把两次探测认为是一个连接了。
- 发现收件人在黑名单中,立即重置 TCP 链接
from scapy.all import *
send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='S', seq=0))
send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='A', seq=1))
send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='A', seq=1) / 'RCPT TO: [email protected]\r\n')
这与上面的 MAIL FROM 的例子基本上是一样的。不同之处只有一点就是有的时候可以看到两个 TCP RST 的回包。
3. 发现收件人在黑名单中,发回用户不存在的错误消息
from scapy.all import *
send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='S', seq=0))
send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='A', seq=1) / 'EHLO anything-here\nRCPT TO: [email protected]\n')
得到的错误消息是 “551 User not local; please try \r\n”。有的时候还会伴随有数个连续的 TCP RST。同样因为包根本没有到对方的服务器,而且这个服务器压根就不存在,所以这个用户不存在的错误消息只能是 GFW 做出的响应。
触发的条件是
目标端口必须是 25
第二个包的第一个命令必须是 EHLO 或者 HELO,内容没有关系
第二个包的第二个命令必须是 RCPT TO,而且邮件地址要在黑名单中。
\r\n 没有关系,\n 也是可以触发的
这次触发的条件是一个合法的 SMTP 请求过程。而之前的触发过程根本就不是合法的 SMTP 请求。而且另外一个特征是这样触发的 TCP RST,会有三个重叠,ack 递加的现象,与 HTTP 全文关键字的响应非常类似。我推测,两型的响应是两个不同的模块。单独对 MAIL FROM 和 RCPT TO 的封锁,与对 HTTP 关键字的封锁类似,属于看到就封型。而后一种更智能的,还会回答错误消息的是能够真正理解 SMTP 协议的模块,可能还用于做邮件全文内容的关键字检测。
电驴 (ed2k) 协议
GFW 还会过滤电驴(ed2k)协议中的查询内容。因为 ed2k 还有一个混淆模式,会加密往来的数据包,GFW 会切断所有使用混淆模式的 ed2k 连接,迫使客户端使用明文与服务器通讯。然后如果客户端发起了搜索请求,查找的关键字中包含敏感词的话就会被用 TCP RST 包切断连接。
对翻墙流量的分析识别
GFW 的第二个目的是封杀翻墙软件。为了达到这个目的 GFW 采取的手段更加暴力。原因简单,对于 HTTP 协议的封杀如果做不好会影响互联网的正常运作,GFW 与互联网是共生的关系,它不会做威胁自己存在的事情。但是对于 TOR 这样的几乎纯粹是为翻墙而存在的协议,只要检测出来就是格杀勿论的了。GFW 具体是如何封杀各种翻墙协议的,我也不是很清楚,事态仍然在不断更新中。但是举两个例子来证明 GFW 的高超技术。
第一个例子是 GFW 对 TOR 的自动封杀,体现了 GFW 尽最大努力去理解协议本身。根据这篇博客( <https://blog.torproject.org/blog/knock-knock-knockin-bridges-doors >)。使用中国的 IP 去连接一个美国的 TOR 网桥,会被 GFW 发现。然后 GFW 回头(15 分钟之后)会亲自假装成客户端,用 TOR 的协议去连接那个网桥。如果确认是 TOR 的网桥,则会封当时的那个端口。换了端口之后,可以用一段时间,然后又会被封。这表现出了 GFW 对于协议的高超检测能力,可以从国际出口的流量中敏锐地发现你连接的 TOR 网桥。据 TOR 的同志说是因为 TOR 协议中的握手过程具有太明显的特征了。另外一点就表现了 GFW 的不辞辛劳,居然会自己伪装成客户端过去连连看。
第二个例子表现了 GFW 根本不在乎加密的流量中的具体内容是不是有敏感词。只要疑似翻墙,特别是提供商业服务给多个翻墙,就会被封杀。使用 ShadowSocks 协议,预先部署密钥,没有明显的握手过程仍然被封。据说是 GFW 已经升级为能够机器识别出哪些加密的流量是疑似翻墙服务的。
总结起来就是,GFW 已经基本上完成了目的一的所有工作。明文的协议从 HTTP 到 SMTP 都可以分析然后关键字检测,甚至电驴这样不是那么大众的协议 GFW 都去搞了。从原理上来说也没有什么好研究的,就是明文,拆包,关键字。GFW 显然近期的工作重心在分析网络流量上,从中识别出哪些是翻墙的流量。这方面的研究还比较少,而且一个显著的特征是自己用没关系,大规模部署就容易出问题。我目前没有在 GFW 是如何封翻墙工具上有太多研究,只能是道听途说了。
应对
GFW 的应对措施是三步中最明显的,因为它最直接。GFW 的重建过程和协议分析的过程需要耐心的试探才能大概推测出 GFW 是怎么实现的。但是 GFW 的应对手段我们每天都可以见到,比如连接重置。GFW 的应对目前可以感受到的只有一个目的就是阻断。但是从广义上来说,应对方式应该不限于阻断。比如说记录下日志,然后做统计分析,秋后算账什么的也可以算是一种应对。就阻断方式而言,其实并不多,那么我们一个个来列举吧。
封 IP
一般常见于人工检测之后的应对。还没有听说有什么方式可以直接使得 GFW 的机器检测直接封 IP。一般常见的现象是 GFW 机器检测,然后用 TCP RST 重置来应对。过了一段时间才会被封 IP,而且没有明显的时间规律。所以我的推测是,全局性的封 IP 应该是一种需要人工介入的。注意我强调了全局性的封 IP,与之相对的是部分封 IP,比如只对你访问那个 IP 封个 3 分钟,但是别人还是可以访问这样的。这是一种完全不同的封锁方式,虽然现象差不多,都是 ping 也 ping 不通。要观摩的话 ping twitter.com 就可以了,都封了好久了。
其实现方式是把无效的路由黑洞加入到主干路由器的路由表中,然后让这些主干网上的路由器去帮 GFW 把到指定 IP 的包给丢弃掉。路由器的路由表是动态更新的,使用的协议是 BGP 协议。GFW 只需要维护一个被封的 IP 列表,然后用 BGP 协议广播出去就好了。然后国内主干网上的路由器都好像变成了 GFW 的一份子那样,成为了帮凶。
如果我们使用 traceroute 去检查这种被全局封锁的 IP 就可以发现,IP 包还没有到 GFW 所在的国际出口就已经被运营商的路由器给丢弃了。这就是 BGP 广播的作用了。
DNS 劫持
这也是一种常见的人工检测之后的应对。人工发现一个不和谐网站,然后就把这个网站的域名给加到劫持列表中。其原理是基于 DNS 与 IP 协议的弱点,DNS 与 IP 这两个协议都不验证服务器的权威性,而且 DNS 客户端会盲目地相信第一个收到的答案。所以你去查询 facebook.com 的话,GFW 只要在正确的答案被返回之前抢答了,然后伪装成你查询的 DNS 服务器向你发错误的答案就可以了。
下图为 GFW 对域名 telegram.org 的劫持:
可见许多地区的 IP 被解析到 Twitter 和 Facebook 等已被封锁的 IP 上。
TCP RST 阻断
TCP 协议规定,只要看到 RST 包,连接立马被中断。从浏览器里来看就是连接已经被重置。我想对于这个错误大家都不陌生。据我个人观感,这种封锁方式是 GFW 目前的主要应对手段。大部分的 RST 是条件触发的,比如 URL 中包含某些关键字。还有一些网站,会被无条件 RST。也就是针对特定的 IP 和端口,无论包的内容就会触发 RST。比较著名的例子是 https 的 wikipedia。GFW 在 TCP 层的应对是利用了 IPv4 协议的弱点,也就是只要你在网络上,就假装成任何人发包。所以 GFW 可以很轻易地让你相信 RST 确实是 Google 发的,而让 Google 相信 RST 是你发的。
封端口#
GFW 除了自身主体是挂在骨干路由器旁路上的入侵检测设备,利用分光技术从这个骨干路由器抓包下来做入侵检测 (所谓 IDS),除此之外这个路由器还会被用来封端口 (所谓 IPS)。GFW 在检测到入侵之后可以不仅仅可以用 TCP RST 阻断当前这个连接,而且利用骨干路由器还可以对指定的 IP 或者端口进行从封端口到封 IP,设置选择性丢包的各种封禁措施。可以理解为骨干路由器上具有了类似 “iptables” 的能力(网络层和传输层的实时拆包,匹配规则的能力)。这个 iptables 的能力在 CISCO 路由器上叫做 ACL Based Forwarding (ABF)。而且规则的部署是全国同步的,一台路由器封了你的端口,全国的挂了 GFW 的骨干路由器都会封。一般这种封端口都是针对翻墙服务器的,如果检测到服务器是用 SSH 或者 VPN 等方式提供翻墙服务。GFW 会在全国的出口骨干路由上部署这样的一条 ACL 规则,来封你这个服务器 + 端口的下行数据包。也就是如果包是从国外发向国内的,而且 src(源 ip)是被封的服务器 ip,sport(源端口)是被封的端口,那么这个包就会被过滤掉。这样部署的规则的特点是,上行的数据包是可以被服务器收到的,而下行的数据包会被过滤掉。
如果被封端口之后服务器采取更换端口的应对措施,很快会再次被封。而且多次尝试之后会被封 IP。初步推断是,封端口不是 GFW 的自动应对行为,而是采取黑名单加人工过滤地方式实现的。一个推断的理由就是网友报道,封端口都是发生在白天工作时间。
在进入了封端口阶段之后,还会有继发性的临时性封其他端口的现象,但是这些继发性的封锁具有明显的超时时间,触发了之后(触发条件不是非常明确)会立即被封锁,然后过了一段时间就自动解封。
HTTPS 间歇性丢包#
对于 Github 的 HTTPS 服务,GFW 不愿意让其完全不能访问。所以采取的办法是对于 Github 的某些 IP 的 443 端口采取间歇性丢包的措施。其原理应该类似于封端口,是在骨干路由器上做的丢包动作。但是触发条件并不只是看 IP 和端口,加上了时间间隔这样一个条件。
翻墙原理#
前面从原理上讲解了 GFW 的运作原理。翻墙的原理与之相对应,分为两大类。第一类是大家普遍的使用的绕道的方式。IP 包经由第三方中转已加密的形式通过 GFW 的检查。这样的一种做法更像 “翻” 墙,是从墙外绕过去的。第二类是找出 GFW 检测过程的中一些 BUG,利用这些 BUG 让 GFW 无法知道准确的会话内容从而放行。这种做法更像 “穿” 墙。曾经引起一时轰动的西厢计划第一季就是基于这种方式的实现。
基于绕道法的翻墙方式无论是 VPN 还是代理,原理都是类似的。都是以国外有一个代理服务器为前提,然后你与代理服务器通信,代理服务器再与目标服务器通信。
绕道法对于 IP 封锁来说,因为最终的 IP 包是由代理服务器在墙外发出的,所以国内骨干路由封 IP 并不会产生影响。对于 TCP 重置来说,因为 TCP 重置是以入侵检测为前提的,客户端与代理之间的加密通信规避了入侵检测,使得 TCP 重置不会被触发。
但是对于反 DNS 污染来说,VPN 和代理代理却有不同。基于 VPN 的翻墙方法,得到正确的 DNS 解析的结果需要设置一个国外的没有被污染的 DNS 服务器。然后发 UDP 请求去解析域名的时候,VPN 会用绕道的方式让 UDP 请求不被劫持地通过 GFW。
但是 SOCKS 代理和 HTTP 代理这些更上层的代理协议则可以选择不同的方式。因为代理与应用之间有更紧密的关系,应用程序比如浏览器可以把要访问的服务器的域名直接告诉本地的代理。然后 SOCKS 代理可以选择不在本地做解析,直接把请求发给墙外的代理服务器。在代理服务器去与目标服务器做连接的时候再在代理服务器上做 DNS 解析,从而避开了 GFW 的 DNS 劫持。
VPN 与代理的另外一个主要区别是应用程序是如何使用上代理去访问国外的服务器的。先来看不加代理的时候,应用程序是如何访问网络的。
应用程序把 IP 包交给操作系统,操作系统会去决定把包用机器上的哪块网卡发出去。VPN 的客户端对于操作系统来说就是一个虚拟出来的网卡。应用程序完全不用知道 VPN 客户端的存在,操作系统甚至也不需要区分 VPN 客户端与普通网卡的区别。
VPN 客户端在启动之后会把操作系统的缺省路由改成自己。这样所有的 IP 包都会经由这块虚拟的网卡发出去。这样 VPN 就能够再打包成加密的流量发出去(当然线路还是之前的线路),发回去的加密流量再解密拆包交还给操作系统。
应用层的代理则不同。其流量走不走代理的线路并不是由操作系统使用路由表选择网卡来决定的,而是在应用程序里自己做的。也就是说,对于操作系统来说,使用代理的 TCP 连接和不使用代理的 TCP 连接并没有任何的不同。应用程序自己去选择是直接与目标服务器建立连接,还是与代理服务器建立 TCP 连接,然后由 SOCKS 代理服务器去建立第二个 TCP 连接,两个 TCP 连接的数据由代理服务器中转。
绕道法的翻墙原理就是这些了,相对来说非常简单。其针对的都是 GFW 的分析那一步,通过加密使得 GFW 无法分析出流量的原文从而让 GFW 放行。但是 GFW 最近的升级表明,GFW 虽然无法解密这些加密的流量,但是 GFW 可以结合流量与其他协议特征探测出这些流量是不是 “翻墙” 的,然后就直接暴力的切断。绕道法的下一步发展就是要从原理弄明白,GFW 是如何分析出翻墙流量的,从而要么降低自身的流量特征避免上短名单被协议分析,或者通过混淆协议把自己伪装成其他的无害流量。
穿墙原理
DNS 劫持观测
我们要做的第一个实验是用 python 代码观测到 DNS 劫持的全过程。
应用层观测
dig 是 DNS 的客户端,可以很方便地构造出我们想要的 DNS 请求。 dig @8.8.8.8 twitter.com 。可以得到相应如下:
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5494
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;twitter.com. IN A
;; ANSWER SECTION:
twitter.com. 4666 IN A 59.24.3.173
;; Query time: 110 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Sun Jan 13 13:22:10 2013
;; MSG SIZE rcvd: 45
可以很清楚地看到我们得到的错误答案 59.24.3.173。
抓包观测#
使用 iptables 我们可以让特定的 IP 包经过应用层的代码,从而使得我们用 python 观测 DNS 查询过程提供了可能。代码如下:
from netfilterqueue import NetfilterQueue
import subprocess
import signal
def observe_dns_hijacking(nfqueue_element):
print('packet past through me')
nfqueue_element.accept()
nfqueue = NetfilterQueue()
nfqueue.bind(0, observe_dns_hijacking)
def clean_up(*args):
subprocess.call('iptables -D OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
subprocess.call('iptables -D INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
signal.signal(signal.SIGINT, clean_up)
try:
subprocess.call('iptables -I INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
subprocess.call('iptables -I OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
print('running..')
nfqueue.run()
except KeyboardInterrupt:
print('bye')
执行此脚本,再使用 dig @8.8.8.8 twitter.com 应该可以看到 package past through me。这就说明 DNS 的请求和答案都经过了 python 代码了。
上一步主要是验证 NetfilterQueue 是不是工作正常。这一步则要靠 dpkt 的了。代码如下:
from netfilterqueue import NetfilterQueue
import subprocess
import signal
import dpkt
import traceback
import socket
def observe_dns_hijacking(nfqueue_element):
try:
ip_packet = dpkt.ip.IP(nfqueue_element.get_payload())
dns_packet = dpkt.dns.DNS(ip_packet.udp.data)
print(repr(dns_packet))
for answer in dns_packet.an:
print(socket.inet_ntoa(answer['rdata']))
nfqueue_element.accept()
except:
traceback.print_exc()
nfqueue_element.accept()
nfqueue = NetfilterQueue()
nfqueue.bind(0, observe_dns_hijacking)
def clean_up(*args):
subprocess.call('iptables -D OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
subprocess.call('iptables -D INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
signal.signal(signal.SIGINT, clean_up)
try:
subprocess.call('iptables -I INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
subprocess.call('iptables -I OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
print('running..')
nfqueue.run()
except KeyboardInterrupt:
print('bye')
执行此脚本,再使用 dig @8.8.8.8 twitter.com 应该可以看到类似如下的输出:
DNS(ar=[RR(type=41, cls=4096)], qd=[Q(name='twitter.com')], id=8613, op=288)
DNS(an=[RR(name='twitter.com', rdata=';\x18\x03\xad', ttl=19150)], qd=[Q(name='twitter.com')], id=8613, op=33152)
.24.3.173
DNS(an=[RR(name='twitter.com', rdata='\xc7;\x95\xe6', ttl=27), RR(name='twitter.com', rdata='\xc7;\x96\x07', ttl=27), RR(name='twitter.com', rdata="\xc7;\x96'", ttl=27)], ar=[RR(type=41, cls=512)], qd=[Q(name='twitter.com')], id=8613, op=33152)
.59.149.230
.59.150.7
.59.150.39
可以看到我们发出去了一个包,收到了两个包。其中第一个收到的包是 GFW 发回来的错误答案,第二个包才是正确的答案。但是由于 dig 只取第一个返回的答案,所以我们实际看到的解析结果是错误的。
观测劫持发生的位置#
利用 IP 包的 TTL 特性,我们可以把 TTL 值从 1 开始递增,直到我们收到错误的应答为止。结合 TTL EXECEEDED ICMP 返回的 IP 地址,就可以知道 DNS 请求是在第几跳的路由器分光给 GFW 的。代码如下:
from netfilterqueue import NetfilterQueue
import subprocess
import signal
import dpkt
import traceback
import socket
import sys
DNS_IP = '8.8.8.8'
# source http://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BC%93%E5%AD%98%E6%B1%A1%E6%9F%93
WRONG_ANSWERS = {
'4.36.66.178',
'8.7.198.45',
'37.61.54.158',
'46.82.174.68',
'59.24.3.173',
'64.33.88.161',
'64.33.99.47',
'64.66.163.251',
'65.104.202.252',
'65.160.219.113',
'66.45.252.237',
'72.14.205.99',
'72.14.205.104',
'78.16.49.15',
'93.46.8.89',
'128.121.126.139',
'159.106.121.75',
'169.132.13.103',
'192.67.198.6',
'202.106.1.2',
'202.181.7.85',
'203.161.230.171',
'207.12.88.98',
'208.56.31.43',
'209.36.73.33',
'209.145.54.50',
'209.220.30.174',
'211.94.66.147',
'213.169.251.35',
'216.221.188.182',
'216.234.179.13'
}
current_ttl = 1
def locate_dns_hijacking(nfqueue_element):
global current_ttl
try:
ip_packet = dpkt.ip.IP(nfqueue_element.get_payload())
if dpkt.ip.IP_PROTO_ICMP == ip_packet['p']:
print(socket.inet_ntoa(ip_packet.src))
elif dpkt.ip.IP_PROTO_UDP == ip_packet['p']:
if DNS_IP == socket.inet_ntoa(ip_packet.dst):
ip_packet.ttl = current_ttl
current_ttl += 1
ip_packet.sum = 0
nfqueue_element.set_payload(str(ip_packet))
else:
if contains_wrong_answer(dpkt.dns.DNS(ip_packet.udp.data)):
sys.stdout.write('* ')
sys.stdout.flush()
nfqueue_element.drop()
return
else:
print('END')
nfqueue_element.accept()
except:
traceback.print_exc()
nfqueue_element.accept()
def contains_wrong_answer(dns_packet):
for answer in dns_packet.an:
if socket.inet_ntoa(answer['rdata']) in WRONG_ANSWERS:
return True
return False
nfqueue = NetfilterQueue()
nfqueue.bind(0, locate_dns_hijacking)
def clean_up(*args):
subprocess.call('iptables -D OUTPUT -p udp --dst %s -j QUEUE' % DNS_IP, shell=True)
subprocess.call('iptables -D INPUT -p udp --src %s -j QUEUE' % DNS_IP, shell=True)
subprocess.call('iptables -D INPUT -p icmp -m icmp --icmp-type 11 -j QUEUE', shell=True)
signal.signal(signal.SIGINT, clean_up)
try:
subprocess.call('iptables -I INPUT -p icmp -m icmp --icmp-type 11 -j QUEUE', shell=True)
subprocess.call('iptables -I INPUT -p udp --src %s -j QUEUE' % DNS_IP, shell=True)
subprocess.call('iptables -I OUTPUT -p udp --dst %s -j QUEUE' % DNS_IP, shell=True)
print('running..')
nfqueue.run()
except KeyboardInterrupt:
print('bye')
执行 dig +tries=30 +time=1 @8.8.8.8 twitter.com 可以得到类似下面的输出:
.158.100.166
.158.11.150
* 219.158.97.30
* * 219.158.27.30
* 72.14.215.130
* 209.85.248.60
* 216.239.43.19
* * END
出现 * 号前面的那个 IP 就是挂了 GFW 的路由了。脚本只能执行一次,第二次需要重启。另外同一个 DNS 不能被同时查询,把 8.8.8.8 改成你没有在用的 DNS。这个脚本的一个 “副作用” 就是 dig 返回的答案是正确的了,因为错误的答案被丢弃了。
反向观测
前面我们已经知道从国内请求国外的 DNS 服务器大体是怎么一个被劫持的过程了。接下来我们在国内搭建一个服务器,从国外往国内发请求,看看是不是可以观测到被劫持的现象。
把路由器的 WAN 口的防火墙打开。配置本地的 dnsmasq 为使用非标准端口代理查询从而保证本地做 dig 查询的时候可以拿到正确的结果。然后在国外的服务器上执行 dig @国内路由器 ip twitter.com
可以看到收到的答案是错误的。执行前面的路由跟踪代码,结果如下:
.248.76.73
.158.33.181
.158.29.129
.158.19.165
* 219.158.96.225
* * * 219.158.101.233
END
可以看到不但有 DNS 劫持,而且 DNS 劫持发生在非常靠近国内路由器的位置。这也证实了论文中提出的观测结果。GFW 并没有严格地部署在出国境前第一跳的位置,而是更加靠前。并且是双向的,至少 DNS 劫持是双向经过实验证实了。
反 DNS 劫持#
通过避免 GFW 重建请求反 DNS 劫持
使用非标准端口
这个实验就非常简单了。使用 53 之外的端口查询 DNS,观测是否有错误答案被返回。
使用的 DNS 服务器是 OpenDNS,端口为 5353 端口。使用非标准端口的 DNS 服务器不多,并不是所有的 DNS 服务器都会提供非标准端口供查询。结果如下:
; <<>> DiG 9.9.1-P3 <<>> @208.67.222.222 -p 5353 twitter.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5367
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 8192
;; QUESTION SECTION:
;twitter.com. IN A
;; ANSWER SECTION:
twitter.com. 5 IN A 199.59.150.39
twitter.com. 5 IN A 199.59.148.82
twitter.com. 5 IN A 199.59.148.10
;; Query time: 194 msec
;; SERVER: 208.67.222.222#5353(208.67.222.222)
;; WHEN: Mon Jan 14 11:47:46 2013
;; MSG SIZE rcvd: 88
可见,非标准端口还是可以得到正确结果的。但是这种穿墙并不能被应用程序直接使用,因为几乎所有的应用程序都不支持使用非标准端口查询。有很多种办法把端口变成 53 端口能用:
使用本地 DNS 服务器转发(dnsmasq,pdnsd)
用 NetfilterQueue 改写 IP 包
用 iptables 改写 IP 包:
iptables -t nat -I OUTPUT --dst 208.67.222.222 -p udp --dport 53 -j DNAT --to-destination 208.67.222.222:5353
使用 TCP 查询
这个实验就更加简单了,也是一条命令:
dig +tcp @8.8.8.8 twitter.com
GFW 在日常是不屏蔽 TCP 的 DNS 查询的,所以可以得到正确的结果。但是和非标准端口一样,几乎所有的应用程序都不支持使用 TCP 查询。已知的 TCP 转 UDP 方式是使用 pdnsd 或者 unbound 转。
但是 GFW 现在不屏蔽 TCP 的 DNS 查询并不代表 GFW 不能这么干。做一个小实验:
root@OpenWrt:~# dig +tcp @8.8.8.8 dl.dropbox.com
;; communications error to 8.8.8.8#53: connection reset
可以看到 GFW 是能够知道你在查询什么的。与 HTTP 关键字过滤一样,一旦发现查询的内容不恰当,立马就发 RST 包过来切断连接。那么为什么 GFW 不审查所有的 TCP 的 DNS 查询呢?原因很简单,用 TCP 查询的绝对少数,尚不值得这么去干。而且就算你能查询到正确域名,GFW 自认为还有 HTTP 关键字过滤和封 IP 等后着守着呢,犯不着在 DNS 上卡这么死。
使用单向代理#
严格来说单向代理并不是穿墙,因为它仍然需要在国外有一个代理服务器。使用代理服务器把 DNS 查询发出去,但是 DNS 查询并不经由代理服务器而是直接发回客户端。这样的实现在目前有更好的反劫持的手段(比如非标准端口)的情况下并不是一个有实际意义的做法。但是对于观测 GFW 的封锁机制还是有帮助的。据报道在敏感时期,对 DNS 不仅仅是劫持,而是直接丢包。通过单向代理可以观测丢包是针对出境流量的还是入境流量的。
客户端需要使用 iptables 把 DNS 请求转给 NetfilterQueue,然后用 python 代码把 DNS 请求包装之后发给中转代理。对于应用程序来说,这个包装的过程是透明的,它仍然认为请求是直接发给 DNS 服务器的。
客户端代码如下:
from netfilterqueue import NetfilterQueue
import subprocess
import signal
import traceback
import socket
IMPERSONATOR_IP = 'x.x.x.x'
IMPERSONATOR_PORT = 19840
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
def smuggle_packet(nfqueue_element):
try:
original_packet = nfqueue_element.get_payload()
print('smuggled')
udp_socket.sendto(original_packet, (IMPERSONATOR_IP, IMPERSONATOR_PORT))
nfqueue_element.drop()
except:
traceback.print_exc()
nfqueue_element.accept()
nfqueue = NetfilterQueue()
nfqueue.bind(0, smuggle_packet)
def clean_up(*args):
subprocess.call('iptables -D OUTPUT -p udp --dst 8.8.8.8 --dport 53 -j QUEUE', shell=True)
signal.signal(signal.SIGINT, clean_up)
try:
subprocess.call('iptables -I OUTPUT -p udp --dst 8.8.8.8 --dport 53 -j QUEUE', shell=True)
print('running..')
nfqueue.run()
except KeyboardInterrupt:
print('bye')
服务器端代码如下:
import socket
import dpkt.ip
def main_loop(server_socket, raw_socket):
while True:
packet_bytes, from_ip = server_socket.recvfrom(4096)
packet = dpkt.ip.IP(packet_bytes)
dst = socket.inet_ntoa(packet.dst)
print('%s:%s => %s:%s' % (socket.inet_ntoa(packet.src), packet.data.sport, dst, packet.data.dport))
raw_socket.sendto(packet_bytes, (dst, 0))
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
server_socket.bind(('0.0.0.0', 19840))
raw_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
try:
raw_socket.setsockopt(socket.SOL_IP, socket.IP_HDRINCL, 1)
main_loop(server_socket, raw_socket)
finally:
raw_socket.close()
finally:
server_socket.close()
在路由器上运行的时候要把 WAN 的防火墙规则改为接受 INPUT,否则进入的 UDP 包会因为没有对应的出去的 UDP 包而被过滤掉。这是单向代理的一个缺陷,需要在墙上开洞。把防火墙整个打开是一种开洞的极端方式。后面专门讨论单向代理的时候会有更多关于防火墙凿洞的讨论。
第二个运行的条件是服务器所在的网络没有对 IP SPROOFING 做过滤。服务器实际上使用了和 GFW 发错误答案一样的技术,就是伪造 SRC 地址。通过把 SRC 地址填成客户端所在的 IP 地址,使得 DNS 查询的结果不需要经过代理服务器中装直接到达客户端。
通过丢弃错误答案反 DNS 劫持#
使用 iptables 过滤
前两种方式都是针对 GFW 的重建这一步。因为 GFW 没有在日常的时候监听所有 UDP 端口以及监听 TCP 流量,所以非标准端口或者 TCP 的 DNS 查询可以被放行。选择性丢包则针对的是 GFW 的应对措施。既然 GFW 发错误的答案回来,只要我们不认它给的答案,等正确的答案来就是了。
代码如下:
mport sys
import subprocess
# source http://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BC%93%E5%AD%98%E6%B1%A1%E6%9F%93
WRONG_ANSWERS = {
'4.36.66.178',
'8.7.198.45',
'37.61.54.158',
'46.82.174.68',
'59.24.3.173',
'64.33.88.161',
'64.33.99.47',
'64.66.163.251',
'65.104.202.252',
'65.160.219.113',
'66.45.252.237',
'72.14.205.99',
'72.14.205.104',
'78.16.49.15',
'93.46.8.89',
'128.121.126.139',
'159.106.121.75',
'169.132.13.103',
'192.67.198.6',
'202.106.1.2',
'202.181.7.85',
'203.161.230.171',
'207.12.88.98',
'208.56.31.43',
'209.36.73.33',
'209.145.54.50',
'209.220.30.174',
'211.94.66.147',
'213.169.251.35',
'216.221.188.182',
'216.234.179.13'
}
rules = ['-p udp --sport 53 -m u32 --u32 "4 & 0x1FFF = 0 && 0 >> 22 & 0x3C @ 8 & 0x8000 = 0x8000 && 0 >> 22 & 0x3C @ 14 = 0" -j DROP']
for wrong_answer in WRONG_ANSWERS:
hex_ip = ' '.join(['%02x' % int(s) for s in wrong_answer.split('.')])
rules.append('-p udp --sport 53 -m string --algo bm --hex-string "|%s|" --from 60 --to 180 -j DROP' % hex_ip)
try:
for rule in rules:
print(rule)
subprocess.call('iptables -I INPUT %s' % rule, shell=True)
print('running..')
sys.stdin.readline()
except KeyboardInterrupt:
print('bye')
finally:
for rule in reversed(rules):
subprocess.call('iptables -D INPUT %s' % rule, shell=True)
本地有了这些 iptables 规则之后就可以丢弃掉 GFW 发回来的错误答案,从而得到正确的解析结果。这个脚本用到了两个 iptables 模块一个是 u32 一个是 string。这两个内核模块不是所有的 linux 机器都有的。比如大部分的 Android 手机都没有这两个内核模块。所以上面的脚本适合内核模块很容易安装的场景,比如你的 ubuntu pc。因为 linux 的内核模块与内核版本(每次编译基本都不同)是一一对应的,所以不同的 linux 机器是无法共享同样的内核模块的。所以基于内核模块的方案天然地具有安装困难的缺陷。
使用 nfqueue 过滤#
对于没有办法自己安装或者编译内核模块的场景,比如最常见的 Android 手机,厂家不告诉你内核的具体版本以及编译参数,普通用户是没有办法重新编译 linux 内核的。对于这样的情况,iptables 提供了 nfqueue,我们可以把内核模块做的 ip 过滤的工作交给用户态(也就是普通的应用程序)来完成。
CLEAN_DNS = '8.8.8.8'
RULES = []
for iface in network_interface.list_data_network_interfaces():
# this rule make sure we always query from the "CLEAN" dns
RULE_REDIRECT_TO_CLEAN_DNS = (
{'target': 'DNAT', 'iface_out': iface, 'extra': 'udp dpt:53 to:%s:53' % CLEAN_DNS},
('nat', 'OUTPUT', '-o %s -p udp --dport 53 -j DNAT --to-destination %s:53' % (iface, CLEAN_DNS))
)
RULES.append(RULE_REDIRECT_TO_CLEAN_DNS)
RULE_DROP_PACKET = (
{'target': 'NFQUEUE', 'iface_in': iface, 'extra': 'udp spt:53 NFQUEUE num 1'},
('filter', 'INPUT', '-i %s -p udp --sport 53 -j NFQUEUE --queue-num 1' % iface)
)
RULES.append(RULE_DROP_PACKET)
# source http://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BC%93%E5%AD%98%E6%B1%A1%E6%9F%93
WRONG_ANSWERS = {
'4.36.66.178',
'8.7.198.45',
'37.61.54.158',
'46.82.174.68',
'59.24.3.173',
'64.33.88.161',
'64.33.99.47',
'64.66.163.251',
'65.104.202.252',
'65.160.219.113',
'66.45.252.237',
'72.14.205.99',
'72.14.205.104',
'78.16.49.15',
'93.46.8.89',
'128.121.126.139',
'159.106.121.75',
'169.132.13.103',
'192.67.198.6',
'202.106.1.2',
'202.181.7.85',
'203.161.230.171',
'203.98.7.65',
'207.12.88.98',
'208.56.31.43',
'209.36.73.33',
'209.145.54.50',
'209.220.30.174',
'211.94.66.147',
'213.169.251.35',
'216.221.188.182',
'216.234.179.13',
'243.185.187.39'
}
def handle_nfqueue():
try:
nfqueue = NetfilterQueue()
nfqueue.bind(1, handle_packet)
nfqueue.run()
except:
LOGGER.exception('stopped handling nfqueue')
dns_service_status.error = traceback.format_exc()
def handle_packet(nfqueue_element):
try:
ip_packet = dpkt.ip.IP(nfqueue_element.get_payload())
dns_packet = dpkt.dns.DNS(ip_packet.udp.data)
if contains_wrong_answer(dns_packet):
# after the fake packet dropped, the real answer can be accepted by the client
LOGGER.debug('drop fake dns packet: %s' % repr(dns_packet))
nfqueue_element.drop()
return
nfqueue_element.accept()
dns_service_status.last_activity_at = time.time()
except:
LOGGER.exception('failed to handle packet')
nfqueue_element.accept()
def contains_wrong_answer(dns_packet):
if dpkt.dns.DNS_A not in [question.type for question in dns_packet.qd]:
return False # not answer to A question, might be PTR
for answer in dns_packet.an:
if dpkt.dns.DNS_A == answer.type:
resolved_ip = socket.inet_ntoa(answer['rdata'])
if resolved_ip in WRONG_ANSWERS:
return True # to find wrong answer
else:
LOGGER.info('dns resolve: %s => %s' % (dns_packet.qd[0].name, resolved_ip))
return False # if the blacklist is incomplete, we will think it is right answer
return True # to find empty answer
其原理是一样的,过滤所有的 DNS 应答,如果发现是错误的答案就丢弃。因为是基于 nfqueue 的,所以只要 linux 内核支持 nfqueue,而且 iptables 可以添加 nfqueue 的 target,就可以使用以上方式来丢弃 DNS 错误答案。目前已经成功在主流的 android 手机上运行该程序,并获得正确的 DNS 解析结果。另外,上面的实现利用 iptables 的重定向能力,达到了更换本机 dns 服务器的目的。无论机器设置的 dns 服务器是什么,通过上面的 iptables 规则,统统给你重定向到干净的 DNS(8.8.8.8)。
自此 DNS 穿墙的讨论基本上就完成了。DNS 劫持是所有 GFW 封锁手段中最薄弱的一环,有很多种方法都可以穿过。如果不想写代码,用非标准端口是最容易的部署方式。如果愿意部署代码,用 nfqueue 丢弃错误答案是最可靠通用的方式,不依赖于特定的服务器。
封 IP 观测
观测 twitter.com
首先使用 dig 获得 twitter.com 的 ip 地址:
; <<>> DiG 9.9.1-P3 <<>> +tcp @8.8.8.8 twitter.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 8015
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;twitter.com. IN A
;; ANSWER SECTION:
twitter.com. 7 IN A 199.59.149.230
twitter.com. 7 IN A 199.59.150.39
twitter.com. 7 IN A 199.59.150.7
根据前面的内容我们知道使用 dns over tcp,大部分的域名解析都不会被干扰的。这里得到了三个 ip 地址。先来测试 199.59.149.230
traceroute to 199.59.149.230 (199.59.149.230), 30 hops max, 38 byte packets
123.114.32.1 19.862 ms 4.267 ms 101.431 ms
61.148.163.73 920.148 ms 5.108 ms 3.868 ms
124.65.56.129 7.596 ms 7.742 ms 7.735 ms
123.126.0.133 5.310 ms 7.745 ms 7.573 ms
* * *
* * *
这个结果是最常见的。在骨干路由器上,针对这个 ip 丢包了。这种封锁方式就是最传统的封 IP 方式,BGP 路由扩散,现象就是针对上行流量的丢包。再来看 199.59.150.39
traceroute to 199.59.150.39 (199.59.150.39), 30 hops max, 38 byte packets
123.114.32.1 14.046 ms 20.322 ms 19.918 ms
61.148.163.229 7.461 ms 7.182 ms 7.540 ms
124.65.56.157 4.491 ms 3.342 ms 7.260 ms
123.126.0.93 6.715 ms 7.309 ms 7.438 ms
219.158.4.126 5.326 ms 3.217 ms 3.596 ms
219.158.98.10 3.508 ms 3.606 ms 4.198 ms
219.158.33.254 140.965 ms 133.414 ms 136.979 ms
129.250.4.107 132.847 ms 137.153 ms 134.207 ms
61.213.145.166 253.193 ms 253.873 ms 258.719 ms
199.16.159.15 257.592 ms 258.963 ms 256.034 ms
199.16.159.55 267.503 ms 268.595 ms 267.590 ms
199.59.150.39 266.584 ms 259.277 ms 263.364 ms
在我撰写的时候,这个 ip 还没有被封。但是根据经验,twitter.com 享受了最高层次的 GFW 关怀,新的 ip 基本上最慢也是隔日被封的。不过通过这个 traceroute 可以看到 219.158.4.126 其实就是那个之前捣乱的服务器,包是在它手里被丢掉的(严格来说并不一定是 219.158.4.126,因为 ip 包经过的路由对于不同的目标 ip 设置不同的端口都可能会不一样)。再来看 199.59.150.7
traceroute to 199.59.150.7 (199.59.150.7), 30 hops max, 38 byte packets
123.114.32.1 11.379 ms 10.420 ms 23.008 ms
61.148.163.229 6.102 ms 6.777 ms 7.373 ms
61.148.153.61 5.638 ms 3.148 ms 3.235 ms
123.126.0.9 3.473 ms 3.306 ms 3.216 ms
219.158.4.198 2.839 ms !H * 6.136 ms !H
这次同样是封 IP,但是现象不同。通过抓包可以观察到是什么问题:
:46:11.355913 IP (tos 0x0, ttl 251, id 0, offset 0, flags [none], proto ICMP (1), length 56)
.158.4.198 > 123.114.40.44: ICMP host r-199-59-150-7.twttr.com unreachable, length 36
IP (tos 0x0, ttl 1, id 0, offset 0, flags [DF], proto UDP (17), length 38)
.114.40.44.45021 > r-199-59-150-7.twttr.com.33449: UDP, length 10
原来 219.158.4.198 发回来了一个 ICMP 包,内容是地址不可到达(unreachable)。于是 traceroute 就在那里断掉了。
如果把 unreachable 类型的 ICMP 包丢弃掉,会发现 ip 包仍然过不去
traceroute to 199.59.150.7 (199.59.150.7), 30 hops max, 38 byte packets
123.114.32.1 4.866 ms 3.165 ms 3.212 ms
61.148.163.229 3.107 ms 3.104 ms 3.270 ms
61.148.153.61 6.001 ms 7.246 ms 7.398 ms
123.126.0.9 7.840 ms 7.223 ms 7.443 ms
* * *
这次就和被丢包了是一样的观测现象了。
同时,可以看到我们仍然是收到了 icmp 地址不可到达的包的,只是被我们 drop 掉了。
观测被封 ip 的反向流量
之前的观测中,被封的 ip 是 ip 包的 dst。如果我们从国外往国内发包,其 src 是被封的 ip,那么 ip 包是否会被 GFW 过滤掉呢?登录到一台国外的 vps 上执行下面的 python 代码:
from scapy.all import *
send(IP(src="http://drops.wooyun.org/papers/199.59.150.7", dst="123.114.40.44")/ICMP())
然后在国内的路由器上执行抓包程序
cpdump: listening on pppoe-wan, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
:41:14.294671 IP (tos 0x0, ttl 50, id 1, offset 0, flags [none], proto ICMP (1), length 28)
r-199-59-150-7.twttr.com > 123.114.40.44: ICMP echo request, id 0, seq 0, length 8
:41:14.294779 IP (tos 0x0, ttl 64, id 25013, offset 0, flags [none], proto ICMP (1), length 28)
.114.40.44 > r-199-59-150-7.twttr.com: ICMP echo reply, id 0, seq 0, length 8
可以看到,如果该 ip 是 src 而不是 dst 并不会被 GFW 过滤。这一行为有两种可能:要么 GFW 认为封 dst 就可以了,不屑于再封 src 了。另外一种可能是 GFW 封 twitter 的 IP 用的是路由表扩散技术,而传统的路由表是基于 dst 做路由判断的(高级的路由器可以根据 src 甚至端口号做为路由的依据),所以 dst 路由表导致的路由黑洞并不会影响该 ip 为 src 的情况。我相信是后者,但是 GFW 在封个人翻墙主机上所表现的实力(对大量的 ip 做精确到端口的全国性丢包)让我们相信,GFW 很容易把封锁变成双向的。不过说实话,在这个硬实力的背后,靠的更多的是 CISCO 下一代骨干网路由器的超强处理能力,而不是 GFW 自身。
单向代理
因为 GFW 对 IP 的封锁是针对上行流量的,所以使得单向代理就可以突破封锁。上行的 IP 包经过单向代理转发给目标服务器,下行的 IP 包直接由目标服务器发回给客户端。代码与 DNS(UDP 协议)的单向代理是一样的。因为单向代理利用的是 IP 协议,所以 TCP 与 UDP 都是一样的。除了单向代理,目前尚没有其他的办法穿过 GFW 访问被封的 IP,只能使用传统的翻墙技术,代理或者 VPN 这些。
结语
GFW 与网民之间已经或者即将形成某种稳态,这种稳态是双方斗争状况下的动态平衡,是需要有意识维护的。一个无法控制的网络是无法被政府所容忍的,当网络无法控制时政府是不吝于切断一切网络的(你一定知道我在说什么),稳态的破坏也就意味着环境的毁灭。一个理想的稳态就是网络处于 “看起来” 可以控制的状态,让 GFW 处于不断取得小型封锁成功的虚幻胜利感之中,网民个人各自掌握非中心化的翻墙方法。一个中心化的大众翻墙方法(最典型的例子就是设置 hosts 静态解析)必定无法避免被当局发现并被 GFW 封锁。下一代的翻墙方法应该是去中心化的(p2p)、小众的、多样化的、混合型的、动态更新的。
参考资料
https://blog.neargle.com/SecNewsBak/drops/ 翻墙路由器的原理与实现 .html
https://fqrouter.tumblr.com/post/43400982633 / 详述 gfw 对 smtp 协议的三种封锁手法
https://gist.github.com/4524294
https://gist.github.com/4524299
https://gist.github.com/4524927
https://gist.github.com/4531012
https://gist.github.com/4530465
缓解假墙伪墙攻击勒索的多种技术手段#
墙封锁一个网站有 DNS 污染、IP 封锁、TCP Reset(TCP 连接重置)等手段。而一个网站一旦被墙,一般情况下是无法直接通过 301(或 302)跳转到其他网站的。如果只是 IP 被封还好说,换 IP 通常能解决问题。但如果是根据域名关键字进行的 TCP Reset,这时候不管怎么换 IP(除非是国内 IP)都无法解除封锁,当然也不可能进行 301 跳转(浏览器在收到 HTTP 服务器的 301 跳转 Response 之前 TCP 连接就已经被墙 Reset 而断开了,浏览器根本收不到 HTTP 服务器的任何 Response)。而 DNS 污染的话自然更不用多说,只能换域名了,301 跳转更不可能做到。然而,现在出现了很多号称可以解决域名被墙的服务,可以在网站被墙后通过 301 跳转到新的网站上。经过测试,还真能做到绕过墙的 TCP Reset 封锁,而这些服务的 IP 却都在海外(并非是使用了国内 IP 避免被墙的原因),而客户端只需要一个正常的浏览器即可(即客户端并不需要开启科学上网)。那么它们是怎么做到的呢?
要解释清楚其中的技术原理,还得回到 2010 年的西厢计划很早就经常科学上网的同学们应该都对西厢计划并不陌生,它是一个只需要运行在客户端就能绕过很多封锁访问目标网站的工具,解决 TCP Reset 的原理是对本地的 TCP/IP 协议进行修改,在不伤害客户端和服务器之间的 TCP 连接的前提下让墙误以为 TCP 连接已经断开或者无法正确跟踪到 TCP 连接。之后出现的INTANG 项目样是这个想法的延续。
不过,不管是西厢计划还是 INTANG,都是运行在客户端上的工具,理论上只在服务器上运行无法起到效果,经过测试也能看到实际和理论相符。那么有没有一种工具可以在只服务器上运行,修改 TCP/IP 协议从而绕过封锁的工具呢?这方面同样有团队做了研究,研究的成果就是Geneva 项目GFW Report也对其做了详细介绍
在这篇文章列举了 6 种可以绕过 TCP Reset 的规则,6 种规则都可以在只客户端部署生效(这时候服务器并不需要运行Geneva而前 4 种可以在只服务器部署生效(这时候客户端并不需要运行Geneva)。
不过 Geneva 的官方 Github 中只收录了客户端的规则,文章中的服务器规则并没有被收录在 Geneva 的官方 Github 中。而且文章中的策略 3 只给出了客户端的规则,遗漏了服务器端的规则。经过阅读 Geneva 的规则介绍和策略 3 的描述,我已经重新还原了策略 3 的服务器规则,重新收录了 4 种服务器规则到我自己的Github Fork
经过本地环境的模拟加上 tcpdump 抓包观察测试,看到还原的策略 3 服务器规则和文章中描述的行为一致,可以认为就是策略 3 本来的服务器规则。但是,在之后的真实环境的测试中发现这 4 种服务器策略全都失效了(不管 HTTP 还是 HTTPS 都已失效),墙依然对 TCP 进行了 Reset。经过抓包看到服务器的行为确实和文章中描述一致,所以可以确认并非是由于 Geneva 没有正常工作导致的,而是墙已经为了应对这 4 种策略进行进化了。所以,墙并不是一成不变的,而是会进化的,那我们又该怎么办呢?
讲到这里,就不得不提另一个策略发现工具SymTCP虽然现有的 4 种策略已经失效,但并不代表我们不能发现新的策略。而 SymTCP 就是新策略发现工具,通过自动学习可以自动发现新的策略绕过墙的 TCP Reset。之后我们就能把新的策略转换为Geneva不过,这样的话我们就会陷入到和墙的无休止争斗中,不断发现新策略,而墙则不断封锁新策略。而且规则的转换也是一个麻烦事,暂时还没有工具可以自动从SymTCP转换为 Geneva 的规则,需要人工转换。并且需要修改 SymTCP 使其不仅可以发现客户端规则同样也能发现服务器端规则。
那么,有没有一种一劳永逸的方法,使墙再怎么进化也无法避免这种策略的影响,而且这种策略只需要运行在服务器上,从而绕过封锁呢?在下结论之前,我们需要来研究一下一个正常的 HTTP 协议通讯是怎么进行的:
浏览器发起 TCP 连接,经过 3 步(次)握手建立和服务器的 TCP 连接。
浏览器发送 HTTP Request。
服务器收到 Request,发送 HTTP Response。
而墙通常在看到浏览器发送的 HTTP Request 中包含关键字就会进行 TCP Reset。讲到这里,聪明的同学或许已经想到了:如果服务器不等 HTTP Request,而在 TCP 连接建立后立即发送 HTTP Response,在墙进行 TCP Reset 之前就将 Response 送到浏览器进行抢答,是不是就能绕过 TCP Reset 了?而且还能无视之后墙的进化(因为浏览器的请求根本还没有经过墙)?说干就干,由于抢答模式不符合 HTTP 规范,所以常见的 HTTP 服务器无法实现抢答模式,所以让我们写个 Python 小程序来测试一下:
import socket
import threading
import time
def main():
serv_sock = socket.socket()
serv_sock.bind(('0.0.0.0', 80))
serv_sock.listen(50)
# 关闭Nagle算法,立即发送数据
serv_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# 不等待,立即关闭连接
serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
while True:
cli_sock, _ = serv_sock.accept()
try:
cli_sock.sendall(b'''HTTP/1.1 302 Moved Temporarily\r\n'''
b'''Content-Type: text/html\r\n'''
b'''Content-Length: 0\r\n'''
b'''Connection: close\r\n'''
b'''Location: https://www.microsoft.com/\r\n\r\n''')
except Exception: # 防止客户端提前关闭连接抛异常
pass
def wait_second():
time.sleep(1) # 等待1秒钟,确保数据发送完毕
cli_sock.close()
threading.Thread(target=wait_second).start()
if __name__ == '__main__':
main()
写完了来测试一下,发现依旧被 TCP Reset 了。那么,问题出在哪里?让我们重新回到上述 HTTP 协议通讯的 3 个步骤中的第 1 步 ——TCP 的 3 步握手:
从 TCP 的 3 步握手中,我们可以看到第 3 步中客户端发送了 ACK 就已经完成了 TCP 连接的建立,这时候客户端并不需要再等服务器的回复就能立即发送数据。也就是说,浏览器会在发送 ACK 后立即发送 HTTP Request,ACK 和 HTTP Request 几乎是同时发出的。而服务器在收到浏览器的 ACK 后基本也就代表着已经收到了 HTTP Request 了,抢答失败!
那么,有没有办法让浏览器在 TCP 连接建立后延迟发送 HTTP Request,而又不改动客户端行为呢?讲到这里,对 TCP 协议比较熟悉的同学或许已经想到了,那就是 TCP window size。而通过调用 setsockopt () 就能修改 TCP window size。让我们来修改一下 Python 小程序,把 window size 改为 1 再进行测试(TCP 连接建立完成后,客户端只能发送 1 个字节,等待服务器的确认后才能继续发送更多的数据):
import socket
import struct
import threading
import time
def main():
serv_sock = socket.socket()
serv_sock.bind(('0.0.0.0', 80))
serv_sock.listen(50)
# 设置TCP window size为1
serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1)
# 不等待,立即关闭连接
serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
# 关闭Nagle算法,立即发送数据
serv_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
while True:
cli_sock, _ = serv_sock.accept()
try:
cli_sock.sendall(b'''HTTP/1.1 302 Moved Temporarily\r\n'''
b'''Content-Type: text/html\r\n'''
b'''Content-Length: 0\r\n'''
b'''Connection: close\r\n'''
b'''Location: https://www.microsoft.com/\r\n\r\n''')
except Exception: # 防止客户端提前关闭连接抛异常
pass
def wait_second():
time.sleep(1) # 等待1秒钟,确保数据发送完毕
cli_sock.close()
threading.Thread(target=wait_second).start()
if __name__ == '__main__':
main()
改完测试,发现在 Linux 下仍旧被 TCP Reset 了(但在 Windows 下成功跳转了)。什么原因?通过抓包,我们看到对 TCP window size 的修改并没有生效,window size 依旧很大。在查阅了Linux man page后我们看到关于 SO_RCVBUF 有这么一段话:
The minimum (doubled) value for this option is 256.
这也就意味着即使我们通过 setsockopt () 将 SO_RCVBUF 设置为 1,Linux 内核也会将其作为 256 处理(Windows 却没有这个限制)。而 Linux man page 中也对 SO_RCVBUFFORCE 做了说明:只能突破最大值的限制(but the rmem_max limit can be overridden),但不能突破最小值的限制。而 256 字节基本就可以容纳整个 HTTP Request。看来通过 setsockopt () 行不通(不管 SO_RCVBUF 还是 SO_RCVBUFFORCE 都行不通),Linux 下我们得找别的方法。
讲到这里,我们很自然地又想到了Geneva上述 Geneva 的策略 2 中服务器规则正是利用了 TCP window size 做到的四字节分割(设置 window size 为 4)。这样,就绕过了 setsockopt () 的限制,直接对 TCP 数据包进行修改了。
在我们把四字节分割法部署到服务器运行 Geneva 后,再结合上述 Python 小程序,经过测试我们发现已经成功绕过了 TCP Reset,浏览器跳转到了微软网站。我们终于成功了!
然而,在浏览器第二次访问服务器时发现依然被 TCP Reset 了。不过,这已经影响不到 301 跳转(上述 Python 小程序还是 302 跳转,需要 301 的同学自行修改)了,301 跳转的话浏览器已经被重定向到新的网站了,不会再次访问这个服务器(需要保证新旧网站不能使用相同 IP),但这并不妨碍我们继续探究一下为什么第二次访问会被 TCP Reset:通过抓包我们看到,第一次访问时浏览器虽然在第一个附带用户数据的数据包中只发送了 4 个字节,但后续会将剩余的整个 HTTP Request 通过一个数据包发送到服务器导致 TCP Reset。而墙是有审查残留的,一段时间(几分钟)内不管是否出现关键字,对源 IP 和目标 IP 之间的 TCP 连接会进行无差别的 Reset。所以在之后的这段审查残留时间内,只要 TCP 连接建立就会被 Reset,抢答模式无法起到作用。
知道了原因我们就能采取对策了,我们知道客户端是因为收到了服务器确认数据包中的 TCP window size 很大,所以才能一次性把剩余的 Request 发送完毕,所以需要对后续的 TCP window size 做同样的修改,保证客户端看到的 window size 一直处于比较小的水平:通过对 TCP 协议的了解,我们知道连接建立时的 window size 是通过 SYN+ACK 包确定的,而后续的 window size 是通过 ACK 或 PSH+ACK 包确定的。所以,我们对规则 2 做少许的修改就能做到对后续 window size 的修改:
[TCP:flags:A]-tamper{TCP:window:replace:1}-|
[TCP:flags:PA]-tamper{TCP:window:replace:1}-|
在服务器上我们同时运行规则 2 和上述修改后的 2 条规则(需要开 3 个Geneva
进程,注意第 2、第 3 个进程需要在命令行中指定 --in-queue-num 和 --out-queue-num 避免和第 1 个冲突),我们终于能稳定地运行上述抢答模式,再也不会被 TCP Reset 了。
实际上我们可以将 3 条规则中的 window size 都设置得更小一些,甚至设置为 0,避免客户端发送任何数据(实际上由于 window size 探测机制的原因,客户端仍旧会以极慢的速度一个字节一个字节地发送数据,不过不影响我们的抢答模式):
[TCP:flags:SA]-tamper{TCP:window:replace:0}-|
[TCP:flags:A]-tamper{TCP:window:replace:0}-|
[TCP:flags:PA]-tamper{TCP:window:replace:0}-|
至此,HTTP 的抢答模式就基本完成了。至于海外 301 跳转的那些服务可以同时服务于多个网站,原理也很简单:它们的名称虽然都是 301 跳转,但实际上并不一定必须使用 301 跳转 —— 以上 Python 小程序可以修改为通过 HTTP 200 返回一个正常的 HTML 页面,其中嵌入一个 JavaScript,在 JavaScript 中就能判断浏览器的网址进行条件跳转了。至于跳转规则,那大家就能在 JavaScript 中充分发挥自己的想象了。另外,由于 Geneva 和上述小程序都是用 Python 编写的(甚至都没有使用 asyncio),性能会比较差一些。Geneva 会自己添加 iptables 的 NFQUEUE 规则,不过规则太过于宽松,导致不需要处理的数据包也会经过 Geneva,并且会有规则覆盖的问题。所以大家需要在启动 Geneva 后手动删除这些规则,自行添加更精确的规则(3 条 iptables 规则需要分别设置成只处理 OUTPUT 链中 TCP 80 端口的 SYN+ACK、ACK 和 PSH+ACK,避免条件重叠)。另外需要注意的是 Geneva 区分客户端模式还是服务器模式,在服务器上运行的话需要加上 --server-side 参数。不过经过我的测试,即使使用精确的 iptables 规则也差不多只能利用 20Mbps 左右的带宽。如果希望有更高的性能,可以使用 C/C++(结合 libev,或 asio,或直接 epoll;或者使用 golang、rust 等)结合
libnetfilter_queue
利用 iptables 的 NFQUEUE 来完成,可以跑满千兆带宽。其实 Geneva 的底层用的也是 libnetfilter_queue 和 NFQUEUE。由于本系列只做概念验证,而且因为篇幅的限制(本文已经很长了),在此就不展开 C/C++ 的实现了,感兴趣的同学可以和我联系,如果感兴趣的同学比较多的话我就再新开一个系列讲一下这方面的内容。另外需要注意的是,Geneva 的编译环境为 Python 3.6,使用 Debian 10 自带的 Python 3.8/3.9 会出现各种莫名其妙的问题,建议大家还是从 Python 官网下载 Python 3.6 的源码进行编译使用 Geneva。嫌麻烦的同学如果不想折腾 Geneva 也可以将服务器换成 Windows 系统,可以直接使用上述 Python 小程序完成抢答模式。
在解决了 HTTP 的 TCP Reset 问题后,我们还需要解决 HTTPS 的 TCP Reset。而 HTTPS 由于需要完成 TLS 握手才能发送 HTTP Response,所以抢答模式似乎无法应用于 HTTPS.
我们知道,HTTPS 是需要客户端和服务器之间完成 TLS 握手后才能收发 HTTP Request 和 Response。然而,墙是在 TLS 握手时通过 SNI 中的域名信息进行了 TCP Reset,或通过 ESNI 头进行 TCP 阻断这时候还没有到能发送 HTTP Request 或 Response 的这个阶段,所以 HTTP Response 抢答模式无法应用于 HTTPS。那么,我们该怎么办呢?
让我们回到 TCP Reset 本身 —— 既然是 TCP Reset,那么它只会对 TCP 协议生效。如果有一个应用层协议,其底层不是 TCP 呢?相信聪明的同学已经想到了,那就是 HTTP 3.0(简称 HTTP/3 或 h3)。H3 的底层协议是 QUIC,而 QUIC 是基于 UDP 而非 TCP 的。经过我的测试,发现墙现在无法识别 QUIC 协议,不管 QUIC 协议中出现任何敏感词都能无障碍过墙。这也是为什么有些网站(如 v2ex)在解决了 DNS 污染(比如客户端主动加 hosts)后,并且连接过一次(首次还是需要科学上网,原因之后会讲)后,就能关闭科学上网访问了。也就是说,使用了 H3 后,我们甚至不需要进行 301 跳转,直接就能无障碍访问服务器了(但仍建议使用 301 跳转,否则可能会使封锁升级,如 DNS 污染)。虽然 H3 现在仍旧处于草案阶段,但各大浏览器都已经进行了支持,而其中 Google Chrome 对 H3 的支持是最好的。
那么,如何在服务器上部署并开启 H3 呢?由于 H3 现在仍是草案阶段,所以 Nginx 的正式版并不支持 H3,需要更换为Nginx-QUIC来支持 H3。编译使用 Nginx-QUIC 也可以参考这篇文章这篇文章
另外,Cloudflare 也已经支持了 H3,可以自行开启(在 “网络” 设置中打开 “HTTP/3(使用 QUIC)”)。在 Cloudflare 中开启 H3 后,Cloudflare 和服务器的通讯仍旧可以使用 HTTP/2(简称 h2)或 HTTP/1.1,并不需要服务器支持 H3,而是由 Cloudflare 进行协议转换。
H3 主要是依赖 Alt-Svc 这个 HTTP 头来进行协议选则的。比如我们可以添加 HTTP 头:
Alt-Svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400
指出服务器支持 H3,H3 的 UDP 端口为 443,有效期(过了有效期后浏览器又会重新使用 H2 或 HTTP/1.1 进行访问)为 1 天(86400 秒),支持最新的 H3 草案以及 27、28、29 草案。另外,我们也可以灵活地修改 Alt-Svc—— 比如可以在 “:443” 之前添加 IP 或域名,做到 HTTP 和 HTTPS 使用不用的 IP 或域名,方便我们在自己的服务器上部署 HTTP 抢答模式,又能使用 Cloudflare 的 H3 协议转换,或者使用不同的域名从而在原域名被 DNS 污染的情况下老用户(没有过有效期 86400 秒的)依然可以通过 H3 访问服务器(因为 H3 是另一个域名,而不是被 DNS 污染的那个域名。或者直接使用 IP,从根本上杜绝了 DNS 污染,只不过之后有可能遭到 IP 封锁)。另外,有效时间也可以进行适当延长(比如从 1 天延长到 1 个月或更长时间),避免客户端尝试 H2 或 HTTP/1.1 并且延长老用户的过期时间。我们可以在还没遇到 HTTPS 的 TCP Reset 时就开启 H3,这样即使之后遭遇了 HTTPS 的 TCP Reset,曾经访问过网站的老用户在 H3 有效期内也能继续访问网站。而且我们也不必担心 UDP 数据包被 ISP 丢弃(俗称 UDP 被 QoS)的问题,因为浏览器在 H3 连接失败的时候会快速回退到 H2 和 HTTP/1.1。
然而,H3 是一个 Alternative 服务。首次访问服务器时,浏览器并不会主动使用 H3,还是会优先使用 H2 或 HTTP/1.1。当获取到 Alt-Svc 头后,浏览器才会在之后的访问中优先使用 H3。这也是为什么有些网站(如 v2ex)需要在科学上网的情况下访问过一次后才能关闭科学上网进行访问(当然,DNS 污染首先还是需要用户自行修改 hosts 解决)。那么,我们如何让用户全程不使用科学上网的情况下访问服务器呢?
首先想到的是通过上面提到的 HTTP 抢答模式提供 Alt-Svc 头,不过可惜的是现在的主流浏览器会忽略 HTTP 中的 Alt-Svc 头,只接受 HTTPS 中的 Alt-Svc 头。而如果 HTTPS 本来就已经被 TCP Reset 的话,浏览器就无法获取 Alt-Svc 头了。那么,那些提供 HTTPS 的 301 海外跳转服务是怎么做的呢?
在调查和尝试了几个支持 HTTPS 的 301 海外跳转服务后,我们发现,它们根本就没有解决 HTTPS 的 TCP Reset 问题,HTTPS 依然被 TCP Reset 了,而它们宣称支持 HTTPS 中 301 跳转的做法就是在 HTTP 抢答模式下加上普通的 TLS 服务。因为大部分网站遭遇的只是 HTTP 中的 TCP Reset 而 HTTPS 并不会被 TCP Reset,所以只需要解决 HTTP 的 301 跳转,再加上普通的 HTTPS,表面上就能同时做到 HTTP 和 HTTPS 的 301 跳转。而且在调查过程中我们还发现有个跳转服务主页的 HTTPS 也被 TCP Reset 了,而他们自己却对此毫无办法。那么,对于新用户访问 HTTPS 的 TCP Reset,我们也只能止步于此,束手无策了吗?那也未必。
其实,从本系列一开始,我们就假设 301 跳转服务器是在国外。如果使用的是国内服务器,那么就能避免墙的识别了(因为不过墙)。但使用国内服务器(似乎)有个绕不过去的坎:备案系统 —— 使用 HTTP 会提示域名没有备案,使用默认端口 443 的 HTTPS 同样会有 TCP Reset。我们又该怎么办?其实,备案系统其实就是一个简化版的墙,没有 TCP 流量重组的功能,使用上面提到的抢答模式同样也能绕过备案系统。而且正是因为备案系统没有 TCP 流量重组的功能,我们甚至可以在 TCP 模式的 HTTP 和 HTTPS 中设置 TCP window size(比如 Linux 上可以使用 Geneva;Windows 上可以自行编写一个反向代理,在其中设置 SO_RCVBUF 为 1,两者的用法在上面都已进行说明,在此不再重复),从而可以直接通过 HTTP 和 HTTPS 绕过备案系统而无需使用 301 跳转(因为备案系统没有 TCP 流量重组功能,所以只需要在连接初始时设置个较小的 TCP window size,之后恢复正常即可。这也是很多国内免备案服务器的原理和使用的方案)。不过,绕过备案系统展示网页有一定的风险,301 跳转风险会小一些,希望大家还是要权衡好利弊。
在讨论好 HTTPS 中防 TCP Reset 的方案后,最后让我们来聊一聊 DNS 污染。
其实,在撰写本文之前,我曾去尝试过几个声称可以解决域名污染的 301 海外跳转的服务,但无一例外都失败了,都无法解决 DNS 污染。然后我也去咨询了提供了这些服务的人,他们的说法大致分为两种:
需要将被 DNS 污染的域名的 NS 记录指向国内的 DNS 服务(如 DNSPod、阿里云等),然后需要等一段时间,运气好的话过一段时间就会解封了(对于这种说法,我也曾经亲自验证过,将一个被 DNS 污染的域名的 NS 记录转移回国内,等了几个月,依然被污染)。
域名污染指的是域名被关键字 Reset,而不是 DNS 污染(这个说法和大多数人理解的不同,将域名污染解释为了 TCP Reset,和 DNS 污染分为了两个概念),他的服务只能解决域名污染,不能解决 DNS 污染。
那么,对于 DNS 污染,我们只能束手无策,或只能碰运气转移回国内了吗?那也未必。不过,由于解决 DNS 污染所需要的成本较高,所以这也是为什么之前 H3 虽然能让用户在原有域名下继续访问,我仍旧建议使用 301 跳转的原因。否则封锁升级为 DNS 污染后连 301 跳转都会变得比较困难了。
讲到这里,细心的同学应该已经发现,其实在刚才 H3 的使用方法中,已经介绍了如何使老用户在 DNS 污染的情况下继续进行访问的方法了。这是解决 DNS 污染部分问题的方法之一。那么,还有没有别的方法也能解决部分问题呢?其实,在 H3 的方案中,我们主要利用了浏览器的 Cache 中记录了 H3 的服务信息,来让老用户通过不同的域名或 IP 进行访问的。那么,浏览器的 Cache 中除了能保存 H3 的信息外,也是可以保存其他内容的。讲到这里,聪明的同学应该已经想到了。没错,就是 Cache-Control(或使用 Expires 也有相同的效果)。通过这个 HTTP 头,我们可以将一个页面的过期时间设置成很长,在过期之前,浏览器并不会发起 HTTP 请求,甚至没有网络的离线情况下都能访问(使用 F5 刷新除外,这时候浏览器会忽略过期时间从而发起 HTTP 请求)。在这个页面中,我们可以引用别的域名下的 JavaScript 脚本文件,在 JavaScript 而非 HTML 中渲染整个网页。这样,老用户同样可以在 DNS 污染的情况下继续访问我们的服务器。不过,这种做法对 SEO 不是很友好,但我们可以使用 HTML 和 JavaScript 同时渲染的方法让搜索引擎可以进行索引 ——HTML 中仍旧是正常内容给搜索引擎进行索引,而浏览器会加载 JavaScript,使用 JavaScript 重新渲染一遍网页,避免 Cache 没有过期而呈现老页面的问题。老用户的问题可以解决,但新用户怎么办呢?或者我们有没有办法从根本上来解决 DNS 污染呢?而且听说现在有一些价格昂贵的污染清洗服务,它们真的能从根本上解决 DNS 污染吗?它们是怎么做的呢?
如果我们想从根本上解决问题,首先我们还需要了解整个 DNS 系统是怎么工作的:
DNS 服务器分为递归查询服务器、DNS 代理和权威服务器(称为 ADNS)。我们把递归查询服务器和 DNS 代理统称为 LDNS。
普通用户上网所使用的一般是 ISP 提供的 LDNS,它会负责向 ADNS 查询真实的 A(和 AAAA 以及其它)记录。
ADNS 即是域名的 NS 记录所指向的服务器。
我们知道,DNS 污染是墙在海外 ADNS 返回正确的结果之前进行了抢答,返回了错误的结果。这样,在国内 LDNS 向海外 ADNS 查询的时候,同样会受到 DNS 污染,从而返回给普通用户错误的结果。那么,我们有没有办法劫持 ISP 的 LDNS,从而让其返回我们想要的 IP 而不是墙返回的错误 IP 呢?这样,虽然 DNS 污染仍旧存在,但普通用户却得到了正确的 IP,从而可以正常访问我们的服务器了。
讲到这里,就不得不提到 2008 年曾经轰动全球的 DNS 投毒攻击案了。在这篇文章中,Kaminsky 可以修改任意 LDNS 中缓存的 A(或 AAAA 以及其它)记录,虽然在经过了那次事件后这个漏洞更难被利用了,但终究无法完全修复,我们仍旧可以利用其中的原理劫持 ISP 的 LDNS(能猜中源端口和 QID 就能进行劫持),将被污染域名的 IP 换成自己想要的 IP。而且由于墙污染的 TTL 较小,我们也能更快地利用这个漏洞而不需要每次等待 1 天的时间。所以短则几天,慢则几个星期就能劫持成功。这也是现在有些价格不菲的污染清洗服务所采用的方案之一。当然,某些攻击团队也同样在利用这个漏洞就行 DNS 劫持,虽然导致的结果是 LDNS 被劫持而非墙的 DNS 污染,但对于普通用户所造成的结果是一致的 —— 网站无法访问。不过,要实施这种 DNS 劫持需要源 IP 欺骗,现在能进行源 IP 欺骗的服务器已经越来越少了。那么我们还有没有别的方法无需源 IP 欺骗来劫持 LDNS 呢?
既然大家已经看了上面的文章么让我们来重新细致地梳理一下整个 DNS 查询过程。以浏览器访问 https://www.youtube.com/ 为例:
操作系统向 ISP 的 LDNS 发起请求 www.youtube.com 的 A(以及 AAAA)记录。
LDNS 查询缓存中有没有 www.youtube.com 的 A(或 AAAA)记录,如有则返回给客户端,如没有则执行第 3 条。
LDNS 查询(可从缓存中查询)DNS 根服务器(当前为 13 个)的 A(或 AAAA)记录。
LDNS 向根服务器(或从缓存中)查询 .com 的 ADNS。
LDNS 向 .com 的 ADNS 发起查询 youtube.com 的 ADNS(即 youtube.com 的 NS 记录)。
LDNS 向 youtube.com 的 ADNS 发起查询 www.youtube.com 的 A(或 AAAA)记录。之后返回给客户端。
我们知道, youtube.com 是个被 DNS 污染的域名,所以第 5 和第 6 步会受到墙的 DNS 污染,而前 4 步不会。第 6 步我们也很熟悉了,国内 IP 向国外 IP 发起查询请求时墙就会抢答 www.youtube.com 的错误 A(或 AAAA)记录。而第 5 步中,由于 LDNS 在国内,查询到 youtube.com 的 NS 记录同样会受到污染。我们同样知道,墙的 DNS 污染虽然成功概率接近 100%,但仍有很小的概率会污染失败。那么我们能不能不停地向 LDNS 请求 www.youtube.com 的 A(或 AAAA)记录,在墙污染失败的时候,LDNS 就能刷新到正确的 IP 地址了呢?可惜的是,LDNS 是有缓存的,在缓存有效期内,不会再次向 ADNS 发起请求。即使在缓存失效后偶尔会由于污染失败得到了正确的 IP 地址,但在缓存再次失效后由于污染再次回到了错误的 IP 地址。所以被污染的概率仍旧接近 100%。墙看上去似乎无懈可击,我们该怎么办呢?
让我们重新回到第 5 条,使用国内 IP 向 .com 的 ADNS 请求 youtube.com 的 NS 记录。以 Linux 为例:
dig ns youtube.com @e.gtld-servers.net
(e.gtld-servers.net 为 .com 的其中一个 ADNS)
我们看到墙返回了污染的结果, youtube.com 被污染的 NS 是…… 咦?不对!墙竟然返回的是 A(或 AAAA)记录,而不是我们查询的 NS 记录!而且墙的污染是有很小的概率会失败的!相信聪明的同学已经想到了 —— 由于墙返回的是不是 NS 记录,所以 LDNS 没有获取到 youtube.com 的 NS 记录,自然无法将 youtube.com 的 NS 记录存入缓存中。所以,在下次客户端请求 youtube.com 的 NS 记录时,LDNS 会再次向 .com 的 ADNS 请求 youtube.com 的 NS 记录而不是从缓存中获取。既然不存在缓存,我们就能一直向 LDNS 发起请求,而 LDNS 就会一直向 ADNS 发起请求,直到墙的污染失败出现,LDNS 终于获得了正确的 NS 记录。而由于 NS 记录本身是带有 TTL 的,所以会被存入 LDNS 的缓存之中,在缓存过期之前不会再受到墙的污染。而我们可以将 NS 记录的 TTL 设置得非常长,从而可以在很长得时间内让墙得污染无法生效。而在 TTL 过期之后,我们可以利用同样的方法再次让 LDNS 获得正确的 NS 记录。
在解决了第 5 条中的污染后,我们还需要解决第 6 条中的污染。而第 6 条中墙返回的确实是查询的 A(或 AAAA)记录,会被存入 LDNS 缓存,也就无法利用上述方法了。我们该怎么办呢?相信聪明的同学也已经想到了。没错,就是将 NS 记录转移回国内,这样 DNS 请求就不会过墙,自然就不会受到污染了。可是,不对呀?刚才不是讲过我也曾经亲自验证过,将一个被 DNS 污染的域名的 NS 记录转移回国内,等了几个月,依然被污染么?那是因为之前的测试只是将 NS 记录指向了国内服务器,我们并没有大量地发送 NS 查询请求到 LDNS,所以 LDNS 并没有获得正确的 NS 记录,所以污染仍旧存在。而且,ISP 的 LDNS 是分运营商并且分区域的。只将一个 LDNS 中的 NS 刷新到正确结果只能解决一个运营商的一小片区域中的污染,如果想要在全国范围内解决污染,需要使用大量的 IP 地址(因为很多 ISP 的 LDNS 限制了查询请求的发起 IP 只能是本地宽带用户),不停地对大量的 LDNS 查询 NS 记录,直到全国大部分地区的 LDNS 都获取到了正确的 NS 记录,才能在大范围内解决 DNS 污染。而且即使 LDNS 获取到了正确的 NS 记录,查询仍然要继续,因为缓存是有过期时间的。而这,也是现在很多昂贵的污染清洗服务所采用的方案之一。
参考资料
https://github.com/lehui99/articles
GFW 如何检测与封锁加密流量#
1. 前言
完全加密的翻墙协议是翻墙生态系统中的基石。不同于像 TLS 这样的协议以明文握手开始,完全加密(随机化)的协议 -- 如 VMess 、Shadowsocks 和 Obfs4 -- 被设计成连接中的每个字节都与随机数据没有区别。这些 协议的设计理念是:它们应该很难被审查者抓住特征,因此阻断的成本很高。
2021 年 11 月 6 日,开始有大量来自中国的互联网用户报告说他们的 Shadowsocks 和 VMess 服务器被封锁。 11 月 8 日,一个 Outline 开发者报告说来自中国的使用量突然下降。这次封锁的开始时间恰逢 2021 年 11 月 8 日至 11 日召开的中国共产党第十九届中央委员会第六次全体会议 。能够封锁这些翻墙工具代表中国的防火长城(GFW)探测能力上了一个新的台阶。据我们所知,虽然中国自 2019 年 5 月以来一直在采用被动流量分析和主动探测相结合的方式来识别 Shadowsocks 服务器,但这是审查者第一次能够仅基于被动流量分析,就实时地大规模封锁完全加密的代理。
在这项工作中,我们对 GFW 被动检测和封锁全加密流量的新系统进行了测量和描述。我们发现,审查者应用了至少五条粗糙但高效的规则来豁免那些不太可能是完全加密的流量;然后,它阻止其余未豁免的流量。这些豁免规则基于常见的协议指纹、以及第一个 TCP 数据包的有效数据包中 ASCII 字符的比例、位置和最大连续数。
我们还分析了这种新形式的被动封锁与 GFW 广为人知的主动探测系统 之间的关系:二者是独立运行的。我们同时还发现,主动探测系统也依赖于这种流量分析算法,并额外应用了一条基于数据包长度的豁免规则。因此,能够逃避这种新的封锁的规避策略,也可以帮助防止 GFW 识别并随后主动探测代理服务器。
自 2022 年 1 月以来,这些规避策略被广泛采用和部署,已帮助数百万用户绕过这一新的封锁技术。据反馈,截至 2023 年 2 月,这些工具采用的所有规避策略在中国仍然有效。
2. 背景
2.1 流量混淆策略
将混淆翻墙流量的方法可大致分为两类:隐写 (steganography) 和多态 (polymorphism) 。隐写代理的目标是使翻墙流量看起来像像正常的流量一样;多态的目标是使翻墙流量看起来不像应该被禁止的流量。
实现隐写的两种最常见的方法是模仿 (mimicking) 和隧道传输 (tunneling)。其中模仿类协议有着根本性的缺陷,因为将原始流量通过被允许的协议进行隧道传输是一种更抗封锁的方法。但即使使用隧道传输,翻墙软件的设计者仍然需要额外的努力让翻墙协议的指纹与流行的实现方式的指纹保持完全一致,以避免受到基于协议指纹的封锁。例如,在 2012 年,中国和埃塞俄比亚部署了深度包检测系统,通过 Tor 使用的不常见的密码套件来检测 Tor 流量 。审查设备供应商之前已经根据 meek 发出的 TLS 指纹和 SNI 值来识别并封锁它了 。
为了避免这种复杂性,许多流行的翻墙软件选择了多态的设计。实现多态性的一个常见方法是,从连接中的第一个数据包开始,就对其有效数据包进行完全加密。由于没有任何明文或固定的包头结构指纹,审查者没办法简单地使用正则表达式或通过寻找流量中的特定模式来识别代理流量。这种设计在 2009 年首次被引入 Obfuscated OpenSSH。此后,Obfsproxy 、Shadowsocks、Outline、VMess、ScrambleSuit 、Obfs4 都采用了这种设计。而 Geph4、Lantern 、Psiphon3 和 Conjure 也部分采用这种设计。
完全加密的流量经常被称为 “看起来什么都不像” 的流量,又或者被误解为 “没有特征”;然而,更准确的描述应该是 "看起来像随机数据"。事实上,这种流量确实有一个使其与其他流量不同的重要特点:完全加密的流量与随机流量是无法区分的。由于没有可识别的协议头,整个连接中的流量都是均匀且高熵的,甚至从第一个数据包中就已经如此。相比之下,即使像 TLS 这样的加密协议也还有相对低熵的握手包,用以传达支持的版本和扩展。
2.2 主动探测攻击及其防御措施
在主动探测攻击中,审查者向被怀疑的服务器发送精心制作的有效数据包,并测量它的反应。如果服务器以与众不同的方式回应这些探测(例如让审查者将其作为代理使用),审查者就可以识别并封锁它。 早在 2011 年 8 月,人们就观察到 GFW 向接受过来自中国的 SSH 登录的外国 SSH 服务器发送看似随机的有效数据包 。2012 年,GFW 首先寻找一个独特的 TLS 密码来怀疑 Tor 流量;然后向可疑的服务器发送主动探测,以确认其猜测。2015 年,Ensafi 等人对 GFW 针对各种协议的主动探测攻击进行了详细分析 。自 2019 年 5 月起,中国部署了一个审查系统,分两步检测和封锁 Shadowsocks 服务器:它首先使用每个连接中第一个数据包有效数据包的长度和熵来被动地识别可能的 Shadowsocks 流量,然后在分阶段地向可疑的服务器发送各种探针,以确认其猜测 。作为回应,研究人员提出了各种针对主动探测攻击的防御措施,包括让服务器对各种连接的反应保持一致 和应用前置 。 Shadowsocks、Outline 和 V2Ray 都采用了防主动探测的设计 ,使得它们自 2020 年 9 月以来在中国就没再被封锁过 ,直到最近在 2021 年 11 月被再次封锁 。
3. 方法
我们在中国境内外的主机之间制作并发送各种测试探针,让它们被 GFW 观察到。我们在两个端点主机上抓包并比较流量,来观察 GFW 的反应。这种记录使我们能够识别任何被丢弃或被操纵的数据包,包括主动探测。
实验时间线和实验节点。
我们在上表中总结了所有主要实验的时间线和所使用的实验节点。我们总共使用了腾讯云北京(AS45090)的 10 台 VPS 和阿里云北京(AS37963)的 1 台 VPS。 我们没有观察到中国境内的实验节点或任何受影响的国外节点之间的审查行为有任何差异。我们使用了 Digital Ocean 旧金山(AS14061)的四台 VPS:其中三台受到了新审查机制的影响,剩下一台则没有受到影响。我们根据 IP2Location 数据库 检查了我们的 VPS 的 IP 地址,并确认它们的地理位置与供应商所报告的位置相符。
触发审查制度。
因为外界观察者是无法区分完全加密的流量与随机数据的,所以除了使用实际的翻墙工具外,我们还开发了测量工具用来在我们的研究中发送随机数据以触发封锁。这些工具完成一个 TCP 握手后,会发送一个给定长度的随机有效数据包,然后关闭连接。
使用残留审查 (residual censorship) 来确认封锁。
与 GFW 封锁许多其他协议的方式类似,在一个连接触发审查后,GFW 会阻止所有具有相同客户端 IP、服务器 IP、服务器端口的后续连接 180 秒。这种残留审查允许我们通过从同一客户端发送后续连接到服务器的同一端口来确认封锁。我们逐一进行共计五次的 TCP 连接,中间有一秒钟的时间间隔。如果五次连接都失败了,我们就得出结论,这个连接被封锁了。如果一个连接被封锁,我们在接下来的 180 秒内不会再使用它进行进一步的测试。
对重复测试的概率阻断进行计算。
我们经常要用相同的有效数据包进行多次连接,才能观察到封锁。在第 6.3 节中,我们解释了这是因为 GFW 采用了一种概率阻断策略,大约只有四分之一的概率触发审查。为了减少这种概率行为造成的测量误差,我们在对任何一次阻断(或不阻断)的观察下判断之前,都要发送最多 25 次有着相同有效数据包的连接。如果我们能够连续成功地用相同的有效数据包进行 25 次连接,那么我们就得出结论,该有效数据包(或服务器)不受这种新审查的影响。如果在至少发送一次有效数据包后,随后的 5 次连接尝试都(由于残留审查而)超时了,那么我们就将有效数据包(和服务器)标记为受到了新的审查的影响。在整个研究过程中,我们对所有有效数据包的测试,都采用了这种重复连接的方法。
- 测量并认识新审查技术的特点
4.1 基于熵的豁免规则(Ex1)
我们观察到,为 1 的比特数影响了一个连接是否被阻断。为了确定这一点,我们向服务器重复发送连接,并观察哪些连接被阻止。在每个连接中,我们发送 256 个不同的字节模式中的一个,由 1 个字节重复 100 次组成(例如,\x00\x00\x00...,\x01\x01\x01...,...,\xff\xff\xff...)。 我们对每个模式都发送 25 次包含这一模式的连接到我们的服务器,并观察是否有任何模式导致后续连接被阻断。如果有某个连接被阻断,则表明它的有效数据包触发了审查。我们发现共有 40 个字节的模式触发了封锁,而其余 216 个模式没有。 被封锁的模式例子包括 \x0f\x0f...,\x17\x17\x17...,和 \x1b\x1b\x1b...(以及其他 37 个)。
所有被阻断的模式的每个字节的八位比特中都恰好有 4 位是 1 比特(例如,二进制的 \x1b 是 00011011)。 我们猜想每个字节的 1 比特的数量可能起作用,因为均匀的随机数据将有接近相同数量的二进制的 0 比特和 1 比特。实质上,这是在测量客户端数据包内的比特的熵。
我们发送这样的字节组合来确认这一猜想:组合中的每种字节单独发送都被允许,但组合起来发送就会被禁止。例如,\xfe\xfe\xfe... 和 \x01\x01\x01... 都没有被单独封锁,但这些字节作为 \xfe\x01\xfe\x01... 一起发送却被阻止。 我们注意到 \xfe\x01 的 16 位比特中有 8 位被设置为 1(平均每个字节设置 4 个比特),而 \xfe 的 8 位比特中有 7 位被设置为 1,\x01 的 8 为比特中有 1 位被设置为 1。这解释了为什么它们单独发送被允许,但组合起来发送就被阻止。
当然,随机或加密的数据不会总是正好有一半的比特被设置为 1。我们通过发送一串 50 个随机字节(400 比特),并设置了越来越多的比特为 1 的实验,来测试 GFW 需要多接近一半的比特才能阻断。 我们产生了 401 个比特串,其中有 0-400 个比特被设置为 1,并对每个字符串的比特位置进行洗牌,以产生一组随机字符串,每个字节设置 0-8 比特(以 0.02 比特 / 字节为增量)。对于每个字符串,我们发送 25 次连接含有它的连接,以观察它是否会引发后续连接的封锁。我们发现,所有具有 3.4 比特 / 字节的字符串都没有被封锁,而 3.4 至 4.6 比特 / 字节的字符串则被封锁了。
这其中有一个例外,那就是有一个 4.26 比特 / 字节集的字符串也没有被封锁。这是因为它有超过 50% 的字节是明文 ASCII 字符;我们接下来会介绍这是另一条豁免规则(Ex2)。我们重复了我们的实验,并确认其他具有相同 1 比特数但明文 ASCII 字符较少的字符串,确实被阻止了。
综上所述,我们发现,如果一个连接中,客户端的第一个数据包中 1 比特比例偏离一半,GFW 就会豁免这个连接。这相当于对熵的粗略测量:随机(加密)数据 z 总有接近一半的比特被设置为 1,而其他协议由于明文或有零填充的协议头,每字节的 1 比特数通常较少。例如,谷歌浏览器 105 版发送的 TLS Client Hello 包,由于用零填充,每字节平均只有 1.56 个 1 比特,属于豁免范围。
4.2 基于明文 ASCII 字符的豁免规则 (Ex2~4)
我们观察到在第 4.1 节中发现的比特计数规则有几个例外。例如,模式 \x4b\x4b\x4b... 没有被封锁,尽管每个字节正好设置了 4 位。事实上,实际上有 70 个字节(8 选 4)正好有 4 位为 1 比特,但是我们的分析发现,其中只有 40 个触发了审查。那另外 30 个呢?
这另外 30 个字节的值都属于明文 ASCII 字符的字节范围,即 0x20-0x7e。 我们推测,GFW 豁免这些字符可能是为了允许明文的人类可读协议。
我们发现,GFW 有三种关于明文 ASCII 字符的豁免方式,都是基于连接中客户端发送的第一个数据包的有效数据包:如果前六个字节是明文(Ex2);如果超过一半的字节是明文(Ex3);或者如果它包含超过 20 个连续的明文字节(Ex4),则允许连接。
前六个字节是明文(Ex2)
我们观察到,如果一个连接的前 6 个字节在明文字节范围 0x20-0x7e 内,那么 GFW 就会豁免该连接。如果前 6 个字节中有超出这个范围的字符,那么连接就可能会被阻止,前提是它没有符合其他豁免的规则(例如,每个字节集有少于 3.4 位的 1 比特)。 我们通过生成不同有效数据包进行测试,其中前 n 字节来自不同的字符集(如明文 ASCII 字符),而消息的其余分部将是随机的非明文字符。 我们观察到,对于 n<6,连接被阻断,但对于 n>=6,即前 n 字节都是明文 ASCII 字符时,没有发生阻断。
第一个数据包有一半的有效数据包是明文(Ex3)
如果第一个数据包的有效数据包中超过一半的字节属于明文 ASCII 范围 0x20-0x7e,那么 GFW 就会豁免该连接。 我们通过构造并发送这样的有效数据包来测试:其前 10 字节由明文 ASCII 范围以外的字符组成(例如 0xe8),然后是一个 6 个字节的重复序列:5 个在这个明文范围内(如 0x4b),而最后一个在明文范围外。我们重复这个 6 字节的序列 5 次,然后在字符串的末尾用明文范围外的 n 个字节来填充。 这个实验给我们一个可变长度的模式,随着我们增加 n,明文 ASCII 范围内的字节的比例减少了。 我们发现,对于 n<10,连接不会被阻断,而对于 n>=10,连接会被阻断。 这相当于当明文字符的比例小于或等于一半时被阻断,而当大于一半时不被阻断。
我们设计这样的有效数据包是为了避免触发其他 GFW 豁免规则,例如比特比例(Ex1)、明文前缀(Ex2)或连续的明文字符(Ex4)。 例如,我们分别使用 0x4b 和 0xe8 作为明文和不明文字符,因为它们都正好有 4 位的设置。 这可以防止 GFW 因为前面讨论过的 1 比特比例规则(Ex1)而豁免封锁我们的连接的情况。 此外,我们避免让明文字符 0x4b 连续出现,因为我们观察到这样的模式也能豁免封锁连接,这一点我们接下来会讨论。 我们用其他同样符合这些限制条件的模式(如 0x8d 和 0x2e)重复了我们的实验,并观察到相同的结果。
超过 20 个连续的字节是明文(Ex4)
一个明文字符的连续出现也可以免除封锁,即使明文字符的总比例不到一半。 为了测试这一点,我们发送了一个由明文范围以外的字符(0xe8)组成的 100 个字节的模式,以及来自明文范围的不同数量的连续字节(我们使用 0x4b)。 我们的有效数据包从 10 个字节的 0xe8 开始,接着是 n 字节的 0x4b,然后是 90-n 字节的 0xe8,总长度为 100 个字节。 我们让变量 n 在 0-90 之间变化,并把每个相同的数据包都向我们的服务器发送 25 次。 我们发现,对于 n<=20,连接被阻断了。当 n>20,连接没有被阻断。 这证明当有连续的明文 ASCII 字符出现时,连接会被豁免。 当然,当 n>50,连接也会被豁免,因为豁免规则 Ex3。
其他编码方式
我们测试了如果第一个数据包中包含中文字符,是否也可以与明文 ASCII 字符一样,让连接免于阻断。 我们使用了以 UTF-8 编码的 6-36 个中文字符串,以及 GBK(与我们使用的 GB2312 字符相同)。 所有这些测试连接都被阻断了,这表明不存在基于汉字的豁免规则。 这可能是因这些编码中出现的汉字的情况很少,或者是因为如果要解析这些编码,会对审查系统增加不合理的复杂性,因为很难知道一个编码字符串的开始或结束位置。
4.3 基于常见协议的豁免规则(Ex5)
为了避免误伤流行的协议,我们观察到 GFW 明确地豁免了两种流行的协议。 GFW 似乎是用客户端数据包的前 3-6 个字节来推断协议:如果它们与已知协议的字节相匹配,连接就会被免除阻断,即使数据包的其余部分不符合该协议。我们测试了六种常见的协议,发现 TLS 和 HTTP 协议被明确地豁免了。这个豁免列表可能并不详尽,因为可能还有其他我们没有测试的豁免协议。
TLS。 TLS 连接以 TLS ClientHello 消息开始,该消息的前三个字节会使 GFW 豁免连接。我们观察到,GFW 豁免了任何前三个字节与以下正则表达式匹配的连接:
(\x16-\x17]\x03[\x00-\x09)
这对应于一个字节的记录类型 (record type),后面是一个两字节的版本 (version)。 我们列举了所有 256 个 XX\x03\x03 的模式,并在后面加上 97 个字节的随机数据。我们发现除了那些以 0x16(对应 TLS 中的 Handshake 包,用于 ClientHello)或 0x17 (对应 TLS 中的应用数据类包 (Application Data))开始的模式外,其他所有模式都被封锁。 虽然通常的 TLS 连接不会以应用数据开头 , 但当 TLS 被用于多路径 TCP(MPTCP) 时, 常见的情况是,其中一个 TCP 子流被用于 ClientHello,而其他子流在 TCP 连接建立后立即发送应用数据。 到目前为止,只有 TLS 的(0x00-0x03) 版本被定义 ,但 GFW 甚至允许更晚的(尚未定义)版本。
HTTP。 审查者用来识别 HTTP 流量的字节模式很简单,就是在 HTTP 请求方法的后面跟有一个空格。如果一个信息以 GET、PUT、POST 或 HEAD 开头,那么这个连接就会被免于阻断。每个请求方法的后面的空格字符(0x20)是让连接免于屏蔽的必要条件。如果不包括这个空格字符,或用任何其他字节代替它,就不能豁免连接。其他的 HTTP 请求方式(OPTIONS, DELETE, CONNECT, TRACE, PATCH)均因为前 6 个字节是明文字符,而已经满足明文豁免规则(Ex2)。同时,我们发现 HTTP 请求方法是不区分大小写的:GeT、get 和类似的变体都可以使连接被豁免。请求方式的错误拼写(例如,TEG)不属于豁免范围。
不被豁免的协议。 我们测试了其他常见的协议:SSH、SMTP 和 FTP 将被豁免,因为它们都以至少 6 个字节的明文 SCII 开头(规则 Ex2)。DNS-over-TCP 由于包含很大一部分的零,使得它被 Ex1 规则豁免。然而,如果在 DNS-over-TCP 消息后附加足够多的随机数据,它将被阻止。
上面观察到的现象让大家提出了一个问题:为什么审查者使用明确的规则来豁免 TLS 和 HTTP,而不是其他协议。 毕竟,审查者不需要明确地豁免这两种协议:HTTP 通常会都满足前 6 个字节为明文 SCII 的豁免规则(Ex2),而 TLS ClientHello 包由于有许多零字段,其也会因位数熵相对较低而满足 Ex1 豁免规则。也许这是因为审查者可以采用这些简单而高效的规则来快速地豁免大部分的网络流量(TLS 和 HTTP),而不需要进行如计算数据包中 1 比特的比例、明文 SCII 的比例等更深入的分析。
4.4 GFW 是如何阻断连接的
一旦 GFW 检测到完全加密的流量,就会按照下面介绍的方式阻断后续流量。
丢弃从客户端到服务器的数据包。 我们先触发 GFW 的阻断,然后比较在客户端和服务器捕获的数据包。我们观察到,在触发审查后,客户端的数据包被 GFW 丢弃,并没有到达服务器。然而,服务器发送的数据包没有被阻断,客户端仍然可以收到。
UDP 流量不受影响。 新的审查系统只限于 TCP。发送一个具有随机有效数据包的 UDP 不能触发审查。此外,即使某个具有相同客户端 IP、服务器 IP、服务器端口的连接由于 TCP 连接而被封锁,往来于同一(服务器 IP、服务器端口)的 UDP 数据包也不受影响。由于没有 UDP 拦截,用户在使用 Shadowsocks 时可能会遇到奇怪的现象:他们仍然可以使用某些依赖 UDP 的网站或应用程序(如 QUIC 或 FaceTime),但无法访问使用 TCP 的网站。这是因为 Shadowsocks 用 TCP 代理 TCP 流量,用 UDP 代理 UDP 流量。审查者不检测或阻止 UDP 流量,可能反映了其更糟就是更好 (worse is better) 的工程思维。从实际情况来看,目前的 TCP 封锁已经足够有效地让这些流行的翻墙工具瘫痪,而如果增加 UDP 审查,则需要额外的资源,并给审查系统引入额外的复杂性。
所有端口的流量都可能被阻断。 我们在美国建立了一个监听在所有端口(从 1 到 65535)的服务器。然后,我们让中国的客户端不断地用 50 字节的随机有效数据包与美国服务器的每个端口进行连接,并在某个端口被封锁后停止反复地连接这一端口。我们发现,从 1 到 65535 的所有端口都可能被封锁。因此,在一个不寻常的端口上运行翻墙服务器并不能缓解封锁。我们也没有观察到使用不同端口会导致不同的审查行为。
GFW 存在残留审查 (residual censorship)
我们发现,这个新的审查系统一旦阻断了一个连接,它就会在后续的 120 或 180 秒内继续丢弃所有具有相同客户端 IP、服务器 IP、服务器端口的 TCP 数据包。这种行为通常被称为 “残留审查”。与其他一些残留审查系统不同 ,GFW 的残留审查定时器不会在观察到更多触发审查的数据包后被重置。
我们还发现,GFW 似乎限制了它在任何给定时间内残留审查的连接的数量。我们让中国的客户端重复性地同时连接到一个服务器的 500 个端口。在每个连接中,客户端发送 50 字节的随机数据,然后关闭连接。我们记录了每次发生残留审查的持续时长。如上图所示,与只有一个端口被封锁时的 180 秒持续时间相比,该实验中的残留审查持续时间大幅下降。
4.5 GFW 是如何重组流量的
在这一节中,我们将研究 GFW 的新审查系统是如何重新组合流量,并考虑流量方向。
一个完整的 TCP 握手是必要的。 我们观察到,发送一个 SYN 包,然后再发送一个包含随机数据的 PSH+ACK 包(在服务器没有完成握手的情况下),并不足以触发阻断。这样的残留审查更难被攻击者利用。
只有客户端到服务器的数据包可以触发阻断。 我们发现,GFW 不仅检查随机数据是否被发送到属于受影响的 IP 范围内的目标 IP 地址,而且还检查并只在随机数据从客户端发送到服务器时才进行阻断。这里的服务器是指在 TCP 握手过程中发送 SYN+ACK 的主机。
我们通过在两台主机之间设置的四个实验来了解这一点。在第一个实验中,我们让在中国的客户端连接并向美国服务器发送随机数据;在第二个实验中,我们仍然让中国的客户端连接到美国服务器,但让美国服务器向客户端发送随机数据;在第三个实验中,我们让美国的客户端连接并向中国服务器发送随机数据;在第四个实验中,我们让美国的客户端连接到中国服务器,但随后让中国服务器向美国客户端发送随机数据。只有第一个实验中的连接被封锁了。
GFW 只检查第一个数据包。 GFW 似乎只分析 TCP 连接中的第一个数据包,而不对有多个数据包的流量进行重新组合。我们通过以下实验来测试这一点。在 TCP 握手后,我们发送第一个数据包,其中只有一个字节的有效数据包 \x21。在等待一秒钟后,我们再发送带有 200 字节随机有效数据包的第二个数据包。我们重复了 25 次实验,但连接从未被封锁。 这是因为在看到第一个数据包后,GFW 已经通过规则 Ex1 豁免了连接,因为它的有效数据包中包含 100% 明文 ASCII。换句话说,如果 GFW 在其流量分析过程中把多个数据包重新组合成一个流,它就能够阻止这些连接。
我们发现,GFW 不会等到看到服务器的 ACK 响应时才去阻止一个连接。我们用一个 iptables 规则将我们的服务器配置为放弃任何传出的 ACK 数据包。然后我们用 200 字节的随机有效数据包与服务器建立连接。尽管服务器没有发送任何 ACK 数据包,GFW 仍然阻止了这些连接。
GFW 对第一个数据包等待时间超过了 5 分钟。 我们研究了 GFW 会在 TCP 握手之后,但在看到第一个数据包之前,对一个 TCP 连接进行了多长时间的监控。根据观察,它需要一个完整的 TCP 握手来触发封锁,我们因此推断 GFW 可能是有状态的。因此,我们有理由怀疑 GFW 只在有限的时间内监控一个连接,因为要永久追踪一个连接的状态而不放弃的开销很大。
我们的客户端完成了 TCP 握手,然后等待了 100 秒、180 秒或 300 秒,然后发送 200 字节的随机数据。接着,我们重复了这个实验,但使用 iptables 规则丢弃了任何 RST 或 TCP keepalive 数据包,以防它们帮助 GFW 保持对连接的追踪。 我们发现这些连接仍然触发了阻断,这表明 GFW 对连接状态的追踪至少有 5 分钟。
- 与主动探测系统的关系
正如第 2.2 节所介绍的,GFW 自 2019 年以来一直在向 Shadowsocks 服务器发送主动探测探针。在这一节中,我们研究了这个新发现的实时阻断系统和已知的主动探测系统之间的关系。通过测量实验和对历史数据集的分析,我们发现,虽然这两个审查系统并行工作,但主动探测系统的流量分析模块应用了上述总结的所有五条豁免规则,并且还用一条额外的规则,来检查第一个数据包的有效数据包长度。我们还表明,主动探测系统使用的流量分析算法可能自 2019 年以来有所进化。
主动探测实验。 在部署这个新的实时阻断系统之前,从外界推断主动探测系统的流量分析算法,是极具挑战性的。这是因为 GFW 在看到触发连接和发送主动探测之间设置了一个任意长度的延迟 。这就使得我们很难说明 GFW 的哪些探测是由我们发送的哪些连接触发的。现在我们已经在第 4 节中推断出了这个新的阻断系统的流量检测规则列表,我们可以测试被豁免的有效数据包是否也不会被主动探测系统所怀疑。
我们在 2022 年 5 月 19 日和 6 月 8 日之间进行了实验。如表 2 所示,我们制作了 14 种不同类型的有效数据包:其中 3 种是长度为 2、50 和 200 字节的随机数据;其余 11 种是具有不同长度的数据,这些数据仅能被算法 1 中的某一个豁免规则豁免。然后,我们从中国北京腾讯云的一个 VPS 向美国旧金山 DigitalOcean 的两个不同主机的 14 个端口,发送了 14 种有效数据包。其中一台美国主机已知受到当前阻断系统的影响,而另一台美国主机则不受影响。这样,如果我们收到来自 GFW 的任何探测,我们就知道当前封锁系统使用的某些豁免规则没有被主动探测系统使用。
我们在中国的客户端总共向两台美国服务器的每个端口发送了约 17 万次连接。然后我们采取措施,将来自 GFW 的主动探测与其他互联网扫描探测隔离开。我们根据 IP2Location 数据库和 AbuseIPDB 检查每个探测的源 IP 地址。如果它是一个非中国的 IP 或者来自一个已知的被用来扫描的 IP 地址,我们就不认为它是来自 GFW 的主动探测。我们进一步检查并确认该探针是否属于 GFW 发送的任何已知类型的探针。
这两个系统独立工作。 新的审查机器纯粹是根据被动流量分析做出封杀决定,而不依赖中国主动探测基础设施。我们之所以知道这一点,是因为虽然 GFW 仍然向服务器发送主动探测,但在超过 99% 的测试中,GFW 在封锁一个连接之前没有向服务器发送过任何主动探测。举个例子,如上表所总结的,我们进行了 33119 次连接,但只收到 179 次主动探测。事实上,与之前的工作的发现相似,主动探测很少被触发。
我们想强调的是,这一发现并不意味着对主动探测的防御没有必要或不再重要。恰恰相反,我们认为 GFW 对纯被动流量分析的依赖,部分原因是 Shadowsocks、Outline、VMess 和其他许多翻墙软件已经对主动探测采取了有效的防御措施。GFW 仍然向服务器发送主动探测这一事实,意味着审查者仍然试图使用主动探测,尽可能准确地识别翻墙服务器。
主动探测系统对可疑流量应用了五条豁免规则,并增加了一条基于数据包长度的豁免规则。 这个实验表明了两点。首先,主动探测系统应用一个额外的规则来检查连接中的有效数据包的长度。在我们的案例中,只有 200 字节有效数据包的连接曾经触发了主动探测,而 2 字节或 50 字节的连接则从来没有。其次,如果流量符合的五条豁免规则中的任何一条,那么该流量也不会触发主动探测系统。
自 2019 年以来,主动探测系统已经有所发展。 我们想知道 GFW 的检测规则是否曾经被用来触发主动探测。为了分析它,我们收集了 282 个能触发 GFW 主动探测的数据包然后我们写了一个程序来确定一个有效数据包是否会被当前的阻断系统豁免,并将获得的 282 个有效数据包输入该程序。结果,以前触发主动探测的 45 个探测被豁免了(根据规则 Ex3)。2022 年 5 月 19 日,我们反复发送这 45 个有效数据包,让它们被 GFW 看到,并确认它们确实被当前的阻断系统豁免了。对于每个有效数据包,我们用它从腾讯云北京的 VPS 到 Digital Ocean 旧金山的水槽服务器进行了 25 次连接。 这个结果表明, 自 2020 年以来,GFW 很可能已经更新了其主动探测系统的流量分析模块。此外,目前 GFW 发送的探针也与 2020 年观察到的探针不同 。 新的探针基本上是随机有效数据包,分别以 16、64 和 256 字节为中位数的分布。对于这些长度中的每一个,GFW 发送的探针数量大致相同:一台服务器收到了 48、46 和 47 个探针,另一台收到了 238、228 和 233 个探针。
- 了解阻断策略
在本节中,我们进行了测量实验,以确定审查者的封锁策略。我们发现,可能是为了减少误报和降低运营成本,审查者策略性地将封锁范围限制在热门数据中心的特定 IP 范围内,并对发往这些 IP 范围的所有连接采用概率性封堵策略,封锁率大约为 26%。
6.1 互联网扫描实验
2022 年 5 月 12 日,我们从位于美国的服务器上对互联网上 10% 的 IPv4 地址的 TCP 80 端口进行了扫描。按照前人工作中,如何识别在互联网扫描中发现不可靠主机的方法 ,我们排除了那些 TCP 响应窗口为 0 的服务器(因为我们无法向他们发送数据),以及不接受后续连接的 IP 地址。这就给我们留下了 700 万个可扫描的 IP 地址。然后,我们将这 700 万个 IP 地址随机平均分成九个组,并将每组分配到腾讯云北京数据中心的九个实验节点上。然后,我们将一个我们编写的测量程序安装在所有九个实验节点上,并用它进行实验。对于每个 IP,该程序连续连接到其 80 端口,最多 25 次,每次连接间有一秒钟的间隔。在每个连接中,我们发送相同的 50 个字节的随机的、可以触发封锁的数据。如果我们在发送数据后看到连续 5 次连接超时(连接失败),我们就将该 IP 标记为受影响新审查系统的影响。 反之,如果所有 25 次连接都成功,我们则将该 IP 标记为未受到影响。我们将完全无法连接的 IP 标记为未知(例如,服务器关闭,或者与 GFW 无关的网络故障使我们无法首先连接)。
我们还重复了这个过程,但发送了 50 个字节的 \x00,这个数据包并会不触发 GFW 的封锁。如果一个服务器在这个测试中也被标记为受到影响,那这很可能是由于服务器封锁了我们的连接,而不是 GFW 封锁的。我们从受到影响的 IP 结果中排除这些 IP。这样就只剩下 600 多万个 IP 了。
最后,我们排除了可能是由于间歇性网络故障或不可靠的有利条件造成的 "模棱两可" 的结果。 具体来说,我们排除了那些被我们的随机数据包或全零数据包扫描标记为未知(我们从未能够连接),或有间歇性连接超时(例如,几个连接超时,但不是连续的 5 个)的 IP。这就留下了 550 万个我们可以很容易地将其标记为不受影响(所有 25 个连接都成功了)或受影响(在某些时候,在我们发送随机数据后,它似乎被封锁了)的 IP 地址。
6.2 并非所有子网或自治系统都受到同等程度的审查
在经过处理的 550 万个 IP 中,98% 的 IP 地址没有受到 GFW 封锁的影响,这表明中国在采用这种新的审查系统时是相当保守的。我们使用 pyasn 以及 2022 年 4 月的 AS 数据库,将这 550 万个 IP 地址归入其分配的 IP 前缀和 AS 中。 对于大于 / 20 的 IP 前缀,我们将其分成每 / 20 前缀一组,以保持分配的大小大致相同。我们的 550 万个 IP 包括了 538 个至少有 5 个测量结果的 AS,其中绝大多数基本不受 GFW 的阻断影响。
下图显示了受影响的自治系统(AS)和 / 20 IP 前缀的分布情况。我们发现,90% 以上的 AS 是以全有或全无的方式受到影响的:要么我们在 AS 中测试的所有 IP 地址都受到 GFW 的阻断影响,要么我们在 AS 中测试的所有 IP 地址都没有受到影响。我们还观察到,只有少数 AS 受到影响:超过 95% 的 AS 被观察到只有不到 10% 的 IP 地址受到影响,只有 7 个 AS 被观察到其中有超过 30% 的 IP 地址受到了影响。
下图显示了受影响最大的 AS。虽然测量结果偏向于显示较大规模的 AS(因为在我们的扫描中占有更多的 IP),但它显示了受到严重影响的 AS(例如,阿里巴巴美国,Constant)和未受影响的 AS(Akamai,Cloudflare)。此外,一些 AS 既有受影响的 IP 前缀,也有不受影响的 IP 前缀(亚马逊、Digital OCean、Linode)。我们看到的所有受影响或部分受影响的 AS 都是受欢迎的,可用于托管代理服务器的 VPS 供应商。而未受影响的大型 AS 通常不向个人客户出售 VPS 主机(如 CDN)
6.3 概率封堵的特点
正如第 3 节所介绍的,在得出任何关于封锁的结论之前,我们发送最多 25 次具有相同有效数据包的连接。这是必要的,因为审查者仅是有概率地实行封锁的。换句话说,仅仅向受影响的服务器发送一次随机的有效数据包,只是有时会触发阻断;但是,如果一个人不断向受影响的服务器发送相同有效数据包的连接,那么阻断终会发生。这就产生了一个疑问:一个连接被封锁的概率是多少?以及为什么审查者只是有概率地实行封锁?
估计封锁概率。 在我们对 10% 的互联网的扫描中(第 6.2 节)),有 109,489 个 IP 地址被我们标记为受到封锁影响。如第 5 节所示,在被封锁之前,我们可以与每个 IP 地址进行成功的随机数据连接的数量分布符合一个几何分布。这个结果表明,对每次连接的阻断是独立的,概率大约为 26.3% 。
为什么采用概率封锁。 我们猜想,审查者采用概率封锁可能有两个原因:首先,它允许审查者只检查四分之一的连接,减少计算资源。第二,它帮助审查者减少对非翻墙连接的误伤。虽然这种减少也是以降低审查成功率为代价的,但残留审查可能弥补了这一点:一旦一个连接被封锁,其随后的连接也会被封锁数分钟。这使得翻墙流量一旦被发现就很难再成功连接。这也可能进一步支持了之前的说法,即审查者更重视降低检测中的错误阻断概率,而不执着于极高的审查成功率。
- 规避策略
7.1 可定制的有效载荷前缀
豁免规则 Ex2 和 Ex5 只查看连接中的前几个字节。这样做能让 GFW 高效地豁免不是完全加密的流量;但这样的做法同时也使其可以被用来规避检测。具体来说,我们建议在(翻墙)连接的第一个数据包的有效载荷上预置一个可定制的前缀。
可定制的 IV 头。 Shadowsocks 连接以初始化向量(IV)开始,根据加密方式的不同,其长度为 16 或 32 字节。正如第 4.2 节所介绍的,将 IV 的前 6 个(或更多)字节变成明文 ASCII,将使这些连接被 Ex2 规则豁免。同样,将 IV 的前 3、4 或 5 个字节变成普通协议头,将使连接被 Ex5 规则豁免(例如,将 IV 的前 3 个字节变成 0x16 0x03 0x03)。这些对策只需对客户端进行很小的改动,而对服务器没有任何改动,因此已经被许多流行的翻墙工具所采用。将 32 字节 IV 的前几个字节限制为明文 ASCII,不会将其随机性降低到影响加密安全性的程度。例如,即使将前 6 个字节固定为某个明文 ASCII,IV 中仍然有 26 个随机字节,这仍然比典型的 16 字节 IV 的随机性要大。
局限性。 这是一个权宜之计,有可能被审查者很容易地阻止。审查者可能会跳过前几个字节,将检测规则应用于连接中的其余数据。协议模仿在实践中也很困难 [39]。审查者可以执行更严格的检测规则,或对服务器进行主动探测,以检查它是否真的在运行 TLS 或 HTTP。然而,这一策略在自 2022 年 1 月被许多流行的翻墙工具采用后,直到目前的 2023 年 2 月仍然有效。这一事实强调了即使是简单的应对方案也能有效对抗资源有限的审查者 。
7.2 改变 Popcount
正如第 4.1 节所介绍的,如果一个连接的第一个数据包每字节的 1 比特数量的平均值(popcount)小于等于 3.4 或大于等于 4.6(Ex1),GFW 就会豁免该连接。基于这一观察,人们可以通过在数据包中插入额外的 1(或者 0)来增加(减少)popcount,以绕过封锁。我们设计并分析了一个灵活的方案,它可以将每字节的 popcount 改变为任何给定的值或范围。
从一个高度概括的层面看,我们把原始的完全加密的数据包作为输入:通过只对密文进行操作,我们无需承担其保密性被破坏的风险。当发送一个数据包时,我们首先计算其每字节的平均 popcount;如果该值大于 4,那么我们计算我们必须向数据包中添加多少个 1 比特,以获得一个超过 4.6popcount 的载荷。反之,如果 popcount 小于 4,那么我们要计算要增加多少个 0 比特才能使 popcount 减少到 3.4 以下。在任何一种情况下,我们在原始密文中添加必要数量的 1 比特或 0 比特,然后添加 4 个字节,表示所添加的比特数,最终给我们一个比特串 B,使其每字节的 popcount 值会被豁免。
当然,简单地添加 1 或 0 会很容易产生协议指纹。为了解决这个问题,我们进行比特级的随机重新排序。特别是,我们利用现有的共享秘密,如密码,作为一个种子,以确定的方式构建一个置换向量。在每个连接中,我们更新这个排列向量,并在发送前用它来对比特串 B 中的所有比特重新排序。为了解码,接收方首先更新排列向量,然后用它来还原对比特串的排序;然后读取最后 4 个字节来确定增加的比特数,删除该比特数,从而能够恢复原来的(完全加密)数据包。
在实践中,我们额外采取了两个步骤来进一步混淆流量。因为如果所有的连接都共享相同的平均字节 popcount 值,那这是就会成为一个明显的指纹,所以我们将 popcount 的目标值设置为一个可参数化的范围。其次,由于明文的 4 字节长度标签可能成为一个指纹,所以我们对其进行加密(与这些翻墙工具对代理流量进行加密的方式相同)。
这个方案有几个优点。首先,该方案支持可参数化地调整平均每字节 popcount 的值,以防 GFW 更新其 popcount 规则来缩小被豁免的 popcount 范围。其次,由于它的精心设计,没有明显的指纹会向审查者发出信号,表明这是一个经过 popcount 调整的数据包。最后,它的流量开销很低;它只添加严格需要数量的 1(或 0)比特。在最坏的情况下,即把 popcount 从 4 增加到 4.6,这只产生了大约 17.6% 的额外开销。因此,它不仅可以应用于第一个数据包,还可以应用于连接中的每一个数据包。这样即使审查者在未来检测除第一个数据包以外的数据包,这一策略也仍然有效。
参考文献
Great Firewall Report: [](1. 前言
完全加密的翻墙协议是翻墙生态系统中的基石。不同于像 TLS 这样的协议以明文握手开始,完全加密(随机化)的协议 -- 如 VMess 、Shadowsocks 和 Obfs4 -- 被设计成连接中的每个字节都与随机数据没有区别。这些 协议的设计理念是:它们应该很难被审查者抓住特征,因此阻断的成本很高。
2021 年 11 月 6 日,开始有大量来自中国的互联网用户报告说他们的 Shadowsocks 和 VMess 服务器被封锁。 11 月 8 日,一个 Outline 开发者报告说来自中国的使用量突然下降。这次封锁的开始时间恰逢 2021 年 11 月 8 日至 11 日召开的中国共产党第十九届中央委员会第六次全体会议 。能够封锁这些翻墙工具代表中国的防火长城(GFW)探测能力上了一个新的台阶。据我们所知,虽然中国自 2019 年 5 月以来一直在采用被动流量分析和主动探测相结合的方式来识别 Shadowsocks 服务器,但这是审查者第一次能够仅基于被动流量分析,就实时地大规模封锁完全加密的代理。
在这项工作中,我们对 GFW 被动检测和封锁全加密流量的新系统进行了测量和描述。我们发现,审查者应用了至少五条粗糙但高效的规则来豁免那些不太可能是完全加密的流量;然后,它阻止其余未豁免的流量。这些豁免规则基于常见的协议指纹、以及第一个 TCP 数据包的有效数据包中 ASCII 字符的比例、位置和最大连续数。
我们还分析了这种新形式的被动封锁与 GFW 广为人知的主动探测系统 之间的关系:二者是独立运行的。我们同时还发现,主动探测系统也依赖于这种流量分析算法,并额外应用了一条基于数据包长度的豁免规则。因此,能够逃避这种新的封锁的规避策略,也可以帮助防止 GFW 识别并随后主动探测代理服务器。
自 2022 年 1 月以来,这些规避策略被广泛采用和部署,已帮助数百万用户绕过这一新的封锁技术。据反馈,截至 2023 年 2 月,这些工具采用的所有规避策略在中国仍然有效。
2. 背景
2.1 流量混淆策略
将混淆翻墙流量的方法可大致分为两类:隐写 (steganography) 和多态 (polymorphism) 。隐写代理的目标是使翻墙流量看起来像像正常的流量一样;多态的目标是使翻墙流量看起来不像应该被禁止的流量。
实现隐写的两种最常见的方法是模仿 (mimicking) 和隧道传输 (tunneling)。其中模仿类协议有着根本性的缺陷,因为将原始流量通过被允许的协议进行隧道传输是一种更抗封锁的方法。但即使使用隧道传输,翻墙软件的设计者仍然需要额外的努力让翻墙协议的指纹与流行的实现方式的指纹保持完全一致,以避免受到基于协议指纹的封锁。例如,在 2012 年,中国和埃塞俄比亚部署了深度包检测系统,通过 Tor 使用的不常见的密码套件来检测 Tor 流量 。审查设备供应商之前已经根据 meek 发出的 TLS 指纹和 SNI 值来识别并封锁它了 。
为了避免这种复杂性,许多流行的翻墙软件选择了多态的设计。实现多态性的一个常见方法是,从连接中的第一个数据包开始,就对其有效数据包进行完全加密。由于没有任何明文或固定的包头结构指纹,审查者没办法简单地使用正则表达式或通过寻找流量中的特定模式来识别代理流量。这种设计在 2009 年首次被引入 Obfuscated OpenSSH。此后,Obfsproxy 、Shadowsocks、Outline、VMess、ScrambleSuit 、Obfs4 都采用了这种设计。而 Geph4、Lantern 、Psiphon3 和 Conjure 也部分采用这种设计。
完全加密的流量经常被称为 “看起来什么都不像” 的流量,又或者被误解为 “没有特征”;然而,更准确的描述应该是 "看起来像随机数据"。事实上,这种流量确实有一个使其与其他流量不同的重要特点:完全加密的流量与随机流量是无法区分的。由于没有可识别的协议头,整个连接中的流量都是均匀且高熵的,甚至从第一个数据包中就已经如此。相比之下,即使像 TLS 这样的加密协议也还有相对低熵的握手包,用以传达支持的版本和扩展。
2.2 主动探测攻击及其防御措施
在主动探测攻击中,审查者向被怀疑的服务器发送精心制作的有效数据包,并测量它的反应。如果服务器以与众不同的方式回应这些探测(例如让审查者将其作为代理使用),审查者就可以识别并封锁它。 早在 2011 年 8 月,人们就观察到 GFW 向接受过来自中国的 SSH 登录的外国 SSH 服务器发送看似随机的有效数据包 。2012 年,GFW 首先寻找一个独特的 TLS 密码来怀疑 Tor 流量;然后向可疑的服务器发送主动探测,以确认其猜测。2015 年,Ensafi 等人对 GFW 针对各种协议的主动探测攻击进行了详细分析 。自 2019 年 5 月起,中国部署了一个审查系统,分两步检测和封锁 Shadowsocks 服务器:它首先使用每个连接中第一个数据包有效数据包的长度和熵来被动地识别可能的 Shadowsocks 流量,然后在分阶段地向可疑的服务器发送各种探针,以确认其猜测 。作为回应,研究人员提出了各种针对主动探测攻击的防御措施,包括让服务器对各种连接的反应保持一致 和应用前置 。 Shadowsocks、Outline 和 V2Ray 都采用了防主动探测的设计 ,使得它们自 2020 年 9 月以来在中国就没再被封锁过 ,直到最近在 2021 年 11 月被再次封锁 。
3. 方法
我们在中国境内外的主机之间制作并发送各种测试探针,让它们被 GFW 观察到。我们在两个端点主机上抓包并比较流量,来观察 GFW 的反应。这种记录使我们能够识别任何被丢弃或被操纵的数据包,包括主动探测。
实验时间线和实验节点。
我们在上表中总结了所有主要实验的时间线和所使用的实验节点。我们总共使用了腾讯云北京(AS45090)的 10 台 VPS 和阿里云北京(AS37963)的 1 台 VPS。 我们没有观察到中国境内的实验节点或任何受影响的国外节点之间的审查行为有任何差异。我们使用了 Digital Ocean 旧金山(AS14061)的四台 VPS:其中三台受到了新审查机制的影响,剩下一台则没有受到影响。我们根据 IP2Location 数据库 检查了我们的 VPS 的 IP 地址,并确认它们的地理位置与供应商所报告的位置相符。
触发审查制度。
因为外界观察者是无法区分完全加密的流量与随机数据的,所以除了使用实际的翻墙工具外,我们还开发了测量工具用来在我们的研究中发送随机数据以触发封锁。这些工具完成一个 TCP 握手后,会发送一个给定长度的随机有效数据包,然后关闭连接。
使用残留审查 (residual censorship) 来确认封锁。
与 GFW 封锁许多其他协议的方式类似,在一个连接触发审查后,GFW 会阻止所有具有相同客户端 IP、服务器 IP、服务器端口的后续连接 180 秒。这种残留审查允许我们通过从同一客户端发送后续连接到服务器的同一端口来确认封锁。我们逐一进行共计五次的 TCP 连接,中间有一秒钟的时间间隔。如果五次连接都失败了,我们就得出结论,这个连接被封锁了。如果一个连接被封锁,我们在接下来的 180 秒内不会再使用它进行进一步的测试。
对重复测试的概率阻断进行计算。
我们经常要用相同的有效数据包进行多次连接,才能观察到封锁。在第 6.3 节中,我们解释了这是因为 GFW 采用了一种概率阻断策略,大约只有四分之一的概率触发审查。为了减少这种概率行为造成的测量误差,我们在对任何一次阻断(或不阻断)的观察下判断之前,都要发送最多 25 次有着相同有效数据包的连接。如果我们能够连续成功地用相同的有效数据包进行 25 次连接,那么我们就得出结论,该有效数据包(或服务器)不受这种新审查的影响。如果在至少发送一次有效数据包后,随后的 5 次连接尝试都(由于残留审查而)超时了,那么我们就将有效数据包(和服务器)标记为受到了新的审查的影响。在整个研究过程中,我们对所有有效数据包的测试,都采用了这种重复连接的方法。
- 测量并认识新审查技术的特点
4.1 基于熵的豁免规则(Ex1)
我们观察到,为 1 的比特数影响了一个连接是否被阻断。为了确定这一点,我们向服务器重复发送连接,并观察哪些连接被阻止。在每个连接中,我们发送 256 个不同的字节模式中的一个,由 1 个字节重复 100 次组成(例如,\x00\x00\x00...,\x01\x01\x01...,...,\xff\xff\xff...)。 我们对每个模式都发送 25 次包含这一模式的连接到我们的服务器,并观察是否有任何模式导致后续连接被阻断。如果有某个连接被阻断,则表明它的有效数据包触发了审查。我们发现共有 40 个字节的模式触发了封锁,而其余 216 个模式没有。 被封锁的模式例子包括 \x0f\x0f...,\x17\x17\x17...,和 \x1b\x1b\x1b...(以及其他 37 个)。
所有被阻断的模式的每个字节的八位比特中都恰好有 4 位是 1 比特(例如,二进制的 \x1b 是 00011011)。 我们猜想每个字节的 1 比特的数量可能起作用,因为均匀的随机数据将有接近相同数量的二进制的 0 比特和 1 比特。实质上,这是在测量客户端数据包内的比特的熵。
我们发送这样的字节组合来确认这一猜想:组合中的每种字节单独发送都被允许,但组合起来发送就会被禁止。例如,\xfe\xfe\xfe... 和 \x01\x01\x01... 都没有被单独封锁,但这些字节作为 \xfe\x01\xfe\x01... 一起发送却被阻止。 我们注意到 \xfe\x01 的 16 位比特中有 8 位被设置为 1(平均每个字节设置 4 个比特),而 \xfe 的 8 位比特中有 7 位被设置为 1,\x01 的 8 为比特中有 1 位被设置为 1。这解释了为什么它们单独发送被允许,但组合起来发送就被阻止。
当然,随机或加密的数据不会总是正好有一半的比特被设置为 1。我们通过发送一串 50 个随机字节(400 比特),并设置了越来越多的比特为 1 的实验,来测试 GFW 需要多接近一半的比特才能阻断。 我们产生了 401 个比特串,其中有 0-400 个比特被设置为 1,并对每个字符串的比特位置进行洗牌,以产生一组随机字符串,每个字节设置 0-8 比特(以 0.02 比特 / 字节为增量)。对于每个字符串,我们发送 25 次连接含有它的连接,以观察它是否会引发后续连接的封锁。我们发现,所有具有 3.4 比特 / 字节的字符串都没有被封锁,而 3.4 至 4.6 比特 / 字节的字符串则被封锁了。
这其中有一个例外,那就是有一个 4.26 比特 / 字节集的字符串也没有被封锁。这是因为它有超过 50% 的字节是明文 ASCII 字符;我们接下来会介绍这是另一条豁免规则(Ex2)。我们重复了我们的实验,并确认其他具有相同 1 比特数但明文 ASCII 字符较少的字符串,确实被阻止了。
综上所述,我们发现,如果一个连接中,客户端的第一个数据包中 1 比特比例偏离一半,GFW 就会豁免这个连接。这相当于对熵的粗略测量:随机(加密)数据 z 总有接近一半的比特被设置为 1,而其他协议由于明文或有零填充的协议头,每字节的 1 比特数通常较少。例如,谷歌浏览器 105 版发送的 TLS Client Hello 包,由于用零填充,每字节平均只有 1.56 个 1 比特,属于豁免范围。
4.2 基于明文 ASCII 字符的豁免规则 (Ex2~4)
我们观察到在第 4.1 节中发现的比特计数规则有几个例外。例如,模式 \x4b\x4b\x4b... 没有被封锁,尽管每个字节正好设置了 4 位。事实上,实际上有 70 个字节(8 选 4)正好有 4 位为 1 比特,但是我们的分析发现,其中只有 40 个触发了审查。那另外 30 个呢?
这另外 30 个字节的值都属于明文 ASCII 字符的字节范围,即 0x20-0x7e。 我们推测,GFW 豁免这些字符可能是为了允许明文的人类可读协议。
我们发现,GFW 有三种关于明文 ASCII 字符的豁免方式,都是基于连接中客户端发送的第一个数据包的有效数据包:如果前六个字节是明文(Ex2);如果超过一半的字节是明文(Ex3);或者如果它包含超过 20 个连续的明文字节(Ex4),则允许连接。
前六个字节是明文(Ex2)
我们观察到,如果一个连接的前 6 个字节在明文字节范围 0x20-0x7e 内,那么 GFW 就会豁免该连接。如果前 6 个字节中有超出这个范围的字符,那么连接就可能会被阻止,前提是它没有符合其他豁免的规则(例如,每个字节集有少于 3.4 位的 1 比特)。 我们通过生成不同有效数据包进行测试,其中前 n 字节来自不同的字符集(如明文 ASCII 字符),而消息的其余分部将是随机的非明文字符。 我们观察到,对于 n<6,连接被阻断,但对于 n>=6,即前 n 字节都是明文 ASCII 字符时,没有发生阻断。
第一个数据包有一半的有效数据包是明文(Ex3)
如果第一个数据包的有效数据包中超过一半的字节属于明文 ASCII 范围 0x20-0x7e,那么 GFW 就会豁免该连接。 我们通过构造并发送这样的有效数据包来测试:其前 10 字节由明文 ASCII 范围以外的字符组成(例如 0xe8),然后是一个 6 个字节的重复序列:5 个在这个明文范围内(如 0x4b),而最后一个在明文范围外。我们重复这个 6 字节的序列 5 次,然后在字符串的末尾用明文范围外的 n 个字节来填充。 这个实验给我们一个可变长度的模式,随着我们增加 n,明文 ASCII 范围内的字节的比例减少了。 我们发现,对于 n<10,连接不会被阻断,而对于 n>=10,连接会被阻断。 这相当于当明文字符的比例小于或等于一半时被阻断,而当大于一半时不被阻断。
我们设计这样的有效数据包是为了避免触发其他 GFW 豁免规则,例如比特比例(Ex1)、明文前缀(Ex2)或连续的明文字符(Ex4)。 例如,我们分别使用 0x4b 和 0xe8 作为明文和不明文字符,因为它们都正好有 4 位的设置。 这可以防止 GFW 因为前面讨论过的 1 比特比例规则(Ex1)而豁免封锁我们的连接的情况。 此外,我们避免让明文字符 0x4b 连续出现,因为我们观察到这样的模式也能豁免封锁连接,这一点我们接下来会讨论。 我们用其他同样符合这些限制条件的模式(如 0x8d 和 0x2e)重复了我们的实验,并观察到相同的结果。
超过 20 个连续的字节是明文(Ex4)
一个明文字符的连续出现也可以免除封锁,即使明文字符的总比例不到一半。 为了测试这一点,我们发送了一个由明文范围以外的字符(0xe8)组成的 100 个字节的模式,以及来自明文范围的不同数量的连续字节(我们使用 0x4b)。 我们的有效数据包从 10 个字节的 0xe8 开始,接着是 n 字节的 0x4b,然后是 90-n 字节的 0xe8,总长度为 100 个字节。 我们让变量 n 在 0-90 之间变化,并把每个相同的数据包都向我们的服务器发送 25 次。 我们发现,对于 n<=20,连接被阻断了。当 n>20,连接没有被阻断。 这证明当有连续的明文 ASCII 字符出现时,连接会被豁免。 当然,当 n>50,连接也会被豁免,因为豁免规则 Ex3。
其他编码方式
我们测试了如果第一个数据包中包含中文字符,是否也可以与明文 ASCII 字符一样,让连接免于阻断。 我们使用了以 UTF-8 编码的 6-36 个中文字符串,以及 GBK(与我们使用的 GB2312 字符相同)。 所有这些测试连接都被阻断了,这表明不存在基于汉字的豁免规则。 这可能是因这些编码中出现的汉字的情况很少,或者是因为如果要解析这些编码,会对审查系统增加不合理的复杂性,因为很难知道一个编码字符串的开始或结束位置。
4.3 基于常见协议的豁免规则(Ex5)
为了避免误伤流行的协议,我们观察到 GFW 明确地豁免了两种流行的协议。 GFW 似乎是用客户端数据包的前 3-6 个字节来推断协议:如果它们与已知协议的字节相匹配,连接就会被免除阻断,即使数据包的其余部分不符合该协议。我们测试了六种常见的协议,发现 TLS 和 HTTP 协议被明确地豁免了。这个豁免列表可能并不详尽,因为可能还有其他我们没有测试的豁免协议。
TLS。 TLS 连接以 TLS ClientHello 消息开始,该消息的前三个字节会使 GFW 豁免连接。我们观察到,GFW 豁免了任何前三个字节与以下正则表达式匹配的连接:
(\x16-\x17]\x03[\x00-\x09)
这对应于一个字节的记录类型 (record type),后面是一个两字节的版本 (version)。 我们列举了所有 256 个 XX\x03\x03 的模式,并在后面加上 97 个字节的随机数据。我们发现除了那些以 0x16(对应 TLS 中的 Handshake 包,用于 ClientHello)或 0x17 (对应 TLS 中的应用数据类包 (Application Data))开始的模式外,其他所有模式都被封锁。 虽然通常的 TLS 连接不会以应用数据开头 , 但当 TLS 被用于多路径 TCP(MPTCP) 时, 常见的情况是,其中一个 TCP 子流被用于 ClientHello,而其他子流在 TCP 连接建立后立即发送应用数据。 到目前为止,只有 TLS 的(0x00-0x03) 版本被定义 ,但 GFW 甚至允许更晚的(尚未定义)版本。
HTTP。 审查者用来识别 HTTP 流量的字节模式很简单,就是在 HTTP 请求方法的后面跟有一个空格。如果一个信息以 GET、PUT、POST 或 HEAD 开头,那么这个连接就会被免于阻断。每个请求方法的后面的空格字符(0x20)是让连接免于屏蔽的必要条件。如果不包括这个空格字符,或用任何其他字节代替它,就不能豁免连接。其他的 HTTP 请求方式(OPTIONS, DELETE, CONNECT, TRACE, PATCH)均因为前 6 个字节是明文字符,而已经满足明文豁免规则(Ex2)。同时,我们发现 HTTP 请求方法是不区分大小写的:GeT、get 和类似的变体都可以使连接被豁免。请求方式的错误拼写(例如,TEG)不属于豁免范围。
不被豁免的协议。 我们测试了其他常见的协议:SSH、SMTP 和 FTP 将被豁免,因为它们都以至少 6 个字节的明文 SCII 开头(规则 Ex2)。DNS-over-TCP 由于包含很大一部分的零,使得它被 Ex1 规则豁免。然而,如果在 DNS-over-TCP 消息后附加足够多的随机数据,它将被阻止。
上面观察到的现象让大家提出了一个问题:为什么审查者使用明确的规则来豁免 TLS 和 HTTP,而不是其他协议。 毕竟,审查者不需要明确地豁免这两种协议:HTTP 通常会都满足前 6 个字节为明文 SCII 的豁免规则(Ex2),而 TLS ClientHello 包由于有许多零字段,其也会因位数熵相对较低而满足 Ex1 豁免规则。也许这是因为审查者可以采用这些简单而高效的规则来快速地豁免大部分的网络流量(TLS 和 HTTP),而不需要进行如计算数据包中 1 比特的比例、明文 SCII 的比例等更深入的分析。
4.4 GFW 是如何阻断连接的
一旦 GFW 检测到完全加密的流量,就会按照下面介绍的方式阻断后续流量。
丢弃从客户端到服务器的数据包。 我们先触发 GFW 的阻断,然后比较在客户端和服务器捕获的数据包。我们观察到,在触发审查后,客户端的数据包被 GFW 丢弃,并没有到达服务器。然而,服务器发送的数据包没有被阻断,客户端仍然可以收到。
UDP 流量不受影响。 新的审查系统只限于 TCP。发送一个具有随机有效数据包的 UDP 不能触发审查。此外,即使某个具有相同客户端 IP、服务器 IP、服务器端口的连接由于 TCP 连接而被封锁,往来于同一(服务器 IP、服务器端口)的 UDP 数据包也不受影响。由于没有 UDP 拦截,用户在使用 Shadowsocks 时可能会遇到奇怪的现象:他们仍然可以使用某些依赖 UDP 的网站或应用程序(如 QUIC 或 FaceTime),但无法访问使用 TCP 的网站。这是因为 Shadowsocks 用 TCP 代理 TCP 流量,用 UDP 代理 UDP 流量。审查者不检测或阻止 UDP 流量,可能反映了其更糟就是更好 (worse is better) 的工程思维。从实际情况来看,目前的 TCP 封锁已经足够有效地让这些流行的翻墙工具瘫痪,而如果增加 UDP 审查,则需要额外的资源,并给审查系统引入额外的复杂性。
所有端口的流量都可能被阻断。 我们在美国建立了一个监听在所有端口(从 1 到 65535)的服务器。然后,我们让中国的客户端不断地用 50 字节的随机有效数据包与美国服务器的每个端口进行连接,并在某个端口被封锁后停止反复地连接这一端口。我们发现,从 1 到 65535 的所有端口都可能被封锁。因此,在一个不寻常的端口上运行翻墙服务器并不能缓解封锁。我们也没有观察到使用不同端口会导致不同的审查行为。
GFW 存在残留审查 (residual censorship)
我们发现,这个新的审查系统一旦阻断了一个连接,它就会在后续的 120 或 180 秒内继续丢弃所有具有相同客户端 IP、服务器 IP、服务器端口的 TCP 数据包。这种行为通常被称为 “残留审查”。与其他一些残留审查系统不同 ,GFW 的残留审查定时器不会在观察到更多触发审查的数据包后被重置。
我们还发现,GFW 似乎限制了它在任何给定时间内残留审查的连接的数量。我们让中国的客户端重复性地同时连接到一个服务器的 500 个端口。在每个连接中,客户端发送 50 字节的随机数据,然后关闭连接。我们记录了每次发生残留审查的持续时长。如上图所示,与只有一个端口被封锁时的 180 秒持续时间相比,该实验中的残留审查持续时间大幅下降。
4.5 GFW 是如何重组流量的
在这一节中,我们将研究 GFW 的新审查系统是如何重新组合流量,并考虑流量方向。
一个完整的 TCP 握手是必要的。 我们观察到,发送一个 SYN 包,然后再发送一个包含随机数据的 PSH+ACK 包(在服务器没有完成握手的情况下),并不足以触发阻断。这样的残留审查更难被攻击者利用。
只有客户端到服务器的数据包可以触发阻断。 我们发现,GFW 不仅检查随机数据是否被发送到属于受影响的 IP 范围内的目标 IP 地址,而且还检查并只在随机数据从客户端发送到服务器时才进行阻断。这里的服务器是指在 TCP 握手过程中发送 SYN+ACK 的主机。
我们通过在两台主机之间设置的四个实验来了解这一点。在第一个实验中,我们让在中国的客户端连接并向美国服务器发送随机数据;在第二个实验中,我们仍然让中国的客户端连接到美国服务器,但让美国服务器向客户端发送随机数据;在第三个实验中,我们让美国的客户端连接并向中国服务器发送随机数据;在第四个实验中,我们让美国的客户端连接到中国服务器,但随后让中国服务器向美国客户端发送随机数据。只有第一个实验中的连接被封锁了。
GFW 只检查第一个数据包。 GFW 似乎只分析 TCP 连接中的第一个数据包,而不对有多个数据包的流量进行重新组合。我们通过以下实验来测试这一点。在 TCP 握手后,我们发送第一个数据包,其中只有一个字节的有效数据包 \x21。在等待一秒钟后,我们再发送带有 200 字节随机有效数据包的第二个数据包。我们重复了 25 次实验,但连接从未被封锁。 这是因为在看到第一个数据包后,GFW 已经通过规则 Ex1 豁免了连接,因为它的有效数据包中包含 100% 明文 ASCII。换句话说,如果 GFW 在其流量分析过程中把多个数据包重新组合成一个流,它就能够阻止这些连接。
我们发现,GFW 不会等到看到服务器的 ACK 响应时才去阻止一个连接。我们用一个 iptables 规则将我们的服务器配置为放弃任何传出的 ACK 数据包。然后我们用 200 字节的随机有效数据包与服务器建立连接。尽管服务器没有发送任何 ACK 数据包,GFW 仍然阻止了这些连接。
GFW 对第一个数据包等待时间超过了 5 分钟。 我们研究了 GFW 会在 TCP 握手之后,但在看到第一个数据包之前,对一个 TCP 连接进行了多长时间的监控。根据观察,它需要一个完整的 TCP 握手来触发封锁,我们因此推断 GFW 可能是有状态的。因此,我们有理由怀疑 GFW 只在有限的时间内监控一个连接,因为要永久追踪一个连接的状态而不放弃的开销很大。
我们的客户端完成了 TCP 握手,然后等待了 100 秒、180 秒或 300 秒,然后发送 200 字节的随机数据。接着,我们重复了这个实验,但使用 iptables 规则丢弃了任何 RST 或 TCP keepalive 数据包,以防它们帮助 GFW 保持对连接的追踪。 我们发现这些连接仍然触发了阻断,这表明 GFW 对连接状态的追踪至少有 5 分钟。
- 与主动探测系统的关系
正如第 2.2 节所介绍的,GFW 自 2019 年以来一直在向 Shadowsocks 服务器发送主动探测探针。在这一节中,我们研究了这个新发现的实时阻断系统和已知的主动探测系统之间的关系。通过测量实验和对历史数据集的分析,我们发现,虽然这两个审查系统并行工作,但主动探测系统的流量分析模块应用了上述总结的所有五条豁免规则,并且还用一条额外的规则,来检查第一个数据包的有效数据包长度。我们还表明,主动探测系统使用的流量分析算法可能自 2019 年以来有所进化。
主动探测实验。 在部署这个新的实时阻断系统之前,从外界推断主动探测系统的流量分析算法,是极具挑战性的。这是因为 GFW 在看到触发连接和发送主动探测之间设置了一个任意长度的延迟 。这就使得我们很难说明 GFW 的哪些探测是由我们发送的哪些连接触发的。现在我们已经在第 4 节中推断出了这个新的阻断系统的流量检测规则列表,我们可以测试被豁免的有效数据包是否也不会被主动探测系统所怀疑。
我们在 2022 年 5 月 19 日和 6 月 8 日之间进行了实验。如表 2 所示,我们制作了 14 种不同类型的有效数据包:其中 3 种是长度为 2、50 和 200 字节的随机数据;其余 11 种是具有不同长度的数据,这些数据仅能被算法 1 中的某一个豁免规则豁免。然后,我们从中国北京腾讯云的一个 VPS 向美国旧金山 DigitalOcean 的两个不同主机的 14 个端口,发送了 14 种有效数据包。其中一台美国主机已知受到当前阻断系统的影响,而另一台美国主机则不受影响。这样,如果我们收到来自 GFW 的任何探测,我们就知道当前封锁系统使用的某些豁免规则没有被主动探测系统使用。
我们在中国的客户端总共向两台美国服务器的每个端口发送了约 17 万次连接。然后我们采取措施,将来自 GFW 的主动探测与其他互联网扫描探测隔离开。我们根据 IP2Location 数据库和 AbuseIPDB 检查每个探测的源 IP 地址。如果它是一个非中国的 IP 或者来自一个已知的被用来扫描的 IP 地址,我们就不认为它是来自 GFW 的主动探测。我们进一步检查并确认该探针是否属于 GFW 发送的任何已知类型的探针。
这两个系统独立工作。 新的审查机器纯粹是根据被动流量分析做出封杀决定,而不依赖中国主动探测基础设施。我们之所以知道这一点,是因为虽然 GFW 仍然向服务器发送主动探测,但在超过 99% 的测试中,GFW 在封锁一个连接之前没有向服务器发送过任何主动探测。举个例子,如上表所总结的,我们进行了 33119 次连接,但只收到 179 次主动探测。事实上,与之前的工作的发现相似,主动探测很少被触发。
我们想强调的是,这一发现并不意味着对主动探测的防御没有必要或不再重要。恰恰相反,我们认为 GFW 对纯被动流量分析的依赖,部分原因是 Shadowsocks、Outline、VMess 和其他许多翻墙软件已经对主动探测采取了有效的防御措施。GFW 仍然向服务器发送主动探测这一事实,意味着审查者仍然试图使用主动探测,尽可能准确地识别翻墙服务器。
主动探测系统对可疑流量应用了五条豁免规则,并增加了一条基于数据包长度的豁免规则。 这个实验表明了两点。首先,主动探测系统应用一个额外的规则来检查连接中的有效数据包的长度。在我们的案例中,只有 200 字节有效数据包的连接曾经触发了主动探测,而 2 字节或 50 字节的连接则从来没有。其次,如果流量符合的五条豁免规则中的任何一条,那么该流量也不会触发主动探测系统。
自 2019 年以来,主动探测系统已经有所发展。 我们想知道 GFW 的检测规则是否曾经被用来触发主动探测。为了分析它,我们收集了 282 个能触发 GFW 主动探测的数据包然后我们写了一个程序来确定一个有效数据包是否会被当前的阻断系统豁免,并将获得的 282 个有效数据包输入该程序。结果,以前触发主动探测的 45 个探测被豁免了(根据规则 Ex3)。2022 年 5 月 19 日,我们反复发送这 45 个有效数据包,让它们被 GFW 看到,并确认它们确实被当前的阻断系统豁免了。对于每个有效数据包,我们用它从腾讯云北京的 VPS 到 Digital Ocean 旧金山的水槽服务器进行了 25 次连接。 这个结果表明, 自 2020 年以来,GFW 很可能已经更新了其主动探测系统的流量分析模块。此外,目前 GFW 发送的探针也与 2020 年观察到的探针不同 。 新的探针基本上是随机有效数据包,分别以 16、64 和 256 字节为中位数的分布。对于这些长度中的每一个,GFW 发送的探针数量大致相同:一台服务器收到了 48、46 和 47 个探针,另一台收到了 238、228 和 233 个探针。
- 了解阻断策略
在本节中,我们进行了测量实验,以确定审查者的封锁策略。我们发现,可能是为了减少误报和降低运营成本,审查者策略性地将封锁范围限制在热门数据中心的特定 IP 范围内,并对发往这些 IP 范围的所有连接采用概率性封堵策略,封锁率大约为 26%。
6.1 互联网扫描实验
2022 年 5 月 12 日,我们从位于美国的服务器上对互联网上 10% 的 IPv4 地址的 TCP 80 端口进行了扫描。按照前人工作中,如何识别在互联网扫描中发现不可靠主机的方法 ,我们排除了那些 TCP 响应窗口为 0 的服务器(因为我们无法向他们发送数据),以及不接受后续连接的 IP 地址。这就给我们留下了 700 万个可扫描的 IP 地址。然后,我们将这 700 万个 IP 地址随机平均分成九个组,并将每组分配到腾讯云北京数据中心的九个实验节点上。然后,我们将一个我们编写的测量程序安装在所有九个实验节点上,并用它进行实验。对于每个 IP,该程序连续连接到其 80 端口,最多 25 次,每次连接间有一秒钟的间隔。在每个连接中,我们发送相同的 50 个字节的随机的、可以触发封锁的数据。如果我们在发送数据后看到连续 5 次连接超时(连接失败),我们就将该 IP 标记为受影响新审查系统的影响。 反之,如果所有 25 次连接都成功,我们则将该 IP 标记为未受到影响。我们将完全无法连接的 IP 标记为未知(例如,服务器关闭,或者与 GFW 无关的网络故障使我们无法首先连接)。
我们还重复了这个过程,但发送了 50 个字节的 \x00,这个数据包并会不触发 GFW 的封锁。如果一个服务器在这个测试中也被标记为受到影响,那这很可能是由于服务器封锁了我们的连接,而不是 GFW 封锁的。我们从受到影响的 IP 结果中排除这些 IP。这样就只剩下 600 多万个 IP 了。
最后,我们排除了可能是由于间歇性网络故障或不可靠的有利条件造成的 "模棱两可" 的结果。 具体来说,我们排除了那些被我们的随机数据包或全零数据包扫描标记为未知(我们从未能够连接),或有间歇性连接超时(例如,几个连接超时,但不是连续的 5 个)的 IP。这就留下了 550 万个我们可以很容易地将其标记为不受影响(所有 25 个连接都成功了)或受影响(在某些时候,在我们发送随机数据后,它似乎被封锁了)的 IP 地址。
6.2 并非所有子网或自治系统都受到同等程度的审查
在经过处理的 550 万个 IP 中,98% 的 IP 地址没有受到 GFW 封锁的影响,这表明中国在采用这种新的审查系统时是相当保守的。我们使用 pyasn 以及 2022 年 4 月的 AS 数据库,将这 550 万个 IP 地址归入其分配的 IP 前缀和 AS 中。 对于大于 / 20 的 IP 前缀,我们将其分成每 / 20 前缀一组,以保持分配的大小大致相同。我们的 550 万个 IP 包括了 538 个至少有 5 个测量结果的 AS,其中绝大多数基本不受 GFW 的阻断影响。
下图显示了受影响的自治系统(AS)和 / 20 IP 前缀的分布情况。我们发现,90% 以上的 AS 是以全有或全无的方式受到影响的:要么我们在 AS 中测试的所有 IP 地址都受到 GFW 的阻断影响,要么我们在 AS 中测试的所有 IP 地址都没有受到影响。我们还观察到,只有少数 AS 受到影响:超过 95% 的 AS 被观察到只有不到 10% 的 IP 地址受到影响,只有 7 个 AS 被观察到其中有超过 30% 的 IP 地址受到了影响。
下图显示了受影响最大的 AS。虽然测量结果偏向于显示较大规模的 AS(因为在我们的扫描中占有更多的 IP),但它显示了受到严重影响的 AS(例如,阿里巴巴美国,Constant)和未受影响的 AS(Akamai,Cloudflare)。此外,一些 AS 既有受影响的 IP 前缀,也有不受影响的 IP 前缀(亚马逊、Digital OCean、Linode)。我们看到的所有受影响或部分受影响的 AS 都是受欢迎的,可用于托管代理服务器的 VPS 供应商。而未受影响的大型 AS 通常不向个人客户出售 VPS 主机(如 CDN)
6.3 概率封堵的特点
正如第 3 节所介绍的,在得出任何关于封锁的结论之前,我们发送最多 25 次具有相同有效数据包的连接。这是必要的,因为审查者仅是有概率地实行封锁的。换句话说,仅仅向受影响的服务器发送一次随机的有效数据包,只是有时会触发阻断;但是,如果一个人不断向受影响的服务器发送相同有效数据包的连接,那么阻断终会发生。这就产生了一个疑问:一个连接被封锁的概率是多少?以及为什么审查者只是有概率地实行封锁?
估计封锁概率。 在我们对 10% 的互联网的扫描中(第 6.2 节)),有 109,489 个 IP 地址被我们标记为受到封锁影响。如第 5 节所示,在被封锁之前,我们可以与每个 IP 地址进行成功的随机数据连接的数量分布符合一个几何分布。这个结果表明,对每次连接的阻断是独立的,概率大约为 26.3% 。
为什么采用概率封锁。 我们猜想,审查者采用概率封锁可能有两个原因:首先,它允许审查者只检查四分之一的连接,减少计算资源。第二,它帮助审查者减少对非翻墙连接的误伤。虽然这种减少也是以降低审查成功率为代价的,但残留审查可能弥补了这一点:一旦一个连接被封锁,其随后的连接也会被封锁数分钟。这使得翻墙流量一旦被发现就很难再成功连接。这也可能进一步支持了之前的说法,即审查者更重视降低检测中的错误阻断概率,而不执着于极高的审查成功率。
- 规避策略
7.1 可定制的有效载荷前缀
豁免规则 Ex2 和 Ex5 只查看连接中的前几个字节。这样做能让 GFW 高效地豁免不是完全加密的流量;但这样的做法同时也使其可以被用来规避检测。具体来说,我们建议在(翻墙)连接的第一个数据包的有效载荷上预置一个可定制的前缀。
可定制的 IV 头。 Shadowsocks 连接以初始化向量(IV)开始,根据加密方式的不同,其长度为 16 或 32 字节。正如第 4.2 节所介绍的,将 IV 的前 6 个(或更多)字节变成明文 ASCII,将使这些连接被 Ex2 规则豁免。同样,将 IV 的前 3、4 或 5 个字节变成普通协议头,将使连接被 Ex5 规则豁免(例如,将 IV 的前 3 个字节变成 0x16 0x03 0x03)。这些对策只需对客户端进行很小的改动,而对服务器没有任何改动,因此已经被许多流行的翻墙工具所采用。将 32 字节 IV 的前几个字节限制为明文 ASCII,不会将其随机性降低到影响加密安全性的程度。例如,即使将前 6 个字节固定为某个明文 ASCII,IV 中仍然有 26 个随机字节,这仍然比典型的 16 字节 IV 的随机性要大。
局限性。 这是一个权宜之计,有可能被审查者很容易地阻止。审查者可能会跳过前几个字节,将检测规则应用于连接中的其余数据。协议模仿在实践中也很困难 [39]。审查者可以执行更严格的检测规则,或对服务器进行主动探测,以检查它是否真的在运行 TLS 或 HTTP。然而,这一策略在自 2022 年 1 月被许多流行的翻墙工具采用后,直到目前的 2023 年 2 月仍然有效。这一事实强调了即使是简单的应对方案也能有效对抗资源有限的审查者 。
7.2 改变 Popcount
正如第 4.1 节所介绍的,如果一个连接的第一个数据包每字节的 1 比特数量的平均值(popcount)小于等于 3.4 或大于等于 4.6(Ex1),GFW 就会豁免该连接。基于这一观察,人们可以通过在数据包中插入额外的 1(或者 0)来增加(减少)popcount,以绕过封锁。我们设计并分析了一个灵活的方案,它可以将每字节的 popcount 改变为任何给定的值或范围。
从一个高度概括的层面看,我们把原始的完全加密的数据包作为输入:通过只对密文进行操作,我们无需承担其保密性被破坏的风险。当发送一个数据包时,我们首先计算其每字节的平均 popcount;如果该值大于 4,那么我们计算我们必须向数据包中添加多少个 1 比特,以获得一个超过 4.6popcount 的载荷。反之,如果 popcount 小于 4,那么我们要计算要增加多少个 0 比特才能使 popcount 减少到 3.4 以下。在任何一种情况下,我们在原始密文中添加必要数量的 1 比特或 0 比特,然后添加 4 个字节,表示所添加的比特数,最终给我们一个比特串 B,使其每字节的 popcount 值会被豁免。
当然,简单地添加 1 或 0 会很容易产生协议指纹。为了解决这个问题,我们进行比特级的随机重新排序。特别是,我们利用现有的共享秘密,如密码,作为一个种子,以确定的方式构建一个置换向量。在每个连接中,我们更新这个排列向量,并在发送前用它来对比特串 B 中的所有比特重新排序。为了解码,接收方首先更新排列向量,然后用它来还原对比特串的排序;然后读取最后 4 个字节来确定增加的比特数,删除该比特数,从而能够恢复原来的(完全加密)数据包。
在实践中,我们额外采取了两个步骤来进一步混淆流量。因为如果所有的连接都共享相同的平均字节 popcount 值,那这是就会成为一个明显的指纹,所以我们将 popcount 的目标值设置为一个可参数化的范围。其次,由于明文的 4 字节长度标签可能成为一个指纹,所以我们对其进行加密(与这些翻墙工具对代理流量进行加密的方式相同)。
这个方案有几个优点。首先,该方案支持可参数化地调整平均每字节 popcount 的值,以防 GFW 更新其 popcount 规则来缩小被豁免的 popcount 范围。其次,由于它的精心设计,没有明显的指纹会向审查者发出信号,表明这是一个经过 popcount 调整的数据包。最后,它的流量开销很低;它只添加严格需要数量的 1(或 0)比特。在最坏的情况下,即把 popcount 从 4 增加到 4.6,这只产生了大约 17.6% 的额外开销。因此,它不仅可以应用于第一个数据包,还可以应用于连接中的每一个数据包。这样即使审查者在未来检测除第一个数据包以外的数据包,这一策略也仍然有效。
参考文献
Great Firewall Report: How the Great Firewall of China Detects and Blocks Fully Encrypted Traffic
总结#
"黑客技术就像魔术,处处充满了欺骗"
"不要沉迷于网络技术,人才是突破信息系统的关键":
"只要敢做就能赢"
MRX 有三项条规:
第一:没有一个系统是安全的
第二:敢做就能赢
第三:在虚拟空间以及聚会空间享乐,不要把你的乐趣局限在虚拟世界
We lived on farms and then we lived in cities, and now we're going to live on the Internet!
You're not an asshole, Mark. You're just trying so hard to be.