分享

linux实现流量监控的几种方法

 心不留意外尘 2016-05-02

http://blog.sina.com.cn/s/blog_6a1837e90100v9ye.html


一、使用iptables命令;

    URL: http://www./post/340/

二、修改Netfilter的limit模块;

    URL: http://blog.csdn.net/dog250/article/details/6940578

三、iptables的五个HOOK点;

四、使用libpcap:需移植,暂未深入研究;

五、修改内核:暂无方案;

 

附:http://whyxx.blog.51cto.com/2227948/560914 

 

--------------------------------------------------------------------------------------------------

一、使用iptables命令:

 

 相信不少朋友都知道,使用Linux搭建路由网关,提供nat上网服务是非常简单的事情,而且性能也不错。但现在p2p的工具很多,有时候带宽会被这些工具在无意中就占满了(例如:使用迅雷、BT下载等)。这时候,总希望看看到底是谁在占用带宽。这样的工具有很多,如ntop、bandwidthd、iftop、IPTraf、MRTG等等,它们也提供了非常方便的图形监控界面,操作也非常简单。可惜,它们都有一些缺点,像实时性不够、IP流量分散、需要使用Web来查看等,恰好这些就是好我需要的。
    为此,我利用iptables的统计功能,编写了一个小脚本来实现要求。(原想用Perl的Net::Pcap模块的对数据包解码统计的,但既然有现成的,为什么不用呢?)O(∩_∩)O哈哈~

一、查看网卡流量
首先,可能我们需要查看的是服务器上总的网卡流量。这个Linux提供了很好的数据:

引用
# cat /proc/net/dev
Inter-|   Receive                                                 Transmit
face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
    lo:10020933   79976                              0 10020933   79976                           0
  eth0:3274190272 226746109 438150 858758 369237                     0 2496830239 218418052                           0
  sit0:                                                                             0
  tun0:                                                                             0
  tun1:    4675      51                                 8116      48                           0
  tun2:   51960     562                               249612    3077                           0
  ppp0:4163571679 12086605                              0 3089285665 15934370                           0


这是网络启动后,通过服务器上各网卡的总流量,但不是很直观。(受排版影响,不太好看)
这里,提供一个小工具:


这工具不是我写的,作者是。使用非常简单:

引用
# sh flow.sh
Usage: flow.sh <ethernet device> <sleep time>
    e.g. flow.sh eth0 2
# sh flow.sh ppp0 2
IN: 232 KByte/s   OUT: 30 KByte/s
IN: 230 KByte/s   OUT: 38 KByte/s
IN: 241 KByte/s   OUT: 30 KByte/s


给出您要监控的网卡设备,然后是间隔时间,即会告诉您该设备的流量。

二、查看客户端IP实际流量的原理
接下来,进入我们的正题。除了通过上述脚本可以查看到网卡的实际流量外,我们该如何查看每个客户端的单独流量呢?先说说原理吧。
1、iptables设置
该过程最主要的就是利用了iptables的统计功能。
当我们用iptables实现nat转发后,所有的数据包要出去,必须要通过这台网关服务器,也就是说,我们只要在上面监控即可。并且,这些数据包都会经过iptables的FORWARD chain。这时,我们只要给iptables加上下述语句:

# iptables -I FORWARD -s 192.168.228.200 -j ACCEPT
# iptables -I FORWARD -d 192.168.228.200 -j ACCEPT


那么,通过192.168.228.200(客户端)经该服务器路由网关转发出去的数据包就会记录在iptables FORWARD chain中。
如:

引用
# iptables -v -n -x -L FORWARD
Chain FORWARD (policy DROP 5 packets, 351 bytes)
    pkts      bytes target     prot opt in     out     source               destination
2834533 360907743 ACCEPT     all  --             192.168.228.200      0.0.0.0/0
3509528 3253144061 ACCEPT     all  --             0.0.0.0/0            192.168.228.200


这样,我们通过一些简单的运算,就可以得到实际的流量:

引用
# iptables -L -v -n -x|grep '192.168.228.200';sleep 3;iptables -L -v -n -x|grep '192.168.228.200'
2872143 365711591 ACCEPT     all  --             192.168.228.200      0.0.0.0/0
3555831 3297100630 ACCEPT     all  --             0.0.0.0/0            192.168.228.200
2872750 365777302 ACCEPT     all  --             192.168.228.200      0.0.0.0/0
3556591 3297814562 ACCEPT     all  --             0.0.0.0/0            192.168.228.200
# echo '(3297814562-3297100630)/1024/3'|bc
232
# echo '(365777302-365711591)/1024/3'|bc
21


原理就是这么简单。
※ 注意,FORWARD chain记录的流量中,不经过该网关转发的流量不会记录。也就是说,若你从该服务器上直接下载,流量是记录在INPUT和OUTPUT chain,而不是FORWARD中的。要统计那些数据,方法是相同的。

--------------------------------------------------------------------------------------------------

二、修改Netfilter的limit模块

1.问题和思路

linux内核的netfilter框架中有一个叫做limit的模块,用于匹配单位时间内过往的包的数量,注意,这个模块实现了一个match,而不能直接用于流控的目的,因此你不能直接使用下列的命令实现流控:
iptables –A FORWARD –s xxx –d yyy –m limit ...  –j DROP
因为这样的话,所有匹配到的数据包就都被drop掉了。你应该这么做:
iptables –A FORWARD –s xxx –d yyy –m limit ... –j ACCEPT
iptables –A FORWARD –s xxx –d yyy –j DROP
然而仍然需要注意的是,这个match是基于包的数量的,而不是基于数据字节流量的,因此这种流控方式很不准确,如上,限制单个基于ip地址的流在每秒发送20个数据包,而这20个数据包可能是20个mtu大小的数据包,也可能是20个1字节ip载荷大小的数据包,也可能仅仅是20个tcp的ack包,这样的流控显然不是真正的流控。
    我现在需要做的是基于单个源ip进行秒级别的入口流量的字节限速,怎么做呢?当然可以通过tc来做,那就是使用tc的police策略来进行配置,可是那样的话有问题,第一个问题就是police没有队列,这就意味着所有超额的流量将被丢弃而不是被缓存,这也许就是tc社区为何说linux入口限速做的不甚好的原因之所在吧;第二个问题就是你需要把所有需要被限速的ip地址作为filter的匹配规则显式的配置出来,而这会导致策略表的快速膨胀,大大增加了内存的占用。因此不到万不得已,我不会再考虑使用tc来完成这个流控。
    接下来要考虑的就是使用iptables统计来完成流控。因为netfilter会纪录所有rule的统计信息,因此周期的调用iptables–L –x –n …然后将统计信息相减后除以调用周期,使用外部脚本来完成这个流控实际上也是可以的。然而这又会面对和tc同样的问题,既然需要iptables来统计信息,那么统计哪些流量的信息你同样需要显式配置出来,这同样会导致filter表的膨胀,最终导致内存占用以及遍历filter的转发效率的降低。
    于是乎,办法还要想别的,最直接的办法就是自己实现。简单点考虑,我也不要什么队列,既然tc都没有入口整形队列,那我也不要,超过限额的全部丢弃即可。最直接的方案就是修改netfilter的limit模块,因为它足够简单,扩展它时阻力最小,于是乎,改了它!修改动作很少,基本分为四点:
第一:维护一个list_head,保存所有的到达本机的ip数据报的源ip地址;
第二:修改match函数,在源ip链表中寻找该数据包的源ip,若找到,取出统计信息,看看一秒内流量是否超限,若是,则匹配,若没有则不匹配;如果在链表中没有找到,则创建一个entry,记录下当前时间和当前数据包长度,返回不匹配;将找到的entry取出,重新插入到head位置,或者将新创建的entry插入到head位置,这样可以模拟lru,为第四步创造好处;
第三:如果链表长度满了,则匹配所有的数据包;
第四:需要新增加entry且链表已经满了时,根据entry的上次更新时间以及最短不惑跃时间看是否能删除某一个entry。
上述四个步骤大体上分两个阶段实现,第一阶段暂时不实现第四点,这也符合我的一贯风格,第四点以及模块释放时的善后工作暂时没有测试,首先要把功能先跑通。现在假设已经实现了上述所有,我只需要配置以下的规则就可以实现针对每一个源ip进行限速了:
iptables –A –FORWARD/INPUT –m –limit 20/sec –j MY_CHAIN
注意,上述的20/sec已经不再是基于包数量的了,而是基于字节的,并且,我没有直接drop掉这些包,而是交给了一个自定义的chain来处理,这样可以方便的将机制和策略进行分离,或许管理员并不是想丢弃这些超限包,而只是纪录下日志,或许管理员会永远封死这些ip地址,也许仅仅封死一段时间,待收到罚金之后再给予开放…

2.实现

首先定义数据结构。以下的数据结构是一个包装,定义了一个全局的链表,以及一些控制参数,由于这个只是个测试版,因此没有考虑多处理器的并发处理,因此也就没有定义spin_lock,在正式的实现中,一定要定义一个lock的。

  1. struct src_controler  
  2.         struct list_head src_list;  
  3.         int curr;     //当前一共有多少了entry   
  4.         int max;    //最多能有多少个entry   
  5. };  


下面的一个结构体定义了一个源地址entry中包含哪些东西,无非就是一秒内已经过去了多长的数据包以及时间戳等信息。

  1. struct src_entry  
  2.         struct list_head list;  
  3.         __u32   src_addr;    //源地址   
  4.         unsigned long prev;    //上次的时间戳   
  5.         unsigned long passed;    //一秒内已经过去了多少数据   
  6. };  


struct src_controler *src_ctl;    //全局变量
接下来就是修改模块的初始化和卸载函数

  1. static int __init xt_limit_init(void)  
  2.  
  3.         int ret;  
  4.         src_ctl kmalloc(sizeof(struct src_controler), GFP_KERNEL); //初始化全局变量   
  5.         memset(src_ctl, 0, sizeof(struct src_controler));  
  6.         INIT_LIST_HEAD(&src_ctl->src_list);    //初始化全局变量的链表   
  7.         src_ctl->curr 0;  
  8.         src_ctl->max 1000;    //本应该通过模块参数传进来的,这里写死,毕竟是个测试版   
  9.   
  10.         ret xt_register_match(&ipt_limit_reg);  
  11.         if (ret)  
  12.                 return ret;  
  13.   
  14.         ret xt_register_match(&limit6_reg);  
  15.         if (ret)  
  16.                 xt_unregister_match(&ipt_limit_reg);  
  17.   
  18.         return ret;  
  19.  
  20. static void __exit xt_limit_fini(void)  
  21.  
  22.         xt_unregister_match(&ipt_limit_reg);  
  23.         xt_unregister_match(&limit6_reg);  
  24.         //这里应该有一个清理链表的操作,测试版没有实现   
  25.  


