TCP/IP实现(七) ICMP协议

一.概述

       ICMP协议用于在网络间传递请求与应答以及差错消息。比如Ping程序便是应用了请求与应答,而Traceroute应用的却是差错消息来实现的。

      对于ICMP的输入往往要经过多次处理,首先由icmp_input函数进行处理,进行首部校验,并根据ICMP类型做进一步处理,对于ICMP类型超过ICMP处理范围的(即大于icmp_type>ICMP_MAXTYPE,可能是用户自己定义的某种类型)ICMP报文,会直接交付给应用进程进行处理。接着可能会被上层传输协议处理。最后还肯能通过rip_input函数交付给应用进程(比如Ping,Traceroute)。icmp_input函数处理的总流程如下:

void icmp_input(mbuf m, int hlen)
{
    struct ip* ip = mtod(m, struct ip*); // 指向IP首部
    int icmplen = ip->ip_len; // ICMP长度
    struct icmp *icp = mtod(m, struct icmp*); // 指向ICMP首部

    进行ICMP首部校验

    if(icp->icmp_type > ICMP_MAXTYPE) {
        goto raw; // 直接交付应用进程处理
    }

    switch(icp->icmp_code){
        处理各种类型的icmp报文
        
        default:
            break;
    }

    raw:
        rip_input(m);// 将ICMP报文交由应用进程进行处理
        return;

    freeit:
        mfree(m);
}

二.ICMP差错消息

       在博文《TCP/IP实现(五) IP协议》中的TCP/IP错误处理流程一节中提到过ICMP错误消息的处理,对于一些错误消息内核会将其转化为错误码,并赋值给errno,应用程序可以通过查看errno来得知错误类型。

       对以下几种情况都不会产生ICMP差错报文:1)对一份ICMP差错报文永远不会产生另一份ICMP差错报文。2)目的地址是广播地址或多播地址的IP数据报。3)作为链路层广播的数据报。4)不是IP分片的第一个分片。5)源地址不是单个主机的数据报(即源地址不能是零地址,环回地址,广播地址或多播地址)。对于2,3,5条是为了防止对广播分组进行响应而造成广播风暴。

1.ICMP差错处理

      ICMP差错报文一般是在当某个数据报无法到达目的主机或目标端口时,由目的主机或中间路由器生成ICMP差错报文,并将其返回至源端。接下来我们来思考一下这样一个问题:ICMP报文是主机间进行交换的,并未指明端口号和协议,那么内核是如何知道该将错误告知哪个上层呢?要想回答这个问题就必须知道ICMP差错报文的组成。ICMP差错报文除了ICMP首部外,必须包含生成该差错的IP数据报首部(包含所有选项),且必须包含跟在IP首部之后的8个字节(icmp::icmp_dun::id_ip::di_ip即icmp::icmp_ip指向该数据报首部)。而UDP首部总共只有8个字节(必包含源端口号),TCP首部中的源端口号也在前8个字节中,这也就解释了是如何区分将错误信息告知哪个进程了(或说是上层更准确)。

       当源端收到ICMP差错报文后便会在icmp_input函数中将其转化为一个协议无关的差错码。并调用运输层协议的pr_ctlinput函数,该函数根据icmp差错报文中携带的错误IP数据包将到达分组分用到正确的协议。其大致伪代码如下:

// switch 内部
case ICMP_UNREACH:
    switch(code) {
        case ICMP_UNREACH_NET:
        case xxx:
        ...
            code += PRC_UNREACH_NET;
            break;
        case xxx:
        ...
        default:
            goto badcode;
    }
    goto deliver;

case ICMP_TIMXCEED:
    if(code > 1)
        goto badcode;
    code = xxx;
    goto deliver;

case xxx:
     xxx
         goto badcode;
     goto deliver;
 
deliver:
    // 检查ICMP报文长度
    // ICMP_ADVLENMIN  = 8(ICMP首部长度) + sizeof(struct ip) + 8)
    // ICMP_ADVLEN(icp): (8 + ((p)->icmp_ip.ip_hl << 2) + 8) 即 8 + IP首部(含选项) + 8
    if(icmplen < ICMP_ADVLENMIN || icmplen < ICMP_ADVLEN(icp)) {
        icmpstat.icps_badlen++; // 错误长度计数
        goto freeit;
    }
    NTOHS(icp->icmp_ip.ip_len);// 将错误IP数据中的长度字段转为主机字节序
    
    pr_ctlinput(code, 错误数据报的目的IP, icp->icmp_ip);//icp->icmp_ip指向错误IP首部
    

      

三.ICMP请求与应答处理

     ICMP请求报文一般用于在网络中请求获取其它节点的一下信息,比如回显,掩码询问等等。

1.回显请求

     所谓回显请求即接收端将收到的数据重新发回至源端,Ping程序便是通过这个原理实现的,将发送时的时间记录于ICMP报文的数据部分,当目的端返回后用当前时间减去发送事时间便是往返时间rtt。回显请求的处理很简单,将ICMP的类型由ICMP_ECHO改为ICMP_ECHOREPLY(icmp_code 总为0)。再由icmp_reflect函数进行应答。其代码如下:

case ICMP_ECHO:
    icp->icmp_type = ICMP_ECHOREPLY;
    goto reflect;

    /*其它ICMP请求类型*/

    reflect:
        icmp.icp_reflect++;
        xxx;
        icmp_reflect(m);
        return; // 注意此处使用了return,因此对TCP/IP中已定义的请求不会由rip_input函数交付给应用,即不会调制第一节中的raw代码段
        

2.时间戳请求

       时间戳请求中包含3个时间,发送请求时间由请求方填写,其余两个:收到请求的时间和发出回答的时间由应答方进行填写。此处对齐实现不做说明。其仍然交由函数icmp_reflect进行回复,icmp_reflect函数用于将ICMP回答或差错发回给请求端或无效数据报的源端。

3.回答处理

      内核从不产生任何ICMP请求报文,因此也不处理任何ICMP报文。内核将其收到的所有回答传给等待ICMP报文的进程,且路由器发现报文也交由其相关的处理程序(路由守护程序)。其伪代码如下:

case xxx_REPLY:
case xxx_REPLY:
...
case ICMP_ROUTERSOLICIT:
default:
    break;
}//switch
raw:
    rip_input(m);
    return;

四.ICMP输出处理

       当数据过长时,IP层会对数据进行分片处理,因此当导致错误的数据报不是第一个分片时,icmp不会回应ICMP错误报文。

五.ICMP程序举例 PING & Traceroute

猜你喜欢

转载自blog.csdn.net/qq_34228327/article/details/84144384