NETLINKでネットワークインタフェースの状態変化を検知する
LinuxでNETLINKを使ったネットワークインタフェースの状態変化(up/down, IPアドレスの追加/削除)を検知してみる。
NETLINKはネットワークインタフェースの状態変化をカーネルからユーザランドのアプリケーションに対してソケットインタフェース経由で通知してくれる仕組み。
詳細についてはMan page of NETLINKを参照。
ソースコード
netlink.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <time.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <sys/ioctl.h> #include <net/if.h> #include <netinet/in.h> #include <asm/types.h> #include <linux/netlink.h> #include <linux/rtnetlink.h> #include <errno.h> #define EPOLL_SIZE (1) #define CTRLNL_OK (0) #define CTRLNL_NG (-1) struct rtnl_handle { int fd; struct sockaddr_nl local; struct sockaddr_nl peer; __u32 seq; __u32 dump; }; static int ctrlnl_open(struct rtnl_handle* rth, unsigned subscriptions, int protocol) { int ret = CTRLNL_NG; int rc = 0; int addr_len = 0; memset(rth, 0, sizeof(rth)); do { rth->fd = socket(AF_NETLINK, SOCK_RAW, protocol); if (rth->fd < 0) { perror("Cannot open netlink socket"); break; } memset(&rth->local, 0, sizeof(rth->local)); rth->local.nl_family = AF_NETLINK; rth->local.nl_groups = subscriptions; if (bind(rth->fd, (struct sockaddr*)&rth->local, sizeof(rth->local)) < 0) { perror("Cannot bind netlink socket"); close(rth->fd); break; } addr_len = sizeof(rth->local); if (getsockname(rth->fd, (struct sockaddr*)&rth->local, &addr_len) < 0) { perror("Cannot getsockname"); close(rth->fd); break; } if (addr_len != sizeof(rth->local)) { fprintf(stderr, "Wrong address length %d\n", addr_len); close(rth->fd); break; } if (rth->local.nl_family != AF_NETLINK) { fprintf(stderr, "Wrong address family %d\n", rth->local.nl_family); close(rth->fd); break; } rth->seq = time(NULL); ret = CTRLNL_OK; } while(0); return ret; } static void ctrlnl_getifname(int ifindex, char* ifname, int length) { int fd = -1; struct ifreq ifr = { .ifr_ifindex = ifindex, }; fd = socket(AF_INET, SOCK_DGRAM, 0); if (fd < 0) { perror("socket"); } else { if (ioctl(fd, SIOCGIFNAME, &ifr) < 0) { perror("ioctl"); } else { snprintf(ifname, length-1, "%s", ifr.ifr_name); } close(fd); } } static void ctrlnl_getrtaddr(struct rtattr** rtas, struct ifaddrmsg* ifa, int length) { struct rtattr* rta = IFA_RTA(ifa); while(RTA_OK(rta, length)) { if (rta->rta_type <= IFA_MAX) { rtas[rta->rta_type] = rta; } rta = RTA_NEXT(rta, length); } } static int ctrlnl_showinfo(struct nlmsghdr* h) { int ret = CTRLNL_OK; char ifname[64] = {0}; struct ifaddrmsg* ifa = NLMSG_DATA(h); struct rtattr* rtas[IFA_MAX+1]; printf("######### Recv Netlink Message ##########\n"); ctrlnl_getifname(ifa->ifa_index, ifname, sizeof(ifname)); printf("ifname = %s\n", ifname); printf("nlmsg_type = %#x\n", h->nlmsg_type); switch(h->nlmsg_type) { case RTM_NEWLINK: case RTM_DELLINK: break; case RTM_NEWADDR: case RTM_DELADDR: { ctrlnl_getrtaddr(rtas, ifa, (h->nlmsg_len - NLMSG_SPACE(sizeof(*ifa)))); printf("ifa_family = %#x\n", ifa->ifa_family); char addr[128] = {0}; if (rtas[IFA_LOCAL]) { inet_ntop(ifa->ifa_family, RTA_DATA(rtas[IFA_LOCAL]), addr, sizeof(addr)); } if (rtas[IFA_ADDRESS]) { inet_ntop(ifa->ifa_family, RTA_DATA(rtas[IFA_ADDRESS]), addr, sizeof(addr)); } printf("address = %s\n", addr); } break; default: fprintf(stderr, "illegal message type.\n"); ret = CTRLNL_NG; break; } printf("\n"); return ret; } static int ctrlnl_recv(int fd) { int ret = CTRLNL_NG; struct nlmsghdr* h; char buf[4096] = {0}; ssize_t rs = 0; rs = recv(fd, buf, sizeof(buf), 0); if (rs < 0) { perror("recv"); } else { for (h = (struct nlmsghdr*)buf; NLMSG_OK(h, rs); h = NLMSG_NEXT(h, rs)) { ret = ctrlnl_showinfo(h); if (CTRLNL_OK != ret) { fprintf(stderr, "ctrlnl_showinfo(%d) failed.\n", ret); break; } } } return ret; } static int ctrlnl_close(int fd) { int ret = CTRLNL_OK; int rc = 0; rc = close(fd); if (rc < 0) { perror("close"); ret = CTRLNL_NG; } return ret; } static int ctrlnl_poll(int fd) { int rc = 0; int ret = CTRLNL_NG; struct sockaddr_in addr; int epfd = -1; int nfds = -1; struct epoll_event events[EPOLL_SIZE]; struct epoll_event evt = { .events = EPOLLIN, .data.fd = fd, }; epfd = epoll_create(EPOLL_SIZE); if (epfd < 0) { perror("epoll_create"); goto error_end; } rc = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt); if (rc < 0) { perror("epoll_ctl"); close(epfd); goto error_end; } while(1) { nfds = epoll_wait(epfd, events, EPOLL_SIZE, -1); if (nfds < -1) { perror("epoll_wait"); close(epfd); goto error_end; } int i; for (i = 0; i < nfds; i++) { if (events[i].data.fd == fd) { ret = ctrlnl_recv(fd); if (CTRLNL_OK != ret) { fprintf(stderr, "ctrlnl_recv(%d) failed.\n", ret); } } } } error_end: return ret; } int main(void) { int ret = -1; struct rtnl_handle rth; do { ret = ctrlnl_open(&rth, RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR, NETLINK_ROUTE); if (CTRLNL_OK != ret) { fprintf(stderr, "ctrlnl_open(%d) failed.\n", ret); break; } ret = ctrlnl_poll(rth.fd); if (CTRLNL_OK != ret) { fprintf(stderr, "ctrlnl_poll(%d) failed.\n", ret); } ret = ctrlnl_close(rth.fd); if (CTRLNL_OK != ret) { fprintf(stderr, "ctrlnl_close(%d) failed.\n", ret); } } while(0); return 0; }
実行結果
ビルドしたバイナリを実行後にdown/up, IPv4付与をやってみる
user@user-VirtualBox ~ $ sudo ifconfig enp0s9 enp0s9 Link encap:イーサネット ハードウェアアドレス 08:00:27:7c:40:f4 UP BROADCAST RUNNING MULTICAST MTU:1500 メトリック:1 RXパケット:0 エラー:0 損失:0 オーバラン:0 フレーム:0 TXパケット:3635 エラー:0 損失:0 オーバラン:0 キャリア:0 衝突(Collisions):0 TXキュー長:1000 RXバイト:0 (0.0 B) TXバイト:601190 (601.1 KB) user@user-VirtualBox ~ $ user@user-VirtualBox ~ $ sudo ifconfig enp0s9 down user@user-VirtualBox ~ $ sudo ifconfig enp0s9 up user@user-VirtualBox ~ $ sudo ifconfig enp0s9 172.16.1.1
表示結果
user@user-VirtualBox ~ $ gcc -o netlink netlink.c user@user-VirtualBox ~ $ ./netlink ######### Recv Netlink Message ########## ifname = enp0s9 nlmsg_type = 0x10 ######### Recv Netlink Message ########## ifname = enp0s9 nlmsg_type = 0x15 ifa_family = 0xa address = fe80::5e20:28f4:aaab:aade ######### Recv Netlink Message ########## ifname = enp0s9 nlmsg_type = 0x10 ######### Recv Netlink Message ########## ifname = enp0s9 nlmsg_type = 0x10 ######### Recv Netlink Message ########## ifname = enp0s9 nlmsg_type = 0x10 ######### Recv Netlink Message ########## ifname = enp0s9 nlmsg_type = 0x10 ######### Recv Netlink Message ########## ifname = enp0s9 nlmsg_type = 0x14 ifa_family = 0xa address = fe80::5e20:28f4:aaab:aade ######### Recv Netlink Message ########## ifname = enp0s9 nlmsg_type = 0x14 ifa_family = 0xa address = fe80::5e20:28f4:aaab:aade ######### Recv Netlink Message ########## ifname = enp0s9 nlmsg_type = 0x14 ifa_family = 0x2 address = 172.16.1.1
適当に参考になりそうなソースコードを集めて作ってみたが何とか動いた。
コマンド操作に対して通知回数が多い気がするのはIPv6のRAが動いているからか?(よく分かってない)
ifconfig,ipコマンドとかNet-SNMPのソースを見ればもう少し理解が深まりそう。