最后,编写match回调函数,删掉原来的,自己写新的逻辑

  1. static int  
  2. ipt_limit_match(const struct sk_buff *skb,  
  3.                 const struct net_device *in,  
  4.                 const struct net_device *out,  
  5.                 const struct xt_match *match,  
  6.                 const void *matchinfo,  
  7.                 int offset,  
  8.                 unsigned int protoff,  
  9.                 int *hotdrop)  
  10.  
  11.         struct xt_rateinfo *r ((struct xt_rateinfo *)matchinfo)->master;  
  12.         unsigned long now jiffies, prev 0;  
  13.         struct list_head *lh;  
  14.         struct src_entry *entry NULL;  
  15.         struct src_entry *find_entry;  
  16.         unsigned long nowa;  
  17.         struct iphdr *iph skb->nh.iph;  
  18.         __u32 this_addr iph->saddr;  
  19.   
  20.         list_for_each(lh, &src_ctl->src_list) //遍历链表,找到这个ip地址对应的entry   
  21.                 find_entry list_entry(lh, struct src_entry, list);  
  22.                 if (this_addr == find_entry->src_addr)  
  23.                         entry find_entry;  
  24.                         break;  
  25.                  
  26.          
  27.         if (entry) //如果找到,将其加在头,这样实现了一个简单的lru   
  28.                 prev entry->prev;  
  29.                 list_del(&entry->list);  
  30.                 list_add(&entry->list, &src_ctl->src_list);  
  31.         else    //如果没有找到,看看能否添加   
  32.                 if (src_ctl->curr+1 src_ctl->max)  
  33. add_entry:  
  34.                         entry kmalloc(sizeof(struct src_entry), GFP_KERNEL);  
  35.                         memset(entry, 0, sizeof(struct src_entry));  
  36.                         entry->src_addr this_addr;  
  37.                         prev entry->prev now 1000;  
  38.                         list_add(&entry->list, &src_ctl->src_list);  
  39.             src_ctl->curr++;    //正确做法是atomic_inc   
  40.                 else //如果已经满了,那么看看能否删除最后的那个不活动的entry   
  41.                         entry list_entry(src_ctl->src_list.prev, struct src_entry, list);  
  42.                         if (now-entry->prev 1000)  
  43.                                 goto add_entry;  
  44.                         return 1;  
  45.                  
  46.          
  47.         nowa entry->passed skb->len;  
  48.         if (now-prev 1000)    //这里的1000其实应该是HZ变量的值,由于懒得引头文件了,直接写死了。如果距上次统计还没有到1秒,则累加数据,不匹配   
  49.                 entry->passed nowa;  
  50.                 return 0;  
  51.         else  
  52.                 entry->prev now;      
  53.                 entry->passed 0;  
  54.                 if (r->burst >= nowa)    //如果到达了1秒,则判断是否超限,如果超限,则匹配,没有超限则重置字段,不匹配   
  55.                         return 0;  
  56.                 else  
  57.                         return 1;  
  58.                  
  59.          
  60.         return -1;    //不会到达这里   
  61.  


编译之:
make -C /usr/src/kernels/2.6.18-92.el5-i686 SUBDIRS=`pwd` modules
使用之:
#!/bin/bash
iptables -A INPUT -d 192.168.1.247/32 -m limit --limit 1/sec --limit-burst $1 -j $2
运行上述脚本:test.sh 1000 DROP
然后下载大文件,看看是否被限速了!...
注意,上述的实现中,数据单位是字节,其实正常起码应该是100字节,做成可配置的会更好。

3.优化和反思

优化一:上述实现中,使用list_head在大量源ip的情况下,遍历链表的开销比较大,虽然lru原则可以最大的减小这种开销,但是还是很大,特别是用户并不想超限,反而间隔相对久的时间访问一次,大量这样的用户和大量频繁访问的用户混杂在一起,频繁访问的用户的entry会一直在前面,遍历时开销较小,而大量间隔相对久访问的用户的entry会在后面,遍历开销比较大,这会不会导致dos攻击,我由于没有环境还真的没有测试。事实上使用hash表来组织它们是更好的选择,linux的ip_conntrack中的纪录就是使用hash表来组织的,软件嘛,就这几种数据结构。
优化二:上述实现中,仅仅是针对源地址进行流量匹配,而没有管目的地址,因为开始说了,针对目的地址的流控可以用tc实现,然而那样的话,需要显式配置filter,很不方便,因此这个实现应该加个配置,用于针对任意目的地址进行流量控制,比tc方便多了。

优化三:上述实现中,数据单位是字节,这样很不合理,应该是可以配置的才对,比如默认是字节,还可以是k,m,g等等。

