Netfilter框架研究

新年的第一篇文章。2020年,冲!

Netfilter框架

netfilter是linux内核中的一个数据包处理框架,netfilter的功能包括数据包过滤、修改,SNAT/DNAT等.netfilter在内核协议栈的不同位置实现了5个hook点,其他内核模块(比如ip_tables)可以向这些hook点注册处理函数,这样当数据包经过这些hook点时,其上注册的处理函数就被依次调用,用户层工具像iptables可以向这些hook点注册处理函数,这样当数据包进过这些hook点时,其上注册的处理函数就被一次调用,用户曾工具像iptables一般都需要相应内核模块ip_tables配合以完成与netfilter的交互.netfilter hooks,ip{6}_tables,connection tracking和NAT子系统一起构成了netfilter框架的主要部分.

上图展示了netfilter框架在协议栈的位置,,通过此图清楚地显示了netfilter框架是如何处理不同协议栈路径上的数据包的.

iptables的定位是IPv4 packet filter,它只处理IP数据包,而ebtables只工作在链路层Link Layer处理的是以太网帧(比如修改源目mac地址)。图中用有颜色的长方形方框表示iptables或ebtables的表和链,绿色小方框表示network level,即iptables的表和链。蓝色小方框表示bridge level,即ebtables的表和链,由于处理以太网帧相对简单,因此链路层的蓝色小方框相对较少。

图中椭圆型的是conntrack,即connection tracking,这是netfilter提供的连接跟踪机制,此即使运行内核审查通过次数的所有网络数据包.并能识别次数据包属于哪个网络连接(比如数据包a属于IP1:8888->IP2:80这个tcp连接,数据包b属于IP3:9999->IP4:53这个udp连接).因此,连接跟踪机制使内核能够更正并记录通过此处所有网络链接及其状态.图中可以清楚看到连接跟踪代码所处的网络栈位置,如果不想让某些数据包被跟踪(NOTRACK),那就要找位于椭圆型conntrack之前的表和链来设置规则.conntrack机制是iptables实现状态匹配(-m state)以及NAT的基础,它由单独的内核模块nf_conntrack实现的.

connection tracking

当加载内核模块nf_conntrack后,conntrack机制就开始工作,如上图.椭圆型conntrack在内核中有两处位置(PREROUTING和OUTPUT之前)能够跟踪数据包.对于每个通过conntrack的数据包,内核都为其生成一个conntrack条目用以跟踪此连接,对于后续通过的数据包,内核会判断若此数据包属于一个已有的连接,则更新所对应的conntrack条目的状态(比如由原来的NEW状态变为ESTABLISHED状态),否则内核会为它新建一个conntrack条目.所有的conntrack条目都存放在一张表里,成为连接跟踪表.

连接跟踪表

查看当前系统的连接跟踪表

查看当前系统是否存在连接跟踪表之前,需要确认当前系统中存在conntrack模块.

1
sudo sysctl -a | grep conntrack

如果有结果输出,则说明机器中存在conntrack模块 (一般机器都存在这个模块)
连接跟踪表存放于系统内存中.不同的系统查看连接跟踪表的方式不同.
对于Centos

1
sudo cat /proc/net/nf_conntrack

Ubuntu没有/proc/net/nf_conntrack, 所以这个命令仅仅只是适用于centos.在ubuntu中可以使用如下:

1
2
sudo apt install conntrack
sudo conntrack -L -o extend

输出的结果示例:

1
ipv4 2 tcp 6 431916 ESTABLISHED src=172.22.44.167 dst=172.22.44.196 sport=44972 dport=18012 src=172.22.44.196 dst=172.22.44.167 sport=18012 dport=44972 [ASSURED] mark=0 zone=0 use=2

其中的括号表示的是flag. [ASSURED] 请求和响应都有流量 [UNREPLIED] 没收到响应,哈希表满的时候这些连接先扔掉.

每个conntrack条目表示一个连接,连接协议可以是tcp,udp,icmp等,它包含了数据包的原始方向信息(src=172.22.44.167 dst=172.22.44.196 sport=44972 dport=18012)和期望的响应包信息(src=172.22.44.196 dst=172.22.44.167 sport=18012 dport=44972),这样内核能够在后续到来的数据包是别处属于此连接的双向数据包,并更新此连接的状态.

在内核中,连接跟踪表是一个二维数组结构的哈系表(hash table),哈系表的大小记做HASHSIZE,哈系表的每一项(hash table entry)称作bucket,因此哈系表中有HASHSIZE个bucket存在.每个bucket包含一个链表(linked list),每个链表能够存放若干个conntrack条目(bucket size).对于一个新收到的数据包,内核使用如下步骤判断其是否属于一个已有的连接.

  • 内核提取次数据包信息(源目的IP,Port,协议号)进行Hash计算得到一个Hash值,在哈系表中以此Hash计算做索引,索引结果为数据包所属的bucket(链表).这一步hash计算时间固定并且很短.
  • 遍历hash得到的bucket查找是否有匹配的conntrack条目.bucket size越大,遍历时间越长.

设置最大连接跟踪数

系统最大允许连接跟踪数 CONNTRACK_MAX = 连接跟踪表大小(HASHSIZE)*Bucket大小(buecket size).从连接跟踪表获取bucket是hash操作时间很短,而遍历bucket相对费时,因此为了conntrack性能考虑,bucket size越小越好。

由于bucket size不能直接设置,为了是bucket size值为8,我们需要同时设置 CONNTRACK_MAX和 HASHSIZE,因为他们的比值就是bucket size.

计算连接跟踪所占内存

连接跟踪表存储在系统内存中,因此需要考虑内存占用问题,可以用下面公式计算设置不同的最大连接数所占最大系统内存.

1
total_mem_used(bytes) = CONNTRACK_MAX * sizeof(struct ip_conntrack) + HASHSIZE * sizeof(struct list_head)

例如,在默认情况下,最大连接跟踪数是262144,hashsize是65535.在测试ubuntu18.04上面,sizeof(struct ip_conntrack) 是376,sizeof(struct list_head)是16. 所以: total_mem_used=262144*376+65535*16=99614704,约为9MB.

conntrack条目

在 查看当前系统的连接跟踪表 中已经列举了一条conntrack记录.如下:

1
ipv4 2 tcp 6 431916 SYN_SENT src=172.22.44.167 dst=172.22.44.196 sport=44972 dport=18012  [UNREPLIED]  src=172.22.44.196 dst=172.22.44.167 sport=18012 dport=44972 mark=0 zone=0 use=2.

conntrack维护的所有信息都包含在这个条目中,通过它就可以知道某个链接处于什么状态.

  • ipv4,此连接使用的是ipv4的协议
  • 2 表示ipv4的网络层协议编号是2,参考 include/linux/socket.h中对所有的网络层协议号的定义.
  • tcp, 表示一个tcp连接
  • 6, 是tcp的协议编号 参考 /include/uapi/linux/in.h 中对所有的传输层协议号的定义
  • 431916, 是这条conntrack条目在当时时间点的生存时间/过期时间.(每个conntrack都会有生存时间,从设置值开始倒计时没倒计时完成后此条目将被清除).查看超时设置 中可以查看conntrack为所有的协议设置的超时时间.若后续有收到属于此连接的数据包,则此生存时间将被重置(重新从设置值开始倒计时,并且状态改变),生存时间设置值也会相应改为新状态的值.
  • SYN_SENT, 是到此刻为止conntrack跟踪到的这个连接的状态(内核角度),SYN_SENT表示这个连接只在一个方向发欧式你关了一个初始TCP SYN包,还未看到响应的SYN+ACK(只有tcp才会有这个字段)
  • src=172.22.44.167 dst=172.22.44.196 sport=44972 dport=18012 是从数据包中提取的此廉价的源目地址,源目端口,是conntrack首次看到次数据包时候的信息.
  • [UNREPLIED] 说明此刻为止,这个连接还没有收到任何响应,当一个连接已经收到响应, [UNREPLIED] 标志就会被移除
  • 接下来的src=172.22.44.196 dst=172.22.44.167 sport=18012 dport=44972 和前面的地址端口是相反的.这部分不是数据包中带有的信息,是conntrack填充的信息,代表conntrack希望收到的响应表信息.若后续conntrack跟踪到某个数据包信息与此部分匹配,则此数据包就是此链接的响应数据包.注意这部分确定了conntrack如何判断响应包(tcp/udp),icmp是依据另外几个字段.

conntrack机制并不能够修改或过滤数据包,它只是更正网络连接并维护链接跟踪表,以提供给iptables做状态匹配使用,也就是说,如果你iptables中用不到状态匹配,那就没有必要启用conntrack.

iptables的状态匹配

iptables是带有状态的防火墙,它使用-m state模块从链接跟踪表查找数据包状态.上面我们分析的那条conntrack条目处于SYN_SENT状态,这是内核记录的状态,数据包在内核中可能会有几种不同的状态,但是映射到用户空间iptables,只有5种状态可用:NEW,ESTABLISHED,RELATED,INVALID和UNTRACKED。这五种状态并不等同于tcp协议中的那2种状态,但是他们其中还是存在某种对应关系.

NEW

NEW匹配连接的第一个包。意思就是,iptables从连接跟踪表中查到此包是某连接的第一个包。判断此包是某连接的第一个包是依据conntrack当前”只看到一个方向数据包”([UNREPLIED]),不关联特定协议,因此NEW并不单指tcp连接的SYN包

ESTABLISHED

ESTABLISHED匹配连接的响应包及后续的包。意思是,iptables从连接跟踪表中查到此包是属于一个已经收到响应的连接(即没有[UNREPLIED]字段)。因此在iptables状态中,只要发送并接到响应,连接就认为是ESTABLISHED的了。这个特点使iptables可以控制由谁发起的连接才可以通过,比如A与B通信,A发给B数据包属于NEW状态,B回复给A的数据包就变为ESTABLISHED状态。ICMP的错误和重定向等信息包也被看作是ESTABLISHED,只要它们是我们所发出的信息的应答。

当一个连接与另一个已经是ESTABLISHED的连接有关时,这个连接就被认为是RELATED。这意味着,一个连接要想成为RELATED,必须首先有一个已经是ESTABLISHED的连接存在。这个ESTABLISHED连接再产生一个主连接之外的新连接,这个新连接就是RELATED状态了,当然首先conntrack模块要能”读懂”它是RELATED。拿ftp来说,FTP数据传输连接就是RELATED与先前已建立的FTP控制连接,还有通过IRC的DCC连接。有了RELATED这个状态,ICMP错误消息、FTP传输、DCC等才能穿过防火墙正常工作。有些依赖此机制的TCP协议和UDP协议非常复杂,他们的连接被封装在其它的TCP或UDP包的数据部分(可以了解下overlay/vxlan/gre),这使得conntrack需要借助其它辅助模块才能正确”读懂”这些复杂数据包,比如nf_conntrack_ftp这个辅助模块

INVALID

INVALID匹配那些无法识别或没有任何状态的数据包。这可能是由于系统内存不足或收到不属于任何已知连接的ICMP错误消息。一般情况下我们应该DROP此类状态的包

UNTRACKED

UNTRACKED状态比较简单,它匹配那些带有NOTRACK标签的数据包。需要注意的一点是,如果你在raw表中对某些数据包设置有NOTRACK标签,那上面的4种状态将无法匹配这样的数据包,因此你需要单独考虑NOTRACK包的放行规则

管理连接跟踪表

使用用户空间工具conntrack,提供了对连接跟踪表增删查改功能,可以使用 yum install conntrack-tools安装.使用方法如下:

1
2
3
4
5
6
# 显示所有的conntrack条目
$ conntrack -L
# 过滤信息
$ conntrack -L -p tcp --dport 34856
# 更新mark字段
$conntrack -U -p tcp --port 3486 --mark 10

Netfilter重要数据结构及相关函数

钩子点枚举类型

iptables&Netfilter简介中提到了网络层中Netfilter中的五个钩子节点(NF_IP_PRE_ROUTING,NF_IP_FORWARD,NF_IP_POST_ROUTING,NF_IP_LOCAL_IN,NF_IP_LOCAL_OUT),在内核中是以枚举数据类型进行标记的.如下:

1
2
3
4
5
6
7
8
9
#referer: include/uapi/linux/netfilter.h#L46
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
};

注册和解注册钩子函数

1
2
3
4
5
# referer:/include/linux/netfilter.h
int nf_register_hook(struct nf_hook_ops *reg);
void nf_unregister_hook(struct nf_hook_ops *reg);
int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n);
void nf_unregister_hooks(struct nf_hook_ops *reg, unsigned int n);

这些函数用于将自定义的钩子操作(struct nf_hook_ops)注册到制定的钩子节点中.

1
2
3
4
5
6
7
8
9
10
11
12
13
#referer:include/linux/netfilter.h#L59
struct nf_hook_ops
{
struct list_head list;

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

这个结构体中存储了自定义的钩子函数(nf_hookfn),函数优先级(priority),处理协议类型(pf),钩子函数神小的钩子节点(hooknum)等信息.
钩子函数声明

1
2
3
4
5
#referer:include/linux/netfilter.h#L53
typedef unsigned int nf_hookfn(unsigned int hooknum,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,

如果我们自己实现一个内核模块,该模块需要在Netfilter框架的几个钩子节点中对进程的数据包进行处理,则该内核米快需要向Netfilter中的钩子节点注册钩子函数,我们需要按照nf_hookfn函数的声明类型,提供我们的实现,再按照之前提供的注册接口将相关数据类型注册到内核中使之生效.

Netfilter hooks example

在NF_INET_LOCAL_IN处注册一个hook函数,打印数据包的源和目的MAC地址和IP地址.

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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/tcp.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>


MODULE_LICENSE("GPLv3");
MODULE_AUTHOR("SHI");
MODULE_DESCRIPTION("Netfliter test");

static unsigned int
nf_test_in_hook(unsigned int hook, struct sk_buff *skb, const struct net_device *in,
const struct net_device *out, int (*okfn)(struct sk_buff*));

static struct nf_hook_ops nf_test_ops[] __read_mostly = {
{
.hook = nf_test_in_hook,
.owner = THIS_MODULE,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN,
.priority = NF_IP_PRI_FIRST,
},
};

void hdr_dump(struct ethhdr *ehdr) {
printk("[MAC_DES:%x,%x,%x,%x,%x,%x"
"MAC_SRC: %x,%x,%x,%x,%x,%x Prot:%x]\n",
ehdr->h_dest[0],ehdr->h_dest[1],ehdr->h_dest[2],ehdr->h_dest[3],
ehdr->h_dest[4],ehdr->h_dest[5],ehdr->h_source[0],ehdr->h_source[1],
ehdr->h_source[2],ehdr->h_source[3],ehdr->h_source[4],
ehdr->h_source[5],ehdr->h_proto);
}

#define NIPQUAD(addr) \
((unsigned char *)&addr)[0], \
((unsigned char *)&addr)[1], \
((unsigned char *)&addr)[2], \
((unsigned char *)&addr)[3]
#define NIPQUAD_FMT "%u.%u.%u.%u"

static unsigned int
nf_test_in_hook(unsigned int hook, struct sk_buff *skb, const struct net_device *in,
const struct net_device *out, int (*okfn)(struct sk_buff*)) {
struct ethhdr *eth_header;
struct iphdr *ip_header;
eth_header = (struct ethhdr *)(skb_mac_header(skb));
ip_header = (struct iphdr *)(skb_network_header(skb));
hdr_dump(eth_header);
printk("src IP:'"NIPQUAD_FMT"', dst IP:'"NIPQUAD_FMT"' \n",
NIPQUAD(ip_header->saddr), NIPQUAD(ip_header->daddr));
return NF_ACCEPT;
}

static int __init init_nf_test(void) {
int ret;
ret = nf_register_hooks(nf_test_ops, ARRAY_SIZE(nf_test_ops));
if (ret < 0) {
printk("register nf hook fail\n");
return ret;
}
printk(KERN_NOTICE "register nf test hook\n");
return 0;
}

static void __exit exit_nf_test(void) {
nf_unregister_hooks(nf_test_ops, ARRAY_SIZE(nf_test_ops));
}

module_init(init_nf_test);
module_exit(exit_nf_test)

上面的是LKM.关于LKM,可以参考lkm入门&netlink通信示例

  • 在init_nf_test, 其通过Netfilter提供的nf_register_hooks接口将自定义的nf_test_ops注册到钩子节点中.nf_test_ops是一个nf_hook_ops的结构体,其内部包含了所有的关键元素.
  • nf_test_in_hook函数内部,检查没一个传递过来的数据包,将比源MAC地址,目的MAc地址,源IP地址以及目的IP地址打印出来.最后返回NF_ACCEPT,将数据包交给下一个钩子函数处理.

载入此LKM,通过 dmesg|tail 得到的结果是:

1
2
3
4
5
6
7
8
9
10
[452013.507230] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8]
[452013.507237] src IP:'10.6.124.55', dst IP:'10.6.124.54'
[452013.944960] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8]
[452013.944968] src IP:'10.6.124.55', dst IP:'10.6.124.54'
[452014.960934] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8]
[452014.960941] src IP:'10.6.124.55', dst IP:'10.6.124.54'
[452015.476335] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8]
[452015.476342] src IP:'10.6.124.55', dst IP:'10.6.124.54'
[452016.023311] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8]
[452016.023318] src IP:'10.6.124.55', dst IP:'10.6.124.54'

参考

云计算底层技术-netfilter框架研究
Linux 网络层收发包流程及 Netfilter 框架浅析