自那日听琴之后,多日不见莺莺,张生害了相思病,趁红娘探病之机会,托她捎信给莺莺,莺莺回信约张生月下相会。夜晚,小姐莺莺在后花园弹琴,张生听到琴声,攀上墙头一看,是莺莺在弹琴。急欲与小姐相见,便翻墙而入,莺莺见他翻墙而入,反怪他行为下流,发誓不再见他,致使张生病情愈发严重。
《西厢记》
上篇《TCP:学得越多越不懂》发出来以后,有朋友很委婉地说:“如果能结合现实生产场景会有意义一点。”
经过深刻的反思,我决定虚心接受建议,写一点理论结合实践的内容。
== 回忆杀 ==
曾经在猫扑和天涯冲浪的网虫应该都还记得,谷歌当时还是Goooooogle,是可以直接访问的。
但是如果想搜索一些奇怪的词汇(比如███),一点击"手气不错",浏览器马上就会显示无法访问,并且这个现象会持续几分钟。
就像这样:
sorry,贴错了,应该是这样:
于是很多小伙伴就换到一个号称自己更懂中文的搜索引擎了。
当年这个广告拍得不错
作为一个曾被新自由主义(Neoliberalism)洗脑的年轻人,我在寻找“自由”的路上发现了墙的存在,也知道了这是方校长的杰作。
但是墙到底是个什么样的存在呢?
== 防火墙 ==
我们的防火墙,其名源自《The Great Firewall of China: How to Build and Control an Alternative Version of the Internet》这本书。
虽然名字叫防火墙(Firewall,简称FW),但严格来说,(在早期)它其实是一个入侵检测系统(Instrusion Detection System,简称IDS)。
和FW不同的是,IDS是监听设备,不需要部署在链路中间,只要能把流量旁路引出供它分析即可。
IDS示意图,来自网络
通过旁路分析,IDS可以在不影响现有流量的情况下部署(只要路由器/交换机上有镜像端口即可),在IDS出现异常时(例如在流量高峰IDS设备性能不足时 )也不会导致网络中断。
曾经有人发现,在流量特别大的时候,墙的检测功能有时会失效,因此推测其是旁路引流进行分析的(符合IDS的特征)。
既然是旁路的,就无法直接Drop数据包,为了达到阻断通信的目的,需要利用协议的特性来实现。
== RST大法 ==
看了上篇《TCP:学得越多越不懂》的同学,对报文的控制位里的 RST 可能还有点印象,在遇到异常情况时,可用于通知对方重置连接(细节详见RFC 793):
If the receiving TCP is in a non-synchronized state (i.e. SYN-SENT, SYN-RECEIVED), it returns to LISTEN on receiving an acceptable reset. If the TCP is in one of the synchronized states (ESTABLISHED, FIN-WAIT1/2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT), it aborts the connection and informs its user
https://tools.ietf.org/html/rfc793
注:因引用字数限制,略有删减
有些同学可能像我一样懒得读英文原文,所以翻译一下:
如果连接状态处于“非连接完成”状态(例如SYN-SEND, SYN-RECEIVED),当收到reset时会将状态返回LISTEN;
如果TCP状态是 ESTABLISHED, FIN-WAIT-1, ..., LAST-ACK, TIME-WAIT 其中之一时,放弃连接并通知用户。
忘了上述状态含义的话,可以再回顾下这张状态流转图:
tcp连接状态图,截取自rfc 793
这就是上篇里提到的“我们敬爱的防火墙很爱用它”的原因了:
当检测到“入侵行为”时(例如HTTP报文中出现了███)发送RST,按照RFC 793规范的TCP协议栈实现,收到RST后就应当放弃本次连接。
于是你就在浏览器上看到连接被重置(reset)了。
== 反RST大法 ==
那么,如果我忽略RST包,不就可以不被墙欺骗吗?
实际上,用 iptables 来实现这一点很简单:
$ iptables -A INPUT -p tcp --tcp-flags RST RST -j DROP
很不幸,方校长的团队对此的解决方法也非常简单,只要向双方都发送RST包就可以了。
当然如果在服务器一端也忽略RST,就可以成功绕过墙的忽悠——据说剑桥大学有人实验验证过,确实可行。
可惜的是,用户通常没法控制服务器端忽略RST,因此这个方法的实用价值不高。
但是这个思路为西厢计划做好了铺垫。
== 西厢计划 ==
我看到这个项目的名字的时候 ,真佩服作者的脑洞。
了解这个计划的原理之后,就更佩服作者的脑洞了。
前面说到,墙是在检测到某个关键词的时候才会发送RST包。
为了检测关键词,它需要工作在应用层(HTTP协议)。
而为了工作在应用层,它需要维护TCP连接的状态。
由于那时的设备性能比较弱(所以会出现高峰期检测失效的情况),为了提高吞吐量,方校长团队的方案是:实现一个简化的TCP栈。
RFC 793规范中定义了很多有效性检测,例如检测序列号是否有效来过滤old duplicates等,以保证通信的可靠性。
但这不是墙的需求,因此可以去掉很多规则,从而提高分析性能。
那么,如果我可以欺骗墙,这个连接已经被关闭,那么后续该连接的包就会被墙认为是网络中滞留的无效包,绕过关键词检测。
具体该怎么办呢?
== 第一阶段 ==
上篇提到了一个细节:
虽然ISN=4000,但是发送方发送的第一个包,SEQ是4001开始的,TCP协议规定 SYN 需要占一个序号(虽然SYN并不是实际传输的数据),所以前面示意图中ACK的seq是 x+1 。同样,FIN也会占用一个序号,这样可以保证FIN报文的重传和确认不会有歧义。
felix021,公众号:felix021TCP:学得越多越不懂
我们知道,在三次握手的最后一步,A本应发送一个ACK(seq=y+1)。
但如果这时候 A 发送了一个 FIN 呢?
B收到以后,由于此时连接尚未建立,会直接忽略这个包。
而墙实现的TCP栈比较简陋,它认为A已经关闭了链接,因此A后续发送的包就不会再触发关键词检测。
但是注意,TCP是双向的,虽然A主动关闭连接,但是B仍然可能有数据要发送(划重点:面试题“为什么TCP断开连接需要4次”的答案),因此还需要欺骗墙说在B这侧也终止链接了。
这又该怎么办呢?
== 第二阶段 ==
显然我们不能让服务器直接发一个FIN,否则这个连接就真完了。
幸运的是,RFC 793给了一个“梯子”:
If the connection is in any non-synchronized state (LISTEN, SYN-SENT, SYN-RECEIVED), and the incoming segment acknowledges something not yet sent (the segment carries an unacceptable ACK), or ...(省略)..., a reset is sent.
Reset Generation, RFC 793 [Page 35]
翻译:如果连接处于“非连接完成”状态,收到一个无效的ACK,应当发出一个reset。
如果A在三次握手的最后一步,没有按规范要求发送ACK(seq=y+1),而是发送ACK(seq=y),那么B在收到以后就会按照协议的要求回复一个RST:
这时我们可以在 A 上用“反RST大法”,忽略服务端返回的RST,这个连接就不受影响。
但是墙的TCP栈认为客户端会按照协议终止连接,于是就不再有必要检测服务端后续的报文了。
== 大结局 ==
从此张生和崔莺莺过上了幸福的生活。
方校长的团队当然不会放任这种事情的发生,西厢计划没过多久就失效了。
随着技术的进步、性能的提升,现在墙似乎已经集成到了链路中、可以直接DROP数据包,不再需要RST大法了。
不过为了业务需要,企业可以向电信主管部门申请VPN用于正常的生产经营。
例如字节跳动,为了建设21世纪数字丝绸之路,通过技术出海,在40多个国家和地区排在应用商店总榜前列,包括韩国、印尼、马来西亚、俄罗斯、土耳其等“一带一路”沿线的主要国家。
如果你也想过上幸福的生活,不妨投个简历,一起为一带一路做贡献吧。
关于字节跳动面试的详情,可参考我之前写的《程序员面试指北:面试官视角》
~ 投递链接 ~
网盟广告(穿山甲)-后端开发(上海)
https://job.toutiao.com/s/sBAvKe
网盟广告(穿山甲)-后端开发(北京)
https://job.toutiao.com/s/sBMyxk
网盟广告(穿山甲)-广告策略研发(上海)
https://job.toutiao.com/s/sBDMAK
其他地区、其他职能线
https://job.toutiao.com/s/sB9Jqk
[参考文章]
[1] “西厢计划”原理小解
https://blog.youxu.info/2010/03/14/west-chamber/
[2] 从Linux协议栈代码和RFC看西厢计划原理
https://blog.csdn.net/dog250/article/details/7246895
[3] RFC 793 - TRANSMISSION CONTROL PROTOCOL
https://tools.ietf.org/html/rfc793
喜欢的话请点亮“在看”,分享给你的朋友↓