Linux协议栈-netfilter(5)-iptables

文章转载------------------------------点击打开链接

iptables是用户态的配置工具,用于实现网络层的防火墙,用户可以通过iptables命令设置一系列的过滤规则,来截获特定的数据包并进行过滤或其他处理。

iptables命令通过与内核中的netfilter交互来起作用。我们知道netfilter通过挂在每个hook点上的hook函数来过滤数据包,并且将过滤规则存放在几个表中供hook函数使用。相应的,iptables工具也定义了同样的几张规则表来对应netfilter中的表,以及定义了不同的链(chain)来对应netfilter中的hook点。这样,通过iptables命令生成的规则可以很容易的作用到内核中的netfilter模块,netfilter根据这些规则做真正的过滤工作。

netfilter模块中,不同的协议族定义了各自的一套hook点,例如,IPv4、IPv6、ARP、BRIDGE等协议族都分别定义了各自的netfilter处理流程以及各自的hook点,我们比较常见的就是IPv4协议定义的5个hook点(PRE_ROUTING,LOCAL_IN,LOCAL_OUT,FORWARD和POST_ROUTING)。同样的,为了和netfilter交互,在用户态分别有iptables、ip6tables、arptables、ebtables等命令来定义各协议族的过滤规则。

要使用iptables命令,则必须将netfilter模块编进内核。本文内容基于内核版本2.6.31。

1. 数据结构

netfilter中的每个表都定义成一个struct xt_table类型的结构体。例如下面定义的表:

   struct xt_table       *iptable_filter;

   struct xt_table      *iptable_mangle;

   struct xt_table      *iptable_raw;

   struct xt_table      *nat_table;

xt_table结构定义如下,表中的实际规则就放在其中的private成员中

  1. struct xt_table
  2. {
  3. struct list_head list;
  4. /* 在哪些hook点上注册了hook函数,是一个位图 */
  5. unsigned int valid_hooks;
  6. /*表的实际数据 */
  7. struct xt_table_info *private;
  8. struct module *me;
  9. /* 所属协议族 */
  10. u_int8_t af;
  11. /*表名,供用户空间设置iptables规则,或者内核匹配iptables规则 */
  12. const char name[XT_TABLE_MAXNAMELEN];
  13. };

而private里的内容是:

  1. /* The table itself */
  2. struct xt_table_info
  3. {
  4. /* 表的大小 */
  5. unsigned int size;
  6. /* Number of entries,即规则的数量 */
  7. unsigned int number;
  8. /* Initial number of entries,一般为上一次修改规则时的number */
  9. unsigned int initial_entries;
  10. /* 在每个hook点作用的entry的偏移(注意是相对于最后一个参数entry的偏移,即第一个hook的第一个ipt_entry的hook_entry为0) */
  11. unsigned int hook_entry[NF_INET_NUMHOOKS];
  12. /* 规则表的最大下界 */
  13. unsigned int underflow[NF_INET_NUMHOOKS];
  14. /* 规则表入口,即真正的规则存储结构. 在遍历一个规则表时,以此作为表的起始(即第一个ipt_entry)。由定义可知这是一个数组,每个元素对应每个CPU上的规则 入口。 */
  15. void *entries[ 1];
  16. };

所以,通过private就可以定位到规则的实际内容了。

例如NAT表的定义如下,可知在定义时初始化好了下面4个成员。而在注册时会赋值list成员,在添加新规则时会给private赋值。
  1. static struct xt_table nat_table = {
  2. .name = "nat",
  3. .valid_hooks = ( 1 << NF_INET_PRE_ROUTING) | \
  4. ( 1 << NF_INET_POST_ROUTING) | \
  5. ( 1 << NF_INET_LOCAL_OUT),
  6. .me = THIS_MODULE,
  7. .af = AF_INET,
  8. };

一条iptables规则包括三个部分:ipt_entry、ipt_entry_matches、ipt_entry_target。

