iptables&Netfilter简介

IPtable&Netfilter原理

要对两者介绍,必须要要知道两者间的关系.
Netfilter是Linux2.4引入的一个子系统,它作为一个通用的,抽象的框架,提供一整套的hook函数的管理机制,使得诸如数据包过滤,网络地址转换(NAT)和基于协议类型的链接跟踪成为了可能.Netfilter在内核中的位置如下:

这幅图很直观地反应了用户空间的iptables和内核空间的基于Netfilter的ip_tables模块之间的关系和其通讯方式,以及Netfilter在这其中扮演的角色. Netfilter会在协议栈中的每个数据包的不同地方设置HOOK点.如下所示:

在每个关键点上,有很多已经按照优先级预先注册了的回调函数,这些回调函数形成了一条链.对于每个到来的数据包会依次被那些回调函数处理在根据情况是放行,修改还是丢弃. 这些回调函数的作用依次是:

  • PREROUTING: 处理外部进来的包,刚被网卡接受,此时的数据包还没有做路由决策
  • INPUT: 外部进来的包,已经做完路由决策,发往本机的数据包
  • FORWARD: 外部进来的包,路由决策之后,需要转发的包.
  • OUTPUT: 本机产生需要向外发送的数据包
  • POSTROUTING: 路由决策之后,向外发送的数据包.

每个回调函数必须要向Netfilter框架返回下列几个值:

  • NF_ACCEPT , 这个返回值告诉Netfilter该数据包被接受了并且该数据包应当被递交到网络协议栈的下一阶段.
  • NF_DROP, 丢弃该数据包,不再传输.
  • NF_STOLEN, 模块接管该数据,告诉Netfilter”忘掉”该数据报.该回调函数将会出来该数据包,并且Netfilter应该放弃对该数据包做任何处理.但是,这并不意味着该数据包的资源已经被释放.这个数据包以及它独自的sk_buff数据结构依然有效,只是回调函数从Netfilter获取了该数据包的所有权.
  • NF_QUEUE, 对该数据包进程排队(通常用于将该数据报给用户空间的进程进行处理)
  • NF_REPEAT, 再次调用该回调函数,应当谨慎使用这个值,以免造成死循环.

要想Netfilter针对具体的包执行具体的动作,就需要设置规则.根据功能划分,Netfilter分为Filter,NAT,Mangle和RAW四个功能.由这四个功能派生成为了iptables的4张表.

  • Filter, 用于存放所有与防火墙相关操作的默认表
  • NAT(Network Address Translation): 用于网络地址转换
  • Mangle: 用于对特定数据包的修改
  • raw: 用于配置数据包,raw中的数据包不会被系统跟踪

iptables规则库管理

iptables的规则被存储在不同的链中,相当与一个规则库,当我们修改玩规则,用iptables-save命令进行存储.

RAW表

RAW表之使用PREOUTING和OUTPUT链,具有最高优先级,可以对收到的数据包在连接跟踪之前处理.RAW表主要作用就是运行我们对某些也定的数据包打上 NOTRACK 标记,被打上了NOTRACK标记的数据包将跳过NAT表和 ip_conntrack处理,即不再做地址转换和数据包的链接跟踪处理了.如大量访问的web服务器,可以让80端口不再让iptables做数据包的链接跟踪处理,以提高用户的访问速度.

1
2
3
iptables -t raw -A PREOUTING -p tcp --dport 80 -j NOTRACK
iptables -t raw -A PREOUTING -p tcp --sport 80 -j NOTRACK
iptables -A FORWARD -m state --state UNTRACKED -j ACCEPT

Mangle表

Mangle表主要用于修改数据包的TOS(Type of Service,服务类型),TTL(Time To Live,生存周期),以及为数据包设置Mark标记,以实现QoS(Quality Of Service,服务质量)调整以及策略路由等应用,mange表对应的内核模块为iptables_mangle.由于需要相应的路由设备支持,应用并不广泛.注意Mark并没有真正地改动数据包,只是在内核空间为包设了一个标记.

nat表

此表仅用于NAT,也就是转换包的源或目标地址.注意,就像我们前面说过的,只有流的第一个包会被这个链匹配,其后的包会自动被做相同的处理.实际的操作分为三类: DNAT,SNAT, MASQUERADE

  • DNAT 操作主要用在一种这样的情况,你有一个合法的IP地址,要把对防火墙的访问重定向到其他的机子上(比如DMZ).也就是说,我们改变的是目的地址,以使包能从路由到某台机器.
  • SNAT 改变包的源地址,这在极大程度上可以隐藏你的本地网络或者DMZ等.一个很好的来自是我们知道防火墙的外部地址,但必须用这个地址替换本地网络地址.有了这个操作,防火墙就能自动地对包做SNAT和De-SNAT(就是反向的SNAT),以使LAN能连接到Internet.
  • MASQUEREAD,地址伪装, 算是snat中的一种特例,可以实现自动化的snat.在iptables中有着和SNAT相近的效果,但也有一些区别,但使用SNAT的时候,出口ip的地址范围可以是一个,也可以是多个.但是对于SNAT,不管是几个地址,必须明确的指明要SNAT的ip,假如当前系统用的是ADSL动态拨号方式,那么每次拨号,出口ip就会改变,而且改变幅度很大,不一定是某个范围内的地址.这个时候如果按照现在的方式来配置iptables就会出现问题了,因为每次拨号后,服务器的地址都会变化 ,而iptables规则内的ip是不会随着自动变化的,每次地址变化后都必须手工修改一次iptables,把规则里面的固定ip修改成新的ip,这样是非常不好用的. MASQUEREAD 就是针对场景设计的,他的作用是,从服务器的网卡上,自动话去当前IP地址来做NAT. 如iptables -t nat -A POSTROUTING -s 10.8.0.0/255.255.255.0 -o eth0 -j MASQUEREAD. 按照上面的配置, 不用制定SNAT的目标IP, 不管现在eth0的出口获得了怎样的动态IP, MASQUEREAD会自动读取eth0现在的IP地址然后做SNAT出去,这样就实现了很多的动态SNAT地址转换.

filter表

filter表用来过滤数据包,我们可以在任何时候匹配包并过滤他们.我们就是在这里根据包的内容对包做DROP或ACCEPT的.当然,我们也可以在其他地方做些过滤,但是这个表才是设计用来过滤的.
每个表分别作用在netfilter框架中的不同的Hook点上.在iptables,这些Hook点被称为链.他们的对应关系如下:

  • filter表:INPUT,FORWARD,OUTPUT
  • nat表:PREROUTING,POSTROUTING
  • mangle表:PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING
  • raw表:PREROUTING,OUTPUT

以上4个表,按照处理优先级从高到底,依次是: RAW>Mangle>NAT>Filter

Netfilter与iptables的关系

netfilter组件也称为 内核空间(kernelspace),是内核的一部分,由一些信息包过滤表组成,这些表(filter,nat,mangle,raw)包含内核用来控制信息包过滤处理的规则集
iptables组建是一种工具,也称为用户空间(userspace),它使插入,修改和除去信息包过滤表中的规则变得容易.
通过使用用户空间,可以构建自己的定制规则,这些规则存储在内核空间的信息包过滤表中.这些规则具有目标,它们告诉内核对来自某些源,前往某些目的地或具有某些系诶类型的信息包做些什么.如果某个信息包与规则匹配,那么使用目标ACCEPT运行该信息包通过,还可以使用目标DROP或REJECT来阻塞并杀死信息包.对于可对信息包执行的其他操作,还有许多其他的目标.

iptables规则库管理

iptables的规则被存储在各种不同的链中,相当与一个规则库,当我们修改玩规则可用iptables-save命令存储.

iptables命令格式

iptables的命令结构是: iptables [-t table] command [chain] [rules] [-j target]

  • -t Table,Table对应的四张表是filter,nat,mangle以及raw
  • command
    • -A 在指定链的末尾添加(append)一条新的规则
    • -D 删除(delete)指定链中的某一条规则,可以按照规则序号和内容删除
    • -I 在指定链中插入(insert)一条新规则,默认在第一行添加
    • -R 修改,替换(replace)指定链中的某一条规则,可以按照规则序号和内容替换
    • -L 列出(list)指定链中所有的规则进行查看
    • -E 重命名用户定义的链,不改变链本身
    • -F 清空(flush)
    • -N 新建(new-chain) 一条用户自己定义的规则链
    • -X 删除制定表中用户自定义的规则链(delete-chain)
    • -P 设置指定链的默认策略(policy)
    • -Z 将所有表的所有链的字节和数据包计数清零
    • -n 使用数字形式(numberic)显示输出结果
    • -v 查看规则表详细信息(verbose)的信息
    • -V 查看版本(version)
    • -h 获取帮助(help)
  • chain,链名称,指 INPUT链,OUTPUT链,FORWARD链,PRETOUTING链,POSTROUTING链
  • rules,规则/限定条件
  • target,动作,常见的包括 ACCEPT(通过),DROP(丢弃),REJECT(丢弃并返回发送端一个Destination Unreachable的ICMP封包)

示例命令

  • 将进入到本机的ICMP报文全部丢弃,禁止ping iptables -t filter -A INPUT -p icmp -j DROP
  • 在INPUT链中第二条规则位置插入一条规则(运行TCP协议通过) iptables -t filter -I INPUT 2 tcp -j ACCEPT
  • 删除INPUT链中的第二条规则 iptables -t filter -D INPUT 2
  • 将192.168.0.200进入本机的icmp协议包都丢弃 iptables -A INPUT -p icmp -s 192.168.0.200 -j DROP
  • 不允许192.168.0.200主机通过本机的DNS服务来执行域名解析 iptables -A INPUT -p udp -s 192.168.0.200 --dport 53 -j REJECT
  • 允许192.168.1.0/24 网段的主机向本机192.168.0.1提出任何服务请求 iptables -A INPUT -p all -s 192.168.1.0/24 -d 192.168.0.1 -j ACCEPT
  • 允许客户端主机从eth1这个接口访问本机的SSH服务 iptables -A INPUT -p tcp -i eth1 --dport 22 -j ACCEPT
  • 不允许本机的应用程序从eth0接口发送出去数据包去访问edu.uuu.com.tw以外的网站 iptables -A OUTPUT -o th0 -p tcp -d ! edu.uuu.com.tw --dport 80 -j REJECT
  • 不允许本企业内部的主机访问企业以外的任何网站 iptable -A FORWARD -i eth1 -o eth0 -p tcp --dport 80 -j DROP
  • 将FORWARD链的默认策略设置为DROP iptables -t filter -p FORWAED DROP

规则匹配过程

实际应用中每个链中包含的规则数量不尽相同,无论那一个FIlter表其规则匹配过程都是 First Match, 即优先执行. 当我们在防火墙上添加的规则被逐条加入到INPUT链中,被顺序编号,例如rule1,rule2等.当封包进入INPUT链之后,Filter机制会以这个数据包的特征从INPUT链内的第一条规则逐一向下匹配.如果数据包遇到第一条规则允许通过,那么这个数据包就会进入到本地进程httpd,而不管下面的rule2,rule3.相反如果第一条规则说要丢弃,即使是rule2规则运行通过也不起作用,这就是 First Match原则.
在使用规则时要注意First Match原则,尽量减少不必要的规则.因为当数据包进入防火墙之后会在特定的链中逐一对比,规则条数越多,数据包在防火墙中滞留的时间就越长,防火墙性能就会降低.

Netfilter原理

Netfilter hook点的定义

对于每种类型的协议,数据包丢会一次按照hook点的方向进行传输,每个hook点上Netfilter又按照优先级挂了很多hook函数.这些hook函数就是用来处理数据包的.Netfilter使用NF_HOOK(include/linux/netfilter.h)宏在协议栈内部切入到Netfilter框架中.在2.6内核中定义如下:

1
2
3
4
5
6
7
8
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN)

#define NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, thresh) \
({int __ret; \
if ((__ret=nf_hook_thresh(pf, hook, &(skb), indev, outdev, okfn, thresh, 1)) == 1)\
__ret = (okfn)(skb); \
__ret;})

关于宏NF_HOOK各个参数的解释说明:

  • pf: 协议族名称,Netfilter架构统一可以用于IP层之外,因此这个变量还可以有诸如PF_INET6,PF_DECent等名字. 在TCP建立 中已经说明了目前所有的协议簇.
  • hook: HOOK点的名字,对于IP层,就是PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING 五个值
  • skb: struct sk_buff是存储网络包的重要的数据结构
  • indev: 数据包进来的设备 以struct net_device结构表示
  • outdev: 数据包出去的设备, 以struct net_device 结构表示
  • okfn: 函数指针, 当所有的该HOOK点的所有等级函数调用完后,转而走此流程

NF_HOOK_THRESH宏想对于NF_HOOK仅仅只是增加了一个thresh参数,这个参数就是用来制定该宏通过该宏去遍历钩子函数时的优先级.同时, 该宏内部又调用了nf_hook_thresh函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static inline int nf_hook_thresh(int pf, unsigned int hook,
struct sk_buff **pskb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sk_buff *), int thresh,
int cond)
{
if (!cond)
return 1;
#ifndef CONFIG_NETFILTER_DEBUG
if (list_empty(&nf_hooks[pf][hook]))
return 1;
#endif
return nf_hook_slow(pf, hook, pskb, indev, outdev, okfn, thresh);
}

该函数只是增加了一个cond参数,该参数为0则放弃遍历,直接返回1. 为1 则执行nf_hook_slow按照okfn定义的优先级去遍历钩子函数.
net/netfiltr.core.h文件中定义了一个二维的结构体数组, 用来存储不同协议钩子点的回调处理函数.struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
我们以AF_INET/PF_INET为例,它的序号是2,对应的钩子函数就是:

  • PRE_ROUTING nf_hooks[2][0]
  • LOCAL_IN nf_hooks[2][1]
  • FORWARD nf_hooks[2][2]
  • LOCAL_OUT nf_hooks[2][3]
  • POST_ROUTING nf_hooks[2][4]

Netfilter的执行过程

NF_IP_PRE_ROUTING

net/ipv4/ip_input.c里的ip_rcv函数.该函数主要用来处理网络层报文的入口函数.它到Netfilter框架的切入点为:

1
NF_HOOK(PF_INET,NF_IP_PRE_ROUTING,skb,dev,NULL,ip_rcv_finish)

如果当前收到了一个IP报文(PF_INET),吧么就把这个报文传到Netfilter的NF_IP_PRE_ROUTING过滤点,调用nf_hook_slow()函数在过滤点(nf_hooks[2][0])是否已经有人注册了相关的用于处理数据包的钩子函数.如果有,则遍历nf_hooks[2][0]去寻找匹配的match和相应的target.根据返回到Netfilter框架中的值来进一步决定该如何处理该数据包(由钩子模块处理还是由ip_rcv_finish函数继续处理还是交由ip_rcv_finish函数继续处理).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
referer:net/netfilter/core.c#L110
/* Returns 1 if okfn() needs to be executed by the caller,
* -EPERM for NF_DROP, 0 otherwise. */
int nf_hook_slow(int pf, unsigned int hook, struct sk_buff **pskb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sk_buff *),
int hook_thresh)
{
struct list_head *elem;
unsigned int verdict;
int ret = 0;

/* We may already have this, but read-locks nest anyway */
rcu_read_lock();

elem = &nf_hooks[pf][hook];
next_hook:
verdict = nf_iterate(&nf_hooks[pf][hook], pskb, hook, indev,
outdev, &elem, okfn, hook_thresh);
if (verdict == NF_ACCEPT || verdict == NF_STOP) {
ret = 1;
goto unlock;
} else if (verdict == NF_DROP) {
kfree_skb(*pskb);
ret = -EPERM;
} else if ((verdict & NF_VERDICT_MASK) == NF_QUEUE) {
NFDEBUG("nf_hook: Verdict = QUEUE.\n");
if (!nf_queue(pskb, elem, pf, hook, indev, outdev, okfn,
verdict >> NF_VERDICT_BITS))
goto next_hook;
}
unlock:
rcu_read_unlock();
return ret;
}

# 调用nf_iterate()遍历去选找所有满足要求的hook函数.
# referer:net/netfilter/core.c#L66
unsigned int nf_iterate(struct list_head *head,
struct sk_buff **skb,
int hook,
const struct net_device *indev,
const struct net_device *outdev,
struct list_head **i,
int (*okfn)(struct sk_buff *),
int hook_thresh)
{
unsigned int verdict;

/*
* The caller must not block between calls to this
* function because of risk of continuing from deleted element.
*/
list_for_each_continue_rcu(*i, head) {
// 根据优先级查找双向链表nf_hooks[][],找到对应的回调函数来处理数据包
struct nf_hook_ops *elem = (struct nf_hook_ops *)*i;

if (hook_thresh > elem->priority)
continue;

/* Optimization: we don't need to hold module
reference here, since function can't sleep. --RR */
verdict = elem->hook(hook, skb, indev, outdev, okfn);
if (verdict != NF_ACCEPT) {
#ifdef CONFIG_NETFILTER_DEBUG
if (unlikely((verdict & NF_VERDICT_MASK)
> NF_MAX_VERDICT)) {
NFDEBUG("Evil return from %p(%u).\n",
elem->hook, hook);
continue;
}
#endif
if (verdict != NF_REPEAT)
return verdict;
*i = (*i)->prev;
}
}
return NF_ACCEPT;
}

