一、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 地址為(008048123456), 此時主機直接把目標 MAC 地址寫入以太網幀首部發送就可以了;如果在 ARP 緩存表中沒有找到相對應的 IP 地址,此時比較不幸,我們的數據需要被延遲發送,隨后主機會先在網絡上發送一個廣播(ARP 請求,以太網目的地址為 FFFFFFFFFFFF),廣播的 ARP 請求表示同一網段內的所有主機將會收到這樣一條信息:“192.168.1.37 的 MAC 地址是什么?請回答”。網絡 IP 地址為 192.168.1.37(開發板)的主機接收到這個幀后,它有義務做出這樣的回答(ARP 應答):“192.168.1.37 的 MAC 地址是(008048123456)”。 這樣,主機就知道了開發板的 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 結構在緩存表數據隊列中的作用,如圖 92 所示。
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 包的組成結構。如圖 93 所示
以太網目的地址和以太網源地址:分別表示以太網目的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, ðbroadcast, (struct eth_addr *)netif>hwaddr, &netif>ip_addr, ðzero, 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 *)ðbroadcast; //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 六個字節值為ffffffffffff。
多播包:判斷目的 IP 地址是不是 D 類 IP 地址,如果是,則 MAC 地址可以直接計算得出,即將 MAC 地址 01005E000000 的低 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; //返回函數操作結果 }