ARP 地址解析协议

ARP(Address Resolution Protocol)

地址解析协议

引言

传统IPv4网络需要使用32位的IPv4地址和48位的硬件地址, 才可以将一个帧发送至另外一台主机 地址解析协议 ARP 提供了在IPv4地址和硬件地址之间的映射, ARP 仅用于IPv4, IPv6使用邻居发现协议, 它被合并入ICMPv6

ARP缓存

ARP高效运行的关键是维护在每个主机和路由器上的ARP缓存, 该缓存使用地址解析为每个接口维护从网络层地址到硬件地址的最新映射

Linux下使用arp命令查看ARP缓存

$ arp
Address     HWtype HWaddress         FlagsMask  Iface
192.168.1.1 ether  f4:0f:3b:2a:4b:ec C          eno1

arp -a 用于显示缓存中的所有条目

$ arp -a
domain (192.168.199.121) at f4:0f:24:2a:4b:ec [ether] on eno1

ARP帧格式

字段DSTSRCTYPEHardwareTypeProtocolTypeHardwareLengthProtoclLengthOp
字节66222112
字段SRC MACSRC IPDST MACDST IPPaddingFCS
字节6666184

字段解释

字段名称字节
DST目的地址6[ff:ff:ff:ff:ff:ff]
SRC来源地址6[12:34:56:78:9a:bc]
TYPE长度或类型2ARP 该字段必须为 0x0806
Hardware Type硬件类型20x01
Protocol Type协议类型20x0800
Hardware Len硬件大小10x06
Protocol Len协议大小10x04
Op操作2ARP请求=0x01 ARP应答=0x02 RARP请求=0x03 RARP应答=0x04
SRC MAC源硬件地址6[12:34:56:78:9a:bc]
SRC IP源IPv4地址4[123.123.123.123]
DST MAC目标硬件地址6[12:34:56:78:9a:bc]
DST IP目标IPv4地址4[123.123.123.123]
Padding填充字段180x00
FCS4

ARP 缓存超时

在大多数实现中, 完成条目的超时为20分钟, 不完整条目的超时时间为3分钟

ARP数据包发送与接收

发送ARP数据包

  1. Linux:

    • 首先实现一个 htons, 因为 Socket() 中 protocol 参数占用两个字节,所以实现一个 int16 的 htons 就可以了
        func Htons16(n int) int {
            return (n & 0xFF) << 8 + (n >> 8) & 0xFF
        }
    
    • 定义 Arp Packet 结构
        // ArpPacket  ARP数据包
        type ArpPacket struct {
            DstMac [6]byte // 目的地址
            SrcMac [6]byte // 来源地址
            Frame  uint16  // 长度或类型
    
            HwType     uint16   // 硬件类型
            ProtoType  uint16   // 协议类型
            HwLen      byte     // 硬件大小
            ProtoLen   byte     // 协议大小
            Op         uint16   // 操作
            ArpSrcMac  [6]byte  // 源硬件地址
            ArpSrcIp   [4]byte  // 源IPv4地址
            ArpDstMac  [6]byte  // 目标硬件地址
            ArpDstIp   [4]byte  // 目标IPv4地址
            ArpPadding [18]byte // 填充字段
            //ArpFCS     [4]byte
        }
    
    • 构造 Arp 数据包
        // 构造arp数据包
        packet := new(arp.ArpPacket)
        // 构造头部信息
        copy(packet.DstMac[:], dstMac)
        copy(packet.SrcMac[:], eno1.HardwareAddr)
        packet.Frame = syscall.ETH_P_ARP
    
        // 构造ARP类型
        packet.HwType = 0x01
        packet.ProtoType = 0x0800
        packet.HwLen = 0x06
        packet.ProtoLen = 0x04
        packet.Op = ArpRequest
    
        // 构造来源ARP地址信息
        srcIp := net.ParseIP("192.168.199.153")
        copy(packet.ArpSrcMac[:], eno1.HardwareAddr)
        copy(packet.ArpSrcIp[:], srcIp.To4())
    
        // 构造目的ARP地址信息
        dstIp := net.ParseIP("192.168.199.101")
        //copy(packet.ArpDstMac[:], dstMac)
        copy(packet.ArpDstIp[:], dstIp.To4())
    
    • 建立原始套接字
        sockfd, err := syscall.Socket(
            syscall.AF_PACKET,
            syscall.SOCK_RAW,
            arp.Htons16(syscall.ETH_P_ARP))
        if err != nil {
            log.Fatalln(err)
        }
    
    • 发送数据包并关闭 Socket
     1    // 将Packet结构转为bytes
     2    buffer := bytes.NewBuffer(make([]byte, 0))
     3    if err := binary.Write(buffer, binary.BigEndian, packet); err != nil {
     4        log.Fatalln(err)
     5    }
     6
     7    // 发送数据
     8    if err = syscall.Sendto(sockfd, buffer.Bytes(), 0, linklayer); err != nil {
     9        log.Fatalln(err)
    10    }
    11
    12    // 关闭套接字
    13
    14    if err := syscall.Close(sockfd); err != nil {
    15        log.Fatalln(err)
    16    }
    
  2. Mac OS X

    需要使用BPF实现, 等有空的时候再实现

接收ARP数据包

  1. Linux

    • 建立原始套接字
        // 通过 Linux 原始套接字完成数据包的接收
        recvFd, err := syscall.Socket(
            syscall.AF_PACKET,
            syscall.SOCK_RAW,
            arp.Htons16(syscall.ETH_P_ARP))
        if err != nil {
            log.Fatalln(err)
        }
    
    • 接受 Arp 数据包
        // 数据会读取进buf切片中
        buf := make([]byte, 60)
    
        n, fromAddr, err := syscall.Recvfrom(recvFd, buf, 0)
        if err != nil {
            log.Fatalln(err)
        }
    
    • 解析数据包
        packet := new(arp.ArpPacket)
        buffer := bytes.NewBuffer(buf)
        if err := binary.Read(buffer, binary.BigEndian, packet); err != nil {
            log.Fatalln(err)
        }
    
    • 关闭套接字
        // 关闭套接字
        if err := syscall.Close(recvFd); err != nil {
            log.Fatalln(err)
        }
    
  2. Mac OS X

    需要使用BPF实现, 等有空的时候再实现