實現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)。—— 地址解析協議

報文格式

ARP 報文格式

以太網首部: 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

ls /dev/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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM