netlink监听网络变化代码(转载)+流程分析(原创+转载)+数据结构以及相关宏的解析(原创)

一.netlink监听网络变化代码(Linux下使用NetLink 监听网络变化

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <sys/types.h>
#include <asm/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/route.h>

#define BUFLEN 2*1024
#define MSG_BUF_LEN 1*1024

#define t_assert(x) { \
	if(x)  {ret = -__LINE__;goto error;} \
}

/*Ctrl + C exit*/
static volatile int keepRunning = 1;

void intHandler(int dummy)
{
	keepRunning = 0;
}

/*
 * decode RTA, save into tb
 */
void parse_rtattr(struct rtattr **tb, int max, struct rtattr *attr, int len)
{
	for (; RTA_OK(attr, len); attr = RTA_NEXT(attr, len)) {
		if (attr->rta_type <= max) {
			tb[attr->rta_type] = attr;
		}
	}
}

/*
 * show link information
 * triggered when netinterface link state changed
 * like add/del Ethertnet cable, add/del NIC,
 * enable/disable netinterface etc.
 */
void print_ifinfomsg(struct nlmsghdr *nlh)
{
	int len;
	struct rtattr *tb[IFLA_MAX + 1];
	struct ifinfomsg *ifinfo;

	bzero(tb, sizeof(tb));
	ifinfo = NLMSG_DATA(nlh);
	len = IFLA_PAYLOAD(nlh);
	printf("PAYLOAD:%d\n", len);
	printf("nlmsg_len:%d, nlmsg_seq:%d, nlmsg_pid:%d\n", nlh->nlmsg_len, nlh->nlmsg_seq, nlh->nlmsg_pid);
	parse_rtattr(tb, IFLA_MAX, IFLA_RTA (ifinfo), len);

	printf("%s: %s ", (nlh->nlmsg_type==RTM_NEWLINK)?"NEWLINK":"DELLINK",
		(ifinfo->ifi_flags & IFF_UP) ? "up" : "down");

	if(tb[IFLA_IFNAME])
		printf("%s\t", RTA_DATA(tb[IFLA_IFNAME]));
	printf("\n");
}

/*
 * show IP address information
 * triggered when  IP address changed
 */
void print_ifaddrmsg(struct nlmsghdr *nlh)
{
	int len;
	struct rtattr *tb[IFA_MAX + 1];
	struct ifaddrmsg *ifaddr;
	char tmp[20];

	bzero(tb, sizeof(tb));
	ifaddr = NLMSG_DATA(nlh);
	len = IFA_PAYLOAD(nlh);
	printf("PAYLOAD:%d\n", len);
	parse_rtattr(tb, IFA_MAX, IFA_RTA (ifaddr), len);

	printf("%s ", (nlh->nlmsg_type==RTM_NEWADDR)?"NEWADDR":"DELADDR");
	if (tb[IFA_LABEL]) {
		printf("%s ", RTA_DATA(tb[IFA_LABEL]));
	}
	if (tb[IFA_ADDRESS]) {
		inet_ntop(ifaddr->ifa_family, RTA_DATA(tb[IFA_ADDRESS]), tmp, sizeof(tmp));
		printf("%s ", tmp);
	}
	printf("\n");
}

/*
 * show route information
 * tiggered when route changed
 */
void print_rtmsg(struct nlmsghdr *nlh)
{
	int len;
	struct rtattr *tb[RTA_MAX + 1];
	struct rtmsg *rt;
	char tmp[20];

	bzero(tb, sizeof(tb));
	rt = NLMSG_DATA(nlh);
	len = RTM_PAYLOAD(nlh);
	printf("PAYLOAD:%d\n", len);
	parse_rtattr(tb, RTA_MAX, RTM_RTA(rt), len);

	printf("%s: ", (nlh->nlmsg_type==RTM_NEWROUTE)?"NEWROUTE":"DELROUTE");

	if (tb[RTA_DST]) {
		inet_ntop(rt->rtm_family, RTA_DATA(tb[RTA_DST]), tmp, sizeof(tmp));
		printf("RTA_DST %s ", tmp);
	}
	if (tb[RTA_SRC]) {
		inet_ntop(rt->rtm_family, RTA_DATA(tb[RTA_SRC]), tmp, sizeof(tmp));
		printf("RTA_SRC %s ", tmp);
	}
	if (tb[RTA_GATEWAY]) {
		inet_ntop(rt->rtm_family, RTA_DATA(tb[RTA_GATEWAY]), tmp, sizeof(tmp));
		printf("RTA_GATEWAY %s ", tmp);
	}

	printf("\n");
}
int main(int argc, char *argv[])
{
	int socket_fd;
	int ret = 0;

	/* select() used */
/*
	fd_set rd_set;
	struct timeval timeout;
	int select_r;
*/
	struct sockaddr_nl my_addr;
	struct sockaddr_nl peer_addr;
	struct nlmsghdr *nlh = NULL;
	struct nlmsghdr *nh = NULL;
	struct msghdr msg;
	struct iovec iov;
	int len = 0;

	signal(SIGINT, intHandler);

	/* open netlink socket */
	socket_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
	t_assert((socket_fd > 0) ? 0 : socket_fd);

	/* set socket option (receive buff size) or you can use default size*/
/*
	int skb_len = BUFLEN;
	int optvalue;
	socklen_t optlen;

	getsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &optvalue, &optlen);
	printf("default socket recvbuf optvalue:%d, optlen:%d\n", optvalue, optlen);
	t_assert(setsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &skb_len, sizeof(skb_len)));
	getsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &optvalue, &optlen);
	printf("set socket recvbuf optvalue:%d, optlen:%d\n", optvalue, optlen);
*/

	/*set recvmsg type and bind socket*/
	bzero(&my_addr, sizeof(my_addr));
	my_addr.nl_family = AF_NETLINK;
	my_addr.nl_pid = getpid();
	my_addr.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV4_ROUTE;
	t_assert(bind(socket_fd, (struct sockaddr *) &my_addr, sizeof(my_addr)));


	/* fill msghdr */
	nlh = (struct nlmsghdr*)malloc(MSG_BUF_LEN);
	t_assert(!nlh);
	nh = nlh;
	iov.iov_base = (void *)nlh;
	iov.iov_len = MSG_BUF_LEN;
	bzero(&msg, sizeof(msg));
	/* For recvmsg,this domain is used to
	 * save the peer sockaddr info, init to NULL to ignore this domain
	 * or malloc memory for it to catch && save the peer sockaddr info
	 *
	 * ignore
	 * msg.msg_name = NULL;
	 * msg.msg_namelen = 0;
	 *
	 * catch && save
	 * msg.msg_name = (void *)&peer_addr;
	 * msg.msg_namelen = sizeof(peer_addr);
	 *
	 *
	 * For sendmsg this domain should be filled with the peer sockaddr info
	 * (also could be ignored)
	 *
	 * for connected communication like TCP
	 * this domain could be inited to NULL(ignore)
	 *
	 * for none connected communication like UDP
	 * this domain should be filled with the peer sockaddr info
	 * msg.msg_name = (void *)&peer_addr;
	 * msg.msg_namelen = sizeof(peer_addr);
	 */
	bzero(&peer_addr,sizeof(peer_addr));
	msg.msg_name = &peer_addr;
	msg.msg_namelen = sizeof(peer_addr);
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

/* recvmsg block way
 *
 * recvmsg() revceive msg using block way by default(flags is set to 0)
 */
	while (keepRunning) {
		len = recvmsg(socket_fd, &msg, 0);
		printf("peer_addr.nl_family:%d, peer_addr.nl_pid:%d\n", ((struct sockaddr_nl *)(msg.msg_name))->nl_family, ((struct sockaddr_nl *)(msg.msg_name))->nl_pid);
		printf("MSG_LEN:%d\n", len);
		for (nlh = nh; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
			switch (nlh->nlmsg_type) {
			default:
				printf("unknown msg type\n");
				printf("nlh->nlmsg_type = %d\n", nlh->nlmsg_type);
				break;
			case NLMSG_DONE:
			case NLMSG_ERROR:
				break;
			case RTM_NEWLINK:
			case RTM_DELLINK:
				print_ifinfomsg(nlh);
				break;
			case RTM_NEWADDR:
			case RTM_DELADDR:
				print_ifaddrmsg(nlh);
				break;
			case RTM_NEWROUTE:
			case RTM_DELROUTE:
				print_rtmsg(nlh);
				break;
			}
		}
	}

/* receive msg none-block way
 *
 * by using select()
 */

/*
	while (keepRunning) {
		FD_ZERO(&rd_set);
		FD_SET(socket_fd, &rd_set);
		timeout.tv_sec = 5;
		timeout.tv_usec = 0;
		select_r = select(socket_fd + 1, &rd_set, NULL, NULL, &timeout);
		if (select_r < 0) {
			perror("select");
		} else if (select_r > 0) {
			if (FD_ISSET(socket_fd, &rd_set)) {
				len = recvmsg(socket_fd, &msg, 0);
				printf("MSG_LEN:%d\n", len);
				for (nlh = nh; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
					switch (nlh->nlmsg_type) {
					default:
						printf("unknown msg type\n");
						printf("nlh->nlmsg_type = %d\n", nlh->nlmsg_type);
						break;
					case NLMSG_DONE:
					case NLMSG_ERROR:
						break;
					case RTM_NEWLINK:
					case RTM_DELLINK:
						print_ifinfomsg(nlh);
						break;
					case RTM_NEWADDR:
					case RTM_DELADDR:
						print_ifaddrmsg(nlh);
						break;
					case RTM_NEWROUTE:
					case RTM_DELROUTE:
						print_rtmsg(nlh);
						break;
					}

				}
			}
			bzero(nh, MSG_BUF_LEN);
		}
	}
*/
	close(socket_fd);
	free(nh);

error:
	if (ret < 0) {
		printf("Error at line %d\nErrno=%d\n", -ret, errno);
	}
	return ret;
}



