ioctl/ioctl和arp缓存

概述

socket编码通讯的时候,目的地址一般都是域名或者对端的IP地址,如果是域名先有一个域名解析的过程以获取到对端的IP地址,在以太网通讯中,IP属于三层通讯,往下的第二层是MAC地址,如果是局域网通讯就需要根据IP地址查询到MAC地址,填充第二层的数据包。
arp缓存就是为了方便快速查找mac地址而设计的,通常有以太网协议栈自己维护的,所以并不需要我们主动参与。

系统提供的命令和接口

Linux系统提供了一些操作命令和接口文件,方便用户查看、操作arp缓存,命令包括arp、ip neigh等,还有接口文件 /proc/net/arp,简单说明如下。

arp命令操作

Linux中用命令 arp 来实现arp缓存的查询、和增删操作,命令具体使用方式通过 arp -h 或者 man arp,可以查看,比如:

  1. arp -an 可以查看当前 arp 缓存的记录
  2. arp -i 可以查看指定网卡的 arp 缓存的记录
  3. arp -d 192.168.1.x 可以删除一条记录
  4. arp -s 192.168.1.x aa:bb:cc:ee:dd:11 temp 可以新增一条临时记录,当然arp命令会判断host(192.168.1.x)是否合理(可以匹配到某一张网卡的网段)。

ip neigh 命令操作

事实上 arp 命令已经被标记为 obsolete , 推荐使用 ip neigh 命令来代替 arp 命令,虽然我觉得 arp 命令更加直观简单。

同样可以用 ip neigh help 来查看使用方法:

# ip neigh help
Usage: ip neigh { add | del | change | replace }
        { ADDR [ lladdr LLADDR ] [ nud STATE ] | proxy ADDR } [ dev DEV ]
        [ router ] [ extern_learn ] [ protocol PROTO ]

    ip neigh { show | flush } [ proxy ] [ to PREFIX ] [ dev DEV ] [ nud STATE ]
                  [ vrf NAME ]

STATE := { permanent | noarp | stale | reachable | none |
           incomplete | delay | probe | failed }

proc 文件查看

另外,还可以通过文件查看当前缓存情况:

# cat /proc/net/arp 
IP address       HW type     Flags       HW address            Mask     Device
172.18.89.100    0x1         0x2         aa:bb:cc:ee:dd:11     *        ens34
172.18.89.1      0x1         0x2         00:50:56:c0:00:01     *        ens34
172.18.145.254   0x1         0x2         00:50:56:e3:75:35     *        ens33
172.18.145.2     0x1         0x2         00:50:56:ea:60:d5     *        ens33

ioctl 代码支持 arp 的相关操作

实际我们代码中,如果要查看、修改 arp 缓存,我们可以利用system或者popen,加上上面的命令来完成编码,但是形式上有点丑陋。

另外,也可以通过 ioctl 函数来完成对 arp 缓存的操作,包括查询、增加(修改)和删除,下面逐一说明。

先看几个结构体和宏定义:

#define SIOCDARP        0x8953          /* delete ARP table entry       */
#define SIOCGARP        0x8954          /* get ARP table entry          */
#define SIOCSARP        0x8955          /* set ARP table entry          */

#define ATF_COM         0x02            /* completed entry (ha valid)   */
#define ATF_PERM        0x04            /* permanent entry              */
#define ATF_PUBL        0x08            /* publish entry                */
#define ATF_USETRAILERS 0x10            /* has requested trailers       */
#define ATF_NETMASK     0x20            /* want to use a netmask (only for proxy entries) */
#define ATF_DONTPUB     0x40            /* don't answer this addresses  */

struct arpreq {
        struct sockaddr arp_pa;         /* protocol address              */
        struct sockaddr arp_ha;         /* hardware address              */
        int             arp_flags;      /* flags                         */
        struct sockaddr arp_netmask;    /* netmask (only for proxy arps) */
        char            arp_dev[IFNAMSIZ];
};

查询

ioctl 可以根据ip地址查询mac地址(可惜不能根据 mac 查询 ip):

static int do_test_ioctol_case0()
{
    const char *dev = "enp4s0";
    const char *host = "192.168.3.101";
    int sockfd = -1;
    struct arpreq arpreqest;
    struct sockaddr_in *arp_pa = (struct sockaddr_in *)&arpreqest.arp_pa;
    unsigned char *mac = NULL;

    //  一个必要的 socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    //  明确要获取哪一张网卡的信息,用网卡的名称来指示
    memset(&arpreqest, 0, sizeof(arpreqest));
    strncpy(arpreqest.arp_dev, dev, IFNAMSIZ);

    arp_pa->sin_family = AF_INET;
    arp_pa->sin_addr.s_addr = inet_addr(host);

    if(ioctl(sockfd, SIOCGARP, &arpreqest) < 0)
    {
        if(ENXIO == errno)
        {
            //  设备名称不对,设备不存在
            printf("Failed to find host(%s)\n", host);
        }
        else if(ENODEV == errno)
        {
            //  找不到这条记录
            printf("Failed to find dev(%s)\n", dev);
        }
        else
        {
            //  其他错误
            printf("Failed to exec(SIOCGARP): %m\n");
        }

        goto out;
    }

    if(arpreqest.arp_flags & ATF_COM)
    {
        //  有完整的记录,就表示有mac地址
        mac = (unsigned char *)&arpreqest.arp_ha.sa_data[0];
        printf("Host(%s)'s hardware address is: %02x:%02x:%02x:%02x:%02x:%02x", host, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }
    if(arpreqest.arp_flags & ATF_PERM)
    {
        printf("\t%s", "Permanent");
    }
    if(arpreqest.arp_flags & ATF_PUBL)
    {
        printf("\t%s", "Publish");
    }
    if(arpreqest.arp_flags & ATF_USETRAILERS)
    {
        printf("\t%s", "Has requested trailers");
    }
    if(arpreqest.arp_flags & ATF_NETMASK)
    {
        printf("\t%s", "Use a netmask");
    }
    if(arpreqest.arp_flags & ATF_DONTPUB)
    {
        printf("\t%s", "Don's answer this addresses");
    }

    printf("\n");

out:
    close(sockfd);

    return 0;
}

增加

增加记录的代码和查询的比较类似,需要注意的是查询只写 ip 地址,增加需要填写 ip和mac 地址:

static int do_test_ioctol_case1()
{
    const char *dev = "enp4s0";
    const char *host = "192.168.3.102";
    const char *macstr = "11:22:33:aa:bb:ce";

    int sockfd = -1;
    struct arpreq arpreqest;
    struct sockaddr_in *arp_pa = (struct sockaddr_in *)&arpreqest.arp_pa;
    unsigned char mac[6] = {0};

    //  一个必要的 socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    //  明确要获取哪一张网卡的信息,用网卡的名称来指示
    memset(&arpreqest, 0, sizeof(arpreqest));

    strncpy(arpreqest.arp_dev, dev, IFNAMSIZ);
    // arpreqest.arp_flags = ATF_PERM | ATF_COM;
    arpreqest.arp_flags = ATF_COM;

    arp_pa->sin_family = AF_INET;
    arp_pa->sin_addr.s_addr = inet_addr(host);

    sscanf(macstr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]);
    memcpy((unsigned char*)arpreqest.arp_ha.sa_data, mac, 6);
    // get_hw_addr((unsigned char*)arpreqest.arp_ha.sa_data, mac);

    if(ioctl(sockfd, SIOCSARP, &arpreqest) < 0)
    {
        if(ENXIO == errno)
        {
            //  设备名称不对,设备不存在
            printf("Failed to find host(%s)\n", host);
        }
        else if(ENODEV == errno)
        {
            //  找不到这条记录
            printf("Failed to find dev(%s)\n", dev);
        }
        else
        {
            //  其他错误
            printf("Failed to exec(SIOCGARP): %m\n");
        }

        goto out;
    }

    printf("\n");

out:
    close(sockfd);

    return 0;
}

删除

删除的代码可以和增加的代码几乎完全一样,ioctl 的第二个参数不一样而已:

static int do_test_ioctol_case2()
{
    const char *dev = "enp4s0";
    const char *host = "192.168.3.102";
    // const char *macstr = "11:22:33:aa:bb:ce";

    int sockfd = -1;
    struct arpreq arpreqest;
    struct sockaddr_in *arp_pa = (struct sockaddr_in *)&arpreqest.arp_pa;
    // unsigned char mac[6] = {0};

    //  一个必要的 socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    //  明确要获取哪一张网卡的信息,用网卡的名称来指示
    memset(&arpreqest, 0, sizeof(arpreqest));

    strncpy(arpreqest.arp_dev, dev, IFNAMSIZ);
    // arpreqest.arp_flags = ATF_PERM | ATF_COM;
    // arpreqest.arp_flags = ATF_COM;

    arp_pa->sin_family = AF_INET;
    arp_pa->sin_addr.s_addr = inet_addr(host);

    //  删除的时候,可以不设置mac地址,只设置IP地址。但是不能确ip地址。
    // sscanf(macstr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]);
    // memcpy((unsigned char*)arpreqest.arp_ha.sa_data, mac, 6);

    if(ioctl(sockfd, SIOCDARP, &arpreqest) < 0)
    {
        if(ENXIO == errno)
        {
            //  设备名称不对,设备不存在
            printf("Failed to find host(%s)\n", host);
        }
        else if(ENODEV == errno)
        {
            //  找不到这条记录
            printf("Failed to find dev(%s)\n", dev);
        }
        else
        {
            //  其他错误
            printf("Failed to exec(SIOCGARP): %m\n");
        }

        goto out;
    }

    printf("\n");

out:
    close(sockfd);

    return 0;
}

重要结构体回顾:

struct arpreq

struct arpreq {
        struct sockaddr arp_pa;         /* protocol address              */
        struct sockaddr arp_ha;         /* hardware address              */
        int             arp_flags;      /* flags                         */
        struct sockaddr arp_netmask;    /* netmask (only for proxy arps) */
        char            arp_dev[IFNAMSIZ];
};
  1. 这个结构体中,不管哪个操作都需要正确设置 arp_dev。

  2. struct sockaddr arp_pa 不管什么操作,这个用于设置IP地址,需要注意的是记得设置协议类型(比如IPV4用AT_INET)。

  3. struct sockaddr arp_ha 设置操作,设置mac地址;查询操作,返回的mac地址保存于这个结构体中;删除操作,该字段不起作用。

  4. int arp_flags 有以下几个可选项

    #define ATF_COM         0x02            /* completed entry (ha valid)   */
    #define ATF_PERM        0x04            /* permanent entry              */
    #define ATF_PUBL        0x08            /* publish entry                */
    #define ATF_USETRAILERS 0x10            /* has requested trailers       */
    #define ATF_NETMASK     0x20            /* want to use a netmask (only for proxy entries) */
    #define ATF_DONTPUB     0x40            /* don't answer this addresses  */