三LWIP學習筆記之ARP協議


一、ARP協議簡介  

  ARP,全稱 Address Resolution Protocol,譯作地址解析協議,ARP 協議與底層網絡接口密切相關。TCP/IP 標准分層結構中,把 ARP 划分為了網絡層的重要組成部分。 當一個主機上的應用程序要向目標主機發送數據時,它只知道目標主機的 IP 地址,而在協議棧底層接口發送數據包時,需要將該 IP 地址轉換為目標主機對應的 MAC 地址,這樣才能在數據鏈路上選擇正確的通道將數據包傳送出去,在整個轉換過程中發揮關鍵作用的就是 ARP 協議了。 在本章中將看到:
  ARP 協議的原理;
  ARP 緩存表及其創建、維護、查詢;
  ARP 報文結構;
  ARP 層數據包的接收處理;
  ARP 層數據包的發送。
  ARP 層是將底層鏈路與協議上層連接起來的紐帶,是以太網通信中不可或缺的協議。

1、物理地址與網絡地址

  網卡的 48 位 MAC 地址都保存在網卡的內部存儲器中,另一方面,TCP/IP 協議有自己的地址:32bit 的 IP 地址(網絡地址),網絡層發送數據包時只知道目的主機的 IP 地址,而底層接口(如以太網驅動程序)必須知道對方的硬件地址才能將數據發送出去。 

  為了解決地址映射的問題,ARP 協議提供了一種地址動態解析的機制,ARP 的功能是在 32 bit的 IP 地址和采用不同網絡技術的硬件地址之間提供動態映射,為上層將底層的物理地址差異屏蔽起來,這樣上層的因特網協議便可以靈活的使用 IP 地址進行通信。 

2、ARP協議的本質  

  ARP 協議的基本功能是使用目標主機的 IP 地址,查詢其對應的 MAC 地址,以保證底層鏈路上數據包通信的進行。

  舉一個簡單的例子來看看 ARP 的功能。假如我們的主機(192.168.1.78)需要向開發板(192.168.1.37)發送一個 IP 數據包,當發送數據時,主機會在自己的 ARP 緩存表中尋找是否有目標 IP 地址。如果找到了,也就知道了目標 MAC 地址為(00­80­48­12­34­56), 此時主機直接把目標 MAC 地址寫入以太網幀首部發送就可以了;如果在 ARP 緩存表中沒有找到相對應的 IP 地址,此時比較不幸,我們的數據需要被延遲發送,隨后主機會先在網絡上發送一個廣播(ARP 請求,以太網目的地址為 FF­FF­FF­FF­FF­FF),廣播的 ARP 請求表示同一網段內的所有主機將會收到這樣一條信息:“192.168.1.37 的 MAC 地址是什么?請回答”。網絡 IP 地址為 192.168.1.37(開發板)的主機接收到這個幀后,它有義務做出這樣的回答(ARP 應答):“192.168.1.37 的 MAC 地址是(00­80­48­12­34­56)”。 這樣,主機就知道了開發板的 MAC 地址,先前被延遲的數據包就可以發送了,此外,主機會將這個地址對保存在緩存表中以便后續數據包發送時使用。 ARP 的實質就是對緩存表的建立、更新、查詢等操作。 

二、數據結構

  源文檔中的 etharp.c 和 etharp.h 文件實現了以太網中 ARP 協議的全部數據結構和函數定義,ARP 協議實現過程中有兩個重要的數據結構,即 ARP 緩存表和 ARP 報文。 

1、ARP表  

  ARP 協議的核心在於 ARP 緩存表,ARP 的實質就是對緩存表的建立、更新、查詢等操作。ARP 緩存表由緩存表項(entry)組成,每個表項記錄了一組 IP 地址和 MAC 地址綁定信息,當然除了這兩個基本信息外,還包含了與數據包發送控制、緩存表項管理相關的狀態、控制信息。LwIP中描述緩存表項的數據結構叫 etharp_entry,這個結構比較簡單,如下所示: 

————etharp.c—————————————————————————
struct etharp_entry 
{   
struct etharp_q_entry *q; //數據包緩沖隊列指針   struct ip_addr ipaddr; //目標 IP 地址   struct eth_addr ethaddr; // MAC 地址   enum etharp_state state; //描述該 entry 的狀態   u8_t ctime; //描述該 entry 的時間信息   struct netif *netif; //對應網絡接口信息 }; ——————————————————————————————————

  描述緩沖隊列的數據結構也很簡單,叫做 etharp_q_entry,該結構的定義如下: 

————etharp.h——————————————————
struct etharp_q_entry 
{   
struct etharp_q_entry *next; //指向下一個緩沖數據包   struct pbuf *p; //指向數據包 pbuf }; —————————————————————————————————