优化四:应该实现一个机制,定期清理不活跃的entry,以防止内存占用率过高。

反思:为何在入口位置的流控不实现队列呢?我们还是要想想流控的目的是什么,其一就是避免拥塞-网络的拥塞以及主机上层缓冲区的拥塞,对于接收数据而言,无论如何,流量对到达此地之前的网络的影响已经发生了,对往后的网络的影响还没有发生,因此对于已经发生的影响,没有必要再去进行速率适配了,直接执行动作即可。

     如果你真的还需要limit模块完成它本来的功能,那么就别改limit模块了,还是直接写一个为好,这样也更灵活,毕竟我们也就不需要再配置--limit 1/sec去迎合limit的语法了,具体方法参见《编写iptables模块实现不连续IP地址的DNAT-POOL

修正:
如果同时下载多个局域网内的大文件,会发现上述的match回调函数工作的不是很好,速度并没有被限制住,这是因为我计时统计统计的粒度太粗,一秒统计一次,这一秒中,很多大包将溜过去,因此需要更细粒度的统计,那就是实时的统计,使用数据量/时间间隔这个除式来统计,代码如下:

  1. static int  
  2. ipt_limit_match(const struct sk_buff *skb,  
  3.                 const struct net_device *in,  
  4.                 const struct net_device *out,  
  5.                 const struct xt_match *match,  
  6.                 const void *matchinfo,  
  7.                 int offset,  
  8.                 unsigned int protoff,  
  9.                 int *hotdrop)  
  10.  
  11.         struct xt_rateinfo *r ((struct xt_rateinfo *)matchinfo)->master;  
  12.         unsigned long now jiffies, prev 0;  
  13.         struct list_head *lh;  
  14.         struct src_entry *entry NULL;  
  15.         struct src_entry *find_entry;  
  16.         unsigned long nowa;  
  17.     unsigned long rate;  
  18.         struct iphdr *iph skb->nh.iph;  
  19.         __u32 this_addr iph->saddr;  
  20.   
  21.         list_for_each(lh, &src_ctl->src_list)  
  22.                 find_entry list_entry(lh, struct src_entry, list);  
  23.                 if (this_addr == find_entry->src_addr)  
  24.                         entry find_entry;  
  25.                         break;  
  26.                  
  27.          
  28.         if (entry)  
  29.                 prev entry->prev;  
  30.                 list_del(&entry->list);  
  31.                 list_add(&entry->list, &src_ctl->src_list);  
  32.         else  
  33.                 if (src_ctl->curr+1 src_ctl->max)  
  34. add_entry:  
  35.                         entry kmalloc(sizeof(struct src_entry), GFP_KERNEL);  
  36.                         memset(entry, 0, sizeof(struct src_entry));  
  37.                         entry->src_addr this_addr;  
  38.                         prev entry->prev now 1000;  
  39.                         list_add(&entry->list, &src_ctl->src_list);  
  40.                         src_ctl->curr++;  
  41.                 else  
  42.                         entry list_entry(src_ctl->src_list.prev, struct src_entry, list);  
  43.                         if (now-entry->prev 1000)  
  44.                                 goto add_entry;  
  45.                         return 1;  
  46.                  
  47.          
  48.         nowa entry->passed skb->len;  
  49.     entry->passed nowa;  
  50.     if (now-prev 0)  
  51.         rate entry->passed/(now-prev);  
  52.     else  
  53.         rate nowa;  
  54.     entry->prev now;  
  55.     entry->passed 0;  
  56.     if (rate r->burst)  
  57.         return 1;  
  58.      
  59.         return 0;  
  60.  

 

--------------------------------------------------------------------------------------------------

三、使用iptables的HOOK挂载点:

 

1. 模块分析

针对ipv4协议,内核的防火墙框架NetfilterIP层精心挑选了五个插入点:NF_IP_PRE_ROUTING,NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING,分别对应IP层的五个不同位置。这样,用户可以选择合适的切入点添加自己的内核模块。