ipt_entry_matches由多个ipt_entry_match组成,ipt_entry结构主要保存标准匹配的内容,ipt_entry_match 结构主要保存扩展匹配的内容,ipt_entry_target结构主要保存规则的动作。

  1. struct ipt_entry
  2. {
  3. /* ipt_ip结构:将要进行匹配动作的IP数据报报头的描述 */
  4. struct ipt_ip ip;
  5. /* 经过这个规则后数据报的状态:未改变,已改变,不确定 */
  6. unsigned int nfcache;
  7. /* Size of ipt_entry + matches,target在matches的后面 */
  8. u_int16_t target_offset;
  9. /* Size of ipt_entry + matches + target,即下一个ipt_entry的开始 */
  10. u_int16_t next_offset;
  11. /* 指向数据报所经历的上一个规则地址。还可以作为hook mask表示规则作用于那个
  12. hook点上 */
  13. unsigned int comefrom;
  14. /* 匹配这个规则的数据报的计数以及字节计数 */
  15. struct xt_counters counters;
  16. /* 存放matches(一条规则可有0到多个match)和一个target */
  17. unsigned char elems[ 0];
  18. };

结构体中的elems成员用于定位规则的matches,target_offset用于定位规则的target,next_offset指向下一条规则入口即下一个ipt_entry。这样,只要定位到一个表中第一个规则的ipt_entry,就可以找到这个表中的所有规则了。

一个表中可以有多条规则,下图以IPV4上注册的表以及nat表的规则为例,说明了表中规则的存放形式,该图显示了IPv4协议有filter和nat等规则表,并显示了nat表的规则的存放位置。


NAT表允许在PRE_ROUTING、LOCAL_OUT、POST_ROUTING三个链上设置规则,所以,struct xt_table_info的hook_entry[]和underflow[]成员分别有三个数组元素,用来定位每个链(即hook点)上的第一条规则和最后一条规则。

每个链可以有多条规则,每条规则都是由entry+matches+target组成的,所以在遍历每个链上的规则时,就根据struct ipt_entry来定位每条规则的位置。

用iptables命令还可以创建自定义的子链,例如用户新建一个自定义链NEW_PRE_CHAIN:

iptables -N NEW_PRE_CHAIN

然后设置了两条规则添加到NEW_PRE_CHAIN链上。

接着在PREROUTING链(对应netfilter的hook点NF_INET_PRE_ROUTING)上追加一条跳转到NEW_PRE_CHAIN子链的规则,并将这条规则放到NAT表中,例如:

iptables -t nat -A PREROUTING -i eth1 -p tcp -s 192.168.2.0/24 -d 192.168.2.1 --dport 8080 -j NEW_PRE_CHAIN

上面这条规则的意思是在nat表中添加一条规则:从eth1进来的TCP包,在经过在PREROUTING链时进行判断,如果源IP是192.168.2.x网段,目的IP是192.168.2.1,目的端口为8080,则跳转到NEW_PRE_CHAIN链上继续匹配规则。

说是子链,实际上仍然和原有规则放在一起。例如上面在子链中新添加两条规则后,netfilter的NAT表的规则就变成了:


可以看到两条新的PRE_ROUTING规则被紧跟在原有规则后面存放,并且在内核中的NAT表中并没有关于子链NEW_PRE_CHAIN的信息,因为“子链”的概念是用户态的iptables命令才使用的,iptables做一些处理后将规则传到内核,而内核中netfilter的工作就不会那么复杂。

用户态的iptables命令传入的match和target在内核都要有对应的match和target。内核中所有的match和target都注册在全局数组xt中,该数组每个元素是一个struct xt_af结构,存储一类地址族的matches和targets,如NFPROTO_IPV4。

static struct xt_af *xt;

  1. struct xt_af {
  2. struct mutex mutex;
  3. struct list_head match; //该协议的match集合
  4. struct list_head target; //该协议的taget集合
  5. };

注册函数为xt_register_match(struct xt_match *match)和xt_register_target(struct xt_target *target)。

find_check_entry()函数中可以看到内核如何根据用户态传过来的规则中match和target的name来匹配内核支持的match和target。struct xt_match 和struct xt_target结构都有name成员,用户态传入的name必须是内核已注册的,才能找到对应项添加到一条规则中去。例如,iptables命令想要使用DNAT这个target,则内核中必须要定义了对应“DNAT”的target函数。

