iptables 常用,iptables-nfqueue 少用。因需要深度理解 iptables-nfqueue,所以顺手撸了一遍 iptables-nfqueue 的源代码。

iptables-nfqueue ,aka NFQUEUE,是将网络包发送到用户程序去做接收/丢弃决定的 iptables 特性。NFQUEUE 需要用户程序监听 NFQUEUE 队列,并对接收到的网络包响应对应的判决结果。

NFQUEUE 的使用

例子参考:使用 Go 对接 iptables NFQUEUE 的例子

使用的 iptables 规则如下:

1
iptables -t raw -I PREROUTING -p tcp --syn -j NFQUEUE --queue-num 1 --queue-bypass
  • --queue-num 指当前规则将对应的网络包放到 1 号队列
  • --queue-bypass 指当没有用户程序监听当前队列时,默认放行网络包,而不是丢包

NFQUEUE 的使用注意事项

队列容量

NFQUEUE 是有容量限制的,默认是 1024;用户程序可以在监听队列的时候设置容量大小。如果队列满了,iptables 默认会丢包;在内核 3.6 之后,可以使用 --fail-open 选项将这默认丢包改为默认接收。

如何知道队列满了后发生丢包呢?

有两种方式:

  1. 每次丢包都会打印一条系统日志
  2. cat /proc/net/netfilter/nfnetlink_queue 里有丢包统计

无法通过程序的方式实时感知到队列满了、或者丢包了。

复制到用户程序的网络包的大小

将网络包复制到用户程序的时候,对网络包的大小是有限制的,默认是 65531;用户程序可以在监听队列的时候设置复制网络包的最大值。

如果只需要根据 IP 和端口进行判决,则可以将这个限制设置为 40(三层和四层的头部总大小),避免不必要的网络包内容复制。

没有响应判决结果

如果没有对网络包响应其判决结果,则该网络包会永远阻塞在 iptablesiptables 不会自动丢弃这样的网络包。

NFQUEUE 的工作机制

NFQUEUE 的工作机制可分上下两场:

  1. 上半场:
    1. 接收 -j NFQUEUE 发送过来的网络包
    2. 入队
    3. 通过 nfnetlink 将网络包发给用户程序
  2. 下半场:
    1. 接收用户程序通过 nfnetlink 发送过来的判决结果
    2. 出队
    3. 处理判决结果
    4. 继续 iptables 后续的处理

上半场的函数调用栈如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
|-->NF_HOOK()                   // include/linux/netfilter.h
    |-->nf_hook()
    |-->nf_hook_slow()          // net/netfilter/core.c
        |-->nf_queue()          // net/netfilter/nf_queue.c
            |-->__nf_queue()
                |-->struct nf_queue_handler *qh->outfn()
               /
              /
             /
            |-->nfqnl_enqueue_packet()          // net/netfilter/nfnetlink_queue.c
                |-->__nfqnl_enqueue_packet()
                |-->nfnetlink_unicast()
                |-->__enqueue_entry()

下半场的函数调用栈如下:

1
2
3
4
5
6
7
8
|-->nfqnl_recv_verdict()            // net/netfilter/nfnetlink_queue.c
    |-->verdict_instance_lookup()
    |-->find_dequeue_entry()
        |-->__dequeue_entry()
    |-->nfqnl_reinject()
    |-->nf_reinject()               // net/netfilter/nf_queue.c
        |-->entry->state.okfn()
            |-->//继续 `iptables` 后续的处理

NFQUEUE 的源码分析

NFQUEU 的源代码分为 nf_queuenfnetlink_queue 两部分。nf_queueiptables 的一部分,nfnetlink_queuenetfilter 的一个子模块。

nfnetlink_queuenf_queue 跟用户程序之间的桥梁,是基于 nfnetlink 的专门用于 NFQUEUE 的通信机制;而 nfnetlink 是一套基于 netlink 的专门用于 netfilter 的通信机制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// net/netfilter/nfnetlink_queue.c

// 模块初始化入口
module_init(nfnetlink_queue_init);
// nfnetlink_queue 模块初始化函数
static int __init nfnetlink_queue_init(void)
{
    int status;
    status = register_pernet_subsys(&nfnl_queue_net_ops); // 注册到每个网络命名空间的 nfnl_queue 选项
    netlink_register_notifier(&nfqnl_rtnl_notifier);
    status = nfnetlink_subsys_register(&nfqnl_subsys);  // 将 nfnetlink_queue 注册到 nfnetlink 子系统中
    status = register_netdevice_notifier(&nfqnl_dev_notifier);
    return status;
}