在这里,我们针对从网卡接收进来的包进行过滤处理,基于源地址+端口号进行过滤,该程序的功能可以丢弃来自指定IP地址+端口号的数据包。需要针对所有进入系统的包进行过滤,不属于发往本机的数据包我们不予理会,因此我们选择模块的切入点在NF_IP_LOCAL_IN,该切入点的位置在IP层向上层协议栈传递数据包的函数ip_local_deliver()  

NF_HOOK(PF_INET,NF_IP_LOCAL_IN,skb,skb->dev,NULL,ip_local_deliver_finish)

IP packet NF_HOOK() IPv4 协议栈上钩出来以后,就进入 linux- 2.4.19/net/core/netfilter.c 中的 nf_hook_slow() 函数进行处理。这个函数干的主要事情,就是根据 nf_hooks[][] 数组开始处理 packet。而这个nf_hooks[][]链表二维数组的原型为:  

struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];

该二维数组就是根据【协议类型】【钩子点】存储的该协议类型在这个钩子点上的处理函数,任何一个希望使用Netfilter钩子的模块都需要将模块处理函数在nf_hooks数组的相应链表上进行注册。我们编写的防火墙函数模块,实际上就是生成一个struct nf_hook_ops结构的实例,并用nf_register_hook将其登记在nf_hooks[][]上。该结构的原型为:

struct nf_hook_ops
{
       struct list_head list;  //
链表
       nf_hookfn *hook;   //
处理函数指针
       int pf;             //
协议好
       int hooknum;       //HOOK

       int priority;         //
优先级
};

   该结构的本质,是一个 nf_hookfn 函数。这个函数将对被钓上来的 IP packet 进行初步的处理。现在来看看nf_hookfn函数的原型:

            typedef unsigned int nf_hookfn(unsigned int hooknum,
                                        struct sk_buff **skb,
                                        const struct net_device *in,
                                        const struct net_device *out,
                                        int (*okfn)(struct sk_buff *));

   nf_hookfn函数的第一个参数用于指定hook类型。第二个参数一个指向指针的指针,该指针指向的指针指向一个 sk_buff数据结构,网络堆栈用sk_buff数据结构来描述数据包。紧跟在skb之后的两个参数是指向net_device数据结构的指针,net_device数据结构被Linux内核用于描述所有类型的网络接口。这两个参数中的第一个——in,用于描述数据包到达的接口,毫无疑问,参数out用于描述数据包离开的接口。必须明白,在通常情况下,这两个参数中将只有一个被提供。例如:参数in只用于NF_IP_PRE_ROUTINGNF_IP_LOCAL_IN hook,参数out只用于NF_IP_LOCAL_OUTNF_IP_POST_ROUTING hook。最后,传递给hook函数的最后一个参数是一个命名为okfn函数指针,该函数以一个sk_buff数据结构作为它唯一的参数,并且返回一个整型的值。

 

2. 模块设计                  

了解了HOOK函数接收到的信息中最有趣和最有用的部分后,看看我们如何以各种各样的方式来利用这些信息来过滤数据包。我们的模块实现的基于源地址+端口号进行过滤的功能其实可以划分为两个规则:

首先,根据数据包的源地址进行过滤,我们从sk_buff数据结构中提取感兴趣的信息。skb参数是一个指向sk_buff数据结构的指针的指针吗。为了避免犯错误,声明一个另外的指向skb_buff数据结构的指针并且将skb指针指向的指针赋值给这个新的指针是一个好习惯,就像这样:
  
     struct sk_buff *sb = *skb;

   
这样,你访问这个数据结构的元素时只需要反引用一次就可以了。获取一个数据包的IP头通过使用sk_buff数据结构中的网络层包头来完成。这个头位于一个联合中,可以通过sk_buff->nh.iph这样的方式来访问。示例代码1中的函数演示了当得到一个数据包的sk_buff数据结构时,如何利用它来检查收到的数据包的源IP地址与被禁止的地址是否相同。

