3.7 ICMPv6 数据包处理

3.7 ICMPv6 数据包处理

1. echo请求和应答

  • 处理echo请求: icmpv6_echo_reply()
  • 处理echo回应: ping_rcv()
static void icmpv6_echo_reply(struct sk_buff *skb)
{
    // 获取网络空间
	struct net *net = dev_net(skb->dev);
    // 获取 icmpv6 头部
	struct icmp6hdr *icmph = icmp6_hdr(skb);

	u32 mark = IP6_REPLY_MARK(net, skb->mark);
	
    // 设置源地址。并根据是不是单播而启用它
	saddr = &ipv6_hdr(skb)->daddr;

	if (!ipv6_unicast_destination(skb) &&
	    !(net->ipv6.sysctl.anycast_src_echo_reply &&
	      ipv6_anycast_destination(skb_dst(skb), saddr)))
		saddr = NULL;

    // 复制并修改icmpv6的头部,并将ICMPv6的type改成 ICMPV6_ECHO_REPLY
	memcpy(&tmp_hdr, icmph, sizeof(tmp_hdr));
	tmp_hdr.icmp6_type = ICMPV6_ECHO_REPLY;
	
    // 指定协议为ICMPv6, 并初始化其他基础信息
	memset(&fl6, 0, sizeof(fl6));
	fl6.flowi6_proto = IPPROTO_ICMPV6;
	fl6.daddr = ipv6_hdr(skb)->saddr;
	if (saddr)
		fl6.saddr = *saddr;
	fl6.flowi6_oif = icmp6_iif(skb);
	fl6.fl6_icmp_type = ICMPV6_ECHO_REPLY;
	fl6.flowi6_mark = mark;
	fl6.flowi6_uid = sock_net_uid(net, NULL);
    // 调用这个函数进行安全策略验证
	security_skb_classify_flow(skb, flowi6_to_flowi(&fl6));

	local_bh_disable();
    // 使用这个函数锁定传输套接字,并设置mark等信息
	sk = icmpv6_xmit_lock(net);
	if (!sk)
		goto out_bh_enable;
	sk->sk_mark = mark;
	np = inet6_sk(sk);
	
    // 如果报文出口为指定
	if (!fl6.flowi6_oif && ipv6_addr_is_multicast(&fl6.daddr))
		fl6.flowi6_oif = np->mcast_oif; //如果是组播则设置组播地址
	else if (!fl6.flowi6_oif)
		fl6.flowi6_oif = np->ucast_oif; // 如果是单播则设置单播地址
	// 并查找合适的路由
	if (ip6_dst_lookup(net, sk, &dst, &fl6))
		goto out;
    // 如果规定了ipsec,则使用这个函数查找相关的IPSec安全关联规则。
	dst = xfrm_lookup(net, dst, flowi6_to_flowi(&fl6), sk, 0);
	if (IS_ERR(dst))
		goto out;

	idev = __in6_dev_get(skb->dev);
	
    //准备ICMPv6 信息缓冲区
	msg.skb = skb;
	msg.offset = 0;
	msg.type = ICMPV6_ECHO_REPLY;
	
    // 初始化IPV6选项
	ipcm6_init_sk(&ipc6, np);
	ipc6.hlimit = ip6_sk_dst_hoplimit(np, &fl6, dst);
	ipc6.tclass = ipv6_get_dsfield(ipv6_hdr(skb));
	
    //将ICMPv6 Echo Reply消息添加到新的数据包中,并尝试发送。如果成功,则将数据推入待发送队列;如果不成功,则更新统计信息并释放资源。
	if (ip6_append_data(sk, icmpv6_getfrag, &msg,
			    skb->len + sizeof(struct icmp6hdr),
			    sizeof(struct icmp6hdr), &ipc6, &fl6,
			    (struct rt6_info *)dst, MSG_DONTWAIT)) {
		__ICMP6_INC_STATS(net, idev, ICMP6_MIB_OUTERRORS);
		ip6_flush_pending_frames(sk); //丢弃缓冲区
	} else {
        // 报文入栈
		icmpv6_push_pending_frames(sk, &fl6, &tmp_hdr,
					   skb->len + sizeof(struct icmp6hdr));
	}
    
    // 释放路由信息
	dst_release(dst);
    // 最后释放套接字,关闭软中断
out:
	icmpv6_xmit_unlock(sk);
out_bh_enable:
	local_bh_enable();
}

