阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

在Linux的连接跟踪(nf_conntrack)中缓存私有数据省去每次查找

404次阅读
没有评论

共计 28752 个字符,预计需要花费 72 分钟才能阅读完成。

前面说过很多次,conntrack 作为一中连接跟踪机制,如果它本身是可扩展的,那么将会是多么令人激动的一件事,当你看了 N 多文档代码之后,你发现它确实是可以扩展的,但是却没有感到激动,因为你可能发现:

1. 它可以注册一个 account 扩展,但是计数机制却很原始;
2. 我希望增加一个新型的扩展,却不得不重新编译内核;

怎么办?我曾经很生气地默默指责过当初实现这个的人,想当然的认为将扩展本身也做成可扩展的,而不是写死几个特定的扩展将是一个多么容易的事,我一直憋着没有去做这个实现,就是因为觉得它太简单,在工作中也确实需要一个新的扩展,然而既有的扩展类型中没有,为了不重新编译内核,我只好盗用了 acct 扩展。采用了一个 OO 中典型的封装方法:

struct my_ext {
    struct orig_ext;
    char info[0];
};


是时候改变一些事态了。基于下面几个原因,在周六的早上,我突然决定在周末完成它:
1. 外部因素:好不容易感冒了,作为一个羸弱的人,我不希望得到别人的同情,只需要获得周末的安静,感冒发烧是最好的选择;
2. 内部因素:年终总结完了,工作计划也确定了,后面是个收网的过程,稳为重,不需要太激进,因此也就没有什么技术上不可控的因素,心理安了,事就可以开始做了;
可能我又要笑话自己了,不就写个简单的模块么?怎么搞得跟诸葛孔明布阵一样 … 如此感性且主观一人怎么就 …
      不管怎么讲,这个模块看起来确实是简单的。然而一旦做起来,发现有两个比较严重的问题:
1. 反射内省问题
如果 conntrack 的 extend 有 128 个 slot,每个 slot 里面放一个私有数据。问题是,程序怎么知道哪个 slot 里面有哪个数据。程序有能力存储,但是程序自己却不知道这一点 … 这就是一个怪圈,你必须让数据成为自描述的,或者就规定死第 n 号 slot 必须放路由项,第 m 个 slot 必须放 socket… 现有的 nf conntrack 模块使用了后一种方法,即枚举 nf_ct_ext_id 做的事。
      可是我还是想随机选择 slot,这样更加灵活。自描述的数据结构也看了不好,ASN.1 太复杂,且内核数据更多的不是标识属性,而是定义一种行为,google 的 protocol buffer 也不是很合适,需要定义太多的回调函数来完成反射自省.. 后来我想了一个办法,那就是定义个索引蓝图,标识“slot 索引的索引”,而不是标识具体 slot 的位置。
      这就需要定义一个新的枚举,定义蓝图:

enum idx_idx{
ROUTE,
SOCKET,
AND_SO_ON,
IDX_IDX_NUM
};

然后定义一个数组来标识真正的索引:

int idx[IDX_IDX_NUM];

定义一个 bitmap 来表示 slot 的使用情况即可,具体的做法可以看代码,一目则了然。
2. 内存寻址问题
内核内存是宝贵的,不是说物理内存用不起,而是它的虚拟地址空间也是有限的,因此建议使用 64 位系统,如果是 32 位系统,如果希望内核保存比较大的数据结构,请在编译的时候按照 2G/2G 或者 1G/3G 来拆分地址空间,前者情况用户和内核各自占据 2G,后者的话内核占 3G,用户仅仅占 1G。
      也许就是因为存在这个内存问题,Linux 的 nf conntrack 限制了 extend 的内存使用,其最大长度字段数据类型是 u8。由于我知道我的系统,所以我将其改为了 u16。你必须要知道的是,nf connrtack 的 extend 内存使用时是连续的,你不能采用一个 sizeof(char *)大小的空间保存一个指针,然后这个指针指向一个超级大的连续空间 … 但是为什么不能呢?还是因为代码的普适问题,我了解我的系统,所以我可以使用保存指针的做法。另外我还保留了数组的方式,总之,数组和指针分工是明确的,数组用于 extend 的寻址,而指针用于数据的获取。
      代码包括一个框架和一个测试程序,内核还是 2.6.32 amd64,已经在 github 上了:https://github.com/marywangran/extension-of-nf_conntrack-ext
      还是在这里贴一份备份,怕哪天 github 被 wall 了 …

修改 include/net/netfilter/nf_conntrack_extend.h:

 

--- nf_conntrack_extend.h.orig  2014-03-29 12:55:26.000000000 +0800
+++ nf_conntrack_extend.h   2015-01-15 17:28:39.000000000 +0800
@@ -3,13 +3,17 @@

 #include <net/netfilter/nf_conntrack.h>

+#define NFCT_EXT_EXT
+
 enum nf_ct_ext_id
 {
    NF_CT_EXT_HELPER,
    NF_CT_EXT_NAT,
    NF_CT_EXT_ACCT,
    NF_CT_EXT_ECACHE,
-   NF_CT_EXT_NEW,
+#ifdef NFCT_EXT_EXT
+   NF_CT_EXT_EXT,
+#endif
    NF_CT_EXT_NUM,
 };

@@ -17,13 +21,21 @@
 #define NF_CT_EXT_NAT_TYPE struct nf_conn_nat
 #define NF_CT_EXT_ACCT_TYPE struct nf_conn_counter
 #define NF_CT_EXT_ECACHE_TYPE struct nf_conntrack_ecache
-#define NF_CT_EXT_NEW_TYPE struct nf_conntrack_new
+#ifdef NFCT_EXT_EXT
+#define NF_CT_EXT_EXT_TYPE struct nf_conntrack_ext
+#endif

 /* Extensions: optional stuff which isn't permanently in struct. */
 struct nf_ct_ext {
    struct rcu_head rcu;
+#ifdef NFCT_EXT_EXT
+   /* 内存不再是个事儿 */
+   u16 offset[NF_CT_EXT_NUM];
+   u16 len;
+#else
    u8 offset[NF_CT_EXT_NUM];
    u8 len;
+#endif
    char data[0];
 };

@@ -80,10 +92,18 @@
    unsigned int flags;

    /* Length and min alignment. */
+#ifdef NFCT_EXT_EXT
+   /* 内存不再是个事儿 */
+   u16 len;
+   u16 align;
+   /* initial size of nf_ct_ext. */
+   u16 alloc_size;
+#else
    u8 len;
    u8 align;
    /* initial size of nf_ct_ext. */
    u8 alloc_size;
+#endif
 };

 int nf_ct_extend_register(struct nf_ct_ext_type *type);

 

增加 include/net/netfilter/nf_conntrack_ext.h:

 

/*
 * (C) 2015 marywangran <marywangran@126.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#ifndef _NF_CONNTRACK_EXT_H
#define _NF_CONNTRACK_EXT_H
#include <net/net_namespace.h>
#include <linux/netfilter/nf_conntrack_common.h>
#include <linux/netfilter/nf_conntrack_tuple_common.h>
#include <net/netfilter/nf_conntrack.h>
#include <net/netfilter/nf_conntrack_extend.h>

#define MAX_EXT_SLOTS	8	
#define BITINT	1


struct nf_conntrack_ext {
	/* 必须有一个数组用于自省或者反射 */
	int	bits_idx[MAX_EXT_SLOTS];
	int	bits[BITINT];
	char *slot[MAX_EXT_SLOTS];
};


int nf_ct_exts_add(const struct nf_conn *ct, void *ext);

void *nf_ct_exts_get(const struct nf_conn *ct, int idx); 

void nf_ct_exts_remove(const struct nf_conn *ct, int idx);

struct nf_conntrack_ext *nf_conn_exts_find(const struct nf_conn *ct);

struct nf_conntrack_ext *nf_conn_exts_add(struct nf_conn *ct, gfp_t gfp);
extern int nf_conntrack_exts_init();
extern void nf_conntrack_exts_fini();

#endif /* _NF_CONNTRACK_EXT_H */

 

修改 net/netfilter/nf_conntrack_core.c:

 

--- nf_conntrack_core.c.orig    2014-03-29 13:00:17.000000000 +0800
+++ nf_conntrack_core.c 2015-01-15 17:01:28.000000000 +0800
@@ -42,6 +42,10 @@
 #include <net/netfilter/nf_conntrack_extend.h>
 #include <net/netfilter/nf_conntrack_acct.h>
 #include <net/netfilter/nf_conntrack_ecache.h>
+#ifdef NFCT_EXT_EXT
+/* 引入 extend 的 extend 头文件 */
+#include <net/netfilter/nf_conntrack_ext.h>
+#endif
 #include <net/netfilter/nf_nat.h>
 #include <net/netfilter/nf_nat_core.h>

@@ -644,8 +648,11 @@
    }

    nf_ct_acct_ext_add(ct, GFP_ATOMIC);
-
    nf_ct_ecache_ext_add(ct, GFP_ATOMIC);
+#ifdef NFCT_EXT_EXT
+   /* 在创建 conntrack 的时候初始化 extend 的 extend */
+   nf_conn_exts_add(ct, GFP_ATOMIC);
+#endif

    spin_lock_bh(&nf_conntrack_lock);
    exp = nf_ct_find_expectation(net, tuple);
@@ -1130,6 +1137,10 @@

    nf_ct_free_hashtable(net->ct.hash, net->ct.hash_vmalloc,
                 net->ct.htable_size);
+#ifdef NFCT_EXT_EXT
+   /* 析构 extend 的 extend */
+   nf_conntrack_exts_fini();
+#endif
    nf_conntrack_ecache_fini(net);
    nf_conntrack_acct_fini(net);
    nf_conntrack_expect_fini(net);
@@ -1344,9 +1355,19 @@
    ret = nf_conntrack_ecache_init(net);
    if (ret < 0)
        goto err_ecache;
+#ifdef NFCT_EXT_EXT
+   /* 注册 extend 的 extend */
+   ret = nf_conntrack_exts_init();
+   if (ret < 0)
+       goto err_exts;
+#endif

    return 0;

+#ifdef NFCT_EXT_EXT
+err_exts:
+   nf_conntrack_ecache_fini(net);
+#endif
 err_ecache:
    nf_conntrack_acct_fini(net);
 err_acct:

 

增加 net/netfilter/nf_conntrack_ext.c:

 

/* conntrack 扩展的扩展实现文件. */

/*
 * conntrack 扩展的扩展实现文件.
 * 技术核心:*		1. 位图
 *		2. 索引的索引数组(外部维护的一个‘蓝图’)
 * (C) 2015 marywangran <marywangran@126.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <linux/kernel.h>
#include <net/netfilter/nf_conntrack_extend.h>
#include <net/netfilter/nf_conntrack_ext.h>

/* 这个 spin lock 应该和每一个 ext 绑定而不是全局的!*/
static DEFINE_SPINLOCK(nfct_ext_lock);

static struct nf_ct_ext_type ext_extend __read_mostly = {.len	= sizeof(struct nf_conntrack_ext),
	.align	= __alignof__(struct nf_conntrack_ext),
	.id	= NF_CT_EXT_EXT,
	.flags		= NF_CT_EXT_F_PREALLOC,
};

/* 
 * 增加一个数据到 extend 的 extend
 * 注意:需要自己在外部维护一个关于索引的索引的数组
 **/
int nf_ct_exts_add(const struct nf_conn *ct, void *ext)
{
	int ret_idx = -1;
	struct nf_conntrack_ext *exts = NULL;

	if (!ext) {goto out;}

	exts = nf_conn_exts_find(ct);
	if (!exts) {goto out;}
	spin_lock(&nfct_ext_lock);
	ret_idx = find_first_zero_bit(exts->bits, MAX_EXT_SLOTS);
	if (ret_idx > MAX_EXT_SLOTS) {
		ret_idx = -1;
		spin_unlock(&nfct_ext_lock);
		goto out;
	}
	if (exts->slot[ret_idx]) {
		ret_idx = -1;
		spin_unlock(&nfct_ext_lock);
		goto out;
	}
	set_bit(ret_idx, exts->bits);
	exts->slot[ret_idx] = (char *)ext;
	spin_unlock(&nfct_ext_lock);
out:
	return ret_idx;
};
EXPORT_SYMBOL(nf_ct_exts_add);

/*
 * 根据 ID 的 index 获取保存在 conntrack 上的数据
 **/
void *nf_ct_exts_get(const struct nf_conn *ct, int idx)
{
	char *ret = NULL;
	struct nf_conntrack_ext *exts;

	if (idx > MAX_EXT_SLOTS || idx < 0) {goto out;}

	exts = nf_conn_exts_find(ct);
	if (!exts) {goto out;}
	spin_lock(&nfct_ext_lock);
	if (! test_bit(idx, exts->bits)) {spin_unlock(&nfct_ext_lock);
		goto out;
	}
	ret = exts->slot[idx];
	spin_unlock(&nfct_ext_lock);
out:
	return (void *)ret;
}
EXPORT_SYMBOL(nf_ct_exts_get);

/*
 * 根据 ID 的 index 删除保存在 conntrack 上的数据
 **/
