Linux內核鄰接子系統(tǒng)(arp協(xié)議) 的工作原理(一)
Linux內核鄰接子系統(tǒng)(二層到三層)
鄰接子系統(tǒng)的核心
鄰居子系統(tǒng),提供三層地址到二層地址之間的映射,提供二層首部緩存加速二層頭的封裝,提供二層報文頭的封裝。
在IPv4當中,實現(xiàn)這種轉換的協(xié)議為地址解析協(xié)議(Address ResolutionProtocol,ARP),而在IPv6則為鄰居發(fā)現(xiàn)協(xié)議(Neighbour Discovery Protocol,NDISC或ND),鄰接子系統(tǒng)為執(zhí)行L3到L2映射提供了獨立于協(xié)議的基礎設施。
IPv6 NDP: 鄰居發(fā)現(xiàn)協(xié)議(Neighbour Discovery Protocol, NDP),鄰居子系統(tǒng)為了執(zhí)行L3到L2的映射提供了獨立于協(xié)議的基礎設施。請求和應答分別為鄰居請求和鄰居應答。。 ND(Neighbor Discovery,鄰居發(fā)現(xiàn))協(xié)議是IPv6的一個關鍵協(xié)議,它綜合了IPv4中的ARP,ICMP路由發(fā)現(xiàn)和ICMP重定向等協(xié)議,并對他們做了改進。作為IPv6的基礎性協(xié)議,ND協(xié)議還提供了前綴發(fā)現(xiàn),鄰居不可達檢測,重復地址檢測,地址自動配置等功能 NDP 加強了地址解析協(xié)議與底層鏈路的獨立性、增強了安全性、減小了報文傳播范圍
**在第2層發(fā)送數據包時,為創(chuàng)建L2報頭,需要使用L2目標地址,使用鄰接系統(tǒng)進行請示和應答,便可根據主機的L3地址獲悉其L2地址(或獲悉這樣的L3地址不存在)。在最常用的數據鏈路層(L2)–以太網中,主機的L2地址為MAC地址。傳輸當前主機生成的外出數據包或轉發(fā)當前主機收到的數據包。**有時不需要鄰接子系統(tǒng)的幫助也能夠獲悉目標地址。比如發(fā)送廣播時,在這種情況L2目標地址是固定的,例如,在以太網中為FF:FF:FF:FF:FF:FF,有時目標地址是組播地址,L3組播地址和L2組播地址的映射關系是固定的。
鄰居子系統(tǒng),提供三層地址到二層地址之間的映射,提供二層首部緩存加速二層頭的封裝,提供二層報文頭的封裝。 如下,鄰居表信息,表達了IP地址是x.x.x.x的下一跳,它的mac地址是xx:xx:xx:xx:xx:xx,通過出接口ethx能夠到達。
Linux鄰接系統(tǒng)的基本數據結構是鄰居,表示與當前鏈路相連的網絡結點,用結構neighbour來表示。
struct neighbour 如果一臺主機和你的計算機連接在同一個LAN上(也就是說,你和這臺主機通過一個共享介質相連或點對點直接相連),那么它就是你的鄰居(neighbor),而且它有相同的L3網絡配置。
【文章福利】小編推薦自己的Linux內核技術交流群:【891587639】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ?


具體內核源碼分析如下: include\net\neighbour.h
parms: 與鄰居相關聯(lián)的neigh_parms對象,由相關聯(lián)的鄰接表的構造函數對其進行初始化。例如,在IPv4中,方法arp_constructor()將parms初始化為相關聯(lián)的網絡設備的arp_parms。不要將其與鄰接表的neigh_parms對象混為一談。
refcnt: 引用計數器。neigh_hold()宏會將其加1,而neigh_release()宏則將其減1。僅當這個引用計數器的值被減為0時,方法neigh_release()才會調用方法neigh_destroy()來釋放鄰居對象。
timer: 每個neighbour對象都有一個定時器。定時器回調函數為方法neigh_timer_handler(),它可以修改鄰居的網絡不可達檢測(NUD)狀態(tài)。在發(fā)送請求時,鄰居的狀態(tài)為NUD_INCOMPLETE或NUD_PROBE。如果請求數達到或超過neigh_max_probes()的值,將把鄰居的狀態(tài)設置為NUD_FAILED,并調用方法neigh_invalidate()。
ha_lock: 對鄰居硬件地址( ha)提供訪問保護。
ha: 鄰居對象的硬件地址。在以太網中,它為鄰居的MAC地址。
hh: L2報頭的硬件報頭緩存(一個hh_cache對象)。
output: 一個指向傳輸方法(如方法neigh_resolve_output()或neigh_direct_output() )的指針。其值取決于NUD狀態(tài),因此在鄰居的生命周期內可賦給其不同的值。在方法neigh_alloc()中初始化鄰居對象時,會將其設置為方法neigh_blackhole()。這個方法將丟棄數據包并返回-ENETDOWN。下面是設置output回調函數的輔助方法。
void neigh_connect(struct neighbour *neigh):將指定鄰居的output回調函數設置為neigh->ops->connected_output。 void neigh_suspect(struct neighbour *neigh):將指定鄰居的output回調函數設置為neigh->ops->output。 **nud_state: 鄰居的NUD狀態(tài)。**在鄰居的生命周期內,可動態(tài)地修改nud_state的值。在7.5節(jié)中,表7-1描述了基本的NUD狀態(tài)及其Linux符號。NUD狀態(tài)機非常復雜。
下面這張狀態(tài)機圖描述的很清楚(狀態(tài)變化)。

NUD_INCOMPLETE:該狀態(tài)是請求報文已發(fā)送,但尚未收到應答的狀態(tài)。該狀態(tài)下還沒解析到硬件地址,因此尚無可用硬件地址,如果有報文要輸出到該鄰居,會將其緩存起來。 這個狀態(tài)會啟動一個定時器,如果在定時器到期時還沒有接收到鄰居的回應,則會重復發(fā)送請求報文,否則發(fā)送請求報文的次數打到上限,便會進入NUD_FAILED。 NUD_REACHABLE :該狀態(tài)以及得到并緩存了鄰居的硬件地址。進入該狀態(tài)首先設置鄰居項相關的output函數(該狀態(tài)使用neighbors_ops結構的connectd_outpt),然后查看是否存在要發(fā)送給該鄰居的報文。如果在該狀態(tài)下閑置時間達到上限,便會進入NUD_STATLE。 NUD_STALE :該狀態(tài)一旦有報文要輸出到該鄰居,則會進入NUD_DELAY并將該報文輸出。如果在該狀態(tài)下閑置時間達到上限,且此時的引用計數為1,則通過垃圾回收機制將其刪除,在該狀態(tài)下,報文的輸出不收限制,使用慢速發(fā)送過程 NUD_DELAY :該狀態(tài)下表示NUD_STATE狀態(tài)下發(fā)送的報文已經發(fā)出,需得到鄰居的可達性確認的狀態(tài)。在為接收到鄰居的應答或確認時也會定時地重發(fā)請求,如果發(fā)送請求報文的次數到上限,如果收到鄰居的應答,進入NUD_REACHABLE,否則進入NUD_FAILED,在該狀態(tài)下,報文的輸出不收限制,使用慢速發(fā)送過程。 NUD_PROBE :過渡狀態(tài),和NUD_INCOMPLETE 狀態(tài)類似,在未收到鄰居狀態(tài)的應答或者確認時,也會定時的重發(fā)請求,直到收到鄰居的應答、確認、或者嘗試發(fā)送請求報文的次數達到上限,如果收到應答或者確認就會進入NUD_REACHABLE,如果嘗試發(fā)送請求到達上限,則進入NUD_FAILD狀態(tài),在該狀態(tài),報文的輸出也不受限制,使用慢速發(fā)送過程。 NUD_FAILED:由于沒有收到應答報文而無法訪問狀態(tài), NUD_NOARP:標識鄰居無需將三層地址協(xié)議映射到二層地址協(xié)議。如一些三層overlay的虛擬接口,loopback等。 NUD_PERMANENT: 設置鄰居表項的硬件地址為靜態(tài)。 dead: 一個標志**,在鄰居對象處于活動狀態(tài)時被設置。**創(chuàng)建鄰居對象時,在方法_neigh_create()末尾將其設置為0。對于dead標志未被設置的鄰居對象,調用方法neigh_destroy()將會失敗。方法neigh_flush_dev()將dead標志設置為1,但不會刪除鄰居。被標記為失效( dead標志被設置)的鄰居由垃圾收集器刪除。
primary_key: 鄰居的IP地址(L3地址),鄰接表查找是根據primary_key進行的。primary_key的長度因協(xié)議而異。例如,對于IPv4來說,其長度為4字節(jié);對于IPv6來說,其長度為sizeof(struct in6_addr),因為結構in6_addr表示IPv6地址。因此,primary_key被定義為0字節(jié)的數組,分配鄰居時,必須考慮使用的協(xié)議。詳情請參閱后面描述結構neigh_table的成員時,對entry_size和key_len的解釋。
**為避免在每次傳輸數據包時都發(fā)送請求,內核將L3地址和L2地址之間的映射存儲在了被稱為鄰接表的數據結構中。**在IPv4中,這個表就是ARP表,有時被稱為ARP緩存,但它們指的是一回事。在IPv6中,鄰接表就是NDISC表(也叫NDISC緩存)。ARP表( arp_tbl )和NDISC表( nd_tbl)都是結構neigh_table的實例。下面就來看看結構neigh_table。
**proxy_timer:主機被配置為ARP代理時,它可能不會立即處理請求,而是過一段時間再處理。這是因為,對于ARP代理主機來說,可能有大量的請求需要處理(這不同于不是ARP代理的主機,通常它們需要處理的ARP請求較少)。有時候,你可能希望延遲對這種廣播做出應答,讓擁有要解析的IP地址的主機先收到請求。**這種延遲是隨機的,最長不超過參數proxy_delay的值。對于ARP來說,代理定時器處理程序為方法neigh proxy_process()。proxy_timer由方法neigh_table_init_no_netlink()進行初始化。
stats:鄰居統(tǒng)計信息( neigh_statistics )對象,包含針對每個CPU的計數器,如allocs(方法neigh_alloc()分配的鄰居對象數)、destroys(方法neigh_destroy()釋放的鄰居對象數)等。鄰居統(tǒng)計信息計數器由NEIGH_CACHE_STAT_INC宏進行遞增操作。請注意,由于這些統(tǒng)計信息是針對每個CPU的計數器的,因此NEIGH_CACHE_STAT_INC宏將調用this_cpu_inc()宏。要顯示ARP統(tǒng)計信息和NDISC統(tǒng)計信息,可分別使用cat /proc/netstat/arp_cache和cat/proc/net/stat/ndisc_cache。在7.5節(jié)中,描述了結neigh_statistics,并指出了每個計數器的遞增方法。
phash_buckets:鄰接代理散列表,是在方法neigh_table_init_no_netlink()中分配的。鄰接表的初始化工作是使用方neigh_table_init()完成的。
在IPv4中,ARP模塊定義了ARP表(一個名為arp_tbl的neigh_table結構實例),并將其作為參數傳遞給方法neigh_table_init()(參見net/ipv4/arp.c中的方法arp_init() ).
在IPv6中,NDISC模塊定義了NDSIC表(一個名為nd_tbl的neigh_table結構實例),并將其作為參數傳遞給方法neigh_table_init()(參見net/ipv6/ndisc.c中的方法ndisc_init() )。方法neigh_table_init()還可調用方法neigh_table_init_no_netlink(),后者將調用方法neigh_hash_alloc()創(chuàng)建鄰接散列表(對象nht ),以便為8個散列條目分配空間。
使用鄰接子系統(tǒng)的每種L3協(xié)議都還注冊一個協(xié)議處理程序。對于IPv4來講,ARP數據包處理程序方法為arp_rcv() arp.c
struct neigh_ops 每個鄰居對象結構neigh_ops中定義一組方法,它包含一個協(xié)議簇成員和4個函數指針,具體內核源碼如下
鄰居創(chuàng)建是由_neigh_create()處理
方法_neigh_create()首先調用方法neigh_alloc(),以分配一個鄰居對象并執(zhí)行各種初始化。在某些情況下,方法neigh_alloc()還將調用同步垃圾收集器(方法neigh_forced_gc() ). 接下來,方法__neigh_create()將調用指定鄰接表的constructor方法(對于ARP來說,該方法為arp_constructor();對于NDISC來說,該方法為ndisc_constructor() ),以執(zhí)行因協(xié)議而異的設置工作。 在constructor方法中,將處理組播地址和環(huán)回地址等特殊情況。例如,在方法arp_constructor()中,需調用方法arp_mc_map(),來根據鄰居的IPv4 primary_key地址設置鄰居的硬件地址( ha ),再將nud_state設置為NUD_NOARP,因為組播地址不需要ARP。 對于廣播地址也需特殊對待。例如,在方法arp_constructor()中,如果鄰居類型為RTN_BROADCAST,就將其硬件地址( ha)設置為網絡設備的廣播地址( net_device對象的broadcast字段)、并將nud_state設置為NUD_NOARP。 在方法_neigh_create()的最后,將dead標志初始化為0,并將鄰居對象添加到鄰居散列表中。 鄰居刪除是由neigh_release()處理
方法neigh_release()將鄰居的引用計數器減1。如果它變成了0,就調用方法neigh_destroy()將鄰居對象釋放。方法neigh_destroy()會檢查鄰居的dead標志。如果該標志為0,就不會將鄰居刪除。 用戶空間和鄰接子系統(tǒng)之間的交互 管理ARP表,可使用iproute2包中的命令ip neigh,也可以使用net_tools包中的命令arp
arp: 由net/ipv4/arp.c中的萬法arp_seq_show(處理)。 ip neigh show(或ip neighbour show ):由net/ core/neighbour.c中的方法neigh_dump_info()處理。 請注意,命令ip neigh show顯示鄰接表條目的NUD狀態(tài),如NUD_REACHABLE或NUD_STALE。另外,命令arp只顯示IPv4鄰接表(ARP表),而命令ip顯示IPv4 ARP表和IPv6鄰接表。如果只想顯示IPv6鄰接表,可使用命令ip -6 neigh show。
ARP和NDISC模塊還可通過procfs導出數據。這意味著,要顯示ARP表,還可執(zhí)行命令cat/proc/net/arp(這個procfs條目由方法arp_seq_show()處理,該方法也用于處理前面提到的命令arp )。要顯示ARP統(tǒng)計信息,可使用命令cat/proc/net/stat/ arp_cache;而要顯示NDISC統(tǒng)計信息,可使用命令cat /proc/net/stat/ndisc_cache(這兩個命令都由方法neigh_stat_seq_show()處理)。
net\ipv4\arp.c
**要添加鄰居條目,可使用命令ip neigh add。**這個命令由方法neigh_add()處理。執(zhí)行命令ipneigh add時,可指定要添加的鄰居條目的狀態(tài)(如NUD_PERMANENT、NUD_STALE、NUD_REACHABLE等),如下所示。 ip neigh add 192.168.0.121 dev etho lladdr 00:30:48:5b:cc:45 nud permanent
要刪除鄰居條目,可使用命令ip neigh del(這個命令由方法neigh_delete()處理),如下所示。 ip neigh del 192.168.0.121 dev etho
要在代理ARP表中添加條目,可使用命令ip neigh add proxy,如下所示。ip neigh add proxy 192.168.2.11 dew etho 這種添加工作也可由方法neigh_add()進行處理,但該方法將在從用戶空間傳遞而來的數據中設置標志NTF_PROXY(參見對象ndm的ndm_flags字段),因此將調用方法pneigh_lookup()在代理鄰接表( phash_buckets )中執(zhí)行查找。如果沒有找到,方法pneigh_lookup()將在代理鄰接散列表中添加一個條目。
要從代理ARP表中刪除條目,可使用命令ip neigh del proxy,如下所示。ip neigh del proxy 192.168.2.11 dev etho 這種刪除工作由方法neigh_delete()處理。同樣,在這種情況下,將在從用戶空間傳遞而來的數據中設置NTF_PROXY標志(參見對象ndm的ndm_flags字段),因此將調用方法pneigh_delete().將條目從代理鄰接表中刪除。
使用命令ip ntable可顯示和控制鄰接表的參數,如下所示。ip ntable show:顯示所有鄰接表的參數。
ip ntable change:修改鄰接表參數的值,由方法neightbl_set()處理,如ip ntable change name arp_cache queue 20 dev etho。 還可以使用命令arp add在ARP表中添加條目。另外,還可以像下面這樣在ARP表中添加靜態(tài)條目: arp -s 。靜態(tài)ARP條目不會被鄰接子系統(tǒng)垃圾收集器刪除,但會在重啟后消失。
鄰接核心不會使用方法register_netdevice_notifier()注冊任何事件,而ARP和NDISC模塊則會注冊網絡事件。在ARP中,方法arp_netdev_event()將被注冊為netdev事件的回調函數,它調用通用方法neigh_changeaddr()以及方法rt_cache_flush()來處理MAC地址變更事件。從內核3.11起,在IFF_NOARP標志發(fā)生變化時,將調用方法neigh_changeaddr()來處理NETDEV_CHANGE事件。當設備使用方法_dev_notify_flags()修改其標志或使用方法netdev_state_change()修改其狀態(tài)時,都將觸發(fā)NETDEV_CHANGE事件。在NDISC中,方法ndisc_netdev_event()被注冊為netdev事件的回調函數,它處理NETDEV_CHANGEADDR、NETDEV_DOwN和NETDEV_NOTIFY_PEERS事件。
