Linux内核ICMP协议的实现

主要参考了《深入Linux内核架构》和《精通Linux内核网络》相关章节

Internet控制消息协议(ICMP)

本节只讨论ICMPv4协议

**近些年来,ICMP协议已逐渐成为开发监控和测量应用程序的基础。**不幸的是,ICMP协议也时常被用作安全攻击的基础,例如,DoS或者远程指纹收集(remote fingerprintcollection)。因此,网络管理员时常把路由器和防火墙配置成过滤掉多数ICMP消息类型。偶尔,网络管理员过滤掉太多了,因而违反了RFC建议案。无论消息是否被过滤掉,消息通常有速率限制。因此,建立在ICMP之上的任何应用程序就测量和监控目的而言,不见得都可靠。然而,因为测量本来就不是原本设计目标,ICMP通常无法让监控应用程序收集到其所需的所有信息。相反的,为了收集完整信息,另外又开发了专用应用程序,而其基础通常是TCP或UDP。

ICMP(第四层协议)主要用作发送有关网络层(L3)错误和控制消息的机制,让你能够通过发送ICMP消息来获取有关通信环境中问题的反馈。

  • 这些消息提供了错误处理和诊断功能。
  • ICMP也可以被用来发起各种安全攻击。例如,Smurf攻击是一种拒绝服务攻击,它使用IP广播地址,以广播方式向计算机网络发送大量ICMP数据包,并将受害者的IP地址用作源地址。

ICMP相对比较简单,但对于确保系统正确的行为而言至关重要。

ICMPv4消息分两类:错误消息和信息消息(RFC 1812中称为“查询消息”)。ICMPv4被用于ping和traceroute等诊断工具。

ICMP协议的应用程序

ICMP消息可以由内核及用户空间应用程序传输。用户空间应用程序会使用raw IP套接字界面。有两个著名的网络除错工具是traceroute和ping,都是使用ICMP协议。其他使用raw IP套接字接口来进行传输或监听ICMP消息的是各种路由协议

ping

著名工具ping实际上是一个用户空间应用程序(位于iputils包中)。它打开一个原始套接字并发送一条ICMP_ECHO消息,进而收到以ICMP_REPLY消息的方式返回的响应,显示出来回时间及其他信息。

traceroute

**traceroute可用于寻找发出此命令的主机与特定目的地IP地址间的路由线路。**该路由线路就是一份途经路由器的IP地址列表。

traceroute可以使用UDP或ICMP来达到它的目标(注2)。默认情况下,traceroute使用UDP,但是,你可以用-工切换选项迫使它使用ICMP。后面就会知道,UDP法也得依赖ICMP消息才能成功。这两种做法背后都有相当多的窍门。

我们来说明这种基于ICMP的技术如何运作。当入口IP封包的IP报头的TTL字段为1而且需要做转发时,接收者会丢弃该封包而传送一条类型为ICMP_TIME_EXCEEDED而代码为工CMP_EXC_TTL的ICMP消息给来源地。**traceroute就是利用这条规则,一次找出一个居中跳点:传送ICMP_ECHO消息给目的地IP地址,而TTL字段的值会递增(从1起算),如此就可确保所有居中主机都会产生ICMP_TIME_EXCEEDED消息,而最后一个主机(也就是目的主机)则会以ICMP_ECHOREPLY消息回复。**图25-5是其范例。

image-20220719185938584

图中没有包含ICMP回复消息的TTL字段值,因为不同操作系统会使用不同的值(64和255是最常见者)。

**基于UDP协议的技术也有点相似:依然是利用TTL字段的处理方式,只不过不是用ICMP_ECHO消息,而是使用UDP封包和很高的目的地端口号(终端主机不太可能使用)。**当IP封包来到终端主机时,终端主机会返回类型为ICMP_DEST_UNREACH而代码为工CMP_PORT__UNREACH的ICMP消息。图25-6是这样的范例。

image-20220719190020610

就ICMP和UDP这两种情况而言,**居中主机是通过独立的“探测”封包逐一被发现出来的。**这样有两种结果值得一提:

  • 和居中路由器相关的来回时间会在不同时间点上反应出网络拥塞状态。因此,第n个居中路由器的来回时间通常会比第n-1个居中路由器的来回时间还要大,但不见得都是如此。
  • 用于到达第 n 跳的中间路由器可能与用于到达第 (n-1) 跳的中间路由器不同。通往指定目的地所选的路径牵涉到好几项不同因素,例如,动态路由变更、负载均衡器等。traceroute命令的源代码(你可以从最常见Linux 发行套件的下载服务器那儿下载)有一些例子值得一读。

ICMPv4初始化

ICMPv4的初始化是在引导阶段调用的方法inet_init()中完成的。方法inet_init()调用方法icmp_init(),后者再调用方法icmp_sk_init()。与其他IPv4协议一样,ICMPv4的注册也是在inet_init()中完成的。

  • 在方法icmp_sk_init()中
    • 为每个CPU创建一个ICMPv4套接字,并将其存储在一个数组中。
    • 将一些ICMP procfs变量初始化为默认值
  • 要访问当前的套接字,可使用方法icmp_sk(struct net *net)。这些套接字供方法icmp_push_reply()使用。
static const struct net_protocol icmp_protocol = {
	.handler =	icmp_rcv,
	.err_handler =	icmp_err,
	.no_policy =	1,
	.netns_ok =	1,
};
  • icmp_rcv: handler回调函数。这意味着,对于到来的数据包,如果其IP报头中的协议字段为IPPROTO_ICMP( Ox1),将调用icmp_rcv()。
  • no_policy:这个标志被设置为1,表示无需执行IPsec策略检查。例如,在ip_local_deliver_finish()中不会调用方法xfrm4_policy_check(),因为设置了标志no_policy。
  • netns_ok:这个标志被设置为l,表示这个协议支持网络命名空间。对于netns_ok字段为0的协议,方法inet_add_protocol()将失败,并返回错误-EINVAL。

ICMPv4报头

ICMPv4报头由类型(8位)、代码(8位)、校验和( 16位)和32位的可变部分(其内容取决于ICMPv4类型和代码)组成。ICMPv4报头的后面是有效载荷,其中包含原始数据包的IPv4报头和部分有效载荷。RFC1812指出,在确保ICMPv4数据报不超过576字节的前提下,有效载荷应尽可能多地包含原始数据报的内容。长度为576字节是根据RFC 791确定的。该RFC指出:所有主机都必须能够接收长达576字节的数据报。

struct icmphdr {
  __u8		type;
  /* 这一字段可以分辨出ICMP消息的类型。有些时候,类型就足以明确分辨消息,而其他时候则需要代码来区分同一种消息类型的不同变种。 */
  __u8		code;
  __sum16	checksum;
  union {
	struct {
		__be16	id;
		__be16	sequence;
	} echo;
	__be32	gateway;
	struct {
		__be16	__unused;
		__be16	mtu;
	} frag;
	__u8	reserved[4];
  } un;
};

image-20220719163049766

ICMP有效载荷

**当内核在处理入口IP封包而检测到错误时,就会传送ICMP错误消息。所有ICMP错误消息在ICMP有效载荷中都包含相同信息:触发ICMP消息传输的IP封包的IP报头,外加一部分IP有效载荷。**所得IP封包必须不能超过576个字节,包括外面的IP报头以及ICMP报头〔后一条规则是在RFC1812的4.3.2.3节中陈述的(更新了RFC 792的报头的定义)。根据旧版RFC 792所述,ICMP有效载荷只需包括原有的IP报头,外加原有传输报头 (64位)]。

**图25-2是ICMP_FRAG_NEEDED错误消息的范例(根据RFC 792)。图25-2 (a)就是触发ICMP消息传输的片段,而图25-2 (b)是ICMP消息。**注意,ICMP有效载荷也包括原有IP报头及一段传输报头。Linux和 RFC 1812兼容,因此,包括图25-2 (a)中所示的额外的区块(尺寸至多576字节)。

原有IP报头的“协议”字段会由ICMP消息的目的主机使用,用于分辨出正确的传输协议(此例为TCP),此外,ICMP有效载荷中一部分的传输报头(包含源端口和目的端口号)可以让同一个目的主机用于分辨出本地套接字。因此,目的主机就可多少追查出其造成错误的原因。

ICMPv4消息类型对应处理函数

icmp_control

