在Linux上实现一个可用的stateless双向静态NAT模块

本文涉及的产品
公网NAT网关,每月750个小时 15CU
简介:

关于Linux上如何配置NAT的资料已经不少,可谓铺天盖地!本文与此无关。本文提供一种iptables之外的方式。
iptables? 不!why?因为iptables配置的NAT是stateful的,它的实现依赖一个叫做conntrack的模块,什么是 conntrack?Oh,NO!这可是我的专长,但我不想在本文中说它,认识我的人都知道,我扯这个话题我能扯上12个小时...都还扯不完。也许你不 知道什么是stateful NAT,但是如果你是一个有心人,或者说是一个技术还算精湛的Linux网络管理员或者爱好者,你肯定在配置NAT的时候遇到过这样那样的问题,比如“在 一个连接已经建立的时候再配置NAT为何不能及时生效”,“为什么iptables配置NAT之后数据只能从一个方向主动发往另一个方向”之类的。这就是 state在作怪,你知道的,IP是无state的,但是NAT加入了第四层的逻辑之后就有了state,这就是stateful NAT,也就是iptables -t nat ..配置出来的NAT固有的性质,你改变不了。起码我在iptables最新的版本中看到的NAT还是stateful的。有的时候..    
       有的时候,你可能,你必须...
       你必须配置一种stateless的NAT,双向的,静态的。这个问题,唉..
       这个问题折腾了我半年,2013年的前9个月,一个让我欢喜让我忧的三个季度,我的精力几乎全部扑在了一件事上,从寒冬到40+摄氏度的高温,从早上6点 半出发去上班到半夜10点多还呆在机房...要不是大前天收拾书架时发现了一张当时还没有报销掉的120元的例行加班打车票,我本来不想写这个模块了。 120块不算什么,但借此机会回忆往事,顺便补上残缺的那一部分,算是给自己报销了,而且价值远大于120元。我得承认,那三个季度里并不是 stateless NAT显得最为重要,我之所以在一年后的今天把它拿出来,是因为其它的问题都被我当时就overcome了,不管花多久,曾经有过72小时惊魂解决 conntrack confirm问题,有过由于混乱急躁和陌生女人一起吃烤肉被老婆诈出真相...但是就这个stateless NAT始终没有解决,没有解决,这是why?
       我做的是一个产品而不是个人试验,我所在的是一个公司的团队而不是干私活儿,所有使用的技术必须经过技术预研,确保可行性,更重要的是,要保证所有人处在 一样的节奏,也许是并行,大的旋律却始终是个一,这是在玩卡农啊。我不能加入一些个人色彩,比如个人的突发奇想(日后我坦白,这一点我做的不好!),如果 非要加入,那么必须有下面这么一个过程:
把领导,团队所有人拉进来开会,培训,确保任何研发人员和技术负责人都对使用的技术了如指掌不留死角。
但是哪有这个时间?!人性是脆弱的,不管人生多么坚强。任何人都可能突然哪天挂了,由于不可抗原因去别的城市或者国家了,和根本不熟识的同事由于接开水打了一架离职了...如果你做的东西也因此而离去了,对于一个公司而言,说明你根本没来过。......有点扯远了。
       所以,即便当时我已经可以实现stateless NAT(当时我采用了路由target的方式,也许我之前的blog有写过),我也不能拿来用,我只能找机会,找一个闲暇的午后,一个没有紧急任务的下 午,让所有与此相关的人都了解了这个技术,然后用与不用就是一个需求问题了,最好的办法除了代码还是代码,对于程序员而言,说1G个字符都是瞎掰,没有能 跑起来的代码都是扯淡,就算代码狠烂也无所谓,正因为如此现实主义的性格,我喜欢这个职业,不发狠话,不长篇阔论,不打架,不煽动,只要代码能跑起来,仅 此而已。
       对了,stateless NAT在Linux 2.4已经有好的实现了,就是使用tc/policy routing来完成,但是2.6内核下已经很难做到了。作者基于解耦合的考虑将这个模块留给了真正想实现它的人,也许我算一个。我之所以做如此多别人看 来没有意义的事,因为我想表达一个理念,那就是“开发运维”和“运维开发”的理念,这类人一定是将来最炙手可热的人。当我面对无数次Cisco认证工程师 的责难后,我的第一个想法不是骂他们或打他们(没人家词汇丰富且[注意:此处不能用‘或’,要用‘且’]打不过人家就尴尬了,即便这些都不是问题,不还有 法律的吗...),我的第一个想法就是,在Linux上能不能实现相同的功能,目的不是爆他们的菊花,而是让他觉得我可以爆他。幸运的是,我都做到了,每 当此刻,我回到家里都会写几个模块然而测试,就像公司的技术预研一样。然后就呈现给朋友们,有兴趣的都可以去测试,这种事没钱赚,也得不到肯定,没用到 git,也不是神马GPL开源,就是特殊的朋友圈分享,我十分讨厌分类,我十分喜欢随便。然后,然后,正如小小的话,我编码,写了一堆烂代码,Linux 上基于Netfilter实现的一个双向,静态的,无状态的NAT,代码不复杂,只有几百行,但是....
        但是,问题有二:
1.这个模块初级,但是可用,这是我自我肯定的一面;
2.这个模块有大量可疑改进的空间,我自我否定。

代码如下:

/* 
 *
 * 用法:
 * 对目标地址为1.2.1.2的数据包做目标地址转换,目标转为192.168.1.8
 * echo +1.2.1.2 192.168.1.8 dst >/proc/net/static_nat
 * 上述命令会同时添加一条反向的SNAT映射
 *
 * 请解释:
 * echo +192.168.184.250 192.168.184.154 src >/proc/net/static_nat
 *
 */

#include <linux/module.h>
#include <linux/skbuff.h>
#include <net/ip.h>
#include <net/netfilter/nf_conntrack.h>

#define DIRMASK	0x11
#define BUCKETS	1024


#define NAT_OPT_DEL			0x01
#define NAT_OPT_FIND		0x04

#define NAT_OPT_ACCT_BIT	0x02


enum nat_dir {
	DIR_SNAT,
	DIR_DNAT,
	DIR_NUM
};

/*
 * 记录统计信息
 */
struct nat_account {
	u32 nat_packets;
	u32 nat_bytes;
};

struct static_nat_entry {
	__be32 addr[DIR_NUM];
	enum nat_dir type;
	struct nat_account acct[DIR_NUM];
	struct hlist_node node[DIR_NUM];
};

static DEFINE_SPINLOCK(nat_lock);

/* 保存SNAT映射 */
struct hlist_head *src_list;

/* 保存DNAT映射 */
struct hlist_head *dst_list;

/*
 * 用一个IP地址(对于PREROUTING是daddr,对于POSTROUTING是saddr)作为key来获取value。
 */
static __be32 get_address_from_map(struct sk_buff *skb, unsigned int dir, __be32 addr_key, unsigned int opt)
{
	__be32 ret = 0, cmp_key, ret_value;
	u32 hash;
	struct hlist_head *list;
	struct hlist_node *iter, *tmp;
	struct static_nat_entry *ent;

	hash = jhash_1word(addr_key, 1);
	hash = hash%BUCKETS;

	spin_lock(&nat_lock);
	if (dir == DIR_DNAT) {
		list = &dst_list[hash];
	} else if (dir == DIR_SNAT) {
		list = &src_list[hash];
	} else {
		spin_unlock(&nat_lock);
		goto out;
	}

	hlist_for_each_safe(iter, tmp, list) {
		ent = hlist_entry(iter, struct static_nat_entry, node[dir]);
		/* 注意反转 */
		cmp_key = (ent->type == dir) ?
							ent->addr[0]:ent->addr[1];
		ret_value = (ent->type == dir) ?
							ent->addr[1]:ent->addr[0];
		if (addr_key == cmp_key) {
			ret = ret_value;
			if (opt == NAT_OPT_DEL) {
				if (dir == ent->type) {
					hlist_del(&ent->node[0]);
					hlist_del(&ent->node[1]);
					kfree(ent);
				} else {
					ret = 0;
				}
			}
			if (opt & NAT_OPT_ACCT_BIT) {
				ent->acct[dir].nat_packets ++;
				ent->acct[dir].nat_bytes += skb == NULL?1:skb->len;
			}
			break;
		} 
	}
	spin_unlock(&nat_lock);
out:
	return ret;
}

/*
 * 更新第四层的校验码信息
 */
