供稿 |  eBay Infraops Team

作者 | 梅南翔

审稿 | 杨胜辉

编辑 | 顾欣怡

本文4536字,预计阅读时间14分钟

更多干货请关注“eBay技术荟”公众号

导读

本文对负载均衡器的 DSR(Direct Server Return)模式 进行了原理分析,并以业界普遍使用的F5 LTM+CentOS为例,探究了DSR配置的关键技术点,最后借助 Linux内核分析 ,对F5提供的官方步骤提出了优化方案。希望能对读者有所启发和帮助。

一、背景

在现代企业内部, 负载均衡器 (LoadBalancer,以下简称 LB )被大量使用。负载均衡器的常用模式如下图1所示:

图1 负载均衡的常用模式(代理模式)

(点击可查看大图)

负载均衡的常用模式可概述为下(详情可见: 分享 | eBay流量管理之负载均衡及应用交付 ):

1. 客户端向负载均衡器提供的虚拟IP地址VIP发送请求 (CIP → VIP)

2. 负载均衡器从提前配置的多个子网IP地址中选择一个(SNAT IP),替代客户端请求的源IP,并依据负载均衡算法,选择一个服务器IP作为目的IP,发送请求 (SNIP → SIP)

3. 服务器处理后将响应结果发回负载均衡器 (SIP → SNIP)

4. 负载均衡器将最终结果返回给客户端 (VIP → CIP

但负载均衡器的这种 经典SNAT模式 ,在基础架构运维中有以下 缺点

1. 由于信息量的因素,网络请求的回包往往会比请求包大很多。一般达到 10倍 [1] 20 Mbps 的请求,其回包可能达到 200 Mbps 。这样一来,由于回包也是经过LB,就会大量增加LB的带宽使用,减小LB的有效处理容量。

2. 基础架构的服务(DNS,MAIL,LDAP等)工作在TCP/UDP传输层之上,所以无法像其它工作在HTTP协议以上的应用那样,用HTTP header里边的X-Forwarded-For字段来保存客户端真实IP。 这些基础架构服务的请求包在被LB进行SNAT之后,客户端的真正IP被替换为LB的SNAT IP。 这样造成的结果就是,后端服务器无法知道真实的客户端IP是什么,给问题排查、攻击检测、运行指标统计等运维活动带来极大不便。

而DSR (Direct Server Return,服务器直接返回)技术 (其在Layer 2实现时叫Direct Routing,F5称之为nPath), 顾名思义,就是让后端服务器绕开LB直接回包给客户端 (如下图2 [2] ),从而实现节省LB带宽和获取客户端真实IP的目标。

图2 DSR数据流图

(点击可查看大图)

二、原理

那么DSR又是通过怎样的独到之处,跟LB的看家本领SNAT叫板的呢? 原来,在DSR模式下,当网络请求包到达LB时,LB并不做SNAT,而是把包的源IP地址原封不动地转发给后端服务器。 当后端服务器拿到请求包以后,由于包里边携带了客户端的源IP地址,它就可以直接将回包通过网络路由给这个源IP地址,到达客户端手中。

不过,等等,这幸福来得也太突然了吧?让我们来仔细分析一下,看看这中间的重重险阻在哪里。

设想一下,服务端如果傻傻地用[src=SIP;dst=CIP]回包,那么客户端收到这个包以后,会怎么样?——当然是丢掉。为什么?因为客户端也不傻,它知道自己请求的是VIP,所以期待的是VIP给它的回包。至于这个服务端SIP又是个什么鬼?不需要,丢弃即可。因此,为了绕过客户端这道安全防线,聪明的工程师们想到了一个绝好的办法, 就是让服务端伪装自己的IP地址为VIP,用[src=VIP;dst=CIP]回包,以期能骗过客户端,让客户端误以为是LB的VIP回复给它的。

那是不是我们直接在服务端网卡上配置VIP,让服务端通过该网卡应答就可以?答案是No,因为这样虽然确保了客户端收到的应答包是[src=VIP,dst=CIP],但带来的问题是:服务端和LB将同时应答ARP,引发IP地址冲突。所以,服务端伪装VIP可以,但一定要低调,万万不可让其他人知道—— 有一个绝好的办法,就是将VIP偷偷地配置到loopback网卡或者tunl0这些本地的网络接口上 ,既骗过了内核,让它以为真的有这个合法IP地址,又不会被外边的设备发现,一石二鸟,想想都激动。

还有一个问题,怎么样才能让服务端内核以[src=VIP,dst=CIP]的组合回包呢?由于内核是严格按照(src→dst, dst→src)的方式进行回包的,那让服务端内核如此回包的最好办法,当然是让LB转发[src=CIP,dst=VIP]的包给服务端的内核啦。但是,这听上去有点像天方夜谭,服务端的VIP是一个“偷偷摸摸”的配置,所以LB发出的[src=CIP,dst=VIP]的IP包,在路由时,一定是找到了LB上的VIP,绝无可能被路由到服务端去。怎么办?我可太“南”了…

针对上述问题,有一个办法,就是LB跳过L3三层路由,直接把[src=CIP, dst=VIP]的IP包,塞到一个目标MAC地址为服务端网卡MAC地址的L2网络包里,送达服务端网卡 (相当于将从客户端到LB的L2数据包更换一下目标MAC地址。对应于NAT,这种实现方法也被称作MAT,即MAC Address Translation [3] ,参见图3)。

图3 L2 DSR替换请求包的目标MAC地址

(点击可查看大图)

这个方法实现简单,但有一个硬伤,就是当LB和后端服务器不在同一个L2网络的时候就无能为力了。所以我们还是回到L3网络来考虑这个问题,也就是如何让LB把[src=CIP,dst=VIP]的IP包转发给服务端的内核。 如果把网络通信类比于传统信件,那我们现在的需求就是,让LB送一封“收件人”为VIP的信件,并且保证该信件是送到服务端而不是LB自己。 通过这一类比,我们不难想出一个好办法,就是让LB给服务端送一封信([dst=SIP]),然后在这封信里再嵌入一封“收件人”为VIP的信([dst=VIP]);同时在外层信封上标注——这里边装的不是TCP,也不是UDP,而是另外一封信(协议)。这样当服务端收到外边的信封以后,它继续拆开里面的信,就可以获取到[src=CIP,dst=VIP]的数据了。

事实上,这种做法就是 网络隧道(Network Tunnel) 的概念——将一种网络协议包(inner packet)作为payload嵌入外层网络协议包中,并利用外层网络协议进行寻址和传输,从而实现原本割裂的内层网络像隧道一样被打通了的效果。

最常见的网络隧道就是我们使用的VPN :当拨入VPN之后,我们就可以访问10.x.x.x这种内网地址了。很显然是10.x.x.x这种IP包被封装进了某种特殊的通道,也就是一些基于公网VPN协议的网络隧道。 类似于VPN,在DSR这种场景下,我们就是要将访问VIP的IP包通过至SIP形成的隧道进行传输。 Linux支持的IP in IP协议主要有 IPIPGRE 两种,IPIP隧道不对内层IP数据进行加密 [4] ,所以它最简单,效率最高,如图4所示 。由于是内网可信网络,因此选用IPIP这种最简单高效的隧道协议。

图4 IPIP隧道协议

(点击可查看大图)

我们可以让LB生成这样的一个IPIP包[src=CIP, dst=SIP, protocol=IPIP, payload=[src=CIP, dst=VIP] ]并发出去。由于外层的IP包目标地址是服务端IP,所以外层IP包顺利到达服务端内核;服务端内核拆包一看,这里边又是一个IPIP包,于是进一步解包,发现是[src=CIP,dst=VIP]的包。 这时内核检查dst=VIP是本地一个合法的IP地址(因为本地网卡已经配置了VIP),于是欣然接受,并根据TCP/UDP的目标端口转给应用程序处理。 而应用程序处理完,就顺理成章地交由内核,按照(src→dst, dst→src)的原则,产生[src=VIP, dst=CIP]的IP包进入网络路由至客户端,问题解决!此时的数据流图参见下图5:

图5 使用IPIP隧道的DSR数据流图

(点击可查看大图)

三、探究

下面,我们以业界普遍使用的 F5负载均衡器 LTM + CentOS 7 为例,实战分析探究一下DSR配置的关键技术。

Step 1

如下所示,在LB上配置一个有2个成员的负载均衡池(pool),并创建一个VIP对应到该pool:

  • 该pool指定的profile为IPIP,表示LB和该pool的成员通讯,走IPIP隧道;

  • 要关闭PVA-Acceleration,因为DSR模式下,client不再直接与VIP建立TCP连接,所以用不到L4的PVA硬件加速;

  • 该VIP指定translate-address disabled,表示不对请求该VIP的流量进行SNAT。

(点击可查看大图)

Step 2

此时,我们在server端先不做任何配置,直接从client去telnet VIP,并且从server端分别用VIP和CIP抓包(如表1),看有什么现象发生。

表1 分别用VIP和CIP作为条件抓包

(点击可查看大图)

从wireshark中可以看到,LB在收到[CIP,VIP]的请求后,产生一个IPIP的包[src=CIP, dst=SIP, protocol=IPIP, payload=[src=CIP, dst=VIP] ]。这时候,由于我们还没有从服务端正确地配置IPIP Tunnel和VIP, 所以这个包无法正确地被识别和处理, 导致了 3次 SYN包的重传和 1次 RST (图6中的包3,5,7和9),同时服务端以ICMP的形式告诉client,由于目标IP地址无法抵达的缘故,之前的包无法被正常传递 [5] (图中包2,4,6,8,10)。

图6 LB通过IPIP隧道发到服务端的数据包

(点击可查看大图)

Step 3

进一步分析,由于服务端的内核还没有加载IPIP模块,所以它不能识别Protocol=IPIP的数据包,无法解析出嵌入在IPIP包里的内层IP数据包。由于VIP只出现在内层IP包里,因此以VIP作为filter抓包,自然是抓不到了。 现在我们尝试让内核加载IPIP模块并再次抓包,看它能否识别并解开内层IP包。

我们重新做一次表1的telnet和tcpdump,相比于上一次的抓包,这次我们以VIP作为filter就可以抓到内层IP包了。 这说明内核加载IPIP模块后就可以识别Protocol=IPIP的数据包了。 但到这里,由于我们并没有在服务端的任何网络接口(interface)上配置VIP,所以服务端仍然未作出回包处理。

Step 4

下一步,就是将VIP配置到tunl0或者lo上,让内核识别到这是一个合法的IP地址。在F5提供的参考配置文档 [6] 中,就是将VIP配置到loopback网卡lo:1上,并在tunl0上配置SIP。但是仔细思考一下,F5官方提供的这个配置并不令人十分满意——根据我们前边的分析,我们只要在loopback或者tunl0这类本地网卡上配置VIP,让内核认为只是一个合法的地址就行了,为什么还要多此一举地往tunl0上配置SIP?

既然有这个疑问了,我们索性逐步推进,看看, 如果不在tunl0上配置SIP到底会发生什么?

Step 4.1

首先,我们在lo:1上配置VIP,并且不在tunl0上配置任何IP地址。

再次重做telnet,并用CIP作为filter抓包(语句见表1),结果如图7所示,只能抓到客户端到服务端的请求包(奇数行为eth0收到的外层包,偶数行为tunl0收到的内层包, 总共是 1 SYN+ 3次 SYN重传,和 1次 RST )。

图7 服务端未回包

(点击可查看大图)

可见服务端内核确实收到了[dst=VIP]的包,但既没有送给应用程序,也没有回包,那是内核将这些包丢弃了吗?此时用dmesg命令检查一下内核日志,发现在对应时间段有 5条 下述日志:

这个报错是什么意思呢?原来,网络攻击者(例如DDOS)在攻击时,都会伪装自己的IP地址(IP Address Spoofing),以绕开IP源地址检查或是防止DDOS回包回到自身。由于源地址无法路由可达(route back),这时服务端就会不断保持连接并等待,直到连接超时,造成资源耗竭无法正常提供服务。

为了应对这种攻击,Linux内核加上了一个叫做 反向路径过滤(reverse path filtering [7] )的机制,这个机制会检查,收到包的源地址是否能从收包接口(interface)路由可达,如果不可达,就会将该包丢弃,并记录 “martian source” 日志 。回到我们问题的场景,tunl0在收到内部IP包[src=CIP, dst=VIP]这个包之后,就会去尝试验证从tunl0这个接口能不能连通CIP=10.218.98.18,由于tunl0无法路由到CIP,所以出现了上述的报错。

那有什么办法,既能够保证一定程度的安全,又可以支持DSR这种场景呢?rp_filter这个参数有 3个 [8 ] ,如图8,其中Loose mode下,只要从任何接口可以路由到源IP地址(而不限于收包接口),就不做丢弃处理。所以, 我们可以将tunl0的rp_filter设置成 2 ,这样由于CIP可以经由eth0路由,验证应该会成功。

图8 Linux内核关于rp_filter的定义

(点击可查看大图)

Step 4.2

如下所示, 设置tunl0的rp_filter= 2 ,也就是对从tunl0进入的IP网络包,如果其源IP地址可经由机器上任意网卡(例如eth0)路由到,就认为它是一个合法的报文。

然后重新做一次telnet和tcpdump,奇怪的是,我们依旧没有看到服务端内核回包,并且内核日志仍旧报 “martian source” 的错误。这又是怎么回事呢?看来,F5官方文档让在tunl0上配置SIP的要求似乎是不能省略的。

Step 4.3

接下来我们往tunl0上配置SIP,然后重做telnet和tcpdump。不出所料,此时服务端给出了正确的DSR回包(限于篇幅,此处略去抓包结果)。

到这里,DSR误打误撞地完成了配置,但我心里还有些难以平复—— Tunl0网卡的作用在于接收IPIP隧道内的内层IP包,但是服务端内核在回包时,是直接将[src=VIP, dst=CIP]的IP包经由eth0路由出去的,根本就没有tunl0什么事。 所以tunl0上理论上不需要配置任何IP,但为什么在tunl0上配置SIP以后,就能解决"martian source"的问题呢?

怀着这个问题,我在网上找了很久的答案都没找到,最终通过eBPF对linux内核源码进行分析, 发现在linux的rp_filter代码中,如果网络接口(这里是tunl0)不配置任何ip(ifa_list == NULL),那么就会返回验证失败。

具体请参考github链接:

https://github.com/centurycoder/martian_source

(点击可查看大图)

Step 5

既然linux内核要求tunl0上必须配置IP地址,那是不是意味着我们必须按照F5提供的官方步骤进行配置呢?不是的,我们前面已经分析,只要是在loopback或者tunl网卡上配置VIP骗过内核就行, 那我们就索性将VIP直接配置到tunl0网卡上,这样既实现了tunl0上有IP地址的要求,又满足了VIP是一个合法目标地址的要求。

我们将第4步的配置回退后,在tunl0上配置VIP和rp_filter,如下所示:

然后按照表1重新做一次telnet和tcpdump,如图9所示,客户端和服务端通过DSR成功完成TCP的3次握手( 1 是eth0端口上抓到的外层包, 2 是tunl0上抓到的内层包,它们其实是同一个 SYN包 ,所以被wireshark标记为TCP Out-of-Order; 3 是服务端回复的 SYN/ACK包 ,它不走Tunnel,所以没有成对出现; 4 5 是客户端发的 ACK包 ,分别是eth0抓到的外层包和tunl0抓到的内层包,被wireshark误认为是重复的ACK)。

图9 成功的DSR通讯

(点击可查看大图)

四、总结

到这里,我们就完成了DSR的原理分享和探究,并且通过对原理以及Linux内核源码的分析,对F5官方提供的步骤进行了解析和优化。总的来说,从运用方面来看, 相比于SNAT模式,DSR在提高LB带宽容量、保持客户端真实IP等方面有很大优势。 但在选择DSR或者SNAT模式时还要考虑实际运用场景。 DSR往往更适用于LB仅用作负载均衡功能的场景 ,而如果LB还承担SSL offload、Cache、加速或者其它安全相关功能时,则只能选取经典SNAT模式 [9] 。希望本文能对各位读者理解和运用DSR技术有所帮助和启发。

参考资料:

[1]https://kemptechnologies.com/au/white-papers/what-is-direct-server-return/

[2]https://www.loadbalancer.org/blog/15-years-later-we-still-love-dsr/

[3]https://www.haproxy.com/support/technical-notes/an-0053-en-server-configuration-with-an-aloha-in-direct-server-return-mode-dsr/

[4]https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/

[5]https://tools.ietf.org/html/rfc792

[6]https://techdocs.f5.com/en-us/bigip-15-0-0/big-ip-local-traffic-manager-implementations/configuring-layer-3-npath-routing.html

[7]https://www.slashroot.in/linux-kernel-rpfilter-settings-reverse-path-filtering

[8]https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

[9]https://devcentral.f5.com/s/articles/the-disadvantages-of-dsr-direct-server-return

您可能还感兴趣:

干货 | Rheos SQL: 高效易用的实时流式SQL处理平台

分享 | “三高”产品设计的这些坑,你是不是也踩过?(上)

分享 | “三高”产品设计的这些坑,你是不是也踩过?(下)

一探究竟 | eBay流量管理之看不见的手

解密 | 一桩由数据洁癖引发的DNS悬案

分享 | eBay流量管理之Kubernetes网络硬核排查案例

分享 | eBay流量管理之负载均衡及应用交付

:point_down:点击 阅读原文 ,一键投递 

eBay大量优质职位,等的就是你

相关文章