TCP实现之:一个ping包的旅行
前言
所谓的ping指的是linux、windows等操作系统下的一个用来探测网络是否连通的命令,其基本格式为:ping <目的ip地址>,其本质为发送一个ICMP协议包。
1. ICMP协议简介
ICMP(Internet Control Message Protocol)网络控制报文协议属于TCP/IP协议簇的一个子协议,用于在主机、路由器等设备之间传递控制消息,属于L3协议。控制消息指的是网络是否通畅、主机是否可达、路由是否可用等网络本身的消息。虽然它并不传递用户数据,但其对数据的传递十分重要。虽然ICMP与IP同属于网络层协议,但是ICMP却依赖于IP协议,其报文需要IP协议为其封装,基本报文格式如下:

IP头部的Protocol值为1就说明这是一个ICMP报文,ICMP头部中的类型(Type)域用于说明ICMP报文的作用及格式,此外还有一个代码(Code)域用于详细说明某种ICMP报文的类型,所有数据都在ICMP头部后面。当Type=8,Code=0时代表这是一个ping请求包,Type=0,Code=0时代表这是一个ping回应包。
2. ping命令原理
ping命令的原理如下图所示,首先发送方发送一个ICMP数据包,该包为ping的请求包,其Type为8,Code为0,标识符和序列号用于唯一标识给数据包。回应方接收到该数据包后返回一个Type为0Code为0的数据包,其序列号和标识符以及数据与收到的数据包相同。

3. 源码分析
3.1 ICMP数据包的接收
对于ICMP数据包的接收,这里不再从头讲起,请参考上一章节《TCP/IP实现之:报文的接收与发送》,这里我们将从netif_receive_skb函数讲起。
正如之前讲的,netif_receive_skb在处理报文时,会遍历ptype_base接收处理函数结构体链表,从中找到与之匹配的协议处理函数。ICMP协议的net_protocol结构体的定义如下:
static const struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
.err_handler = icmp_err,
.no_policy = 1,
.netns_ok = 1,
};
icmp_rcv
从中我们可以看出ICMP报文的处理函数为icmp_rcv,现在我们来好好看看这个函数。这个函数会对ICMP报文进行必要的检查,在各个检查项都通过后将其交给各个类型(Type)的处理函数,
int icmp_rcv(struct sk_buff *skb)
{
struct icmphdr *icmph;
struct rtable *rt = skb_rtable(skb);
struct net *net = dev_net(rt->dst.dev);
bool success;
//进行某种校验
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
struct sec_path *sp = skb_sec_path(skb);
int nh;
if (!(sp && sp->xvec[sp->len - 1]->props.flags &
XFRM_STATE_ICMP))
goto drop;
if (!pskb_may_pull(skb, sizeof(*icmph) + sizeof(struct iphdr)))
goto drop;
nh = skb_network_offset(skb);
skb_set_network_header(skb, sizeof(*icmph));
if (!xfrm4_policy_check_reverse(NULL, XFRM_POLICY_IN, skb))
goto drop;
skb_set_network_header(skb, nh);
}
__ICMP_INC_STATS(net, ICMP_MIB_INMSGS);
//验证报文的校验和,保证报文的完整性
if (skb_checksum_simple_validate(skb))
goto csum_error;
if (!pskb_pull(skb, sizeof(*icmph)))
goto error;
//获取报文的头部
icmph = icmp_hdr(skb);
ICMPMSGIN_INC_STATS(net, icmph->type);
/*
* ICMP的Type的最大值为18,超过这个值的报文将被丢弃。
*/
if (icmph->type > NR_ICMP_TYPES)
goto error;
/*
* 处理多播或者广播的ICMP报文。一般这种报文应该被丢弃,对于ping包和时间戳包,
* 允许用户通过sysctl进行配置是否处理这种包。
*/
if (rt->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST)) {
if ((icmph->type == ICMP_ECHO ||
icmph->type == ICMP_TIMESTAMP) &&
net->ipv4.sysctl_icmp_echo_ignore_broadcasts) {
goto error;
}
if (icmph->type != ICMP_ECHO &&
icmph->type != ICMP_TIMESTAMP &&
icmph->type != ICMP_ADDRESS &&
icmph->type != ICMP_ADDRESSREPLY) {
goto error;
}
}
/*
* icmp_pointers为一个icmp_control类型结构体的数组,其中每个实例的位置按照其type值来进行放置,
* 因此icmp_pointers[icmph->type]即为Type值为icmph->type的报文的处理函数结构体。
*/
success = icmp_pointers[icmph->type].handler(skb);
if (success) {
consume_skb(skb);
return NET_RX_SUCCESS;
}
drop:
kfree_skb(skb);
return NET_RX_DROP;
csum_error:
__ICMP_INC_STATS(net, ICMP_MIB_CSUMERRORS);
error:
__ICMP_INC_STATS(net, ICMP_MIB_INERRORS);
goto drop;
}
下面我们来分析一下ping包,即Type为8的ICMP报文的处理函数。
static const struct icmp_control icmp_pointers[NR_ICMP_TYPES + 1] = {
......
[ICMP_ECHO] = {
.handler = icmp_echo,
},
[9] = {
.handler = icmp_discard,
.error = 1,
},
......
};
从上面的代码中我们可以看出ping包的接收处理函数为icmp_echo,下面我们来重点分析一下该函数。该函数很简单,它对ping的回显响应的数据包进行了封装,并将其交给icmp_reply函数。
icmp_echo
static bool icmp_echo(struct sk_buff *skb)
{
struct net *net;
net = dev_net(skb_dst(skb)->dev);
//检查sysctl的ping响应是否开启
if (!net->ipv4.sysctl_icmp_echo_ignore_all) {
struct icmp_bxm icmp_param;
/* 构造icmp_bxm结构体,并将其交给icmp_reply函数。icmp_bxm结构体是用于构建icmp响应包的结构体,
* 从这里我们可以看出其将响应包的Type设为ICMP_ECHOREPLY,即0,其余的icmp头部信息与接收到的数据
* 包相同。
*/
icmp_param.data.icmph = *icmp_hdr(skb);
icmp_param.data.icmph.type = ICMP_ECHOREPLY;
icmp_param.skb = skb;
icmp_param.offset = 0;
icmp_param.data_len = skb->len;
icmp_param.head_len = sizeof(struct icmphdr);
icmp_reply(&icmp_param, skb);
}
/* should there be an ICMP stat for ignored echos? */
return true;
}
icmp_reply
icmp_reply函数的主要作用包括:(1)查找路由;(2)速率限制;(3)调用icmp_push_reply函数进行数据发送。
首先,它对函数中要用到的一些变量进行了初始化。
static void icmp_reply(struct icmp_bxm *icmp_param, struct sk_buff *skb)
{
struct ipcm_cookie ipc;
struct rtable *rt = skb_rtable(skb);
struct net *net = dev_net(rt->dst.dev);
struct flowi4 fl4;
struct sock *sk;
struct inet_sock *inet;
__be32 daddr, saddr;
u32 mark = IP4_REPLY_MARK(net, skb->mark);
int type = icmp_param->data.icmph.type;
int code = icmp_param->data.icmph.code;
然后,它对IP报头的选项进行了检查,确认其是否需要进行回显,并将本地CPU上的软中断进行了关闭。在使用icmpv4_global_allow获取ICMP是否允许发送时需要禁用本地软中断。
if (ip_options_echo(net, &icmp_param->replyopts.opt.opt, skb))
return;
/* Needed by both icmp_global_allow and icmp_xmit_lock */
local_bh_disable();
icmpv4_global_allow用来检查ICMP包的发送是否超过sysctl_icmp_msgs_per_sec的限制,这个参数限制了每秒钟允许发送的ICMP包的数量。
/* global icmp_msgs_per_sec */
if (!icmpv4_global_allow(net, type, code))
goto out_bh_enable;
对ICMP报文的一些参数进行设置,并查找路由。
sk = icmp_xmit_lock(net);
if (!sk)
goto out_bh_enable;
inet = inet_sk(sk);
icmp_param->data.icmph.checksum = 0;
ipcm_init(&ipc);
inet->tos = ip_hdr(skb)->tos;
sk->sk_mark = mark;
daddr = ipc.addr = ip_hdr(skb)->saddr;
saddr = fib_compute_spec_dst(skb);
if (icmp_param->replyopts.opt.opt.optlen) {
ipc.opt = &icmp_param->replyopts.opt;
if (ipc.opt->opt.srr)
daddr = icmp_param->replyopts.opt.opt.faddr;
}
memset(&fl4, 0, sizeof(fl4));
fl4.daddr = daddr;
fl4.saddr = saddr;
fl4.flowi4_mark = mark;
fl4.flowi4_uid = sock_net_uid(net, NULL);
fl4.flowi4_tos = RT_TOS(ip_hdr(skb)->tos);
fl4.flowi4_proto = IPPROTO_ICMP;
fl4.flowi4_oif = l3mdev_master_ifindex(skb->dev);
security_skb_classify_flow(skb, flowi4_to_flowi(&fl4));
rt = ip_route_output_key(net, &fl4);
if (IS_ERR(rt))
goto out_unlock;
对发包速率进行检查,检查通过后调用icmp_push_reply进行发包。
//对发送速率进行限制,允许的话调用icmp_push_reply进行发送,不允许的话调用ip_rt_put
if (icmpv4_xrlim_allow(net, rt, &fl4, type, code))
icmp_push_reply(icmp_param, &fl4, &ipc, &rt);
ip_rt_put(rt);
out_unlock:
icmp_xmit_unlock(sk);
out_bh_enable:
/* 开启本地软中断 */
local_bh_enable();
}
icmp_push_reply
icmp_push_reply函数会调用ip_append_data函数将数据添加到套接口的skb发送队列,并调用ip_push_pending_frames函数将套接口发送队列中的报文全部发送出去。对于这两个函数以及后面的发包流程,请参阅IP协议源码分析部分。
static void icmp_push_reply(struct icmp_bxm *icmp_param,
struct flowi4 *fl4,
struct ipcm_cookie *ipc, struct rtable **rt)
{
struct sock *sk;
struct sk_buff *skb;
//获取到发送要使用的INET套接口
sk = icmp_sk(dev_net((*rt)->dst.dev));
//调用ip_append_data,将要发送的数据缓存到sk->sk_write_queue,即报文发送队列
if (ip_append_data(sk, fl4, icmp_glue_bits, icmp_param,
icmp_param->data_len+icmp_param->head_len,
icmp_param->head_len,
ipc, rt, MSG_DONTWAIT) < 0) {
__ICMP_INC_STATS(sock_net(sk), ICMP_MIB_OUTERRORS);
//将缓存中的数据发送出去,并对其进行内存释放
ip_flush_pending_frames(sk);
} else if ((skb = skb_peek(&sk->sk_write_queue)) != NULL) {
struct icmphdr *icmph = icmp_hdr(skb);
__wsum csum = 0;
struct sk_buff *skb1;
/* 计算报文的校验码 */
skb_queue_walk(&sk->sk_write_queue, skb1) {
csum = csum_add(csum, skb1->csum);
}
csum = csum_partial_copy_nocheck((void *)&icmp_param->data,
(char *)icmph,
icmp_param->head_len, csum);
icmph->checksum = csum_fold(csum);
skb->ip_summed = CHECKSUM_NONE;
//该函数用于将sk中挂起的报文全部发送出去
ip_push_pending_frames(sk, fl4);
}
}
3.2 ICMP数据包的发送
在进行ping包回显请求发送时,icmp_send函数将会被调用,该函数是__icmp_send函数的封装。__icmp_send函数处理逻辑与发送icmp回显响应包基本一致,这里就不再赘述。
本文深入解析了TCP/IP协议簇中的ICMP协议,详细介绍了ICMP协议的基本结构及其在网络控制中的作用。通过分析ping命令的工作原理,展示了从发送ping请求到接收回应的整个过程,特别关注了ICMP数据包在内核中的接收与处理流程,包括ICMP协议头的解析、错误检查、报文校验和类型判断等关键步骤。同时,文章探讨了如何通过内核函数如`icmp_rcv`和`icmp_echo`处理ICMP报文,以及如何构建并发送ping回应。通过对源码的剖析,读者可以更深入理解网络协议栈的工作机制。

1981

被折叠的 条评论
为什么被折叠?



