这里介绍可以列举网卡设备的三种方式:

  • ioctl
    通过ioctl系统调用的SIOCGIFCONF请求获取网卡名,通过SIOCGIFINDEX请求获取网卡唯一索引。
  • getifaddrs
    这个函数可以返回网卡的信息和地址,区分协议族,也就是每个网卡名在返回数据中可能存在多次。这个函数的实现上依赖rtnetlink。
  • rtnetlink
    通过netlink与内核交互,功能强大且多样,获取网卡信息只是其非常小的一个功能,参考iproute2工具包的代码实现。

ioctl

linux支持一些标准ioctl系统调用配置网卡设备。可以通过任意socket文件描述符使用这种方式以配置网卡设备,与socket的协议族和类型无关。以下内容参考man netdevice

通过ioctl操作网卡设备需要认识两个数据结构。

更多内容参考 man page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct ifreq {
char ifr_name[IFNAMSIZ]; /* Interface name */
union {
struct sockaddr ifr_addr;
struct sockaddr ifr_dstaddr;
struct sockaddr ifr_broadaddr;
struct sockaddr ifr_netmask;
struct sockaddr ifr_hwaddr;
short ifr_flags;
int ifr_ifindex;
int ifr_metric;
int ifr_mtu;
struct ifmap ifr_map;
char ifr_slave[IFNAMSIZ];
char ifr_newname[IFNAMSIZ];
char *ifr_data;
};
};

/*
ifreq结构是调用参数的主要结构,大部分请求填充ifr_name以标识需要操作的网卡。
其他成员是一个联合体共享内存空间,每次只有一个有意义,通过请求类型决定使用的成员。
特殊情况是SIOCGIFNAME请求,这个请求填充ifr_index,由内核填充ifr_name并返回。
*/

struct ifconf {
int ifc_len; /* size of buffer */
union {
char *ifc_buf; /* buffer address */
struct ifreq *ifc_req; /* array of structures */
};
};

/*
ifconf结构用在SIOCGIFCONF请求,这个请求的作用是获取网卡地址的列表,同时会填充网卡名。
ifc_buf和ifc_req实际上指向的是同一个地址,只是省去了类型转换。
ifc_len标识了可用的ifc_buf的字节长度,请求返回后ifc_len标识了内核填充的有效的字节长度。
SIOCGIFCONF请求的局限性在于,只能够获取具有ipv4地址的网卡。
*/

通过ioctl获取网卡名和唯一索引的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>

#include <sys/ioctl.h>
#include <net/if.h>

int main() {

int ret;
int fd;

struct ifconf ifc;
char buf[10240];
struct ifreq *ifr;

fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) {
printf("socket failed, %s\n", strerror(errno));
return -1;
}

bzero(buf, sizeof(buf));
ifc.ifc_len = sizeof(buf);
ifc.ifc_req= (void *)&buf;

ret = ioctl(fd, SIOCGIFCONF, &ifc);
if (ret == -1) {
printf("SIOCGIFCONF failed, %s\n", strerror(errno));
return -1;
}

if (ifc.ifc_len == sizeof(buf)) {
printf("buf overflow\n");
return -1;
}

ifr = ifc.ifc_req;
while((char *)ifr < buf + ifc.ifc_len) {

ret = ioctl(fd, SIOCGIFINDEX, ifr);
if (ret == -1) {
printf("SIOCGIFINDEX failed, %s\n", strerror(errno));
return -1;
}

printf("%s index %d\n", ifr->ifr_name, ifr->ifr_ifindex);
ifr ++;
}

close(fd);
return 0;
}

我的测试环境输出如下:

1
2
lo index 1
eno16780032 index 2

getifaddrs

这个函数使用上很简单,通过输入一个二级指针,函数内部会调用rtnetlink获取网卡数据并分配内存组织数据,将输入指针指向分配的内存,因此在使用结束后需要手动调用freeifaddrs以释放内存。参考man getifaddrs

函数原型及结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int getifaddrs(struct ifaddrs **ifap);

void freeifaddrs(struct ifaddrs *ifa);

