【lwip】09-IPv4協定&超全原始碼實現分析

2022-11-05 15:00:43

目錄

前言

預設主講ipv4。

概念性的內容簡單過一遍即可,主要還是在原始碼實現方面。

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

9.1 IP協定簡述

IP 協定(Internet Protocol),又稱之為網際協定,IP 協定處於 IP 層工作,它是整個 TCP/IP 協定棧的核心協定,上層協定都要依賴 IP 協定提供的服務,IP 協定負責將資料包從源主機傳送到目標主機,通過 IP 地址作為唯一識別碼。

IP 協定是一種無連線的不可靠資料包交付協定,協定本身不提供任何的錯誤檢查與恢復機制。

IP地址是協定地址,MAC地址是硬體地址,所處的層級不一樣。

常見的廣域網路由器就工作在IP層。

在本次筆記lwip原始碼實現分析內容概述:

  • IP地址及其分類、特殊IP地址;
  • 子網劃分、子網掩碼、NAT等概念;
  • IP層封包結構一級資料包輸入處理;
  • IP層封包的傳送及分片操作;
  • IP層分片資料重灌過程。

9.2 IP地址分類

A類地址:1.0.0.1—126.155.255.254

B類地址:128.0.0.1—191.255.255.254

C類地址:192.0.0.1—223.255.255.254

D類地址:224.0.0.1—239.255.255.254

E類地址:240.0.0.1—255.255.255.254

具體更加細緻的地址作用,自行百度。

9.2.1 私有地址

A類的:10.x.x.x 是私有地址。

B類的:172.16.0.0—172.31.255.255 是私有地址。

C類的:全都是。192.0.0.1—223.255.255.254 是私有地址。

9.2.2 受限廣播地址

受限廣播地址是網路號與主機號都為 1 的地址:255.255.255.255

為了避免這個廣播地址往整個網際網路里傳送廣播包,在任何情況下,路由器都會禁止轉發目的地址為 255.255.255.255 的廣播封包,要不然會給整個網際網路帶來網路性災難。

9.2.3 直接廣播地址

直接廣播地址是主機號全為 1 而得到的地址,廣播地址代表本網路內的所有主機,使用該地址可以向網路內的所有主機傳送資料。

A 類地址的廣播地址為:XXX.255.255.255

B 類地址的廣播地址為:XXX.XXX.255.255

C 類地址的廣播地址為:XXX.XXX.XXX.255

9.2.4 多播地址

多播地址屬於分類編址中的 D 類地址,D 類地址只能用作目的地址,而不能作為主機中的源地址。

多播地址用在一對多的通訊。

9.2.5 環回地址

127.x.x.x 是保留地址,用作迴圈測試用(127.0.0.1 為保留地址,一般用於環回地址)

9.2.6 本地鏈路地址

169.254.x.x 是本地鏈路地址。

AUTOIP協定使用。

即是如果 IP 地址是自動獲取 IP 地址,而在網路上又沒有找到可用的 DHCP 伺服器,就會得到其中一個 IP。

9.2.7 本網路本主機地址

IP 地址 32bit 全為 0 的地址(0.0.0.0)表示的是本網路本主機,只能做源IP地址。

在當裝置啟動時但又不知道自己的 IP 地址情況下常見。

9.2.8 子網

子網掩碼 & 判斷是否在同一子網
IP 與 子網掩碼 做 按位元與 ,就可以得出該 IP 的子網網段。
如:

  • 子網掩碼:255.255.255.0
  • IP-1: 192.168.1.2 & 255.255.255.0 = 192.168.1.0
  • IP-2: 192.168.1.123 & 255.255.255.0 = 192.168.1.0
  • IP-3: 192.168.2.123 & 255.255.255.0 = 192.168.2.0
  • 因為 192.168.1.0 = 192.168.1.0,所以IP-1與IP-2處於同一子網。
  • 因為 192.168.1.0 != 192.168.2.0,所以IP-1與IP-3不在同一子網。

在發封包時,子網的作用

  • 若源IP和目標IP在同一子網:直接獲取目標IP主機的MAC,然後把封包丟出去。
  • 若源IP和目標IP不在同一子網:獲取預設閘道器的 MAC ,然後把封包丟給預設閘道器那邊。

9.2.9 NAT 概念

在計算機網路中,網路地址轉換(Network Address Translation,縮寫為 NAT),也叫做網路掩蔽或者 IP 掩蔽(IP masquerading)。

當前只需要知道NAT就是網路地址轉換的作用即可,詳細技術細節自行百度。

9.3 IP資料包

9.3.1 版本號欄位

佔用4 bit。

這個欄位規定了資料包的 IP 協定版本。

如:

  • IPv4:值為4。
  • IPv6:值為6。

9.3.2 首部長度欄位

佔用4 bit。

用於記錄 IP 首部的資料的長度。

單位,字。最大可以表示15*4=60位元組。

IP首部的長度預設是20 byte,但是如果有選項欄位,就不止20 byte了。

9.3.3 服務型別(TOS:type of service)欄位

佔用8 bit。

該欄位用於描述當前IP資料包急需的服務型別,如:

  • 最小延時;
  • 最大吞吐量;
  • 最高可靠性;
  • 最小費用等。

路由器在轉發資料包時,可以根據該欄位的值來為資料包選擇最合理的路由路徑。

9.3.4 總長度欄位

佔用16 bit。

是 IP 資料包的總長度(IP首部+資料區)。

單位,位元組。最大能表示65535位元組。

資料包很少有超過1500位元組的,因為乙太網資料框的資料最大長度為1500位元組。

如果一個IP資料包過大時,需要進行分片處理。

9.3.5 標識欄位

標識欄位、標誌欄位和13位偏移欄位常在IP資料包分片時使用。

佔用16 bit。

用於標識IP層發出去的每一份IP資料包,每傳送一份,該值+1。

如果IP資料包被分片,該欄位在每個分片的IP資料包上是一致的,表示屬於同一個IP資料包。

在接收端會根據該欄位識別同一個IP資料包進行重灌。

9.3.6 標誌欄位

佔用3 bit。

第一個bit保留未用。

第二個bit是不分片標誌位

  • 0:則表示 IP 層在必要的時候可以對其進行分片處理。
  • 1:則表示 IP 資料包在傳送的過程中不允許進行分片,如果這個 IP 資料包的大小超過鏈路層能承載的大小,這個 IP 資料包將被丟棄。

第三個bit是更多分片標誌位

  • 0:後續沒有更多分片。即是當前分片的IP資料包是最後一個分片。
  • 1:表示後續還有分片。即是當前分片的IP資料包不是最後一個分片。

9.3.7 分片偏移量欄位

佔用13 bit。

表示當前分片所攜帶的資料在整個 IP 資料包中的相對偏移位置。

單位,8位元組(2個字)。

目標主機要接收到從0分片偏移量到最高分片偏移量的所有分片才能進行組裝出完整的IP資料包。

9.3.8 生存時間(Time-To-Live,TTL)欄位

佔用8 bit。

該欄位用來確保資料包不會永遠在網路中迴圈。

IP資料包沒經過一臺路由器處理,該值-1。

如果TTL欄位下降到0,則路由器會丟棄該資料包,且會返回一個 ICMP 差錯報文給源主機。

9.3.9 協定欄位

佔用8 bit。

表示上層協定型別。即是表示資料區的資料是哪個協定的資料包。如:

  • 6:表示TCP協定。
  • 17:表示UDP協定。
  • 其它值可以自行度娘。

9.3.10 首部校驗和欄位

佔用16 bit。

只針對IP首部做校驗,並不關係資料區在傳輸過程中是否出錯,所以對於資料區的校驗需要由上層協定負責。

路由器要對每個收到的 IP 資料包計算其首部檢驗和,如果資料包首部中攜帶的檢驗和與計算得到的檢驗和不一致,則表示出現錯誤,路由器一般會丟棄檢測出錯誤的 IP 資料包。

需要注意的是,由於IP資料包首部的TTL欄位每結果應該路由器都會-1,所以IP資料包首部檢驗和欄位每經過一個路由器都要重新計算賦值。

參考:關於wireshark的Header checksum出問題解決方案:https://www.it610.com/article/1290714377560858624.htm

檢驗和計算可能由網路網路驅動,協定驅動,甚至是硬體完成。

高層校驗通常是由協定執行,並將完成後的包轉交給硬體。

比較新的網路硬體可以執行一些高階功能,如IP檢驗和計算,這被成為checksum offloading。網路驅動不會計算校驗和,只是簡單將校驗和欄位留空或填入無效資訊,交給硬體計算。

傳送資料時首部校驗和計算:二進位制反碼求和。

  • 把IP封包的校驗和欄位置為全0。
  • 將首部中的每 2 個位元組當作一個數,依次求和。
  • 把結果取反碼。
  • 把得到的結果存入校驗和欄位中。

接收資料時,首部校驗和驗證過程:

  • 首部中的每 2 個位元組當作一個數,依次進行求和,包括校驗和欄位。
  • 檢查計算出的校驗和的結果是否全為1(反碼應為16個0)。
  • 如果等於零,說明被整除,校驗和正確。否則,校驗和就是錯誤的,協定棧要拋棄這個封包。

為什麼計算出的校驗和結果全為1?

因為如果校驗依時次求和,不包含校驗和欄位的話,得出的值就是校驗和欄位的反碼。

校驗和的反碼和校驗和求和,當然是全1啦。

9.3.11 二進位制反碼求和

IP/ICMP/IGMP/TCP/UDP等協定的校驗和演演算法都是相同的。

二進位制反碼求和:(求和再反碼,結果一致)

  • 對兩個二進位制數進行加法運算。
  • 加法規則:0+0=0,0+1=1,1+1=10(進位1加到下一bit)。
  • 若最高兩位相加仍然有進位,則在最後的結果中+1即可。
  • 對最終結果取反碼。

相關原始碼參考LwIP\core\inet_chksum.c中的lwip_standard_chksum()

這裡提取版本3分析:

  • 前期先確保4位元組對齊,如果不是4位元組對齊,就補到4位元組對齊。

  • 後面採用32 bit累加。溢位後,在低位+1。

    • 為什麼?:這裡讀者可能會有個疑問,IP封包的校驗和不是要求16 bit求和的嗎?這裡為什麼能用32 bit求和?
    • 答:起始要求是16 bit,但是實際計算時只要大於16 bit即可,因為到最後,可以把高位摺疊加到低位。
    • 例子:按32bit累加,溢位就在低位+1。其實就是兩組兩個(高、低)16 bit對應累加,低16 bit累加的進位給高16 bit里加回1了。而高16 bit累加的進位在底16 bit里加回1了(手動)。這樣,累加到最後剩下32bit。把高16bit和低16bit進行累加,進位再加1即可快速得到16bit的校驗和。
  • 資料後部分可能不是8位元組對齊,所以剩餘的位元組也需要16bit校驗和處理。

思路圖:

由於目的是16 bit的校驗和。其實可以看成兩組2個8bit對應相加,低8bit組進位給高8bit組,高8bit組進位給低8bit組。所以相加值是對應高、低8bit相互獨立的。

而下面函數就是利用這個特性,如果首位元組為奇地址,先單獨取出來放到t的高地址,因為後續的統計位元組順序是返的。等待全部統計完畢後,再把兩個位元組順序調換即可。

如果是偶地址開始,那符合校驗和規則,最後不需要調換位元組順序。

#if (LWIP_CHKSUM_ALGORITHM == 3) /* Alternative version #3 */
/**
 * An optimized checksum routine. Basically, it uses loop-unrolling on
 * the checksum loop, treating the head and tail bytes specially, whereas
 * the inner loop acts on 8 bytes at a time.
 *
 * @arg start of buffer to be checksummed. May be an odd byte address.
 * @len number of bytes in the buffer to be checksummed.
 * @return host order (!) lwip checksum (non-inverted Internet sum)
 *
 * by Curt McDowell, Broadcom Corp. December 8th, 2005
 */
u16_t
lwip_standard_chksum(const void *dataptr, int len)
{
  const u8_t *pb = (const u8_t *)dataptr; /* 取資料的地址 */
  const u16_t *ps;
  u16_t t = 0;
  const u32_t *pl;
  u32_t sum = 0, tmp;

  int odd = ((mem_ptr_t)pb & 1); /* 判斷是否為奇地址 */

  if (odd && len > 0) { /* 如果不是2直接對齊 */
    /* 快取奇地址上的位元組,存於 t 的高位。資料地址偏移為偶,2位元組對齊。 */
    /* 存到高位是為了和後面位元組序保持一致,方便在最後一次性更正。 */
    ((u8_t *)&t)[1] = *pb++;
    len--; /* 位元組數-1 */
  }

  /* 2位元組對齊的資料起始地址 */
  ps = (const u16_t *)(const void *)pb;

  if (((mem_ptr_t)ps & 3) && len > 1) {/* 如果不是4位元組對齊 */
    /* 把多出來的兩位元組儲存到sum */
    sum += *ps++;
    len -= 2;
  }

  /* 4位元組對齊的資料起始地址 */
  pl = (const u32_t *)(const void *)ps;

  while (len > 7)  {
    tmp = sum + *pl++;          /* ping */
    if (tmp < sum) {
      tmp++;                    /* 溢位,手動+1 */
    }

    sum = tmp + *pl++;          /* pong */
    if (sum < tmp) {
      sum++;                    /* 溢位,手動+1 */
    }

    len -= 8;
  }

  /* 摺疊高、低16bit */
  sum = FOLD_U32T(sum);

  /* 處理剩餘的位元組 */
  ps = (const u16_t *)pl;

  /* 2位元組處理 */
  while (len > 1) {
    sum += *ps++;
    len -= 2;
  }

  /* 剩餘單位元組 */
  if (len > 0) {                /* 補到前面t的低位 */
    ((u8_t *)&t)[0] = *(const u8_t *)ps;
  }

  sum += t;                     /* 把t也一起16bit校驗和 */

  /* 兩次摺疊高、低16bit */
  sum = FOLD_U32T(sum);
  sum = FOLD_U32T(sum);

  if (odd) { /* 因為前面是從第二個位元組和第三個位元組開始進行統計的,位元組序反了,這裡在結果更正。 */
    sum = SWAP_BYTES_IN_WORD(sum);
  }

  return (u16_t)sum; /* 返回校驗和 */
}
#endif

9.3.12 源IP欄位

佔用32 bit。

為源主機的IP地址。

9.3.13 目標IP欄位

佔用32 bit。

為目標主機的IP地址。

9.3.14 選項欄位

0到40位元組。

對於IP資料包首部來說,其大小必須為4位元組的整數倍。

如果選項欄位長度不為4的倍數,則需要用0進行填充。

在 LwIP 中只識別選項欄位,不會處理選項欄位的內容。

該欄位在IPv6報文中已經被移除了。

9.3.15 資料區欄位

IP 資料包的最後的一個欄位,裝載著當前IP資料包的資料,是上層協定的資料包。

9.3.16 對應wireshark包分析

9.4 IP首部資料結構

注意:網路位元組序是大端的。

ipv4的IP首部資料結構:對應IP首部報文圖。

/* The IPv4 header */
struct ip_hdr {
  /* version / header length */
  PACK_STRUCT_FLD_8(u8_t _v_hl); /* 版本號欄位(4)+首部長度欄位(4)單位字 */
  /* type of service */
  PACK_STRUCT_FLD_8(u8_t _tos); /* 服務型別TOS欄位(8) */
  /* total length */
  PACK_STRUCT_FIELD(u16_t _len); /* 總長度欄位(16)單位位元組 */
  /* identification */
  PACK_STRUCT_FIELD(u16_t _id); /* 標識欄位欄位(16) */
  /* fragment offset field */
  PACK_STRUCT_FIELD(u16_t _offset); /* 標誌欄位(3)+分片偏移量欄位(13)單位兩字 */
#define IP_RF 0x8000U        /* 標誌欄位第一位:欄位保留 */
#define IP_DF 0x4000U        /* 標誌欄位第二位:不發片掩碼 */
#define IP_MF 0x2000U        /* 標誌欄位第三位:更多分片掩碼 */
#define IP_OFFMASK 0x1fffU   /* 分片偏移量欄位的掩碼 */
  /* time to live */
  PACK_STRUCT_FLD_8(u8_t _ttl); /* TTL欄位(8) */
  /* protocol*/
  PACK_STRUCT_FLD_8(u8_t _proto); /* 上層協定型別欄位(8) */
  /* checksum */
  PACK_STRUCT_FIELD(u16_t _chksum); /* 首部校驗和欄位(16) */
  /* source and destination IP addresses */
  PACK_STRUCT_FLD_S(ip4_addr_p_t src); /* 源IP欄位(32) */
  PACK_STRUCT_FLD_S(ip4_addr_p_t dest); /* 目的IP欄位(32) */
} PACK_STRUCT_STRUCT;

