内核版本:2.6.34 这部分的重点是三个核心的数据结构-邻居表、邻居缓存、代理邻居表,以及NUD状态转移图。
总的来说,要成功添加一条邻居表项,需要满足两个条件:1. 本机使用该表项;2. 对方主机进行了确认。同时,表项的添加引入了NUD(Neighbour Unreachability Detection)机制,从创建NUD_NONE到可用NUD_REACHABLE需要经历一系列状态转移,而根据达到两个条件顺序的不同,可以分为两条路线: 先引用再确认- NUD_NONE -> NUD_INCOMPLETE -> NUD_REACHABLE 先确认再引用- NUD_NONE -> NUD_STALE -> NUD_DELAY -> NUD_PROBE -> NUD_REACHABLE
下面还是从接收函数入手,当匹配号协议号是0x0806,会调用ARP模块的接收函数arp_rcv()。 arp_rcv() ARP接收函数 首先是对arp协议头进行检查,比如大小是否足够,头部各数值是否正确等,这里略过代码,直接向下看。每个协议处理都一样,如果被多个协议占有,则拷贝一份。
if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL)
goto out_of_mem;
NEIGH_CB(skb)实际就是skb->cb,在skb声明为u8 char[48],它用作每个协议模块的私有数据区(control buffer),每个协议模块可以根据自身需求在其中存储私有数据。而arp模块就利用了它存储控制结构neighbour_cb,它声明如下,占8字节。这个控制结构在代理ARP中使用工作队列时会发挥作用,sched_next代表下次被调度的时间,flags是标志。
memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb));
struct neighbour_cb {
unsigned long sched_next;
unsigned int flags;
};
函数最后调用arp_process,其间插入netfilter(关于netfilter,参见前篇:),作为开始处理ARP报文的起点。
return NF_HOOK(NFPROTO_ARP, NF_ARP_IN, skb, dev, NULL, arp_process);
arp_process() 这个函数开始对报文进行处理,首先会从skb中取出arp报头部分的信息,如sha, sip, tha, tip等,这部分可查阅代码,这里略过。ARP不会查询环路地址和组播地址,因为它们没有对应的mac地址,因此遇到这两类地址,直接退出。
if (ipv4_is_loopback(tip) || ipv4_is_multicast(tip))
goto out;
如果收到的是重复地址检测报文,并且本机占用了检测了地址,则调用arp_send发送响应。对于重复地址检测报文(ARP报文中源IP为全0),它所带有的邻居表项信息还没通过检测,此时缓存它显然没有意义,也许下一刻就有其它主机声明它非法,因此,重复地址检测报文中的信息不会加入邻居表中。
if (sip == 0) {
if (arp->ar_op == htons(ARPOP_REQUEST) &&
inet_addr_type(net, tip) == RTN_LOCAL &&
!arp_ignore(in_dev, sip, tip))
arp_send(ARPOP_REPLY, ETH_P_ARP, sip, dev, tip, sha, dev->dev_addr, sha);
goto out;
}
下面要处理的地址解析报文,并且要解析的地址在路由表中存在
if (arp->ar_op == htons(ARPOP_REQUEST) &&
ip_route_input(skb, tip, sip, 0, dev) == 0)
第一种情况,如果要解析的是本机地址,则调用neigh_event_ns(),并根据查到的邻居表项n发送ARP响应报文。这里neigh_event_ns的功能是在arp_tbl中查找是否已含有对方主机的地址信息,如果没有,则进行创建,然后会调用neigh_update来更新状态。收到对方主机的请求报文,会导致状态迁移到NUD_STALE。
if (addr_type == RTN_LOCAL) {
……
if (!dont_send) {
n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
if (n) {
arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha);
neigh_release(n);
}
}
goto out;
}
#NUD_INCOMPLETE也迁移到NUD_STALE,作何解释? 第二种情况,如果要解析的不是本机地址,则要判断是否支持转发,是否支持代理ARP(代理ARP是陆由器的功能,因此能转发是先决条件),如果满足条件,那么按照代理ARP流程处理。首先无论如何,主机得通了存在这样一个邻居,因此要在在arp_tbl中查找并(如果不存在)创建相应邻居表项;然后,对于代理ARP,这个流程实际上会执行两遍,第一遍走else部分,第二遍走if部分。第一次的else代码段会触发定时器,通过定时器引发报文重新执行arp_process函数,并走if部分。 -第一遍的else部分:调用pneigh_enqueue()将报文skb加入tbl->proxy_queue队列,同时设置NEIGH_CB(skb)的值,具体可看后见的代理表项处理。 -第二遍的if部分,发送ARP响应报文,行使代理ARP的功能。
else if (IN_DEV_FORWARD(in_dev)) {
if (addr_type == RTN_UNICAST &&
(arp_fwd_proxy(in_dev, dev, rt) ||
arp_fwd_pvlan(in_dev, dev, rt, sip, tip) ||
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;
}
}
补充:neigh_event_ns()与neigh_release()配套使用并不代表创建后又被释放,neigh被释放的条件是neigh->refcnt==0,但neigh创建时的refcnt=1,而neigh_event_ns会使refcnt+1,neigh_release会使-1,此时refcnt的值还是1,只有当下次单独调用neigh_release时才会被释放。 查找是否已存在这样一个邻居表项。如果ARP报文是发往本机的响应报文,那么neigh会更新为NUD_REACHABLE状态;否则,维持原状态不变。#个人认为,这段代码是处理NUD_INCOMPLETE/NUD_PROBE/NUD_DELAY向NUD_REACHABLE迁移的,但如果一台主机A发送一个对本机的ARP响应报文,那么会导致neigh从NUD_NONE直接迁移到NUD_REACHABLE,当然,按照正常流程,一个ARP响应报文肯定是由于本机发送了ARP请求报文,那样neigh已经处于NUD_INCOMPLETE状态了。
n = __neigh_lookup(&arp_tbl, &sip, dev, 0);
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_process是接收到ARP报文的处理函数,它涉及到的是邻居表项在收到arp请求和响应的情况,下图反映了arp_process中所涉及的状态转移:收到arp请求,NUD_NONE -> NUD_STALE;收到arp响应,NUD_INCOMPLETE/NUD_DELAY/NUD_PROBE -> NUD_REACHABLE。根据之前分析,我认为还存在NUD_NONE -> NUD_REACHABLE和NUD_INCOMPLETE -> NUD_STALE的转移,作何解释?
NUD状态 每个邻居表项在生效前都要经历一系列的状态迁移,每个状态都有不同的含义,在前面已经多次提到了NUD状态。要添加一条有效的邻居表项,有效途径有两条: 先引用再确认- NUD_NONE -> NUD_INCOMPLETE -> NUD_REACHABLE 先确认再引用- NUD_NONE -> NUD_STALE -> NUD_DELAY -> NUD_PROBE -> NUD_REACHABLE 其中neigh_timer_handler定时器、neigh_periodic_work工作队列会异步的更改NUD状态,neigh_timer_handler用于NUD_INCOMPLETE, NUD_DELAY, NUD_PROBE, NUD_REACHABLE状态;neigh_periodic_work用于NUD_STALE。注意neigh_timer_handler是每个表项一个的,而neigh_periodic_work是唯一的,NUD_STALE状态的表项没必要单独使用定时器,定期检查过期就可以了,这样大大节省了资源。 neigh_update则专门用于更新表项状态,neigh_send_event则是解析表项时的状态更新,能更新表项的函数很多,这里不一一列出。
neigh_timer_handler 定时器函数 当neigh处于NUD_INCOMPLETE, NUD_DELAY, NUD_PEOBE, NUD_REACHABLE时会添加定时器,即neigh_timer_handler,它处理各个状态在定时器到期时的情况。 当neigh处于NUD_REACHABLE状态时,根据NUD的状态转移图,它有三种转移可能,分别对应下面三个条件语句。neigh->confirmed代表最近收到来自对应邻居项的报文时间,neigh->used代表最近使用该邻居项的时间。 -如果超时,但期间收到对方的报文,不更改状态,并重置超时时间为neigh->confirmed+reachable_time; -如果超时,期间未收到对方报文,但主机使用过该项,则迁移至NUD_DELAY状态,并重置超时时间为neigh->used+delay_probe_time; -如果超时,且既未收到对方报文,也未使用过该项,则怀疑该项可能不可用了,迁移至NUD_STALE状态,而不是立即删除,neigh_periodic_work()会定时的清除NUD_STALE状态的表项。
if (state & NUD_REACHABLE) {
if (time_before_eq(now,
neigh->confirmed + neigh->parms->reachable_time)) {
NEIGH_PRINTK2("neigh %p is still alive.\n", neigh);
next = neigh->confirmed + neigh->parms->reachable_time;
} else if (time_before_eq(now,
neigh->used + neigh->parms->delay_probe_time)) {
NEIGH_PRINTK2("neigh %p is delayed.\n", neigh);
neigh->nud_state = NUD_DELAY;
neigh->updated = jiffies;
neigh_suspect(neigh);
next = now + neigh->parms->delay_probe_time;
} else {
NEIGH_PRINTK2("neigh %p is suspected.\n", neigh);
neigh->nud_state = NUD_STALE;
neigh->updated = jiffies;
neigh_suspect(neigh);
notify = 1;
}
}
下图是对上面表项处于NUD_REACHABLE状态时,定时器到期后3种情形的示意图:
当neigh处于NUD_DELAY状态时,根据NUD的状态转移图,它有二种转移可能,分别对应下面二个条件语句。 -如果超时,期间收到对方报文,迁移至NUD_REACHABLE,记录下次检查时间到next; -如果超时,期间未收到对方的报文,迁移至NUD_PROBE,记录下次检查时间到next。 在NUD_STALE->NUD_PROBE中间还插入NUD_DELAY状态,是为了减少ARP包的数目,期望在定时时间内会收到对方的确认报文,而不必再进行地址解析。
else if (state & NUD_DELAY) {
if (time_before_eq(now,
neigh->confirmed + neigh->parms->delay_probe_time)) {
NEIGH_PRINTK2("neigh %p is now reachable.\n", neigh);
neigh->nud_state = NUD_REACHABLE;
neigh->updated = jiffies;
neigh_connect(neigh);
notify = 1;
next = neigh->confirmed + neigh->parms->reachable_time;
} else {
NEIGH_PRINTK2("neigh %p is probed.\n", neigh);
neigh->nud_state = NUD_PROBE;
neigh->updated = jiffies;
atomic_set(&neigh->probes, 0);
next = now + neigh->parms->retrans_time;
}
}
当neigh处于NUD_PROBE或NUD_INCOMPLETE状态时,记录下次检查时间到next,因为这两种状态需要发送ARP解析报文,它们过程的迁移依赖于ARP解析的进程。
else {
/* NUD_PROBE|NUD_INCOMPLETE */
next = now + neigh->parms->retrans_time;
}
经过定时器超时后的状态转移,如果neigh处于NUD_PROBE或NUD_INCOMPLETE,则会发送ARP报文,先会检查报文发送的次数,如果超过了限度,表明对方主机没有回应,则neigh进入NUD_FAILED,被释放掉。
if ((neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) &&
atomic_read(&neigh->probes) >= neigh_max_probes(neigh)) {
neigh->nud_state = NUD_FAILED;
notify = 1;
neigh_invalidate(neigh);
}
检查完后,如果还未超过限度,则会发送ARP报文,neigh->ops->solicit在创建表项neigh时被赋值,一般是arp_solicit,并且增加探测计算neigh->probes。
if (neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) {
struct sk_buff *skb = skb_peek(&neigh->arp_queue);
/* keep skb alive even if arp_queue overflows */
if (skb)
skb = skb_copy(skb, GFP_ATOMIC);
write_unlock(&neigh->lock);
neigh->ops->solicit(neigh, skb);
atomic_inc(&neigh->probes);
kfree_skb(skb);
}
实际上,neigh_timer_handler处理启用了定时器状态超时的情况,下图反映了neigh_timer_handler中所涉及的状态转移,值得注意的是NUD_DELAY -> NUD_REACHABLE的状态转移,在arp_process中也提到过,收到arp reply时会有表项状态NUD_DELAY -> NUD_REACHABLE。它们两者的区别在于arp_process处理的是arp的确认报文,而neigh_timer_handler处理的是4层的确认报文。
neigh_periodic_work NUD_STALE状态的定时函数 当neigh处于NUD_STALE状态时,此时它等待一段时间,主机引用到它,从而转入NUD_DELAY状态;没有引用,则转入NUD_FAIL,被释放。不同于NUD_INCOMPLETE、NUD_DELAY、NUD_PROBE、NUD_REACHABLE状态时的定时器,这里使用的异步机制,通过定期触发neigh_periodic_work()来检查NUD_STALE状态。
tbl->parms.base_reachable_time = 30 HZ
当初始化邻居表时,添加了neigh_periodic_work工作 neigh_table_init() -> neigh_table_init_no_netlink():
INIT_DELAYED_WORK_DEFERRABLE(&tbl->gc_work, neigh_periodic_work);
当neigh_periodic_work执行时,首先计算到达时间(reachable_time),其中要注意的是
p->reachable_time = neigh_rand_reach_time(p->base_reachable_time);
unsigned long neigh_rand_reach_time(unsigned long base)
{
return (base ? (net_random() % base) + (base >> 1) : 0);
}
因此,reachable_time实际取值是1/2 base ~ 2/3 base,而base = base_reachable_time,当表项处于NUD_REACHABLE状态时,会启动一个定时器,时长为reachable_time,即一个表项在不被使用时存活时间是1/2 base_reachable_time ~ 2/3 base_reachable_time。 然后它会遍历整个邻居表,每个hash_buckets的每个表项,如果在gc_staletime内仍未被引用过,则会从邻居表中清除。
for (i = 0 ; i <= tbl->hash_mask; i++) {
np = &tbl->hash_buckets[i];
while ((n = *np) != NULL) {
…..
if (atomic_read(&n->refcnt) == 1 &&
(state == NUD_FAILED ||
time_after(jiffies, n->used + n->parms->gc_staletime))) {
*np = n->next;
n->dead = 1;
write_unlock(&n->lock);
neigh_cleanup_and_release(n);
continue;
}
……
}
在工作最后,再次添加该工作到队列中,并延时1/2 base_reachable_time开始执行,这样,完成了neigh_periodic_work工作每隔1/2 base_reachable_time执行一次。 schedule_delayed_work(&tbl->gc_work, tbl->parms.base_reachable_time >> 1); neigh_periodic_work定期执行,但要保证表项不会刚添加就被neigh_periodic_work清理掉,这里的策略是:gc_staletime大于1/2 base_reachable_time。默认的,gc_staletime = 30,base_reachable_time = 30。也就是说,neigh_periodic_work会每15HZ执行一次,但表项在NUD_STALE的存活时间是30HZ,这样,保证了每项在最差情况下也有(30 - 15)HZ的生命周期。
neigh_update 邻居表项状态更新 如果新状态是非有效(!NUD_VALID),那么要做的就是删除该表项:停止定时器neigh_del_timer,设置neigh状态nud_state为新状态new。除此之外,当是NUD_INCOMPLETE或NUD_PROBE状态时,可能有暂时因为地址没有解析而暂存在neigh->arp_queue中的报文,而现在表项更新到NUD_FAILED,即解析无法成功,那么这么暂存的报文也只能被丢弃neigh_invalidate。
if (!(new & NUD_VALID)) {
neigh_del_timer(neigh);
if (old & NUD_CONNECTED)
neigh_suspect(neigh);
neigh->nud_state = new;
err = 0;
notify = old & NUD_VALID;
if ((old & (NUD_INCOMPLETE | NUD_PROBE)) &&
(new & NUD_FAILED)) {
neigh_invalidate(neigh);
notify = 1;
}
goto out;
}
中间这段代码是对比表项的地址是否发生了变化,略过。#个人认为NUD_REACHABLE状态时,新状态为NUD_STALE是在下面这段代码里面除去了,因为NUD_REACHABLE状态更好,不应该回退到NUD_STALE状态。但是当是NUD_DELAY, NUD_PROBE, NUD_INCOMPLETE时仍会被更新到NUD_STALE状态,对此很不解???
else {
if (lladdr == neigh->ha && new == NUD_STALE &&
((flags & NEIGH_UPDATE_F_WEAK_OVERRIDE) ||
(old & NUD_CONNECTED)))
new = old;
}
新旧状态不同时,首先删除定时器,如果新状态需要定时器,则重新设置定时器,最后设置表项neigh为新状态new。
if (new != old) {
neigh_del_timer(neigh);
if (new & NUD_IN_TIMER)
neigh_add_timer(neigh, (jiffies +
((new & NUD_REACHABLE) ?
neigh->parms->reachable_time :
0)));
neigh->nud_state = new;
}
如果邻居表项中的地址发生了更新,有了新的地址值lladdr,那么更新表项地址neigh->ha,并更新与此表项相关的所有缓存表项neigh_update_hhs。
if (lladdr != neigh->ha) {
memcpy(&neigh->ha, lladdr, dev->addr_len);
neigh_update_hhs(neigh);
if (!(new & NUD_CONNECTED))
neigh->confirmed = jiffies -
(neigh->parms->base_reachable_time << 1);
notify = 1;
}
如果表项状态从非有效(!NUD_VALID)迁移到有效(NUD_VALID),且此表项上的arp_queue上有项,表明之前有报文因为地址无法解析在暂存在了arp_queue上。此时表项地址解析完成,变为有效状态,从arp_queue中取出所有待发送的报文skb,发送出去n1->output(skb),并清空表项的arp_queue。
if (!(old & NUD_VALID)) {
struct sk_buff *skb;
while (neigh->nud_state & NUD_VALID &&
(skb = __skb_dequeue(&neigh->arp_queue)) != NULL) {
struct neighbour *n1 = neigh;
write_unlock_bh(&neigh->lock);
/* On shaper/eql skb->dst->neighbour != neigh :( */
if (skb_dst(skb) && skb_dst(skb)->neighbour)
n1 = skb_dst(skb)->neighbour;
n1->output(skb);
write_lock_bh(&neigh->lock);
}
skb_queue_purge(&neigh->arp_queue);
}
neigh_event_send 当主机需要解析地址,会调用neigh_resolve_output,主机引用表项明显会涉及到表项的NUD状态迁移,NUD_NONE->NUD_INCOMPLETE,NUD_STALE->NUD_DELAY。 neigh_event_send -> __neigh_event_send 只处理nud_state在NUD_NONE, NUD_STALE, NUD_INCOMPLETE状态时的情况:
if (neigh->nud_state & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE))
goto out_unlock_bh;
不处于NUD_STALE和NUD_INCOMPLETE状态,则只能是NUD_NONE。此时主机要用到该邻居表项(注意是通过neigh_resolve_output进入的),但还没有,因此要通过ARP进行解析,并且此时没有收到对方发来的任何报文,要进行的ARP是广播形式。 在发送ARP报文时有3个参数- ucast_probes, mcast_probes, app_probes,分别代表单播次数,广播次数,app_probes比较特殊,一般情况下为0,当使用了arpd守护进程时才会设置它的值。如果已经收到过对方的报文,即知道了对方的MAC-IP,ARP解析会使用单播形式,次数由ucast_probes决定;如果未收到过对方报文,此时ARP解析只能使用广播形式,次数由mcasat_probes决定。 当mcast_probes有值时,neigh进入NUD_INCOMPLETE状态,设置定时器,注意此时neigh_probes(表示已经进行探测的次数)初始化为ucast_probes,目的是只进行mcast_probes次广播;当mcast_probes值为0时(表明当前配置不允许解析),neigh进入NUD_FAILED状态,被清除。
if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) {
if (neigh->parms->mcast_probes + neigh->parms->app_probes) {
atomic_set(&neigh->probes, neigh->parms->ucast_probes);
neigh->nud_state = 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;
}
}
当neigh处于NUD_STALE状态时,根据NUD的状态转移图,主机引用到了该邻居表项,neigh转移至NUD_DELAY状态,设置定时器。
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);
}
当neigh处于NUD_INCOMPLETE状态时,需要发送ARP报文进行地址解析,__skb_queue_tail(&neigh->arp_queue, skb)的作用就是先把要发送的报文缓存起来,放到neigh->arp_queue链表中,当完成地址解析,再从neigh->arp_queue取出报文,并发送出去。
if (neigh->nud_state == NUD_INCOMPLETE) {
if (skb) {
if (skb_queue_len(&neigh->arp_queue) >= neigh->parms->queue_len) {
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;
}
邻居表的操作neigh_create 创建邻居表项 首先为新的邻居表项struct neighbour分配空间,并做一些初始化。传入的参数tbl就是全局量arp_tbl,分配空间的大小是tbl->entry_size,而这个值在声明arp_tbl时初始化为sizeof(struct neighbour) + 4,多出的4个字节就是key值存放的地方。
n = neigh_alloc(tbl);
拷贝key(即IP地址)到primary_key,而primary_key就是紧接neighbour的4个字节,看下struct neighbor的声明 - u8 primary_key[0];设置n->dev指向接收到报文的网卡设备dev。
memcpy(n->primary_key, pkey, key_len);
n->dev = dev;
哈希表是牺牲空间换时间,保证均匀度很重要,一旦某个表项的值过多,链表查找会降低性能。因此当表项数目entries大于初始分配大小hash_mask+1时,执行neigh_hash_grow将哈希表空间倍增,这也是内核使用哈希表时常用的方法,可变大小的哈希表。
if (atomic_read(&tbl->entries) > (tbl->hash_mask + 1))
neigh_hash_grow(tbl, (tbl->hash_mask + 1) << 1);
通过pkey和dev计算哈希值,决定插入tbl->hash_buckets的表项。
hash_val = tbl->hash(pkey, dev) & tbl->hash_mask;
搜索tbl->hash_buckets[hash_val]项,如果创建的新ARP表项已存在,则退出;否则将其n插入该项的链表头。
for (n1 = tbl->hash_buckets[hash_val]; n1; n1 = n1->next) {
if (dev == n1->dev && !memcmp(n1->primary_key, pkey, key_len)) {
neigh_hold(n1);
rc = n1;
goto out_tbl_unlock;
}
}
n->next = tbl->hash_buckets[hash_val];
tbl->hash_buckets[hash_val] = n;
附一张创建ARP表项并插入到hash_buckets的图:
neigh_lookup 查找ARP表项 查找函数很简单,以IP地址和网卡设备(即pkey和dev)计算哈希值hash_val,然后在tbl->hash_buckets查找相应项。
hash_val = tbl->hash(pkey, dev);
for (n = tbl->hash_buckets[hash_val & tbl->hash_mask]; n; n = n->next) {
if (dev == n->dev && !memcmp(n->primary_key, pkey, key_len)) {
neigh_hold(n);
NEIGH_CACHE_STAT_INC(tbl, hits);
break;
}
}
代理ARP 代理ARP的相关知识查阅google。要明确代理ARP功能是针对陆由器的(或者说是具有转发功能的主机)。开启ARP代理后,会对查询不在本网段的ARP请求包回应。 回到之前的arp_process代码,处理代理ARP的情况,这实际就是进行代理ARP的条件,IN_DEV_FORWARD是支持转发,RTN_UNICAST是与路由直连,arp_fwd_proxy表示设备支持代理行为,arp_fwd_pvlan表示支持代理同设备进出,pneigh_lookup表示目的地址的代理。这两种arp_fwd_proxy和arp_fwd_pvlan都只是网卡设备的一种性质,pneigh_lookup则是一张代理邻居表,它的内容都是手动添加或删除的,三种策略任一一种满足都可以进行代理ARP。
else if (IN_DEV_FORWARD(in_dev)) {
if (addr_type == RTN_UNICAST &&
(arp_fwd_proxy(in_dev, dev, rt) ||
arp_fwd_pvlan(in_dev, dev, rt, sip, tip) ||
pneigh_lookup(&arp_tbl, net, &tip, dev, 0)))
pneigh_lookup 查找或添加代理邻居表项[proxy neighbour] 以[pkey=tip, key_len=4]计算hash值,执行__pneigh_lookup_1在phash_buckets中查找。
u32 hash_val = pneigh_hash(pkey, key_len);
n = __pneigh_lookup_1(tbl->phash_buckets[hash_val], net, pkey, key_len, dev);
如果在phash_buckets中查找到,或者不需要创建新表项,则函数返回,此时它的功能仅仅是lookup。
if (n || !creat)
goto out;
而当传入参数create=1时,则它的功能不仅是lookup,还会在表项不存在时create。同neighbour结构一样,键值pkey存储在pneigh结构的后面,这样当pkey变化时,修改十分容易。创建操作很直观,为pneigh和pkey分配空间,初始化些变量,最后插入phash_buckets。
n = kmalloc(sizeof(*n) + key_len, GFP_KERNEL);
……
write_pnet(&n->net, hold_net(net));
memcpy(n->key, pkey, key_len);
……
n->next = tbl->phash_buckets[hash_val];
tbl->phash_buckets[hash_val] = n;
pneigh_enqueue 将报文加入代理队列 首先计算下次调度的时间,这是一个随机值,记录到sched_next中;设置flags|=LOCALLY_ENQUEUED表明报文是本地加入的。
unsigned long sched_next = now + (net_random() % p->proxy_delay);
……
NEIGH_CB(skb)->sched_next = sched_next;
NEIGH_CB(skb)->flags |= LOCALLY_ENQUEUED;
然后将报文加入proxy_queue,并设置定时器proxy_timer,下次超时时间为刚计算的值sched_next,这样,下次超时时就会处理proxy_queue队列中的报文。
__skb_queue_tail(&tbl->proxy_queue, skb);
mod_timer(&tbl->proxy_timer, sched_next);
这里的tbl当然是arp_tbl,它的proxy_timer是在初始化时设置的arp_init() -> neigh_table_init_no_netlink()中:
setup_timer(&tbl->proxy_timer, neigh_proxy_process, (unsigned long)tbl);
neigh_proxy_process 代理ARP的定时器 skb_queue_walk_safe如同for循环一样,它遍历proxy_queue,一个个取出其中的报文skb,查看报文的调度时间sched_next与当前时间now的差值。 如果tdif<=0则表明调度时间已到或已过,报文要被处理了,从proxy_queue上取出该报文,调用tbl->proxy_redo重新发送报文,tbl->proxy_redo也是在arp初始化时赋值的,实际上就是arp_process()函数。结合上面的分析,它会执行arp_process中代理ARP处理的else部分,发送响应报文。 如果tdif>0则表明调度时间还未到,else if部分的功能就是记录下最近要过期的调度时间到sched_next。
skb_queue_walk_safe(&tbl->proxy_queue, skb, n) {
long tdif = NEIGH_CB(skb)->sched_next - now;
if (tdif <= 0) {
struct net_device *dev = skb->dev;
__skb_unlink(skb, &tbl->proxy_queue);
if (tbl->proxy_redo && netif_running(dev))
tbl->proxy_redo(skb);
else
kfree_skb(skb);
dev_put(dev);
} else if (!sched_next || tdif < sched_next)
sched_next = tdif;
}
重新设置proxy_timer的定时器,下次超时时间为刚刚记录下的最近要调度的时间sched_next + 当前时间jiffies。
del_timer(&tbl->proxy_timer);
if (sched_next)
mod_timer(&tbl->proxy_timer, jiffies + sched_next);
以一张简单的图来说明ARP代理的处理过程,过程一是入队列等待,过程二是出队列发送。不立即处理ARP代理请求报文的原因是为了性能,收到报文后会启动定时器,超时时间是一个随机变量,保证了在大量主机同时进行此类请求时不会形成太大的负担。
邻居表缓存 邻居表缓存中存储的就是二层报头,如果缓存的报头正好被用到,那么直接从邻居表缓存中取出报文就行了,而不用再额外的构造报头,加快了协议栈的响应速度。 neigh_hh_init 创建新的邻居表缓存 当发送报文时,如果还没有对方主机MAC地址,则调用neigh_resove_output进行地址解析,此时会判断dst->hh为NULL时,就会调用neigh_hh_init创建邻居表缓存,加速下次的报文发送。 首先在邻居表项所链的所有邻居表缓存项n->hh匹配协议号protocol,找到,则说明已有缓存,不必再创建,neigh_hh_init会直接返回;未找到,则会创建新的缓存项hh。
for (hh = n->hh; hh; hh = hh->hh_next)
if (hh->hh_type == protocol)
break;
下面代码段创建了新的缓存项hh,并初始化了hh的内容,其中dev->header_ops->cache会赋值hh->hh_data,即[SRCMAC, DSTMAC, TYPE]。如果赋值失败,释放掉刚才分配的hh;如果赋值成功,将hh链入n->hh的链表,并根据NUD状态赋值hh->hh_output。
if (!hh && (hh = kzalloc(sizeof(*hh), GFP_ATOMIC)) != NULL) {
seqlock_init(&hh->hh_lock);
hh->hh_type = protocol;
atomic_set(&hh->hh_refcnt, 0);
hh->hh_next = NULL;
if (dev->header_ops->cache(n, hh)) {
kfree(hh);
hh = NULL;
} else {
atomic_inc(&hh->hh_refcnt);
hh->hh_next = n->hh;
n->hh = hh;
if (n->nud_state & NUD_CONNECTED)
hh->hh_output = n->ops->hh_output;
else
hh->hh_output = n->ops->output;
}
}
最后,创建成功的hh,陆由缓存dst->hh指向新创建的hh。
if (hh) {
atomic_inc(&hh->hh_refcnt);
dst->hh = hh;
}
从hh的创建过程可以看出,通过邻居表项neighbour的缓存hh可以遍历所有的与neighbour相关的缓存(即目的MAC相同,但协议不同);通过dst的缓存hh只能指向相关的一个缓存(尽管dst->hh->hh_next也许有值,但只会使用dst->hh)。 这里解释了为什么neighbour和dst都有hh指针指向缓存项,可以这么说,neighbour指向的hh是全部的,dst指向的hh是特定一个。两者的作用:在发送报文时查找完陆由表找到dst后,会直接用dst->hh,得到以太网头;而当远程主机MAC地址变更时,通过dst->neighbour->hh可以遍历所有缓存项,从而全部更改,而用dst->hh得一个个查找,几乎是无法完成的。可以这么说,dst->hh是使用时用的,neigh->hh是管理时用的。
neigh_update_hhs 更新缓存项 更新缓存项更新的实际就是缓存项的MAC地址。比如当收到一个报文,以它源IP为键值在邻居表中查找到的neighbour表项的n->ha与报文源MAC值不同时,说明对方主机的MAC地址发生了变更,此时就需要更新所有以旧MAC生成的hh为新MAC。 邻居表项是以IP为键值查找的,因此通过IP可以查找相关的邻居表项neigh,前面说过neigh->hh可以遍历所有以之相关的缓存项,所以遍历它,并调用update函数。以以太网卡为例,update = neigh->dev->header_ops->cache_update ==> eth_header_cache_update,而eth_header_cache_update函数就是用新的MAC地址覆盖hh->data中的旧MAC地址。 neigh_update_hhs函数也说明了neighbour->hh指针的作用。
for (hh = neigh->hh; hh; hh = hh->hh_next) {
write_seqlock_bh(&hh->hh_lock);
update(hh, neigh->dev, neigh->ha);
write_sequnlock_bh(&hh->hh_lock);
}
补充:缓存项hh的生命期从创建时起,会一直持续到邻居表项被删除,也就是调用neigh_destroy时,删除neigh->hh指向的所有缓存项。
参考:《Understanding Linux Network Internals》