網際網路控制消息協議

網際網路控制消息協議（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