网络协议栈(14)iptables实现浅析

一、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\net\ipv4\netfilter文件夹下可以发现很多ipt_xxx.c类型的文件,这些文件每个都对应一个特定的target实现,例如ipt_LOG.c中对应的LOG target,ipt_MASQUERADE.c对应的MASQUERADE.c目标等。
1、初始化结构
以linux-2.6.21\net\ipv4\netfilter\iptable_mangle.c初始化为例,其中使用的到数据结构及定义为
linux-2.6.21\include\linux\netfilter\x_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);

posted on 2019-03-07 09:05  tsecer  阅读(417)  评论(0编辑  收藏  举报

导航