由於IP首部部分欄位的操作涉及到bit,所以lwip也封裝出對應的宏操作。

/* 均為網路位元組序 */
/* Macros to get struct ip_hdr fields: */
#define IPH_V(hdr)  ((hdr)->_v_hl >> 4) /* 獲取版本號 */
#define IPH_HL(hdr) ((hdr)->_v_hl & 0x0f) /* 獲取首部長度欄位值 */
#define IPH_HL_BYTES(hdr) ((u8_t)(IPH_HL(hdr) * 4)) /* 獲取首部長度,單位位元組 */
#define IPH_TOS(hdr) ((hdr)->_tos) /* 獲取服務型別TOS */
#define IPH_LEN(hdr) ((hdr)->_len) /* 獲取IP資料包總長度 */
#define IPH_ID(hdr) ((hdr)->_id) /* 獲取標識欄位 */
#define IPH_OFFSET(hdr) ((hdr)->_offset) /* 獲取標誌欄位+分片偏移量欄位 */
#define IPH_OFFSET_BYTES(hdr) ((u16_t)((lwip_ntohs(IPH_OFFSET(hdr)) & IP_OFFMASK) * 8U)) /* 獲取分片偏移量,單位位元組 */
#define IPH_TTL(hdr) ((hdr)->_ttl) /* 獲取TTL */
#define IPH_PROTO(hdr) ((hdr)->_proto) /* 獲取協定型別 */
#define IPH_CHKSUM(hdr) ((hdr)->_chksum) /* 獲取首部校驗和欄位 */

/* Macros to set struct ip_hdr fields: */
#define IPH_VHL_SET(hdr, v, hl) (hdr)->_v_hl = (u8_t)((((v) << 4) | (hl))) /* 設定版本號 */
#define IPH_TOS_SET(hdr, tos) (hdr)->_tos = (tos) /* 設定服務型別TOS */
#define IPH_LEN_SET(hdr, len) (hdr)->_len = (len) /* 設定總長度欄位值 */
#define IPH_ID_SET(hdr, id) (hdr)->_id = (id) /* 設定標識 */
#define IPH_OFFSET_SET(hdr, off) (hdr)->_offset = (off) /* 設定標誌欄位+分片偏移量欄位 */
#define IPH_TTL_SET(hdr, ttl) (hdr)->_ttl = (u8_t)(ttl) /* 設定TTL */
#define IPH_PROTO_SET(hdr, proto) (hdr)->_proto = (u8_t)(proto) /* 設定協定型別 */
#define IPH_CHKSUM_SET(hdr, chksum) (hdr)->_chksum = (chksum) /* 設定首部校驗和 */

9.5 網路卡路由

9.5.1 路由網路卡匹配

從官方原始碼看,匹配網路卡的流程:

ip4_route_src()

  • 先函數LWIP_HOOK_IP4_ROUTE_SRC()匹配。
  • 然後到ip4_route()基函數匹配。

9.5.2 路由網路卡匹配基函數

ip4_route()

  • 多播IP優先匹配多播專用網路卡ip4_default_multicast_netif

  • 遍歷網路卡:

    • 有效網路卡先匹配子網;
    • 子網匹配失敗就匹配閘道器,網路卡不能有廣播能力。
  • 環回IP:如果開啟了各個網路卡的環回功能,且沒有建立環回網路卡:

    • 說明:因為建立了環回網路卡,在遍歷連結串列時,就會把環回IP 127.x.x.x都會匹配到環回網路卡。
    • 對於環回IP,優先匹配預設網路卡netif_default
    • 再遍歷網路卡,第一個協定棧有效的網路卡即可。
  • 勾點匹配:(由使用者實現)

    • LWIP_HOOK_IP4_ROUTE_SRC(src, dest);
    • LWIP_HOOK_IP4_ROUTE(dest);
  • 以上都沒有匹配成功,則使用netif_default,必須條件:

    • 預設網路卡netif_default存在;
    • 預設網路卡協定棧有效;
    • 預設網路卡資料鏈路有效;
    • 預設網路卡IP有效。
    • 匹配的目的IP不能為環回IP(因為如果是環回IP,前面已經匹配過了,除非沒有開啟該功能)
/**
 * Finds the appropriate network interface for a given IP address. It
 * searches the list of network interfaces linearly. A match is found
 * if the masked IP address of the network interface equals the masked
 * IP address given to the function.
 *
 * @param dest the destination IP address for which to find the route
 * @return the netif on which to send to reach dest
 */
struct netif *
ip4_route(const ip4_addr_t *dest)
{
#if !LWIP_SINGLE_NETIF
  struct netif *netif;

  /* 確保在tcpip核心鎖內 */
  LWIP_ASSERT_CORE_LOCKED();

#if LWIP_MULTICAST_TX_OPTIONS /* 開啟了組播TX功能 */
  /* 如果目的IP是一個組播地址,且協定棧內建立了組播專用網路卡,則選用組播專用網路卡介面 */
  if (ip4_addr_ismulticast(dest) && ip4_default_multicast_netif) {
    return ip4_default_multicast_netif;
  }
#endif /* LWIP_MULTICAST_TX_OPTIONS */

  /* bug #54569: in case LWIP_SINGLE_NETIF=1 and LWIP_DEBUGF() disabled, the following loop is optimized away */
  LWIP_UNUSED_ARG(dest);

  /* 遍歷網路卡連結串列 */
  NETIF_FOREACH(netif) {
    /* 網路卡協定棧有效,網路卡鏈路層也有效,網路卡的IP地址不為全0 */
    if (netif_is_up(netif) && netif_is_link_up(netif) && !ip4_addr_isany_val(*netif_ip4_addr(netif))) {
      /* 匹配傳入的目的IP和網路卡是否處於同一個子網 */
      if (ip4_addr_net_eq(dest, netif_ip4_addr(netif), netif_ip4_netmask(netif))) {
        /* 匹配成功。返回netif,以轉發IP封包 */
        return netif;
      }
      /* 當前網路卡子網匹配不成功 */
      /* 那就匹配下閘道器,匹配規則:網路卡沒有廣播功能,目的IP和閘道器IP一致。也算匹配成功,因為上層的目的是到閘道器。當前網路卡能把資料傳達到網路卡。 */
      if (((netif->flags & NETIF_FLAG_BROADCAST) == 0) && ip4_addr_eq(dest, netif_ip4_gw(netif))) {
        /* 匹配成功。返回netif,以轉發IP封包 */
        return netif;
      }
    }
  }

  /* 到這,遍歷網路卡,匹配失敗 */

/* 開啟了環回功能,但是沒有建立環回網路卡。(因為如果建立了環回網路卡,在遍歷網路卡連結串列時就已經處理過了,這裡不需要再處理) */
#if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF
  /* 如果目的IP是一個環回IP地址,則優先返回預設網路卡,否則返回網路卡連結串列中第一個協定棧使能了的網路卡作為當前環回網路卡 */
  if (ip4_addr_isloopback(dest)) { /* 目的IP是一個環回IP */
    if ((netif_default != NULL) && netif_is_up(netif_default)) {
      /* 優先考慮預設網路卡。如果有預設網路卡,且預設網路卡協定棧使能了,則以此網路卡作為本次環回網路卡 */
      return netif_default;
    }
    /* 預設網路卡沒有匹配成功,則從網路卡連結串列中找一個協定棧已使能的網路卡作為本次環回網路卡 */
    NETIF_FOREACH(netif) {
      if (netif_is_up(netif)) {
        return netif;
      }
    }
    return NULL; /* 都沒找到,那就匹配失敗 */
  }
#endif /* LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF */

#ifdef LWIP_HOOK_IP4_ROUTE_SRC /* 如果開啟了匹配網路卡的勾點宏函數 */
  netif = LWIP_HOOK_IP4_ROUTE_SRC(NULL, dest); /* 使用宏勾點匹配 */
  if (netif != NULL) {
    return netif;
  }
#elif defined(LWIP_HOOK_IP4_ROUTE) /* 網路卡匹配勾點 */
  netif = LWIP_HOOK_IP4_ROUTE(dest); /* 使用宏勾點匹配 */
  if (netif != NULL) {
    return netif;
  }
#endif
#endif /* !LWIP_SINGLE_NETIF */

  /* 上述方案都無法匹配到網路卡,就檢查網路卡網路卡是否正常,正常則就返回預設網路卡 */

  if ((netif_default == NULL) || !netif_is_up(netif_default) || !netif_is_link_up(netif_default) ||
      ip4_addr_isany_val(*netif_ip4_addr(netif_default)) || ip4_addr_isloopback(dest)) {
    LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_route: No route to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
                ip4_addr1_16(dest), ip4_addr2_16(dest), ip4_addr3_16(dest), ip4_addr4_16(dest)));
    IP_STATS_INC(ip.rterr);
    MIB2_STATS_INC(mib2.ipoutnoroutes);
    return NULL;
  }

  return netif_default; /* 返回預設網路介面 */
}

9.5.3 路由網路卡匹配支援源IP和目的IP網路卡匹配的介面

匹配網路卡,一般是按照目的IP來匹配,但是可以通過LWIP_HOOK_IP4_ROUTE_SRC()勾點宏函數來實現源IP地址和目的IP地址匹配。

ip4_route_src()

  • 如果源IP地址不為空,則會先傳入LWIP_HOOK_IP4_ROUTE_SRC()勾點函數來匹配網路卡。
  • 勾點函數匹配失敗或者源IP地址為空,則由ip4_route()只根據目的IP地址匹配。
#ifdef LWIP_HOOK_IP4_ROUTE_SRC
/**
 * Source based IPv4 routing must be fully implemented in
 * LWIP_HOOK_IP4_ROUTE_SRC(). This function only provides the parameters.
 */
struct netif *
ip4_route_src(const ip4_addr_t *src, const ip4_addr_t *dest)
{
  if (src != NULL) {
    /* 當src==NULL時,勾點會從ip4_route(dest)呼叫 */
    struct netif *netif = LWIP_HOOK_IP4_ROUTE_SRC(src, dest);
    if (netif != NULL) {
      return netif;
    }
  }
  return ip4_route(dest);
}
#endif /* LWIP_HOOK_IP4_ROUTE_SRC */

9.5.4 路由網路卡匹配的勾點函數

通過分析前面的基函數和介面函數,可發現其實現是支援宏勾點函數,即是支援使用者自己實現網路卡匹配的邏輯的。

有兩個宏勾點:

  • LWIP_HOOK_IP4_ROUTE_SRC(src, dest):勾點入口引數有源IP和目的IP。
  • LWIP_HOOK_IP4_ROUTE(dest):勾點入口引數只有目的IP。

9.5.5 收包網路卡匹配

當IP層收到一個IP報文時,也要收包網路卡匹配。

而且IP包的輸入和輸出的網路卡匹配是不一樣的,比如普通的IP單播包,輸出時,只需要找到目的IP和網路卡處於同一個子網或者是該網路卡的閘道器即可匹配。而輸入時,需要明確目的IP就是該網路卡IP。

收包的網路卡匹配除了ip4_input_accept()這個主要函數外,還有很多獨立的匹配條件,具體看IP層輸入章節。

這裡只分析ip4_input_accept()

  • 在呼叫該API前,應該先設定全域性IP資料結構成員值:struct ip_globals ip_data;

  • 需要被匹配的網路卡必須在協定棧方向使能了,且IP地址為有效地址。

    • 單播包,目的地址和網路卡地址一致,網路卡匹配成功。
    • 廣播包,IP地址bit全1,必定是廣播地址。如果網路卡就被廣播能力,且IP地址的主機號bit全1,也是子網廣播地址。都匹配成功。
    • 環回,沒有環回網路卡,且目的IP地址為環回IP IPADDR_LOOPBACK。匹配成功。
/** Return true if the current input packet should be accepted on this netif */
static int
ip4_input_accept(struct netif *netif)
{
  LWIP_DEBUGF(IP_DEBUG, ("ip_input: iphdr->dest 0x%"X32_F" netif->ip_addr 0x%"X32_F" (0x%"X32_F", 0x%"X32_F", 0x%"X32_F")\n",
                         ip4_addr_get_u32(ip4_current_dest_addr()), ip4_addr_get_u32(netif_ip4_addr(netif)),
                         ip4_addr_get_u32(ip4_current_dest_addr()) & ip4_addr_get_u32(netif_ip4_netmask(netif)),
                         ip4_addr_get_u32(netif_ip4_addr(netif)) & ip4_addr_get_u32(netif_ip4_netmask(netif)),
                         ip4_addr_get_u32(ip4_current_dest_addr()) & ~ip4_addr_get_u32(netif_ip4_netmask(netif))));

  /* 網路卡是否在協定棧中使能且網路卡地址有效? */
  if ((netif_is_up(netif)) && (!ip4_addr_isany_val(*netif_ip4_addr(netif)))) {
    /* 是否是單播到這個介面地址? */
    if (ip4_addr_eq(ip4_current_dest_addr(), netif_ip4_addr(netif)) ||
        /* 或者廣播這個介面的網路地址? */
        ip4_addr_isbroadcast(ip4_current_dest_addr(), netif)
#if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF /* 如果開啟環回功能,但是沒有建立環回網路卡 */
        /* 目的IP是一個環回的IP地址,也就是給當前網路卡的,能匹配成功 */
        || (ip4_addr_get_u32(ip4_current_dest_addr()) == PP_HTONL(IPADDR_LOOPBACK))
#endif /* LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF */
       ) {
      LWIP_DEBUGF(IP_DEBUG, ("ip4_input: packet accepted on interface %c%c\n",
                             netif->name[0], netif->name[1]));
      /* accept on this netif */
      return 1;
    }
#if LWIP_AUTOIP
    /* 更改netif地址後,鏈路本地地址的連線必須保持(RFC3927 ch. 1.9) */
    /* 即是netif網路卡地址更新為可路由地址了,原本本地鏈路地址的連線必須保持,所以封包能得到當前網路卡。匹配成功 */
    if (autoip_accept_packet(netif, ip4_current_dest_addr())) {
      LWIP_DEBUGF(IP_DEBUG, ("ip4_input: LLA packet accepted on interface %c%c\n",
                             netif->name[0], netif->name[1]));
      /* accept on this netif */
      return 1;
    }
#endif /* LWIP_AUTOIP */
  }
  return 0;
}

9.6 IP層資料流圖

9.7 IP層輸出

ipv4。

當上層需要傳送資料時,會先將自己的封包組裝在一個pbuf中。並將payload指標指向對應協定首部。

然後呼叫ip_output()傳送資料,需要給ip_output()函數提供源IP、目的IP、協定型別、TTL等重要資訊讓其組IP包。

該函數直接或者間接呼叫ip4_route_src()根據目的IP選出一個匹配的網路卡作為本次IP封包傳輸網路卡。

選出後呼叫ip4_output_if()進行IP封包組包,並呼叫netif->output()傳送出去。或者呼叫netif_loop_output()環回到本網路卡。

9.7.1 傳送資料包

上層呼叫ip_output()把資料轉交給IP層處理,lwip支援ipv4和ipv6,這裡預設分析ipv4,因為ipv6也是一大塊,後面有時間再完整分析下。

/**
 * @ingroup ip
 * Output IP packet, netif is selected by source address
 */
#define ip_output(p, src, dest, ttl, tos, proto) \
        (IP_IS_V6(dest) ? \
        ip6_output(p, ip_2_ip6(src), ip_2_ip6(dest), ttl, tos, proto) : \
        ip4_output(p, ip_2_ip4(src), ip_2_ip4(dest), ttl, tos, proto))

9.7.2 ip層前期處理:ip4_output()

ipv4發包:

  • 檢查pbuf的參照ref是否為1。為1才能說明當前pbuf沒有被其它地方參照,因為IP層處理可能會改變這個pbuf的部分指標值,如payload。
  • 呼叫ip4_route_src()匹配網路卡。
  • 呼叫ip4_output_if()把封包傳入IP層處理。
