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 超时差错

超时差错报文 有2个code码: ICMPV6_EXC_HOPLIMIT
和 ICMPV6_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 目的地不可达差错
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 参数异常差错
-
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; } ... }