为netfilter/iptables增添新功能模块:ipp2p
一个防火墙功能模块包含两部分:内核空间的ko模块和用户空间的so模块。如下:
而且文件的命令都非常有讲究。例如我们有个模块名叫AAA,那么内核中该模块的文件名一般为ipt_AAA.c和ipt_AAA.h;对应的用户空间模块叫libipt_AAA.c。今天我通过简单的向防火墙添加ipp2p扩展功能模块的例子,向大家展示一下相关操作和注意事项。
下载ipp2p源码:http://www.ipp2p.org/
最新版的ipp2p-0.8.2.tar.gz支持的内核2.6.17,iptables支持1.3.1。不过这个不影响,我们稍对其源文件进行修改就可以适应我们2.6.21平台了。修改后的源码从“ ipp2p-0.8.2.zip ”下载。
下载后对其解压,然后进到ipp2p目录下。接下来有两种方式来编译模块:
1、 直接在该目录下执行make。(备注:该目录下的Makefile我已经改过了)
执行完make后ipt_ipp2p.ko和libipt_ipp2p.so就都生成了。
chmod x ipt_ipp2p.ko
2、 依次按下列步骤来操作:
仍然是在ipp2p-0.8.2解压目录里,我系统中如下:
然后将每个文件拷贝到对应的目录下:
在我机器上,内核源码在/usr/src/linux-2.6.21,iptables源码在/usr/src/iptables-1.4.0目录下。
接下来修改/usr/src/linux-2.6.21/net/ipv4/netfilter/ipt_ipp2p.c文件:
同样修改/usr/src/iptables-1.4.0/extensions/libipt_ipp2p.c文件如下:
接换到内核源码目录/usr/src/linux-2.6.21目录下
先备份net/ipv4/netfilter/目录下原来的Makefile文件,然后照着下面自己写个新的Makefile。如下所示:
ipt_ipp2p.ko就已经编译出来了,加上执行权限后将其拷贝到/lib/modules/2.6.21目录里。
最后编译用户空间的so模块,进入到/usr/src/iptables-1.4.0:
因为我系统中有两个版本的iptables 1.3.5和1.4.0,其中1.3.5的库文件位于/lib/iptables目录;1.4.0我将其放置在/lib/iptables-1.4.0/iptables目录。
最后我们来使用一下ipp2p模块,执行iptables -m ipp2p -h应该可以看到下列提示:加载ipp2p内核模块:
insmod /lib/modules/2.6.21/kernel/net/ipv4/netfilter/ipt_ipp2p.ko
总结,虽然方法一干净利落,犀利快捷,可是不利于我们理解netfilter/iptables代码的组织架构。方法二虽然麻烦,但学的知识更多些。大家各取所需吧。
网上还流传另外一种通过补丁包的方式来为iptables扩展功能模块,这里我简单提一下,因为那玩意儿更easy。一般是到ftp://ftp.netfilter.org/pub/patch-o-matic-ng/下载对应的补丁文件,在本地将其解压后进入补丁文件夹,一般执行:./runme ipp2p,然后根据提示输入一些配置路径就OK了。它会自动帮你完成文件的拷贝,Makefile的修改,很方便。感兴趣的朋友可以去试一下。
未完,待续…
自己开发一个match模块
今天我们来写一个很简单的match来和大家分享如何为iptables开发扩展功能模块。这个模块是根据IP报文中有效载荷字段的长度来对其进行匹配,支持固定包大小,也支持一个区间范围的的数据包,在用户空间的用法是:
iptables -A FORWARD -m pktsize --size XX[:YY] -j DROP
这条规则在FORWARD链上对于大小为XX(或大小介于XX和YY之间)的数据包进行匹配,数据包的长度不包括IP头部的长度。为了简单起见,这个模块没有处理“!”情况,因为只是阐述开发过程。
OK,下面我们开始动手吧。我们这个模块名字叫pktsize,所以内核中该模块对应的文件是ipt_pktsize.h和ipt_pktsize.c;用户空间的文件名为libipt_pktsize.c。
我们先来定义头文件,因为这个匹配模块功能很单一,所以设计它的数据结构主要包含两部分,如下:
#ifndef __IPT_PKTSIZE_H #define __IPT_PKTSIZE_H
#define PKTSIZE_VERSION "0.1" //我们自己定义的用户保存规则中指定的数据包大小的结构体 struct ipt_pktsize_info { u_int32_t min_pktsize,max_pktsize; //数据包的最小和最大字节数(不包括IP头) };
#endif //__IPT_EXLENGTH_H |
一、用户空间的开发
我们知道用户空间的match是用struct iptables_match{}结构表示的,所以我们需要去实例化一个该对象,然后对其关键成员进行初始化赋值。一般情况我们需要实现parse函数、help函数、final_check函数、print和save函数就已经可以满足基本要求了。我们先把整体代码框架搭起来:
#include <stdio.h> #include <netdb.h> #include <string.h> #include <stdlib.h> #include <getopt.h> #include <ctype.h> #include <iptables.h> #include <linux/netfilter_ipv4/ipt_pktsize.h>
static void help(void) { //Todo: your code }
/*用于解析命令行参数的回调函数; 如果成功则返回true */ static int parse(int c, char **argv, int invert, unsigned int *flags, const void *entry, struct ipt_entry_match **match) { return 1; }
static void final_check(unsigned int flags) { //Todo: your code }
static void print(const void *ip, const struct ipt_entry_match *match, int numeric) { //Todo: your code }
static void save(const void *ip, const struct ipt_entry_match *match) { //Todo: your code }
static struct iptables_match pktsize= { .next = NULL, .name = "pktsize", .version = IPTABLES_VERSION, .size = IPT_ALIGN(sizeof(struct ipt_pktsize_info)), .userspacesize = IPT_ALIGN(sizeof(struct ipt_pktsize_info)), .help = &help, .parse = &parse, .final_check = &final_check, .print = &print, .save = &save };
void _init(void) { register_match(&pktsize); } |
下面我们分别来实现这些回调函数,并对其做简单解释:
help()函数:当我们在命令输入iptables -m pktsize -h时用于显示该模块用法的帮助信息,所以很简单,你想怎么提示用户都可以:
static void 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()供其他人来调用,如下:
static void __print(struct ipt_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); }
static void print(const void *ip, const struct ipt_entry_match *match, int numeric) { printf("size "); __print((struct ipt_pktsize_info *)match->data); } |
从命令行终端输入的数据包大小的规则参数“XX:YY”其实最终是在ipt_entry_match结构体的data成员里保存着的,关于该结构体参见博文三的图解。
save()函数:该函数和print类似:
static void save(const void *ip, const struct ipt_entry_match *match) { printf("--size "); __print((struct ipt_pktsize_info *)match->data); } |
final_check()函数:如果你的模块有些长参数格式是必须的,那么当用户调用了你的模块但又没进一步制定必须参数时,一般在这个函数里做校验限制。如,我的模块带了一个必须按参数--size ,而且后面必须跟数值,所以该函数内容如下:
static void final_check(unsigned int flags) { if (!flags) exit_error(PARAMETER_PROBLEM, "\npktsize-parameter problem: for pktsize usage type: iptables -m pktsize --help\n"); } |
parse()函数:该函数是我们的核心,参数的解析最终是在该函数中完成的。因为我们用到长参数格式,所以必须引入一个结构体struct option{},我们在博文十三中已经见过,不清楚原理和用法的童鞋可以回头复习一下。
这里我们的模块只有一个扩展参数,所以该结构非常简单,如果你有多个,则必须一一处理:
static struct option opts[] = { { "size", 1, NULL, '1' }, {0} }; //并且还要将该结构体对象赋给:pktsize.extra_opts= opts; //解析参数的具体函数单独出来,会使得parse()函数的结构很优美 /* 我们的输入参数的可能格式如下: xx 指定数据包大小 XX :XX 范围是0~XX YY: 范围是YY~65535 xx:YY 范围是XX~YY */ static void parse_pkts(const char* s,struct ipt_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) exit_error(PARAMETER_PROBLEM, "pktsize min. range value `%u' greater than max. " "range value `%u'", info->min_pktsize, info->max_pktsize); }
static int parse(int c, char **argv, int invert, unsigned int *flags, const void *entry, struct ipt_entry_match **match) { struct ipt_pktsize_info *info = (struct ipt_pktsize_info *)(*match)->data; switch(c){ case '1': if (*flags) exit_error(PARAMETER_PROBLEM, "size: `--size' may only be " "specified once"); parse_pkts(argv[optind-1], info); *flags = 1; break; default: return 0; } return 1; } |
该文件的最终版本从“ libipt_pktsize.zip ”下载。
用户空间要用的libipt_pktsize.so的源代码我们就算编写完成了,迫不及待的去试一下吧。当前,我的iptables确实不认识pktsize模块。
我将libipt_pktsize.c拷贝到/usr/src/iptables-1.4.0/ extensions目录下,并修改该目录下的Makefile文:
然后在/usr/src/iptables-1.4.0/目录下单独执行一次make命令,最后将extensions/目录下编译出来的libipt_pktsize.so拷贝到iptables的库目录里,例如/lib/iptables-1.4.0/iptables。
此时,当我们再在命令行执行一次iptables -m pktsize -h时,在末尾处可以看到如下的信息:
就证明我们的模块已经被iptables正确识别并成功加载了。
一、内核空间的开发
同样的,开发内核的Netfilter模块时,我们还是先搭其框架:
#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_ipv4/ipt_pktsize.h>
MODULE_AUTHOR("Koorey Wung <wjlkoorey@gmail.com>"); MODULE_DESCRIPTION("iptables pkt size range match module."); MODULE_LICENSE("GPL");
static int match(const struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct xt_match *match, const void *matchinfo, int offset, unsigned int protoff, int *hotdrop) { return 1; }
static struct ipt_match pktsize_match = { .name = "test", .family = AF_INET, .match = match, .matchsize = sizeof(struct ipt_pktsize_info), .destroy = NULL, .me = THIS_MODULE, };
static int __init init(void) { return xt_register_match(&pktsize_match); }
static void __exit fini(void) { xt_unregister_match(&pktsize_match); }
module_init(init); module_exit(fini); |
通过前面几篇博文我们已经知道,内核中用struct ipt_match{}结构来表示一个match模块。我们要开发match的内核部分时,也必须去实例化一个struct ipt_match{}对象,然后对其进行必要的初始化设置,最后通过xt_register_match()将其注册到xt[AF_INET].match全局链表中就OK了,就这么简单。
我们这里例子非常简单,只实现最关键的核心函数:match()函数。不过这已经满足我们需求了,我们的match函数做的事情也很simple,就是计算数据包的有效载荷:
static int match(const struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct xt_match *match, const void *matchinfo, int offset, unsigned int protoff, int *hotdrop) { const struct ipt_pktsize_info *info = matchinfo; const struct iphdr *iph = skb->nh.iph;
int pkttruesize = ntohs(iph->tot_len)-(iph->ihl*4);
if(pkttruesize>=info->min_pktsize && pkttruesize <=info->max_pktsize){ return 1; } else{ return 0; } return 1; } |
但有一点需要明确,如果数据包匹配了match函数返回1;否则返回0.
该文件的最终版本从“ ipt_pktsize.zip ”下载。

当我们的模块已经被内核认亲后,那感觉真的是无以言表啊。废话不多说,我们赶紧执行一条规则看看:
曾经有个哥们说他在使用owner模块时出现了同样的问题,这会不会是由于同样的原因导致的呢?如果你是严格遵循我的教程来的,那么这里我要说一定就是:这个问题是我特意留出的。细心的童鞋回头看代码时应该很容易找出问题了。原因:
这里有一点要提醒大家注意,内核中的模块名和用户空间的模块名必须一致。这里我们将pktsize_mach.name改为“pktsize”,重新编译,然后将其拷贝。在重新执行insmod前,先执行rmmod ipt_pktsize将原来的模块卸载掉,最后再次执行那条规则:
今天通过这个简单的例子,向大家示范一下为Netfitler/iptables开发功能模块的方法。整体来说还是比较简单,当然要写出更有意义,更高效的模块需要对协议栈、TCP/IP原理、网络编程等有较好的基础才行。
未完,待续…
向Netfilter中注册自己的hook函数
数据包在协议栈中传递时会经过不同的HOOK点,而每个HOOK点上又被Netfilter预先注册了一系列hook回调函数,当每个清纯的数据包到达这些点后会被这些可恶hook函数轮番调戏一番。有时候我们就在想,只让系统自带的这些恶棍来快活,我自己能不能也make一个hook出来和它们同流合污呢?答案是肯定的。
我们来回顾一下目前系统中已经注册了的hook函数可分为以下几类:
它们在协议栈中位置如下:
首先我们心里要非常清楚的知道我们将要开发的这个hook函数位于哪个HOOK点的什么级别,它的前后分别是哪些函数,这一点很重要,因为遇到问题时至少心里有个谱。
我们今天讲的这个hook函数功能很简单,主要是向大家展示开发流程和方法。细节性的东西还需要每个人日积月累的修炼才行。
要注册一个hook函数需要用到nf_register_hook()或者nf_register_hooks()系统API和一个struct nf_hook_ops{}类型的结构体对象。最简单的hook函数如下:
#include <linux/kernel.h> #include <linux/module.h> #include <linux/ip.h> #include <linux/version.h> #include <linux/skbuff.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> #include <linux/moduleparam.h> #include <linux/netfilter_ipv4/ip_tables.h>
MODULE_LICENSE("GPL"); MODULE_AUTHOR("koorey KING"); MODULE_DESCRIPTION("My hook test");
static int pktcnt = 0; //我们自己定义的hook回调函数,丢弃每第5×n(n=1,2,3,4…)个ICMP报文。 static unsigned int myhook_func(unsigned int hooknum, struct sk_buff **skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { const struct iphdr *iph = (*skb)->nh.iph; if(iph->protocol == 1){ atomic_inc(&pktcnt); if(pktcnt%5 == 0){ printk(KERN_INFO "%d: drop an ICMP pkt to %u.%u.%u.%u !\n", pktcnt,NIPQUAD(iph->daddr)); return NF_DROP; } } return NF_ACCEPT; }
static struct nf_hook_ops nfho={ .hook = myhook_func, //我们自己的hook回调处理函数 .owner = THIS_MODULE, .pf = PF_INET, .hooknum = NF_IP_LOCAL_OUT, //挂载在本地出口处 .priority = NF_IP_PRI_FIRST, //优先级最高 };
static int __init myhook_init(void) { return nf_register_hook(&nfho); }
static void __exit myhook_fini(void) { nf_unregister_hook(&nfho); }
module_init(myhook_init); module_exit(myhook_fini); |
我们在LOCAL_OUT这个HOOK点上,以最高优先级NF_IP_PRI_FIRST注册了一个名为myhook_func()的函数。从本机发出的所有数据包从协议栈进入Netfilter框架时,最先都会被该函数所看到,然后我们在这里就可以“胡作非为”了。
这个模块最后会被编译成名为myhook.ko的驱动模块,然后用insmod来将其加载。具体操作流程如下:可以看到,我们自己的hook函数已经成功run起来了。我们可能不仅局限于做这么简单一个hook,没什么意义,也没啥成就感。况且这种hook压根儿就没有存在的价值,因为我们完全可以通过iptables来配置相应的规则而达到同样的目的。
OK,那我们就改造一下刚写的这个hook。让它实现的功能是:每收到5个ICMP报文就向指定的IP地址发送一个UDP报文。由于这个功能的开发牵扯到内核协议栈编程,关于协议栈部分打算在以后的系列博文中详细阐述。这里仅做个简单的普及入门就可以了。
我们要实现的功能是从内核中发一个报文,这完全不同于之前在用户层通过socket套接字编程的模式。
Godbach兄的文章http://blog.chinaunix.net/u/33048/showart_2043789.html,以及内核版的精华帖《教你修改以及重构skb》都是非常经典的参考文章。不太明白的童鞋可以去拜读一下:http://linux.chinaunix.net/bbs/thread-1152885-1-4.html。
再重申一下我们的目标,每收到5个ICMP报文就向指定IP(例如118.6.24.132)发送一个UDP报文。这里我是在内核里自己去DIY一个新的skb出来,构造数据包和发送数据包的过程如下:
#define ETH "eth0" //接口名称 #define SIP "192.168.6.130" //接口的IP地址 #define DIP "118.6.24.132" //要发送UDP报文的目的IP地址 #define SPORT 39804 //源端口 #define DPORT 6980 //目的端口 unsigned char SMAC[ETH_ALEN] = {0x00,0x0C,0x29,0x33,0x2C,0x3C}; //eth0网卡地址 unsigned char DMAC[ETH_ALEN] = {0x00,0x50,0x56,0xF4,0x8B,0xB3};//默认网关的网卡地址
static int build_and_xmit_udp(char * eth, u_char * smac, u_char * dmac, u_char * pkt, int pkt_len,u_long sip, u_long dip, u_short sport, u_short dport) { struct sk_buff * skb = NULL; struct net_device * dev = NULL; struct ethhdr * ethdr = NULL; struct iphdr * iph = NULL; struct udphdr * udph = NULL; u_char * pdata = NULL;
if(NULL == smac || NULL == dmac) goto out;
if(NULL == (dev= dev_get_by_name(eth))) goto out; //通过alloc_skb()来为一个新的skb申请内存结构 skb = alloc_skb(pkt_len + sizeof(struct iphdr) + sizeof(struct udphdr) + LL_RESERVED_SPACE(dev), GFP_ATOMIC);
if(NULL == skb) goto out; skb_reserve(skb, LL_RESERVED_SPACE(dev));
skb->dev = dev; skb->pkt_type = PACKET_OTHERHOST; skb->protocol = __constant_htons(ETH_P_IP); skb->ip_summed = CHECKSUM_NONE; skb->priority = 0;
skb->nh.iph = (struct iphdr*)skb_put(skb, sizeof(struct iphdr)); skb->h.uh = (struct udphdr*)skb_put(skb, sizeof(struct udphdr));
pdata = skb_put(skb, pkt_len); //预留给上层用于数据填充的接口 { if(NULL != pkt) memcpy(pdata, pkt, pkt_len); }
//“从上往下”填充skb结构,依次是UDP层--IP层--MAC层 udph = (struct udphdr *)skb->h.uh; memset(udph, 0, sizeof(struct udphdr)); udph->source = sport; udph->dest = dport; skb->csum = 0; udph->len = htons(sizeof(struct udphdr)+pkt_len); udph->check = 0; //填充IP层 iph = (struct iphdr*)skb->nh.iph; iph->version = 4; iph->ihl = sizeof(struct iphdr)>>2; iph->frag_off = 0; iph->protocol = IPPROTO_UDP; iph->tos = 0; iph->daddr = dip; iph->saddr = sip; iph->ttl = 0x40; iph->tot_len = __constant_htons(skb->len); iph->check = 0; iph->check = ip_fast_csum((unsigned char *)iph,iph->ihl);
skb->csum = skb_checksum(skb, iph->ihl*4, skb->len - iph->ihl * 4, 0); udph->check = csum_tcpudp_magic(sip, dip, skb->len - iph->ihl * 4, IPPROTO_UDP, skb->csum); //填充MAC层 skb->mac.raw = skb_push(skb, 14); ethdr = (struct ethhdr *)skb->mac.raw; memcpy(ethdr->h_dest, dmac, ETH_ALEN); memcpy(ethdr->h_source, smac, ETH_ALEN); ethdr->h_proto = __constant_htons(ETH_P_IP); //调用dev_queue_xmit()发送报文 if(0 > dev_queue_xmit(skb)) goto out;
out: if(NULL != skb) { dev_put (dev); kfree_skb (skb); } return(NF_ACCEPT); } |
上面这部分代码看不懂没关系,因为它需要比较熟练的内核协议栈编程知识,大家可以从整体上对其有个感性的把握就可以了。后面如果有时间我会再写个TCP/IP内核协议栈分析的系列文章,虽然CU上有很多大牛已经在写了,但每个人的收获不一样,和大家分享也是学习的另一种形式。好了,闲话不多说。我们这个hook的最终版本在“ myhook.zip ”下载。
接下来,激动人心的时刻又到了,我们来验证一下我们的hook函数是否可以按预期一样地进行工作。编译和加载流程如前面所述。我们为上层应用层往UDP报文中填充数据预留了接口,所以我们可以以如下的形式来调用build_and_xmit_udp()接口:
build_and_xmit_udp(ETH,SMAC,DMAC,”hello”,5,in_aton(SIP),in_aton(DIP),htons(SPORT),htons(DPORT));
通过wireshark抓包来验证一下是不是每收到5个ICMP报文就往118.6.24.132地址发送一个内容仅有“hello”字符串的UDP报文:
经过这么一番“改造”,我们自定义这个hook函数算是有点特色了。至此我们今天的内容就全部讲完了。估计有些人可能觉得还少了点什么,有没有悟性比较高的童鞋提出几点质疑来?没错,就是我们这个hook里设置的IP地址是固定的,包括MAC地址、源和目的端口以及发送的内容。用户空间我们根本没法对这些属性进行操作,骤然间,这个模块的可操作性和易用性大打折扣。那么我们到底如何才能从用户空间来操作这个新注册的hook呢?
未完,待续…
从用户空间来操作内核中Netfilter框架里自定义的HOOK函数
本文承上一篇博客。主要是和大家探讨一下如何从用户空间操作我已经注册到Netfilter中的自定义hook函数。有些童鞋可能就纳闷,难道iptables不能操作到么?如果我们需要让iptables操作我们在Netfilter框架中做过的扩展,那么最有效最直接的办法就是开发一个match或者target。但我们现在注册的这个hook很明显和iptables命令行工具没多大关系,你要让iptables来管理这不是强人所难么。当然如果你一要这么干的话,办法肯定是有的,但者不属于本文所要讨论的范畴。
说到内核空间与用户空间的通信,完全可以作为一个专题来讲,以后有机会把这方面总结一下写出来和大家分享。今天我们要用的方法就是借鉴了iptables和内核的通信方式,即采用getsockopt/setsockopt来实现用户空间和内核空间的交互。
还是继续上一篇的练习代码,我们在它的基础上继续修改润色。和注册hook函数时的操作非常类似,我们首先要实例化一个struct nf_sockopt_ops{}结构体对象,然后用Linux提供的nf_register_sockopt()函数来将该对象注册到全局双向链表nf_sockopts中去,当我们在用户空间调用g(s)etsockopt时经过层层系统调用,最后就会在nf_sockopts链表中找到我们已经注册的响应函数。关于g(s)etsockopt的执行流程,感兴趣的童鞋可以回头看一下博文十二里的详细讲解。罗嗦的这么多,大家都耐烦了吧。OK,我们赶紧动手。
… //添加必要的头文件 #include <linux/string.h> #include <linux/types.h> #include <asm/uaccess.h>
//增加我们自定义的扩充命令字 #define SOCKET_OPT_BASE 128 #define SOCKET_OPT_SETTARGET (SOCKET_OPT_BASE) #define SOCKET_OPT_GETTARGET (SOCKET_OPT_BASE) #define SOCKET_OPT_MAX (SOCKET_OPT_BASE +1)
#define ETH "eth0" //interface name #define SIP "192.168.6.130" //#define DIP "118.6.24.132" //把原来这个注释掉,现在看它越看越不顺眼。 … #define ADDRLEN 16 //IP地址的长度16字节,格式一般为xxx.xxx.xxx.xxx\0 static char dstIP[ADDRLEN]={0}; //这个就是我们要操作的目的IP。
//为了醒目,我将两个接口分别在不同的函数中来实现。 static int recv_cmd(struct sock *sk,int cmd, void __user *user,unsigned int len) { int ret = 0; if(cmd == SOCKET_OPT_SETTARGET) { memset(dstIP,0,ADDRLEN); if(0!=(ret = copy_from_user(dstIP,user,len)) { printk("error: can not copy data from userspace\n"); return -1; } printk("The target IP from User: %s \n",dstIP); } return ret; }
static int send_cmd(struct sock *sk,int cmd, void __user *user,int *len) { int ret = 0; if(cmd == SOCKET_OPT_GETTARGET) { if(0!=(ret =copy_to_user(user,dstIP,ADDRLEN))) { printk("error: can not copy data to userspace\n"); return -1; } printk("The target IP to User: %s \n",dstIP); } return ret; }
static struct nf_sockopt_ops my_sockops = { .pf = PF_INET, .set_optmin = SOCKET_OPT_SETTARGET, .set_optmax = SOCKET_OPT_MAX, .set = recv_cmd, .get_optmin = SOCKET_OPT_GETTARGET, .get_optmax = SOCKET_OPT_MAX, .get = send_cmd }; … static int index=1; static unsigned int hook_func(unsigned int hooknum, struct sk_buff **skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { const struct iphdr *iph = (*skb)->nh.iph; int ret = NF_ACCEPT;
if(iph->protocol == 1){ atomic_inc(&pktcnt); if(pktcnt%5 == 0) { //简单校验一下IP地址的合法性 if(strcmp(dstIP,””) !=0 && strcmp(dstIP,”0.0.0.0”)!=0) { printk(KERN_INFO "Sending the %d udp pkt !\n",index++); ret = build_and_xmit_udp(ETH,SMAC,DMAC,"hello",5, in_aton(SIP),in_aton(dstIP), htons(SPORT),htons(DPORT)); }else{ printk(“Have %d tims:target IP illegal.Nothing to do!\n”,pktcnt/5); } } } return ret; } … static int __init myhook_init(void) { nf_register_sockopt(&my_sockops); //注册我们的sockops对象 return nf_register_hook(&nfho); }
static void __exit myhook_fini(void) { nf_unregister_hook(&nfho); nf_unregister_sockopt(&my_sockops); //注销我们的sockops对象 } |
以上代码的着色部分,需要大家格外留意。因为是示例,所以合法性校验及错误处理的几个地方就是简单象征性地照顾了一下。内核部分的改动就弄完了,接下来我们继续写用户空间的代码usermyhook.c,如下:
#include <unistd.h> #include <stdio.h> #include <sys/socket.h> #include <linux/in.h> #include <errno.h> #include <string.h>
#define SOCKET_OPS_BASE 128 #define SOCKET_OPT_SETTARGET (SOCKET_OPS_BASE) #define SOCKET_OPT_GETTARGET (SOCKET_OPS_BASE) #define SOCKET_OPT_MAX (SOCKET_OPS_BASE +1)
int main(int argc,char** argv) { int sockfd,len,ret; char targetIP[16]={0};
if(0>(sockfd = socket(AF_INET,SOCK_RAW,IPPROTO_RAW))) { printf("can not create a socket\n"); return -1; }
//仅为示范而生。未作输入合法性和参数合法性校验。 if('s' == *argv[1]) { len = strlen(argv[2])+1; ret = setsockopt(sockfd,IPPROTO_IP,SOCKET_OPT_SETTARGET,argv[2],len); if(0 != ret) { printf("setsockopt error: - %d : %s\n",errno,strerror(errno)); return -1; } printf("setsockopt: ret=%d, wanted IP=%s\n",ret,argv[2]); }else{ len = sizeof(char)*16; ret = getsockopt(sockfd,IPPROTO_IP,SOCKET_OPT_GETTARGET,targetIP,&len); if(0 != ret) { printf("getsockopt error: - %d : %s\n",errno,strerror(errno)); return -1; } printf("getsockopt: ret=%d,gotten IP=%s\n",ret,targetIP); } close(sockfd); return 0; } |
将该文件编译:gcc -o umhook usermyhook.c
把重新编译出来的myhook.ko模块加入内核,然后用我们编译出来的umhook工具来动态指定我们要往哪个IP地址发送UDP报文。该工具的用法:
./umhook “s” “182.134.150.6” //设置目的IP
./umhook “g” //获取目的IP
当我们的myhook.ko模块刚加载时内核中的目的IP地址dstIP={0},所以在探测到第五个ICMP报文时并没有发送UDP报文;紧接着,我们用./umhook “s” “123.4.5.6”设置目的IP地址为“123.4.5.6”之后,内核探测到这次改变,打印出“The target IP from User:123.4.5.6”的提示信息;然后,在第10个ICMP报文被探测到后发送了第一条UDP到我们所配置的目的地址,抓包工具也有证实;之后,我们又将目的IP改为“123.4.5.7”,内核打印:“The target IP from User:123.4.5.7”在第15,20个ICMP报文被探测到后又发了两条UDP报文到新IP地址;最后,用./umhook “s” “”命令将目的IP清除掉。整个过程十分流程自然,而我们的心情也无比的愉悦。
后记:
整个Netfilter系列从清明节开始陆陆续续一直写到端午前夕,也算是对自己有个交代了,另外也总结出来和大家分享一下自己的心得和收获。看到网上经常有人问学习Netfilter有什么好资料或教程,其实个人觉得,Netfilter是无缝嵌入到协议栈里的。如果你想了解它的基本原理那么就需要一点协议栈知识就足够,如果你想为它做开发,那么在掌握了内核编程的基础上,还需要对协议栈的实现有相当深厚的底蕴才可以。由于本人也是刚接触Netfilter不久,学识浅薄,分析难免有所疏漏的地方的还请各位高手和大侠为小弟指正。
完。