目录
3 arp 邻居项的创建 + arp solicit 请求发送流程
前言
在arp初始化时,通过调用dev_add_pack将arp协议的接收处理函数添加到了三层协议数据包处理函数相关的hash链表ptype_base中(关于三层协议数据包处理函数相关的hash链表,参见《linux内核协议栈 三 / 四层协议接收数据处理函数以及相关的全局 hash表 / 数组》)。当底层接收到属于本机的arp数据包时,就会调用arp_rcv进行后续处理。
1 arp数据包文接收 arp_rcv()
功能:对接收到的arp数据包的处理函数
- 首先对arp数据包进行合理性检查。
- 调用NF_HOOK,判断是否需要对arp进行进一步的处理,对于需要进一步处理的数据包,则调用 arp_process() 进行后续处理。
/*
* Receive an arp request from the device layer.
*/
static int arp_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
const struct arphdr *arp;
if (dev->flags & IFF_NOARP ||
skb->pkt_type == PACKET_OTHERHOST ||
skb->pkt_type == PACKET_LOOPBACK)
goto freeskb;
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
goto out_of_mem;
/* ARP header, plus 2 device addresses, plus 2 IP addresses. */
if (!pskb_may_pull(skb, arp_hdr_len(dev)))
goto freeskb;
arp = arp_hdr(skb);
if (arp->ar_hln != dev->addr_len || arp->ar_pln != 4)
goto freeskb;
memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb));
return NF_HOOK(NFPROTO_ARP, NF_ARP_IN, skb, dev, NULL, arp_process);
freeskb:
kfree_skb(skb);
out_of_mem:
return 0;
}
1.1 处理arp请求 arp_process()【核心】
arp_process() 用于处理一个arp请求,主要是考虑如下几个方面:
- arp数据包的格式是否正确,是否是属于系统支持的邻居项协议
- 是否需要丢弃接收到的arp数据包
- 处理符合条件的arp数据包。
下面是处理arp包的几个条件:
丢弃数据包的标准:
- arp_process只处理request、reply的arp数据包,丢弃其他类型的数据包
- 对于类型为request的数据包,丢弃目的地址是组播或者loopback的arp数据。
对于需要处理的数据包,大致可以分为几个方面:
1、对本机发送的arp 请求的应答数据包的处理
2、arp 请求数据包
a)目的地址是本地地址,且源地址不为0的arp 请求数据包
b)目的地址是本地地址,且源地址为0的重复地址检测的arp请求数据包
3、非本地发送的arp请求的应答数据包,执行arp代理,具体原理以及过程参见《ARP代理(善意的欺骗)》
/*
* Process an arp request.
*/
static int arp_process(struct sk_buff *skb)
{
struct net_device *dev = skb->dev;
struct in_device *in_dev = __in_dev_get_rcu(dev);
struct arphdr *arp;
unsigned char *arp_ptr;
struct rtable *rt;
unsigned char *sha;
__be32 sip, tip;
u16 dev_type = dev->type;
int addr_type;
struct neighbour *n;
struct net *net = dev_net(dev);
/* arp_rcv below verifies the ARP header and verifies the device
* is ARP'able.
*/
if (in_dev == NULL)
goto out;
//调用arp_hdr获取skb数据中arp头的开始指针
arp = arp_hdr(skb);
//判断设备的类型与数据包中的硬件类型是否相符
switch (dev_type) {
default:
if (arp->ar_pro != htons(ETH_P_IP) ||
htons(dev_type) != arp->ar_hrd)
goto out;
break;
case ARPHRD_ETHER:
case ARPHRD_FDDI:
case ARPHRD_IEEE802:
/*
* ETHERNET, and Fibre Channel (which are IEEE 802
* devices, according to RFC 2625) devices will accept ARP
* hardware types of either 1 (Ethernet) or 6 (IEEE 802.2).
* This is the case also of FDDI, where the RFC 1390 says that
* FDDI devices should accept ARP hardware of (1) Ethernet,
* however, to be more robust, we'll accept both 1 (Ethernet)
* or 6 (IEEE 802.2)
*/
if ((arp->ar_hrd != htons(ARPHRD_ETHER) &&
arp->ar_hrd != htons(ARPHRD_IEEE802)) ||
arp->ar_pro != htons(ETH_P_IP))
goto out;
break;
case ARPHRD_AX25:
if (arp->ar_pro != htons(AX25_P_IP) ||
arp->ar_hrd != htons(ARPHRD_AX25))
goto out;
break;
case ARPHRD_NETROM:
if (arp->ar_pro != htons(AX25_P_IP) ||
arp->ar_hrd != htons(ARPHRD_NETROM))
goto out;
break;
}
/* Understand only these message types */
if (arp->ar_op != htons(ARPOP_REPLY) &&
arp->ar_op != htons(ARPOP_REQUEST))
goto out;
/*
* Extract fields
*/
//获取arp数据包中源mac地址、源ip地址、目的mac地址、目的ip地址
arp_ptr = (unsigned char *)(arp + 1);
sha = arp_ptr;
arp_ptr += dev->addr_len;
memcpy(&sip, arp_ptr, 4);
arp_ptr += 4;
switch (dev_type) {
#if IS_ENABLED(CONFIG_FIREWIRE_NET)
case ARPHRD_IEEE1394:
break;
#endif
default:
arp_ptr += dev->addr_len;
}
memcpy(&tip, arp_ptr, 4);
/*
* Check for bad requests for 127.x.x.x and requests for multicast
* addresses. If this is one such, delete it.
*/
//丢弃目的地址是组播或者loopback的arp数据(对于组播地址和loopback地址是不需要arp)
if (ipv4_is_multicast(tip) ||
(!IN_DEV_ROUTE_LOCALNET(in_dev) && ipv4_is_loopback(tip)))
goto out;
/*
* Special case: We must set Frame Relay source Q.922 address
*/
if (dev_type == ARPHRD_DLCI)
sha = dev->broadcast;
/*
* Process entry. The idea here is we want to send a reply if it is a
* request for us or if it is a request for someone else that we hold
* a proxy for. We want to add an entry to our cache if it is a reply
* to us or if it is a request for our address.
* (The assumption for this last is that if someone is requesting our
* address, they are probably intending to talk to us, so it saves time
* if we cache their address. Their address is also probably not in
* our cache, since ours is not in their cache.)
*
* Putting this another way, we only care about replies if they are to
* us, in which case we add them to the cache. For requests, we care
* about those for us and those for our proxies. We reply to both,
* and in the case of requests for us we add the requester to the arp
* cache.
*/
/* Special case: IPv4 duplicate address detection packet (RFC2131) */
/*对于源ip地址是0的arp请求,一般用于重复地址检测,
./arping -U x.x.x.x
此时如果 arp 类型为request,且目的ip地址是本地地址,且可以进行arp应答时,
则调用arp_send发送arp reply数据包。
问题:对于源地址为0的数据包,在发送arp 应答报文时,为什么没有先查找路由表呢?
答:在我们建立路由表时,都会建立一个全零的默认路由,所以对于目的ip为0的
数据包,其路由是一直存在的。所以在处理时不用查找路由表,直接生成
arp reply数据包,并发送出去。
[root@xxxx ~]# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.x.x.x 0.0.0.0 UG 0 0 0 eth0
*/
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;
}
//对于arp 类型为request的数据包,且能找到到目的地址tip的路由,则执行下面的代码
if (arp->ar_op == htons(ARPOP_REQUEST) &&
ip_route_input_noref(skb, tip, sip, 0, dev) == 0) {
//获取tip对应的路由缓存
rt = skb_rtable(skb);
addr_type = rt->rt_type;
/*1、如果路由缓存对应的ip地址类型为local,则调用neigh_event_ns(被动学习arp处理),查找符合条件的邻居项
a)如果找到符合条件的邻居项(找不到则创建一个arp表项),则调用arp_send发送对该arp request包的reply包,并返回
b)直接返回。
2、如果路由缓存对应的ip地址类型不是local,则进行arp proxy的处理,完成后直接返回
*/
if (addr_type == RTN_LOCAL) {
int 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 = 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;
} else if (IN_DEV_FORWARD(in_dev)) {//arp代理过程
if (addr_type == RTN_UNICAST &&
(arp_fwd_proxy(in_dev, dev, rt) ||
arp_fwd_pvlan(in_dev, dev, rt, sip, tip) ||
(rt->dst.dev != dev &&
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);
return 0;
}
goto out;
}
}
}
/* Update our ARP tables */
/*
1、对于arp reply数据包,进入下面的处理流程
2、对于arp request数据包,且没有找到tip ip对应的路由缓存
*/
//调用__neigh_lookup,查找arp_tbl的neighbour hash bucket,查找sip对应的邻居项
n = __neigh_lookup(&arp_tbl, &sip, dev, 0);
//对于系统允许非arp请求的arp reply,则进行如下处理
if (IN_DEV_ARP_ACCEPT(in_dev)) {///proc/sys/net/ipv4/conf/all/arp_accept
/* Unsolicited ARP is not accepted by default.
It is possible, that this option should be enabled for some
devices (strip is candidate)
*/
/*
1、对于非由arp请求的arp reply,且没有相应的neighbour,则强制创建新的neighbour
2、对于sip与tip相等的arp request,也强制创建新的neighbour(arping -U x.x.x.x)
3、注意这里源ip有要求:对端必须是网关或者直连路由可达
*/
if (n == NULL &&
(arp->ar_op == htons(ARPOP_REPLY) ||
(arp->ar_op == htons(ARPOP_REQUEST) && tip == sip)) &&
inet_addr_type(net, sip) == RTN_UNICAST)
n = __neigh_lookup(&arp_tbl, &sip, dev, 1);
}
/*如果查找到符合条件的neighbour,则执行如下代码
1、对于发给给本机的arp reply报文,则将邻居项设置为reach状态
2、对于发给给本机的arp request报文,则将邻居项状态设置为stale状态
最后调用neigh_update,更新neighbour的状态
3、参数 sha 用于更新远端mac即下一跳的mac地址
*/
if (n) {
int state = NUD_REACHABLE;
int override;
/* If several different ARP replies follows back-to-back,
use the FIRST one. It is possible, if several proxy
agents are active. Taking the first reply prevents
arp trashing and chooses the fastest router.
*/
override = time_after(jiffies, n->updated + n->parms->locktime);
/* Broadcast replies and request packets
do not assert neighbour reachability.
*/
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);
}
out:
consume_skb(skb);
return 0;
}
2 arp数据包发送 arp_send()
arp_send() 就是 arp_create() 的封装函数,相比arp_creare(),增加了判断设备是否为NOARP的设备。
/*
* Create and send an arp packet.
*/
void arp_send(int type, int ptype, __be32 dest_ip,
struct net_device *dev, __be32 src_ip,
const unsigned char *dest_hw, const unsigned char *src_hw,
const unsigned char *target_hw)
{
struct sk_buff *skb;
/*
* No arp on this interface.
*/
if (dev->flags&IFF_NOARP)
return;
skb = arp_create(type, ptype, dest_ip, dev, src_ip,
dest_hw, src_hw, target_hw);
if (skb == NULL)
return;
arp_xmit(skb);
}
EXPORT_SYMBOL(arp_send);
/*
* Send an arp packet.
*/
void arp_xmit(struct sk_buff *skb)
{
/* Send it off, maybe filter it using firewalling first. */
NF_HOOK(NFPROTO_ARP, NF_ARP_OUT, skb, NULL, skb->dev, dev_queue_xmit);
}
EXPORT_SYMBOL(arp_xmit);
2.1 arp 数据包构造 arp_create()
该函数主要是申请一个缓存,并根据arp协议的格式,创建一个arp数据包。该函数还是比较简单的。
/*
* Create an arp packet. If (dest_hw == NULL), we create a broadcast
* message.
*/
struct sk_buff *arp_create(int type, int ptype, __be32 dest_ip,
struct net_device *dev, __be32 src_ip,
const unsigned char *dest_hw,
const unsigned char *src_hw,
const unsigned char *target_hw)
{
struct sk_buff *skb;
struct arphdr *arp;
unsigned char *arp_ptr;
int hlen = LL_RESERVED_SPACE(dev);
int tlen = dev->needed_tailroom;
/*
* Allocate a buffer
*/
//首先调用alloc_skb,申请缓存空间
skb = alloc_skb(arp_hdr_len(dev) + hlen + tlen, GFP_ATOMIC);
if (skb == NULL)
return NULL;
skb_reserve(skb, hlen); //留出源、目的mac地址的空间
skb_reset_network_header(skb);//设置三层头部指针
arp = (struct arphdr *) skb_put(skb, arp_hdr_len(dev));//设置arp头指针
skb->dev = dev;
skb->protocol = htons(ETH_P_ARP);
if (src_hw == NULL)//设置源、目的mac地址
src_hw = dev->dev_addr;
if (dest_hw == NULL)
dest_hw = dev->broadcast;
/*
* Fill the device header for the ARP frame
*/
//通过调用eth_header,填充二层头部
if (dev_hard_header(skb, dev, ptype, dest_hw, src_hw, skb->len) < 0)
goto out;
/*
* Fill out the arp protocol part.
*
* The arp hardware type should match the device type, except for FDDI,
* which (according to RFC 1390) should always equal 1 (Ethernet).
*/
/*
* Exceptions everywhere. AX.25 uses the AX.25 PID value not the
* DIX code for the protocol. Make these device structure fields.
*/
switch (dev->type) {
//设置硬件协议类型与软件协议类型,对于Ethernet硬件类型为1软件类型为0x0800
default:
arp->ar_hrd = htons(dev->type);
arp->ar_pro = htons(ETH_P_IP);
break;
#if IS_ENABLED(CONFIG_AX25)
case ARPHRD_AX25:
arp->ar_hrd = htons(ARPHRD_AX25);
arp->ar_pro = htons(AX25_P_IP);
break;
#if IS_ENABLED(CONFIG_NETROM)
case ARPHRD_NETROM:
arp->ar_hrd = htons(ARPHRD_NETROM);
arp->ar_pro = htons(AX25_P_IP);
break;
#endif
#endif
#if IS_ENABLED(CONFIG_FDDI)
case ARPHRD_FDDI:
arp->ar_hrd = htons(ARPHRD_ETHER);
arp->ar_pro = htons(ETH_P_IP);
break;
#endif
}
//设置硬件协议长度、软件协议长度、arp包类型
arp->ar_hln = dev->addr_len;
arp->ar_pln = 4;
arp->ar_op = htons(type);
//设置arp的源mac、ip 与目的mac、ip地址
arp_ptr = (unsigned char *)(arp + 1);
memcpy(arp_ptr, src_hw, dev->addr_len);
arp_ptr += dev->addr_len;
memcpy(arp_ptr, &src_ip, 4);
arp_ptr += 4;
switch (dev->type) {
#if IS_ENABLED(CONFIG_FIREWIRE_NET)
case ARPHRD_IEEE1394:
break;
#endif
default:
if (target_hw != NULL)
memcpy(arp_ptr, target_hw, dev->addr_len);
else
memset(arp_ptr, 0, dev->addr_len);
arp_ptr += dev->addr_len;
}
memcpy(arp_ptr, &dest_ip, 4);
return skb;
out:
kfree_skb(skb);
return NULL;
}
EXPORT_SYMBOL(arp_create);
2.2 arp_send() 发送场景
对于arp_send,既可以发送arp请求数据包,也可以发送arp应答报文,主要是在arp_process中调用。对于应答报文,回复的依据为:
- 对于重复地址检测请求,则发送一个arp reply消息。
- 对于发往本地的arp request,则发送一个arp reply消息,并将邻居项的状态设置为NUD_STALE。
3 arp 邻居项的创建 + arp solicit 请求发送流程
3.1 创建+发送流程
参见《linux内核协议栈 邻居协议之通用邻居项的状态机机制【核心】》中的第4节
3.2 arp请求报文发送 arp_solicit()
在neigh_resolve_output里就会调用到__neigh_event_send() 判断数据包是否可以直接发送出去,如果此时邻居项的状态为NUD_NONE,则会将邻居项的状态设置为NUD_INCOMPLETE,并将要发送的数据包缓存到邻居项的队列中。在该函数的结尾处通过来判断变量 immediate_probe 来决定是否立刻调用 neigh_probe() 来发送arp请求。而 immediate_probe 变量的置位就是将邻居的状态由 “NUD_NONE” 迁移至 "NUD_INCOMPLETE"时进行的。
对于arp 来说,其在 neigh_probe() 调用 neigh->ops->solicit 即为arp_solicit。
在分析之前,首先需要理解 arp announce的级别,当发送arp请求的主机对应的ip地址不止一个时,以下是 arp announce 级别决定如何选择ip地址:
- 0:任何ip地址都可以
- 1:尽可能选择和目的ip处于同一个子网的ip地址,否则使用级别2的选择
- 2:优先使用主地址
static void arp_solicit(struct neighbour *neigh, struct sk_buff *skb)
{
__be32 saddr = 0;
u8 dst_ha[MAX_ADDR_LEN], *dst_hw = NULL;
struct net_device *dev = neigh->dev;
__be32 target = *(__be32 *)neigh->primary_key;
int probes = atomic_read(&neigh->probes);
struct in_device *in_dev;
rcu_read_lock();
//获取源设备ip层的相关信息
in_dev = __in_dev_get_rcu(dev);
if (!in_dev) {
rcu_read_unlock();
return;
}
switch (IN_DEV_ARP_ANNOUNCE(in_dev)) {
default:
/*判断数据包的源地址是否为本地地址。
若是,则将源地址设置为数据包的源地址;
若不是,则调用inet_select_addr选择一个源地址*/
case 0: /* By default announce any local IP */
if (skb && inet_addr_type(dev_net(dev),
ip_hdr(skb)->saddr) == RTN_LOCAL)
saddr = ip_hdr(skb)->saddr;
break;
case 1: /* Restrict announcements of saddr in same subnet */
/*判断数据包的源地址是否为本地地址。
若是,则优先选择与目的ip地址在相同子网上的ip地址,否则则调用
inet_select_addr优先使用主地址*/
if (!skb)
break;
saddr = ip_hdr(skb)->saddr;
if (inet_addr_type(dev_net(dev), saddr) == RTN_LOCAL) {
/* saddr should be known to target */
if (inet_addr_onlink(in_dev, target, saddr))
break;
}
saddr = 0;
break;
case 2: /* Avoid secondary IPs, get a primary/preferred one */
break;//调用inet_select_addr优先获取一个符合条件的主地址
}
rcu_read_unlock();
/*
若此时还没有设置源ip地址,则调用inet_select_addr获取ip地址,该函数主要实现的功能:
1、对于指定设备dev所关联的ip配置块,查找scope小于等于RT_SCOPE_LINK且与目的地址
属于同一子网的地址作为源ip地址
2、对于符合scope条件的ip地址,若没有子网相同的地址,则选择主地址作为源ip地址
3、对于在指定设备dev上找不到满足scope条件的ifaddr结构,则遍历所有dev设备,找到
符合条件的ifaddr结构,并将其主地址作为源ip地址。
*/
if (!saddr)
saddr = inet_select_addr(dev, target, RT_SCOPE_LINK);
probes -= neigh->parms->ucast_probes;
if (probes < 0) {//判断arp请求报文是否到达上限,若到达上限则不发送
if (!(neigh->nud_state & NUD_VALID))
pr_debug("trying to ucast probe in NUD_INVALID\n");
neigh_ha_snapshot(dst_ha, neigh, dev);
dst_hw = dst_ha;
} else {
probes -= neigh->parms->app_probes;
if (probes < 0) {
#ifdef CONFIG_ARPD
neigh_app_ns(neigh);
#endif
return;
}
}
//调用arp_send发送arp请求包
arp_send(ARPOP_REQUEST, ETH_P_ARP, target, dev, saddr,
dst_hw, dev->dev_addr, NULL);
}