用arpsend命令学习ARP
ARP(Address Resolution Protocol,地址解析协议)的任务是将IP地址(网络层地址)解析为MAC地址(链路层地址)。本文首先简要介绍arpsend
命令的使用方法,然后通过tcpdump
和Wireshark分析ARP分组(ARP packet=ARP数据包)的格式,最后通过分析arpsend
命令的源代码,看看如何实现ARP分组的发送和接收。
1. 安装arpsend命令
在进行有关ARP的实验时,最容易想到的工具是arp
命令。然而arp
命令只能操作ARP表(参考https://github.com/ecki/net-tools/blob/master/arp.c#L785),并不能向任意的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节的说法:
应该无需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是属于网络层)的分组。
源代码:https://github.com/blueboxgroup/vzctl/blob/master/src/arpsend.c
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 https://datatracker.ietf.org/doc/html/rfc826
RFC 5227 https://datatracker.ietf.org/doc/html/rfc5227