iptables深入解析:filter篇

之前大致写过iptables即netfilter的基本框架,在安全领域用到的比较多,一般小的设备防火墙,也有自己开发的比如思科的ACL,简单的包过滤什么的,都可以支持,但是它最大的缺点就是影响性能,或许我们需要借鉴tcpdump/wireshark的机制(它们都包含了对报文的解析) ,现在流行的dpi 什么的, 甚至工控领域也用到了。虽然它没有那么完美,但是框架比较好,而我们可以去改进和定制它.
参考:iptables1.4.21 kernel 3.8.13

iptables的机制分两个部分:
1. 用户空间的iptables工具
2. 内核netfilter机制的支持

它分ipv4部分和ipv6部分,这里只分析ipv4部分.代码目录(内核):
net/ipv4/netfilter
net/ipv6/netfilter
net/netfilter

这里先分析内核部分。
我们都知道netfiter分四个基本模块
1. Ct 链接追踪
2. Filter 过滤
3. Nat 地址转换
4. Mangle 修改数据报文

CT是基础核心模块是状态防火墙和nat的基础. 而其他模块会维护一个全局表,也即我们用iptables命令的时候需要指定的表。
它们工作在内核的五个钩子点上,即链的概念.

对于上面的模块在实际代码中是相对独立的初始化的:
net/ipv4/netfilter/
1.iptable_filter.c –>iptable_filter_init
2. iptable_mangle.c –>iptable_mangle_init
3.iptable_nat.c –>iptable_nat_init
4.iptable_raw.c –>iptable_raw_init
5.iptable_security.c —>iptable_security_init

而关于链接追踪它是其他的基础,比较特殊。
Net/netfilter/nf_conntrack_core.c
nf_conntrack_init
又被nf_conntrack_standalone.c

register_pernet_subsys这个函数不多说,它是内核命名空间注册子系统的一个接口.
对于内核netfilter那么多代码,想简单理清一个框架思路,首先还是要看makefile:
1. Net/netfilter/Makefile

首先是一些基础核心的代码。然后
1. Netfilter_netlink接口相关的
2. 链接追踪支持的协议l3/l4等:nf_conntrack_l4proto_register、nf_conntrack_l3proto_register
3. 链接追踪的 helpers:nf_conntrack_helper_register (主要关联连接,其他ct的识别等)
4. 其他就是match注册和targets注册
5. 核心CT的初始化等.
6. nat和ct相关的基础。
7.工具ipset、ipvs

那么net/ipv4/netfilter/Makefile呢?
它主要针对ipv4协议的处理:
1. Nat相关的helpers和协议注册
2. 链接追踪
3. Filter、mangle、nat、raw、security实例
4. Matches和targets的注册
5. Arp其他一些东西.

做了一个简单的了解,那么就从filter说起,要使配置生效比如filter那么它会从内核调用ipt_do_table查询rules配置并触发target。

具体先看filter初始化流程:
先看iptable_filter.c中的初始化函数:
iptable_filter_init
注册钩子需要注册函数接口:nf_register_hooks

很明显filter只在local_in /local_out/ forward三个钩子,并且它们的钩子函数最终都调用了

那么我们就从ipt_do_table函数分析吧
关于参数传递除了struct xt_table需要说明下外其他不需要说明了吧
1. NF_HOOK的调用查询hook函数
elem = &nf_hooks[pf][hook];
找到对应的nf_hook_ops
2. hook函数的调用
对比2.6.32和3.8.13已结有了较大的改动,不过原理一样。
在iptable_filter.c中 filter的钩子函数已经统一成一个函数了即

对于ipt_do_table最需要我们关心的就是net->ipv4.iptable_filter这个东西从哪里来的,当然我们知道它就是存放rules的地方
从搜索的内核代码来看:

上述代码的12,13 行初始化了它,而net则是inet层初始化的时候创建的全局变量.
net->ipv4.iptable_filter的类型是struct xt_table *
用struct xt_table表示,每一个功能模块都需要维护一个表,注册API:ipt_register_table(它是xt_register_table的封装)
Include/linux/netfilter中X_tables.h

还有另外一个重要的结构体: struct xt_table_info *private;
当然关于tables的rules需要用户空间来下发配置。以及注册match和target相关的东西.关于rules的结构和表的关系后续我们会讲到.
关于全局表的维护:通过API接口注册的表都挂到了net->xt.tables中.
xt的类型是struct netns_xt:

还有另外一个全局变量Struct xt_af xt;

有了表,我们看看match和target的注册:match和target放到了xt.match 和xt.target中(和之前内核不太一样了表和match/target已经分开处理了)
list_add(&match->list,&xt[af].match);
注册接口:
xt_register_table
xt_register_match
xt_register_target

对应的结构体:
Struct xt_table
Struct xt_match
Struct xt_target

