收藏 分销(赏)

LinuxIPv4路由功能的分析.doc

上传人:pc****0 文档编号:7201220 上传时间:2024-12-27 格式:DOC 页数:26 大小:502KB
下载 相关 举报
LinuxIPv4路由功能的分析.doc_第1页
第1页 / 共26页
LinuxIPv4路由功能的分析.doc_第2页
第2页 / 共26页
点击查看更多>>
资源描述
IPv4路由设计与实现相关知识 文档编号:00-6201-100 当前版本:1.0.0.0 创建日期:2011-9-1 编写作者:ganjingwei 路由知识总结 前言 3 关于此文档 3 参考资料 3 第一章 路由的基本概念 4 1.1 路由的作用 4 1.2 路由的工作原理 4 第二章 数据结构 5 2.1 fib_table 5 2.2 fn_hash 5 2.3 fn_zone 5 2.4 fib_node 6 2.5 fib_alias 6 2.6 fib_info 7 2.7 fib_nh 7 第三章 路由表的创建 9 3.1 创建路由表 9 3.2 掩码长度分类级 10 3.3 网络地址分类级 12 3.4 路由信息分类级 13 3.5 具体创建过程 16 第四章 Linux路由功能实现 19 2.1 数据包流程 19 2.2 数据包路由过程 20 2.3 相关代码 22 2.3.1 ip_rcv_finish函数 22 2.3.2 ip_route_input函数 22 2.3.3 ip_route_input_slow函数 23 2.3.4 __mkroute_input函数 25 2.3.5 fib_lookup函数 26 前言 关于此文档 此文档是本人这段时间内学习Linux网络协议栈路由功能相关知识,总结并且整理出来的文档。供大家参考。 本文档描述Linux相关知识,各章节说明如下: 1 前言,即此章节; 2 路由理论知识; 3 相关数据结构; 4 路由表的创建; 5 Linux路由的实现及数据包流程。 参考资料 网络资源。 本文中的所有代码以broadcom4.12L.01为依据,内核代码为2.6。 第一章 路由的基本概念 1.1 路由的作用 路由器工作在ISO层次结构中的三层,根据三层协议的各种信息,完成数据包的选路和转发,最终达到目的地。 1.2 路由的工作原理 路由与网桥的区别,在于路由了解整个网络,而网桥只了解相连接的主机或网络。因此路由在选路的过程中,更具有目的性。 路由维持一张(多张)路由表来支持选路的功能。路由表与网桥表不同,路由表是人为配置的(这里说的人为配置,可能是用户通过接口进行配置,也可以是应用层本身对内核进行路由配置,比如动态更新路由自主学习等)。 每个路由表中记录的信息有:源地址(网络)、目的地址(网络)、目的网关、scope(距离)。查询根据这些来匹配。匹配过程中,先匹配一个路由缓存,再匹配路由表。路由缓存是由内核维持的,而路由表是人为配置的。缓存根据最近使用的表项来更新,目的是为了提高查询效率,和CPU的cache一样。在内核中,路由表由fib_table_hash[]表示,路由缓存由rt_hash_table[]表示。 第二章 数据结构 2.1 fib_table 这是一个路由表的最高级结构。 struct fib_table { struct hlist_node tb_hlist; u32 tb_id; unsigned tb_stamp; int tb_default; int (*tb_lookup)(struct fib_table *tb, const struct flowi *flp, struct fib_result *res); int (*tb_insert)(struct fib_table *, struct fib_config *); int (*tb_delete)(struct fib_table *, struct fib_config *); int (*tb_dump)(struct fib_table *table, struct sk_buff *skb, struct netlink_callback *cb); int (*tb_flush)(struct fib_table *table); void (*tb_select_default)(struct fib_table *table, const struct flowi *flp, struct fib_result *res); unsigned char tb_data[0]; }; 2.2 fn_hash 这是路由表一级查找(按掩码查找)的散列结构体,包含了散列表头和散列数组。 struct fn_hash { struct fn_zone *fn_zones[33]; struct fn_zone *fn_zone_list; }; 2.3 fn_zone 这是路由表一级查找(按掩码查找)的具体信息保存的结构体。 struct fn_zone { struct fn_zone *fz_next; /* Next not empty zone */ struct hlist_head *fz_hash; /* Hash table pointer */ int fz_nent; /* Number of entries */ int fz_divisor; /* Hash divisor */ u32 fz_hashmask; /* (fz_divisor - 1) */ #define FZ_HASHMASK(fz) ((fz)->fz_hashmask) int fz_order; /* Zone order */ __be32 fz_mask; #define FZ_MAS K(fz) ((fz)->fz_mask) }; 2.4 fib_node 这是路由表二级查找(网络地址查找)具体信息存放的结构体,其中fn_key就是网络地址,通过fn_key和掩码长度来计算散列定位。 struct fib_node { struct hlist_node fn_hash; struct list_head fn_alias; __be32 fn_key; struct fib_alias fn_embedded_alias; }; 2.5 fib_alias 这个结构体包含了路由的具体信息的结构体,但是它不对其进行描述,而是描述一些键值,用于标识和排序那些信息。真正的信息包含在fa_info中。 struct fib_alias { struct list_head fa_list; struct fib_info *fa_info; u8 fa_tos; u8 fa_type; u8 fa_scope; u8 fa_state; #ifdef CONFIG_IP_FIB_TRIE struct rcu_head rcu; #endif }; 2.6 fib_info 这个才是真正的路由信息结构。 struct fib_info { struct hlist_node fib_hash; struct hlist_node fib_lhash; struct net *fib_net; int fib_treeref; atomic_t fib_clntref; int fib_dead; unsigned fib_flags; int fib_protocol; __be32 fib_prefsrc; u32 fib_priority; u32 fib_metrics[RTAX_MAX]; #define fib_mtu fib_metrics[RTAX_MTU-1] #define fib_window fib_metrics[RTAX_WINDOW-1] #define fib_rtt fib_metrics[RTAX_RTT-1] #define fib_advmss fib_metrics[RTAX_ADVMSS-1] int fib_nhs; #ifdef CONFIG_IP_ROUTE_MULTIPATH int fib_power; #endif struct fib_nh fib_nh[0]; #define fib_dev fib_nh[0].nh_dev }; 2.7 fib_nh 这个结构被包含在fib_info中,是路由下一条的信息,一般来说只有一个,但是如果有多通路,也就是配置了CONFIG_IP_ROUTE_MULTIPATH,才会有多个。 struct fib_nh { struct net_device *nh_dev; struct hlist_node nh_hash; struct fib_info *nh_parent; unsigned nh_flags; unsigned char nh_scope; #ifdef CONFIG_IP_ROUTE_MULTIPATH int nh_weight; int nh_power; #endif #ifdef CONFIG_NET_CLS_ROUTE __u32 nh_tclassid; #endif int nh_oif; __be32 nh_gw; }; 第三章 路由表的创建 3.1 创建路由表 默认路由表由内核创建的函数调用流程为: inet_init() ->ip_init() ->ip_fib_init() ->fib_net_init() ->ip_fib_net_init()[net/ipv4/fib_frontend.c] 在这个函数中,首先为路由表分配空间,这里的每个表项hlist_head实际都会链接一个单独的路由表,FIB_TABLE_HASHSZ表示了分配多少个路由表,一般情况下至少有两个——LOCAL和MAIN。注意这里仅仅是表头的空间分配,还没有真正分配路由表空间。 net->ipv4.fib_table_hash = kzalloc( sizeof(struct hlist_head)*FIB_TABLE_HASHSZ, GFP_KERNEL); 接下来的err = fib4_rules_init(net); 真正分配了路由表空间 local_table = fib_hash_table(RT_TABLE_LOCAL); if (local_table == NULL) return -ENOMEM; main_table = fib_hash_table(RT_TABLE_MAIN); if (main_table == NULL) goto fail; 然后将local和main表链入之前的fib_table_hash中 hlist_add_head_rcu(&local_table->tb_hlist, &net->ipv4.fib_table_hash[TABLE_LOCAL_INDEX]); hlist_add_head_rcu(&main_table->tb_hlist, &net->ipv4.fib_table_hash[TABLE_MAIN_INDEX]); 最后生成的结构如图3-1,LOCAL表位于fib_table_hash[0],MAIN表位于fib_table_hash[1];两张表通过结构tb_hlist链入链表,而tb_id则标识了功能,255是LOCAL表,254是MAIN表。 图3-1 路由表内核内部结构 路由表的查找效率是第一位的,因此内核在实现时使用了多级索引来进行加速: 第一级:fn_zone,按不同掩码长度分类(如/5和/24); 第二级:fib_node,按不同网络地址分类(如124.44.33.0/24); 第三级:fib_info,按下一跳路由信息。 当然,我们创建路由表也要按照这个顺序。 3.2 掩码长度分类级 关于上一节说的struct fn_hash,它表示了不同子网掩码长度hash表(即fn_zone),对于ipv4,从0~32共33个。而fn_hash的实现则是fib_table的最后一个参数unsigned char tb_data[0]。 注意这里fn_zone还只是空指针,我们还只完成了路由表初始化的一部分。在启动阶段还会调用inet_rtm_newroute()->fib_table_insert()->fn_new_zone()[fib_hash.c]来创建fn_zone结构,前面已经讲过,fn_zone一共有33个,其中掩码长度为0[/0]表示为默认路由,fn_zone可以理解为相同掩码的地址集合。 首先为fn_zone分配空间 struct fn_zone *fz = kzalloc(sizeof(struct fn_zone), GFP_KERNEL); 传入参数z代表掩码长度,z=0的掩码用于默认路由,一般只有一个,所以fz_divisor只需设1;其它设为16;这里要提到fz_divisor的作用,fz->fz_hash并不是个单链表,而是一个哈希表,而哈希表的大小就是fz_divisor。 if (z) { fz->fz_divisor = 16; } else { fz->fz_divisor = 1; } fz_hashmask实际是用于求余数的,当算出hash值,再hash & fz_hashmask就得出了在哈希表的位置;而fz_hash就是下一层的哈希表了,前面已经提过路由表被多组分层了,这里fz_hash就是根据fz_divisor大小来创建的;fz_order就是子网掩码长度;fz_mask就是子网掩码。 fz->fz_hashmask = (fz->fz_divisor - 1); fz->fz_hash = fz_hash_alloc(fz->fz_divisor); fz->fz_order = z; fz->fz_mask = inet_make_mask(z); 从子网长度大于新添加fz的fn_zone中挑选一个不为空的fn_zones[i],将新创建fz设成fn_zones[i].next;然后将fz根据掩码长度添加到fn_zones[]中相应位置;fn_zone_list始终指向掩码长度最长的fn_zone。 for (i=z+1; i<=32; i++) if (table->fn_zones[i]) break; if (i>32) { fz->fz_next = table->fn_zone_list; table->fn_zone_list = fz; } else { fz->fz_next = table->fn_zones[i]->fz_next; table->fn_zones[i]->fz_next = fz; } table->fn_zones[z] = fz; fn_hash包含33数组元素,每个元素存放一定掩码长度的fn_zone,其中fn_zone[i]存储掩码长度为i。而fn_zone通过内部属性fz_next又彼此串连起来,形成单向链表,其中fn_zone_list可以看作链表头,而这里链表的组织顺序是倒序的,即从掩码长到短。其中结构如图3-2所示。 图3-2 路由表一级查找结构 3.3 网络地址分类级 到上一节为止,fz_hash所分配的哈希表还没有插入内容,这部分为fib_insert_node()完成: inet_rtm_newroute()->fib_table_insert()->fib_insert_node()[net/ipv4/fib_hash.c]。 这里f是fib_node,可以理解为具有相同网络地址的路由项集合。根据fn_key(网络地址)和fz(掩码长度)来计算hash值,决定将f插入fz_hash的哪个项。 struct hlist_head *head = &fz->fz_hash[fn_hash(f->fn_key, fz)]; hlist_add_head(&f->fn_hash, head); } 如果fib_node不存在,则会创建它,这里的kmem_cache_zalloc()其实就是内存分配。 new_f = kmem_cache_zalloc(fn_hash_kmem, GFP_KERNEL); if (new_f == NULL) goto out; INIT_HLIST_NODE(&new_f->fn_hash); INIT_LIST_HEAD(&new_f->fn_alias); new_f->fn_key = key; f = new_f; 3.4 路由信息分类级 路由表最后一层是fib_info,具体的路由信息都存储在此,它由fib_create_info()创建。 首先为fib_info分配空间,由于fib_info的最后一个属性是struct fib_nh fib_nh[0],因此大小是fib_info + nhs * fib_nh,这里的fib_nh代表了下一跳(next hop)的信息,nhs代表了下一跳的数目,一般情况下nhs=1,除非配置了支持多路径。 fi = kzalloc(sizeof(*fi)+nhs*sizeof(struct fib_nh), GFP_KERNEL); 设置fi的相关属性 fi->fib_net = hold_net(net); fi->fib_protocol = cfg->fc_protocol; fi->fib_flags = cfg->fc_flags; fi->fib_priority = cfg->fc_priority; fi->fib_prefsrc = cfg->fc_prefsrc; fi->fib_nhs = nhs; 使fi后面所有的nh->nh_parent指向fi,设置后如图3-3所示 change_nexthops(fi) { nexthop_nh->nh_parent = fi; } endfor_nexthops(fi) 图3-3 fib_info结构体 设置fib_nh的属性,这里仅展示了单一路径的情况: struct fib_nh *nh = fi->fib_nh; nh->nh_oif = cfg->fc_oif; nh->nh_gw = cfg->fc_gw; nh->nh_flags = cfg->fc_flags; 然后,再根据cfg->fc_scope值来设置nh的其余属性。如果scope是RT_SCOPE_HOST,则设置下一跳scope为RT_SCOPE_NOWHERE。 if (cfg->fc_scope == RT_SCOPE_HOST) { struct fib_nh *nh = fi->fib_nh; nh->nh_scope = RT_SCOPE_NOWHERE; nh->nh_dev = dev_get_by_index(net, fi->fib_nh->nh_oif); } 如果scope是RT_SCOPE_LINK或RT_SCOPE_UNIVERSE,则设置下跳: change_nexthops(fi) { if ((err = fib_check_nh(cfg, fi, nexthop_nh)) != 0) goto failure; } endfor_nexthops(fi) 最后,将fi链入链表中,这里要注意的是所有的fib_info(只要创建了的)都会加入fib_info_hash中,如果路由项使用了优先地址属性,还会加入fib_info_laddrhash中。 hlist_add_head(&fi->fib_hash, &fib_info_hash[fib_info_hashfn(fi)]); if (fi->fib_prefsrc) { struct hlist_head *head; head = &fib_info_laddrhash[fib_laddr_hashfn(fi->fib_prefsrc)]; hlist_add_head(&fi->fib_lhash, head); } 无论fib_info在路由表中位于哪个掩码、哪个网段结构下,都与fib_info_hash和fib_info_laddrhash无关,这两个哈希表与路由表独立,主要是用于加速路由信息fib_info的查找。哈希表的大小为fib_hash_size,当超过这个限制时,fib_hash_size * 2(如果哈希函数够好,每个bucket都有一个fib_info)。fib_info在哈希表如图3-4所示。 图3-4 fib_info结构与它的hash表 由于路由表信息也可能要以设备dev为键值搜索,因此还存在fib_info_devhash哈希表,用于存储nh的设置dev->ifindex。 change_nexthops(fi) { hash = fib_devindex_hashfn(nexthop_nh->nh_dev->ifindex); head = &fib_info_devhash[hash]; hlist_add_head(&nexthop_nh->nh_hash, head); } endfor_nexthops(fi) 3.5 具体创建过程 上面讲过了路由表各个部分的创建,现在来看下它们是如何一起工作的,在fib_table_insert()[net/ipv4/fib_hash.c]完成整个的路由表创建过程。下面来看下fib_table_insert()函数: 从fn_zones中取出掩码长度为fc_dst_len的项,如果该项不存在,则创建它(fn_zone的创建前面已经讲过)。 fz = table->fn_zones[cfg->fc_dst_len]; if (!fz && !(fz = fn_new_zone(table, cfg->fc_dst_len))) return -ENOBUFS; 然后创建fib_info结构,(前面已经讲过) fi = fib_create_info(cfg); 然后在掩码长度相同项里查找指定网络地址key(如145.222.33.0/24),查找的结果如图3-5所示. f = fib_find_node(fz, key); 图3-5 查找网络地址key 如果不存在该网络地址项,则创建相应的fib_node,并加入到链表fz_hash中。 if (!f) { new_f = kmem_cache_zalloc(fn_hash_kmem, GFP_KERNEL); if (new_f == NULL) goto out; INIT_HLIST_NODE(&new_f->fn_hash); INIT_LIST_HEAD(&new_f->fn_alias); new_f->fn_key = key; f = new_f; } …… fib_insert_node(fz, new_f); 如果存在该网络地址项,则在fib_node的属性fn_alias中以tos和fi->fib_priority作为键值查找。一个fib_node可以有多个fib_alias相对应,这些fib_alias以链表形式存在,并按tos并从大到小的顺序排列。因此,fib_find_alias查找到的是第一个fib_alias->tos不大于tos的fib_alias项。 fa = fib_find_alias(&f->fn_alias, tos, fi->fib_priority); 如果查找到的fa与与要插入的路由项完全相同,则按照设置的标置位进行操作,NLM_F_REPLACE则替换掉旧的,NLM_F_APPEND添加在后面。 设置要插入的fib_alias的属性,包括最重要的fib_alias->fa_info设置为fi。 new_fa->fa_info = fi; new_fa->fa_tos = tos; new_fa->fa_type = cfg->fc_type; new_fa->fa_scope = cfg->fc_scope; new_fa->fa_state = 0; 如果没有要插入路由的网络地址项fib_node,则之前已经创建了新的,现在将它插入到路由表中fib_insert_node();然后将new_fa链入到fib_node->fn_alias中。 if (new_f) fib_insert_node(fz, new_f); list_add_tail(&new_fa->fa_list, (fa ? &fa->fa_list : &f->fn_alias)); 最后,由于新插入的路由表项,会发出通告,告知所以加入RTNLGRP_IPV4_ROUTE组的成员,这个功能可以在linux中使用”ip route monitor”来测试。最终的路由表如图3-6所示。 rtmsg_fib(RTM_NEWROUTE, key, new_fa, cfg->fc_dst_len, tb->tb_id, &cfg->fc_nlinfo, 0); 图3-6 路由表总体结构 第四章 Linux路由功能实现 2.1 数据包流程 图4-1 Linux网络层的数据包函数调用流程 如图4-1所示,ip_rcv是IPv4数据包的基本接收函数,由下层调用。这个函数完成一系列的校验、协议处理等等,然后进入第一个HOOK点,这是在本机路由前的点。 在ip_rcv_finish函数中,会调用ip_route_input函数来进行本机路由,判断是发送给本机的,还是需要转发的,由此来知道下一处理函数是ip_local_deliver,还是ip_forward。至于ip_route_input是如何进行路由的,将在下一节进行讲解。 ip_local_deliver以上,是本机数据包流程,这与路由无关,这里不做赘述。 ip_forward进行一些路由的处理,比如设置网关、MTU,TTL减1等等。然后进入ip_forward_finish,根据之前设置的skb->dst->output函数来确定去处。这个output也是在之前的路由过程中确定的,具体是单播、多播、还是广播等等,视之前的路由和协议而定。 2.2 数据包路由过程 之前说了ip_rcv_finish函数中,调用ip_route_input函数来进行本机路由。具体流程如图4-2所示。 图4-2 路由过程 ip_route_input函数中,首先去路由缓存rt_ hash_table中查找,如果找到则直接返回。如果没有找到,则调用ip_route_input_slow来查找路由表。 ip_route_input_slow函数中调用fib_lookup来查找。fib_lookup有两种定义,根据不同的功能编译开关。一种是只查找main表和local表,这是低级路由。另一种则会遍历rule表,先匹配应该查找哪一张路由表(可能是高级路由配置的),然后再对该表进行查询。 如果查询结果是本机的数据包,则会在ip_route_input_slow函数的后面部分进行数据包路由信息的更改,最重要的是入口函数改成ip_local_deliver。然后调用rt_intern_hash函数来更新路由缓存。 如果结果是需要转发的,则调用ip_mkroute_input->__mkroute_input来做进一步的处理(ip_mkroute_input中有一个分支,多路路由,这里不作介绍)。也就是把查找到的路由信息加入路由缓存,并把路由结果传递给数据包。 2.3 相关代码 2.3.1 ip_rcv_finish函数 …………………………………… if (skb->dst == NULL) { int err = ip_route_input(skb, READ32_ALIGNED(iph->daddr), READ32_ALIGNED(iph->saddr), iph->tos, skb->dev); if (unlikely(err)) { if (err == -EHOSTUNREACH) IP_INC_STATS_BH(dev_net(skb->dev), IPSTATS_MIB_INADDRERRORS); else if (err == -ENETUNREACH) IP_INC_STATS_BH(dev_net(skb->dev), IPSTATS_MIB_INNOROUTES); goto drop; } } …………………………………… 2.3.2 ip_route_input函数 …………………………………… for (rth = rcu_dereference(rt_hash_table[hash].chain); rth; rth = rcu_dereference(rth->u.dst.rt_next)) { if (((rth->fl.fl4_dst ^ daddr) | (rth->fl.fl4_src ^ saddr) | (rth->fl.iif ^ iif) | rth->fl.oif | (rth->fl.fl4_tos ^ tos)) == 0 && rth->fl.mark == skb->mark && net_eq(dev_net(rth->u.dst.dev), net) && !rt_is_expired(rth)) { dst_use(&rth->u.dst, jiffies); RT_CACHE_STAT_INC(in_hit); rcu_read_unlock(); skb->rtable = rth; return 0; } …………………………………… return ip_route_input_slow(skb, daddr, saddr, tos, dev); 2.3.3 ip_route_input_slow函数 …………………………………… if ((err = fib_lookup(net, &fl, &res)) != 0) { if (!IN_DEV_FORWARD(in_dev)) goto e_hostunreach; goto no_route; } …………………………………… if (res.type == RTN_LOCAL) { int result; result = fib_validate_source(saddr, daddr, tos, net->loopback_dev->ifindex, dev, &spec_dst, &itag); if (result < 0) goto martian_source; if (result) flags |= RTCF_DIRECTSRC; spec_dst = daddr; goto local_input; } …………………………………… err = ip_mkroute_input(skb, &res, &fl, in_dev, daddr, saddr, tos); …………………………………… local_input: rth = dst_alloc(&ipv4_dst_ops); if (!rth) goto e_nobufs; rth->u.dst.output= ip_rt_bug; rth->rt_genid = rt_genid(net); atomic_set(&rth->u.dst.__refcnt, 1); rth->u.dst.flags= DST_HOST; if (IN_DEV_CONF_GET(in_dev, NOPOLICY)) rth->u.dst.flags |= DST_NOPOLICY; rth->fl.fl4_dst = daddr; rth->rt_dst = daddr; rth->fl.fl4_tos = tos; rth->fl.mark = skb->mark; rth->fl.fl4_src = saddr; rth->rt_src = saddr; rth->rt_iif = rth->fl.iif = dev->ifindex; rth->u.dst.dev = net->loopback_dev; dev_hold(rth->u.dst.dev); rth->idev = in_dev_get(rth->u.dst.dev); rth->rt_gateway = daddr; rth->rt_spec_dst= spec_dst; rth->u.dst.input= ip_local_deliver; rth->rt_flags = flags|RTCF_LOCAL; if (res.type == RTN_UNREACHABLE) { rth->u.dst.input= ip_error; rth->u.dst.error= -err; rth->rt_flags &= ~RTCF_LOCAL; } rth->rt_type = res.type; hash = rt_hash(daddr, saddr, fl.iif, rt_genid(net)); err = rt_intern_hash(hash, rth, &skb->rtable); …………………………………… 2.3.4 __mkroute_input函数 …………………………………… rth->u.dst.flags= DST_HOST; if (IN_DEV_CONF_GET(in_dev, NOPOLICY)) rth->u.dst.flags |= DST_NOPOLICY; if (IN_DEV_CONF_GET(out_dev, NOXFRM)) rth->u.dst.flags |= DST_NOXFRM; rth->fl.fl4_dst = daddr; rth->rt_dst
展开阅读全文

开通  VIP会员、SVIP会员  优惠大
下载10份以上建议开通VIP会员
下载20份以上建议开通SVIP会员


开通VIP      成为共赢上传
相似文档                                   自信AI助手自信AI助手

当前位置:首页 > 百科休闲 > 其他

移动网页_全站_页脚广告1

关于我们      便捷服务       自信AI       AI导航        抽奖活动

©2010-2025 宁波自信网络信息技术有限公司  版权所有

客服电话:4009-655-100  投诉/维权电话:18658249818

gongan.png浙公网安备33021202000488号   

icp.png浙ICP备2021020529号-1  |  浙B2-20240490  

关注我们 :微信公众号    抖音    微博    LOFTER 

客服