解析Linux下Netfilter & iptables:开发一个match模块

来源:互联网 发布:淘宝上的好店铺推荐 编辑:程序博客网 时间:2024/05/22 23:20

http://www.bairimeng.net/2012/11/27/netfilter_iptables_matc/

一、说明
最近的项目需要软件组基于Netfilter和iptables开发Linux内核模块,以完成一系列防火墙功能,说白了防火墙就是过滤规则。
为了熟悉Netfilter和iptables的开发,于是开发过程中写下这篇笔记,以达到温故知新的作用。

(盗图自重=///=)
上图说明了Netfilter模块是如何运行的,它指出我们需要开发两个东西,一个是用户态的共享库so,一个是内核态的内核库ko。
命名规则有讲究,如果模块叫Mymodules,那么内核态源代码一般命名为ipt_Mymodules.c,头文件为ipt_Mymodules.h,用户态源代码为libipt_Mymodules.c。
我的Linux系统内核是2.6.27.41,iptables版本是1.4.3.2,因为Linux网路部分有些许头文件和2.6.24之前有些许差异,而iptables也有些概念和低版本有差异,所以将逐步记录。

二、模块描述
为了练习开发过程,和我看的资料一样,我们来设计一个最简单的模块,这个模块能匹配IP报文中有效荷载字段。用法如下:

iptables -A FORWARD -m pktsize –size XX[:YY] -j DROP

关于iptables的资料请自行百度~上述规则说明,在FORWARD挂载点上对于大小在XX[到YY,可省略]的数据包进行匹配,数据包长度不包括IP头。
从规则可以看到我们的模块名为pktsize,所以我们要建立3个新文件,分别是ipt_pktsize.c,libipt_pktsize.c,ipt_pktsize.h。
因为头文件两边均要用到,所以我们先来定义头文件ipt_pktsize.h。

?
1
2
3
4
5
6
7
8
9
10
ifndef __IPT_PKTSIZE_H
#define __IPT_PKTSIZE_H
 
#define PKTSIZE_VERSION "0.1"
// 我们自己定义的用户保存规则中指定档数据包大小的结构体
structipt_pktsize_info {
    // 数据包的最小和最大字节数,不包括IP头
    u_int32_t min_pktsize, max_pktsize;
};
#endif // __IPT_EXLENGTH_H

我们定义了一个结构体ipt_pktsize_info,内含2个成员,代表什么一目了然。我们还定义了这个模块的版本号。

三、用户态开发
在netfilter/iptables体系中,我们使用struct xtables_match{}结构来表示用户态的match,所以我们要实例化一个这个结构,并赋上必要的初值,这个结构的详细定义在iptables的源码include/xtables.h中。
在我们开发用户态模块中,一般要实现以下几个函数:
1>help()
2>parse()
3>final_check()
4>print()
5>save()
详细解释将在代码中给出,本身已经到了猫叫喵喵的程度了,相信都知道是大致干嘛的。

OK,开始动手,我们先搭框架:

?
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
#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#include <getopt.h>
#include <ctype.h>
#include <xtables.h>
#include <linux/netfilter_ipv4/ipt_pktsize.h>
   
staticvoid PKTSIZE_help(void)
{
    // >>>TODO<<<
}
  
staticint PKTSIZE_parse(intc, char** argv, intinvert, 
    unsignedint* flags, constvoid* entry,
    structxt_entry_match** match)
{
    // >>>TODO<<<
    return1;
}
  
staticvoid PKTSIZE_final_check(unsigned intflags)
{
    // >>>TODO<<<
}
   
staticvoid __print(structipt_pktsize_info* info)
{
    // >>>TODO<<<
}
  
staticvoid PKTSIZE_print(constvoid* ip,
    conststruct xt_entry_match* match, intnumeric)
{
    // >>>TODO<<<
}
   
staticvoid PKTSIZE_save(constvoid* ip,
     conststruct xt_entry_match* match)
{
    // >>>TODO<<<
}
  
staticstruct xtables_match pktsize =
{
    .next       = NULL,
    .name       = "pktsize",
    .version    = XTABLES_VERSION,
    .family     = NFPROTO_IPV4,
    .size       = XT_ALIGN(sizeof(structipt_pktsize_info)),
    .userspacesize  = XT_ALIGN(sizeof(structipt_pktsize_info)),
    .help       = PKTSIZE_help,
    .parse      = PKTSIZE_parse,
    .final_check    = PKTSIZE_final_check,
    .print      = PKTSIZE_print,
    .save       = PKTSIZE_save,
    // .extra_opts  = PKTSIZE_opts    这一句以后将被添上
};
  
void_init(void)
{
    xtables_register_match(&pktsize);
}

下面我们来填充所有要实现的函数,并做解释。

help():当我们在命令行输入iptables -m pktsize -h时 用于显示该模块用法的帮助信息。

?
1
2
3
4
5
6
7
8
9
10
11
staticvoid PKTSIZE_help(void)
{
    printf(
    "pktsize v%s options:\n"
    " --size size[:size]  Match packet size against value or range\n"
    "\nExamples:\n"
    " iptables -A FORWARD -m pktsize --size 65 -j DROP\n"
    " iptables -A FORWARD -m pktsize --size 80:120 -j DROP\n"
    ,PKTSIZE_VERSION
    );
}

print():该函数用于打印用户输入参数的,因为其他人 可能也会需要输出规则参数,所以封装成一个子函数__print() 供其他人调用。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
staticvoid __print(structipt_pktsize_info* info)
{
    if(info->max_pktsize == info->min_pktsize)
        printf("%u", info->min_pktsize);
    else
        printf("%u:%u", info->min_pktsize, info->max_pktsize);
}
  
staticvoid PKTSIZE_print(constvoid* ip,
    conststruct xt_entry_match* match, intnumeric)
{
    printf("size ");
    __print((structipt_pktsize_info *)match->data);
}

可以注意到print()函数传入一个xt_entry_match结构体,事实上我们的ipt_pktsize_info数据存在这个结构体的data成员中。
xt_entry_match的详细定义在iptables源码的include/linux/netfilter/x_tables.h中。

save():该函数跟print类似 。

?
1
2
3
4
5
6
staticvoid PKTSIZE_save(constvoid* ip,
     conststruct xt_entry_match* match)
{
    printf("--size ");
    __print((structipt_pktsize_info*)match->data);
}

final_check():如果你的模块有些长参数时必须的, 那么当用户调用了你的模块但又没有进一步制定必须参数时, 一般在这个函数里做校验限制。 如,我的模块带一个必须参数—size,而且后面必须跟数值。

?
1
2
3
4
5
6
staticvoid PKTSIZE_final_check(unsigned intflags)
{
    if(!flags)
        xtables_error(PARAMETER_PROBLEM, 
            "\npktsize-parameter problem:for pktsize usage type:iptables -m pktsize --help\n");
}

parse():这是我们的核心,用于解析命令行参数的回调函数,成功则返回true 该函数是核心,参数的解析最终是在该函数中完成的,因为我们 用到长参数格式,所以必须引入一个结构体struct option{}。

?
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
staticstruct option PKTSIZE_opts[] =
{
    {"size", 1, NULL, '1'},
    {0}
};
  
// 并且还要将结构体对象赋值给
// pktsize.extra_opts = opts;
  
// 解析参数的具体函数单独出来,会使parse()函数结构很优美
// 我们的输入参数可能格式如下:
//  xx  指定数据包大小xx
//  <img src="http://www.bairimeng.net/wp-includes/images/smilies/icon_mad.gif" alt=":x" class="wp-smiley"> x   范围是0-xx
//  yy: 范围是yy-65535
//  xx:yy   范围是xx-yy
  
staticvoid parse_pkts(constchar* s,
    structipt_pktsize_info* info)
{
    char* buff,*cp;
    buff = strdup(s);
      
    if(NULL == (cp = strchr(buff,':')))
    {
        info->min_pktsize = info->max_pktsize =
            strtol(buff, NULL, 0);
    }
    else
    {
        *cp = '\0';
        cp++;
          
        info->min_pktsize = strtol(buff, NULL, 0);
        info->max_pktsize = (cp[0]?
            strtol(cp, NULL, 0):0xFFFF);
    }
  
    free(buff);
      
    if(info->min_pktsize > info->max_pktsize)
        xtables_error(PARAMETER_PROBLEM,
            "pktsize min.range value '%u' greater than max.range value '%u'",
            info->min_pktsize,
            info->max_pktsize
            );
}
  
staticint PKTSIZE_parse(intc, char** argv, intinvert, 
    unsignedint* flags, constvoid* entry,
    structxt_entry_match** match)
{
    structipt_pktsize_info* info = (structipt_pktsize_info*)(*match)->data;
    switch(c)
    {
        case'1':
            if(*flags)
                xtables_error(PARAMETER_PROBLEM,
                "size: '--size' may only be specified once"
                );
            parse_pkts(argv[optind-1], info);
            *flags = 1;
            break;
        default:
            return0;
    }
    return1;
}

OK,用户态的所有功能完成,完整代码如下:

?
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#include <getopt.h>
#include <ctype.h>
#include <xtables.h>
#include <linux/netfilter_ipv4/ipt_pktsize.h>
  
// help()TODO: 当我们在命令行输入iptables -m pktsize -h时
// 用于显示该模块用法的帮助信息。
staticvoid PKTSIZE_help(void)
{
    printf(
    "pktsize v%s options:\n"
    " --size size[:size]  Match packet size against value or range\n"
    "\nExamples:\n"
    " iptables -A FORWARD -m pktsize --size 65 -j DROP\n"
    " iptables -A FORWARD -m pktsize --size 80:120 -j DROP\n"
    ,PKTSIZE_VERSION
    );
}
  
// parse()TODO:用于解析命令行参数的回调函数,成功则返回true
// 该函数是核心,参数的解析最终是在该函数中完成的,因为我们
// 用到长参数格式,所以必须引入一个结构体struct option{}。 
  
// 这里只有一个扩展参数,所以结构简单,有多个则必须一一处理
staticstruct option PKTSIZE_opts[] =
{
    {"size", 1, NULL, '1'},
    {0}
};
  
// 并且还要将结构体对象赋值给
// pktsize.extra_opts = opts;
  
// 解析参数的具体函数单独出来,会使parse()函数结构很优美
// 我们的输入参数可能格式如下:
//  xx  指定数据包大小xx
//  <img src="http://www.bairimeng.net/wp-includes/images/smilies/icon_mad.gif" alt=":x" class="wp-smiley"> x   范围是0-xx
//  yy: 范围是yy-65535
//  xx:yy   范围是xx-yy
  
staticvoid parse_pkts(constchar* s,
    structipt_pktsize_info* info)
{
    char* buff,*cp;
    buff = strdup(s);
      
    if(NULL == (cp = strchr(buff,':')))
    {
        info->min_pktsize = info->max_pktsize =
            strtol(buff, NULL, 0);
    }
    else
    {
        *cp = '\0';
        cp++;
          
        info->min_pktsize = strtol(buff, NULL, 0);
        info->max_pktsize = (cp[0]?
            strtol(cp, NULL, 0):0xFFFF);
    }
  
    free(buff);
      
    if(info->min_pktsize > info->max_pktsize)
        xtables_error(PARAMETER_PROBLEM,
            "pktsize min.range value '%u' greater than max.range value '%u'",
            info->min_pktsize,
            info->max_pktsize
            );
}
  
  
  
staticint PKTSIZE_parse(intc, char** argv, intinvert, 
    unsignedint* flags, constvoid* entry,
    structxt_entry_match** match)
{
    structipt_pktsize_info* info = (structipt_pktsize_info*)(*match)->data;
    switch(c)
    {
        case'1':
            if(*flags)
                xtables_error(PARAMETER_PROBLEM,
                "size: '--size' may only be specified once"
                );
            parse_pkts(argv[optind-1], info);
            *flags = 1;
            break;
        default:
            return0;
    }
    return1;
}
// final_check()TODO:如果你的模块有些长参数时必须的,
// 那么当用户调用了你的模块但又没有进一步制定必须参数时,
// 一般在这个函数里做校验限制。
// 如,我的模块带一个必须参数--size,而且后面必须跟数值
staticvoid PKTSIZE_final_check(unsigned intflags)
{
    if(!flags)
        xtables_error(PARAMETER_PROBLEM, 
            "\npktsize-parameter problem:for pktsize usage type:iptables -m pktsize --help\n");
}
  
// print()TODO:该函数用于打印用户输入参数的,因为其他人
// 可能也会需要输出规则参数,所以封装成一个子函数__print()
// 供其他人调用
staticvoid __print(structipt_pktsize_info* info)
{
    if(info->max_pktsize == info->min_pktsize)
        printf("%u", info->min_pktsize);
    else
        printf("%u:%u", info->min_pktsize, info->max_pktsize);
}
  
staticvoid PKTSIZE_print(constvoid* ip,
    conststruct xt_entry_match* match, intnumeric)
{
    printf("size ");
    __print((structipt_pktsize_info *)match->data);
}
  
// save()TODO:该函数跟print类似
staticvoid PKTSIZE_save(constvoid* ip,
     conststruct xt_entry_match* match)
{
    printf("--size ");
    __print((structipt_pktsize_info*)match->data);
}
  
staticstruct xtables_match pktsize =
{
    .next       = NULL,
    .name       = "pktsize",
    .version    = XTABLES_VERSION,
    .family     = NFPROTO_IPV4,
    .size       = XT_ALIGN(sizeof(structipt_pktsize_info)),
    .userspacesize  = XT_ALIGN(sizeof(structipt_pktsize_info)),
    .help       = PKTSIZE_help,
    .parse      = PKTSIZE_parse,
    .final_check    = PKTSIZE_final_check,
    .print      = PKTSIZE_print,
    .save       = PKTSIZE_save,
    .extra_opts     = PKTSIZE_opts
};
 
void_init(void)
{
    xtables_register_match(&pktsize);
}

现在我们将要将它编译成so库,现在如果我们使用iptables -m pktsize -h会提示说不能加载名为’pktsize’的match,缺少了/lib/xtables/libipt_pktsize.so,所以我们要把这个so编译出来,并放到那个目录下。

我们可以从Netfilter官网上下载到iptables-1.4.3.2的源码包,然后解压缩,1.4.3.2和1.4.0的Make不一样,所以make过程也有所不同,网上的资料大多是1.4.0的,所以我只说下1.4.3.2的。

我们在iptables的源码目录下能找到一个configure文件,这个文件会进行Makefile配置,这一点跟1.4.0不一样,运行它。
可以看到不停的有东西刷出来,别管他,一般配置过程结束之后,会在源码目录下产生一些Makefile。

我们将我们的libipt_pktsize.c复制到extensions/下,1.4.0需要修改extensions下的Makefile文件,但是1.4.3.2不需要,却多了上面那个配置过程。

然后我们返回iptables源码目录,执行一次make。
同样是一堆东西刷出来,它会编译整个iptables,因为本身比较小,所以我们也不改动Makefile了,干脆等它全部编译完成,然后我们只需要extensions/libipt_pktsize.so这个库文件。将它复制到iptables的库中,比如我的是lib/xtables/。

这时候我们执行iptables -m pktsize -h就应该可以看到help()函数输出的信息了。
在末尾看到了我们的模块的帮助信息,可喜可贺。
到这里我们用户态的模块已经开发完毕,iptables已经成功加载了我们的模块,但是我们还需要开发一个内核态的模块,使得Netfilter能调用它。

四、内核态开发
内核中,我们使用xt_match{}结构体来表示一个match,我们也要实例化一个这玩意儿,然后使用xt_register_match()来将它注册到xt[AF_INET].match链上面就行了,就这么简单。。

?
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
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/version.h>
#include <linux/netfilter_ipv4/ip_tables.h>
#include <linux/netfilter/x_tables.h>
#include <linux/netfilter_ipv4/ipt_pktsize.h>
  
MODULE_AUTHOR("Sekai <sekaiamber@lolicon.me>");
MODULE_DESCRIPTION("Iptables pkt size range match module.");
MODULE_LICENSE("GPL");
  
staticbool match(conststruct sk_buff* skb, conststruct net_device* in,
    conststruct net_device* out, conststruct xt_match* match,
    constvoid* matchinfo, intoffset, unsigned intprotoff, bool* hotdrop)
{
    // >>>TODO<<<
    returnfalse;
}
  
staticstruct xt_match pktsize_match __read_mostly = 
{
    .name       = "pktsize",// >>>>TODO<<<<
    .family     = AF_INET,
    .match      = match,
    .matchsize  = sizeof(structipt_pktsize_info),
    .destroy    = NULL,
    .me     = THIS_MODULE
};
  
staticint __init init(void)
{
    returnxt_register_match(&pktsize_match);
}
  
staticvoid __exit fini(void)
{
    xt_unregister_match(&pktsize_match);
}
  
module_init(init);
module_exit(fini);

我们在这里只需实现一个match函数,来告诉Netfilter数据包经过这个match时将要干嘛。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
staticbool match(conststruct sk_buff* skb, conststruct net_device* in,
    conststruct net_device* out, conststruct xt_match* match,
    constvoid* matchinfo, intoffset, unsigned intprotoff, bool* hotdrop)
{
    conststruct ipt_pktsize_info* info = matchinfo;
    conststruct iphdr* iph = ip_hdr(skb);
      
    intpkttruesize = ntohs(iph->tot_len) - (iph->ihl * 4);
      
    if(pkttruesize >= info->min_pktsize && pkttruesize <= info->max_pktsize)
    {
        returntrue;
    }
    else
    {
        returnfalse;
    }
}

这里要注意skb这个结构,是sk_buff,这个结构的完整定义在Linux内核源码的include/linux/skbuff.h中,在Linux内核2.4.23之前,这个结构和后面不太一样。
sk_buff代表了收到的数据包的一系列指针集合,在2.4.23内核之前,它的成员中含有3个共同体union,分别名为h,nh,mac,这三者分别代表传输层(L4)的头,网络层(L3)的头,数据链路层(L2)的头,而在之后版本的内核中,这三个字段被改成了类型为sk_buff_data_t的transport_header,network_header,mac_header,sk_buff_data_t的详细定义也在skbuff.h中,它是一个宏:

?
1
2
3
4
5
#ifdef NET_SKBUFF_DATA_USES_OFFSET
typedefunsigned intsk_buff_data_t;  // 使用偏移来表示数据头位置
#else
typedefunsigned char*sk_buff_data_t;  // 使用指针来表示数据头位置
#endif

所以事实上sk_buff_data_t是一个标志而已,所以我们要使用内核提供的函数来获得iphdr,这表示一个数据包的信息,详细定义在内核源码include/linux/ip.h中。

OK,编码完成,完整代码如下:

?
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
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/version.h>
#include <linux/netfilter_ipv4/ip_tables.h>
#include <linux/netfilter/x_tables.h>
#include <linux/netfilter_ipv4/ipt_pktsize.h>
  
MODULE_AUTHOR("Sekai <sekaiamber@lolicon.me>");
MODULE_DESCRIPTION("Iptables pkt size range match module.");
MODULE_LICENSE("GPL");
  
  
// TODO
staticbool match(conststruct sk_buff* skb, conststruct net_device* in,
    conststruct net_device* out, conststruct xt_match* match,
    constvoid* matchinfo, intoffset, unsigned intprotoff, bool* hotdrop)
{
    conststruct ipt_pktsize_info* info = matchinfo;
    conststruct iphdr* iph = ip_hdr(skb);
      
    intpkttruesize = ntohs(iph->tot_len) - (iph->ihl * 4);
      
    if(pkttruesize >= info->min_pktsize && pkttruesize <= info->max_pktsize)
    {
        returntrue;
    }
    else
    {
        returnfalse;
    }
}
  
staticstruct xt_match pktsize_match __read_mostly = 
{
    .name       = "pktsize",// >>>>TODO<<<<
    .family     = AF_INET,
    .match      = match,
    .matchsize  = sizeof(structipt_pktsize_info),
    .destroy    = NULL,
    .me     = THIS_MODULE
};
  
staticint __init init(void)
{
    returnxt_register_match(&pktsize_match);
}
  
staticvoid __exit fini(void)
{
    xt_unregister_match(&pktsize_match);
}
  
module_init(init);
module_exit(fini);

内核态的模块已经编码完成,所以我们要对他进行编译成ko库。
我们复制ipt_pktsize.h头文件到系统引用路径中,比如我的是/usr/include/linux/netfilter_ipv4/。当然这个遵循你ipt_pktsize.c中引用头文件的位置,相对位置一般是/usr/include/。
然后复制ipt_pktsize.c源文件到Linux内核源代码中比如我是~/下载/linux-2.6.27.41/net/ipv4/netfilter/中。
然后将目录下的Makefile文件备份,新建一个Makefile,我们只需要编译一个我们的ipt_pktsize.ko就可以了,不需要全部编译。
新建Makefile,输入下列内容:

保存,然后执行make。
编译成功,我们就将ipt_pktsize.ko复制到系统模块目录中去,比如我的是/lib/modules/2.6.27.XXXXXXXXXXXXX(懒得打)/kernel/net/ipv4/netfilter/。然后安装模块即可。


上图第一行,我们将ko文件设置为全部可执行,第二行看一下有没有模块,这时候pktsize还没被安装,第三行复制到系统模块目录,第四个命令安装模块,手滑没打 sudo,权限不够,第五个命令继续,
第六个命令lsmod,发现模块已经成功被识别了。

然后我们来使用我们的模块配置一下iptables。

可以看到Chain Forward链上已经绑上了我们编写的模块了!大功告成!

原创粉丝点击