2. 用户态使用iptables添加一条规则的流程

用iptables -vxnL或iptables –t filter -vxnL命令可以看到filter表上的所有规则。用iptables –t nat -vxnL命令可以看到nat表中的所有规则。

iptables工具是用户空间和内核的netfilter模块通信的手段,因此iptables中也有“表”和“hook点”的概念,只是hook点被称为内建chain。

Iptables命令中的内建链与Netfilter中hook点的对应关系如下:

  1. static const char *hooknames[] = {
  2. [HOOK_PRE_ROUTING] = "PREROUTING",
  3. [HOOK_LOCAL_IN] = "INPUT",
  4. [HOOK_FORWARD] = "FORWARD",
  5. [HOOK_LOCAL_OUT] = "OUTPUT",
  6. [HOOK_POST_ROUTING] = "POSTROUTING",
  7. #ifdef HOOK_DROPPING
  8. [HOOK_DROPPING] = "DROPPING"
  9. #endif
  10. };

用户配置完iptables规则之后,传给内核的是一个ipt_replace结构,其中包含了内核所需要的所有内容:

  1. /* The argument to IPT_SO_SET_REPLACE. */
  2. struct ipt_replace
  3. {
  4. /* 表名 */
  5. char name[IPT_TABLE_MAXNAMELEN];
  6. /* hook mask */
  7. unsigned int valid_hooks;
  8. /* 新规则的entry数 */
  9. unsigned int num_entries;
  10. /* Total size of new entries */
  11. unsigned int size;
  12. /* Hook entry points. */
  13. unsigned int hook_entry[NF_INET_NUMHOOKS];
  14. /* Underflow points. */
  15. unsigned int underflow[NF_INET_NUMHOOKS];
  16. /* 旧规则的entry数 */
  17. unsigned int num_counters;
  18. /* The old entries' counters. */
  19. struct xt_counters __user *counters;
  20. /* 规则本身 */
  21. struct ipt_entry entries[0];
  22. };

该结构包含了表名,规则挂载的hook点,ipt entry的数目等信息,该结构的最后为实际的规则内容,基本包含了内核中struct xt_table和struct xt_table_info结构所需要的内容。传递的过程通过getsockopt()和setsockopt()系统调用来完成,这两个系统调用的函数原型为:

int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);

 int setsockopt(ints, int level, int optname, const void *optval, socklen_t optlen);

其中getsockopt的参数optname可取的值为IPT_SO_GET_INFO、IPT_SO_GET_ENTRIES、IPT_SO_GET_REVISION_MATCH和IPT_SO_GET_REVISION_TARGET。

setsockopt的参数optname可取的值为IPT_SO_SET_REPLACE和IPT_SO_SET_ADD_COUNTERS,所有修改规则的动作(添加、修改、删除等)都通过IPT_SO_SET_REPLACE完成,而IPT_SO_SET_ADD_COUNTERS更新表中每个ipt_entry的counters成员。

当然这些都是iptables工具做的事情,我们只要会使用iptables命令即可。而我们知道可以自定义链,这也是iptables工具所的要处理的事情,实际上内核是不知道有自定义链的。

在ip_tables_init函数中调用nf_register_sockopt(&ipt_sockopts)注册get和set方法,如下:

  1. ipt_sockopts-> set = do_ipt_set_ctl;
  2. ipt_sockopts->get = do_ipt_get_ctl;

用户改变iptables规则时,通过ipt_sockopts注册的这两个函数进行进一步工作,例如set方法就是将用户空间传过来的ipt_replace来替换旧的iptables规则。这个工作在do_replace()函数中完成。

do_replace()处理流程如下:


该函数流程比较清晰,最终结果是更新了内核中的某个xt_table表,如NAT表。图中未列出一系列的合法性检查,如size和valid_hook的检查,以及对entry/match/target的匹配和检查等。

3. netfilter进行规则匹配的流程

3.1 ipt_do_table()

