互联网控制消息协议

互联网控制消息协议（Internet Control Message Protocol，缩写：ICMP）是互联网协议族的核心协议之一。它用于网际协议（IP）中发送控制消息，提供可能发生在通信环境中的各种问题反馈。通过这些信息，使管理者可以对所发生的问题作出诊断，然后采取适当的措施解决。

ICMP 依靠IP来完成它的任务，它是IP的主要部分。它与传输协议（如TCP和UDP）显著不同：它一般不用于在两点间传输数据。它通常不由网络程序直接使用，除了 ping 和 traceroute 这两个特别的例子。

IPv4中的ICMP被称作ICMPv4，IPv6中的ICMP则被称作ICMPv6。

## 技术细节

ICMP是在 RFC 792 中定义的互联网协议族之一。通常用于返回的错误信息或是分析路由。ICMP错误消息总是包括了源数据并返回给发送者。

ICMP错误消息的例子之一是TTL值过期。每个路由器在转发数据报的时候都会把IP包头中的TTL值减1。如果TTL值为0，“TTL在传输中过期”的消息将会回报给源地址。

每个ICMP消息都是直接封装在一个IP数据包中的，因此，和UDP一样，ICMP是不可靠的。

虽然ICMP是包含在IP数据包中的，但是对ICMP消息通常会特殊处理，会和一般IP数据包的处理不同，而不是作为IP的一个子协议来处理。在很多时候，需要去查看ICMP消息的内容，然后发送适当的错误消息到那个原来产生IP数据包的程序，即那个导致ICMP讯息被传送的IP数据包。

很多常用的工具是基于ICMP消息的。traceroute 是通过发送包含有特殊的TTL的包，然后接收ICMP超时消息和目标不可达消息来实现的。ping 则是用ICMP的"Echo request"（类别代码：8）和"Echo reply"（类别代码：0）消息来实现的。

## ICMP报文结构

### 报头

ICMP报头从IP报头的第160位开始（IP首部20字节）（除非使用了IP报头的可选部分）。

Bits｜160-167｜168-175｜176-183｜184-191

160｜Type｜Code｜校验码（checksum）

192｜Rest of Header

- Type - ICMP的类型,标识生成的错误报文。
- Code - 进一步划分ICMP的类型,该字段用来查找产生错误的原因.；例如，ICMP的目标不可达类型可以把这个位设为1至15等来表示不同的意思。
- Checksum - Internet校验和（RFC 1071），用于进行错误检查，该校验和是从ICMP头和以该字段替换为0的数据计算得出的。
- Rest of Header - 报头的其余部分，四字节字段，内容根据ICMP类型和代码而有所不同。

### 填充数据

填充的数据紧接在ICMP报头的后面（以8位为一组）：

- Linux的"ping"工具填充的ICMP除了8个8字节的报头以外，默认情况下还另外填充数据使得总大小为64字节。
- Windows的"ping.exe"填充的ICMP除了8个8字节的报头以外，默认情况下还另外填充数据使得总大小为40字节。

## 报文类型

类型｜代码｜状态｜描述｜查询｜差错

0 - 响应回显｜0｜Echo响应 (被程序ping使用）｜●

1 and 2｜未分配｜保留｜●

3 - 目的不可达｜0｜目标网络不可达｜●

1｜目标主机不可达｜●

2｜目标协议不可达｜●

3｜目标端口不可达｜●

4｜要求分段并设置DF flag标志｜●

5｜源路由失败｜●

6｜未知的目标网络｜●

7｜未知的目标主机｜●

8｜源主机隔离（作废不用）｜●

9｜禁止访问的网络｜●

10｜禁止访问的主机｜●

11｜对特定的TOS 网络不可达｜●

12｜对特定的TOS 主机不可达｜●

13｜由于过滤 网络流量被禁止｜●

14｜主机越权｜●

15｜优先权终止生效｜●

4 - 源端关闭｜0｜弃用｜源端关闭（拥塞控制）｜●

5 - 重定向｜0｜重定向网络｜●

1｜重定向主机｜●

2｜基于TOS 的网络重定向｜●

3｜基于TOS 的主机重定向｜●

6｜弃用｜备用主机地址

7｜未分配｜保留

8 - 请求回显｜0｜Echo请求｜●

9 - 路由器通告｜0｜路由通告｜●

10 - 路由器请求｜0｜路由器的发现/选择/请求｜●

11 - ICMP 超时｜0｜TTL 超时｜●

1｜分片重组超时｜●

12 - 参数问题：错误IP头部｜0｜IP 报首部参数错误｜●

1｜丢失必要选项｜●

2｜不支持的长度

13 - 时间戳请求｜0｜时间戳请求｜●

14 - 时间戳应答｜0｜时间戳应答｜●

15 - 信息请求｜0｜弃用｜信息请求｜●

16 - 信息应答｜0｜弃用｜信息应答｜●

17 - 地址掩码请求｜0｜弃用｜地址掩码请求｜●

18 - 地址掩码应答｜0｜弃用｜地址掩码应答｜●

19｜保留｜因安全原因保留

20 至 29｜保留｜Reserved for robustness experiment

30 - Traceroute｜0｜弃用｜信息请求

31｜弃用｜数据报转换出错

32｜弃用｜手机网络重定向

33｜弃用｜Where-Are-You（originally meant for IPv6）

34｜弃用｜Here-I-Am（originally meant for IPv6）

35｜弃用｜Mobile Registration Request

36｜弃用｜Mobile Registration Reply

37｜弃用｜Domain Name Request

38｜弃用｜Domain Name Reply

39｜弃用｜SKIP Algorithm Discovery Protocol, Simple Key-Management for Internet Protocol

40｜Photuris, Security failures

41｜实验性的｜ICMP for experimental mobility protocols such as Seamoby RFC4065

42 到 255｜保留｜保留

235｜实验性的｜RFC3692（RFC 4727）

254｜实验性的｜RFC3692（RFC 4727）

255｜保留｜保留

### 源站抑制

源站抑制报文旨在请求发送方降低发往路由器或主机的报文发送速率。在接收的过程中，当接收方没有足够的接收缓冲区来处理接收到的报文，或者接收这个报文会导致临近其本身的缓冲区限制时，就会触发源站抑制报文。

数据被从一个或一群主机高速地发往网络上的一个路由器，虽然路由器有缓冲机制，但是路由器的缓冲区大小通常（由于物理内存有限的原因）被限制。因此，如果路由器的通信量过大，路由器最终会（由于内存耗尽，导致必须丢弃掉接收到的数据报）无法继续处理超过输入缓冲区限制的部分数据，直到路由器缓冲队列有空余空间可以存放新的数据报。但是由于网络层（Network Layer）缺乏确认讯息（ACK）机制，因此客户端无法获知数据是否成功抵达接收方。所以研究者提出了源站抑制这一补救措施来解决这一问题：当路由器发现流入数据速率远远高于流出数据速率时，会发送ICMP源站抑制报文给源站，通知源站应该降低其数据传输速度或等待一定时间后再尝试发送更多数据。当源站接收到ICMP源站抑制报文时会减慢数据发送的速度，或者在再次尝试发送数据前等待一定的时间，使得路由器能够（在处理完当前接收到的数据之后）清空输入缓冲队列。

但是因为有研究表明“源站抑制是一种无效的（不公平的）补救措施”，所以路由的源站抑制报文已在1995年被 RFC 1812 弃用。此外，（路由）转发和回应任何形式的源站抑制报文已在2012年被 RFC 6633 弃用

00｜01｜02｜03｜04｜05｜06｜07｜08｜09｜10｜11｜12｜13｜14｜15｜16｜17｜18｜19｜20｜21｜22｜23｜24｜25｜26｜27｜28｜29｜30｜31

类型（Type） = 4｜代码（Code） = 0｜检验和（Checksum）

未使用IP数据报头部和源数据报数据的前8个字节

其中:

类型（Type）必须设置为4

代码（Code）必须设置为0

IP数据报头部 和其附加的数据用于发送端根据回应报文匹配对应的请求报文

### 重定向

<image>

重定向 报文是网关发出的，用于要求主机或路由器改变数据报的传输路径的ICMP报文。ICMP 重定向是路由器将路由信息传达给主机的机制。这种类型的报文通知主机更新它的路由信息（请求主机改变其路由）。如果一个主机在通信时将数据报发送给了路由器R1，而R1将这个数据报转发给了另一个路由器R2，且主机到路由器R2之间有一条直连的路径（也就是说，此主机和路由器R2处于同一以太网段上），那么路由器R1会发送一条重定向报文给主机，来通知它到路由器R2可用路径里有一条更短、更优化的路径。这个主机在接收到这个重定向报文之后应该改变其路由至这个优化版本的路由信息，来将抵达这个目的地的数据报直接发送到路由器R2，并且路由器仍将原始数据报发送到预期目的地。但是，如果一个数据报携带有路由信息，那么即使有更加优化的路径，路由器也不会发送重定向报文。并且，RFC 1122 指出，重定向报文应该只由网关发送，而不应该由互联网主机发送。

00｜01｜02｜03｜04｜05｜06｜07｜08｜09｜10｜11｜12｜13｜14｜15｜16｜17｜18｜19｜20｜21｜22｜23｜24｜25｜26｜27｜28｜29｜30｜31

类型（Type） = 5｜代码（Code）｜检验和（Checksum）

路由器的IP 地址激发重定向报文的数据报IP首部及其数据的前8字节

其中:

类型（Type）必须设置为 5.

代码（Code）指定重定向的原因，见下表

代码（Code）｜描述

0｜针对网络的重定向报文

1｜针对主机的重定向报文

2｜针对网络和服务类型的重定向报文

3｜针对主机和服务类型的重定向报文

路由器的IP 地址（IP address）是一个32位的网关IP地址，该地址指明了该数据报应该被重定向到的路由器地址。

激发重定向报文的数据报IP首部及其数据的前8字节（IP header）用于收到重定向报文的主机根据回应报文匹配对应的请求报文，来确定该数据报的目的站地址。

### 超时

超时 报文是网关产生并发送给源站的ICMP报文，用于通知源站有数据报因为存活时间递减至0而被此网关丢弃。当主机等待数据报分片的过程中超时而无法重新组装数据报分片时也会产生该报文。

超时报文也用于traceroute工具来识别两个主机之间的路径上的网关。

00｜01｜02｜03｜04｜05｜06｜07｜08｜09｜10｜11｜12｜13｜14｜15｜16｜17｜18｜19｜20｜21｜22｜23｜24｜25｜26｜27｜28｜29｜30｜31

类型（Type） = 11｜代码（Code）｜校验和（Checksum）

未使用激发超时报文的数据报IP首部及其数据的前8字节

其中:

类型（Type）必须设置为 11

代码（Code）指定重超时的原因，见下表

Code｜Description

0｜存活时间计数超时

1｜分片重装超时

激发超时报文的数据报IP首部及其数据的前8字节 这些信息用于源站根据收到的超时报文来确定具体哪个数据报已被丢弃。对于高层协议，比如用户数据报协议 和 传输控制协议而言，额外的8字节数据指明了已被丢弃的数据报中的源端口与目的端口。

### 时间戳请求

时间戳请求 报文主要用于互联网机器（包括路由器和主机）之间同步时钟。起始时间戳是发送端最后一次改动该数据报的时间戳(为世界标准时午夜开始计算的毫秒数)。在该类型的报文中，接收时间戳和传输时间戳未被使用。

00｜01｜02｜03｜04｜05｜06｜07｜08｜09｜10｜11｜12｜13｜14｜15｜16｜17｜18｜19｜20｜21｜22｜23｜24｜25｜26｜27｜28｜29｜30｜31

类型（Type） = 13｜代码（Code） = 0｜检验和（Checksum）

标识符（Identifier）｜序号（Sequence number）

起始时间戳（Originate timestamp）

接收时间戳（Receive timestamp）

传输时间戳（Transmit timestamp）

其中:

类型（Type）必须设置为 13

代码（Code）必须设置为 0

标识符（Identifier） and 序号（Sequence）用于在时间戳请求报文和时间戳回答报文之间建立关联

起始时间戳（Originate timestamp）为世界标准时午夜开始计算的毫秒数。如果没有可用的世界标准时参考，则可以将最高有效位设置为1以指示这是一个非标准时间值。

### 时间戳回答

时间戳回答 报文是对时间戳请求报文的回答报文。时间戳回答报文由接收到的时间戳请求报文其中的起始时间戳和接收时间戳（回应端主机接收到请求报文并创建时间戳回应报文的时间，单位为毫秒）、传输时间戳（时间戳回答报文被发送出去的时间，单位为毫秒）组成。

00｜01｜02｜03｜04｜05｜06｜07｜08｜09｜10｜11｜12｜13｜14｜15｜16｜17｜18｜19｜20｜21｜22｜23｜24｜25｜26｜27｜28｜29｜30｜31

类型（Type） = 14｜代码（Code） = 0｜检验和（Checksum）

标识符（Identifier）｜序号（Sequence number）

起始时间戳（Originate timestamp）

接收时间戳（Receive timestamp）

传输时间戳（Transmit timestamp）

其中:

类型（Type）必须设置为 14

代码（Code）必须设置为 0

标识符（Identifier）和 序号（Sequence number）用于在时间戳请求报文和时间戳回答报文之间建立关联。

起始时间戳（Originate timestamp）是发送端最后一次改动该数据报的时间戳。

接收时间戳（Receive timestamp）是回应端主机接收到请求报文并创建时间戳回应报文的时间，单位为毫秒。

传输时间戳（Transmit timestamp）是最后一次修改回应报文并将其发送出去的时间，单位为毫秒。

所有的时间戳都是世界标准时午夜起始的毫秒数。如果这个时间不能表示为毫秒或者没有可用的世界标准时参考值，则可以使用任何格式的时间值并将最高有效位设置为1以指示这是一个非标准时间值。

### 地址掩码请求

地址掩码请求 报文是主机为了得到一个合适的子网掩码而发送到路由器的ICMP请求报文。

接收到此请求报文的机器应当发送一个地址掩码回答报文给源站。

00｜01｜02｜03｜04｜05｜06｜07｜08｜09｜10｜11｜12｜13｜14｜15｜16｜17｜18｜19｜20｜21｜22｜23｜24｜25｜26｜27｜28｜29｜30｜31

类型（Type） = 17｜代码（Code） = 0｜检验和（Checksum）

标识符（Identifier）｜序号（Sequence number）

地址掩码（Address mask）

其中:

类型（Type）必须设置为 17

代码（Code）必须设置为 0

地址掩码（Address mask）可以为0

由于ICMP 地址掩码请求可能会被用于嗅探攻击来收集特定网络的信息，因此该报文默认情况下被Cisco IOS禁用。

### 地址掩码回答

地址掩码回答 报文携带有地址掩码信息，用于回答地址掩码请求报文。

00｜01｜02｜03｜04｜05｜06｜07｜08｜09｜10｜11｜12｜13｜14｜15｜16｜17｜18｜19｜20｜21｜22｜23｜24｜25｜26｜27｜28｜29｜30｜31

类型（Type） = 18｜代码（Code） = 0｜校验和（Checksum）

标识符（Identifier）｜序号（Sequence number）

地址掩码（Address mask）

其中:

类型（Type）必须设置为 18

代码（Code）必须设置为 0

地址掩码（Address mask）为待回答的地址掩码

### 源站不可达

源站不可达 报文是由主机或入站网关用于通知客户端出于目的站无法连接的报文。这些原因可能包括：物理连接失效（也即网络距离无限大），或指定的地址或端口处于非激活状态，或者数据报长度过长而导致必须分片但是IP首部指定了“不分片”选项导致无法分片。如果是TCP端口不可达，则会返回TCP RST，而不会返回此报文。如果是IP多播的情况，也不会返回此报文。

00｜01｜02｜03｜04｜05｜06｜07｜08｜09｜10｜11｜12｜13｜14｜15｜16｜17｜18｜19｜20｜21｜22｜23｜24｜25｜26｜27｜28｜29｜30｜31

类型（Type） = 3｜代码（Code）｜检验和（Checksum）

未使用｜下一跳的MTU值

激发ICMP地址不可达报文的数据报IP首部及其数据的前8字节

其中:

类型（Type）必须设置为 3

代码（Code）字段用于指示具体导致源站不可达的原因。见下表。

代码（Code）｜解释（Description）

0｜网络不可达

1｜主机不可达

2｜协议不可达

3｜端口不可达

4｜需要分片但是DF(Do not Fragment)置位

5｜源路由失败

6｜目的网络未知

7｜目的主机未知

8｜源主机被隔离

9｜与受到管理禁控的目的网络通信

10｜与受到管理禁控的目的主机通信

11｜对于指明的服务类型，网络不可达

12｜对于指明的服务类型，主机不可达

13｜出于管理目的禁止通信

14｜主机越权.

15｜优先权剥夺生效

Next-hop MTU 当需要分片但是DF(Do not Fragment)置位的错误发生时，包含了下一跳网络的MTU的值。

IP header 用于源站根据收到的源站不可达报文来确定具体哪个数据报引起了源站不可达错误。

### C语言

```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/ip_icmp.h>

unsigned short checksum(void *b, int len) {
    unsigned short *buf = b;
    unsigned int sum = 0;
    unsigned short result;

    for (sum = 0; len > 1; len -= 2)
        sum += *buf++;
    if (len == 1)
        sum += *(unsigned char*)buf;

    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    result = ~sum;
    return result;
}

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
    if (sockfd < 0) {
        perror("socket");
        return 1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    inet_pton(AF_INET, "8.8.8.8", &addr.sin_addr);

    char sendbuf[64];
    char recvbuf[128];

    struct icmphdr *icmp = (struct icmphdr *)sendbuf;
    memset(sendbuf, 0, sizeof(sendbuf));

    icmp->type = ICMP_ECHO;
    icmp->code = 0;
    icmp->un.echo.id = getpid() & 0xFFFF;
    icmp->un.echo.sequence = 1;
    icmp->checksum = checksum(sendbuf, sizeof(sendbuf));

    struct timeval start, end;
    gettimeofday(&start, NULL);

    if (sendto(sockfd, sendbuf, sizeof(sendbuf), 0,
               (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("sendto");
        return 1;
    }

    socklen_t addrlen = sizeof(addr);
    int n = recvfrom(sockfd, recvbuf, sizeof(recvbuf), 0,
                     (struct sockaddr *)&addr, &addrlen);
    if (n < 0) {
        perror("recvfrom");
        return 1;
    }

    gettimeofday(&end, NULL);
    double rtt = (end.tv_sec - start.tv_sec) * 1000.0 +
                 (end.tv_usec - start.tv_usec) / 1000.0;

    printf("Reply from 8.8.8.8: bytes=%d time=%.2f ms\n", n, rtt);

    close(sockfd);
    return 0;
}
```

## 外部链接

- RFC 792, Internet Control Message Protocol
- ICMP Sequence Diagram
- RFC 1122, Requirements for Internet Hosts -- Communication Layers
- RFC 1716, Towards Requirements for IP Router
- RFC 1812, Requirements for IP Version 4 Routers
- RFC 6633, Deprecation of ICMP Source Quench Messages
- Filtering ICMP on firewalls
- IANA