二. 关于代码中一些知识点的讲解,便于初学者理解

(1)netlink socket的创建与绑定。(更多请参考Netlink Socket

创建一个套接字:socket()


int socket(int domain,int type, int protocol);

domain指代地址族,PF_NETLINK(或者AF_NETLINK,两者的定义相同“#define AF_NETLINK 16; #define PF_NETLINK AF_NETLINK”),套接字类型不是SOCK_RAW就是SOCK_DGRAM,因为netlink是一个面向数据报的服务。

protocol选择该套接字使用哪种netlink特征,以下是几种预定义的协议类:型:NETLINK_ROUTE,NETLINK_FIREWALL,NETLINK_APRD,NETLINK_ROUTE6_FW。你也可以非常容易的添加自己的netlink协议。

这里我们代码中用的是NETLINK_ROUTE。

绑定一个套接字:bind()

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);


将一个本地socket地址(my_addr)与打开的socket绑定。
 

   struct sockaddr_nl {

       sa_family_t    nl_family;  /* AF_NETLINK   */

       unsigned short nl_pad;     /* zero         */

       __u32          nl_pid;     /* process pid */

       __u32          nl_groups;  /* mcast groups mask */

     } nladdr;

nl_pid:

在这儿被当做该netlink套接字的本地地址,应设置为当前进程的pid,相当于IP地址(用于标识当前进程,如果当前进程只是接收消息,那么nl_pid设为什么都无关)

nl_groups:

如果该应用想接收发送给特定多播组的netlink消息,所有感兴趣的多播组bit应该or在一起,并填充到nl_groups域。否则, nl_groups应该被显式至零,说明该应用只接受发送到该应用的消息(单播unicast)。

注:

如果想要向peer发送netlink消息,还需要创建一个peer_addr,此时nl_pid填写的是peer的信息,nl_groups应至0(单播)。

peer_addr用于发送和接收peer的消息用,在代码中已经说明如何使用。


(2)文件描述符集合与select函数。

recvmsg()接收消息时默认采用堵塞方式,要想实现非堵塞,可以采用select()或者poll()(man recvmsg可以查询到)

 
 
int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);