ping_rcv() 的处理流程见 ICMPv4.

2. 错误报文处理

icmpv6_rcv() 中,对 ICMPv6 报文类型是错误消息的报文,都将使用 icmpv6_notify()进行处理。

void icmpv6_notify(struct sk_buff *skb, u8 type, u8 code, __be32 info)
{
	const struct inet6_protocol *ipprot;
	int inner_offset;
	__be16 frag_off;
	u8 nexthdr;
	struct net *net = dev_net(skb->dev);

	// 先检查是否可以从当前数据包中检查出IPV6头部
	if (!pskb_may_pull(skb, sizeof(struct ipv6hdr)))
		goto out;

	// 获取IPV6下一个头部,这可能是扩展头部或者是上层协议头部。
	nexthdr = ((struct ipv6hdr *)skb->data)->nexthdr;
	if (ipv6_ext_hdr(nexthdr)) {
		/* now skip over extension headers */
		// 如果这是一个扩展头部,则跳过,并找到实际上层协议头部
		inner_offset = ipv6_skip_exthdr(skb, sizeof(struct ipv6hdr),
						&nexthdr, &frag_off);
		if (inner_offset < 0)
			goto out;
	} else {
		inner_offset = sizeof(struct ipv6hdr);
	}

	// 然后,函数再次尝试从当前缓冲区中提取至少包含内部协议头8字节的数据。
	/* Checkin header including 8 bytes of inner protocol header. */
	if (!pskb_may_pull(skb, inner_offset + 8))
		goto out;

	/* BUGGG_FUTURE: we should try to parse exthdrs in this packet.
	   Without this we will not able f.e. to make source routed
	   pmtu discovery.
	   Corresponding argument (opt) to notifiers is already added.
	   --ANK (980726)
	 */

	// 查找对应nexthdr 协议的处理协议ipprot
	ipprot = rcu_dereference(inet6_protos[nexthdr]);
	if (ipprot && ipprot->err_handler)
		// 执行这个协议的错误处理函数,这个函数在注册报文接收函数的时候一起注册了。
		ipprot->err_handler(skb, NULL, type, code, inner_offset, info);

	// 最后都会调用原始套接字来处理这个错误
	raw6_icmp_error(skb, nexthdr, type, code, inner_offset, info);
	return;

out:
	// 错误报文数据统计
	__ICMP6_INC_STATS(net, __in6_dev_get(skb->dev), ICMP6_MIB_INERRORS);
}

下面是一些主动发送错误报文的场景。

3. 发送错误报文

3.1 超时差错

image-20240405161926510

超时差错报文 有2个code码: ICMPV6_EXC_HOPLIMITICMPV6_EXC_FRAGTIME

  • 跳数限制超时
    跳数限制相当于IPV4中的TTL。每一台设备转发之后就会减1. 当跳数限制计数器变成0之后,会调用icmpv6_send()发送一条ICMPV6_TIME_EXCEED/ICMPV6_EXC_HOPLIMIT 的消息。

    int ip6_forward(struct sk_buff *skb)
    {
        ...
    	/*
    	 *	check and decrement ttl
    	 */
    	if (hdr->hop_limit <= 1) {
    		/* Force OUTPUT device used as source address */
    		skb->dev = dst->dev;
    		icmpv6_send(skb, ICMPV6_TIME_EXCEED, ICMPV6_EXC_HOPLIMIT, 0);
    		__IP6_INC_STATS(net, idev, IPSTATS_MIB_INHDRERRORS);
            ...
    	}
        
        ...
    }
    
  • 分段重组超时

    在分段重组超时时,将调用 icmpv6_send() 发送一条ICMPV6_TIME_EXCEED/ICMPV6_EXC_FRAGTIME 分段重组超时的消息。

    void ip6frag_expire_frag_queue(struct net *net, struct frag_queue *fq)
    {
    	...
    	icmpv6_send(head, ICMPV6_TIME_EXCEED, ICMPV6_EXC_FRAGTIME, 0);
    	...
    }
    

3.2 目的地不可达差错

image-20240405162341045

static void ip6_rt_init_dst_reject(struct rt6_info *rt, struct fib6_info *ort)
{
	rt->dst.error = ip6_rt_type_to_error(ort->fib6_type);

	switch (ort->fib6_type) {
	case RTN_BLACKHOLE:
		rt->dst.output = dst_discard_out;
		rt->dst.input = dst_discard;
		break;
	case RTN_PROHIBIT:
		rt->dst.output = ip6_pkt_prohibit_out;
		rt->dst.input = ip6_pkt_prohibit;
		break;
	case RTN_THROW:
	case RTN_UNREACHABLE:
	default:
		rt->dst.output = ip6_pkt_discard_out;
		rt->dst.input = ip6_pkt_discard;
		break;
	}
}
  • code0 没有前往目的地的路由 ICMPv6_NOROUTE
    通俗来讲就是没有找到目的地的路由来转发数据包。在下一章会讲到IPTV会注册入口处理函数和出口处理函数,当ort->fib6_type的类型是RTN_THROW \ RTN_UNREACHABLE \ default,会触发这样的调用接口:

    // input
    static int ip6_pkt_discard(struct sk_buff *skb)
    {
    	return ip6_pkt_drop(skb, ICMPV6_NOROUTE, IPSTATS_MIB_INNOROUTES);
    }
     
    // output
    static int ip6_pkt_discard_out(struct net *net, struct sock *sk, struct sk_buff *skb)
    {
    	skb->dev = skb_dst(skb)->dev;
    	return ip6_pkt_drop(skb, ICMPV6_NOROUTE, IPSTATS_MIB_OUTNOROUTES);
    }
    
  • code1 通信被禁止 ICMPV6_ADM_PROHIBITED
    ort->fib6_type的类型是RTN_PROHIBIT时,会注册这样的接口,来发送code1的ICMPv6差错报文。

    static int ip6_pkt_prohibit(struct sk_buff *skb)
    {
    	return ip6_pkt_drop(skb, ICMPV6_ADM_PROHIBITED, IPSTATS_MIB_INNOROUTES);
    }
    
    static int ip6_pkt_prohibit_out(struct net *net, struct sock *sk, struct sk_buff *skb)
    {
    	skb->dev = skb_dst(skb)->dev;
    	return ip6_pkt_drop(skb, ICMPV6_ADM_PROHIBITED, IPSTATS_MIB_OUTNOROUTES);
    }
    
  • code2 超出了源地址范围 ICMPV6_NOT_NEIGHBOUR
    暂不分析,看不懂,是在路由报文转发时,不应对源路由和ipsec的报文进行转发之类的错误消息。

    int ip6_forward(struct sk_buff *skb)
    {
    	...
    	/* IPv6 specs say nothing about it, but it is clear that we cannot
    	   send redirects to source routed frames.
    	   We don't send redirects to frames decapsulated from IPsec.
    	 */
    	if (IP6CB(skb)->iif == dst->dev->ifindex &&
    	    opt->srcrt == 0 && !skb_sec_path(skb)) {
    		...
    	} else {
    		...
    		if (addrtype & IPV6_ADDR_LINKLOCAL) {
    			icmpv6_send(skb, ICMPV6_DEST_UNREACH,
    				    ICMPV6_NOT_NEIGHBOUR, 0);
    			goto error;
    		}
    	}
    	...
    }
    
  • code3 地址不可达 ICMPV6_ADDR_UNREACH
    路由器或者主机无法找到通往指定IPv6地址的有效路径。告知对方由于目标地址无法到达,所以数据包未能成功投递。

    static void ip6_link_failure(struct sk_buff *skb)
    {
    	struct rt6_info *rt;
    	icmpv6_send(skb, ICMPV6_DEST_UNREACH, ICMPV6_ADDR_UNREACH, 0);
        ...
    }
    
  • code4 端口不可达 ICMPV6_PORT_UNREACH
    以UDP报文举例,在收到UDP包后,如果没有找到对应的UDP6的套接字,那么将校验校验和是否正确。如果校验和正确,则出发端口不可达的错误信息。

    int __udp6_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
    		   int proto)
    {
    	...
    	__UDP6_INC_STATS(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
    	icmpv6_send(skb, ICMPV6_DEST_UNREACH, ICMPV6_PORT_UNREACH, 0);
    
    	kfree_skb(skb);
    	return 0;
    	...
    }
    

3.3 需要分段差错

在ICMPv4 中,需要分段 是作为type3,code4的一个消息码。在ICMPv6中单独作为一个差错类型。
转发数据包时,如果数据包的长度大于外出链路的MTU值,且数据包中没有设置local_df位,则回复一条需要分段消息ICMPV6_PKT_TOOBIG

int ip6_forward(struct sk_buff *skb)
{
	...
	mtu = ip6_dst_mtu_forward(dst);
	if (mtu < IPV6_MIN_MTU)
		mtu = IPV6_MIN_MTU;

	if (ip6_pkt_too_big(skb, mtu)) {
		/* Again, force OUTPUT device used as source address */
		skb->dev = dst->dev;
		icmpv6_send(skb, ICMPV6_PKT_TOOBIG, 0, mtu);
		__IP6_INC_STATS(net, idev, IPSTATS_MIB_INTOOBIGERRORS);
		__IP6_INC_STATS(net, ip6_dst_idev(dst),
				IPSTATS_MIB_FRAGFAILS);
		kfree_skb(skb);
		return -EMSGSIZE;
	}
	...
}

3.4 参数异常差错

image-20240405170727684

  • code0 遇到了错误的报头字段 ICMPV6_HDR_FIELD
    一般在校验报头信息时,如果长度或者合法性校验失败的时候,会发送这个差错信息。

    ...
    n = hdr->hdrlen >> 1;
    
    if (hdr->segments_left > n) {
        __IP6_INC_STATS(net, idev, IPSTATS_MIB_INHDRERRORS);
        icmpv6_param_prob(skb, ICMPV6_HDR_FIELD,
                  ((&hdr->segments_left) -
                   skb_network_header(skb)));
        return -1;
    }
    ...
    
  • code1 未知的下一报头类型 ICMPV6_UNK_NEXTHDR
    在数据包input处理的时候,如果没有找到IP报头之后的处理协议报头,并且没有原始套接字接管处理的时候,就会发送这个消息。对应在 ICMPv4 中,这个就是 协议不可达

    static int ip6_input_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
    {
    	...
    	raw = raw6_local_deliver(skb, nexthdr);
    	ipprot = rcu_dereference(inet6_protos[nexthdr]);
    	if (ipprot) {
    		...
    	} else {
    		if (!raw) {
    			if (xfrm6_policy_check(NULL, XFRM_POLICY_IN, skb)) {
    				__IP6_INC_STATS(net, idev,
    						IPSTATS_MIB_INUNKNOWNPROTOS);
                    
    				icmpv6_send(skb, ICMPV6_PARAMPROB,
    					    ICMPV6_UNK_NEXTHDR, nhoff);
    			}
    			kfree_skb(skb);
    		} else {
    			...
    		}
    	}
        ...
    }
    
  • code2 未知的ipv6选项 ICMPV6_UNK_OPTIONS

    在分析扩展报头时如果遇到了未知的option,就会发送这个差错报文。

    static bool ip6_tlvopt_unknown(struct sk_buff *skb, int optoff,
    			       bool disallow_unknowns)
    {
    	switch ((skb_network_header(skb)[optoff] & 0xC0) >> 6) {
    		...
    	case 2: /* send ICMP PARM PROB regardless and drop packet */
    		icmpv6_param_prob(skb, ICMPV6_UNK_OPTION, optoff);
    		return false;
    	}
    	...
    }
    
posted @ 2024-04-05 18:04  kmist  阅读(116)  评论(0编辑  收藏  举报