那么规则呢? Rules呢?我们先看一段注释说明:
/* This structure defines each of the firewall rules. Consists of 3 parts which are
1) general IP header stuff
2) match specificstuff
3) the target to perform if the rule
matches */
struct ipt_entry {

我们继续看ipt_do_table函数
找到第一个ipt_entry然后调用ip_packet_match(五元组匹配模式开启!知道找到匹配的才结束)
比较ip头和 ipt_entry->ipt_ip
不匹配则返回false
1. 比较sip 和dip
然后比较入接口和出接口名字名字
2. 协议的比较
3. 判断IPT_F_FRAG
找到匹配的entry

接着调用entry里的match函数
Ipt_entry->elems :match and target

? ipt_entry:标准匹配结构,主要包含数据包的源、目的IP,出、入接口和掩码等;
? ipt_entry_match:扩展匹配。一条rule规则可能有零个或多个ipt_entry_match结构;
? ipt_entry_target:一条rule规则有且仅有一个target动作。就是当所有的标准匹配和扩展匹配都符合之后才来执行该target.
具体代码:

那么match函数从哪里来?Ipt_entry又在什么赋值的呢?后续我们会看到答案.

如果match匹配那么调用相应的target,这里不要把类似struct xt_match 和struct xt_entry_match搞混了。

例:

对于现在来说,它通过ipt_entry找到rules;关于rules的组成部分

Ipt_entry+ipt_entry_match+ …+target

但是iptables如何传递过来的呢?

又是如何和已经注册的match和target关联起来的呢?上面我们知道了内核通过表找到需要的rules然后解析匹配动作,还没有和应用联系起来,是时候统一一下了.

我们先看一条简单的命令比如过滤tcp协议:
iptables -A OUTPUT -p tcp –dport 31337 -j DROP

我们找到iptables1.4.21的源代码:
iptables命令的主函数在iptables-standalone.c
Iptables_main

而它又被封装了一层在xtables-multi.c:

编译过iptables后,查看sbin下iptables命令都软连接到了xtables-multi。
先看主函数:

我们看到命令行解析是在do_command4里

而opts是什么?
#define opts iptables_globals.opts
还有在之前初始化的时候:xt_params = &iptables_globals;

这个结构体包含什么呢?

关键点就是struct option,既然上面orig_opts和opts指向相同的地方,而xt_params又指向iptables_globals

那么可能好奇struct option了是什么玩意,我们来看一下:
原型在getopt.h中

因为getopt_long里要用到,对这个函数不熟悉的,可以自己编个小程序测试下用法.加深理解。

补充说明:

extern char *optarg; //选项的参数指针

extern int optind, //下一次调用getopt的时,从optind存储的位置处重新开始检查选项。

extern int opterr, //当opterr=0时,getopt不向stderr输出错误信息。

extern int optopt; //当命令行选项字符不包括在optstring中或者选项缺少必要的参数时,该选项存储在optopt中,getopt返回’?’

1.单个字符,表示选项,

2.单个字符后接一个冒号:表示该选项后必须跟一个参数。参数紧跟在选项后或者以空格隔开。该参数的指针赋给optarg。

3 单个字符后跟两个冒号,表示该选项后必须跟一个参数。参数必须紧跟在选项后不能以空格隔开。该参数的指针赋给optarg。(这个特性是GNU的扩张)。

4 optind 下个参数选项的索引,可以通过argv[optind]查看每解析一个选项optind就会加1.

getopt_long根据传递的opt来解析长字符串参数.

对于单字母的参数很容易解析出来,但是类似–dport由于longstring里没有对应的说明那么就会进入default处理。

对于tcp相关的dport处理代码在libxt_tcp.c:tcp match的注册

在注册match的时候xtables_register_match
/* place on linked list of matches pending
full registration */
me->next= xtables_pending_matches;
xtables_pending_matches= me;

这就是参数的解析过程,不论什么参数都是这样解析,那么解析完如何把rules传递给内核呢?

我们在回顾一下iptables配置的命令:

iptables -A OUTPUT -p tcp –dport 31337 -j DROP
下面我们就逐一跟踪命令的解析过程:
(需要说明的是默认是filter表,-t filter)
1. 默认初始化char *table = “filter”;
2. Do_command4命令解析之getopt_long

这个自测过,对于-开头的命令可以直接解析,但是对于–的就需要进入default处理流程。

1> 第一个命令 -A OUTPUT

add_command即把command赋值为CMD_APPEND
cs.invert为0
Chain 为 OUTPUT
或许我们需要回顾下getopt_long

根据上面的初始化值来解析参数A,明显has_arg=1表示A后面跟一个参数即OUTPUT

2>第二个命令-p tcp

cs.options = OPT_PROTOCOL;
cs.protocol = optarg; // optarg为tcp,赋值给cs.protocol
cs.fw.ip.proto =xtables_parse_protocol(cs.protocol); // IPPROTO_TCP
xtables_parse_protocol判断这个协议是否支持
return xtables_chain_protos[i].num;返回协议号
协议初始化在libxtables中xtables.c

Cs.fw.ip.proto值为IPPROTO_TCP

3>. 第三个命令–dport 31337
进入command_default流程后
首先判断cs->target是否为null,
然后判断cs->matches是否为null;点击(此处)折叠或打开

当然第一次处理直接进入load_proto
会根据协议名字struct xtables_match * xtables_find_match(const char *name, enum xtables_tryload tryload, struct xtables_rule_match **matches)
对cs.matchs初始化为tcp match

在xtables_find_match中分两个部分:

完成了cs.matchs初始化为tcp match(cloned)。
找到tcp注册的match之后 ,申请xt_entry_match节点(xtables_calloc),调用xs_init_match(m);
对于tcp match就是:

然后调用xtables_merge_options把tcp match的ext_opt复制到全局的opts中。optind– ,回退重新处理–dport。
重新进入command_default:由于cs.matchs不为null

进入xtables_option_mpcall解析

m->parse即之前已经注册tcp_match中的tcp_parse
由于之前已经把tcp扩展的options添加到了全局表中,所以重新解析后cs.c值为2,optarg即我们传递的端口号

struct xt_tcp *tcpinfo = (struct xt_tcp *)(*match)->data; 和optarg
记得之前找到match后会对xt_entry_match初始化data强制转换成xt_tcp结构指针类型初始化(柔性数组动态扩展)
parse_tcp_ports解析端口信息,如果是数字字符串则转换成数字赋值,如果不是则以它为名字查询服务获得服务的端口号(查询/etc/services通过getservbyname接口)
(1)cs->matchs =xtables_malloc(sizeof(struct xtables_rule_match));
(2)cs->matchs->match = tcp_match; // cloned
(3) cs->matchs->match->m->data 赋值 (dport)

4>. 接着处理第4个参数 –j DROP
cs->options | = OPT_JUMP;
Cs->jumpto=optarg (即DROP) –> “standard”
cs->target = target_standard (cloned) // 跟match find 几乎一样的处理流程。 struct xtable_target
在libxt_standard.c

cs->target->t =xtables_calloc(1, size); 申请空间。 // struct xt_entry_match
cs->target->t赋值strcpy(cs->target->t->u.user.name, cs->jumpto)等。
终于几个参数初步解析完了,其他参数解析也类似。看后续的代码:
一些安全检查后
Shostnetworkmask 、Dhostnetworkmask 初始化为0.0.0.0/0 默认
iptables有参数-d,可以指定网址,比如 iptables -A OUTPUT -d www.baidu.com -j DROP
则会对Dhostnetworkmask赋值处理和解析。
主要是根据主机名解析所有的ip地址,使用了gethostbyname函数. 这只是一个小插曲.
到这里需要注意:

系统初始化时handle为null,*table为“filter”:

在Libip4tc.c中:iptc_init
#define TC_INIT iptc_init
1.建立了socket(TC_AF, SOCK_RAW, IPPROTO_RAW);
2.fcntl(sockfd, F_SETFD, FD_CLOEXEC)
3.getsockopt(sockfd, TC_IPPROTO, SO_GET_INFO, &info, &s)

info结构信息:

它和内核的struct xt_table_info很相似

关于getinfo的操作,它下发到内核获取信息:

4.创建handle,并初始化。
5.getsockopt(h->sockfd, TC_IPPROTO, SO_GET_ENTRIES, h->entries,&tmp) 获取entires信息

一开始认识info 和entries应该为空,其实错了,在初始化filter表的时候,即在iptable_filter_net_init里面做了重要工作:

 

很有意思的是上面这个函数:

为什么这么说,还记得iptcc_find_label这个函数吗?它去查询handle->chains 而在TC_INIT的时候,从内核获取info和entries之后通过parse_table赋值给handle.
补充几个结构体:

 

 

下面看看filter表初始化的时候做了什么工作:

Entry主要初始化了target_offset为ipt_entry大小 和 next_offset 为外加target
Target初始化了name 为XT_STANDARD_TARGET 和target_size
总共初始化了三个entry根据filter的hooknum

在xt_register_table的时候把struct xt_table_info *newinfo;赋给table->private
这也就是为什么用户空间为一开始获取内核的entries。然后才能顺利开展后续工作.
回到主函数命令处理:

然后进入生成entry阶段,这里比较关键,它初始化了iptables table ,后续很重要的部分:

因为它用代码说明了rules的组成:

Ipt_entry+ xt_entry_match+…+xt_entry_target (…表示match可以不止一个,但target只有一个)
申请新的ipt_entry *e空间,这里把ipt_entry->ip 赋值,复制entry_matchs和entry_target.
接着是对command的处理,

append_entry的参数的传递这里就不解释了,前面都分析完了.

1.ip初始化 2.iptc_append_entry. 这个函数很好理解,就是附加entry.

处理完命令 ,然后释放掉原来的空间.

在完成do_comand4后,自然是要提交我们的东西到内核里

即通过ipt_commit函数

原理是利用Setsockopt下发配置,关于setsockopt这里就不详细讲解了.

1 2 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部