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的。
struct src_controler {
struct list_head src_list;
int curr; //当前一共有多少了entry
int max; //最多能有多少个entry
};
下面的一个结构体定义了一个源地址entry中包含哪些东西,无非就是一秒内已经过去了多长的数据包以及时间戳等信息。
struct src_entry {
struct list_head list;
__u32 src_addr; //源地址
unsigned long prev; //上次的时间戳
unsigned long passed; //一秒内已经过去了多少数据
};
struct src_controler *src_ctl; //全局变量
接下来就是修改模块的初始化和卸载函数
static int __init xt_limit_init(void)
{
int ret;
src_ctl = kmalloc(sizeof(struct src_controler), GFP_KERNEL); //初始化全局变量
memset(src_ctl, 0, sizeof(struct src_controler));
INIT_LIST_HEAD(&src_ctl->src_list); //初始化全局变量的链表
src_ctl->curr = 0;
src_ctl->max = 1000; //本应该通过模块参数传进来的,这里写死,毕竟是个测试版
ret = xt_register_match(&ipt_limit_reg);
if (ret)
return ret;
ret = xt_register_match(&limit6_reg);
if (ret)
xt_unregister_match(&ipt_limit_reg);
return ret;
}
static void __exit xt_limit_fini(void)
{
xt_unregister_match(&ipt_limit_reg);
xt_unregister_match(&limit6_reg);
//这里应该有一个清理链表的操作,测试版没有实现
}
最后,编写match回调函数,删掉原来的,自己写新的逻辑
static int
ipt_limit_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)
{
struct xt_rateinfo *r = ((struct xt_rateinfo *)matchinfo)->master;
unsigned long now = jiffies, prev = 0;
struct list_head *lh;
struct src_entry *entry = NULL;
struct src_entry *find_entry;
unsigned long nowa;
struct iphdr *iph = skb->nh.iph;
__u32 this_addr = iph->saddr;
list_for_each(lh, &src_ctl->src_list) { //遍历链表,找到这个ip地址对应的entry
find_entry = list_entry(lh, struct src_entry, list);
if (this_addr == find_entry->src_addr) {
entry = find_entry;
break;
}
}
if (entry) { //如果找到,将其加在头,这样实现了一个简单的lru
prev = entry->prev;
list_del(&entry->list);
list_add(&entry->list, &src_ctl->src_list);
} else { //如果没有找到,看看能否添加
if (src_ctl->curr+1 < src_ctl->max) {
add_entry:
entry = kmalloc(sizeof(struct src_entry), GFP_KERNEL);
memset(entry, 0, sizeof(struct src_entry));
entry->src_addr = this_addr;
prev = entry->prev = now - 1000;
list_add(&entry->list, &src_ctl->src_list);
src_ctl->curr++; //正确做法是atomic_inc
} else { //如果已经满了,那么看看能否删除最后的那个不活动的entry
entry = list_entry(src_ctl->src_list.prev, struct src_entry, list);
if (now-entry->prev > 1000)
goto add_entry;
return 1;
}
}
nowa = entry->passed + skb->len;
if (now-prev < 1000) { //这里的1000其实应该是HZ变量的值,由于懒得引头文件了,直接写死了。如果距上次统计还没有到1秒,则累加数据,不匹配
entry->passed = nowa;
return 0;
} else {
entry->prev = now;
entry->passed = 0;
if (r->burst >= nowa) { //如果到达了1秒,则判断是否超限,如果超限,则匹配,没有超限则重置字段,不匹配
return 0;
} else {
return 1;
}
}
return -1; //不会到达这里
}
编译之:
make -C /usr/src/kernels/2.6.18-92.el5-i686 SUBDIRS=`pwd` modules
使用之:
#!/bin/bashiptables -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回调函数工作的不是很好,速度并没有被限制住,这是因为我计时统计统计的粒度太粗,一秒统计一次,这一秒中,很多大包将溜过去,因此需要更细粒度的统计,那就是实时的统计,使用数据量/时间间隔这个除式来统计,代码如下:
static int
ipt_limit_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)
{
struct xt_rateinfo *r = ((struct xt_rateinfo *)matchinfo)->master;
unsigned long now = jiffies, prev = 0;
struct list_head *lh;
struct src_entry *entry = NULL;
struct src_entry *find_entry;
unsigned long nowa;
unsigned long rate;
struct iphdr *iph = skb->nh.iph;
__u32 this_addr = iph->saddr;
list_for_each(lh, &src_ctl->src_list) {
find_entry = list_entry(lh, struct src_entry, list);
if (this_addr == find_entry->src_addr) {
entry = find_entry;
break;
}
}
if (entry) {
prev = entry->prev;
list_del(&entry->list);
list_add(&entry->list, &src_ctl->src_list);
} else {
if (src_ctl->curr+1 < src_ctl->max) {
add_entry:
entry = kmalloc(sizeof(struct src_entry), GFP_KERNEL);
memset(entry, 0, sizeof(struct src_entry));
entry->src_addr = this_addr;
prev = entry->prev = now - 1000;
list_add(&entry->list, &src_ctl->src_list);
src_ctl->curr++;
} else {
entry = list_entry(src_ctl->src_list.prev, struct src_entry, list);
if (now-entry->prev > 1000)
goto add_entry;
return 1;
}
}
nowa = entry->passed + skb->len;
entry->passed = nowa;
if (now-prev > 0) {
rate = entry->passed/(now-prev);
} else
rate = nowa;
entry->prev = now;
entry->passed = 0;
if (rate > r->burst) {
return 1;
}
return 0;
}
分享到:
相关推荐
基于Netfilter的网络流量统计系统在嵌入式设备上的实现,黄锐,张立民,本文介绍了一种通过在Iptables的Filter表中新建基于不同的源IP地址或目的IP地址的过滤规则来将网络流量分类,并利用Iptables自带的计数器�
利用Netfilter框架和TC实现P2P流量控制
其利用Netfilter在Linux内核空间实现DNS数据的实时过滤与解析,开发misc文件设备驱动模块以提供内核空间与用户空间之间内存映射,并结合读写同步算法,实现两种空间之间数据的快速交换。实验表明,DRMSS显著提高了...
Netfilter_实现机制分析Netfilter_实现机制分析Netfilter_实现机制分析Netfilter_实现机制分析Netfilter_实现机制分析Netfilter_实现机制分析Netfilter_实现机制分析
基于Netfilter内核态网络流量分析研究.pdf
主要介绍,alg 在netfilter 中的实现原理
在分析一个通过应用层匹配来识别P2P流量的模块IPP2P基础上,给出了一个基于Netfilter的P2P流量测量系统。该系统通过高速的内核字符串匹配来识别P2P数据流,在此基础上运用连接跟踪技术进行高效的流量统计,实现对P2P...
基于Netfilter的Android防火墙实现.pdf
ip层netfilter的连接跟踪模块初始化》、《ip层netfilter的连接跟踪模块代码分析》、《ip层netfilter的连接跟踪模块 学习小结》、《ip层netfilter的NAT模块初始化以及NAT原理》、《ip层netfilter的NAT模块代码分析》...
netfilter-nf-conntrack-模块实现分析.doc
针对目前较为关注的VoIP中的SPIT问题,提出了一种基于Netfilter模块的SIP安全网关的实现方案,该方案合理利用了SIP中的重定向、呼叫转移等功能,并借鉴了黑白名单、图灵测试、行为特征分析等技术.实验表明,该方案对...
在Linux内核中安装钩子截获网络数据包 统计流量发送至用户层,用户层统计所有流量。
介绍了Linux内核防火墙的发展,对2.4.x内核中的Netfilter框架的流程和IPv4协议栈中Netfilter的实现进行了分析,通过一个内核防火墙模块实例介绍了基于Netfilter框架下的内核防火墙设计方法,对Netfilter框架下的防火墙...
基于Netfilter框架的网关的IP访问控制技术研究,王萍,,Linux 的防火墙技术是经历了若干代的演进,一步步的发展而来。最开始的 ipfwadm 是 Alan Cox 在 Linux kernel 发展的初期,从 FreeBSD 的内核代码
基于Netfilter的P2P流量测量系统研究
基于Netfilter的NAT技术及其应用
Linux的Netfilter框架和数据包捕获技术
其重要工具模块IPTables从用户态的iptables连接到内核态的Netfilter的架构中,Netfilter与IP协议栈是无缝契合的,并允许使用者对数据报进行过滤、地址转换、处理等操作。 Netfilter的框架 Netfilter分四个基本模块:...