static void nat4_update_l4(struct sk_buff *skb, __be32 oldip, __be32 newip)
{
	struct iphdr *iph = ip_hdr(skb);
	void *transport_hdr = (void *)iph + ip_hdrlen(skb);
	struct tcphdr *tcph;
	struct udphdr *udph;
	bool cond;

	switch (iph->protocol) {
	case IPPROTO_TCP:
		tcph = transport_hdr;
		inet_proto_csum_replace4(&tcph->check, skb, oldip, newip, true);
		break;
	case IPPROTO_UDP:
	case IPPROTO_UDPLITE:
		udph = transport_hdr;
		cond = udph->check != 0;
		cond |= skb->ip_summed == CHECKSUM_PARTIAL;
		if (cond) {
			inet_proto_csum_replace4(&udph->check, skb, oldip, newip, true);
			if (udph->check == 0) {
				udph->check = CSUM_MANGLED_0;
			}
		}
		break;
	}
}

/*
 * 在POSTROUTING上执行源地址转换:
 * 1.正向源地址转换;
 * 2.目标地址转换的逆向源地址转换
 */
static unsigned int ipv4_nat_out(unsigned int hooknum,
				 struct sk_buff *skb,
				 const struct net_device *in,
				 const struct net_device *out,
				 int (*okfn)(struct sk_buff *))
{
	unsigned int ret = NF_ACCEPT;
	__be32 to_trans = 0;

	struct iphdr *hdr = ip_hdr(skb);
	
	to_trans = get_address_from_map(skb, DIR_SNAT, hdr->saddr, NAT_OPT_FIND|NAT_OPT_ACCT_BIT);
	if (!to_trans) {
		goto out;
	}

	if (hdr->saddr == to_trans) {
		goto out;
	}

	/* 执行SNAT */	
	csum_replace4(&hdr->check, hdr->saddr, to_trans);
	nat4_update_l4(skb, hdr->saddr, to_trans);
	hdr->saddr = to_trans;
out:
 	return ret;
}

/*
 * 在PREROUTING上执行目标地址转换:
 * 1.正向目标地址转换;
 * 2.源地址转换的逆向目标地址转换
 */
static unsigned int ipv4_nat_in(unsigned int hooknum,
				      struct sk_buff *skb,
				      const struct net_device *in,
				      const struct net_device *out,
				      int (*okfn)(struct sk_buff *))
{
	unsigned int ret = NF_ACCEPT;
	__be32 to_trans = 0;

	struct iphdr *hdr = ip_hdr(skb);

	if (skb->nfct && skb->nfct != &nf_conntrack_untracked.ct_general) {
		goto out;
	}
	
	to_trans = get_address_from_map(skb, DIR_DNAT, hdr->daddr, NAT_OPT_FIND|NAT_OPT_ACCT_BIT);
	if (!to_trans) {
		goto out;
	}

	if (hdr->daddr == to_trans) {
		goto out;
	}
	
	/* 执行DNAT */
	csum_replace4(&hdr->check, hdr->daddr, to_trans);
	nat4_update_l4(skb, hdr->daddr, to_trans);
	hdr->daddr = to_trans;
	
	/*
	 *  设置一个notrack 防止其被track以及nat.
	 *  这是绝对合适的,因为既然是static的stateless NAT
	 *  我们就不希望它被状态左右
	 **/

	/*
	 * 其实,并不是主要避开基于conntrack的NAT就可以了,因为
	 * conntrack本身就不容你对两个方向的tuple进行随意修改
	 */
	if (!skb->nfct) {
		skb->nfct = &nf_conntrack_untracked.ct_general;
		skb->nfctinfo = IP_CT_NEW;
		nf_conntrack_get(skb->nfct);
	}

out:
	return ret;
}

static struct nf_hook_ops ipv4_nat_ops[] __read_mostly = {
	{
		.hook		= ipv4_nat_in,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_PRE_ROUTING,
		.priority	= NF_IP_PRI_CONNTRACK-1,
	},
	{
		.hook		= ipv4_nat_out,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_POST_ROUTING,
		.priority	= NF_IP_PRI_CONNTRACK+1,
	},
};

static char *parse_addr(const char *input, __be32 *from, __be32 *to)
{
	char *p1, *p2;
	size_t length = strlen(input);
	
	if (!(p1 = memchr(input, ' ', length))) {
		return NULL;
	}

	if (!(p2 = memchr(p1 + 1, ' ', length - (p1 + 1 - input)))) {
		return NULL;
	}

	if (!(in4_pton(input, p1 - input, (u8 *)from, ' ', NULL))
			|| !(in4_pton(p1 + 1, p2 - p1 - 1, (u8 *)to, ' ', NULL))) {
		return NULL;
	}

	return ++p2;
}

static ssize_t static_nat_config_write(struct file *file, const char *buffer, size_t count, loff_t *unused)
{
	int ret = 0;
	size_t length = count;
	__be32 from, to;
	u32 normal, reverse;
	char *buf = NULL;
	char *p;
	struct static_nat_entry *ent;

	if (length) {
		char *pp = (char *)(buffer + (length - 1)); 
		for (; (*pp < (char)32) || (*pp > (char)126); pp--) {
			if (length <= 0) {
				ret = -EINVAL;
				goto out;
			}
			length--;
		}
	} else {
		goto out;
	}

	buf = kzalloc((length + 1), GFP_ATOMIC);
	if (!buf) {
		ret = -ENOMEM;
		goto out;
	}
	memcpy(buf, buffer, length);
	if (!(p = parse_addr(buf + 1, &from, &to))) {
		ret = -EINVAL;
		goto out;
	}

	if ('+' == *buf) {
		ent = (struct static_nat_entry *)kzalloc(sizeof(struct static_nat_entry), GFP_KERNEL);
		if (!ent) {
			ret = -EFAULT;
			goto out;
		}

		/* 计算原始项的hash桶位置 */
		normal = jhash_1word(from, 1);
		normal = normal%BUCKETS;

		/* 计算反转位置的hash桶位置 */
		reverse = jhash_1word(to, 1);
		reverse = reverse%BUCKETS;

		/*
		 *  设置key/value对
		 *  注意,反转类型的hnode其key/value也要反转
		 */
		ent->addr[0] = from;
		ent->addr[1] = to;

		/* 初始化链表节点 */
		INIT_HLIST_NODE(&ent->node[DIR_SNAT]);
		INIT_HLIST_NODE(&ent->node[DIR_DNAT]);

		if (strstr(p, "src")) { /* 添加SNAT项,自动生成DNAT项 */
			/* 首先判断是否已经存在了 */
			if (get_address_from_map(NULL, DIR_SNAT, from, NAT_OPT_FIND) ||
					get_address_from_map(NULL, DIR_SNAT, to, NAT_OPT_FIND)) {
				ret = -EEXIST;
				kfree(ent);
				goto out;
			}

			/* 这是这个entry的type,用来区分生成的两条配置项 */
			ent->type = DIR_SNAT;

			/* 落实到链表 */
			spin_lock(&nat_lock);
			hlist_add_head(&ent->node[DIR_SNAT], &src_list[normal]);
			hlist_add_head(&ent->node[DIR_DNAT], &dst_list[reverse]);
			spin_unlock(&nat_lock);
		} else if(strstr(p, "dst")) { /* 添加DNAT项,自动生成SNAT项 */
			/* 首先判断是否已经存在了 */
			if (get_address_from_map(NULL, DIR_DNAT, from, NAT_OPT_FIND) ||
					get_address_from_map(NULL, DIR_DNAT, to, NAT_OPT_FIND)){
				ret = -EEXIST;
				kfree(ent);
				goto out;
			}

			/* 这是这个entry的type,用来区分生成的两条配置项 */
			ent->type = DIR_DNAT;

			/* 落实到链表 */
			spin_lock(&nat_lock);
			hlist_add_head(&ent->node[DIR_DNAT], &dst_list[normal]);
			hlist_add_head(&ent->node[DIR_SNAT], &src_list[reverse]);
			spin_unlock(&nat_lock);
		} else {
			ret = -EFAULT;
			kfree(ent);
			goto out;
		}

	} else if ('-' ==*buf) {
		u32 r1;

		if (strstr(p, "src")) {
			r1 = get_address_from_map(NULL, DIR_SNAT, from, NAT_OPT_DEL);
			if (r1 == 0) {
				ret = -ENOENT;
				goto out;
			}
		} else if(strstr(p, "dst")) {
			r1 = get_address_from_map(NULL, DIR_DNAT, from, NAT_OPT_DEL);
			if (r1 == 0) {
				ret = -ENOENT;
				goto out;
			}
		} else {
		}

	} else {
		ret = -EINVAL;
		goto out;
	}
	
	ret = count;
out:
	kfree(buf);
	return ret;
}

static ssize_t static_nat_config_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	int len = 0;
	static int done = 0;
	int i;
	char from[15], to[15];
	char *kbuf_to_avoid_user_space_memory_page_fault = NULL;

