TCP实现之:一个ping包的旅行

本文深入解析了TCP/IP协议簇中的ICMP协议,详细介绍了ICMP协议的基本结构及其在网络控制中的作用。通过分析ping命令的工作原理,展示了从发送ping请求到接收回应的整个过程,特别关注了ICMP数据包在内核中的接收与处理流程,包括ICMP协议头的解析、错误检查、报文校验和类型判断等关键步骤。同时,文章探讨了如何通过内核函数如`icmp_rcv`和`icmp_echo`处理ICMP报文,以及如何构建并发送ping回应。通过对源码的剖析,读者可以更深入理解网络协议栈的工作机制。

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回显响应包基本一致,这里就不再赘述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值