0
点赞
收藏
分享

微信扫一扫

用arpsend命令学习ARP

炽凤亮尧 2022-01-30 阅读 11

用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);
}

整体流程还是比较清晰的(省略了超时信号处理的部分):

  1. 解析命令行参数
  2. 创建socket,注意这里的参数不同于创建TCP/UDP的socket时使用的参数
  3. 调用init_device_addresses()解析形如“eth0”“enp0s8”的网卡名称,并初始化全局变量struct sockaddr_ll iaddr;
  4. 创建ARP分组,也就是填充全局变量struct arp_packet pkt;的各个字段
  5. 发送ARP分组
  6. 接收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_ALENIP_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

举报

相关推荐

0 条评论