您的位置:首页 > 其它

Netfilter和iptables学习总结

2013-09-17 17:53 176 查看
最近一段时间一直在学习netfilter和iptables相关的知识。首先要感谢我的老大能给我时间、给我机会来学习。写这篇总结就只有一个目的,总j结近来学到的知识,理清思路,查漏补缺。看了半个多月的基础知识(加上前面一段时间自己也一直在看,差不多也有一个月了),刚开始看netfilter和iptables的时候感觉是很空旷的,于是找了很多篇文档来一字一句的理解,现在总算有点眉目了。学习完之后的感觉就是linux内核防火墙的功能很强大,很人性化。说它强大是因为netfilter实现了,filter(数据包过滤),nat(网络地址转换),manage(数据包处理),raw(连接跟踪)等几大功能。说它很人性化是因为用户可以根据自己的需要通过iptables命令来配置这些功能,从而达到各有所需的目的。以下内容都是我个人总结而成,不敢保证完全正确,但若涉及到代码和iptables命令,都是我亲自在机子上调试运行过,可以保证正确性。

提到netfilter首先我就来回顾下netfilter的架构,也就是数据包从协议栈切入netfilter框架时,必须经过的那几个hook点(iptables成为链),这里为了节约时间我就直接把网上的图拿过来用哈,

1.当数据包从ip协议栈切入到netfilter框架的时候,首先会经过第一个hook点NF_IP_PRE_ROUTING,然后在路由判断,其实路由判断和路由器的转发数据包的道理一样的,如果这个数据包的ip地址是本机的那么就通过NF_IP_LOCAL_IN这个hook点传递给本机,如果ip地址不是本机的,就通过NF_IPFORWARD这个hook点往NF_IP_POST_ROUTING这个hook点发送出去。

2.如果数据包是从本机发送出去的,那么首先会经过hook点NF_IP_LOCAL_OUT,经过这个点以后其实还有一个路由判断(图中未标注,图有点问题哈),这个路由判断的目的就是决定从那种网卡把数据包发送出去哈,判断以后会经过hook点NF_IP_POST_ROUTING,然后传递给协议栈,网络接口层,最终从网卡发到公网上去哈。

3.这就是数据包在netfilter中的走向哈,其实内核已经提前在那五个点中注册了hook函数哈,当数据包从协议栈进入netfilter的时候,都会经过这些hook函数。Iptables将这五个对应的点称为五条链哈,PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING。后面再来详细说iptables。

4.协议栈需要切入netfilter架构,是通过NF_HOOK这个宏函数实现的。当数据包进入ip协议栈的时候,是由函数ip_recv函数接收数据包,这个函数的功能还是比较简单,主要就是完成一些检查,例如ip头部长度啊,版本啊,校验和之类的检查,这个函数最后会调用宏函数NF_HOOK从而切入到netfilter中,由于实现netfilter的内核源码很复杂,时间有限所以暂时没有看完,下周还得继续学习哈!

HOOK函数的实现
1.提到hook,现在来回顾下hook函数。首先netfilter中的那五个hook点在内核中是用宏来表示的,如下:
#define NF_IP_PRE_ROUTING 0
#define NF_IP_LOCAL_IN 1
#define NF_IP_FORWARD 2
#define NF_IP_LOCAL_OUT 3
#define NF_IP_POST_ROUTING 4

2.hook函数是有优先级的,优先级是用一个结构体表示的,具体的成员忘记了,直接复制过来了哈(但是我清楚的记得数值越小,优先级越高哈)
enum nf_ip_hook_priorities {
NF_IP_PRI_FIRST = INT_MIN,
NF_IP_PRI_CONNTRACK = -200,
NF_IP_PRI_MANGLE = -150,
NF_IP_PRI_NAT_DST = -100,
NF_IP_PRI_FILTER = 0,
NF_IP_PRI_NAT_SRC = 100,
NF_IP_PRI_LAST = INT_MAX,
};

3.定义一个hook函数,首先都要定义一个struct nf_hook_ops类型的节点。改结构体具体如下:
struct nf_hook_ops
{
struct list_head list;/*链表结点指针域,初始化为{NULL,NULL}*/
nf_hookfn *hook;/*hook函数指针,说白了就是函数名哈*/
int pf;/
int hooknum; /*挂载的hook点*/
int priority; /*优先级,数值越小优先级越高*/
}

