设为首页 收藏本站
查看: 398|回复: 0

[经验分享] 在Linux上实现一个可用的stateless双向静态NAT模块

[复制链接]
累计签到:1 天
连续签到:1 天
发表于 2014-12-22 09:15:32 | 显示全部楼层 |阅读模式
关于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        0x02enum 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) {                        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) {                        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);                INIT_HLIST_HEAD(&dst_list);        }        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) {                        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.oall:         make -C /lib/modules/`uname -r`/build SUBDIRS=`pwd` modulesclean:        rm -rf *.ko *.o .tmp_versions .*.mod.o .*.o.cmd *.mod.c .*.ko.cmd Module.symvers modules.order
我不看好应用相关的私活儿并不代表我不喜欢,并不代表我不会。工作毕竟已经很累了,干嘛还要继续累啊。工作之余,要搞点别的,别的是什么呢?stateless NAT...这是很多人都不涉足的,我感叹,我悲哀,微斯人,吾谁与归?...



运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-38370-1-1.html 上篇帖子: OpenSSL的SSL/BIO_get_fd 下篇帖子: linux负载均衡集群之LVS-DR Linux
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表