Netfilter是linux内核中的数据包过滤框架,2.4版本及其后的内核包含该框架,该框架使数据包过滤、网络地址转换(NAT)和其他数据包修改功能成为可能。Netfilter框架由之前的ipfwadm和ipchains系统改进并重新设计而来,iptables工具与其紧密关联并依赖其在内核完成相应功能。

Netfilter框架由一组内核中的hook点组成,内核模块可以在网络栈的这些hook点上注册回调函数,当数据包穿过网络栈上相应的hook点时注册的回调函数将被调用,回调函数可以对参数中的数据包做需要的处理并裁决数据包后续的处理方式。

协议族

netfilter是一个通用的数据包过滤框架,支持多种协议族的数据包过滤。本文后续只关注IPV4协议。

ps:本文参考3.10.0-862.el7.x86_64版本内核代码。

本文参考的内核版本中对NFPROTO_INET看起来没有具体使用,未来版本可能会将对NFPROTO_INET的hook同时生效到IPV4与IPV6两个协议族上,也就是注册一个回调函数同时过滤两个协议族的数据包。从支持的协议族所对应的数字可以看到兼容了include/linux/socket.h中各个协议族的定义,对开发者更友好,比如NFPROTO_IPV4 == PF_INETNFPROTO_IPV6 == PF_INET6,开发时混用也不会出错。

定义在include/uapi/linux/netfilter.h
1
2
3
4
5
6
7
8
9
10
enum {
NFPROTO_UNSPEC = 0,
NFPROTO_INET = 1,
NFPROTO_IPV4 = 2,
NFPROTO_ARP = 3,
NFPROTO_BRIDGE = 7,
NFPROTO_IPV6 = 10,
NFPROTO_DECNET = 12,
NFPROTO_NUMPROTO,
};

hook点

我没有关注其他协议族的情况,IPV4下共5个hook点,但是hook点的定义比较有趣。比如:

  • include/uapi/linux/netfilter_ipv4.h文件中定义有NF_IP_PRE_ROUTING
  • include/uapi/linux/netfilter.h文件中定义有NF_INET_PRE_ROUTING

其他几个hook点定义同样值相同。但是netfilter在内核IPV4与IPV6协议栈中调用回调函数时只使用了NF_INET_PRE_ROUTING这一组。不确定其他协议栈是否也是这5个hook点。

各个hook点调用时机如下:

  • NF_INET_PRE_ROUTING
    刚刚进入网络层的数据包通过此hook点,此时还没有判断式数据包的接收者是本地还是转发给其他host。
  • NF_INET_FORWARD
    通过pre_routing后,如果判断数据包应该转发给其他host,则会进一步通过forward点。
  • NF_INET_LOCAL_IN
    通过pre_routing点后,如果判断数据包应该由本地接收,则会进一步通过input点。
  • NF_INET_LOCAL_OUT
    本地向外发出的数据包会先经过output点。
  • NF_INET_POST_ROUTING
    本地向外发出的数据包经过output点后会经过post_routing点,转发的数据包经过forward点后也会经过post_routing点。

netfilter hooks

定义在include/uapi/linux/netfilter.h
1
2
3
4
5
6
7
8
enum nf_inet_hooks {
NF_INET_PRE_ROUTING,
NF_INET_LOCAL_IN,
NF_INET_FORWARD,
NF_INET_LOCAL_OUT,
NF_INET_POST_ROUTING,
NF_INET_NUMHOOKS
};

裁决值

  • NF_DROP
    netfilter框架将丢掉该数据包并释放相关资源。
  • NF_ACCEPT
    回调函数放行了该数据包,将继续调用该hook点后续其他回调函数(如果有的话)或进入后续处理流程。
  • NF_STOLEN
    数据包由该回调函数负责后续处理(包括资源释放),netfilter及协议栈将不再对该数据包做任何动作。
  • NF_QUEUE
    数据包停止其他回调函数调用,由netfilter放入NFQUEUE队列中。参考NFQUEUE 用户态数据包处理
  • NF_REPEAT
    数据包由netfilter再次调用刚刚返回NF_REPEAT值的回调函数,返回该值需要谨慎避免死循环。
  • NF_STOP
    NF_ACCEPT类似,但不再调用该hook点后续的任何回调函数(如果有的话)直接进入后续处理流程。
  • NF_VERDICT_MASK
    裁决动作掩码。由于NF_DROPNF_QUEUE在裁决动作之外可能需要额外的信息附带在返回值中,因此使用该掩码过滤掉其他信息只保留裁决动作。
  • NF_VERDICT_FLAG_QUEUE_BYPASS
    该flag位用于标记NFQUEUE入队列失败之后是否进行后续处理。如果没有该flag,入队列失败后将直接释放数据包资源。如果包含该flag,入队列失败后将等同于NF_ACCEPT做后续处理。
  • NF_VERDICT_QMASK
    该掩码记录了用于NFQUEUE队列号的bit位。
  • NF_VERDICT_QBITS
    回调函数裁决NF_QUEUE时,返回值右移此尾数以取到NFQUEUE队列号。

  • NF_QUEUE_NR
    一个辅助宏,输入NFQUEUE队列号,返回回调函数的合理返回值。

  • NF_DROP_ERR
    一个辅助宏,输入NF_DROP附带的错误值,返回回调函数的合理返回值。
定义在include/uapi/linux/netfilter.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 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_STOP 5
#define NF_MAX_VERDICT NF_STOP

/* we overload the higher bits for encoding auxiliary data such as the queue
* number or errno values. Not nice, but better than additional function
* arguments. */
#define NF_VERDICT_MASK 0x000000ff

/* extra verdict flags have mask 0x0000ff00 */
#define NF_VERDICT_FLAG_QUEUE_BYPASS 0x00008000

/* queue number (NF_QUEUE) or errno (NF_DROP) */
#define NF_VERDICT_QMASK 0xffff0000
#define NF_VERDICT_QBITS 16

#define NF_QUEUE_NR(x) ((((x) << 16) & NF_VERDICT_QMASK) | NF_QUEUE)

#define NF_DROP_ERR(x) (((-x) << 16) | NF_DROP)

例子

netfilter只能在内核态注册回调函数,因此需要编译为内核模块,参考Linux kernel module 内核模块

这里贴一个例子,在IPV4协议栈的5个hook点都注册回调函数,udp端口包含10086的数据包在NF_INET_POST_ROUTINGNF_INET_LOCAL_IN阶段丢掉,udp端口包含10010的数据包在NF_INET_POST_ROUTINGNF_INET_LOCAL_IN阶段放入NFQUEUE队列10010中,其他数据包全部放行。这样也可以联动之前NFQUEUE的例子代码。

ps:注意这里要谨慎一些,因为内核IPV4的所有数据包都会经过该回调函数。

netfilter_example.c
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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/udp.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Hu Yu <hyuuhit@gmail.com>");
MODULE_DESCRIPTION("netfilter example");
MODULE_VERSION("0.1");

#define MY_QUEUE_NUM 10010

static char *hook_name(int hook) {
switch (hook) {
case NF_INET_PRE_ROUTING:
return "pre_routing";
case NF_INET_LOCAL_IN:
return "local_in";
case NF_INET_FORWARD:
return "forward";
case NF_INET_LOCAL_OUT:
return "local_out";
case NF_INET_POST_ROUTING:
return "post_routing";
}
return NULL;
}

static unsigned int generic_hook(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state)
{
struct iphdr *iph;
struct udphdr *udph;
struct udphdr buf;

iph = ip_hdr(skb);

if (iph->protocol != IPPROTO_UDP) {
return NF_ACCEPT;
}

if (skb->len < iph->ihl * 4 + sizeof(*udph)) {
return NF_ACCEPT;
}

if (skb_headlen(skb) < iph->ihl * 4 + sizeof(*udph)) {
udph = (void *)iph + iph->ihl * 4;
} else {
skb_copy_bits(skb, iph->ihl * 4, &buf, sizeof(*udph));
udph = &buf;
}


if (ntohs(udph->source) == 10086 || ntohs(udph->dest) == 10086) {
if (ops->hooknum == NF_INET_POST_ROUTING || ops->hooknum == NF_INET_LOCAL_IN) {
printk("%-15s udp drop %pI4:%d -> %pI4:%d\n", hook_name(ops->hooknum), &iph->saddr, ntohs(udph->source), &iph->daddr, ntohs(udph->dest));
return NF_DROP;
} else {
printk("%-15s udp accept %pI4:%d -> %pI4:%d\n", hook_name(ops->hooknum), &iph->saddr, ntohs(udph->source), &iph->daddr, ntohs(udph->dest));
return NF_ACCEPT;
}
} else if (ntohs(udph->source) == 10010 || ntohs(udph->dest) == 10010) {
if (ops->hooknum == NF_INET_POST_ROUTING || ops->hooknum == NF_INET_LOCAL_IN) {
printk("%-15s udp queue %pI4:%d -> %pI4:%d\n", hook_name(ops->hooknum), &iph->saddr, ntohs(udph->source), &iph->daddr, ntohs(udph->dest));
return NF_QUEUE_NR(MY_QUEUE_NUM);
} else {
printk("%-15s udp accept %pI4:%d -> %pI4:%d\n", hook_name(ops->hooknum), &iph->saddr, ntohs(udph->source), &iph->daddr, ntohs(udph->dest));
return NF_ACCEPT;
}
}
return NF_ACCEPT;
}

static struct nf_hook_ops my_hooks[] = {
{
.hook = generic_hook,
.owner = THIS_MODULE,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_LAST,
},
{
.hook = generic_hook,
.owner = THIS_MODULE,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN,
.priority = NF_IP_PRI_LAST,
},
{
.hook = generic_hook,
.owner = THIS_MODULE,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_FORWARD,
.priority = NF_IP_PRI_LAST,
},
{
.hook = generic_hook,
.owner = THIS_MODULE,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_OUT,
.priority = NF_IP_PRI_LAST,
},
{
.hook = generic_hook,
.owner = THIS_MODULE,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_POST_ROUTING,
.priority = NF_IP_PRI_LAST,
},
};

static int __init my_init(void)
{
printk("my init.\n");
return nf_register_hooks(my_hooks, ARRAY_SIZE(my_hooks));
}

static void __exit my_exit(void)
{
printk("my exit.\n");
nf_unregister_hooks(my_hooks, ARRAY_SIZE(my_hooks));
}

module_init(my_init);
module_exit(my_exit);
Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
obj-m := netfilter_example.o

PWD:=$(shell pwd)
KVER:=$(shell uname -r)
KDIR:=/lib/modules/$(KVER)/build

EXTRA_CFLAGS += -Wall -g

all:
$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
使用nc测试后的dmesg输出
1
2
3
4
5
6
7
8
9
10
[  614.926634] my init.
[ 620.194403] local_out udp accept 10.95.29.17:47690 -> 10.95.29.15:10086
[ 620.194408] post_routing udp drop 10.95.29.17:47690 -> 10.95.29.15:10086
[ 623.164656] local_out udp accept 10.95.29.17:44909 -> 10.95.29.15:10010
[ 623.164662] post_routing udp queue 10.95.29.17:44909 -> 10.95.29.15:10010
[ 632.284199] pre_routing udp accept 10.95.29.15:39701 -> 10.95.29.17:10086
[ 632.284220] local_in udp drop 10.95.29.15:39701 -> 10.95.29.17:10086
[ 650.240826] pre_routing udp accept 10.95.29.15:40502 -> 10.95.29.17:10010
[ 650.240853] local_in udp queue 10.95.29.15:40502 -> 10.95.29.17:10010
[ 667.918573] my exit.

实现原理

注册回调函数

注册回调函数需要nf_hook_ops结构体,里面定义了注册的回调函数、协议族、hook点和优先级(升序排列执行)。

nf_hook_ops结构体定义在include/linux/netfilter.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct nf_hook_ops {
struct list_head list;

/* User fills in from here down. */
nf_hookfn *hook;
struct module *owner;
void *priv;
u_int8_t pf;
unsigned int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;

/* Reserved for use in the future RHEL versions. Set to zero. */
unsigned long __rht_reserved1;
unsigned long __rht_reserved2;
unsigned long __rht_reserved3;
unsigned long __rht_reserved4;
unsigned long __rht_reserved5;
};
nf_hooks定义在net/netfilter/core.c
1
struct list_head nf_hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS] __read_mostly;

所有的回调函数都注册在二维数组全局变量nf_hooks中。list_head是linux内核中常用的双向链表结构,这里不关注。数组的第一个维度是注册回调函数的协议族,第二个维度是注册回调函数的hook点,也就是每一个协议族的每一个hook点都是一个双向链表连接的一组回调函数。NF_MAX_HOOKS值为8,不确定什么协议族有8个hook点。

nf_register_hook根据协议族和hook点确定nf_hooks中的链表,遍历链表根据nf_hook_ops中的优先级插入到链表的合适位置。nf_register_hooks只是nf_register_hook的循环包装,

注销回调函数时只是将该nf_hook_ops结构从链表中移出。

执行回调函数

以IPV4协议族收包为例,在确定网络层协议为IPV4协议后,内核进入ip_rcv函数。经过一系列检查后,最后运行到NF_HOOK,选择NFPROTO_IPV4协议族、NF_INET_PRE_ROUTINGhook点,同时设定协议栈下一步处理函数ip_rcv_finish(这个函数将在该hook点回调函数处理完毕并允许下一步逻辑执行时被调用,参考前文裁决值部分。

NF_HOOK开始的netfilter框架执行代码贴在这里,可以看到代码简短逻辑清晰,但能够支持内核模块(比如ip_tables模块)通过注册各种回调函数完成复杂功能。netfilter作为内核数据包过滤框架,很好的体现了“提供机制,而不是策略”的设计思想。

ip_rcv定义在net/ipv4/ip_input.c
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* Main IP Receive routine.
*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
/*
* ....................
*/

return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, NULL, skb,
dev, NULL,
ip_rcv_finish);
}
include/linux/netfilter.h
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
/**
* 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(u_int8_t pf, unsigned int hook,
struct sock *sk,
struct sk_buff *skb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sock *, struct sk_buff *),
int thresh)
{
if (nf_hooks_active(pf, hook)) {
struct nf_hook_state state;

nf_hook_state_init(&state, hook, thresh, pf,
indev, outdev, sk, okfn);
return nf_hook_slow(skb, &state);
}
return 1;
}

static inline int
NF_HOOK_THRESH(uint8_t pf, unsigned int hook, struct sock *sk,
struct sk_buff *skb, struct net_device *in,
struct net_device *out,
int (*okfn)(struct sock *, struct sk_buff *), int thresh)
{
int ret = nf_hook_thresh(pf, hook, sk, skb, in, out, okfn, thresh);
if (ret == 1)
ret = okfn(sk, skb);
return ret;
}

static inline int
NF_HOOK(uint8_t pf, unsigned int hook, struct sock *sk, struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct sock *, struct sk_buff *))
{
return NF_HOOK_THRESH(pf, hook, sk, skb, in, out, okfn, INT_MIN);
}
net/netfilter/core.c
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
unsigned int nf_iterate(struct list_head *head,
struct sk_buff *skb,
struct nf_hook_state *state,
struct nf_hook_ops **elemp)
{
unsigned int verdict;

/*
* The caller must not block between calls to this
* function because of risk of continuing from deleted element.
*/
list_for_each_entry_continue_rcu((*elemp), head, list) {
if (state->thresh > (*elemp)->priority)
continue;

/* Optimization: we don't need to hold module
reference here, since function can't sleep. --RR */
repeat:
verdict = (*elemp)->hook(*elemp, skb, state->in, state->out,
state);
if (verdict != NF_ACCEPT) {
#ifdef CONFIG_NETFILTER_DEBUG
if (unlikely((verdict & NF_VERDICT_MASK)
> NF_MAX_VERDICT)) {
NFDEBUG("Evil return from %p(%u).\n",
(*elemp)->hook, state->hook);
continue;
}
#endif
if (verdict != NF_REPEAT)
return verdict;
goto repeat;
}
}
return NF_ACCEPT;
}

/* Returns 1 if okfn() needs to be executed by the caller,
* -EPERM for NF_DROP, 0 otherwise. */
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state)
{
struct nf_hook_ops *elem;
unsigned int verdict;
int ret = 0;

/* We may already have this, but read-locks nest anyway */
rcu_read_lock();

elem = list_entry_rcu(&nf_hooks[state->pf][state->hook],
struct nf_hook_ops, list);
next_hook:
verdict = nf_iterate(&nf_hooks[state->pf][state->hook], skb, state,
&elem);
if (verdict == NF_ACCEPT || verdict == NF_STOP) {
ret = 1;
} else if ((verdict & NF_VERDICT_MASK) == NF_DROP) {
kfree_skb(skb);
ret = NF_DROP_GETERR(verdict);
if (ret == 0)
ret = -EPERM;
} else if ((verdict & NF_VERDICT_MASK) == NF_QUEUE) {
int err = nf_queue(skb, elem, state,
verdict >> NF_VERDICT_QBITS);
if (err < 0) {
if (err == -ECANCELED)
goto next_hook;
if (err == -ESRCH &&
(verdict & NF_VERDICT_FLAG_QUEUE_BYPASS))
goto next_hook;
kfree_skb(skb);
}
}
rcu_read_unlock();
return ret;
}