示例代码1 : 检查收到的数据包的源IP
  
     unsigned char *deny_ip = "\x7f\x00\x00\x01"; 
    
     ...

         static int check_ip_packet(struct sk_buff *skb)
         {
            
             if (!skb )return NF_ACCEPT;
             if (!(skb->nh.iph)) return NF_ACCEPT;
        
             if (skb->nh.iph->saddr == *(unsigned int *)deny_ip) {
             return NF_DROP;
             }

             return NF_ACCEPT;
         }
  
  
这样,如果数据包的源地址与我们设定的丢弃数据包的地址匹配,那么该数据包将被丢弃。为了使这个函数能按预期的方式工作,deny_ip的值应当以网络字节序(Big-endian,与Intel相反)存放。虽然这个函数不太可能以一个空的指针作为参数来调用,带一点点偏执狂从来不会有什么坏处。当然,如果错误确实发生了,那么该函数将会返回NF_ACCEPT。这样Netfilter可以继续处理这个数据包。示例代码2展现了用于演丢弃匹配给定IP地址的数据包的简单模块。
  
  
示例代码2: 基于数据包源地址的过滤


#define __KERNEL__
#define MODULE

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#include <linux/ip.h>                 
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>


static struct nf_hook_ops nfho;


static unsigned char *drop_ip = "\x7f\x00\x00\x01";


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 *))
{
   struct sk_buff *sb = *skb;
  
   if (sb->nh.iph->saddr == *(unsigned int *)drop_ip) {
   // if (sb->nh.iph->saddr == drop_ip) {
       printk("Dropped packet from... %d.%d.%d.%d\n",
     *drop_ip, *(drop_ip + 1),
 *(drop_ip + 2), *(drop_ip + 3));
       return NF_DROP;
   } else {
       return NF_ACCEPT;
   }
}


int init_module()
{
  
   nfho.hook       = hook_func;        
   nfho.hooknum  = NF_IP_LOCAL_IN;
   nfho.pf       = PF_INET;
   nfho.priority = NF_IP_PRI_FIRST;  

   nf_register_hook(&nfho);

   return 0;
}


void cleanup_module()
{
   nf_unregister_hook(&nfho);
}

其次,另一个要实现的简单规则是基于数据包的TCP目的端口进行过滤。这只比检查IP地址的要求要高一点点,因为我们需要自己创建一个TCP头的指针。获取一个TCP头的指针是一件简单的事情——分配一个tcphdr数据结构(linux/tcp.h 中定义)的指针,并将它指向我们的数据包中IP头之后的数据。示例代码3给出了检查数据包的TCP目的端口是否与某个我们要丢弃数据包的端口匹配的代码。

  
  
示例代码3: 检查收到的数据包的TCP目的端口
         unsigned char *deny_port = "\x00\x19";  

     ...

         static int check_tcp_packet(struct sk_buff *skb)
         {
             struct tcphdr *thead;

            
             if (!skb ) return NF_ACCEPT;
             if (!(skb->nh.iph)) return NF_ACCEPT;

            
             if (skb->nh.iph->protocol != IPPROTO_TCP) {
                 return NF_ACCEPT;
             }

             thead = (struct tcphdr *)(skb->data +
                                      (skb->nh.iph->ihl * 4));

            
             if ((thead->dest) == *(unsigned short *)deny_port) {
                 return NF_DROP;
             }
        
         return NF_ACCEPT;
         }

3. 行动

 

4. 总结

--------------------------------------------------------------------------------------------------

 

附:

linux iptables是由两个组件组成:NetfilterIptables组成:

Netfilter组件称之为内核空间,是linux内核的一部分,是在Linux内核中为拦截额操作数据包提供的一套框架。其框架包含以下三部分:

