将IPTables改造为TCP负载均衡器
在深入研究Linux网络安全配置实用程序iptables的过程中,我们了解了为什么以及如何构建适用于处理IoT应用程序流量的复杂TCP路由器和负载均衡器。
在大多数平台即服务(PAAS)中仅仅用于托管Web应用程序,用HTTP协议访问。但是在内存、CPU和电池受限的环境中,如物联网,我们通常不使用HTTP而是首选定制的、快速且轻量级的基于TCP的协议。
进一步思考一下,其实应用程序构建和运行阶段与Web应用程序的阶段是非常相似的。编程语言(特别是NodeJS)和数据库通常也是共享的。因此,PaaS托管物联网应用程序的唯一关键因素只有TCP转发层。
这个TCP转发层必须拥有以下能力:
- 将原始TCP数据包路由到正确的应用程序
- 在多个容器之间负载均衡
对于HTTP转发,Scalingo使用OpenResty。但是它不能用于TCP(或者我们认为他不能,看结论)。这就是我们选择基于iptables作为实现的原因。
网络基础设施和目标
首先,定义一下我们的网络。 在本文中,我们将考虑两个不同的网段:
- 我们的公共网络:192.168.1.0/24客户端
- 我们的专用网络:10.0.0.0/24服务器所在的位置(用于托管应用程序容器)
公共网络有一个客户端IP:192.168.1.2,专用网络有三个服务器IP:10.0.0.2,10.0.0.3和10.0.0.4。
配置的最后一部分是前端接入服务器,它处在在两个网络中,分别用于有IP:10.0.0.1和192.168.1.1。
infra-bb947fed7e2a5f22b89d118cbec88bc0
在以下部分中,除非另有说明,我们将假设每个操作和命令都在前端接入服务器上操作。
NAT
让我们以尝试将请求地址192.168.1.1:27017的所有流量重定向到专用网络地址10.0.0.2:1234开始。
这是通过称为网络地址转换(或NAT)来完成的。 在本文中,我们将重点介绍两种不同的NAT方法:DNAT和SNAT。
DNA
DNAT通过改变TCP/IP包头中的目的地址来实现。
这种实现方法必须重写TCP包头中的IP地址,所以TCP包的目的IP地址应该被重写为10.0.0.2,端口应该被重写为1234。
这将产生以下转换:
PACKET RECEIVED PACKET FORWARDED
|---------------------| |---------------------|
| IP PACKET | | IP PACKET |
| | | |
| SRC: 192.168.1.2 | | SRC: 192.168.1.2 |
| DST: 192.168.1.1 | | DST: 10.0.0.2 |
| |---------------| | | |---------------| |
| | TCP PACKET | | =(DNAT)=> | | TCP PACKET | |
| | DPORT: 27017 | | | | DPORT: 1234 | |
| | SPORT: 23456 | | | | SPORT: 23456 | |
| | ... DATA ... | | | | ... DATA ... | |
| |---------------| | | |---------------| |
|---------------------| |---------------------|
要实现这个功能,我们需要用iptablesnat表的PREROUTING链。
iptables \
-A PREROUTING # 添加一条规则到 PREROUTING 链
-t nat # PREROUTING 链的作用域是 nat 表
-p tcp # 使这个规则仅对tcp包生效
-d 192.168.1.1 # 并且只有目的IP地址是192.168.1.1时
--dport 27017 # 并且目标端口是27017
-j DNAT # 使用DNAT
--to-destination # 改变TCP/IP目的地址头
10.0.0.2:1234 # 修改后的目的地址是10.0.0.2:1234
这就是全部的配置,现在如果我们尝试连接iptables主机的27017端口的话流量将会被转发到10.0.0.2的1234端口上。
在客户端上我们实验一下:
user@client ~ $ echo "Hi from client" | nc 192.168.1.1 27017
这个命令会一直挂在那,服务器端也没有动静。
通过查看Server端收到的数据包,我们能看出iptables规则生效了并且流量被转发到了正确的目的地。
user@server-1 ~ $ tcpdump -i eth1
15:19:17.832609 IP 192.168.1.2.23456 > 10.0.0.2.1234: Flags [S],
seq 37761180, win 29200, options [mss 1460,sackOK,
TS val 21306607 ecr 0,nop,wscale 6], length 0
SNAT
客户端的命令挂在那的原因是服务端不知道怎么样能把响应发回去,因为来源IP地址是192.168.1.2,这个IP不在他所处的网段里。
解决方法是在接入服务器上把数据包的来源IP和端口也修改掉,这是通过SNAT的方法来实现的。
这将会发生以下转换:
PACKET RECEIVED PACKET FORWARDED
|-------------------| |-------------------| |-------------------|
| IP PACKET | | IP PACKET | | IP PACKET |
| | | | | |
| SRC: 192.168.1.2 | | SRC: 192.168.1.2 | | SRC: 10.0.0.1 |
| DST: 192.168.1.1 | | DST: 10.0.0.2 | | DST: 10.0.0.2 |
| |---------------| | | |---------------| | | |---------------| |
| | TCP PACKET | |=(DNAT)=>| | TCP PACKET | |=(SNAT)=>| | TCP PACKET | |
| | DPORT: 27017 | | | | DPORT: 1234 | | | | DPORT: 1234 | |
| | SPORT: 23456 | | | | SPORT: 23456 | | | | SPORT: 38921 | |
| | ... DATA ... | | | | ... DATA ... | | | | ... DATA ... | |
| |---------------| | | |---------------| | | |---------------| |
|-------------------| |-------------------| |-------------------|
SNAT发生在所有路由规则之后(包括我们的DNAT规则),所以我们需要把SNAT规则加到iptables nat 表的POSTROUTING链。
iptables \
-A POSTROUTING
-t nat
-p tcp
-d 10.0.0.2 # 应用这个规则如果数据目的IP是10.0.0.2
--dport 1234 # 目标地址端口是1234
-j SNAT # 使用SNAT
--to-source 10.0.0.1 # 改写目的IP地址为10.0.0.1
Iptables在内存中维护一个转换表,并自动处理从服务器返回的连接,将它们重定向到客户端。
还是用我们刚才用过的nc命令:
user@client ~ $ echo "Hi from client" | nc 192.168.1.1 27017
Hi from server
通过查看Server端收到的包,我们看到源IP和目标IP地址都被接入服务器修改了。
user@server-1 ~ $ tcpdump -i eth1
15:29:37.384773 IP 10.0.0.1.38921 > 10.0.0.2.1234:
Flags [S], seq 3215489734, win 29200, options [mss 1460,sackOK,
TS val 21461495 ecr 0,nop,wscale 6], length 0
系统安全
iptables通常被用作防火墙,他的主要功能是通过添加一些规则来删除没有明确允许通过的数据包,每个iptables链都有默认策略,如果数据包没有满足这个链中的任何一条规则就会使用默认策略,默认策略是DROP,也就是会被丢弃,每个链接如果没有被明确允许接受的话就会被丢弃。
iptables -t filter -P FORWARD DROP
上边所说的SNAT和DNAT规则仅仅修改包头,还没有对过滤器起作用,由于默认策略是丢弃,我们现在需要明确接收入口流量并转发到后端server,规则如下:
# Accept traffic to Server 1
iptables -t filter -A FORWARD -d 10.0.0.2 --dport 1234 -j ACCEPT
# Accept traffic from Server 1
iptables -t filter -A FORWARD -s 10.0.0.2 --sport 1234 -j ACCEPT
我们现在可以通过我们的前端接入服务器的TCP端口27017转发流量到一个单节点的应用上了。
负载均衡
现在下一步就是分发链接到多个节点上了。
为了在多个节点上做负载均衡,一个解决方案是修改现有的SNAT规则使端上的请求从总是转发到单个节点上转为分发到多个节点上。
为了在Server1、Server2、Server3上分发请求,我们可以临时定义这些规则:
iptables -A PREROUTING -t nat -p tcp -d 192.168.1.1 --dport 27017 \
-j DNAT --to-destination 10.0.0.2:1234
iptables -A PREROUTING -t nat -p tcp -d 192.168.1.1 --dport 27017 \
-j DNAT --to-destination 10.0.0.3:1234
iptables -A PREROUTING -t nat -p tcp -d 192.168.1.1 --dport 27017 \
-j DNAT --to-destination 10.0.0.4:1234
但是iptables引擎实现决定了,第一条规则会一直生效,这个例子中Server1会接收到所有的链接。
为了解决这个问题,iptables包含一个模块叫statistic,这个模块会基于一些统计信息跳过或者匹配一个规则。
这个模块支持两种模式:
- random:基于概率跳过这个规则
- nth:基于轮询算法跳过这个规则
注意这个负载均衡仅在TCP链接建立阶段生效,一旦链接建立了,这个链接将会一直被转发到同一个后端服务器。
随机负载均衡
要在3台服务器上实现真正的负载均衡,上边的3条规则需要修改成这样:
iptables -A PREROUTING -t nat -p tcp -d 192.168.1.1 --dport 27017 \
-m statistic --mode random --probability 0.33 \
-j DNAT --to-destination 10.0.0.2:1234
iptables -A PREROUTING -t nat -p tcp -d 192.168.1.1 --dport 27017 \
-m statistic --mode random --probability 0.5 \
-j DNAT --to-destination 10.0.0.3:1234
iptables -A PREROUTING -t nat -p tcp -d 192.168.1.1 --dport 27017 \
-j DNAT --to-destination 10.0.0.4:1234
注意这定义的3个概率并不都是0.33,原因在于规则是按照顺序执行的。
在0.33的概率下,第一条规则将会有33%的的概率被执行,66%的概率被跳过。
在0.5的概率下,第二条规则将会有50%的概率被执行,50%的概率被跳过,然而由于这个规则放在第一条之后,只有66%的概率会被执行到,因此这条规则被执行的概率是50% * 66% = 33%。
最后只有33%的链接会到达最后一条规则,这时只有第三条规则可以使用。
你可以基于规则条目总数 n 和规则所处的位置 i 计算每个规则的概率,计算公式是 。
轮询
另一种做负载均衡的方式是 nth 算法,这个算法实现了轮询算法 round robin algorithm。
这个算法接受2个不同的参数:every(n)和 packet,规则将会从每n个链接的包p开始计算。
要达到在3个节点间做负载均衡的目的我们需要创建这3条规则:
iptables -A PREROUTING -t nat -p tcp -d 192.168.1.1 --dport 27017 \
-m statistic --mode nth --every 3 --packet 0 \
-j DNAT --to-destination 10.0.0.2:1234
iptables -A PREROUTING -t nat -p tcp -d 192.168.1.1 --dport 27017 \
-m statistic --mode nth --every 2 --packet 0 \
-j DNAT --to-destination 10.0.0.3:1234
iptables -A PREROUTING -t nat -p tcp -d 192.168.1.1 --dport 27017 \
-j DNAT --to-destination 10.0.0.4:1234
允许流量通过
由于我们的过滤器中 FORWARD 链的默认策略是 DROP,我们需要允许访问这3个服务器,这个可以通过这6条iptables规则做到:
iptables -t filter -A FORWARD -d 10.0.0.2 --dport 1234 -j ACCEPT
iptables -t filter -A FORWARD -d 10.0.0.3 --dport 1234 -j ACCEPT
iptables -t filter -A FORWARD -d 10.0.0.4 --dport 1234 -j ACCEPT
iptables -t filter -A FORWARD -s 10.0.0.2 --sport 1234 -j ACCEPT
iptables -t filter -A FORWARD -s 10.0.0.3 --sport 1234 -j ACCEPT
iptables -t filter -A FORWARD -s 10.0.0.4 --sport 1234 -j ACCEPT
现在如果客户端请求我们的服务器时,将会得到下面的输出结果:
user@client ~ $ echo "Hi from client" | nc 192.168.1.1 27017
Hi from 10.0.0.2
user@client ~ $ echo "Hi from client" | nc 192.168.1.1 27017
Hi from 10.0.0.3
user@client ~ $ echo "Hi from client" | nc 192.168.1.1 27017
Hi from 10.0.0.4
user@client ~ $ echo "Hi from client" | nc 192.168.1.1 27017
Hi from 10.0.0.2
[...]
结论
这篇文章中我们展示了如何基于iptables和linux内核构建一个TCP负载均衡器,我们用这个方法创建了一个用于IoT产品的TCP网关,同理我们也可以创建一个用于访问数据库的接入点。











网友评论