/* 每一行的最大长度 */
#define MAX_LINE_CHARS	128

	if (done) {
		done = 0;
		goto out;
	}
	
	/*
	 * 分配一块内核内存,为了避免直接操作用户内存而引发页面调度,
	 * 页面调度会导致睡眠切换,而我们操作的内容处在自旋锁的保护
	 * 下,所以不能切换!
	 */

	/*
	 * 问题:
	 * 我这里仅仅分配count大小的内存,是因为这个版本不支持多次读,
	 * 只能一次读完。也许我应该学学seq read的方法。
	 */
	kbuf_to_avoid_user_space_memory_page_fault = kzalloc(count, GFP_KERNEL);
	if (!kbuf_to_avoid_user_space_memory_page_fault) {
		len = -ENOMEM;
		done = 1;
		goto out;
	}	

	spin_lock(&nat_lock);
	len += sprintf(kbuf_to_avoid_user_space_memory_page_fault + len, "Source trans table:\n");
	if (len + MAX_LINE_CHARS > count) {
		goto copy_now;
	}
	for (i = 0; i < BUCKETS; i++) {
		struct hlist_node *iter, *tmp;
		struct static_nat_entry *ent;
		hlist_for_each_safe(iter, tmp, &src_list[i]) {
			ent = hlist_entry(iter, struct static_nat_entry, node[DIR_SNAT]);
			sprintf(from, "%pI4", (ent->type == DIR_SNAT)? &ent->addr[0]:&ent->addr[1]);
			sprintf(to, "%pI4", (ent->type == DIR_SNAT)? &ent->addr[1]:&ent->addr[0]);
			len += sprintf(kbuf_to_avoid_user_space_memory_page_fault + len, "From:%-15s To:%-15s    [%s]  [Bytes:%u  Packet:%u]\n", 
						from, 
						to, 
						(ent->type == DIR_SNAT)?"STATIC":"AUTO",
						ent->acct[DIR_SNAT].nat_bytes,
						ent->acct[DIR_SNAT].nat_packets);

			if (len + MAX_LINE_CHARS > count) {
				goto copy_now;
			}
		} 
	}
	len += sprintf(kbuf_to_avoid_user_space_memory_page_fault + len, "\nDestination trans table:\n");
	if (len + MAX_LINE_CHARS > count) {
		goto copy_now;
	}
	for (i = 0; i < BUCKETS; i++) {
		struct hlist_node *iter, *tmp;
		struct static_nat_entry *ent;
		hlist_for_each_safe(iter, tmp, &dst_list[i]) {
			ent = hlist_entry(iter, struct static_nat_entry, node[DIR_DNAT]);
			sprintf(from, "%pI4", (ent->type == DIR_DNAT)? &ent->addr[0]:&ent->addr[1]);
			sprintf(to, "%pI4", (ent->type == DIR_DNAT)? &ent->addr[1]:&ent->addr[0]);
			len += sprintf(kbuf_to_avoid_user_space_memory_page_fault + len, "From:%-15s To:%-15s    [%s]  [Bytes:%u  Packet:%u]\n", 
						from, 
						to, 
						(ent->type == DIR_DNAT)?"STATIC":"AUTO",
						ent->acct[DIR_DNAT].nat_bytes,
						ent->acct[DIR_DNAT].nat_packets);


			if (len + MAX_LINE_CHARS > count) {
				goto copy_now;
			}
		} 
	}
copy_now:
	spin_unlock(&nat_lock);
	done = 1;
	/* 这里已经解除自旋锁 */
	if (copy_to_user(buf, kbuf_to_avoid_user_space_memory_page_fault, len))  {
		len = EFAULT;
		goto out;
	}
	
out:
	if (kbuf_to_avoid_user_space_memory_page_fault) {
		kfree(kbuf_to_avoid_user_space_memory_page_fault);
	}
	return len;
}

static const struct file_operations static_nat_file_ops = {
	.owner		= THIS_MODULE,
	.read		= static_nat_config_read,
	.write		= static_nat_config_write,
};

static int __init nf_static_nat_init(void)
{
	int ret = 0;
	int i;

	src_list = kzalloc(sizeof(struct hlist_head) * BUCKETS, GFP_KERNEL);
	if (!src_list) {
		ret = -ENOMEM;
		goto out;
	}
	dst_list = kzalloc(sizeof(struct hlist_head) * BUCKETS, GFP_KERNEL);
	if (!dst_list) {
		ret = -ENOMEM;
		goto out;
	}

	ret = nf_register_hooks(ipv4_nat_ops, ARRAY_SIZE(ipv4_nat_ops));
	if (ret < 0) {
		printk("nf_nat_ipv4: can't register hooks.\n");
		goto out;
	}

	if (!proc_create("static_nat", 0644, init_net.proc_net, &static_nat_file_ops)) {
        ret = -ENOMEM;
		goto out;
	}

	for (i = 0; i < BUCKETS; i++) {
		INIT_HLIST_HEAD(&src_list[i]);
		INIT_HLIST_HEAD(&dst_list[i]);
	}
	return ret;
out:
	if (src_list) {
		kfree(src_list);
	} 
	if (dst_list) {
		kfree(dst_list);
	} 

	return ret;
}

static void __exit nf_static_nat_fini(void)
{
	int i;

	remove_proc_entry("static_nat", init_net.proc_net);
	nf_unregister_hooks(ipv4_nat_ops, ARRAY_SIZE(ipv4_nat_ops));

	spin_lock(&nat_lock);
	for (i = 0; i < BUCKETS; i++) {
		struct hlist_node *iter, *tmp;
		struct static_nat_entry *ent;
		hlist_for_each_safe(iter, tmp, &src_list[i]) {
			ent = hlist_entry(iter, struct static_nat_entry, node[0]);
			hlist_del(&ent->node[DIR_SNAT]);
			hlist_del(&ent->node[DIR_DNAT]);
			kfree(ent);
		} 
	}
	spin_unlock(&nat_lock);
	if (src_list) {
		kfree(src_list);
	} 
	if (dst_list) {
		kfree(dst_list);
	} 
}

module_init(nf_static_nat_init);
module_exit(nf_static_nat_fini);

MODULE_DESCRIPTION("STATIC two-way NAT");
MODULE_AUTHOR("marywangran@126.com");
MODULE_LICENSE("GPL");


Makefile:

obj-m += nf_rawnat.o

all: 
	make -C /lib/modules/`uname -r`/build SUBDIRS=`pwd` modules

clean:
	rm -rf *.ko *.o .tmp_versions .*.mod.o .*.o.cmd *.mod.c .*.ko.cmd Module.symvers modules.order


我不看好应用相关的私活儿并不代表我不喜欢,并不代表我不会。工作毕竟已经很累了,干嘛还要继续累啊。工作之余,要搞点别的,别的是什么呢?stateless NAT...这是很多人都不涉足的,我感叹,我悲哀,微斯人,吾谁与归?



 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1592083

相关文章
|
1月前
|
网络协议 Ubuntu Linux
Linux 动态/静态配置ip网卡信息
Linux 动态/静态配置ip网卡信息
36 0
|
1月前
|
存储 Shell Linux
【Shell 命令集合 系统设置 】Linux 生成并更新内核模块的依赖 depmod命令 使用指南
【Shell 命令集合 系统设置 】Linux 生成并更新内核模块的依赖 depmod命令 使用指南
32 0
|
1月前
|
Shell Linux C语言
【Shell 命令集合 系统设置 】⭐Linux 卸载已加载的内核模块rmmod命令 使用指南
【Shell 命令集合 系统设置 】⭐Linux 卸载已加载的内核模块rmmod命令 使用指南
29 1
|
4月前
|
安全 网络协议 Linux
Linux加强篇010-使用Apache服务部署静态网站
山重水复疑无路,柳暗花明又一村
282 0
|
4月前
|
网络协议 Linux 网络架构
Linux三种网络模式 | 仅主机、桥接、NAT
Linux三种网络模式 | 仅主机、桥接、NAT
171 0
|
1月前
|
存储 Linux Shell
【Shell 命令集合 系统设置 】Linux 获取指定模块的元信息 minfo命令 使用指南
【Shell 命令集合 系统设置 】Linux 获取指定模块的元信息 minfo命令 使用指南
29 0
|
1月前
|
Linux Shell 文件存储
【Shell 命令集合 系统设置 】Linux 加载和卸载内核模块 modprobe命令 使用指南
【Shell 命令集合 系统设置 】Linux 加载和卸载内核模块 modprobe命令 使用指南
47 1
|
1天前
|
Linux
Linux(23) Linux 4G模块不能获取IP排查思路
Linux(23) Linux 4G模块不能获取IP排查思路
5 0
|
27天前
|
应用服务中间件 Linux PHP
Linux下安装php环境并且配置Nginx支持php-fpm模块
Linux下安装php环境并且配置Nginx支持php-fpm模块
28 0
|
1月前
|
存储 Linux Shell
【Shell 命令集合 系统设置 】Linux 显示Linux内核模块的详细信息 modinfo命令 使用指南
【Shell 命令集合 系统设置 】Linux 显示Linux内核模块的详细信息 modinfo命令 使用指南
26 0