ioctl/ioctl读取网卡信息

概述

通过ioctl可以实现open、close、read、write之外的操作,本文主要描述通过ioctl获取网卡信息的方式。

ioctl函数的原型:

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

获取网卡列清单 SIOCGIFCONF

重要结构体:

struct ifconf  {
    int     ifc_len;
    union {
        char __user *ifcu_buf;
        struct ifreq __user *ifcu_req;
    } ifc_ifcu;
};

#define ifc_buf ifc_ifcu.ifcu_buf               /* buffer address       */
#define ifc_req ifc_ifcu.ifcu_req               /* array of structures  */

其中 ifc_len 表示联合体 ifc_ifcu 的字节数,后面两个宏定义为了方便访、直观地访问联合体的成员。

获取网卡清单的简化版本代码(不考虑各种异常情况)

//  获取系统的网卡清单
static int do_get_interface_list()
{
#define MAX_IFCS 64 // 最多获取 64 张网卡信息
    int sockfd = -1;
    struct ifconf ifc;
    struct ifreq ifreqlist[MAX_IFCS]; //   这个结构体后面有解释
    int ifreqcount = 0;
    int ifreqidx = 0;

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

    //  ifc 用于保存获取到的网卡信息
    //  入参时 ifc_len 需要 ifc_req 表明最多可以保存多少张网卡信息(字节数)
    //  函数 ioctl 返回时, ifc_len 指明了实际读取到的网卡的数量(字节数)
    ifc.ifc_len = sizeof(ifreqlist);
    ifc.ifc_req = ifreqlist;

    //  读取网卡信息到 ifc 中
    ioctl(sockfd, SIOCGIFCONF, &ifc);
    close(sockfd);

    //  结果输出
    //  计算读取到的网卡数量,字节数除以每一张网卡信息结构的长度
    ifreqcount = ifc.ifc_len / sizeof(struct ifreq);
    printf("总共 %d 张网卡,清单如下:", ifreqcount);
    for (ifreqidx = 0; ifreqidx < ifreqcount; ifreqidx++)
    {
        //  网卡名称
        printf("\nName: %s\t", ifreqlist[ifreqidx].ifr_name);
        if (ifreqlist[ifreqidx].ifr_addr.sa_family == AF_INET)
        {
            //  通过 SIOCGIFCONF 可以读取到的基本信息是 struct ifreq 的第二个联合体成员的第一种情况 struct  sockaddr ifru_addr;
            printf("Ip Address %s", inet_ntoa(((struct sockaddr_in *)&ifreqlist[ifreqidx].ifr_addr)->sin_addr));
        }
    }
    printf("\n");

    return 0;
}

这份代码在我的虚机上执行后输出如下:

总共 4 张网卡,清单如下:
Name: lo    Ip Address 127.0.0.1
Name: ens33    Ip Address 172.18.145.128
Name: ens34    Ip Address 172.18.89.127
Name: virbr0    Ip Address 192.168.122.1

结构体 struct ifreq

struct ifreq {
#define IFHWADDRLEN     6
    union
    {
            char    ifrn_name[IFNAMSIZ];            /* if name, e.g. "en0" */
    } ifr_ifrn;

    union {
            struct  sockaddr ifru_addr;        //    本小节中,只需要关注这个就够了,其它的后面继续用到。
            struct  sockaddr ifru_dstaddr;
            struct  sockaddr ifru_broadaddr;
            struct  sockaddr ifru_netmask;
            struct  sockaddr ifru_hwaddr;
            short   ifru_flags;
            int     ifru_ivalue;
            int     ifru_mtu;
            struct  ifmap ifru_map;
            char    ifru_slave[IFNAMSIZ];   /* Just fits the size */
            char    ifru_newname[IFNAMSIZ];
            void __user *   ifru_data;
            struct  if_settings ifru_settings;
    } ifr_ifru;
};

#define ifr_name        ifr_ifrn.ifrn_name      /* interface name       */
#define ifr_hwaddr      ifr_ifru.ifru_hwaddr    /* MAC address          */
#define ifr_addr        ifr_ifru.ifru_addr      /* address              */
#define ifr_dstaddr     ifr_ifru.ifru_dstaddr   /* other end of p-p lnk */
#define ifr_broadaddr   ifr_ifru.ifru_broadaddr /* broadcast address    */
#define ifr_netmask     ifr_ifru.ifru_netmask   /* interface net mask   */
#define ifr_flags       ifr_ifru.ifru_flags     /* flags                */
#define ifr_metric      ifr_ifru.ifru_ivalue    /* metric               */
#define ifr_mtu         ifr_ifru.ifru_mtu       /* mtu                  */
#define ifr_map         ifr_ifru.ifru_map       /* device map           */
#define ifr_slave       ifr_ifru.ifru_slave     /* slave device         */
#define ifr_data        ifr_ifru.ifru_data      /* for use by interface */
#define ifr_ifindex     ifr_ifru.ifru_ivalue    /* interface index      */
#define ifr_bandwidth   ifr_ifru.ifru_ivalue    /* link bandwidth       */
#define ifr_qlen        ifr_ifru.ifru_ivalue    /* Queue length         */
#define ifr_newname     ifr_ifru.ifru_newname   /* New name             */
#define ifr_settings    ifr_ifru.ifru_settings  /* Device/proto settings*/

struct ifreq 是一个神奇的结构体,除了第一个成员表示 interface 的名称(比如eth0、enp1s0)之外,第二个成员就是一个箩筐,可以选择性的表示各种含义。

正如代码注释中所言,本小节中,我们利用 SIOCGIFCONF 读取到的 struct ifreq ,除了网卡名称 ifr_name 之外,就是网卡的IP地址 ifr_addr ,变量类型为 struct sockaddr , 当 sa_family 为 AF_INET 时 sa_data 的 2~5 个字节表示 IPV4 地址,可以对比以下两个结构体(它们之间可以相互强制转换的):

struct sockaddr {
    sa_family_t     sa_family;      /* address family, AF_xxx       */
    char            sa_data[14];    /* 14 bytes of protocol address */
};

#define __SOCK_SIZE__   16              /* sizeof(struct sockaddr)      */
struct sockaddr_in {
  __kernel_sa_family_t  sin_family;     /* Address family               */
  __be16                sin_port;       /* Port number                  */
  struct in_addr        sin_addr;       /* Internet address             */

  /* Pad to size of `struct sockaddr'. */
  unsigned char         __pad[__SOCK_SIZE__ - sizeof(short int) -
                        sizeof(unsigned short int) - sizeof(struct in_addr)];
};

获取网卡各种信息

利用 SIOCGIFCONF 获取到了网卡清单,但是每张网卡实际上就获取到了两个信息(名称、IP地址),如果要获取更多的信息,就需要额外再调用ioctl了,下面逐一说明。

简化版本的代码

//  获取系统特定网卡的基本信息,包括物理地址、IPv4地址、子网掩码、广播地址等
static int do_get_interface_infos(const char *dev)
{
    // const char *dev = "ens33";
    int sockfd = -1;
    struct ifreq ifr;
    unsigned char mac[6] = {0};

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

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

    printf("\n\t网卡%s的信息如下:\n", dev);

    //  获取网卡物理地址
    ioctl(sockfd, SIOCGIFHWADDR, &ifr);
    memcpy(mac, ifr.ifr_hwaddr.sa_data, 6); //  方便打印结果
    printf("\tSIOCGIFHWADDR Hardware address is %02x:%02x:%02x:%02x:%02x:%02x\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

    //  获取网卡的IPV4地址
    ioctl(sockfd, SIOCGIFADDR, &ifr);
    printf("\tSIOCGIFADDR IP address is %s\n", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr));

    //  在pppoe的网卡中,这个可以获取到pppoe的对端地址
    ioctl(sockfd, SIOCGIFDSTADDR, &ifr);
    printf("\tSIOCGIFDSTADDR IP address is %s\n", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr));

    //  获取网卡的子网掩码地址
    ioctl(sockfd, SIOCGIFNETMASK, &ifr);
    printf("\tSIOCGIFNETMASK Netmask address is %s\n", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_netmask)->sin_addr));

    //  获取网卡的广播地址
    ioctl(sockfd, SIOCGIFBRDADDR, &ifr);
    printf("\tSIOCGIFBRDADDR Broadcast address is %s\n", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_broadaddr)->sin_addr));

    //  获取网卡的MTU
    ioctl(sockfd, SIOCGIFMTU, &ifr);
    printf("\tSIOCGIFMTU MTU is %d\n", ifr.ifr_mtu);

    close(sockfd);
    return 0;
}

获取网卡的状态 SIOCGIFFLAGS

网卡有各种状态,比如up、down、runing,以及是否处于混杂模式等,通过 SIOCGIFFLAGS 可以获取到这些状态,下面是简化版本的代码。

