前面一篇文章提到了两个Netfilter上的新玩意儿,其中之一就是bpf match模块,另一个nftables的ball还没有放出来,即使放出来了,它的吸引力也没有bpf match大,毕竟伯克利的东西不可小觑啊!虽然nftables也是借用了BPF的思想,但是能看得出的仅在代码层面上,反之,bpf match模块却是iptables质的飞跃,综合状态机,JIT(及时编译技术),它可以在很短的时间内完成数据包的匹配判断,据作者那帮子说相比传统iptables硬编码match来讲,及时编译过基于状态机的match集合的匹配执行效率要提高X倍!我只是听说,并没有亲见。
这东西一直没有时间仔细尝试,昨晚小小半夜就开始闹,我也就再也睡不着了,想起了iptables的bpf模块,不由想赶紧试一把,然而为了配置一条成功的使用bpf模块的iptables规则,前期工作可真不少啊,列举如下,每个步骤我会在后面详述:
1.下载一个最新的Debian 7发行版并安装(本可以在现有的版本直接升级的,但早就想有一个全新的环境,趁此夜晚网速极快,下载吧)
2.下载最新的kernel source以及最新的iptables source
3.构建编译工具套件
4.编译最新的内核以及驱动以及iptables
5.编写可以生效的使用bpf模块的iptables规则
这5个步骤中,第1步时间相对长一些,我是下载了全部的映像,一共14G左右,其实如果选择网络安装,会更加省时一些,我无所谓了,反正是深夜,下载过程中磁盘猛转,我也不闲着,昨天刚买了半套《罗马人的故事》(因为钱比较紧张,先买半套,看完了再买吧),下载过程中我看了大半本《胜者的迷思》,正沉浸在苏拉进军罗马的XX中,下载结束。第2个步骤的下载简直不是什么问题,具体的链接我就不给出了,想玩的都知道,版本通告一下:
kernel:3.9.6
iptables:1.4.19.1
接下来的事情就不能一笔带过了,但是在详述之前还是要带几笔,第3部是必要的,因为既然选择了Debian,就是喜欢它的工具集,否则随便一个发行版即可,第5步看似只是一个水到渠成的结果,然后这个步骤是我花时间最多的。
构建编译工具套件
Debian自身有一套编译kernel的工具,称为kernel-package,它可以将最终生成的kernel映像,driver,initrd映像以及开发使用的headers打包成一个deb包,可以直接安装在Debian发行版上,如果没有这个套件,你毫无疑问就要花很多时间来编译内核以及driver了,光是initrd就不一定能一次性做成功,即使你把这些都封装进了shell,你也要时刻关注shell的执行,起码不可能按下回车后去再看一段《胜者的迷思》了,由于使用了kernel-package,在编译过程中,我竟然把《胜者的迷思》看完了!Debian的工具是专门为Debian上的开发者打造的,所以即使有问题,早就被其它人通告给了Debian社区,早就改正了。构建这个工具套件很简单:
apt-get install kernel-package
另外,为了能使用menuconfig配置kernel,还要安装一个东西:
apt-get install libncurses5-dev
编译最新的内核以及驱动
必要的工具构建完毕,剩下的就是编译内核了,这次折腾的直接目的是玩bpf模块,因此切记要在menuconfig配置内核的时候使能BPF配置,它在Core Netfilter Configuration中的"bpf" match support,其它的就先别管了,直接使用Debian 7的config文件即可,然后就可以目睹kernel-package的风采了,注意上述的操作要分步骤完成:
第一步:cp /boot/config-3.2.0-4-amd64 /usr/src/linux-3.9.6/.config
第二步:make menuconfig
第三步:make-kpkg --initrd kernel-image && make-kpkg kernel-headers && find /usr/src/ -type deb -name *.deb|xargs -i dpkg -i {} &&reboot
第四步:找本书看,或者找个电影看,但是为了让电脑全速编译,还是换一台电脑心理上会得到安慰(Note:心理上!);
以上就编译完了新内核了,uname看一下,已经升级到3.9.6了,kernel-header也在/usr/src下就位,/lib/modules/3.9.6/build也就位,剩下的工作就是编译iptables了,这个十分简单,也分为几个步骤:
第一步:configure --enable-nfbpf-compiler;这个enable特别重要,其实这是我在写iptables规则屡次不成功后返回来重新加上这个选项编译的,一开始我是直接configure的,自以为耍小聪明可以过关,结果在编写iptables测试的时候悲剧了!
第二步:make && make install
准备工作到此就做完了,此时的时间是上午8点半,吃完早饭因为要去买菜做午饭(由于今天老婆带女儿上早教去了,我留守做饭,由于我做事比较分步有条理,所以就比较慢,做个简单的午饭也要一个步骤一个步骤的从早晨八点开始...),iptables测试工作放在午后进行...
编写可以生效的使用bpf模块的iptables规则
终于可以写规则了,下意识的写下以下的规则:
iptables -A INPUT -m bpf --bytecode '傻眼了' -j DROP
其实我的要求很简单,就是想将bytecode设置为icmp,可是又不知道bpf的具体语法以及指令,原理是再明白不过了,可是到了实际操作,又懒得查手册,既然不想查手册,也就很难自己写一个翻译程序出来,比如将tcp port 1234之类的翻译成符合bpf语法的bytecode,于是想到从tcpdump里面把代码抠出来,然而那种行为有点巧夺天工了,一向不太在意代码的我,如今被代码折腾了。其实啊,像tcpdump这样成熟的程序,一定有什么参数可以将bpf的code打印出来的,这真的就是我的直觉,大多数玩过GNU代码的都知道,作者在写这段代码的时候,肯定print过这些东西,最终发布时,就作为一种调试信息留下了,既然代码都开源了,为何不让用代码的人更方便呢?事实也确实如是,man一下tcpdump,就会发现其d,dd,ddd选项,正是做这个的,以下就是这些东西,我还是以匹配icmp为例:
@首先看一下十进制的code
root@zhaoya:/usr/src/linux-3.9.6/net/netfilter# tcpdump -i any icmp -ddd
40 0 0 14
21 0 3 2048
48 0 0 25
21 0 1 1
6 0 0 65535
6 0 0 0
@虽然没有意义,也看一下16进制的code吧,起码知道每行是个4元组{操作码,匹配判读,匹配判断,参数}
root@zhaoya:/usr/src/linux-3.9.6/net/netfilter# tcpdump -i any icmp -dd
{ 0x28, 0, 0, 0x0000000e },
{ 0x15, 0, 3, 0x00000800 },
{ 0x30, 0, 0, 0x00000019 },
{ 0x15, 0, 1, 0x00000001 },
{ 0x6, 0, 0, 0x0000ffff },
{ 0x6, 0, 0, 0x00000000 },
@最后领略一下它的助记码,类似汇编的指令
root@zhaoya:/usr/src/linux-3.9.6/net/netfilter# tcpdump -i any icmp -d
(000) ldh [14]
(001) jeq #0x800 jt 2 jf 5 #IP协议
(002) ldb [25]
(003) jeq #0x1 jt 4 jf 5 #ICMP协议
(004) ret #65535
(005) ret #0
既然有了这个,我当然喜出望外了,于是赶紧测试,google了一下bytecode的语法,还算简单:
code=$(tcpdump -i any icmp -ddd| tr '\n' ',')
即可,于是上述的“傻眼了”被code填充,拍下回车,ping网关,竟然是通的!dmesg也没有任何报错,于是我想得到,是bytecode有问题!然而到哪里去找正确的呢?失望之余,真心有种自己写一个解析器的欲望了,厨房打开抽油烟机稳定了一下情绪,还是google吧,别人做好的东西为何自己再做一遍呢!google发现iptables本来就有一个叫做nfbpf-compiler的工具,在utils里面,于是我就重新编译了它,其实在iptables的bpf模块的help中就明确写出了一个格式,那就是:
--bytecode `nfbpf_compiler RAW <filter>`
过于激动,文档都没有看仔细,真是罪过!于是按照这个格式,将<filter>替换为'icmp',果真成功了,ping不通了,于是我想看看通过nfbpf_compiler生成的code和tcpdump生成的code有何不同,以下是工具生成的:
root@zhaoya:/usr/src/linux-3.9.6/net/netfilter# nfbpf_compile RAW 'icmp'
7,48 0 0 0,84 0 0 240,21 0 3 64,48 0 0 9,21 0 1 1,6 0 0 65535,6 0 0 0
太大不同了啊,到底怎么回事?我注意到了RAW参数,我隐约记得RAW是一个linktype,当时研究tcpdump时好像注意过,因为有人问我EN10MB是什么类型,我当时也不知道,查资料时发现了它其实就是一个链路层类型,和它并列的还是ATM等(这些曾经网络层的协议在TCP/IP的天下都退化成了链路层),那么RAW是什么,其实和socket的RAW差不多,都是指裸IP,也就是说它没有链路层或者说忽略链路层。到此,我终于明白,tcpdump的eth系列网卡翻译过来的code明确就是EN10MB,也就是以太网协议,通过tcpdump -i eth0 -L看一下它都可以使用哪些linktype,令人失望!于是想用lo做实验,可是lo也是和eth0一样,lo怎么会模拟以太网卡啊啊啊啊!在Mac上,lo0明确就是NULL,这才合理啊!具体参见 http://www.tcpdump.org/linktypes.html吧,反正即使看了也得不到什么有用的,我们大多接触到的都是EN10MB!看一下nfbpf_compiler使用EN10MB的结果:
root@zhaoya:/usr/src/linux-3.9.6/net/netfilter# nfbpf_compile EN10MB 'icmp'
6,40 0 0 12,21 0 3 2048,48 0 0 23,21 0 1 1,6 0 0 65535,6 0 0 0
和tcpdump的结果及其类似!
反过来思考一下,bpf match为何不能使用NE10MB code呢?我看了xt_bpf patch的跟帖,作者的例子就是EN10MB的啊!为何发布版不行了呢?难道是怕linktype太多,如果没有处理就容易panic??看来沙漏细腰部位的IP代码好写啊!不管怎样,事情总算是搞定了,总结一下有几点教训:
1.玩新东西之前一定要看文档,看手册,现在不再一头扎进代码里了,这是进步,但是在有了问题的时候也别指望仅仅看代码就能解决。
2.编译时的参数选项一定要注意,有时候并不是程序的bug,只是因为你在编译的时候就自动放弃了那个你想要的功能。
3.还是那句话,别人已经做好的东西,自己就别再做了(bpf编译程序写了一半...),已经不是10年前那个把排序算法写一遍的青春期晚期了,现在要惜时如金,想练手就练别人没人练过的!
最后,其实不像本文题目所说的那样,这个历程并不悲惨,这是因为大家普遍都喜欢悲剧罢了,一部悲剧看完以后,你会发现,可能它就是一部带有黑色幽默的喜剧。当然,大半夜被女儿吵闹的声音吵醒看似挺悲惨的,但是有孩子的知道,真的悲惨吗?可能也是一种幸福吧,没孩子的,将来会懂得...
《胜者的迷思》,今日完结,明日早晨五点《条条大道通罗马》!另,还有一个好玩的,那就是Debian上的ufw!