Netfilter源码分析(7)

2020-05-27 00:00:00 函数 分析 调用 匹配 链表

内核中的match

接下来的流程,似乎应该是分析扩展match及target的匹配了,如继续分析do_match:
static inline
int do_match(struct ipt_entry_match *m,
             const struct sk_buff *skb,
             const struct net_device *in,
             const struct net_device *out,
             int offset,
             const void *hdr,
             u_int16_t datalen,
             int *hotdrop)
{
        /* Stop iteration if it doesn't match */
        if (!m->u.kernel.match->match(skb, in, out, m->data,
                                      offset, hdr, datalen, hotdrop))
                return 1;
        else
                return 0;
}

虽然函数只有一句话,但是m->u.kernel.match->match()这是什么东东?不明白。因为至目前为止,我们对于
扩展的match和target在内核中的结构、组织、注册等东东都没有接触过,只是在分析iptables时接触过用户态
的那个基于插件形式的框架。所以,函数流程分析至此,要中断一下了。从内核中match的组织分析起。

我们在编译内核的netfilter选项时,有ah、esp、length……等一大堆的匹配选项,他们既可以是模块的形式注册,
又可以是直接编译进内核,所以,他们应该是以单独的文件形式,以:
module_init(init);
module_exit(cleanup);
这样形式存在的,我们在源码目录下边,可以看到Ipt_ah.c、Ipt_esp.c、Ipt_length.c等许多文件,这些就是我们
所要关心的了,另一方面,基本的TCP/UDP 的端口匹配,ICMP类型匹配不在此之列,所以,应该有初始化的地方,
我们注意到Ip_tables.c的init中,有如下语句:
        /* Noone else will be downing sem now, so we won't sleep */
        down(&ipt_mutex);
        list_append(&ipt_target, &ipt_standard_target);
        list_append(&ipt_target, &ipt_error_target);
        list_append(&ipt_match, &tcp_matchstruct);
        list_append(&ipt_match, &udp_matchstruct);
        list_append(&ipt_match, &icmp_matchstruct);
        up(&ipt_mutex);

可以看到,这里注册了standard_target、error_target两个target和tcp_matchstruct等三个match。

这两个地方,就是涉及到match在内核中的注册了,以Ipt_*.c为例,它们都是以下结构:
#include XXX

MODULE_AUTHOR()
MODULE_DESCRIPTION()
MODULE_LICENSE()

static int match()
{
}

static int checkentry()
{
}

static struct ipt_match XXX_match = { { NULL, NULL }, "XXX", &match,
                &checkentry, NULL, THIS_MODULE };

static int __init init(void)
{
        return ipt_register_match(&XXX_match);
}

static void __exit fini(void)
{
        ipt_unregister_match(&XXX_match);
}

module_init(init);
module_exit(fini);

其中,init函数调用ipt_register_match对一个struct ipt_match结构的XXX_match进行注册,
另外,有两个函数match和checkentry。

先来看struct ipt_match结构:


struct ipt_match
{
        struct list_head list;        /* 组织链表的成员,前面提到过很多次了,通常初始化成{NULL,NULL}*/
                
        const char name[IPT_FUNCTION_MAXNAMELEN];        /*match的名称*/
        /*匹配函数,到时候进行该match的匹配,就要调用它了,这也是我们为关心的实现
        返回非0表示匹配成功,如果返回0且hotdrop设为1,则表示该报文应当立刻丢弃*/
        int (*match)(const struct sk_buff *skb,
                     const struct net_device *in,
                     const struct net_device *out,
                     const void *matchinfo,
                     int offset,
                     const void *hdr,
                     u_int16_t datalen,
                     int *hotdrop);
        /* 在使用本Match的规则注入表中之前调用,进行有效性检查,如果返回0,规则就不会加入iptables中 */
        int (*checkentry)(const char *tablename,
                          const struct ipt_ip *ip,
                          void *matchinfo,
                          unsigned int matchinfosize,
                          unsigned int hook_mask);
        
        /* 在包含本Match的规则从表中删除时调用,与checkentry配合可用于动态内存分配和释放 */        
        void (*destroy)(void *matchinfo, unsigned int matchinfosize);
        
        /* 表示当前Match是否为模块(NULL为否)*/        
        struct module *me;                
};

有了对这个结构的认识,就可以很容易地理解init函数了。我们也可以猜测,ipt_register_match的作用可能就是建立一个
双向链表的过程,到时候要用某个match的某种功能,遍历链表,调用其成员函数即可。

当然,对于分析filter的实现,每个match/target的匹配函数的确是我们关心的重点,但是这里为了不中断分析系统框架,就
不再一一分析每个match的match函数,以后专门搞个章节来分析。

接着来看ipt_register_match函数是如何建立双向链表的(猜一下,应该也是调用list_add函数吧……)

int
ipt_register_match(struct ipt_match *match)
{
        int ret;

        MOD_INC_USE_COUNT;
        ret = down_interruptible(&ipt_mutex);
        if (ret != 0) {
                MOD_DEC_USE_COUNT;
                return ret;
        }
        if (!list_named_insert(&ipt_match, match)) {
                duprintf("ipt_register_match: `%s' already in list!\n",
                         match->name);
                MOD_DEC_USE_COUNT;
                ret = -EINVAL;
        }
        up(&ipt_mutex);

        return ret;
}

可以看到,是通过调用list_named_insert(&ipt_match, match)来实现,函数个参数是链表头,第二个参数
是当前待插入接点,并没有如偶想像的直接调用list_add函数,list_named_insert,根据名称排序插入?继续看看先:
/* Returns false if same name already in list, otherwise does insert. */
static inline int
list_named_insert(struct list_head *head, void *new)
{
        if (LIST_FIND(head, __list_cmp_name, void *,
                      new + sizeof(struct list_head)))
                return 0;
        list_prepend(head, new);
        return 1;
}

涉及到两点:先调用宏LIST_FIND,再调用list_prepend,list_prepend是一个建立链表的过程:
/* Prepend. */
static inline void
list_prepend(struct list_head *head, void *new)
{
        ASSERT_WRITE_LOCK(head);
        list_add(new, head);
}
这个前面已提到很多次了,不再继续分析。

那么LIST_FIND呢?
/* Return pointer to first true entry, if any, or NULL.  A macro
   required to allow inlining of cmpfn. */
#define LIST_FIND(head, cmpfn, type, args...)                \
({                                                        \
        const struct list_head *__i = (head);                \
                                                        \
        ASSERT_READ_LOCK(head);                                \
        do {                                                \
                __i = __i->next;                        \
                if (__i == (head)) {                        \
                        __i = NULL;                        \
                        break;                                \
                }                                        \
        } while (!cmpfn((const type)__i , ## args));        \
        (type)__i;                                        \
})

由于可知了,LIST_FIND的作用是遍历链表中的每一个节点,然后将每一个节点与当前待处理的节点通过
第二个参数(函数指针)来处理,对于此次调用,
LIST_FIND(head, __list_cmp_name, void *,new + sizeof(struct list_head))
cmpfn就是__list_cmp_name。
而new + sizeof(struct list_head)表示的是struct ipt_match结构跳过一个struct list_head结构,刚好
就是待处理节点的名称,即当然待插入的match的name。
那么,__list_cmp_name函数就很一目了然,就是比较两个name是否相同。
/* If the field after the list_head is a nul-terminated string, you
   can use these functions. */
static inline int __list_cmp_name(const void *i, const char *name)
{
        return strcmp(name, i+sizeof(struct list_head)) == 0;
}

回过头来,list_named_insert的作用就是先看链表中待插入match的名称是否已存在,不存在再进行链表节点的
插入。

同样的,对于第二种形式:

list_append(&ipt_match, &tcp_matchstruct);
list_append(&ipt_match, &udp_matchstruct);
list_append(&ipt_match, &icmp_matchstruct);
因为他们不是以可选插件的形式存在,不需要检查是否注册,查接建立链表即可:

/* Append. */
static inline void
list_append(struct list_head *head, void *new)
{
        ASSERT_WRITE_LOCK(head);
        list_add((new), (head)->prev);
}

OK,了解了内核中match的注册、组织、链表的构建,那么我们要说的match的匹配就是一个很简单的事情了——遍历双向链表,调用每一个节点match封装好的match函数,就OK了。
现在内核中的某条规则是match1+match2+match3+target……这样子,
而检测对应的match节点的函数又被封装成一个双向链表:
node1--node2--node3……

每检测一个包,匹配每一条规则的每一个match都根据match名称来遍历一次链表,无疑是一个效率非常低的事情(事实上,起先我没有仔细地看代码,想当然地以为就是这样,多亏思一克给我指出来)。但是,如果规则的match中,在检测之前,根据match的名称,把其对应的检测函数与链表中的对应函数关连起来,那么,我们就可以直接使用IPT_MATCH_ITERATE宏来遍历规则中的每一个match,然后直接调用:
match->match检测函数
这样的形式来进行数据包的匹配,无疑效率就得到了极大的提升。也就是:
do_match函数中的
if (!m->u.kernel.match->match(skb, in, out, m->data,offset, hdr, datalen, hotdrop))
我将在下一节继续分析为什么通过m->u.kernel.match->match就可以直接定位到相应的match函数。

不过,事实上重点应该是分析每一个match函数,这个以后用单独的内容来分析。  


文章来源CU社区:[原创]Netfilter源码分析-我来抛砖,望能引玉

相关文章