/**
 * Simple interface to ip_output_if. It finds the outgoing network
 * interface and calls upon ip_output_if to do the actual work.
 *
 * @param p the packet to send (p->payload points to the data, e.g. next
            protocol header; if dest == LWIP_IP_HDRINCL, p already includes an
            IP header and p->payload points to that IP header)
 * @param src the source IP address to send from (if src == IP4_ADDR_ANY, the
 *         IP  address of the netif used to send is used as source address)
 * @param dest the destination IP address to send the packet to
 * @param ttl the TTL value to be set in the IP header
 * @param tos the TOS value to be set in the IP header
 * @param proto the PROTOCOL to be set in the IP header
 *
 * @return ERR_RTE if no route is found
 *         see ip_output_if() for more return values
 */
err_t
ip4_output(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,
           u8_t ttl, u8_t tos, u8_t proto)
{
  struct netif *netif;
  
  /* 下傳到IP層的pbuf的ref必須為1,即是沒有被其它地方參照,因為pbuf下傳到IP層後,pbuf的payload指標會被更改。
    如果這個pbuf被其它地方參照了,可能會導致資料混亂。 */
  LWIP_IP_CHECK_PBUF_REF_COUNT_FOR_TX(p);

  /* 根據目的IP地址為資料包尋找一個合適的網路介面(匹配網路卡) */
  if ((netif = ip4_route_src(src, dest)) == NULL) { /* 沒找到,記錄資訊,返回錯誤 */
    LWIP_DEBUGF(IP_DEBUG, ("ip4_output: No route to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
                           ip4_addr1_16(dest), ip4_addr2_16(dest), ip4_addr3_16(dest), ip4_addr4_16(dest)));
    IP_STATS_INC(ip.rterr);
    return ERR_RTE;
  }

  /* 匹配到網路卡,傳入IP層處理:組包、傳送 */
  return ip4_output_if(p, src, dest, ttl, tos, proto, netif);
}

9.7.3 發包前的網路卡匹配

IP層收到上層的封包後,需要匹配到網路介面,才能組IP包發出去。

這裡呼叫ip4_route_src()進行網路卡匹配。具體分析參考前面。

9.7.4 組建、傳送IP包

注意幾個函數的區別:

  • ip4_output_if():這個函數封裝了底層IP層組包、傳送的實現函數。
  • ip4_output_if_src():這個函數就是IP層組包、傳送的實現函數。不支援IP首部的選項欄位。
  • ip4_output_if_opt():這也是IP層組包、傳送的實現函數,會用選中的網路卡IP地址覆蓋傳入的源IP地址。支援IP首部的選項欄位。
  • ip4_output_if_opt_src():這也是IP層組包、傳送的實現函數,不會用選中的網路卡IP地址覆蓋傳入的源IP地址。支援IP首部的選項欄位。

相關宏:

IP_OPTIONS_SEND

  • IP首報文首部選項欄位宏開關。
  • 如果開啟了該宏,則會呼叫上述帶_opt字樣的函數,操作IP首部報文的選項欄位。

LWIP_IP_HDRINCL:預設為NULL

  • 如果把這個宏當目的IP地址傳入IP層組包、傳送的相關函數ip4_output_if()或其底層函數時,表示當前這個pbuf已經組好IP首部了。
  • 一般用於TCP重傳。

LWIP_CHECKSUM_CTRL_PER_NETIF

  • 允許每個網路卡設定checksum功能。

相關變數:

  • ip_id:IP首部標識,全域性值。

我們就分析ip4_output_if_opt_src()函數,比較全。

ip4_output_if_opt_src()

  • 先處理選項欄位,在處理IP首部其它欄位。
  • struct pbuf *p:傳輸層協定需要傳送的封包pbuf,payload指標已指向傳輸層協定首部。
  • const ip4_addr_t *src:源IP地址。
  • const ip4_addr_t *dest:目的IP地址。
  • u8_t ttl:IP首部TTL欄位。
  • u8_t tos:IP首部TOS欄位。
  • u8_t proto:IP首部上層協定欄位。
  • struct netif *netif:傳送IP資料包的網路卡。
  • void *ip_options:IP首部選項欄位值。
  • u16_t optlen:IP首部選項欄位的長度。

概要內容:

  • 通過目的IP判斷當前pbuf是否已經組好IP報文首部。如果組好了,就不需要繼續重組了。如tcp重傳。

  • 如果傳入的pbuf報文還沒組好IP報文首部,則根據傳入的相關資料和IP報文內容進行組包。

  • 組好包後檢查目的IP是否是環回IP(如環回IP、當前網路卡的IP),如果是就呼叫netif_loop_output()進行環回處理。

  • 如果不是環回封包,就需要發到資料鏈路。

    • IP分片:如果IP報文總長大於網路卡MTU,則需要呼叫ip4_frag()進行IP分片。
    • 如果不需要IP分片,直接呼叫netif->output()將IP報文發出。
err_t
ip4_output_if_opt_src(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,
                      u8_t ttl, u8_t tos, u8_t proto, struct netif *netif, void *ip_options,
                      u16_t optlen)
{
#endif /* IP_OPTIONS_SEND */
  struct ip_hdr *iphdr;
  ip4_addr_t dest_addr;
#if CHECKSUM_GEN_IP_INLINE
  u32_t chk_sum = 0;
#endif /* CHECKSUM_GEN_IP_INLINE */

  LWIP_ASSERT_CORE_LOCKED(); /* 確保在tcpip核心鎖內 */
  LWIP_IP_CHECK_PBUF_REF_COUNT_FOR_TX(p); /* pbuf沒有被其它地方參照 */

  MIB2_STATS_INC(mib2.ipoutrequests); /* 流量統計 */

  /* IP首部是否已經組建好 */
  if (dest != LWIP_IP_HDRINCL) { /* IP首部未組建好 */
    u16_t ip_hlen = IP_HLEN; /* 預設IP首部長度 */
#if IP_OPTIONS_SEND /* 開啟了IP首部選項欄位操作 */
    u16_t optlen_aligned = 0;
    if (optlen != 0) {
#if CHECKSUM_GEN_IP_INLINE
      int i;
#endif /* CHECKSUM_GEN_IP_INLINE */
      if (optlen > (IP_HLEN_MAX - IP_HLEN)) { /* 判斷IP首部欄位是否超限 */
        /* optlen 過長,導致IP首部超出限制 */
        LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output_if_opt: optlen too long\n"));
        /* 狀態資訊統計 */
        IP_STATS_INC(ip.err);
        MIB2_STATS_INC(mib2.ipoutdiscards);
        return ERR_VAL;
      }
      /* 根據協定要求,IP首部選項欄位長度要求是4位元組的整數倍 */
      optlen_aligned = (u16_t)((optlen + 3) & ~3); /* 4位元組往大對齊 */
      ip_hlen = (u16_t)(ip_hlen + optlen_aligned); /* 總長度欄位 */
      /* 先處理選項欄位 */
      /* pbuf payload偏移到IP首部選項欄位位置 */
      if (pbuf_add_header(p, optlen_aligned)) {
        LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output_if_opt: not enough room for IP options in pbuf\n"));
        /* payload偏移失敗,統計資訊返回錯誤 */
        IP_STATS_INC(ip.err);
        MIB2_STATS_INC(mib2.ipoutdiscards);
        return ERR_BUF;
      }
      /* 先處理選項欄位,拷貝選項欄位值到IP首部中 */
      MEMCPY(p->payload, ip_options, optlen);
      if (optlen < optlen_aligned) {
        /* 多餘位元組補0 */
        memset(((char *)p->payload) + optlen, 0, (size_t)(optlen_aligned - optlen));
      }
#if CHECKSUM_GEN_IP_INLINE
      /* 先統計這部分的首部校驗和 */
      for (i = 0; i < optlen_aligned / 2; i++) {
        chk_sum += ((u16_t *)p->payload)[i];
      }
#endif /* CHECKSUM_GEN_IP_INLINE */
    }
#endif /* IP_OPTIONS_SEND */
    /* 常見IP首部處理 */
    /* pbuf payload指標偏移 */
    if (pbuf_add_header(p, IP_HLEN)) {
      LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output: not enough room for IP header in pbuf\n"));
      /* payload偏移失敗,統計資訊返回錯誤 */
      IP_STATS_INC(ip.err);
      MIB2_STATS_INC(mib2.ipoutdiscards);
      return ERR_BUF;
    }

    iphdr = (struct ip_hdr *)p->payload;
    LWIP_ASSERT("check that first pbuf can hold struct ip_hdr",
                (p->len >= sizeof(struct ip_hdr)));

    IPH_TTL_SET(iphdr, ttl); /* 填寫TTL欄位 */
    IPH_PROTO_SET(iphdr, proto); /* 填寫上層協定欄位 */
#if CHECKSUM_GEN_IP_INLINE
    chk_sum += PP_NTOHS(proto | (ttl << 8)); /* 首部校驗和統計 */
#endif /* CHECKSUM_GEN_IP_INLINE */

    ip4_addr_copy(iphdr->dest, *dest); /* 填寫目的IP欄位 */
#if CHECKSUM_GEN_IP_INLINE
    /* 首部校驗和統計 */
    chk_sum += ip4_addr_get_u32(&iphdr->dest) & 0xFFFF;
    chk_sum += ip4_addr_get_u32(&iphdr->dest) >> 16;
#endif /* CHECKSUM_GEN_IP_INLINE */

    IPH_VHL_SET(iphdr, 4, ip_hlen / 4); /* 填寫IP版本號+IP首部長度 */
    IPH_TOS_SET(iphdr, tos); /* 填寫TOS服務欄位 */
#if CHECKSUM_GEN_IP_INLINE
    /* 首部校驗和統計 */
    chk_sum += PP_NTOHS(tos | (iphdr->_v_hl << 8));
#endif /* CHECKSUM_GEN_IP_INLINE */
    IPH_LEN_SET(iphdr, lwip_htons(p->tot_len)); /* 填寫總長度欄位 */
#if CHECKSUM_GEN_IP_INLINE
    /* 首部校驗和統計 */
    chk_sum += iphdr->_len;
#endif /* CHECKSUM_GEN_IP_INLINE */
    IPH_OFFSET_SET(iphdr, 0); /* 填寫標誌欄位+分片偏移量欄位 */
    IPH_ID_SET(iphdr, lwip_htons(ip_id)); /* 填寫標識欄位 */
#if CHECKSUM_GEN_IP_INLINE
    /* 首部校驗和統計 */
    chk_sum += iphdr->_id;
#endif /* CHECKSUM_GEN_IP_INLINE */
    ++ip_id; /* ip首部標識,全域性值 */

    /* 填寫源IP位址列位 */
    if (src == NULL) {
      ip4_addr_copy(iphdr->src, *IP4_ADDR_ANY4);
    } else {
      /* src cannot be NULL here */
      ip4_addr_copy(iphdr->src, *src);
    }

#if CHECKSUM_GEN_IP_INLINE
    /* 首部校驗和統計 */
    chk_sum += ip4_addr_get_u32(&iphdr->src) & 0xFFFF;
    chk_sum += ip4_addr_get_u32(&iphdr->src) >> 16;
    /* 二次16bit摺疊。因為一次可能會溢位。第二次是為了解決溢位。 */
    chk_sum = (chk_sum >> 16) + (chk_sum & 0xFFFF);
    chk_sum = (chk_sum >> 16) + chk_sum;
    chk_sum = ~chk_sum; /* 反碼 */
    IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) { /* 當前網路卡支援IP首部的checksum功能 */
      iphdr->_chksum = (u16_t)chk_sum; /* 填寫首部校驗和欄位。網路位元組序,因為統計的時候就以網路位元組序統計,所以這裡不必轉換 */
    }
#if LWIP_CHECKSUM_CTRL_PER_NETIF
    else {/* 如果不支援checksum功能 */
      IPH_CHKSUM_SET(iphdr, 0); /* 填寫首部校驗和欄位為0 */
    }
#endif /* LWIP_CHECKSUM_CTRL_PER_NETIF*/
#else /* CHECKSUM_GEN_IP_INLINE */
    IPH_CHKSUM_SET(iphdr, 0); /* 預設填寫首部校驗和欄位為0 */
#if CHECKSUM_GEN_IP
    IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) {
      IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, ip_hlen)); /* IP首部校驗和欄位更新為實際值 */
    }
#endif /* CHECKSUM_GEN_IP */
#endif /* CHECKSUM_GEN_IP_INLINE */
  } else { /* IP頭已經包含在p。如TCP重傳 */
    if (p->len < IP_HLEN) { /* 長度校驗 */
      LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output: LWIP_IP_HDRINCL but pbuf is too short\n"));
      IP_STATS_INC(ip.err);
      MIB2_STATS_INC(mib2.ipoutdiscards);
      return ERR_BUF;
    }
    iphdr = (struct ip_hdr *)p->payload;
    ip4_addr_copy(dest_addr, iphdr->dest); /* 獲取目的IP地址 */
    dest = &dest_addr;
  }
  /* 狀態記錄 */
  IP_STATS_INC(ip.xmit);

  LWIP_DEBUGF(IP_DEBUG, ("ip4_output_if: %c%c%"U16_F"\n", netif->name[0], netif->name[1], (u16_t)netif->num));
  ip4_debug_print(p);

#if ENABLE_LOOPBACK /* 環回功能 */
  if (ip4_addr_eq(dest, netif_ip4_addr(netif)) /* 目的IP為源網路卡IP,則是環回 */
#if !LWIP_HAVE_LOOPIF
      || ip4_addr_isloopback(dest) /* 目的IP是環回IP欄位,也是環回 */
#endif /* !LWIP_HAVE_LOOPIF */
     ) {
    /* 封包環回,則不用通過資料鏈路層,直達對應網路卡的環回連結串列即可 */
    LWIP_DEBUGF(IP_DEBUG, ("netif_loop_output()"));
    return netif_loop_output(netif, p);
  }
#if LWIP_MULTICAST_TX_OPTIONS
  if ((p->flags & PBUF_FLAG_MCASTLOOP) != 0) { /* 該pbuf是要環回的UDP組播 */
    netif_loop_output(netif, p); /* 環回到本網路卡 */
  }
#endif /* LWIP_MULTICAST_TX_OPTIONS */
#endif /* ENABLE_LOOPBACK */
#if IP_FRAG
  /* 如果介面mtu設定為0,不用分片 */
  /* 分片檢查 */
  if (netif->mtu && (p->tot_len > netif->mtu)) { /* IP報文超出網路卡MTU,則需要分片處理 */
    return ip4_frag(p, netif, dest); /* 需要分片處理 */
  }
#endif /* IP_FRAG */

  /* 不需要分片處理 */
  LWIP_DEBUGF(IP_DEBUG, ("ip4_output_if: call netif->output()\n"));
  return netif->output(netif, p, dest); /* IP層傳送封包。至此,IP報文處理完畢,下一步交給ARP或者直接到資料鏈路處理。 */
}

9.7.5 IP資料包分片

注意:lwip分片偏移不支援IP首部帶選項欄位的。

從IP報文首部就可知,有分片概念。

不是每個底層網路卡都能承載每個 IP 資料包長度的報文。如:

  • 乙太網幀最大能承載 1500 個位元組的資料。
  • 某些廣域網鏈路的幀可承載不超過 576 位元組的資料。

一個鏈路層幀能承載的最巨量資料量叫做最大傳送單元(Maximum TransmissionUnit,MTU)。

IP 資料包的分片偏移量是用 8 的整數倍記錄的,所以每個資料包中的分片資料大小也必須是 8 的整數倍。

IP資料包分片主要關注IP首部的標識欄位、標誌欄位和分片偏移量欄位。具體往前看。

參考圖:

相關原始碼實現在ip4_frag.c

相關宏:

  • LWIP_NETIF_TX_SINGLE_PBUF:分片是否支援新建一整個pbuf處理。

    • 1:分片時,直接申請各個IP分片包的pbuf即可(含IP首部+資料區)。
    • 0:分片時,申請各個分片的管理區,MEMP_FRAG_PBUF型別。其資料結構為pbuf_custom_ref。該資料結構包含本次IP分片包的原IP報文pbuf地址,釋放參照的api,指向分片IP報文資料區的pbuf。然後將這個分片IP首部的pbuf和這個IP報文資料區的pbuf拼接起來即可。組成新的分片IP報文。