// 注册到每个网络命名空间的 nfnl_queue 选项
static struct pernet_operations nfnl_queue_net_ops = {
    .init		= nfnl_queue_net_init,
    ...
};
// 每个网络命名空间内的 nfnetlink_queue 的初始化函数
static int __net_init nfnl_queue_net_init(struct net *net)
{
#ifdef CONFIG_PROC_FS
  // 注册的路径为 /proc/net/netfilter/nfnetlink_queue,nfnetlink_queue 的一些统计信息会写入到这里
    if (!proc_create_net("nfnetlink_queue", 0440, net->nf.proc_netfilter,
            &nfqnl_seq_ops, sizeof(struct iter_state)))
        return -ENOMEM;
#endif
    nf_register_queue_handler(net, &nfqh);
    return 0;
}

// 每个网络命名空间内的 nfnetlink_queue 的“环卫工人”
static const struct nf_queue_handler nfqh = {
    .outfn		= nfqnl_enqueue_packet, // 在后面分析上半场的时候再分析该函数
    .nf_hook_drop	= nfqnl_nf_hook_drop,
};

// 将 nfnetlink_queue 注册到 nfnetlink 子系统中
static const struct nfnetlink_subsystem nfqnl_subsys = {
    .name		= "nf_queue",
    ...
    .cb		= nfqnl_cb,
};
static const struct nfnl_callback nfqnl_cb[NFQNL_MSG_MAX] = {
    [NFQNL_MSG_PACKET]	= { .call_rcu = nfqnl_recv_unsupp, // 不支持的选项
                    .attr_count = NFQA_MAX, },
    [NFQNL_MSG_VERDICT]	= { .call_rcu = nfqnl_recv_verdict, // 用户程序进行判决的函数
                    .attr_count = NFQA_MAX,
                    .policy = nfqa_verdict_policy },
    [NFQNL_MSG_CONFIG]	= { .call = nfqnl_recv_config,  // 用户程序配置 nfnetlink 的函数
                    .attr_count = NFQA_CFG_MAX,
                    .policy = nfqa_cfg_policy },
    [NFQNL_MSG_VERDICT_BATCH]={ .call_rcu = nfqnl_recv_verdict_batch,  // 用户程序进行批量判决的函数
                    .attr_count = NFQA_MAX,
                    .policy = nfqa_verdict_batch_policy },
};

用户程序绑定 NFQUEUE

内核的 nfnetlink_queue 已初始化完毕,当用户程序需要监听 NFQUEUE 队列,会执行哪些代码呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// net/netfilter/nfnetlink_queue.c

static int nfqnl_recv_config(...)
{
    ...
    queue = instance_lookup(q, queue_num); // 根据 queue_num 查找队列结构体实例

    if (cmd != NULL) {
        switch (cmd->command) {
        case NFQNL_CFG_CMD_BIND:
            ...
            // 使用用户程序指定的 queue_num 创建一个队列监听结构体的实例
            queue = instance_create(q, queue_num,
                        NETLINK_CB(skb).portid);
            ...
            break;
        case NFQNL_CFG_CMD_UNBIND:
            ...
            // 销毁 queue_num 对应的队列结构体实例
            instance_destroy(q, queue);
            goto err_out_unlock;
        case NFQNL_CFG_CMD_PF_BIND:
        case NFQNL_CFG_CMD_PF_UNBIND:
            break;
        default:
            ret = -ENOTSUPP;
            goto err_out_unlock;
        }
    }

    if (nfqa[NFQA_CFG_PARAMS]) {
        struct nfqnl_msg_config_params *params =
            nla_data(nfqa[NFQA_CFG_PARAMS]);

        // 设置队列的复制模式和网络包复制范围
        nfqnl_set_mode(queue, params->copy_mode,
                ntohl(params->copy_range));
    }

    if (nfqa[NFQA_CFG_QUEUE_MAXLEN]) {
        __be32 *queue_maxlen = nla_data(nfqa[NFQA_CFG_QUEUE_MAXLEN]);
        // 设置队列容量
        queue->queue_maxlen = ntohl(*queue_maxlen);
    }
    return 0;
}

NFQUEUE 上半场

用户程序运行起来后,网络包是怎么从 iptables 到达用户程序的?

下面从 PREROUTING 链为 iptables 入口,到 iptables-nfqueue,再到 nfnetlink_queue 发送 netlink 消息给用户程序。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// net/ipv4/ip_input.c

int ip_rcv(...)
{
    ...
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
               net, NULL, skb, dev, NULL,
               ip_rcv_finish);
}

// include/linux/netfilter.h

NF_HOOK(...)
{
    int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);
    ...
    return ret;
}
// 执行一个 netfilter 钩子
static inline int nf_hook(...)
{
    struct nf_hook_entries *hook_head = NULL;
    ...
    switch (pf) {
    case NFPROTO_IPV4:
        hook_head = rcu_dereference(net->nf.hooks_ipv4[hook]);
        break;
    case NFPROTO_IPV6:
        hook_head = rcu_dereference(net->nf.hooks_ipv6[hook]);
        break;
    }
    if (hook_head) {
        struct nf_hook_state state;
        nf_hook_state_init(&state, hook, pf, indev, outdev,
                   sk, net, okfn);
        ret = nf_hook_slow(skb, &state, hook_head, 0);
    }
    return ret;
}

// net/netfilter/core.c

// 返回值:
//          1: 调用者需要执行 okfn()
//          -EPERM: NF_DROP
//          0: 其它情况
int nf_hook_slow(...)
{
    unsigned int verdict;
    int ret;
    for (; s < e->num_hook_entries; s++) {
    // 执行 iptables 规则,得到规则的判决
        verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
        switch (verdict & NF_VERDICT_MASK) {
        case NF_ACCEPT:  // -j ACCEPT
            break;
        case NF_DROP:    // -j DROP
        case NF_QUEUE:   // -j QUEUE
            ret = nf_queue(skb, state, e, s, verdict);
      break;
        default:
            return 0;
        }
    }
    return 1;
}

// net/netfilter/nf_queue.c

/* 从这个函数离开的网络包必须从 nf_reinject() 回来 */
int nf_queue(...)
{
    int ret;
    ret = __nf_queue(skb, state, entries, index, verdict >> NF_VERDICT_QBITS); // 其中 verdict >> NF_VERDICT_QBITS 是 iptables 规则中 --queue-num 指定的队列号
    ...
    return 0;
}

static int __nf_queue(...)
{
    struct nf_queue_entry *entry = NULL;
    const struct nf_queue_handler *qh;

    // 查找当前网络命名空间里 nfnetlink_queue 的“环卫工人”,在系统初始化时已设置了 queue_handler
    qh = rcu_dereference(net->nf.queue_handler);
    // 初始化一个 nfnetlink_queue “队员”
    entry = kmalloc(sizeof(*entry) + route_key_size, GFP_ATOMIC);
    *entry = (struct nf_queue_entry) {
        .skb	= skb, // 引用当前网络包
        .state	= *state,  // 当前 iptables 状态
        .hook_index = index,
        .size	= sizeof(*entry) + route_key_size,
    };
    // 处理当前“队员”
    status = qh->outfn(entry, queuenum);
    return 0;
}

// net/netfilter/nfnetlink_queue.c

static int
nfqnl_enqueue_packet(struct nf_queue_entry *entry, unsigned int queuenum)
{
    // 找到 queuenum 对应的队列
    queue = instance_lookup(q, queuenum);
    if ((queue->flags & NFQA_CFG_F_GSO) || !skb_is_gso(skb))
        return __nfqnl_enqueue_packet(net, queue, entry);
    ...
    return err;
}

static int
__nfqnl_enqueue_packet(...)
{
    struct sk_buff *nskb;
    nskb = nfqnl_build_packet_message(net, queue, entry, &packet_id_ptr);
    if (queue->queue_total >= queue->queue_maxlen) { // 队列满了
        if (queue->flags & NFQA_CFG_F_FAIL_OPEN) {  // --fail-open,也可以是设置 flags
            failopen = 1;
        } else {
            queue->queue_dropped++; // 丢包统计
            net_warn_ratelimited("nf_queue: full at %d entries, dropping packets(s)\n",
                         queue->queue_total); // 打印一条系统日志
        }
    }
    entry->id = ++queue->id_sequence; // 自增的“队员”ID,nf_reinject() 时就根据这个 ID 去查找对应的“队员”
    // 通过 nfnetlink 将消息发送给用户程序
    err = nfnetlink_unicast(nskb, net, queue->peer_portid, MSG_DONTWAIT);
    if (err < 0) {
        if (queue->flags & NFQA_CFG_F_FAIL_OPEN) {
            failopen = 1;
        } else {
            queue->queue_user_dropped++; // 发送失败的统计
        }
    }
    __enqueue_entry(queue, entry); // “队员”入队
    return 0;
}

NFQUEUE 下半场

用户程序响应回来的判决是怎么生效的呢?

nfnetlink_queue 初始化 知道,用户回复判决会调用函数 nfqnl_recv_verdict,就接着该函数进行分析吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// net/netfilter/nfnetlink_queue.c

static int nfqnl_recv_verdict(...)
{
    u_int16_t queue_num = ntohs(nfmsg->res_id);
    struct nfnl_queue_net *q = nfnl_queue_pernet(net);

    queue = verdict_instance_lookup(q, queue_num,
                    NETLINK_CB(skb).portid);
    verdict = ntohl(vhdr->verdict);
    entry = find_dequeue_entry(queue, ntohl(vhdr->id));
    nfqnl_reinject(entry, verdict); // nf_queue 出去,nf_reinject 回来
    return 0;
}

// 根据 ID 在队列里找“队员”
static struct nf_queue_entry *
find_dequeue_entry(struct nfqnl_instance *queue, unsigned int id)
{
    struct nf_queue_entry *entry = NULL, *i;
    list_for_each_entry(i, &queue->queue_list, list) {
        if (i->id == id) {
            entry = i;
            break;
        }
    }
    if (entry)
        __dequeue_entry(queue, entry); // “队员”出队,对应 __enqueue_entry
    return entry;
}

static void nfqnl_reinject(struct nf_queue_entry *entry, unsigned int verdict)
{
  ...
    nf_reinject(entry, verdict);
}

void nf_reinject(struct nf_queue_entry *entry, unsigned int verdict)
{
    switch (verdict & NF_VERDICT_MASK) {
    case NF_ACCEPT:
    case NF_STOP:
        entry->state.okfn(entry->state.net, entry->state.sk, skb); // 继续 iptables 后续的处理
        break;
    ...
    }
    kfree(entry);
}

NFQUEUE 的统计信息

每个 NFQUEUE 的统计信息保存在 struct nfqnl_instance 中,并且每个一行地打印到 /proc/net/netfilter/nfnetlink_queue 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct nfqnl_instance {
    ...
    u32 peer_portid;                    // 该 id 可能是用户程序的 pid
    unsigned int queue_maxlen;          // 队列容量
    unsigned int copy_range;            // 网络包复制范围
    unsigned int queue_dropped;         // 因队列满了而发生的丢包数量
    unsigned int queue_user_dropped;    // 将网络包发送给用户程序失败的数量
    unsigned int queue_total;           // 当前队列里网络包的数量
    ...
};

统计信息是如何打印到 /proc/net/netfilter/nfnetlink_queue 的?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int __net_init nfnl_queue_net_init(struct net *net)
{
    ...
    if (!proc_create_net("nfnetlink_queue", 0440, net->nf.proc_netfilter,
            &nfqnl_seq_ops, sizeof(struct iter_state)))
    ...
    return 0;
}

static const struct seq_operations nfqnl_seq_ops = {
    ...
    .show	= seq_show,
};

static int seq_show(struct seq_file *s, void *v)
{
    const struct nfqnl_instance *inst = v;
    seq_printf(s, "%5u %6u %5u %1u %5u %5u %5u %8u %2d\n",
        inst->queue_num,           // 当前队列号码
        inst->peer_portid,         // 监听当前队列的用户程序的 pid
        inst->queue_total,         // 当前队列里等待处理的网络包数量
        inst->copy_mode,           // 复制模式,一般使用 NFQNL_COPY_PACKET 复制网络包模式
        inst->copy_range,          // 网络包复制范围,默认 65531
        inst->queue_dropped,       // 因队列满了而发生的丢包数量
        inst->queue_user_dropped,  // 将网络包发送给用户程序失败的数量
        inst->id_sequence,         // 下一个“队员”的 ID
        1);                        // 固定是 1
    return 0;
}

纵观 NFQUEUE 的源代码,NFQUEUE 并没有直接提供 API 去获取丢包事件。

总结

NFQUEUE 因为需要将网络包发送给用户程序,所以它的性能并不高;但相比于堆叠 iptables 规则,NFQUEUE 的处理方式更加灵活。而对于 XDPxt_bpf 等拥有同等灵活性的新技术而言,NFQUEUE 的适用范围更广、能够适配很多老旧系统。