iptables学习笔记

前言

本文是《更安全的Linux网络》一书的阅读学习笔记。

最近在研究Docker和Snort时,发现两者都要求iptables的背景知识。基于BFS、DFS以及拓扑排序的原则,我决定先研究一下iptables。

在网络上搜索iptables的教程时,有幸找到陈勇勋先生这本大作。该书出版时间较早,如今已经绝版。

实验环境说明:

uname -r
3.10.0-862.14.4.el7.x86_64

cat /etc/redhat-release 
CentOS Linux release 7.5.1804 (Core) 

iptables --version
iptables v1.4.21

ifconfig | grep 172
        inet 172.16.56.138  netmask 255.255.255.0  broadcast 172.16.56.255

用来与CentOS通信的机器地址为172.16.56.1

CentOS 7 提供了iptables和firewalld两种操作内核Netfilter的接口,默认使用的是firewalld。事实上,firewalld的底层依然调用了iptables命令行,但是firewalld本身不兼容iptables语法(我们不能使用iptables规则去操作firewalld)。这里我们研究iptables,所以需要关闭firewalld,开启iptables:

# stop firewalld
systemctl stop firewalld
systemctl disable firewalld
# install iptables services
yum install -y iptables-services iptables-devel.x86_64 iptables.x86_64
# start iptables
systemctl enable iptables
systemctl start iptables
# check status
systemctl status iptables

为了避免SELinux对实验结果的影响,请先暂时关闭它:

setenforce 0

用以下命令开启数据包转发:

echo "1" > /proc/sys/net/ipv4/ip_forward

第一部分:防火墙的基本概念

1.1 基础知识

要学习防火墙,网络知识必不可少。具体到这里,就是以太网和TCP/IP协议蔟相关的背景知识。

在学习本章时,我发现过去自己对TCP连接的理解有一个问题:过去写C/S模式的程序时,把socket和port的概念弄混了。S端在accept时会返回一个新的socket文件描述符,但是并没有占用新的端口。新启用的socket和原来的监听socket使用同一个端口。其实socket只是TCP/IP协议的一种实现,而端口的目的仅仅为了帮助系统理解进程与数据包之间的对应关系。至于一个进程内部怎么去利用这个端口,怎么去处理这个端口的数据包,操作系统是不管的。

OK,言归正传。

cat /etc/services

上述文件给出了协议规定的部分端口与服务之间的对应关系。

简单来说,防火墙的主要任务是“根据匹配条件,放行合法数据包,过滤非法数据包”。“条件”是防火墙的重中之重,各种条件构成一系列的规则,这些规则才是真正的守夜人。

我们可以把条件划分为三大类:

  • 各层封包包头信息:如以太网连接层的src/dst-MAC,网络层的IP地址等
  • 封包内的payload信息:如应用层HTTP协议中指定的访问目标“www.example.com”字符串等
  • 连接状态:这是宏观概念,例如允许内网数据包流出,不允许外网数据包流入等

关于“连接状态”,这里要补充一点:如果我们希望内网主机能够访问外网服务,但是限制外网数据包进入内网,在这样的场景下,比如内网host去访问www.baidu.com:80,由于通信是双向的,势必会有外网数据包进入内网的需求,但是它们似乎会被防火墙挡住。其实不是的。防火墙提供了“连接状态判别”机制,会对这种情况下的流入数据包放行。详细内容在后面。

1.2 防火墙分类

前面讲过条件分类,那么防火墙有那些分类呢?

按照过滤技术区分:

  • 包过滤防火墙:能够检查的最小单位为“一个封包”,成本不高
  • 应用层防火墙:可以检查应用层payload的每个字节,成本较高

按照网络拓扑结构区分:

  • 单机式防火墙

早期一些企业的网络架构可能如下图所示:

虽然安装了单机防火墙,但任何来自互联网的攻击行为都由Mail和Web服务器自行承担,风险较大。

  • 网关式防火墙

根据拓扑细节不同,主要有三种形式:

形式1中,Mail与Web服务器被放在内网,这很危险,它们一旦被攻击成功将直接导致内网沦陷。

形式2在形式1的基础上做了改进,加入了我们熟知的DMZ,安全性有很大提高。在这种结构下,我们还可以做一点改进,就是将服务器放入企业内网,然后在DMZ放置一个反向代理:

当然,这样做将使开销增大。

形式3与形式2基本相同,区别是构建了多层次内网。在这种形势下需要注意的是,使用不同厂商的防火墙产品,以实现最佳防御效果。

  • 通透式防火墙

网关防火墙本身就是一个路由器,它的加入使得原拓扑环境下的IP配置需要重新调整。通透式防火墙则工作在OSI第二层,相当于“Bridge+Filter”,它既不需要IP地址,也不会引入路由问题:

第二部分:基础篇

本部分介绍Linux防火墙的基本结构及使用方法。

本节导图:

2.1 Netfilter的文件结构

Netfilter是第三代Linux防火墙,在它之前有IpfwadmIp chains,但Netfilter与Linux本身是两个相互独立的组织。

Netfilter以模块形式存在于Linux内核中:

+-------------------------+
|       Application       |
+-------------------------+
|         Kernel          |
|  +-> (Netfilter) ->-+   |
+--|------------------|---+
|  |     Hardware     |   |
| eth0               eth1 |
+--|------------------|---+
in |                  | out
-->+                  +--->

在2.6.14版本的内核之前,针对IPv4和IPv6的模块被分别放在以下路径:

/lib/modules/`uname -r`/kernel/net/ipv4/netfilter
/lib/modules/`uname -r`/kernel/net/ipv6/netfilter

从2.6.14开始,它们被逐渐整合成协议无关的模块,并放在以下路径:

/lib/modules/`uname -r`/kernel/net/netfilter

模块举例如下:

ls /lib/modules/`uname -r`/kernel/net/netfilter
ipset                          nft_log.ko.xz          xt_LED.ko.xz
ipvs                           nft_masq.ko.xz         xt_length.ko.xz
nf_conntrack_amanda.ko.xz      nft_meta.ko.xz         xt_limit.ko.xz
nf_conntrack_broadcast.ko.xz   nft_nat.ko.xz          xt_LOG.ko.xz
nf_conntrack_ftp.ko.xz         nft_queue.ko.xz        xt_mac.ko.xz
...

2.2 Netfilter的逻辑结构

上面讲到的不同模块将会提供不同的匹配过滤功能,但具体要不要过滤、怎么过滤,需要我们为Netfilter制定符合目的的规则(与用户交互并接受规则的是iptables,后面会讲到)。这些规则会被装填到一块结构性内存中四个不同的表内:Filter、NAT、Mangle和RAW(每个表有不同的规则链),这正是Netfilter提供的四大功能:

Table Chain Table Chain
Filter INPUT Mangle PREROUTING
- FORWARD - INPUT
- OUTPUT - FORWARD
NAT PREROUTING - OUTPUT
- INPUT - POSTROUTING
- OUTPUT RAW PREROUTING
- POSTROUTING - OUTPUT

它们的用途简述如下:

  • Filter:封包过滤
  • NAT:顾名思义
  • Mangle:修改封包内容
  • RAW:为封包穿越防火墙加速,提升防火墙性能

2.3 Netfilter的Filter机制

毋庸置疑,Filter是Netfilter最重要的功能。该机制对于封包有如下分类:

  • INPUT类:指网络上其他主机发给本机进程的封包
  • OUTPUT类:指本机进程发给其他主机的封包
  • FORWARD类:指网络上其他主机发给其他主机的封包,本机在其中扮演路由器角色

Filter表包含与分类同名的三条规则链,其中的规则将作用于对应类型的封包:

INPUT Chain FORWARD Chain OUTPUT Chain
rule 1 rule 1 rule 1
rule 2 rule 2 rule 2
... ... ...
Default Policy Default Policy Default Policy

而规则本身正是需要我们去设计的。举例来说,如果我们要限制本机访问外部的Web服务,那么可以向OUTPUT链中写入一条”丢弃本机进程产生的、目的地为XXX-URL、协议为TCP且端口为80的封包“。

Filter机制的逻辑流程如下:

Packet-IN ---> ROUTING-TABLE ---> FORWARD-CHAIN --+
                    |                             |
                    V                             |
                INPUT-CHAIN   +-> ROUTING-TABLE   |
                    |         |        |          |
                    V         |        V          |
               LOCAL-PROCESS -+   OUTPUT-CHAIN    |
                                       |          |
                                       V          |
Packet-OUT <--------------------------------------+

上图中的ROUTING-TABLE实际上是同一个。对上述流程做如下解释:

  • 进入的封包先由路由表判断是INPUT还是FORWARD类型,接着被分派给不同的规则链处理。若INPUT链放行,则封包被交给本机进程,否则丢弃;若FOWARD链放行,则封包从另一个网络接口被送出,否则丢弃
  • 本机发出的封包会先经过路由表决定路由,然后被OUTPUT链处理,若放行则从路由表指定的接口被送出,否则丢弃

那么,具体的规则匹配方式是怎样的呢?

举例来说,当我们向INPUT链添加规则时,先添加规则被放在前面,后添加规则被放在后面(队列结构)。Netfilter采用优先匹配原则,从链头到链尾依次对封包应用规则,一旦某规则生效,不再进行后续匹配。如果我们添加的所有规则都没能匹配成功,则链尾的Default Policy将作用于封包。该缺省规则永远位于链尾,不同链的缺省规则相互独立。缺省规则可以是两个值之一:ACCEPTDROP

2.4 Netfilter与Iptables

前面有提到Iptables。准确来说,它是“规则编辑工具”,用来对内存中的规则进行增删改查。针对IPv4和IPv6,分别有iptablesip6tables两个工具。

Linux防火墙的完整称呼应该是“Netfilter/Iptables”。与Netfilter类似,Iptables也是模块化存储的。在我的环境中,模块存储路径为/usr/lib64/xtables

ls /usr/lib64/xtables
libip6t_ah.so          libipt_unclean.so     libxt_nfacct.so
libip6t_DNAT.so        libxt_addrtype.so     libxt_NFLOG.so
libip6t_DNPT.so        libxt_AUDIT.so        libxt_NFQUEUE.so
libip6t_dst.so         libxt_bpf.so          libxt_NOTRACK.so
libip6t_eui64.so       libxt_cgroup.so       libxt_osf.so
libip6t_frag.so        libxt_CHECKSUM.so     libxt_owner.so
...

Iptables的模块与Netfilter的模块往往是一对一地存在。例如,在Netfilter模块目录中有xt_string.ko.xz,对应地,在Iptables路径下则有libxt_string.so。当我们下达与xt_string.ko有关的规则时,Iptables将依赖libxt_string.so去检查语法正确性,并加载xt_string.ko模块,最后写入规则到内存。因此,应该同时升级Netfilter与Iptables。

Netfilter官方网站对两者的描述如下:

netfilter is a set of hooks inside the Linux kernel that allows kernel modules to register callback functions with the network stack. A registered callback function is then called back for every packet that traverses the respective hook within the network stack.
iptables is a generic table structure for the definition of rulesets. Each rule within an IP table consists of a number of classifiers (iptables matches) and one connected action (iptables target).
netfilter, ip_tables, connection tracking (ip_conntrack, nf_conntrack) and the NAT subsystem together build the major parts of the framework.

2.5 Iptables的使用方法

我们来分析一下Iptables的命令结构。输入iptables -h,得到:

iptables v1.4.21

Usage: iptables -[ACD] chain rule-specification [options]
       iptables -I chain [rulenum] rule-specification [options]
       ...

Commands:
Either long or short options are allowed.
  --append  -A chain		Append to chain
  ...

