一、linux下的防火墙基础iptables
这个功能在之前的说法叫做ipchains,后来才修改为这个名字。名字本身通常会体现出很多的信息,这些信息可能根本不需要专业知识,只是因为这些术语本身不是我们的母语,加上翻译中信息的缺失和转义等问题,导致很多术语本身的意义并没有体现出来。拿我们最为熟悉的程序设计语言来说,如果是以英语为母语,那么这些所谓的编程事实上是和我们平时说话差不多的,只是更加的官方化和形式化。这对于早期的PASCAL语言更是如此,基本的句子就是模仿英语的自然语言,C语言有些相对简化,但是也还是以英语为基础,例如for可以翻译为“对于”,而脚本语言中常见的foreach则可以跟进一步的翻译为“对于每一个”,大家可以想象在这样的环境下编程是一个什么样的感觉。
而对于从ipchains到iptables的转换,本身通过两个单词也透露出了很多的信息。所谓的“chain”是一个链结构,或者说是我们常见的有向图结构,只是更为简单,可以认为是一个常见的有向树结构;而一个table则是一个正则的二维结构。在“编译原理”中,开始就讲了词法分析使用的自动机模型,其中最开始的结构就是一个有向图,然后转换为一个二维的矩阵,事实上在工程中,大量的有向图也的确是通过二维数字来表示的,例如我们熟知的floyd算法,dijstras算法等。
如果是一个二维结构,那么可以认为它的第一维就是不同的功能表,例如用于做NAT转换的nat表,用于做修改的mangle表,防火墙的netfilter表等。根据linux内核一个报文经过的节点来说可以分为5个大家比较熟悉的报文拦截点,也就是
/* IP Hooks */
/* After promisc drops, checksum checks. */
#define NF_IP_PRE_ROUTING 0
/* If the packet is destined for this box. */
#define NF_IP_LOCAL_IN 1
/* If the packet is destined for another interface. */
#define NF_IP_FORWARD 2
/* Packets coming from a local process. */
#define NF_IP_LOCAL_OUT 3
/* Packets about to hit the wire. */
#define NF_IP_POST_ROUTING 4
#define NF_IP_NUMHOOKS 5
这就是iptables中二维表的开始。通常一个表会在上面五个拦截点的多个地方都设置拦截,可以想象的就是对于一个NAT转化来说,正向和反向的转换都是要设置拦截点的。虽然这个二维结构和很多的图结构一样通常比较稀疏,但是使用这种表结构来表示对于维护、理解和扩展来说都是一种比较好的方法。
再进一步说,对于同一个table的同一个拦截点,可以挂载不同的匹配规则,所以这个地方可以看做是以三维的结构也未尝不可。
二、简单的iptables的例子
网络上关于iptables的文档通过google搜索iptable出现的第一条就是http://www.frozentux.net/iptables-tutorial/cn/iptables-tutorial-cn-1.1.19.html中给出的文档,这个文档比较详细,也比较长,所以我没有看完。但是通过里面例子的说明,可以和这个iptables的结构有一个比较好的对应。刚才说了一个二维结构的例子,那么这个二维结构中保存的内容是什么呢?
通过之前的文档可以看到,iptables参数中有两个比较重要的概念,一个是match信息,不同的table在不同的挂接点有自己的代理,那么具体用户要拦截什么样的信息是有用户态确定的,而内核只是提供功能。用户通常需要完成的功能无非是对“特定类型的报文" "执行特定的操作"。
这个特定的报文就是报文的各个参数,例如端口号、ip地址、TTL信息,使用的协议等等,我们可以设置不同的规则来进行不同的匹配,这些通常称为match信息(我们也可以认为是一种”过滤“机制,只是没有人这么叫)。
特定的操作这个概念就不叫宽泛,因为特定的操作往往是不确定的,事先不确定的东西,但是我们可以认为是一种”动作“,而它的术语名称为”target“。
这个基本的模型还是很简单明了的,就像我们写程序一样,if condition dosomething一样天经地义,只是问题复杂之后模糊了它原本的意义。
现在再反过头来看这个target,target是一些特定的动作,在文档的 6.5. Targets/Jumps中有详细说明,例如其中说明了DROP LOG MASQUERADE REJECT RETURN等一些不同的target,这些target有不同的默认动作和意义,这一点再之后再详细说明。
我们看文档中的一个例子
iptables -t nat -A POSTROUTING -p tcp --dst $HTTP_IP --dport 80 -j SNAT --to-source $LAN_IP
其中的 t nat就是一个内核中预定义的一个iptable功能表,用于完成网络地址转换,这里可以认为是table的第一维,或者说是行内容,其中的 -A POSTROUTING体现了第二维和第三维,其中的 POSTROUTING 是在内核中五个挂接点中的第一个入口点,而A则体现了第三维,是在这个table的这个检测点上追加一项,而其它的检测项不受影响。然后的-p tcp --dst $HTTP_IP --dport 80是体现的是该规则在挂接点上的匹配规则,相当于 if condition中的condition,然后通过-j SNAT指明这次操作的目标(动作),最后的--to-source $LAN_IP 为转换的参数。
三、内核中相关数据
在内核中,不同的target一般有各自对应的源文件,大家在内核的linux-2.6.21
etipv4
etfilter文件夹下可以发现很多ipt_xxx.c类型的文件,这些文件每个都对应一个特定的target实现,例如ipt_LOG.c中对应的LOG target,ipt_MASQUERADE.c对应的MASQUERADE.c目标等。
1、初始化结构
以linux-2.6.21
etipv4
etfilteriptable_mangle.c初始化为例,其中使用的到数据结构及定义为
linux-2.6.21includelinux
etfilterx_tables.h
#define ipt_standard_target xt_standard_target
struct xt_standard_target
{
struct xt_entry_target target;
int verdict;
};
struct xt_entry_target
{
union {
struct {
u_int16_t target_size;
/* Used by userspace */
char name[XT_FUNCTION_MAXNAMELEN-1];
u_int8_t revision;
} user;
struct {
u_int16_t target_size;
/* Used inside the kernel */
struct xt_target *target;
} kernel;
/* Total length */
u_int16_t target_size;
} u;
unsigned char data[0];
};
/* Yes, Virginia, you have to zero the padding. */
struct ipt_ip {
/* Source and destination IP addr */
struct in_addr src, dst;
/* Mask for src and dest IP addr */
struct in_addr smsk, dmsk;
char iniface[IFNAMSIZ], outiface[IFNAMSIZ];
unsigned char iniface_mask[IFNAMSIZ], outiface_mask[IFNAMSIZ];
/* Protocol, 0 = ANY */
u_int16_t proto;
/* Flags word */
u_int8_t flags;
/* Inverse flags */
u_int8_t invflags;
};
struct ipt_entry
{
struct ipt_ip ip;
/* Mark with fields that we care about. */
unsigned int nfcache;
/* Size of ipt_entry + matches */
u_int16_t target_offset;
/* Size of ipt_entry + matches + target */
u_int16_t next_offset;
/* Back pointer */
unsigned int comefrom;
/* Packet and byte counters. */
struct xt_counters counters;
/* The matches (if any), then the target. */
unsigned char elems[0];
};
/* Standard entry. */
struct ipt_standard
{
struct ipt_entry entry;
struct ipt_standard_target target;
};
#define MANGLE_VALID_HOOKS ((1 << NF_IP_PRE_ROUTING) |
(1 << NF_IP_LOCAL_IN) |
(1 << NF_IP_FORWARD) |
(1 << NF_IP_LOCAL_OUT) |
(1 << NF_IP_POST_ROUTING))
/* Ouch - five different hooks? Maybe this should be a config option..... -- BC */
static struct
{
struct ipt_replace repl;
struct ipt_standard entries[5];
struct ipt_error term;
} initial_table __initdata
= { { "mangle", MANGLE_VALID_HOOKS, 6,
sizeof(struct ipt_standard) * 5 + sizeof(struct ipt_error),
{ [NF_IP_PRE_ROUTING] = 0,
[NF_IP_LOCAL_IN] = sizeof(struct ipt_standard),
[NF_IP_FORWARD] = sizeof(struct ipt_standard) * 2,
[NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard) * 3,
[NF_IP_POST_ROUTING] = sizeof(struct ipt_standard) * 4 },
{ [NF_IP_PRE_ROUTING] = 0,
[NF_IP_LOCAL_IN] = sizeof(struct ipt_standard),
[NF_IP_FORWARD] = sizeof(struct ipt_standard) * 2,
[NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard) * 3,
[NF_IP_POST_ROUTING] = sizeof(struct ipt_standard) * 4 },
0, NULL, { } },
{
/* PRE_ROUTING */
{ { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 },
0,
sizeof(struct ipt_entry),
sizeof(struct ipt_standard),
0, { 0, 0 }, { } },
{ { { { IPT_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },
-NF_ACCEPT - 1 } },
/* LOCAL_IN */
{ { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 },
0,
sizeof(struct ipt_entry),
sizeof(struct ipt_standard),
0, { 0, 0 }, { } },
{ { { { IPT_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },
-NF_ACCEPT - 1 } },
/* FORWARD */
{ { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 },
0,
sizeof(struct ipt_entry),
sizeof(struct ipt_standard),
0, { 0, 0 }, { } },
{ { { { IPT_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },
-NF_ACCEPT - 1 } },
/* LOCAL_OUT */
{ { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 },
0,
sizeof(struct ipt_entry),
sizeof(struct ipt_standard),
0, { 0, 0 }, { } },
{ { { { IPT_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },
-NF_ACCEPT - 1 } },
/* POST_ROUTING */
{ { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 },
0,
sizeof(struct ipt_entry),
sizeof(struct ipt_standard),
0, { 0, 0 }, { } },
{ { { { IPT_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },
-NF_ACCEPT - 1 } },
},
/* ERROR */
{ { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 },
0,
sizeof(struct ipt_entry),
sizeof(struct ipt_error),
0, { 0, 0 }, { } },
{ { { { IPT_ALIGN(sizeof(struct ipt_error_target)), IPT_ERROR_TARGET } },
{ } },
"ERROR"
}
}
};
大家可以看一下,这个结构的初始化非常的复杂,可以认为是蔚为壮观,主要是考验大家的眼力劲儿的。不过大家不用震惊,因为这的确只是一个最为简单原始的初始化结构,因为考虑到之前说过一个table的二维结构(或者说是三维结构),这里只是为table内的每个挂接点定义最为原始的关节点头结构,从而可以让用户态的iptables注册的各种节点能够挂接到这个地方,那么为什么定义的这么复杂呢?是因为它使用的接口是要给其它模块公用的,公用就要考虑到很多比较特殊的场景,从而看起来这个结构臃肿而诡异。
这个结构通过
ret = ipt_register_table(&packet_mangler, &initial_table.repl)
注册给通用接口,从而建立自己的挂接点。这个接口以两个参数为原型,净第二个参数声明的原始头位置注册到以第一个参数为原型的xt_table结构的void *private;成员中。这一点可以在ipt_do_table函数中得到验证,代码为
struct xt_table_info *private;
……
private = table->private;
2、注册钩子
前面的内容虽然复杂,但是终归是一些数据结构的注册,相当于一些基础设施建设,但是没有涉及到真正的动作,而这个动作的注册就需要通过使用其中的
static struct nf_hook_ops ipt_ops[] = {
{
.hook = ipt_route_hook,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_PRE_ROUTING,
.priority = NF_IP_PRI_MANGLE,
},
{
.hook = ipt_route_hook,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_LOCAL_IN,
.priority = NF_IP_PRI_MANGLE,
},
{
.hook = ipt_route_hook,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_FORWARD,
.priority = NF_IP_PRI_MANGLE,
},
{
.hook = ipt_local_hook,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_LOCAL_OUT,
.priority = NF_IP_PRI_MANGLE,
},
{
.hook = ipt_route_hook,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_POST_ROUTING,
.priority = NF_IP_PRI_MANGLE,
},
};
和
ret = nf_register_hooks(ipt_ops, ARRAY_SIZE(ipt_ops));
来完成,而这里注册的钩子函数进一步再使用此处初始化的packet_mangler结构,从而完成数据和代码的汇合。例如在
/* The work comes in here from netfilter.c. */
static unsigned int
ipt_route_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_mangler);
}
3、执行注册
当用户态通过iptables添加一个规则的时候,它事实上是向内核添加了一个新的struct ipt_entry实例,这个实例中包含了设置的匹配信息,保存在结构中的struct ipt_ip ip;域中;还包含了这个规则的target,通过结构中的u_int16_t target_offset;给出。其中这个ipt_entry结构比较复杂,事实上它并不是一个定长结构,它可以包含任意多的matches和一个target,而他们这些附加类型则通过结构中的
/* Size of ipt_entry + matches */
u_int16_t target_offset;
/* Size of ipt_entry + matches + target */
u_int16_t next_offset;
来确定,所以对于这个结构的遍历并不能通过数组下标来顺序访问,而要通过特定的接口来完成。例如
/* fn returns 0 to continue iteration */
#define IPT_MATCH_ITERATE(e, fn, args...)
({
unsigned int __i;
int __ret = 0;
struct ipt_entry_match *__match;
for (__i = sizeof(struct ipt_entry);
__i < (e)->target_offset;
__i += __match->u.match_size) {
__match = (void *)(e) + __i;
__ret = fn(__match , ## args);
if (__ret != 0)
break;
}
__ret;
})
4、ipt_do_table函数
e = get_entry(table_base, private->hook_entry[hook]); 这里首先通过hook得到table的检测点,相当于表的第hook列,然后接下来是第三维的遍历,也就是同一检测点的多个检测功能的遍历。
/* For return from builtin chain */
back = get_entry(table_base, private->underflow[hook]);
……
do {
IP_NF_ASSERT(e);
IP_NF_ASSERT(back);
if (ip_packet_match(ip, indev, outdev, &e->ip, offset)) { 首先是ipt_ip结构中各项匹配。
struct ipt_entry_target *t;
if (IPT_MATCH_ITERATE(e, do_match,
*pskb, in, out,
offset, &hotdrop) != 0)各个附加match的遍历匹配。
goto no_match;
……
t = ipt_get_target(e);
IP_NF_ASSERT(t->u.kernel.target);
/* Standard target? */
if (!t->u.kernel.target->target) { target为一个指示,例如NF_STOP、NF_DROP等各种动作。
int v;
} else {
verdict = t->u.kernel.target->target(pskb,执行内核函数,例如MASQUERADE中的masquerade_target。
in, out,
hook,
t->u.kernel.target,
t->data);
……
no_match:
e = (void *)e + e->next_offset;
}
} while (!hotdrop);