Iptables状态跟踪机制介绍和优化探讨
苏研大云人同时发文
前言
iptables是最常用的一种Linux主机防火墙,借助于netfilter优秀的性能和扩展,虽历经多年,但仍不落伍。OpenStack中安全组功能,floating IP的实现,以及fwaas的主流方案大多是依赖iptables而实现的。
与包过滤防火墙不同的是,iptables是一种状态防火墙,可以记录和跟踪数据流的状态。正是基于此,才会有以上优秀方案的落地。
从Linux2.6.15的内核版本后,iptables开始支持状态跟踪(conntrack),该功能依赖于netfilter的内核模块nf_conntrack。此后,iptables可以根据包的状态进行二次的过滤拦截和状态跟踪。它也是state/ctstate和nat的主要依赖模块。
状态跟踪
conntrack将数据流的状态信息以Hash表的形式储存在内存中,包括五元组信息以及超时时间等。这里说的状态跟踪并非是指状态协议(如TCP)中连接状态的跟踪,而是conntrack特有的与网络传输协议无关的状态的跟踪。
下面介绍一下conntrack中的状态分类以及在TCP和UDP这两种4层协议中,是如何进行状态跟踪的。
五种状态
conntrack共可以为连接标记五种状态,分别如下:
NEW:
新建连接请求的数据包,且该数据包没有和任何已有连接相关联。
判断的依据是conntrack当前“只看到一个方向数据包(UNREPLIED)”,没有回包。
ESTABLISHED:
该连接是某NEW状态连接的回包,也就是完成了连接的双向关联。
RELATED:
匹配那些属于helper模块定义的特殊协议的网络连接,该连接属于已经存在的一个ESTABLISHED连接的衍生连接。
简而言之,A连接已经是ESTABLISHED,而B连接如果与A连接相关,那么B连接就是RELATED。这部分不理解没有关系,也很难一句话说清,后面章节会用大量笔墨来阐明它。
INVALID:
匹配那些无法识别或没有任何状态的数据包。这可能是由于系统内存不足或收到不属于任何已知连接的ICMP错误消息,也就是垃圾包,一般情况下我们都会DROP此类状态的包。
UNTRACKED :
这是一种特殊状态,或者说并不是状态。它是管理员在raw表中,为连接设置NOTRACK规则后的状态。这样做,便于提高包过滤效率以及降低负载。
conntrack是一种状态跟踪和记录的机制,本身并不能过滤数据包,只是提供包过滤的依据。 有状态是一种过滤依据,无状态实际也是一种过滤依据。
因此,需要小心的是,设置了NOTRACK,不代表是放行,只是它的状态是UNTRACKED。所以如果想要对这种包放行或者处理,同样需要配置相应的规则。具体使用方法可以查看下面“设置NOTRACK”部分的内容。
TCP的状态跟踪
从TCP开始讨论的原因也是因为TCP本身是状态协议。TCP通过三次握手建立连接,分别是SYN,SYN/ACK和ACK,当完成之后连接成为ESTABLISHED状态。也就是说TCP完成ESTABLISHED的时候,C和S两端已经进行了三次交互。
对于iptables而言,每一次握手,都需要对连接过滤,如前面所说NEW和ESTABLISHED状态,分别指的是,当nf_conntrack第一次发现该连接的时候,会将其状态设置为NEW,当反方向也出现包的时候,即认为是ESTABLISHED。如下图,观察在TCP的三次握手过程(SYN,SYN/ACK,ACK)中,conntrack状态(NEW,ESTABLISHED)出现的时机。可以看到,第1次握手与第2次握手间是NEW,第2次握手之后就是ESTABLISHED。某种角度来说,可以认为两次握手对conntrack而言就已经完成了ESTABLISHED
讨论到这里,已经把本章节的两个主角(TCP和conntrack中的ESTABLISHED状态)请了出来,下面举个具体的例子,看下TCP状态的变化。
打开/proc/net/nf_conntrack,可以看到类似如下的内容。
先说下每行entry的意思,第1列是网络层协议名称;第2列是协议代号;第3列代表传输层协议名称;第4列是传输层协议代号,其中tcp是6,udp是17;第5列的117指的是TTL;第6列是TCP的状态;第7列是表示源目地址以及对应端口;第8列是表示是否收到回应包;第9列表示期望收到的回包的源目地址及端口。
ipv4 2 tcp 6 117 SYN_SENT src=192.168.1.5 dst=192.168.1.7 sport=1031 dport=23 [UNREPLIED] src=192.168.1.7 dst=192.168.1.5 sport=23 dport=1031 use=1
第1次握手时(SYN),TCP的状态是SYN_SENT,且有 [UNREPLIED] 标记,意味着还没有收到回包。此时,连接是NEW的状态,这点很好理解。
接着往下看
ipv4 2 tcp 6 57 SYN_RECV src=192.168.1.5 dst=192.168.1.7 sport=1031 dport=23 src=192.168.1.7 dst=192.168.1.5 sport=23 dport=1031 use=1
第2次握手时(SYN/ACK),TCP连接的状态为SYN_RECV,也就是说,kernel收到了回复(replay), 在初次握手时候的[UNREPLIED]的tag被去除。需要注意的是,此时conntrack已经是ESTABLISHED,再往下看
ipv4 2 tcp 6 431999 ESTABLISHED src=192.168.1.5 dst=192.168.1.7 sport=1031 dport=23 src=192.168.1.7 dst=192.168.1.5 sport=23 dport=1031 [ASSURED] use=1
第3次握手时(ACK),TCP连接的状态变为ESTABLISHED,也就是说,Client发出了ACK的确认标记包,此时三次握手完毕,TCP连接建立。
TCP总结: 对Client而言,TCP状态与conntrack状态对比是SYN_SENT(NEW),SYN_RECV(ESTABLISHED),ESTABLISHED(ESTABLISHED);
配置iptables规则
对于Client是192.168.1.5:1031,Server是192.168.1.7:23。需求是Client可以主动连接Server,而反向主动的连接不可以。Client上防火墙规则如下:
#iptables -A INPUT -s 192.168.1.7 -p tcp --dport 1031 --sport 23 -m conntrack --ctstate ESTABLISHED -j ACCEPT
#iptables -A OUTPUT -d 192.168.1.7 -p tcp --sport 1031 --dport 23 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
在这里顺便提及一下-m conntrack --ctstate与-m state --state的关系。
官方给的说法是这样:
The conntrack match is an extended version of the state match, which makes it possible to match packets in a much more granular way. It let’s you look at information directly available in the connection tracking system, without any “frontend” systems, such as in the state match.
大意是ctstate是state的扩展版本(内核版本>=2.5开始支持),包括状态参数也是基本相同,不用过多纠结,不过既然出了新的写法,个人还是推荐新的写法和规则。
UDP的状态跟踪
UDP是无状态的传输协议,不需要三次握手也没有SYN和ACK等各种标签和状态。虽然没有三次握手的概念,但是我们还是来看三次连接的状态记录。
如下图,观察在UDP连接过程中,conntrack状态(NEW,ESTABLISHED)出现的时机。可以看到,第1次连接与第2次连接间是NEW的状态,第2次连接之后就是ESTABLISHED。对UDP而言,也可以认为两次握手对conntrack而言就已经完成了ESTABLISHED
ipv4 2 udp 17 20 src=192.168.1.2 dst=192.168.1.5 sport=137 dport=1025 [UNREPLIED] src=192.168.1.5 dst=192.168.1.2 sport=1025 dport=137 use=1
首先可以看到的是UDP没有SYN_SENT这种TCP特有的状态标签,但是有 [UNREPLIED] 的标识,说明这个包是初次发出的包,还没有收到回应。此时conntrack中对应的状态是NEW,接着往下看。
ipv4 2 udp 17 170 src=192.168.1.2 dst=192.168.1.5 sport=137 dport=1025 src=192.168.1.5 dst=192.168.1.2 sport=1025 dport=137 [ASSURED] use=1
同样还是没有标识,但是 [UNREPLIED] 变成了 [ASSURED] ,表明已经收到了回包,且连接建立完成。另外TTL变成了170,这时由于在该连接状态下,默认的TTL是180,而第一次连接时默认值是30。此时conntrack中定义的状态是ESTABLISHED。接着往下看
ipv4 2 udp 17 175 src=192.168.1.5 dst=195.22.79.2 sport=1025 dport=53 src=195.22.79.2 dst=192.168.1.5 sport=53 dport=1025 [ASSURED] use=1
Client发出第3次包之后,可以发现TTL变为了175,说明TTL更新了,也同时说明第二次和第三次中包的状态或者说标签是一致的,所以TTL默认值才会相同。
UDP总结: 对Client而言,UDP过程与conntrack状态对比是,第一次连接(NEW),第二次连接(ESTABLISHED);
配置iptables规则
对于Client是192.168.1.5:1031,Server是192.168.1.7:23。需求是Client可以主动连接Server,而反向主动的连接不可以。Client上iptables规则如下:
#Client
#iptables -A INPUT -s 192.168.1.7 -p udp --dport 1031 --sport 23 -m conntrack --ctstate ESTABLISHED -j ACCEPT
#iptables -A OUTPUT -d 192.168.1.7 -p udp --sport 1031 --dport 23 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
回头再看
通过对以上知识的整理,iptables的状态跟踪(conntrack)思想若隐若现。
网络防火墙更关心的“进”和“出”,他有自己的考虑和规则,至于进出的包依循的是什么,他并不关心。
当连接初次出现的时候,该连接就是NEW,当出现了对应的反向连接的时候,那么该连接就是ESTABLISHED。看起来有点像UDP,是不是?
至于为什么会设计成这种模式,可能是考虑到防火墙只是涉及进出两个方向,而两次握手已经可以代表两次的方向,也可能考虑到对UDP以及ICMP的兼容等问题,在这就不去深入讨论了。
helper扩展
如前所述,iptables不关心进出包所使用的协议,只是适配符合OSI七层模型设计的协议。但是某些协议比较特殊,它们不遵循OSI的设计,这些协议包括FTP,SIP,H.323等。
以FTP为例,在其被动模式下,使用21号端口作为控制端口,使用1024~65536的随机端口被动接受Client连接,作为数据传输端口,因此数据传输时其dport无法预知。
为了解决这种问题,在conntrack的基础上,扩展出了helper机制,以确定某个连接是否与已经存在的某个ESTABLISHED连接相关(related)。nf_conntrack_core.h在nf_conntrack_expect结构体中定义了expectation(期望)的概念。指的是,将来某段时间,可能会出现的具有某种属性和特点的连接。
以FTP为例,其管理端口是21,那么其数据端口就是与21端口连接相关的连接。借助于helper,conntrack将这种状态定义为RELATED。由于这些模块是扩展模块,因此conntrack默认并没有加载,需要额外加载如支持FTP的nf_conntrack_ftp,支持SIP的nf_conntrack_sip,支持RAS的nf_conntrack_h323等。
特殊的FTP
FTP是一种文件传输协议,包括主动(PORT)模式和被动(PASV)模式。两种模式下,其服务都开放两种端口,分别是控制端口和数据端口。
在被动模式时,使用21号端口作为控制端口,使用1024~65536的随机端口作为其数据传输端口接受被动连接;在主动模式时,使用21号端口作为控制端口,使用20号端口作为数据传输端口主动连接Client的任意1024-65535的端口。如果按照一般协议来处理,被动模式时,需要放行1024-65535的INPUT流量,主动模式时,需要放行1024-65535的OUTPUT流量。这样很明显很危险。
那么对于FTP的服务器而言,他的规则应该配成什么样的?举例说明之,
如果FTP的server是192.168.0.103,Client是192.168.0.105。下面是分别在两种模式时,在FTP server上的iptables的规则。
主动模式的iptables规则
#iptables -A INPUT -s 192.168.0.105 -p tcp --dport 20 -m conntrack --ctstate ESTABLISHED -j ACCEPT
#iptables -A OUTPUT -d 192.168.0.105 -p tcp --sport 20 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
被动模式的iptables规则
#iptables -A INPUT -s 192.168.0.105 -p tcp --dport 1024: -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
#iptables -A OUTPUT -d 192.168.0.105 -p tcp --sport 1024: -m conntrack --ctstate ESTABLISHED -j ACCEPT
conntrack参数和配置优化
正如前面所说,nf_conntrack的状态记录是写在内存中的,比较耗费内存资源,因此开启了nf_conntrack之后,当分配给conntrack的资源表耗尽,会报如下错误:
kernel: nf_conntrack: table full, dropping packet.
说明当前服务器的conntrack_max和conntrack_buckets值已经小于当前负载需要。
使用如下命令查看当前连接的hash表数,以及最大连接数。
查看当前系统的conntrack_max和conntrack_buckets值
[root@iptables ~]# dmesg | grep conntrack
[ 120.182019] nf_conntrack version 0.5.0 (7796 buckets, 31184 max)
也可以使用如下命令,分别查看当前机器的conntrack_max和conntrack_buckets:
[root@iptables ~]# cat /proc/sys/net/netfilter/nf_conntrack_max
[root@iptables ~]# cat /proc/sys/net/netfilter/nf_conntrack_buckets
最大连接数(conntrack_max)是conntrack_bucket的4倍,这是由于每条hash表记录占4个字节。这两个值的默认值是根据当前机器的内存(不包括swap)计算出来的。参考以下公式:
# CONNTRACK_MAX = RAMSIZE (in bytes) / 16384 / (ARCH / 32)
# (ARCH为机器CPU的架构,64或32)
使用如下命令,查看当前有多少连接被nf_conntrack跟踪:
[root@iptables proc]# cat /proc/sys/net/netfilter/nf_conntrack_count
一般使用3种方式进行性能调优,包括修改配置参数,设置NOTRACK,以及卸载nf_conntrack模块
修改配置参数
#增大conntrack_max 以及 conntrack_buckets 的值:
# echo 100000 > /proc/sys/net/netfilter/nf_conntrack_max
# echo 25000 > /proc/sys/net/netfilter/nf_conntrack_buckets
#缩短timeout的值,默认是43200秒:
# echo 600 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
设置NOTRACK
iptables 中的 raw 表是优先级最高的表,因此其可以设置一些预先的规则。通过设置NOTRACK 给不需要被跟踪的连接打标记, nf_conntrack 就不会跟踪该连接,在/proc/net/nf_conntrack中就看不到该连接。下面举个例子:
对sip为192.168.0.105,想要访问本机ssh的连接,要求在本机仅不对该连接进行状态跟踪,并且放行该连接的进出流量:
#首先是将进出的流量都打上NOTRACK的tag,在raw表中设置NOTRACK标记
#iptables -t raw -A PREROUTING -s 192.168.0.105 -p tcp --dport 22 -j NOTRACK
#iptables -t raw -A OUTPUT -d 192.168.0.105 -p tcp --sport 22 -j NOTRACK
#然后在filter表中,对UNTRACKED的流量放行
#iptables -A INPUT -s 192.168.0.105 -p tcp --dport 22 -m conntrack --ctstate UNTRACKED -j ACCEPT
#iptables -A OUTPUT -d 192.168.0.105 -p tcp --sport 22 -m conntrack --ctstate UNTRACKED -j ACCEPT
卸载nf_conntrack模块
首先需要将iptables中所有表中使用state和ctstate的规则全部删除并清空nat表。
#使用如下命令清空全部表规则:
#iptables -F -t table_name
#iptables -X -t table_name
#查看nf_conntrack模块的使用情况
#lsmod | grep nf_conntrack
#然后使用如下命令进行卸载
#modprobe -r nf_conntrack
如果在卸载的时候提示模块在使用,就需要把依赖的模块一一卸载。
不推荐卸载nf_conntrack模块,只要iptables还有规则用到nat和state/ctstate,就不适合关掉该模块,否则这些规则会失效。
总结
conntrack的状态跟踪与传输协议本身无关,仅考虑连接是否新产生,连接是否有响应,适配一般的传输协议。helper的扩展,定义了“期望”的概念,让conntrack可以在之前的基础上,兼容对不符合ISO设计的特殊传输协议的按需扩展。但是,conntrack对状态跟踪的背后,是操作系统资源的消耗,因此在实际生产中,对于系统资源有限且无需开启状态跟踪的时候,可以适当考虑对conntrack的优化。
参考
1. Secure use of iptables and connection tracking helpers
2. Iptables Tutorial 1.2.2
3. RFC of TCP
4. iptables深入解析:ct篇
5. iptables man page
6. patch of nf_ct_helper: allow to disable automatic helper assignment