您好,登錄后才能下訂單哦!
這篇文章主要介紹Linux實現ARP緩存老化時間原理問題的示例分析,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
一.問題
眾所周知,ARP是一個鏈路層的地址解析協議,它以IP地址為鍵值,查詢保有該IP地址主機的MAC地址。協議的詳情就不詳述了,你可以看RFC,也可以看教科書。這里寫這么一篇文章,主要是為了做一點記錄,同時也為同學們提供一點思路。具體呢,我遇到過兩個問題:
1.使用keepalived進行熱備份的系統(tǒng)需要一個虛擬的IP地址,然而該虛擬IP地址到底屬于哪臺機器是根據熱備群的主備來決定的,因此主機器在獲得該虛擬IP的時候,必須要廣播一個免費的arp,起初人們認為這沒有必要,理由是不這么做,熱備群也工作的很好,然而事實證明,這是必須的;
2.ARP緩存表項都有一個老化時間,然而在linux系統(tǒng)中卻沒有給出具體如何來設置這個老化時間。那么到底怎么設置這個老化時間呢?
二.解答問題前的說明
ARP協議的規(guī)范只是闡述了地址解析的細節(jié),然而并沒有規(guī)定協議棧的實現如何去維護ARP緩存。ARP緩存需要有一個到期時間,這是必要的,因為ARP緩存并不維護映射的狀態(tài),也不進行認證,因此協議本身不能保證這種映射永遠都是正確的,它只能保證該映射在得到arp應答之后的一定時間內是有效的。這也給了ARP欺騙以可乘之機,不過本文不討論這種欺騙。
像Cisco或者基于VRP的華為設備都有明確的配置來配置arp緩存的到期時間,然而Linux系統(tǒng)中卻沒有這樣的配置,起碼可以說沒有這樣的直接配置。Linux用戶都知道如果需要配置什么系統(tǒng)行為,那么使用sysctl工具配置procfs下的sys接口是一個方法,然而當我們google了好久,終于發(fā)現關于ARP的配置處在/proc/sys/net/ipv4/neigh/ethX的時候,我們最終又迷茫于該目錄下的N多文件,即使去查詢Linux內核的Documents也不能清晰的明了這些文件的具體含義。對于Linux這樣的成熟系統(tǒng),一定有辦法來配置ARP緩存的到期時間,但是具體到操作上,到底怎么配置呢?這還得從Linux實現的ARP狀態(tài)機說起。
如果你看過《Understading Linux Networking Internals》并且真的做到深入理解的話,那么本文講的基本就是廢話,但是很多人是沒有看過那本書的,因此本文的內容還是有一定價值的。
Linux協議棧實現為ARP緩存維護了一個狀態(tài)機,在理解具體的行為之前,先看一下下面的圖(該圖基于《Understading Linux Networking Internals》里面的圖26-13修改,在第二十六章):
在上圖中,我們看到只有arp緩存項的reachable狀態(tài)對于外發(fā)包是可用的,對于stale狀態(tài)的arp緩存項而言,它實際上是不可用的。如果此時有人要發(fā)包,那么需要進行重新解析,對于常規(guī)的理解,重新解析意味著要重新發(fā)送arp請求,然后事實上卻不一定這樣,因為Linux為arp增加了一個“事件點”來“不用發(fā)送arp請求”而對arp協議生成的緩存維護的優(yōu)化措施,事實上,這種措施十分有效。這就是arp的“確認”機制,也就是說,如果說從一個鄰居主動發(fā)來一個數據包到本機,那么就可以確認該包的“上一跳”這個鄰居是有效的,然而為何只有到達本機的包才能確認“上一跳”這個鄰居的有效性呢?因為Linux并不想為IP層的處理增加負擔,也即不想改變IP層的原始語義。
Linux維護一個stale狀態(tài)其實就是為了保留一個neighbour結構體,在其狀態(tài)改變時只是個別字段得到修改或者填充。如果按照簡單的實現,只保存一個reachable狀態(tài)即可,其到期則刪除arp緩存表項。Linux的做法只是做了很多的優(yōu)化,但是如果你為這些優(yōu)化而絞盡腦汁,那就悲劇了...
三.Linux如何來維護這個stale狀態(tài)
在Linux實現的ARP狀態(tài)機中,最復雜的就是stale狀態(tài)了,在此狀態(tài)中的arp緩存表項面臨著生死抉擇,抉擇者就是本地發(fā)出的包,如果本地發(fā)出的包使用了這個stale狀態(tài)的arp緩存表項,那么就將狀態(tài)機推進到delay狀態(tài),如果在“垃圾收集”定時器到期后還沒有人使用該鄰居,那么就有可能刪除這個表項了,到底刪除嗎?這樣看看有木有其它路徑使用它,關鍵是看路由緩存,路由緩存雖然是一個第三層的概念,然而卻保留了該路由的下一條的ARP緩存表項,這個意義上,Linux的路由緩存實則一個轉發(fā)表而不是一個路由表。
如果有外發(fā)包使用了這個表項,那么該表項的ARP狀態(tài)機將進入delay狀態(tài),在delay狀態(tài)中,只要有“本地”確認的到來(本地接收包的上一跳來自該鄰居),linux還是不會發(fā)送ARP請求的,但是如果一直都沒有本地確認,那么Linux就將發(fā)送真正的ARP請求了,進入probe狀態(tài)。因此可以看到,從stale狀態(tài)開始,所有的狀態(tài)只是為一種優(yōu)化措施而存在的,stale狀態(tài)的ARP緩存表項就是一個緩存的緩存,如果Linux只是將過期的reachable狀態(tài)的arp緩存表項刪除,語義是一樣的,但是實現看起來以及理解起來會簡單得多!
再次強調,reachable過期進入stale狀態(tài)而不是直接刪除,是為了保留neighbour結構體,優(yōu)化內存以及CPU利用,實際上進入stale狀態(tài)的arp緩存表項時不可用的,要想使其可用,要么在delay狀態(tài)定時器到期前本地給予了確認,比如tcp收到了一個包,要么delay狀態(tài)到期進入probe狀態(tài)后arp請求得到了回應。否則還是會被刪除。
四.Linux的ARP緩存實現要點
在blog中分析源碼是兒時的記憶了,現在不再浪費版面了。只要知道Linux在實現arp時維護的幾個定時器的要點即可。
1.Reachable狀態(tài)定時器
每當有arp回應到達或者其它能證明該ARP表項表示的鄰居真的可達時,啟動該定時器。到期時根據配置的時間將對應的ARP緩存表項轉換到下一個狀態(tài)。
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);
...
}
一旦這個定時器到期,將執(zhí)行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狀態(tài)的表項沒有被及時刪除,那么試著執(zhí)行一下下面的命令:
[plain] view plaincopyprint?ip route flush cache
ip route flush cache然后再看看ip neigh ls all的結果,注意,不要指望馬上會被刪除,因為此時垃圾回收定時器還沒有到期呢...但是我敢保證,不長的時間之后,該緩存表項將被刪除。
五.第一個問題的解決
在啟用keepalived進行基于vrrp熱備份的群組上,很多同學認為根本不需要在進入master狀態(tài)時重新綁定自己的MAC地址和虛擬IP地址,然而這是根本錯誤的,如果說沒有出現什么問題,那也是僥幸,因為各個路由器上默認配置的arp超時時間一般很短,然而我們不能依賴這種配置。請看下面的圖示:
如果發(fā)生了切換,假設路由器上的arp緩存超時時間為1小時,那么在將近一小時內,單向數據將無法通信(假設群組中的主機不會發(fā)送數據通過路由器,排出“本地確認”,畢竟我不知道路由器是不是在運行Linux),路由器上的數據將持續(xù)不斷的法往原來的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緩存表項(如果有的話),然而問題是,該表項更新的結果狀態(tài)卻是stale,這只是ARP的規(guī)定,具體在代碼中體現是這樣的,在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);
由此可見,只有實際的外發(fā)包的下一跳是1.1.1.1時,才會通過“本地確認”機制或者實際發(fā)送ARP請求的方式將對應的MAC地址映射reachable狀態(tài)。
更正:在看了keepalived的源碼之后,發(fā)現這個擔心是多余的,畢竟keepalived已經很成熟了,不應該犯“如此低級的錯誤”,keepalived在某主機切換到master之后,會主動發(fā)送免費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這個文件。其它的都只是優(yōu)化行為的措施。比如gc_stale_time這個文件記錄的是“ARP緩存表項的緩存”的存活時間,該時間只是一個緩存的緩存的存活時間,在該時間內,如果需要用到該鄰居,那么直接使用表項記錄的數據作為ARP請求的內容即可,或者得到“本地確認”后直接將其置為reachable狀態(tài),而不用再通過路由查找,ARP查找,ARP鄰居創(chuàng)建,ARP鄰居解析這種慢速的方式。
默認情況下,reachable狀態(tài)的超時時間是30秒,超過30秒,ARP緩存表項將改為stale狀態(tài),此時,你可以認為該表項已經老化到期了,只是Linux的實現中并沒有將其刪除罷了,再過了gc_stale_time時間,表項才被刪除。在ARP緩存表項成為非reachable之后,垃圾回收器負責執(zhí)行“再過了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)
//隨計化很重要,防止“共振行為”引發(fā)的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)
//隨計化很重要,防止“共振行為”引發(fā)的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下載外部網絡一個超大文件或者進行持續(xù)短連接,使用sysctl將base_reachable_time設置為5秒,將gc_stale_time為5秒。
在兩個場景下都使用ping命令來ping本地局域網的默認網關,然后迅速Ctrl-C掉這個ping,用ip neigh show all可以看到默認網關的arp表項,然而在場景1下,大約5秒之內,arp表項將變?yōu)閟tale之后不再改變,再ping的話,表項先變?yōu)閐elay再變?yōu)閜robe,然后為reachable,5秒之內再次成為stale,而在場景2下,arp表項持續(xù)為reachable以及dealy,這說明了Linux中的ARP狀態(tài)機。那么為何場景1中,當表項成為stale之后很久都不會被刪除呢?其實這是因為還有路由緩存項在使用它,此時你刪除路由緩存之后,arp表項很快被刪除。
以上是“Linux實現ARP緩存老化時間原理問題的示例分析”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業(yè)資訊頻道!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。