相關資料結構:

pbuf_custom資料結構:

/** A custom pbuf that holds a reference to another pbuf, which is freed
 * when this custom pbuf is freed. This is used to create a custom PBUF_REF
 * that points into the original pbuf. */
struct pbuf_custom_ref {
  /** 'base class' */
  struct pbuf_custom pc; /* 使用者的控制區。包含一個pbuf和一個釋放該pbuf的api */
  /* 指向被參照的原始pbuf的指標 */
  struct pbuf *original;
};

pbuf_custom_ref資料結構:

struct pbuf_custom {
  /* The actual pbuf */
  struct pbuf pbuf;
  /**This function is called when pbuf_free deallocates this pbuf(_custom) */
  pbuf_free_custom_fn custom_free_function;
};

相關資料結構圖:

  • 沒開啟LWIP_NETIF_TX_SINGLE_PBUF宏的IP分片報文資料結構:

  • 開啟LWIP_NETIF_TX_SINGLE_PBUF宏的分片IP報文資料結構(按簡單的畫):

ip4_frag()

  • 與分片重組ip4_reass()這個API對應。

  • 需要注意的是:需要檢查本次分片處理傳入的原IP報文struct pbuf *p是否也是一個分片包。如果是,那麼它可能不是頂層原IP報文分片的最後一片,這樣的話,在本次分片處理最後一片的分片IP報文首部標誌欄位的還有更多分片標誌位不能置位0。因為在頂層未分片IP報文角度看來,這還不是真正意義上的最後一片。

  • 下面函數分析時,按LWIP_NETIF_TX_SINGLE_PBUF宏分支分析更加助於理解。

  • 處於非LWIP_NETIF_TX_SINGLE_PBUF宏分支:如果讀者需要分析,然後看不懂這個分支,可以看下我的筆記:

    • 先申請一個儲存分片IP首部的pbuf:rambuf
    • 然後再申請一個pbuf_custom_ref資料結構的偽pbuf:pcr
    • 然後把未分片的IP報文的pbuf對應分片的資料區地址給到pcr->pc->pbuf->payload,共用資料區記憶體嘛。
    • 然後把分片IP首部的pbuf和分片IP的資料pbuf拼接起來:pbuf_cat(rambuf, pcr->pc->pbuf->payload);,這樣就組成了分片的IP報文了。
/**
 * Fragment an IP datagram if too large for the netif.
 *
 * Chop the datagram in MTU sized chunks and send them in order
 * by pointing PBUF_REFs into p.
 *
 * @param p ip packet to send
 * @param netif the netif on which to send
 * @param dest destination ip address to which to send
 *
 * @return ERR_OK if sent successfully, err_t otherwise
 */
err_t
ip4_frag(struct pbuf *p, struct netif *netif, const ip4_addr_t *dest)
{
  struct pbuf *rambuf; /* 分片的pbuf結構 */
#if !LWIP_NETIF_TX_SINGLE_PBUF
  /* 用於處理分片IP報文與原IP報文資料區pbuf記憶體共用使用 */
  struct pbuf *newpbuf;
  u16_t newpbuflen = 0;
  u16_t left_to_copy;
#endif
  struct ip_hdr *original_iphdr;
  struct ip_hdr *iphdr;
  const u16_t nfb = (u16_t)((netif->mtu - IP_HLEN) / 8); /* 分片中允許的最巨量資料量 */
  u16_t left, fragsize; /* 待傳送資料長度和當前傳送的分片的資料長度 */
  u16_t ofo; /* 當前分片偏移量 */
  int last; /* 是否為最後一個分片 */
  u16_t poff = IP_HLEN; /* 傳送的資料在原始資料包pbuf中的偏移量 */
  u16_t tmp;
  int mf_set; /* 傳入的pbuf是否是分片包,後續是否還有更多分片。 */

  original_iphdr = (struct ip_hdr *)p->payload;
  iphdr = original_iphdr;
  if (IPH_HL_BYTES(iphdr) != IP_HLEN) { /* IP首部長度欄位 */
    /* ip4_frag() 不支援IP首部帶選項欄位的IP報文分片 */
    return ERR_VAL;
  }
  LWIP_ERROR("ip4_frag(): pbuf too short", p->len >= IP_HLEN, return ERR_VAL);

  /* 儲存原始的分片偏移量欄位 */
  tmp = lwip_ntohs(IPH_OFFSET(iphdr));
  ofo = tmp & IP_OFFMASK;
  /* 判斷pbuf包是否已經被分片,被分片後後續是否還有更多分片。如果有,那麼本次分片的最後一個分片不能標誌後續沒有更多分片。 */
  mf_set = tmp & IP_MF;
   
  /* 獲取還剩多少資料還沒分片處理 */
  left = (u16_t)(p->tot_len - IP_HLEN);

  while (left) { /* 迴圈分片處理 */
    /* 當前分片需要填充的資料size */
    fragsize = LWIP_MIN(left, (u16_t)(nfb * 8));

#if LWIP_NETIF_TX_SINGLE_PBUF /* 分片支援新建整個pbuf */
    /* 申請新的pbuf記憶體資源裝載當前分片 */
    rambuf = pbuf_alloc(PBUF_IP, fragsize, PBUF_RAM);
    if (rambuf == NULL) {
      goto memerr;
    }
    LWIP_ASSERT("this needs a pbuf in one piece!",
                (rambuf->len == rambuf->tot_len) && (rambuf->next == NULL));
    /* 拷貝IP報文資料區的資料到分片IP包中 */
    poff += pbuf_copy_partial(p, rambuf->payload, fragsize, poff);
    /* pbuf騰出IP首部空間 */
    if (pbuf_add_header(rambuf, IP_HLEN)) {
      pbuf_free(rambuf);
      goto memerr;
    }
    /* 填充原IP報文首部到當前分片,後面再處理分片的IP首部 */
    SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);
    iphdr = (struct ip_hdr *)rambuf->payload;
#else /* LWIP_NETIF_TX_SINGLE_PBUF */ /* 分片支援資料區共用。這個宏分支,感興趣的同學可以看下 */
    /* 先申請分片IP首部的pbuf */
    rambuf = pbuf_alloc(PBUF_LINK, IP_HLEN, PBUF_RAM);
    if (rambuf == NULL) {
      goto memerr;
    }
    LWIP_ASSERT("this needs a pbuf in one piece!",
                (rambuf->len >= (IP_HLEN)));
    /* 拷貝原IP報文首部到分片IP報文首部 */
    SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);
    iphdr = (struct ip_hdr *)rambuf->payload;

    /* 本次分片IP報文佔用的size */
    left_to_copy = fragsize;
    while (left_to_copy) { /* 分片IP報文組裝完畢為止 */
      struct pbuf_custom_ref *pcr; /* 參照原IP報文資料區pbuf的偽pbuf */
      u16_t plen = (u16_t)(p->len - poff); /* 原IP報文當前pbuf節點還剩多少資料沒處理 */
      LWIP_ASSERT("p->len >= poff", p->len >= poff);
      newpbuflen = LWIP_MIN(left_to_copy, plen); /* 選出能處理的size */

      if (!newpbuflen) { /* 原IP報文當前pbuf所有資料已經分片處理完畢,可以處理下一個pbuf */
        poff = 0;
        p = p->next; /* 跳到下一個分片IP報文處理繼續組裝當前IP分片 */
        continue;
      }
      pcr = ip_frag_alloc_pbuf_custom_ref(); /* 為偽pbuf pcr申請資源 */
      if (pcr == NULL) {
        pbuf_free(rambuf);
        goto memerr;
      }
      /* 把原IP報文資料區當前分片的資料pbuf參照到pcr->pc->pbuf這個pbuf中 */
      newpbuf = pbuf_alloced_custom(PBUF_RAW, newpbuflen, PBUF_REF, &pcr->pc,
                                    (u8_t *)p->payload + poff, newpbuflen);
      if (newpbuf == NULL) {
        ip_frag_free_pbuf_custom_ref(pcr);
        pbuf_free(rambuf);
        goto memerr;
      }
  
      pbuf_ref(p); /* 當前pbuf參照值+1 */
      pcr->original = p; /* 儲存當前pbuf地址到pcr */
      pcr->pc.custom_free_function = ipfrag_free_pbuf_custom; /* 專門的偽pbuf釋放API */

      /* 拼接pbuf,把本次分片IP首部、資料區的各個pbuf都鏈在一起 */
      pbuf_cat(rambuf, newpbuf);
      left_to_copy = (u16_t)(left_to_copy - newpbuflen); /* 檢查本次分片IP報文是否整理完畢 */
      if (left_to_copy) { /* 還沒填充完畢,需要繼續填充 */
        poff = 0;
        p = p->next;
      }
    }
    poff = (u16_t)(poff + newpbuflen); /* 分片在原IP報文中的偏移值更新 */
#endif /* LWIP_NETIF_TX_SINGLE_PBUF */

    /* 更正分片IP首部 */
    /* 本次分片是否為最後一次分片 */
    last = (left <= netif->mtu - IP_HLEN);

    /* 設定新的偏移量和MF標誌 */
    tmp = (IP_OFFMASK & (ofo));
    if (!last || mf_set) { /* 本函數分片處理的最後一片 且 傳入的原IP報文不是分片報文 */
      /* 標誌位第三位更多分片標記為0,已經為頂層原IP報文的最後一片 */
      tmp = tmp | IP_MF;
    }
    IPH_OFFSET_SET(iphdr, lwip_htons(tmp)); /* 重寫分片IP首部的標誌位欄位+分片偏移量欄位 */
    IPH_LEN_SET(iphdr, lwip_htons((u16_t)(fragsize + IP_HLEN))); /* 重寫分片IP報文總長度 */
    IPH_CHKSUM_SET(iphdr, 0); /* 設定分片IP報文首部校驗欄位,預設為0 */
#if CHECKSUM_GEN_IP
    IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) {
      IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, IP_HLEN)); /* 更新分片IP報文首部校驗欄位 */
    }
#endif /* CHECKSUM_GEN_IP */

    /* 傳送分片IP報文 */
    netif->output(netif, rambuf, dest);
    /* 分片狀態資訊記錄 */
    IPFRAG_STATS_INC(ip_frag.xmit);

    /* Unfortunately we can't reuse rambuf - the hardware may still be
     * using the buffer. Instead we free it (and the ensuing chain) and
     * recreate it next time round the loop. If we're lucky the hardware
     * will have already sent the packet, the free will really free, and
     * there will be zero memory penalty.
     */
    /* 釋放分片空間。因為這rambuf只是分片函數內部處理建立的,所以呼叫netif->output()傳送出去後要在這裡釋放掉。 */
    pbuf_free(rambuf);
    /* 需要處理的分片待傳送資料減少 */
    left = (u16_t)(left - fragsize);
    /* 分片偏移量增加 */
    ofo = (u16_t)(ofo + nfb);
  }
  MIB2_STATS_INC(mib2.ipfragoks);
  return ERR_OK;
memerr:
  MIB2_STATS_INC(mib2.ipfragfails);
  return ERR_MEM;
}

9.8 IP層輸入

9.8.1 接收資料包

翻看前面網路介面層章節應該知道底層接收資料流,在網路卡驅動收到資料,呼叫netif->input()把對應API和pbuf轉發到lwip核心執行緒執行。

以乙太網為例,網路卡接收傳輸執行緒通過把資料給到netif->input(),該函數內部外包乙太網鏈路層ethernet_input()到lwip核心執行緒去跑,如果是ARP協定的乙太網幀,則傳到ARP模組etharp_input()處理。如果是IPv4的乙太網幀,則傳到ip4_input()處理。

相關宏:

LWIP_HOOK_IP4_INPUT(p, inp):IPV4接收封包勾點函數。

  • 接收資料時檢查了IP報文的版本為IPV4,就會把這個IP報文傳給這個勾點函數。
  • 如果勾點放回true,則表示由勾點處理,外部丟棄。

LWIP_IP_ACCEPT_UDP_PORT(dst_port):在netif關閉時接受私人廣播通訊用。

ip4_input()

  • IP報文校驗。

  • 傳入勾點LWIP_HOOK_IP4_INPUT(p, inp)

  • 匹配目的網路卡。就是判斷當前IP報文是不是給我的。

    • 多播包:

      • 開啟了IGMP:當前網路卡在當前IP報文目的IP組播內,匹配成功。
      • 沒有開啟IGMP:當前網路卡有效即可匹配成功。
    • 廣播包和單播包:都呼叫ip4_input_accept()API匹配。前面有分析。

      • 先匹配收到該IP報文的網路卡;
      • 再遍歷網路卡連結串列。注意:如果沒有環回功能或者有環回網路卡,且IP報文目的IP地址是環回欄位的IP地址,不能遍歷網路卡連結串列。因為環回,需要用環回介面,在前面匹配網路卡就應該配上了,不會跑到這裡。
    • 開啟了DHCP,且面前沒有匹配上網路卡:

      • 通過IP報文可以判斷當前IP報文是否是UDP協定。
      • 通過UDP協定解析目的埠是否是DHCP使用者端埠LWIP_IANA_PORT_DHCP_CLIENT(68)。
      • 是DHCP報文,直接收到當前網路卡。不需要做目的IP校驗。
  • 沒有匹配到網路卡,即是IP報文不是給我們的,但是開啟了IP_FORWARD轉發功能。如果目的IP不是廣播地址,可以呼叫ip4_forward()進行轉發。

  • 匹配到網路卡,處理IP報文,如果IP報文被分片了,需要呼叫ip4_reass()重組。

  • 收到完整的IP報文後,根據IP報文的上層協定型別欄位,給到對應的協定模組。lwip支援:

    • IP_PROTO_UDP:UDP協定。udp_input(p, inp);
    • IP_PROTO_TCP:TCP協定。tcp_input(p, inp);
    • IP_PROTO_ICMP:ICMP協定。icmp_input(p, inp);
    • IP_PROTO_IGMP:IGMP協定。igmp_input(p, inp, ip4_current_dest_addr());
    • LWIP_RAW:原報文上報。
/**
 * This function is called by the network interface device driver when
 * an IP packet is received. The function does the basic checks of the
 * IP header such as packet size being at least larger than the header
 * size etc. If the packet was not destined for us, the packet is
 * forwarded (using ip_forward). The IP checksum is always checked.
 *
 * Finally, the packet is sent to the upper layer protocol input function.
 *
 * @param p the received IP packet (p->payload points to IP header)
 * @param inp the netif on which this packet was received
 * @return ERR_OK if the packet was processed (could return ERR_* if it wasn't
 *         processed, but currently always returns ERR_OK)
 */
err_t
ip4_input(struct pbuf *p, struct netif *inp)
{
  const struct ip_hdr *iphdr;
  struct netif *netif;
  u16_t iphdr_hlen;
  u16_t iphdr_len;
#if IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMP
  int check_ip_src = 1;
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMP */
#if LWIP_RAW
  raw_input_state_t raw_status;
#endif /* LWIP_RAW */

  LWIP_ASSERT_CORE_LOCKED();

  IP_STATS_INC(ip.recv);
  MIB2_STATS_INC(mib2.ipinreceives);

  /* 識別IP頭 */
  iphdr = (struct ip_hdr *)p->payload;
  if (IPH_V(iphdr) != 4) { /* 如果IP版本不是ipv4,丟棄該報文 */
    LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_WARNING, ("IP packet dropped due to bad version number %"U16_F"\n", (u16_t)IPH_V(iphdr)));
    ip4_debug_print(p);
    pbuf_free(p);
    IP_STATS_INC(ip.err);
    IP_STATS_INC(ip.drop);
    MIB2_STATS_INC(mib2.ipinhdrerrors);
    return ERR_OK;
  }

#ifdef LWIP_HOOK_IP4_INPUT
  if (LWIP_HOOK_IP4_INPUT(p, inp)) { /* 傳入IP接收封包勾點函數處理 */
    /* IP報文也就被處理了 */
    return ERR_OK;
  }
#endif

  /* 獲取IP首部長度(以位元組為單位) */
  iphdr_hlen = IPH_HL_BYTES(iphdr);
  /* 獲取IP報文長度(以位元組為單位) */
  iphdr_len = lwip_ntohs(IPH_LEN(iphdr));

  /* 修剪pbuf。對於小於60位元組的封包尤其需要這樣做 */
  if (iphdr_len < p->tot_len) {
    pbuf_realloc(p, iphdr_len);
  }

  /* 報頭長度超過第一個pbuf長度,或者IP長度超過總pbuf長度? */
  if ((iphdr_hlen > p->len) || (iphdr_len > p->tot_len) || (iphdr_hlen < IP_HLEN)) {
    if (iphdr_hlen < IP_HLEN) {
      LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
                  ("ip4_input: short IP header (%"U16_F" bytes) received, IP packet dropped\n", iphdr_hlen));
    }
    if (iphdr_hlen > p->len) {
      LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
                  ("IP header (len %"U16_F") does not fit in first pbuf (len %"U16_F"), IP packet dropped.\n",
                   iphdr_hlen, p->len));
    }
    if (iphdr_len > p->tot_len) {
      LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
                  ("IP (len %"U16_F") is longer than pbuf (len %"U16_F"), IP packet dropped.\n",
                   iphdr_len, p->tot_len));
    }
    /* 校驗錯誤,丟棄當前pbuf,並記錄相關資訊 */
    pbuf_free(p);
    IP_STATS_INC(ip.lenerr);
    IP_STATS_INC(ip.drop);
    MIB2_STATS_INC(mib2.ipindiscards);
    return ERR_OK;
  }

  /* 首部校驗和欄位校驗 */
#if CHECKSUM_CHECK_IP
  IF__NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_CHECK_IP) {
    if (inet_chksum(iphdr, iphdr_hlen) != 0) {

      LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
                  ("Checksum (0x%"X16_F") failed, IP packet dropped.\n", inet_chksum(iphdr, iphdr_hlen)));
      ip4_debug_print(p);
      pbuf_free(p);
      IP_STATS_INC(ip.chkerr);
      IP_STATS_INC(ip.drop);
      MIB2_STATS_INC(mib2.ipinhdrerrors);
      return ERR_OK;
    }
  }
#endif

  /* 拷貝IP地址到對齊的ip_addr_t。ip_data為全域性變數 */
  ip_addr_copy_from_ip4(ip_data.current_iphdr_dest, iphdr->dest);
  ip_addr_copy_from_ip4(ip_data.current_iphdr_src, iphdr->src);

  /* 將報文與介面匹配,即這個報文是我們的嗎? */
  if (ip4_addr_ismulticast(ip4_current_dest_addr())) { /* 多播包 */
#if LWIP_IGMP /* IGMP處理 */
    if ((inp->flags & NETIF_FLAG_IGMP) && (igmp_lookfor_group(inp, ip4_current_dest_addr()))) { /* 當前網路卡在IP報文目的IP的組播組內 */
      /* IGMP snooping交換機需要允許源地址為0.0.0.0 (RFC 4541) */
      ip4_addr_t allsystems;
      IP4_ADDR(&allsystems, 224, 0, 0, 1);
      if (ip4_addr_eq(ip4_current_dest_addr(), &allsystems) &&
          ip4_addr_isany(ip4_current_src_addr())) {
        check_ip_src = 0; /* 標記後面忽略源IP地址 */
      }
      netif = inp; /* 組播匹配成功 */
    } else {
      netif = NULL; /* 組播匹配失敗 */
    }
#else /* LWIP_IGMP */
    if ((netif_is_up(inp)) && (!ip4_addr_isany_val(*netif_ip4_addr(inp)))) {
      netif = inp; /* 如果不支援IGMP協定功能,但是當前網路卡收到這個組播包,就直接匹配當前網路卡即可 */
    } else {
      netif = NULL;
    }
#endif /* LWIP_IGMP */
  } else { /* 單播或廣播IP包 */
    /* 優先匹配當前網路卡 */
    if (ip4_input_accept(inp)) {
      netif = inp; /* 匹配成功 */
    } else { /* 當前網路卡匹配失敗,就遍歷網路卡連結串列進行匹配 */
      netif = NULL;
#if !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF
      /* 如果網路卡沒有環回功能,或者已經建立了環回網路卡。如果當前IP報文的目的IP是環回型別的,則不會跑到這裡,而是在對應網路卡的環回資料佇列中。 */
      if (!ip4_addr_isloopback(ip4_current_dest_addr())) /* IP報文的目的IP不能是環回型別 */
#endif /* !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF */
      {
#if !LWIP_SINGLE_NETIF
        NETIF_FOREACH(netif) { /* 遍歷網路卡連結串列 */
          if (netif == inp) { /* 跳過已經嘗試匹配的網路卡 */
            /* we checked that before already */
            continue;
          }
          if (ip4_input_accept(netif)) { /* 進行匹配 */
            break;
          }
        }
#endif /* !LWIP_SINGLE_NETIF */
      }
    }
  }

#if IP_ACCEPT_LINK_LAYER_ADDRESSING
  /* 通過DHCP報文時,不考慮目的地址。DHCP流量使用鏈路層定址(如乙太網MAC),所以我們不能對IP進行過濾。
   * 參考RFC 1542章節3.1.1,參考RFC 2131)。
   * 
   * 如果想在netif關閉時接受私有廣播通訊,定義LWIP_IP_ACCEPT_UDP_PORT(dst_port),例如:
   * #define LWIP_IP_ACCEPT_UDP_PORT(dst_port) ((dst_port) == PP_NTOHS(12345))
   */
  if (netif == NULL) {
    /* 遠端埠是DHCP伺服器? */
    if (IPH_PROTO(iphdr) == IP_PROTO_UDP) { /* UDP協定型別 */
      const struct udp_hdr *udphdr = (const struct udp_hdr *)((const u8_t *)iphdr + iphdr_hlen);
      LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_TRACE, ("ip4_input: UDP packet to DHCP client port %"U16_F"\n",
                                              lwip_ntohs(udphdr->dest)));
      if (IP_ACCEPT_LINK_LAYER_ADDRESSED_PORT(udphdr->dest)) { /* 目的埠是DHCP使用者端埠。能通過協定棧IP層 */
        LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_TRACE, ("ip4_input: DHCP packet accepted.\n"));
        netif = inp; /* 匹配成功 */
        check_ip_src = 0; /* 標記後面忽略源IP地址 */
      }
    }
  }
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */

  /* 廣播或多播包的源地址?相容RFC 1122: 3.2.1.3 */
#if LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING
  if (check_ip_src /* 忽略源IP地址的跳過:如IGMP和DHCP */
#if IP_ACCEPT_LINK_LAYER_ADDRESSING
      /* 允許DHCP伺服器源地址為0.0.0.0 (RFC 1.1.2.2: 3.2.1.3/a) */
      && !ip4_addr_isany_val(*ip4_current_src_addr())
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */
     )
#endif /* LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING */
  {
    if ((ip4_addr_isbroadcast(ip4_current_src_addr(), inp)) || /* 廣播包 */
        (ip4_addr_ismulticast(ip4_current_src_addr()))) { /* 或者是多播包 */
      /* IP報文無效 */
      LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_WARNING, ("ip4_input: packet source is not valid.\n"));
      /* 釋放資源並記錄相關資訊 */
      pbuf_free(p);
      IP_STATS_INC(ip.drop);
      MIB2_STATS_INC(mib2.ipinaddrerrors);
      MIB2_STATS_INC(mib2.ipindiscards);
      return ERR_OK;
    }
  }

  /* 網路卡匹配完畢 */

  if (netif == NULL) { /* 網路卡匹配失敗 */
    /* 不給我們包,路由或丟棄 */
    LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_TRACE, ("ip4_input: packet not for us.\n"));
#if IP_FORWARD /* 支援路由轉發 */
    if (!ip4_addr_isbroadcast(ip4_current_dest_addr(), inp)) { /* 廣播包不能轉發 */
      /* 嘗試在(其他)介面轉發IP報文 */
      ip4_forward(p, (struct ip_hdr *)p->payload, inp);
    } else
#endif /* IP_FORWARD */
    { /* 廣播包不支援轉發,丟棄 */
      IP_STATS_INC(ip.drop);
      MIB2_STATS_INC(mib2.ipinaddrerrors);
      MIB2_STATS_INC(mib2.ipindiscards);
    }
    pbuf_free(p);
    return ERR_OK;
  }

  /* 網路卡匹配成功,封包是給我們的 */

  /* 封包由多個分片組成? */
  if ((IPH_OFFSET(iphdr) & PP_HTONS(IP_OFFMASK | IP_MF)) != 0) {
#if IP_REASSEMBLY /* 支援IP分片重組 */
    LWIP_DEBUGF(IP_DEBUG, ("IP packet is a fragment (id=0x%04"X16_F" tot_len=%"U16_F" len=%"U16_F" MF=%"U16_F" offset=%"U16_F"), calling ip4_reass()\n",
                           lwip_ntohs(IPH_ID(iphdr)), p->tot_len, lwip_ntohs(IPH_LEN(iphdr)), (u16_t)!!(IPH_OFFSET(iphdr) & PP_HTONS(IP_MF)), (u16_t)((lwip_ntohs(IPH_OFFSET(iphdr)) & IP_OFFMASK) * 8)));
    /* 重新組裝包 */
    p = ip4_reass(p);
    if (p == NULL) { /* IP包還沒組裝好 */
      return ERR_OK;
    }
    /* IP報文重組好了,更新IP首部指標 */
    iphdr = (const struct ip_hdr *)p->payload;
#else /* IP_REASSEMBLY == 0, 不支援分片重組 */
    /* 不支援分片重組,遇到分片IP報文,直接丟棄 */
    pbuf_free(p);
    LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("IP packet dropped since it was fragmented (0x%"X16_F") (while IP_REASSEMBLY == 0).\n",
                lwip_ntohs(IPH_OFFSET(iphdr))));
    IP_STATS_INC(ip.opterr);
    IP_STATS_INC(ip.drop);
    /* unsupported protocol feature */
    MIB2_STATS_INC(mib2.ipinunknownprotos);
    return ERR_OK;
#endif /* IP_REASSEMBLY */
  }

#if IP_OPTIONS_ALLOWED == 0 /* no support for IP options in the IP header? */

#if LWIP_IGMP
  /* 在IGMP訊息中有一個額外的「路由器警報」選項,我們允許但不監督 */
  /* 如果IP報文帶選項欄位,lwip只支援IGMP */
  if ((iphdr_hlen > IP_HLEN) &&  (IPH_PROTO(iphdr) != IP_PROTO_IGMP)) {
#else
  if (iphdr_hlen > IP_HLEN) { /* LWIP不支援接收處理帶選項欄位的IP報文。(IGMP協定除外) */
#endif /* LWIP_IGMP */
    LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("IP packet dropped since there were IP options (while IP_OPTIONS_ALLOWED == 0).\n"));
    pbuf_free(p);
    IP_STATS_INC(ip.opterr);
    IP_STATS_INC(ip.drop);
    /* unsupported protocol feature */
    MIB2_STATS_INC(mib2.ipinunknownprotos);
    return ERR_OK;
  }
#endif /* IP_OPTIONS_ALLOWED == 0 */

  /* 傳送到上層 */
  LWIP_DEBUGF(IP_DEBUG, ("ip4_input: \n"));
  ip4_debug_print(p);
  LWIP_DEBUGF(IP_DEBUG, ("ip4_input: p->len %"U16_F" p->tot_len %"U16_F"\n", p->len, p->tot_len));

  /* 更新這個全域性IP資料 */
  ip_data.current_netif = netif; /* 上層處理該封包的網路卡 */
  ip_data.current_input_netif = inp; /* 資料鏈路接收到該封包的網路卡 */
  ip_data.current_ip4_header = iphdr; /* IP報文首部 */
  ip_data.current_ip_header_tot_len = IPH_HL_BYTES(iphdr); /* IP首部長度(單位:位元組) */

#if LWIP_RAW /* RAW */
  /* 傳到RAW輸入 */
  raw_status = raw_input(p, inp);
  if (raw_status != RAW_INPUT_EATEN) /* RAW傳入失敗,那就傳給其它上層協定模組 */
#endif /* LWIP_RAW */
  {
    pbuf_remove_header(p, iphdr_hlen); /* 移到有效載荷,不需要檢查 */

    switch (IPH_PROTO(iphdr)) { /* 根據IP報文協定欄位傳入對應協定模組 */
#if LWIP_UDP
      case IP_PROTO_UDP: /* UDP協定 */
#if LWIP_UDPLITE
      case IP_PROTO_UDPLITE: /* UDP-Lite協定 */
#endif /* LWIP_UDPLITE */
        MIB2_STATS_INC(mib2.ipindelivers);
        udp_input(p, inp); /* 傳入UDP協定模組 */
        break;
#endif /* LWIP_UDP */
#if LWIP_TCP
      case IP_PROTO_TCP: /* TCP協定 */
        MIB2_STATS_INC(mib2.ipindelivers);
        tcp_input(p, inp); /* 傳入TCP協定模組 */
        break;
#endif /* LWIP_TCP */
#if LWIP_ICMP
      case IP_PROTO_ICMP: /* ICMP協定 */
        MIB2_STATS_INC(mib2.ipindelivers);
        icmp_input(p, inp); /* 傳入ICMP協定模組 */
        break;
#endif /* LWIP_ICMP */
#if LWIP_IGMP
      case IP_PROTO_IGMP: /* IGMP協定 */
        igmp_input(p, inp, ip4_current_dest_addr()); /* 傳入IGMP協定模組 */
        break;
#endif /* LWIP_IGMP */
      default: /* 其它 */
#if LWIP_RAW
        if (raw_status == RAW_INPUT_DELIVERED) {
          MIB2_STATS_INC(mib2.ipindelivers);
        } else
#endif /* LWIP_RAW */
        {
#if LWIP_ICMP
          /* 不是廣播包,也不是多播包,資料鏈路層給到我們網路卡不支援的協定,傳送ICMP目的協定不可達 */
          if (!ip4_addr_isbroadcast(ip4_current_dest_addr(), netif) &&
              !ip4_addr_ismulticast(ip4_current_dest_addr())) {
            pbuf_header_force(p, (s16_t)iphdr_hlen); /* 移動到ip頭,不需要檢查 */
            icmp_dest_unreach(p, ICMP_DUR_PROTO); /* 傳送ICMP目的協定不可達 */
          }
#endif /* LWIP_ICMP */

          LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("Unsupported transport protocol %"U16_F"\n", (u16_t)IPH_PROTO(iphdr)));

          IP_STATS_INC(ip.proterr);
          IP_STATS_INC(ip.drop);
          MIB2_STATS_INC(mib2.ipinunknownprotos);
        }
        pbuf_free(p); /* 丟棄,釋放pbuf */
        break;
    }
  }

  /* @todo: this is not really necessary... */
  ip_data.current_netif = NULL;
  ip_data.current_input_netif = NULL;
  ip_data.current_ip4_header = NULL;
  ip_data.current_ip_header_tot_len = 0;
  ip4_addr_set_any(ip4_current_src_addr());
  ip4_addr_set_any(ip4_current_dest_addr());

  return ERR_OK;
}

9.8.2 IP資料包轉發

如果封包給到我們網路卡資料鏈路層了,但是目的IP不是給我們網路卡IP層的,那可能是想通過我們網路卡轉發該包。

相關宏:

  • IP_FORWARD_ALLOW_TX_ON_RX_NETIF

    • 允許ip_forward()在接收到封包的netif上傳送封包。這應該只用於無線網路。
    • 為1時,請確保netif驅動程式正確標記傳入的鏈路層廣播/組播封包等使用相應的pbuf標誌!

呼叫ip4_forward()將封包轉發:

  • 先判斷IP包是否能轉發。主要呼叫ip4_canforward()判斷。

    • 勾點函數:LWIP_HOOK_IP4_CANFORWARD(src, dest)。現有使用者實現勾點裁定能否轉發當前IP包。
    • 鏈路層的廣播包。不能轉發。
    • 鏈路層的多播包。不能轉發。
    • 目的IP為255.x.x.x的IP包。不能轉發。
    • 目的IP為127.x.x.x(環回)的IP包。不能轉發。
    • 目的IP為169.254.x.x(本地鏈路IP)的IP包,不能轉發。
  • 匹配新網路卡。呼叫ip4_route_src(src, dest)路由匹配網路卡,進行轉發該IP包。

  • 檢查路由匹配成功的網路卡。

    • 一般情況下,是不能轉發回原鏈路的。
    • 如果需要轉發回原鏈路,一般只能用於無線網路卡。
  • IP報文TTL欄位值減1。如果TTL值為0,則,丟棄該IP包,並通過ICMP icmp_dest_unreach(struct pbuf *p, enum icmp_dur_type t)告知源端路由不可達。(如果當前報文也是ICMP報文,則不用回覆ICMP路由不可達)

  • TTL欄位值更新後,IP報文的首部校驗和欄位也要更新。

    • 技巧:由於IP報文首部只是TTL欄位減1,所以首部校驗和欄位只需要加1即可。
  • 使用新路由匹配的網路卡把當前IP包轉發出去。

    • 需要分片:呼叫ip4_frag()轉發出去。
    • 不需要分片:呼叫網路卡IP介面netif->output()轉發出去。

