skaiuijing

引言

ARP是一种ip层用于获取对应的网卡mac地址的协议。

好了,相信你已经知道什么是ARP了,现在让我们实现ARP协议。

实现

Linux中arp的实现相当复杂,这里以后再讲吧。

代码使用笔者编写的玩具网络协议栈。

执行过程

当一个ARP数据包请求到来时,每个对应的ip会判断该数据包的目标ip是不是自己,如果不是,那么就丢弃无视,如果是,那么就构造一个arp回复包,告知目标ip自己的mac地址。

从这个过程中,我们可以看见两个角色:请求方和回复方。

数据结构

与ARP相关的数据结构实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ARP 协议头部结构,对应 RFC 826 中的 ARP 报文格式
struct arp_hdr {
unsigned short ar_hrd; // 硬件类型(Hardware Type),如 Ethernet 为 1
unsigned short ar_pro; // 协议类型(Protocol Type),如 IPv4 为 0x0800
unsigned char ar_hln; // 硬件地址长度(Hardware Address Length),Ethernet 为 6
unsigned char ar_pln; // 协议地址长度(Protocol Address Length),IPv4 为 4
unsigned short ar_op; // 操作码(Opcode),1 表示请求,2 表示响应
} __attribute__((packed)); // 禁止结构体自动对齐,确保与网络字节流格式一致

// ARP 报文的以太网封装结构,包含请求方和目标方的地址信息
struct arp_ether {
struct arp_hdr ea_hdr; // ARP 报文头部
unsigned char arp_sha[6]; // Sender Hardware Address:发送方 MAC 地址
unsigned int arp_spa; // Sender Protocol Address:发送方 IP 地址
unsigned char arp_tha[6]; // Target Hardware Address:目标方 MAC 地址(请求时为 0)
unsigned int arp_tpa; // Target Protocol Address:目标方 IP 地址
} __attribute__((packed)); // 同样禁止结构体对齐,确保与实际报文格式匹配

// ARP 缓存条目结构,用于维护 IP-MAC 映射关系
struct arp_cache {
struct list_node node; // 链表节点,用于挂载到 ARP 缓存链表中

unsigned int ipaddr; // 缓存的 IP 地址(目标地址)
unsigned char hwaddr[6]; // 对应的 MAC 地址(硬件地址)

struct rtentry *rt_ac; // 路由条目指针,与路由表关联
struct buf *last_buf; // 最近挂起的数据包(尚未发送,等待 ARP 解析)
long count_asked; // 已发送的 ARP 请求次数,用于重试或失败判断
};

请求方的数据包

当然,实际数据包的内容中是纯数据,因此笔者在前面加上字段说明:

在不清楚目标mac的情况下,请求方会直接使用全1作为目标mac,也就是广播所有网段内的ip:

1
2
3
4
5
6
7
8
9
10
11
12
Ethernet Header:
Destination MAC: ff:ff:ff:ff:ff:ff # 广播地址
Source MAC: 00:11:22:33:44:55 # 请求方 MAC
EtherType: 0x0806 # 表示 ARP 协议

ARP Payload:
Hardware Type: 0x0001 # Ethernet
Protocol Type: 0x0800 # IPv4
Hardware Size: 6 # MAC 地址长度
Protocol Size: 4 # IP 地址长度
Opcode: 0x0001 # 请求 (ARP Request)

这一段数据包的实际内容如下:

1
2
3
4
5

Sender MAC: 00:11:22:33:44:55 # 请求方 MAC
Sender IP: 192.168.1.1 # 请求方 IP
Target MAC: 00:00:00:00:00:00 # 未知,待解析
Target IP: 192.168.1.2 # 目标 IP

回复方的数据包

1
2
3
4
5
6
7
8
9
10
11
12
Ethernet Header:
Destination MAC: 00:11:22:33:44:55 # 请求方 MAC
Source MAC: aa:bb:cc:dd:ee:ff # 响应方 MAC
EtherType: 0x0806 # ARP 协议

ARP Payload:
Hardware Type: 0x0001 # Ethernet
Protocol Type: 0x0800 # IPv4
Hardware Size: 6 # MAC 地址长度
Protocol Size: 4 # IP 地址长度
Opcode: 0x0002 # 响应 (ARP Reply)

同理,这一段内容实际如下:

1
2
3
4
Sender MAC:     aa:bb:cc:dd:ee:ff        # 响应方 MAC
Sender IP: 192.168.1.2 # 响应方 IP
Target MAC: 00:11:22:33:44:55 # 请求方 MAC
Target IP: 192.168.1.1 # 请求方 IP

明白这一点后,我们开始实现代码。

代码实现

队列操作

arp需要实现一个缓存功能,实际上,在真实的操作系统中,arp是有一个定时器的,一般arp条目超过十五分钟没有更新,那么就会被释放清除,这里为了简洁起见,就不介绍了:

我们实现入队、出队即可,当然,这里为了方便,直接使用链表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

struct arp_cache AcHead;
struct list_node ArpInQue;

unsigned char broadcast_mac[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};


void arp_init()
{
list_node_init(&(AcHead.node));
list_node_init(&(ArpInQue));
AcHead.rt_ac = &RouteEntry;
}

void arp_cache_add_tail(struct arp_cache *ac)
{
list_add(&(AcHead.node), &(ac->node));
}

void arp_cache_remove_tail(struct arp_cache *ac)
{
list_remove(&(ac->node));
}



void arp_InQue_add_tail(struct buf *sk)
{
list_add(&ArpInQue, &(sk->node));

}

void arp_InQue_remove_tail(struct buf *sk)
{
list_remove(ArpInQue.prev);
}

ARP请求

对于ARP请求,我们需要构造对应的字段:

在ether_output中,会构造网卡对应的字段,因此,在这里,我们只需要构造arp对应的字段即可:

也就是硬件类型、协议类型、mac长度、ip长度、请求类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

void arp_request(unsigned int *sip, unsigned int *tip)
{
struct buf *sk;
struct eth_hdr *eh;
struct arp_ether *ae;
struct arp_hdr *ah;
struct _sockaddr sa;

sk = buf_get(sizeof(struct buf));
sk->data += sizeof(struct eth_hdr);
sk->data_len += sizeof(struct arp_ether);

ae = (struct arp_ether *)sk->data;
ah = &(ae->ea_hdr);

ah->ar_hrd = htons(ARPHRD_ETHER);
ah->ar_pro = htons(ETHERTYPE_IP);
ah->ar_hln = sizeof(ae->arp_sha);
ah->ar_pln = sizeof(ae->arp_spa);
ah->ar_op = htons(ARPOP_REQUEST);

memcpy(ae->arp_sha, OwnerNet.hwaddr, 6);
memcpy(&(ae->arp_spa), sip, 4);

memset(ae->arp_tha, 0, 6);
memcpy(&(ae->arp_tpa), tip, 4);

eh = (struct eth_hdr *)sa.sa_data;
eh->ether_type = htons(ETHERTYPE_ARP);
memcpy(eh->ether_dhost, broadcast_mac, 6);

sa.sa_family = AF_UNSPEC;
sa.sa_len = sizeof(sa);

ether_output(&OwnerNet, sk, &sa);

}

ARP回复

同样的,在判断目标arp请求是当前ip后,arp回复需要构造回复请求并发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

static void arp_reply(struct buf *sk)
{
struct _sockaddr sa;
struct eth_hdr *eh;
struct arp_ether *pkt = (struct arp_ether *)sk->data;

pkt->ea_hdr.ar_hrd = htons(ARPHRD_ETHER);
pkt->ea_hdr.ar_pro = htons(ETHERTYPE_IP);
pkt->ea_hdr.ar_hln = 6;
pkt->ea_hdr.ar_pln = 4;
pkt->ea_hdr.ar_op = htons(ARPOP_REPLY);

memcpy(pkt->arp_tha, pkt->arp_sha, 6);
pkt->arp_tpa = pkt->arp_spa;

memcpy(pkt->arp_sha, OwnerNet.hwaddr, 6);
pkt->arp_spa = OwnerNet.ipaddr.addr;

eh = (struct eth_hdr *)sa.sa_data;
memcpy(eh->ether_dhost, pkt->arp_tha, 6);
eh->ether_type = htons(ETHERTYPE_ARP);
sa.sa_family = AF_UNSPEC;
sa.sa_len = sizeof(sa);

ether_output(&OwnerNet, sk, &sa);
}

验证

现在我们编译协议栈并运行,然后使用命令:

1
arping -I tap0 192.168.1.200

如果终端打印:

1
2
3
4
5
skaiuijing@ubuntu:~/NetStack$ arping -I tap0 192.168.1.200
ARPING 192.168.1.200 from 192.168.1.100 tap0
Unicast reply from 192.168.1.200 [9E:4D:9E:E3:48:9F] 0.621ms
Unicast reply from 192.168.1.200 [9E:4D:9E:E3:48:9F] 0.713ms
Unicast reply from 192.168.1.200 [9E:4D:9E:E3:48:9F] 0.769ms

这说明,我们的arp协议构造成功了。

结语

下一节讲解ARP欺骗,顺便讲解如何使用eBPF技术实现ARP过滤、欺骗、重定向等功能。

Linux网络性能优化与eBPF技术是本系列的核心。

关于网络协议栈实现部分,TCP协议部分笔者会耗费较多篇幅并进行代码具体讲解。剩下的实现笔者会给出核心代码实现,简单理解框架即可。

对实现网络协议栈细节感兴趣的读者可以参考Linux或者FreeBSD等操作系统内核的实现。