深入理解Netfilter和iptables

本系列博文主要侧重于分析Netfilter的实现机制、原理和设计思想层面的东西,同时包括从用户态的iptables到内核态的Netfilter的交互过程和通信手段等。至于iptables的入门用法方面的东西,网上随便一搜罗就有一大堆,我这里不浪费笔墨了。

很多人在接触iptables之后就会这么一种感觉:我通过iptables命令配置的每一条规则,到底是如何生效的呢?内核又是怎么去执行这些规则匹配呢?如果iptables不能满足我当下的需求,那么我是否可以去对其进行扩展呢?这些问题,都是我在接下来的博文中一一和大家分享的话题。这里需要指出:因为Netfilter与IP协议栈是无缝契合的,所以如果你要是有协议栈方面的基础,在阅读本文时一定会感觉轻车熟路。当然,如果没有也没关系,因为我会在关键点就协议栈的入门知识给大家做个普及。只是普及哦,不会详细深入下去的,因为涉及的东西太多了,目前我还正在研究摸索当中呢。好了,废话不多说,进入正题。

备注:本人研究的内核版本是2.6.21,iptables的版本是1.4.0。

什么是Netfilter?

为了说明这个问题,我们首先看一个网络通信的基本模型:

深入理解Netfilter和iptables

在数据的发送过程中,从上至下依次是“加头”的过程,每到达一层,数据就会被加上该层的头部信息。与此同时,接收数据方就是个“剥头”的过程,当从网卡接收到数据包之后,在往协议栈的上层传递过程中依次剥去每层的头部,最终到达用户那儿的就是裸数据了。

那么,对于IPv4协议栈,其“栈”模式底层机制基本就是像下面这个样子:

深入理解Netfilter和iptables

对于接收到的每个数据包,都从“A”点进来,经过路由判决,如果是发送给本地主机的就经过“B”点,然后往协议栈的上层继续传递;否则,如果该数据包的目的主机不是本机,那么就经过“C”点,然后顺着“E”点将该数据包发送出去。

对于欲发送的每个数据包,首先也有一个路由判决,以确定该数据包从哪个接口出去,然后经过“D”点,最后也是顺着“E”点将该数据包发送出去。

协议栈中的那五个关键点A、B、C、D和E就是我们Netfilter大展拳脚的地方了。

Netfilter是Linux 2.4.x引入的一个子系统,它作为一个通用的、抽象的框架,提供了一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。Netfilter在内核中的位置如下图所示:

深入理解Netfilter和iptables

上面这幅图,很直观地反映了用户空间的iptables和内核空间的基于Netfilter的ip_tables模块之间的关系和其通讯方式,以及Netfilter在这其中所扮演的角色。

回到前面讨论的关于协议栈的那五个关键点“ABCDE”上来。Netfilter在netfilter_ipv4.h中将这五个点重新命了名,如下图所示,意思我就不再解释了,猫叫喵喵而已:

深入理解Netfilter和iptables

/* 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

在每个关键点上,有很多已经按照优先级预先注册了的回调函数(后面再说这些函数是什么以及干什么用的)。有些人也喜欢把这些函数称为“钩子函数(Hooks)”,说的是同一个东西。这些函数被埋伏在这些关键点,形成了一条链。对于每个到来的或发出的数据包会依次被这些回调函数“调戏”一番后再视情况是将其放行,丢弃还是怎么滴。但是,无论如何,这些回调函数最后必须向Netfilter报告一下该数据包的死活情况,因为毕竟每个数据包都是Netfilter从别人协议栈那儿借调过来给兄弟们Happy的,别个再怎么滴也总得“活要见人,死要见尸”吧。每个钩子函数最后必须向Netfilter框架返回下列几个值之一:

  • NF_ACCEPT 继续正常传输数据包。该返回值告诉Netfilter:到目前为止,该数据包还是被接受的,并且该数据包应当被递交给网络协议栈的下一个阶段;
  • NF_DROP 丢弃该数据包,不再传输;
  • NF_STOLEN 模块接管该数据包,告诉Netfilter”忘掉“该数据包,也就是说本模块”偷(stolen)“了这个数据包。该回调函数将从此开始对数据包进行处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter获取了该数据包的所有权。
  • NF_QUEUE 对该数据包进行排队(通常用于将数据包传递给用户空间的进程进行处理);
  • NF_REPEAT 再次调用该回调函数,应当谨慎使用该值,以免造成死循环。
/* Responses from hook functions. */
#define NF_DROP 0
#define NF_ACCEPT 1
#define NF_STOLEN 2
#define NF_QUEUE 3
#define NF_REPEAT 4
#define NF_MAX_VERDICT NF_REPEAT

为了让我们显得更专业点,我们开始做些约定:上面提到的五个关键点后面我们就叫它们为hook点,每个hook点所注册的那些回调函数都将其称为hook函数。

Linux 2.6版内核的Netfilter目前支持IPv4、IPv6以及DECnet等协议栈,这里我们主要研究IPv4协议。关于协议类型、hook点、hook函数以及优先级,我们通过下图给大家做个详细展示:

深入理解Netfilter和iptables

对于每种类型的协议,数据包都会依次按照hook点的方向进行传输,每个hook点上Netfilter又按照优先级挂载了很多hook函数。这些hook函数就是用来处理数据包的。

Netfilter使用NF_HOOK(include/linux/netfilter.h)宏在协议栈内部切入到Netfilter框架中。2.6版本内核对于该宏的定义如下:

/* This is gross, but inline doesn't cut it for avoiding the function
   call in fast path: gcc doesn't inline (needs value tracking?). --RR */

/* HX: It's slightly less gross now. */

#define NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, thresh)           \
({int __ret;                                       \
if ((__ret=nf_hook_thresh(pf, hook, &(skb), indev, outdev, okfn, thresh, 1)) == 1)\
    __ret = (okfn)(skb);                               \
__ret;})

#define NF_HOOK_COND(pf, hook, skb, indev, outdev, okfn, cond)               \
({int __ret;                                       \
if ((__ret=nf_hook_thresh(pf, hook, &(skb), indev, outdev, okfn, INT_MIN, cond)) == 1)\
    __ret = (okfn)(skb);                               \
__ret;})

#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
    NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN)

关于宏NF_HOOK各个参数的说明如下:
1. pf:协议族名称,Netfilter架构同样可以用于IP层之外,因此,该变量还可以有诸如PF_INET6、PF_DECnet等名字。
2. hook:hook点的名字,对于IP层,其值即为前面的五个关键点值;
3. skb:不解释,sk_buff结构体变量,即数据包指针;
4. indev:数据包进入的设备,以struct net_device结构表示;
5. outdev:数据包出去的设备,以struct net_device结构表示;后面可以看到,以上五个参数将传递给nf_register_hook中注册的处理函数。
6. okfn:函数指针,当所有的该hook点的所有注册函数被调用完之后,转而执行此流程。

对于NF_HOOK_THRESH,其定义如上代码。

我们发现NF_HOOK_THRESH宏只增加了一个thresh参数,该参数就是用来执行该宏去遍历hook函数时的优先级,同时,该宏内部又调用了nf_hook_thresh函数。

/**
 *    nf_hook_thresh - call a netfilter hook
 *   
 *    Returns 1 if the hook has allowed the packet to pass.  The function
 *    okfn must be invoked by the caller in this case.  Any other return
 *    value indicates the packet has been consumed by the hook.
*/
static inline int nf_hook_thresh(int pf, unsigned int hook,
                 struct sk_buff **pskb,
                 struct net_device *indev,
                 struct net_device *outdev,
                 int (*okfn)(struct sk_buff *), int thresh,
                 int cond)
{
    if (!cond)
        return 1;
#ifndef CONFIG_NETFILTER_DEBUG
    if (list_empty(&nf_hooks[pf][hook]))
        return 1;
#endif
    return nf_hook_slow(pf, hook, pskb, indev, outdev, okfn, thresh);
}

该函数只增加了一个参数cond,该参数为0则放弃遍历,并且也不执行okfn函数;如果该参数为1,则进行下一步操作。对于下一步的调用,其是条件性的。如果没有设置CONFIG_NETFILTER_DEBUG环境变量,那么,下一步则直接执行nf_hook_slow函数。而如果设置了CONFIG_NETFILTER_DEBUG环境变量,那么,情况就有所不同了。内核需要首先检查对应协议族和对应hook点的注册钩子函数链是否为空,如果是的,则返回1,而NF_HOOK_THRESH宏的后续工作则为直接执行okfn函数指针对应的处理过程,反之,nf_hook_thresh就去执行nf_hook_slow函数。那么,nf_hook_slow函数到底干了什么事?见【R】处。

要清楚地说明这种特殊情况的行为,我们必须对list_empty(&nf_hooks[pf][hook])语句进行深入地分析。

在net/core/netfilter.c文件中,定义了一个二维的结构体数组,用来存储不同协议栈hook点的回调处理函数。

struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];

其中,行数NPROTO为32,即目前内核所支持的最大协议族数;列数NF_MAX_HOOKS为hook点的个数,目前在2.6内核中该值为8。因此,nf_hooks数组的最终结构如下图所示:

深入理解Netfilter和iptables

在include/linux/socket.h中IP协议AF_INET(PF_INET)的序号为2,因此我们就可以得到TCP/IP协议族的钩子函数挂载点为:

  • PRE_ROUTING: nf_hooks[2][0]
  • LOCAL_IN: nf_hooks[2][1]
  • FORWARD: nf_hooks[2][2]
  • LOCAL_OUT: nf_hooks[2][3]
  • POST_ROUTING: nf_hooks[2][4]

同时我们看到,在2.6内核的IP协议栈中,从协议栈正常的流程切入到Netfilter框架中,然后顺序地依次地调用每个hook点所有的钩子函数的相关操作有如下几处:

1.net/ipv4/ip_input.c中的ip_rcv函数。该函数主要用来处理网络层的IP报文的入口函数,它到Netfilter框架的切入点为:

NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);

根据前面的理解,这句代码意义已经很直观明确了。那就是:如果协议栈当前接收到了一个IP报文(PF_INET),那么就把这个报文传到NF_IP_PRE_ROUTING过滤点,去检查【R】在那个过滤点(nf_hooks[2][0])是否有人注册了相关的用于处理数据包的钩子函数。如果有,则依次遍历链表nf_hooks[2][0]去需找匹配的match和相应的target,根据返回到Netfilter框架中的值来进一步决定该如何处理该数据包(由钩子模块处理还是交由ip_rcv_finish函数继续处理)。

【R】:刚才说到的所谓”检查“。其核心就是nf_hook_slow函数。该函数本质上做的事很简单,其根据优先级查找双向链表nf_hooks[][],找到对应的回调函数来处理数据包,详细代码如下:

int nf_hook_slow(int pf, unsigned int hook, struct sk_buff *skb,
         struct net_device *indev,
         struct net_device *outdev,
         int (*okfn)(struct sk_buff *),
         int hook_thresh)
{
    struct list_head *elem;
    unsigned int verdict;
    int ret = 0;
    /* We may already have this, but read-locks nest anyway */
    rcu_read_lock();
#ifdef CONFIG_NETFILTER_DEBUG
    if (skb->nf_debug & (1 << hook)) {
        printk("nf_hook: hook %i already set.\n", hook);
        nf_dump_skb(pf, skb);
    }
    skb->nf_debug |= (1 << hook);
#endif
    elem = &nf_hooks[pf][hook];
next_hook:
    verdict = nf_iterate(&nf_hooks[pf][hook], &skb, hook, indev,
                 outdev, &elem, okfn, hook_thresh);
    if (verdict == NF_QUEUE) {
        NFDEBUG("nf_hook: Verdict = QUEUE.\n");
        if (!nf_queue(skb, elem, pf, hook, indev, outdev, okfn))
            goto next_hook;
    }
    switch (verdict) {
    case NF_ACCEPT:
        ret = okfn(skb);
        break;
    case NF_DROP:
        kfree_skb(skb);
        ret = -EPERM;
        break;
    }
    rcu_read_unlock();
    return ret;
}
static unsigned int nf_iterate(struct list_head *head,
                   struct sk_buff **skb,
                   int hook,
                   const struct net_device *indev,
                   const struct net_device *outdev,
                   struct list_head **i,
                   int (*okfn)(struct sk_buff *),
                   int hook_thresh)
{
    /*
     * The caller must not block between calls to this
     * function because of risk of continuing from deleted element.
     */
    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 */
        switch (elem->hook(hook, skb, indev, outdev, okfn)) {
        case NF_QUEUE:
            return NF_QUEUE;
        case NF_STOLEN:
            return NF_STOLEN;
        case NF_DROP:
            return NF_DROP;
        case NF_REPEAT:
            *i = (*i)->prev;
            break;
#ifdef CONFIG_NETFILTER_DEBUG
        case NF_ACCEPT:
            break;
        default:
            NFDEBUG("Evil return from %p(%u).\n",
                elem->hook, hook);
#endif
        }
    }
    return NF_ACCEPT;
}

2.net/ipv4/ip_forward.c中的ip_forward函数,它的切入点为:

NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, rt->u.dst.dev, ip_forward_finish);