ip4_canforward()

/**
 * Determine whether an IP address is in a reserved set of addresses
 * that may not be forwarded, or whether datagrams to that destination
 * may be forwarded.
 * @param p the packet to forward
 * @return 1: can forward 0: discard
 */
static int
ip4_canforward(struct pbuf *p)
{
  u32_t addr = lwip_htonl(ip4_addr_get_u32(ip4_current_dest_addr()));

#ifdef LWIP_HOOK_IP4_CANFORWARD
  /* 先勾點函數裁定 */
  int ret = LWIP_HOOK_IP4_CANFORWARD(p, addr);
  if (ret >= 0) {
    return ret;
  }
#endif /* LWIP_HOOK_IP4_CANFORWARD */

  if (p->flags & PBUF_FLAG_LLBCAST) {
    /* don't route link-layer broadcasts */
    return 0;
  }
  if ((p->flags & PBUF_FLAG_LLMCAST) || IP_MULTICAST(addr)) {
    /* don't route link-layer multicasts (use LWIP_HOOK_IP4_CANFORWARD instead) */
    return 0;
  }
  if (IP_EXPERIMENTAL(addr)) {
    return 0;
  }
  if (IP_CLASSA(addr)) {
    u32_t net = addr & IP_CLASSA_NET;
    if ((net == 0) || (net == ((u32_t)IP_LOOPBACKNET << IP_CLASSA_NSHIFT))) {
      /* don't route loopback packets */
      return 0;
    }
  }
  return 1;
}

ip4_forward()

/**
 * Forwards an IP packet. It finds an appropriate route for the
 * packet, decrements the TTL value of the packet, adjusts the
 * checksum and outputs the packet on the appropriate interface.
 *
 * @param p the packet to forward (p->payload points to IP header)
 * @param iphdr the IP header of the input packet
 * @param inp the netif on which this packet was received
 */
static void
ip4_forward(struct pbuf *p, struct ip_hdr *iphdr, struct netif *inp)
{
  struct netif *netif;

  PERF_START;
  LWIP_UNUSED_ARG(inp);

  if (!ip4_canforward(p)) {
    goto return_noroute;
  }

  /* RFC3927 2.7:不轉發鏈路本地地址 */
  if (ip4_addr_islinklocal(ip4_current_dest_addr())) {
    LWIP_DEBUGF(IP_DEBUG, ("ip4_forward: not forwarding LLA %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
                           ip4_addr1_16(ip4_current_dest_addr()), ip4_addr2_16(ip4_current_dest_addr()),
                           ip4_addr3_16(ip4_current_dest_addr()), ip4_addr4_16(ip4_current_dest_addr())));
    goto return_noroute;
  }

  /* 路由匹配網路卡,找到將此IP包轉發到的網路卡 */
  netif = ip4_route_src(ip4_current_src_addr(), ip4_current_dest_addr());
  if (netif == NULL) {
    LWIP_DEBUGF(IP_DEBUG, ("ip4_forward: no forwarding route for %"U16_F".%"U16_F".%"U16_F".%"U16_F" found\n",
                           ip4_addr1_16(ip4_current_dest_addr()), ip4_addr2_16(ip4_current_dest_addr()),
                           ip4_addr3_16(ip4_current_dest_addr()), ip4_addr4_16(ip4_current_dest_addr())));
    /* @todo: send ICMP_DUR_NET? */
    goto return_noroute;
  }
#if !IP_FORWARD_ALLOW_TX_ON_RX_NETIF /* 不允許轉發到原鏈路(無線網路卡可以) */
  /* 不要將封包轉發到到達的同一個網路介面上 */
  if (netif == inp) {
    LWIP_DEBUGF(IP_DEBUG, ("ip4_forward: not bouncing packets back on incoming interface.\n"));
    goto return_noroute;
  }
#endif /* IP_FORWARD_ALLOW_TX_ON_RX_NETIF */

  /* 已經路由匹配到能轉發當前IP包的網路卡了 */

  /* TTL減1 */
  IPH_TTL_SET(iphdr, IPH_TTL(iphdr) - 1);

  if (IPH_TTL(iphdr) == 0) { /* TTL值已經到達0了 */
    MIB2_STATS_INC(mib2.ipinhdrerrors);
#if LWIP_ICMP
    /* 不要傳送ICMP報文來響應ICMP報文 */
    if (IPH_PROTO(iphdr) != IP_PROTO_ICMP) {
      icmp_time_exceeded(p, ICMP_TE_TTL);
    }
#endif /* LWIP_ICMP */
    return;
  }

  /* 更新IP報文首部校驗和。技巧:首部校驗和加1即可。注意網路位元組序。 */
  if (IPH_CHKSUM(iphdr) >= PP_HTONS(0xffffU - 0x100)) {
    IPH_CHKSUM_SET(iphdr, (u16_t)(IPH_CHKSUM(iphdr) + PP_HTONS(0x100) + 1));
  } else {
    IPH_CHKSUM_SET(iphdr, (u16_t)(IPH_CHKSUM(iphdr) + PP_HTONS(0x100)));
  }

  /* 當我們有多個netifs,其中至少有一個具有校驗和解除安裝功能時,IP轉發需要將各個校驗和欄位設定為0,以防止HW硬體演演算法計算無效的校驗和 */
  /* https://github.com/lwip-tcpip/lwip/commit/61c67fc2295e522c7a12175581d6928c3951c0bf */
  /* http://savannah.nongnu.org/bugs/?func=detailitem&item_id=56288 */
  /* 這裡有點沒看明白:如果網路卡沒有硬體校驗和解除安裝功能,lwip協定棧網路卡開啟CHECKSUM_GEN_IP宏使用軟體校驗和功能,那在這裡轉發IP包時,豈不是把封包校驗和欄位都清空了? */
  if (CHECKSUM_GEN_IP || NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_IP)) { /* 如果使用軟體生成了校驗和,這裡需要清空 */
    IPH_CHKSUM_SET(iphdr, 0);
  }
  switch (IPH_PROTO(iphdr)) { /* 上層協定的校驗和也清空 */
#if LWIP_UDP
#if LWIP_UDPLITE
  case IP_PROTO_UDPLITE:
#endif
  case IP_PROTO_UDP:
    if (CHECKSUM_GEN_UDP || NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_UDP)) {
      ((struct udp_hdr *)((u8_t *)iphdr + IPH_HL_BYTES(iphdr)))->chksum = 0;
    }
    break;
#endif
#if LWIP_TCP
  case IP_PROTO_TCP:
    if (CHECKSUM_GEN_TCP || NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_TCP)) {
      ((struct tcp_hdr *)((u8_t *)iphdr + IPH_HL_BYTES(iphdr)))->chksum = 0;
    }
    break;
#endif
#if LWIP_ICMP
  case IP_PROTO_ICMP:
    if (CHECKSUM_GEN_ICMP || NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_ICMP)) {
      ((struct icmp_hdr *)((u8_t *)iphdr + IPH_HL_BYTES(iphdr)))->chksum = 0;
    }
    break;
#endif
  default:
    /* there's really nothing to do here other than satisfying 'switch-default' */
    break;
  }

  LWIP_DEBUGF(IP_DEBUG, ("ip4_forward: forwarding packet to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
                         ip4_addr1_16(ip4_current_dest_addr()), ip4_addr2_16(ip4_current_dest_addr()),
                         ip4_addr3_16(ip4_current_dest_addr()), ip4_addr4_16(ip4_current_dest_addr())));

  IP_STATS_INC(ip.fw);
  MIB2_STATS_INC(mib2.ipforwdatagrams);
  IP_STATS_INC(ip.xmit);

  PERF_STOP("ip4_forward");
  /* 分片檢查&處理 */
  /* mtu為0,表示該網路卡不支援分片 */
  if (netif->mtu && (p->tot_len > netif->mtu)) { /* 需要分片 */
    if ((IPH_OFFSET(iphdr) & PP_NTOHS(IP_DF)) == 0) {
#if IP_FRAG
      /* 分片發包 */
      ip4_frag(p, netif, ip4_current_dest_addr());
#else /* IP_FRAG */
      /* @todo: send ICMP Destination Unreachable code 13 "Communication administratively prohibited"? */
#endif /* IP_FRAG */
    } else {
#if LWIP_ICMP
      /* 傳送ICMP目的不可達程式碼4: "Fragmentation Needed and DF Set" */
      icmp_dest_unreach(p, ICMP_DUR_FRAG);
#endif /* LWIP_ICMP */
    }
    return;
  }
  /* 不需要分片就直接發出IP層 */
  netif->output(netif, p, ip4_current_dest_addr());
  return;
return_noroute:
  MIB2_STATS_INC(mib2.ipoutnoroutes);
}

9.8.3 IP資料包重組

如果IP層收到一個IP報文,目的IP是給我們網路卡IP層的,且檢查當前IP包的首部是是一個分片包,則需要IP報文重組。

重組IP報文的原始碼實現比較複雜,所以需要耐心分析。

重灌IP報文比分片IP報文要難,就是因為,重灌IP報文的每個IP包到達的時間不是按順序的,會出現後發的IP分片包比先發的IP分片包早到達(網路路由問題),這需要我們按序重組好。

注意:lwip當前不支援IP首部帶選項欄位的IP報文進行分片和重組。

9.8.3.1 相關資料結構

程式碼的資料結構是非常重要的,通過對資料結構的邏輯處理能封裝出各種功能的API。

一個完整IP報文由多個IP分片包組成。

這個IP報文用struct ip_reassdata資料結構管理;

而每個IP分片包用struct ip_reass_helper資料結構管理。

  • 為了節省空間,這個資料結構和IP包pbuf的空間共用。把收到的分片pbuf的IP包首部重置為這個資料結構。因為最終的IP報文只需要一個IP首部即可,每個分片的IP首部空間可以利用起來。

lwip協定棧用全域性變數struct ip_reassdata *reassdatagrams;管理整個協定棧的各個在重組中的IP報文,為單向連結串列。

重組中的IP報文資料結構struct ip_reassdata

/** IP reassembly helper struct.
 * This is exported because memp needs to know the size.
 */
struct ip_reassdata {
  struct ip_reassdata *next; /* 單向連結串列節點 */
  struct pbuf *p; /* pbuf連結串列。是當前IP分片包的pbuf,各個分片的鏈,是由struct ip_reass_helper資料結構管理。 */
  struct ip_hdr iphdr; /* 當前IP報文的IP首部 */
  u16_t datagram_len; /* 已經收到的IP報長度或IP報文總長度。如果收到最後一個分片,則當前值為IP報文總長度。 */
  u8_t flags; /* 是否收到最後一個分片 */
  u8_t timer; /* 設定超時間隔 */
};

各個IP分片包的資料結構struct ip_reass_helper

struct ip_reass_helper {
  PACK_STRUCT_FIELD(struct pbuf *next_pbuf); /* 下一個IP分片 */
  PACK_STRUCT_FIELD(u16_t start); /* 分片中資料在IP報文的起始位置 */
  PACK_STRUCT_FIELD(u16_t end); /* 分片中資料在IP報文的結束位置 */
} PACK_STRUCT_STRUCT;

9.8.3.2 相關宏

IP_REASS_MAXAGE:預設15。為每個重組IP報文有效期。超時還沒重組好就需要刪除重組IP報文。

IP_REASS_MAX_PBUFS:系統重組IP報文所有pbuf節點上限值。

IP_REASS_FREE_OLDEST:若開啟,遇到系統重組IP報文所有pbuf節點達到系統上限值IP_REASS_MAX_PBUFS後,允許釋放舊的重組IP報文所有IP分片。

9.8.3.3 相關函數

ip4_reass(struct pbuf *p)

  • IPv4分片報文重組函數。
  • 供給lwip核心接收IP包時使用的API。
  • 不支援待選項欄位的IP報文進行重組。

ip_reass_enqueue_new_datagram(struct ip_hdr *fraghdr, int clen):新建一個重組IP報文條目並插入reassdatagrams連結串列中。

ip_reass_dequeue_datagram(struct ip_reassdata *ipr, struct ip_reassdata *prev):釋放一個重組IP報文條目。

ip_reass_free_complete_datagram(struct ip_reassdata *ipr, struct ip_reassdata *prev):釋放一個重組IP報文條目及其所有pbuf。

ip_reass_remove_oldest_datagram(struct ip_hdr *fraghdr, int pbufs_needed):刪除老的重組IP報文。

ip_reass_chain_frag_into_datagram_and_validate(struct ip_reassdata *ipr, struct pbuf *new_p, int is_last):插入IP分片到對應重組IP報文。

9.8.3.4 ip4_reass()

原始碼參考:ip4_frag.c

IP報文重組呼叫ip4_reass()先分析該函數的總體框架,後面再分析各個函數的細節:

  • 檢查IP分片包。

    • 收到一個IP分片後,檢查IP分片的合法性。IP首部長度是否符合要求。目前LWIP不支援帶選項的IP包分片和重組。
    • IP首部長度不能大於IP包長度。
    • 確保重組中的IP報文所有pbuf節點不能超過系統上限值IP_REASS_MAX_PBUFS。如果預判超出了,需要呼叫ip_reass_remove_oldest_datagram()刪除最老的重組中的IP報文,直至夠空間記錄當前IP分片包的pbuf節點數為止。
  • 記錄IP分片包到reassdatagrams連結串列中對應IP報文。

    • 找到當前IP分片的重組IP報文:檢索reassdatagrams連結串列,是否有當前IP分片的IP報文。如果沒有,需要呼叫ip_reass_enqueue_new_datagram()建立一個新的重組IP報文資料結構,並插入到reassdatagrams連結串列中。如果有,還需要檢查當前IP分片是否是IP報文中的第一個IP分片包,如果是,需要把這個IP分片包的IP首部更新到重組IP報文資料結構的IP報文首部欄位ip_reassdata->iphdr

      • 匹配重組IP報文條件:源IP、目的IP、IP標識這三個欄位一致即可。
    • 預判IP報文是否溢位:如果當前IP分片包是最後一個,可以通過IP分片偏移量加上當前IP分片包的長度可以計算出完整的IP報文長度,如果這個長度溢位IP報文的長度欄位,則丟棄,並刪除重組中的IP報文。

    • 當前IP分片插入重組IP報文的IP分片連結串列中。呼叫ip_reass_chain_frag_into_datagram_and_validate()將其插入。返回值由以下三個:

      • IP_REASS_VALIDATE_TELEGRAM_FINISHED:所有IP分片已經接收完畢,可以完成IP報文重組工作。
      • IP_REASS_VALIDATE_PBUF_QUEUED:當前IP分片正常插入,但是當前IP報文還沒有接收完所有IP分片。
      • IP_REASS_VALIDATE_PBUF_DROPPED:當前IP分片插入失敗,即是丟棄當前IP分片。
    • 更新reassdatagrams連結串列中所有pbuf節點的數量值ip_reass_pbufcount(全域性)。因為系統設定有上限IP_REASS_MAX_PBUFS,所以要動態記錄。

  • 當前IP分片是否為最後一個分片。

    • 如果是最後一個分片,則更新重組IP報文資料結構中的長度值為總長度值ip_reassdata->datagram_len,標記重組IP報文收到最後一個IP分片ip_reassdata->flags |= IP_REASS_FLAG_LASTFRAG
  • IP報文的所有IP分片包是否已經接收完畢。

    • 如果所有IP分片都收到了,則可進行重組。
  • 重組完整IP報文。遍歷所有IP分片,併合並。節省空間。

    • 統計IP報文的總長度。首部+資料=IP_HLEN+ip_reassdata->datagram_len
    • 把重組IP報文資料結構中的IP首部欄位拷貝到第一個分片的IP首部作為完整IP報文的首部。
    • 遍歷重組IP報文中所有IP分片,將其合併,除了首個IP分片的IP首部作為完整IP報文的首部外,其它IP分片的IP首部都移除。
    • 至此,完整的IP報文pbuf重組完成。
/**
 * Reassembles incoming IP fragments into an IP datagram.
 *
 * @param p points to a pbuf chain of the fragment
 * @return NULL if reassembly is incomplete, ? otherwise
 */
struct pbuf *
ip4_reass(struct pbuf *p)
{
  struct pbuf *r; /* pbuf指標 */
  struct ip_hdr *fraghdr; /* IP首部指標 */
  struct ip_reassdata *ipr; /* 重組IP報文指標 */
  struct ip_reass_helper *iprh; /* IP分片節點管理資料結構指標 */
  u16_t offset, len, clen; /* 分片偏移量,分片長度,分片pbuf節點數 */
  u8_t hlen; /* IP分片首部長度 */
  int valid; /* 插入IP分片後的結果 */
  int is_last; /* 最後一個分片標誌 */

  IPFRAG_STATS_INC(ip_frag.recv);
  MIB2_STATS_INC(mib2.ipreasmreqds);

  fraghdr = (struct ip_hdr *)p->payload; /* 獲取IP分片首部 */

  if (IPH_HL_BYTES(fraghdr) != IP_HLEN) { /* 不支援帶選項欄位的IP報文重組 */
    LWIP_DEBUGF(IP_REASS_DEBUG, ("ip4_reass: IP options currently not supported!\n"));
    IPFRAG_STATS_INC(ip_frag.err);
    goto nullreturn;
  }

  offset = IPH_OFFSET_BYTES(fraghdr); /* 獲取分片偏移量 */
  len = lwip_ntohs(IPH_LEN(fraghdr)); /* 獲取分片包長度 */
  hlen = IPH_HL_BYTES(fraghdr); /* 獲取分片首部長度 */
  if (hlen > len) { /* 校驗 */
    /* invalid datagram */
    goto nullreturn;
  }
  len = (u16_t)(len - hlen); /* 獲取IP分片資料欄位長度 */

  /* 檢查是否允許我們排隊更多的資料包 */
  clen = pbuf_clen(p); /* 獲取當前IP分片包含多少個pbuf節點 */
  /* 預測當前分片插入重組IP報文後,整個系統的重組IP報文的所有pbuf節點是否超過系統上限值IP_REASS_MAX_PBUFS。
     若超過,需要釋放老的重組IP報文來騰出空間 */
  if ((ip_reass_pbufcount + clen) > IP_REASS_MAX_PBUFS) { /* 預測的結果超過系統上限值 */
#if IP_REASS_FREE_OLDEST /* 允許釋放老的重組IP報文 */
    if (!ip_reass_remove_oldest_datagram(fraghdr, clen) || /* 釋放失敗 */
        ((ip_reass_pbufcount + clen) > IP_REASS_MAX_PBUFS)) /* 或者儘可能釋放了,但是還是不能滿足當前IP分片的插入 */
#endif /* IP_REASS_FREE_OLDEST */
    { /* 需要丟棄當前IP分片 */
      /* 沒有資料包被釋放,仍然有太多的pbuf進入佇列 */
      LWIP_DEBUGF(IP_REASS_DEBUG, ("ip4_reass: Overflow condition: pbufct=%d, clen=%d, MAX=%d\n",
                                   ip_reass_pbufcount, clen, IP_REASS_MAX_PBUFS));
      IPFRAG_STATS_INC(ip_frag.memerr);
      /* @todo: 傳送ICMP時間超過這裡? */
      /* 丟棄當前IP分片 */
      goto nullreturn;
    }
  }

  /* 在當前資料包佇列中查詢分片所屬的重組IP報文,記住佇列中的前一個,以便以後退出佇列 */
  for (ipr = reassdatagrams; ipr != NULL; ipr = ipr->next) {
    /* 檢查傳入的片段是否與當前出現在重組緩衝區中的片段相匹配。如果是這樣,我們繼續將這個片段複製到緩衝區中 */
    if (IP_ADDRESSES_AND_ID_MATCH(&ipr->iphdr, fraghdr)) { /* 匹配成功。根據源IP、目的IP和IP標識這三個欄位進行匹配。 */
      LWIP_DEBUGF(IP_REASS_DEBUG, ("ip4_reass: matching previous fragment ID=%"X16_F"\n",
                                   lwip_ntohs(IPH_ID(fraghdr))));
      IPFRAG_STATS_INC(ip_frag.cachehit);
      break; /* 跳出 */
    }
  }

  if (ipr == NULL) { /* 還沒有當前IP分片的重組IP報文呢,需要新建 */
    /* 建立新的重組IP報文,並插入到全域性連結串列reassdatagrams */
    ipr = ip_reass_enqueue_new_datagram(fraghdr, clen);
    if (ipr == NULL) { /* 建立或插入失敗,丟棄當前IP分片 */
      goto nullreturn;
    }
  } else { /* 已經存在 */
    if (((lwip_ntohs(IPH_OFFSET(fraghdr)) & IP_OFFMASK) == 0) && /* IP報文的第一個IP分片 */
        ((lwip_ntohs(IPH_OFFSET(&ipr->iphdr)) & IP_OFFMASK) != 0)) { /* 還沒有記錄過第一個分片的IP首部 */
      /* 最先收到的IP分片不一定是IP報文的第一個IP分片。這裡需要第一個IP分片的首部是因為對於超過ICMP時間和之後,為了複製所有選項 */
      SMEMCPY(&ipr->iphdr, fraghdr, IP_HLEN); /* IP報文的第一個IP分片的IP首部作為整個IP報文首部的基礎 */
    }
  }

  /* 此時,我們已經建立了一個新重組IP報文,或者指向一個現有重組IP報文 */

  /* 檢查整個IP報文的長度欄位是否合法 */
  /* 可以通過最後一個IP分片的偏移量和分片資料區長度可以計算出完整的IP報文的資料區總長度 */
  is_last = (IPH_OFFSET(fraghdr) & PP_NTOHS(IP_MF)) == 0;
  if (is_last) {
    u16_t datagram_len = (u16_t)(offset + len);
    if ((datagram_len < offset) || (datagram_len > (0xFFFF - IP_HLEN))) {
      /* u16_t 溢位,無法處理此操作 */
      goto nullreturn_ipr;
    }
  }
  /* 找到插入這個pbuf的正確位置 */
  /* @todo: 如果片段重疊,則修剪pbufs */
  valid = ip_reass_chain_frag_into_datagram_and_validate(ipr, p, is_last);
  if (valid == IP_REASS_VALIDATE_PBUF_DROPPED) { /* 插入失敗,丟棄當前IP分片 */
    goto nullreturn_ipr;
  }
  /* 如果我們來到這裡,pbuf已經被加入佇列 */

  /* 更新重組IP報文資料結構相關欄位 */

  ip_reass_pbufcount = (u16_t)(ip_reass_pbufcount + clen); /* 更新全域性當前所有重組IP報文的pbuf節點數,用於判斷是否踩上限線 */
  if (is_last) { /* 如果是最後一個分片,則可以知道整個IP報文的長度 */
    u16_t datagram_len = (u16_t)(offset + len); /* 偏移量+當前分片IP包資料區長度就是IP報文資料區總長度 */
    ipr->datagram_len = datagram_len; /* 轉為記錄IP報文資料區總長度 */
    ipr->flags |= IP_REASS_FLAG_LASTFRAG; /* 標記已經收到最後一個IP分片,且上面長度值改為記錄IP報文資料區總長度 */
    LWIP_DEBUGF(IP_REASS_DEBUG,
                ("ip4_reass: last fragment seen, total len %"S16_F"\n",
                 ipr->datagram_len));
  }

  if (valid == IP_REASS_VALIDATE_TELEGRAM_FINISHED) { /* 全部IP分片接收完畢,可以進行重組 */
    struct ip_reassdata *ipr_prev;
    u16_t datagram_len = (u16_t)(ipr->datagram_len + IP_HLEN); /* IP報文總長度 */

    r = ((struct ip_reass_helper *)ipr->p->payload)->next_pbuf; /* r指向第二個分片 */

    /* 將原始IP頭複製回第一個pbuf,作為完整IP報文的IP首部 */
    fraghdr = (struct ip_hdr *)(ipr->p->payload);
    SMEMCPY(fraghdr, &ipr->iphdr, IP_HLEN);
    IPH_LEN_SET(fraghdr, lwip_htons(datagram_len));
    IPH_OFFSET_SET(fraghdr, 0); /* 清空偏移量和標誌欄位 */
    IPH_CHKSUM_SET(fraghdr, 0); /* 清空首部校驗和 */
    /* @todo: do we need to set/calculate the correct checksum? */
#if CHECKSUM_GEN_IP
    IF__NETIF_CHECKSUM_ENABLED(ip_current_input_netif(), NETIF_CHECKSUM_GEN_IP) {
      IPH_CHKSUM_SET(fraghdr, inet_chksum(fraghdr, IP_HLEN)); /* 重置IP首部校驗和 */
    }
#endif /* CHECKSUM_GEN_IP */

    p = ipr->p; /* p指向第一個分片 */

    /* 遍歷所有分片將其合併到第一個分片的pbuf中 */
    while (r != NULL) {
      iprh = (struct ip_reass_helper *)r->payload; /* 獲取IP分片管理區 */

      pbuf_remove_header(r, IP_HLEN); /* 隱藏每個後續片段的IP頭 */
      pbuf_cat(p, r); /* 合併分片 */
      r = iprh->next_pbuf; /* 指向下一個分片。遍歷 */
    }
  
    /* 重組完畢,需要移除重組IP報文相關資源 */

    if (ipr == reassdatagrams) { /* reassdatagrams連結串列是一個沒有哨兵的單向非迴圈連結串列。如果移除的是首節點,則不需要記錄前一個節點 */
      ipr_prev = NULL;
    } else { /* 如果不是首個節點,則需要知道這個節點的前一個節點,才能把當前節點從單向連結串列中正常刪除 */
      for (ipr_prev = reassdatagrams; ipr_prev != NULL; ipr_prev = ipr_prev->next) {
        if (ipr_prev->next == ipr) {
          break; /* 找到了 */
        }
      }
    }

    /* 釋放重組IP報文的節點 */
    ip_reass_dequeue_datagram(ipr, ipr_prev);

    /* 並調整當前排隊等待重組的pbuf的數量 */
    clen = pbuf_clen(p);
    LWIP_ASSERT("ip_reass_pbufcount >= clen", ip_reass_pbufcount >= clen);
    ip_reass_pbufcount = (u16_t)(ip_reass_pbufcount - clen);

    MIB2_STATS_INC(mib2.ipreasmoks);

    /* 返回重組後的IP報文pbuf */
    return p;
  }
  /* 還沒收到所有IP分片 */
  LWIP_DEBUGF(IP_REASS_DEBUG, ("ip_reass_pbufcount: %d out\n", ip_reass_pbufcount));
  return NULL;

nullreturn_ipr: /* 丟棄當前IP分片 */
  LWIP_ASSERT("ipr != NULL", ipr != NULL);
  if (ipr->p == NULL) {
    /* 在建立新的資料包條目後刪除pbuf:也刪除該條目 */
    LWIP_ASSERT("not firstalthough just enqueued", ipr == reassdatagrams);
    ip_reass_dequeue_datagram(ipr, NULL);
  }

nullreturn:
  LWIP_DEBUGF(IP_REASS_DEBUG, ("ip4_reass: nullreturn\n"));
  IPFRAG_STATS_INC(ip_frag.drop);
  pbuf_free(p); /* 丟棄當前IP分片,釋放pbuf資源 */
  return NULL;
}

9.8.3.5 ip_reass_enqueue_new_datagram()

ip_reass_enqueue_new_datagram()

  • 申請重組IP報文節點ip_reassdata資料結構的空間。
  • 如果MEMP_REASSDATA記憶體池空間不足,則可以釋放老的重組IP報文節點。
  • 初始化該結構體。
  • 有效期設定為IP_REASS_MAXAGE
  • 插入reassdatagrams連結串列。
  • 儲存IP分片包首部。
/**
 * Enqueues a new fragment into the fragment queue
 * @param fraghdr points to the new fragments IP hdr
 * @param clen number of pbufs needed to enqueue (used for freeing other datagrams if not enough space)
 * @return A pointer to the queue location into which the fragment was enqueued
 */
static struct ip_reassdata *
ip_reass_enqueue_new_datagram(struct ip_hdr *fraghdr, int clen)
{
  struct ip_reassdata *ipr;
#if ! IP_REASS_FREE_OLDEST
  LWIP_UNUSED_ARG(clen);
#endif

  /* 申請一個新的reassdata結構體 */
  ipr = (struct ip_reassdata *)memp_malloc(MEMP_REASSDATA);
  if (ipr == NULL) {
#if IP_REASS_FREE_OLDEST
    /* 空間不足就釋放老的重組IP報文條目 */
    if (ip_reass_remove_oldest_datagram(fraghdr, clen) >= clen) {
      ipr = (struct ip_reassdata *)memp_malloc(MEMP_REASSDATA);
    }
    if (ipr == NULL)
#endif /* IP_REASS_FREE_OLDEST */
    {
      IPFRAG_STATS_INC(ip_frag.memerr);
      LWIP_DEBUGF(IP_REASS_DEBUG, ("Failed to alloc reassdata struct\n"));
      return NULL; /* 申請資源失敗 */
    }
  }
  memset(ipr, 0, sizeof(struct ip_reassdata));
  ipr->timer = IP_REASS_MAXAGE;/* 有效期賦值 */

  /* 插入單向連結串列頭 */
  ipr->next = reassdatagrams;
  reassdatagrams = ipr;
  /* 複製IP頭,以便稍後進行測試和輸入 */
  /* @todo: no ip options supported? */
  SMEMCPY(&(ipr->iphdr), fraghdr, IP_HLEN);
  return ipr;
}

9.8.3.6 ip_reass_dequeue_datagram()

ip_reass_dequeue_datagram()

  • 釋放重組IP報文條目資源。
  • 在過期還沒重組好,或者已經重組好,或者需要為新的重組IP報文騰空間,都需要把不需要的重組IP報文刪除,並從連結串列中移除。
  • ipr:需要刪除的重組IP報文節點。
  • prev:被刪除節點的前一個節點。由呼叫者提供。「奇怪的設計」
/**
 * Dequeues a datagram from the datagram queue. Doesn't deallocate the pbufs.
 * @param ipr points to the queue entry to dequeue
 */
static void
ip_reass_dequeue_datagram(struct ip_reassdata *ipr, struct ip_reassdata *prev)
{
  if (reassdatagrams == ipr) { /* 如果刪除的是連結串列頭,需要更新連結串列頭 */
    /* 更新連結串列頭 */
    reassdatagrams = ipr->next;
  } else {
    /* 它不是第一個,所以它必須有一個有效的'prev' */
    LWIP_ASSERT("sanity check linked list", prev != NULL);
    prev->next = ipr->next; /* 從連結串列中移除當前重組IP報文條目 */
  }

  /* 釋放資源 */
  memp_free(MEMP_REASSDATA, ipr);
}

9.8.3.7 ip_reass_free_complete_datagram()

ip_reass_free_complete_datagram()

  • 釋放重組IP報文節點及其所有pbuf。
  • ICMP:如果收到了第一個IP分片,在重組刪除時,需要返回一個ICMP超時。
  • 釋放重組IP報文的所有pbuf。
  • 釋放重組IP報文。
/**
 * Free a datagram (struct ip_reassdata) and all its pbufs.
 * Updates the total count of enqueued pbufs (ip_reass_pbufcount),
 * SNMP counters and sends an ICMP time exceeded packet.
 *
 * @param ipr datagram to free
 * @param prev the previous datagram in the linked list
 * @return the number of pbufs freed
 */
