Keep learning, keep living...

0%

nf_ct_deliver_cached_events崩溃修复或规避方案

之前的文章<<nf_ct_deliver_cached_events崩溃分析>>分析了nf_conntrack内核模块中存在的一个BUG。由于CentOS7一直没有修复该问题,甚至到当前最新的CentOS8 streamkernel-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]

通过kprobepre_handlerHOOK函数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的回调函数, 因而也可以替换netfilterstruct nf_hook_ops结构中的相应的ipv4_confirm

因为ipv4_confirm的注册优先级是NF_IP_PRI_CONNTRACK_CONFIRM, 而NF_IP_PRI_CONNTRACK_CONFIRMINT_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
/* Confirm a connection: returns NF_DROP if packet must be dropped. */
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);

//net_crit_ratelimited("original ipv4_confirm called");

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的方案可以有以下这几种:

  1. 重新编译nf_conntrack模块进行替换
  2. 通过ftrace替换为我们自己实现的ipv4_confirm
  3. 替换netfilternf_hook_ops结构中的ipv4_confirm为我们自己实现的ipv4_confirm
  4. 在倒数第二优先级的位置注册我们自己实现的ipv4_confirm,跳过最后的原始ipv4_confirm
  5. 在我们自己的内核模块中提前调用原始的ipv4_confirm

结合我们的主要场景,从兼容性和稳定性角度我们选择最后的方案。

参考: