【lwip】10-ICMP協定&原始碼分析

2022-11-06 12:00:37

前言

參考:RFC 792

原文:李柱明部落格:https://www.cnblogs.com/lizhuming/p/16861945.html

10.1 ICMP簡介

IP 協定是一種不可靠、無連線的協定,只在各個主機間交付資料,但是對於資料的到達與否,IP 協定並不關心。

但是有些源主機希望能得到當資料沒能傳送到目標的時候有個迴應,不然目標主機都不知道發的資料到了哪裡。

所以 IP 協定並不完美,這就出現的 ICMP。

ICMP 是「Internet Control Message Protocol」(網際報文控制協定)的縮寫。

ICMP協定用於在IP主機、路由器之間傳遞控制訊息,包括封包錯誤資訊、網路狀況資訊和主機狀況資訊等。

ICMP屬於網路層。

雖然從報文上看ICMP報文是基於IP報文的,但是ICMP從協定和功能上看是屬於網路層的,因為ICMP報文的目的不是目的主機的某個應用程式,不為應用程式提供傳輸服務,而是是IP協定的輔助協定。

所以其報文主要分兩大類:ICMP差錯報告報文和ICMP查詢報文。

10.2 ICMP報文

10.2.1 ICMP報文格式

IPCM報文封裝在IP資料區中,如圖:

其ICMP報文格式如下圖:

ICMP報文由8位元組首部和可變長度的資料部分組成。

不同型別的ICMP報文,ICMP 報文首部的格式也會有點差異,但是首部的前 4 個位元組都是通用的:

  • 型別(type)欄位:佔用1位元組。表示產生這種型別 ICMP 報文的原因。
  • 程式碼(code)欄位:佔用1位元組。進一步描述了產生這種型別 ICMP 報文的具體原因。
  • 校驗和欄位:佔用2位元組。記錄包括 ICMP 報文資料部分在內的整個 ICMP 資料包的校驗和。其計算方法和IP首部校驗和一樣。

參考RFC 792

  • 根據各種ICMP報文型別來定義剩餘非固定的報文格式。
  • 也可以參考該協定找到各個欄位的意義。

10.2.2 ICMP報文型別

ICMP 報文有兩大型別:ICMP差錯報告報文和ICMP查詢報文。

ICMP差錯報告報文主要是用來向 IP 資料包源主機返回一個差錯報告資訊,而這個差錯報告資訊產生的原因是路由器或者主機不能對當前資料包進行正常的處理。

簡單來說就是源主機傳送的資料包沒法到目標主機中,或者到達了目標主機而無法遞交給上層協定。便會產生ICMP差錯報告報文返回給源主機。

ICMP查詢報文用於一臺主機向另一臺主機發起一個請求,如果目標主機收到這個查詢的請求後,就會按照查詢報文的格式向源主機做出應答。如ping。

結合ICMP報文型別欄位的表格:

ICMP報文型別 具體型別 描述
差錯報告報文
3 目的不可達
4 源站抑制
5 重定向
11 超時
12 引數錯誤報文
查詢報文
0或8 回顯請求或回顯應答
9或10 路由器詢問或通告
13或14 時間戳請求或應答
15或16 資訊請求或資訊應答
17或18 掩碼請求或應答

10.2.3 ICMP報文固定首部欄位意義

參考RFC 792

型別 程式碼 描述 查詢 差錯
0 0 回顯應答(如ping應答)
3
目的不可達:
0 網路不可達
1 主機不可達
2 協定不可達
3 埠不可達
4 需要進行分片,但設定不了分片位元
5 源站選路失敗
6 目的網路不認識
7 目的主機不認識
8 源主機被隔離(作廢不用)
9 目的網路被強制禁止
10 目的主機被強制禁止
11 由於服務型別TOS,網路不可達
12 由於服務型別TOS,主機不可達
13 由於過濾,通訊被強制禁止
14 主機越權
15 優先權中止生效
4 0 源站抑制
5
重定向:
0 對網路重定向
1 對主機重定向
2 對服務型別和網路重定向
3 對服務型別和主機重定向
8 0 請求回顯(如ping請求)
9 0 路由器通告
10 0 路由器請求
11
超時:
0 傳輸期間TTL為0
1 分片資料包重灌超時
12
引數問題:
0 壞IP首部
1 缺少必須的選項
13 0 時間戳請求
14 0 時間戳應答
15 0 資訊請求
16 0 資訊應答
17 0 地址掩碼請求
18 0 地址掩碼應答

10.3 ICMP差錯報告報文

注意,有幾種資料包出現錯誤是不會產生對應的差錯報文的:

  1. 攜帶ICMP差錯報文的資料包。
  2. 不是某資料包第一個分片的分片資料包。
  3. 具有多播地址的資料包。
  4. 具有其它特殊目的地址的資料包。(如環回、多播、廣播地址等等)

10.3.1 目的不可達

型別 程式碼 描述 查詢 差錯
3
目的不可達:
0 網路不可達
1 主機不可達
2 協定不可達
3 埠不可達
4 需要進行分片,但設定不了分片位元
5 源站選路失敗
6 目的網路不認識
7 目的主機不認識
8 源主機被隔離(作廢不用)
9 目的網路被強制禁止
10 目的主機被強制禁止
11 由於服務型別TOS,網路不可達
12 由於服務型別TOS,主機不可達
13 由於過濾,通訊被強制禁止
14 主機越權
15 優先權中止生效

當路由器不能給資料包找到合適的路由路徑,或者主機不能將資料包遞交給上層協定時,相應的IP資料包就會被丟棄,然後返回給源主機一個目的站不可達的ICMP差錯控制報文。

ICMP 目的不可達報文首部剩下的 4 位元組全部未用:目的不可達的ICMP報文格式

10.3.2 源站抑制

型別 程式碼 描述 查詢 差錯
4 0 源站抑制

ICMP源站抑制報文格式參考 目的不可達的ICMP報文格式

ICMP源站抑制報文的目的就是告訴源主機,資料降速,這邊快處理不過來了。

源站抑制詳細描述

如果閘道器沒有必要的緩衝區空間,則可以丟棄Internet資料包,以便將資料包排隊輸出到目的地網路路由上的下一個網路。

如果閘道器丟棄一個資料包,它可以向該資料包的Internet源主機傳送一個源站抑制訊息。

如果資料包到達得太快而無法處理,目標主機也可以傳送源抑制訊息。

源站抑制訊息是向主機發出的請求,要求它降低向Internet目的地傳送通訊的速度。

閘道器可以為它丟棄的每一個訊息傳送一個源站抑制訊息。

在接收到源站抑制訊息時,源主機應該降低它向指定目的地傳送通訊的速率,直到它沒有收到源站抑制訊息為止。

然後,源主機可以逐漸增加它向目標傳送流量的速度,直到它再次接收到源站抑制訊息為止。

閘道器或主機可以在接近其容量限制時傳送源站抑制訊息,而不是等待直到容量超過再發。

因為這樣可以觸發源站抑制的資料包可以被交付。

程式碼0可以從閘道器或主機接收。

10.3.3 重定向(改變路由)

型別 程式碼 描述 查詢 差錯
5
重定向:
0 對網路重定向
1 對主機重定向
2 對服務型別和網路重定向
3 對服務型別和主機重定向

封包格式:

在以下情況下,閘道器會向主機傳送重定向訊息。

閘道器Gl從閘道器所連線的網路上的主機接收網際網路資料包,然後檢查自己的路由表,得到資料包internet目的網路X的路由上的下一個閘道器G2的地址。

如果G2與資料包internet源地址標識的主機在同一網路上,則向該主機傳送重定向訊息。

重定向訊息建議主機將其網路X的流量直接傳送到閘道器G2,因為這是到達目的地的較短路徑。

閘道器將原始資料包的資料轉發到其internet目的地。

