实现Arp报文发送和接收

继上次实现了 Ping 之后,尝试进入更底层的网络接口层实现局域网的 ARP 报文收发

ARP 协议概述

ARP(Address Resolution Protocol) 地址解析协议是用来通过网络层地址(IP地址)去寻找数据链路层地址(MAC地址)的网络传输协议.

在以太网(Ethernet)协议中规定,同一局域网中的一台主机要和另一台主机进行直接通信,必须要知道目标主机的 MAC 地址。而在 TCP/IP 协议中,网络层和传输层只关心目标主机的IP地址。这就导致在以太网中使用 IP 协议时,数据链路层的以太网协议接到上层IP协议提供的数据中,只包含目的主机的IP地址。于是需要一种方法,根据目的主机的IP地址,获得其MAC地址。这就是 ARP 协议要做的事情。所谓地址解析(address resolution)就是主机在发送帧前将目标IP地址转换成目标MAC地址的过程。另外,当发送主机和目的主机不在同一个局域网中时,即便知道对方的MAC地址,两者也不能直接通信,必须经过路由转发才可以。所以此时,发送主机通过ARP协议获得的将不是目的主机的真实MAC地址,而是一台可以通往局域网外的路由器的MAC地址。于是此后发送主机发往目的主机的所有帧,都将发往该路由器,通过它向外发送。这种情况称为委托ARP或ARP代理(ARP Proxy)。—— 地址解析协议

报文格式

以太网首部: net/ethernet.h

typedef struct  ether_header {
    u_char  ether_dhost[ETHER_ADDR_LEN]; /* 目标以太网地址 */
    u_char  ether_shost[ETHER_ADDR_LEN]; /* 源以太网地址 */
    u_short ether_type;                  /* 帧类型 */
} ether_header_t;
// ETHER_ADDR_LEN 为 6

ARP 请求/应答: net/if_arp.h

struct  arphdr {
    u_short ar_hrd;         /* 硬件类型 format of hardware address */
#define ARPHRD_ETHER    1       /* ethernet hardware format */
#define ARPHRD_IEEE802  6       /* token-ring hardware format */
#define ARPHRD_FRELAY   15      /* frame relay hardware format */
#define ARPHRD_IEEE1394 24      /* IEEE1394 hardware address */
#define ARPHRD_IEEE1394_EUI64 27 /* IEEE1394 EUI-64 */
    u_short ar_pro;         /* 协议类型 format of protocol address */
    u_char  ar_hln;         /* 硬件地址长度 length of hardware address */
    u_char  ar_pln;         /* 协议地址长度 length of protocol address */
    u_short ar_op;          /* 操作码 one of: */
#define ARPOP_REQUEST   1       /* request to resolve address */
#define ARPOP_REPLY     2       /* response to previous request */
#define ARPOP_REVREQUEST 3      /* request protocol address given hardware */
#define ARPOP_REVREPLY  4       /* response giving protocol address */
#define ARPOP_INVREQUEST 8      /* request to identify peer */
#define ARPOP_INVREPLY  9       /* response identifying peer */
/*
 * The remaining fields are variable in size,
 * according to the sizes above.
 */
#ifdef COMMENT_ONLY
    u_char  ar_sha[];       /* 源硬件地址  sender hardware address */
    u_char  ar_spa[];       /* 源协议地址  sender protocol address */
    u_char  ar_tha[];       /* 目标硬件地址 target hardware address */
    u_char  ar_tpa[];       /* 目标协议地址 target protocol address */
#endif
};

实现

在 Linux 系统上, 可以通过 PF_PACKET 创建由用户态程序收发数据链接层数据的 Packet Socket, 从而发送完全自定义的 ARP 报文。但是在基于 BSD 的系统(比如 MacOS) 上, 是不支持 PF_PACKET 类型的 Socket 的,这时候就要利用 BPF(Berkeley Packet Filter)伯克利包过滤器来实现原始链路层数据的收发. —— BPF

Berkeley Packet Filter

数据包过滤器显示为字符特殊设备 /dev/bpfN(N为0~N, 一台机器上可能会提供多个 bpf 文件)。打开设备后,必须使用 ioctl 调用并结合 BIOCSETIF, 将文件描述符绑定到特定的网络接口。给定的接口可以由多个侦听器共享,并且每个描述符下面的过滤器将看到相同的数据包流。--- man bpf

打开 BPF 设备

int openBpf()
{
    char _buf[32];
    int bfd = -1;
    int i = 0;
    // 查找一个可用的 BPF 设备
    for (i = 0; i < 255; i++)
    {
        snprintf(_buf, sizeof(_buf), "/dev/bpf%u", i);
        bfd = open(_buf, O_RDWR);
        if (bfd > 0)
        {
            break;
        }
    }
    return bfd;
}

设置 BPF 文件

int setupBpf(int fd, const char *ifname) {
    // ifname 为硬件接口名字, 比如 en0 就代表网卡一
    struct ifreq request;
    strlcpy(request.ifr_name, ifname, sizeof(request.ifr_name) - 1);
    /* 将硬件接口和BPF文件描述符绑定 */
    int resp = ioctl(fd, BIOCSETIF, &request);
    if (resp < 0) {
        perror("BIOCSETIF failed: ");
        return -1;
    }

    /* 返回附加接口下的数据链接层的类型, 也就是返回我们绑定的硬件接口(en0)支持的数据层类型 */
    u_int type;
    if (ioctl(fd, BIOCGDLT, &type) < 0) {
        perror("BIOCGDLT failed: ");
        return -1;
    }

    if (type != DLT_EN10MB) {
        // 如果不是支持 10MB 的网卡
        printf("unsupported datalink type\n");
        return -1;
    }

    /* 启用即时模式, 启用即时模式后,读取数据包后立即返回。否则, 读取将阻塞, 直到内核 buffer 变满或发生超时 */
    int enable = 1;
    if (ioctl(fd, BIOCIMMEDIATE, &enable) < 0) {
        perror("BIOCSIMMEDIATE failed: ");
        return -1;
    }
    return 0;
}

DNS 解析

/* 根据域名或IP地址获取实际 IP地址, 并写入到 sockaddr_in 结构体中 */
struct sockaddr_in getsockaddrbyhost(const char *host) {
    hostent *h = gethostbyname(host);
    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr = *(in_addr *)(h->h_addr);
    return addr;
}

获取本机 IP地址和 MAC地址

int getAddrs(struct sockaddr_in *protocolAddr, u_char *hardwareAddr) {
    struct ifaddrs *addrs, *addr;
    struct sockaddr_dl hardwareDl;
    /* getifaddrs 会返回当前计算机网络接口的信息, 可以看作它会把 ifconfig 命令的内容返给你 */
    if (getifaddrs(&addrs) < 0) {
        perror("[getifaddrs]");
        return -1;
    }
    addr = addrs;
    /* 这里我固定了获取网卡一(en0)的地址 */
    while (addr) {
        if (strcmp("en0", addr->ifa_name) == 0 && addr->ifa_addr->sa_family == AF_INET)
        {
            memcpy(protocolAddr, (struct sockaddr_in *)(addr->ifa_addr), sizeof(struct sockaddr_in));
        }
        if (strcmp("en0", addr->ifa_name) == 0 && addr->ifa_addr->sa_family == AF_LINK)
        {
            memcpy(&hardwareDl, (struct sockaddr_dl *)(addr->ifa_addr), sizeof(struct sockaddr_dl));
        }
        addr = addr->ifa_next;
    }

    freeifaddrs(addrs);

    if (!protocolAddr || !hardwareAddr)
    {
        LOG_D(TAG, "not get ifaddrs");
        return -1;
    }
    memcpy(hardwareAddr, LLADDR(&hardwareDl), hardwareDl.sdl_alen);
    return 0;
}

发送 ARP 报文

void arp(const char *host) {
    /* 获取目标机器的IP地址 */
    sockaddr_in targetaddr = getsockaddrbyhost(host);
    LOG_D(TAG, "target: %s", inet_ntoa(targetaddr.sin_addr));

    /* 获取本机的IP地址和MAC地址 */
    struct sockaddr_in protocolAddr;
    struct sockaddr_dl hardwarAddr;
    u_char senderHardwareAddress[ETHER_ADDR_LEN];
    if (getAddrs(&protocolAddr, senderHardwareAddress) < 0) {
        perror("[getAddrs]");
        exit(1);
    }

    /* ether_header: 14, arp_header: 28 */
    int etherSize = 14;
    int arpSize = 28;
    int packSize = etherSize + arpSize;
    char buf[packSize];
    bzero(buf, sizeof(buf));

    /* 填充以太网头部 */
    ether_header_t *eaddr = (ether_header_t *)buf;
    static const u_char etherBroadcast[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
    // 目标MAC地址设为广播地址
    memcpy(eaddr->ether_dhost, etherBroadcast, 6);
    // 帧类型设为 ARP
    eaddr->ether_type = htons(ETHERTYPE_ARP);

    /* 填充 ARP 请求 */
    struct arphdr *arphdr = (struct arphdr *)(buf + etherSize);
    // 硬件类型
    arphdr->ar_hrd = htons(ARPHRD_ETHER);
    // 协议类型
    arphdr->ar_pro = htons(ETHERTYPE_IP);
    // 硬件地址长度
    arphdr->ar_hln = sizeof(senderHardwareAddress);
    // 协议地址长度
    arphdr->ar_pln = sizeof(targetaddr.sin_addr);
    // 操作码 ARPOP_REQUEST 表示请求
    arphdr->ar_op = htons(ARPOP_REQUEST);
    int offset = sizeof(arphdr->ar_hrd) +
                 sizeof(arphdr->ar_pro) +
                 sizeof(arphdr->ar_op) +
                 sizeof(arphdr->ar_hln) +
                 sizeof(arphdr->ar_pln) + etherSize;
    // 源硬件地址
    memcpy(buf + offset, senderHardwareAddress, ETHER_ADDR_LEN);
    offset += ETHER_ADDR_LEN;
    // 源协议地址
    memcpy(buf + offset, &(protocolAddr.sin_addr), 4);
    offset += 4;
    // 目标硬件地址
    memset(buf + offset, 0, ETHER_ADDR_LEN);
    offset += ETHER_ADDR_LEN;
    // 目标协议地址
    memcpy(buf + offset, &(targetaddr.sin_addr), 4);
    /* 输出 ARP 请求 */
    outputArp(arphdr);

    /* 打开 BPF 设备并设置 */
    int bfd = openBpf();
    if (bfd < 0) {
        LOG_D(TAG, "[openBpf] failed");
        exit(1);
    }
    setupBpf(bfd, "en0");

    /* 写入数据 */
    ssize_t writed = write(bfd, buf, packSize);
    if (writed < 0) {
        perror("writev failed.");
    } else {
        LOG_D(TAG, "writed %d", writed);
        /* 写入成功之后读取数据 */
        readBpf(bfd);
    }

    close(bfd);
}

读取 ARP 报文

void readBpf(int fd) {
    int bufSize;
    /* Returns the required buffer length for reads on bpf files */
    if (ioctl(fd, BIOCGBLEN, &bufSize) < 0) {
        perror("BIOCGBLEN failed: ");
        exit(1);
    }
    LOG_D(TAG, "BIO Buffer: %d", bufSize);
    char re[bufSize];

    int finish = 1;
    while (finish) {
        /* 从 BPF 设备中读取数据 */
        ssize_t readed = read(fd, re, bufSize);
        if (readed < 0) {
            perror("read failed.");
            break;
        }
        else if (readed == 0) {
            LOG_D(TAG, "read end.");
            break;
        }
        LOG_D(TAG, "read %d bytes data.", readed);

        /* 接收的数据的头部是 bpf_hdr */
        const struct bpf_hdr *bpfHeader = (struct bpf_hdr *)re;
        LOG_D(TAG, "bpf header tstamp: %", bpfHeader->bh_tstamp);
        LOG_D(TAG, "bpf header len: %d", bpfHeader->bh_hdrlen);
        LOG_D(TAG, "bpf header data len: %d", bpfHeader->bh_datalen);
        LOG_D(TAG, "bpf header cap len: %d", bpfHeader->bh_caplen);
        /* 从 re 中取出以太网头部 */
        ether_header_t *eaddr = (ether_header_t *)(re + bpfHeader->bh_hdrlen);

        u_short etherType = ntohs(eaddr->ether_type);
        if (etherType == ETHERTYPE_ARP) {
            LOG_D(TAG, "Received ARP");
            /* 从 re 中取出ARP数据 */
            const struct arphdr *arp = (struct arphdr *)(re + bpfHeader->bh_hdrlen + sizeof(ether_header_t));
            /* 由于会收到很多局域网中其他设备发出的 ARP 请求, 所以只接收第一次的 Reply, 表示是对我们发出的 Request 的响应. 更严谨的应该根据 Reply 包中的目标ip地址和目标mac地址是不是我们的地址来过滤 */
            if (arp->ar_op == ntohs(ARPOP_REPLY)) {
                LOG_D(TAG, "Received ARP Reply");
                outputArp(arp);
                finish = 0;
            }
        }
    }
}

结果

arp 192.168.31.1

target: 192.168.31.1
Hardware type: 1
Protocol type: 2048
Opereation code: 1
Hardware address len: 6
Protocol address len: 4
Source hardware address: 0x88000000:0xe9000000:0xfe000000:0x53000000:0xed000000:0x16000000
Source ip address: 192.168.31.77
Dest hardware address: 0:0:0:0:0:0
Dest ip address: 192.168.31.1
writed 42
BIO Buffer: 4096
Received ARP
Received ARP
Received ARP Reply
Hardware type: 1
Protocol type: 2048
Opereation code: 2
Hardware address len: 6
Protocol address len: 4
Source hardware address: 0x28000000:0x6c000000:0x7000000:0x3c000000:0xca000000:0x8d000000
Source ip address: 192.168.31.1
Dest hardware address: 0x88000000:0xe9000000:0xfe000000:0x53000000:0xed000000:0x16000000
Dest ip address: 192.168.31.77
Request Reply

完整源码

https://github.com/stefanJi/NetUtitily/blob/master/src/arp.cpp

原文地址:https://www.cnblogs.com/jiy-for-you/p/12217394.html

时间: 2024-10-15 07:41:18

实现Arp报文发送和接收的相关文章

ARP报文发送的可视化实现

一.安装VS2013,下载wpdpack,为VS2010配置WinpCap环境: ⑴首先在View中选择Property Manager,然后展开工程,再展开Debug|Win32 ,接着右击 Mircrosoft.Cpp.Win32.user选择Properties(此处设置全局有效) ⑵之后分三步: ①设置环境目录 在VC++ Directiories中 Include Directories和Library Directories中添加路径. 假如将wpdpack放到c盘.则: Inclu

ARP报文与arp_ignore arp_announce

背景: (1)根据缺省的TCP/IP协议栈处理,响应报文的源地址等于请求报文的目的IP. (2)关于ARP表: IP.MAC.网络接口的映射表:列表中的IP都属于本设备所在的网段,发送/转发非本地网段时候是通过网关,所以只需要使用网关对应的MAC即可. (3)ARP条目更新的条件(满足任意一个即可): 1.收到ARP request:目的MAC是广播,且Target IP是本机上的IP:(这里说Target IP是为了与目的IP区分) 2.收到ARP reply:目的MAC是广播或本机MAC,且

stm32之CAN发送、接收详解

CAN接收报文并过滤之标识符过滤:(重点.难点) 在CAN协议里,报文的标识符不代表节点的地址,而是跟报文的内容相关的.因此,发送者以广播的形式把报文发送给所有的接收者.节点在接收报文时-根据标识符的值-决定软件是否需要该报文:如果需要,就拷贝到SRAM里:如果不需要,报文就被丢弃且无需软件的干预. 为满足这一需求,bxCAN为应用程序提供了14个位宽可变的.可配置的过滤器组(13~0),以便只接收那些软件需要的报文.硬件过滤的做法节省了CPU开销,否则就必须由软件过滤从而占用一定的CPU开销.

ARP报文格式

ARP/RARP报文格式 地址解析协议ARP(Address Resolution Protocol)是用来将IP地址解析为MAC地址的协议. 报文格式 字段 长度(bit) 含义 Ethernet Address of destination 48比特 目的以太网地址.发送ARP请求时,为广播的MAC地址,0xFF.FF.FF.FF.FF.FF. Ethernet Address of sender 48比特 源以太网地址. Frame Type 16比特 表示后面数据的类型.对于ARP请求或

XMPP系列(四)---发送和接收文字消息,获取历史消息功能

今天开始做到最主要的功能发送和接收消息.获取本地历史数据. 先上到目前为止的效果图:              首先是要在XMPPFramework.h中引入数据存储模块: //聊天记录模块的导入 #import "XMPPMessageArchiving.h" #import "XMPPMessageArchivingCoreDataStorage.h" #import "XMPPMessageArchiving_Contact_CoreDataObje

microduino实现红外线发送与接收

一.目标:使用microduino套件实现红外线的发送与接收 二.材料:(1)模块:Microduino Core/Core+ (核心板),Microduino-USBTTL(下载程序) (2)传感器:红外线发射器,红外线接收器 (3)其它设备:Micro-USB线,面包板,导线 - 红外线发射器使用说明: 目前的红外发射器有两种规格,两脚的(左图)和三脚的(右图) 两脚的结构和普通LED管相似,长引脚接高电平,短引脚接低电平 三脚的连接情况分别是:5V,OUT,GND,其中OUT是指定的输出端

JavaWeb学习总结(五十一)——邮件的发送与接收原理

一. 邮件开发涉及到的一些基本概念 1.1.邮件服务器和电子邮箱 要在Internet上提供电子邮件功能,必须有专门的电子邮件服务器.例如现在Internet很多提供邮件服务的厂商:sina.sohu.163等等他们都有自己的邮件服务器. 这些邮件服务器类似于现实生活中的邮局,它主要负责接收用户投递过来的邮件,并把邮件投递到邮件接收者的电子邮箱中. 电子邮箱(E-Mail地址)的获得需要在邮件服务器上进行申请,确切地说,电子邮箱其实就是用户在邮件服务器上申请的一个账户,用户在邮件服务器上申请了一

DICOM医学图像处理:DIMSE消息发送与接收“大同小异”之DCMTK fo-dicom mDCM

背景: 从DICOM网络传输一文开始,相继介绍了C-ECHO.C-FIND.C-STORE.C-MOVE等DIMSE-C服务的简单实现,博文中的代码给出的实例都是基于fo-dicom库来实现的,原因只有一个:基于C#的fo-dicom库具有高封装性.对于初学者来说实现大多数的DIMSE-C.DIMSE-N服务几乎都是"傻瓜式"操作--构造C-XXX-RQ.N-XXX-RQ然后绑定相应的OnResponseReceived处理函数即可.本博文希望在前几篇预热的基础上,对比DCMTK.fo

linux网卡数据流 发送与接收

通常网卡驱动代码量比较大,但是都离不开发送与接收,掌握好骨干也就好理解与调试了. 数据发送 : 发送函数:xxx_start_xmit() 上层封装好了sk_buff丢进kernel后,buf存储在skb->data中,通 过 xxx_start_xmit(),发送出去. 下面的实例是通过写入某个设备节点发送出去,当然这个设备肯定是串口或者sdio等传输设备, 也可以直接调用写入函数将buf送出去. 比如sdio的写入: sdio_memcpy_toio(xxx->func, addr, bu