写了一个叫 address-book 的玩具项目
在实现 Packer Plugin 的时候,遇到了一个头疼的问题,就是一个新装好的 VM,如何感知它的 IP 地址呢?
通常虚拟化软件都会通过在虚拟机中安装的 VM Tools 来进行 IP 的获取,但是各种 Linux 发行版官方镜像又不会携带这些东西,要安装 VM Tools 又最好能够通过 IP SSH 上去,这就成了一个死结 。
那么在 VM 之外有没有办法能够感知到它的 IP 呢?答案是肯定的。
实现思路
通常情况下,一台虚拟机加入到一个网络中,它需要一个 IP 地址,这个地址通常有两个来源:
- 静态地址
- DHCP
静态地址就是用户或软件直接设置 IP 地址、子网掩码、网关等配置,写什么就是什么。如果因为写错了或者 IP 别人已经占用了,就无法正常访问网络。
DHCP(动态主机设置协议)则是通过虚拟机与外部的 DHCP 服务器交互,申领一个 IP 地址来使用。这中间便是通过 DHCP 协议来进行沟通交流,获取 IP、网关等配置的。
不难看出如果一个 VM 通过 DHCP 协议获取 IP 地址,那么它就会在网络中有活动了,我们如果是网络中的一员,那么就必然能够有所觉察了。
从 DHCP 数据包中解析 IP
既然用到 DHCP,那就得先了解 DHCP,了解一次 DHCP 是如何完成的。DHCP 完整的步骤分为四步
- DISCOVER:客户端(我们的虚机)发送一个 IP 地址为「255.255.255.255」的 UDP 数据包,也就是向全网广播:我,新来的,想要一个 IP 地址。
- OFFER:DHCP 服务器收到了这个广播的消息后,会发送 OFFER 数据包,就是说:刚刚吆喝那小子,你就用这个 IP、子网掩码、网关配置吧。
- REQUEST:客户端收到 OFFER 之后,还需要确认一下,顺便告诉其它的 DHCP 服务器:你们不用等我了,我已经接了某某某的 OFFER 了。
- ACK:最后 DHCP 服务器会反馈:我也知道了,再给你发一遍,你就用这个地址了。
至此 VM 就配置好了 IP。当然实际场景下,可能一个 VM 原本就使用过 IP,可以只发送 REQUEST 请求续租 IP。
了解了这个流程就很简单了,我么可以在收到 OFFER 数据包的时候,判断 dst MAC 地址是不是 VM 的 MAC 地址,是的话就可以解析数据包中所携带的 IP 信息了。当然这时候 IP 地址还没有完全敲定,我们还可以等一等 ACK 的数据包,也是判断是不是发到我们期望的 MAC 地址,如果是,就从 UDP Payload 中解析 IP 数据了。
通常来说,一台计算机拿到 IP 地址之后,就得全网嘚瑟一下,那接下来就会发 ARP(地址解析协议)了。
从 ARP 数据包中解析 IP
我们通常基于 IP 协议的数据传输使用的都是 IP 地址,但是在同一网络内,两台主机通过 IP 地址进行互相通信的时候,需要知晓目标 IP 的物理地址(MAC 地址),系统会检测本地的 ARP 表,看下有没有这个 IP,如果没有的话就会向网络中广播:谁有 192.168.5.10 这个 IP 呀,回我一下。设置了 192.168.5.10 这个 IP 的机器收到后,就会回复一条:是我了,没错。
那么我们就需要关注:
- 任何一个 ARP 包都可以从 L2 层得到发包的 MAC 地址,并在 ARP 协议层找到 Source IP
- Reply 类型的数据包就可以从 ARP 协议层找到回应者的 IP 与 MAC 地址,同时也包含接受者的 MAC 与 IP 地址。
通过对得到的 IP/MAC 地址进行匹配,就可以得到目标虚拟机的 IP 地址了。
编写程序
流量抓包,肯定离不开 libpcap 这个库了,这里使用 Golang 开发,因此核心用到的是 github.com/google/gopacket
这个包。
首先需要获取创建 Handle,并在得到 handle 实例后设置 Filter,我们只关心 UDP 67 68 端口(DHCP 协议使用的端口)与 ARP 协议。下面的代码还顺带过滤了 ICMP 协议(Ping 命令使用的协议),不过实际没有用到相关的数据包。
1 | func NewCapture(db *Database, params CaptureParams) (*Capture, error) { |
创建 Handle 之后,就可以使用 Handle 创建一个 PacketSource,我们从 source 中不断读取收到的数据包。
1 | func (c *Capture) Run() { |
具体处理数据包解析出实际的 IP/MAC 地址信息的就是 getAddress()
方法:
1 | func (c *Capture) getAddresses(packet gopacket.Packet) []Address { |
我们通过 packet.Layer()
方法来判断当前数据包是否为某个协议的数据包,然后只处理 ARP 与 DHCP 的数据包。
最后就按照上面的描述进行地址的解析:
- 从 ARP 数据包中获取地址:https://github.com/microud/address-book/blob/main/capture.go#L52
- 从 DHCP 数据包中获取地址:https://github.com/microud/address-book/blob/main/capture.go#L85
小结
除了上面的核心代码,还会将所有捕获到的 IP/MAC 地址记录在内存数据库中,以供查询。因此程序提供了两个接口,可以用来根据 IP 查 MAC,也可以根据 MAC 查 IP,还可以列出当前记录的所有的地址记录,就像一个通讯录。
有了这个服务之后,Packer 就有一定办法可以获取到新建 VM 的 IP 地址了,不过还有一点局限性就是可能会有一些主机,即使加入了网络,也是一声不吭,完全自闭。这种情况就先不考虑了。