NF_IP_FORWARD

net/ipv4/ip_forward.c中的ip_forward函数,它的切入点是:

1
NF_HOOK(PF_INET,NF_IP_FORWARD,skb,skb->dev,rt->u.dst.dev,ip_forward_finish)

在经过路由抉择后,所有需要本机转发的报文都会交由ip_forward函数处理.这里,该函数由NF_IP_FORWARD过滤点切入到Netfilter框架,在nf_hooks[2][2]过滤点进行匹配查找,最后根据返回值来确定ip_forward_finish函数的执行情况.

NF_IP_POST_ROUTING

net/ipv4/ip_output.c中的ip_output函数,它切入Netfilter框架的切入点是:

1
NF_HOOK_COND(PF_INET,NF_IP_POST_ROUTING,skb,NULL,dev,ip_finsish_output,!(IPCB(skb)->flasg & IPSKB_REROUTED));

如果需要陷入Netfilter框架则数据包会在nf_hooks[2][4]过滤点去进行匹配查找.

NF_IP_LOCAL_IN

net/ipv4/ip_input.c中的ip_local_deliver函数处理所有目的地是本机的数据包.其切入函数是:

1
NF_HOOK(PF_INET,NF_IP_LOCAL_IN,skb,skb-<dev,NULL,ip_local_deliver_finish);

发给本机的数据包,首先全部回去nf_hooks[2][1]过滤点上检测是否有相关数据包的回调处理函数,如果有则执行匹配和动作,最后根据返回值执行ip_local_dliver_finish函数.

NF_IP_LOCAL_OUT

net/ipv4/ip_output.c中的ip_push_pending_frames函数. 该函数是将IP分片重组成完整的IP报文,然后发送出去.进入Netfilter框架的切入点为:

1
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, skb->dst->dev, dst_output);

对于所有从本机出发的报文都会首先去Netfilter的nf_hooks[2][3]过滤点去过滤.一般情况下来说,不管是路由器还是PC端,很少有人限制自己机器发出去的报文.因为这样做的潜在风险也是显而易见的,往往会因为一些不恰当的设置导致某些服务失效,所以在这个过滤点上拦截的数据包的情况非常少.当然也不排除特殊情况.

连接跟踪

连接跟踪系统使得iptables基于连接上细纹而不是单个包来作出规则判断,给iptables提供了有状态操作的功能.
跟踪系统包将和已有的链接继续你比较,如果包所属的连接已经存在就更新连接状态,否则就创建一个新连接.如果 raw table的某个chain 对包比较为目标是 NOTRACK,那这个包会跳过链接跟踪系统.

连接的状态

连接跟踪系统中的连接状态有:

  • NEW: 如果到达的包关联不到任何已有的链接,但包是合法的,就为这个包创建一个新连接.对 面向连接(connection-aware)的协议例如TCP以及非面向连接的(connectionless)的协议例如UDP都适用.
  • ESTABLISHED: 当一个连接收到应答方向的合法包时,状态从NEW变成ESTABLISHED. 对TCP这个合法包其实就是SYN/ACK包;对UDP和ICMP是源和目的IP与原包相反的包
  • REALTED: 包不属于已有的连接,但是和已有的连接有一定的关系,这可能是辅助连接(helper connection),例如FTP数据传输连接,或者是其他协议试图建立链接时的ICMP应答包.
  • INVALID: 包不属于已有连接,并且因为某些元应不用来创建一个新连接,例如无法识别,无法路由等等.
  • UNTRACKED: 如果在raw table中标记为目标是UNTRACKED,这个包将不会进入到连接跟踪系统.
  • SNAT: 包的元地址被NAT修改之后会进入的虚拟状态.连接跟踪系统据此在收到反向包时对地址做方向转换.
  • DNAT: 包的目的地址被NAT修改之后会进入的虚拟状态.连接跟踪系统据此在收到反向包是对地址做反向转换.

这些状态可以定位到连接生命周期内部,管理员可以编写出更加细粒度,适用范围更大,更安全的规则.下篇文章会对这些状态进行详细地讲解。

题外话

上周六去杭州参加了学长的婚礼。祝福学长找到了生命中的另一半,从此过上幸福美满的生活,事业和家庭双丰收。

参考

netfilter/iptables 简介
(一)洞悉linux下的Netfilter&iptables:什么是Netfilter?
Netfilter/Iptables入门
Netfilter/iptables简介
Linux数据包路由原理、Iptables/netfilter入门学习