3

nf_ct_deliver_cached_events崩溃修复或规避方案

 1 year ago
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.
neoserver,ios ssh client

nf_ct_deliver_cached_events崩溃修复或规避方案

2023-02-15 Kernel

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

通过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

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK