Linux netfilter hacking HOWTO

来源:百度文库 编辑:神马文学网 时间:2024/04/27 04:06:13
本文描述了Linux的netfilter架构,如何在此架构下编写程序,以及在此架构之上实现的主要功能模块,如包过滤,连接跟踪和地址转换等。
1、介绍
大家好:
这篇文章就像是一段旅程,有些路程路途平坦,但有些路程却崎岖不平。我可以给你的最好建议就是端一大杯可口的咖啡或者热巧克力,坐到一把舒适的椅子上,喝一口,然后进入有时看起来是艰险的Linux网络世界。
为了更好地理解netfilter框架,我建议你先看看Packet Filtering HOWTO和NAT HOWTO。如果你想了解内核编程方面的知识,请阅读
大家好
这篇文章就像是一段旅程,有些路程路途平坦,但有些路程却崎岖不平。我可以给你的最好建议就是端一大杯可口的咖啡或者热巧克力,坐到一把舒适的椅子上,喝一口,然后进入有时看起来是艰险的Linux网络世界。Rusty's Unreliable Guide to Kernel Hacking 和Rusty's Unreliable Guide to Kernel Locking。
(C) 2000 Paul `Rusty' Russell. Licenced under the GNU GPL
1.1、什么是netfilter
netfilter是一个独立在基本的Berkeley socket接口之外的包处理框架。它包括四个部分。首先,每个协议族定义一些“钩子”(IPV4定义了五个),它们是一些仔细选择的分布在协议栈中包处理路径上的店。在每一个点上,协议栈会调用netfilter框架并传递它相应的包和钩子的编号。
其次,内核可以在不同协议的不同钩子上注册函数去监听流经这些钩子的包。所以,当一个包经过netfilter框架时,它会检查是否在此协议的此钩子上有注册的函数;如果有,这些函数就有机会按一定的顺序去检查(或者修改)这个包,接着它会丢弃这个包(NF_DROP),允许它通过(NF_ACCEPT),告诉netfilter不要理睬这个包(NF_STOLEN),或者告诉netfilter将这个包放入队列传递到用户空间(NF_QUEUE)。
第三,放入队列的包可以送往用户空间(由ip_queue模块完成);这些包可以被异步处理。
最后,代码有很好的注释和文档。这对任何一个试验性的项目都是非常必要的。Netfilter的格言是(来自 Cort Dougan):“那么,这如何比KDE做得更好?”。(这个格言用另一句话说就是“给我一个理由,让我用ipchains”)除了基础的框架之外,许多实现了与ipchains类似功能的模块被加入内核,比如,一个可扩展的NAT系统,一个可扩展的包过滤系统(iptables)。
1.2、在2.0和2.2中,我们犯了什么错?
1、没有把包传递到用户空间的基础设施
l  内核编码比较复杂
l  内核编码必须使用c/c++
l  动态过滤策略不属于内核
l  2.2引入了通过netlink将包传递到用户空间的机制,但是将包重新注入内核非常慢,而且很难通过合法性检查。比如,如果将包注入内核时声明这个包来自一个特定的设备就不太可能。
2、透明代理是一个大麻烦
l  我们必须查找每一个包来确定是否有socket绑定到它的地址
l  Root用户允许绑定外部地址
l  不能重定向本地生成的包
l  重定向没有处理UDP应答:重定向UDP named包到1153端口不能工作,因为一些客户端不能处理53端口之外的应答包
l  重定向没有和tcp/udp的端口分配机制很好的配合:用户可能会得到一个由重定向规则适用的端口
l  在2.1系列中,至少崩溃了两次以上
l  代码非常具有侵略性。在2.2.1中#ifdef CONFIG_IP_TRANSPARENT_PROXY在11个文件中出现了34次。与之相比较,CONFIG_IP_FIREWALL只在5个文件中出现了10次。
3、不可能创建独立于接口地址的包过滤规则
l  必须知道本地接口的地址才能区分本机产生的包或者本机发生的包
l  转发链只知道出接口的信息,这就意味着你必须通过网络拓扑的信息才能知道这个包来自哪里
4、地址伪装的代码放到了包过滤代码之中:
这两部分代码之间的交叉使得代码变得复杂
l  在input链上,应答包看起来就像是发给本机的
l  在forward链上,解伪装的包根本看不到
l  在output链上,包看起来就像是本机发出的包
5、TOS处理,重定向,ICMP不可达和标记(它会影响端口转发,路由和QOS)同样被放到了包过滤的代码中
6、ipchains的代码既不模块化,也不能被扩展(例如,mac地址过滤,ip过滤项过滤等)
7、缺少一个有效的基础架构,使得在现有的架构上,需要使用不同的技术来实现不同的功能
l  地址伪装,每种协议一个模块
l  通过路由进行快速静态地址转换(没有针对协议的处理)
l  端口转发,重定向和自动转发
l  Linux NAT和Virtual server项目
8、包过滤和CONFIG_NET_FASTROUTE选项不兼容
l  转发的包都会经过三个链
l  没有办法实现跳过某个链的功能
9、不能检查因为路由保护而丢弃的包(比如:源路由验证)
10、没有办法自动读取包过滤规则上的计数器
11、CONFIG_IP_ALWAYS_DEFRAG是编译时的选项,使得制作一个通过的内核变得困难
1.3、你是谁?
我是唯一一个傻得去完成这段代码的人。作为ipchains的合作者和当前LINUX内核防火墙的维护者,我看到人们使用当前系统所遇到的问题,并且越来越清楚他们需要什么样的功能。
1.4、为什么它会崩溃?
Woah!这应该是你上周碰到的问题。
因为我不是你们所想象的那么聪明的程序员,并且也没有测试所有的情境(缺少时间,设备和灵感),所以有问题很正常。但是我创建了一个测试集,欢迎大家扩展这个测试集。
2、哪里可以取到最新代码?
netfilter.org的CVS服务器上有最新的HOWTOS,用户空间工具和测试集。如果是随意的浏览,可以使用一下链接。如果是取最新的代码,可以执行一下操作:
1、匿名登录netfilter的CVS服务器
cvs –d :pserver:cvs@pserver.netfilter.org:/cvspublic login
2、如果提示输入密码,输入‘cvs’
3、取出代码使用命令:
#cvs –d :pserver:cvs@pserver.netfilter.org/cvspublic co netfilter/userspace
4、更新到最新版本,使用
cvs update –d -p
3、netfilter的框架
netfilter仅仅是协议栈中不同点上的一系列钩子(目前只有IPv4,IPv6,DECnet)。理想状况下的IPv4的流程图如下所示:
A Packet Traversing the Netfilter System:    --->[1]--->[ROUTE]--->[3]--->[4]--->                 |            ^                 |            |                 |         [ROUTE]                 v            |                [2]          [5]                 |            ^                 |            |                 v            |    包从左面所示的箭头流入:通过简单的合法性检查(例如,包长度合法,IP校验合法,不是混杂模式收到的包),然后流到netfilter框架中的NF_IP_PRE_ROUTING[1]的钩子上。接下来,包进入路由代码,它判断这个包是发往另一个接口还是发往一个本地进程。路由代码会丢弃那些不能路由的包。如果这个包发往本机,那么在发往本机进程前(如果有的话),netfilter框架中的NF_IP_LOCAL_IN[2]钩子会被调用。如果这个包是发往另一个接口,netfilter框架中的NF_IP_FORWARD[3]会被调用。接下来,在把这个包发往网卡之前,最有一个netfilter钩子NF_IP_POST_ROUTING[4]会被调用。    本机产生的包在发生之前会调用NF_IP_LOCAL_OUT[5]钩子。此处你可以看到,在这个钩子之后会调用路由代码:实际上,路由代码在此之前已经被调用过一次(确定包的源地址和IP选项)。
如果你想修改路由,你必须自己修改skb->dst里面的值,可以参考NAT代码。3.1、Netfilter Base
这里我们有一个IPv4的netfilter的例子,你可以看到每个钩子是何时被激活的,这是netfilter核心所在。
内核模块可以注册监听任何一个钩子。内核模块在注册函数时必须指定此函数在钩子上条用的优先级;这样当网络协议栈代码在调用netfilter的钩子函数时,内核模块注册在这个钩子上的函数就会按优先级的顺序被调用,这样就能方便的处理网络包。一个模块可以让netfilter做如下五件事情:
l  NF_ACCEPT:继续按正常的协议栈流程处理。
l  NF_DROP:丢弃包,中止协议栈的流程。
l  NF_STOLEN:这个包已被缓存,中止协议栈的流程。
l  NF_QUEUE:把包放到队列中(通常会把包转发到用户空间处理)。
l  NF_REPEAT:再此调用这个钩子函数。
有关netfilter的其他话题(处理队列中的包,精彩的评论等)会在后面的内核部分描述。
在这个基础上,我们可以创建相对复杂的包处理程序,如下面两章所示。
3.2、包选择:IP Tables
IP Tables是一个构建在netfilter框架之上的包选择系统。它是ipchains的直系厚道(ipchains从ipfwadm演变而来,而ipfwadm从BSD的ipfw IIRC演变而来)。IP Tables提供更强的可扩展性。内核模块可以注册一个新的表,并且 可以让一个包经过一个特定的表。这个包选择系统可以被用来做包过滤(filter表),地址转换(nat表)和通常的包处理(mangle表)。
在netfilter上注册的钩子函数如下所示(这些钩子函数按它们被调用的顺序排列):
--->PRE------>[ROUTE]--->FWD---------->POST------>       Conntrack    |       Mangle   ^    Mangle       Mangle       |       Filter   |    NAT (Src)       NAT (Dst)    |                |    Conntrack       (QDisc)      |             [ROUTE]                    v                |                    IN Filter       OUT Conntrack                    |  Conntrack     ^  Mangle                    |  Mangle        |  NAT (Dst)                    v                |  Filter3.2.1、包过滤
“filter”表只过滤包,但不会修改包。
Iptables相对于ipchains德一个优势在于它比较小并且快速,它只在
NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT三个点上注册钩子函数。这就意味着对每一个包来说,有且仅有一个点可以过滤它。这使得用户可以更方便地使用它。同时,netfilter框架在NF_IP_FORWARD点上同时提供入接口和出接口,这使得过滤更加简单。
注意:我已经把ipc和ipfwadm的内核部分作为内核模块移植到了netfilter上。用户可以继续使用旧的ipchains或ipfwadm工具而不需要额外的升级。
3.2.2地址转换
对“nat”表来说,包会在两个钩子上被处理:对非本地包来说,NF_IP_PRE_ROUTING和NF_IP_POST_ROUTING两个钩子可以用于目的转换盒源转换,并且 在相对应的钩子上座相反的操作。如果打开CONFIG_IP_NF_NAT_LOCAL编译选项,可以在NF_IP_LOCAL_OUT和NF_IP_LOCAL_IN钩子上修改本地包的地址。
“nat”表与“filter”表完全不同,在“nat”表上,只有新连接的第一个包会经过表:当第一个包经过表之后,会创建相应的状态,后续的统一连接的包会按状态中所记录的信息来做相应的处理。
3.2.3、地址伪装,端口转发和透明代理
我吧地址转换分为源转换(包的源地址会改变)和目的转换(包的目的地址会改变)。
地址伪装是特殊形式的源转换,端口转发和透明代理是特殊形式的目的转换。它们都可以在地址转换的框架内实现,而不需要额外的编程。
3.2.4、包处理
包处理表(“mangle”)用于修改包的信息。一般涉及到TOS或TCPMSS的应用程序会使用这个表。Mangle表在五个钩子上都可以注册钩子函数。(需要注意的是这个改动是从2.4.18版本开始的,以前的版本中mangle表只在三个点子上注册钩子函数)。
3.3、连接跟踪
连接跟踪是地址转换的基础,但是它被实现成一个独立的模块。这允许包过滤代码可以通过扩展来简单方便地使用连接跟踪(比如“state”模块)。
3.4、其他
新框架的灵活性给人们在实现一些有趣的功能或增强已有功能,或者完全替换现有功能提供了便利。
4、编程信息
告诉你一个秘密,我的宠物hamster编写了所有的代码,在我的宠物的计划里,我只是个代理,所以,如果抱怨代码里有错误,去抱怨我的宠物,而不是我。
4.1、理解 ip_tables
Iptables在内存中提供了一个规则数组(因此命名为iptables),这些信息会在包经过钩子函数时被检查。当一个表注册后,用户空间的程序可以使用getsockopt和setsockopt函数读取或替换表的内容。
Iptables没有注册任何netfilter钩子函数:其他模块会注册钩子函数并把包当做一个参数传递给它。模块必须把netfilter钩子寒素和ip_tables分开注册,并且提供一种机制,使得调用钩子函数时可以访问ip_tables。
4.1.1、ip_tables数据结构
为了使用方便,同样的数据结构可以再用户空间和内核空间使用,当然,一些变量只能在内核中使用。
每条规则都包含如下几个部分:
l  一个ipt_entry结构
l  零个或多个ipt_entry_match结构,每一个都会附带一个变长数据
l  一个ipt_entry_target结构,包含一个变长数据
规则的可变特性会扩展带来了巨大的灵活性,就像我们所看到的,每个macth和target都会附带一定量的数据。这些数据会造成一些陷阱,所以我们必须让这些数据对齐。我们保证ipt_entry,ipt_entry_match和ipt_entry_target结构的大小合适,并且所有的数据都通过IPT_ALIGN()宏来保证和机器最大对齐值一致。
Ipt_entry有如下域:
l  一个ipt_ip结构,用于匹配IP头的信息
l  一个nf_cache位域,用于标识这条规则检查包的哪一部分
l  一个target_offset域,表示从规则头开始到ipt_entry_target结构之间的偏移。这个偏移值必须正确对齐(用IPT_MACRO宏)
l  一个next_offset域,标识这条规则的总长度,包括这条规则中所有的match和target,这个值也必须正确对齐
l  一个comefrom域,被规则链用于向前回溯
l  一个ipt_counters结构域,标识匹配这条规则的包计数和字节计数
Ipt_entry_match和ipt_entry_target很相似,它们都包含一个表示总长度的域(match_size和target_size)和一个指向match和target名的联合(在用户空间使用),还有一个指针(在内核空间使用)。
由于规则数据结构比较复杂,所有内核提供了如下的辅助函数:
Ipt_get_target()
这个inline函数返回规则中target的指针
IPT_MATCH_ITERATE()
这个宏对规则上的每一个macth调用给定的函数。这个函数的第一个参数时结构ipt_match_entry,其他参数(如果有的话)是在IPT_MATCH_ITERATE()宏中提供的参数。这个函数返回0表示继续遍历,或者是一个非0值表示停止。
IPT_ENTRY_ITERATE()
这个宏的参数时一个指向规则的指针,规则表的总长度,和一个需要调用的函数。函数的第一个参数时结构ipt_entry,其他参数是(如果有的话)是在IPT_ENTRY_ITERATE()宏中提供的参数。这个函数返回0表示继续遍历,或者是一个非0值标识停止
4.1.2、用户空间的ip_tables
用户空间有四种操作:它可以读当前的表,读表的信息(钩子函数的位置和表的大小),替换表(并且获得旧的计数器),或者添加一个新的计数器。
它允许在用户空间中模拟任意原子操作:通过使用libiptc库,用户空间的程序可以实现“添加/删除/替换”的语义。
由于这些规则表可以传入内核空间,内核对齐就成了一个重要的问题,特别是那些用户空间和啮合空间的数据结构长度不一致的机器(比如Sparc64的内核时64位,而用户空间是32位)。可以通过修改libiptc.h文件中的IPT_ALIGN宏的定义来解决这个问题。
4.1.3、ip_tables的使用和遍历
内核从钩子函数中指定的位置开始遍历规则表。当规则被检查时,如果ipt_ip结构被匹配,这个规则上的每一个ipt_entry_match结构按顺序被检查(同时与此match相关的函数被调用)。如果match函数返回0,遍历过程就在这条规则上停止。如果这个规则的‘hotdrop’参数为1,匹配规则的包会被立即丢弃(这会在匹配到一些异常包时使用,比如在tcp match函数中就有这样的操作)。
如果规则完全匹配,计数器就会增加,同时ipt_entry_target会被检查:如果这是一个标准target,它的‘verdict’值就是结果(负值表示这个包如何处理,正值表示跳转的偏移量)。如果结果是正值,但是偏移不是下一条规则,‘back’变量就会被赋值,前一个‘back’值会被赋到此规则的‘comefrom’域。
如果不是标准target,target的函数会被调用:它返回一个值(非标准的target不能跳转,否则会破坏静态循环检测算法)。这个值可以是IPT_CONTINUE,表示继续检查下一条规则。
4.2、扩展iptables
因为我比较懒,所以iptables做得非常容易扩展。这是一个将工作交给其他人的手段,开源软件都是这样(自由软件,RMS会说,与自由有关,当我写下这些的时候,我正在听他的一个演讲)。
扩展iptables基本上有两方面的工作:通过写一个新模块扩展内核,然后写一个新的共享库来扩展用户空间的iptables。
4.2.1、内核
写一个内核模块非常简单,你可以参考例子。有一个需要注意的地方就是你的代码必须是可重入的:可能存在一个包从用户空间发出,而同时另一个包从中断到达。事实上,从2.3.4开始,SMP可以在每个CPU上的网卡中断上收发网络包。
这些函数你需要知道:
init_module()
这是模块的入口。它返回负值表示出错,或者返回0表示成功地将自己注册到了内核。
cleanup_module()
这是模块的出口,在卸载模块是调用。
ipt_register_match()
这个函数用来注册一个新的match类型。你可以传递给它一个ipt_macth的结构,这个结构通常被声明成一个静态变量(文件范围内)。
ipt_register_target()
这个函数用来注册一个新的target类型。你可以传递给它一个ipt_target的结构,这个结构通常被声明成一个静态变量(文件范围内)。
ipt_unregister_target()
卸载你的注册的target
ipt_unregister_match()
卸载你主持的match
在你的新match或target的扩展空间中做一些有趣的事情(比如提供一个计数器)时要注意。在SMP机器上,整个规则表通过memcpy给每个cpu负值一份:如果你需要保存集中的信息,你可以参考在limit match中所使用的方法。
4.2.2、新Match函数
新的match一般都是一个独立的模块。但是可以让你的模块具有有可扩展的特性,当然这不是必须的。一种方法试试用netfilter框架提供的nf_register_sockopt函数允许用户与你的模块直接交互。另一种方法试把内部符号导出给其他模块使用,netfilter和ip_tables就是使用这种方法。
新match的核心是一个ipt_match结构,这个结构有以下域:
list
这个域被初始化为‘{NULL,NULL}’。
name
这个域是match的名字,这个名字在用户空间中被引用。名字必须和模块的名称匹配(比如,如果名字是“mac”,模块名称必须是“ipt_mac.o”),这样才能自动加载。
match
这个域是指向match函数的一个指针,函数的参数时skb,进、出设备的指针(其中之一可能为空,这与钩子的位置相关),一个指向规则中match数据的指针(这个结构在用户空间中初始化),IP offset(非0标识一个没有头部的分片),一个指向协议头的指针(比如只有IP头部),数据的长度(例如包长度减去IP头的长度),和一个指向hotdrop变量的指针。如果包匹配规则,它必须返回一个非0值,同时,如果它返回0,它可以设置hotdrop变量为1,表示包必须被立即丢弃。
checkentry
这个域是一个函数指针,这个函数检查规则的参数;如果函数返回0,这条规则就不会被用户接受。例如,tcp match值接受tcp包,如果规则中的ipt_ip部分没有指定规则的协议时tcp,它就会返回0。tablename变量允许你的match去控制它可以被那些表引用,hook_mask变量表示规则可以被哪些钩子引用:如果你的match域netfilter的钩子没有关系,你可以不设置这个值。
destroy
这个域是一个函数指针,这个函数在引用这个match的变量被删除时调用,它允许你在checkentry中动态分配内存,并在这个函数中它们释放掉。
me
这个域被设置为“THIS_MODULE”,它是一个指向你的模块的指针。它的使用计数在规则创建或释放时增减。这样就可以防止用户删除一个模块(在cleanup_module()时调用),而其他规则在引用这个模块。
4.2.3、新的target
如果你的target修改数据包(头部或者是数据部分),它必须调用skb_unshare来copy整个数据包,特别是这个数据包是被克隆的:否则原始socket中skbuff是被修改过的(必须用户在使用tcpdump的时候会看到一些很奇怪的包)。
新target同样是一个独立的模块。上面关于新match的讨论通用适用于新target。
新target的核心是一个ipt_target结构,它被当做参数传递给ipt_register_target()函数,这个结构有如下域:
list
这个域被初始化成‘{NULL,NULL}’。
name
这个域是target函数的名称,这个名字在用户空间中被引用。名字必须和模块的名称一致(例如,如果名称是“REJECT”,那么模块名称必须是“ipt_REJECT.o”),这是模块自动加载所必须的。
target
这个指向target函数的指针,函数的参数时skbuff,钩子的编号,入、出设备的指针(这两个指针都可以为空),一个指向target数据的指针,一个指向表中规则位置的指针。target函数可以返回IPT_CONTINUE (-1),如果需要继续遍历;或者是netfilter所定义的值(NF_DROP,NF_ACCEPT,NF_STOLEN etc.)。
checkentry
这个域指向一个函数,它检查规则的定义,如果它返回0,那么这条规则就不能被用户接受。
destroy
这个域指向一个函数,它在引用此target的实体被删除时调用。这就允许你在checkentry中动态分配资源而在此函数将其释放。
me
这个域被设置为`THIS_MODULE',它是一个指向你的模块的指针。它的使用计数在引用此target的规则创建或释放时增减。这就防止用户释放一个模块(调用cleanup_module()函数),而同时还有一条规则在引用这个规则。
4.2.4、新table
如果有需求,你可以创建一个新的table。为了完成这个任务,你可以调用ipt_register_table()函数注册一个ipt_table结构,这个结构有如下域:
list
这个域被初始化为{ NULL, NULL }。
name
这个域是table的名字,它会在用户空间中被引用。这个名字必须和模块名称一致(例如,如果 table的名称是nat,那么模块名称必须是iptable_nat.o),这是模块自动加载所必须的。
table
这是一个ipt_replace结构,它在用户空间里被用来替换table结构。它的'counters'指针必须被设置为NULL。这个数据结构可以被声明为'_initdata',这样就可以在启动之后释放掉。
valid_hooks
这是一个IPv4钩子的字掩码,它代表可以引用表的钩子:它被用来检查规则指针是否正确,而且可以在ipt_match和ipt_target的checkentry函数中计算可能被引用的钩子。
lock
这是整个表的读写自旋锁。它被初始化为RW_LOCK_UNLOCKED。
private
这个域在ip_tables代码中内部引用。
4.2.5、用户空间工具
现在你可以开始写内核模块了,但是你也许会需要在用户空间来设置内核的某些选项。我们不会为每个 iptables扩展创建一个分支,相反,我们使用共享库。
新的表通常不需要对iptables进行扩展:用户使用"-t"参数来引用新的表。
共享库必须有一个'_init()'函数,它会在加载时调用:这与内核模块的'init_module'函数等价。在这个函数中,需要调用register_match()或register_target,这取决于你的共享库中提供的是新的 match还是新的target。
你需要提供一个共享库:它可以用来初始化结构或者提供附加的选项。我坚持使用共享库,即使这个库什么都不做,这样可以减少有关共享库找不到的问题报告。
在iptables.h中定义了一些有用的函数,特别是如下几个:
check_inverse()
检查参数是否有'!',如果有,设置参数的'invert'标记。如果它返回真,你就可以增加optind,就像例子中所做的一样。
string_to_number()
把字串转换成一个给定范围内的数字,如果字符串的格式错误或者数字超出了范围,它返回-1。'string_to_number'依赖于'strtol'(参见手册),这就意味着,如果字串开头是"0x",字串将被转成16进制的数字,如果是"0",字串将被转换成8进制的数字。
exit_error()
这个函数在有错误发生的时候调用。通常,第一个参数是`PARAMETER_PROBLEM,表示用户的使用方式有错误。
4.2.6、新的Match函数
你的共享库中的_init()函数调用register_match()函数这次一个iptables_match结构,这个结构有如下域:
next
这个指针用于创建一个match的链表(就像规则链表一样)。它必须被初始化为NULL。
name
match的名称,它必须和库的名称一致(比如tcp和libipt_tcp.so)
version
通常被设置为IPTABLES_VERSION:这保证iptables程序不会加载错误的共享库。
size
match数据的大小,你必须使用IPT_ALIGN()宏来保证它正确地对齐。
userspacesize
对有些match来说,内核会在内部修改一些值(比如limit match)。这就意味着,只用memcmp来比较两条规则是否相同是不够的(它会在删除匹配规则的函数中调用)。在这种情况下,把所有不改变的域放到结构的开始部分,同时把不改变的域的大小值赋给userspacesize。一般情况下,这个值和size值相同。
help
打印match的使用语法的函数。
init
这个函数被用来初始化ipt_entry_match结构中的扩展空间(如果有的话),并设置 nfcache位的值。如果你检查的值不能用linux/include/netfilter_ipv4.h中所定义的值来表达,你可以简单的把nfcache逻辑或NFC_UNKNOWN。这个函数在parse()函数之前调用。
parse
这个函数用于解析命令行上的参数:如果某些参数是必须的,它返回非0值。如果选项前有'!', 'invert'值为真。'flags'指针在match库中使用,它用一个位与的掩码来表示命令行上有哪些选项。你需要确认你是否修改了nfcache域。如果需要的话,你可以通过重新分配内存来扩充ipt_entry_match 结构的大小,但是你必须保证你所传递的结构的大小值是通过IPT_ALIGN宏对齐的。
final_check
这个函数在命令行选项被解析后调用。它处理你ipt_entry_match中的flags项。它可以检查是否所有的必选项都已经有了,如果没有,则调用exit_error()。
print
由规则的列表函数调用(到标准输出)来打印match的额外信息。如果用户指定了-n选项,则打印成数字的flag会被置位。
save
它与parse函数刚好相反:它被iptables-save调用,用来生成规则的命令行。
extra_opts
这是一个以NULL结束的数组,表示你的库所能处理的扩展选项。它和当前的选项一起传递给getopt_long,详情请参考手册。get_long的返回值做为第一个参数('c')传递给你的'parse()'函数。
在结构的最后有其他区的一些域,这些域由iptables内部使用,你不需要设置它们。
4.2.7、新的target
你的共享库中的_init函数调用register_target()函数注册iptables_target结构,它的域和上面所描述的iptables_match的域类似。
4.2.8、使用'libiptc'
libiptc是一个用于列表、处理内核模块中规则的库。它目前用在iptables程序中,当然,它也可以用来方便地写其他工具。使用这些函数,你必须是根用户。
内核中的表只是一个规则表和一些代表入口的数字。链名(比如"INPUT")用来表示这些规则。用户定义的链通过在用户定义的链之前插入一个错误代码来标识,它的链名放在target的扩展数据中(标准链的位置由三个表的入口位置所定义)。
以下几个是标准的target:ACCEPT,DROP,QUEUE(它们被相应的解释为NF_ACCEPT,NF_DROP和NF_QUEUE), RETURN(它被翻译为IPT_RETURN,这个值在ip_tables中会特殊处理),和JUMP(它把链的名称翻译为表中的偏移值)。
当iptc_init调用时,表(包括计数器)被读出。这个表可以用下面这些函数处理:
`iptc_insert_entry()', `iptc_replace_entry()', `iptc_append_entry()', `iptc_delete_entry()', `iptc_delete_num_entry()', `iptc_flush_entries()', `iptc_zero_entries()', `iptc_create_chain()' `iptc_delete_chain()', `iptc_set_policy()'。
对表的修改直到调用iptc_commit()后才被提交。这就意味着如果有两个用户调用库来处理同一个链就会出现死锁。这就需要加锁来避免这种情况,但是目前的实现中还没有锁。
计数器不存在死锁的问题。在读写操作过程中,内核计数器的增加会在新的表中体现出来。
以下是一些辅助函数:
iptc_first_chain()
这个函数返回表中的第一个链。
iptc_next_chain()
这个函数返回表中下一个链的名称,如果不存在,返回NULL。
iptc_builtin()
返回真,如果这个链名是一个内建的链。
iptc_first_rule()
返回给定链中的第一条规则,如果没有规则,返回NULL。
iptc_next_rule()
返回给的链中的下一条规则,如果到了链尾,返回NULL。
iptc_get_target()
取给定规则中的target。如果这是一个扩展的target,返回target的名称。如果这是一个到其他链的跳转,返回那个链的名称。如果这是一个判断(比如DROP),返回其名称。如果没有 target(一条统计规则),则返回空串。
需要指出的是,应该使用这个函数,而不是直接返回ipt_entry结构中的verdict值。使用这个函数可以在标准的判断值上增加更多的解释。
iptc_get_policy()
取内建链的策略值,并把counter参数填充为匹配此策略的计数。
iptc_strerror()
这个函数对iptc库中的每个错误值返回一个描述性的字串。如果函数出错,它通常会设置errno:这个值可以传递给iptc_strerror,它可以输出错误信息。
4.3、理解地址转换
欢迎来到内核地址转换的世界。需要指出的是,目前所涉及的架构,首要目标是满足完备性而不是高效。将来的工作会大大地提升系统的性能。但目前来说,让我满意的是基本的功能都工作正常。
地址转换分为连接跟踪(它不会修改包的内容)和地址转换两部分。连接跟踪同样也会被iptables模块使用,因此在iptables里用到的一些连接跟踪的状态,在地址转换中根本不会用到。
4.3.1、连接跟踪
连接跟踪的钩子函数在NF_IP_LOCAL_OUT和NF_IP_PRE_ROUTING钩子上的优先级较高,这样就可以在包进入系统之前记录它。
skb中的nfct域是一个指向ip_conntrack结构中infos[]数组中的某个项的指针。因此我们可以通过nfct所指向的域在infos[]数组中的位置来判断skb的状态:这个指针把skb与状态结构以及skb的状态关联起来。
取nfct域最好的方法是调用ip_conntrack_get函数,如果nfct没有设置,它返回NULL,如果已经设置,它返回指向连接的指针,并且给ctinfo赋值,ctinfo描述了包与连接的关系。这个枚举类型有以下几个值:
IP_CT_ESTABLISHED
这个包属于已建立连接,并且在原方向上。
IP_CT_RELATED
这个包与连接相关,并且在原方向上。
IP_CT_NEW
这个包将创建一个新连接(很明显,它在原方向上)。
IP_CT_ESTABLISHED + IP_CT_IS_REPLY
这个包属于已建立连接,并且在应答方向上。
IP_CT_RELATED + IP_CT_IS_REPLY
这个包与连接相关,并且在应答方向上。
应答方向的包可以通过检查状态值是否大于等于IP_CT_IS_REPLY来判断。
4.4、扩展连接跟踪和地址转换
这个框架需要适应任意协议和地址映射类型。有些映射类型会比较特殊,比如负载均衡/高可用映射等。
从内部来说,在做绑定或匹配规则前,连接跟踪会把一个包转换成一个"tuple",它代表了连接跟踪对包感兴趣的部分。这个tuple有一个可改变部分和一个不可改变部分,叫做"src"和"dst",这是源转换中对第一个包的视图(对目的转换来说,这是第一个应答包的视图)。这个tuple对同一流的同一方向上的包是一样的。
例如,对一个TCP包的tuple来说,它包含一个可改变部分:源地址和源端口;一个不可改变部分:目的地址和目的端口。可改变部分和不可改变部分并不一定是同一类型,例如,一个ICMP包的tuple包括可改变部分:源地址和ICMP的ID,不可改变部分:目的地址和ICMP的类型和代码。
每一个tuple都有一个反向的tuple,它代表应答方向上的tuple。例如,ICMP ping包的tuple是icmp id 12345,从192.168.1.1到1.2.3.4,而ICMP应答包的tuple是icmp id 12345,从1.2.3.4到192.168 .1.1。
这些tuple的类型是ip_conntrack_tuple,它的用途很广泛。事实上,一个包的完整信息就包括:这个包是在哪个钩子函数中处理的(这会决定哪些信息是可改变的),从哪个设备进、出,和这个包的tuple。
大多数tuple都包含在ip_conntrack_tuple_hash结构中,这个结构有一个链表结构和一个指向tuple所代表连接的指针。
连接的数据结构类型是ip_conntrack:它有两个ip_conntrack_tuple_hash类型的域,一个表示原方向的包(tuplehash[IP_CT_DIR_ORIGINAL]),一个表示应答方向的包(tuplehash[IP_CT_DIR_REPLY])。
地址转换代码首先检查skbuff的nfct域来判断当前包是否属于一个已创建的连接。如果不是,它是否要创建一个新的连接,如果是,这个包在哪个方向上。如果是已存在连接,它就可以根据连接中的可改变部分来决定需要修改包的哪个部分。
如果是创建新的连接,它首先遍历nat表来查找这个包匹配的规则。如果有规则匹配到,它就会初始化连接中两个方向上可改变的部分。同时修改连接中的应答方向上的tuple,使得应答包可以匹配到这个连接。
如果没有匹配到任何规则,地址转换代码也会创建一个空绑定。这不会修改任何包,但是可以防止其他流错误地映射到已存在的连接上。有时,我们可能无法创建一个空绑定,因为在当前地连接上已经有了一个映射,在这种情况下,协议处理代码可能会重新映射当前流,即使这是一个空绑定。
4.4.1、标准地址转换target
地址转换target和其他iptables的扩展target的基本情况一样,但是它只检查nat表。SNAT和DNAT都有一个 ip_nat_multi_range结构的参数,这个参数描述了映射地址的范围。一个ip_nat_range结构描述了地址范围 (最小地址和最大地址)和与协议相关(比如tcp协议的端口)的端口范围(最小端口和最大端口)。这个结构中还有一个flags域描述IP地址是否可以被映射(有些时候,我们只想映射协议tuple中的某一部分,而不是地址),另外它还描述协议相关部分的映射范围是否正确。
ip_nat_multi_range结构是一个ip_nat_rangle元素的数组,这就意味这一个范围可以是"1.1.1.1-1.1. 1.2 ports 50-55 并且 1.1.1.2 port 80"。每一个范围元素都加到了范围集合中(这是一个集合,符合集合的约束)。
4.4.2、新的协议
4.4.3、深入内核
实现一个新协议的地址转换,首先需要考虑的是一个tuple中,哪些是可改变的,哪些是不可改变的。tuple中的参数可以唯一确定一个流。可改变部分是我们可以做地址转换的部分:对tcp来说,它是源端口,对ICMP来说,它是 icmp ID;一些被用作“流标识"。剩余的部分同样可以唯一的标识一个流,但是我们不能改变它(例如tcp的目的端口, ICMP的类型等)。
一旦确定了这些事情,你可以写一个连接跟踪的扩展,你需要调用ip_conntrack_register_protocol()函数去注册一个ip_conntrack_protocol结构。
ip_conntrack_protocol结构中有如下域:
list
初始化为{ NULL, NULL }。
proto
协议号,可以参考/etc/protocols
name
协议名称,这个名称是用户可以看到的名称,所以最好使用/etc/protocols中的经典名称。
pkt_to_tuple
给定一个网络包,取网络包的参数构造一个tuple。'datah'指针指向网络包的头 (只有IP头),datalen表示网络包的长度。如果网络包的长度小于标准头部的长度,返回0;datalen必须至少有8个字节长(这是框架强制规定)。
invert_tuple
这个函数根据tuple构造一个应答的tuple。
print_tuple
这个函数用于打印tuple中与协议相关的部分,通常会把tuple的值打印到一个缓冲区中。函数会返回打印缓冲区的长度。这个函数也被/proc函数用来打印状态信息。
print_conntrack
这个函数用来打印conntrack结构中的私有值,当然,也被/proc函数用来打印状态信息。
packet
如果连接在已建立状态下收到的包就调用这个函数。你可以得到一个指向conntrack结构的指针,IP头,长度和ctinfo。对这个包,你可以返回一个判断(通常是NF_ACCEPT),或者是-1,表示这个包不是正常的包。如果你愿意,你可以在着个函数中删除连接,但是你必须遵守一定的规则以避免死锁(请参考ip_conntrack_ proto_icmp.c的代码):
if (del_timer(&ct->timeout))
ct->timeout.function((unsigned long)ct);
new
这个函数在创建一个新连接时调用。这个函数没有ctinfo参数,因为创建连接的第一个包的ctinfo是 IP_CT_NEW。如果创建失败,这个函数返回0,或者返回新连接的超时值。
当你完成并测试了你的新协议的连接跟踪代码,你就可以在地址转换代码中加入相应的对新协议的处理。这就意味这你需要写一个新的模块来扩展地址转换代码。首先,你需要调用ip_nat_protocol_register()函数注册一个 ip_nat_protocol结构,这个结构有如下域:
list
初始化为{ NULL, NULL }。
name
协议的名称。这个名称会呈现给用户,所以最好使用/etc/protocols中定义的经典名称,这也有利于自动加载,这个问题会在后面讲到。
protonum
协议号,请参考/etc/protocols。
manip_pkt
这是连接跟踪中pkt_to_tuple函数的另一半:你可以把它想象成"tuple_to_pkt"。当然,它们还有一些区别:你可以得到IP头的指针,网络包的总长度。这是因为在有些协议中(UDP,TCP)需要知道IP 头的信息。你可以从ip_nat_tuple_manip中得到tuple中需要修改的部分(比如src域),而不是修改整个tuple,你还可以知道需要做哪种转换。
in_range
这个函数可以告诉我们给定的tuple中的可改变部分是否在给定范围之内。这个函数用了点小技巧:我们已经知道了对tuple做哪种转换,它会告诉我们如何去解释范围的涵义(我们的目标是源范围还是目的范围?)
这个函数可以检查已创建的映射是否在正确的范围内,并且可以告诉所做的修改是否必要。
unique_tuple
这个函数是地址转换代码的核心:给定一个tuple和范围,我们在给定的范围内修改tuple的值并保证这个新的tuple是唯一的。如果我们不能找到一个未使用的tuple,函数返回0。我们同样会得到一个指向conntrack结构的指针,这个指针会在ip_nat_used_tuple()函数中用到。
一般的方法是简单的在范围之内遍历所有可用值,并使用ip_nat_used_tuple()函数检查新的tuple是否已经存在。重复这个过程,直到ip_nat_used_tuple()函数返回false。
空绑定在这种情况下已经被检查了,这是因为:空绑定要么超出了给定范围,要么已经存在。
如果没有设置IP_NAT_RANGE_PROTO_SPECIFIED标记,那么只需要做地址转换,而不是地址伪装:在指定的范围内做地址转换。如果不需要做转换(例如,在TCP的目的转换中,除非要求,否则不转换其目的端口),就返回 0。
print
给定一个字符缓冲区,一个match tuple和掩码,打印协议相关的部分并返回打印缓冲区的长度。
print_range
给定一个字符缓冲区和范围,打印协议相关的范围并返回打印缓冲区的长度。如果 IP_NAT_RANGE_PROTO_SPECIFIED没有设置,这个函数不会被调用。
4.4.4、新的地址转换target
这部分很有趣。你可以写新的地址转换target来提供新的映射类型。在基本的映射类型之外,我们还提供了 MASQUERADE和REDIRECT。它们都很简单,足够你学会如何写一个新的地址转换target.
它们的写法和其他iptables target没什么区别,但是在内部,它们会扩展连接并调用ip_nat_setup_info() 函数。
4.4.5、helper
连接跟踪的helper可以使得连接跟踪代码处理多连接的协议(比如FTP),并且可以创建新连接并把它标识为当前连接的子连接,子连接的地址从协议流中获取。
地址转换的helper做两件事情:首先,它修改当前流中的协议地址;其次;它可以根据原有连接的信息来对新连接做地址转换。
4.4.6、连接跟踪helper
4.4.7、描述
连接跟踪的任务就是确定某个包是否属于一个已建立的连接。它通过以下手段来完成这项工作:
l  告诉netfilter我们的模块对哪些包感兴趣(通常helper只监控一个特定的端口)
l  注册一个函数。这个函数会在包匹配时被调用
l  通过调用ip_conntrack_expect_related()函数来创建一个expect,标识将要创建的相关连接
当新连接的第一个包到来时,还需要做一些额外的工作,模块注册了一个回调函数来完成这些工作。
4.4.8、可用的结构和函数
你的内核模块首先需要调用ip_conntrack_helper_register()函数来注册一个ip_conntrack_helper结构,这个结构有如下的域:
list
所有的连接跟踪helper放到一个链表中。初始化为{ NULL, NULL }。
name
helper的名字,可以使用helper所关心的协议名称(比如ftp,irc等)。
flags
一个标记集合,有如下值可选: IP_CT_HELPER_F_REUSE_EXPECT如果达到expect的上限,是否要重用。
me
指向helper模块的指针。用THIS_MODULE宏初始化。
max_expected
可以创建未确认的expect的最大数。
timeout
未确认的expect的存活时间(单位:秒)。expect超时后被删除。
tuple
一个ip_conntrack_tuple结构,表示此模块关心的连接的参数。
mask
另一个ip_conntrack_tuple结构,是上面tuple的掩码,描述tuple关心的域。
help
对每一个匹配tuple+mask的包调用找个函数。
4.4.9、一个连接跟踪helper范例
#define FOO_PORT        111static int foo_expectfn(struct ip_conntrack *new){        /* 当新连接的第一个包到达时调用 */        return 0;}static int foo_help(const struct iphdr *iph, size_t len,                 struct ip_conntrack *ct,                 enum ip_conntrack_info ctinfo){        /* 分析协议 */        /* 取出协议相关的地址和端口 */        ct->help.ct_foo_info = ...        if (there_will_be_new_packets_related_to_this_connection)        {                struct ip_conntrack_expect exp;                memset(&exp, 0, sizeof(exp));                exp.t = tuple_specifying_related_packets;                exp.mask = mask_for_above_tuple;                exp.expectfn = foo_expectfn;                exp.seq = tcp_sequence_number_of_expectation_cause;                /* 新连接相关的信息 */                exp.help.exp_foo_info = ...                ip_conntrack_expect_related(ct, &exp);        }        return NF_ACCEPT;}               static struct ip_conntrack_helper foo;static int __init init(void){        memset(&foo, 0, sizeof(struct ip_conntrack_helper);        foo.name = "foo";        foo.flags = IP_CT_HELPER_F_REUSE_EXPECT;        foo.me = THIS_MODULE;        foo.max_expected = 1;   /* one expectation at a time */        foo.timeout = 0;        /* expectation never expires */        /* we are interested in all TCP packets with destport 111 */        foo.tuple.dst.protonum = IPPROTO_TCP;        foo.tuple.dst.u.tcp.port = htons(FOO_PORT);        foo.mask.dst.protonum = 0xFFFF;        foo.mask.dst.u.tcp.port = 0xFFFF;        foo.help = foo_help;        return ip_conntrack_helper_register(&foo);  }static void __exit fini(void){        ip_conntrack_helper_unregister(&foo);}4.4.10、地址转换helper
4.4.11、描述
地址转换helper对一些应用协议做特殊处理。通常是对协议数据的改动:比如在FTP协议中,PORT命令会把客户端的IP地址和端口告诉服务器,让服务器来连接。这种情况下,FTP协议的地址转换helper必须替换这个地址和端口,否则服务器无法连接客户端。
如果应用协议是基于TCP的,就会复杂一点。一个原因就是可能会改变包的大小(以FTP协议为例,替换后的 IP地址、端口可能和原来的IP地址、端口长度不一样)。如果我们改变了包的大小,在NAT的两边,syn/ack的序列号就会不同。(例如:如果我们扩展了4个字节,我们就需要把找个方向上的tcp包的序列号加上这个偏移值)
与原连接相关的连接需要做地址转换。以FTP为例,所有数据连接的包都需要修改为与原连接对应的地址,而不是通过nat表的规则来创建地址转换结构。
l  对原连接的包调用回调函数foo_help
l  对新连接的包调用回调函数foo_nat_expected
4.4.12、可用的结构和函数
在地址转换helper模块的_init函数中需要调用ip_nat_helper_register()函数来注册一个ip_nat_helper结构,这个结构有如下域:
list
初始化为{ NULL, NULL }
name
helper的名称,可以使用协议名。
flags
标记值,有以下选项: IP_NAT_HELPER_F_ALWAYS对每一个包都调用helper的函数,而不仅仅是那些包含helper所感兴趣的地址和端口的包 IP_NAT_HELPER_F_STANDALONE这是一个独立的helper,没有与之对应的连接跟踪的helper。
me
指向helper模块。可以用THIS_MODULE宏初始化
tuple
一个ip_conntrack_tuple结构,描述helper所感兴趣包的信息
mask
一个ip_conntrack_tuple结构,与上一个结构相与可以描述helper所感兴趣的属性。
help
对每一个和tuple+mask匹配的包调用这个函数
expect
对新连接的第一个包调用这个函数以创建连接的地址转换结构
写一个地址转换helper与写一个连接跟踪helper非常类似。
4.4.13、地址转换helper的例子
#define FOO_PORT        111static int foo_nat_expected(struct sk_buff **pksb,                        unsigned int hooknum,                        struct ip_conntrack *ct,                        struct ip_nat_info *info)/* called whenever the first packet of a related connection arrives.   params:      pksb    packet buffer                hooknum HOOK the call comes from (POST_ROUTING, PRE_ROUTING)                ct      information about this (the related) connection                info    &ct->nat.info   return value: Verdict (NF_ACCEPT, ...){        /* 对新连接调用ip_nat_setup_info创建地址转换结构,返回NF_ACCEPT */}       static int foo_help(struct ip_conntrack *ct,                        struct ip_conntrack_expect *exp,                    struct ip_nat_info *info,                    enum ip_conntrack_info ctinfo,                    unsigned int hooknum,                    struct sk_buff  **pksb)/* called for every packet where conntrack detected an expectation-cause   params:      ct      struct ip_conntrack of the master connection                exp     struct ip_conntrack_expect of the expectation                        caused by the conntrack helper for this protocol                info    (STATE: related, new, established, ... )                hooknum HOOK the call comes from (POST_ROUTING, PRE_ROUTING)                pksb    packet buffer*/{        /* 协议分析并替换地址端口,并重新计算checksum和序列号 */}static struct ip_nat_helper hlpr; static int __init(void){        int ret;        memset(&hlpr, 0, sizeof(struct ip_nat_helper));        hlpr.list = { NULL, NULL };        hlpr.tuple.dst.protonum = IPPROTO_TCP;        hlpr.tuple.dst.u.tcp.port = htons(FOO_PORT);        hlpr.mask.dst.protonum = 0xFFFF;        hlpr.mask.dst.u.tcp.port = 0xFFFF;        hlpr.help = foo_help;        hlpr.expect = foo_nat_expect;        ret = ip_nat_helper_register(hlpr);        return ret;}static void __exit(void){        ip_nat_helper_unregister(&hlpr);}4.5、理解netfilter
netfilter很简单,前面的章节已经很详细的描述了它的结构。但是,有时需要超越地址转换和ip_tables 框所提供的便利,或者你想完全替换它们。
将来在netfilter中一个重要的问题是缓存。每个skb都有一个nfcache域:这是一个位掩码,表示包头的哪些部分被检查了,还有包是否被修改了。每个检查包的netfilter钩子都用一个位来标识自己是否检查过这个包,这样就可以写一个缓存系统来缓存那些不会被netfilter转发的包。
最重要的标记是NFC_ALTERED,表示这个包被修改了(这个标记在IPv4的NF_IP_LOCAL_OUT钩子中使用过,它会重新路由修改过的包),还有NFC_UNKNOWN,它表示不应该缓存这个包,因为这个包有些无法预测的项。如果有疑问,可以在你的钩子函数中设置NFC_UNKNOWN标记。
4.6、写一个新netfilter模块
4.6.1、挂接到Netfilter的钩子上
为了在内核里接收或处理网络包,你可以写一个模块注册一个netfilter钩子函数。实际钩子的位置与协议相关,并且在协议相关的netfilter头文件中定义,例如"netfilter_ipv4.h"。
为了注册和卸载netfilter钩子函数,你可以使用函数nf_register_hook和nf_unregister_hook函数。它们都由一个nf_hook_ops结构的参数,这个结构的域如下所示:
list
初始化为{ NULL,NULL }
hook
这个函数在包到的这个钩子时调用。你的函数必须返回NF_ACCEPT,NF_DROP或者NF_QUEUE。如果返回值是NF_ACCEPT,下一个在这个钩子点上的钩子函数会被调用。如果返回值是NF_DROP,网络包被丢弃。如果返回值是NF_QUEUE,这个包被放入队列。你有一个指向skb的指针,因此你可以对skb做任何事。
flush
当前没有被使用。它被设计为当cache被清空后,如果包通过这个钩子函数时调用。目前被置为NULL。
pf
协议族值。对IPv4协议来说是NF_INET。
hooknum
钩子点的位置。比如NF_IP_LOCAL_OUT。
4.6.2、如何处理放入队列的包
这个接口当前被ip_queue使用,你可以注册一个函数来处理给定协议的队列包。这和注册一个钩子函数类似,但是你可以阻塞当前的处理流程。当然,你只能看到那些返回NF_QUEUE的包。
有两个函数可以用来注册队列处理函数,一个是nf_register_queue_handler,一个是nf_unregister_ queue_handler()。你注册的函数有一个参数是void*。
如果没有注册任何函数来处理队列,NF_QUEUE返回值相当于NF_DROP。
一旦你注册了相应的处理函数,你就可以处理队列中的网络包。你可以做任何事,但是你在完成处理后必须调用nf_reinject函数。当你调用nf_reinject函数时,你传递给它一个skb,一个nf_info结构(这个结构由你的队列处理函数给出),和一个判断值:NF_DROP表示丢弃这些包,NF_ACCEPT表示这些包会继续被netfilter 处理,NF_QUEUE表示重新将这些包放入队列,NF_REPEART表示对这些包重新调用缓存它们的钩子函数(注意避免无限循环)
你可以检查nf_info结构来得到关于网络包的信息,比如接口和钩子点等。
4.6.3、用户空间命令
用户空间需要和netfilter模块交互。我们使用的是setsockopt机制。注意,每个新协议都需要自己修改setsockopt 的代码来调用nf_setsockopt(对getsockopt也是一样,需要修改getsockopt的代码),不过目前IPv4, IPv6和DECnet的已经修改了。
一个常用的方法是用nf_register_sockopt注册一个nf_sockopt_opt结构,这个结构有以下域:
list
初始化为{ NULL,NULL }
pf
协议族的编号,比如PF_INET。
set_optmin
set_optmax
描述setsockopt可以设置值的范围,如果是0,表示不设置任何值。
set
这个函数会在setsockopt中调用。你需要检查NET_ADMIN属性以确认用户是否有权利修改此参数
get_optmin
get_optmax
描述getsockopt可以取值的范围,如果是0,表示不能取任何值。
get
这个函数会在getsockopts中调用。你需要检查NET_ADMIN属性以确认用户是否有权利取这个值。
最后需要注意的两个问题
4.7、在用户空间处理包
你可以在ip_queue模块中使用libipq库,而且所有在内核中可以做的事你都可以在用户空间完成。这就意味着,除了速度的限制,你完全可以在用户空间开发你的代码。除非你需要过滤很大带宽的包,否则你会发现这种方法比在内核中处理包更方便。
在早期的netfilter中,我将一个试验性质的iptables移植到了用户空间。Netfilter对人们是开放的,他们可以用他们喜欢的语言去编写netfilter模块。
5、将2.0或2.2的过滤模块移植到2.4上
请参考ip_fw_compat.c文件,它封装了一个简单的层次,可以使得移植工作更简单。
6、      如何在netfilter里撰写隧道相关的应用
在2.4内核中编写隧道(或者封装)驱动,必须遵循以下两个规则(代码可以参考net/ipv4/ipip.c):
l  如果你要修改一个包(例如:封装或者去封装),你需要释放掉skb->nfct的引用计数。如果你是把修改后的包放入一个新的skb,你不需要释放旧包的skb->nfct;但是,如果不是创建一个新的包,你必须释放这个包的skb->nfct。否则,地址转换代码就会使用旧的连接跟踪信息来修改包,这样将导致错误的结果。
l  需要保证封装的包流过LOCAL_OUT钩子,同时解封装的包走过PRE_ROUTING钩子(大部分的隧道都使用ip_rcv,在这个函数里已经做了这些)。否则,用户就不能过滤那些流经隧道的包。
最经典的办法是在你的封装或解封装包的代码前插入如下的代码:
/* 这样会让netfilter认为这个包与前一个不同! */
#ifdef CONFIG_NETFILTER
nf_conntrack_put(skb->nfct);
skb->nfct = NULL;
#ifdef CONFIG_NETFILTER_DEBUG
skb->nf_debug = 0;
#endif
#endif
通常,你还需要做第二步,就是找到新封装的包会在哪里调用ip_send,然后用下面的代码替换它:
/* 新的包从本机发出 */
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, ip_send);
遵循这些规则意味着如果用户需要过滤一个流经隧道的包,他将会看到包是按如下的顺序流经协议栈的:
1.  FORWARD hook:正常的包(从eth0->eth1)
2.  LOCAL_OUT hook:封装的包(到eth1)
对应答包来说:
1.  LOCAL_IN hook:封装的应答包(来自eth1)
2.  FORWARD hoot:正常的应答包(从eth1->eth0)
7、      测试集
在CVS仓库里有一个测试集:测试集覆盖的范围越广,你就越有信心去修改代码而不用担心修改会导致代码不能工作。基本的测试和复杂的测试同样重要:复杂的测试都是由基本测试组成(就像你所知道的,在优化代码之前,先让它能跑起来)。
测试都很简单:它们都是些shell脚本,位于testsuite/目录下。测试脚本按字母顺序运行,因此 '01test'在'02test'之前运行。目前有五个测试目录:
00netfilter/
基本的netfilter框架测试
01iptables/
iptables测试
02conntrack/
连接跟踪测试
03NAT/
地址转换测试
04ipchains-compat/
ipchians/ipfwadm的兼容性测试
在testsuite/目录下,有个test.sh脚本。它会配置两个虚拟接口(tap0和tap1),并且打开转发,删除所有的netfilter模块。然后,它会调用上面五个目录中的test.sh脚本,直到测试结束或者测试出错。这个测试脚本有两个参数:"-v"表示打印测试过程中的信息;另一个是测试的名称,如果给定测试的名称,那么只有相关目录下的测试脚本会被调用。
编写一个测试
在相应的目录下创建一个新的测试脚本,测试脚本的名称需要正确命名,这样可以保证它会按正确的顺序被调用。例如,如果要测试ICMP应答的连接跟踪(02conntrack/02reply.sh),我们需要首先保证 ICMP请求的连接跟踪正确(02conntrack/01simple.sh)。
最好是创建一些小文件,每个测试一个项目,这样有利于在运行测试集时分离和定位错误。
如果测试出错,请调用"exit 1",它会返回一个错误值,如果你的测试出错,你应该打印出错误信息。如果你的测试完全正确,你需要在最后调用"exit 0"来返回。你需要检查每一条命令的返回值,你可以在脚本开头调用'set -e'或者在每个命令后面附加"|| exit 1"字串。
load_module和remove_module两个函数可以帮你加载和卸载模块。你不应该依赖于系统去自动加载模块,除非你是在测试这个功能。
运行环境和环境变量
你可以使用两个接口:tap0和tap1。它们的地址分别用变量$TAP0和$TAP1表示。它们的掩码都是255.255.255.0,而它们所在的网络分别用$TAP0NET和STAP1NET表示。
测试中会创建一个临时文件$TMPFILE,它会在测试结束后删除
你的测试脚本放在testsuite/目录下,你需要在这个目录下运行它们。因此,如果你要访问其他命令(比如iptables),你需要在命令前加"../userspace"路径。
如果设置了$VERBOSE值,你的脚本可以打印更多信息(这意味着用户在命令行上使用了-v参数)
有用工具
在tools目录下有一些有用的工具,在发生错误的情况下,它们返回非零值。
7.3.1、gen_ip
你可以使用gen_ip生成IP数据包,并在标准输出上打印出来。你可以通过将标准输出定向到 /dev/tap0和/dev/tap1来模拟向这个两个设备发包(这两个设备在测试集开始运行时创建)
gen_ip是一个简单的工具,但它的命令行参数却非常复杂,让我们先来看看它的命令行参数:
FRAG=offset,length
创建一个包,然后取它的offset处,length长度的分片。
MF
设置包的"More Fragment"标志
MAC=xx:xx:xx:xx:xx:xx
设置包的源MAC
TOS=tos
设置包的tos域(0到255)
下面是一些必选项
source ip
包的源地址
dest ip
包的目的地址
length
包的总长度,包括包头的长度
protocol
包的协议号,比如17=UDP
然后是一些与协议相关的选项:对UDP(17)来说,它们是源端口和目的端口。对ICMP(1)来说,它们是 ICMP的类型和号码。如果类型是0或8(ping应答或请求),还有两个必须要填的项(ID和序列号)。对TCP来说,它们是源端口,目的端口,还有标记("SYN", "SYN/ACK", "ACK", "RST" or "FIN"),这是必选项。还有三个可选项:"OPT="后面是逗号分隔的选项值,"SYN="后面是序列号,"ACK="后面是ACK的序列号。最后,可选项DATA后面是TCP包的内容。
7.3.2、rcv_ip
你可以使用rcv_ip来接收并打印网络包,它打印的格式与gen_ip的输入格式相同(除分片包外)。
这对分析网络包很有用处。它有两个必选的参数:
wait time
在标准输入上等待一个包的最大时间
iterations
连续接收包的数量
它有一个可选参数"DATA"。如果有这个参数rcv_ip会将包的内容打印到标准输出上
标准的在shell脚本中使用rcv_ip的方式如下:
# Set up job control, so we can use & in shell scripts.set -m# Wait two seconds for one packet from tap0../tools/rcv_ip 2 1 < /dev/tap0 > $TMPFILE &# Make sure that rcv_ip has started running.sleep 1# Send a ping packet../tools/gen_ip $TAP1NET.2 $TAP0NET.2 100 1 8 0 55 57 > /dev/tap1 || exit 1# Wait for rcv_ip,if wait %../tools/rcv_ip; then :else    echo rcv_ip failed:    cat $TMPFILE    exit 1fi7.3.3、gen_err
这个程序从标准输入上读入一个包(可以用gen_ip生成),并根据它生成一个ICMP错误应答
它有三个参数:源地址,类型和号码。ICMP包的目的地址将设置为从标准输入上读入包的源地址。
7.3.4、local_ip
这个函数从标准输入读入一个包并通过原始socket发送到内核中。这样,这个包看起来就像是从本地发出的包。(与之对应的是,如果向ethertap设备写入包,这个包看起来就像是从远端发出的包)
几点建议
所有的工具都假设它们都只需读写一次。这对ethercap设备是正确的,但是如果你使用了管道,情况可能就不是这样了。
dd用来剪切包。dd有一个obs(块输出)选项,它可以在一次写动作里将整个包输出到标准输出上。
先测试能够成功的情况。比如,如果需要测试包是否被禁止。首先要测试包是否能够正常通过, 然后测试包是否被禁止。否则,一些异常错误可能导致包不能通过。
写一些精确的测试,而不是“随便试试,看有什么情况发生"的测试。如果一个精确的测试出现错误,错误原因可以定位。但如果是一个随意的测试,错误现象就不能重现,这样对定位错误没有任何帮助。
如果一个命令出错,但是没有输出任何信息。你可以在脚本开头加入"-x"选项(例如:'#!/bin/sh -x'),这样就能看到是哪一条命令出错了。
如果测试错误随机出现,就需要检查那些随机的网络流量(试着将所有的外部接口停掉)。例如,我有一次和Andrew Tridgell在同一网络,就被网络上的Windows广播包搞得很恼火。
8、  动机
在我开发ipchains的时候,我意识到包过滤的位置可能有问题。我给Alan cox发邮件告诉他这个问题,他的回答是"为什么不先完成目前的代码,或许它是正确的"。这样,实用主义占了上风,ipchains还是按原来的思路开发了出来。
在我完成ipchains后,对ipfwadm的改动就非常少了。主要的工作集中在撰写手册和帮助。在这个过程中,我发现在LINUX社区中,诸如包过滤、地址伪装、端口转发等概念非常混乱,这样导致使用这些功能变得非常困难。
在给这个工具做技术支持的过程中,我不断了解到用户在尝试做什么,他们的困难是什么。自由软件对用户有用处,他们才会使用它(不是吗?)。这就意味着,你需要写简单的软件。ipchains的错误不是在文档上,而是在系统的架构上。
有了ipchains的经验,我了解到了用户需要什么样的包过滤工具。但是,这里有两个问题:
首先,我不想再重回安全领域。做为一个安全顾问,你就像是在你的良心和钱包之间拔河一样。基本上,你卖的是你对安全的感觉,但它与真正的安全有一定的差距。也许在一个军事基地工作,和那些真正懂安全的人一起工作,安全咨询才有用武之地。
第二个问题是我不能只关注那些初级用户。越来越多的大公司和ISP都在什么这个工具。我必须使这个工具足够可靠,而能够在更复杂的环境中使用。
这些问题在我1998年7月在Usenix上碰到WatchGuard的名人David Bonn时得到了解决。他们需要一个内核的编程人员。最终,他们同意我去他们西雅图的办公室一个月并谈谈如何定一个合同来资助我的新代码和我目前正在支持的代码。他们给的比我要求的多,报酬也没有打折。这就意味着,在一段时间里,我不需要考虑做咨询工作来糊口了。
WatchGuard给了我接触他们的大客户的机会,并允许我平等的支持所有的用户,即使有些用户是他们的竞争对手。
接着,我开始写netfilter并把ipchains移植过去,目前这项工作已经完成了。不幸的是,内核中所有的地址伪装代码都被删除了:把地址伪装代码独立出来胜于把整个过滤代码删除掉。但是地址伪装还是需要在netfilter框架上重新实现。
同样,根据我在实现ipfwadm的"设备地址"(我把它从ipchains中删掉了)时的经验,如果仅仅把地址伪装代码独立出来是不会有人帮我把它移植到netfilter框架上的,即使他们需要这个功能。
因此,我需要在当前代码上实现尽可能多的功能,至少要比2.2提供的功能多,这样就可以鼓励新用户使用它。这意味这替换透明代理,地址伪装和端口转发,换句话说,就是实现一个完整的地址转换层。
即使我要把目前的地址伪装层移植过来而不是写一个新的、通用的地址转换系统,地址伪装的代码也太旧了,并且缺少维护。目前已没有人支持这段代码了。这也显示出,正式的用户没有使用地址伪装,而家庭用户也很少使用它。虽然Juan Ciarlante做了一些修正,但是这段代码已经走到了尽头,现在需要重新写一个来替换它。
请注意,我并不是最适合重写地址转换的那个人。我没有使用过地址伪装,而且当时也没有时间研究这段代码。这也许是它占用了我更长时间的原因。但重写的结果令人满意,从我的角度来书,我学到了很多。毫无疑问,一旦我们获取了更多用户如何使用这些工具的信息,我们就会写出来一个更好的版本来替换当前的版本。
9、  致谢
感谢那些帮助过我的人,特别是Harald Welte,它撰写了协议helper一节。