之前的文章<<nf_ct_deliver_cached_events崩溃分析>> 分析了nf_conntrack
内核模块中存在的一个BUG。由于CentOS7
一直没有修复该问题,甚至到当前最新的CentOS8 stream
的kernel-4.18.0-383.el8
版本,这个问题依旧没有修复,这样就无法通过升级官方内核的方法来解决该问题了,只能我们自己来想办法进行修复或规避。
最直观的思路是修改代码后重新编译相关的内核模块进行替换。但在我们无法直接控制的环境中替换模块不是太理想,理想的方案还是能在我们的内核模块中进行修复或者规避。
类似于LivePatch
的思路,可以直接HOOK
存在BUG的函数:nf_conntrack_confirm
, 重新实现正确的逻辑。但该函数是inline
函数, 在内核中没有符号:
1 2 3 4 5 6 7 8 9 [root@k8smaster ~]# cat /proc/kallsyms |grep nf_conntrack_confirm ffffffffc0664050 r __ksymtab___nf_conntrack_confirm [nf_conntrack] ffffffffc0667b59 r __kstrtab___nf_conntrack_confirm [nf_conntrack] ffffffffc06647b0 r __kcrctab___nf_conntrack_confirm [nf_conntrack] ffffffffc06570e0 t __nf_conntrack_confirm [nf_conntrack] [root@k8smaster ~]# cat /proc/kallsyms |grep ipv4_confirm ffffffffb5e9b470 t ipv4_confirm_neigh ffffffffc061c280 t ipv4_confirm [nf_conntrack_ipv4]
通过kprobe
的pre_handler
来HOOK
函数ipv4_confirm
中调用nf_conntrack_confirm
的具体指令跳过后续的nf_conntrack_confirm
执行逻辑理论上是可行的,但毕竟要修改IP
寄存器,有较大的稳定性风险。所以不考虑这种方法。
退而求其次,既然nf_conntrack_confirm
无法HOOK
, 可以考虑HOOK
更外层的ipv4_confirm
函数。nf_conntrack_confirm
调用之前的逻辑不变,将nf_conntrack_confirm
调用改为调用正确的实现。这种方法看上去可接受。但存在一个比较大的问题,不同的版本内核如里ipv4_confirm
的逻辑不同,我们的实现也要跟着进行调整。可以作为一种备选方案。而HOOK
函数ipv4_confirm
可以使用ftrace
来进行。由于ipv4_confirm
本身是注册的netfilter
的回调函数, 因而也可以替换netfilter
的struct nf_hook_ops
结构中的相应的ipv4_confirm
。
因为ipv4_confirm
的注册优先级是NF_IP_PRI_CONNTRACK_CONFIRM
, 而NF_IP_PRI_CONNTRACK_CONFIRM
是INT_MAX
, 会在最后才调用,我们也可以在INT_MAX-1
的位置注册我们修复后的ipv4_confirm
, 在该函数中返回NF_STOP
跳过最后的原来的ipv4_confirm
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_CONNTRACK_DEFRAG = -400 , NF_IP_PRI_RAW = -300 , NF_IP_PRI_SELINUX_FIRST = -225 , NF_IP_PRI_CONNTRACK = -200 , NF_IP_PRI_MANGLE = -150 , NF_IP_PRI_NAT_DST = -100 , NF_IP_PRI_FILTER = 0 , NF_IP_PRI_SECURITY = 50 , NF_IP_PRI_NAT_SRC = 100 , NF_IP_PRI_SELINUX_LAST = 225 , NF_IP_PRI_CONNTRACK_HELPER = 300 , NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, NF_IP_PRI_LAST = INT_MAX, };
因为这个BUG的触发的一个先决条件就是conntrack
冲突的大量发生,这是由于NFQUEUE
导致的。因而我们可以考虑在NFQUEUE
之前就确认该conntrack entry
。因而我们可以在我们的内核模块中针对UDP
流量在返回NF_QUEUE_NR
之前,先调用内核的ipv4_confirm
。之后再将数据包通过NFQUEUE
机制送到用户态,减少conntrack entry
冲突的可能性。
由于在我们的内核模块中调用了一次ipv4_confirm
, ipv4_confirm
本身会被调用两次。从源码进行分析, 第二次执行ipv4_confirm
时,__nf_conntrack_confirm
不会再执行,而再次执行nf_ct_deliver_cached_events
会再次调用一次通知链来通知连接跟踪状态的改变,看上去没有什么影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static inline int nf_conntrack_confirm (struct sk_buff *skb) { struct nf_conn *ct = (struct nf_conn *)skb_nfct(skb); int ret = NF_ACCEPT; if (ct && !nf_ct_is_untracked(ct)) { if (!nf_ct_is_confirmed(ct)) ret = __nf_conntrack_confirm(skb); if (likely(ret == NF_ACCEPT)) nf_ct_deliver_cached_events(ct); } return ret; }
因为这种方案不需要重新实现ipv4_confirm
,直接调用原有的ipv4_confirm
,可以以最小的代价兼容多种不同的内核版本,因而选择这种方案。
修复的内核模块示意代码如下:
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 #define pr_fmt(fmt) "[%s]: " fmt, KBUILD_MODNAME #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/skbuff.h> #include <linux/ip.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> #include <linux/kallsyms.h> #include <net/udp.h> MODULE_LICENSE("GPL" ); MODULE_DESCRIPTION("nq" ); MODULE_ALIAS("module nq netfiler" ); static int nfqueue_no = 8 ;MODULE_PARM_DESC(nfqueue_no, "nfquene number" ); module_param(nfqueue_no, int , 0600 ); static int confirm_in_advance = 0 ;MODULE_PARM_DESC(confirm_in_advance, "Confirm conntrack entry in advacne" ); module_param(confirm_in_advance, int , 0600 ); typedef unsigned int (*orig_ipv4_confirm_t ) (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) ;static orig_ipv4_confirm_t orig_ipv4_confirm = NULL ;static unsigned int nf_hook_out (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 = ip_hdr(skb); u8 proto = iph->protocol; if (proto != IPPROTO_UDP) { return NF_ACCEPT; } if (orig_ipv4_confirm) { int ret = (*orig_ipv4_confirm)(ops, skb, in, out, state); if (ret != NF_ACCEPT) { return ret; } } return NF_QUEUE_NR(jiffies % nfqueue_no); } static struct nf_hook_ops nfhooks [] = { { .hook = nf_hook_out, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_POST_ROUTING, .priority = NF_IP_PRI_FIRST, }, }; int __init nq_init (void ) { if (confirm_in_advance) { orig_ipv4_confirm = kallsyms_lookup_name("ipv4_confirm" ); if (orig_ipv4_confirm == NULL ) { pr_crit("Cannot get ipv4_confirm address" ); return -1 ; } pr_info("Origianl ipv4_confirm: %p" , orig_ipv4_confirm); } nf_register_hooks(nfhooks, ARRAY_SIZE(nfhooks)); pr_info("module init\n" ); return 0 ; } void __exit nq_exit (void ) { nf_unregister_hooks(nfhooks, ARRAY_SIZE(nfhooks)); pr_info("module exit\n" ); return ; } module_init(nq_init); module_exit(nq_exit);
总结下来,修复或规避该BUG的方案可以有以下这几种:
重新编译nf_conntrack
模块进行替换
通过ftrace
替换为我们自己实现的ipv4_confirm
替换netfilter
中nf_hook_ops
结构中的ipv4_confirm
为我们自己实现的ipv4_confirm
在倒数第二优先级的位置注册我们自己实现的ipv4_confirm
,跳过最后的原始ipv4_confirm
在我们自己的内核模块中提前调用原始的ipv4_confirm
结合我们的主要场景,从兼容性和稳定性角度我们选择最后的方案。
参考: