iptable的基本工作原理
netfilter在IP协议栈中有五个hook点,分别是:NF_IP_PRE_ROUTING
、NF_IP_LOCAL_IN
、NF_IP_FORWARD
、NF_IP_LOCAL_OUT
、NF_IP_POST_ROUTING
。netfilter允许其它内核模块往这些hook点上注册回调函数,当数据流过IP协议栈时,那些回调函数便会被调用,iptable就是基于netfilter来实现的。
内核的iptable模块会构建四张表,它们分别是raw、mangle、nat、filter表,这四张表分别由四个小模块实现,四个表的实现模块会往netfilter在IP协议栈的hook点上注册回调,其中:
- raw表会在
NF_INET_PRE_ROUTING
和NF_INET_LOCAL_OUT
注册 - mangle表会在
NF_IP_PRE_ROUTING
、NF_IP_LOCAL_IN
、NF_IP_FORWARD
、NF_IP_LOCAL_OUT
、NF_IP_POST_ROUTING
注册 - nat表会在
NF_IP_PRE_ROUTING
、NF_IP_LOCAL_IN
、NF_IP_LOCAL_OUT
、NF_IP_POST_ROUTING
注册 - filter表会在
NF_IP_LOCAL_IN
、NF_IP_FORWARD
、NF_IP_LOCAL_OUT
注册
iptable的表中有一个’链’的概念,对应于netfilter的五个hook点,iptable总共有PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING五种链,表往NF_INET_PRE_ROUTING
链上注册了回调那么它就会有PREROUTING链,表往NF_IP_LOCAL_IN
链上注册了回调那么它就会有INPUT链,表往NF_IP_FORWARD
链上注册了回调那么它就会有FORWARD链,表往NF_IP_LOCAL_OUT
链上注册了回调那么它就会有OUTPUT链,表往NF_IP_POST_ROUTING
链上注册了回调那么它就会有POSTROUTING链。
在应用层可以通过iptables工具往四个表的链上添加规则,比如说往filter表的INPUT链上添加了规则,那么数据包在经过NF_IP_LOCAL_IN
这个hook点时,filter表注册的回调函数被调用,filter表的回调函数判断出当前的hook点是NF_IP_LOCAL_IN
,那么它就会去执行INPUT链上的规则。
接下来通过一个简单的例子来感受数据包经过netfilter的hook点时iptable执行规则的过程,假设filter表的规则如下:
Chain INPUT (policy ACCEPT)
target prot opt source destination
test_chain udp -- anywhere anywhere
Chain FORWARD (policy DROP)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Chain test_chain (1 references)
target prot opt source destination
RETURN udp -- anywhere anywhere udp dpt:8888
ACCEPT udp -- anywhere anywhere udp dpt:6666
DROP udp -- anywhere anywhere udp dpt:7777
filter表中除了三个主链外(INPUT\FORWARD\OUTPUT),还有一个自定义链(test_chain),当数据包经过NF_IP_LOCAL_IN
这个hook点时,filter表的回调被调用,开始执行INPUT链中的规则,对于上面的例子而言执行过程是这样的:
- INPUT链的第一条规则被执行,它判断ip报文的协议字段是否是udp,如果是则跳转到test_chain去执行,如果不是则执行INPUT链的下一条规则(INPUT链已经没有其它规则就执行默认规则,上面的例子中INPUT链的默认规则是接收报文
policy ACCEPT
) - 假设该报文是udp报文,跳转到test_chain执行第一条规则,若udp报文的目的端口为8888则返回(
target RETURN
)到INPUT链去执行下一条规则,若不是则执行test_chain的第二条规则 - 假设执行test_chain的第二条规则,若报文目的端口为6666则结束当前的iptable执行过程,返回
NF_ACCEPT
给netfilter,告知netfilter当前表允许该报文通过,若目的端口不是6666则执行test_chain的第三条规则 - 假设执行test_chain的第三条规则,若报文目的端口为7777则结束当前的iptable执行过程,返回
NF_DROP
给netfilter,告知netfilter丢弃该报文,若目的端口不是7777则执行test_chain的默认规则,自定义链的默认规则是返回到调用处(target RETURN) - 如果从test_chain返回到了INPUT链,或者INPUT链的第一条规则没有被匹配上,执行INPUT链的下一条规则,INPUT链已经没有其它规则就执行它的默认规则,结束当前的iptable执行过程,返回
NF_ACCEPT
给netfilter,告知netfilter当前表允许该报文通过
netfilter根据filter表的执行结果来做不同操作,如果是NF_DROP
,netfilter就会丢弃该报文,如果是NF_ACCEPT
,netfilter会继续执行NF_IP_LOCAL_IN
这个hook点的其它回调函数(可能不止iptable在该hook点上注册了回调),当所有回调都返回了NF_ACCEPT
时,netfilter才会真正的放行该报文。
iptable 规则的构成
iptable规则主要由匹配条件(match)和动作(target)两部分构成,匹配条件满足时就会执行动作。匹配条件分为标准匹配条件和扩展匹配条件两类,动作也分为标准动作和扩展动作两类。
标准匹配条件
目前能够通过iptables命令配置的标准匹配条件包括下面这些:
-s: 匹配源ip,例如-s 192.168.0.l, -s 192.168.0.0/24
-d: 匹配目的ip,例如-d 192.168.1.l, -d 192.168.1.0/24
-p: 匹配ip报文的协议字段,例如 -p tcp, -p udp (icmp,sctp,all,udplite...)
-i: 匹配报文输入接口,例如 -i eth1
-o: 匹配报文输出接口,例如 -o br-lan
-f: 匹配ip分片
!:反向匹配,例如 !-p tcp 表示不匹配tcp报文
扩展匹配条件
iptables指令指定扩展匹配条件时,需要先使用-m
指定扩展模块,然后再指定扩展模块的参数,例如-m mark --mark 0x1/0xff
,不同的扩展模块有不同的参数,具体有哪些参数可以参考iptables的帮助手册(man 8 iptables-extensions
),或者直接参考iptables命令的帮助信息: iptables -m <match> -h
(match
是扩展模块的名字)。
一条iptable规则可以配置多个扩展条件,例如iptables -A INPUT -m mark --mark 0x1/0xff -p udp -m udp --dport 6666 -j ACCEPT
。
有的扩展模块需要使用-p
指定了协议后才可以使用,例如-p udp -m udp --dport 6666
, -p icmp -m icmp --icmp-type echo-reply
在应用层iptables的代码中,不同的扩展匹配条件的解析代码是放在不同的c文件中的,例如mark
这个匹配条件的解析代码就在libxt_mark.c
文件中,其它的匹配条件也有它们对应的 libxt_***.c
或者libipt_***.c
文件。同时在内核中,不同的匹配条件也是由不同的模块实现的,它们分散在xt_***.c
文件中(比如xt_mark.c
)。
标准动作
目前能够通过iptables命令配置的标准动作包括下面这些:
-j ACCEPT: 不再继续执行当前表中在当前hook点的规则,返回NF_ACCEPT给netfilter,告知netfilter当前表允许报文通过
-j DROP: 不再继续执行当前表中在当前hook点的规则,返回NF_DROP给netfilter,告知netfilter当前表不允许报文通过,需要丢弃该报文
-j <chain_name>: 除了PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING这五个主链,我们可以给iptable创建自定义链,也可以在自定义链中添加规则,iptable规则使用-j <chain_name>就可以跳转到自定义链去执行
-j RETURN: 不再继续执行当前链中的规则,如果当前链是自定义链那就返回到调用当前链的地方去执行下一条规则,如果当前链已经是主链,那就执行主链的默认规则,然后将结果返回给netfilter
-j QUEUE: 不再继续执行当前表中在当前hook点的规则,返回NF_QUEUE给netfilter, 告知netfilter将报文传递给应用层的进程去处理
扩展动作
iptables指令指定扩展动作时,需要先使用-j
指定扩展模块,然后再指定扩展模块提供的动作,例如-j CONNMARK --save-mark
,不同的扩展模块有不同的动作,具体有哪些动作可以参考iptables的帮助手册(man 8 iptables-extensions
),或者直接参考iptables命令的帮助信息: iptables -j <target> -h
(target
是扩展模块的名字)。
每条iptable规则可以有多个匹配条件,但只能有一个动作,这个动作可以是标准动作也可以是扩展动作。
内核描述iptable规则的结构体
内核和应用层的iptables程序都使用struct ipt_entry
类型的结构体变量来描述iptable规则,它的成员如下:
// uapi/linux/netfilter_ipv4/ip_tables.h
struct ipt_entry {
struct ipt_ip ip; // 记录标准的匹配条件
/* Mark with fields that we care about. */
unsigned int nfcache;
/* Size of ipt_entry + matches */
__u16 target_offset; // 描述target所处偏移值
/* Size of ipt_entry + matches + target */
__u16 next_offset; // 下一条iptable规则的偏移值
/* Back pointer */
unsigned int comefrom;
/* Packet and byte counters. */
struct xt_counters counters;
/* The matches (if any), then the target. */
unsigned char elems[0]; // 紧跟着ipt_entry的就是iptable规则的扩展match和target
};
iptable规则的标准匹配条件存储在一个struct ipt_ip
类型的结构变量中,它的成员如下:
struct ipt_ip {
/* Source and destination IP addr */
struct in_addr src, dst; // 源,目的地址
/* Mask for src and dest IP addr */
struct in_addr smsk, dmsk; // 源,目的地址的掩码
char iniface[IFNAMSIZ], outiface[IFNAMSIZ]; // 输入输出接口
unsigned char iniface_mask[IFNAMSIZ], outiface_mask[IFNAMSIZ];
/* Protocol, 0 = ANY */
__u16 proto; // ip协议
/* Flags word */
__u8 flags;
/* Inverse flags */
__u8 invflags; // 反向匹配标记,比如iptable规则中有'! -p udp'这样一个匹配条件,那么就会给invflags位或上IPT_INV_SRCIP(invflags |= IPT_INV_SRCIP),反向匹配什么就会给invflags位或上什么标置位(IPT_INV_SRCIP,IPT_INV_DSTIP,IPT_INV_VIA_IN,IPT_INV_VIA_OUT,具体有哪些标记可以参考iptables-1.8.7/iptables/xtables.c文件的inverse_for_options数组)
};
因为每条iptable规则的扩展匹配条件的个数、匹配条件的大小以及动作的大小都是不确定的,所以在struct ipt_entry
结构中并没有成员直接描述匹配条件和动作,创建iptable规则时会根据扩展匹配条件和动作所占用的空间来分配内存,分配的内存中前面的部分是一个struct ipt_entry
类型的结构体变量,紧跟着ipt_entry
的是扩展匹配条件和动作,大致如下图所示,通过ipt_entry::elems
就能访问到该规则的匹配条件。
描述扩展匹配条件的结构体
内核和应用层的iptables程序使用struct xt_entry_match
类型的结构体变量来描述扩展匹配条件,它的成员如下:
// uapi/linux/netfilter/x_tables.h
struct xt_entry_match {
union {
struct {
__u16 match_size; // 该匹配条件所占用的内存空间大小,包括sizeof(struct xt_entry_match) + data size
/* Used by userspace */
char name[XT_EXTENSION_MAXNAMELEN]; // match模块(扩展匹配模块)的名字,应用层将iptable规则传递到内核前,需要将match模块的名字填充在这里,内核会根据match模块的名字去查找合适内核模块
__u8 revision;
} user; // user由应用层的iptables程序使用
struct {
__u16 match_size; // 该匹配条件所占用的内存空间大小,包括sizeof(struct xt_entry_match) + data size
/* Used inside the kernel */ // 内核在收到应用层传递过来的iptable规则后,会根据match模块的名字去查找对应的内核match模块,内核match模块会向iptable核心部分注册一个struct xt_match类型的结构体变量,这里这个match指针就是用来记录xt_match的
struct xt_match *match;
} kernel; // kernel由内核使用
/* Total length */
__u16 match_size;
} u;
unsigned char data[0]; // 不同类型的匹配条件,它们使用的私有数据是不同的,给iptable规则分配xt_entry_match时会根据匹配条件所需的私有数据尺寸来分配内存,私有数据紧随着xt_entry_match存放,这里的data就是用来访问私有数据的符号
};
在使用iptables规则时,只要有一个-m xxxx
就会产生一个xt_entry_match
,而不同类型的match会有不同大小的私有数据,比如说mark
,它的私有数据会放在一个struct xt_mark_mtinfo1
类型的结构体变量中。假如iptable规则使用了mark
这个match,那么分配的用于存储该match的内存总大小为sizeof(struct xt_entry_match) + sizeof(struct xt_mark_mtinfo1)
。
描述动作的结构体
内核和应用层的iptables程序都使用struct xt_entry_target
类型的结构体变量来描述标准和扩展动作,它的成员如下:
// uapi/linux/netfilter/x_tables.h
struct xt_entry_target {
union {
struct {
__u16 target_size; // 该动作所占用的内存空间大小,包括sizeof(struct xt_entry_target) + data size
/* Used by userspace */
char name[XT_EXTENSION_MAXNAMELEN]; // target模块的名字,应用层将iptable规则传递到内核前,需要将target模块的名字填充在这里,内核会根据target模块的名字去查找合适内核模块
__u8 revision;
} user; // user由应用层的iptables程序使用
struct {
__u16 target_size; // 该动作所占用的内存空间大小,包括sizeof(struct xt_entry_target) + data size
/* Used inside the kernel */
struct xt_target *target; // 内核在收到应用层传递过来的iptable规则后,会根据target模块的名字去查找对应的内核target模块,内核target模块会向iptable核心部分注册一个struct xt_target类型的结构体变量,xt_target中会提供一个重要的回调函数,当执行iptable规则时,若iptable规则与ip报文匹配,那么xt_target的回调就会被调用。这里这个target就是用来记录xt_target的指针。
} kernel; // kernel由内核使用
/* Total length */
__u16 target_size;
} u;
unsigned char data[0]; // 与match类似,不同的target有不同的私有数据,target的私有数据紧随着xt_entry_target存放,这里这个data是用来访问私有数据的符号
};
特殊的target
iptable有多种类型的target,有两种是比较独特特:一是xt_standard_target
(用于描述标准动作),二是xt_error_target
(用于描述自定义链)。
xt_standard_target
的结构体成员如下:
// uapi/linux/netfilter/x_tables.h
struct xt_standard_target {
struct xt_entry_target target;
int verdict;
};
如果一条iptable规则它使用的是标准动作,那么它就会用xt_standard_target::verdict
来记录标准动作,大致的规律如下:
target | xt_standard_target::verdict |
---|---|
-j ACCEPT | -NF_ACCEPT - 1 |
-j DROP | -NF_DROP - 1 |
-j QUEUE | -NF_QUEUE - 1 |
-j RETURN | -NF_REPEAT-1 |
-j chain_name | 自定义链所处位置 |
上表中的NF_ACCEPT
,NF_DROP
,NF_QUEUE
,NF_REPEAT
它们定义于inluce/uapi/linux/netfilter.h
文件,它们是netfilter的标准返回值,都是正值,所以当动作为ACCEPT|DROP|QUEUE|RETURN
时,xt_standard_target::verdict
就是负值,这是为了与-j chain_name
这种情况做区分。当iptable的动作是跳转到自定义链时,xt_standard_target::verdict
就会记录自定义链的与表头部地址的偏移值,这样内核通过xt_standard_target::verdict
便可以找到自定义链所在地址。
当iptable规则使用标准动作时,xt_entry_target::user::name
这个变量的取值有点特殊,是“”(空字符串),并且标准动作对应的xt_target
是没有提供回调的。
xt_error_target
的结构体成员如下:
// uapi/linux/netfilter/x_tables.h
struct xt_error_target {
struct xt_entry_target target;
char errorname[XT_FUNCTION_MAXNAMELEN];
};
iptable的自定义链其实也是用ipt_entry
来描述的,只不过它会使用xt_error_target
这种类型的target,xt_error_target::errorname
存储的就是自定链的名字,对于自定义链而言,xt_entry_target::user::name
是ERROR
。
iptable 表的结构
假设filter表当前的链和规则如下:
# iptables -t filter -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy DROP)
target prot opt source destination
test_chain udp -- anywhere anywhere udp dpt:6666
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Chain test_chain (1 references)
target prot opt source destination
表中有五个主链,只有一个自定义链test_chain
,只有FORWARD
链上有一条规则,此时的filter表的逻辑结构将如下图所示:
xt_table
是内核用来描述iptable表的数据结构,上图中,xt_table::valid_hooks
告诉我们filter表在哪些hook点上注册了回调,xt_table::private
指针指向了一个struct xt_table_info
类型的结构体变量,它记录着主链所处位置,紧随xt_table_info
尾部的就是iptable的规则(ipt_entry
),对于上图需要留意这些地方:
- iptable表的规则不是以链表的形式组织起来的,它们就是一个挨着一个排列的,排在最前面的是那五个主链的规则,随后才是自定义链的规则
- 每个链都有一条默认规则,每个表尾部都隐藏着一条target为
xt_error_target
的ipt_entry
来标记表的结束位置,每个自定义链都有一条target为xt_error_target
的ipt_entry
来标记链的起始位置,所以上面的filter表才会存在7个ipt_entry
(四个链的默认规则+标志test_chain链的起始位置的ipt_entry
+标志filter表的结束位置的ipt_entry
+FOWARD链的第一条规则) xt_table_info::hook_entry[]
这个数组中记录的是PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING
这些主链的第一条规则所处偏移值,若链中没有添加规则,那xt_table_info::hook_entry[]
就记录着默认规则所处偏移值xt_table_info::underflow[]
这个数组记录的是PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING
这些主链的默认规则所处偏移值- 自定义链的默认规则的target是RETURN,不能修改。
PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING
这几个主链的默认规则的target是ACCEPT,但可以通过iptables -P INPUT DROP
指令来修改,有的表也可以通过参数来配置主链的target(比如filter表的内核模块提供一个名为forward
的参数来配置FORWARD
链的默认target)。 - 上图中
ipt_entry 0\1\2\3\5
都是用的标准动作,标准动作不会提供target回调(target.kernel.target->target = NULL
)。实际的iptable表中有很多使用扩展动作的ipt_entry
,它们使用的target模块会提供target回调。 ipt_entry 4
和ipt_entry 6
它们分别代表test_chain
和表的尾部,它们的target回调是ipt_error
函数,自定义链和表尾部的ipt_entry
不代表任何规则,正常情况下它们是不会被执行的,一旦执行,ipt_error
函数就会报错返回。- 标志自定义链的起始位置的
ipt_entry
和标志filter表的结束位置的ipt_entry
它们的xt_entry_target::user::name
值都为ERROR
, 但它们的xt_error_target::errirname
是不同的,自定义链的xt_error_target::errirname
是链的名字,表的结束位置的ipt_entry
的xt_error_target::errirname
是ERROR
iptables -t filter -L -vv
指令可以将表的内容dump出来,可以用来观察表的结构。
代码分析
接下来从代码上分析内核初始化表的过程、应用层iptables添加规则的过程以及iptable规则的执行过程
内核初始化表的过程
在内核中,iptable的代码层级结构大概是这样的
iptable和ip6table的每个表都由一个单独的内核模块实现,每个表有一个对应的c文件,ip_tables.c
文件中是iptable的一些公用代码,x_tables.c
文件中是iptable和ip6table的公用代码。无论是iptable还是ip6table的表,都是使用struct xt_table
和struct xt_table_info
这两个类型的结构体变量来描述的,所以表的初始化过程其实就是构造xt_table
和xt_table_info
的过程,xt_table
和xt_table_info
的结构体成员如下:
// linux_**/include/linux/netfilter/x_tables.h
struct xt_table {
struct list_head list; // 用于链接到全局链表的节点
/* What hooks you will enter on */
unsigned int valid_hooks; // 在哪些hook点上有回调
/* Man behind the curtain... */
struct xt_table_info *private; // 指向xt_table_info的指针
/* Set this to THIS_MODULE if you are a module, otherwise NULL */
struct module *me;
u_int8_t af; /* address/protocol family */
int priority; /* hook order */ // 表的优先级
/* called when table is needed in the given netns */
int (*table_init)(struct net *net);
/* A unique name... */
const char name[XT_TABLE_MAXNAMELEN]; // 表的名字
};
struct xt_table_info {
/* Size per table */
unsigned int size; // 对于iptable而言,size是当前表中所有ipt_entry所占空间的大小
/* Number of entries: FIXME. --RR */
unsigned int number; // ipt_entry的数目
/* Initial number of entries. Needed for module usage count */
unsigned int initial_entries;
/* Entry points and underflows */
unsigned int hook_entry[NF_INET_NUMHOOKS]; // 记录PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING这些主链的第一条规则所处偏移值
unsigned int underflow[NF_INET_NUMHOOKS]; // 记录PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING这些主链的默认规则所处偏移值
/*
* Number of user chains. Since tables cannot have loops, at most
* @stacksize jumps (number of user chains) can possibly be made.
*/
unsigned int stacksize;
void ***jumpstack; // 执行iptable规则时,遇到那种target为'-j chain',就会跳转到指定的自定义链上去执行,这个jumpstack就是用来保存返回地址的,自定义链执行完后就通过jumpstack返回原来的规则然后执行下一条
unsigned char entries[0] __aligned(8);
};
接下来以iptable filter表为例来进行分析,iptable filter模块会静态分配一个struct xt_table
类型的结构体变量,需要注意的是这个静态分配的xt_table
并不是最终的xt_table
,它只是包含了filter表的一些关键信息,比如filter表的名字、要在哪些hook点上注册回调、回调的优先级等,这些信息在表的初始化过程中会用到。
// linux/net/ipv4/netfilter/iptable_filter.c
#define FILTER_VALID_HOOKS ((1 << NF_INET_LOCAL_IN) | \
(1 << NF_INET_FORWARD) | \
(1 << NF_INET_LOCAL_OUT))
static const struct xt_table packet_filter = {
.name = "filter",
.valid_hooks = FILTER_VALID_HOOKS, // 后面初始化过程中会根据这个valid_hooks来向netfilter注册回调,filter表只会在NF_INET_LOCAL_IN、NF_INET_FORWARD、NF_INET_LOCAL_OUT这三个hook点上注册
.me = THIS_MODULE,
.af = NFPROTO_IPV4,
.priority = NF_IP_PRI_FILTER, // filter表的优先级,可以参考linux/include/uapi/linux/netfilter/netfilter_ipv4.h的nf_ip_hook_priorities,在同一hook点上优先级从高到低分别是raw、mangle、nat、filter
.table_init = iptable_filter_table_init, // table_init是表的初始化函数,网络命名空间初始化时,它就会被调用去构造'xt_table'和'xt_table_info'
};
iptable filter模块初始化的时候iptable_filter_init
函数会被调用,它的代码如下:
// linux/net/ipv4/netfilter/iptable_filter.c
static int __init iptable_filter_init(void)
{
// 1 根据valid_hooks分配 'nf_hook_ops','nf_hook_ops'是注册到netfilter的结构,filter表需要在NF_INET_LOCAL_IN、NF_INET_FORWARD、NF_INET_LOCAL_OUT这三个hook点上注册回调,那么就会分配三个'nf_hook_ops',
// 稍后构造'xt_table'和'xt_table_info'的过程中就会把这里分配的'nf_hook_ops'注册到netfilter,注册给netfilter回调函数是iptable_filter_hook,当数据经过hook点时iptable_filter_hook函数被调用执行iptable规则
filter_ops = xt_hook_ops_alloc(&packet_filter, iptable_filter_hook);
.....
// 2 linux可以创建多个网络命名空间,网络命名空间之间是相互隔离的,路由表、iptable表以及其它很多东西在每个网络命名空间都有一份,这里的register_pernet_subsys注册的回调在网络命名空间初始化时就会被调用,
// 最终会调用到iptable_filter_table_init函数,iptable_filter_table_init就会构造属于该网络命名空间的filter表
ret = register_pernet_subsys(&iptable_filter_net_ops);
......
}
iptable_filter_table_init
函数代码如下:
// linux/net/ipv4/netfilter/iptable_filter.c
static bool forward __read_mostly = true;
module_param(forward, bool, 0000);
static int __net_init iptable_filter_table_init(struct net *net)
{
struct ipt_replace *repl;
......
// 1 根据静态构造的packet_filter分配并初始化一个struct ipt_replace 类型的结构体变量,struct ipt_replace这个结构和xt_table_info很像,稍后看看它的结构体成员
repl = ipt_alloc_initial_table(&packet_filter);
......
// 2 filter这个内核模块提供了一个foward参数,通过forward参数可以配置FORWARD链默认规则的target
((struct ipt_standard *)repl->entries)[1].target.verdict =
forward ? -NF_ACCEPT - 1 : -NF_DROP - 1;
// 3 根据packet_filter和步骤1中构造的ipt_replace来分配并初始化 'xt_table'和'xt_table_info',使用net->ipv4.iptable_filter来记录最终的'xt_table',将filter_ops注册到netfilter
err = ipt_register_table(net, &packet_filter, repl, filter_ops,
&net->ipv4.iptable_filter);
......
}
struct ipt_replace
结构体成员如下:
struct ipt_replace {
/* Which table. */
char name[XT_TABLE_MAXNAMELEN]; // 表的名字
/* Which hook entry points are valid: bitmask. You can't
change this. */
unsigned int valid_hooks; // 在哪些hook点上有回调
/* Number of entries */
unsigned int num_entries; // 表中有多少个ipt_entry
/* Total size of new entries */
unsigned int size; // 表中所有ipt_entry所占用的空间
/* Hook entry points. */
unsigned int hook_entry[NF_INET_NUMHOOKS]; // 记录PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING这些主链的第一条规则所处偏移值
/* Underflow points. */
unsigned int underflow[NF_INET_NUMHOOKS]; // 记录PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING这些主链的默认规则所处偏移值
/* Information about old entries: */
/* Number of counters (must be equal to current number of entries). */
unsigned int num_counters;
/* The old entries' counters. */
struct xt_counters __user *counters;
/* The entries (hang off end: not really an array). */
struct ipt_entry entries[0]; // 紧随ipt_replace尾部的就是iptable的规则
};
对于filter表而言,ipt_alloc_initial_table
函数会为INPUT/FORWARD/OUTPUT
这三个链分配默认的ipt_entry
,同时还会分配一个用于标志表的结束位置的ipt_entry
,分配完ipt_entry
后会给ipt_replace
各个结构体成员都赋好值,以便ipt_register_table
函数使用。
接下来看看ipt_register_table
函数是构造xt_table
和xt_table_info
、注册netfilter回调的过程:
// linux/net/ipv4/netfilter/ip_tables.c
int ipt_register_table(struct net *net, const struct xt_table *table,
const struct ipt_replace *repl,
const struct nf_hook_ops *ops, struct xt_table **res)
{
int ret;
struct xt_table_info *newinfo;
struct xt_table_info bootstrap = {0};
void *loc_cpu_entry;
struct xt_table *new_table;
// 1 分配 xt_table_info, ipt_replace中记录了所有ipt_entry占用的空间,这里分配的空间其实是sizeof(xt_table_info) + repl->size
newinfo = xt_alloc_table_info(repl->size);
// 2 将ipt_replace中的ipt_entry拷贝到xt_table_info
loc_cpu_entry = newinfo->entries;
memcpy(loc_cpu_entry, repl->entries, repl->size);
// 3 应用层的iptables工具配置了iptable后,translate_table函数也会被调用,它负责两件事:
// 一是检查xt_table_info的各个参数是否正确,比如说xt_table_info::hook_entry、ipt_entry::next_offset这些偏移值
// 二是转换ipt_entry的match和target,前面提到过,match和target的结构中有一部分参数是应用层使用的,translate_table会把他们转换为内核可以使用的参数,比如说根据match的名字('xt_entry_match::user::name')去找到match模块注册的' xt_match',然后把'xt_match'记录在'xt_entry_match::kernel::match'
ret = translate_table(net, newinfo, loc_cpu_entry, repl);
// 4 创建xt_table,将xt_table_info赋值给xt_table::private,将xt_table链接到一个全局的链表中
new_table = xt_register_table(net, table, &bootstrap, newinfo);
// 5 把flter表的xt_table记录到net->ipv4.iptable_filter,后面执行iptable时就通过net->ipv4.iptable_filter来找到filter表
WRITE_ONCE(*res, new_table);
// 6 调用netfilter提供的函数,将filter表的那三个nf_hook_ops注册到netfilter
ret = nf_register_net_hooks(net, ops, hweight32(table->valid_hooks));
......
}
iptables添加规则的过程
iptables支持很多指令,它能够给链删除、插入、追加规则,也能够删除、新建自定义链,它的代码里分支比较多,这里以iptables追加规则(iptables -A
)为例来分析,大致的过程如下:
- 初始化扩展匹配条件和扩展动作模块
- 解析iptables的命令行参数,解析
dst ip/src ip/ protocol/iif/oif
等公共匹配条件,根据扩展匹配条件或动作的名字查找扩展模块,调用扩展模块注册的回调来解析模块的私有参数 - 通过
SO_GET_INFO
和SO_GET_ENTRIES
这两个socket option从内核读取整张表,表里面的ipt_entry
是一个挨着一个紧密排列的,不方便插入或删除ipt_entry
,所以读出表后会对表进行解析,将其构造成链表的结构 - 根据解析后的命令行参数构造
ipt_entry
,将新建的ipt_entry
插入链表的合适位置 - 将链表形式的表重新编译成
ipt_entry
紧密排列的那种格式,然后通过SO_SET_REPLACE
这个socket option传递到内核,替换内核中的那张表
接下来看看iptables的代码。
扩展模块
扩展匹配条件和扩展动作这些模块分布在名为libxt_*.c
和libipt_*.c
的文件中(比如libxt_mark.c
),扩展match模块会构造一个或多个struct xtables_match
类型的结构体变量,在iptables初始化扩展模块的时候扩展模块会把xtables_match
链入到全局的链表中,以mark
模块为例:
// iptables-1.8.7/extensions/libxt_mark.c
// mark模块有两个版本,所以它构建了两个xtables_match
static struct xtables_match mark_mt_reg[] = {
{
.family = NFPROTO_UNSPEC,
.name = "mark", // 模块的名字
.revision = 0, // 版本
.version = XTABLES_VERSION,
.size = XT_ALIGN(sizeof(struct xt_mark_info)), // xt_mark_info是mark模块的私有数据结构,如果iptable规则的用了mark作为匹配条件,那么给该匹配条件实际分配的内存大小为 sizeof(xt_entry_match) + sizeof(xt_mark_info)
.userspacesize = XT_ALIGN(sizeof(struct xt_mark_info)),
.help = mark_mt_help,
.print = mark_print,
.save = mark_save,
.x6_parse = mark_parse, // 解析mark模块私有参数的回调,mark_parse会把解析后的参数放到xt_mark_info里,更新内核中表的时候就一并传递到内核
.x6_options = mark_mt_opts, // mark_mt_opts 是mark模块的私有参数列表
.xlate = mark_xlate,
},
{
.version = XTABLES_VERSION,
.name = "mark",
.revision = 1,
.family = NFPROTO_UNSPEC,
.size = XT_ALIGN(sizeof(struct xt_mark_mtinfo1)),
.userspacesize = XT_ALIGN(sizeof(struct xt_mark_mtinfo1)),
.help = mark_mt_help,
.print = mark_mt_print,
.save = mark_mt_save,
.x6_parse = mark_mt_parse,
.x6_options = mark_mt_opts,
.xlate = mark_mt_xlate,
},
};
每个扩展模块都会提供一个_init
函数,这个_init
其实是一个宏,它在编译的时候会被展开,对于mark
模块而言,它展开后就是libxt_mark_init
,iptable初始化模块时就会调用它
// iptables-1.8.7/extensions/libxt_mark.c
void _init(void)
{
// xtables_register_matches会将xtables_match链入全局的链表(xtables_pending_matches)中,稍后解析iptables命令行参数时就是通过xtables_pending_matches来找到mark模块的xtables_match
xtables_register_matches(mark_mt_reg, ARRAY_SIZE(mark_mt_reg));
}
与match模块类似,扩展target模块会构建一个或多个struct xtables_target
类型的结构体变量,然后在iptables初始化扩展模块的时候将xtables_target
链入全局链表xtables_pending_targets
,与xtables_match
类似,xtables_target
同样会提供target私有数据的大小,并提供解析函数,这里不再具体分析。
iptables追加规则的代码流程
// iptables-1.8.7/iptables/iptables_standalone.c
iptables_main(int argc, char *argv[])
{
char *table = "filter"; // 默认操作的表为filter表,可以通过-t参数指定其它表
struct xtc_handle *handle = NULL;
// 1 初始化扩展模块,调用它们的init函数
init_extensions();
init_extensions4();
// 2 从内核读取旧表解析旧表,解析命令行参数,构造ipt_entry并插入以链表形式组织的表
ret = do_command4(argc, argv, &table, &handle, false);
if (ret) {
// 将链表形式组织的表重新编译,发给内核替换旧表
ret = iptc_commit(handle);
iptc_free(handle);
}
......
}
do_command4
函数如下(省略了很多代码)
// iptables-1.8.7/iptables/iptables.c
int do_command4(int argc, char *argv[], char **table,
struct xtc_handle **handle, bool restore)
{
struct iptables_command_state cs = {
.jumpto = "",
.argv = argv,
}; // iptables_command_state用于临时存储一些参数,像protocol\iif\oif\match\target等
struct ipt_entry *e = NULL;
unsigned int nsaddrs = 0, ndaddrs = 0;
struct in_addr *saddrs = NULL, *smasks = NULL; // iptables添加规则时可以指定多个源ip和目的ip,这里的这些saddrs、smasks、daddrs、dmasks就是用来暂存源和目的ip的
struct in_addr *daddrs = NULL, *dmasks = NULL;
......
const char *chain = NULL; // 记录要操作的链的名字
const char *shostnetworkmask = NULL, *dhostnetworkmask = NULL; // shostnetworkmask 和 dhostnetworkmask分别用于记录源ip和目的ip
......
unsigned int rulenum = 0, command = 0; // command用于记录iptables指令(追加、插入、删除、新建链等)
......
//1 解析应用层参数
opterr = 0;
opts = xt_params->orig_opts;
while ((cs.c = getopt_long(argc, argv,
"-:A:C:D:R:I:L::S::M:F::Z::N:X::E:P:Vh::o:p:s:d:j:i:fbvw::W::nt:m:xc:g:46",
opts, NULL)) != -1) {
switch (cs.c) {
case 'A': // 记录iptable指令和要操作的链
add_command(&command, CMD_APPEND, CMD_NONE, cs.invert);
chain = optarg;
break;
......
case 'p': // 解析protocol,暂存于cs.fw.ip.proto,如果使用了反向匹配(例如 ! -p tcp), set_option会给那么cs.fw.ip.invflags位或上 XT_INV_PROTO
set_option(&cs.options, OPT_PROTOCOL, &cs.fw.ip.invflags, cs.invert);
/* Canonicalize into lower case */
for (cs.protocol = optarg; *cs.protocol; cs.protocol++)
*cs.protocol = tolower(*cs.protocol);
cs.protocol = optarg;
cs.fw.ip.proto = xtables_parse_protocol(cs.protocol);
......
break;
case 's': // 解析ip 源地址, 因为可能传递多个ip地址(例如-s 192.168.0.1,192.168.0.2),所以这里并没有把源ip记录到cs.fw.ip,只用cs.fw.ip.invflags记录了反向匹配的标志(若使用了'!', 则 cs.fw.ip.invflags |= IPT_INV_SRCIP)
set_option(&cs.options, OPT_SOURCE, &cs.fw.ip.invflags, cs.invert);
shostnetworkmask = optarg; // shostnetworkmask 记录着源ip
break;
case 'd': // ip 目的地址, 因为应用层可能传递多个ip地址(例如-d 192.168.0.1,192.168.0.2),所以这里并没有把目的ip记录到cs.fw.ip,只用cs.fw.ip.invflags记录了反向匹配的标志(若使用了'!', 则 cs.fw.ip.invflags |= IPT_INV_DSTIP)
set_option(&cs.options, OPT_DESTINATION, &cs.fw.ip.invflags,
cs.invert);
dhostnetworkmask = optarg; // shostnetworkmask 记录着目的ip
break;
......
case 'j': // 解析target,command_jump会做几件事:
// 如果是-j chain,则把chain名记录在cs->jumpto,不给cs->target赋值
// 如果是-j ACCEPT/DROP/RETURN/QUEUE,那么使用libxt_standard.c中的构建的'xtables_target'给cs->target赋值
// 如果是其它扩展动作,那就用扩展动作对应的'xtables_target'给cs->target赋值
// 如果cs->target不为空,则分配一个 xt_entry_target (分配的内存大小为sizeof(xt_entry_target) + sizeof(target私有数据结构)),将target的名字记录到'xt_entry_target::user::name'(标准动作的name有点特殊,叫做"standard")
set_option(&cs.options, OPT_JUMP, &cs.fw.ip.invflags,
cs.invert);
command_jump(&cs, optarg);
break;
case 'i': // 解析输入接口,暂时存放在cs.fw.ip.iniface,若使用了反向匹配标志, 则 cs.fw.ip.invflags |= IPT_INV_VIA_IN
set_option(&cs.options, OPT_VIANAMEIN, &cs.fw.ip.invflags,
cs.invert);
xtables_parse_interface(optarg,
cs.fw.ip.iniface,
cs.fw.ip.iniface_mask);
break;
case 'o': // 解析输出接口,暂时存放在cs.fw.ip.outiface,若使用了反向匹配标志, 则 cs.fw.ip.invflags |= IPT_INV_VIA_OUT
set_option(&cs.options, OPT_VIANAMEOUT, &cs.fw.ip.invflags,
cs.invert);
xtables_parse_interface(optarg,
cs.fw.ip.outiface,
cs.fw.ip.outiface_mask);
break;
case 'f': // 解析分段参数,用于匹配ip分段,暂时存放在cs.fw.ip.flags, 若使用了反向匹配标志, 则 cs.fw.ip.invflags |= IPT_INV_FRAG
set_option(&cs.options, OPT_FRAGMENT, &cs.fw.ip.invflags,
cs.invert);
cs.fw.ip.flags |= IPT_F_FRAG;
break;
......
case 'm': // command_match解析扩展匹配条件,它完成下面几件事:
// 找到match模块对应的 xtables_match,把它间接的链接到cs->matches指向的链表上 (因为可能有多个match,所以这里要用链表来管理)
// 分配xt_entry_match (分配的内存空间为 sizeof(xt_entry_match) + sizeof(match私有数据结构))
command_match(&cs);
break;
......
case 't': // 解析table
*table = optarg;
table_set = true;
break;
......
default: // 解析match或者target模块的私有参数,-m xxxx或者-j xxxx后面跟着的就是match或者target的私有参数,代码就会运行到这里,command_default会查看cs->target记录的xtables_target或者cs->matches记录的xtables_match是不是有x6_parse回调,有就调用,对xtables_match也是一样的操作
if (command_default(&cs, &iptables_globals) == 1)
/* cf. ip6tables.c */
continue;
break;
}
cs.invert = false;
}
......
// 如果没有传递源ip和目的ip就给它赋0.0.0.0/0,让规则能够匹配所有ip
if (command & (CMD_REPLACE | CMD_INSERT | CMD_DELETE | CMD_APPEND | CMD_CHECK)) {
if (!(cs.options & OPT_DESTINATION))
dhostnetworkmask = "0.0.0.0/0";
if (!(cs.options & OPT_SOURCE))
shostnetworkmask = "0.0.0.0/0";
}
// 解析-s和-d指定的地址,将解析后的地址暂存在saddrs/smasks和daddrs/dmasks
if (shostnetworkmask)
xtables_ipparse_multiple(shostnetworkmask, &saddrs, &smasks, &nsaddrs);
if (dhostnetworkmask)
xtables_ipparse_multiple(dhostnetworkmask, &daddrs, &dmasks, &ndaddrs);
......
// 创建socket, 使用 SO_GET_INFO 和 SO_GET_ENTRIES 获取内核中那张表, 存储在xtc_handle 中,然后再表解析为链表的形式,方便后面执行插入删除之类的动作
if (!*handle)
*handle = iptc_init(*table); // libiptc.c 的 TC_INIT函数
if (command == CMD_APPEND
|| command == CMD_DELETE
|| command == CMD_CHECK
|| command == CMD_INSERT
|| command == CMD_REPLACE) {
// INPUT和PREROUTING链不能使用-o参数,因为那个时候skb其实都还没有确定输出设备
if (strcmp(chain, "PREROUTING") == 0
|| strcmp(chain, "INPUT") == 0) {
/* -o not valid with incoming packets. */
if (cs.options & OPT_VIANAMEOUT)
xtables_error(PARAMETER_PROBLEM,
"Can't use -%c with %s\n",
opt2char(OPT_VIANAMEOUT),
chain);
}
// OUTPUT和POSTROUTING链不能使用-i参数,因为那个时候skb的device字段指向的是输出设备
if (strcmp(chain, "POSTROUTING") == 0
|| strcmp(chain, "OUTPUT") == 0) {
/* -i not valid with outgoing packets */
if (cs.options & OPT_VIANAMEIN)
xtables_error(PARAMETER_PROBLEM,
"Can't use -%c with %s\n",
opt2char(OPT_VIANAMEIN),
chain);
}
......
// !cs.target表示target是-j chain, iptc_is_chain会判断目标链是否存在
if (!cs.target
&& (strlen(cs.jumpto) == 0
|| iptc_is_chain(cs.jumpto, *handle))) {
size_t size;
// 初始化 xt_standard_target
cs.target = xtables_find_target(XT_STANDARD_TARGET,
XTF_LOAD_MUST_SUCCEED);
size = sizeof(struct xt_entry_target)
+ cs.target->size;
cs.target->t = xtables_calloc(1, size);
cs.target->t->u.target_size = size;
strcpy(cs.target->t->u.user.name, cs.jumpto); // 把目标链的名字放在了cs.target->t->u.user.name
if (!iptc_is_chain(cs.jumpto, *handle))
cs.target->t->u.user.revision = cs.target->revision;
xs_init_target(cs.target);
}
if (!cs.target) {
.....
} else {
// 使用上面解析的那些参数构造一个 ipt_entry, 但它仍然不是最终的ipt_entry, 因为还没有处理多个源ip和目的ip的情况, 每个[daddr,saddr]对都会产生一个ipt_entry
e = generate_entry(&cs.fw, cs.matches, cs.target->t);
free(cs.target->t);
}
}
// 执行iptable指令
switch (command) {
case CMD_APPEND:
// append_entry会基于上面构造的ipt_entry为每个[daddr,saddr]对都构造一个ipt_entry,然后调用libipt.c文件的TC_APPEND_ENTRY函数来将ipt_entry插入到链表形式的表中
ret = append_entry(chain, e,
nsaddrs, saddrs, smasks,
ndaddrs, daddrs, dmasks,
cs.options&OPT_VERBOSE,
*handle);
break;
......
}
......
}
iptables从内核读取旧表调用的函数如下:
// iptables-1.8.7/libiptc/libiptc.c
struct xtc_handle * TC_INIT(const char *tablename)
{
struct xtc_handle *h;
STRUCT_GETINFO info;
unsigned int tmp;
socklen_t s;
int sockfd;
......
// 1 创建socket
sockfd = socket(TC_AF, SOCK_RAW, IPPROTO_RAW);
......
// 2 通过SO_GET_INFO获取内核中表的信息,内核会,这里主要是为了获取旧表中ipt_entry所占用的空间
s = sizeof(info);
strcpy(info.name, tablename);
if (getsockopt(sockfd, TC_IPPROTO, SO_GET_INFO, &info, &s) < 0)
......
// 3 根据步骤2在中获取到的信息分配内存
h = alloc_handle(&info);
......
h->sockfd = sockfd;
h->info = info;
h->entries->size = h->info.size;
tmp = sizeof(STRUCT_GET_ENTRIES) + h->info.size;
// 4 读取内核旧表中的所有ipt_entry
if (getsockopt(h->sockfd, TC_IPPROTO, SO_GET_ENTRIES, h->entries, &tmp) < 0)
......
// 5 将表解析成链表结构
if (parse_table(h) < 0)
......
}
TC_INIT
函数执行完成后,我们会得到一个struct xtc_handle
类型的结构体变量,它包含了内核中旧表的信息和所有ipt_entry
,它的结构体成员如下:
struct xtc_handle {
int sockfd;
int changed; /* Have changes been made? */
// 解析表的过程中会为每个链生成一个struct chain_head类型的结构体变量,所有链对应的chain_head都会链接在chains这个链表上
struct list_head chains;
struct chain_head *chain_iterator_cur; // 解析过程中指向当前链的指针
struct rule_head *rule_iterator_cur; // 解析过程中指向当前规则的指针
unsigned int num_chains; // 自定义链的个数
......
STRUCT_GETINFO info; // info是从内核获取上来的表信息
STRUCT_GET_ENTRIES *entries; // 获取内核表信息后,会分配一片内存来存储ipt_entry,entries就是指向那片内存区域的指针
};
// 对于iptable而言,STRUCT_GETINFO 和 STRUCT_GET_ENTRIES如下
#define STRUCT_GETINFO struct ipt_getinfo
#define STRUCT_GET_ENTRIES struct ipt_get_entries
struct ipt_getinfo {
char name[XT_TABLE_MAXNAMELEN]; // 表名字
unsigned int valid_hooks; // 在哪些hook点上有回调
unsigned int hook_entry[NF_INET_NUMHOOKS]; // 主链第一条规则的偏移
unsigned int underflow[NF_INET_NUMHOOKS]; // 主链默认规则的偏移
unsigned int num_entries; // ipt_entry数目
unsigned int size; // 表中所有ipt_entry占用的内存
};
struct ipt_get_entries {
char name[XT_TABLE_MAXNAMELEN];
unsigned int size; // 表中所有ipt_entry占用的内存
struct ipt_entry entrytable[0]; // 所有的ipt_entry
};
解析表的parse_table
函数如下所示:
static int parse_table(struct xtc_handle *h)
{
STRUCT_ENTRY *prev;
unsigned int num = 0;
struct chain_head *c;
......
// 1 对表中的所有ipt_entry调用cache_add_entry函数。
// cache_add_entry函数会给每个链分配一个 struct chain_head 类型的结构体变量,并把它们链接到 xtc_handle::chains
// cache_add_entry函数会给每条规则分配一个 struct rule_head类型的结构体变量,并把它们链接到对应的chain_head
ENTRY_ITERATE(h->entries->entrytable, h->entries->size,
cache_add_entry, h, &prev, &num);
......
// 2 有的规则使用target是'-j chain',这里会把目标链对应的chain_head记录到rule_head::jump
list_for_each_entry(c, &h->chains, list) {
struct rule_head *r;
list_for_each_entry(r, &c->rules, list) {
struct chain_head *lc;
STRUCT_STANDARD_TARGET *t;
if (r->type != IPTCC_R_JUMP)
continue;
t = (STRUCT_STANDARD_TARGET *)GET_TARGET(r->entry);
lc = iptcc_find_chain_by_offset(h, t->verdict);
if (!lc)
return -1;
r->jump = lc;
lc->references++;
}
}
return 1;
}
解析过后,xt_handle
的逻辑结构大概就如下图所示:
追加iptable规则的函数如下:
// iptables-1.8.7/libiptc/libiptc.c
int TC_APPEND_ENTRY(const IPT_CHAINLABEL chain,
const STRUCT_ENTRY *e,
struct xtc_handle *handle)
{
struct chain_head *c;
struct rule_head *r;
iptc_fn = TC_APPEND_ENTRY;
// 1 要从哪个链追加,就找哪个chain_head
if (!(c = iptcc_find_label(chain, handle)))
......
// 2 根据传递进来的ipt_entry分配 rule_head
if (!(r = iptcc_alloc_rule(c, e->next_offset)))
......
......
// 3 将规则链接到链上
list_add_tail(&r->list, &c->rules);
c->num_rules++;
......
}
完成规则追加后,还需要将链表结构的表重新编译,然后发送给内核:
// iptables-1.8.7/libiptc/libiptc.c
int TC_COMMIT(struct xtc_handle *handle)
{
/* Replace, then map back the counters. */
STRUCT_REPLACE *repl;
STRUCT_COUNTERS_INFO *newcounters;
struct chain_head *c;
int ret;
size_t counterlen;
int new_number;
unsigned int new_size;
iptc_fn = TC_COMMIT;
// 1 iptcc_compile_table_prep函数会先遍历一遍链表形式的表,然后将各个链和规则的偏移值先计算出来方便后面构造ipt_entry时使用,同时也计算需要分配多大的内存才能装下所有ipt_entry
new_number = iptcc_compile_table_prep(handle, &new_size);
......
// 2 根据步骤1中的计算结果分配ipt_replace,ipt_replace尾部会有一片连续的内存空间用来存储ipt_entry,稍后会把ipt_replace传递到内核,然后使用它来更新内核中的表
repl = malloc(sizeof(*repl) + new_size);
......
memset(repl, 0, sizeof(*repl) + new_size);
......
strcpy(repl->name, handle->info.name);
repl->num_entries = new_number;
repl->size = new_size;
repl->num_counters = handle->info.num_entries;
repl->valid_hooks = handle->info.valid_hooks;
// 3 再一次遍历遍链表形式的表,把链和规则都拷贝到ipt_replace尾部那片内存空间
ret = iptcc_compile_table(handle, repl);
......
// 4 通过SO_SET_REPLACE将ipt_replace传递到内核
ret = setsockopt(handle->sockfd, TC_IPPROTO, SO_SET_REPLACE, repl,
sizeof(*repl) + repl->size);
......
}
内核收到应用层的请求后,调用的函数如下:
// linux/net/ipv4/netfilter/ip_tables.c
static int do_replace(struct net *net, const void __user *user, unsigned int len)
{
int ret;
struct ipt_replace tmp;
struct xt_table_info *newinfo;
void *loc_cpu_entry;
struct ipt_entry *iter;
// 1 拷贝应用层传递的 ipt_replace,此时没有拷贝ipt_replace尾部的ipt_entry
if (copy_from_user(&tmp, user, sizeof(tmp)) != 0)
return -EFAULT;
......
// 2 根据应用层传递的ipt_entry总大小分配一个xt_table_info,稍后就把应用层传递的ipt_entry拷贝到 xt_table_info
newinfo = xt_alloc_table_info(tmp.size);
......
if (copy_from_user(loc_cpu_entry, user + sizeof(tmp), tmp.size) != 0)
.......
// 3 应用层会把match和target的名字放在xt_entry_match::user::name和xt_entry_target::user::name,translate_table会根据match和target的名字找到内核中对应的match和target模块,然后将它们赋值给 xt_entry_match::kernel::match和 xt_entry_match::kernel::target
ret = translate_table(net, newinfo, loc_cpu_entry, &tmp);
......
// 4 使用步骤2中分配的 xt_table_info替换旧的 xt_table_info
ret = __do_replace(net, tmp.name, tmp.valid_hooks, newinfo,
tmp.num_counters, tmp.counters);
.......
}
内核中每个iptable match模块都会构造一个struct xt_match
类型的结构体变量,并在模块初始化时将其链入到一个全局的链表中,上面的translate_table
函数就是通过那个全局的链表来查找内核match模块并给xt_entry_match::kernel::match
赋值。类似的,内核中的iptable target模块会构造一个struct xt_target
类型的结构体变量,并在模块初始化时将其链入到一个全局的链表中。内核的这些iptable match和target模块通常位于linux_*/net/netfilter/xt_**.c
这些文件中,比如mark模块:
//linux_*/net/netfilter/xt_mark.c
static struct xt_target mark_tg_reg __read_mostly = {
.name = "MARK",
.revision = 2,
.family = NFPROTO_UNSPEC,
.target = mark_tg,
.targetsize = sizeof(struct xt_mark_tginfo2),
.me = THIS_MODULE,
};
static struct xt_match mark_mt_reg __read_mostly = {
.name = "mark",
.revision = 1,
.family = NFPROTO_UNSPEC,
.match = mark_mt,
.matchsize = sizeof(struct xt_mark_mtinfo1),
.me = THIS_MODULE,
};
xt_match
会提供一个match回调,执行iptable的过程中就会调用match回调来确定报文是否与iptable规则匹配,xt_target
会提供一个target回调,当报文与iptable规则匹配后就会调用target回调来执行动作。
但标准动作对应的xt_target
有点特殊,它不会提供target回调,内核在执行iptable规则时也会根据xt_entry_target::kernel::target
的值来判段该target是不是标准动作。
iptable规则的执行过程
filter 表初始化流程中向netfilter注册的回调函数是iptable_filter_hook
,它的代码如下:
// linux_*/net/ipv4/netfilter/iptable_filter.c
static unsigned int iptable_filter_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
// filter、raw、mangle、nat表初始化时会把xt_table的地址赋值于 'net::ipv4::iptable_filter','net::ipv4::iptable_raw','net::ipv4::iptable_mangle','net::ipv4::nat_table'
// ipt_do_table通过'net::ipv4::iptable_filter'来获取filter表
return ipt_do_table(skb, state, state->net->ipv4.iptable_filter);
}
ipt_do_table
函数如下:
// linux_*/net/ipv4/netfilter/ip_tables.c
// skb指向携带报文的sk_buff
// state 由netfilter构造,通过state可以知道当前在哪个hook点上
// table 指向表
unsigned int
ipt_do_table(struct sk_buff *skb, const struct nf_hook_state *state, struct xt_table *table)
{
unsigned int hook = state->hook; // 获取hook点
static const char nulldevname[IFNAMSIZ] __attribute__((aligned(sizeof(long))));
const struct iphdr *ip;
/* Initializing verdict to NF_DROP keeps gcc happy. */
unsigned int verdict = NF_DROP;
const char *indev, *outdev;
const void *table_base; // table_base 稍后将指向 xt_table_info::entries, 也就是表中第一个ipt_entry的起始位置
struct ipt_entry *e, **jumpstack; // jumpstack稍后将指向xt_table_info::jumpstack,在执行target为-j chain的ipt_entry时,jumpstack会把返回地址存储下来
unsigned int stackidx, cpu;
const struct xt_table_info *private; // private指针稍后将指向 xt_table_info
struct xt_action_param acpar; // 在执行iptable规则的过程中acpar被用来暂存match和target参数
unsigned int addend;
// 1 根据skb和table初始化部分参数
stackidx = 0;
ip = ip_hdr(skb);
indev = state->in ? state->in->name : nulldevname; // 输入设备
outdev = state->out ? state->out->name : nulldevname; // 输出设备
acpar.fragoff = ntohs(ip->frag_off) & IP_OFFSET; // ip分段标志
acpar.thoff = ip_hdrlen(skb);
acpar.hotdrop = false;
acpar.state = state;
WARN_ON(!(table->valid_hooks & (1 << hook)));
local_bh_disable();
addend = xt_write_recseq_begin();
private = READ_ONCE(table->private); // private指向xt_table_info
cpu = smp_processor_id();
table_base = private->entries; //table_base指向第一条ipt_entry
jumpstack = (struct ipt_entry **)private->jumpstack[cpu];
......
// 2 从主链第一个ipt_entry开始,挨个挨个的执行ipt_entry,直到某个ipt_entry返回NF_ACCEPT\NF_DROP
e = get_entry(table_base, private->hook_entry[hook]);
do {
const struct xt_entry_target *t;
const struct xt_entry_match *ematch;
struct xt_counters *counter;
WARN_ON(!e);
// 2.1 判断ip报文是否满足ip_entry的公共匹配条件(src ip/dst ip/iif/oif/protocol/fragment)
if (!ip_packet_match(ip, indev, outdev, &e->ip, acpar.fragoff)) {
no_match:
e = ipt_next_entry(e); // 不满足,获取下一个ipt_entry
continue;
}
// 2.2 判断ip报文是否满足ip_entry的所有扩展匹配条件,只要有一个不满足就算匹配失败
xt_ematch_foreach(ematch, e) {
// 将match模块的xt_match地址暂存于acpar.match
acpar.match = ematch->u.kernel.match;
// 将match模块的私有参数地址暂存于acpar.matchinfo
acpar.matchinfo = ematch->data;
// 调用match模块的match回调
if (!acpar.match->match(skb, &acpar))
goto no_match;
}
counter = xt_get_this_cpu_counter(&e->counters);
ADD_COUNTER(*counter, skb->len, 1);
// 2.3 获取target
t = ipt_get_target_c(e);
WARN_ON(!t->u.kernel.target);
......
// 2.4 tagert模块没提供target回调,那就说明该target就代表标准动作
if (!t->u.kernel.target->target) {
int v;
// 2.4.1 获取xt_standard_target::verdict,值小于0说明target是'-j ACCEPT/DROP/RETURN',值大于0说明target是'-j chain',
v = ((struct xt_standard_target *)t)->verdict;
if (v < 0) {
// 如果target是'-j ACCEPT/DROP',返回NF_ACCEPT或者NF_DROP给netfilter
if (v != XT_RETURN) {
verdict = (unsigned int)(-v) - 1;
break;
}
// 如果target是'-j RETURN'
if (stackidx == 0) { // stackidx为0表示,当前已经位于主链,主链中的规则使用了'-j RETURN',那就去执行主链的默认规则
e = get_entry(table_base,
private->underflow[hook]);
} else { // 当前还位于自定义链中,从jumpstack中获取返回地址
e = jumpstack[--stackidx];
e = ipt_next_entry(e);
}
continue;
}
// 2.4.2 xt_standard_target::verdict值大于0,说明target是'-j chain',那么就需要跳转到自定义链去执行
if (table_base + v != ipt_next_entry(e) &&
!(e->ip.flags & IPT_F_GOTO)) {
if (unlikely(stackidx >= private->stacksize)) {
verdict = NF_DROP;
break;
}
// 使用jumpstack记录跳转前ipt_entry的地址
jumpstack[stackidx++] = e;
}
// 跳转到自定义链的第一条规则
e = get_entry(table_base, v);
continue;
}
// 2.5 代码执行到这里代表target是扩展动作
acpar.target = t->u.kernel.target;
acpar.targinfo = t->data;
// 2.5.1 调用扩展动作模块提供的target回调
verdict = t->u.kernel.target->target(skb, &acpar);
// 2.5.2 如果扩展模块返回了XT_CONTINUE,那就继续执行下一条规则,否则将返回值直接返给netfilter
if (verdict == XT_CONTINUE) {
/* Target might have changed stuff. */
ip = ip_hdr(skb);
e = ipt_next_entry(e);
} else {
/* Verdict */
break;
}
} while (!acpar.hotdrop);
xt_write_recseq_end(addend);
local_bh_enable();
if (acpar.hotdrop)
return NF_DROP;
else return verdict;
}