一.問題 眾所周知,ARP是一個鏈路層的地址解析協議,它以IP地址為鍵值,查詢保有該IP地址主機的MAC地址。協議的詳情就不詳述了,你可以看RFC,也可以看教科書。這裡寫這麼一篇文章,主要是為了做一點記錄,同時也為同學們提供一點思路。具體呢,我遇到過兩個問題:
1.使用keepalived進行熱備份的系統需要一個虛擬的IP地址,然而該虛擬IP地址到底屬於哪台機器是根據熱備群的主備來決定的,因此主機器在獲得該虛擬IP的時候,必須要廣播一個免費的arp,起初人們認為這沒有必要,理由是不這麼做,熱備群也工作的很好,然而事實證明,這是必須的;
2.ARP緩存表項都有一個老化時間,然而在linux系統中卻沒有給出具體如何來設置這個老化時間。那麼到底怎麼設置這個老化時間呢?
二.解答問題前的說明 ARP協議的規范只是闡述了地址解析的細節,然而並沒有規定協議棧的實現如何去維護ARP緩存。ARP緩存需要有一個到期時間,這是必要的,因為ARP緩存並不維護映射的狀態,也不進行認證,因此協議本身不能保證這種映射永遠都是正確的,它只能保證該映射在得到arp應答之後的一定時間內是有效的。這也給了ARP欺騙以可乘之機,不過本文不討論這種欺騙。
像Cisco或者基於VRP的華為設備都有明確的配置來配置arp緩存的到期時間,然而Linux系統中卻沒有這樣的配置,起碼可以說沒有這樣的直接配置。Linux用戶都知道如果需要配置什麼系統行為,那麼使用sysctl工具配置procfs下的sys接口是一個方法,然而當我們google了好久,終於發現關於ARP的配置處在/proc/sys/net/ipv4/neigh/ethX的時候,我們最終又迷茫於該目錄下的N多文件,即使去查詢Linux內核的Documents也不能清晰的明了這些文件的具體含義。對於Linux這樣的成熟系統,一定有辦法來配置ARP緩存的到期時間,但是具體到操作上,到底怎麼配置呢?這還得從Linux實現的ARP狀態機說起。
如果你看過《Understading Linux Networking Internals》並且真的做到深入理解的話,那麼本文講的基本就是廢話,但是很多人是沒有看過那本書的,因此本文的內容還是有一定價值的。
Linux協議棧實現為ARP緩存維護了一個狀態機,在理解具體的行為之前,先看一下下面的圖(該圖基於《Understading Linux Networking Internals》裡面的圖26-13修改,在第二十六章):
在上圖中,我們看到只有arp緩存項的reachable狀態對於外發包是可用的,對於stale狀態的arp緩存項而言,它實際上是不可用的。如果此時有人要發包,那麼需要進行重新解析,對於常規的理解,重新解析意味著要重新發送arp請求,然後事實上卻不一定這樣,因為Linux為arp增加了一個“事件點”來“不用發送arp請求”而對arp協議生成的緩存維護的優化措施,事實上,這種措施十分有效。這就是arp的“確認”機制,也就是說,如果說從一個鄰居主動發來一個數據包到本機,那麼就可以確認該包的“上一跳”這個鄰居是有效的,然而為何只有到達本機的包才能確認“上一跳”這個鄰居的有效性呢?因為Linux並不想為IP層的處理增加負擔,也即不想改變IP層的原始語義。
Linux維護一個stale狀態其實就是為了保留一個neighbour結構體,在其狀態改變時只是個別字段得到修改或者填充。如果按照簡單的實現,只保存一個reachable狀態即可,其到期則刪除arp緩存表項。Linux的做法只是做了很多的優化,但是如果你為這些優化而絞盡腦汁,那就悲劇了...
三.Linux如何來維護這個stale狀態 在Linux實現的ARP狀態機中,最復雜的就是stale狀態了,在此狀態中的arp緩存表項面臨著生死抉擇,抉擇者就是本地發出的包,如果本地發出的包使用了這個stale狀態的arp緩存表項,那麼就將狀態機推進到delay狀態,如果在“垃圾收集”定時器到期後還沒有人使用該鄰居,那麼就有可能刪除這個表項了,到底刪除嗎?這樣看看有木有其它路徑使用它,關鍵是看路由緩存,路由緩存雖然是一個第三層的概念,然而卻保留了該路由的下一條的ARP緩存表項,這個意義上,Linux的路由緩存實則一個轉發表而不是一個路由表。
如果有外發包使用了這個表項,那麼該表項的ARP狀態機將進入delay狀態,在delay狀態中,只要有“本地”確認的到來(本地接收包的上一跳來自該鄰居),linux還是不會發送ARP請求的,但是如果一直都沒有本地確認,那麼Linux就將發送真正的ARP請求了,進入probe狀態。因此可以看到,從stale狀態開始,所有的狀態只是為一種優化措施而存在的,stale狀態的ARP緩存表項就是一個緩存的緩存,如果Linux只是將過期的reachable狀態的arp緩存表項刪除,語義是一樣的,但是實現看起來以及理解起來會簡單得多!
再次強調,reachable過期進入stale狀態而不是直接刪除,是為了保留neighbour結構體,優化內存以及CPU利用,實際上進入stale狀態的arp緩存表項時不可用的,要想使其可用,要麼在delay狀態定時器到期前本地給予了確認,比如tcp收到了一個包,要麼delay狀態到期進入probe狀態後arp請求得到了回應。否則還是會被刪除。
四.Linux的ARP緩存實現要點 在blog中分析源碼是兒時的記憶了,現在不再浪費版面了。只要知道Linux在實現arp時維護的幾個定時器的要點即可。
1.Reachable狀態定時器
每當有arp回應到達或者其它能證明該ARP表項表示的鄰居真的可達時,啟動該定時器。到期時根據配置的時間將對應的ARP緩存表項轉換到下一個狀態。
2.垃圾回收定時器
定時啟動該定時器,具體下一次什麼到期,是根據配置的base_reachable_time來決定的,具體見下面的代碼:
復制代碼代碼如下:
static void neigh_periodic_timer(unsigned long arg)
{
...
if (time_after(now, tbl->last_rand + 300 * HZ)) { //內核每5分鐘重新進行一次配置
struct neigh_parms *p;
tbl->last_rand = now;
for (p = &tbl->parms; p; p = p->next)
p->reachable_time =
neigh_rand_reach_time(p->base_reachable_time);
}
...
/* Cycle through all hash buckets every base_reachable_time/2 ticks.
* ARP entry timeouts range from 1/2 base_reachable_time to 3/2
* base_reachable_time.
*/
expire = tbl->parms.base_reachable_time >> 1;
expire /= (tbl->hash_mask + 1);
if (!expire)
expire = 1;
//下次何時到期完全基於base_reachable_time);
mod_timer(&tbl->gc_timer, now + expire);
...
}
static void neigh_periodic_timer(unsigned long arg)
{
...
if (time_after(now, tbl->last_rand + 300 * HZ)) { //內核每5分鐘重新進行一次配置
struct neigh_parms *p;
tbl->last_rand = now;
for (p = &tbl->parms; p; p = p->next)
p->reachable_time =
neigh_rand_reach_time(p->base_reachable_time);
}
...
/* Cycle through all hash buckets every base_reachable_time/2 ticks.
* ARP entry timeouts range from 1/2 base_reachable_time to 3/2
* base_reachable_time.
*/
expire = tbl->parms.base_reachable_time >> 1;
expire /= (tbl->hash_mask + 1);
if (!expire)
expire = 1;
//下次何時到期完全基於base_reachable_time);
mod_timer(&tbl->gc_timer, now + expire);
...
}
一旦這個定時器到期,將執行neigh_periodic_timer回調函數,裡面有以下的邏輯,也即上面的...省略的部分:
復制代碼代碼如下:
if (atomic_read(&n->refcnt) == 1 && //n->used可能會因為“本地確認”機制而向前推進
(state == NUD_FAILED ||time_after(now, n->used + n->parms->gc_staletime))) {
*np = n->next;
n->dead = 1;
write_unlock(&n->lock);
neigh_release(n);
continue;
}
if (atomic_read(&n->refcnt) == 1 && //n->used可能會因為“本地確認”機制而向前推進
(state == NUD_FAILED ||time_after(now, n->used + n->parms->gc_staletime))) {
*np = n->next;
n->dead = 1;
write_unlock(&n->lock);
neigh_release(n);
continue;
}
如果在實驗中,你的處於stale狀態的表項沒有被及時刪除,那麼試著執行一下下面的命令:
[plain] view plaincopyprint?ip route flush cache
ip route flush cache然後再看看ip neigh ls all的結果,注意,不要指望馬上會被刪除,因為此時垃圾回收定時器還沒有到期呢...但是我敢保證,不長的時間之後,該緩存表項將被刪除。
五.第一個問題的解決 在啟用keepalived進行基於vrrp熱備份的群組上,很多同學認為根本不需要在進入master狀態時重新綁定自己的MAC地址和虛擬IP地址,然而這是根本錯誤的,如果說沒有出現什麼問題,那也是僥幸,因為各個路由器上默認配置的arp超時時間一般很短,然而我們不能依賴這種配置。請看下面的圖示:
如果發生了切換,假設路由器上的arp緩存超時時間為1小時,那麼在將近一小時內,單向數據將無法通信(假設群組中的主機不會發送數據通過路由器,排出“本地確認”,畢竟我不知道路由器是不是在運行Linux),路由器上的數據將持續不斷的法往原來的master,然而原始的matser已經不再持有虛擬IP地址。
因此,為了使得數據行為不再依賴路由器的配置,必須在vrrp協議下切換到master時手動綁定虛擬IP地址和自己的MAC地址,在Linux上使用方便的arping則是:
[plain] view plaincopyprint?arping -i ethX -S 1.1.1.1 -B -c 1
arping -i ethX -S 1.1.1.1 -B -c 1這樣一來,獲得1.1.1.1這個IP地址的master主機將IP地址為255.255.255.255的ARP請求廣播到全網,假設路由器運行Linux,則路由器接收到該ARP請求後將根據來源IP地址更新其本地的ARP緩存表項(如果有的話),然而問題是,該表項更新的結果狀態卻是stale,這只是ARP的規定,具體在代碼中體現是這樣的,在arp_process函數的最後:
復制代碼代碼如下:
if (arp->ar_op != htons(ARPOP_REPLY) || skb->pkt_type != PACKET_HOST)
state = NUD_STALE;
neigh_update(n, sha, state, override ? NEIGH_UPDATE_F_OVERRIDE : 0);
if (arp->ar_op != htons(ARPOP_REPLY) || skb->pkt_type != PACKET_HOST)
state = NUD_STALE;
neigh_update(n, sha, state, override ? NEIGH_UPDATE_F_OVERRIDE : 0);
由此可見,只有實際的外發包的下一跳是1.1.1.1時,才會通過“本地確認”機制或者實際發送ARP請求的方式將對應的MAC地址映射reachable狀態。
更正:在看了keepalived的源碼之後,發現這個擔心是多余的,畢竟keepalived已經很成熟了,不應該犯“如此低級的錯誤”,keepalived在某主機切換到master之後,會主動發送免費arp,在keepalived中有代碼如是:
復制代碼代碼如下:
vrrp_send_update(vrrp_rt * vrrp, ip_address * ipaddress, int idx)
{
char *msg;
char addr_str[41];
if (!IP_IS6(ipaddress)) {
msg = "gratuitous ARPs";
inet_ntop(AF_INET, &ipaddress->u.sin.sin_addr, addr_str, 41);
send_gratuitous_arp(ipaddress);
} else {
msg = "Unsolicited Neighbour Adverts";
inet_ntop(AF_INET6, &ipaddress->u.sin6_addr, addr_str, 41);
ndisc_send_unsolicited_na(ipaddress);
}
if (0 == idx && debug & 32) {
log_message(LOG_INFO, "VRRP_Instance(%s) Sending %s on %s for %s",
vrrp->iname, msg, IF_NAME(ipaddress->ifp), addr_str);
}
}
vrrp_send_update(vrrp_rt * vrrp, ip_address * ipaddress, int idx)
{
char *msg;
char addr_str[41];
if (!IP_IS6(ipaddress)) {
msg = "gratuitous ARPs";
inet_ntop(AF_INET, &ipaddress->u.sin.sin_addr, addr_str, 41);
send_gratuitous_arp(ipaddress);
} else {
msg = "Unsolicited Neighbour Adverts";
inet_ntop(AF_INET6, &ipaddress->u.sin6_addr, addr_str, 41);
ndisc_send_unsolicited_na(ipaddress);
}
if (0 == idx && debug & 32) {
log_message(LOG_INFO, "VRRP_Instance(%s) Sending %s on %s for %s",
vrrp->iname, msg, IF_NAME(ipaddress->ifp), addr_str);
}
}
六.第二個問題的解決 扯了這麼多,在Linux上到底怎麼設置ARP緩存的老化時間呢?
我們看到/proc/sys/net/ipv4/neigh/ethX目錄下面有多個文件,到底哪個是ARP緩存的老化時間呢?實際上,直接點說,就是base_reachable_time這個文件。其它的都只是優化行為的措施。比如gc_stale_time這個文件記錄的是“ARP緩存表項的緩存”的存活時間,該時間只是一個緩存的緩存的存活時間,在該時間內,如果需要用到該鄰居,那麼直接使用表項記錄的數據作為ARP請求的內容即可,或者得到“本地確認”後直接將其置為reachable狀態,而不用再通過路由查找,ARP查找,ARP鄰居創建,ARP鄰居解析這種慢速的方式。
默認情況下,reachable狀態的超時時間是30秒,超過30秒,ARP緩存表項將改為stale狀態,此時,你可以認為該表項已經老化到期了,只是Linux的實現中並沒有將其刪除罷了,再過了gc_stale_time時間,表項才被刪除。在ARP緩存表項成為非reachable之後,垃圾回收器負責執行“再過了gc_stale_time時間,表項才被刪除”這件事,這個定時器的下次到期時間是根據base_reachable_time計算出來的,具體就是在neigh_periodic_timer中:
復制代碼代碼如下:
if (time_after(now, tbl->last_rand + 300 * HZ)) {
struct neigh_parms *p;
tbl->last_rand = now;
for (p = &tbl->parms; p; p = p->next)
//隨計化很重要,防止“共振行為”引發的ARP解析風暴
p->reachable_time = neigh_rand_reach_time(p->base_reachable_time);
}
...
expire = tbl->parms.base_reachable_time >> 1;
expire /= (tbl->hash_mask + 1);
if (!expire)
expire = 1;
mod_timer(&tbl->gc_timer, now + expire);
if (time_after(now, tbl->last_rand + 300 * HZ)) {
struct neigh_parms *p;
tbl->last_rand = now;
for (p = &tbl->parms; p; p = p->next)
//隨計化很重要,防止“共振行為”引發的ARP解析風暴
p->reachable_time = neigh_rand_reach_time(p->base_reachable_time);
}
...
expire = tbl->parms.base_reachable_time >> 1;
expire /= (tbl->hash_mask + 1);
if (!expire)
expire = 1;
mod_timer(&tbl->gc_timer, now + expire);
可見一斑啊!適當地,我們可以通過看代碼注釋來理解這一點,好心人都會寫上注釋的。為了實驗的條理清晰,我們設計以下兩個場景:
1.使用iptables禁止一切本地接收,從而屏蔽arp本地確認,使用sysctl將base_reachable_time設置為5秒,將gc_stale_time為5秒。
2.關閉iptables的禁止策略,使用TCP下載外部網絡一個超大文件或者進行持續短連接,使用sysctl將base_reachable_time設置為5秒,將gc_stale_time為5秒。
在兩個場景下都使用ping命令來ping本地局域網的默認網關,然後迅速Ctrl-C掉這個ping,用ip neigh show all可以看到默認網關的arp表項,然而在場景1下,大約5秒之內,arp表項將變為stale之後不再改變,再ping的話,表項先變為delay再變為probe,然後為reachable,5秒之內再次成為stale,而在場景2下,arp表項持續為reachable以及dealy,這說明了Linux中的ARP狀態機。那麼為何場景1中,當表項成為stale之後很久都不會被刪除呢?其實這是因為還有路由緩存項在使用它,此時你刪除路由緩存之後,arp表項很快被刪除。
七.總結 1.在Linux上如果你想設置你的ARP緩存老化時間,那麼執行sysctl -w net.ipv4.neigh.ethX=Y即可,如果設置別的,只是影響了性能,在Linux中,ARP緩存老化以其變為stale狀態為准,而不是以其表項被刪除為准,stale狀態只是對緩存又進行了緩存;
2.永遠記住,在將一個IP地址更換到另一台本網段設備時,盡可能快地廣播免費ARP,在Linux上可以使用arping來玩小技巧。