本文部分翻译自Using NFQUEUE and libnetfilter_queue

NFQUEUE是iptables和ip6tables的一种target,用于将数据包委托给用户态应用程序裁决如何处理数据包。用户态应用程序可以使用libnetfilter_queue库连接到该队列获取包含了数据包的内核消息,并必须为数据包做出一个裁决。

下面是一个例子,将udp目的端口10010的发出包置入队列10010交由用户态应用程序裁决。

1
sudo iptables -A OUTPUT -p udp --dport 10010 -j NFQUEUE --queue-num 10010

原理

iptables依赖内核netfilter框架完成功能,是netfilter框架的一个用户态工具。NFQUEUE同样依赖netfilter框架并且需要内核包含nfnetlink_queue子系统(2.6.14及以后的内核版本)。

当一个数据包命中规则到达NFQUEUE target,数据包在内核中被放入以数字序号区分的队列,队列由固定长度的链表实现,链表中保存数据包及元数据(内核skb结构),当数据包收到用户态裁决时会从队列中释放,每一个数据包必须有一个裁决,队列满时新到达的数据包将被内核做drop处理。

用户态应用程序可以读取多个数据包以做出裁决,数据包的裁决可以与读取顺序无关,过慢的裁决对导致内核队列满,内核将drop新的数据包。

内核和用户态程序使用nfnetlink协议通信。这是一个完全基于消息的协议,不包含任何共享内存。当一个数据包入队列,内核向socket发送一个nfnetlink格式的消息,消息包含数据包数据和相关信息,用户态程序读取这个socket就可以获取消息。用户态程序裁决一个数据包时,需要组织一个nfnetlink格式的消息,消息中包含数据包在队列中的索引号,然后将消息发送给socket。

使用libnetfilter_queue

libnetfilter_queue的主要信息来源参考Doxygen generated documentation

更多生产环境代码例子可以参考suricatasource-nfq.c

centos7.5下安装libnetfilter_queue-devel以获取头文件及动态链接库。

1
sudo yum install libnetfilter_queue-devel

下面例子的代码演示了libnetfilter_queue的基本使用,包括基本的ACCEPT、DROP以及修改数据包后ACCEPT。另外使用中发现了一个libnetfilter_queue在计算udp校验和时的bug,因此将计算校验和的相关代码拷贝到了示例代码中并修复。该bug在libnetfilter_queue 1.0.3版本代码中依然存在。

nfqueue_example.c 代码中读取队列号10010中的数据,对所有读取到的tcp数据包做ACCEPT裁决,对udp目的端口10086的数据包做DROP裁决,对udp目的端口10010的数据包将udp数据全部修改为字符'h'并以'\n'结尾后返回修改后的数据及ACCEPT裁决。
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <errno.h>
#include <string.h>
#include <netinet/in.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/tcp.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <linux/netfilter.h>
#include <libnetfilter_queue/libnetfilter_queue.h>
#include <libnetfilter_queue/libnetfilter_queue_udp.h>
#include <libnetfilter_queue/libnetfilter_queue_ipv4.h>

#define QUEUE_NUM 10010

static uint16_t checksum(uint32_t sum, uint16_t *buf, int size)
{
while (size > 1) {
sum += *buf++;
size -= sizeof(uint16_t);
}
if (size)
sum += *(uint8_t *)buf;

sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >>16);

return (uint16_t)(~sum);
}

static uint16_t checksum_tcpudp_ipv4(struct iphdr *iph)
{
uint32_t sum = 0;
uint32_t iph_len = iph->ihl*4;
uint32_t len = ntohs(iph->tot_len) - iph_len;
uint8_t *payload = (uint8_t *)iph + iph_len;

sum += (iph->saddr >> 16) & 0xFFFF;
sum += (iph->saddr) & 0xFFFF;
sum += (iph->daddr >> 16) & 0xFFFF;
sum += (iph->daddr) & 0xFFFF;
sum += htons(iph->protocol);
// 这是一个bug
//sum += htons(IPPROTO_TCP);
sum += htons(len);

return checksum(sum, (uint16_t *)payload, len);
}
static void udp_compute_checksum_ipv4(struct udphdr *udph, struct iphdr *iph)
{
/* checksum field in header needs to be zero for calculation. */
udph->check = 0;
udph->check = checksum_tcpudp_ipv4(iph);
}


int cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_data *nfad, void *data) {
uint32_t id;
struct nfqnl_msg_packet_hdr *ph;
unsigned char *payload;
int r;
struct iphdr *iph;
struct udphdr *udph;
struct tcphdr *tcph;
char saddr_str[16];
char daddr_str[16];
struct in_addr tmp_in_addr;

ph = nfq_get_msg_packet_hdr(nfad);
if (ph) {
id = ntohl(ph->packet_id);
r = nfq_get_payload(nfad, &payload);
if (r >= sizeof(*iph)) {
iph = (struct iphdr *)payload;
tmp_in_addr.s_addr = iph->saddr;
strcpy(saddr_str, inet_ntoa(tmp_in_addr));
tmp_in_addr.s_addr = iph->daddr;
strcpy(daddr_str, inet_ntoa(tmp_in_addr));
if (iph->protocol == IPPROTO_UDP) {
if (iph->ihl * 4 + sizeof(*udph) <= r) {
udph = (struct udphdr *)(payload + iph->ihl * 4);
if (ntohs(udph->dest) == 10010) {
if (iph->ihl * 4 + sizeof(*udph) < r && ntohs(iph->tot_len) - iph->ihl * 4 == ntohs(udph->len) && ntohs(udph->len) - sizeof(*udph) > 0) {
int offset = iph->ihl * 4 + sizeof(*udph);
memset(payload + offset, 'h', ntohs(udph->len) - sizeof(*udph));
memset(payload + iph->ihl * 4 + ntohs(udph->len) - 1, '\n', 1);
// 由于修改了udp数据包内容,因此需要重新计算udp校验和。
// libnetfilter_queue在udp校验和计算时有bug,因此计算校验和代码拷贝到本文件后修复使用。
//nfq_udp_compute_checksum_ipv4(udph, iph);
udp_compute_checksum_ipv4(udph, iph);
printf("ACCEPT & modified protocol:udp %s:%u -> %s:%u\n", saddr_str, ntohs(udph->source), daddr_str, ntohs(udph->dest));
} else {
printf("ACCEPT & !modified protocol:udp %s:%u -> %s:%u\n", saddr_str, ntohs(udph->source), daddr_str, ntohs(udph->dest));
}
nfq_set_verdict(qh, id, NF_ACCEPT, r, payload);
} else if (ntohs(udph->dest) == 10086) {
printf("DROP protocol:udp %s:%u -> %s:%u\n", saddr_str, ntohs(udph->source), daddr_str, ntohs(udph->dest));
nfq_set_verdict(qh, id, NF_DROP, 0, NULL);
} else {
printf("ACCEPT protocol:udp %s:%u -> %s:%u\n", saddr_str, ntohs(udph->source), daddr_str, ntohs(udph->dest));
nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
} else {
printf("ACCEPT protocol:udp %s -> %s\n", saddr_str, daddr_str);
nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
} else if (iph->protocol == IPPROTO_TCP) {
if (iph->ihl * 4 + sizeof(*tcph) <= r) {
tcph = (struct tcphdr *)(payload + iph->ihl *4);
printf("ACCEPT protocol:tcp %s:%u -> %s:%u\n", saddr_str, ntohs(tcph->source), daddr_str, ntohs(tcph->dest));
nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
} else {
printf("ACCEPT protocol:tcp %s -> %s\n", saddr_str, daddr_str);
nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
} else {
printf("ACCEPT protocol:%d %s -> %s\n", iph->protocol, saddr_str, daddr_str);
nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
} else {
printf("ACCEPT unknown protocol\n");
nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
}

return 0;
}

int main() {
struct nfq_handle *h;
struct nfq_q_handle *qh;
int r;

char buf[10240];

h = nfq_open();
if (h == NULL) {
perror("nfq_open error");
goto end;
}

if (nfq_unbind_pf(h, AF_INET) != 0) {
perror("nfq_unbind_pf error");
goto end;
}
if (nfq_bind_pf(h, AF_INET) != 0) {
perror("nfq_bind_pf error");
goto end;
}

qh = nfq_create_queue(h, QUEUE_NUM, &cb, NULL);
if (qh == NULL) {
perror("nfq_create_queue error");
goto end;
}

if (nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff) != 0) {
perror("nfq_set_mod error");
goto end;
}

while(1) {
r = recv(nfq_fd(h), buf, sizeof(buf), 0);
if (r == 0) {
printf("recv return 0. exit");
break;
} else if (r < 0) {
perror("recv error");
break;
} else {
nfq_handle_packet(h, buf, r);
}
}

end:
if (qh) nfq_destroy_queue(qh);
if (h) nfq_close(h);
return 0;
}
编译代码并运行
1
2
gcc -Wall -g -l netfilter_queue nfqueue_example.c
sudo ./a.out
在另外的会话中设置iptables。在filter表OUTPUT链中添加规则,将tcp和udp目的端口10010、10086的数据包放入NFQUEUE队列号10010中。
1
2
3
4
sudo iptables -A OUTPUT -p udp --dport 10010 -j NFQUEUE --queue-num 10010
sudo iptables -A OUTPUT -p udp --dport 10086 -j NFQUEUE --queue-num 10010
sudo iptables -A OUTPUT -p tcp --dport 10010 -j NFQUEUE --queue-num 10010
sudo iptables -A OUTPUT -p tcp --dport 10086 -j NFQUEUE --queue-num 10010

使用nc测试几种数据包发送后,可以看到刚刚运行的程序输出。

1
2
3
4
ACCEPT  protocol:tcp 10.95.29.17:44660 -> 10.95.29.15:10010
ACCEPT protocol:tcp 10.95.29.17:39562 -> 10.95.29.15:10086
ACCEPT & modified protocol:udp 10.95.29.17:38600 -> 10.95.29.15:10010
DROP protocol:udp 10.95.29.17:46122 -> 10.95.29.15:10086

由于对OUTPUT包的修改在PF_PACKET数据包捕获之前,因此如果运行tcpdump抓取数据包,可以看到相应的ACCEPT、DROP及数据包内容的修改都已经生效。

如果是对INPUT包的修改,由于PF_PACKET数据包捕获在netfilter之前,那么tcpdump将无法看到修改后的内容,但是修改后的内容将对内核协议栈生效。

ps:测试环境使用完成后记得清理iptables规则。