就每种ICMP类型而言,都有一个icmp_control数据结构实例(定义在net/ipv4/icmp.c中)。其中有一个字段,指向一个函数的指针,而该函数会被调用处理入口ICMP消息。下面是它的字段:

ICMPv4模块定义了一个icmp_control对象数组——icmp_pointers。它将ICMPv4消息类型作为索引。下面来看看结构icmp_control的定义以及数组icmp_pointers。

net\ipv4\icmp.c

struct icmp_control {
	bool (*handler)(struct sk_buff *skb);
	short   error;		/* This ICMP is classed as an error message */
};
/*
 *	This table is the definition of how we handle ICMP.
 */
static const struct icmp_control icmp_pointers[NR_ICMP_TYPES + 1] {
...

NR_ICMP_TYPES是最大的ICMPv4消息类型编号,其值为18( include/uapi/linux/icmp.h )。这个数组中的icmp_control对象都是错误消息,如“目的地不可达”消息( ICMP_DEST_UNREACH ),因为字段error为1;字段error为0时,表示信息消息,如回应((ICMP_ECHO)。有些处理程序被分派给多种消息类型。下面来讨论处理程序及其管理的ICMPv4消息类型。

处理函数

ping_rcv

方法ping_ rcv()负责处理接收ping应答( ICMP_ ECHOREPLY )的工作。它是在ICMP套接字代码(netipv4/ping.c)中实现的。

在3.0之前的内核中,要发送ping,必须在用户空间创建一个原始套接字。有ping应答( ICMP_ ECHOREPLY消息)到来时,由发送ping的套接字进行处理。为帮助理解这是如何实现的,来看看ip_ local _deliver_ _finish()。 这个方法会处理到来的IPv4数据包,并将其交给相应的套接字。

**在Linux内核3.0中集成ICMP套接字(ping套接字)时,情况发生了变化。ping套接字将在3.3节讨论。这里需要指出的是,引入ICMP套接字后,==ping的发送方可以不是原始套接字。==例如,你可以这样创建一个套接字,socket(PF_INET,SOCK_DGRAN,PROT_ICMP),并使用它来发送ping数据包。**这个套接字不是原始套接字,因此回应应答不会被交给原始套接字,因为没有侦听它的原始套接字。为避免这种问题,ICMPv4模块使用回调函数ping_rcv()来处理ICMP_ECHOREPLY消息的接收工作。ping模块位于IPv4层( net/ipv4/ping.c ),但netlipv4/ping.c中的大多数代码都是双栈代码(适用于IPv4和IPv6),因此方法ping_rcv()也负责处理ICMPV6_ECHO_REPLY消息(请参见net/ipv6/icmp.c中的icmpv6_rcv() )。本章后面将更详细地讨论ICMP套接字。

icmp_discard

icmp_discard()是一个空处理程序,用于不存在的消息类型(编号在头文件中没有声明的消息类型)以及不需要做任何处理的消息,如ICMP_TIMESTAMPREPLY。ICMP_TIMESTAMP和ICMP_TIMESTAMPREPLY消息用于同步时间。发送方在ICMP_TIMESTAMP请求中发送始发( originate )时间戳,而接收方发送包含3个时间戳的ICMP_TIMESTAMPREPLY——时间戳请求的发送方发送的始发时间戳、接收时间戳和传输时间戳。有一些比ICMPv4时间戳消息更常用的时间同步协议,如网络时间协议(Network Time Protocol,NTP)。这里还需要说说地址掩码(AddressMask)请求(ICMP_ADDRESS )。它通常由主机发送给路由器,旨在获取合适的子网掩码。收到这种消息后,接收方应使用地址掩码应答消息进行应答。

ICMP_ADDRESS和 ICMP_ADDRESSREPLY消息以前由方法 icmp_address()和 icmp.address_reply()处理,而现在也会被icmp_discard()处理。原因是:可通过其他方式获得子网掩码,如DHCP。

icmp_unreach

消息类型ICMP_DEST_UNREACH、ICMP_TIME_EXCEED、ICMP_PARAME-TERPROB和ICMP_QUENCH由icmp_unreach()处理。
在很多情况下都会发送ICMP_DEST_UNREACH消息,3.1.4节将介绍其中一部分。在以下两种情况下会发送ICMP_TIME_EXCEEDED消息。
在ip_forward()中,数据包的TTL都会被减1。RFC 1700推荐将IPv4数据包的TTL设置为64。如果TTL变成了0,就表明应该将数据包丢弃,因为可能存在环路。因此,在ip_forward()中,如果发现TTL为0,将调用方法icmp_send()。

在以下两种情况下会发送ICMP_TIME_EXCEEDED消息。
在ip_forward()中,数据包的TTL都会被减1。RFC 1700推荐将IPv4数据包的TTL设置为64。如果TTL变成了0,就表明应该将数据包丢弃,因为可能存在环路。因此,在ip_forward()中,如果发现TTL为0,将调用方法icmp_send()。

ip_options

在方法ip_options_compile()或ip_options_rcv_srr() ( net/ipv4/ip_options.c )中,未能成功地分析IPv4报头选项时,将发送一条ICMP_PARAMETERPROB消息。选项是IPv4报头中可选的变长字段(最多40字节)。IP选项将在第4章讨论。

消息类型ICMP_QUENCH实际上已被摒弃。RFC 1812的4.3.3.3节(“Source Quench”)指出:路由器不会发送ICMP信源抑制消息,它还可能会忽略收到的ICMP信源抑制消息。ICMP_QUENCH消息旨在缓解拥塞,但事实证明这种解决方案不管用。

icmp_redirect

ICMP_REDIRECT消息由icmp_redirect()处理。RFC 1122的3.2.2.2节指出:主机不应发送ICMP重定向消息,重定向消息仅供网关发送。

以前,icmp_redirect()处理ICMP_REDIRECT消息时调用ip_rt_redirect(),但现在不需要这样做。因为协议处理程序能够妥善地将重定向消息传播给路由选择代码。事实上,在内核3.6中,方法ip_rt_redirect()已被删除。因此,方法icmp_redirect()首先执行完整性检查,再调用icmp_socket_deliver()。后者会将数据包交给原始套接字并调用协议错误处理程序(如果有的话)。
ICMP_REDIRECT消息将在第6章进行更详细的讨论。

icmp_echo

icmp_echo()处理回应(ping)请求(ICMP_ECHO )。它调用icmp_reply()发送回应应答(ICMP_ECHOREPLY )。如果设置了net->ipv4.sysctl_icmp_echo_ignore_all,将不会发送应答。关于如何配置ICMPv4 procfs条目,请参阅3.5节以及Documentation/networking/ip-sysctl.txt。

icmp_timestamp

icmp_timestamp()处理ICMP时间戳请求(ICMP_TIMESTAMP )。它调用icmp_reply()来发送ICMP_IMESTAMPREPLY。

接收ICMPv4消息

方法ip_local_deliver_finish()处理目的地为当前机器的数据包。收到ICMP数据包后,此方法便将其交给注册ICMPv4协议的原始套接字。在方法icmp_rcv()中进行处理。

方法ip_local_deliver_finish()处理目的地为当前机器的数据包。收到ICMP数据包后,这个方法便将其交给注册了ICMPv4协议的原始套接字。

在方法icmp_rcv()中,首先将InMsgs SNMP计数器(ICMP_MIB_INMSGS )加1,再核实校验和是否正确。

  • 如果校验和不正确,就将SNMP计数器InCsumErrors和InErrors (ICMP_MIB_CSUMERRORS和ICMP_MIB_INERRORS)都加1,再释放SKB,并返回0。在这种情况下,方法icmp_rcv()不会返回错误。实际上,方法icmp_rcv()总是返回0。为何在校验和不对时返回0呢?因为收到错误的ICMP消息时,除将其丢弃外无需做其他特殊处理。协议处理程序返回负的错误代码时,将再次尝试对数据包进行处理,但在这里不需要这样做。更详细的信息请参阅方法ip_local_deliver_finish()的实现过程。

  • 接下来,检查ICMP报头,以确定ICMP消息的类型,将相应的procfs消息类型计数器(每种ICMP消息类型都有一个procfs计数器)加1,并执行完整性检查,以确认类型编号没有超过最大允许值(NR_ICMP_TYPES )。RFC 1122的第3.2.2节指出,如果收到的ICMP消息的类型未知,必须默默地丢弃它。因此,如果消息类型编号超出了范围,将把InErrors SNMP计数器(ICMP_MIB_INERRORS)加1,并释放SKB。

  • 如果数据包为广播或组播方式,且为ICMP_ECHO或ICMP_TIMESTAMP消息,

    • 将读取变量net->ipv4.sysctl_icmp_echo_ignore_broadcasts,以核实广播/组播回应请求是否被允许。这个变量的值默认为1,可通过procfs进行配置(写入/proc/sys/net/ipv4./icmp_echo_ignore_broadcasts )。如果这个变量被设置完成,将默默地丢弃数据包。这种做法遵循的是RFC 1122第3.2.2.6节和3.2.2.8节的规定,即可默默地丢弃目的地为IP广播地址或IP组播地址的ICMP回应请求及目的地为IP广播地址或IP组播地址的ICMP时间戳请求消息。
  • 接下来,校验广播或组播方式所对应的消息类型,即是否为ICMP_ECHO、ICMP_TIMESTAMP、ICMP_ADDRESS或ICMP_ADDRESSREPLY消息。如果不是上述消息类型,将丢弃数据包并返回0。接下来,根据消息类型从数组icmp_pointers中取回相应的条目,并调用合适的处理程序。

发送ICMPv4消息

用于发送ICMPv4消息的方法有两个:一是方法icmp_reply(),用于发送两种ICMP请求的响应;二是方法icmp_send(),用于发送当前机器在特定条件下主动发送的ICMPv4消息。

用于发送ICMPv4消息的方法有两个:

  • 一是方法icmp_reply(),用于发送两种ICMP请求(ICMP_ECHO和ICMP_TIMESTAMP)的响应(被动)
  • 二是方法icmp_send(),用于发送当前机器在特定条件下**主动发送**的ICMPv4消息(本节介绍这种消息的发送)。这两个方法最终都调用icmp_push_reply()来执行实际发送数据包的工作。

在方法icmp_echo()和icmp_timestamp()中,分别调用方法icmp_reply()来响应ICMP_ECHO和ICMP_TIMESTAMP消息。

而在IPv4网络栈的很多地方,如Netfilter、转发代码(ip_forward.c )、ipip和ip_gre等隧道中,都调用了方法icmp_send()。

代码2:ICMP_PROT_UNREACH(协议不可达)

IP报头的协议字段(长8位)指定的协议不存在时,将向发送方发送一条ICMP_DEST_UNREACH/ICMP_PROT_UNREACH消息,**因为没有针对指定协议的协议处理程序(协议处理程序数组将协议号用作索引,因此对于不存在的协议,没有相应的处理程序)。**所谓不存在的协议,指的是下面两种情形之一:Pv4报头中的协议号是错误的,没有包含在协议号列表中(该列表可在include/uapi/linux/in.h中找到);内核不支持该协议,因此该协议没有注册,协议处理程序数组中没有相应的条目。由于这样的数据包无法处理,因此需要向发送方发回ICMPv4“目的地不可达”消息。这种应答中的代码ICMP_PROT_UNREACH指出了导致错误的原因——“协议不可达”。

代码3:ICMP_PORT_UNREACH(端口不可达)

接收UDPv4数据包时,将查找匹配的UDP套接字。如果没有找到匹配的套接字,将检查校验和是否正确。如果不正确,就将数据包默默地丢弃;如果正确,就更新统计信息,并返回一条ICMP“目的地不可达”/“端口不可达”消息。

代码4:ICMP_FRAG_NEEDED(需要分片)

转发数据包时,如果其长度超过了外出链路的MTU,且在IPv4报头(IP_DF)中没有设置分段( DF )位,将把数据包丢弃,并向发送方发回一条代码为ICMP_FRAG_NEEDED的ICMP_DEST_UNREACH消息。

代码5:ICMP_SR_FAILED(目的不可达)

转发数据包时,如果其严格路由选择( strict routing)和网关(gatewaying)选项被设置,将把数据包丢弃,并发回一条代码为ICMP_SR_FAILED的“目的地不可达”消息。

方法icmp_reply()和icmp_send()都支持速率限制,它们调用icmpv4_xrlim_allow()如果速率限制检查允许发送当前数据包(icmpv4_xrlim_allow()返回true),它们就发送该数据包。
在如下情况不会进行速率限制检查:消息的类型未知、数据包为PMTU发现数据包、设备为环回设备、ICMP类型在速率掩码中未指定。

快速参考

type

#define ICMP_ECHOREPLY		0	/* Echo Reply			*/
#define ICMP_DEST_UNREACH	3	/* Destination Unreachable	*/
#define ICMP_SOURCE_QUENCH	4	/* Source Quench		*/
#define ICMP_REDIRECT		5	/* Redirect (change route)	*/
#define ICMP_ECHO		8	/* Echo Request			*/
#define ICMP_TIME_EXCEEDED	11	/* Time Exceeded		*/
#define ICMP_PARAMETERPROB	12	/* Parameter Problem		*/
#define ICMP_TIMESTAMP		13	/* Timestamp Request		*/
#define ICMP_TIMESTAMPREPLY	14	/* Timestamp Reply		*/
#define ICMP_INFO_REQUEST	15	/* Information Request		*/
#define ICMP_INFO_REPLY		16	/* Information Reply		*/
#define ICMP_ADDRESS		17	/* Address Mask Request		*/
#define ICMP_ADDRESSREPLY	18	/* Address Mask Reply		*/
#define NR_ICMP_TYPES		18

code

image-20220719194133512

procfs条目

内核提供了一种在用户空间中对各种子系统的设置进行配置的方式。方法是:将值写入/proc下的条目中。这些条目被称为procfs条目。所有ICMPv4 procfs条目都由结构netns_ipv4中的变量表示。这个结构是在include/net/netns/ipv4.h中定义的。它是网络命名空间(结构net )中的一个对象。网络命名空间及其实现将在第14章讨论。下面列出了与ICMPv4 netns_ipv4元素对应的sysctl变量的名称,还指出了它们的用途、默认值以及对其进行初始化的方法。

  1. sysctl_icmp_echo_ignore_all
    设置了icmp_echo_ignore_all时,将不会对回应请求(ICMP_ECHO)做出应答。
    对应的procfs条目为/proc/sys/net/ipv4/icmp_echo_ignore_all,在icmp_sk_init()中被初始化为0。

  2. sysctl_icmp_echo_ignore_broadcasts
    收到组播/广播回应(ICMP_ECHO)消息或时间戳(ICMP_TIMESTAMP)消息时,读取sysctl_icmp_echo_ignore_broadcasts,以核实是否允许广播/组播。如果这个变量被设置,将丢弃数据包并返回0。
    对应的procfs条目为/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts,在icmp_sk_init()中被初始化为1。

  3. sysctl_icmp_ignore_bogus_error_responses
    有些路由器违反RFC1122的规定,在收到广播帧时发送伪造的响应。在方法icmp_unreach()中,会检查这个标志。如果它被设置为TRUE,内核就不会将警告(“发送的ICMP消息类型非法……”)写人日志。
    对应的procfs条目为/proc/sys/net/ipv4/icmp_ignore_bogus_error_responses,在icmp_sk_init()中被初始化为1。

  4. sysctl_icmp_ratelimit
    对于类型与ICMP速率掩码(参见本节后面的icmp_ratemask )匹配的ICMP数据包,将其最大速率限制为指定值。如果该值为0,则表示禁用速率限制;否则表示响应间的最小间隔,单位为毫秒。
    对应的procfs条目为/proc/sys/net/ipv4/icmp_ratelimit,在icmp_sk_init()中被初始化为1*HZ。5. sysctl_icmp_ratemask
    指定要对哪些ICMP消息类型进行速率限制的掩码。每位对应于一种ICMPv4消息类型。对应的procfs条目为/proc/sys/net/ipv4/icmp_ratemask ,在icmp_sk_init()中被初始化为Ox1818。

  5. sysctl_icmp_errors_use_inbound_ifaddr
    在方法icmp_send()中,将检查这个变量的值。如果它没有被设置,发送ICMP错误消息时将使用出站接口的主地址;否则,发送ICMP消息时将使用导致ICMP错误的数据包的入站接口的主地址。
    对应的procfs条目为/proc/sys/net/ipv4/icmp_errors_use_inbound_ifaddr,在icmp_sk_init()中被初始化为0。

注意有关ICMP sysctl变量及其类型和默认值的更详细信息,请参阅Documentation/networking/ip-sysctl.txt。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值