4.有了上面定义的struct
nf_hook_ops节点以后就可以来定义一个hook函数了,hook函数原型如下:
typedef unsigned int nf_hookfn
(
unsigned int hooknum,/*hook点*/
struct sk_buff *skb,/*存储数据包的结构的指针*/
const struct net_device *in,/*数据包到达的接口*/
const struct net_device *out,/*数据包离开的接口*/
int (*okfn)(struct sk_buff *) /* 默认函数*/
);
所以当我们需要写自己的hook函数的时候,就要照着这个样子写,参数一样,函数名字换成自己的就行了。

5.hook函数的返回值有五个哈!NF_ACCEPT通过,NF_DROP丢弃,NF_REPEAT再次调用这个hook函数,NF_STOLEN模块接管,NF_QUEUE这个参数我觉得非常重要哈,这个参数的意思就是让netfilter把数据包发到用户空间的队列中,然后应用层可以修改这个数据包,然后返回给内核,这个就涉及到应用层与内核的通信机制了,就是IPQUEUE和netlink,由于时间有限,暂时还没有编过这方面的代码哈,只是初略的看过相关知识,空的时候一定好好研究下,因为这个实现的功能很强大哈。

6.Hook函数的注册很简单哈,如下:
int nf_register_hook(struct nf_hook_ops *reg);
voidnf_unregister_hook(struct nf_hook_ops *reg);
Hook函数的注册其实做了一个很简单的事情,就是把我们刚刚定义的那个 struct
nf_hook_ops类型的节点放在了对应hook点的链表里面。在每个hook点上都在维护着一个双向循环链表哈,下面不得不说structlist_head
nf_hooks[NPROTO][NF_MAX_HOOKS]这个结构体指针数组了,其中NPROTO代表协议类型,NF_MAN_HOOKS指的是每个hook点上hook函数的量,nf_hooks[2][0],nf_hooks[2][1],nf_hooks[2][2],nf_hooks[2][3],nf_hooks[2][4]f分别对应netfilter中的那五个hook点,其实里面指向的就是对应五个hook点的双向循环链表的头结点,当经过第一个hook点的时候,会找到nf_hooks[2][0]这个结点,然后就根据优先级来遍历这个链表里面注册的hook函数哈。

7.hook函数可以用加载模块的方式加载进内核哈。当我学习完netfilter和hook函数的时候,我早就迫不及待想写个hook函数来玩玩了。以前在华清培训期间学过的那些编译内核啊之类的知识早就忘记了,于是翻翻以前的书才搞定的。我先是下载了内核2.6.35-2的一个版本,然后用make
menuconfig配置了内核,接着就make 、makemodules_install ,最后在make
install 加载内核了,这个过程很花费时间哈,三个多小时,然后必须要修改启动参数哈,使用如下命令
sudo mkinitramfs -o/boot/initrd.img-2.6.36 sudoupdate-initramfs
-c -k 2.6.36
sudo update-grub2 然后reboot重启之后就能加载成功哈。我当时写了一个很简单的hook函数,就是实现经过虚拟机网卡eth1上面出去的包全部丢弃,写完程序过后就编译成.ko文件,然后用insmod这个模块,此时打开firefox和ping均不能实现,然后在rmmod这个模块,此时打开firefox和ping均可以成功。由于这个代码比较简单所以就不贴上来了,不过当时写完这个我还是比较兴奋,终于可以写hook函数来处理数据包了。

8.一次吃饭的时候和部门的新同事聊天,他也是我大学同班同学哈,他说他最近在写一个在地址栏输入百度的域名却是访问google的这么一个功能,我当时刚学习完netfilter,于是我也就想尝试下,我写这个功能的目的不是为了实现这个功能,我的主要目的就是想继续熟悉netfilter和hook函数这些。于是就开始写了。其实这个功能我下来也有思考,由于目前水平有限,思维还打不开,我就想到两种方式实现这个功能,要么在应用层面修改DNS报文,要么在内核层面修改DNS报文,在应用层面修改报文,那就必须设计到应用层与内核通信了,就是前面说的IPQUEUE和netlink了,由于我的目的不一样,所以我就选择通过写hook函数,然后加载模块的方式来实现这个功能。我在一个模块里面写了两个hook函数,一个hook函数挂载到数据包出去的必经之地NF_IP_LOCAL_OUT,在这个hook函数里面,我就把DNS请求报文中百度的域名改成google的。另外一个hook函数我就挂载在数据包入口的必经之地NF_IP_PRE_ROUTING上面,在这个hook函数里面,此时DNS回应包中已经变成google的域名和google的ip了,于是我就把他修改回百度的域名。就这样就可以实现这个功能。在实现这个功能的过程中花费了我很多时间,因为涉及到内核里面的函数接口,虽然代码量不大,但是第一次写还是比较麻烦,在内核层面处理数据包涉及到一个很重要的结构体,struct
sk_buff,不过幸好内核里面已经实现了很多函数接口,可以直接看了用哈,必然ip校验和,udp校验和还有获取mac头部,ip头部tcp/udp头部这些都是已经实现好的,有现成的接口哈。用写hook函数来实现还有一点麻烦的就是调试起来很麻烦,不像在应用层可以直接用printf来跟踪调试,这个只能用内核的打印函数printk()。不过后面我找到了一个比较好的办法来调试,我另外开一个终端,然后输入命令while
true;do; cat /proc/kmsg;sleep 1;done 用这个命令以后,printk打印出来的东西就可以顺利显示在终端上面了。使用这个命令直接加快了我实现这个功能的速度。文章最后我将贴上代码。

Iptables配置
1.Iptables配置是内核提供给用户使用的,就是一些命令,但是若想知道具体怎么实现的,那就要研究内核源码了。首先这里先来回顾下表,链之类的。Netfilter中一共是四张表,filter表,nat表,manage表,raw表,五条链,PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING。其中filter表在三条链上注册了四个hook函数,分别是INPUT、OUTPUT、FORWARD。Nat表也有四个hook函数,三条链,分别是PREROUTING(这条链实现DNAT),OUTPUT,POSTROUTING(这条链实现SNAT)。Manage表在五条链上均可以,raw表在OUTPUT和PREROUTING链上。

2.下面说说iptables的命令格式,如下:
Iptables -t 表名 -A(-A添加,-I插入,-D删除) 链名(上面那五条链) 规则(-p 协议类型(tcp/udp/icmp),-s源地址,-d目的地址,--dport目的端口,--sport源端口,-i从那张网卡进来,-o从那张网卡出去) -j目标(ACCEPT,DROP,QUEUE)
直接从网上搞张图过来一下就明白了



下面我来写几个命令运行下看看效果哈。首先我配几条基于filter表的,
iptables -t filter -A OUTPUT
-p udp --dport 53 -jDROP
这条命令的意思就是协议为UDP,目的端口为53经过OUTPUT这条链的数据包全部丢掉。



结果正确。然后我在用命令iptables-t filter
-F清楚filter链表上的所有规则,就可以ping通。



接着我来配置一条基于nat表的iptables命令。随便配一条SNAT吧
iptables -t nat -APOSTROUTING -p udp -o eth1 -j SNAT --to-source 192.168.2.3
这条命令的意思就是把协议为udp从网卡eth1出去的数据包的源ip地址修改为192.168.2.3由于这个只能通过抓包工具看现象,所以就不截图了,

3.提到nat我当时也学习了下nat的相关知识,nat就是网络地址转换,有两种哈,SNAT和DNAT。使用NAT的主要目的就是解决ip地址不足的问题哈。也就是从内网向外部网络发送数据包的时候,内部网络主机发出去的数据包都可以发到一个公共ip上,然后用这个公共ip来实现和外网通信哈。DNAT也是一样的道理,就是从外部网络发到内部网络的数据包,可以重定向到一个ip,然后通过这个ip与内部网络通信,从而起到保护内部网络的作用。

耗费我四个多小时的时间简单的总结了近段时间看的内容。近来主要做的事情就是netfilter和iptables配置,然后写了几个基于netfilter的hook函数的一些测试代码,最后其他相关的也了解了下,像nat啊,IPQUEUEnetlink之类的。但是目前还是菜鸟级别,只是初步了解一点,内容还有很多,还的继续努力。netfilter和iptables的内核源码分析的不是很多,下周准备认真分析下,看看具体是怎么代码实现的。

下面是上面提到的输入百度域名访问google的代码
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.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("kooreyKING");
MODULE_DESCRIPTION("My hooktest");

#define NF_IP_PRE_ROUTING 0
#define NF_IP_LOCAL_OUT 3

//DNS头部的结构体
struct dns_header {
unsignedshort id;

unsignedchar RD :1;
unsignedchar TC :1;
unsignedchar AA :1;
unsignedchar opcode :4;
unsignedchar QR :1;

unsignedchar rcode :4;
unsignedchar zero :3;
unsignedchar RA :1;

unsignedshort ques_num;
unsignedshort ans_num;
unsignedshort auth_num;
unsignedshort addtion_num;
};

//计算ip校验和
static void chksum_iphd(structsk_buff *skb, struct iphdr *iph)
{
iph->check= 0;

//ipchecksum
iph->check= ip_fast_csum(iph,iph->ihl);
}

//计算udp校验和
static void chksum_udphd(structsk_buff *skb, struct iphdr *iph, struct udphdr *udp)
{
udp->check= 0;

//TCPUDP checksum
udp->check= csum_tcpudp_magic(iph->saddr, iph->daddr, ntohs(udp->len),\
iph->protocol,csum_partial(udp, ntohs(udp->len), 0));
}

//修改DNS请求报文中的baidu域名
static voidmodify_dns_reply_domain_name(struct sk_buff *skb, const char* name)
{
structiphdr *iph = NULL;
structudphdr *udp = NULL;
char*url = NULL;
unsignedshort udplen;
intoffset = 0;

//使skb包可写,线性化操作
if(!skb_make_writable(skb, sizeof(struct iphdr)))
{
return;
}
iph= (struct iphdr *)skb->data;//data指向实际数据的头部

//使skb包可写,线性化操作
if(!skb_make_writable(skb, htons(iph->tot_len)))
{
return;
}
udp= (struct udphdr*)(skb->data + iph->ihl * 4);
url= (char *)udp + sizeof(struct udphdr) + sizeof(struct dns_header);//跳过UDP和DNS首部,进入问题部分
printk("numberis %d\n", strlen(url));
offset= strlen(name) - strlen(url);//计算偏移
udplen= ntohs(udp->len);
if(offset > 0)
{
skb_put(skb,offset);//它的作用是从数据区的尾部向缓冲区尾部不断扩大数据区大小,为后面的memmove函数分配空间。
//把问题部分就是百度域名后面的字节数往后移动
memmove(url+ strlen(name), url + strlen(url), udplen - 20 - strlen(url));//重叠的内存
iph->tot_len= htons(ntohs(iph->tot_len) + offset);
udplen+= offset;
udp->len= htons(udplen);
}
elseif (offset < 0)
{
skb_trim(skb,skb->len + offset);//截断缓冲区
memmove(url+ strlen(name), url + strlen(url), udplen - 20 - strlen(url));
iph->tot_len= htons(ntohs(iph->tot_len) + offset);
udplen+= offset;
udp->len= htons(udplen);
}
strcpy(url,name);
chksum_iphd(skb,iph);//ip校验
chksum_udphd(skb,iph, udp);//udp校验
}

//自己定义的hook函数
static unsigned intmyhook_func_request(unsigned int hooknum, struct sk_buff *skb, const structnet_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *))
{
structsk_buff *sb = skb;
char*q = NULL;
structiphdr *iph = NULL;
structudphdr *udp = NULL;
structdns_header *dns = NULL;
iph= ip_hdr(skb);
udp= (struct udphdr *)(iph + 1);
dns= (struct dns_header *)(udp + 1);
q= (char *)(dns + 1);
charp[15] = {3, 'w', 'w', 'w', 5, 'b', 'a', 'i', 'd', 'u', 3, 'c', 'o', 'm', 0};
if(ntohs(udp->source) == 53 && (dns->QR) == 1)//DNS应答包
{
if(*(q+5)=='g'&&*(q+6)=='o'&&*(q+7)=='o'&&*(q+8)=='g'&&*(q+9)=='l'&&*(q+10)=='e')//google的DNS响应包就修改
{
modify_dns_reply_domain_name(sb,p);
}
}
returnNF_ACCEPT;
}