  用一個圖來看看 etharp_q_entry 結構在緩存表數據隊列中的作用,如圖 9­2 所示。 

        

  state 是個枚舉類型,它描述該緩存表項的狀態,LwIP 中定義一個緩存表項可能有三種不同的狀態,用枚舉型 etharp_state 進行描述。 

————etharp.c—————————————————————————
enum etharp_state 
{   ETHARP_STATE_EMPTY
= 0, //empty 狀態   ETHARP_STATE_PENDING, //pending 狀態   ETHARP_STATE_STABLE //stable 狀態 }; ————————————————————————————————

  編譯器為 ARP 表預先定義了 ARP_TABLE_SIZE(通常為 10)個表項空間,因此 ARP 緩存表內部最多只能存放 ARP_TABLE_SIZE 條 IP 地址與 MAC 地址配對信息。 

————etharp.c——————————————————————
static struct etharp_entry arp_table[ARP_TABLE_SIZE]; //定義 ARP 緩存表
——————————————————————————————————

  ETHARP_STATE_EMPTY 狀態(empty) :初始化的時候為empty狀態。    

  ETHARP_STATE_PENDING 狀態(pending):表示該表項處於不穩定狀態,此時該表項只記錄到了IP 地址,但是還未記錄到對應的 MAC 地址。 很可能的情況是,LwIP 內核已經發出一個關於該 IP地址的 ARP 請求到數據鏈路上,但是還未收到 ARP 應答。 

  ETHARP_STATE_STABLE 狀態(stable) :當 ARP 表項被更新后,它就記錄了一對完整的 IP 地址和MAC 地址 。

  在ETHARP_STATE_PENDING 狀態下會設定超時時間(10秒),當計數超時后,對應的表項將被刪除;在ETHARP_STATE_STABLE 狀態下也會設定超時時間(20分鍾),當計數超時后,對應的表項將被刪除。

  最后一個字段,網絡接口結構指針 netif,在 ARP 表項中維護這樣一個指針還是很有用的,因為該結構中包含了網絡接口的 MAC 地址和 IP 地址等信息,在發送數據包的時候,這些信息都起着至關重要的作用。   

  ctime 為每個表項的計數器,周期性的去調用一個 etharp_tmr 函數,這個函數以 5 秒為周期被調用,在這個函數中,它會將每個ARP 緩存表項的 ctime 字段值加 1,當相應表項的生存時間計數值 ctime 大於系統規定的某個值時,系統將刪除對應的表項。 

————etharp.c————————————————————
//穩定狀態表項的最大生存時間計數值:240*5s=20min
#define ARP_MAXAGE 240
//PENDING 狀態表項的最大生存時間計數值:2*5s=10s
#define ARP_MAXPENDING 2
void etharp_tmr(void)
{
  u8_t i;
  for (i = 0; i < ARP_TABLE_SIZE; ++i)
  { //對每個表項操作,包括空閑狀態的表項     arp_table[i].ctime++; //先將表項 ctime 值加 1     //如果表項是 stable 狀態,且生存值大於 ARP_MAXAGE,     //或者是 pending 狀態且其生存值大於 ARP_MAXPENDING,則刪除表項     if ( ((arp_table[i].state == ETHARP_STATE_STABLE) && //stable 狀態         (arp_table[i].ctime >= ARP_MAXAGE))           || //或者           ((arp_table[i].state == ETHARP_STATE_PENDING) && //pending 狀態             (arp_table[i].ctime >= ARP_MAXPENDING)) )
    {       
if (arp_table[i].q != NULL)
      { //如果表項上的數據隊列中有數據,         free_etharp_q(arp_table[i].q); //則釋放隊列中的所有數據         arp_table[i].q = NULL; //隊列設置為空       }       arp_table[i].state = ETHARP_STATE_EMPTY; //將表項狀態改為未用,即刪除     }//if   }//for } ——————————————————————————————————

2、ARP報文

  源主機如何告訴目的主機:我需要你的 MAC 地址;而目的主機如何回復:這就是我的 MAC 地址。ARP 報文(或者稱之為 ARP 數據包),這就派上用場了。 ARP 請求和 ARP 應答,它們都是被組裝在一個 ARP 數據包中發送的,
這里先來看看一個典型的 ARP 包的組成結構。如圖 9­3 所示

          

  以太網目的地址和以太網源地址:分別表示以太網目的MAC地址和源MAC地址,目的地址全1時是特殊地址以太網廣播地址。在 ARP 表項建立前,源主機只知道目的主機的 IP 地址,並不知道其 MAC 地址,所以在數據鏈路上,源主機只有通過廣播的方式將 ARP請求數據包發送出去,同一網段上的所有以太網接口都會接收到廣播的數據包。 

  楨類型:ARP-0x0806、IP-0x0800、PPPoE-0x8864

  硬件類型:表示發送方想要知道的硬件類型。1-以太網MAC地址

  協議類型:表示要映射的協議地址類型,0x0800-表示要映射為IP地址  

  硬件地址長度和協議地址長度:以太網ARP請求和應答來說分別為6和4,代表MAC地址長度和IP地址長度。在 ARP 協議包中留出硬件地址長度字段和協議地址長度字段可 以使得 ARP 協議在任何網絡中被使用,而不僅僅只在以太網中。 

  op:指出ARP數據包的類型,ARP請求(1),ARP應答(2)

  在以太網的數據幀頭部中和 ARP 數據包中都有發送端的以太網MAC 地址。對於一個 ARP 請求包來說,除接收方以太網地址外的所有字段都應該被填充相應的值。當接收方主機收到一份給自己的 ARP 請求報文后,它就把自己的硬件地址填進去,然后將該請求數據包的源主機信息和目的主機信息交換位置,並把操作字段 op 置為 2,最后把該新構建的數據包發送回去,這就是 ARP 應答。 

  關於上圖中的這個結構,在 ARP 中用了一大堆的數據結構和宏來描述它們。

————etharp.h————————————————
#ifndef ETHARP_HWADDR_LEN
#define ETHARP_HWADDR_LEN 6 //以太網物理地址長度
#endif
PACK_STRUCT_BEGIN //我們移植時實現的結構體封裝宏
struct eth_addr 
{ //定義以太網 MAC 地址結構體 eth_addr,禁止編譯器自對齊   PACK_STRUCT_FIELD(u8_t addr[ETHARP_HWADDR_LEN]); } PACK_STRUCT_STRUCT; PACK_STRUCT_END PACK_STRUCT_BEGIN //定義以太網數據幀首部結構體 eth_hdr,禁止編譯器自對齊 struct eth_hdr
{   PACK_STRUCT_FIELD(
struct eth_addr dest); //以太網目的地址(6 字節)   PACK_STRUCT_FIELD(struct eth_addr src); //以太網源地址(6 字節)   PACK_STRUCT_FIELD(u16_t type); //幀類型(2 字節) } PACK_STRUCT_STRUCT; PACK_STRUCT_END //定義以太網幀頭部長度宏,其中 ETH_PAD_SIZE 已定義為 0 #define SIZEOF_ETH_HDR (14 + ETH_PAD_SIZE) PACK_STRUCT_BEGIN //定義 ARP 數據包結構體 etharp_hdr,禁止編譯器自對齊 struct etharp_hdr
{   PACK_STRUCT_FIELD(u16_t hwtype);
//硬件類型(2 字節)   PACK_STRUCT_FIELD(u16_t proto); //協議類型(2 字節)   PACK_STRUCT_FIELD(u16_t _hwlen_protolen); //硬件+協議地址長度(2 字節)   PACK_STRUCT_FIELD(u16_t opcode); //操作字段 op(2 字節)   PACK_STRUCT_FIELD(struct eth_addr shwaddr); //發送方 MAC 地址(6 字節)   PACK_STRUCT_FIELD(struct ip_addr2 sipaddr); //發送方 IP 地址(4 字節)   PACK_STRUCT_FIELD(struct eth_addr dhwaddr); //接收方 MAC 地址(6 字節)   PACK_STRUCT_FIELD(struct ip_addr2 dipaddr); //接收方 IP 地址(4 字節) } PACK_STRUCT_STRUCT; PACK_STRUCT_END #define SIZEOF_ETHARP_HDR 28 //宏,ARP 數據包長度 //宏,包含 ARP 數據包的以太網幀長度 #define SIZEOF_ETHARP_PACKET (SIZEOF_ETH_HDR + SIZEOF_ETHARP_HDR) #define ARP_TMR_INTERVAL 5000 //定義 ARP 定時器周期為 5 秒,不同幀類型的宏定義 #define ETHTYPE_ARP 0x0806 #define ETHTYPE_IP 0x0800 //ARP 數據包中 OP 字段取值宏定義 #define ARP_REQUEST 1 //ARP 請求 #define ARP_REPLY 2 //ARP 應答 ————————————————————————————

  發送 ARP 請求數據包的函數叫 etharp_request,看名字就曉得了。這個函數很簡單,它是通過調用 etharp_raw 函數來實現的,調用后者時,需要為它提供 ARP數據包中各個字段的值,后者直接將各個字段的值填寫到在一個 ARP 包中發送(該函數並不知道發送的是 ARP 請求還是 ARP 響應,它只管組裝並發送,所以稱之為 raw) 

————etharp.c——————————————————
//函數功能:根據各個參數字段組織一個 ARP 數據包並發送
//參數 netif:發送 ARP 包的網絡接口結構
//參數 ethsrc_addr:以太網幀首部中的以太網源地址值
//參數 ethdst_addr:以太網幀首部中的以太網目的地址值
//參數 hwsrc_addr:ARP 數據包中的發送方 MAC 地址
//參數 ipsrc_addr:ARP 數據包中的發送方 IP 地址
//參數 hwdst_addr:ARP 數據包中的接收方 MAC 地址
//參數 ipdst_addr:ARP 數據包中的接收方 IP 地址
//參數 opcode:ARP 數據包中的 OP 字段值,請求ARP為1,應答ARP為2
//注:ARP 數據包中其他字段使用預定義值,例如硬件地址長度為 6,協議地址長度為 4
err_t etharp_raw(struct netif *netif, const struct eth_addr *ethsrc_addr,
const struct eth_addr *ethdst_addr,
const struct eth_addr *hwsrc_addr, const struct ip_addr *ipsrc_addr,
const struct eth_addr *hwdst_addr, const struct ip_addr *ipdst_addr,
const u16_t opcode)
{
  struct pbuf *p; //數據包指針
  err_t result = ERR_OK; //返回結果
  u8_t k;
  struct eth_hdr *ethhdr; //以太網數據幀首部結構體指針
  struct etharp_hdr *hdr; // ARP 數據包結構體指針
  //先在內存堆中為 ARP 包分配空間,大小為包含 ARP 數據包的以太網幀總大小
  p = pbuf_alloc(PBUF_RAW, SIZEOF_ETHARP_PACKET, PBUF_RAM);
  if (p == NULL) 
  { //若分配失敗則返回內存錯誤     return ERR_MEM;   }   //到這里,內存分配成功   ethhdr = p­>payload; // ethhdr 指向以太網幀首部區域   hdr = (struct etharp_hdr *)((u8_t*)ethhdr + SIZEOF_ETH_HDR);// hdr 指向 ARP 首部   hdr­>opcode = htons(opcode); //填寫 ARP 包的 OP 字段,注意大小端轉換   k = ETHARP_HWADDR_LEN; //循環填寫數據包中各個 MAC 地址字段   while(k > 0)
  {     k--­­;     hdr­
>shwaddr.addr[k] = hwsrc_addr­>addr[k]; //ARP 頭部的發送方 MAC 地址     hdr­>dhwaddr.addr[k] = hwdst_addr­>addr[k]; //ARP 頭部的接收方 MAC 地址     ethhdr­>dest.addr[k] = ethdst_addr­>addr[k]; //以太網幀首部中的以太網目的地址     ethhdr­>src.addr[k] = ethsrc_addr­>addr[k]; //以太網幀首部中的以太網源地址   }   hdr­>sipaddr = *(struct ip_addr2 *)ipsrc_addr; //填寫 ARP 頭部的發送方 IP 地址   hdr­>dipaddr = *(struct ip_addr2 *)ipdst_addr; //填寫 ARP 頭部的接收方 IP 地址   //下面填充一些固定字段的值   hdr­>hwtype = htons(HWTYPE_ETHERNET); //ARP 頭部的硬件類型為 1,即以太網   hdr­>proto = htons(ETHTYPE_IP); //ARP 頭部的協議類型為 0x0800   //設置兩個長度字段   hdr­>_hwlen_protolen = htons((ETHARP_HWADDR_LEN << 8) | sizeof(struct ip_addr));   ethhdr­>type = htons(ETHTYPE_ARP); //以太網幀首部中的幀類型字段,ARP 包   result = netif­>linkoutput(netif, p); //調用底層數據包發送函數   pbuf_free(p); //釋放數據包   p = NULL;   return result; //返回發送結果 }   //特殊 MAC 地址的定義,以太網廣播地址   const struct eth_addr ethbroadcast = {{0xff,0xff,0xff,0xff,0xff,0xff}};   //該值用於填充 ARP 請求包的接收方 MAC 字段,無實際意義   const struct eth_addr ethzero = {{0,0,0,0,0,0}};   //函數功能:發送 ARP 請求   //參數 netif:發送 ARP 請求包的接口結構   //參數 ipaddr:請求具有該 IP 地址主機的 MAC   err_t etharp_request(struct netif *netif, struct ip_addr *ipaddr)   {     //該函數只是簡單的調用函數 etharp_raw,為函數提供所有相關參數     return etharp_raw(netif, (struct eth_addr *)netif­>hwaddr, &ethbroadcast,     (struct eth_addr *)netif­>hwaddr, &netif­>ip_addr, &ethzero,     ipaddr, ARP_REQUEST);   } ——————————————————————————————————

三、ARP層數據包輸入

1、以太網數據包遞交

  在我們說網卡驅動的時候講到了數據包接收函數 ethernetif_input,這個函數是源碼作者提供的一個以太網數據包接收和遞交函數,它的功能是調用底層數據包接收函數 low_level_input 讀取網卡中的數據包,然后在將該數據包遞交給相應的上層處理。

————ethernetif.c————————————————
static void ethernetif_input(struct netif *netif)
{
  struct ethernetif *ethernetif; //用戶自定義的網絡接口信息結構,這里無用處
  struct eth_hdr *ethhdr; //以太網幀頭部結構指針
  struct pbuf *p;
  ethernetif = netif­>state; //獲得自定義的網絡信息結構,無實際意義
  p = low_level_input(netif); //調用底層函數讀取一個數據包
  if (p == NULL) return; //如果數據包為空,則直接返回
  //到這里數據包不為空
  ethhdr = p­>payload; //將 ethhdr 指向數據包中的以太網頭部
  switch (htons(ethhdr­>type)) 
  { //判斷幀類型,注意大小端轉換     case ETHTYPE_IP: //對於 IP 包和 ARP 包,都調用注冊的 netif­>input 函數     case ETHTYPE_ARP: //進行處理
      etharp_arp_input(netif, (struct eth_addr*)(netif­>hwaddr), p);
      if (netif­>input(p, netif)!=ERR_OK)
      { //未完成正常的處理,則釋放數據包        pbuf_free(p);        p = NULL;       }       break;     default: //對於其他類型的數據包,直接釋放掉,不做處理       pbuf_free(p);       p = NULL;       break;   }//switch } ——————————————————————————————————————

2、ARP數據包處理

  首先,若這個請求的 IP 地址與本機地址不匹配,那么就不需要返回 ARP 應答,但由於該 ARP 請求包中包含了發送請求的主機的 IP 地址 和 MAC 地址,可以將這個地址對加入到 ARP 緩存表中,以便后續使用;其次,如果 ARP 請求與本機 IP 地址匹配,此時,除了進行上述的記錄源主機的 IP 地址和 MAC 地址外,還需要給源主機返回一個 ARP 應答。整個過程清楚后,就可以來看具體的代碼實現了。 

————etharp.c————————————————————
//函數功能:處理 ARP 數據包,更新 ARP 緩存表,對 ARP 請求進行應答
//參數 ethaddr:網絡接口的 MAC 地址
void etharp_arp_input(struct netif *netif, struct eth_addr *ethaddr, struct pbuf *p)
{
  struct etharp_hdr *hdr; //指向 ARP 數據包頭部的變量
  struct eth_hdr *ethhdr; //指向以太網幀頭部的變量
  struct ip_addr sipaddr, dipaddr; //暫存 ARP 包中的源 IP 地址和目的 IP 地址
  u8_t i;
  u8_t for_us; //用於指示該 ARP 包是否是發給本機的
  //接下來判斷 ARP 包是否是放在一個 pbuf 中的,由於整個 ARP 包都使用結構 etharp_hdr
  //進行操作,如果 ARP 包是分裝在兩個 pbuf 中的,那么對於結構體 etharp_hdr 的操作將
  //無意義,我們直接丟棄掉這種類型的 ARP 包
  if (p­>len < SIZEOF_ETHARP_PACKET) 
  { //ARP 包不能分裝在兩個 pbuf 中     pbuf_free(p); //否則直接刪除,函數返回     return;   }   ethhdr = p­>payload; // ethhdr 指向以太網幀首部   hdr = (struct etharp_hdr *)((u8_t*)ethhdr + SIZEOF_ETH_HDR); //hdr 指向 ARP 包首部   //這里需要判斷 ARP 包的合法性,丟棄掉那些類型、長度不合法的 ARP 包   if ((hdr­>hwtype != htons(HWTYPE_ETHERNET)) || //是否為以太網硬件類型     (hdr­>_hwlen_protolen != htons((ETHARP_HWADDR_LEN << 8) | sizeof(struct ip_addr))) ||       (hdr­>proto != htons(ETHTYPE_IP)) || //協議類型為 IP         (ethhdr­>type != htons(ETHTYPE_ARP)))
  { //是否為 ARP 數據包     pbuf_free(p); //若不符合,則刪除數據包,函數返回     return;   }   //這里需要將 ARP 包中的兩個 IP 地址數據拷貝到變量 sipaddr 和 dipaddr 中,因為后面   //會使用這兩個 IP 地址,但 ARP 數據包中的 IP 地址字段並不是字對齊的,不能直接使用   SMEMCPY(&sipaddr, &hdr­>sipaddr, sizeof(sipaddr)); //拷貝發送方 IP 地址到 sipaddr 中   SMEMCPY(&dipaddr, &hdr­>dipaddr, sizeof(dipaddr)); //拷貝接收方 IP 地址到 dipaddr 中   //下面判斷這個 ARP 包是否是發送給我們的   if (netif­>ip_addr.addr == 0)
  { //如果網卡 IP 地址未配置     for_us = 0; //那么肯定不是給我們的,設置標志 for_us 為 0   } else
  { //如果網卡 IP 地址已經設置,則將目的 IP 地址與網卡 IP 地址比較     for_us = ip_addr_cmp(&dipaddr, &(netif­>ip_addr)); //若相等,for_us 被置為 1   }   //下面我們開始更新 ARP 緩存表   if (for_us)
  { //如果這個 ARP 包(請求或響應)是給我們的,則更新 ARP 表     update_arp_entry(netif, &sipaddr, &(hdr­>shwaddr), ETHARP_TRY_HARD);   } else
  {//若不是給我們的,也更新 ARP 表,但是不設置 ETHARP_TRY_HARD 標志     update_arp_entry(netif, &sipaddr, &(hdr­>shwaddr), 0);   }   //到這里,ARP 更新完畢,需要對 ARP 請求做出處理   switch (htons(hdr­>opcode))
  { //判斷 ARP 數據包的 op 字段     case ARP_REQUEST: //如果是 ARP 請求       if (for_us)
      { //且請求中的 IP 地址與本機的匹配,則需要返回 ARP 應答         //ARP 應答的返回很簡單,不需要再重新申請一個 ARP 數據包空間,         //而是直接將該 ARP 請求包中的相應字段進行改寫,構造一個應答包         hdr­>opcode = htons(ARP_REPLY); //將 op 字段改為 ARP 響應類型         hdr­>dipaddr = hdr­>sipaddr; //設置接收端 IP 地址         //設置發送端 IP 地址為網絡接口中的 IP 地址         SMEMCPY(&hdr­>sipaddr, &netif­>ip_addr, sizeof(hdr­>sipaddr));         //接下來,設置四個 MAC 地址字段         i = ETHARP_HWADDR_LEN;         while(i > 0)
        { //目標 MAC 地址可以直接從原來的 ARP 包中得到           i--­­; //源 MAC 地址我們已經通過參數 ethaddr 傳入           hdr­>dhwaddr.addr[i] = hdr­>shwaddr.addr[i];//設置 ARP 包的接收端 MAC 地址           ethhdr­>dest.addr[i] = hdr­>shwaddr.addr[i]; //以太網幀中的目標 MAC 地址           hdr­>shwaddr.addr[i] = ethaddr­>addr[i]; // ARP 頭部的發送端 MAC 地址           ethhdr­>src.addr[i] = ethaddr­>addr[i]; //以太網幀頭部的源 MAC 地址         }         //對於 ARP 包中的其他字段的值(硬件類型、協議類型、長度字段等)         //保持它們的值不變,因為在前面已經測試過了它們的有效性         netif­>linkoutput(netif, p); //直接發送 ARP 應答包       } else if (netif­>ip_addr.addr == 0)
        {//ARP 請求數據包不是給我們的, 不做任何處理         } //這里只打印一些調試信息,筆者已將它們去掉       else
        {         }       
break;    case ARP_REPLY: //如果是 ARP 應答,我們已經在最開始更新了 ARP 表     break; //這里神馬都不用做了    default:     break;   }// switch   pbuf_free(p); //刪除數據包 p } ————————————————————————————————————

3、ARP攻擊

4、ARP緩存表更新

四、ARP層數據包輸出

1、ARP層數據處理總流程

        

 

2、廣播包與多播包的發送  

  etharp_output 函數被 IP 層的數據包發送函數 ip_output 調用,它首先根據目的 IP地址的類型為數據包選擇不同的處理方式:當目的 IP 地址為廣播或者多播地址時,etharp_output可以直接根據這個目的地址構造出相應的特殊 MAC 地址,同時把 MAC 地址作為參數,和數據包一起交給 etharp_send_ip 發送;當目的 IP 地址為單播地址時,需要調用 etharp_query 函數在 ARP表中查找與目的 IP 地址對應的 MAC 地址,若找到,則函數 etharp_send_ip 被調用,以發送數據包;若找不到,則函數 etharp_request 被調用它會發送一個關於目的 IP 地址的 ARP 請求包,出現這種情況時,我們還需要將 IP 包掛接的相應表項的緩沖隊列中,直到對應的ARP 應答返回時,該數據包才會被發送出去。 

————etharp.c——————————————
//函數功能:發送一個 IP 數據包 pbuf 到目的地址 ipaddr 處,該函數被 IP 層調用
//參數 netif:指向發送數據包的網絡接口結構
//參數 q:指向 IP 數據包 pbuf 的指針
//參數 ipaddr:指向目的 IP 地址
err_t etharp_output(struct netif *netif, struct pbuf *q, struct ip_addr *ipaddr)
{
  struct eth_addr *dest, mcastaddr;
  if (pbuf_header(q, sizeof(struct eth_hdr)) != 0) {//調整 pbuf 的 payload 指針,使其指向
  return ERR_BUF; //以太網幀頭部,失敗則返回
  }
  dest = NULL;
  if (ip_addr_isbroadcast(ipaddr, netif)) 
  {//如果是廣播 IP 地址     dest = (struct eth_addr *)&ethbroadcast; //dest 指向廣播 MAC 地址   } else if (ip_addr_ismulticast(ipaddr))
  {//如果是多播 IP 地址     mcastaddr.addr[0] = 0x01; //則構造多播 MAC 地址     mcastaddr.addr[1] = 0x00;     mcastaddr.addr[2] = 0x5e;     mcastaddr.addr[3] = ip4_addr2(ipaddr) & 0x7f;     mcastaddr.addr[4] = ip4_addr3(ipaddr);     mcastaddr.addr[5] = ip4_addr4(ipaddr);     dest = &mcastaddr; // dest 指向多播 MAC 地址   } else { //如果為單播 IP 地址     //判斷目的 IP 地址是否為本地的子網上,若不在,則修改 ipaddr     if (!ip_addr_netcmp(ipaddr, &(netif­>ip_addr), &(netif­>netmask)))
    {       
if (netif­>gw.addr != 0)
      { //需要將數據包發送到網關處,由網關轉發         ipaddr = &(netif­>gw); //更改變量 ipaddr,數據包發往網關處       } else
      { //如果網關未配置,返回錯誤         return ERR_RTE;       }     }     //對於單播包,調用 etharp_query 查詢其 MAC 地址並發送數據包     return etharp_query(netif, ipaddr, q);   }   //對於多播和廣播包,由於得到了它們的目的 MAC 地址,所以可以直接發送   return etharp_send_ip(netif, q, (struct eth_addr*)(netif­>hwaddr), dest); } ————————————————————————————————

  廣播包:調用函數 ip_addr_isbroadcast 判斷目的 IP 地址是否為廣播地址,如果是廣播包,則目的 MAC 地址不需要查詢 arp 表,由於廣播 MAC 地址的 48 位均為 1,即目的 MAC 六個字節值為ff­ff­ff­ff­ff­ff。

  多播包:判斷目的 IP 地址是不是 D 類 IP 地址,如果是,則 MAC 地址可以直接計算得出,即將 MAC 地址 01­00­5E­00­00­00 的低 23 位設置為 IP 地址的低 23 位。對於以上的兩種數據包,etharp_output 直接調用函數 etharp_send_ip 將數據包發送出去。

  單播包:要比較目的 IP 和本地 IP 地址,看是否是局域網內的,若不是局域網內的,則將目的IP 地址設置為默認網關的地址,然后再統一調用 etharp_query 函數查找目的 MAC 地址,最后將數據包發送出去。 

————etharp_send_ip————————————————————
//函數功能:填寫以太網幀頭部,發送以太網幀
//參數 p:指向以太網幀的 pbuf
//參數 src:指向源 MAC 地址
//參數 dst:指向目的 MAC 地址
static err_t
etharp_send_ip(struct netif *netif, struct pbuf *p, struct eth_addr *src, struct eth_addr *dst)
{
  struct eth_hdr *ethhdr = p­>payload; //指向以太網幀頭部
  u8_t k;
  k = ETHARP_HWADDR_LEN;
  while(k > 0)
  {     k--­­;     ethhdr­
>dest.addr[k] = dst­>addr[k]; //填寫目的 MAC 字段     ethhdr­>src.addr[k] = src­>addr[k]; //填寫源 MAC 字段   }   ethhdr­>type = htons(ETHTYPE_IP); //填寫幀類型字段   return netif­>linkoutput(netif, p); //調用網卡數據包發送函數 } ————————————————————————————————————

  這個函數尤其簡單,直接根據傳入的參數填寫以太網幀首部的三個字段,然后調用注冊的底層數據包發送函數將數據包發送出去。 

3、單播包的發送

  如果給定的 IP 地址不在 ARP 表中,則一個新的 ARP 表項會被創建,此時該表項處於 pending 狀態,同時一個關於該 IP 地址的 ARP 請求會被廣播出去,再同時要發送的 IP 數據包會被掛接在該表項的數據緩沖指針上;如果 IP 地址在 ARP 表中有相應的表項存在,但該表項處於pending 狀態,則操作與前者相同,即發送一個 ARP 請求和掛接數據包;如果 IP 地址在 ARP 表中有相應的表項存在,且表項處於 stable 狀態,此時再來判斷給定的數據包是否為空,不為空則直接將該數據包發送出去,為空則向該 IP 地址發送一個 ARP 請求。 

//函數功能:查找單播 IP 地址對應的 MAC 地址,並發送數據包
//參數 ipaddr:指向目的 IP 地址
//參數 q:指向以太網數據幀的 pbuf
err_t etharp_query(struct netif *netif, struct ip_addr *ipaddr, struct pbuf *q)
{
  struct eth_addr * srcaddr = (struct eth_addr *)netif->hwaddr;
  err_t result = ERR_MEM;
  s8_t i;
  //調用函數 find_entry 查找或創建一個 ARP 表項
  i = find_entry(ipaddr, ETHARP_TRY_HARD);
  if (i < 0) 
  { //若查找失敗,則 i 小於 0,直接返回
    return (err_t)i;
  }
  //如果表項的狀態為 empty,說明表項是剛創建的,且其中已經記錄了 IP 地址
  if (arp_table[i].state == ETHARP_STATE_EMPTY) 
  { //將表項的狀態改為 pending
    arp_table[i].state = ETHARP_STATE_PENDING;
  }
  if ((arp_table[i].state == ETHARP_STATE_PENDING) || (q == NULL)) 
  {//數據包為空,或
    result = etharp_request(netif, ipaddr); //表項為 pending 態,則發送 ARP 請求包
  }
  if (q != NULL) 
  {//數據包不為空,則進行數據包的發送或者將數據包掛接在緩沖隊列上
    if (arp_table[i].state == ETHARP_STATE_STABLE) {//ARP 表穩定,則直接發送數據包
      result = etharp_send_ip(netif, q, srcaddr, &(arp_table[i].ethaddr));
    } else if (arp_table[i].state == ETHARP_STATE_PENDING) {//否則,掛接數據包
        struct pbuf *p;
        int copy_needed = 0;//是否需要重新拷貝整個數據包,數據包全由 PBUF_ROM
        p = q; //類型的 pbuf 組成時,才不需要拷貝
        while (p) { //判斷是否需要拷貝整個數據包
          if(p->type != PBUF_ROM) {
            copy_needed = 1;
            break;
          }
          p = p->next;
        }
        if(copy_needed) { //如果需要拷貝,則申請內存堆空間
          p = pbuf_alloc(PBUF_RAW, p->tot_len, PBUF_RAM);//申請一個 pbuf 空間
          if(p != NULL) {申請成功,則執行拷貝操作
          if (pbuf_copy(p, q) != ERR_OK) {//拷貝失敗,則釋放申請的空間
            pbuf_free(p);
            p = NULL;
          }
        }
        } else { //如果不需要拷貝
          p = q; //設置 p
          pbuf_ref(p); //增加 pbuf 的 ref 值
        }
        //到這里,p 指向了我們需要掛接的數據包,下面執行掛接操作
        if (p != NULL) {
          struct etharp_q_entry *new_entry; //為數據包申請一個 etharp_q_entry 結構
          new_entry = memp_malloc(MEMP_ARP_QUEUE); //在內存池 POOL 中
          if (new_entry != NULL) 
          { //申請成功,則進行掛接操作
            new_entry->next = 0; //設置 etharp_q_entry 結構的指針
            new_entry->p = p;
            if(arp_table[i].q != NULL) { //若緩沖隊列不為空
              struct etharp_q_entry *r;
              r = arp_table[i].q;
                while (r->next != NULL) {//則找到最后一個緩沖包結構
                  r = r->next;
                }
                r->next = new_entry; //將新的數據包掛接在隊列尾部
              } else { //緩沖隊列為空
                arp_table[i].q = new_entry; //直接掛接在緩沖隊列首部
              }
              result = ERR_OK;
          } else { //etharp_q_entry 結構申請失敗,則
            pbuf_free(p); //釋放數據包空間
          }
         // if (p != NULL)
     }//else if
  }// if (q != NULL)
  return result; //返回函數操作結果


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM