前言

我们的程序,在实际的网络部署时,一般比较复杂,会经过很多的网络设备,防火墙就是其中的一种。做开发的同事,一般对这块了解不多,也很可能被防火墙坑到。比如,应用一般需要访问数据库,为了避免频繁建立连接,一般是会提前建立一个连接池,每次来一个请求,就从连接池取一个连接来用,用完再归还到池子里。

连接池中的连接是啥呢,其实就是和数据库之间的完成了三次握手后的socket,这个socket在白天时,一般经常有数据传输,而到了凌晨这种,可能就很少数据传输,等到了第二天,某个请求来了,从池子里取了某个socket,就直接发送数据,此时,很可能就会出现一个read timeout的情况。

为啥呢,是数据库不返回数据吗?不一定,如果应用服务器和db服务器之间,经过了防火墙的话,很可能,你这个socket发出去的包,直接就防火墙给丢弃了,根本没有到达数据库。如果想搜索这块相关的案例,可以搜索“防火墙 长连接”关键字。

状态防火墙(stateful firewall)

现在的防火墙,基本都是有状态的,什么叫做有状态呢?以tcp为例,当服务端收到第一次握手(syn)时,此时,会认为这是要新建立连接,当三次握手完成后,再收到客户端在该socket上发来的请求报文时,此时,就知道这个报文不再是要新建立连接了,socket的状态此时也是established。说白了,握手时候的报文和握手完成后的报文,是不一样的,我们是可以区别对待的,这就是通俗意义上的“有状态”。

而在此之前,都是无状态防火墙(stateless firewall),不管是什么报文,都是一视同仁,没法根据状态来区分处理。

接下来,我们可以以iptables为例,理解下状态防火墙(iptables就是典型的状态防火墙)。

iptables中的状态

该部分参考官网:https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html#STATEMACHINE

iptables是用户态中的命令,在内核中由netfilter实现,netfilter中实现连接的状态跟踪的机制,叫做conntrack(connection track)。在这个机制中,一共定义了4种状态:NEW, ESTABLISHED, RELATED,INVALID,UNTRACKED。

注意这里的conntrack,中文就是连接追踪,既然是连接,那么,我们怎么区分一个连接呢,比如,在tcp中,就是依靠四元组,这个四元组就可以唯一标识一个连接。

我们可以定义一个hash表,在收到一个tcp报文时,检查下这个四元组在hash表中是否存在(key就是四元组),如果不存在,我们就可以认为这个报文此时是新来的,也就是状态是new;当我们收到服务端返回的报文时,我们又可以检测下四元组在本地hash表是否存在,发现已经存在了,此时就可以认为状态是established。

这里大家如果要看这个状态的详细解释,可以查看官网文档;另外,我发现一篇文章也不错:

https://mp.weixin.qq.com/s/kv32lyWak4dCaPjMI4_6jw

NEW:
新建连接请求的数据包,且该数据包没有和任何已有连接相关联。 判断的依据是conntrack当前“只看到一个方向数据包(UNREPLIED)”,没有回包。
ESTABLISHED:
该连接是某NEW状态连接的回包,也就是完成了连接的双向关联。

其他几种状态,RELATEDINVALID我感觉一般比较少用,至于UNTRACKED ,可以等后续再慢慢领悟。

实际感受状态变化

客户端首次握手(syn)

可以通过cat /proc/net/nf_conntrack查看连接状态。

如收到首次握手请求后,会看到如下内容:

tcp      6 117 SYN_SENT src=192.168.1.5 dst=192.168.1.35 sport=1031 \
     dport=23 [UNREPLIED] src=192.168.1.35 dst=192.168.1.5 sport=23 \
     dport=1031 use=1

各个字段的意思:

  • tcp,协议名

  • 6,传输层协议代号,其中tcp是6,udp是17,

  • 117,ttl,表示这个conntrack的过期时间,类似于redis里的key的ttl

  • SYN_SENT,socket此时的状态。官方:The value SYN_SENT tells us that we are looking at a connection that has only seen a TCP SYN packet in one direction

  • src=192.168.1.5 dst=192.168.1.35 sport=1031 dport=23部分:

    the source IP address, destination IP address, source port and destination port

  • UNREPLIED(未回复)

    we have seen no return traffic for this connection,表示我们还没看到回包,现在只有单方向的包

  • src=192.168.1.35 dst=192.168.1.5 sport=23 dport=1031

    这是我们期待的回包的样子

服务端返回syn+ack

tcp      6 57 SYN_RECV src=192.168.1.5 dst=192.168.1.35 sport=1031 \
     dport=23 src=192.168.1.35 dst=192.168.1.5 sport=23 dport=1031 \
     use=1

收到syn+ack后,此时,我们的状态由SYN_SENT变成SYN_RECV,此时,也移除了之前的UNREPLIED关键字

客户端第三次握手 ack

tcp      6 431999 ESTABLISHED src=192.168.1.5 dst=192.168.1.35 \
     sport=1031 dport=23 src=192.168.1.35 dst=192.168.1.5 \
     sport=23 dport=1031 [ASSURED] use=1

状态变成ESTABLISHED,增加了关键字:ASSURED

其他

涉及到socket关闭的部分,导致的状态变化请参考官方文档:https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html#STATEMACHINE

数据库连接池长时间不用,乍一用还用不了,结果是防火墙的锅-LMLPHP

简单来说,就是两边都完成关闭,状态才变成closed。

状态标记发生的阶段

我们上面可以看到在一个connection上状态的变化,说白了,这部分就是给一个报文打上标记,关于状态的标记,后续我们才能基于这个状态label进行匹配。

所以,我们对于这个标记发生的时间,需要特别注意,可以看下,下图的左上角的绿色方块(connection tracking)就是状态标记发生的位置,基本上是最前面了(最上面是网卡,再下来是raw,再下来就是connection tracking了)。

在这个绿色方块执行完成后,状态就已经标记好了,我们就可以根据状态来匹配这些报文来进行accept或者drop了,具体看下文。

数据库连接池长时间不用,乍一用还用不了,结果是防火墙的锅-LMLPHP

iptables模拟长连接超时后继续使用该连接的场景

部署图

我们的两台机器如下:

数据库连接池长时间不用,乍一用还用不了,结果是防火墙的锅-LMLPHP

服务器:10.0.2.15:2222,监听2222端口

客户端:10.0.2.4

iptables配置

放行第一次握手请求

三次握手的报文都需要放行,第一次握手请求的特征是,此时,根据前面我们讲的状态,第一次握手到来时,本地是找不到该四元组的,所以state是new,再加上目的端口是2222,因此,我们的匹配条件是:

--dport 2222 -m state --state NEW,因此,命令如下:

iptables -I INPUT 4 -p tcp --dport 2222 -m state --state NEW -j ACCEPT

放行第二次、第三次握手请求

此时,马上会收到2222端口返回的syn + ack,此时,状态就会变成established。

这种报文的特征是,state为established,这种怎么放心呢,这种其实默认就放心了,不需要我们再干啥。

数据库连接池长时间不用,乍一用还用不了,结果是防火墙的锅-LMLPHP

第三次请求,也是同理,状态会被标记为established。

等待conntrack记录过期或手动删除、清空

我们这边就需要模拟连接建立后,等conntrack记录过期后,客户端再使用这个socket会发生什么。

那么,我们怎么来查看这些conntrack呢,需要安装工具:yum install conntrack-tools

查看hash表
[root@node1 ~]# conntrack -L
tcp      6 431997 ESTABLISHED src=10.0.2.2 dst=10.0.2.15 sport=9526 dport=22 src=10.0.2.15 dst=10.0.2.2 sport=22 dport=9526 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1

我们可以看到过期时间为431997,单位是秒,差不多是5天。这个初始值是432000,来自于内核参数:net.netfilter.nf_conntrack_tcp_timeout_established :

[root@node1 ~]# sysctl -a |grep net.netfilter.nf_conntrack_tcp_timeout_established
net.netfilter.nf_conntrack_tcp_timeout_established = 432000

我们测试的话,这个时间就太长了,我们可以修改这个内核参数进行设置。我们可以改成60s过期:

[root@node1 ~]# vim /etc/sysctl.conf
net.netfilter.nf_conntrack_tcp_timeout_established = 60

执行sysctl -p

我们也可以选择手动删除、清空这些conntrack记录:

删除目标端口为2222的记录:
conntrack -D -p tcp --dport 2222

清空整个hash表
conntrack -F

实时监控hash表的变动(增删改)
conntrack -E

丢弃客户端在长时间空闲的长连接上发过来的包

由于我们上一步执行了conntrack记录删除,此时,客户端发的报文,在iptables看来,又是全新的四元组,也就是会把状态标记为new,但是,这种请求报文和握手报文明显不同,这种报文的tcp层一般会设置psh标记,握手请求里则会设置syn等,所以,我们按照这个进行区分。

匹配条件为:--dport 2222 -m state --state NEW -m tcp --tcp-flags PSH PSH,动作为丢弃:drop

iptables -I INPUT 4 -p tcp --dport 2222 -m state --state NEW -m tcp  --tcp-flags PSH PSH  -j DROP

ok,接下来开始测试。

最终的iptables效果

数据库连接池长时间不用,乍一用还用不了,结果是防火墙的锅-LMLPHP

实际测试

用到了该博客中的客户端、服务端程序:

https://blog.csdn.net/fengcai_ke/article/details/125717134

server.c,主要是监听2222端口;client.c,就是连接并发送请求。

服务端发起监听,客户端发起连接,完成三次握手:

[root@node1 ~]# ./server 

[root@node2 ~]# ./client 
please input:

此时,可以看到三次握手:

数据库连接池长时间不用,乍一用还用不了,结果是防火墙的锅-LMLPHP

服务端的conntrack -E,也可以看到hash表的变化:

[root@node1 ~]# conntrack -E
    [NEW] tcp      6 120 SYN_SENT src=10.0.2.4 dst=10.0.2.15 sport=46216 dport=2222 [UNREPLIED] src=10.0.2.15 dst=10.0.2.4 sport=2222 dport=46216
 [UPDATE] tcp      6 60 SYN_RECV src=10.0.2.4 dst=10.0.2.15 sport=46216 dport=2222 src=10.0.2.15 dst=10.0.2.4 sport=2222 dport=46216
 [UPDATE] tcp      6 432000 ESTABLISHED src=10.0.2.4 dst=10.0.2.15 sport=46216 dport=2222 src=10.0.2.15 dst=10.0.2.4 sport=2222 dport=46216 [ASSURED]

可以看到established的conntrack:

[root@node1 ~]# conntrack -L|grep 2222
conntrack v1.4.4 (conntrack-tools): 5 flow entries have been shown.
tcp      6 431857 ESTABLISHED src=10.0.2.4 dst=10.0.2.15 sport=46216 dport=2222 src=10.0.2.15 dst=10.0.2.4 sport=2222 dport=46216 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1

接下来,删除conntrack记录:

删除记录:
[root@node1 ~]# conntrack -D -p tcp --dport 2222
tcp      6 431800 ESTABLISHED src=10.0.2.4 dst=10.0.2.15 sport=46216 dport=2222 src=10.0.2.15 dst=10.0.2.4 sport=2222 dport=46216 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1
conntrack v1.4.4 (conntrack-tools): 1 flow entries have been deleted.
查询:
[root@node1 ~]# conntrack -L|grep 2222
conntrack v1.4.4 (conntrack-tools): 4 flow entries have been shown.

客户端使用该socket发起请求:

[root@node2 ~]# ./client 
please input:11
send result
: Success

可以看到,服务端2222,没回复,客户端一直重传:

数据库连接池长时间不用,乍一用还用不了,结果是防火墙的锅-LMLPHP

观察iptables,可以发现,确实匹配上了drop那条:

数据库连接池长时间不用,乍一用还用不了,结果是防火墙的锅-LMLPHP

参考

https://blog.csdn.net/fengcai_ke/article/details/125717134

https://blog.csdn.net/weixin_46768610/article/details/109354433

https://www.cnblogs.com/saolv/p/13096965.html

https://mp.weixin.qq.com/s/kv32lyWak4dCaPjMI4_6jw

https://mp.weixin.qq.com/s/bXWAd62R8-g0PyInB9MFIQ

https://mp.weixin.qq.com/s/vTys4GJH_tH5jGrDmR2etw

09-28 22:42