1:为每种网络协议(IPV4IPv6等)定义一套HOOK函数,这些HOOK函数在数据包流过IP协议栈的几个关键点被调用。在这几个点钟,协议栈将把数据包及HOOK函数标号作为参考调用Netfilter框架。

2:内核的任何模块可以对每种协议的一个或多个HOOK函数进行注册以实现挂接,这样当某个数据包被传递给Netfilter框架时,内核能检测是否有哪个模块对该协议和HOOK函数进行了注册。若注册了,则调用该模块,这样这些模块就有机会检查该数据包丢弃/修改/传入用户空间的队列。

3:那些在用户控件队列中排队的数据包是被传递给用户空间异步的处理。

IPV4中定义了5HOOK,如图  

linux实现流量监控的几种方法

Netfilter根据网络报文的流向,分为三部分:流入,流经,流出。

在以下几个点插入处理过程:

NF_IP_PRE_ROUTING,在报文作路由以前执行;

NF_IP_FORWARD,在报文转向另一个NIC以前执行;

NF_IP_POST_ROUTING,在报文流出以前执行;

NF_IP_LOCAL_IN,在流入本地的报文作路由以后执行;

NF_IP_LOCAL_OUT,在本地报文做流出路由前执行。

分析:当网络上的数据包从左边进入系统,数据包经过第一个点调用HOOK函数NF_IP_PRE_ROUTING进行处理;然后就进入本地路由表,查看该数据包是需要转发还是发给本地;若该数据包时发往本地的,则该数据包经过HOOK函数NF_IP_LOCAL_IN处理以后传递给上层协议;若该数据包是需要转发,则会被NF_IP_FORWARD处理;经过转发的数据包最后由NF_IP_POST_ROUTING处理后发送到网络上;本地产生的数据经过路由选择后,调用NF_IP_LOCAL_OUT进行处理,然后经过NF_IP_POST_ROUTING处理后发送到网络上。

 

当这些HOOK函数被经过的数据包调用时将返回下列值之一,告知Netfilter如果对数据包采取相应动作。

NF_ACCEPT    继续正常的报文处理

NF_DROP      丢弃报文

NF_STOLEN    HOOK函数处理了该报文,不要再继续传送

NF_QUEUE     将报文入列,通常交由用户程序处理

NF_REPEAT    再次调用该HOOK函数

 

Iptables用户空间工具

iptables 组件是一种工具,也称为用户空间。它使插入、修改和除去信息包过滤表中的规则变得容易。这些规则存储在表的对象中。

Table

1 filter

该表的作用主要用于包过滤。它在LOCAL_INFORWARDLOCAL_OUT三处HOOK函数进行了注册。

2Nat

该表的作用主要用于地址转换。它在PRE_ROUTINGPOST_ROUTING两处HOOK函数进行了注册。

3Mangle

该表的作用是进行数据包内容的修改。它在Netfilter的所有5HOOK函数进行了注册。

Chain

 在每张表中,内核使用链管理数据包,每个链中包含了一组规则表。

表与链的关系:

Table

Chain

Filter

INPUT

Filter

FORWARD

Filter

OUTPUT

Nat

PREROUTING

Nat

OUTPUT

Nat

POSTOUTING

Mangle

PREROUTING

Mangle

INPUT

Mangle

OUTPUT

Mangle

FORWARD

Mangle

POSTROUTING

 INPUT=HOOK函数 LOCALIN

 OUTPUT=HOOK函数 LOCALOUT

 PREROUTING=HOOK函数 PREROUTING

 POSTROUTING=HOOK函数 POSTROUTING

  FORWARD=HOOK函数 FORWARD

 链的名字与HOOK函数名相似,链的作用和位置也与HOOK函数相吻合,可以说链就是实现HOOK函数调用的方式。

Target 目标

链的每个规则都会有一个目标,目标决定了该规则对数据包如何处理。

常用目标:

ACCEPT:允许

DROP:拒绝

REJECT:同DROP一样,不过它会向发送方返回个错误信息

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多