前面一直讲到根据ipt_do_table()来匹配match并执行target,那我们就从ipt_do_table()开始分析。hook点上的hook函数就是通过这个ipt_do_table()来查找表中规则并将规则作用于数据包的。

  1. unsigned int
  2. ipt_do_table (struct sk_buff *skb,
  3. unsigned int hook,
  4. const struct net_device *in,
  5. const struct net_device *out,
  6. struct xt_table *table)
  7. {
  8. #define tb_comefrom ((struct ipt_entry *)table_base)->comefrom
  9. static const
  10. char nulldevname[IFNAMSIZ] __attribute__((aligned( sizeof( long))));
  11. const struct iphdr *ip;
  12. u_int16_t datalen;
  13. bool hotdrop = false;
  14. /* Initializing verdict to NF_DROP keeps gcc happy. */
  15. unsigned int verdict = NF_DROP;
  16. const char *indev, *outdev;
  17. void *table_base;
  18. struct ipt_entry *e, *back;
  19. struct xt_table_info *private;
  20. struct xt_match_param mtpar;
  21. struct xt_target_param tgpar;
  22. ip = ip_hdr(skb);
  23. /* 是否挂在正确的hook点 */
  24. IP_NF_ASSERT(table->valid_hooks & ( 1 << hook));
  25. xt_info_rdlock_bh();
  26. /* 获取规则内容 */
  27. private = table-> private;
  28. /* 获得表的规则总入口 */
  29. table_base = private->entries[smp_processor_id()];
  30. /* 获得表在该hook点定义的规则的入口 */
  31. e = get_entry(table_base, private->hook_entry[hook]);
  32. /* 如果没有match部分也没有target函数,直接根据verdict的值返回 */
  33. if (e->target_offset <= sizeof(struct ipt_entry) &&
  34. (e->ip.flags & IPT_F_NO_DEF_MATCH)) {
  35. struct ipt_entry_target *t = ipt_get_target(e);
  36. if (!t->u.kernel.target->target) {
  37. int v = ((struct ipt_standard_target *)t)->verdict;
  38. if ((v < 0) && (v != IPT_RETURN)) {
  39. ADD_COUNTER(e->counters, ntohs(ip->tot_len), 1);
  40. xt_info_rdunlock_bh();
  41. return ( unsigned)(-v) - 1; /****finish****/
  42. }
  43. }
  44. }
  45. /* Initialization */
  46. datalen = skb->len - ip->ihl * 4;
  47. indev = in ? in->name : nulldevname;
  48. outdev = out ? out->name : nulldevname;
  49. /* 初始化match的参数 */
  50. mtpar.fragoff = ntohs(ip->frag_off) & IP_OFFSET;
  51. mtpar.thoff = ip_hdrlen(skb);
  52. mtpar.hotdrop = &hotdrop;
  53. mtpar.in = tgpar.in = in;
  54. mtpar.out = tgpar.out = out;
  55. mtpar.family = tgpar.family = NFPROTO_IPV4;
  56. mtpar.hooknum = tgpar.hooknum = hook;
  57. /* For return from builtin chain */
  58. back = get_entry(table_base, private->underflow[hook]);
  59. do {
  60. struct ipt_entry_target *t;
  61. /* 匹配规则的entry部分和match部分 */
  62. IP_NF_ASSERT(e);
  63. IP_NF_ASSERT(back);
  64. /* skb相应字段是否和规则匹配,即匹配entry部分 */
  65. if (!ip_packet_match(ip, indev, outdev,
  66. &e->ip, mtpar.fragoff) ||
  67. /* 将skb放在e的所有match函数中依次进行match */
  68. IPT_MATCH_ITERATE(e, do_match, skb, &mtpar) != 0) {
  69. /* 如果entry或match匹配失败,就不再走下一个match,直接
  70. 跳到下一个entry,依次匹配matches。 */
  71. e = ipt_next_entry(e);
  72. continue;
  73. }
  74. /* 经过上面的匹配,e指向了匹配成功的ipt_entry */
  75. /* 增加统计计数(数据报的字节总数不包含链路层数据长度) */
  76. ADD_COUNTER(e->counters, ntohs(ip->tot_len), 1);
  77. /* 获得target部分 */
  78. t = ipt_get_target(e);
  79. IP_NF_ASSERT(t->u.kernel.target);
  80. /* 如果target函数为NULL,则根据verdict判断是否返回 */
  81. if (!t->u.kernel.target->target) {
  82. int v;
  83. v = ((struct ipt_standard_target *)t)->verdict;
  84. if (v < 0) {
  85. /* Pop from stack? */
  86. if (v != IPT_RETURN) {
  87. verdict = ( unsigned)(-v) - 1;
  88. break; /****finish****/
  89. }
  90. /* 如果返回的是IPT_RETURN,则要返回到父链 */
  91. e = back;
  92. back = get_entry(table_base, back->comefrom);
  93. continue;
  94. }
  95. /* 没有target且verdict>0时,verdict为自定义链起始位置的相对偏移量 */
  96. if (table_base + v != ipt_next_entry(e)
  97. && !(e->ip.flags & IPT_F_GOTO)) {
  98. /* Save old back ptr in next entry */
  99. struct ipt_entry *next = ipt_next_entry(e);
  100. next->comefrom = ( void *)back - table_base;
  101. /* set back pointer to next entry */
  102. back = next;
  103. }
  104. /* 跳到下一个entry */
  105. e = get_entry(table_base, v);
  106. continue;
  107. }
  108. /* Targets which reenter must return
  109. abs. verdicts */
  110. /* 初始化target的参数 */
  111. tgpar.target = t->u.kernel.target;
  112. tgpar.targinfo = t->data;
  113. /* 为skb执行target函数,返回处理结果verdict */
  114. verdict = t->u.kernel.target->target(skb, &tgpar);
  115. /* Target might have changed stuff. */
  116. ip = ip_hdr(skb);
  117. datalen = skb->len - ip->ihl * 4;
  118. /* 如果返回continue,接着走下一个entry。
  119. 返回其他的则处理结束,直接返回verdict */
  120. if (verdict == IPT_CONTINUE)
  121. e = ipt_next_entry(e);
  122. else
  123. /* 返回target函数本身的处理结果 */
  124. /* Verdict */
  125. break; /****finish****/
  126. } while (!hotdrop);
  127. xt_info_rdunlock_bh();
  128. if (hotdrop)
  129. return NF_DROP;
  130. else return verdict;
  131. #undef tb_comefrom
  132. }

注意,找到一个匹配成功的规则,执行target之后就不再遍历下一条规则了,无论target结果怎样。

代码中最主要的内容就是while循环中遍历表中规则,匹配entry和match并执行target。该函数的返回值为规则的处理结果,代码中有三处会停止遍历并返回结果,已用注释/****finish****/标出,返回值都是存放在一个名为verdict的变量中,这个值有多重含义:

1.   如果规则指定了target函数,例如配置iptables命令时指定-j SNAT,verdict就是target函数(如ipt_snat_target)的返回值,如NF_ACCEPT等。

2.   如果规则没有指定target函数,例如配置iptables命令时指定-j ACCEPT。这时如果verdict<0,并且verdict !=  IPT_RETURN,就把(unsigned)(-v) – 1作为返回值。

3.   如果规则没有指定target函数,且verdict<0,verdict ==  IPT_RETURN,则需要返回到父链继续匹配后续规则,例如上面设置的NEW_PRE_CHAIN子链规则匹配完成后需要跳回父链。

4.   如果规则没有指定target函数,且verdict>0,则verdict的值是子链相对于表规则入口的偏移,即后续应该跳到该子链去匹配规则,例如上面设置的NEW_PRE_CHAIN子链规则入口。

对于前两种取值,verdict是作为ipt_do_table()的返回值返回到调用者的,对于后两种取值,则需要继续while循环。

对于第二种取值,如果verdict<0,那需要通过计算来得出返回结果,例如,如果verdict = 0xfffffffe,则(unsigned)(-v) – 1就是1,即NF_ACCEPT,同样的,verdict = 0xffffffff即NF_DROP,等等。

IPT_CONTINUE 和IPT_RETURN定义如下:
  1. /* CONTINUE verdict for targets */
  2. #define XT_CONTINUE 0xFFFFFFFF
  3. /* For standard target */
  4. #define XT_RETURN (-NF_REPEAT - 1)

在一个链中还可以设置默认规则,即如果所有规则都不匹配,就走默认规则。默认规则一般设置为NF_ACCEPT或NF_DROP等。默认规则的target在iptables命令中被称为policy

例如,我们在NAT表中设置PRE_ROUTING链上的policy是ACCEPT:

       iptables -t nat -P PREROUTING ACCEPT

那在这条链上的最后一条规则不是我们自己手动添加的,而是一条没有match和target的默认规则,verdict被设置为0xfffffffe。

  1. # iptables -t nat -L
  2. Chain PREROUTING (policy ACCEPT)
  3. ••••••

注意,只能设置内建链的policy,不能设置自定义链,因为policy是内核中要使用的,内核并不知道自定义链的存在。另外,NAT表的各链的policy都不能为DROP,因为NAT表本来就不是用来过滤的,想要DROP的规则可以放到filter表中。

3.2 以NAT举例规则匹配流程

我们以下面的规则为例来说明规则匹配过程:

iptables -t nat -I POSTROUTING 3 -o eth1 -j MASQUERADE

这条规则作用在POST_ROUTING链,从eth1发出的数据包,去执行MASQUERADE动作,规则放在NAT表中。所以在NAT的POST_ROUTING的hook函数中会去执行该规则,即nf_nat_out()。

先说一下struct ipt_entry结构中,有一个成员ip,它是一个struct ipt_ip类型的结构体,用来匹配一些基本信息,所以规则的匹配工作并不完全在match中进行,ipt_entry也负责匹配一些内容。

  1. struct ipt_ip {
  2. /* Source and destination IP addr */
  3. struct in_addr src, dst;
  4. /* Mask for src and dest IP addr */
  5. struct in_addr smsk, dmsk;
  6. char iniface[IFNAMSIZ], outiface[IFNAMSIZ];
  7. unsigned char iniface_mask[IFNAMSIZ], outiface_mask[IFNAMSIZ];
  8. /* Protocol, 0 = ANY */
  9. u_int16_t proto;
  10. /* Flags word */
  11. u_int8_t flags;
  12. /* Inverse flags */
  13. u_int8_t invflags;
  14. };

可以看到这个结构可以完成源IP和目的IP、入口和出口设备、协议类型等的匹配。负责匹配ipt_entry部分的函数为ip_packet_match()。

上面的nat规则只要出口是eth1即可,所以该规则并没有match部分。在ipt_do_table()函数中找到target为MASQUERADE,对应到内核的masquerade_tg()函数,该函数负责将skb对应的其关联的nf_conn结构实例进行NAT转换,在下次数据包经过该hook点时,masquerade_tg()会判断conntrack状态而直接返回,从而达到只有第一个包需要查找NAT表,后续skb可以根据conntrack来做NAT的效果。

3.3 SNAT和masquerade

这是两个iptables规则的target,实际上他俩没有区别,只是nat配置规则的两种写法而已。我们先来看一下规则的写法:

iptables -t nat-I POSTROUTING 1  -o $wan_if -jMASQUERADE

iptables -t nat-I POSTROUTING 2  -s $lan_ip/$lan_mask -d$lan_ip/$lan_mask -j SNAT --to-source $wan_ip

规则中的-j选项指定target,这两个target分别对应内核的下面两个函数:

masquerade_tg(structsk_buff *skb, const struct xt_target_param *par);

ipt_snat_target(structsk_buff *skb, const struct xt_target_param *par);

snat规则指定了应该将源ip变成什么地址,而masquerade需要在出口设备的IP列表中选择一个合适的作为转换IP。二者都会调用nf_nat_setup_info()对ct进行转换。

注意,一般情况下路由器只允许内网到外网的NAT,所以必须做完SNAT后,外网的数据包才可以通过转换后的conntrack条目到达内网,所以我们通常不会设置DNAT规则。

同样的,REDIRECT和DNAT这两个target的作用一样,都是修改数据包的目的地址。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值