在上一篇文章中,以 ip_masq_ftp 为例分析了 ip_masq_app 的实现。在这篇文章里,将以 ip_masq_portfw 为例分析 ip_masq_mod 的实现。
1. 概述
Linux 2.2.x 中实现了地址伪装。它在发出包时修改包的源地址和源端口 (OUTBOUND 方向,由 ip_fw_masquerade 实现),并在收到应答包时修改其目的地址和目的端口 (INBOUND 方向,由 ip_fw_demasquerade 实现)。它还可以在正常的流程中加入一些检查点,并在这些检查点上调用特定的函数去实现一些扩展的功能。在上一篇文章中,以 ip_masq_ftp 为例分析了 ip_masq_app 的实现。在这篇文章里,将以 ip_masq_portfw 为例分析 ip_masq_mod 的实现。(ip_masq_app 与 ip_masq_mod 的主要区别在于 ip_masq_app 检查应用协议中的动态地址或端口,而 ip_masq_mod 检查 IP 和 TCP 头中的地址或端口)
先来看看检查点的位置,如图:
图中 ip_fw_masquerade 中的检查点 ip_masq_mod_out_rule 在内核代码中并没有使用,画在这里是为了让图看起来对称一点 :-);ip_masq_mod_in_rule 根据规则检查本机收到的包,这个包有可能是地址伪装的应答包,或是需要做目的转换的包,或者是发往本机上层的包,检查的结果决定是否要继续后面的动作;ip_masq_mod_out_create 和 ip_masq_mod_in_create 分别根据包所匹配的规则创建相应的地址伪装结构;ip_masq_mod_out_update 和 ip_masq_mod_in_update 则用新的参数更新已创建的地址伪装结构;ip_masq_app_pkt_out 和 ip_masq_app_pkt_in 是 ip_masq_app 模块的两个检查点,上一篇文章中讲到的 masq_ftp_out 和 masq_ftp_in 两个函数就分别在这两个检查点上被调用。(判断一个包是在 OUTBOUND 方向还是在 INBOUND 方向可以看这个包是转发的包还是发往本机的包。需要转发的包是在 OUTBOUND 方向上,本机收到的包是在 INBOUND 方向上) 实现一个 ip_masq_mod 的模块,先要定义一个 ip_masq_mod 的结构,以 ip_masq_portfw 为例,它定义了如下的结构:
static struct ip_masq_mod portfw_mod = { NULL,/* next */ NULL,/* next_reg */ "portfw",/* name */ ATOMIC_INIT(0),/* nent */ ATOMIC_INIT(0),/* refcnt */ proc_ent, portfw_ctl, NULL,/* masq_mod_init */ NULL,/* masq_mod_done */ portfw_in_rule, portfw_in_update, portfw_in_create, portfw_out_rule, portfw_out_update, portfw_out_create, };
(在这个结构中用粗体标记的函数没有实现)。可以看到,结构定义了在检查点上调用的函数和描述结构信息的一些参数。结构定义之后需要注册到一个全局的 ip_masq_mod_reg_base 链表之中(指针 next_reg 用于指向这个链表上的下一个结构),这个表上有所有注册的 ip_masq_mod 结构。同时还有一个链表 ip_masq_mod_lkp_base(指针 next 用于指向这个链表上的下一个结构),一般会在添加模块规则时将结构同时注册到这个链表中,如果没有模块规则,结构不会在这个链表上(在检查点上查找相应结构时就引用这个链表,这样做的目的是为了减少不必要的遍历)。结构中的 proc_ent 项代表 proc 文件系统中的一个文件,它实现了用户空间读模块规则的接口。
2. IP_MASQ_MOD
ip_masq_portfw 实现了目的地址转换。用下图所示的环境分析它在检查点上定义的函数:
图中 A、C 是内网主机,B 是外网主机。在 FW 配置规则如下:
ipmasqadm portfw -a -L 192.168.0.88
80
-R
10.0.0.1 80 -p 2
ipmasqadm portfw -a -L 192.168.0.88
80
-R
10.0.0.2 80 -p 1 ipchains -A forward -s 10.0.0.0/8 -j MASQ
前两条规则是 portfw 的规则,它把对 FW 外网地址的端口为 80(一般是 HTTP 服务)的访问映射到内网的监听端口为 80的服务器上。它们映射相同的服务,以轮询的方式去选择真正转换后的地址。第三条规则是地址伪装的规则,加这条规则的目的是为了使服务器返回的包可以走到 ip_fw_masquerade 函数中,进而恢复原来的目的地址。 下面从模块初始化,模块释放,portfw 的规则,和 portfw 定义的方法四个方面分析 ip_masq_portfw 的实现。
2.1. 初始化
模块初始化的流程从 init_module 开始(如果将 ip_masq_portfw 编译成内核模块)。它调用 ip_portfw_init,在这个函数里初始化了两个规则链表,一个保存 TCP 协议的规则,一个保存 UDP 协议的规则。然后在这个函数里将前面定义的结构 portfw_mod 注册到系统的链表中。
2.2. 释放
模块释放的流程从 cleanup_module 开始。它调用 ip_portfw_done,在这个函数里将结构 portfw_mod 从系统链表中取下(取下时要判断结构的引用计数是否等于 0,等于 0 才可取下)。
2.3. 规则
规则分两部分讨论,一是添加、删除规则的接口,二是读规则的接口。 规则匹配收到包的协议,目的地址,目的端口,其结构如下:
struct ip_portfw { struct list_head list; __u32
laddr, raddr; __u16
lport, rport; atomic_tpref_cnt;/* pref "counter" down to 0 */ int pref;/* user set pref */ };
在这个结构中,list 用于双向链表的前后指针;laddr, lport 是 FW 的地址和端口(结构中没有定义协议是因为不同协议的规则保存在不同的链表中);pref_cnt 是一个优先值的计数,它的初值是 pref(在添加规则时指定),以后每次匹配到这个规则,都将这个值减 1,如果减为 0,将这条规则移到链表的末尾,并重置这个值为 pref,用这样的方法,实现了一个简单的轮询算法(代码在 portfw_in_create 中)。
2.3.1. 添加、删除规则
规则的添加和删除是通过用户空间的配置工具完成,它调用系统调用去作用于规则表,调用过程如下:
从图中可以看到,作用于规则表的函数是由 setsockopt 函数以 level 为 SOL_IP 调用的(level 想当于协议栈中的层,地址伪装在 IP 层上实现,所以用户接口也是以 SOL_IP 作为 level)。portfw_ctl 是一个分派函数,根据用户空间传来的不同的命令调用不同的函数如 ip_portfw_add 用于添加规则(地址、端口相同的规则,只更新其优先值),ip_portfw_del 用于删除规则。读写规则链表都需要得到锁 portfw_lock,然后才能进行读写动作。
2.3.2. 读规则
读规则是通过 proc 文件系统的接口完成的。用户空间的程序通过读文件 /proc/net/ip_masq/portfw 的内容得到当前系统中配置的 portfw 的规则。在 portfw_mod 结构中有指向 proc_dir_entry 数据结构的指针 proc_ent,在这个结构中定义了读 proc 文件的接口 portfw_procinfo。proc_dir_entry 的注册是在注册 portfw_mode 结构时完成的。一般在 ip_masq_mo 的实现中都会定义这个函数。
2.4. 方法
ip_masq_portfw 只实现了两个在检查点上调用的函数。
2.4.1. portfw_in_rule
portfw_in_rule 对 FW 收到的包(其目的地址是 FW 的地址)进行匹配。如果这个包需要做目的地址转换(匹配到了 portfw 规则表中的规则;或者其目的端口是在地址伪装端口的范围之内 [61000-65096];或是目的端口是指定的伪装端口(mport_count 不为 0)),则继续执行后面的过程;如果不需要做目的地址转换,则直接返回。这一步的检查避免对每个收到的包都去查伪装结构的表。
2.4.2. portfw_in_create
在这个函数里创建对应目的转换的伪装结构。首先,根据收到包的协议引用不同的规则表。然后用收到包的目的地址和目的端口匹配 portfw 规则。如果找到,创建地址伪装结构(这个结构将会在服务器应答包的源转换时被引用,同一连接的后续包也将引用这个结构,直到这个结构因连接超时被删除)。接下来,它减小规则的 pref_cnt, 并且如果 pref_cnt 为零,将规则移至链表末尾,并冲置 pref_cnt 的值。
3. 总结
实现一个 ip_masq_mod 的要点是定义 ip_masq_mod 的结构,并实现它在检查点上调用的函数。其中区别在于规则的组织以及创建地址伪装结构的条件,这可以根据应用的不同选择不同的实现方式。
关于作者
朱小平,现从事网络安全的开发,比较感兴趣的方向是操作系统和网络协议栈的实现。写文章,只是为整理思路,发现问题。邮件地址为 droplet@163.net ,如有相同兴趣,可以共同进步。