void nf_ct_exts_remove(const struct nf_conn *ct, int idx)
{
	struct nf_conntrack_ext *exts;
	if (idx > MAX_EXT_SLOTS || idx < 0) {goto out;}

	exts = nf_conn_exts_find(ct);
	if (!exts) {goto out;}

	spin_lock(&nfct_ext_lock);
	if (! test_bit(idx, exts->bits)) {spin_unlock(&nfct_ext_lock);
		goto out;
	}
	clear_bit(idx, exts->bits);
	exts->slot[idx] = NULL;
	spin_unlock(&nfct_ext_lock);
out:
	return;
};
EXPORT_SYMBOL(nf_ct_exts_remove);

struct nf_conntrack_ext *nf_conn_exts_find(const struct nf_conn *ct)
{return nf_ct_ext_find(ct, NF_CT_EXT_EXT);
}
EXPORT_SYMBOL(nf_conn_exts_find);

struct nf_conntrack_ext *nf_conn_exts_add(struct nf_conn *ct, gfp_t gfp)
{
	struct nf_conntrack_ext *exts;

	exts = nf_ct_ext_add(ct, NF_CT_EXT_EXT, gfp);
	if (!exts) {printk("failed to add extensions area");
		return NULL;
	}

	/* 初始化 */
	{
		int i;
		for (i = 0; i < MAX_EXT_SLOTS; i++) {exts->bits_idx[i] = -1;
			exts->slot[i] = NULL;
		}
	}
	return exts;
}
EXPORT_SYMBOL(nf_conn_exts_add);

int nf_conntrack_exts_init()
{
	int ret;

	ret = nf_ct_extend_register(&ext_extend);
	if (ret < 0) {printk("nf_conntrack_ext: Unable to register extension\n");
		goto out;
	}
	printk("nf_conntrack_ext: register extension OK\n");

	return 0;
out:
	return ret;
}

void nf_conntrack_exts_fini()
{nf_ct_extend_unregister(&ext_extend);
}

 

测试程序nf_conntrack_private_data_auto_save_restore.c:

 

#include <linux/module.h>  
#include <linux/skbuff.h>  
#include <net/tcp.h>
#include <net/netfilter/nf_conntrack_ext.h>
  
MODULE_AUTHOR("marywangran");  
MODULE_LICENSE("GPL");  

/*
 * 必须定义一个用于自省的数组索引
 * 否则就会陷入“数据 - 元数据 - 元元数据 - 元元元数据...”的无限自指怪圈!* 这也是 AI 所面临的问题:自我意识是根本:being 知道某件事,并且 being 知道“being 知道某件事”,* 并且 being 知道“being 知道‘being 知道某件事’”...
 */
enum ext_idx_idx {
	CONN_ORIG_ROUTE,
	CONN_REPLY_ROUTE,
	CONN_SOCK, 
	CONN_AND_SO_ON, 
	NUM
};

static inline void
nf_ext_put_sock(struct sock *sk)
{if ((sk->sk_protocol == IPPROTO_TCP) && (sk->sk_state == TCP_TIME_WAIT)){inet_twsk_put(inet_twsk(sk));
	} else {sock_put(sk);
	}
}

static void
nf_ext_destructor(struct sk_buff *skb)
{
	struct sock *sk = skb->sk;
	skb->sk = NULL;
	skb->destructor = NULL;
	if (sk) {nf_ext_put_sock(sk);
	}
}

/* 缓存 socket 的 HOOK 函数 */
static unsigned int ipv4_conntrack_save_sock (unsigned int hooknum,  
                                      struct sk_buff *skb,  
                                      const struct net_device *in,  
                                      const struct net_device *out,  
                                      int (*okfn)(struct sk_buff *))  
{
	struct nf_conn *ct;  
	enum ip_conntrack_info ctinfo;  
	struct nf_conntrack_ext *exts;
	ct = nf_ct_get(skb, &ctinfo);  
	if (!ct || ct == &nf_conntrack_untracked) {goto out;}
	if ((ip_hdr(skb)->protocol != IPPROTO_UDP) && 
					(ip_hdr(skb)->protocol != IPPROTO_TCP)) {goto out;}
	exts = nf_conn_exts_find(ct);
	if (exts) {  
		/* 缓存 socket,注意,只有 INPUT 的恢复缓存 socket 才有比较大的意义 */
		if (exts->bits_idx[CONN_SOCK] == -1) {if (skb->sk == NULL){goto out;}
			if ((ip_hdr(skb)->protocol == IPPROTO_TCP) && skb->sk->sk_state != TCP_ESTABLISHED) {goto out;}
			exts->bits_idx[CONN_SOCK] = nf_ct_exts_add(ct, skb->sk);
		}
	} 
out:
	return NF_ACCEPT;
}

/* 缓存路由项的 HOOK 函数 */
static unsigned int ipv4_conntrack_save_dst (unsigned int hooknum,  
                                      struct sk_buff *skb,  
                                      const struct net_device *in,  
                                      const struct net_device *out,  
                                      int (*okfn)(struct sk_buff *))  
{  
	struct nf_conn *ct;  
	enum ip_conntrack_info ctinfo;  
	struct nf_conntrack_ext *exts;
	ct = nf_ct_get(skb, &ctinfo);  
	if (!ct || ct == &nf_conntrack_untracked) {goto out;}
	exts = nf_conn_exts_find(ct);
	if (exts) {  
		/* 缓存路由。注意,有两个方向。IP 无方向,两个方向路由都要缓存 */
		int dir = CTINFO2DIR(ctinfo);  
		int idx = (dir == IP_CT_DIR_ORIGINAL)?CONN_ORIG_ROUTE:CONN_REPLY_ROUTE;
		if (exts->bits_idx[idx] == -1) {struct dst_entry *dst = skb_dst(skb);
			if (dst) {dst_hold(dst); 
				exts->bits_idx[idx] = nf_ct_exts_add(ct, dst);
			}
		} 
	} 
out:
	return NF_ACCEPT;  
}  

/* 获取缓存 socket 的 HOOK 函数 */
static unsigned int ipv4_conntrack_restore_sock (unsigned int hooknum,  
                                      struct sk_buff *skb,  
                                      const struct net_device *in,  
                                      const struct net_device *out,  
                                      int (*okfn)(struct sk_buff *))  
{  
	struct nf_conn *ct;  
	enum ip_conntrack_info ctinfo;  
	struct nf_conntrack_ext *exts;
	ct = nf_ct_get(skb, &ctinfo);  
	if (!ct || ct == &nf_conntrack_untracked){goto out;}
	if ((ip_hdr(skb)->protocol != IPPROTO_UDP) && 
			(ip_hdr(skb)->protocol != IPPROTO_TCP)) {goto out;}

	exts = nf_conn_exts_find(ct);
	if (exts) {  
		/* 获取缓存的 socket */
		if (exts->bits_idx[CONN_SOCK] != -1) {struct sock *sk = (struct sock *)nf_ct_exts_get(ct, exts->bits_idx[CONN_SOCK]);
			if (sk) {if ((ip_hdr(skb)->protocol == IPPROTO_TCP) && sk->sk_state != TCP_ESTABLISHED) {goto out;}
				if (unlikely(!atomic_inc_not_zero(&sk->sk_refcnt))) {goto out;}
				skb_orphan(skb);
				skb->sk = sk;
				/* 曾经在上面 atomic inc 了引用计数,等到转交给下任 owner 的时候,一定要 put */
				skb->destructor = nf_ext_destructor;
			}
		}
	}
out:
	return NF_ACCEPT;
}
  
/* 获取缓存路由项的 HOOK 函数 */
static unsigned int ipv4_conntrack_restore_dst (unsigned int hooknum,  
                                      struct sk_buff *skb,  
                                      const struct net_device *in,  
                                      const struct net_device *out,  
                                      int (*okfn)(struct sk_buff *))  
{  
	struct nf_conn *ct;  
	enum ip_conntrack_info ctinfo;  
	struct nf_conntrack_ext *exts;
	ct = nf_ct_get(skb, &ctinfo);  
	if (!ct || ct == &nf_conntrack_untracked) {goto out;}

	exts = nf_conn_exts_find(ct);
	if (exts) {  
		/* 获取缓存的路由 */
		int dir = CTINFO2DIR(ctinfo);  
		int idx = (dir == IP_CT_DIR_ORIGINAL)?CONN_ORIG_ROUTE:CONN_REPLY_ROUTE;
		if (exts->bits_idx[idx] != -1) {struct dst_entry *dst = (struct dst_entry *)nf_ct_exts_get(ct, exts->bits_idx[idx]);
			if (dst) {dst_hold(dst);
				skb_dst_set(skb, dst);
			}
		}  
	} 
out:
	return NF_ACCEPT;  
}  

/*
 * 总体图景:* OUTPUT:缓存 socket
 * INPUT:恢复 socket
 *
 * POSTROUTING|INPUT:缓存路由
 * PREROUTING:恢复路由
 */
static struct nf_hook_ops ipv4_conn_cache_ops[] __read_mostly = {  
	{  
		.hook           = ipv4_conntrack_save_dst,  
		.owner          = THIS_MODULE,  
		.pf             = NFPROTO_IPV4,  
		.hooknum        = NF_INET_POST_ROUTING,  
		.priority       = NF_IP_PRI_CONNTRACK + 1,  
	},  
	{  
		.hook           = ipv4_conntrack_save_sock,  
		.owner          = THIS_MODULE,  
		.pf             = NFPROTO_IPV4,  
		.hooknum        = NF_INET_LOCAL_OUT,  
		.priority       = NF_IP_PRI_CONNTRACK + 1,  
	},  
	{  
		.hook           = ipv4_conntrack_save_dst,  
		.owner          = THIS_MODULE,  
		.pf             = NFPROTO_IPV4,  
		.hooknum        = NF_INET_LOCAL_IN,  
		.priority       = NF_IP_PRI_CONNTRACK + 1,  
	},
	{  
		.hook           = ipv4_conntrack_restore_sock,  
		.owner          = THIS_MODULE,  
		.pf             = NFPROTO_IPV4,  
		.hooknum        = NF_INET_LOCAL_IN,  
		.priority       = NF_IP_PRI_CONNTRACK + 2,  
	},
	{  
		.hook           = ipv4_conntrack_restore_dst,  
		.owner          = THIS_MODULE,  
		.pf             = NFPROTO_IPV4,  
		.hooknum        = NF_INET_PRE_ROUTING,  
		.priority       = NF_IP_PRI_CONNTRACK + 1,  
	},  
};  
  
static int __init cache_dst_and_sock_demo_init(void)  
{  
	int ret;  
	ret = nf_register_hooks(ipv4_conn_cache_ops, ARRAY_SIZE(ipv4_conn_cache_ops));  
	if (ret) {goto out;;}
	return 0;
out:	
	return ret;  
}  
  
static void __exit cache_dst_and_sock_demo_fini(void)  
{nf_unregister_hooks(ipv4_conn_cache_ops, ARRAY_SIZE(ipv4_conn_cache_ops));  
}  
  
module_init(cache_dst_and_sock_demo_init);  
module_exit(cache_dst_and_sock_demo_fini);

在测试程序中,我缓存了路由项以及到达本机数据包的 socket,这样仅仅查询到 conntrack 就可以直接将路由和 socket 取出来了,取值的过程由于存在索引数组和索引的索引数组,因此就是数组下标寻址,不再需要查询。

前面说过很多次,conntrack 作为一中连接跟踪机制,如果它本身是可扩展的,那么将会是多么令人激动的一件事,当你看了 N 多文档代码之后,你发现它确实是可以扩展的,但是却没有感到激动,因为你可能发现:

1. 它可以注册一个 account 扩展,但是计数机制却很原始;
2. 我希望增加一个新型的扩展,却不得不重新编译内核;

怎么办?我曾经很生气地默默指责过当初实现这个的人,想当然的认为将扩展本身也做成可扩展的,而不是写死几个特定的扩展将是一个多么容易的事,我一直憋着没有去做这个实现,就是因为觉得它太简单,在工作中也确实需要一个新的扩展,然而既有的扩展类型中没有,为了不重新编译内核,我只好盗用了 acct 扩展。采用了一个 OO 中典型的封装方法:

struct my_ext {
    struct orig_ext;
    char info[0];
};


是时候改变一些事态了。基于下面几个原因,在周六的早上,我突然决定在周末完成它:
1. 外部因素:好不容易感冒了,作为一个羸弱的人,我不希望得到别人的同情,只需要获得周末的安静,感冒发烧是最好的选择;
2. 内部因素:年终总结完了,工作计划也确定了,后面是个收网的过程,稳为重,不需要太激进,因此也就没有什么技术上不可控的因素,心理安了,事就可以开始做了;
可能我又要笑话自己了,不就写个简单的模块么?怎么搞得跟诸葛孔明布阵一样 … 如此感性且主观一人怎么就 …
      不管怎么讲,这个模块看起来确实是简单的。然而一旦做起来,发现有两个比较严重的问题:
1. 反射内省问题
如果 conntrack 的 extend 有 128 个 slot,每个 slot 里面放一个私有数据。问题是,程序怎么知道哪个 slot 里面有哪个数据。程序有能力存储,但是程序自己却不知道这一点 … 这就是一个怪圈,你必须让数据成为自描述的,或者就规定死第 n 号 slot 必须放路由项,第 m 个 slot 必须放 socket… 现有的 nf conntrack 模块使用了后一种方法,即枚举 nf_ct_ext_id 做的事。
      可是我还是想随机选择 slot,这样更加灵活。自描述的数据结构也看了不好,ASN.1 太复杂,且内核数据更多的不是标识属性,而是定义一种行为,google 的 protocol buffer 也不是很合适,需要定义太多的回调函数来完成反射自省.. 后来我想了一个办法,那就是定义个索引蓝图,标识“slot 索引的索引”,而不是标识具体 slot 的位置。
      这就需要定义一个新的枚举,定义蓝图:

enum idx_idx{
ROUTE,
SOCKET,
AND_SO_ON,
IDX_IDX_NUM
};

然后定义一个数组来标识真正的索引:

int idx[IDX_IDX_NUM];

定义一个 bitmap 来表示 slot 的使用情况即可,具体的做法可以看代码,一目则了然。
2. 内存寻址问题
内核内存是宝贵的,不是说物理内存用不起,而是它的虚拟地址空间也是有限的,因此建议使用 64 位系统,如果是 32 位系统,如果希望内核保存比较大的数据结构,请在编译的时候按照 2G/2G 或者 1G/3G 来拆分地址空间,前者情况用户和内核各自占据 2G,后者的话内核占 3G,用户仅仅占 1G。
      也许就是因为存在这个内存问题,Linux 的 nf conntrack 限制了 extend 的内存使用,其最大长度字段数据类型是 u8。由于我知道我的系统,所以我将其改为了 u16。你必须要知道的是,nf connrtack 的 extend 内存使用时是连续的,你不能采用一个 sizeof(char *)大小的空间保存一个指针,然后这个指针指向一个超级大的连续空间 … 但是为什么不能呢?还是因为代码的普适问题,我了解我的系统,所以我可以使用保存指针的做法。另外我还保留了数组的方式,总之,数组和指针分工是明确的,数组用于 extend 的寻址,而指针用于数据的获取。
      代码包括一个框架和一个测试程序,内核还是 2.6.32 amd64,已经在 github 上了:https://github.com/marywangran/extension-of-nf_conntrack-ext
      还是在这里贴一份备份,怕哪天 github 被 wall 了 …

修改 include/net/netfilter/nf_conntrack_extend.h:

 

--- nf_conntrack_extend.h.orig  2014-03-29 12:55:26.000000000 +0800
+++ nf_conntrack_extend.h   2015-01-15 17:28:39.000000000 +0800
@@ -3,13 +3,17 @@

 #include <net/netfilter/nf_conntrack.h>

+#define NFCT_EXT_EXT
+
 enum nf_ct_ext_id
 {
    NF_CT_EXT_HELPER,
    NF_CT_EXT_NAT,
    NF_CT_EXT_ACCT,
    NF_CT_EXT_ECACHE,
-   NF_CT_EXT_NEW,
+#ifdef NFCT_EXT_EXT
+   NF_CT_EXT_EXT,
+#endif
    NF_CT_EXT_NUM,
 };

@@ -17,13 +21,21 @@
 #define NF_CT_EXT_NAT_TYPE struct nf_conn_nat
 #define NF_CT_EXT_ACCT_TYPE struct nf_conn_counter
 #define NF_CT_EXT_ECACHE_TYPE struct nf_conntrack_ecache
-#define NF_CT_EXT_NEW_TYPE struct nf_conntrack_new
+#ifdef NFCT_EXT_EXT
+#define NF_CT_EXT_EXT_TYPE struct nf_conntrack_ext
+#endif

 /* Extensions: optional stuff which isn't permanently in struct. */
 struct nf_ct_ext {
    struct rcu_head rcu;
+#ifdef NFCT_EXT_EXT
+   /* 内存不再是个事儿 */
+   u16 offset[NF_CT_EXT_NUM];
+   u16 len;
+#else
    u8 offset[NF_CT_EXT_NUM];
    u8 len;
+#endif
    char data[0];
 };

@@ -80,10 +92,18 @@
    unsigned int flags;

    /* Length and min alignment. */
+#ifdef NFCT_EXT_EXT
+   /* 内存不再是个事儿 */
+   u16 len;
+   u16 align;
+   /* initial size of nf_ct_ext. */
+   u16 alloc_size;
+#else
    u8 len;
    u8 align;
    /* initial size of nf_ct_ext. */
    u8 alloc_size;
+#endif
 };

 int nf_ct_extend_register(struct nf_ct_ext_type *type);

 

增加 include/net/netfilter/nf_conntrack_ext.h:

 

/*
 * (C) 2015 marywangran <marywangran@126.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#ifndef _NF_CONNTRACK_EXT_H
#define _NF_CONNTRACK_EXT_H
#include <net/net_namespace.h>
#include <linux/netfilter/nf_conntrack_common.h>
#include <linux/netfilter/nf_conntrack_tuple_common.h>
#include <net/netfilter/nf_conntrack.h>
#include <net/netfilter/nf_conntrack_extend.h>

#define MAX_EXT_SLOTS	8	
#define BITINT	1


struct nf_conntrack_ext {
	/* 必须有一个数组用于自省或者反射 */
	int	bits_idx[MAX_EXT_SLOTS];
	int	bits[BITINT];
	char *slot[MAX_EXT_SLOTS];
};


int nf_ct_exts_add(const struct nf_conn *ct, void *ext);

void *nf_ct_exts_get(const struct nf_conn *ct, int idx); 

void nf_ct_exts_remove(const struct nf_conn *ct, int idx);

struct nf_conntrack_ext *nf_conn_exts_find(const struct nf_conn *ct);

struct nf_conntrack_ext *nf_conn_exts_add(struct nf_conn *ct, gfp_t gfp);
extern int nf_conntrack_exts_init();
extern void nf_conntrack_exts_fini();

#endif /* _NF_CONNTRACK_EXT_H */

 

修改 net/netfilter/nf_conntrack_core.c:

 

--- nf_conntrack_core.c.orig    2014-03-29 13:00:17.000000000 +0800
+++ nf_conntrack_core.c 2015-01-15 17:01:28.000000000 +0800
@@ -42,6 +42,10 @@
 #include <net/netfilter/nf_conntrack_extend.h>
 #include <net/netfilter/nf_conntrack_acct.h>
 #include <net/netfilter/nf_conntrack_ecache.h>
+#ifdef NFCT_EXT_EXT
+/* 引入 extend 的 extend 头文件 */
+#include <net/netfilter/nf_conntrack_ext.h>
+#endif
 #include <net/netfilter/nf_nat.h>
 #include <net/netfilter/nf_nat_core.h>

@@ -644,8 +648,11 @@
    }

    nf_ct_acct_ext_add(ct, GFP_ATOMIC);
-
    nf_ct_ecache_ext_add(ct, GFP_ATOMIC);
+#ifdef NFCT_EXT_EXT
+   /* 在创建 conntrack 的时候初始化 extend 的 extend */
+   nf_conn_exts_add(ct, GFP_ATOMIC);
+#endif

    spin_lock_bh(&nf_conntrack_lock);
    exp = nf_ct_find_expectation(net, tuple);
@@ -1130,6 +1137,10 @@

    nf_ct_free_hashtable(net->ct.hash, net->ct.hash_vmalloc,
                 net->ct.htable_size);
+#ifdef NFCT_EXT_EXT
+   /* 析构 extend 的 extend */
+   nf_conntrack_exts_fini();
+#endif
    nf_conntrack_ecache_fini(net);
    nf_conntrack_acct_fini(net);
    nf_conntrack_expect_fini(net);
@@ -1344,9 +1355,19 @@
    ret = nf_conntrack_ecache_init(net);
    if (ret < 0)
        goto err_ecache;
+#ifdef NFCT_EXT_EXT
+   /* 注册 extend 的 extend */
+   ret = nf_conntrack_exts_init();
+   if (ret < 0)
+       goto err_exts;
+#endif

    return 0;

+#ifdef NFCT_EXT_EXT
+err_exts:
+   nf_conntrack_ecache_fini(net);
+#endif
 err_ecache:
    nf_conntrack_acct_fini(net);
 err_acct:

 

增加 net/netfilter/nf_conntrack_ext.c:

 

/* conntrack 扩展的扩展实现文件. */

/*
 * conntrack 扩展的扩展实现文件.
 * 技术核心:*		1. 位图
 *		2. 索引的索引数组(外部维护的一个‘蓝图’)
 * (C) 2015 marywangran <marywangran@126.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <linux/kernel.h>
#include <net/netfilter/nf_conntrack_extend.h>
#include <net/netfilter/nf_conntrack_ext.h>

/* 这个 spin lock 应该和每一个 ext 绑定而不是全局的!*/
static DEFINE_SPINLOCK(nfct_ext_lock);

static struct nf_ct_ext_type ext_extend __read_mostly = {.len	= sizeof(struct nf_conntrack_ext),
	.align	= __alignof__(struct nf_conntrack_ext),
	.id	= NF_CT_EXT_EXT,
	.flags		= NF_CT_EXT_F_PREALLOC,
};

/* 
 * 增加一个数据到 extend 的 extend
 * 注意:需要自己在外部维护一个关于索引的索引的数组
 **/
int nf_ct_exts_add(const struct nf_conn *ct, void *ext)
{
	int ret_idx = -1;
	struct nf_conntrack_ext *exts = NULL;

	if (!ext) {goto out;}

	exts = nf_conn_exts_find(ct);
	if (!exts) {goto out;}
	spin_lock(&nfct_ext_lock);
	ret_idx = find_first_zero_bit(exts->bits, MAX_EXT_SLOTS);
	if (ret_idx > MAX_EXT_SLOTS) {
		ret_idx = -1;
		spin_unlock(&nfct_ext_lock);
		goto out;
	}
	if (exts->slot[ret_idx]) {
		ret_idx = -1;
		spin_unlock(&nfct_ext_lock);
		goto out;
	}
	set_bit(ret_idx, exts->bits);
	exts->slot[ret_idx] = (char *)ext;
	spin_unlock(&nfct_ext_lock);
out:
	return ret_idx;
};
EXPORT_SYMBOL(nf_ct_exts_add);

/*
 * 根据 ID 的 index 获取保存在 conntrack 上的数据
 **/
void *nf_ct_exts_get(const struct nf_conn *ct, int idx)
{
	char *ret = NULL;
	struct nf_conntrack_ext *exts;

	if (idx > MAX_EXT_SLOTS || idx < 0) {goto out;}

	exts = nf_conn_exts_find(ct);
	if (!exts) {goto out;}
	spin_lock(&nfct_ext_lock);
	if (! test_bit(idx, exts->bits)) {spin_unlock(&nfct_ext_lock);
		goto out;
	}
	ret = exts->slot[idx];
	spin_unlock(&nfct_ext_lock);
out:
	return (void *)ret;
}
EXPORT_SYMBOL(nf_ct_exts_get);

/*
 * 根据 ID 的 index 删除保存在 conntrack 上的数据
 **/
void nf_ct_exts_remove(const struct nf_conn *ct, int idx)
{
	struct nf_conntrack_ext *exts;
	if (idx > MAX_EXT_SLOTS || idx < 0) {goto out;}

	exts = nf_conn_exts_find(ct);
	if (!exts) {goto out;}

	spin_lock(&nfct_ext_lock);
	if (! test_bit(idx, exts->bits)) {spin_unlock(&nfct_ext_lock);
		goto out;
	}
	clear_bit(idx, exts->bits);
	exts->slot[idx] = NULL;
	spin_unlock(&nfct_ext_lock);
out:
	return;
};
EXPORT_SYMBOL(nf_ct_exts_remove);

struct nf_conntrack_ext *nf_conn_exts_find(const struct nf_conn *ct)
{return nf_ct_ext_find(ct, NF_CT_EXT_EXT);
}
EXPORT_SYMBOL(nf_conn_exts_find);

struct nf_conntrack_ext *nf_conn_exts_add(struct nf_conn *ct, gfp_t gfp)
{
	struct nf_conntrack_ext *exts;

	exts = nf_ct_ext_add(ct, NF_CT_EXT_EXT, gfp);
	if (!exts) {printk("failed to add extensions area");
		return NULL;
	}

	/* 初始化 */
	{
		int i;
		for (i = 0; i < MAX_EXT_SLOTS; i++) {exts->bits_idx[i] = -1;
			exts->slot[i] = NULL;
		}
	}
	return exts;
}
EXPORT_SYMBOL(nf_conn_exts_add);

int nf_conntrack_exts_init()
{
	int ret;

	ret = nf_ct_extend_register(&ext_extend);
	if (ret < 0) {printk("nf_conntrack_ext: Unable to register extension\n");
		goto out;
	}
	printk("nf_conntrack_ext: register extension OK\n");

	return 0;
out:
	return ret;
}

void nf_conntrack_exts_fini()
{nf_ct_extend_unregister(&ext_extend);
}

 

测试程序nf_conntrack_private_data_auto_save_restore.c:

 

#include <linux/module.h>  
#include <linux/skbuff.h>  
#include <net/tcp.h>
#include <net/netfilter/nf_conntrack_ext.h>
  
MODULE_AUTHOR("marywangran");  
MODULE_LICENSE("GPL");  

/*
 * 必须定义一个用于自省的数组索引
 * 否则就会陷入“数据 - 元数据 - 元元数据 - 元元元数据...”的无限自指怪圈!* 这也是 AI 所面临的问题:自我意识是根本:being 知道某件事,并且 being 知道“being 知道某件事”,* 并且 being 知道“being 知道‘being 知道某件事’”...
 */
enum ext_idx_idx {
	CONN_ORIG_ROUTE,
	CONN_REPLY_ROUTE,
	CONN_SOCK, 
	CONN_AND_SO_ON, 
	NUM
};

static inline void
nf_ext_put_sock(struct sock *sk)
{if ((sk->sk_protocol == IPPROTO_TCP) && (sk->sk_state == TCP_TIME_WAIT)){inet_twsk_put(inet_twsk(sk));
	} else {sock_put(sk);
	}
}

static void
nf_ext_destructor(struct sk_buff *skb)
{
	struct sock *sk = skb->sk;
	skb->sk = NULL;
	skb->destructor = NULL;
	if (sk) {nf_ext_put_sock(sk);
	}
}

/* 缓存 socket 的 HOOK 函数 */
static unsigned int ipv4_conntrack_save_sock (unsigned int hooknum,  
                                      struct sk_buff *skb,  
                                      const struct net_device *in,  
                                      const struct net_device *out,  
                                      int (*okfn)(struct sk_buff *))  
{
	struct nf_conn *ct;  
	enum ip_conntrack_info ctinfo;  
	struct nf_conntrack_ext *exts;
	ct = nf_ct_get(skb, &ctinfo);  
	if (!ct || ct == &nf_conntrack_untracked) {goto out;}
	if ((ip_hdr(skb)->protocol != IPPROTO_UDP) && 
					(ip_hdr(skb)->protocol != IPPROTO_TCP)) {goto out;}
	exts = nf_conn_exts_find(ct);
	if (exts) {  
		/* 缓存 socket,注意,只有 INPUT 的恢复缓存 socket 才有比较大的意义 */
		if (exts->bits_idx[CONN_SOCK] == -1) {if (skb->sk == NULL){goto out;}
			if ((ip_hdr(skb)->protocol == IPPROTO_TCP) && skb->sk->sk_state != TCP_ESTABLISHED) {goto out;}
			exts->bits_idx[CONN_SOCK] = nf_ct_exts_add(ct, skb->sk);
		}
	} 
out:
	return NF_ACCEPT;
}

/* 缓存路由项的 HOOK 函数 */
static unsigned int ipv4_conntrack_save_dst (unsigned int hooknum,  
                                      struct sk_buff *skb,  
                                      const struct net_device *in,  
                                      const struct net_device *out,  
                                      int (*okfn)(struct sk_buff *))  
{  
	struct nf_conn *ct;  
	enum ip_conntrack_info ctinfo;  
	struct nf_conntrack_ext *exts;
	ct = nf_ct_get(skb, &ctinfo);  
	if (!ct || ct == &nf_conntrack_untracked) {goto out;}
	exts = nf_conn_exts_find(ct);
	if (exts) {  
		/* 缓存路由。注意,有两个方向。IP 无方向,两个方向路由都要缓存 */
		int dir = CTINFO2DIR(ctinfo);  
		int idx = (dir == IP_CT_DIR_ORIGINAL)?CONN_ORIG_ROUTE:CONN_REPLY_ROUTE;
		if (exts->bits_idx[idx] == -1) {struct dst_entry *dst = skb_dst(skb);
			if (dst) {dst_hold(dst); 
				exts->bits_idx[idx] = nf_ct_exts_add(ct, dst);
			}
		} 
	} 
out:
	return NF_ACCEPT;  
}  

/* 获取缓存 socket 的 HOOK 函数 */
static unsigned int ipv4_conntrack_restore_sock (unsigned int hooknum,  
                                      struct sk_buff *skb,  
                                      const struct net_device *in,  
                                      const struct net_device *out,  
                                      int (*okfn)(struct sk_buff *))  
{  
	struct nf_conn *ct;  
	enum ip_conntrack_info ctinfo;  
	struct nf_conntrack_ext *exts;
	ct = nf_ct_get(skb, &ctinfo);  
	if (!ct || ct == &nf_conntrack_untracked){goto out;}
	if ((ip_hdr(skb)->protocol != IPPROTO_UDP) && 
			(ip_hdr(skb)->protocol != IPPROTO_TCP)) {goto out;}

	exts = nf_conn_exts_find(ct);
	if (exts) {  
		/* 获取缓存的 socket */
		if (exts->bits_idx[CONN_SOCK] != -1) {struct sock *sk = (struct sock *)nf_ct_exts_get(ct, exts->bits_idx[CONN_SOCK]);
			if (sk) {if ((ip_hdr(skb)->protocol == IPPROTO_TCP) && sk->sk_state != TCP_ESTABLISHED) {goto out;}
				if (unlikely(!atomic_inc_not_zero(&sk->sk_refcnt))) {goto out;}
				skb_orphan(skb);
				skb->sk = sk;
				/* 曾经在上面 atomic inc 了引用计数,等到转交给下任 owner 的时候,一定要 put */
				skb->destructor = nf_ext_destructor;
			}
		}
	}
out:
	return NF_ACCEPT;
}
  
/* 获取缓存路由项的 HOOK 函数 */
static unsigned int ipv4_conntrack_restore_dst (unsigned int hooknum,  
                                      struct sk_buff *skb,  
                                      const struct net_device *in,  
                                      const struct net_device *out,  
                                      int (*okfn)(struct sk_buff *))  
{  
	struct nf_conn *ct;  
	enum ip_conntrack_info ctinfo;  
	struct nf_conntrack_ext *exts;
	ct = nf_ct_get(skb, &ctinfo);  
	if (!ct || ct == &nf_conntrack_untracked) {goto out;}

	exts = nf_conn_exts_find(ct);
	if (exts) {  
		/* 获取缓存的路由 */
		int dir = CTINFO2DIR(ctinfo);  
		int idx = (dir == IP_CT_DIR_ORIGINAL)?CONN_ORIG_ROUTE:CONN_REPLY_ROUTE;
		if (exts->bits_idx[idx] != -1) {struct dst_entry *dst = (struct dst_entry *)nf_ct_exts_get(ct, exts->bits_idx[idx]);
			if (dst) {dst_hold(dst);
				skb_dst_set(skb, dst);
			}
		}  
	} 
out:
	return NF_ACCEPT;  
}  

/*
 * 总体图景:* OUTPUT:缓存 socket
 * INPUT:恢复 socket
 *
 * POSTROUTING|INPUT:缓存路由
 * PREROUTING:恢复路由
 */
static struct nf_hook_ops ipv4_conn_cache_ops[] __read_mostly = {  
	{  
		.hook           = ipv4_conntrack_save_dst,  
		.owner          = THIS_MODULE,  
		.pf             = NFPROTO_IPV4,  
		.hooknum        = NF_INET_POST_ROUTING,  
		.priority       = NF_IP_PRI_CONNTRACK + 1,  
	},  
	{  
		.hook           = ipv4_conntrack_save_sock,  
		.owner          = THIS_MODULE,  
		.pf             = NFPROTO_IPV4,  
		.hooknum        = NF_INET_LOCAL_OUT,  
		.priority       = NF_IP_PRI_CONNTRACK + 1,  
	},  
	{  
		.hook           = ipv4_conntrack_save_dst,  
		.owner          = THIS_MODULE,  
		.pf             = NFPROTO_IPV4,  
		.hooknum        = NF_INET_LOCAL_IN,  
		.priority       = NF_IP_PRI_CONNTRACK + 1,  
	},
	{  
		.hook           = ipv4_conntrack_restore_sock,  
		.owner          = THIS_MODULE,  
		.pf             = NFPROTO_IPV4,  
		.hooknum        = NF_INET_LOCAL_IN,  
		.priority       = NF_IP_PRI_CONNTRACK + 2,  
	},
	{  
		.hook           = ipv4_conntrack_restore_dst,  
		.owner          = THIS_MODULE,  
		.pf             = NFPROTO_IPV4,  
		.hooknum        = NF_INET_PRE_ROUTING,  
		.priority       = NF_IP_PRI_CONNTRACK + 1,  
	},  
};  
  
static int __init cache_dst_and_sock_demo_init(void)  
{  
	int ret;  
	ret = nf_register_hooks(ipv4_conn_cache_ops, ARRAY_SIZE(ipv4_conn_cache_ops));  
	if (ret) {goto out;;}
	return 0;
out:	
	return ret;  
}  
  
static void __exit cache_dst_and_sock_demo_fini(void)  
{nf_unregister_hooks(ipv4_conn_cache_ops, ARRAY_SIZE(ipv4_conn_cache_ops));  
}  
  
module_init(cache_dst_and_sock_demo_init);  
module_exit(cache_dst_and_sock_demo_fini);

在测试程序中,我缓存了路由项以及到达本机数据包的 socket,这样仅仅查询到 conntrack 就可以直接将路由和 socket 取出来了,取值的过程由于存在索引数组和索引的索引数组,因此就是数组下标寻址,不再需要查询。

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-20发表,共计28752字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中

星哥玩云

星哥玩云
星哥玩云
分享互联网知识
用户数
4
文章数
19350
评论数
4
阅读量
7959136
文章搜索
热门文章
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛 NAS-3:安装飞牛 NAS 后的很有必要的操作 前言 如果你已经有了飞牛 NAS 系统,之前...
我把用了20年的360安全卫士卸载了

我把用了20年的360安全卫士卸载了

我把用了 20 年的 360 安全卫士卸载了 是的,正如标题你看到的。 原因 偷摸安装自家的软件 莫名其妙安装...
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛 NAS 中安装 Navidrome 音乐文件中文标签乱码问题解决、安装 FntermX 终端 问题背景 ...
阿里云CDN
阿里云CDN-提高用户访问的响应速度和成功率
随机文章
恶意团伙利用 PHP-FPM 未授权访问漏洞发起大规模攻击

恶意团伙利用 PHP-FPM 未授权访问漏洞发起大规模攻击

恶意团伙利用 PHP-FPM 未授权访问漏洞发起大规模攻击 PHP-FPM(FastCGl Process M...
150元打造低成本NAS小钢炮,捡一块3865U工控板

150元打造低成本NAS小钢炮,捡一块3865U工控板

150 元打造低成本 NAS 小钢炮,捡一块 3865U 工控板 一块二手的熊猫 B3 工控板 3865U,搭...
告别Notion焦虑!这款全平台开源加密笔记神器,让你的隐私真正“上锁”

告别Notion焦虑!这款全平台开源加密笔记神器,让你的隐私真正“上锁”

  告别 Notion 焦虑!这款全平台开源加密笔记神器,让你的隐私真正“上锁” 引言 在数字笔记工...
240 元左右!五盘位 NAS主机,7 代U硬解4K稳如狗,拓展性碾压同价位

240 元左右!五盘位 NAS主机,7 代U硬解4K稳如狗,拓展性碾压同价位

  240 元左右!五盘位 NAS 主机,7 代 U 硬解 4K 稳如狗,拓展性碾压同价位 在 NA...
从“纸堆”到“电子化”文档:用这个开源系统打造你的智能文档管理系统

从“纸堆”到“电子化”文档:用这个开源系统打造你的智能文档管理系统

从“纸堆”到“电子化”文档:用这个开源系统打造你的智能文档管理系统 大家好,我是星哥。公司的项目文档存了一堆 ...

免费图片视频管理工具让灵感库告别混乱

一言一句话
-「
手气不错
12.2K Star 爆火!开源免费的 FileConverter:右键一键搞定音视频 / 图片 / 文档转换,告别多工具切换

12.2K Star 爆火!开源免费的 FileConverter:右键一键搞定音视频 / 图片 / 文档转换,告别多工具切换

12.2K Star 爆火!开源免费的 FileConverter:右键一键搞定音视频 / 图片 / 文档转换...
免费无广告!这款跨平台AI RSS阅读器,拯救你的信息焦虑

免费无广告!这款跨平台AI RSS阅读器,拯救你的信息焦虑

  免费无广告!这款跨平台 AI RSS 阅读器,拯救你的信息焦虑 在算法推荐主导信息流的时代,我们...
把小米云笔记搬回家:飞牛 NAS 一键部署,小米云笔记自动同步到本地

把小米云笔记搬回家:飞牛 NAS 一键部署,小米云笔记自动同步到本地

把小米云笔记搬回家:飞牛 NAS 一键部署,小米云笔记自动同步到本地 大家好,我是星哥,今天教大家在飞牛 NA...
安装Black群晖DSM7.2系统安装教程(在Vmware虚拟机中、实体机均可)!

安装Black群晖DSM7.2系统安装教程(在Vmware虚拟机中、实体机均可)!

安装 Black 群晖 DSM7.2 系统安装教程(在 Vmware 虚拟机中、实体机均可)! 前言 大家好,...
仅2MB大小!开源硬件监控工具:Win11 无缝适配,CPU、GPU、网速全维度掌控

仅2MB大小!开源硬件监控工具:Win11 无缝适配,CPU、GPU、网速全维度掌控

还在忍受动辄数百兆的“全家桶”监控软件?后台偷占资源、界面杂乱冗余,想查个 CPU 温度都要层层点选? 今天给...