struct ifaddrs {
struct ifaddrs *ifa_next; /* Next item in list */
char *ifa_name; /* Name of interface */
unsigned int ifa_flags; /* Flags from SIOCGIFFLAGS */
struct sockaddr *ifa_addr; /* Address of interface */
struct sockaddr *ifa_netmask; /* Netmask of interface */
union {
struct sockaddr *ifu_broadaddr;
/* Broadcast address of interface */
struct sockaddr *ifu_dstaddr;
/* Point-to-point destination address */
} ifa_ifu;
#define ifa_broadaddr ifa_ifu.ifu_broadaddr
#define ifa_dstaddr ifa_ifu.ifu_dstaddr
void *ifa_data; /* Address-specific data */
};

/*
ifa_next 填充数据是一个链表,通过ifa_next成员链接。
ifa_name 网卡名
ifa_addr 通用的网络地址类型,可以通过读取sa_family判断地址类型再转换为对应
结构体类型后读取详细信息,比如AF_INET对应sockaddr_in,AF_PACKET对应sockaddr_ll。
*/
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <errno.h>
#include <string.h>
#include <unistd.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <ifaddrs.h>

#include <sys/ioctl.h>
#include <net/if.h>

int main() {
struct ifaddrs *ifa;
struct ifaddrs *ifap;

int ret;
int fd;
struct ifreq ifr;

struct sockaddr *addr;

ret = getifaddrs(&ifa);
if (ret == -1) {
printf("getifaddrs failed, %s\n", strerror(errno));
return -1;
}
ifap = ifa;

fd = socket(AF_INET, SOCK_DGRAM,0);
if (fd == -1) {
printf("socket failed, %s\n", strerror(errno));
return -1;
}

while (ifap != NULL) {
addr = ifap->ifa_addr;


strcpy(ifr.ifr_name, ifap->ifa_name);
ret = ioctl(fd, SIOCGIFINDEX, &ifr);
if (ret == -1) {
printf("SIOCGIFINDEX failed, %s\n", strerror(errno));
return -1;
}

printf("%20s sa_family %4u, index %d\n", ifap->ifa_name, addr->sa_family, ifr.ifr_ifindex);

ifap = ifap->ifa_next;
}

close(fd);
freeifaddrs(ifa);
return 0;
}

我测测试环境输出:

1
2
3
4
5
6
7
8
         lo    sa_family   17, index 1
eno16780032 sa_family 17, index 2
eno33559296 sa_family 17, index 3
lo sa_family 2, index 1
eno16780032 sa_family 2, index 2
lo sa_family 10, index 1
eno16780032 sa_family 10, index 2
eno33559296 sa_family 10, index 3

rtnetlink是一种socket,是netlink的一个族,用于处理读写路由相关操作、网卡相关操作、以及其他操作,具体参考man 7 rtnetlink

netlink是一种socket,用于内核和用户空间交互,创建一个netlink socket的方式是netlink_socket = socket(AF_NETLINK, socket_type, netlink_family);,更多信息参考man 7 netlink

  • AF_NETLINK
    个人觉得更应该使用PF_NETLINK,表示使用netlink协议族。由于在实现上每个协议族仅对应一个地址族,这里PF_NETLINK值等于AF_NETLINK,因此两个在值上可以互换使用。
  • socket_type
    可以使用SOCK_RAW和SOCK_DGRAM,这两者在netlink协议使用上无任何区别。
  • netlink_family
    有一组可以取的值,使用NETLINK_ROUTE将创建一个rtnetlink。

每个netlink消息都包含一个或多个消息头和载荷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/* netlink消息头结构体 */

struct nlmsghdr {
__u32 nlmsg_len; /* Length of message including header. */
__u16 nlmsg_type; /* Type of message content. */
__u16 nlmsg_flags; /* Additional flags. */
__u32 nlmsg_seq; /* Sequence number. */
__u32 nlmsg_pid; /* Sender port ID. */
};

/* 对netlink消息的访问要求使用几个特定的宏,可在/usr/include/linux/netlink.h中查看其实现 */

/* Round the length of a netlink message up to align it properly.
将netlink消息向上对齐到一个合适的值。可以不直接使用这个宏。
*/
int NLMSG_ALIGN(size_t len);

/* Given the payload length, len, this macro returns the aligned length to store
in the nlmsg_len field of the nlmsghdr.
输入载荷的大小,返回可以填入nsmsg_len的大小(因为消息头需要做对齐)
*/
int NLMSG_LENGTH(size_t len);

/* Return the number of bytes that a netlink message with payload of len would occupy.
输入载荷的大小,返回整个netlink消息需要占据的空间大小(因为消息头和整个消息都需要做对齐)
*/
int NLMSG_SPACE(size_t len);

/* Return a pointer to the payload associated with the passed nlmsghdr.
输入消息头,返回载荷数据开始位置的指针(因为消息头需要对齐)
*/
void *NLMSG_DATA(struct nlmsghdr *nlh);

/* Get the next nlmsghdr in a multipart message. The caller must check if the current nlmsghdr
didn't have the NLMSG_DONE set—this function doesn't return NULL on end.
The len argument is an lvalue containing the remaining length of the message buffer.
This macro decrements it by the length of the message header.
输入当前消息头和记录剩余缓冲区长度的变量,返回下一个消息头的指针。(这里需要检查当前消息头的
nlmsg_type是不是NLMSG_DONE,如果是的话表示这个消息是结束消息,自身及后续都不再存在有效载荷。
输入的第二个变量将由此宏减去适当的值,不需要人为再修改该变量)
*/
struct nlmsghdr *NLMSG_NEXT(struct nlmsghdr *nlh, int len);

/* Return true if the netlink message is not truncated and is in a form suitable for parsing.
输入当前消息头和记录剩余缓冲区的长度,如果消息没有被截断将返回true表示可以被正确解析
*/
int NLMSG_OK(struct nlmsghdr *nlh, int len);

/* Return the length of the payload associated with the nlmsghdr.
由于nlmsghdr后根据不同的请求类型会跟随一个不同的结构体,真正的载荷在两层结构体后。
因此这个宏用于封装其他的宏,比如
#define IFLA_RTA(r) ((struct rtattr*)(((char*)(r)) + NLMSG_ALIGN(sizeof(struct ifinfomsg))))
#define IFLA_PAYLOAD(n) NLMSG_PAYLOAD(n,sizeof(struct ifinfomsg))
结合后面的内容就容易理解IFLA_PAYLOAD这个宏拿到了后续载荷的长度,IFLA这个宏拿到了后续载荷的指针
(其中r是后面介绍的ifinfomsg结构的指针)。
*/
int NLMSG_PAYLOAD(struct nlmsghdr *nlh, int len);

/*
#define NLMSG_ALIGNTO 4U
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len <= (len))
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
*/

struct nlmsghdr结构体成员如下:

  • nlmsg_len
    包含了netlink消息头和数据载荷的长度。但是并不等于整个消息所占用的空间,因为整个消息占据的空间需要做对齐。
  • nlmsg_type
    netlink消息的类型,可能是标准netlink消息,比如NLMSG_DONE表示结束、NLMSG_ERROR表示出错、NLMSG_NOOP可以忽略,也可以根据创建socket时最后一个参数可能使用该族所定义的消息类型,比如rtnetlink会使用RTM_NEWLINK、RTM_DELLINK、RTM_GETLINK。
  • nlmsg_flags

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    Standard flag bits in nlmsg_flags
    ───────────────────────────────────────────────────────────────────────────────
    NLM_F_REQUEST Must be set on all request messages.
    NLM_F_MULTI The message is part of a multipart message terminated by
    NLMSG_DONE.
    NLM_F_ACK Request for an acknowledgment on success.
    NLM_F_ECHO Echo this request.


    Additional flag bits for GET requests
    ───────────────────────────────────────────────────────────────────────────
    NLM_F_ROOT Return the complete table instead of a single entry.
    NLM_F_MATCH Return all entries matching criteria passed in message con‐
    tent. Not implemented yet.
    NLM_F_ATOMIC Return an atomic snapshot of the table.
    NLM_F_DUMP Convenience macro; equivalent to (NLM_F_ROOT|NLM_F_MATCH).

    Note that NLM_F_ATOMIC requires the CAP_NET_ADMIN capability or an effective UID of 0.

    Additional flag bits for NEW requests
    ────────────────────────────────────────────────────────────
    NLM_F_REPLACE Replace existing matching object.
    NLM_F_EXCL Don't replace if the object already exists.
    NLM_F_CREATE Create object if it doesn't already exist.
    NLM_F_APPEND Add to the end of the object list.
  • nlmsg_seq
    用于追踪消息,细节参考man 7 netlink

  • nlmsg_pid
    用于追踪消息,细节参考man 7 netlink
在一些api中需要使用sockaddr结构指针,可将netlink地址结构体指针做强制类型转换,如下
1
2
3
4
5
6
struct sockaddr_nl {
sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* Zero. */
pid_t nl_pid; /* Port ID. */
__u32 nl_groups; /* Multicast groups mask. */
};

这里我们的目的是获取网卡信息,因此发送请求的nlmsg_type设置为RTM_GETLINK,接收到的消息在nlmsghdr的载荷头部将是一个struct ifinfomsg的结构体,这个结构体后面是一组struct rtattr结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ifinfomsg {
unsigned char ifi_family; /* AF_UNSPEC */
unsigned short ifi_type; /* Device type */
int ifi_index; /* Interface index */
unsigned int ifi_flags; /* Device flags */
unsigned int ifi_change; /* change mask */
};
/*
ifi_type 标识了设备的类型,类型记录在/usr/include/linux/if_arp.h中,
比如ARPHRD_ETHER值为1,ARPHRD_LOOPBACK值为772。

ifi_index 网卡设备的唯一索引号。

ifi_flags 标识了网卡设备的状态,各状态定义在/usr/include/linux/if.h中,
比如IFF_UP为1<<0,IFF_LOOPBACK为1<<3。
*/
rtattr结构体记录了rtnetlink的属性信息。
1
2
3
4
5
6
7
8
9
10
struct rtattr {
unsigned short rta_len; /* Length of option */
unsigned short rta_type; /* Type of option */
/* Data follows */
};

/*
rta_type 标识了网卡设备的某种类型属性,定义在/usr/include/linux/if_link.h中,
比如IFLA_IFNAME表示网卡接口名。
*/
rtattr结构体需要通过这些宏来操作,参考 man 3 rtnetlink。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
returns true if rta points to a valid routing attribute; attrlen is
the running length of the attribute buffer.
When not true then you must assume there are no more attributes
in the message, even if attrlen is nonzero.
*/
int RTA_OK(struct rtattr *rta, int rtabuflen);

/*
returns a pointer to the start of this attribute's data.
*/
void *RTA_DATA(struct rtattr *rta);

/*
returns the length of this attribute's data.
*/
unsigned int RTA_PAYLOAD(struct rtattr *rta);

/*
gets the next attribute after rta. Calling this macro will update attrlen.
You should use RTA_OK to check the validity of the returned pointer.
*/
struct rtattr *RTA_NEXT(struct rtattr *rta, unsigned int rtabuflen);

/*
returns the length which is required for len bytes of data plus the header.
*/
unsigned int RTA_LENGTH(unsigned int length);

/*
returns the amount of space which will be needed in a message with len bytes of data.
*/
unsigned int RTA_SPACE(unsigned int length);

/*
这些宏在/usr/include/linux/rtnetlink.h

#define RTA_ALIGNTO 4
#define RTA_ALIGN(len) ( ((len)+RTA_ALIGNTO-1) & ~(RTA_ALIGNTO-1) )
#define RTA_OK(rta,len) ((len) >= (int)sizeof(struct rtattr) && \
(rta)->rta_len >= sizeof(struct rtattr) && \
(rta)->rta_len <= (len))
#define RTA_NEXT(rta,attrlen) ((attrlen) -= RTA_ALIGN((rta)->rta_len), \
(struct rtattr*)(((char*)(rta)) + RTA_ALIGN((rta)->rta_len)))
#define RTA_LENGTH(len) (RTA_ALIGN(sizeof(struct rtattr)) + (len))
#define RTA_SPACE(len) RTA_ALIGN(RTA_LENGTH(len))
#define RTA_DATA(rta) ((void*)(((char*)(rta)) + RTA_LENGTH(0)))
#define RTA_PAYLOAD(rta) ((int)((rta)->rta_len) - RTA_LENGTH(0))
*/

标准输出打印网卡名和唯一索引号的例子代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#include <stdio.h>
#include <strings.h>

#include <errno.h>
#include <string.h>

#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <sys/socket.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <linux/if.h>


struct testreq {
unsigned char if_family;
};

int main() {

int fd;
int ret;
struct sockaddr_nl addr;
char buf[20480] = {0};
int len = 1048576;

struct {
struct nlmsghdr nh;
struct ifinfomsg msg;
} req;

struct iovec iov;
struct msghdr msg;

fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd == -1) {
printf("socket failed, %s\n", strerror(errno));
return -1;
}

ret = setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &len, sizeof(len));
if (ret == -1) {
printf("setsockopt failed, %s\n", strerror(errno));
return -1;
}

bzero(&addr, sizeof(addr));
addr.nl_family = AF_NETLINK;
ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1) {
printf("bind failed, %s\n", strerror(errno));
return -1;
}

bzero(&ret, sizeof(ret));
req.nh.nlmsg_len = NLMSG_ALIGN(sizeof(req));
req.nh.nlmsg_type = RTM_GETLINK;
req.nh.nlmsg_flags = NLM_F_ROOT | NLM_F_REQUEST;
req.nh.nlmsg_pid = 0;
req.nh.nlmsg_seq = 0;
req.msg.ifi_family = AF_UNSPEC;

ret = send(fd, &req, sizeof(req), 0);
if (ret == -1) {
printf("send failed, %s\n", strerror(errno));
return -1;
}

iov.iov_base = buf;
iov.iov_len = sizeof(buf);
msg.msg_name = &addr;
msg.msg_namelen = sizeof(addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;

while(1) {
int found = 0;
struct nlmsghdr *nh;
ret = recvmsg(fd, &msg, 0);
if (ret == -1) {
if (errno == EINTR || errno == EAGAIN)
continue;
printf("recvmsg failed, %s\n", strerror(errno));
return -1;
}

if (ret == 0) {
printf("recv EOF\n");
return -1;
}

nh = (struct nlmsghdr *)buf;

while(NLMSG_OK(nh, ret)) {
if (nh->nlmsg_type == NLMSG_DONE) {
found = 1;
break;
} else if (nh->nlmsg_type == NLMSG_ERROR) {
printf("NLMSG_ERROR\n");
return -1;
}

// printf("nlmsg type %hu, seq %u, pid %u\n", nh->nlmsg_type, nh->nlmsg_seq, nh->nlmsg_pid);

struct ifinfomsg *ifi = NLMSG_DATA(nh);
struct rtattr *rta = IFLA_RTA(ifi);
int payload_len = IFLA_PAYLOAD(nh);

/*
printf("ifi_family %u\n", ifi->ifi_family);
printf("ifi_type %u\n", ifi->ifi_type);
printf("ifi_index %d\n", ifi->ifi_index);
printf("ifi_flags %x\n", ifi->ifi_flags);
printf("ifi_change %x\n", ifi->ifi_change);
*/

while(RTA_OK(rta, payload_len)) {
if (rta->rta_type == IFLA_IFNAME) {
char *ifname = RTA_DATA(rta);
printf("%s index %d\n", ifname, ifi->ifi_index);
}
rta = RTA_NEXT(rta, payload_len);
}

nh = NLMSG_NEXT(nh, ret);
}

if (found) {
break;
}
}

return 0;
}

我的测试环境输出如下

1
2
3
lo    index 1
eno16780032 index 2
eno33559296 index 3