【lwip】10-ICMP協議&源碼分析

前言

參考:RFC 792

原文:李柱明博客://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 */
}