nf_ct_deliver_cached_events崩溃修复或规避方案
source link: http://just4coding.com/2023/02/15/ct-fix/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
nf_ct_deliver_cached_events崩溃修复或规避方案
2023-02-15 Kernel
之前的文章<<nf_ct_deliver_cached_events崩溃分析>>分析了nf_conntrack
内核模块中存在的一个BUG。由于CentOS7
一直没有修复该问题,甚至到当前最新的CentOS8 stream
的kernel-4.18.0-383.el8
版本,这个问题依旧没有修复,这样就无法通过升级官方内核的方法来解决该问题了,只能我们自己来想办法进行修复或规避。
最直观的思路是修改代码后重新编译相关的内核模块进行替换。但在我们无法直接控制的环境中替换模块不是太理想,理想的方案还是能在我们的内核模块中进行修复或者规避。
类似于LivePatch
的思路,可以直接HOOK
存在BUG的函数:nf_conntrack_confirm
, 重新实现正确的逻辑。但该函数是inline
函数, 在内核中没有符号:
[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
。
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
会再次调用一次通知链来通知连接跟踪状态的改变,看上去没有什么影响。
/* 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
,可以以最小的代价兼容多种不同的内核版本,因而选择这种方案。
修复的内核模块示意代码如下:
#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的方案可以有以下这几种:
- 重新编译
nf_conntrack
模块进行替换 - 通过
ftrace
替换为我们自己实现的ipv4_confirm
- 替换
netfilter
中nf_hook_ops
结构中的ipv4_confirm
为我们自己实现的ipv4_confirm
- 在倒数第二优先级的位置注册我们自己实现的
ipv4_confirm
,跳过最后的原始ipv4_confirm
- 在我们自己的内核模块中提前调用原始的
ipv4_confirm
结合我们的主要场景,从兼容性和稳定性角度我们选择最后的方案。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK