一.問題
眾所周知,ARP是一個(gè)鏈路層的地址解析協(xié)議,它以IP地址為鍵值,查詢保有該IP地址主機(jī)的MAC地址。協(xié)議的詳情就不詳述了,你可以看RFC,也可以看教科書。這里寫這么一篇文章,主要是為了做一點(diǎn)記錄,同時(shí)也為同學(xué)們提供一點(diǎn)思路。具體呢,我遇到過兩個(gè)問題:
1.使用keepalived進(jìn)行熱備份的系統(tǒng)需要一個(gè)虛擬的IP地址,然而該虛擬IP地址到底屬于哪臺(tái)機(jī)器是根據(jù)熱備群的主備來決定的,因此主機(jī)器在獲得該虛擬IP的時(shí)候,必須要廣播一個(gè)免費(fèi)的arp,起初人們認(rèn)為這沒有必要,理由是不這么做,熱備群也工作的很好,然而事實(shí)證明,這是必須的;
2.ARP緩存表項(xiàng)都有一個(gè)老化時(shí)間,然而在linux系統(tǒng)中卻沒有給出具體如何來設(shè)置這個(gè)老化時(shí)間。那么到底怎么設(shè)置這個(gè)老化時(shí)間呢?
二.解答問題前的說明
ARP協(xié)議的規(guī)范只是闡述了地址解析的細(xì)節(jié),然而并沒有規(guī)定協(xié)議棧的實(shí)現(xiàn)如何去維護(hù)ARP緩存。ARP緩存需要有一個(gè)到期時(shí)間,這是必要的,因?yàn)锳RP緩存并不維護(hù)映射的狀態(tài),也不進(jìn)行認(rèn)證,因此協(xié)議本身不能保證這種映射永遠(yuǎn)都是正確的,它只能保證該映射在得到arp應(yīng)答之后的一定時(shí)間內(nèi)是有效的。這也給了ARP欺騙以可乘之機(jī),不過本文不討論這種欺騙。
像Cisco或者基于VRP的華為設(shè)備都有明確的配置來配置arp緩存的到期時(shí)間,然而Linux系統(tǒng)中卻沒有這樣的配置,起碼可以說沒有這樣的直接配置。Linux用戶都知道如果需要配置什么系統(tǒng)行為,那么使用sysctl工具配置procfs下的sys接口是一個(gè)方法,然而當(dāng)我們google了好久,終于發(fā)現(xiàn)關(guān)于ARP的配置處在/proc/sys/net/ipv4/neigh/ethX的時(shí)候,我們最終又迷茫于該目錄下的N多文件,即使去查詢Linux內(nèi)核的Documents也不能清晰的明了這些文件的具體含義。對(duì)于Linux這樣的成熟系統(tǒng),一定有辦法來配置ARP緩存的到期時(shí)間,但是具體到操作上,到底怎么配置呢?這還得從Linux實(shí)現(xiàn)的ARP狀態(tài)機(jī)說起。
如果你看過《Understading Linux Networking Internals》并且真的做到深入理解的話,那么本文講的基本就是廢話,但是很多人是沒有看過那本書的,因此本文的內(nèi)容還是有一定價(jià)值的。
Linux協(xié)議棧實(shí)現(xiàn)為ARP緩存維護(hù)了一個(gè)狀態(tài)機(jī),在理解具體的行為之前,先看一下下面的圖(該圖基于《Understading Linux Networking Internals》里面的圖26-13修改,在第二十六章):
在上圖中,我們看到只有arp緩存項(xiàng)的reachable狀態(tài)對(duì)于外發(fā)包是可用的,對(duì)于stale狀態(tài)的arp緩存項(xiàng)而言,它實(shí)際上是不可用的。如果此時(shí)有人要發(fā)包,那么需要進(jìn)行重新解析,對(duì)于常規(guī)的理解,重新解析意味著要重新發(fā)送arp請(qǐng)求,然后事實(shí)上卻不一定這樣,因?yàn)長(zhǎng)inux為arp增加了一個(gè)“事件點(diǎn)”來“不用發(fā)送arp請(qǐng)求”而對(duì)arp協(xié)議生成的緩存維護(hù)的優(yōu)化措施,事實(shí)上,這種措施十分有效。這就是arp的“確認(rèn)”機(jī)制,也就是說,如果說從一個(gè)鄰居主動(dòng)發(fā)來一個(gè)數(shù)據(jù)包到本機(jī),那么就可以確認(rèn)該包的“上一跳”這個(gè)鄰居是有效的,然而為何只有到達(dá)本機(jī)的包才能確認(rèn)“上一跳”這個(gè)鄰居的有效性呢?因?yàn)長(zhǎng)inux并不想為IP層的處理增加負(fù)擔(dān),也即不想改變IP層的原始語義。
Linux維護(hù)一個(gè)stale狀態(tài)其實(shí)就是為了保留一個(gè)neighbour結(jié)構(gòu)體,在其狀態(tài)改變時(shí)只是個(gè)別字段得到修改或者填充。如果按照簡(jiǎn)單的實(shí)現(xiàn),只保存一個(gè)reachable狀態(tài)即可,其到期則刪除arp緩存表項(xiàng)。Linux的做法只是做了很多的優(yōu)化,但是如果你為這些優(yōu)化而絞盡腦汁,那就悲劇了...
三.Linux如何來維護(hù)這個(gè)stale狀態(tài)
在Linux實(shí)現(xiàn)的ARP狀態(tài)機(jī)中,最復(fù)雜的就是stale狀態(tài)了,在此狀態(tài)中的arp緩存表項(xiàng)面臨著生死抉擇,抉擇者就是本地發(fā)出的包,如果本地發(fā)出的包使用了這個(gè)stale狀態(tài)的arp緩存表項(xiàng),那么就將狀態(tài)機(jī)推進(jìn)到delay狀態(tài),如果在“垃圾收集”定時(shí)器到期后還沒有人使用該鄰居,那么就有可能刪除這個(gè)表項(xiàng)了,到底刪除嗎?這樣看看有木有其它路徑使用它,關(guān)鍵是看路由緩存,路由緩存雖然是一個(gè)第三層的概念,然而卻保留了該路由的下一條的ARP緩存表項(xiàng),這個(gè)意義上,Linux的路由緩存實(shí)則一個(gè)轉(zhuǎn)發(fā)表而不是一個(gè)路由表。
如果有外發(fā)包使用了這個(gè)表項(xiàng),那么該表項(xiàng)的ARP狀態(tài)機(jī)將進(jìn)入delay狀態(tài),在delay狀態(tài)中,只要有“本地”確認(rèn)的到來(本地接收包的上一跳來自該鄰居),linux還是不會(huì)發(fā)送ARP請(qǐng)求的,但是如果一直都沒有本地確認(rèn),那么Linux就將發(fā)送真正的ARP請(qǐng)求了,進(jìn)入probe狀態(tài)。因此可以看到,從stale狀態(tài)開始,所有的狀態(tài)只是為一種優(yōu)化措施而存在的,stale狀態(tài)的ARP緩存表項(xiàng)就是一個(gè)緩存的緩存,如果Linux只是將過期的reachable狀態(tài)的arp緩存表項(xiàng)刪除,語義是一樣的,但是實(shí)現(xiàn)看起來以及理解起來會(huì)簡(jiǎn)單得多!
再次強(qiáng)調(diào),reachable過期進(jìn)入stale狀態(tài)而不是直接刪除,是為了保留neighbour結(jié)構(gòu)體,優(yōu)化內(nèi)存以及CPU利用,實(shí)際上進(jìn)入stale狀態(tài)的arp緩存表項(xiàng)時(shí)不可用的,要想使其可用,要么在delay狀態(tài)定時(shí)器到期前本地給予了確認(rèn),比如tcp收到了一個(gè)包,要么delay狀態(tài)到期進(jìn)入probe狀態(tài)后arp請(qǐng)求得到了回應(yīng)。否則還是會(huì)被刪除。
四.Linux的ARP緩存實(shí)現(xiàn)要點(diǎn)
在blog中分析源碼是兒時(shí)的記憶了,現(xiàn)在不再浪費(fèi)版面了。只要知道Linux在實(shí)現(xiàn)arp時(shí)維護(hù)的幾個(gè)定時(shí)器的要點(diǎn)即可。
1.Reachable狀態(tài)定時(shí)器
每當(dāng)有arp回應(yīng)到達(dá)或者其它能證明該ARP表項(xiàng)表示的鄰居真的可達(dá)時(shí),啟動(dòng)該定時(shí)器。到期時(shí)根據(jù)配置的時(shí)間將對(duì)應(yīng)的ARP緩存表項(xiàng)轉(zhuǎn)換到下一個(gè)狀態(tài)。
2.垃圾回收定時(shí)器
定時(shí)啟動(dòng)該定時(shí)器,具體下一次什么到期,是根據(jù)配置的base_reachable_time來決定的,具體見下面的代碼:
static void neigh_periodic_timer(unsigned long arg)
{
...
if (time_after(now, tbl->last_rand + 300 * HZ)) { //內(nèi)核每5分鐘重新進(jìn)行一次配置
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;
//下次何時(shí)到期完全基于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)) { //內(nèi)核每5分鐘重新進(jìn)行一次配置
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;
//下次何時(shí)到期完全基于base_reachable_time);
mod_timer(tbl->gc_timer, now + expire);
...
}
一旦這個(gè)定時(shí)器到期,將執(zhí)行neigh_periodic_timer回調(diào)函數(shù),里面有以下的邏輯,也即上面的...省略的部分:
if (atomic_read(n->refcnt) == 1 //n->used可能會(huì)因?yàn)?ldquo;本地確認(rèn)”機(jī)制而向前推進(jìn)
(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可能會(huì)因?yàn)?ldquo;本地確認(rèn)”機(jī)制而向前推進(jìn)
(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;
}
如果在實(shí)驗(yàn)中,你的處于stale狀態(tài)的表項(xiàng)沒有被及時(shí)刪除,那么試著執(zhí)行一下下面的命令:
[plain] view plaincopyprint?ip route flush cache
ip route flush cache然后再看看ip neigh ls all的結(jié)果,注意,不要指望馬上會(huì)被刪除,因?yàn)榇藭r(shí)垃圾回收定時(shí)器還沒有到期呢...但是我敢保證,不長(zhǎng)的時(shí)間之后,該緩存表項(xiàng)將被刪除。
五.第一個(gè)問題的解決
在啟用keepalived進(jìn)行基于vrrp熱備份的群組上,很多同學(xué)認(rèn)為根本不需要在進(jìn)入master狀態(tài)時(shí)重新綁定自己的MAC地址和虛擬IP地址,然而這是根本錯(cuò)誤的,如果說沒有出現(xiàn)什么問題,那也是僥幸,因?yàn)楦鱾€(gè)路由器上默認(rèn)配置的arp超時(shí)時(shí)間一般很短,然而我們不能依賴這種配置。請(qǐng)看下面的圖示:
如果發(fā)生了切換,假設(shè)路由器上的arp緩存超時(shí)時(shí)間為1小時(shí),那么在將近一小時(shí)內(nèi),單向數(shù)據(jù)將無法通信(假設(shè)群組中的主機(jī)不會(huì)發(fā)送數(shù)據(jù)通過路由器,排出“本地確認(rèn)”,畢竟我不知道路由器是不是在運(yùn)行Linux),路由器上的數(shù)據(jù)將持續(xù)不斷的法往原來的master,然而原始的matser已經(jīng)不再持有虛擬IP地址。
因此,為了使得數(shù)據(jù)行為不再依賴路由器的配置,必須在vrrp協(xié)議下切換到master時(shí)手動(dòng)綁定虛擬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這個(gè)IP地址的master主機(jī)將IP地址為255.255.255.255的ARP請(qǐng)求廣播到全網(wǎng),假設(shè)路由器運(yùn)行Linux,則路由器接收到該ARP請(qǐng)求后將根據(jù)來源IP地址更新其本地的ARP緩存表項(xiàng)(如果有的話),然而問題是,該表項(xiàng)更新的結(jié)果狀態(tài)卻是stale,這只是ARP的規(guī)定,具體在代碼中體現(xiàn)是這樣的,在arp_process函數(shù)的最后:
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);
由此可見,只有實(shí)際的外發(fā)包的下一跳是1.1.1.1時(shí),才會(huì)通過“本地確認(rèn)”機(jī)制或者實(shí)際發(fā)送ARP請(qǐng)求的方式將對(duì)應(yīng)的MAC地址映射reachable狀態(tài)。
更正:在看了keepalived的源碼之后,發(fā)現(xiàn)這個(gè)擔(dān)心是多余的,畢竟keepalived已經(jīng)很成熟了,不應(yīng)該犯“如此低級(jí)的錯(cuò)誤”,keepalived在某主機(jī)切換到master之后,會(huì)主動(dòng)發(fā)送免費(fèi)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);
}
}
六.第二個(gè)問題的解決
扯了這么多,在Linux上到底怎么設(shè)置ARP緩存的老化時(shí)間呢?
我們看到/proc/sys/net/ipv4/neigh/ethX目錄下面有多個(gè)文件,到底哪個(gè)是ARP緩存的老化時(shí)間呢?實(shí)際上,直接點(diǎn)說,就是base_reachable_time這個(gè)文件。其它的都只是優(yōu)化行為的措施。比如gc_stale_time這個(gè)文件記錄的是“ARP緩存表項(xiàng)的緩存”的存活時(shí)間,該時(shí)間只是一個(gè)緩存的緩存的存活時(shí)間,在該時(shí)間內(nèi),如果需要用到該鄰居,那么直接使用表項(xiàng)記錄的數(shù)據(jù)作為ARP請(qǐng)求的內(nèi)容即可,或者得到“本地確認(rèn)”后直接將其置為reachable狀態(tài),而不用再通過路由查找,ARP查找,ARP鄰居創(chuàng)建,ARP鄰居解析這種慢速的方式。
默認(rèn)情況下,reachable狀態(tài)的超時(shí)時(shí)間是30秒,超過30秒,ARP緩存表項(xiàng)將改為stale狀態(tài),此時(shí),你可以認(rèn)為該表項(xiàng)已經(jīng)老化到期了,只是Linux的實(shí)現(xiàn)中并沒有將其刪除罷了,再過了gc_stale_time時(shí)間,表項(xiàng)才被刪除。在ARP緩存表項(xiàng)成為非reachable之后,垃圾回收器負(fù)責(zé)執(zhí)行“再過了gc_stale_time時(shí)間,表項(xiàng)才被刪除”這件事,這個(gè)定時(shí)器的下次到期時(shí)間是根據(jù)base_reachable_time計(jì)算出來的,具體就是在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)
//隨計(jì)化很重要,防止“共振行為”引發(fā)的ARP解析風(fēng)暴
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)
//隨計(jì)化很重要,防止“共振行為”引發(fā)的ARP解析風(fēng)暴
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);
可見一斑??!適當(dāng)?shù)兀覀兛梢酝ㄟ^看代碼注釋來理解這一點(diǎn),好心人都會(huì)寫上注釋的。為了實(shí)驗(yàn)的條理清晰,我們?cè)O(shè)計(jì)以下兩個(gè)場(chǎng)景:
1.使用iptables禁止一切本地接收,從而屏蔽arp本地確認(rèn),使用sysctl將base_reachable_time設(shè)置為5秒,將gc_stale_time為5秒。
2.關(guān)閉iptables的禁止策略,使用TCP下載外部網(wǎng)絡(luò)一個(gè)超大文件或者進(jìn)行持續(xù)短連接,使用sysctl將base_reachable_time設(shè)置為5秒,將gc_stale_time為5秒。
在兩個(gè)場(chǎng)景下都使用ping命令來ping本地局域網(wǎng)的默認(rèn)網(wǎng)關(guān),然后迅速Ctrl-C掉這個(gè)ping,用ip neigh show all可以看到默認(rèn)網(wǎng)關(guān)的arp表項(xiàng),然而在場(chǎng)景1下,大約5秒之內(nèi),arp表項(xiàng)將變?yōu)閟tale之后不再改變,再ping的話,表項(xiàng)先變?yōu)閐elay再變?yōu)閜robe,然后為reachable,5秒之內(nèi)再次成為stale,而在場(chǎng)景2下,arp表項(xiàng)持續(xù)為reachable以及dealy,這說明了Linux中的ARP狀態(tài)機(jī)。那么為何場(chǎng)景1中,當(dāng)表項(xiàng)成為stale之后很久都不會(huì)被刪除呢?其實(shí)這是因?yàn)檫€有路由緩存項(xiàng)在使用它,此時(shí)你刪除路由緩存之后,arp表項(xiàng)很快被刪除。
七.總結(jié)
1.在Linux上如果你想設(shè)置你的ARP緩存老化時(shí)間,那么執(zhí)行sysctl -w net.ipv4.neigh.ethX=Y即可,如果設(shè)置別的,只是影響了性能,在Linux中,ARP緩存老化以其變?yōu)閟tale狀態(tài)為準(zhǔn),而不是以其表項(xiàng)被刪除為準(zhǔn),stale狀態(tài)只是對(duì)緩存又進(jìn)行了緩存;
2.永遠(yuǎn)記住,在將一個(gè)IP地址更換到另一臺(tái)本網(wǎng)段設(shè)備時(shí),盡可能快地廣播免費(fèi)ARP,在Linux上可以使用arping來玩小技巧。