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:
注:
如果想要向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的長度