nfds:

应该设置为3个文件描述符集合(readfds/writefds/exceptfds)中最大文件描述符加1,该参数让select变得更有效率,因为此时内核不用再去检查大于这个值的文件描述符是否属于这三个文件描述符集合。

readfds:

用来检测输入是否就绪的文件描述符集合;

writefds:

用来检测输出是否就绪的文件描述符集合;

exceptfds

用来检测异常情况是否发生的文件描述符集合(异常情况指的是连接到处于信包模式下的伪终端主设备上的从设备状态发生了变化;流式套接字上接收到了带外数据);

timeout:

用来设置超时时间,设置为NULL时select()会一直堵塞直到文件描述符集合中有文件描述符就绪,timeout对应的timeval的秒domain和微秒domain设置为0时,只是简单的轮寻指定的文件描述符集合看看其中是否有就绪的文件描述符并立即返回。或者设置一个确定的超时时间,超时后select()会返回。


采用以下宏来操作文件描述符集合 
      void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

FD_CLR将文件描述符fd从文件描述符集合set中删除;

FD_SET将文件描述符fd添加到文件描述符集合set中;

FD_ZERO清空文件描述符集合set;

FD_ISSET检查文件描述符集合set中是否有文件描述符fd。

在调用select()函数之前必须通过FD_ZERO()和FD_SET()来初始化文件描述符集合,以包含我们感兴趣的文件描述符集合。这样调用select()函数,select才会知道我们感兴趣的文件描述符集合。之后select开始工作,当集合中有文件描述符就绪后,select就会修改这些集合,当select返回时,这些文件描述符集合就是处于就绪态的文件描述符集合了(如果在循环中调用select,那么我们必须保证每次都要重新初始化它们)。


(3)以“增加或者删除网络设备”为例讲解相关数据结构和相关宏,也即以print_ifinfomsg(nh)展开介绍,其他类似,不做介绍。

(3.1)用到的几个重要数据结构(本文用的是uclibc库,如果采用Ubuntu开发的话相关文件应该采用/usr/inclue/linux目录下的头文件):

struct msghdr(参见sock通信中msghdr的使用

struct iovec {                    /* Scatter/gather array items */
    void  *iov_base;              /* Starting address */
    size_t iov_len;               /* Number of bytes to transfer */
};

struct msghdr {
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */
    struct iovec *msg_iov;        /* scatter/gather array */
    size_t        msg_iovlen;     /* # elements in msg_iov */
    void         *msg_control;    /* ancillary data, see below */
    size_t        msg_controllen; /* ancillary data buffer len */
    int           msg_flags;      /* flags on received message */
};

msghdr只需关注前4个domain,后面3个很少用到
(msghdr iovec nlmsghdr)三个结构体之间的关系(msghdr iovec nlmsghdr)

msghdr.msg_iov = &iovec

iovec.iov_base = &nlmsghdr

iovec.iov_len = 整个netlink消息的长度(或者接收缓冲去的size)

struct nlmsghdr(定义于uclibc库的netlink.h中,这是netlink接收到的内核消息的header);

     struct nlmsghdr {

       __u32 nlmsg_len;   /* Length of message */

       __u16 nlmsg_type;  /* Message type*/

       __u16 nlmsg_flags; /* Additional flags */

       __u32 nlmsg_seq;   /* Sequence number */

       __u32 nlmsg_pid;   /* Sending process PID */

     };

nlmsg_len指一个netlink消息的长度,包括头信息(下文有介绍),这也是netlink核心所必须的。nlmsg_type用于应用但是对于 netlink核心而言其是透明的。nlmsg_flags用于给定附加的控制信息,其被netlink核心读取和更新。nlmsg_seq和 mlmsg_pid,应用用来跟踪消息,这些对于netlink核心也是透明的。

struct ifinfomsg(定义于uclibc库的rtnetlink.h中,紧跟在nlmsghdr后,这一结构与nlmsghdr之间可能存在PAD(用于nlmsghdr 4 bytes对齐用,如nlmsghdr为10 bytes,那么为了4 bytes对齐,PAD的长度应该为2 bytes)这个结构体的类型由netlink接收到消息的类型决定,这里ifinfomsg对应的接收到的消息类型为RTM_NEWLINK);确定这一结构体类型可参考rtnetlink 中文描述

rtattr(定义于uclibc库的rtnetlink.h中),这是消息的payload部分,可能由一个rtattr或者多个rtattr组成。

    struct rtattr {
	    unsigned short	rta_len;
	    unsigned short	rta_type;
    };


netlink接收到的消息(由一个或多个nlmsg组成)的结构:


(3.2)用于消息处理的宏介绍:

(3.2.1)NLMSG_*宏的介绍(定义于uclibc库的netlink.h中,用于处理“接收到的netlink消息”):

也可以参考netlink(3) - Linux man page

NILMSG_ALIGNTO      4U       对齐的基本长度

NLMSG_ALIGN(len)                 将长度为len的数据进行4 bytes对齐,返回对齐的数据长度

NLMSG_HDRLEN                    sizeof(struct nlmsghdr)+PAD的长度(PAD表示为了4 bytes对齐,需要补齐的长度,下文不再赘述)

NLMSG_LENGTH(len)             若len=sizeof(ifinfomsg),返回值为sizeof(nlmsghdr)+PAD+sizeof(ifinfomsg)的长度,这里len一般指nlmsghdr后跟的结构体(本例中是ifinfomsg)的长度

NLMSG_SPACE(len)               若len=sizeof(ifinfomsg),返回值为sizeof(nlmsghdr)+PAD+sizeof(ifinfomsg)+PAD的长度,这里len一般指nlmsghdr后跟的结构体(本例中是ifinfomsg)的长度

NLMSG_DATA(nlh)                   返回nlmsghdr后跟的结构体(这里是ifinfomsg)的基地址,nlh为nlmsg的基地址。

NLMSG_NEXT(nlh, len)           根据当前的nlmsg的基地址(nlmsghdr的基地址)获取下一个nlmsg的基地址,并将netlinkd的消息长度减掉当前nlmsg的长度。

NLMSG_OK(nlh, len)                 根据当前的nlmsghdr和剩余的netlink消息的长度判断nlmsg是否有效

NLMSG_PAYLOAD(nlh, len)   若len=sizeof(ifinfomsg),根据当前的nlmsghdr和len获取PAYLOAD的长度 ,这里len一般指nlmsghdr后跟的结构体(本例中是ifinfomsg)的长度


(3.2.2)RTA_*宏的介绍(定义于uclibc库的rtnetlink.h中,用于处理“PAYLOAD”):

RTA_ALIGNTO    4            同NLMSG_ALIGNTO   4U

RTA_ALIGN(len)                同NLMSG_ALIGN(len)

RTA_OK(rta, len)                同NLMSG_OK(nlh, len)

RTA_NEXT(rta, attrlen)       同NLMSG_NEXT(nlh, len)

RTA_LENGTH(len)              同NLMSG_LENGTH(len),这里的len指的是rtattr跟的结构,一般是字符串。

RTA_SPACE(len)                同NLMSG_SPACE(len)

RTA_DATA(rta)                    根据rtattr的基地址获取其后的字符串的基地址

RTA_PAYLOAD(rta)             根据rtattr的基地址获取其后的字符串的长度


(3.2.3)IFLA_*宏的介绍(定义于uclibc库的rtnetlink.h中,仅限于处理RTM_NEWLINK, RTM_DELLINK, RTM_GETLINK类型的消息(因为其固定使用ifinfomsg这个结构体)):

IFLA_RTA(r)                        r为ifinfomsg的基地址,返回ifinfomsg后的第一个rtattr基地址

IFLA_PAYLOAD(n)             此宏展开为NLMSG_PAYLOAD(nlh, sizeof(ifinfomsg)),获取PAYLOAD的长度

猜你喜欢

转载自blog.csdn.net/u010020404/article/details/78905561