相关概念
虽然是vxworks2.0.2版本中的,但是与老土的BSD代码基本一样,事实上,最新的ip协议栈的代码上虽然加上不少新鲜的功能,但是其主体也依旧一样.
ifnet
也就是协议栈中的接口的概念,跟arp相关处理的最重要的三个成员是:
- if_ioctl 用于接口上的ioctl命令;
- if_resolve 用于进行地址解析的函数;
- if_output 用于在接口上发送数据包;
在ipAttach时,这三个值都进行了初始化:
pIfp->if_ioctl = (FUNCPTR) ipIoctl;
pIfp->if_output = ipOutput;
pIfp->if_resolve = muxAddrResFuncGet(mib2Tbl.ifType, 0x800);
其中if_resolve的值,实际上就是arpresolve函数.
in_ifaddr
in_ifaddr是ifaddr的一种特殊形式,即ipv4版本的的ifaddr.当我们给接口配置ip地址时,实际上要生成一个in_ifaddr结构体,并与ifnet相关联.那么它与arp最相关的内容实际上是在ifaddr结构体中,它们是:
- ifa_rtrequest 这是一个处理arp相关的函数,在后面我们就会解释到它的用处.
- ia_ifp 与地址相关联的接口.
sockaddr_dl
数据链路层地址,它的作用就是保存MAC地址,其中与ARP处理相关的内容包括:
- sdl_len 长度,如果为0,表示mac信息无效,否则就是有效.这点很重要
- sdl_data 如果有效,保存有mac信息.
llinfo_arp
它就是arp控制结构,整个系统中的llinfo_arp通过一个双向链表连接起来,链表头就是全局变量llinfo_arp.(C语言中,总是喜欢将全局变量定义成结构体的名字).其中现在我们关心的内容包括
- la_rt 指向相关的rtentry,关于rtentry,后面马上就要讲到了.
- la_hold 持有的数据,在arp处理中会使用到,现在只知道它是要通过接口发送的数据包;
- la_asked 计数,用于统计在接收到arp回应前,发出了多少arp请求.
rtentry
路由表项,每一条路由都由一个rtentry表示,与arp相关的内容包括
- rt_ifp 与路由相关联的接口;
- rt_ifa 与路由相关的接口地址;
- rt_genmask 用于clone路由时使用;
- rt_llinfo 指向arp控制结构
- rt_gateway 表示下一跳信息,可能保存mac地址.
- rt_expire arp超时处理使用,如果为0,表示永久有效(用于静态配置的mac).
route
route数据结构主要用于路由处理,它包括两个成员:
- ro_rt 路由引用的rtentry
- ro_dst 目的地址
数据的发送过程
ip_output
ip协议栈发送数据总是以
int
ip_output(m0, opt, ro, flags, imo)
struct mbuf *m0;
struct mbuf *opt;
struct route *ro;
int flags;
struct ip_moptions *imo;
函数调用开发的,对于其中一些特殊情况的处理我们就不会加以描述,我们只对普通情况说明.m0表示要发送的数据,而ro就是发送的路由.ip_output在进行了一大堆的事情之后,就会调用
(*ifp->if_output)(ifp, m,
(struct sockaddr *)dst, ro->ro_rt);
发送数据,其中ifp就是根据路由或者什么的,找到的要outgoing接口.前面我们说过,ipAttach时,就已经指定了if_output为ipOutput函数:
int ipOutput
(
register struct ifnet *ifp,
struct mbuf *m0,
struct sockaddr *dst,
struct rtentry *rt0
)
上面的几个参数比较明显
- ifp为发送数据要使用的接口
- m0是要发送的数据;
- dst是目的地址;
- rt0是使用的路由;
在ipOutput中与arp相关的最重要的一环,就是下面的switch-case语句:
switch (dst->sa_family)
{
case AF_INET:
if (ifp->if_resolve != NULL)
if (!ifp->if_resolve(ac, rt, m, dst, edst))
return (0); /* if not yet resolved */
/* If broadcasting on a simplex interface, loopback a copy */
if ((m->m_flags & M_BCAST) && (ifp->if_flags & IFF_SIMPLEX))
mcopy = m_copy(m, 0, (int)M_COPYALL);
off = m->m_pkthdr.len - m->m_len;
etype = ETHERTYPE_IP;
break;
case AF_UNSPEC:
当dst->sa_family为AF_UNSPEC时,说明dst中保存着对方的MAC地址信息,会导致构造二层帧头,然后放到接口的发送队列中.而接口最终会把它发送出去.但是如果dst是一个ip地址(由AF_INET表示),则情况就不一样了.它首先调用ifp->if_resolve函数.if_resolve函数的作用就是进行ip地址到MAC地址的映射,如果if_resolve能够直接返回对应的mac地址,则调用返回之后,edst就保存着目的mac,随后就可以构造二层报头,入发送队列(跟AF_UNSPEC时的情况一样),否则就直接返回.
由于ipAttach时将if_resolve初始成arpresolve,所以我们还是看看arpresolve:
int
arpresolve(ac, rt, m, dst, desten)
register struct arpcom *ac;
register struct rtentry *rt;
struct mbuf *m;
register struct sockaddr *dst;
register u_char *desten;
前面说过,arpresolve的功能就是进行目的ip地址到mac地址的转换映射;目的地址由参数dst指定,如果arpresolve能够完成映射,则目的mac填写在desten中.这是由下面的代码段完成的
sdl = SDL(rt->rt_gateway);
if ((rt->rt_expire == 0 || rt->rt_expire > tickGet()) &&
sdl->sdl_family == AF_LINK && sdl->sdl_alen != 0) {
bcopy(LLADDR(sdl), (char *)desten, sdl->sdl_alen);
return 1;
}
上面的意思是说,如果当前的arp信息有效,则直接返回mac信息.这个函数中,还处理一些NOARP的处理(对于NOARP的,自己搞定一个mac出来).
if (la->la_hold)
m_freem(la->la_hold);
la->la_hold = m;
上面的这段代码说,将要发送的数据保存在la_hold中,从上面可以看出,arp只保存最后一次请求时的数据.同时也说明了,如果arp无效,则要发送的数据是保存在arp控制信息中的,在后面的分析中,我们可以看到,在处理arp回应时,这个被保存的数据,会被协议栈发送.再接着看代码:
if (rt->rt_expire) {
rt->rt_flags &= ~RTF_REJECT;
if (la->la_asked == 0 ||
(tickGet () - rt->rt_expire >= arpRxmitTicks)) {
rt->rt_expire = tickGet();
if (la->la_asked++ < arp_maxtries)
arpwhohas(ac, &(SIN(dst)->sin_addr));
else {
rt->rt_flags |= RTF_REJECT;
rt->rt_expire += (sysClkRateGet() * arpt_down);
la->la_asked = 0;
}
}
}
上面代码有三个主要功能:
- 清除RTF_REJECT标志,RTF_REJECT标志用于控制发送arp信息的频度,后面会有专门的讲解.
- 如果当前发送的arp请求次数小于arp_maxtries(5次),则调用arpshowhas发送arp请求;
- 否则,设置RTF_REJECT,以抑制ARP请求的发送,并将抑制时间定为20秒;
arpwhohas实际调用arprequest,以请求ARP信息:
static void
arprequest(ac, sip, tip, enaddr)
register struct arpcom *ac;
register u_long *sip, *tip;
register u_char *enaddr;
arpwhohas首先构造一个报文,然后调用接口的发送函数:
bcopy((caddr_t)etherbroadcastaddr, (caddr_t)eh->ether_dhost,
sizeof(eh->ether_dhost));
sa.sa_family = AF_UNSPEC;
sa.sa_len = sizeof(sa);
(*ac->ac_if.if_output)(&ac->ac_if, m, &sa, (struct rtentry *)0);
我们可以看到目的mac在这写成了广播地址(全1),而sa.sa_family设置为AF_UNSPEC,这与我们前面描述的ipOutput是一致的(由前面的描述我们知道,这里调用的函数if_output实际上就是ipOutput).我们也知道,这次调用由于给的目的地址参数sa_family为AF_UNSPEC,所以会导致直接把要发送的数据放到接口发送队列,而不会再调用if_resolve造成死循环.
arp的输入处理
现在,该看看另外一个分支了,arp输入处理,arp的输入由arpintr驱动,它调用in_arpinput以处理跟ip相碰的arp报文:
la = arplookup(isaddr.s_addr, itaddr.s_addr == myaddr.s_addr, 0);
if (la && (rt = la->la_rt) && (sdl = SDL(rt->rt_gateway))) {
if (sdl->sdl_alen &&
bcmp((caddr_t)ea->arp_sha, LLADDR(sdl), sdl->sdl_alen))
logMsg("arp info overwritten for %08x by %s\n",
(int) ntohl(isaddr.s_addr),
(int) ether_sprintf(ea->arp_sha),0,0,0,0);
bcopy((caddr_t)ea->arp_sha, LLADDR(sdl),
sdl->sdl_alen = sizeof(ea->arp_sha));
if (rt->rt_expire)
rt->rt_expire = tickGet() + (sysClkRateGet() *
arpt_keep);
rt->rt_flags &= ~RTF_REJECT;
la->la_asked = 0;
if (la->la_hold) {
(*ac->ac_if.if_output)(&ac->ac_if, la->la_hold,
rt_key(rt), rt);
la->la_hold = 0;
}
}
首先,in_arpinput会调用arplookup,以检查发送者是不是在我们当前的arp缓存中(如果我们在前面发送了arp请求,那么就应该存在一个无效的arp项,如果是对方主请求,那么说明对方想与我们通讯,也需要检查对方在不在的).arplookup传入的第二个参数,表示在不存在arp项的时候,是不是要建立.那么什么时候建立呢?由于arp请求是广播发送的,所以我们可能接收到请求的目的ip不是我们自己的ip地址,在这种情况下,只需要更新arp信息(时标,以防止老化),只有请求是针对我自己的情况下(因为对方可能要与我通讯了,我马上就要使用对方的mac地址了),才会创建新的arp项.
如果arplookup查找出来一个有效的arp项,说明arp要被覆盖了,这也是我们看到
"arp info overwritten for 0xXXXXXXXX by xxxx \n"
这条信息的原因.
arp下面的处理,就是复制目的mac,然后
- 设置rt_expire,将老化时间设置为20分钟.
- 设置la_asked为0;
- 调用if_output发送la_hold.这与我们前面说过的,arp输入处理时,发送保存的数据是一致的.这一次由于系统中arp项的存在,会导致ipPutput调用arpresolve时返回1(有效mac),从而能够正常发送数据.
arp的老化
arp的老化由arptimer完成,arptimer每分钟运行一次,它处理全局链表llinfo_arp,将老化的arp设置为无效.
if (rt->rt_expire && rt->rt_expire <= tickGet())
{
arptfree(la->la_prev); /* timer has expired; clear */
}
我们看到老化操作是在arptfree完成的.
if (rt->rt_refcnt > 0 && (sdl = SDL(rt->rt_gateway)) &&
sdl->sdl_family == AF_LINK) {
sdl->sdl_alen = 0;
la->la_asked = 0;
rt->rt_flags &= ~RTF_REJECT;
return;
}
rtrequest(RTM_DELETE, rt_key(rt), (struct sockaddr *)0, rt_mask(rt),
0, (struct rtentry **)0);
从上面的代码中可以看出,在rt_refcnt大于0的情况下,仅仅将mac信息标志为无效(sdl_len=0),否则调用rtrequest删除相应的arp表项.
arp表项的建立
从前面我们大概知道了整个arp表项,但是我们还没有知道arp在内存中到底是什么.其实我们所谓的arp,就是主机直接路由,它通过clone接口子网路由产生.所以这一次,我们从arp的前生今世开始讲.
接口网络路由的建立
当我们给接口增加/删除地址的时候,都会影响路由表,通过设置地址/掩码,也就确定一个可以直达的网络,如ifAddrAdd("mottsec2","10.0.0.8","10.0.255.255",0xffff0000).这个函数调用经过一系列的传递之后,会调用到in_control,然后调用到in_ifinit,它里面有这样一段代码:
if (ifp->if_ioctl &&
(error = (*ifp->if_ioctl)(ifp, SIOCSIFADDR, (caddr_t)ia))) {
splx(s);
ia->ia_addr = oldaddr;
return (error);
}
因为if_ioctl就是ipIoctl,所以来看看,它作了些什么事情:
switch (cmd)
{
case SIOCSIFADDR:
for (pIa = in_ifaddr; pIa; pIa = pIa->ia_next)
if ((pIa->ia_ifp == (struct ifnet *)ifp) &&
(pIa->ia_addr.sin_addr.s_addr == dt_saddr))
break;
pIa->ia_ifa.ifa_rtrequest = arp_rtrequest;
pIa->ia_ifa.ifa_flags |= RTF_CLONING;
ifp->ac_ipaddr = IA_SIN (data)->sin_addr;
arpwhohas (ifp, &IA_SIN (data)->sin_addr);
break;
其它的不管,其中对我们arp影响最大的两项,就是将ifa_rtrequest设置为arp_rtrequest,并置上了RTF_CLONING标志.incontrol最后会调用rtinit,并由rtinit调用rtrequest将接口网络路由加入到系统路由表中.
clone路由的产生
当调用rtalloc查找主机路由时,它实际调用的是rtalloc1,我们来看一下代码:
if (rnh && (rn = rnh->rnh_matchaddr((caddr_t)dst, rnh)) &&
((rn->rn_flags & RNF_ROOT) == 0)) {
newrt = rt = (struct rtentry *)rn;
if (report && (rt->rt_flags & RTF_CLONING)) {
err = rtrequest(RTM_RESOLVE, dst, SA(0),
SA(0), 0, &newrt);
它首先在路由表中查找路由,如果找到了,并且具有clone标志RTF_CLONING,则需要调用rtrequest进行RTM_RESOLVE,这个过程就是clone过程,也就是建立主机直接路由的过程,也就是建立arp的过程.我们经常说过的arp实际上就是保存在路由表中,只不过是主机直接路由罢了.rtrequest在生成主机路由之后,执行下面的代码:
if (ifa->ifa_rtrequest)
ifa->ifa_rtrequest(req, rt, SA(ret_nrt ? *ret_nrt : 0));
我们知道ifa_rtrequest函数就是arp_rtrequest函数,当以RTM_RESOLVE为参数调用时,它负责生成arp控制信息,并把它加入到全局的arp链表中,只是现在它的mac地址还是为空.arp的处理模块会使得它填写有效值.
被动创建的arp项
由前面我们知道,当对方请求本地接口地址的mac信息时,我们会创建相应的arp项,这其实是通过arplookup实现的,其实arplookup也是通过调用rtalloc1的.