ROUTING INPUT PACKETS

本文详细介绍了IP包在接收时如何通过快速路径和慢速路径进行路由决策。快速路径主要依赖路由缓存,通过查找源和目标IP地址、服务类型以及输入接口来确定路由。如果缓存中没有匹配项,则进入慢速路径,通过更全面的查找过程在FIB中寻找最合适的路由。慢速路径还涉及到多播地址处理和网络地址转换等情况。整个过程确保了IP包能够正确地被发送到目的地或转发给适当的接收者。
摘要由CSDN通过智能技术生成

As we will show later in this chapter, the IP receive packet handler makes a decision as to how to route each incoming packet. TCP requests a route for input packets when the accept socket call is issued. There are two stages to this decision process, fast path routing and slow path routing. The fast path consists simply of a lookup in the routing cache. The fast path looks for a match on the incoming interface, the source, and the destination IP addresses to see if there is a cached route that applies to this packet. The slow path consists of a more time-consuming search of the FIB for the most appropriate applicable match. First, we will discuss the fast path routing in this section. A later section will cover slow path routing.

Routing Input Packets—the Fast Path

Each packet has a destination that at any given time may either be known or unknown. As a packet is transmitted, once the destination is known, the destination cache entry, dst, in the socket buffer structure, sk_buff, is used to quickly extract the link layer header including the hardware destination address. The purpose of fast path routing is to find the entry in the route cache and place a pointer to it in the socket buffer. When we are done, the dst field in the socket buffer is a pointer to an entry in the destination cache for a specific route.

For reasons of efficiency, internally we handle incoming packets the same way as outgoing packets. Therefore, we also consider incoming packets as having a destination. Generally, this destination may be a transport protocol or it may be the packet forwarding routine. In this section, we examine how the destination cache entry is obtained by searching the route cache with a key based on a few fields of the incoming packet. The destination of the incoming packet is in the IP forwarding routine, and if that is the case, IP forward will worry about how to extract the link layer header from the destination cache when the packet is ready to be transmitted. Routing information for an incoming packet is obtained during input packet processing. The IP receive routine, after it does some basic validity checking of the incoming IP packet, must decide where to send the packet next. To make this decision, it calls ip_route_input to get the destination for an incoming packet. Ip_route_input, defined in file linux/net/ipv4/route.c, gets a route to the destination by first searching for the route table entry in the routing table cache. If there is no matching entry in the cache, it calls ip_route_input_slow to search the FIB. Ip_route_input returns a zero if a route was found.

int ip_route_input
(struct sk_buff *skb,u32daddr,u32 saddr,
u8 tos, struct net_device *dev)
{

Rth is a pointer to a route table entry. Hash is the first-level hash code to find a location in the rt_hash_table. We are trying to route an input packet, so we set iif to the incoming network interface index.

struct rtable * rth;
unsigned hash;
int iif = dev->ifindex;
tos &= IPTOS_RT_MASK;
 

We call rt_hash_code to calculate a 4-byte hash code from the source and destination addresses, the type of service field, and the input interface in the incoming packet, skb.

hash = rt_hash_code(daddr,saddr ^(iif << 5),tos);

Each location in the hash bucket array, rt_hash_table, has a read/write lock. As a reader of the table, we read-lock the location before searching the chain. Anyone writing to the table must write-lock the same location because a use count is incremented once a route table entry is used.

read_lock(&rt_hash_table[hash].lock);

We use the hash code, hash, to start our search at a location in the hash bucket table. In this location, the field chain points to a linked list of routes. An item on the list can be an entry in the destination cache or a route table entry.

for (rth = rt_hash_table[hash].chain; rth; rth = rth->u.rt_next) {

Now we follow the chain of routes to find a precise match by comparing the destination and source addresses, the input interface index, and the IP ToS field

if (rth->fl.fl4_dst == daddr &&
rth->fl.fl4_src == saddr &&
rth->fl.iif == iif &&
rth->fl.oif == 0 &&
#ifdef CONFIG_IP_ROUTE_FWMARK
rth->fl.fl4_fwmark == skb->nfmark  &&
#endif
rth->fl.fl4_tos == tos) {

If we have a match, we are actually in the fast path for input packets. This means that the route has been resolved and the matching route entry actually points to a destination cache entry, not a route table entry. Both structures begin with a next field so they can be overloaded in the same cache. The dst_entry structure is shown later in this section, and a more detailed explanation of the destination entry cache.

We have a match, and we know that rth actually points to the destination cache. The destination cache is defined by the structure dst_entry, We can access this structure through the union, u, in the beginning of the rtable structure. We update a few fields in the dst_entry structure now that we have a hit. We mark the time when this destination cache entry was last used by setting lastuse to the current time. (The global kernel variable, jiffies, holds the current time in ticks.)

rth->u.dst.lastuse = jiffies;

We increment the reference count, __refcnt by, calling dst_hold. The use count, __use, is incremented also. Cache statistics are updated to indicate that we have a routing cache hit. Finally, the dst field of the socket buffer, skb, is updated to point to rth, which is actually a destination cache entry. We return zero to in dicate that we have found a route for this packet. Before returning, we unlock the location in the hash bucket, rt_hash_table.

dst_hold(&rth->u.dst);
rth->u.dst.__use++;
RT_CACHE_STAT_INC(in_hit);
read_unlock(&rt_hash_table[hash].lock);
skb->dst = (struct dst_entry*)rth;
return 0;
}
RT_CACHE_STAT_INC(in_hlist_search);
}

If we end up here, it means that we did not find a matching route in the route table cache. We could be in the slow path for unicast routes or we may have a multicast destination address. First, we unlock the location in the hash bucket, rt_hash_table, because we are done with the routing cache.

rcu_read_unlock();

We check to see if the destination IP address, daddr, is a multicast address. The comments in the code indicate that multicast recognition in earlier releases was done in the route cache. However, some types of Ethernet interfaces didn’t properly recognize hardware multicast addresses, so the number of entries in the routing cache could explode. To avoid this problem, host multicast address decoding is done here outside the routing cache. Multicast routers will still use the route cache.

if (MULTICAST(daddr)) {

The first thing we do if daddr is a multicast destination address is get the Internet device structure, in_device, from the network interface device structure, dev. In_device structure contains the multicast address list among other things. The in_device structure has a global read/write spin lock so we read lock the structure to protect it.

struct in_device *in_dev;
read_lock(&inetdev_lock);
if ((in_dev = __in_dev_get(dev))!= NULL) {

We call ip_check_mc to see if the destination address of the incoming packet, daddr, is one of the addresses on the list of multicast addresses for the incoming network interface. If so, our is set to TRUE. This means that the input interface has been added to the multicast group for the address, daddr.

int our = ip_check_mc(in_dev, daddr, saddr, skb->nh.iph->protocol);

Ip_route_input_mc is called to route input multicast packets if a couple of specific conditions are met. It is called if our is not zero, indicating that the incoming network interface has subscribed to the multicast address, daddr. Next, we call ip_route_input_mc if multicast routing is configured in the kernel and the multicast forwarding option is set for the input interface. However, if daddr is a multicast address but none of these other conditions are met, we return an error indicating that the input packet, skb, should be dropped.

if (our
#ifdef CONFIG_IP_MROUTE
|| (!LOCAL_MCAST(daddr) &&IN_DEV_MFORWARD(in_dev))
#endif
) {
read_unlock(&inetdev_lock);
return ip_route_input_mc(skb,daddr,saddr,
tos, dev, our);
}
}
read_unlock(&inetdev_lock);
return -EINVAL;
}
 

This is the slow path and we arrive here if there was no routing cache hit for the incoming packet, skb, and daddr is not a multicast address.

return ip_route_input_slow(skb,daddr,saddr,tos,dev); }

The destination entry cache structure, dst_entry. This structure is referenced by the ip_route_input structure in the case where there is a routing table cache hit.

The Main Input Route Resolving Function

In the previous section, we discussed how the routing table cache is accessed for fast path routing of incoming packets. If there is no cache hit in the attempt to route the packet in the fast path, we try to find a route for the incoming packet by accessing the FIB. This is called the slow path.

The function that tries to find a slow path route for the incoming packet is ip_route_input_slow. This function is called from ip_route_input if there is no routing cache hit for the incoming packet, skb.

int ip_route_input_slow(struct sk_buff *skb,
u32 daddr, u32 saddr,
u8 tos, struct net_device *dev)
{

Res points to the result of the FIB search.

struct fib_result res;

In_dev is the Internet device associated with the incoming network interface, dev. This structure was also used in the fast path routing described in the previous section. Since we are routing incoming packets, out_dev is NULL.

struct in_device *in_dev = in_dev_get(dev); struct in_device *out_dev = NULL;

Next, we initialize the flow structure for the FIB search. The fields for addresses and TOS are initialized.

struct flowi fl = { .nl_u = { .ip4_u =
{ .daddr = daddr,
.saddr = saddr,
.tos = tos,
 

We set the scope field in the search key to be as broad as possible. The values for route scope are shown in Table Later in the function, as we attempt to narrow the routing decision, this value may be narrowed based on the result of the FIB lookup.

.scope = RT_SCOPE_UNIVERSE,
#ifdef CONFIG_IP_ROUTE_FWMARK
.fwmark = skb->nfmark
#endif
} } ,
.iif = dev->ifindex } ;

The values in flags are the route control flags and they are derived from information in the FIB as we determine the route. They help provide instructions to the consumers of this packet as to how to dispose of the packet. The route control flags are shown in Table Itag is used for setting the traffic class, the tclassid field in the destination cache entry, used for traffic shaping.

unsigned flags = 0; u32 itag = 0;

Rth points to a routing table cache entry described in an earlier section. Hash is the hash code. Spec_dst is the special destination address.

struct rtable * rth;
unsigned hash;
u32 spec_dst;
int err = -EINVAL;
int free_res = 0;

If in_dev is NULL, there is no AF_INET information for the incoming network interface, dev, and therefore no IP address. IP packets are disabled for this input device.

if (!in_dev) goto out;

We calculate the hash code for finding entries in the routing table cache.

hash=rt_hash_code(daddr,saddr^(key.iif<<5),tos);

Some Martian source addresses are filtered out first because they can’t be detected by a lookup of the FIB. Afterwards, we see if the incoming packet has been sent to a broadcast address, and if so, we jump to brd_input for further processing.

if (MULTICAST(saddr)||BADCLASS(saddr)||LOOPBACK(saddr)) goto martian_source;

We check if we have a broadcast.

if (daddr == 0xFFFFFFFF||(saddr == 0 && daddr == 0)) goto brd_input;

We filter out zero source addresses. They are only accepted if they also have zero broadcast addresses.

if (ZERONET(saddr)) goto martian_source;

If the destination IP address does not make any sense, we filter it out. We should not see packets with destination loopback addresses at this point.

if (BADCLASS(daddr)||ZERONET(daddr)||LOOPBACK(daddr)) goto martian_destination;

At this point, a fast search of the routing table cache did not yield a hit, but the previous checks indicate that we have valid source and destination unicast addresses. Therefore, we need to find a route for the packet in the FIB, and we do this by calling fib_lookup.

Fib_lookup does the search using the flowi structure. It provides the result in a fib_result structure pointed to by res. It returns a zero if the search was successful, or an error if not.

if ((err = fib_lookup(&fl, &res)) !=  0) {
if (!IN_DEV_FORWARD(in_dev))
goto e_inval;
goto no_route;
}
free_res = 1;
RT_CACHE_STAT_INC(in_slow_tot);

The following section is for Network Address Translation (NAT). If it is configured into the kernel, we attempt to re-map the addresses to the translated addresses.

#ifdef CONFIG_IP_ROUTE_NAT

If NAT is configured, typically multiple routing tables will be configured. The additional tables contain the address mapping. Here we access the mapping policy in the FIB rules structure pointed to by the result res. The routing policy is applied before re-mapping the destination address. The unmodified source address is used to do re-routing later.

if (1) { u32 src_map = saddr;

The field r in fib_result points to the fib_rules data structure. It will be non-NULL if multiple tables are configured.

src_map = fib_rules_policy(saddr,&res,&flags);
if (res.type == RTN_NAT) {
key.dst = fib_rules_map_destination(daddr, &res);
fib_res_put(&res);
free_res = 0;
if (fib_lookup(&key, &res))
goto e_inval;
free_res = 1;
if (res.type != RTN_UNICAST)
goto e_inval;
flags |= RTCF_DNAT;
}
key.src = src_map;
}
#endif

The type of the route is returned in the type field of the fib_result. If the returned route type is broadcast, we complete processing at the brd_input label.

if (res.type == RTN_BROADCAST) goto brd_input;

The returned route type is RTN_LOCAL if the destination matched one of our addresses.

if (res.type == RTN_LOCAL) {
int result;
result = fib_validate_source(saddr,daddr, tos,
loopback_dev.ifindex,dev, &spec_dst,&itag);
if (result < 0)
goto martian_source;
if (result)
flags |= RTCF_DIRECTSRC;
 

We know now that we have a local route. The packet should not be forwarded. It should be passed to the higher-level protocols because the packet was sent to one of our local addresses. Spec_dst holds the value for the rt_spec_dst field of rtable. This is the UDP-specific destination address used in certain UDP applications for the source address of an outgoing packet as specified by [RFC 1122]. Finally, we jump to local_input to complete processing.

spec_dst = daddr; goto local_input; }

At this point, we know we will have to forward the incoming packet. It was rejected for the fast route match by ip_route_input, or we wouldn’t have ended up here. By failing the previous tests in this function, we know it is not a multicast packet, it is not a broadcast packet, and it is not being sent to one of our addresses. There are a few other tests before we prepare to route the packet. If the input device is not configured for IP forwarding, we have an error condition. If the route type returned from the FIB is not unicast, we assume there is an error condition; the destination address must have been Martian.

if (!IN_DEV_FORWARD(in_dev))
goto e_inval;
if (res.type != RTN_UNICAST)
goto martian_destination;
 

This section is for multipath routing. It is a check to see if there is more than one gateway machine or next hop defined for this route.

#ifdef CONFIG_IP_ROUTE_MULTIPATH
if (res.fi->fib_nhs > 1 &&  key.oif == 0)
fib_select_multipath(&fl, &res);
#endif

We get a pointer to the inet_device associated with the output network in terface in the route we got from the FIB. All valid network interface devices that handle IP packets should have an inet_device.

out_dev = in_dev_get(FIB_RES_DEV(res));
if (out_dev == NULL) {
if (net_ratelimit())
printk(KERN_CRIT "Bug in ip_route_input_slow(). ""Please, report n");
goto e_inval;
}

The main reason why we call fib_validate_source here is to calculate the specific destination address, spec_dst. However, the function also helps in making the routing decisions. It returns a number less than zero if the source address is bad, and a number greater than zero if the destination is reachable by direct connection.

err = fib_validate_source(saddr,daddr,
tos,FIB_RES_OIF(res), dev,&spec_dst, &itag);
if (err < 0)
goto martian_source;
 

We set RTCF_DIRECTSRC in the router control flags to indicate that the destination is directly reachable if fib_validate_source returns a positive value.

if (err) flags |= RTCF_DIRECTSRC;

Now we check to see if an ICMP redirect error will need to be sent. An ICMP redirect is sent from a router back to the sender to tell it that the packet was sent to the wrong router. In the following test, we are checking to see that the gateway address returned by the RIB_RES_GW macro would be reached through the same interface through which the packet arrived. This is the basic redirect case. Stevens has an excellent explanation of ICMP redirect errors. If this test passes, we set RTCF_DOREDIRECT in the control flags. Later, the ip_forward function will actually generate the ICMP message.

if (out_dev == in_dev && err  && !
(flags & (RTCF_NAT | RTCF_MASQ)) &&
(IN_DEV_SHARED_MEDIA(out_dev) ||
inet_addr_onlink(out_dev, saddr,  FIB_RES_GW(res))))
flags |= RTCF_DOREDIRECT;
 

If the protocol of the incoming packet is not IP, it is not routed.

if (skb->protocol !=  __constant_htons(ETH_P_IP)) {
if (out_dev == in_dev && !(flags  & RTCF_DNAT))
goto e_inval;
}

At this point, we know that the incoming packet will be routed. We also know that the destination machine or the gateway is theoretically reachable. Therefore, we allocate a location in the destination cache by calling dst_alloc. Remember that the routing table cache entry can also point to a destination cache entry, dst_entry.

rth = dst_alloc(&ipv4_dst_ops); if (!rth) goto e_nobufs; atomic_set(&rth->u.dst.__refcnt, 1);

The flags field of the dst_entry is not used very widely. Other fields in it are set in preparation for entering the route in the route cache.

rth->u.dst.flags= DST_HOST;
if (in_dev->cnf.no_policy)
rth->u.dst.flags |= DST_NOPOLICY;
if (in_dev->cnf.no_xfrm)
rth->u.dst.flags |= DST_NOXFRM;
rth->fl.fl4_dst = daddr;
rth->rt_dst = daddr;
rth->fl.fl4_tos = tos;
#ifdef CONFIG_IP_ROUTE_FWMARK
rth->fl.fl4_fwmark = skb->nfmark;
#endif
rth->fl.fl4_src = saddr;
rth->rt_src = fl.fl4_dst;
rth->rt_gateway = daddr;
#ifdef CONFIG_IP_ROUTE_NAT
rth->rt_src_map = fl.fl4_src;
rth->rt_dst_map = fl.fl4_dst;
if (flags&RTCF_DNAT)
rth->rt_gateway= fl.fl4_dst;
#endif
rth->rt_iif =
rth->fl.iif = dev->ifindex;
rth->u.dst.dev = out_dev->dev;
dev_hold(rth->u.dst.dev);
rth->fl.oif = 0;
rth->rt_spec_dst= spec_dst;

Here, we set the input field of the dst_entry to ip_forward.

Later, after ip_route_input_slow returns, the IP receive handler processing will find this route in the routing cache, de-reference the function pointer, and pass the packet to ip_forward.

rth->u.dst.input = ip_forward; rth->u.dst.output = ip_output;

Rt_set_nexthop sets information in the route hash entry, including the traffic classifier, tclassid, MTU, metrics, and route type. Next, the routing control flags are set.

rt_set_nexthop(rth, &res, itag); rth->rt_flags = flags;

If fast routing is configured and the device has the capability, the fast routing control flag is set in the route cache entry.

#ifdef CONFIG_NET_FASTROUTE
if (netdev_fastroute && 
!(flags&(RTCF_NAT|RTCF_MASQ|RTCF_DOREDIRECT))) {
struct net_device *odev = rth->u.dst.dev;
if (odev != dev &&
dev->accept_fastpath &&
odev->mtu >= dev->mtu &&
dev->accept_fastpath(dev,  &rth->u.dst) == 0)
rth->rt_flags |= RTCF_FAST;
}
#endif

We are almost done. We call rt_intern_hash to insert the route hash entry into the routing hash table, rt_hash_table.

intern: err = rt_intern_hash(hash, rth, (struct rtable**)&skb->dst);

Now we are done with routing input packets. We decrement the reference count in the input inet_device, in_dev, and if we used it, the output inet_device, out_dev. Fib_res_put actually decrements the reference count in the fib_info structure attached to the fib_res if there is one. When we return a nonzero value, it tells the caller to drop the input packet it is trying to route.

done:

in_dev_put(in_dev);
if (out_dev)
in_dev_put(out_dev);
if (free_res)
fib_res_put(&res);
out: return err;

This label, brd_input, is where we end up if the packet we are trying to route is a broadcast packet or the FIB search returned a broadcast route. If the packet is OK, we set the routing control flags to indicate it is a broadcast, set the specific destination address, and proceed to local input route processing.

brd_input: if (skb->protocol != __constant_htons(ETH_P_IP)) goto e_inval;

If the source address of the incoming packet is zero, we must set the specific destination address, spec_dst, so a UDP application will know how to set the source address of a response packet.

if (ZERONET(saddr)) spec_dst = inet_select_addr(dev, 0,RT_SCOPE_LINK); else {

If the source address wasn’t zero, we validate the source address. While we are at it, we also assign the specific destination address, spec_dst.

err=fib_validate_source(saddr,0,tos,0,dev,&spec_dst,&itag);
if (err < 0)
goto martian_source;
if (err)
flags |= RTCF_DIRECTSRC;
}
flags |= RTCF_BROADCAST;
res.type = RTN_BROADCAST;
RT_CACHE_STAT_INC(in_brd);

The local input label is used for cases where the packet is intended for local consumption. We allocate a route cache entry. This cache entry is also a destination cache entry because a packet using this route will not be sent out any interface. It will be consumed locally.

local_input:
rth = dst_alloc(&ipv4_dst_ops);
if (!rth)
goto e_nobufs;

We set fields in the routing hash entry. The output function isn’t used because we will be consuming these packets, not transmitting them. We set the destination address, ToS, and the source address. The dst_entry flags field is set to DST_HOST.

rth->u.dst.output= ip_rt_bug;
atomic_set(&rth->u.dst.__refcnt, 1);
rth->u.dst.flags= DST_HOST;
if (in_dev->cnf.no_policy)
rth->u.dst.flags |= DST_NOPOLICY;
rth->fl.fl4_dst = daddr;
rth->rt_dst = daddr;
rth->fl.fl4_tos = tos;
#ifdef CONFIG_IP_ROUTE_FWMARK
rth->fl.fl4_fwmark = skb->nfmark;
#endif
rth->fl.fl4_src = saddr;
rth->rt_src = saddr;
#ifdef CONFIG_IP_ROUTE_NAT
rth->rt_dst_map = fl.fl4_dst;
rth->rt_src_map = fl.fl4_src;
#endif
#ifdef CONFIG_NET_CLS_ROUTE
rth->u.dst.tclassid = itag;
#endif

The destination device is the loopback device. The input interface is set to the networking interface that received the packet we are trying to route. The most important field of the dst_entry structure is the input function pointer, which is set to ip_local_deliver. This function will receive any input packets using this entry in the routing cache. In addition, if the routing control flags rt_flags has RTCF_LOCAL set, it indicates that the incoming packet is for local consumption.

rth->rt_iif =
rth->fl.iif = dev->ifindex;
rth->u.dst.dev = &loopback_dev;
dev_hold(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;
#endif

The route type in the FIB result is unreachable if we got here from the no_route label, and if so, we indicate an error and unset the local flag.

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;
goto intern;

We came to this label, no_route, if the FIB search indicated that the destination of the packet we were trying to route is unreachable.

no_route:
rt_cache_stat[smp_processor_id()].in_no_route++;
spec_dst=inet_select_addr(dev,0,RT_SCOPE_UNIVERSE);
res.type= TN_UNREACHABLE;
goto local_input;
 

If our previous FIB search indicated that the destination address was Martian (entirely unknown), we log the appropriate errors but we don’t cache the route.

martian_destination:
rt_cache_stat[smp_processor_id()].in_martian_dst++;
#ifdef CONFIG_IP_ROUTE_VERBOSE
if (IN_DEV_LOG_MARTIANS(in_dev)&&net_ratelimit())
printk(KERN_WARNING "martian destination  %u.%u.%u.%u from ""%u.%u.%u.%u, dev %s n",
NIPQUAD(daddr), NIPQUAD(saddr),dev->name);
#endif
e_inval:
err = -EINVAL;
goto done;
e_nobufs:
err = -ENOBUFS;
goto done;

If the FIB search indicated that the source address was Martian, we also increment the statistics. This is why the code attempts to log the MAC layer header information.

martian_source:
RT_CACHE_STAT_INC(in_martian_src);
#ifdef CONFIG_IP_ROUTE_VERBOSE
if (IN_DEV_LOG_MARTIANS(in_dev)&&net_ratelimit()) {

If the source address is Martian, we print out the MAC address. This is because the MAC address may be the only indication that the source address is bogus.

printk(KERN_WARNING "martian source%u.%u.%u.%u from ""%u.%u.%u.%u, on dev %s n",
NIPQUAD(daddr), NIPQUAD(saddr),dev->name);
if (dev->hard_header_len) {
int i;
unsigned char *p = skb->mac.raw;
printk(KERN_WARNING "ll header: ");
for (i = 0; i < dev->hard_header_len;i++, p++) {
printk("%02x", *p);
if (i < (dev->hard_header_len - 1))
printk(":");
}
printk(" n");
}
}
#endif
goto e_inval;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值