在经过路由抉择后,所有需要本机转发的报文都会交由ip_forward函数进行处理。这里,该函数由NF_IP_FORWARD过滤点切入到Netfilter框架,在nf_hooks[2][2]过滤点执行匹配查找。最后根据返回值来确定ip_forward_finish函数的执行情况。

int ip_forward(struct sk_buff *skb)
{
    struct iphdr *iph;    /* Our header */
    struct rtable *rt;    /* Route we use */
    struct ip_options * opt    = &(IPCB(skb)->opt);
    if (!xfrm4_policy_check(NULL, XFRM_POLICY_FWD, skb))
        goto drop;
    if (IPCB(skb)->opt.router_alert && ip_call_ra_chain(skb))
        return NET_RX_SUCCESS;
    if (skb->pkt_type != PACKET_HOST)
        goto drop;
    skb->ip_summed = CHECKSUM_NONE;

    /*
     *    According to the RFC, we must first decrease the TTL field. If
     *    that reaches zero, we must reply an ICMP control message telling
     *    that the packet's lifetime expired.
     */
    iph = skb->nh.iph;
    if (iph->ttl <= 1)
                goto too_many_hops;
    if (!xfrm4_route_forward(skb))
        goto drop;
    iph = skb->nh.iph;
    rt = (struct rtable*)skb->dst;
    if (opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
        goto sr_failed;
    /* We are about to mangle packet. Copy it! */
    if (skb_cow(skb, LL_RESERVED_SPACE(rt->u.dst.dev)+rt->u.dst.header_len))
        goto drop;
    iph = skb->nh.iph;
    /* Decrease ttl after skb cow done */
    ip_decrease_ttl(iph);
    /*
     *    We now generate an ICMP HOST REDIRECT giving the route
     *    we calculated.
     */
    if (rt->rt_flags&RTCF_DOREDIRECT && !opt->srr)
        ip_rt_send_redirect(skb);
    skb->priority = rt_tos2priority(iph->tos);
    return NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, rt->u.dst.dev,
               ip_forward_finish);
sr_failed:
        /*
     *    Strict routing permits no gatewaying
     */
         icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
         goto drop;
too_many_hops:
        /* Tell the sender its packet died... */
        icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
drop:
    kfree_skb(skb);
    return NET_RX_DROP;
}

3.net/ipv4/ip_output.c中的ip_output函数,它的切入点为:

NF_HOOK_COND(PF_INET, NF_IP_POST_ROUTING, skb, NULL, dev, ip_finish_output, !(IPCB(skb)->flags & IPSKB_REROUTED));

这里,我们看到切入点从无条件宏NF_HOOK改成了有条件宏NF_HOOK_COND,调用该宏的条件是:如果协议栈当前所处理的数据包中没有重新路由的标记,数据包才会进入Netfilter框架。否则,直接调用ip_finish_output函数走协议栈去处理。除此之外,有条件宏和无条件宏再无其他任何差异。

如果需要陷入Netfilter框架,则数据包会在nf_hooks[2][4]过滤点去进行匹配查找。

4.net/ipv4/ip_input.c中的ip_local_deliver函数。干函数处理所有目的地址是本机的数据包,其切入点为:

NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,ip_local_deliver_finish);

发往本机的数据包,首先会全部去往nf_hooks[2][1]过滤点上检测是否有相关数据包的回调处理函数,如果有则执行匹配动作,最后根据返回值执行ip_local_deliver_finish函数。

/*
 *     Deliver IP Packets to the higher protocol layers.
*/
int ip_local_deliver(struct sk_buff *skb)
{
    /*
     *    Reassemble IP fragments.
     */
    if (skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {
        skb = ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER);
        if (!skb)
            return 0;
    }
    return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
               ip_local_deliver_finish);
}

5.net/ipv4/ip_output.c中的ip_push_pending_frame函数。该函数将IP分片重组成完整的IP报文,然后发送出去。进入Netfilter框架的切入点为:

NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, skb->dst->dev, dst_output);

对于所有从本机发出去的报文都会首先去Netfilter的nf_hooks[2][3]过滤点去过滤。一般情况下来来说,不管是路由器还是PC中端,很少有人限制自己机器发出去的报文。因为这样做的潜在风险也是显而易见的,往往会因为一些不恰当的设置导致某些服务失效,所以在这个过滤点上拦截数据包的情况非常少。当然也不排除真的有特殊需求的情况。

/*
 *    Combined all pending IP fragments on the socket as one IP datagram
 *    and push them out.
*/
int ip_push_pending_frames(struct sock *sk)
{
    struct sk_buff *skb, *tmp_skb;
    struct sk_buff **tail_skb;
    struct inet_sock *inet = inet_sk(sk);
    struct ip_options *opt = NULL;
    struct rtable *rt = inet->cork.rt;
    struct iphdr *iph;
    int df = 0;
    __u8 ttl;
    int err = 0;
    if ((skb = __skb_dequeue(&sk->sk_write_queue)) == NULL)
        goto out;
    tail_skb = &(skb_shinfo(skb)->frag_list);
    /* move skb->data to ip header from ext header */
    if (skb->data < skb->nh.raw)
        __skb_pull(skb, skb->nh.raw - skb->data);
    while ((tmp_skb = __skb_dequeue(&sk->sk_write_queue)) != NULL) {
        __skb_pull(tmp_skb, skb->h.raw - skb->nh.raw);
        *tail_skb = tmp_skb;
        tail_skb = &(tmp_skb->next);
        skb->len += tmp_skb->len;
        skb->data_len += tmp_skb->len;
        skb->truesize += tmp_skb->truesize;
        __sock_put(tmp_skb->sk);
        tmp_skb->destructor = NULL;
        tmp_skb->sk = NULL;
    }
    /* Unless user demanded real pmtu discovery (IP_PMTUDISC_DO), we allow
     * to fragment the frame generated here. No matter, what transforms
     * how transforms change size of the packet, it will come out.
     */
    if (inet->pmtudisc != IP_PMTUDISC_DO)
        skb->local_df = 1;
    /* DF bit is set when we want to see DF on outgoing frames.
     * If local_df is set too, we still allow to fragment this frame
     * locally. */
    if (inet->pmtudisc == IP_PMTUDISC_DO ||
        (!skb_shinfo(skb)->frag_list && ip_dont_fragment(sk, &rt->u.dst)))
        df = htons(IP_DF);
    if (inet->cork.flags & IPCORK_OPT)
        opt = inet->cork.opt;
    if (rt->rt_type == RTN_MULTICAST)
        ttl = inet->mc_ttl;
    else
        ttl = ip_select_ttl(inet, &rt->u.dst);
    iph = (struct iphdr *)skb->data;
    iph->version = 4;
    iph->ihl = 5;
    if (opt) {
        iph->ihl += opt->optlen>>2;
        ip_options_build(skb, opt, inet->cork.addr, rt, 0);
    }
    iph->tos = inet->tos;
    iph->tot_len = htons(skb->len);
    iph->frag_off = df;
    if (!df) {
        __ip_select_ident(iph, &rt->u.dst, 0);
    } else {
        iph->id = htons(inet->id++);
    }
    iph->ttl = ttl;
    iph->protocol = sk->sk_protocol;
    iph->saddr = rt->rt_src;
    iph->daddr = rt->rt_dst;
    ip_send_check(iph);
    skb->priority = sk->sk_priority;
    skb->dst = dst_clone(&rt->u.dst);
    /* Netfilter gets whole the not fragmented skb. */
    err = NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL,
              skb->dst->dev, dst_output);
    if (err) {
        if (err > 0)
            err = inet->recverr ? net_xmit_errno(err) : 0;
        if (err)
            goto error;
    }
out:
    inet->cork.flags &= ~IPCORK_OPT;
    if (inet->cork.opt) {
        kfree(inet->cork.opt);
        inet->cork.opt = NULL;
    }
    if (inet->cork.rt) {
        ip_rt_put(inet->cork.rt);
        inet->cork.rt = NULL;
    }
    return err;
error:
    IP_INC_STATS(IPSTATS_MIB_OUTDISCARDS);
    goto out;
}

小结

整个Linux内核中Netfilter框架的HOOK机制可以概括如下:

在数据包流经内核协议栈的整个过程中,在一些已预定义的关键点上PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT和POST_ROUTING,内核会根据数据包的协议族PF_INET去往这些hook点上去查找是否注册有钩子函数。如果没有,则直接返回okfn函数指针所指向的函数继续走协议栈;如果有,则调用nf_hook_slow函数,从而进入到Netfilter框架中进一步调用已注册在该过滤点下的钩子函数,再根据其返回值来确定是否继续执行由函数指针okfn所指向的函数。

上一篇:深入Java微服务之网关系列2:常见Java网关实现方案对比


下一篇:如何把代码写的更优雅,你需要这一份代码精进书单!