Options:
    --ipv4	-4		Nothing (line is ignored by ip6tables-restore)
    --table	-t      table	table to manipulate (default: `filter')
    ...

可以发现,Iptables命令基本由“Command”与“Option”两部分构成。

我们可以借助思维导图来整理重要参数:

一些参数具有缺省值,如-t的缺省值为filter。而右上方的操作,无外乎增删改查。

另外,我们先给出三个规则示例,以方便测试。暂不去理会其内涵,后面再详述:

-p tcp -j ACCEPT
-p udp -j ACCEPT
-p icmp -j ACCEPT

现在,我们整合上述信息,执行一些iptables操作:

iptables -t filter -L

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     all  --  anywhere             anywhere             state RELATED,ESTABLISHED
ACCEPT     icmp --  anywhere             anywhere            
ACCEPT     all  --  anywhere             anywhere            
ACCEPT     tcp  --  anywhere             anywhere             state NEW tcp dpt:ssh
REJECT     all  --  anywhere             anywhere             reject-with icmp-host-prohibited

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         
REJECT     all  --  anywhere             anywhere             reject-with icmp-host-prohibited

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
iptables -t filter -L FORWARD

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         
REJECT     all  --  anywhere             anywhere             reject-with icmp-host-prohibited

上述规则都很好理解。

来做一次清空操作,并添加一个新的INPUT规则:

iptables -t filter -F
iptables -t filter -A INPUT -p icmp -j ACCEPT
iptables -t filter -L

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     icmp --  anywhere             anywhere            

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination    

将FORWARD的缺省规则设为DROP:

iptables -t filter -P FORWARD DROP
iptables -t filter -L FORWARD

Chain FORWARD (policy DROP)
target     prot opt source               destination   

注意,-F删除操作不会影响缺省规则。

其他参数可以自行组合尝试,很简单:

iptables -t filter -I INPUT 2 -p tcp -j ACCEPT
iptables -t filter -R INPUT 2 -p tcp -j ACCEPT
iptables -t filter -D INPUT 2

我们可以将所学迁移到别的表中:

iptables -t nat -L

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination    

2.6 Iptables的语法

在简单尝试后,我们来看一下iptables的语法。它可以分为“基本语法”与“高级语法”,区别为是否使用了基本模块iptable_filter.ko之外的模块的功能(在我的环境中,这个基本模块目前还是位于IPv4的目录:net/ipv4/netfilter/iptable_filter.ko.xz)。

下面分别是一个基本语法和一个高级语法的例子:

iptables -t filter -A INPUT -p icmp -j DROP
iptables -t filter -A INPUT -m mac --mac-source 00:E0:18:00:7C:A4 -j DROP

不同模块的语法会有一些差异。后面我们通过示例介绍基本语法。对于容易理解的语法,将不再介绍其内涵。

例1:实验

iptables -A INPUT -p icmp -s 172.16.56.1 -j DROP

我们先清空规则表,然后让172.16.56.1去不中断地ping它,再执行上述命令,发现ping发生变化:

ping 172.16.56.138
PING 172.16.56.138 (172.16.56.138): 56 data bytes
64 bytes from 172.16.56.138: icmp_seq=0 ttl=64 time=0.726 ms
...
64 bytes from 172.16.56.138: icmp_seq=15 ttl=64 time=0.528 ms
64 bytes from 172.16.56.138: icmp_seq=16 ttl=64 time=0.601 ms
64 bytes from 172.16.56.138: icmp_seq=17 ttl=64 time=0.321 ms
64 bytes from 172.16.56.138: icmp_seq=18 ttl=64 time=0.608 ms
Request timeout for icmp_seq 19
Request timeout for icmp_seq 20
Request timeout for icmp_seq 21
^C
--- 172.16.56.138 ping statistics ---
23 packets transmitted, 19 packets received, 17.4% packet loss
round-trip min/avg/max/stddev = 0.321/0.593/0.827/0.138 ms

例1:延伸

  • -p icmp -p tcp -p udp -p all
  • -s 172.16.56.0/24 -d www.baidu.com
  • -j ACCEPT -j DROP(丢弃封包且不给src回应) -j REJECT(丢弃封包并返回dst不可达的ICMP报文给src)

使用REJECT的效果:

ping 172.16.56.138
PING 172.16.56.138 (172.16.56.138): 56 data bytes
92 bytes from 172.16.56.138: Destination Port Unreachable
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 9ca7   0 0000  40  01 1556 172.16.56.1  172.16.56.138

例2:实验

iptables -A INPUT -p udp -s 172.16.56.1 --dport 10000 -j REJECT

先清空规则列表,然后在138上监听:

nc -u -l 10000

去尝试通信,接着输入规则,结果如下:

ncat -u 172.16.56.138 10000
hello
hello
Ncat: Connection refused.

例2:延伸

  • --dport --sport (指定端口前要-p指定协议)

例3

iptables -A INPUT -p tcp -s 172.16.56.1 --dport 23 -j ACCEPT
iptables -A INPUT -p all -s 172.16.56.0/24 -d 172.16.56.138 -j ACCEPT

iptables -A INPUT -p tcp -i ens33 --dport 22 -j ACCEPT

例3:延伸

  • -i -o 匹配封包出入口

例4:实验

借助!来对限制条件取反:

iptables -A OUTPUT -o ens33 -p tcp ! -d www.hao123.com --dport 80 -j REJECT

执行后,可以看一下结果:

wget www.hao123.com &> log.dat
cat log.dat | grep "awaiting response" | tail -n 1
HTTP request sent, awaiting response... 200 OK

wget www.baidu.com &> log.dat
cat log.dat 
--2018-12-19 16:41:53--  http://www.baidu.com/
Resolving www.baidu.com (www.baidu.com)... 115.239.210.27, 115.239.211.112
Connecting to www.baidu.com (www.baidu.com)|115.239.210.27|:80... failed: Connection refused.
Connecting to www.baidu.com (www.baidu.com)|115.239.211.112|:80... failed: Connection refused.

将上例稍稍改变一下,就可以作为网关,限制内网主机访问外部其他网站:

iptables -A FORWARD -i eth1 -o ens33 -p tcp ! -d www.hao123.com --dport 80 -j REJECT

例4:延伸

  • !放在哪是个有趣的问题。一开始我和书中一样,放在-dwww.hao123.com之间,但这样会报错

2.7 借助Filter机制构建单机式防火墙

从本节开始,我们来尝试给出规则去构建一系列的防火墙。

构建防火墙的步骤:

  1. 列出所有要求
  2. 根据要求生成规则

构建防火墙的原则:

“先拒绝所有的连接,再逐一开放对外提供的服务。”

网络拓扑如下:

Net: 172.16.56.0/24
PC1: 172.16.56.1
PC2: 172.16.56.164
PC3: 172.16.56.138

在PC3上构建单机防火墙,要求:

  • 网段下任何主机都可访问PC3上除SSH以外的服务
  • 网段下只有PC1可访问PC3上的所有服务(25/80/110端口)

按照前述的原则与要求,我们构建以下规则:

iptables -P INPUT DROP
iptables -A INPUT -p tcp -s 172.16.56.0/24 -d 172.16.56.138 --dport 25 -j ACCEPT
iptables -A INPUT -p tcp -s 172.16.56.0/24 -d 172.16.56.138 --dport 80 -j ACCEPT
iptables -A INPUT -p tcp -s 172.16.56.0/24 -d 172.16.56.138 --dport 110 -j ACCEPT
iptables -A INPUT -p tcp -s 172.16.56.1 -d 172.16.56.138 --dport 22 -j ACCEPT

现在我们看一下执行结果:

iptables -L INPUT
Chain INPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     tcp  --  172.16.56.0/24       localhost.localdomain  tcp dpt:smtp
ACCEPT     tcp  --  172.16.56.0/24       localhost.localdomain  tcp dpt:http
ACCEPT     tcp  --  172.16.56.0/24       localhost.localdomain  tcp dpt:pop3
ACCEPT     tcp  --  172.16.56.1          localhost.localdomain  tcp dpt:ssh

测试一下:

# 172.16.56.1
ssh centos@172.16.56.138 -o ConnectTimeout=5
centos@172.16.56.138's password:
Last login: Thu Dec 20 10:31:00 2018 from 172.16.56.1
[centos@localhost ~]$

# 172.16.56.164
ssh centos@172.16.56.138 -o ConnectTimeout=5
ssh: connect to host 172.16.56.138 port 22: Connection timed out

注意,上面的测试是不完全的。真正在部署防火墙时,要基于逻辑来对所有条件分支进行覆盖性测试。

现在来讨论四个问题:

一、为什么iptables -L有时会变得很慢?

这是因为iptables默认会对IP和端口进行反向解析。可以通过iptables -L -n禁止解析来加快显示。

二、PC1用SSH连接PC3时,虽然连接成功,但耗时比过去长不少,这是为什么?

该问题涉及到TCP的连接状态,我将在下一个问题中解答。

三、在现有规则下,PC3能够用SSH去连接其他主机的SSH服务吗?

不可以。例如,我们尝试访问PC2上的sshd:

ssh seed@172.16.56.164 -o ConnectTimeout=5
ssh: connect to host 172.16.56.164 port 22: Connection timed out

其实道理很简单,发出的包经过OUTPUT的缺省规则ACCEPT,能够抵达PC2,但PC2返回的包却被挡在防火墙外。我们可以借助xt_state.ko模块(该模块在iptables中名为state)提供的“连接追踪”功能,在不设置新的放行规则的条件下达到上述目的。

下面,我们先对state模块做简要了解。

在标准TCP/IP描述中,连接状态分为12种,而state模块的描述只有4种(当然,它们是完全不相干的两种定义方式):

  • ESTABLISHED
  • TCP:第一个发出的封包为服务请求封包,如果该封包能够顺利通过防火墙,那么后续的所有来往封包均为此状态
  • UDP:与TCP情况类似
  • ICMP:与TCP情况类似
  • NEW:与协议无关,描述每一条连接中的第一个封包
  • RELATED:描述被动产生的响应封包,它不属于现有任何链接;路由器在收到traceroute发出的TTL刚好减为0的封包时,将返回一个Time to live exceeded封包,它就是RELATED状态(同样与协议无关,只要回应回来的封包是因为本机先送出一个封包导致另一连接的产生,那么这一条新连接上的所有封包都属于RELATED状态封包)
  • INVALID:描述不属于其他三个状态的封包

为了安全起见,我们应该将下面这条规则放在链首:

iptables -A INPUT -p all -m state --state INVALID -j DROP

现在我们来回答问题二。当PC1连接PC3时,PC3的sshd服务将PC1的IP发给DNS服务器做反向解析,然而从DNS服务器发回的返回包将遇到问题三同样的情况,所以sshd无法得到DNS响应,将不断重试,直到timeout。因此问题二与问题三的实质相同,都可以借助state模块去判别ESTABLISHED状态来解决。

开始实验:

iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT

我们再次用PC3去SSH连接PC2,成功:

ssh seed@172.16.56.164 -o ConnectTimeout=5
The authenticity of host '172.16.56.164 (172.16.56.164)' can't be established.
ECDSA key fingerprint is SHA256:p1zAio6c1bI+8HDp5xa+eKRi561aFDaPE1/xq1eYzCI.
ECDSA key fingerprint is MD5:37:61:b8:e9:07:af:1c:f1:6a:49:94:ea:de:19:cf:b4.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '172.16.56.164' (ECDSA) to the list of known hosts.
seed@172.16.56.164's password: 
Welcome to Ubuntu 16.04.2 LTS (GNU/Linux 4.8.0-36-generic i686)

Last login: Wed Dec 19 22:48:53 2018 from 172.16.56.1
[12/20/18]seed@VM:~$ 

事实上,该规则加到规则链第几行也是有讲究的。这里先加到最后就好,后面再详细介绍。

四、该如何管理这些规则?

我们通过命令行制定的规则在系统重启后会消失。可以借助命令来保存规则:

service iptables save

但为了后期维护方便,我们最好不要用这种方式。可以将规则写成Shell脚本,配置正确的读写执行权限,然后加入开机启动就好。下面是一个例子:

#!/bin/bash
# Set Variable
IPT=/sbin/iptables
SERVER=172.16.56.138
PARTNER=172.16.56.1
NETWORK=172.16.56.0/24
# Clear Original Rule
iptables -t filter -F
# Set INPUT Rule
$IPT -P INPUT DROP
$IPT -A INPUT -p all -m state --state INVALID -j DROP
$IPT -A INPUT -p tcp -s $NETWORK -d $SERVER --dport 25 -j ACCEPT
$IPT -A INPUT -p tcp -s $NETWORK -d $SERVER --dport 80 -j ACCEPT
$IPT -A INPUT -p tcp -s $NETWORK -d $SERVER --dport 110 -j ACCEPT
$IPT -A INPUT -p tcp -s $PARTNER -d $SERVER --dport 22 -j ACCEPT
$IPT -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

2.8 借助Filter机制构建网关式防火墙

下面分别给出网络拓扑、防火墙要求和防火墙脚本,由于尚未介绍NAT机制,我们假设内网IP为公网IP而不必做NAT转换。

网络拓扑:

NET1: 192.168.0.0/24
Router/Firewall:
    eth0: 10.0.1.200
    eth1: 192.168.0.1
PC1: 192.168.0.200
PC2: 192.168.0.100
PC3: 10.0.1.100

防火墙要求:

  • PC1只能访问PC3的SMTP与POP3服务
  • NET1网段内其他主机只能访问Internet的DNS/SMTP/POP3/HTTP/HTTPS服务
  • Internet主机不得访问NET1内任何主机

防火墙脚本:

#!/bin/bash
# Set Variable
IPT=/sbin/iptables
MAIL_SRV=10.0.1.100
MAIL_PORT=25,110
INTERNET_TCP_PORT=25,110,80,443
INTERNET_UDP_PORT=53
ACC_PC=192.168.0.200
# Clear Original Rule
iptables -t filter -F
# Set Default Policy
$IPT -P INPUT DROP
$IPT -P FORWARD DROP
# Set INPUT Rule
$IPT -A INPUT -p all -m state --state INVALID -j DROP
$IPT -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Set Forward Rule
$IPT -A FORWARD -i eth0 -o eth1 -m state --state INVALID -j DROP
$IPT -A FORWARD -i eth0 -o eth1 -m state --state \
    ESTABLISHED,RELATED -j ACCEPT
$IPT -A FORWARD -i eth1 -o eth0 -m multiport \
    -p tcp -s $ACC_PC -d $MAIL_SRV --dport $MAIL_PORT -j ACCEPT
$IPT -A FORWARD -i eth1 -o eth0 -p all -s $ACC_PC -j DROP
$IPT -A FORWARD -i eth1 -o eth0 -m multiport \
    -p tcp --dport $INTERNET_TCP_PORT -j ACCEPT
$IPT -A FORWARD -i eth1 -o eth0 -m multiport \
    -p udp --dport $INTERNET_UDP_PORT -j ACCEPT

看一下结果:

Chain INPUT (policy DROP)
target     prot opt source               destination         
DROP       all  --  anywhere             anywhere             state INVALID
ACCEPT     all  --  anywhere             anywhere             state RELATED,ESTABLISHED

Chain FORWARD (policy DROP)
target     prot opt source               destination         
DROP       all  --  anywhere             anywhere             state INVALID
ACCEPT     all  --  anywhere             anywhere             state RELATED,ESTABLISHED
ACCEPT     tcp  --  192.168.0.200        10.0.1.100           multiport dports smtp,pop3
DROP       all  --  192.168.0.200        anywhere            
ACCEPT     tcp  --  anywhere             anywhere             multiport dports smtp,pop3,http,https
ACCEPT     udp  --  anywhere             anywhere             multiport dports domain

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

我们在上述脚本中使用了multiport模块来指定不连续的多个端口,因为默认不使用模块的情况下我们只能以类似于25:110的形式指定从25到110的连续端口。

2.9 Netfilter的NAT机制

NAT可以应用在C端和S端,作用分别是“保护C端主机/节约公网IP”和“保护S端主机”。另外,根据拓扑结构可以将其分为四种:一对多、多对多、一对一及NAPT。本节我们对这一重要的机制进行研究学习。

由于IPv4地址资源枯竭,我们往往采用多个内网IP对应一个公网IP的网络架构。“不使用NAT、只是简单地将公网IP配置给网关路由”的方案不可行:内网发出的封包经过路由器到达公网,其src依然为私有IP;由于公网路由器通常不检查src,只检查dst,所以该封包可以抵达目的主机;但目的主机返回给私有IP的封包将被公网路由器丢弃。

广为人知的NAT解决方案是,路由器在向外转发封包时将src改为公网IP,并记录一个对应关系;在向内转发封包时将dst改为记录中的私有IP。其中,修改src的步骤称为SNAT,修改dst的步骤称为DNAT。

书上第92页关于NAT流程的讲解似乎有误,我从Netfilter官网的Linux 2.4 NAT HOWTO找了一些资料:

Source NAT is always done post-routing, just before the packet goes out onto the wire.
Destination NAT is always done before routing, when the packet first comes off the wire.

从2.2节我们知道,NAT表有4条链,事实上,IPNUT链是后来加入的,可以参考iptables: built-in INPUT chain in nat table?What do input and output chains in NAT table do?相关的commit

结合以上所有信息,我尝试画出封包流经NAT机制的过程:

Packet-IN ---> PREROUTING(DNAT) ---> ROUTING-TABLE ---------------------+
                                          |                             |
                                          V                             |
                                        INPUT       +----> OUTPUT       |
                                          |         |        |          |
                                          V         |        V          |
                                     LOCAL-PROCESS -+   ROUTING-TABLE   |
                                                             |          |
                                                             V          |
Packet-OUT <--- POSTROUTING(SNAT) --------------------------------------+

上图中的ROUTING-TABLE实际上是同一个。对上述流程做如下解释:

  • NAT机制中的INPUT/OUTPUT链与Filter机制中的同名链没有关系
  • NAT机制中的INPUT/OUTPUT链的存在是为了满足以下需求:本地进程产生的封包送离本机前,希望做一次DNAT(以前会直接进入POSTROUTING做SNAT,没有DNAT的机会);网络上发给本机进程的封包在抵达进程前,希望做一次SNAT(同样地,以前会直接进入PREROUTING做DNAT,没有SNAT的机会)。现在,我们可以把这些规则放在INPUT/OUTPUT链中
  • 上述流程与封包流向无关

下面介绍本节开头提到的四种不同拓扑结构的NAT。

一对多NAT

即目前IPv4资源枯竭情况下广泛使用的局域网方案。

假设有一NAT主机,其eth0连接公网,IP为10.0.1.200,eth1连接172.16.56.0/24的内网,为了让内网机器通过NAT机制访问公网,配置方法如下:

iptables -t nat -A POSTROUTING -o eth0 -s 172.16.56.0/24 -j SNAT --to 10.0.1.200

当我们给出了一个方向(SNAT或DNAT)的规则后,Netfilter会自动判别另一个方向的响应封包。因此我们只需要给出上述一条命令即可。

如果公网IP是通过DHCP等方式动态分配的,需要将上述命令稍作修改:

iptables -t nat -A POSTROUTING -o eth0 -s 172.16.56.0/24 -j MASQUERADE

MASQUERADE意为使用外出网卡上的IP来作为源IP。

多对多NAT

与一对多NAT类似,但要求NAT主机的对外网卡具有多个连续公网IP:

iptables -t nat -A POSTROUTING -o eth0 -s 172.16.56.0/24 -j SNAT --to 10.0.1.200-10.0.1.205

一对一NAT

在第一部分对防火墙类型的介绍中我们提到,将服务器放在内网中能够提高安全性。在下图所示的拓扑结构中,我们希望192.168.0.1/192.168.0.2能够分别与10.0.1.201/10.0.1.202进行一对一NAT转换:

以Web Server为例,为了让外网客户能够访问到Web服务,我们添加如下规则:

iptables -t nat -A PREROUTING -i eth0 -d 10.0.1.201 -j DNAT --to 192.168.0.1

当然,如果我们希望Web服务器能够去访问外网的其他服务,那么还需要添加以下规则:

iptables -t nat -A POSTROUTING -o eth0 -s 192.168.0.1 -j SNAT --to 10.0.1.201

NAPT

NAPT即Network Address Port Translation,使用场景与上面介绍的一对一NAT类似,区别是我们只拥有一个公网IP:

所以,我们变通一下,将一对一IP映射的需求降为一对一端口映射。具体如下:

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to 192.168.0.1:80
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j DNAT --to 192.168.0.1:443
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 25 -j DNAT --to 192.168.0.2:25
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 110 -j DNAT --to 192.168.0.2:110

2.10 Netfilter的Mangle机制

Mangle机制可以修改封包内容。例如,修改IP包头的TTL值,从而欺骗依据ping来判断目标操作系统类型的扫描器。它的结构与NAT类似:

Packet-IN ---> PREROUTING ---> ROUTING-TABLE ---> FORWARD ---------+
                                     |                             |
                                     V                             |
                                   INPUT       +----> OUTPUT       |
                                     |         |        |          |
                                     V         |        V          |
                                LOCAL-PROCESS -+   ROUTING-TABLE   |
                                                        |          |
                                                        V          |
Packet-OUT <--- POSTROUTING ---------------------------------------+

2.11 Netfilter的完整结构

从网上找到一张对封包流经Netfilter过程介绍较为完整的图片:

上图中不同颜色的圆点代表不同的规则表。

事实上,将NAT机制与Filter机制结合使用才能使服务器得到更高安全性(在实际应用时一定要注意上图中不同机制不同环节的先后顺序对src/dst等的影响)。

总结

到目前为止,我们已经学习了除RAW外的所有机制。RAW与“连接追踪”机制有关,因此,在后面学习过该机制后,我们再来学习RAW

这本书让我想起了王爽老师的《汇编语言》,这样的书籍能够让你在阅读中从点到面,逐步形成一个知识体系,而不是像一般的手册那样,让你觉得所有知识散落在那里,一团乱麻。

第三部分:Netfilter模块的匹配方式与处理方法

本部分我们来学习如何使用不同模块提供的匹配方式(Matches)和处理方法(Targets),达到进一步的封包筛选目的。

本部分导图:

3.1 匹配方式(Matches)

“匹配方式”是Netfiter筛选封包的基本单元。

3.1.1 内建匹配方式

Netfilter的四大机制分别由以下四大模块提供:

  • iptable_filter.ko
  • iptable_mangle.ko
  • iptable_nat.ko
  • iptable_raw.ko

这些模块中内建了一些匹配方式:

  • Interface (-i -o)
  • IP Address (-s -d)
  • Protocol (-p 参考/etc/protocols)

例1:ICMP协议高级匹配

我们希望配置规则,使得自身能够去ping外部机器,但外部机器ping自己的请求被丢弃。很明显,下面的命令不能实现这个目的:

iptables -A INPUT -p icmp -j DROP

结合ICMP协议,我们给出以下命令:

iptables -A INPUT -p icmp --icmp-type=8 -j DROP

例2:TCP协议高级匹配

我们已经介绍过--sport/--dport,现在介绍依据TCP Flags(8 bits,用于TCP连接控制)的匹配。一些重要的标志如下:

范围 描述
bit 1 Finish 连接终止信号
bit 2 Synchronize 连接请求信号
bit 3 Reset 立即中断连接
bit 5 Acknowledge 确认回复信号

我们借助Wireshark回顾一下TCP连接的建立和终止过程:

建立:C-SYN->S; S-SYN,ACK->C; C-ACK->S

终止:理论上需要4次握手,但抓包往往出现其他情况(例如我下面的)

客户端先终止连接:

服务端先终止连接:

我们暂时不去深究协议内容,专注于Netfilter本身。可以明确的是,正常封包不会同时具备SYN/FIN标志。为了避免主机接收到这样的封包后出现异常,我们可以对这一类异常封包进行过滤。

有如下规则:

iptables -A INPUT -p tcp --dport 22 -j ACCEPT

我们可以对其修改,只允许第一个封包带有SYN标志:

iptables -A INPUT -p tcp --syn --dport 22 -m state --state NEW -j ACCEPT

结果如下:

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:ssh flags:FIN,SYN,RST,ACK/SYN state NEW

或者,使用如下命令,检查所有TCP Flags,过滤掉同时包含SYN与FIN的封包:

iptables -A INPUT -p tcp --tcp-flags ALL SYN,FIN -j DROP

结果如下:

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
DROP       tcp  --  anywhere             anywhere             tcp flags:FIN,SYN,RST,PSH,ACK,URG/FIN,SYN

一个变种是,不检查其他标志,只检查SYN与FIN同时为1,效果相同:

iptables -A INPUT -p tcp --tcp-flags SYN,FIN -j DROP

结果如下:

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
DROP       tcp  --  anywhere             anywhere             tcp flags:FIN,SYN/FIN,SYN

3.1.2 延伸匹配方式

现在我们介绍其他模块提供的匹配功能,可以到前文给出的Netfilter模块路径下查看有哪些模块可以使用。

例1:MAC地址匹配

该功能由xt_mac.ko模块提供,简单举例:

限制只有某台机器可以访问MySQL服务器:

iptables -A INPUT -p tcp --dport 3306 -m mac --mac-source 00:11:22:33:44:55 -j ACCEPT

例2:Multiport匹配

该模块在上一部分中已经讲过,不再赘述。需要注意的是除了--dport/--sport还可以用--port来匹配来源或目的端口。

例3:IP范围匹配

iptables -A INPUT -m iprange --src-range 172.16.56.3-172.16.56.100 -j DROP

例4:MARK匹配

这种匹配方式的思想很有趣,它大大提升了匹配自由度。上一部分最后,我们给出了一张封包流经Netfilter完整流程的图吗?我们可以在Mangle表中借助MARK来将符合特定条件的封包打上标记,然后在逻辑链下游的表中对标记过的封包进行处理。例如:

iptables -t mangle -A PREROUTING -p tcp --dport 80 -j MARK --set-mark 80
iptables -t filter -A FORWARD -p all -m mark --mark 80 -j DROP

执行后的Mangle表:

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         
MARK       tcp  --  anywhere             anywhere             tcp dpt:http MARK set 0x50

执行后的Filter表:

Chain FORWARD (policy DROP)
target     prot opt source               destination         
DROP       all  --  anywhere             anywhere             mark match 0x50

关于MARK还可以参考基于IPTABLES MARK机制实现策略路由关于IPTABLES 各种MARK 功能的用法。MARK作用于内核,不会修改封包内容。

例5:所有者匹配

该功能由xt_owner.ko模块提供,只适用于OUTPUT链。它提供的匹配方式如下:

  • --uid-owner userid
  • --gid-owner groupid

结合Linux基础知识就很好理解。这里举一个例子:

iptables -A OUTPUT -p all -m owner --uid-owner centos -j DROP

例6:TTL值匹配

iptables -A INPUT -m ttl --ttl-eq 64 -j REJECT

另外还有--ttl-lt/--ttl-gt

例7:封包状态匹配

上一部分中我们已经基本了解过state模块提供的匹配方式。这里我们深入探讨一下,在不同的通信协议中state规定的四种连接状态的具体含义。

TCP协议

Client发送SYN请求的封包经过防火墙将被标识为NEW,Server以SYN&ACK响应后该连接上的所有封包均被认定为ESTABLISHED

在CentOS 7中,/proc/net/nf_conntrack是连接追踪数据库,/proc/sys/net/nf_conntrack_max给出该数据库中记录连接数的上限。

该数据库中的一条记录如下:

ipv4     2 tcp      6 431819 ESTABLISHED src=172.16.56.138 dst=172.16.56.1 sport=41028 dport=10000 src=172.16.56.1 dst=172.16.56.138 sport=10000 dport=41028 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 zone=0 use=2

UDP协议

与TCP类似。

ICMP协议

与TCP类似。

例8:AH及ESP协议的SPI值匹配

iptables -A FORWARD -p ah -m ah --ahspi 300 -j ACCEPT
iptables -A FORWARD -p esp -m esp --espspi 200 -j ACCEPT

例9:pkttype匹配

这里的类型指是unicastbroadcast还是multicast

例如,我们可以过滤掉ping的广播包:

iptables -A FORWARD -i eth0 -p icmp -m pkttype --pkt-type broadcast -j DROP

这针对的是下面这种操作:

ping -b 172.16.56.255

例10:封包长度匹配

以ICMP封包为例,我们往往用MTU和MSS去描述封包长度:

  • Maximum Transmission Unit = IP包头 + ICMP包头 + DATA
  • Maximum Segment Size = ICMP包头 + DATA

计算一下,可以得到正常Windows系统ping的MTU,并用以下命令放行:

iptables -A INPUT -p icmp --icmp-type 8 -m length --length 92 -j ACCEPT
iptables -A INPUT -p icmp --icmp-type 8 -j DROP

另外,100::10050:100分别匹配长度大于等于100、小于等于100及50到100。

例11:limit特定封包重复率匹配

有时我们不想完全禁止外部ping主机,因为自己可能也有这个需求。这时,我们可以限制频率,例如设定:每分钟只能进入10个封包,但如果进入多于10个,则限制每分钟只能进入6个。

iptables -A INPUT -p icmp --icmp-type 8 -m limit --limit 6/m --limit-burst 10 -j ACCEPT
iptables -A INPUT -p icmp --icmp-type 8 -j DROP

另外,/s/h/d分别代表秒、小时、天。

设定规则后去ping一下主机。结果符合预期,先进入10个,然后降为每分钟6个,也就是10秒一个:

ping 172.16.56.138
PING 172.16.56.138 (172.16.56.138): 56 data bytes
64 bytes from 172.16.56.138: icmp_seq=0 ttl=64 time=0.312 ms
64 bytes from 172.16.56.138: icmp_seq=1 ttl=64 time=0.618 ms
64 bytes from 172.16.56.138: icmp_seq=2 ttl=64 time=0.559 ms
64 bytes from 172.16.56.138: icmp_seq=3 ttl=64 time=0.576 ms
64 bytes from 172.16.56.138: icmp_seq=4 ttl=64 time=0.626 ms
64 bytes from 172.16.56.138: icmp_seq=5 ttl=64 time=0.595 ms
64 bytes from 172.16.56.138: icmp_seq=6 ttl=64 time=0.535 ms
64 bytes from 172.16.56.138: icmp_seq=7 ttl=64 time=0.492 ms
64 bytes from 172.16.56.138: icmp_seq=8 ttl=64 time=0.498 ms
64 bytes from 172.16.56.138: icmp_seq=9 ttl=64 time=0.610 ms
Request timeout for icmp_seq 10
64 bytes from 172.16.56.138: icmp_seq=11 ttl=64 time=0.645 ms
Request timeout for icmp_seq 12
Request timeout for icmp_seq 13
Request timeout for icmp_seq 14
Request timeout for icmp_seq 15
Request timeout for icmp_seq 16
Request timeout for icmp_seq 17
Request timeout for icmp_seq 18
Request timeout for icmp_seq 19
Request timeout for icmp_seq 20
64 bytes from 172.16.56.138: icmp_seq=21 ttl=64 time=0.765 ms

例12:recent特定封包重复率匹配

这是一个比limit更为强大的模块,具体的使用方法如下:

名称 解释
--name 指定追踪数据库的文件名
--set 将符合条件的来源信息加入数据库,若来源信息已存在,则仅更新数据库
--rcheck 只进行信息匹配,不更改数据库信息
--update 若来源信息已存在,则更新,否则不处理
--remove 若来源信息已存在,则删除,否则不处理
--seconds second 事件发生时,只匹配数据库中前几秒记录,必须与--rcheck或--update配合使用
--hitcount hits 匹配重复发生次数,必须与--rcheck或--update配合使用

同样以ICMP流量控制为例,我们希望每分钟只能进来6个封包:

# rule 1
iptables -A INPUT -p icmp --icmp-type 8 -m recent --name icmp_db --rcheck --second 60 --hitcount 6 -j DROP
# rule 2
iptables -A INPUT -p icmp --icmp-type 8 -m recent --set --name icmp_db

结果如下:

ping 172.16.56.138
PING 172.16.56.138 (172.16.56.138): 56 data bytes
64 bytes from 172.16.56.138: icmp_seq=0 ttl=64 time=0.419 ms
64 bytes from 172.16.56.138: icmp_seq=1 ttl=64 time=0.583 ms
64 bytes from 172.16.56.138: icmp_seq=2 ttl=64 time=0.461 ms
64 bytes from 172.16.56.138: icmp_seq=3 ttl=64 time=0.620 ms
64 bytes from 172.16.56.138: icmp_seq=4 ttl=64 time=0.579 ms
64 bytes from 172.16.56.138: icmp_seq=5 ttl=64 time=0.598 ms
Request timeout for icmp_seq 6
Request timeout for icmp_seq 7
...
Request timeout for icmp_seq 58
Request timeout for icmp_seq 59
64 bytes from 172.16.56.138: icmp_seq=60 ttl=64 time=0.600 ms
64 bytes from 172.16.56.138: icmp_seq=61 ttl=64 time=0.399 ms
64 bytes from 172.16.56.138: icmp_seq=62 ttl=64 time=0.256 ms
64 bytes from 172.16.56.138: icmp_seq=63 ttl=64 time=0.317 ms
64 bytes from 172.16.56.138: icmp_seq=64 ttl=64 time=0.562 ms
64 bytes from 172.16.56.138: icmp_seq=65 ttl=64 time=0.240 ms

需要注意的是,与limit无差别计算封包总量的方式不同,recent是针对不同来源做分别匹配的,也就是说,上述规则允许不同来源的ping在每分钟发生6次。可以看一下/proc/net/xt_recent/icmp_db文件,它也是分条存放的:

src=172.16.56.1 ttl: 64 last_seen: 4364842324 oldest_pkt: 18 4364252889, 4364253887, 4364254888, 4364255884, 4364256883, 4364257884, 4364312924, 4364313926, 4364314928, 4364315929, 4364316928, 4364317925, 4364837357, 4364838351, 4364839345, 4364840340, 4364841333, 4364842324
src=172.16.56.164 ttl: 64 last_seen: 4364907742 oldest_pkt: 9 4364845564, 4364846557, 4364847548, 4364848558, 4364849552, 4364850545, 4364905710, 4364906726, 4364907742

我们来解释一下上述两条规则的原理:遇到符合条件的封包时,第一条规则去icmp_db向前找60秒的记录,如果向前60秒已经有过6次记录,则丢弃该封包;第二条规则将符合条件的封包信息记录到icmp_db文件中。

跟踪一下ping的过程:我们的ping是1秒一次。

  • 防火墙遇到第一个封包时,第一条规则去数据库中找不到记录,所以该封包会交给第二条规则,从而在数据库中留下一个记录。但是第二条规则并没有对封包进行任何处理,所以该封包被交给下一条规则(在我的环境中,也就是INPUT的默认规则ACCEPT);
  • 防火墙遇到第二个封包时,第一条规则在数据库中向前60秒找到1条记录,因此该封包又被交给第二条规则,同样地,记录后又被交给下一条规则;
  • 按这样的方式一直接受6个封包;
  • 直到第七个封包到达时,第一条规则向前搜索60秒发现已经有6条记录,因此以“丢弃”方式“处理”该封包,因此该封包不会被交给后面的规则;
  • 按这样的方式,从第7秒到第60秒都不会接受该来源的封包;
  • 到第61秒时,第一条规则向前搜索60秒只能找到5条记录,所以又开始接受封包,并交给下一条规则;
  • 按这样的方式一直接受6个封包;
  • 到67秒时,发现前60秒(即6到66秒)又有6条记录,因此以“丢弃”方式“处理”封包;
  • ...

我们甚至可以更进一步,实时更新数据库记录,这样以来,如果同一个来源一直ping,那么它的封包将一直无法进入主机:

iptables -A INPUT -p icmp --icmp-type 8 -m recent --name icmp_db --update --second 60 --hitcount 6 -j DROP
iptables -A INPUT -p icmp --icmp-type 8 -m recent --set --name icmp_db

最后,我们可以设置数据库记录来源IP数和每个来源封包信息数的上限:

modprobe xt_recent ip_list_tot=1024 ip_pkt_list_tot=50

recent为我们提供了发挥想象力的空间。例如,我们可以借助它来禁止端口扫描,实质是限制同一来源IP在规定时间内(如10分钟),不得发来超过10个SYN包:

iptables -P INPUT DROP
iptables -F
iptables -A INPUT -p tcp --syn -m recent --name PortScan --update --second 600 --hitcount 10 -j DROP
iptables -A INPUT -p tcp --syn -m state --state NEW -m multiport --dports 22,25,110 -j ACCEPT
iptables -A INPUT -p tcp --syn -m recent --set --name PortScan

例13:载荷匹配

这是一个很酷的功能,类似于应用层防火墙,但是是在网络层直接匹配载荷内容,不必交给应用层,效率更高,且不占用多余空间。局限在于,匹配范围为单个封包。它的本质是字符串匹配,例如:

iptables -A INPUT -p tcp -m string --algo bm --string 'system32' -j DROP

结果如下:

该模块提供的参数如下:

名称 解释
--algo 匹配算法选择,有两个:bm(Boyer-Moore)和kmp(Knuth-Morris-Pratt)
--from/--to 指定匹配起止范围,单位为字节,不指定则默认范围是整个封包
--string 要匹配的字符串

3.2 处理方法(Targets)

处理方法指的是当一个封包符合匹配条件时,可以怎么处理该封包。

3.2.1 内建处理方法

ACCEPTDROP不必过多介绍。

QUEUE处理方法指将符合条件的封包送给用户层程序处理,应用场景比较少。

在介绍RETURN前,我们要了解User Define Chain的概念:用户可以自定义规则链。以下为自定义规则链的命令:

# create a new chain
iptables -N ICMP
# rename a chain
iptables -E ICMP MYICMP
# delete a chain (it must be empty)
iptables -X MYICMP

假设我们已经创建了一个名为ICMP的链。事实上,现在封包经过Netfilter时并不会进入用户自定义链,即使我们做了如下操作:

iptables -A ICMP -p icmp -j DROP

我们需要将它与其他链关联起来,例如,与INPUT链关联起来:

iptables -A INPUT -p icmp -j ICMP

这样规定以后,封包进入INPUT链匹配到该规则后,将转到ICMP链匹配。如果ICMP内的所有规则都没有匹配到,那么最后封包又被转送回INPUT进行下一条规则匹配(这是不是很像汇编指令call的特点?)。

RETURN目的在于,让符合规则的封包提前返回调用用户自定义链的原始链,这里即INPUT。例如,我们执行:

iptables -I ICMP 1 -p all -j RETURN

这样一来,又可以ping通了。

3.2.2 延伸处理方法

我们不再介绍REJECT

LOG用于生成日志,因为Netfilter默认不生成任何日志。由于LOG本身并不处理封包,所以一般我们会将它与其他规则配合使用,例如:

iptables -A INPUT -p tcp --dport 22 -j LOG
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

Netfilter日志与其他各种日志存放在/var/log/messages中。事实上,我们可以将它存放在单独的文件中:

先在iptables中设定日志等级:

iptables -A INPUT -p tcp --dport 22 -j LOG --log-level alert --log-prefix "[SSH-REQUEST] "

然后修改日志服务配置文件:

# add "kern.=alert /var/log/netfilter"
vim /etc/rsyslog.conf
service rsyslog restart

日志内容如下:

Dec 28 15:49:00 localhost kernel: [SSH-REQUEST] IN=ens33 OUT= MAC=00:0c:29:85:db:15:00:50:56:c0:00:08:08:00 SRC=172.16.56.1 DST=172.16.56.138 LEN=52 TOS=0x08 PREC=0x40 TTL=64 ID=0 DF PROTO=TCP SPT=54286 DPT=22 WINDOW=2048 RES=0x00 ACK URGP=0 

ULOGLOG类似,只不过ULOG将日志交给用户层机制处理。

DSCP用于QOS任务,这里不再展开。

MARK在前面已经介绍过,不再赘述。

REDIRECT是一种特殊的DNAT机制。

MASQUERADE是一种特殊的SNAT机制,在上一部分已经介绍过。

NETMAP用于建立全网段一对一NAT映射(如果每对IP都分别建立一对一NAT,将花费大量时间),例如:

iptables -t nat -A PREROUTING -i eth0 -d 10.0.0.0/24 -j NETMAP --to 192.168.1.0/24
iptables -t nat -A POSTROUTING -o eth0 -s 192.168.1.0/24 -j NETMAP --to 10.0.0.0/24

总结

本部分内容看似琐碎,其实很系统。我们要善于总结,不要死记硬背。经过逐渐的学习,相信大家对于Netfilter能够做什么、怎么做已经有很多自己的理解。

第四部分:高级技巧

本部分介绍一些深入且实际的技巧。

本部分导图:

4.1 防火墙性能优化

性能优化的基本原则是“减少不必要的规则匹配”。

4.1.1 规则顺序调整

iptables -A INPUT -p tcp --syn -m state --state NEW --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --syn -m state --state NEW --dport 23 -j ACCEPT
iptables -A INPUT -p tcp --syn -m state --state NEW --dport 25 -j ACCEPT
iptables -A INPUT -p tcp --syn -m state --state NEW --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --syn -m state --state NEW --dport 110 -j ACCEPT
iptables -A INPUT -p all -m state --state ESTABLISHED,RELATED -j ACCEPT

上述规则可以作出两点调整:

  1. 将最后一条规则放到第一去
  2. 将前五条针对单服务的规则按照热门程度(匹配命中次数)依次排列

另外,可以使用以下命令去统计规则匹配频度,从而对规则顺序作出优化:

iptables -L -v -n

4.1.2 善用“指定多目标”模块

匹配多IP和多端口的模块可以帮助减少规则数。上一小节的规则范例可以优化为:

iptables -A INPUT -p all -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --syn -m state --state NEW -m multiport --dports 22,23,25,80,110 -j ACCEPT

4.1.3 善用用户自定义规则

考虑一种情况:我们部署了一个网关防火墙,其后有三台主机。我们为每台主机在网关防火墙上各制定了100条FORWARD规则。这些规则在同一条链中排列,必定有先有后。对于符合第300条规则的封包来说,它将经过299道无用过滤。

为了解决这个问题,可以设置三条用户自定义规则链:

iptables -A FORWARD -p all -d $HOST1 -j $CHAIN_HOST1
iptables -A FORWARD -p all -d $HOST2 -j $CHAIN_HOST2
iptables -A FORWARD -p all -d $HOST3 -j $CHAIN_HOST3

这样一来,无用匹配将大大减少。

除了按照不同主机分割规则链,也可以按照不同协议,不同网络栈层次来划分。

4.2 Netfilter连接处理能力与内存损耗

用于连接追踪统计的文件是/proc/net/nf_conntrack,它的一个项如下:

ipv4     2 tcp      6 431819 ESTABLISHED src=172.16.56.138 dst=172.16.56.1 sport=41028 dport=10000 src=172.16.56.1 dst=172.16.56.138 sport=10000 dport=41028 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 zone=0 use=2

注意,/proc存在于内存中。每多追踪一条连接,就会多消耗一点内存。但是我们可以调整连接追踪数量上限。

4.3 RAW Table

本节介绍RAW表。

只要conntrack模块被加载,那么穿越防火墙的所有连接就会被记录。在一些场景下,我们希望某些连接不被记录下来,就可以使用RAW:

iptables -t raw -A PREROUTING -i eth2 -p tcp --dport 25 -j NOTRACK
iptables -t raw -A PREROUTING -i eth1 -p tcp --sport 25 -j NOTRACK

在第二部分最后的流程图中可以看到,RAW的PREROUTING链处理优先级是最高的。

RAW表的好处就是提高防火墙性能,增加可追踪的连接数量。需要注意的是,RAW表定义下的连接会跳过NAT表。

RAW的PREROUTING链用来处理“防火墙两侧网络之间建立的”以及“到防火墙本身的”连接;OUTPUT链用于处理防火墙自身对外建立的连接(与NAT表的OUTPUT链类似)。

4.4 简单及复杂通信协议处理

Netfilter将通信协议划分为两类:简单的和复杂的。(你是在逗我吗?)

4.4.1 简单通信协议

简单通信协议指的是那些Client访问Server只产生一条连接的协议,如HTTP、SSH、SMTP等。

简单通信协议的处理很简单,就是我们之前学过的那些内容(Filter、NAT都没什么问题)。

4.4.2 复杂通信协议

复杂通信协议如FTP、PPTP等,需要在Client与Server之间建立一条以上的连接。下面以FTP协议为例来介绍Netfilter处理复杂通信协议的方式。

FTP协议分为Passive和Active模式,工作流程分别如下图所示:

Filter在处理FTP通信协议时会遇到问题:在被动模式下,假如Server端被放在一个防火墙后面,那么1955 <- 21后,常规的连接追踪机制并不能生效,所以1956 -> 29318这条连接不被认为是RELATED,所以将被防火墙挡在外面;类似地,在主动模式下,假如Client端被放在一个防火墙后面,也会出现这个问题。

问题的根本在于默认的conntrack模块不能处理FTP协议在控制信道指定的数据端口信息。

解决方案:Netfilter为复杂通信协议提供了特定的判别模块,我们拿来用就好,这里需要的就是ip_conntrack_ftp.ko

先加载模块,后面正常写就好:

modprobe ip_conntrack_ftp
...
iptables -A FORWARD -i eth0 -o eth1 -p all -m state --state ESTABLISHED,RELATED -j ACCEPT

如果需要指定端口,则可以在加载模块时给出:

modprobe ip_conntrack_ftp ports=21,2121,3131

NAT在处理FTP协议时也会遇到问题:默认的NAT机制不能处理FTP协议在控制信道指定的IP地址信息,应用层传递的依然是私有IP。

解决方案类似,加载ip_nat_ftp模块即可。

4.4.3 ICMP封包处理原则

处理原则很简单:

  • 放行所有来自因特网的ESTABLISHED及RELATED状态的ICMP封包
  • 丢弃所有来自因特网的其他状态的ICMP封包

4.4.4 常见网络攻击及防护

PortScan

端口扫描的目的在于探测目标主机开放了那些端口,特征是短时间内向目标主机发出针对不同端口的连接请求。结合此特征,我们可以给出以下论断:

  • 访问服务器上开放的端口属于正常行为(这里仅仅讨论端口扫描层面上的行为)
  • 访问服务器上其他端口属于不正常行为

我们从而可以给出如下防火墙规则:

iptables -A INPUT -p all -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p all -m state --state NEW -m recent --name port_scan --update --seconds 1800 --hitcount 10 -j DROP
iptables -A INPUT -p tcp --syn -m state --state NEW -m multiport --dports 22,25,80,110 -j ACCEPT
iptables -A INPUT -p all -m recent --name port_scan --set
密码暴力破解

此类攻击的特征是,服务端会在短时间内发出很多认证失败的信息。例如,针对POP3暴力破解可以使用如下规则判断防御:

iptables -A OUTPUT -p tcp --sport 110 -m string --algo bm --string "-ERR Authentication failed." -m recent --name pop3 --update --seconds 600 --hitcount 6 -j REJECT
iptables -A OUTPUT -p tcp --sport 110 -m string --algo bm --string "-ERR Authentication failed." -m recent --name pop3 --set

这是针对未加密的通信协议;加密的通信协议处理起来要困难一些,使用服务本身提供的防破解机制会更有效。

总结

陈先生的这本书到此算是读完。后面关于代理服务器和VPN的部分暂时没有需求去阅读。就我读过的部分来说,这本书真值得推荐。我从王爽老师的《汇编语言》、陈勇勋老师的《更安全的Linux网络》及赵鹏老师的《毫无PS痕迹》三本书中学到了一些教育与写作的艺术:他们的书就像厨艺大师烧好的可口的菜,而不是常见书籍那样的简单铺陈。所谓由点及面、提纲挈领、循循善诱,如是而已。

参考文献

  1. 《更安全的Linux网络》 陈勇勋 著
  2. 防火墙(firewalld与iptables)
  3. 关闭CentOS7的firewalld并启用iptables操作
  4. Netfilter Packet Traversal
Per Aspera Ad Astra