ARP協(xié)議與鄰居子系統(tǒng)剖析
學(xué)習(xí)過 TCP/IP 協(xié)議的同學(xué)都應(yīng)該了解過 TCP/IP 五層網(wǎng)絡(luò)模型,如下圖:

從上圖可以看出,ARP協(xié)議?位于 TCP/IP 五層網(wǎng)絡(luò)模型的?網(wǎng)絡(luò)層。那么,ARP協(xié)議?的用途是什么呢?
ARP協(xié)議介紹
在局域網(wǎng)中(同一個路由器內(nèi)),主機(jī)與主機(jī)之間需要通過 MAC 地址進(jìn)行通訊。但由于 MAC 地址過于復(fù)雜,不容易被人類記憶。所以,人們更傾向于使用更容易記憶的 IP 地址。
但局域網(wǎng)只能使用 MAC 地址通訊,那有什么辦法可以通過主機(jī)的 IP 地址來獲取到主機(jī)的 MAC 地址呢?ARP協(xié)議?就應(yīng)運而生。
ARP(Address Resolution Protocol)?即地址解析協(xié)議, 用于實現(xiàn)從 IP 地址到 MAC 地址的映射,即詢問目標(biāo)IP對應(yīng)的MAC地址。
ARP協(xié)議?通過廣播消息,向局域網(wǎng)的所有主機(jī)廣播?ARP請求消息,從而詢問主機(jī)的 IP 地址對應(yīng)的 MAC 地址,如下圖:

如上圖所示,A主機(jī)要與B主機(jī)通信,但是只知道B主機(jī)的 IP 地址,所以這時可以向局域網(wǎng)廣播一條?ARP請求消息,用于詢問 IP 地址為?192.168.1.2?的主機(jī)所對應(yīng)的 MAC 地址。
由于?ARP請求消息?是廣播消息,所以局域網(wǎng)的所有主機(jī)都會收到這條消息,但只有對應(yīng) IP 地址的主機(jī)才會回答這條消息。如上圖的B主機(jī)會回復(fù)一條?ARP應(yīng)答消息,用于告訴A主機(jī)自己的 MAC 地址。
當(dāng)A主機(jī)知道了B主機(jī)的 MAC 地址后,就能通過 MAC 地址與B主機(jī)進(jìn)行通信了。
ARP協(xié)議頭部
每種網(wǎng)絡(luò)協(xié)議都有其協(xié)議頭部,用于本協(xié)議的通信,ARP協(xié)議?的頭部格式如下:

上圖是?ARP協(xié)議?頭部各個字段的信息,其代碼結(jié)構(gòu)定義如下(路徑:?/src/include/linux/if_arp.h):
struct arphdr{unsigned short ar_hrd; /* format of hardware address */unsigned short ar_pro; /* format of protocol address */unsigned char ar_hln; /* length of hardware address */unsigned char ar_pln; /* length of protocol address */unsigned short ar_op; /* ARP opcode (command) *//* 下面部分沒有定義, 因為不同的鏈路層協(xié)議使用的地址格式不一定相同,* 所以下面只是以太網(wǎng)和IP協(xié)議的示例而已.* Ethernet looks like this : This bit is variable sized however...*/unsigned char ar_sha[ETH_ALEN]; /* sender hardware address */unsigned char ar_sip[4]; /* sender IP address */unsigned char ar_tha[ETH_ALEN]; /* target hardware address */unsigned char ar_tip[4]; /* target IP address */}
從代碼可以看出,arphdr?結(jié)構(gòu)的各個字段與上圖一一對應(yīng)。下面說說各個字段的作用:
ar_hrd:硬件類型,如硬件類型是以太網(wǎng),那么設(shè)置為?1。ar_pro:協(xié)議類型,由于 ARP 協(xié)議支持將多種不同協(xié)議地址轉(zhuǎn)換成 MAC 地址(如 IP 協(xié)議、AX.25 協(xié)議等),所以需要通過這個字段指明要轉(zhuǎn)換的協(xié)議類型。如 IP 協(xié)議設(shè)置為?0x0800。ar_hln:硬件地址長度,如以太網(wǎng)地址長度是 6。ar_hln:協(xié)議地址長度,如 IP 地址長度是 4。ar_op:操作碼,如?ARP請求?設(shè)置為 1,而?ARP應(yīng)答?設(shè)置為 2。
下面的字段是不定長的,根據(jù)硬件類型和協(xié)議類型的改變而改變。
譬如:如果是硬件類型是以太網(wǎng),并且協(xié)議類型是 IP 協(xié)議,那么?ar_sha?字段和?ar_tha?字段分別為 6 個字節(jié),而?ar_sip?字段和?ar_tip?字段分別為 4 個字節(jié)。
鄰居子系統(tǒng)
首先說明一下什么是?鄰居:在同一局域網(wǎng)中,每一臺主機(jī)都可以稱為其他主機(jī)的?鄰居。例如?Windows?系統(tǒng)可以在網(wǎng)絡(luò)中查看到同一局域網(wǎng)的鄰居主機(jī),如下圖:

如上圖所示,每一臺主機(jī)都可以稱為?鄰居節(jié)點。
在 Linux 內(nèi)核中,也把局域網(wǎng)中的每臺主機(jī)稱為?鄰居節(jié)點,使用結(jié)構(gòu)?neighbour?來描述,neighbour?結(jié)構(gòu)定義如下:
struct neighbour{struct neighbour *next; // 用于連接哈希表中相同哈希值的鄰居節(jié)點struct neigh_table *tbl; // 管理鄰居節(jié)點的鄰居表結(jié)構(gòu)struct neigh_parms *parms; // 參數(shù)列表struct net_device *dev; // 可以與這個鄰居節(jié)點通信的設(shè)備unsigned long used; // 最后使用時間unsigned long confirmed; // 最后確認(rèn)時間unsigned long updated; // 最后更新時間__u8 flags; // 標(biāo)志位__u8 nud_state; // 鄰居節(jié)點所處于的狀態(tài)__u8 type; // 類型__u8 dead; // 是否已經(jīng)失效atomic_t probes;rwlock_t lock; // 鎖// 鄰居節(jié)點的硬件地址unsigned char ha[(MAX_ADDR_LEN+sizeof(unsigned long)-1)&~(sizeof(unsigned long)-1)];struct hh_cache *hh;atomic_t refcnt; // 引用計數(shù)器int (*output)(struct sk_buff *skb); // 發(fā)送數(shù)據(jù)給此鄰居節(jié)點的接口struct sk_buff_head arp_queue; // 等待ARP回復(fù)的數(shù)據(jù)包列表(需要發(fā)送的數(shù)據(jù)包列表)struct timer_list timer; // 定時器struct neigh_ops *ops; // 操作方法列表u8 primary_key[0]; // 要轉(zhuǎn)換成MAC地址的上層協(xié)議地址(如IP地址)};
在?neighbour?結(jié)構(gòu)中,比較重要的字段有:
ha:鄰居節(jié)點的硬件地址,因為與鄰居節(jié)點通信需要知道其硬件地址(MAC地址)。output:向鄰居節(jié)點發(fā)送數(shù)據(jù)的接口,當(dāng)要向鄰居節(jié)點發(fā)送數(shù)據(jù)時,使用這個接口把數(shù)據(jù)發(fā)送出去。dev:輸出設(shè)備,如果向當(dāng)前鄰居節(jié)點發(fā)送數(shù)據(jù),需要通過這個設(shè)備來發(fā)送。primary_key:要轉(zhuǎn)換成 MAC 地址的上層協(xié)議地址(如 IP 地址),由于上層協(xié)議不確定,所以這里設(shè)置?primary_key?為?柔性數(shù)組(可變大小數(shù)組)(不了解柔性數(shù)組可以查閱相關(guān)的資料)。
如下圖所示:

所以,當(dāng)本機(jī)需要向某一臺?鄰居節(jié)點?主機(jī)發(fā)送數(shù)據(jù)時,首先需要通過上層協(xié)議地址與輸出設(shè)備查找對應(yīng)的?neighbour?對象是否已經(jīng)存在。如果存在,那么就使用這個?neighbour?對象。否則,就新創(chuàng)建一個?neighbour?對象,并初始化其各個字段。
查找鄰居節(jié)點信息
要查找一個?鄰居節(jié)點?的信息,可以通過調(diào)用?neigh_lookup()?函數(shù)來完成,其實現(xiàn)如下:
struct neighbour *neigh_lookup(struct neigh_table *tbl, const void *pkey, struct net_device *dev){struct neighbour *n;u32 hash_val;int key_len = tbl->key_len;hash_val = tbl->hash(pkey, dev); // 通過IP地址與設(shè)備計算鄰居節(jié)點信息的哈希值read_lock_bh(&tbl->lock);// 通過設(shè)備和IP地址從鄰居節(jié)點哈希表中查找鄰居節(jié)點信息for (n = tbl->hash_buckets[hash_val]; n; n = n->next) {if (dev == n->dev && memcmp(n->primary_key, pkey, key_len) == 0) {neigh_hold(n);break;}}read_unlock_bh(&tbl->lock);return n; // 返回鄰居節(jié)點信息對象}
neigh_lookup()?函數(shù)的參數(shù)含義如下:
tbl:鄰居節(jié)點管理表。pkey:上層協(xié)議地址(如 IP 地址)。dev:輸出設(shè)備對象。
neigh_lookup()?函數(shù)工作原理如下:
首先通過上層協(xié)議地址與設(shè)備計算鄰居節(jié)點信息的哈希值。
然后在鄰居節(jié)點哈希表中查找對應(yīng)的鄰居節(jié)點信息,如果找到即返回鄰居節(jié)點信息,否則返回NULL。
創(chuàng)建鄰居節(jié)點信息
當(dāng)?鄰居節(jié)點?信息不存在時,可以通過調(diào)用?neigh_create()?函數(shù)來創(chuàng)建,其實現(xiàn)如下:
struct neighbour *neigh_create(struct neigh_table *tbl, const void *pkey, struct net_device *dev){struct neighbour *n, *n1;u32 hash_val;int key_len = tbl->key_len;int error;n = neigh_alloc(tbl); // 創(chuàng)建一個新的鄰居節(jié)點信息對象if (n == NULL)return ERR_PTR(-ENOBUFS);memcpy(n->primary_key, pkey, key_len); // 復(fù)制上層協(xié)議地址到 primary_key 字段n->dev = dev; // 綁定輸出設(shè)備dev_hold(dev);// 對于ARP協(xié)議會調(diào)用 arp_constructor() 函數(shù)if (tbl->constructor && (error = tbl->constructor(n)) < 0) {neigh_release(n);return ERR_PTR(error);}...hash_val = tbl->hash(pkey, dev);write_lock_bh(&tbl->lock);...// 把鄰居節(jié)點信息添加到鄰居節(jié)點信息管理哈希表中n->next = tbl->hash_buckets[hash_val];tbl->hash_buckets[hash_val] = n;n->dead = 0;neigh_hold(n);write_unlock_bh(&tbl->lock);return n;}
neigh_create()?函數(shù)的工作原理如下:
調(diào)用?
neigh_alloc()?函數(shù)向內(nèi)存申請一個新的鄰居節(jié)點信息對象。把上層協(xié)議地址(如 IP 地址)復(fù)制到?
primary_key?字段中。綁定輸出設(shè)備到?
dev?字段。調(diào)用鄰居節(jié)點信息管理哈希表的?
constructor()?方法來初始化鄰居節(jié)點信息對象,對于?ARP協(xié)議?這里調(diào)用的是?arp_constructor()?函數(shù)。把新創(chuàng)建的鄰居節(jié)點信息對象添加到鄰居節(jié)點信息管理哈希表中。
對于?arp_constructor()?函數(shù),主要是用于初始化鄰居節(jié)點信息對象的操作方法列表和?output?字段,如下:
static struct neigh_ops arp_generic_ops = {AF_INET, // familyNULL, // destructorarp_solicit, // solicitarp_error_report, // error_reportneigh_resolve_output, // outputneigh_connected_output, // connected_outputdev_queue_xmit, // hh_outputdev_queue_xmit // queue_xmit};static int arp_constructor(struct neighbour *neigh){u32 addr = *(u32*)neigh->primary_key;struct net_device *dev = neigh->dev;...neigh->type = inet_addr_type(addr);...if (dev->hard_header_cache)neigh->ops = &arp_hh_ops;elseneigh->ops = &arp_generic_ops;if (neigh->nud_state & NUD_VALID)neigh->output = neigh->ops->connected_output;elseneigh->output = neigh->ops->output;return 0;}
從?arp_constructor()?函數(shù)的實現(xiàn)可以知道,鄰居節(jié)點信息對象的ops?字段被設(shè)置為?arp_generic_ops,而?output?字段會被設(shè)置為?neigh_resolve_output()?函數(shù)(當(dāng)鄰居節(jié)點信息對象不可用時)或者?neigh_connected_output()?函數(shù)(當(dāng)鄰居節(jié)點信息對象可用時)。
所以,一個新創(chuàng)建的?鄰居節(jié)點信息對象?各個字段的值大概如下圖所示:

由于此時還不知道鄰居節(jié)點的 MAC 地址,所以其?ha?字段的值為 0。
向鄰居節(jié)點發(fā)送數(shù)據(jù)
當(dāng)向鄰居節(jié)點發(fā)送數(shù)據(jù)時,需要調(diào)用鄰居節(jié)點信息對象的?output?接口。根據(jù)前面的分析,output?接口被設(shè)置為?neigh_resolve_output()?函數(shù)。我們來分析一下?neigh_resolve_output()?函數(shù)的實現(xiàn):
int neigh_resolve_output(struct sk_buff *skb){struct dst_entry *dst = skb->dst;struct neighbour *neigh;...if (neigh_event_send(neigh, skb) == 0) {int err;struct net_device *dev = neigh->dev;if (dev->hard_header_cache && dst->hh == NULL) {...} else {read_lock_bh(&neigh->lock);err = dev->hard_header(skb, dev, ntohs(skb->protocol),neigh->ha, NULL, skb->len);read_unlock_bh(&neigh->lock);}if (err >= 0)return neigh->ops->queue_xmit(skb);kfree_skb(skb);return -EINVAL;}return 0;...}
neigh_resolve_output()?函數(shù)主要完成三件事:
調(diào)用?
neigh_event_send()?函數(shù)發(fā)送一個查詢鄰居節(jié)點 MAC 地址的 ARP 請求。調(diào)用設(shè)備的?
hard_header()?方法設(shè)置數(shù)據(jù)包的目標(biāo) MAC 地址(如果鄰居節(jié)點的 MAC 地址已經(jīng)獲取到)。如果數(shù)據(jù)包的目標(biāo) MAC 地址設(shè)置成功,調(diào)用鄰居節(jié)點信息對象的?
queue_xmit()?方法把數(shù)據(jù)發(fā)送出去(對于 ARP 協(xié)議,queue_xmit()?方法對應(yīng)的是?dev_queue_xmit()?函數(shù))。
我們再來看看?neigh_event_send()?函數(shù)怎么發(fā)送 ARP 請求:
int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb){write_lock_bh(&neigh->lock);if (!(neigh->nud_state & (NUD_CONNECTED|NUD_DELAY|NUD_PROBE))) {if (!(neigh->nud_state & (NUD_STALE|NUD_INCOMPLETE))) {if (neigh->parms->mcast_probes + neigh->parms->app_probes) {...neigh->nud_state = NUD_INCOMPLETE;...neigh->ops->solicit(neigh, skb); // 發(fā)送查詢鄰居節(jié)點MAC地址的ARP請求...} else {...}}if (neigh->nud_state == NUD_INCOMPLETE) {if (skb) {...__skb_queue_head(&neigh->arp_queue, skb); // 把數(shù)據(jù)包添加到arp_queue隊列中}write_unlock_bh(&neigh->lock);return 1;}...}write_unlock_bh(&neigh->lock);return 0;}
__neigh_event_send()?函數(shù)主要完成兩個工作:
首先調(diào)用鄰居節(jié)點信息對象的?
solicit()?方法發(fā)送一個 ARP 請求,從前面的分析可知,solicit()?方法會被設(shè)置為?arp_solicit()?函數(shù)。然后把要發(fā)送的數(shù)據(jù)包添加到鄰居節(jié)點信息對象的?
arp_queue?隊列中,等待獲取到鄰居節(jié)點 MAC 地址后重新發(fā)送這個數(shù)據(jù)包。
鄰居節(jié)點信息對象的?arp_queue?隊列用于緩存等待發(fā)送的數(shù)據(jù)包,如下圖:

發(fā)送 ARP 請求
通過前面的分析可知,當(dāng)向鄰居節(jié)點發(fā)送數(shù)據(jù)時,如果還不知道鄰居節(jié)點的 MAC 地址,那么首先會調(diào)用?arp_solicit()?函數(shù)發(fā)送一個?ARP請求?來獲取鄰居節(jié)點的 MAC 地址,其實現(xiàn)如下:
static void arp_solicit(struct neighbour *neigh, struct sk_buff *skb){u32 saddr; // 源IP地址(本地IP地址)u8 *dst_ha = NULL; // 接收ARP請求目標(biāo)MAC地址(發(fā)廣播信息設(shè)置為NULL)struct net_device *dev = neigh->dev; // 輸出設(shè)備u32 target = *(u32*)neigh->primary_key; // 要查詢的鄰居節(jié)點IP地址...if (skb && inet_addr_type(skb->nh.iph->saddr) == RTN_LOCAL)saddr = skb->nh.iph->saddr;elsesaddr = inet_select_addr(dev, target, RT_SCOPE_LINK);...arp_send(ARPOP_REQUEST, ETH_P_ARP, target, dev, saddr,dst_ha, dev->dev_addr, NULL);...}
arp_solicit()?函數(shù)最終會調(diào)用?arp_send()?函數(shù)發(fā)送一個 ARP 請求,我們來分析一下?arp_send()?函數(shù)的實現(xiàn):
void arp_send(int type, int ptype, u32 dest_ip,struct net_device *dev, u32 src_ip,unsigned char *dest_hw, unsigned char *src_hw,unsigned char *target_hw){struct sk_buff *skb;struct arphdr *arp;unsigned char *arp_ptr;...// 申請一個數(shù)據(jù)包對象skb = alloc_skb(sizeof(struct arphdr) + 2*(dev->addr_len+4)+ dev->hard_header_len + 15, GFP_ATOMIC);...// ARP請求頭部arp = (struct arphdr *)skb_put(skb, sizeof(struct arphdr) + 2*(dev->addr_len+4));skb->dev = dev;skb->protocol = __constant_htons(ETH_P_ARP);if (src_hw == NULL)src_hw = dev->dev_addr;if (dest_hw == NULL)dest_hw = dev->broadcast;// 下面設(shè)置ARP頭部的各個字段信息...switch (dev->type) {default:arp->ar_hrd = htons(dev->type); // 設(shè)置硬件類型arp->ar_pro = __constant_htons(ETH_P_IP); // 設(shè)置上層協(xié)議類型為IP協(xié)議break;...}arp->ar_hln = dev->addr_len; // 設(shè)置硬件地址長度arp->ar_pln = 4; // 設(shè)置上層協(xié)議地址長度arp->ar_op = htons(type); // ARP請求操作碼類型arp_ptr = (unsigned char *)(arp + 1);memcpy(arp_ptr, src_hw, dev->addr_len); // 復(fù)制源MAC地址(本機(jī)MAC地址)arp_ptr += dev->addr_len;memcpy(arp_ptr, &src_ip,4); // 復(fù)制源IP地址(本機(jī)IP地址)// 復(fù)制目標(biāo)MAC地址(對于查詢請求設(shè)置為0)arp_ptr += 4;if (target_hw != NULL)memcpy(arp_ptr, target_hw, dev->addr_len);elsememset(arp_ptr, 0, dev->addr_len);// 復(fù)制目標(biāo)IP地址arp_ptr += dev->addr_len;memcpy(arp_ptr, &dest_ip, 4);...dev_queue_xmit(skb); // 把數(shù)據(jù)發(fā)送出去return;}
arp_send()?函數(shù)的工作也比較清晰:
首先調(diào)用?
alloc_skb()?函數(shù)申請一個數(shù)據(jù)包對象。然后設(shè)置 ARP 頭部各個字段的信息。
調(diào)用?
dev_queue_xmit()?函數(shù)把數(shù)據(jù)發(fā)送出去。
處理 ARP 回復(fù)
當(dāng)鄰居節(jié)點回復(fù) MAC 地址查詢 ARP 請求時,本機(jī)需要處理此 ARP 回復(fù)。本機(jī)通過?arp_rcv()?函數(shù)來處理 ARP 回復(fù),代碼如下:
int arp_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt){struct arphdr *arp = skb->nh.arph;unsigned char *arp_ptr = (unsigned char *)(arp+1);struct rtable *rt;unsigned char *sha, *tha;u32 sip, tip;u16 dev_type = dev->type;int addr_type;struct in_device *in_dev = in_dev_get(dev);struct neighbour *n;...// 從ARP回復(fù)中獲取數(shù)據(jù)sha = arp_ptr; // 源MAC地址(鄰居節(jié)點的MAC地址)arp_ptr += dev->addr_len;memcpy(&sip, arp_ptr, 4); // 源IP地址(鄰居節(jié)點的IP地址)arp_ptr += 4;tha = arp_ptr; // 目標(biāo)MAC地址(本機(jī)的MAC地址)arp_ptr += dev->addr_len;memcpy(&tip, arp_ptr, 4); // 目標(biāo)IP地址(本機(jī)的IP地址)...n = __neigh_lookup(&arp_tbl, &sip, dev, 0); // 查找鄰居節(jié)點信息對象if (n) {int state = NUD_REACHABLE;int override = 0;if (jiffies - n->updated >= n->parms->locktime)override = 1;if (arp->ar_op != __constant_htons(ARPOP_REPLY)|| skb->pkt_type != PACKET_HOST)state = NUD_STALE;neigh_update(n, sha, state, override, 1); // 更新鄰居節(jié)點信息對象neigh_release(n);}...return 0;}
arp_rcv()?函數(shù)主要完成以下工作:
通過從 ARP 回復(fù)中獲取到鄰居節(jié)點的 MAC 地址和 IP 地址。
通過鄰居節(jié)點的 IP 地址和輸入設(shè)備來查找對應(yīng)的鄰居節(jié)點信息對象。
如果鄰居節(jié)點信息對象已經(jīng)存在,那么調(diào)用?
neigh_update()?函數(shù)更新鄰居節(jié)點信息對象的數(shù)據(jù)。
我們來看看?neigh_update()?函數(shù)怎么更新鄰居節(jié)點信息對象的數(shù)據(jù):
int neigh_update(struct neighbour *neigh, const u8 *lladdr, u8 new, int override, int arp){u8 old;int err;int notify = 0;struct net_device *dev = neigh->dev;...old = neigh->nud_state; // 更新前鄰居節(jié)點信息對象的狀態(tài)...neigh->nud_state = new; // 更新鄰居節(jié)點信息對象的狀態(tài)if (lladdr != neigh->ha) { // 更新鄰居節(jié)點信息對象的MAC地址memcpy(&neigh->ha, lladdr, dev->addr_len);...}...if (!(old&NUD_VALID)) {struct sk_buff *skb;// 如果 arp_queue 隊列中有等待發(fā)送的數(shù)據(jù)包, 現(xiàn)在可以把這些數(shù)據(jù)包發(fā)送出去while (neigh->nud_state & NUD_VALID &&(skb = __skb_dequeue(&neigh->arp_queue)) != NULL){struct neighbour *n1 = neigh;write_unlock_bh(&neigh->lock);...n1->output(skb);write_lock_bh(&neigh->lock);}skb_queue_purge(&neigh->arp_queue);}...return err;}
neigh_update()?函數(shù)主要完成以下工作:
更新鄰居節(jié)點信息對象的?
ha?字段(也就是 MAC 地址)為 ARP 回復(fù)中獲得的鄰居節(jié)點 MAC 地址。如果鄰居節(jié)點對象的?
arp_queue?隊列不為空,說明有等待發(fā)送的數(shù)據(jù)包,此時調(diào)用鄰居節(jié)點信息的?output()?接口把這些數(shù)據(jù)發(fā)送出去。從上下文可知,此時的?output()?還是為?neigh_resolve_output()?函數(shù)。但由于鄰居節(jié)點的 MAC 地址已經(jīng)獲得,所以不會再發(fā)送 ARP 請求。而是調(diào)用設(shè)備的?hard_header()?接口設(shè)置數(shù)據(jù)包的目標(biāo) MAC 地址,然后 調(diào)用?dev_queue_xmit()?函數(shù)把數(shù)據(jù)包發(fā)送出去。
