上回說到流量輸出會走到 neigh_resolve_output 我們來看看鄰居這玩意能玩點什麼花樣,傳說的arp學習在哪裡 int neigh_resolve_output(struct sk_buff *skb) {
struct dst_entry *dst = skb_dst(skb);
struct neighbour *neigh;
int rc = 0;
if (!dst || !(neigh = dst->neighbour)) 異常退出
goto discard;
__skb_pull(skb, skb_network_offset(skb));
if (!neigh_event_send(neigh, skb)) { 判斷鄰居項是否有可用狀态,如果可用,則把資料包發送出去
int err;
struct net_device *dev = neigh->dev;
if (dev->header_ops->cache && !dst->hh) {
write_lock_bh(&neigh->lock);
if (!dst->hh)
neigh_hh_init(neigh, dst, dst->ops->protocol);
err = dev_hard_header(skb, dev, ntohs(skb->protocol),
neigh->ha, NULL, skb->len);
write_unlock_bh(&neigh->lock);
} 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)
rc = neigh->ops->queue_xmit(skb);
else
goto out_kfree_skb;
}
out:
return rc;
discard:
NEIGH_PRINTK1("neigh_resolve_output: dst=%p neigh=%p\n",
dst, dst ? dst->neighbour : NULL);
out_kfree_skb:
rc = -EINVAL;
kfree_skb(skb);
goto out;
} 這裡叫鄰居事件發送,自己瞎叫的
static inline int neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
neigh->used = jiffies;
if (!(neigh->nud_state& (NUD_CONNECTED|NUD_DELAY|NUD_PROBE))) 假設帶着NUD_NONE進來的
return __neigh_event_send(neigh, skb);
return 0;
} 這裡開始涉及狀态的東西了 先對NUD狀态描述下 #define NUD_INCOMPLETE 0x01 一個請求已發送出去,但還沒收到應答,在這個狀态不使用任何硬體位址
#define NUD_REACHABLE 0x02 鄰居的位址被放入緩存,并且知道該鄰居是可到達的
#define NUD_STALE 0x04
#define NUD_DELAY 0x08
#define NUD_PROBE 0x10 這三個用于狀态轉換階段,當本地主機确定鄰居是否可到達時,狀态會發生改變
#define NUD_FAILED 0x20 由于請求失敗,将鄰居标記為不可達
#define NUD_NOARP 0x40 用于标記不要任何協定進行L3到L2的位址映射的鄰居
#define NUD_PERMANENT 0x80 鄰居的L2位址是靜态配置,是以不需要鄰居協定進行位址解析
#define NUD_NONE 0x00 鄰居項剛被建立,還沒有狀态可用
派生狀态 NUD_IN_TIMER : 當某一鄰居項的狀态不是很清晰時,鄰居子系統就為其運作一個定時器 #define NUD_IN_TIMER (NUD_INCOMPLETE|NUD_REACHABLE|NUD_DELAY|NUD_PROBE)
#define NUD_VALID (NUD_PERMANENT|NUD_NOARP|NUD_REACHABLE|NUD_PROBE|NUD_STALE|NUD_DELAY) NUD_CONNECTED : 這個狀态為NUD_VALID的子狀态,沒有未決的确認要處理 #define NUD_CONNECTED (NUD_PERMANENT|NUD_NOARP|NUD_REACHABLE) int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
int rc;
unsigned long now;
write_lock_bh(&neigh->lock);
rc = 0;
if (neigh->nud_state & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE))
goto out_unlock_bh;
now = jiffies;
if (!( neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) {
if (neigh->parms->mcast_probes + neigh->parms->app_probes) { .mcast_probes = 3,
atomic_set(&neigh->probes, neigh->parms->ucast_probes); 重發arp請求的次數 .ucast_probes = 3,
neigh->nud_state = NUD_INCOMPLETE; 設狀态為NUD_INCOMPLETE,
neigh->updated = jiffies;
neigh_add_timer(neigh, now + 1); 且設定定時器
} else {
neigh->nud_state = NUD_FAILED;
neigh->updated = jiffies;
write_unlock_bh(&neigh->lock);
kfree_skb(skb);
return 1;
}
} else if (neigh->nud_state & NUD_STALE) {
NEIGH_PRINTK2("neigh %p is delayed.\n", neigh);
neigh->nud_state = NUD_DELAY;
neigh->updated = jiffies;
neigh_add_timer(neigh,
jiffies + neigh->parms->delay_probe_time);
}
if (neigh->nud_state == NUD_INCOMPLETE) { 如果是NUD_INCOMPLETE狀态
if (skb) {
if (skb_queue_len(&neigh->arp_queue) >= 檢查隊列長度,應該是每個鄰居項都有一個可供緩存待解析的skb
neigh->parms->queue_len) { .queue_len = 3,
struct sk_buff *buff;
buff = __skb_dequeue(&neigh->arp_queue);
kfree_skb(buff); 取出一個 釋放掉
NEIGH_CACHE_STAT_INC(neigh->tbl, unres_discards);
}
__skb_queue_tail(&neigh->arp_queue, skb); 把這個新的放進去
}
rc = 1;
}
out_unlock_bh:
write_unlock_bh(&neigh->lock);
return rc;
} 上面這段函數的主要作用将鄰居狀态置成NUD_INCOMPLETE,并啟動定時器,我們看下定時器裡幹了啥 setup_timer(&n->timer, neigh_timer_handler, (unsigned long)n); neigh_timer_handler(): 大概看了看,都是定時器到期後處理各種狀态遷移,找到我們分析的狀态 if (neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) {
struct sk_buff *skb = skb_peek(&neigh->arp_queue);
if (skb)
skb = skb_copy(skb, GFP_ATOMIC);
write_unlock(&neigh->lock);
neigh->ops->solicit(neigh, skb); 在 arp_constructor()中被置上
atomic_inc(&neigh->probes);
kfree_skb(skb); static const struct neigh_ops arp_hh_ops = {
.family = AF_INET,
.solicit = arp_solicit,
.error_report = arp_error_report,
.output = neigh_resolve_output,
.connected_output = neigh_resolve_output,
.hh_output = dev_queue_xmit,
.queue_xmit = dev_queue_xmit,
}; ->arp_solicit() ->arp_send(ARPOP_REQUEST, ETH_P_ARP, target, dev, saddr,dst_ha, dev->dev_addr, NULL); 通過arp_send發送ARP REQUEST封包 發送看累了,看看arp的接收吧 static int arp_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
struct arphdr *arp;
if (!pskb_may_pull(skb, arp_hdr_len(dev)))
goto freeskb;
arp = arp_hdr(skb);
if (arp->ar_hln != dev->addr_len ||
dev->flags & IFF_NOARP ||
skb->pkt_type == PACKET_OTHERHOST ||
skb->pkt_type == PACKET_LOOPBACK ||
arp->ar_pln != 4)
goto freeskb;
if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL)
goto out_of_mem; 一系列的合法性檢查
memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb)); 這裡把skb的cb區域用作neighbour_cb結構空間
return NF_HOOK(NFPROTO_ARP, NF_ARP_IN, skb, dev, NULL, arp_process);
freeskb:
kfree_skb(skb);
out_of_mem:
return 0;
} struct arphdr arp頭結構
{
__be16 ar_hrd; 硬體位址格式
__be16 ar_pro; 協定位址格式
unsigned char ar_hln; 硬體位址長度
unsigned char ar_pln; 協定位址長度
__be16 ar_op; 指令代碼
#if 0
unsigned char ar_sha[ETH_ALEN];
unsigned char ar_sip[4];
unsigned char ar_tha[ETH_ALEN];
unsigned char ar_tip[4];
#endif
}; 進入arp_process() ->arp = arp_hdr(skb); 獲得ARP頭
->if (arp->ar_op != htons(ARPOP_REPLY) &&arp->ar_op != htons(ARPOP_REQUEST)) goto out; 不是reply 也不是 request 那是啥呢? -> arp_ptr= (unsigned char *)(arp+1);
sha = arp_ptr; 發送端以太網首址
arp_ptr += dev->addr_len;
memcpy(&sip, arp_ptr, 4); 發送端ip位址
arp_ptr += 4;
arp_ptr += dev->addr_len;
memcpy(&tip, arp_ptr, 4); 目的IP位址
-> if (ipv4_is_loopback(tip) || ipv4_is_multicast(tip)) 環回或多點傳播 goto out; -> if (arp->ar_op == htons(ARPOP_REQUEST) && 收到一個請求包
ip_route_input(skb, tip, sip, 0, dev) == 0) {
rt = skb_rtable(skb);
addr_type = rt->rt_type;
if (addr_type == RTN_LOCAL) { 若是送往本機的arp請求包
int dont_send = 0;
if (!dont_send)
dont_send |= arp_ignore(in_dev,sip,tip);
if (!dont_send && IN_DEV_ARPFILTER(in_dev))
dont_send |= arp_filter(sip,tip,dev);
if (!dont_send) {
n = n eigh_event_ns(&arp_tbl, sha, &sip, dev); 更新arp鄰居表
if (n) {
arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha); 發送arp應答
neigh_release(n);
}
}
goto out;
} else if (IN_DEV_FORWARD(in_dev)) { 代理arp
if (addr_type == RTN_UNICAST && rt->u.dst.dev != dev &&
(arp_fwd_proxy(in_dev, rt) || pneigh_lookup(&arp_tbl, net, &tip, dev, 0))) {
n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
if (n)
neigh_release(n);
if (NEIGH_CB(skb)->flags & LOCALLY_ENQUEUED ||
skb->pkt_type == PACKET_HOST ||
in_dev->arp_parms->proxy_delay == 0) {
arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha);
} else {
pneigh_enqueue(&arp_tbl, in_dev->arp_parms, skb);
in_dev_put(in_dev);
return 0;
}
goto out;
}
}
} n = __neigh_lookup(&arp_tbl, &sip, dev, 0); 對arp應答包的處理 用源ip去鄰居表裡查是否有鄰居項
if (IPV4_DEVCONF_ALL(dev_net(dev), ARP_ACCEPT)) {
if (n == NULL &&
arp->ar_op == htons(ARPOP_REPLY) &&
inet_addr_type(net, sip) == RTN_UNICAST)
n = __neigh_lookup(&arp_tbl, &sip, dev, 1);
}
if (n) {
int state = NUD_REACHABLE; 如果存在
int override;
override = time_after(jiffies, n->updated + n->parms->locktime);
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); 更新鄰居項狀态
neigh_release(n);
} 涉及到狀态遷移的東西挺複雜的,我們就不深入分析了,這裡隻是輸出下大體流程和主要資料結構的組織,關于arp表項的狀态遷移 轉網上達人的分析共同學習:
在上圖中,我們看到隻有arp緩存項的reachable狀态對于外發包是可用的,對于stale狀态的arp緩存項而言,它實際上是不可用的。如果此時有人要發包,那麼需要進行重新解析,對于正常的了解,重新解析意味着要重新發送arp請求,然後事實上卻不一定這樣,因為Linux為arp增加了一個“事件點”來“不用發送arp請求”而對arp協定生成的緩存維護的優化措施,事實上,這種措施十分有效。這就是arp的“确認”機制,也就是說,如果說從一個鄰居主動發來一個資料包到本機,那麼就可以确認該包的“上一跳”這個鄰居是有效的,然而為何隻有到達本機的包才能确認“上一跳”這個鄰居的有效性呢?因為Linux并不想為IP 層的處理增加負擔,也即不想改變IP 層的原始語義。
Linux維護一個stale狀态其實就是為了保留一個neighbour結構體,在其狀态改變時隻是個别字段得到修改或者填充。如果按照簡單的實作,隻儲存一個reachable狀态即可,其到期則删除arp緩存表項。Linux的做法隻是做了很多的優化,但是如果你為這些優化而絞盡腦汁,那就悲劇了...
linux中是如何維護這個state狀态的? 在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請求得到了回應。否則還是會被删除。