對於帶有IP源路由選項和目的位址列位中閘道器地址的資料包,即使到達最終目的地的路由比源路由中的下一個地址有更好的路由,也不傳送重定向訊息。

可以從閘道器接收編碼0、1、2和3。

10.3.4 超時

型別 程式碼 描述 查詢 差錯
11
超時:
0 傳輸期間TTL為0
1 分片資料包重灌超時

10.3.5 引數錯誤

型別 程式碼 描述 查詢 差錯
12
引數問題:
0 IP首部異常
1 缺少必須的選項

IP 資料包在網路中傳輸的時候,都是根據其首部進行識別的,如果首部出現錯誤,那麼就會產生嚴重的問題,因此如果 IP 資料包首部出現錯誤就會丟棄資料包,並且向源主機返回一個 ICMP引數錯誤報文。

10.4 ICMP查詢報文

常見的ICMP查詢報文:

  1. 回顯請求或回答。
  2. 路由器查詢和通告。
  3. 時間戳請求或回答。
  4. 資訊請求或回答。
  5. 地址掩碼請求或回答。

10.4.1 回顯

這是當前lwip唯一實現的ICMP查詢報文。

型別 程式碼 描述 查詢 差錯
0 0 回顯應答(如ping應答)
8 0 請求回顯(如ping請求)

報文格式:

識別符號和序列號沒有具體範圍要求,符合位元組限制即可。傳送方可以自由使用這兩個欄位。

如該識別符號可能被用作TCP或UDP中的埠來標識對談,而序列號可能會在每次傳送回顯請求時遞增。

資料欄位可選,但是在請求回顯訊息中接收到的資料必須在回顯應答訊息中返回。(即是同一對回顯的資料必須一致)

10.5 ICMP資料結構

lwip支援的icmp資料結構在icmp.h檔案中。

10.5.1 ICMP資料包資料結構

資料結構:

/** This is the standard ICMP header only that the u32_t data
 *  is split to two u16_t like ICMP echo needs it.
 *  This header is also used for other ICMP types that do not
 *  use the data part.
 */
PACK_STRUCT_BEGIN
struct icmp_echo_hdr {
  PACK_STRUCT_FLD_8(u8_t type);    // 型別欄位
  PACK_STRUCT_FLD_8(u8_t code);    // 程式碼欄位
  PACK_STRUCT_FIELD(u16_t chksum); // 校驗和欄位
  PACK_STRUCT_FIELD(u16_t id);     // 識別符號欄位
  PACK_STRUCT_FIELD(u16_t seqno);  // 序號欄位
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END

操作:

#define ICMPH_TYPE(hdr) ((hdr)->type) // 讀取型別欄位
#define ICMPH_CODE(hdr) ((hdr)->code) // 讀取程式碼欄位
#define ICMPH_TYPE_SET(hdr, t) ((hdr)->type = (t)) // 設定型別欄位
#define ICMPH_CODE_SET(hdr, c) ((hdr)->code = (c)) // 設定程式碼欄位

10.5.2 ICMP型別欄位

#define ICMP_ER   0    /* 回顯回答 */
#define ICMP_DUR  3    /* 目的不可達 */
#define ICMP_SQ   4    /* 源站抑制 */
#define ICMP_RD   5    /* 重定向 */
#define ICMP_ECHO 8    /* 回顯請求 */
#define ICMP_TE  11    /* 資料包超時 */
#define ICMP_PP  12    /* 資料包引數錯誤 */
#define ICMP_TS  13    /* 時間戳請求 */
#define ICMP_TSR 14    /* 時間戳回答 */
#define ICMP_IRQ 15    /* 資訊請求 */
#define ICMP_IR  16    /* 資訊回答 */
#define ICMP_AM  17    /* 地址掩碼請求 */
#define ICMP_AMR 18    /* 地址掩碼回答 */

10.5.3 目的不可達程式碼欄位

/** ICMP目的不可達程式碼欄位 */
enum icmp_dur_type {
  /** 網路不可達 */
  ICMP_DUR_NET   = 0,
  /** 主機不可達 */
  ICMP_DUR_HOST  = 1,
  /** 協定不可達 */
  ICMP_DUR_PROTO = 2,
  /** 埠不可達 */
  ICMP_DUR_PORT  = 3,
  /** 需要分片但不分片置位 */
  ICMP_DUR_FRAG  = 4,
  /** 源路由失敗 */
  ICMP_DUR_SR    = 5
};

10.5.4 超時程式碼欄位

/** ICMP超時程式碼欄位 */
enum icmp_te_type {
  /** 生存時間超時值TTL為0 */
  ICMP_TE_TTL  = 0,
  /** 分片資料包重灌超時 */
  ICMP_TE_FRAG = 1
};

10.6 傳送ICMP差錯報告

10.6.1 傳送ICMP差錯報文基函數

icmp_send_response()函數就是傳送ICMP差錯報文的基函數,可以封裝這個函數實現各種ICMP差錯報文。

該函數主要邏輯:

  • 檢查觸發ICMP差錯報告的IP報文pbuf中的資料size是否符合要求。

    • 目的不可達的ICMP差錯報文一般需要IP首部+8位元組資料。
  • 申請ICMP差錯報文pbuf資源。

  • 組裝ICMP資料包。

  • 路由匹配網路卡。

  • 通過IP層輸出函數傳送ICMP報文。

/**
 * Send an icmp packet in response to an incoming packet.
 *
 * @param p the input packet for which the 'unreachable' should be sent,
 *          p->payload pointing to the IP header
 * @param type Type of the ICMP header
 * @param code Code of the ICMP header
 */
static void
icmp_send_response(struct pbuf *p, u8_t type, u8_t code)
{
  struct pbuf *q; /* ICMP差錯報文pbuf指標 */
  struct ip_hdr *iphdr; /* 觸發ICMP差錯報告的IP頭指標 */
  struct icmp_echo_hdr *icmphdr; /* ICMP報文首部指標 */
  ip4_addr_t iphdr_src; /* 觸發ICMP差錯報告的IP源地址 */
  struct netif *netif; /* 傳送ICMP差錯報告的網路卡 */
  u16_t response_pkt_len; /* ICMP資料區長度 */

  /* 增加試圖傳送的訊息數 */
  MIB2_STATS_INC(mib2.icmpoutmsgs);

  /* IP頭+8 */
  response_pkt_len = IP_HLEN + ICMP_DEST_UNREACH_DATASIZE;
  if (p->tot_len < response_pkt_len) { /* 如果觸發ICMP差錯報告的IP報文pbuf資料區不夠8,就全部上傳即可。 */
    response_pkt_len = p->tot_len;
  }

  /* 申請ICMP差錯報告的pbuf資源,長度為ICMP差錯報告首部+資料區*/
  q = pbuf_alloc(PBUF_IP, sizeof(struct icmp_echo_hdr) + response_pkt_len, PBUF_RAM);
  if (q == NULL) {
    LWIP_DEBUGF(ICMP_DEBUG, ("icmp_time_exceeded: failed to allocate pbuf for ICMP packet.\n"));
    MIB2_STATS_INC(mib2.icmpouterrors);
    return;
  }
  LWIP_ASSERT("check that first pbuf can hold icmp message",
              (q->len >= (sizeof(struct icmp_echo_hdr) + response_pkt_len)));

  iphdr = (struct ip_hdr *)p->payload; /* 提取IP報文首部 */
  LWIP_DEBUGF(ICMP_DEBUG, ("icmp_time_exceeded from "));
  ip4_addr_debug_print_val(ICMP_DEBUG, iphdr->src);
  LWIP_DEBUGF(ICMP_DEBUG, (" to "));
  ip4_addr_debug_print_val(ICMP_DEBUG, iphdr->dest);
  LWIP_DEBUGF(ICMP_DEBUG, ("\n"));

  icmphdr = (struct icmp_echo_hdr *)q->payload;
  icmphdr->type = type; /* 設定ICMP型別欄位 */
  icmphdr->code = code; /* 設定ICMP程式碼欄位 */
  icmphdr->id = 0; /* 設定ICMP標識欄位 */
  icmphdr->seqno = 0; /* 設定ICMP序號欄位 */

  /* 拷貝觸發ICMP差錯的IP報文首部+(<=8)位元組的原資料到ICMP差錯報告資料區 */
  SMEMCPY((u8_t *)q->payload + sizeof(struct icmp_echo_hdr), (u8_t *)p->payload,
          response_pkt_len);

  ip4_addr_copy(iphdr_src, iphdr->src); /* 提取IP源地址 */
#ifdef LWIP_HOOK_IP4_ROUTE_SRC
  {
    ip4_addr_t iphdr_dst;
    ip4_addr_copy(iphdr_dst, iphdr->dest); /* 提取IP目的地址 */
    netif = ip4_route_src(&iphdr_dst, &iphdr_src); /* 路由匹配網路卡 */
  }
#else
  netif = ip4_route(&iphdr_src); /* 路由匹配網路卡 */
#endif
  if (netif != NULL) { /* 匹配網路卡成功 */
    icmphdr->chksum = 0; /* ICMP校驗和欄位 */
#if CHECKSUM_GEN_ICMP
    IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_ICMP) {
      icmphdr->chksum = inet_chksum(icmphdr, q->len); /* 計算ICMP校驗和 */
    }
#endif
    ICMP_STATS_INC(icmp.xmit);
    /* 通過指定網路卡把ICMP報文轉交到IP層傳送出去 */
    ip4_output_if(q, NULL, &iphdr_src, ICMP_TTL, 0, IP_PROTO_ICMP, netif);
  }
  /* 釋放ICMP報文資源 */
  pbuf_free(q);
}

10.6.2 icmp_dest_unreach()目的不可達差錯報告

icmp_dest_unreach()

  • struct pbuf *p:目的不可達的pbuf包。
  • enum icmp_dur_type t:目的不可達的原因。
/**
 * Send an icmp 'destination unreachable' packet, called from ip_input() if
 * the transport layer protocol is unknown and from udp_input() if the local
 * port is not bound.
 *
 * @param p the input packet for which the 'unreachable' should be sent,
 *          p->payload pointing to the IP header
 * @param t type of the 'unreachable' packet
 */
void
icmp_dest_unreach(struct pbuf *p, enum icmp_dur_type t)
{
  MIB2_STATS_INC(mib2.icmpoutdestunreachs);
  icmp_send_response(p, ICMP_DUR, t);
}

10.6.3 icmp_time_exceeded()超時差錯報告

icmp_time_exceeded()

  • struct pbuf *p:目的不可達的pbuf包。
  • enum icmp_te_type t:超時原因。
/**
 * Send a 'time exceeded' packet, called from ip_forward() if TTL is 0.
 *
 * @param p the input packet for which the 'time exceeded' should be sent,
 *          p->payload pointing to the IP header
 * @param t type of the 'time exceeded' packet
 */
void
icmp_time_exceeded(struct pbuf *p, enum icmp_te_type t)
{
  MIB2_STATS_INC(mib2.icmpouttimeexcds);
  icmp_send_response(p, ICMP_TE, t);
}

10.7 接收ICMP報文處理

IP層接收到ICMP報文,會呼叫icmp_input函數處理。

目前LWIP只支援ICMP回顯請求的報文處理,其它ICMP報文直接丟棄。

而對於ICMP回顯請求,LWIP組裝回顯回答報文即可。

icmp_input()

  • 獲取IP報文首部。判斷是否有效:首部長度

  • 判斷pbuf是否有效:ICMP報文長度不能少於ICMP首部長度4Byte。

  • 提取ICMP報文型別欄位:

    • 只處理回顯請求報文,組裝回顯回答ICMP差錯控制報文,用於ping。

      • 開啟了PING功能才支援多播和廣播地址的回顯請求。
    • 其它ICMP報文,只記錄,不處理。

  • LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN表示是否檢查IP報文對應的pbuf包是否有足夠的空間擴充首部。

    • 如果夠空間,那就利用這個pbuf的空間資源作為回顯回答ICMP報文。
    • 如果不夠空間,那就重新申請一個pbuf。
  • 組裝回顯回答ICMP報文。

  • 呼叫ip4_output_if()通過IP層傳送出去。

/**
 * Processes ICMP input packets, called from ip_input().
 *
 * Currently only processes icmp echo requests and sends
 * out the echo response.
 *
 * @param p the icmp echo request packet, p->payload pointing to the icmp header
 * @param inp the netif on which this packet was received
 */
void
icmp_input(struct pbuf *p, struct netif *inp)
{
  u8_t type;
#ifdef LWIP_DEBUG
  u8_t code;
#endif /* LWIP_DEBUG */
  struct icmp_echo_hdr *iecho;
  const struct ip_hdr *iphdr_in;
  u16_t hlen;
  const ip4_addr_t *src;

  ICMP_STATS_INC(icmp.recv);
  MIB2_STATS_INC(mib2.icmpinmsgs);

  iphdr_in = ip4_current_header(); /* 獲取IP報文首部 */
  hlen = IPH_HL_BYTES(iphdr_in); /* 獲取IP報文首部長度 */
  if (hlen < IP_HLEN) { /* IP報文首部長度異常,丟棄 */
    LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: short IP header (%"S16_F" bytes) received\n", hlen));
    goto lenerr;
  }
  if (p->len < sizeof(u16_t) * 2) { /* ICMP報文長度異常,丟棄 */
    LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: short ICMP (%"U16_F" bytes) received\n", p->tot_len));
    goto lenerr;
  }

  type = *((u8_t *)p->payload); /* 獲取ICMP型別欄位 */
#ifdef LWIP_DEBUG
  code = *(((u8_t *)p->payload) + 1); /* 獲取ICMP程式碼欄位 */
  /* if debug is enabled but debug statement below is somehow disabled: */
  LWIP_UNUSED_ARG(code);
#endif /* LWIP_DEBUG */
  switch (type) {
    case ICMP_ER:
      /* This is OK, echo reply might have been parsed by a raw PCB
         (as obviously, an echo request has been sent, too). */
      MIB2_STATS_INC(mib2.icmpinechoreps);
      break;
    case ICMP_ECHO: /* 只處理ICMP差錯控制中的回顯請求 */
      MIB2_STATS_INC(mib2.icmpinechos);
      src = ip4_current_dest_addr(); /* 獲取IP報文的目的地址作為ICMP回顯回答的原IP地址 */

      if (ip4_addr_ismulticast(ip4_current_dest_addr())) { /* 回顯請求的買的地址為多播地址 */
#if LWIP_MULTICAST_PING
        /* 對於組播,使用接收介面的地址作為源地址 */
        src = netif_ip4_addr(inp);
#else /* LWIP_MULTICAST_PING */
        LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: Not echoing to multicast pings\n"));
        goto icmperr;
#endif /* LWIP_MULTICAST_PING */
      }

      if (ip4_addr_isbroadcast(ip4_current_dest_addr(), ip_current_netif())) { /* 回顯請求的買的地址為多播地址 */
#if LWIP_BROADCAST_PING
        /* 對於廣播,使用接收介面的地址作為源地址 */
        src = netif_ip4_addr(inp);
#else /* LWIP_BROADCAST_PING */
        LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: Not echoing to broadcast pings\n"));
        goto icmperr;
#endif /* LWIP_BROADCAST_PING */
      }
      LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: ping\n"));
      if (p->tot_len < sizeof(struct icmp_echo_hdr)) { /* 長度校驗 */
        LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: bad ICMP echo received\n"));
        goto lenerr;
      }
#if CHECKSUM_CHECK_ICMP
      IF__NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_CHECK_ICMP) {
        if (inet_chksum_pbuf(p) != 0) { /* ICMP校驗和校驗失敗 */
          LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: checksum failed for received ICMP echo\n"));
          pbuf_free(p);
          ICMP_STATS_INC(icmp.chkerr);
          MIB2_STATS_INC(mib2.icmpinerrors);
          return;
        }
      }
#endif
#if LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN /* 支援檢查pbuf複用 */
      if (pbuf_add_header(p, hlen + PBUF_LINK_HLEN + PBUF_LINK_ENCAPSULATION_HLEN)) { /* IP報文的pbuf長度不夠,不能複用 */
        /* 長度不夠就重新申請pbuf */
        struct pbuf *r;
        u16_t alloc_len = (u16_t)(p->tot_len + hlen); /* 新pbuf的長度:IP資料區長度+IP首部長度 */
        if (alloc_len < p->tot_len) { /* 溢位,丟棄 */
          LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: allocating new pbuf failed (tot_len overflow)\n"));
          goto icmperr;
        }
        /* 為鏈路頭分配空間的新封包緩衝區 */
        r = pbuf_alloc(PBUF_LINK, alloc_len, PBUF_RAM);
        if (r == NULL) {
          LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: allocating new pbuf failed\n"));
          goto icmperr;
        }
        if (r->len < hlen + sizeof(struct icmp_echo_hdr)) { /* 檢查空間是否足夠 */
          LWIP_DEBUGF(ICMP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("first pbuf cannot hold the ICMP header"));
          pbuf_free(r);
          goto icmperr;
        }
        /* 複製IP頭 */
        MEMCPY(r->payload, iphdr_in, hlen);
        /* 有效負載移到IP資料區 */
        if (pbuf_remove_header(r, hlen)) {
          LWIP_ASSERT("icmp_input: moving r->payload to icmp header failed\n", 0);
          pbuf_free(r);
          goto icmperr;
        }
        /* 拷貝原IP報文資料區到新的pbuf */
        if (pbuf_copy(r, p) != ERR_OK) {
          LWIP_DEBUGF(ICMP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("icmp_input: copying to new pbuf failed"));
          pbuf_free(r);
          goto icmperr;
        }
        /* 釋放就pbuf */
        pbuf_free(p);
        /* 現在,我們有了一個相同的p副本,其中有鏈路頭的空間 */
        p = r;
      } else { /* 如果空間足夠,就把有效負載恢復到IP資料區。因為這裡只需要檢查並報文pbuf有足夠空間即可 */
        /* restore p->payload to point to icmp header (cannot fail) */
        if (pbuf_remove_header(p, hlen + PBUF_LINK_HLEN + PBUF_LINK_ENCAPSULATION_HLEN)) {
          LWIP_ASSERT("icmp_input: restoring original p->payload failed\n", 0);
          goto icmperr;
        }
      }
#endif /* LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN */
      /* 到這裡,所有的檢查都是OK的 */
      /* 通過切換dest和src的ip地址,將icmp型別設定為ECHO_RESPONSE並更新校驗和來生成回顯回答的ICMP差錯控制報文 */
      iecho = (struct icmp_echo_hdr *)p->payload; /* 獲取ICMP報文地址 */
      /* 擴充套件報文首部,成為IP報文 */ 
      if (pbuf_add_header(p, hlen)) {
        LWIP_DEBUGF(ICMP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("Can't move over header in packet"));
      } else {
        err_t ret;
        struct ip_hdr *iphdr = (struct ip_hdr *)p->payload;
        ip4_addr_copy(iphdr->src, *src); /* 設定IP報文的原IP地址 */
        ip4_addr_copy(iphdr->dest, *ip4_current_src_addr());  /* 設定IP報文的目的IP地址 */
        ICMPH_TYPE_SET(iecho, ICMP_ER); /* 設定ICMP報文為回顯回答型別 */
#if CHECKSUM_GEN_ICMP
        IF__NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_ICMP) { /* 設定ICMP校驗和 */
          /* adjust the checksum */
          if (iecho->chksum > PP_HTONS(0xffffU - (ICMP_ECHO << 8))) {
            iecho->chksum = (u16_t)(iecho->chksum + PP_HTONS((u16_t)(ICMP_ECHO << 8)) + 1);
          } else {
            iecho->chksum = (u16_t)(iecho->chksum + PP_HTONS(ICMP_ECHO << 8));
          }
        }
#if LWIP_CHECKSUM_CTRL_PER_NETIF
        else {
          iecho->chksum = 0;
        }
#endif /* LWIP_CHECKSUM_CTRL_PER_NETIF */
#else /* CHECKSUM_GEN_ICMP */
        iecho->chksum = 0;
#endif /* CHECKSUM_GEN_ICMP */

        /* S設定正確的TTL並重新計算頭部校驗和 */
        IPH_TTL_SET(iphdr, ICMP_TTL);
        IPH_CHKSUM_SET(iphdr, 0);
#if CHECKSUM_GEN_IP
        IF__NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_IP) {
          IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, hlen)); /* 設定IP首部校驗和 */
        }
#endif /* CHECKSUM_GEN_IP */

        ICMP_STATS_INC(icmp.xmit);
        /* increase number of messages attempted to send */
        MIB2_STATS_INC(mib2.icmpoutmsgs);
        /* increase number of echo replies attempted to send */
        MIB2_STATS_INC(mib2.icmpoutechoreps);

        /* 傳送ICMP報文 */
        ret = ip4_output_if(p, src, LWIP_IP_HDRINCL,
                            ICMP_TTL, 0, IP_PROTO_ICMP, inp);
        if (ret != ERR_OK) {
          LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: ip_output_if returned an error: %s\n", lwip_strerr(ret)));
        }
      }
      break;
    default:
      if (type == ICMP_DUR) {
        MIB2_STATS_INC(mib2.icmpindestunreachs);
      } else if (type == ICMP_TE) {
        MIB2_STATS_INC(mib2.icmpintimeexcds);
      } else if (type == ICMP_PP) {
        MIB2_STATS_INC(mib2.icmpinparmprobs);
      } else if (type == ICMP_SQ) {
        MIB2_STATS_INC(mib2.icmpinsrcquenchs);
      } else if (type == ICMP_RD) {
        MIB2_STATS_INC(mib2.icmpinredirects);
      } else if (type == ICMP_TS) {
        MIB2_STATS_INC(mib2.icmpintimestamps);
      } else if (type == ICMP_TSR) {
        MIB2_STATS_INC(mib2.icmpintimestampreps);
      } else if (type == ICMP_AM) {
        MIB2_STATS_INC(mib2.icmpinaddrmasks);
      } else if (type == ICMP_AMR) {
        MIB2_STATS_INC(mib2.icmpinaddrmaskreps);
      }
      LWIP_DEBUGF(ICMP_DEBUG, ("icmp_input: ICMP type %"S16_F" code %"S16_F" not supported.\n",
                               (s16_t)type, (s16_t)code));
      ICMP_STATS_INC(icmp.proterr);
      ICMP_STATS_INC(icmp.drop);
  }
  pbuf_free(p);
  return;
lenerr:
  pbuf_free(p);
  ICMP_STATS_INC(icmp.lenerr);
  MIB2_STATS_INC(mib2.icmpinerrors);
  return;
#if LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN || !LWIP_MULTICAST_PING || !LWIP_BROADCAST_PING
icmperr:
  pbuf_free(p);
  ICMP_STATS_INC(icmp.err);
  MIB2_STATS_INC(mib2.icmpinerrors);
  return;
#endif /* LWIP_ICMP_ECHO_CHECK_INPUT_PBUF_LEN || !LWIP_MULTICAST_PING || !LWIP_BROADCAST_PING */
}