static int do_get_interface_flags(const char *dev)
{
    int sockfd = -1;
    struct ifreq ifr;

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

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

    //  获取网卡状态
    if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) == -1)
    {
        printf("Failed to exec ioctl(SIOCGIFFLAGS): %m\n");
    }
    else
    {
        char printbuf[256] = {0};
        int printlen = 0;
        short flags = ifr.ifr_flags;

        printlen += sprintf(&printbuf[printlen], "Interface %s's flags: ", dev);

        //  是否处于 UP 状态
        if (flags & IFF_UP)
        {
            printlen += sprintf(&printbuf[printlen], "%s", "UP, ");
        }
        else
        {
            printlen += sprintf(&printbuf[printlen], "%s", "Down, ");
        }

        if (flags & IFF_BROADCAST)
        {
            printlen += sprintf(&printbuf[printlen], "%s", "Broadcast, ");
        }

        if (flags & IFF_RUNNING)
        {
            printlen += sprintf(&printbuf[printlen], "%s", "Runing, ");
        }

        //  more...

        //  是否支持混杂模式
        if (flags & IFF_PROMISC)
        {
            printlen += sprintf(&printbuf[printlen], "%s", "Promisc, ");
        }
        else
        {
            printlen += sprintf(&printbuf[printlen], "%s", "Not Promisc, ");
        }

        printlen += sprintf(&printbuf[printlen], "%s", "and more flags.\n");

        printf(printbuf);
    }

    close(sockfd);

    return 0;
}

这里列举了所有的 flags:

enum net_device_flags {
/* for compatibility with glibc net/if.h */
#if __UAPI_DEF_IF_NET_DEVICE_FLAGS
        IFF_UP                          = 1<<0,  /* sysfs */
        IFF_BROADCAST                   = 1<<1,  /* volatile */
        IFF_DEBUG                       = 1<<2,  /* sysfs */
        IFF_LOOPBACK                    = 1<<3,  /* volatile */
        IFF_POINTOPOINT                 = 1<<4,  /* volatile */
        IFF_NOTRAILERS                  = 1<<5,  /* sysfs */
        IFF_RUNNING                     = 1<<6,  /* volatile */
        IFF_NOARP                       = 1<<7,  /* sysfs */
        IFF_PROMISC                     = 1<<8,  /* sysfs */
        IFF_ALLMULTI                    = 1<<9,  /* sysfs */
        IFF_MASTER                      = 1<<10, /* volatile */
        IFF_SLAVE                       = 1<<11, /* volatile */
        IFF_MULTICAST                   = 1<<12, /* sysfs */
        IFF_PORTSEL                     = 1<<13, /* sysfs */
        IFF_AUTOMEDIA                   = 1<<14, /* sysfs */
        IFF_DYNAMIC                     = 1<<15, /* sysfs */
#endif /* __UAPI_DEF_IF_NET_DEVICE_FLAGS */
#if __UAPI_DEF_IF_NET_DEVICE_FLAGS_LOWER_UP_DORMANT_ECHO
        IFF_LOWER_UP                    = 1<<16, /* volatile */
        IFF_DORMANT                     = 1<<17, /* volatile */
        IFF_ECHO                        = 1<<18, /* volatile */
#endif /* __UAPI_DEF_IF_NET_DEVICE_FLAGS_LOWER_UP_DORMANT_ECHO */
};

设置各种参数

ioctl 除了可以获取网卡相关的信息外,还可以设置网卡的参数,包括ip地址、子网掩码、mtu等等,下面以设置ip地址为例进行说明。

SIOCSIFADDR 设置网卡 ipv4 地址

static int do_set_interface_ipaddr(const char *dev, const char *newipaddr)
{
    int sockfd = -1;
    struct ifreq ifr;
    struct sockaddr_in *psockaddr = (struct sockaddr_in *)&ifr.ifr_addr;

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

    //  先清空配置
    memset(&ifr, 0, sizeof(ifr));

    //  明确要设置哪一张网卡的信息,用网卡的名称来指示
    strcpy(ifr.ifr_name, dev);

    //  设置 ipv4 的地址, 即时生效的
    psockaddr->sin_family = AF_INET;
    psockaddr->sin_port = htons(0);
    psockaddr->sin_addr.s_addr = inet_addr(newipaddr);

    //  设置网卡地址
    if (ioctl(sockfd, SIOCSIFADDR, &ifr) < 0)
    {
        printf("Failed to ioctl(SIOCSIFADDR): %m\n");
    }

    close(sockfd);

    return 0;
}