用arpsend命令学习ARP协议
ARP(Address Resolution Protocol,地址解析协议)的任务是将IP地址(网络层地址)解析为MAC地址(链路层地址)。本文首先简要介绍arpsend命令的使用方法,然后通过tcpdump和Wireshark分析ARP分组(ARP packet=ARP数据包)的格式,最后通过分析arpsend命令的源代码,看看如何实现ARP分组的发送和接收。
1. 安装arpsend命令
在进行有关ARP的实验时,最容易想到的工具是arp命令。然而arp命令只能操作ARP表(参考github.com/ecki/net-to…),并不能向任意的IP地址发出ARP查询分组。好在还有arpsend命令来弥补arp命令的不足。
arpsend命令的安装方法非常简单,在Ubuntu中可以通过如下命令安装:
# apt install vzctl
# arpsend
Usage: arpsend <-U -i <src_ip_addr> | -D -e <trg_ip_addr> [-e <trg_ip_addr>] ...> [-c <count>] [-w <timeout>] interface_name
为了能够使用gdb调试,可以下载vzctl的源码,并使用-O0 -ggdb3编译选项手动编译:
# apt source vzctl
# cd vzctl-4.9.4
# ./configure CFLAGS="-O0 -ggdb3" --without-ploop --without-cgroup
# make
2. 发送ARP查询分组
安装好arpsend命令后,通过如下命令即可向指定的IP地址发送ARP查询分组:
# arpsend -D -e 172.28.128.2 enp0s8 -v
arpsend: got addresses hw='08:00:27:84:bc:b0', ip='172.28.128.3'
arpsend: send packet: eth '08:00:27:84:bc:b0' -> eth 'ff:ff:ff:ff:ff:ff'; arp sndr '08:00:27:84:bc:b0' '172.28.128.3'; request; arp recipient 'ff:ff:ff:ff:ff:ff' '172.28.128.2'
arpsend: recv unknown packet eth '08:00:27:84:bc:b0' -> eth 'ff:ff:ff:ff:ff:ff'; arp sndr '08:00:27:84:bc:b0' '172.28.128.3'; request; arp recipient 'ff:ff:ff:ff:ff:ff' '172.28.128.2'
arpsend: recv packet eth '08:00:27:bf:03:bd' -> eth '08:00:27:84:bc:b0'; arp sndr '08:00:27:bf:03:bd' '172.28.128.2'; reply; arp recipient '08:00:27:84:bc:b0' '172.28.128.3'
arpsend: 172.28.128.2 is detected on another computer : 08:00:27:bf:03:bd
参数-D -e需要一起使用,表示向-e指定的目标IP地址发送ARP查询分组。-v参数用于开启debug日志。enp0s8指出要从enp0s8这块网卡发送ARP查询分组(接下来使用tcpdump抓包时也是抓取这块网卡上的流量)。
# ifconfig enp0s8
enp0s8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.28.128.3 netmask 255.255.255.0 broadcast 172.28.128.255
inet6 fe80::a00:27ff:fe84:bcb0 prefixlen 64 scopeid 0x20<link>
ether 08:00:27:84:bc:b0 txqueuelen 1000 (Ethernet)
2.1. ARP分组的结构
下面我们先开启tcpdump再执行arpsend,以此来抓取ARP分组。
# tcpdump "arp" -vvvv -ttt -i enp0s8
再次执行arpsend -D -e 172.28.128.2 enp0s8,可以看到tcpdump输出了:
00:00:00.000000 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 172.28.128.2 (Broadcast) tell 172.28.128.3, length 46
00:00:00.000322 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 172.28.128.2 (Broadcast) tell 172.28.128.3, length 46
00:00:00.000008 ARP, Ethernet (len 6), IPv4 (len 4), Reply 172.28.128.2 is-at 08:00:27:bf:03:bd (oui Unknown), length 46
00:00:00.000012 ARP, Ethernet (len 6), IPv4 (len 4), Reply 172.28.128.2 is-at 08:00:27:bf:03:bd (oui Unknown), length 46
导入到Wireshark中:
我们可以从上图得出如下结论:
- Address Resolution Protocol(request)部分:
- ARP查询分组中包含请求发起主机的IP地址和MAC地址(倒数第3行和第4行)
- ARP查询分组使用的是MAC广播地址(倒数第2行,Target MAC address: Broadcast (ff:ff:ff:ff:ff:ff))
- Opcode: request (1)表示这个ARP分组是查询请求。ARP的查询分组和响应分组的格式相同,只是通过这个字段区分
- Ethernet II部分:
- 目标MAC地址也是MAC广播地址,与ARP中的Target MAC address一致
ARP分组是封装在以太网帧中的,而以太网帧有一个类型(Type)字段,该字段表明上一层采用了什么网络协议。这个字段的值通常是Type: IPv4 (0x0800),因为IPv4协议是以太网(数据链路层)的上层网络层的主流协议。但在封装了ARP分组的以太网帧中,该字段的值却是Type: ARP (0x0806),这说明此时以太网的“上层”协议是ARP,需要由ARP模块处理。那么问题来了,ARP模块处理完了ARP分组,接下来应该交给哪个上层协议/模块继续处理呢?
答案就在ARP分组的Protocol type: IPv4 (0x0800)字段,原来是要交给IPv4协议/模块处理啊。不过这里还是有个疑问,按照《计算机网络 自顶向下方法第七版》6.4.1节的说法:
原文:...and (because of the broadcast address) each adapter passes the ARP packet within the frame up to its ARP module. Each of these ARP modules checks to see if its IP address matches the destination IP address in the ARP packet. The one with a match sends back to the querying host a response ARP packet with the desired mapping.
译文:……且(由于广播地址)每个适配器都把在该帧中的ARP分组向上传递给 ARP模块。这些ARP模块中的每个都检查它的IP地址是否与ARP分组中的目的IP地址相匹配。与之匹配的一个给查询主机发送回一个带有所希望映射的响应ARP分组。
应该无需IP协议/模块检查,而是由ARP模块来检查目标IP地址是否与本机网卡的IP地址匹配。这样看来,ARP分组中的Protocol type字段又有什么用呢(恐怕得从RFC826中找答案)?
最后,再来看一下ARP响应分组的格式:
ARP的查询分组和响应分组拥有相同的格式,只是通过Opcode: reply (2)表明这是一个响应,而且Sender MAC address: PcsCompu_bf:03:bd (08:00:27:bf:03:bd)正是Who has 172.28.128.2? Tell 172.28.128.3的答案。
2.2. arpsend命令的源码分析
接下来,我们简单分析一下arpsend命令的源码,看看如何直接发送一个数据链路层(也有人认为ARP是属于网络层)的分组。
int main(int argc, char** argv)
{
// ...
parse_options (argc, argv);
// 对IPv6特殊处理,因为IPv6不再使用ARP,而是使用ICMPv6发送邻居探索消息
sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP));
// ...
if (init_device_addresses(sock, iface) < 0)
exit(EXC_SYS);
create_arp_packet(&pkt);
// ...
sender();
while(1)
{
u_char packet[4096];
struct sockaddr_ll from;
socklen_t alen = sizeof(from);
int cc;
cc = recvfrom(sock, packet, sizeof(packet), 0,
(struct sockaddr *)&from, &alen);
// ...
recv_pack(packet, cc, &from); // recv_pack中会调用finish()退出循环
}
exit(EXC_OK);
}
整体流程还是比较清晰的(省略了超时信号处理的部分):
- 解析命令行参数
- 创建socket,注意这里的参数不同于创建TCP/UDP的socket时使用的参数
- 调用
init_device_addresses()解析形如“eth0”“enp0s8”的网卡名称,并初始化全局变量struct sockaddr_ll iaddr; - 创建ARP分组,也就是填充全局变量
struct arp_packet pkt;的各个字段 - 发送ARP分组
- 接收ARP分组
代码中有几个比较有意思的点:
sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP));,其中的ETH_P_ARP定义在<linux/if_ether.h>:
#define ETH_P_ARP 0x0806 /* Address Resolution packet */
0x0806正是以太网帧Type字段的值。还记得创建TCP/UDP的socket时,socket()第3个参数的值吗?
封装了ARP分组的以太网帧定义在结构体struct arp_packet {中,其中的字段与Wireshark中的字段一一对应。宏ETH_ALEN和IP_ADDR_LEN的定义如下:
// <linux/if_ether.h>
#define ETH_ALEN 6 /* Octets in one ethernet addr */
// arpsend.c
#define IP_ADDR_LEN 4
文章的最后再抛出一个问题,如果arpsend另一个网段中的IP地址,会发生什么?《计算机网络 自顶向下方法第七版》6.4.1节给出了答案,但似乎与arpsend的行为不一致。
3. 参考
《计算机网络 自顶向下方法第七版》6.4.1节
《图解TCP/IP 第5版》5.3节
RFC 826 datatracker.ietf.org/doc/html/rf…
RFC 5227 datatracker.ietf.org/doc/html/rf…