夜已深然未央,准备接着讲述有关Netfilter的故事,行文有点松散,由于未打草稿,有点随意识而流,一气呵成不知是自夸还是自嘲,权当小时候写的日记吧,自幼喜欢每天写日记,中学时更是以退士为名折腾了几箱子抄本,前几年由于喝酒就改为周记了,现在意识到了生命短暂,时间甚是不够用,不能在迷迷糊糊中得过且过,就准备把自己知道的关于Linux网络的东西一点一滴记录下来,本来想继续行文于纸上,然而发现在个人电脑智能手机时代,很多字早就不会写了...上回没有说完关于iptables的故事,本文继续...
一.nftables前传-iptables之弊端
iptables几乎是无人不知无人不晓,人们被圈入了框框也就觉得任何事情都是理所当然,但我例外,和其他很多事情一样,在这个领域,我依然做并且乐于做那个“被排除的人”。
iptables的诸多弊端已经不能再视而不见,然而只有很少的人看到了这些,大多数的人作为使用者,仅仅是使用罢了。在此我不会吐嘈很多了。以下的弊端来自nftables的宣传文档,但是即使是在国外,也引发了超级多的争论:
1.iptables框架在内核态知道的太多,以至于产生了大量的代码冗余。
这一点是显而易见的,比如对于TCP和UDP而言,取sport,dport没有什么不同,但是iptables却使用了两套代码,这只是一个例子,类似的还有很多。
2.iptables的rule结构设计不合理。
这是要着重说明的。
1.iptables的结构
iptables由表,链,规则组成,其中规则又由match,target组成。如下面的结构所示:
Table{
Chain[
Rule
(match,match,match,...)
->target,
Rule
(match,match,match,...)
->target,
...
],
Chain[
...
],
...
}
2.iptables的规则匹配执行流程
iptables的规则是按照配置顺序顺序匹配的,在每一张表的每一个链上依次匹配每一条规则,在每一条规则依次匹配每一个match,全部匹配的match执行该规则的target,由target决定:
a.继续匹配下一条规则
b.对数据包做一些修改
c.跳转到其它的链(即开始从该链依次匹配该链上的每一条规则)
d.返回引发跳转的链(即继续匹配跳转前的链的下一条规则)
e.丢弃数据包
f.接收数据包(即不再继续往下匹配,直接返回)
g.记录日志
h....
整个iptables框架执行的流程如下:
循环1: static breakrule = 0; 遍历一个chain的每一条rule { nomatch = 0; 循环2:遍历一条rule的每一个match { result = rule->match[curr](skb, info); if(result != MATCH) { nomatch = 1; break; } } if (nomatch == 1) { continue该chain的下一条rule; } result = rule->target(skb, info); if (result == DROP) { break丢弃数据包 } else if (result == ACCEPT) { break接受数据包 } else if (result == GOTO) { breakrule = rule; 跳转到相应的chain,执行循环1 } else if (result == RETURN) { break返回调用chain,执行其breakrule的下一条rule } ...}
看了上述的代码就基本知道了iptables的命令实现,程序员能做的就是扩展iptables的功能,具体的做法有两个:写一个match以及写一个target。除此之外,程序员就没辙了,剩下的就看使用者的想象力了...
通过上面的流程,可以发现,包过滤的流程最终要落实到规则匹配,而过滤的动作最终落实到了该规则的target,前面的所有的match匹配返回结果就是0或者非0表示是否匹配,只有所有的match均匹配,才会执行target。这就决定了下面几件事:
a.如果你想实现多个target,就不得不写多条规则
比如实现log和drop,那么就要写两条规则,或者扩展一个LOG_and_DROP target,前者影响效率,后者需要编程。你很在乎性能,同时你又不是程序员不懂编程,你就抓狂了...
b.你可以写一个match,在里面偷偷摸摸做一点事情,但是外部不知道
这一切太不正规了,你可以在一个match里面把一个数据包的校验码改掉,也可以在里面做log,做NAT什么的,但是iptables的框架的本意虽不允许你这么做但是又没有阻止你的行为。
我们可以在iptables执行流的一个细节(上述的流程中未画出)中看到另一个细节,即iptables在match中仅仅确定“是否匹配”真的已经很不够,就连代码都设计得很勉强。如果你看ipt_do_table这个核心函数,会发现一个控制变量名叫hotdrop,这个变量是干什么的呢?按照注释的意思是:
@hotdrop: drop packet if we had inspection problems
这个hotdrop作为传出参数传入每一个match回调函数,用于在match内部指示将一个数据包丢弃。这就暴露出了设计的不足,丢弃一个数据包不是target要做的吗?一个match的职责是抉择该数据包是否匹配,干嘛要指示丢弃它呢?这不是越级么?这只是一个细节,你可以说出一千个理由表明它是合理的,但是它却是丑陋的!
二.一点小历史
弄清楚历史总是能明白更多,这绝对是一句真话,但是恰恰是专业化阻止了大多数的程序员去读历史,哪怕是IT的历史。最好的历史资料就是原著,Netfilter的历史不长,从Linux 2.3.15内核版本被引入至今,不会像老子庄子那样被篡改地体无完肤。
我们当然要看iptables被引入的那段历史。
iptables被引入旨在替掉ipchains,因为当时ipchains的维护者Rusty Russell认识到它拥有诸多的弊端。总的说来,弊端有两个,其它的都是由这两个而发:
a.内核的firewall框架仅仅设置了3个检查点,即input,forward,output,对于环回包以及indev,outdev的控制力很弱;
b.代码写死,匹配项固定,没有可扩展性。
问题就在这里的b。针对问题a,Rusty Russell提出了Netfilter的设计,精心设计了5个HOOK点,解决了几乎所有的控制点的问题,特别是OUTPUT点的设计顶级绝妙,它被安放在路由之后,原因在于Linux协议栈的路由操作之后才会给出完整的过滤匹配项,比如源IP地址,出口设备等,路由之后的OUTPUT同时给了调用者再次路由的权限。FORWARD和INPUT作为路由的二分,同时保持了无用功最少化,因为如果你没有打开ip_forward选项,即便不是INPUT的数据包也不会进入FORWARD,如果根本就没有找到路由,则既不会到达INPUT,也不会到达FORWARD。对于PREROUTING而言,它可以通过conntrack区分本地环回流量和网卡进入流量...不管怎么说,这是内核的工作,这个Netfilter的设计十分完美,至今依然被使用。
对于问题b,Rusty Russell提出了iptables,它是一个高度可扩展的框架,也就是从此时起,iptables拥有了match/target配对的扩展方式,每当需要扩展的时候,每一个match/target除了有用户态的lib之外,还有用内核态的支持,它将ipchains时代的固定匹配模式变成了可以自己编程扩展的了。
针对ipchains的弊端,Rusty Russell可谓是给出了完美的解决方案,然而仅此而已!任何一个来自同一作者的新的框架几乎均是为了解决上一个框架的弊端的,iptables作为一个新秀,在获得欢呼的时候,不会有人去考虑它的弊端,任何事情都是这样,不是吗?
iptables的弊端是被逐步发现的,Rusty Russell作为ipchains和iptables的共同作者,它对待后者取代前者的态度永远都是保守的,一个全新的框架需要另一个人或者团队来提出,而不可能出现在Rusty Russell本人手里以及iptables团队的内部。
针对Netfilter,Rusty写了大量的文档,均在Netfilter网站上可以找到: http://people.netfilter.org/rusty/unreliable-guides/不可否认,这些都是珍贵的第一手资料,对于我们理解Netfilter,可能没有比这些更好的了。任何人都可以从这些原著中找到“XX为何会这样”这种问题的蛛丝马迹,同时它们也是指导你如何改进现有框架的明灯。
三.nftables登场
鉴于iptables的诸多缺点(其实并不是缺点,但是match/target配对的扩展方式导致了开发者延伸了劣势,最终将其确定为缺点),nftables旨在一个个地改进它们。首先是两个问题:
a.如何使用一种统一的方式来解析数据包
在这个问题的解决上,u32 match给了作者思路
b.如何执行多个action
事实上,是iptables的matches/target配对的方式限制了开发者的思路。为何非要区分match和target呢?iptables框架的执行流程限制了match的结果就是个布尔型,所有的动作都在target中执行,如果去掉了这个限制,将整个流程都开放给开发者,那就灵活多了。nftables就在这样的背景下登场了。事实证明,nftables做的比修正弊端更多,走的更远。
首先nftables采用了“虚拟机解释字节码”的方式,使一条rule真的成了“为一个数据包做一些事情”这样灵活的命令,而去掉了“匹配所有的match之后执行一个target”这样的限制。虚拟机执行字节码的方式早就被BPF采用了,我们熟知的tcpdump抓包程序就是利用的它来过滤的数据包。我们来看一下nftables框架的执行流程:
循环1: static breakrule = 0; 遍历一个chain的每一条rule { nomatch = 0; reg[MAX] 循环2:遍历一条rule的每一个expression { void rule->expression[curr]->operations->expr(skb, info, reg) if(reg[VERDICT] != CONTINUE) { break; } } if (reg[VERDICT] == CONTINUE) { continue该chain的下一条rule; } else if (reg[VERDICT] == DROP) { break丢弃数据包 } else if (reg[VERDICT] == ACCEPT) { break接受数据包 } else if (reg[VERDICT] == GOTO) { breakrule = rule; 跳转到相应的chain,执行循环1 } else if (reg[VERDICT] == RETURN) { break调用chain,执行其breakrule的下一条rule } ...}
光从这个流程上看,就已经可以和iptables分出胜负了。可以看到,nftables没有match和target,而是将一条rule抽象成了若干条的表达式,即expression,所谓的表达式就是一个主语加谓词的式子,它是“可执行”的,它可以“做任何事情”,而不仅仅是计算一个匹配结果。除此之外,nftables内置了一组寄存器,其中之一是verdict寄存器,它指示了“下一步要怎么做”。每一条expression执行完了之后,会取出该寄存器,由该寄存器的值来采取下一步的行动。这个verdict寄存器替换了iptables中target返回值,这就可以在一条rule中采取多个动作,每条动作可以解析成一个expression,每一个expression在执行后将verdict寄存器设置为CONTINUE即可!
除了执行流程的显著区别之外,nftables最大的意义在于它对expression进行了抽象,nftables的内核框架可以注册很多种的expression,每一种都有一个操作集,其中expr回调函数执行具体的expression表达式。典型的expression有:
payload表达式:
将一个数据包的某一段数据拷贝到一个nftables寄存器指示的缓冲区,除了出错之外verdict寄存器均为CONTINUE。
compare表达式:
将某段数据和nftables寄存器指示的缓冲区作比较,若不相等则设置verdict寄存器为BREAK。
counter表达式:
按照数据包的大小递增字节计数器以及包计数器的值,保持verdict寄存器为CONTINUE。
log表达式:
对数据包记录日志,保持verdict寄存器为CONTINUE。
nat表达式:
按照nftables寄存器的数值对数据包做NAT,verdict寄存器设置为NAT操作的结果,注意,NAT的参考数据均来自nftables寄存器。
compat表达式:
保持对iptables的兼容性。细分为match寄存器以及target寄存器,其中match表达式调用iptables rule的match,若匹配设置verdict寄存器为CONTINUE,否则为BREAK;target表达式调用iptables rule的target,并根据target的结果设置verdict寄存器。
...
回到nftables的执行流程,结合上述的表达式,看看这一切像什么?
这难道不就是一个解释器吗?类似高级语言比如Python的解释器,将每一个表达式解释执行,我们可以将一条nftables rule分解为一系列的表达式,仅此而已,如下所示:
expr1:reg[verdict] = CONTINUE;reg[0] = skb[m...n];expr2:info[0] = something from userspace;ret = compare(reg[0], info[0]);if (ret == true) ; then reg[verdict] = CONTINUE; else reg[verdict] = BREAK; break; fiexpr3:log_packet(skb);expr4:ret = do_nat_packet(skb, reg[i]/*address to trans*/...); if (ret == true) ; then reg[verdict] = CONTINUE; else reg[verdict] = BREAK; break; fi...
看看吧,一条规则做了多少事情啊啊啊!解释器按照expr1到expr4的顺序执行expression,每次执行下一个expression之前要检查verdict寄存器,那么谁是解释器,当然就是上面的nftables的执行流程啦!
nftables到底是什么玩意儿?实则一个虚拟机!那么这部虚拟机执行的指令来自何方?来自用户态的配置。用户态怎么配置?当然是使用nft命令。nft是什么命令?是类似iptables的命令。nft命令能否举一个例子?能:
nft add rule ip filter input ip saddr 1.1.1.1 drop
这条命令怎么和诸多的表达式对应?答案是nft命令行工具内置了一个”编译器“,将一条human readable命令编译成了一个个的expression代码。编译的细节是什么?可以写一本书,但是了解一下tcpdump的方式也就该能理解了。tcpdump命令最终会将编译好的指令注入到内核的BPF系统,以下是一条很常见的tcpdump命令:
tcpdump -i eth0 dst 1.1.1.1
它会翻译成什么代码呢?后面跟上-dd参数就可以看出来:
root@debian:/usr/local/etc/nftables# tcpdump -i eth0 dst 1.1.1.1 -dd
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 2, 0x00000800 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 4, 5, 0x01010101 },
{ 0x15, 1, 0, 0x00000806 },
{ 0x15, 0, 3, 0x00008035 },
{ 0x20, 0, 0, 0x00000026 },
{ 0x15, 0, 1, 0x01010101 },
{ 0x6, 0, 0, 0x0000ffff },
{ 0x6, 0, 0, 0x00000000 },
具体是什么意思请参考BPF的手册。nftables设置的规则最终也会被”编译“成类似的”指令“注入到内核的nftables系统,形成一个个的expression。要注意,并不是所有的规则指令都是可以编译的,比如iptables兼容指令就不能编译,log指令也不能被编译。
nftables就是这样一个具有美感的包过滤框架,理解了它的运行方式之后,你就可以扩展它了,和iptables扩展match/target不同,对于nftables,你只需要扩展expression即可,就是说你要自己编写expression,然后让nftables虚拟机(即上面的执行流程)执行它就可以了。最后,我们来看一下nftables框架的结构:
Table{
Chain[
Rule
(expression,expression,expression,...)
Rule
(expression,expression,expression,...)
...
],
Chain[
...
],
...
}
expression := expression | datatype | operation | expression | datatype
operation := + | - | * | / | memcpy | contain | ...
datatype := u8 | u16 | u16 | ... | container | ...
container := hash | map | tree | set | list | array | ...
四.这一切到底怎么了
值得注意的是,虽然nftables在美学角度上完胜iptables,但是作为一个框架,它的性能并不十分高效。和nf-hipac相比,iptables并不比nftables输得更惨些。事实上,nftables和iptables一样,对于一条chain上的所有rule,也是逐条遍历的,所不同的只是遍历每条rule时执行具体匹配的方式有所不同。那么和nf-hipac相比,nftables为何成功了?
其实nftables还远远没有成功,它的阻力不是来自性能,而是来自iptables的阵营!nftables作为一个优美的框架,考虑的不仅仅是性能,事实上性能只是其考虑的极小的一方面。作为一个框架,它首先要考虑的是扩展性以及和iptables的兼容性。反对的声音分分钟响彻于耳,iptables并没有错,match/target配对的方式并没有错,match就是要返回true or false,最终的结果就是要target来执行,总之就是要区分match和target,并且各司其职!!
觉得iptables不能执行多个动作的为何不自己写一个可以执行多个动作的target啊啊啊?!
觉得iptables逐条执行且不能完成nf-hipac创举的为何不将nf-hipac封装成一个单独的match啊啊啊?!
封装成单独match的nf-hipac看起来会是:
iptables -A INPUT -m hipac --match-hipac hipac_test -j NOTHING
nf-hipac create hipac_test
nf-hipac add hipac_test -s 1.1.1.1 -j DROP
...
看到iptables的优势了吧,人家根本就不是来和nf-hipac拼性能的,人家是海纳百川的,人家有容乃大!难道姓毛的椅子男要上战场拚刺刀吗?NO!NO!NO!姑且不谈nf-hipac,和上面类似的还有ipset,ipset不就是被封装成一个match而和iptables联动的吗?iptables并不差,错在人们根本就不该直接将每一个简单功能扩展成一套match/target联合体,最终形成令人作呕的代码!是这样吗?
好吧!我承认上面的YY都是对的!但是看看nftables,它是不是也可以这么玩并且玩得更high呢?!是啊!是吧!nftables内部直接内置了诸多的容器类数据类型,比如rbtree,hash等,作为一种复合容器,你往里面放东西就是了,还用写match吗?我这么说的意思是,写过match/target的都知道,要例行多少公事啊,你要注册match或者target,还要复制很多管理代码,看过xtables-addons的都知道其中之苦。使用nftables的话,如果你想为iptables扩展一个功能模块,很多工作都可以在用户态完成,换句话说,如果仅仅是基于skb(即数据包)的内容做过滤,那么nftables便是协议无关的,因为不管什么协议,你都可以将过滤表达式用payload,compare,bit等简单的expression开表示,协议解析的工作在用户态编译nftables指令的时候完成即可,到了内核态,nftables虚拟机只执行表达式,不管协议!
世界在向前走,我们要向前看!看看Linux的包过滤框架,从最初的移植BSD的实现,到现在的nftables,中间经历了多少的坎坷曲折,每次有新东西进来总是会有复古者的谩骂!这下可好,这下可好,Linux内核在主干上直接内置了nftables的支持,正如当初Netfilter进入主干时的情形一样。
Just do it,划时代的nftables,我并不是说它有多么猛,而是说,它真的很干净。
tables,chains,这名字叫得真不好,可是无论如何它也只是个名字而已。iptables要不是因为名字,我也不会为了理解它纠结那么久,现在又来个nftables...Cisco管类似的东西叫做list,即ACL,也只是个名字,如果说tables不好听,list是不是显得更低级呢?不管低级不低级,华为也延续了Cisco的叫法。因此下一代的包过滤框架也叫做tables,估计显示文化上的认同要比其实质更加有用吧,特别是对待起名字这件事上。iptables已经深入人心,nftables这个名字会让人更容易接受。当初iptables替代ipchains,那是革命性的替换,而这次,更多的显示出来的是成熟Linux机制的一种自然而然的演变,或者说进化更合适些吧。
五.nftables用起来
我第一时间想的是在2.6.32上将nftables跑起来,然而失败了,根本就没有办法编译。那么下面就是想办法了,看了很多的宣传文档和HOWTO以及nftables的主站,花了好久clone了git映像,编译通过,终于跑起来了。
后来我干脆直接在kernel.org上下载3.17版本的内核,在make config的时候将nft相关的东西都给选上,然后编译更新内核。同时下载用户态的nftables-0.3版本utils,编译之,过程中缺什么补什么,最终很顺利。在make install之后,首先执行:
nft -f /usr/local/etc/nftables/ipv4-filter
这条命令是在内核中载入了filter表,除了执行预先配置好的文件,你也可以手工载入table。
在table,chain就绪之后,就是在自己希望的chain上添加rule了:
nft add rule ip filter output ip daddr 1.2.3.4 drop
其它的用法请man nft,非常详细但不详尽的文档,另外的好资料在 https://home.regit.org/netfilter-en/nftables-quick-howto以及 https://home.regit.org/2014/01/why-you-will-love-nftables!
我为何没有将其移植到低版本的内核上呢?因为我觉得这太简单了,为何出此狂言?因为nftables仅仅和Netfilter的nf_register/unregister_hooks接口,其它的都是其框架内部做的,其复杂性在于nft_expr_ops,而这个是非常独立的,和既有的内核没有任何关系。对于用户态utils,本身就有一个nftables项目存在,就是一个编译问题,而这个编译是本来就能通过编译的。
六.配置防火墙变成了编程
本文的最后,我来从全局的角度看一下nftables和iptables的最终区别。
我已经从内部原理的角度分析了nftables带来的改变,那么这些给用户到底能带来多少实惠呢?如果没有实惠,那么在用户群中是不会有动力切换到nftables的。实惠不多,只有一个,但是仅此就够了,那就是:nftables让用户可以按照编程的思想来组织自己的配置逻辑。
怎么说呢?我们来看一个wiki上展示的例子吧:
nft add rule ip filter input ip protocol vmap { tcp : jump tcp-chain, udp : jump udp-chain , icmp : jump icmp-chain }
这是什么?这是一条“编程语句”,它拥有一个简单的if-else if-else if逻辑,或者你把它当成switch-case也可以。注意,这可是在一条规则中完成的!如果用iptables的话,你不得不独立写多条规则。以上的语句多么像是:
proto = skb->net_hdr->proto;if (proto == tcp) { tcp_chain(skb);} else if (proto == udp) { udp_chain(skb);} else if (proto == icmp) { icmp_chain(skb);}
nftables变成了真正的编程语言!既然成了编程语言,如果支持变量将会是多么灵活的一件事啊,幸运的是,哦,不,不能说幸运,而是nftables原生的性质,nftables支持“变量”!注意下面的命令:
nft add set filter blackhole { type ipv4_addr\;}
nft add rule ip input ip saddr @blackhole drop
nft add element filter blackhole { 192.168.1.4, 192.168.1.5 }
虽然iptables的ipset match也可以这样做,但是那毕竟只是一个match而已,nftables原生就支持这种语法!甚至,甚至还可以用字典映射策略的语法:
nft add map filter mydict { type ipv4_addr : verdict\; }
nft add rule filter input ip saddr vmap @mydict
nft add element filter mydict { 192.168.0.10 : drop, 192.168.0.11 : accept }
这样一来,管理员将会像程序员一样灵活组织自己的逻辑。
七.一个有点悲观的事实
去搜一下ipchain的文档,几乎没有几个能打开的,然后去搜iptables的,目前人气还很旺,nftables的呢?能搜到结果,但是想用起来要费点劲。这就是前仆后继的过程,可以设想将来的某天,nftables也会像ipchain一样逐渐冷淡,淡出人们的视线...这难道就是Linux环境包过滤框架的宿命吗?
这并不是技术发展的必然,老牌的UNIX工具vi,Emacs直到现在依然是黑客们的利器,网络工具netcat也是小巧便携经久不衰...而Linux的包过滤框架短短的15年间更新换代了多次。令人感到希望的是Netfilter这个底层的框架基本已经稳定,不管是iptables还是nftables,都是基于Netfilter来开发的,而早期的ipfw则不是,那时的(Linux 2.3.15内核之前的)底层包过滤框架及其简陋,因此Netfilter一出现就上位了。值得注意的是,这里面包含了太多的是开发者Rusty Russell的个人风格,Netfilter是他完成的,ipchains也是他,这不禁让人想起了破立有秩的Ingo Molnar,引入了O(1)调度器,然后却用更好的CFS调度器替换了它...UNIX则非常不同,个人因素少之又少...