一段往事
无独有偶,过了N多年,又到了这个时候,碰到了同样的事情...xx年的万圣节,由于服务器瘫痪我们被罚了500块钱,最终发现瘫痪的原因是TPROXY造成的,当时每个大服务区有一台LVS负载均衡设备,工作在NAT模式下,下联一台核心交换机,核心交换机分出N条线路去往各小区域服务器群,每一个小区域服务群拥有一台TPROXY实现的登录代理服务器,仅仅对登录数据包做代理,其它的包直接通过。
事情发生在晚上8点左右,我们当时均在加班,值班客服接到大量的投诉电话,说是服务器无法登录...我们马上停下手头的新工作,投入到抢险第一线,经过简单排查,很快发现是LVS的端口用尽导致,但是排查出它为何用尽却用来一晚上的时间,到了次日早上7点多,我打车回家,路上正碰到上班高峰期,将近9点半才到家,路上基本都在睡觉。到家以后开始写分析文档,下午3点又去了公司(公司规定,如果发生通宵加班,次日上午可以回家休息一上午,下午3点如果不请假则必须正式上班,否则算缺勤...)只为了提交上午写的文档,其实本来想请假的,这才回了家,否则干脆就在公司了,可是事情重大,只能忍着,6点开始开会,一直开到9点灰头土脸的回家,期间说了涉事者每人罚款500元的事...按照那次的报告,提出了锥形设备以及倒锥型部署的概念,也正是本文要说的。
本次的事情和我的关系并不大,也没有谁被罚款,实际上我早就换了工作。我只是根据现场支持的同事们的描述,回想起了我的那段往事,觉得挺一致,就写了篇文章。说实话,几年下来,我在协议栈的层次跌落了,当时的我虽然对IP路由等不是很了解,可是对TCP的细节的把握却非常好,现在我真心的觉得遇到瓶颈了...
开篇之前再说几句题外话:第一,为什么不管我换了工作还是搬了家,公司离家的距离都是那么的远,不管是租的房子还是买的房子,不管是第一家公司还是现在的公司,我的打车费用都是110元到130元之间!第二,当时的公司怎么就不用VPN呢?要是用了,我肯定不会再跑回公司去提交什么文档。
1.透明代理
所谓的透明代理是针对客户端和服务器端两方面来讲的,对于客户端来讲,发起连接时连接的服务端是真实的服务器而不是代理服务器,对于服务器端来讲,收到的请求来自真实客户端而不是代理服务器。当然这其中有偷梁换柱的过程,一切仅仅是看起来很透明而已。2.TPROXY
TPROXY是Linux系统上实现透明代理的首选,它利用了Linux网络的利器-Netfilter实现了一些target,比如TPROXY,另外还有一个socket match用于捕获服务器返回的数据包,确保它们能够再回到代理服务器应用程序。值得注意的是,配置TPROXY一定要确保文件打开描述符的数量足够大,因为代理服务要创建大量的socket,每一个socket对应一个文件描述符。
2.1.IP_TRANSPARENT选项
IP_TRANSPARENT可以实现很神奇的事情,即,它可以bind一个不属于本机的IP地址,作为客户端,它可以使用一个不属于本机地址的IP地址作为源IP发起连接,作为服务端,它可以侦听在一个不属于本机的IP地址上,而这正是透明代理所必须的。面对真实的客户端,透明代理明知道目标地址不是自己,却还是要接受连接,对于真实的服务器,透明代理明显不是真实的客户端,却还要使用真实客户端的地址发起连接。2.2.截获数据
捕获数据就是把本来不该发给这台设备的数据捕获到这台设备的应用层。方法太多了,用ebtables,iptables,策略路由,不管是二层模式还是路由模式,捕获数据都不是问题,具体到细节,还得需要应用层参与,因为数据到达第四层的时候,会去查socket,也就是说必须得有一个socket和数据包关联起来,而一个socket又和一个应用程序关联,最终,一个数据包被捕获后,发给一个应用程序!2.2.1.NAT方式
应用层启用一个服务,侦听一个端口,使用iptables的DNAT或者REDIRECT这些target可以将数据包定向到本机应用程序。此时如果你使用netstat查看,看到的目标地址将是本机socket侦听的地址。2.2.2.纯TPROXY方式
即使不用NAT,也可以通过TPROXY target和策略路由的方式将数据包捕获,首先需要定义一条iptables规则:-A PREROUTING -p tcp -m tcp --dport 80 -j TPROXY --on-port 0 --on-ip 0.0.0.0 --tproxy-mark 100
定义一条rule以及路由:
ip rule add fwmark 100 tab proxy
ip route add local 0.0.0.0/0 dev lo tab proxy
如此一来,不管你发给哪个IP地址的到达80端口的TCP连接,均将被捕获,比如代理服务器的地址是192.168.100.100,而你发包的目标地址是1.2.3.4,只要数据包到达这台机器,均会被捕获到应用层。这是怎么回事呢?使用IP_TRANSPARENT选项可以侦听任意IP或者所有IP,当你侦听所有IP的时候,效果就呈现了,只需下面代码便可以测试:
struct sockaddr_in addr = {0};
int opt = 1;
fd = socket(AF_INET,SOCK_STREAM,0);
addr.sin_family = AF_INET;
addr.sin_port = htons(80);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
setsockopt(fd,SOL_IP, TRANSPARENT,&opt,sizeof(opt));
bind(fd,(struct sockaddr *)(&addr),sizeof(struct sockaddr));
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
listen(fd,5);
虽然本机根本就没有1.2.3.4这个地址,但是如果你在另一台机器上telnet 1.2.3.4 80,确保数据包经过192.168.100.100后,照样可以连通。
3.tuple空间
一个tuple代表了一个socket连接,在一台机器上,在一个命名空间内,tuple必须是唯一的,因为系统要区分一个数据要发给谁而不该发给谁。一般而言,tuple唯一性由第四层来保证,但是在设计的时候,由于NAT这种锥形行为使用了第四层的信息,因此NAT以及ip_conntrack也要确保tuple的唯一性。具体一点说,一个TCP的tuple包括了4元组:源IP地址,源端口号,目标IP地址,目标端口号。4.部署问题
作为一个代理程序,TPROXY要同时扮演客户端和服务器的功能,它一般部署在客户端和服务器的中间,普遍的部署拓扑为沙漏型部署,如下图所示:注意,部署了TPROXY代理之后,tuple空间并没有什么损失,由于客户端IP地址集合非常大,服务器IP地址空间也不小,因此TPROXY代理的确定化侦听端口并不会带来什么损失,此处有一个默认前提,一般的服务都是侦听熟知端口,因此并不存在大量的端口的情况,端口和IP地址的数量不对称性是关键。
4.1.沙漏部署
典型的,如透明代理,负载均衡设备,IPS,IDS,防火墙等,工作在三层或者七层透明模式下的部署都是沙漏部署,七层模式比三层/二层模式更消耗tuple空间,但是在服务器端都是熟知端口的情况下,可以忽略这种损失。4.2.锥形设备
类似NAT设备,NAT模式下的负载均衡设备,这些都属于锥形设备,所谓的锥形设备就是将一个以IP为标识的tuple元素映射成一个以IP-端口对为标识的tuple元组的设备,比如NAT设备可以将不同的IP地址映射成一个IP地址,然后用不同的端口来区分。简单点说,锥形设备就是将区分源IP,复用源端口改为了复用源IP,区分源端口,但是这种转换是不对称的,因为IP地址空间有32位,端口地址空间有16位,锥形转换一定会带来tuple空间的缩小,在目标服务为熟知端口的情况下,这种缩小更为严重,这就是说,源地址转换基本都是发生在出口位置而不是发生在入口位置的。为何服务会偏好熟知端口,因为服务的命名空间是策略化的,和服务的IP地址空间明显不同,端口很难通过类似DNS来检索,记住,IP和服务关系并不大,它只是为了寻址,服务的根本性的标识就是端口!
所以,由于这种端口和IP地址空间的不对称性,在面对不同IP地址的相同端口的服务时,锥形设备并不会减少多少tuple空间的大小,但是一旦锥形设备和服务之间接连七层模式的沙漏设备时,就会带来tuple空间的迅速坍缩!因为七层模式下,一个服务必然要绑定一个确定的端口,从而抵消掉前面锥形设备的端口发散效果。
4.3.倒锥形部署
所谓的倒锥形部署就是将锥形设备和七层模式沙漏设备串行结合在一起的部署方式,比如锥形NAT设备后面接一个TPROXY代理设备。这会带来什么问题呢?很显然,必须在代理设备上保证tuple的唯一性,为了保证这个唯一性,不得不付出一些代价,代价就是牺牲掉大量的tuple空间。也就是说,虽然锥形设备靠端口保证了tuple唯一性,但是锥形设备和沙漏设备串接在一起的时候,由于单独的沙漏设备也要保持tuple唯一性,因此就会抵消掉锥形设备的端口发散效果。锥形设备不是用端口来保证唯一性吗?紧接着的沙漏设备恰好也要这样,就会引起冲突,如下图所示:试想一下,以往的一对多的连接被一对一的连接代替,大家即使都用IP-端口对,tuple空间也会减小!现在看一下到底是什么引发了倒锥型现象!
4.4.NAT方式-地址约束
前面提到过,可以通过目标NAT的方式将数据包导入本地七层,这样的后果就是大量的真实服务器的IP地址映射成了少量的本机IP地址,本来可以用相同的锥形设备的IP地址和锥形设备的源端口访问不同的服务器的连接,此时不得不使用不同的锥形设备的源端口。注意,即使TPROXY前面的锥形设备使用了相同的源端口,也会被TPROXY的NAT模块改成不同的端口,因为TPROXY设备要保证tuple唯一性。这会导致什么问题呢?TPROXY上的端口将会快速被用尽,使得前面的锥形设备不得不使用不同的端口才能连接成功,这就会导致前面锥形设备的端口快速回绕。也许你懂得比较多,会说,我可以多添加几个DNAT,甚至使用port range,那么请问,对于前者,面对浩瀚的客户端地址空间,你在你的一块网卡上加多少IP地址算多呢?对于后者,面对浩瀚的真实客户端的地址空间,你又能建立多少个服务呢?这种想法固然可以缓解倒锥的压力,但是在大量短连接的情况下,大量的TIME_WAIT状态的socket会占据大量的tuple空间。也许对于较真儿的同学,他们会说,有一种TCP的优化,对于客户端初始序列号单调递增的情形,服务端的TIME_WAIT套接字可以重用。面对这样的人,我承认,我输了,我玩不过这些学院派,可是我面对的这台代理服务器并没有实现这个优化!