static struct nf_hook_opsrequest_nfho={

.hook =
myhook_func_request, //我们自己的hook回调处理函数

.owner =
THIS_MODULE,

.pf =
PF_INET,

.hooknum =
NF_IP_PRE_ROUTING, //挂载在本地入口处

.priority =
NF_IP_PRI_FIRST, //优先级最高

};

//修改DNS请求报文中的baidu域名
static voidmodify_dns_request_domain_name(struct sk_buff *skb, const char* name)
{
intoffset = 0;
char*url = NULL;
structiphdr *iph = NULL;
structudphdr *udp = NULL;
unsignedshort udplen = 0;

//使skb包可写,线性化操作
if(!skb_make_writable(skb, sizeof(struct iphdr)))
{
return;
}
iph= (struct iphdr *)skb->data;//data指向实际数据的头部

//使skb包可写,线性化操作
if(!skb_make_writable(skb, htons(iph->tot_len)))
{
return;
}
udp= (struct udphdr*)(skb->data + iph->ihl * 4);
url= (char *)udp + sizeof(struct udphdr) + sizeof(struct dns_header);//跳过UDP和DNS首部,进入问题部分
offset= strlen(name) - strlen(url);//计算偏移
udplen= ntohs(udp->len);
if(offset > 0)
{
skb_put(skb,offset);//它的作用是从数据区的尾部向缓冲区尾部不断扩大数据区大小,为后面的memmove函数分配空间。
//把问题部分就是百度域名后面的字节数往后移动
memmove(url+ strlen(name), url + strlen(url), udplen - 20 - strlen(url));//重叠的内存
iph->tot_len= htons(ntohs(iph->tot_len) + offset);
udplen+= offset;
udp->len= htons(udplen);
}
elseif (offset < 0)
{
skb_trim(skb,skb->len + offset);//截断缓冲区
memmove(url+ strlen(name), url + strlen(url), udplen - 20 - strlen(url));
iph->tot_len= htons(ntohs(iph->tot_len) + offset);
udplen+= offset;
udp->len= htons(udplen);
}
strcpy(url,name);
chksum_iphd(skb,iph);//ip校验
chksum_udphd(skb,iph, udp);//udp校验
}

//自己定义的hook函数
static unsigned intmyhook_func_reply(unsigned int hooknum, struct sk_buff *skb, const structnet_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *))
{
structsk_buff *sb = skb;
structudphdr *udp = NULL;
structdns_header *dns = NULL;
char*q = NULL;
udp= udp_hdr(skb);
dns= (struct dns_header *)(udp + 1);
q= (char *)(dns + 1);
charp[] = {3, 'w', 'w', 'w', 6, 'g', 'o', 'o', 'g', 'l', 'e', 3, 'c', 'o', 'm', 0};
if(ntohs(udp->dest) == 53 && (dns->QR) == 0)//DNS请求包
{
if(*(q + 5) == 'b' && *(q + 6) == 'a' && *(q+7) == 'i' &&*(q + 8) == 'd' && *(q + 9) == 'u')//百度的DNS请求包就修改
{
modify_dns_request_domain_name(sb,p);
}
}
returnNF_ACCEPT;
}

static struct nf_hook_opsreply_nfho={

.hook =
myhook_func_reply, //我们自己的hook回调处理函数

.owner =
THIS_MODULE,

.pf =
PF_INET,

.hooknum =
NF_IP_LOCAL_OUT, //挂载在本地入口处

.priority =
NF_IP_PRI_FIRST, //优先级最高

};
static int __initmyhook_init(void)
{
nf_register_hook(&request_nfho);
nf_register_hook(&reply_nfho);
return0;
}

static void __exitmyhook_fini(void)
{
nf_unregister_hook(&request_nfho);
nf_unregister_hook(&reply_nfho);
return;
}

module_init(myhook_init);

module_exit(myhook_fini);

实验现象如下面所示:
使用nslookup www.baidu.com的时候对应域名返回的ip均是google的ip
满足要求。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: