ConnTrack与NAT源码分析
1. Netfilter Packet Flow
关注上图中conntrack的位置,包含了很多设计上的考虑。
要跟踪有效连接,有效性要求数据包在进入应用之前或者离开网卡之前要通过所有检查。
要尽可能早的跟踪连接,越早介入干扰越小。
要达成这些目的,必须确保数据包通过所有检查 ,并且尽可能早生成跟踪信息,因此这里涉及一个开始生成和最终确认的过程。
开始生成
PRE_ROUTING
是接收方向的数据包最先到达的地方,因此要在这个点生成跟踪信息。LOCAL_OUT
是发送方向的数据包最先开始的地方,因此要在这个点生成跟踪信息。
最终确认
LOCAL_IN
是数据包通过所有检查, 到达应用之前的最后一个hook点,因此要在这个点确认。POST_ROUTING
是数据包通过所有检查, 离开主机之前的最后一个hook点,因此要在这个点确认。
2. 注册IPv4的conntrack
2.1 初始化IPv4 conntrack
nf_conntrack_l3proto_ipv4_init()
是一个内核模块初始化函数,负责初始化IPv4协议的conntrack支持。
调用
need_conntrack()
:表示该功能需要连接跟踪的支持。启用IPv4分片重组(defragmentation) :通过调用
nf_defrag_ipv4_enable()
开启对IPv4数据包分片的重组功能。注册获取原始目的地地址的套接字选项 :调用
nf_register_sockopt(&so_getorigdst)
允许用户空间程序通过套接字选项获取经过NAT转换后数据包的原始目的地地址。注册网络命名空间子系统操作 :调用
register_pernet_subsys(&ipv4_net_ops)
将IPv4连接跟踪的相关操作注册到网络命名空间子系统中。注册Netfilter钩子 :调用
nf_register_hooks(ipv4_conntrack_ops, ARRAY_SIZE(ipv4_conntrack_ops))
,将IPv4连接跟踪的处理函数注册到Netfilter框架的不同钩子点上。注册四层协议处理模块 :分别注册TCP、UDP和ICMPv4的四层协议处理模块,这些模块用于在连接跟踪过程中处理不同类型的IPv4数据包。
注册三层协议处理模块 :最后注册三层协议处理模块,即IPv4协议本身。
若配置支持,初始化
proc
文件系统兼容接口。如果上述任何一步失败,则按照反向顺序进行清理,如取消注册已注册的模块、钩子以及sockopt等。
整个过程确保了IPv4协议及其常用上层协议(TCP、UDP、ICMP)的连接跟踪功能能够正确地初始化并集成到Linux内核的Netfilter子系统中。
2.2. 注册IPv4 conntrack hook函数
在nf_conntrack_l3proto_ipv4_init()
函数中有如下代码,目的是注册Netfilter框架中的hooks,将IPv4 conntrack的操作关联到特定的Netfilter钩子点上,如NF_INET_PRE_ROUTING
、 NF_INET_LOCAL_IN
等,以便在相应阶段处理和追踪IPv4数据包。
注册的hook最终调用到nf_conntrack_in()
,如下图表示。
nf_conntrack_in
函数在netfilter
框架中负责处理进入的数据包,并结合conntrack对数据包进行状态检查、创建或更新相应的conntrack记录,以支持如NAT等网络服务以及防火墙的状态检测功能,重点关注resolve_normal_ct()
函数。
resolve_normal_ct
函数的的核心功能是处理流入的数据包,将它们关联到正确的网络连接跟踪上下文中,并维护这些连接的状态。
从数据包中提取五元组(即源IP、源端口、目标IP、目标端口和协议类型)信息,构建一个
nf_conntrack_tuple
结构体。使用哈希函数计算五元组的哈希值,并在conntrack表中查找匹配项,如果没有找到匹配项,则初始化一个新的连接跟踪条目。
如果找到匹配项,则根据其方向(回复还是原始数据包)确定当前数据包在连接中的状态,并通过
ctinfo
参数返回这个信息(如新建连接、已建立连接的回复部分、相关连接等)。设置
skb->nfct
指向找到或创建的连接跟踪记录,并设置skb->nfctinfo
为相应连接状态信息。
3. NAT实现原理
在这里先提一个问题:DNAT信息是从iptables规则来的还是从conntrack记录来的?
答案在后面总结。
3.1 NAT hook点及处理函数
nf_nat_ipv4_ops
定义了四个nf_hook_ops
结构体实例,用于在Netfilter框架中注册钩子函数,以便在网络数据包处理的不同阶段执行相应的NAT操作。
nf_hook_ops
解释如下:
.hook
:指向一个函数指针,当数据包到达对应的网络过滤点时会调用此函数。这里分别定义了四种不同的处理函数:nf_nat_ipv4_in
:在数据包进入本地网络之前进行目标IP地址的DNAT转换,即改变目的地址。nf_nat_ipv4_out
:在数据包离开本地网络后进行源IP地址的SNAT转换,即改变源地址。nf_nat_ipv4_local_fn
:对于本地生成并即将离开主机的数据包,在输出前执行目标IP地址的DNAT转换。nf_nat_ipv4_fn
:对于进入本地主机的数据包,在输入后执行源IP地址的SNAT转换。
.owner
:标识当前模块,这里是THIS_MODULE
,表示这些钩子函数属于当前正在初始化的内核模块。.pf
:定义了协议族,这里是NFPROTO_IPV4
,表明这些钩子函数适用于IPv4协议的数据包。.hooknum
:指定了Netfilter框架中的特定钩子点位置,包括:NF_INET_PRE_ROUTING
:预路由阶段,数据包刚进入系统还未进行路由决策时。NF_INET_POST_ROUTING
:后路由阶段,数据包已确定路由准备离开本地网络时。NF_INET_LOCAL_OUT
:本地输出阶段,数据包由本地进程生成并准备发送出去时。NF_INET_LOCAL_IN
:本地输入阶段,数据包已经到达本地主机,并经过初步处理后。
.priority
:定义了钩子函数在对应钩子点上的优先级,这里设置为与NAT相关的优先级值NF_IP_PRI_NAT_DST
和NF_IP_PRI_NAT_SRC
,确保这些NAT操作能够按照预期顺序执行。
通过注册一系列的Netfilter钩子函数,实现了在IPv4网络数据包的不同处理阶段执行必要的NAT转换功能,具体注册代码如下。
3.2 Iptables NAT初始化
Linux内核模块初始化时会执行iptable_nat_init()
,用于初始化iptables的NAT功能,这个函数会在Netfilter框架中注册钩子函数,这样就可以捕获数据包并对其进行修改。
Netfilter钩子注册的代码如下,可以看到除了NF_INET_FORWARD
外其他hook点都注册了钩子函数。
上述代码将nf_nat_ipv4_ops
定义的一组钩子函数注册到Netfilter框架中。这些函数在数据包通过不同阶段时执行,负责实现IPv4协议下的DNAT、SNAT等功能。
3.3 DNAT实现逻辑
根据nf_nat_ipv4_ops
可以找到hook点及对应的处理函数,对于DNAT,有如下两条路,最终都会调用到nf_nat_ipv4_fn()
函数。
nf_nat_packet()
是处理NAT的函数,负责根据给定的连接跟踪信息(ct
)、连接跟踪状态信息(ctinfo
)、当前钩子点编号(hooknum
)以及待处理的数据包(skb
),对数据包执行相应的源或目标NAT操作。
首先,根据
hooknum
参数确定要进行的是源NAT(SNAT)还是目标NAT(DNAT),并设置相应的状态位标志statusbit
。判断数据包的方向(
dir
),如果是回复方向,则翻转状态位标志。检查当前连接的状态(
ct->status
)是否设置了与NAT类型对应的状态位。如果设置了,继续执行以下步骤:
初始化一个目标五元组结构体
target
。使用
nf_ct_invert_tuplepr()
函数从当前连接的另一方向的五元组生成反向五元组,这个反向五元组将作为NAT的目标配置。根据反向五元组的信息找到对应的第三层(L3)和第四层(L4)协议处理模块。
调用三层协议处理模块(
l3proto
)的manip_pkt
方法来实际修改数据包的内容,包括IP地址和端口号等,实现NAT转换。若调用失败,返回NF_DROP
表示丢弃数据包。
如果未执行NAT操作或者NAT操作成功,则返回
NF_ACCEPT
,允许数据包继续在网络中传输。
3.4 三层IP NAT
nf_nat_ipv4_manip_pkt()
是IPv4协议的NAT处理函数,主要作用是对IPv4数据包进行源IP地址或目标IP地址的替换,并更新相应的校验和。
首先,确保数据包头部在内核可写缓冲区中,调用
skb_make_writable()
函数来调整数据包的状态以使其内容可以被修改。如果无法使数据包变为可写,则返回false
。通过计算偏移量获取到IPv4头部指针
iph
。调用特定第四层(L4)协议的处理模块(由
l4proto
指向)的manip_pkt
方法来处理对应于上层协议(如TCP、UDP等)的端口转换或其他特定操作。如果这个过程失败,则返回false
。根据
maniptype
参数判断是要执行源NAT(SNAT)还是目标NAT(DNAT)。
对于SNAT,将原始数据包中的源IP地址替换为目标五元组结构体
target
中的新源IP地址。对于DNAT,则替换目标IP地址为
target->dst.u3.ip
对应的目标地址,target
是在nf_nat_ipv4_fn()
中获取,一路传下来的,可以理解为是从conntrack查询而来的。同时,利用
csum_replace4()
函数更新IPv4头部的校验和字段以反映IP地址的变化。
如果所有修改成功完成,函数返回
true
,表示数据包已成功进行了NAT转换。
3.5 四层UDP端口NAT
如下分析UDP的端口NAT操作函数,TCP类似。
udp_manip_pkt()
是处理UDP协议NAT操作的函数,主要任务是在给定的网络数据包中修改源或目标UDP端口号,以实现源地址转换(SNAT)或目标地址转换(DNAT)。
首先检查并确保数据包头部可写,通过调用
skb_make_writable()
函数移动数据包至内核内存区域,以便可以直接修改其内容。获取指向UDP头部的指针
hdr
。根据传入的
maniptype
参数判断是要修改源端口还是目标端口:
如果是
NF_NAT_MANIP_SRC
(源NAT),则获取新源端口newport
(从tupple->src.u.udp.port
获得),并将portptr
指向hdr->source
。如果是
NF_NAT_MANIP_DST
(目标NAT),则获取新目标端口newport
(从tupple->dst.u.udp.port
获得),并将portptr
指向hdr->dest
。
检查UDP校验和是否有效或者数据包的校验和状态为部分校验。如果满足条件,则进行以下步骤:
调用三层协议(L3,如IPv4或IPv6)对应的
csum_update
方法更新整个IP头及TCP/UDP校验和的相关信息。使用
inet_proto_csum_replace2
函数替换旧端口号为新端口号,并相应地更新校验和值。若更新后的校验和为0,则设置为
CSUM_MANGLED_0
,表示校验和已计算但结果为0。
最后将新的端口号赋值给原始端口号所在的内存位置。
函数返回
true
表示成功执行了对UDP数据包的端口号修改操作。
3.6 根据iptables规则设置conntrack
这里再总结下NAT处理链,从PRE_ROUTING挂载的hook函数开始,分析下iptables规则及NAT是如何执行的。
nf_nat_rule_find
的功能是查找和应用NAT规则,并对符合条件的数据包执行相应的NAT转换或维护必要的连接状态。
使用
ipt_do_table()
函数处理给定的数据包(skb
),根据其在Netfilter钩子链中的位置(hooknum
)、输入设备(in
)、输出设备(out
)以及指向IPv4 NAT表(net->ipv4.nat_table
)的指针来查找匹配的NAT规则,这个过程会遍历所有已配置的iptables NAT规则,并对数据包进行相应的操作,如果找到匹配的规则并执行成功,则返回NF_ACCEPT
。如果
ipt_do_table()
函数返回了NF_ACCEPT
,则进一步检查当前连接跟踪条目(ct
)是否已经为当前钩子点初始化了NAT信息,如果没有初始化,调用alloc_null_binding()
函数为该连接分配一个NULL binding,确保后续的NAT处理能够正确进行。
这里就把conntrack和NAT需要的信息关联上了,第一次是从iptable规则中查,然后保存到conntrack中,后续就从conntrack中查找了,相当于缓存了NAT规则信息。
现在可以回答“ DNAT信息是从iptables规则来的还是从conntrack记录来的? ”这个问题了。
答:从上述分析可以看出,Linux内核中处理NAT的函数是根据iptables规则确定的DNAT信息,并结合conntrack记录来执行实际的网络地址转换。
当数据包经过Netfilter框架时,iptables规则首先在相应的钩子点上被应用。如果一个数据包与DNAT规则匹配,则会在conntrack中创建或更新一个条目,该条目记录了用于NAT转换的目标IP地址和端口号等信息。
然后,在后续的数据包处理过程中,如
nf_nat_packet()
这样的函数会使用之前由iptables规则生成并存储在conntrack表中的NAT配置信息来修改数据包的内容,实现NAT。
总结一下,iptables规则定义了NAT应该如何发生,而conntrack则责维护这些规则应用后的状态,并确保双向通信过程中的地址一致性。两者紧密结合共同完成了Linux内核中的NAT功能。
4. UDP的conntrack
在Linux内核的Netfilter连接跟踪子系统中, nf_conntrack_tuple
结构体用于存储网络数据包的关键信息以唯一标识一个网络连接的方向。对于UDP协议, nf_conntrack_tuple
结构通常包含以下信息:
源IP地址 (sip):发送方IP地址。
目标IP地址 (dip):接收方IP地址。
源端口号 (sport):发送方使用的UDP端口号。
目的端口号 (dport):接收方监听的UDP端口号。
协议号 (proto):在网络层通常是IPPROTO_UDP。
根据上述信息,得出结论: 在Kubernetes环境中,如果多次域名解析请求的sip/sport相同,那么会命中同一条cnntrack记录,因为dport永远是53,dip永远是同一个VIP,协议号永远是udp的协议号。
5. 案例分析
在kubernetes环境中,使用UDP协议解析域名, coreDNS
异常重启,出现如下情况。
应用POD的
resolv.conf
文件中的nameserver配置相同的VIP,因此nf_conntrack_tuple
中的dip/dport/proto
都相同。如果应用IP和端口号不变,也就是
nf_conntrack_tuple
中的sip/sport
也相同,那发送方向的nf_conntrack_tuple
的所有元素都相同了。此时
coreDNS
重启。如果上述应用持续向
coreDNS
发送请求,可能会命中kernel bug,导致无效conntrack记录未删除。因为发送方向上
nf_conntrack_tuple
的所有元素都相同,新的DNS解析数据包,命中无效的conntrack记录。
此时会导致什么问题?
答:根据上述分析,此时会导致DNAT出错,比如将数据包的目标地址修改为POD重启前的IP。