绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
Netfilter CONNMARK用法及分析(二)-- 内核代码分析
2020-05-25 11:48:03

本文着重分析内核中CONNMARK的实现,同时还包括MARK的match和target模块的实现。因为CONNMARK模块通常是和MARK模块搭配使用的。

1. CONNMARK及相关模块的选项
这里先列出CONNMARK、MARK和mark模块的iptables命令行的选项,随后我们再逐一分析各个模块的内核实现。iptables版本为v1.3.5。

(1)CONNMARK target的选项
选项 功能
--set-mark value[/mask] 给链接跟踪记录打标记。
--save-mark [--mask mask] 将数据包上的标记值记录到链接跟踪记录上。
--restore-mark [--mask mask] 重新设置数据包的nfmark值。

(2)MARK target 的选项
选项 功能
--set-mark value 设置数据包的nfmark值。
--and-mark value 数据包的nfmark值和value进行按位与运算。
--or-mark value 数据包的nfmark值和value进行按或与运算。

(3)MARK match的选项
选项 功能
[!] --mark value[/mask] 数据包的nfmark值与value进行匹配,其中mask的值为可选的。

其中CONNMARK和MARK match中都有mask选项,这个主要用来指定给出value值中哪几位是需要设置的。通常,如果我们不指定mask的话,其值会被默认初始为0xFFFFFFFFUL。

2. MARK target的内核实现
我这里分析的内核源码版本是2.6.18,该模块的代码见文件xt_MARK.c,其核心的target实现代码如下:

  1. 41 static unsigned int
  2. 42 target_v1(struct sk_buff **pskb,
  3. 43       const struct net_device *in,
  4. 44       const struct net_device *out,
  5. 45       unsigned int hooknum,
  6. 46       const struct xt_target *target,
  7. 47       const void *targinfo,
  8. 48       void *userinfo)
  9. 49 {
  10. 50     const struct xt_mark_target_info_v1 *markinfo = targinfo;
  11. 51     int mark = 0;
  12. 52 
  13. 53     switch (markinfo->mode) {
  14. 54     case XT_MARK_SET:
  15. 55         mark = markinfo->mark;
  16. 56         break;
  17. 57 
  18. 58     case XT_MARK_AND:
  19. 59         mark = (*pskb)->nfmark & markinfo->mark;
  20. 60         break;
  21. 61 
  22. 62     case XT_MARK_OR:
  23. 63         mark = (*pskb)->nfmark | markinfo->mark;
  24. 64         break;
  25. 65     }
  26. 66          /*将用户设置的mark值赋给数据包的nfmark*/
  27. 67     if((*pskb)->nfmark != mark)
  28. 68         (*pskb)->nfmark = mark;
  29. 69 
  30. 70     return XT_CONTINUE;
复制代码

MARK target三个选项中常用的应该是--set-mark,给数据包的nfmark设置标记。诸如下面这条规则

iptables -A POSTROUTING -p tcp --dport 21 -t mangle -j MARK --set-mark 1

就是将符合前面匹配选项的数据包的nfmark值设置为1。在内核中对应的源码为:

  1. 54     case XT_MARK_SET:
  2. 55         mark = markinfo->mark;
  3. 56         break;
  4.         选项--and-mark对应的源码为:
  5. 58     case XT_MARK_AND:
  6. 59         mark = (*pskb)->nfmark & markinfo->mark;
  7. 60         break;
复制代码

功能就是将数据包中记录的nfmark值和用户设置的value进行按位与。同理选项--or-mark就是将数据包中记录的nfmark值和用户设置的value进行按位或,其源码如下:

  1. 62     case XT_MARK_OR:
  2. 63         mark = (*pskb)->nfmark | markinfo->mark;
  3. 64         break;
复制代码

该模块后统一判断一下设置后的mark值是否与数据包中保存的nfmark值相等,如果不等,则更新数据包的nfmark值为刚刚设置的mark。其代码如下:

  1. 67     if((*pskb)->nfmark != mark)
  2. 68         (*pskb)->nfmark = mark;
复制代码

个人觉得不管数据包nfmark和mark是否相等,直接将mark赋给nfmark就可以了。这里加一条if判断,难道是为了当两个值相等的时候,少执行一次写内存的指令?

3. MARK match的内核实现
该模块的实现在内核源码的xt_mark.c中。其核心匹配模块的实现代码如下:

  1. 22 static int
  2. 23 match(const struct sk_buff *skb,
  3. 24       const struct net_device *in,
  4. 25       const struct net_device *out,
  5. 26       const struct xt_match *match,
  6. 27       const void *matchinfo,
  7. 28       int offset,
  8. 29       unsigned int protoff,
  9. 30       int *hotdrop)
  10. 31 {
  11. 32     const struct xt_mark_info *info = matchinfo;
  12. 33 
  13. 34     return ((skb->nfmark & info->mask) == info->mark) ^ info->invert;
  14. 35 }
复制代码

该模块实现的功能相对简单,就是判断一下数据包的nfmark值是否与用户指定的相同。判断的时候要考虑mask以及取反的标记位。
下面这条iptables规则就是实现了对所有标记不为0的数据包都执行ACCEPT动作:

iptables -A POSTROUTING -t mangle -m mark ! --mark 0 -j ACCEPT

假设skb->nfmark为1,由于没有指定mask,所以默认值为0xFFFFFFFFUL,info->mark为0,取反标记位被置位,info->invert为1。因此,((skb->nfmark & info->mask) == info->mark) ^ info->invert的结果应该是(1&0xFFFFFFFFUL)== 0)^1 = 0^1 = 1。
该模块match函数的返回值为1,执行ACCEPT动作。