static int
ip_reass_free_complete_datagram(struct ip_reassdata *ipr, struct ip_reassdata *prev)
{
  u16_t pbufs_freed = 0;
  u16_t clen;
  struct pbuf *p;
  struct ip_reass_helper *iprh;

  LWIP_ASSERT("prev != ipr", prev != ipr);
  if (prev != NULL) {
    LWIP_ASSERT("prev->next == ipr", prev->next == ipr);
  }

  MIB2_STATS_INC(mib2.ipreasmfails);
#if LWIP_ICMP
  iprh = (struct ip_reass_helper *)ipr->p->payload;
  if (iprh->start == 0) {
    /* 收到了第一個分片報文,傳送ICMP超時 */
    /* 首先,從r->p中取出第一個pbuf. */
    p = ipr->p;
    ipr->p = iprh->next_pbuf;
    /* 然後,將原始頭部複製過去 */
    SMEMCPY(p->payload, &ipr->iphdr, IP_HLEN);
    icmp_time_exceeded(p, ICMP_TE_FRAG);
    clen = pbuf_clen(p);
    LWIP_ASSERT("pbufs_freed + clen <= 0xffff", pbufs_freed + clen <= 0xffff);
    pbufs_freed = (u16_t)(pbufs_freed + clen);
    pbuf_free(p); /* 釋放pbuf */
  }
#endif /* LWIP_ICMP */

  /* 首先,free接收的所有pbufs。需要分別釋放各個pbuf,因為它們還沒有被連結 */
  p = ipr->p;
  while (p != NULL) {
    struct pbuf *pcur;
    iprh = (struct ip_reass_helper *)p->payload;
    pcur = p;
    /* 在釋放之前獲取下一個指標 */
    p = iprh->next_pbuf;
    clen = pbuf_clen(pcur);
    LWIP_ASSERT("pbufs_freed + clen <= 0xffff", pbufs_freed + clen <= 0xffff);
    pbufs_freed = (u16_t)(pbufs_freed + clen);
    pbuf_free(pcur);
  }
  /* 然後,從列表中解耦結構ip_reassdata並釋放它 */
  ip_reass_dequeue_datagram(ipr, prev);
  LWIP_ASSERT("ip_reass_pbufcount >= pbufs_freed", ip_reass_pbufcount >= pbufs_freed);
  ip_reass_pbufcount = (u16_t)(ip_reass_pbufcount - pbufs_freed);

  return pbufs_freed;
}

9.8.3.8 ip_reass_remove_oldest_datagram()

ip_reass_remove_oldest_datagram()

  • 刪除老的重組IP報文。
  • IP_REASS_FREE_OLDEST宏控制。
  • fraghdr:當前IP分片。傳入該引數是因為防止在遍歷連結串列刪除老的IP重組IP報文時,跳過這個重組IP報文,因為我們的目的就是為這個重組IP報文新來的IP分片騰空間。
  • pbufs_needed:需要騰出的pbuf數。
  • 邏輯簡單:遍歷、找到最老的非當前重組IP報文的重組IP報文節點、刪除。一直遍歷刪除到夠空閒pbuf數或者沒有其它重組IP報文節點為止。
#if IP_REASS_FREE_OLDEST
/**
 * Free the oldest datagram to make room for enqueueing new fragments.
 * The datagram 'fraghdr' belongs to is not freed!
 *
 * @param fraghdr IP header of the current fragment
 * @param pbufs_needed number of pbufs needed to enqueue
 *        (used for freeing other datagrams if not enough space)
 * @return the number of pbufs freed
 */
static int
ip_reass_remove_oldest_datagram(struct ip_hdr *fraghdr, int pbufs_needed)
{
  /* @todo Can't we simply remove the last datagram in the linked list behind reassdatagrams? */
  struct ip_reassdata *r, *oldest, *prev, *oldest_prev;
  int pbufs_freed = 0, pbufs_freed_current;
  int other_datagrams;

  /* 釋放資料包,直到允許進入'pbufs_needed' pbufs佇列,但不要釋放'fraghdr'所屬的資料包! */
  do { /* 外迴圈:每次迴圈最多釋放一個重組IP報文,但是不一定釋放夠pbuf節點數。 */
    oldest = NULL;
    prev = NULL;
    oldest_prev = NULL; /* 被刪除的節點的前一個節點。單向連結串列刪除節點的邏輯需要。 */
    other_datagrams = 0;
    r = reassdatagrams;
    while (r != NULL) { /* 內迴圈:遍歷reassdatagrams,找出其它最老的重組IP報文 */
      if (!IP_ADDRESSES_AND_ID_MATCH(&r->iphdr, fraghdr)) { /* 和fraghdr不是同一個資料包,都需要被檢索 */
        /* 記錄和fraghdr不是同一個資料包個數 */
        other_datagrams++;
        if (oldest == NULL) {
          oldest = r;
          oldest_prev = prev;
        } else if (r->timer <= oldest->timer) {
          /* 比之前最老的還要老 */
          oldest = r;
          oldest_prev = prev;
        }
      }
      if (r->next != NULL) {
        prev = r;
      }
      r = r->next;
    }
    if (oldest != NULL) {
      /* 釋放資源 */
      pbufs_freed_current = ip_reass_free_complete_datagram(oldest, oldest_prev);
      pbufs_freed += pbufs_freed_current; /* 記錄已經釋放了多少個pbuf節點 */
    }
  } while ((pbufs_freed < pbufs_needed) && (other_datagrams > 1)); /* 如果釋放的pbuf節點滿足要求,或者沒有其它pbuf節點可以釋放了,就退出迴圈 */
  return pbufs_freed; /* 返回釋放pbuf節點的個數 */
}
#endif /* IP_REASS_FREE_OLDEST */

9.8.3.9 ip_reass_chain_frag_into_datagram_and_validate()

ip_reass_chain_frag_into_datagram_and_validate()

  • 檢查和插入一個分片到重組資料包中。

  • ipr:重組IP資料包。

  • new_p:新的分片。

  • is_last:是否是最後一個分片。

  • 返回:

    • IP_REASS_VALIDATE_TELEGRAM_FINISHED:當前重組IP資料包已經收到所有分片。
    • IP_REASS_VALIDATE_PBUF_QUEUED:分片成功插入重組IP資料包中,但是該重組IP資料包還沒有接收到所有IP分片。
    • IP_REASS_VALIDATE_PBUF_DROPPED:插入分片失敗。
  • 獲取分片IP首部資訊統計到重組IP資料包管理中。

  • 插入分片到重組IP資料包的分片連結串列中。

  • 如果最後一個IP分片收到了,就檢查下所有分片是否都收到。

/**
 * Chain a new pbuf into the pbuf list that composes the datagram.  The pbuf list
 * will grow over time as  new pbufs are rx.
 * Also checks that the datagram passes basic continuity checks (if the last
 * fragment was received at least once).
 * @param ipr points to the reassembly state
 * @param new_p points to the pbuf for the current fragment
 * @param is_last is 1 if this pbuf has MF==0 (ipr->flags not updated yet)
 * @return see IP_REASS_VALIDATE_* defines
 */
static int
ip_reass_chain_frag_into_datagram_and_validate(struct ip_reassdata *ipr, struct pbuf *new_p, int is_last)
{
  struct ip_reass_helper *iprh, *iprh_tmp, *iprh_prev = NULL;
  struct pbuf *q;
  u16_t offset, len;
  u8_t hlen;
  struct ip_hdr *fraghdr;
  int valid = 1;

  /* 從當前分片中提取IP長度和IP分片偏移量 */
  fraghdr = (struct ip_hdr *)new_p->payload;
  len = lwip_ntohs(IPH_LEN(fraghdr));
  hlen = IPH_HL_BYTES(fraghdr);
  if (hlen > len) {
    /* 無效的資料包 */
    return IP_REASS_VALIDATE_PBUF_DROPPED;
  }
  len = (u16_t)(len - hlen); /* 獲取IP分片資料區長度 */
  offset = IPH_OFFSET_BYTES(fraghdr); /* 獲取偏移量 */

  /* IP分片的首部有用的資訊已經統計到重組IP報文節點首部了,
     即是IP分片的首部中的資料可以丟棄,該空間可以利用起來,
     當前用於分片連線節點,指向下一個分片。
     即是把IP分片的首部部分空間重置為struct ip_reass_helper資料結構。 */
  LWIP_ASSERT("sizeof(struct ip_reass_helper) <= IP_HLEN",
              sizeof(struct ip_reass_helper) <= IP_HLEN);
  iprh = (struct ip_reass_helper *)new_p->payload; /* 提前分片IP首部重置為struct ip_reass_helper */
  iprh->next_pbuf = NULL;
  iprh->start = offset; /* 偏移量起始 */
  iprh->end = (u16_t)(offset + len);  /* 偏移量尾部 */
  if (iprh->end < offset) {
    /* u16_t 溢位,無法處理此操作 */
    return IP_REASS_VALIDATE_PBUF_DROPPED;
  }

  /* 插入分片連結串列。按分片偏移量升序插入 */
  for (q = ipr->p; q != NULL;) { /* 遍歷分片連結串列 */
    iprh_tmp = (struct ip_reass_helper *)q->payload; /* 分片節點 */
    if (iprh->start < iprh_tmp->start) { /* 找到位置 */
      iprh->next_pbuf = q; /* 插入操作(連結串列) */
      if (iprh_prev != NULL) { /* 插入位置前面也有IP分片 */
        /* 而不是具有最低偏移量的分片 */
#if IP_REASS_CHECK_OVERLAP
        if ((iprh->start < iprh_prev->end) || (iprh->end > iprh_tmp->start)) {
          /* 插入分片與前一分片或後一分片重疊,丟棄 */
          return IP_REASS_VALIDATE_PBUF_DROPPED;
        }
#endif /* IP_REASS_CHECK_OVERLAP */
        iprh_prev->next_pbuf = new_p; /* 插入操作(連結串列) */
        if (iprh_prev->end != iprh->start) {
          /* 前一個分片與當前分片不連續,中間還缺資料,所以可以標記還沒有接收到所有IP分片 */
          valid = 0;
        }
      } else { /* 當前分片插入的位置在接收到的所有分片之前 */
#if IP_REASS_CHECK_OVERLAP
        if (iprh->end > iprh_tmp->start) {
          /* 分片與下一個分片重疊,丟棄 */
          return IP_REASS_VALIDATE_PBUF_DROPPED;
        }
#endif /* IP_REASS_CHECK_OVERLAP */
        /* 更新連結串列頭 */
        ipr->p = new_p;
      }
      break;
    } else if (iprh->start == iprh_tmp->start) { /* 收到重疊IP分片,丟棄 */
      return IP_REASS_VALIDATE_PBUF_DROPPED;
#if IP_REASS_CHECK_OVERLAP
    } else if (iprh->start < iprh_tmp->end) { /* 重疊:直接丟棄,不需要保留新資料包 */
      return IP_REASS_VALIDATE_PBUF_DROPPED;
#endif /* IP_REASS_CHECK_OVERLAP */
    } else { /* 插入分片偏移量比所有分片都要大,即是要插到連結串列最後 */
      /* 檢查到目前為止收到的碎片是否沒有洞 */
      if (iprh_prev != NULL) {
        if (iprh_prev->end != iprh_tmp->start) {
          /* 資料不連續,當前分片和前一個分片之間缺少分片,標記下未接收完所有IP分片 */
          valid = 0;
        }
      }
    }
    q = iprh_tmp->next_pbuf; /* 遍歷下一個分片 */
    iprh_prev = iprh_tmp; /* 儲存前一個分片 */
  }

  /* 如果q為空,即是到達連結串列尾了 */
  if (q == NULL) {
    if (iprh_prev != NULL) { /* 如果 */
      /* 這是(目前),偏移量最大的分片:插入連結串列尾 */
#if IP_REASS_CHECK_OVERLAP
      LWIP_ASSERT("check fragments don't overlap", iprh_prev->end <= iprh->start);
#endif /* IP_REASS_CHECK_OVERLAP */
      iprh_prev->next_pbuf = new_p; /* 插入 */
      if (iprh_prev->end != iprh->start) { /* 還缺少IP分片 */
        valid = 0; /* 標記還沒接收到所有IP分片 */
      }
    } else { /* 當前重組IP報文中還沒有IP分片 */
#if IP_REASS_CHECK_OVERLAP
      LWIP_ASSERT("no previous fragment, this must be the first fragment!",
                  ipr->p == NULL);
#endif /* IP_REASS_CHECK_OVERLAP */
      /* 這是我們收到的這個IP資料包的第一個分片 */
      ipr->p = new_p;
    }
  }

  /* 至此,插入完畢,檢查是否已經收到所有IP分片 */

  /* 如果我們已經收到了最後一個碎片 */
  if (is_last || ((ipr->flags & IP_REASS_FLAG_LASTFRAG) != 0)) { /* 最後一個分片收到了 */
    if (valid) { /* 前面遍歷插入分片前面的所有分片時也沒有發現有資料不連續 */
      /* 繼續檢查後面沒有被遍歷過的IP分片 */
      if ((ipr->p == NULL) || (((struct ip_reass_helper *)ipr->p->payload)->start != 0)) { /* 還沒收到第一個分片 */
        valid = 0; /* 標記還沒接收到所有IP分片 */
      } else { /* 首尾IP分片都收到了,需要遍歷剩餘的 */
        /* 並且檢查這個資料包之後是否沒有漏洞 */
        iprh_prev = iprh;
        q = iprh->next_pbuf;
        while (q != NULL) { /* 遍歷 */
          iprh = (struct ip_reass_helper *)q->payload;
          if (iprh_prev->end != iprh->start) { /* 發現漏洞 */
            valid = 0; /* 標記還沒有接收到所有IP分片 */
            break; /* 退出遍歷 */
          }
          iprh_prev = iprh;
          q = iprh->next_pbuf;
        }
        if (valid) { /* 至此,說明當前IP報文的所有IP分片已到達,打個log慶祝下 */
          LWIP_ASSERT("sanity check", ipr->p != NULL);
          LWIP_ASSERT("sanity check",
                      ((struct ip_reass_helper *)ipr->p->payload) != iprh);
          LWIP_ASSERT("validate_datagram:next_pbuf!=NULL",
                      iprh->next_pbuf == NULL);
        }
      }
    }
    /* 按需返回 */
    return valid ? IP_REASS_VALIDATE_TELEGRAM_FINISHED : IP_REASS_VALIDATE_PBUF_QUEUED;
  }
  /* 至此,IP分片插入正常,但是還沒有收到所有IP分片 */
  return IP_REASS_VALIDATE_PBUF_QUEUED;
}

9.8.3.10 重組IP資料包的超時機制

每個重組的IP資料包都有生命週期,超時都還沒接收完所有IP分片包,則需要放棄等待剩餘分片,並釋放該重組IP資料包所有資源。

決定刪除重組的IP資料包時,需要返回ICMP超時到網路中告知對端放棄本次IP報文接收。

節拍由IP_TMR_INTERVAL決定,預設1000,即是1秒跑一次。

每個重組IP報文最大時間為IP_REASS_MAXAGE,預設15。即是系統收到第一個得到的分片IP開始計時,在15秒內沒有接收完所有IP報文,便要放棄本次重組。

ip_reass_tmr()

  • 概述的目的就是遍歷重組IP報文連結串列,檢查每個正在重組的IP報文有效期,過期的刪除,未過期的減少有效期。
/**
 * Reassembly timer base function
 * for both NO_SYS == 0 and 1 (!).
 *
 * Should be called every 1000 msec (defined by IP_TMR_INTERVAL).
 */
void
ip_reass_tmr(void)
{
  struct ip_reassdata *r, *prev = NULL;

  r = reassdatagrams;
  while (r != NULL) {
    /* 減量計時器,一旦它達到0,清理不完整的分片 */
    if (r->timer > 0) {
      r->timer--; /* 有效期減少 */
      LWIP_DEBUGF(IP_REASS_DEBUG, ("ip_reass_tmr: timer dec %"U16_F"\n", (u16_t)r->timer));
      prev = r;
      r = r->next; /* 遍歷下一個 */
    } else { /* 當前重組IP條目已經過期了 */
      struct ip_reassdata *tmp; /* 過渡變數 */
      LWIP_DEBUGF(IP_REASS_DEBUG, ("ip_reass_tmr: timer timed out\n"));
      tmp = r;
      /* 在釋放之前獲取下一個重組IP報文指標 */
      r = r->next;
      /* 刪除過期的重組IP報文:刪除重組IP報文節點資源及其所有的IP分片pbuf */
      ip_reass_free_complete_datagram(tmp, prev);
    }
  }
}