3.6 ICMPv6 报文接收和发送
3.6 ICMPv6 报文接收和发送
1. ICMPv6 数据包接收流程
当有IP报头协议是58
的数据包到来之后,会调用icmpv6_rcv()
进行处理。数据包的处理流程在上面的流程图中说明的非常清楚,下面就来看一下代码是怎么实现的。
static int icmpv6_rcv(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
struct net_device *dev = icmp6_dev(skb);
struct inet6_dev *idev = __in6_dev_get(dev);
const struct in6_addr *saddr, *daddr;
struct icmp6hdr *hdr;
u8 type;
bool success = false;
// 1. 如果有开启ipsec安全检查,将会在这里进行处理
if (!xfrm6_policy_check(NULL, XFRM_POLICY_IN, skb)) {
...
}
// 2. 增加 ICMP6_MIB_INMSGS 计数器
__ICMP6_INC_STATS(dev_net(dev), idev, ICMP6_MIB_INMSGS);
saddr = &ipv6_hdr(skb)->saddr;
daddr = &ipv6_hdr(skb)->daddr;
// 3. 对报文的完整性进行检查
if (skb_checksum_validate(skb, IPPROTO_ICMPV6, ip6_compute_pseudo)) {
net_dbg_ratelimited("ICMPv6 checksum failed [%pI6c > %pI6c]\n",
saddr, daddr);
goto csum_error;
}
// 4. data指针往下偏移 icmp头部结构体的大小,确保icmp头部完整。如果没有足够icmp头部大小,则报错。
if (!pskb_pull(skb, sizeof(*hdr)))
goto discard_it;
// 5. 从skb报文中拿到icmpv6的头部信息
hdr = icmp6_hdr(skb);
// 6. 获取cimpv6报文的类型
type = hdr->icmp6_type;
// 7. 对对应的报文类型的计数器+1
ICMP6MSGIN_INC_STATS(dev_net(dev), idev, type);
// 8. 根据报文的类型,进行不同的处理。
switch (type) {
// ICMPv6 echo请求报文
case ICMPV6_ECHO_REQUEST:
// 8.1 如果没有开启忽略全部echo报文,则对请求作出回应
if (!net->ipv6.sysctl.icmpv6_echo_ignore_all)
icmpv6_echo_reply(skb);
break;
// ICMPv6 echo 回复报文
case ICMPV6_ECHO_REPLY:
success = ping_rcv(skb);
break;
// ICMPv6 数据包过大 错误
case ICMPV6_PKT_TOOBIG:
// 首先检查数据块区域包含的数据块长度是否不短于ICMP报头的长度,
// 然后调用 icmpv6_notify 对错误情况进行处理
if (!pskb_may_pull(skb, sizeof(struct ipv6hdr)))
goto discard_it;
hdr = icmp6_hdr(skb);
case ICMPV6_DEST_UNREACH:
case ICMPV6_TIME_EXCEED:
case ICMPV6_PARAMPROB:
// 以上这些均为错误消息,使用icmpv6_notify对错误消息进行处理
icmpv6_notify(skb, type, hdr->icmp6_code, hdr->icmp6_mtu);
break;
// 所有邻居发现类消息都有 ndisc_rcv 进行处理
case NDISC_ROUTER_SOLICITATION:
case NDISC_ROUTER_ADVERTISEMENT:
case NDISC_NEIGHBOUR_SOLICITATION: // 相当于ARP请求
case NDISC_NEIGHBOUR_ADVERTISEMENT:// 相当于ARP应答
case NDISC_REDIRECT:
ndisc_rcv(skb);
break;
//组播侦听者查询处理
case ICMPV6_MGM_QUERY:
igmp6_event_query(skb);
break;
// 组播侦听者报告处理
case ICMPV6_MGM_REPORT:
igmp6_event_report(skb);
break;
// 以下消息都由 icmpv6_notify 进行处理
case ICMPV6_MGM_REDUCTION: //退出组播组时发送消息
case ICMPV6_NI_QUERY: //结点信息查询
case ICMPV6_NI_REPLY: //结点信息应答
case ICMPV6_MLD2_REPORT: //MLDv2组播侦听者报告数据包
case ICMPV6_DHAAD_REQUEST: //ICMP归属代理地址发现请求消息
case ICMPV6_DHAAD_REPLY: //ICMP归属代理地址发现应答消息
case ICMPV6_MOBILE_PREFIX_SOL:
case ICMPV6_MOBILE_PREFIX_ADV:
break;
default:
// 如果是信息消息就退出,否则执行错误处理函数
if (type & ICMPV6_INFOMSG_MASK)
break;
net_dbg_ratelimited("icmpv6: msg of unknown type [%pI6c > %pI6c]\n",
saddr, daddr);
/*
* error of unknown type.
* must pass to upper level
*/
// 处理上面特殊处理的类型,默认都执行这个错误处理函数
icmpv6_notify(skb, type, hdr->icmp6_code, hdr->icmp6_mtu);
}
/* until the v6 path can be better sorted assume failure and
* preserve the status quo behaviour for the rest of the paths to here
*/
if (success)
consume_skb(skb);
else
kfree_skb(skb);
return 0;
// 最后是错误报文计数器累加
csum_error:
__ICMP6_INC_STATS(dev_net(dev), idev, ICMP6_MIB_CSUMERRORS);
discard_it:
__ICMP6_INC_STATS(dev_net(dev), idev, ICMP6_MIB_INERRORS);
drop_no_count:
kfree_skb(skb);
return 0;
}
报文的接收流程就在上面代码中表示,处理业务主要是集中在switch的选择中。对于不同报文的处理,放在下一节来看。后面先了解一下ICMPv6 报文的发送流程。
2. ICMPv6 数据包发送流程
在ICMPv6 中,使用icmpv6_send()
和icmpv6_echo_reply()
发送消息,其中 icmpv6_echo_reply
只是回复消息用,主力还是icmpv6_send()
:发送一个ICMP消息作为对错误数据包的响应
void icmp6_send(struct sk_buff *skb, u8 type, u8 code, __u32 info,
const struct in6_addr *force_saddr,
const struct inet6_skb_parm *parm)
{
struct inet6_dev *idev = NULL;
struct ipv6hdr *hdr = ipv6_hdr(skb);
struct sock *sk;
struct net *net;
struct ipv6_pinfo *np;
const struct in6_addr *saddr = NULL;
struct dst_entry *dst;
struct icmp6hdr tmp_hdr;
struct flowi6 fl6;
struct icmpv6_msg msg;
struct ipcm6_cookie ipc6;
int iif = 0;
int addr_type = 0;
int len;
u32 mark;
if ((u8 *)hdr < skb->head ||
(skb_network_header(skb) + sizeof(*hdr)) > skb_tail_pointer(skb))
return;
if (!skb->dev)
return;
// 获取网络命名空间
net = dev_net(skb->dev);
mark = IP6_REPLY_MARK(net, skb->mark);
/*
* Make sure we respect the rules
* i.e. RFC 1885 2.4(e)
* Rule (e.1) is enforced by not using icmp6_send
* in any code that processes icmp errors.
*/
addr_type = ipv6_addr_type(&hdr->daddr);
// 将报文的目的地址,设置成源地址,并根据地址类型进行更新
// 用于检查hdr的目的地址是否有效,并且是否合适发往指定的网络设备skb->dev. 返回值非零则有效。
// 或者检查hdr的目的地址是否是有效的广播或者多播地址,并确认发送设备skb->dev是否可以使用这个地址。
if (ipv6_chk_addr(net, &hdr->daddr, skb->dev, 0) ||
ipv6_chk_acast_addr_src(net, skb->dev, &hdr->daddr))
// 将报文的目的地址设置为源地址
saddr = &hdr->daddr;
/*
* Dest addr check
*/
// 当遇到多播地址或非直连数据包时,除非是遇到数据包过大、未知option、未解析的option,
// 否则把saddr设置成NULL。
if (addr_type & IPV6_ADDR_MULTICAST || skb->pkt_type != PACKET_HOST) {
if (type != ICMPV6_PKT_TOOBIG &&
!(type == ICMPV6_PARAMPROB && code == ICMPV6_UNK_OPTION &&
(opt_unrec(skb, info))))
return;
saddr = NULL;
}
//根据报文的源地址,来设置报文接收接口iif
addr_type = ipv6_addr_type(&hdr->saddr);
/*
* Source addr check
*/
if (__ipv6_addr_needs_scope_id(addr_type)) {
// 如果需要范围标识,iif是传入数据包的入口索引
iif = icmp6_iif(skb);
} else {
// 如果不需要索引,从skb中寻转路由转发表dst
// 并且调用l3mdev_master_ifindex 来获取入口索引iif
dst = skb_dst(skb);
iif = l3mdev_master_ifindex(dst ? dst->dev : skb->dev);
}
/*
* Must not send error if the source does not uniquely
* identify a single node (RFC2463 Section 2.4).
* We check unspecified / multicast addresses here,
* and anycast addresses will be checked later.
*/
// 仅发送单播数据包,如果是任意地址或者组播地址,则返回(任意地址无法识别到发送方)
if ((addr_type == IPV6_ADDR_ANY) || (addr_type & IPV6_ADDR_MULTICAST)) {
net_dbg_ratelimited(
"icmp6_send: addr_any/mcast source [%pI6c > %pI6c]\n",
&hdr->saddr, &hdr->daddr);
return;
}
/*
* Never answer to a ICMP packet.
*/
// 如果触发的消息是ICMPv6的错误消息,则不再发送。
if (is_ineligible(skb)) {
net_dbg_ratelimited(
"icmp6_send: no reply to icmp error [%pI6c > %pI6c]\n",
&hdr->saddr, &hdr->daddr);
return;
}
/* Needed by both icmp_global_allow and icmpv6_xmit_lock */
local_bh_disable();
/* Check global sysctl_icmp_msgs_per_sec ratelimit */
// 如果不是通过回环接口发送的数据包,进而判断icmpv6全局速率限制不通过,则解锁退出
if (!(skb->dev->flags & IFF_LOOPBACK) && !icmpv6_global_allow(type))
goto out_bh_enable;
mip6_addr_swap(skb, parm);
// 初始化fl6 对象,并设置报文协议为IPPROTO_ICMPV6
memset(&fl6, 0, sizeof(fl6));
fl6.flowi6_proto = IPPROTO_ICMPV6; //设置报文协议
fl6.daddr = hdr->saddr; //设置报文的目的地址
if (force_saddr)
saddr = force_saddr;
if (saddr)
fl6.saddr = *saddr; //设置报文的源地址
fl6.flowi6_mark = mark;
fl6.flowi6_oif = iif; //设置报文的出口ID
fl6.fl6_icmp_type = type;
fl6.fl6_icmp_code = code;
fl6.flowi6_uid = sock_net_uid(net, NULL);
fl6.mp_hash = rt6_multipath_hash(net, &fl6, skb, NULL);
// ipsec安全性检查
security_skb_classify_flow(skb, flowi6_to_flowi(&fl6));
// 获取sk套接字,并上锁
sk = icmpv6_xmit_lock(net);
if (!sk)
goto out_bh_enable;
sk->sk_mark = mark;
np = inet6_sk(sk);
// 数据跑流量控制
if (!icmpv6_xrlim_allow(sk, type, &fl6))
goto out;
tmp_hdr.icmp6_type = type;
tmp_hdr.icmp6_code = code;
tmp_hdr.icmp6_cksum = 0;
tmp_hdr.icmp6_pointer = htonl(info);
// 如果还没有设置报文的出口ID
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; //如果是单播地址则使用单拨出口
// 初始化报文
ipcm6_init_sk(&ipc6, np);
// 获取ipv6 转发表
fl6.flowlabel = ip6_make_flowinfo(ipc6.tclass, fl6.flowlabel);
// 路由查找
dst = icmpv6_route_lookup(net, skb, sk, &fl6);
if (IS_ERR(dst))
goto out;
// 设置报文hlimit
ipc6.hlimit = ip6_sk_dst_hoplimit(np, &fl6, dst);
// 初始化报文msg对象。
msg.skb = skb;
msg.offset = skb_network_offset(skb);
msg.type = type;
len = skb->len - msg.offset;
len = min_t(unsigned int, len,
IPV6_MIN_MTU - sizeof(struct ipv6hdr) -
sizeof(struct icmp6hdr));
if (len < 0) {
net_dbg_ratelimited("icmp: len problem [%pI6c > %pI6c]\n",
- &hdr->saddr, &hdr->daddr);
goto out_dst_release;
}
rcu_read_lock();
idev = __in6_dev_get(skb->dev);
// 尝试发送数据包看是否符合发送条件
if (ip6_append_data(sk, icmpv6_getfrag, &msg,
len + sizeof(struct icmp6hdr),
sizeof(struct icmp6hdr), &ipc6, &fl6,
(struct rt6_info *)dst, MSG_DONTWAIT)) {
ICMP6_INC_STATS(net, idev, ICMP6_MIB_OUT
ERRORS);
// 发送失败,则使用flush丢弃缓存队列
ip6_flush_pending_frames(sk);
} else {
// 如果成功,则用push将数据包压入发送队列
icmpv6_push_pending_frames(sk, &fl6, &tmp_hdr,
len + sizeof(struct icmp6hdr));
}
rcu_read_unlock();
out_dst_release:
// 释放路由表
dst_release(dst);
out:
// 最后释放套接字,关闭软中断
icmpv6_xmit_unlock(sk);
out_bh_enable:
local_bh_enable();
}