4. CONNMARK target的内核实现
该模块的实现在内核源码的xt_CONNMARK.c中。其核心动作模块的实现代码如下:

  1. 35 static unsigned int
  2. 36 target(struct sk_buff **pskb,
  3. 37        const struct net_device *in,
  4. 38        const struct net_device *out,
  5. 39        unsigned int hooknum,
  6. 40        const struct xt_target *target,
  7. 41        const void *targinfo,
  8. 42        void *userinfo)
  9. 43 {
  10. 44     const struct xt_connmark_target_info *markinfo = targinfo;
  11. 45     u_int32_t diff;
  12. 46     u_int32_t nfmark;
  13. 47     u_int32_t newmark;
  14. 48     u_int32_t ctinfo;
  15. 49     u_int32_t *ctmark = nf_ct_get_mark(*pskb, &ctinfo);
  16. 50 
  17. 51     if (ctmark) {
  18. 52         switch(markinfo->mode) {
  19. 53         case XT_CONNMARK_SET:
  20. 54         newmark = (*ctmark & ~markinfo->mask) | markinfo->mark;
  21. 55         if (newmark != *ctmark)
  22. 56             *ctmark = newmark;
  23. 57         break;
  24. 58         case XT_CONNMARK_SAVE:
  25. 59         newmark = (*ctmark & ~markinfo->mask) | ((*pskb)->nfmark & markinfo->mask);
  26. 60         if (*ctmark != newmark)
  27. 61             *ctmark = newmark;
  28. 62         break;
  29. 63         case XT_CONNMARK_RESTORE:
  30. 64         nfmark = (*pskb)->nfmark;
  31. 65         diff = (*ctmark ^ nfmark) & markinfo->mask;
  32. 66         if (diff != 0)
  33. 67             (*pskb)->nfmark = nfmark ^ diff;
  34. 68         break;
  35. 69         }
  36. 70     }
  37. 71 
  38. 72     return XT_CONTINUE;
  39. 73 }
复制代码

这段代码开始先取出了数据包对应的链接跟踪中记录mark值的地址:

  1. 49     u_int32_t *ctmark = nf_ct_get_mark(*pskb, &ctinfo);
复制代码

然后判断ctmark是否为NULL:

  1. 51     if (ctmark) {
复制代码

如果ctmark == NULL,说明链接跟踪中没有ct->mark这个字段,惟一可能的原因就是内核的配置文件中并没有对CONFIG_NF_CONNTRACK_MARK进行配置,也就是没有启用Conntrack Mark的功能。否则,就根据用户配置的动作进行相应的实现。
以下将分成三个部分介绍。

(1)给链接跟踪设置标记(--set-mark)
实现代码如下:

  1. 53         case XT_CONNMARK_SET:
  2. 54         newmark = (*ctmark & ~markinfo->mask) | markinfo->mark;
  3. 55         if (newmark != *ctmark)
  4. 56             *ctmark = newmark;
  5. 57         break;
复制代码

这里先按照markinfo->mask中所有被置位的bit,将*ctmark中对应的bit位清零,然后再与markinfo->mark进行按位或。这样就保证了mask中置位的bit位,一定为按照mark中的值重新设置。
55~57行代码的实现和我们第2部分分析的67~68代码的实现相同。

(2)保存标记值到链接跟踪(--save-mark)
实现代码如下:

  1. 58         case XT_CONNMARK_SAVE:
  2. 59         newmark = (*ctmark & ~markinfo->mask) | ((*pskb)->nfmark & markinfo->mask);
  3. 60         if (*ctmark != newmark)
  4. 61             *ctmark = newmark;
  5. 62         break;
复制代码

这部分代码中mask的功能和上面分析的相同。为了简单化分析起见,我们假设用户没有指定mask,默认值为0xFFFFFFFFUL。这样,59行的计算简化为:
newmark = (*pskb)->nfmark
再加上后两行代码:

  1. 60         if (*ctmark != newmark)
  2. 61             *ctmark = newmark;
复制代码

具体的功能就很明确了,将当前数据包记录的nfmark值记录到链接跟踪中的ct->mark中。
那么,按理说如果数据包的流程走到这部分代码,应该就是数据包已经被打上标记了,也就是前面分析的MARK target中--set-mark进行的处理。因此,在下在iptables规则的时候,通常--set-mark的规则在前,--save-mark的规则在后。这就保证了数据包先被打上标记,然后再将数据包上的标记记录到其对应的链接跟踪上。以下几条示例规则就是按照这个流程进行的:

iptables -A POSTROUTING -m mark --mark 0 -p tcp --dport 21 -t mangle -j MARK --set-mark 1
iptables -A POSTROUTING -m mark --mark 0 -p tcp --dport 80 -t mangle -j MARK --set-mark 2
iptables -A POSTROUTING -m mark --mark 0 -t mangle -p tcp -j MARK --set-mark 3
iptables -A POSTROUTING -t mangle -j CONNMARK --save-mark


(3)重新设置数据包的标记值(--restore-mark)
实现代码如下:

  1. 63         case XT_CONNMARK_RESTORE:
  2. 64         nfmark = (*pskb)->nfmark;
  3. 65         diff = (*ctmark ^ nfmark) & markinfo->mask;
  4. 66         if (diff != 0)
  5. 67             (*pskb)->nfmark = nfmark ^ diff;
  6. 68         break;
复制代码

这里我们同样默认用户没有设置mask,其初始值为0xFFFFFFFFUL。因此,64行代码可简化为:

  1.         diff = *ctmark ^ nfmark
复制代码

结合代码

  1. 66         if (diff != 0)
  2. 67             (*pskb)->nfmark = nfmark ^ diff;
复制代码

我们可以看出这个功能就是将链接记录上的*ctmark保存到了(*pskb)->nfmark上。
简单的说来,如果一个链接跟踪记录被打上了标记,那么通过该段代码,就确保了所有属于该链接的数据包也都被打上了该标记。这样,系统中后面的模块就可以根据数据报上的标记进一步的处理,譬如TC可以根据该标记继续带宽管理。

5.总结
那么--restore-mark规则应该怎么与--set-mark和--save-mark进行配合,完成给链接打标记,进而为连接上的所有数据包打标记呢?请看下面几条规则:

  1. iptables -A POSTROUTING -t mangle -j CONNMARK --restore-mark
  2. iptables -A POSTROUTING -t mangle -m mark ! --mark 0 -j ACCEPT
  3. iptables -A POSTROUTING -m mark --mark 0 -p tcp --dport 21 -t mangle -j MARK --set-mark 1
  4. iptables -A POSTROUTING -m mark --mark 0 -p tcp --dport 80 -t mangle -j MARK --set-mark 2
  5. iptables -A POSTROUTING -m mark --mark 0 -t mangle -p tcp -j MARK --set-mark 3 
  6. iptables -A POSTROUTING -t mangle -j CONNMARK --save-mark
复制代码

解释如下:
1)第1条规则就是完成了将链接跟踪上的标记记录到该连接上的每一个数据包中;
2)第2条规则判断数据包的标记,如果不为0,则直接ACCEPT。如果数据包上没有被打标记,则交由后面的规则进行匹配并打标记。这里为什么会出现经过了CONNMARK模块,数据包仍未被打标记呢?可以想到,如果一条链接的第1个数据包经过第1条规则处理之后,由于ct->mark为0,所以其数据包的标记skb->nfmark应该仍旧为0,就需要进行后面规则的匹配。
3)第3~5条规则,则按照匹配选项对符合规则的数据包打上不同的标记。
4)第6条规则就是将数据包上的标记记录到链接跟踪上。这样,当属于该链接的下一个数据包走到第1条规则的时候,就会被打上标记,然后就会命中第2条规则,执行动作ACCEPT。
至此,内核模块CONNMARK、MARK的match和target实现都已分析完毕。如有分析不妥或遗漏指出,请大家多多指正。


文章来源CU社区:  Netfilter CONNMARK用法及分析(二)-- 内核代码分析

分享好友

分享这个小栈给你的朋友们,一起进步吧。

内核源码
创建时间:2020-05-18 13:36:55
内核源码精华帖内容汇总
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

技术专家

查看更多
  • 飘絮絮絮丶
    专家
戳我,来吐槽~