一、机制
该工具相当于内核中的车匪路霸,它们在内核网络栈中各处安装关卡,对内核中报文的流动进行监控管理,它是Linux系统下实现防火墙的基础,利用用户态的iptables的实现就是netfilter机制的一个典型应用。
该机制直接嵌入内核,在内核生成的时候这些监测点就已经被编译入内核,所以是顽固而可靠的检测机制。
二、实现
1、数据结构
这是一个三维的实现模型,第一维可以认为是不同的网络协议簇,第二维是协议簇中事件(对应一些检测位置),第三维就是这个链表中可以挂载的所有检测函数,数组nf_hooks的每个元素只是一个链表头位置,它下面可以挂接任意多的检测函数,从而形成一个动态挂载任意多的检测。
该机制的核心明星就是这个定义于linux-2.6.21
et
etfiltercore.c中的
extern struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
数组,可以看到它的每个元素只是一个队列头,这个结构将会引导某个事件的所有挂接函数。这些函数只管注册自己就好,至于什么时候被调用,那就是内核来决定了,总之少不了你的。通俗的说,这些就是一些回调函数(callback)函数,Don't call me I will call you。
其实我觉得这个功能的实现还算是非常的简洁明快的,所有的核心功能都是在一个文件中实现,并且文件并不大,逻辑看起来也是非常容易理解,所以作为软件开发应该是属于教科书式的好代码,大家有机会应该围观学习一下。
该数组中的每一个队列中挂接的是一个
struct nf_hook_ops
{
struct list_head list;该成员将会把这个结构添加在nf_hooks数组引导的队列头中。
/* User fills in from here down. */
nf_hookfn *hook;
struct module *owner;可以使用动态加载的内核模块
int pf;
int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;这里不是一个先来先服务队列,而是一个优先级队列。
};
结构,其中最为重要的就是那个nf_hookfn函数,它相当于一个钩子的面试官角色,它的返回值将会决定一个报文是否通过这一关的检测。
2、注册
归根到底,一个钩子函数的注册是通过int nf_register_hook(struct nf_hook_ops *reg)函数来完成了,当然这里既然接收零售,就也接收批发,所以还简单实现了一个封装函数int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n),这样不同的模块就可以通过这个接口来一次性注册多个接口。这一点其实还是很有意义的,因为大部分功能模块都会定义一组钩子函数,而这组钩子函数又都是定义在一个数组中的,所以有了这个接口,腰不酸了,腿不疼了,注册也更简单了。例如在linux-2.6.21
etridger_netfilter.c中注册的br_nf_ops数组,其中就有大量的元素,并且该数组声明为static struct nf_hook_ops br_nf_ops[] 形式,这样即使之后在该数组中添加一个新的项目,除了添加本身的代码,哪里都不需要修改。
其实分层就是这样,尽量把重复的代码放在合适的层次,从而减少代码冗余。如果说这里不提供这个简单接口,那么所有的批量注册者都需要自己写一个循环函数来注册自己的一批数组,这无疑是一个代码量和维护量的负担。
而注册的操作也非常简单,事实上我感觉很久没有遇到这么直白的代码了。就是首先遍历挂接点上的链表,然后比较优先级,如果找到自己合适的位置,自己就在这里落脚。
list_for_each(i, &nf_hooks[reg->pf][reg->hooknum]) {
if (reg->priority < ((struct nf_hook_ops *)i)->priority),从这里的比较可以看到,数值越小,优先级越高,并且优先级可以为负数。
break;
}
list_add_rcu(®->list, i->prev);将新注册的钩子函数归位。
3、调用
内核的各个模块需要自己维护自己可能触发检测点的时候进行调用。也就是内核的某个模块在某个流程中会知道此事是一个可能的netfilter检测点,所以它就通过NF_HOOK宏来调用可能的回调。该宏定义为
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn)
NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN)
可以看到,它的前两参数和nf_hooks数组的前两维下标相对应,所以可以直接找到对应的链表头。之后是触发此事检测的一个报文(可以认为是一个面试者),然后是输入网口和输出网口,接下来是如果通过了这个检测将会执行的函数,最后是最低优先级,只有优先级在这个值之上的才可以对该报文进行检测。
如果是能了netfilter机制,那么会执行到nf_hook_thresh--->>>nf_hook_slow--->>>nf_iterate
list_for_each_continue_rcu(*i, head) {
struct nf_hook_ops *elem = (struct nf_hook_ops *)*i;
if (hook_thresh > elem->priority)
continue;
/* Optimization: we don't need to hold module
reference here, since function can't sleep. --RR */
verdict = elem->hook(hook, skb, indev, outdev, okfn);
if (verdict != NF_ACCEPT) {
#ifdef CONFIG_NETFILTER_DEBUG
……
#endif
if (verdict != NF_REPEAT)
return verdict; 如果钩子返回值不是NF_ACCEPT,也不是NF_REPEAT,则直接返回判定值。反过来说,如果是这两个值的话,还要继续迭代,这个链表上的所有串行检测点都必须通过。
*i = (*i)->prev;
}
}
return NF_ACCEPT;这里也说明,如果挂接点上没有任何函数,则直接放行。
当返回到nf_hook_slow函数中之后,
verdict = nf_iterate(&nf_hooks[pf][hook], pskb, hook, indev,
outdev, &elem, okfn, hook_thresh);
if (verdict == NF_ACCEPT || verdict == NF_STOP) {NF_STOP也可以导致一个报文被接收,这就是某些钩子在中间直接放行了报文,之后的钩子将没有机会进行检测。
ret = 1;
goto unlock;
} else if (verdict == NF_DROP) {这里很不幸,报文被丢弃了。
kfree_skb(*pskb);
ret = -EPERM;
} else if ((verdict & NF_VERDICT_MASK) == NF_QUEUE) {这里比较少见,大致看了一下,好像是实现用户态防火墙的一种方法。可能使用之前说过的netlink机制将这个报文发送到用户态的侦听套接口,从而让用户态程序对这个报文进行全面检测,如果检测通过,再发送到内核的netlink套接口中,对应的netlink协议簇为NETLINK_FIREWALL。
NFDEBUG("nf_hook: Verdict = QUEUE.
");
if (!nf_queue(*pskb, elem, pf, hook, indev, outdev, okfn,
verdict >> NF_VERDICT_BITS))
goto next_hook;
}
三、结合用户态iptables实现说明
下面一张图片来自《The Linux? Networking Architecture: Design and Implementation of Network Protocols in the Linux Kernel》中第19.3节中的一张图片
可见其中主要有五个监测点。其中两个ROUTING是分别是即将进出网卡的监测点,对应的两个LOCAL是即将进出本机进程的监测点,而FORWARD则是是否放行过往报文的监测点。我觉得我这个总结还是比较言简意赅的,大家可以搜索一下内核中哪些地方执行通过NF_HOOK调用了这些监测点。
1、iptables
这个是一个用户态的命令行工具,它可能就充当了Linux下的防火墙工具,和大部分linux工具一样,如果你不明白原理就没法用。我一直对其中的-t选项很感兴趣,所以就特地看了一下这个东西。在用户态工具iptables源代码中,其处理函数位于iptables-1.4.9libiptclibiptc.c中的TC_INIT(const char *tablename)函数中,其中主要进行了两个查询操作,都是通过getsockopt系统调用,第一个是IPT_SO_GET_INFO,第二个是IPT_SO_GET_ENTRIES选项,它们分别获得指定名称的内核转换表的模块以及这些模块中的数据项。而内核中的这些项的一些内置的项目可以在内核中找到,例如ip_nat_rule_init--->>>ret = ipt_register_table(&nat_table, &nat_initial_table.repl);注册的,当然还可以找到一些ret = xt_register_target(&ipt_snat_reg);函数之类的。
在iptable_filter.c中注册了两个函数,
/* Entry 1 is the FORWARD hook */
initial_table.entries[1].target.verdict = -forward - 1;
/* Register table */
ret = ipt_register_table(&packet_filter, &initial_table.repl);这个是ipt可以感知的结构,也就是iptable使用的数据结构。
if (ret < 0)
return ret;
/* Register hooks */
ret = nf_register_hooks(ipt_ops, ARRAY_SIZE(ipt_ops));这个是netfilter使用的数据结构,我们知道,iptables是在netfilter的基础上实现的,所以它必须转换为netfilter可以识别的形式,也就是那个最为原始的数组链表中,当注册了这些函数之后,就可以在函数调用的时候再使用上面的ipt结构中注册的数据结构。
static unsigned int
ipt_hook(unsigned int hook,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
return ipt_do_table(pskb, hook, in, out, &packet_filter);
}
可以认为,ipt_ops中注册的钩子就是专门为这个iptables项服务的一组钩子,这组钩子只识别该文件中定义的packet_filter结构,而用户态的iptables程序则可以通过进一步的在这个filter的后面追加新的钩子。就像阿拉灯给你三个愿望,只要你保证最后一个愿望是再来三个愿望的话,你就可以得到无数的愿望。这里就是,你只要得到了一次检测机会,你就可以在这个检测机会中安装任意多的新机会,这也是公务员队伍膨胀的原因。
2、int ipt_register_table(struct xt_table *table, const struct ipt_replace *repl)的第二个参数
static struct
{
struct ipt_replace repl;
struct ipt_standard entries[3];
struct ipt_error term;
} initial_table __initdata
= { { "filter", FILTER_VALID_HOOKS, 4,总共四项
sizeof(struct ipt_standard) * 3 + sizeof(struct ipt_error),该表附带项的总大小,从结构ipt_replace的结束开始
{ [NF_IP_LOCAL_IN] = 0,
[NF_IP_FORWARD] = sizeof(struct ipt_standard),
[NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard) * 2 },此处对hook_entry[NF_IP_NUMHOOKS]成员初始化,数组值指向ipt_replace结构结束后偏移量
{ [NF_IP_LOCAL_IN] = 0,
[NF_IP_FORWARD] = sizeof(struct ipt_standard),
[NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard) * 2 },underflow[NF_IP_NUMHOOKS]数组赋值,意义同上。
0, NULL, { } },num_counters和counters清零。ipt_entry entries[0]清零,最后的两项应该是一些简单的记录向,也就是某些匹配规则满足的报文的统计信息。
3、用户态接口
通过strace iptables -A OUTPUT -j ACCEPT可以看到,它只是执行了四个sockopt系统调用,分别为
getsockopt(3, SOL_IP, 0x40 /* IP_??? */, "filter 342250376B30035436221136620036334301h>J342L377B300300214211366"..., [84]) = 0
getsockopt(3, SOL_IP, 0x41 /* IP_??? */, "filter "..., [952]) = 0
setsockopt(3, SOL_IP, 0x40 /* IP_??? */, "filter "..., 1156) = 0
setsockopt(3, SOL_IP, 0x41 /* IP_??? */, "filter "..., 148) = 0
其中两个系统调用不同,分别对应
#define IPT_SO_SET_REPLACE (IPT_BASE_CTL)
#define IPT_SO_SET_ADD_COUNTERS (IPT_BASE_CTL + 1)
#define IPT_SO_SET_MAX IPT_SO_SET_ADD_COUNTERS
#define IPT_SO_GET_INFO (IPT_BASE_CTL)
#define IPT_SO_GET_ENTRIES (IPT_BASE_CTL + 1)
#define IPT_SO_GET_REVISION_MATCH (IPT_BASE_CTL + 2)
#define IPT_SO_GET_REVISION_TARGET (IPT_BASE_CTL + 3)
#define IPT_SO_GET_MAX IPT_SO_GET_REVISION_TARGET
也就是用户态只有获取/替换接口,而没有追加功能,那么用户态执行追加的时候是如何实现的呢?我想应该是首先通过GET获得系统中所有的配置表,然后在通过replace整体替换。这里就有一个文件,多线程操作这个表的时候会不会出现覆盖的问题,也有可能我理解错误?
4、用户态对报文流向的控制
这里大致有两种方法,一种是标准的方法,就是在-j中指明是ACCEPT、DROP之类的动作,这是一种简单粗暴的做法,当然一般也是最有效的做法。另个是可以在-j中指定另一个的iptables表,例如nat 控制表、LOG表等。那么这里就有一个问题,内核和用户态是如何区分这个jump的目标是一个verdict还是另一个跳转表呢?
这个还好应该比较简单。do_replace--->>>translate_table-->>>find_check_entry--->>find_check_match--->>>xt_find_match
在每个struct ipt_entry结构的最后,可以追加若干个struct xt_entry_match项,但是xt_entry_target和这个结构又非常像,估计可以通用。
定义的宏IPT_MATCH_ITERATE可以遍历ipt_entry之后可能存在的xt_entry_match项。如果说对于ACCEPT之类的内置命令,它们定义于linux-2.6.21 etipv4 etfilterip_tables.c
/* The built-in targets: standard (NULL) and error. */
static struct xt_target ipt_standard_target = {
.name = IPT_STANDARD_TARGET,
……
这个结构的特征就是它没有实现xt_target结构中的(*target)函数结构,这一点将会对之后的判断产生影响。
对于更多的匹配信息,都是通过
struct ipt_entry
{
struct ipt_ip ip;这个结构来匹配的,所有每个ipt_entry都可以有自己的匹配选项。这里的ipt_ip结构包含了所有iptables可以选择的匹配选项,并且在ipt_do_table中通过(ip_packet_match(ip, indev, outdev, &e->ip, offset))函数来完来往报文的匹配。
}
struct ipt_ip {
/* Source and destination IP addr */
struct in_addr src, dst;这两个参数通过-s -d 设置
/* Mask for src and dest IP addr */
struct in_addr smsk, dmsk;
char iniface[IFNAMSIZ], outiface[IFNAMSIZ];通过-i -o设置
unsigned char iniface_mask[IFNAMSIZ], outiface_mask[IFNAMSIZ];
/* Protocol, 0 = ANY */
u_int16_t proto;-p设置
/* Flags word */
u_int8_t flags;
/* Inverse flags */
u_int8_t invflags;!设置
};
5、检测的实施ipt_do_table
t = ipt_get_target(e);
IP_NF_ASSERT(t->u.kernel.target);
/* Standard target? */
if (!t->u.kernel.target->target) {这里由于刚才说的那个标准xt_target没有实现target函数接口,所以走入这个分支。而用户传入的verdict是一个可能的操作动作的负值并减去一。例如iptables -j DROP,则此处的值为-6(其中#define NF_STOP 5)。这个应该也是一个命名空间的划分,正如信号处理函数的SIG_IGN定义为-1一样。
int v;
v = ((struct ipt_standard_target *)t)->verdict;
if (v < 0) {
/* Pop from stack? */
if (v != IPT_RETURN) {
verdict = (unsigned)(-v) - 1;这里进行反向转换出用户态设置的裁定结果。
break;
}
e = back;
back = get_entry(table_base,
back->comefrom);
continue;
}
……
} else {
verdict = t->u.kernel.target->target(pskb, in, out,hook, t->u.kernel.target, t->data);如果是一个确实存在的目标,则执行对应目标的target接口。例如ipt_LOG.c中实现的ipt_log_target接口。
用户态将会在iptcc_map_target--->>>iptcc_standard_map中将这些标准的操作映射为上面所说的verdict数值。
这里有一个有意思的细节,就是REJECT目标并不是直接映射为NF_DROP目标,事实上内核中没有定义NF_REJECT数值,这个是通过一个target有定义的变量来实现,没错,就是在linux-2.6.21 etipv4 etfilteript_REJECT.c中实现的ipt_reject_reg..target = reject,
其代码为
switch (reject->with) {
case IPT_ICMP_NET_UNREACHABLE:
send_unreach(*pskb, ICMP_NET_UNREACH);
break;
case IPT_ICMP_HOST_UNREACHABLE:
send_unreach(*pskb, ICMP_HOST_UNREACH);
break;
case IPT_ICMP_PROT_UNREACHABLE:
send_unreach(*pskb, ICMP_PROT_UNREACH);
break;
case IPT_ICMP_PORT_UNREACHABLE:
send_unreach(*pskb, ICMP_PORT_UNREACH);
break;
case IPT_ICMP_NET_PROHIBITED:
send_unreach(*pskb, ICMP_NET_ANO);
break;
case IPT_ICMP_HOST_PROHIBITED:
send_unreach(*pskb, ICMP_HOST_ANO);
break;
case IPT_ICMP_ADMIN_PROHIBITED:
send_unreach(*pskb, ICMP_PKT_FILTERED);
break;
case IPT_TCP_RESET:
send_reset(*pskb, hooknum);
case IPT_ICMP_ECHOREPLY:
/* Doesn't happen. */
break;
}
return NF_DROP;
}
可以看到,里面有大量的icmp_send消息,这也就是在Fedora Core发行版本中经常出现主机不可达信息的来源,也就是因为用户态设置的防火墙向对方发送了虚假消息,就是主机不可达。不过这里也算是比较友好,毕竟避免了对方无谓的等待。
四、一个iptables使用说明
至于为什么防火墙叫iptables,可能之前有一个叫ipchains的防火墙工具,所以这里的tables和chain对应,就像关系数据区和网状数据库之间的区别吧。
用户态使用规则说明
http://www.frozentux.net/iptables-tutorial/cn/iptables-tutorial-cn-1.1.19.html