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等操作系统内核的实现。