文章目录
这篇笔记记录了ARP协议的初始化部分,涉及主要文件如下:
文件路径 | 描述 |
---|---|
include/linux/arp.h | arp协议头文件 |
net/ipv4/arp.c | arp协议实现文件 |
数据结构
ARP邻居协议对象: arp_tbl
每个邻居协议,都需要提供一个struct neigh_table对象,然后向系统注册自己,arp协议的struct neigh_table对象为arp_table。
struct neigh_table arp_tbl = {
.family = AF_INET, // ARP协议只用于IPv4协议族
// 邻居项大小之所以要加4,是因为邻居项的末尾要保存key,对于arp就是要映射的IPv4地址,所以是4
.entry_size = sizeof(struct neighbour) + 4,
.key_len = 4, // arp协议的key为IPv4地址,所以是4字节
.hash = arp_hash, // 邻居项哈希函数
.constructor = arp_constructor, // ARP邻居项初始化函数
.proxy_redo = parp_redo,
.id = "arp_cache", // 邻居协议名字
.parms = {
.tbl = &arp_tbl,
.base_reachable_time = 30 * HZ,
.retrans_time = 1 * HZ, // solicitations请求报文重试间隔为1s
.gc_staletime = 60 * HZ,
.reachable_time = 30 * HZ, // 可达性维持时间为30s
.delay_probe_time = 5 * HZ, // 延迟可达性确认时间为5s
.queue_len = 3, // 最多缓存3个报文
.ucast_probes = 3, // solicitations单播请求报文重试次数为3次
.mcast_probes = 3, // solicitations多播或广播请求此时为3次
.anycast_delay = 1 * HZ,
.proxy_delay = (8 * HZ) / 10, // 代理请求最大延迟处理事件为0.8s
.proxy_qlen = 64, // 最多缓存64个代理请求
.locktime = 1 * HZ,
},
.gc_interval = 30 * HZ,
.gc_thresh1 = 128,
.gc_thresh2 = 512,
.gc_thresh3 = 1024,
};
ARP报文格式
struct arphdr
{
__be16 ar_hrd; // L2层帧类型,取值ARPHRD_ETHER等
__be16 ar_pro; // L3层地址类型,取值如ETH_P_IP
unsigned char ar_hln; // L2层地址长度
unsigned char ar_pln; // L3层地址长度
__be16 ar_op; // ARP操作码,如ARPOP_REQUEST等
#if 0
/*
* Ethernet looks like this : This bit is variable sized however...
*/
unsigned char ar_sha[ETH_ALEN]; /* sender hardware address */
unsigned char ar_sip[4]; /* sender IP address */
unsigned char ar_tha[ETH_ALEN]; /* target hardware address */
unsigned char ar_tip[4]; /* target IP address */
#endif
};
```c
ARP协议报文的前8字节是固定的,后面部分根据协议地址长度以及L2层地址长度不同有所变化,可见在设计的时候,ARP不仅仅是为IPv4服务的,只不过实际中只有IPv4使用它而已。
# 初始化: arp_init()
该函数在IPv4协议族初始化函数inet_init()中被调用。
```cpp
void __init arp_init(void)
{
// 向系统注册arp邻居协议,注册过程中也会对arp_tbl中的邻居协议公共字段进行初始化
neigh_table_init(&arp_tbl);
// 向网络设备接口层注册数据包接收回调,这样当网卡收到arp类型的报文时,将交给本协议处理
dev_add_pack(&arp_packet_type);
// 创建/proc/net/arp文件节点,该节点展示了当前arp缓存内容
arp_proc_init();
#ifdef CONFIG_SYSCTL
// 创建/proc/sys/net/ipv4/neigh目录,并且在其中创建neight_params对应的参数文件
neigh_sysctl_register(NULL, &arp_tbl.parms, NET_IPV4, NET_IPV4_NEIGH, "ipv4", NULL, NULL);
#endif
// 监听网络设备状态变化
register_netdevice_notifier(&arp_netdev_notifier);
}
arp协议数据包接收处理器
初始化过程中通过dev_add_pack()向设备接口层注册了arp数据包接收函数arp_packet_type,内容如下:
/*
* Called once on startup.
*/
static struct packet_type arp_packet_type = {
.type = __constant_htons(ETH_P_ARP),
.func = arp_rcv,
};
设备接口层收到ETH_P_ARP类型的数据包后,将交给arp_rcv()继续处理。
网络设备状态事件处理: arp_netdev_event()
static int arp_netdev_event(struct notifier_block *this, unsigned long event, void *ptr)
{
struct net_device *dev = ptr;
switch (event) {
case NETDEV_CHANGEADDR: // 关注mac地址变化事件
neigh_changeaddr(&arp_tbl, dev);
// 刷新路由缓存
rt_cache_flush(dev_net(dev), 0);
break;
default:
break;
}
return NOTIFY_DONE;
}
void neigh_changeaddr(struct neigh_table *tbl, struct net_device *dev)
{
// 刷新arp缓存,见笔记“邻居子系统值邻居项的回收”
write_lock_bh(&tbl->lock);
neigh_flush_dev(tbl, dev);
write_unlock_bh(&tbl->lock);
}
arp邻居项初始化: arp_constructor()
当新建一个ARP邻居项时,会回调该函数。
static int arp_constructor(struct neighbour *neigh)
{
// key值就是邻居项要映射的IPv4地址
__be32 addr = *(__be32*)neigh->primary_key;
struct net_device *dev = neigh->dev;
struct in_device *in_dev;
struct neigh_parms *parms;
rcu_read_lock();
// 找到出口IP地址对应的IP地址配置结构
in_dev = __in_dev_get_rcu(dev);
if (in_dev == NULL) {
rcu_read_unlock();
return -EINVAL;
}
// 根据邻居项的IP地址类型设置邻居项类型
neigh->type = inet_addr_type(&init_net, addr);
// arp使用的是网络设备上的邻居协议参数
parms = in_dev->arp_parms;
__neigh_parms_put(neigh->parms);
neigh->parms = neigh_parms_clone(parms);
rcu_read_unlock();
if (!dev->header_ops) {
// 对于未指定首部操作的设备,表示其不需要L2帧头,这种情况下
// 邻居子系统对数据包透传即可
neigh->nud_state = NUD_NOARP;
neigh->ops = &arp_direct_ops;
neigh->output = neigh->ops->queue_xmit;
} else {
/* Good devices (checked by reading texts, but only Ethernet is tested)
ARPHRD_ETHER: (ethernet, apfddi)
ARPHRD_FDDI: (fddi)
ARPHRD_IEEE802: (tr)
ARPHRD_METRICOM: (strip)
ARPHRD_ARCNET:
etc. etc. etc.
ARPHRD_IPDDP will also work, if author repairs it.
I did not it, because this driver does not work even
in old paradigm.
*/
...
if (neigh->type == RTN_MULTICAST) {
// L3多播地址有固定的映射规则,所以设置邻居项状态为NUD_NOARP,并且用arp_mc_map()完成映射
neigh->nud_state = NUD_NOARP;
arp_mc_map(addr, neigh->ha, dev, 1);
} else if (dev->flags&(IFF_NOARP|IFF_LOOPBACK)) {
// 对于环回设备或者设备指示无需ARP的设备,使用它们提供了L2地址即可,
// 直接将网卡设备指明的地址拷贝到ha中作为映射结果
neigh->nud_state = NUD_NOARP;
memcpy(neigh->ha, dev->dev_addr, dev->addr_len);
} else if (neigh->type == RTN_BROADCAST || dev->flags&IFF_POINTOPOINT) {
// 广播地址、P2P设备也不需要进行映射,直接使用L2设备的广播地址即可
neigh->nud_state = NUD_NOARP;
memcpy(neigh->ha, dev->broadcast, dev->addr_len);
}
// 根据网络设备是否支持L2帧头缓存,选择不同的邻居项操作集
if (dev->header_ops->cache)
neigh->ops = &arp_hh_ops;
else
neigh->ops = &arp_generic_ops;
// 根据邻居项状态设置输出函数指针
if (neigh->nud_state&NUD_VALID)
neigh->output = neigh->ops->connected_output;
else
neigh->output = neigh->ops->output;
}
return 0;
}
设置邻居项操作集
邻居项的操作集是在创建是确定的,之后就不会再改变。从上面可以看出,确定它的依据就是网络设备本身的能力:
- 如果网络没有提供header_ops操作集,说明其无需邻居子系统参与L2帧首部的构建,这种情况下ARP协议会透传数据包(arp_direct_ops);
- 如果网络设备没有提供header_ops->cache()回调,说明其不支持L2帧首部缓存,或者其发往同一个邻居的L2帧首部是也是会频繁变化的,缓存没有意义,这种情况下ARP协议会使用最通用的邻居项操作集(arp_generic_ops);
- 和情况2相反,ARP协议会使用能够处理L2帧首部缓存的操作集(arp_hh_ops);
关于这3种操作集的实现分析见邻居子系统数据发送流程。
特殊地址映射
如上,一些特殊的IP地址是可以直接转换出对应的L2地址的,无需进行ARP协议流程,转换规则如下:
- 多播地址有固定的映射规则,见下方arp_mc_map();
- 网络设备无需ARP,或者是loopback设备,直接填充本地网络设备的L2地址即可;
- 广播地址或者点对点网络设备直接使用L2设备的广播地址即可;
arp_mc_map()
int arp_mc_map(__be32 addr, u8 *haddr, struct net_device *dev, int dir)
{
switch (dev->type) { // 根据网络设备类型做不同方式的映射
case ARPHRD_ETHER:
case ARPHRD_FDDI:
case ARPHRD_IEEE802:
ip_eth_mc_map(addr, haddr);
return 0;
...
default:
if (dir) {
memcpy(haddr, dev->broadcast, dev->addr_len);
return 0;
}
}
return -EINVAL;
}
static inline void ip_eth_mc_map(__be32 naddr, char *buf)
{
// IP地址为4字节,mac地址为6字节。前3字节为固定值,IP地址的后3字节映射到mac地址的后3字节
__u32 addr=ntohl(naddr);
buf[0]=0x01;
buf[1]=0x00;
buf[2]=0x5e;
buf[5]=addr&0xFF;
addr>>=8;
buf[4]=addr&0xFF;
addr>>=8;
buf[3]=addr&0x7F;
}