⬆️ ⬇️

A simple Linux network interface monitor using netlink

Once, in one of the projects, I needed to organize strict and reliable control over all network interfaces, routing tables, as well as receive notifications about any changes. A strategic decision was made not to use good old ioctl netdevice (SIOCGIFMETRIC, SIOCSIFNAME, etc.) or direct calls to the corresponding utilities (ifconfig, route, etc.), but to find a more modern and more convenient solution. It was found - libnetlink. It is a library that provides a large number of methods for communicating with the kernel using the netlink mechanism. This library was ideal for my purposes, allowing you to solve a huge number of tasks. Unfortunately, the library turned out to have a not very convenient and rather complicated API, which required to perform a lot of incomprehensible actions. Special fun added almost complete lack of documentation and in general any materials on this topic.

Having thought it over, I decided to sort out netlink and write my library. At the moment, all the functionality for working with notifications, network interfaces, routing tables is implemented, of course, IPv4 and IPv6 are supported. Soon enough, this project will be presented to the public :) In the meantime, I would like to acquaint everyone interested in the wonderful world of netlink, using the example of a simple monitor of network interfaces.



What is netlink?


So, netlink is a convenient way of communication between userspace and the Linux kernel. Communication is carried out using a standard socket, using a special protocol - AF_NETLINK.

Netlink allows you to interact with a large number of kernel subsystems - interfaces, routing, network packet filter. In addition, you can communicate with your kernel module. Of course, the latter should support this type of communication.

Each netlink message is a header, represented by the nlmsghdr structure, as well as a certain number of bytes - the “payload” (playload). This "load" can be some kind of structure, or just RAW data. The message, at the time of delivery, may be divided into several parts. In such cases, each subsequent packet is flagged with the NLM_F_MULTI flag, and the last with the NLMSG_DONE flag. For parsing messages, there is a whole set of macros defined in the header files netlink.h and rtnetlink.h



Create a netlink socket.


The netlink socket declaration looks pretty standard:



socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)



Where AF_NETLINK is the netlink protocol

SOCK_RAW - socket type

NETLINK_ROUTE is a netlink protocol family.

')

The last parameter may be different, depending on what we want to receive from netlink.

I will give a table with the most interesting parameters (the full list of parameters can be found in the documentation):



NETLINK_ROUTE — be notified of changes to the routing table and network interfaces.

It can also be used to change all the parameters of the above objects.

NETLINK_USERSOCK — Reserved for defining user protocols.

NETLINK_FIREWALL - used to transfer IPv4 packets from the network filter to the user level

NETLINK_INET_DIAG - monitoring inet sockets

NETLINK_NFLOG - Network / packet filter ULOG

NETLINK_SELINUX - receive notifications from the Selinux system

NETLINK_NETFILTER - working with the network filter subsystem

NETLINK_KOBJECT_UEVENT - receiving kernel messages



Further, the created socket can be used to send messages, for example, using the send function and receiving messages using recvmsg.



Netlink message


The message header is represented by the structure nlmsghdr

 struct nlmsghdr { __u32 nlmsg_len; //  ,    __u16 nlmsg_type; //    (  ) __u16 nlmsg_flags; //    __u32 nlmsg_seq; //    __u32 nlmsg_pid; //   (PID),   }; 




The nlmsg_type field can point to one of the standard message types:

NLMSG_NOOP - messages of this type are ignored.

NLMSG_ERROR - an error message, and in the payload section there will be an nlmsgerr structure (about it below)

NLMSG_DONE - a message with this flag should complete a message split into several parts



Nlmsgerr structure

 struct nlmsgerr { int error; //     struct nlmsghdr msg; //  ,    }; 




Messages can be one or several (different types are combined using a logical or operation) types:



NLM_F_REQUEST - message - request for something

NLM_F_MULTI - message, part of the message broken into parts

NLM_F_ACK - message - confirmation request

NLM_F_ECHO - echo request. normal direction - requests from the kernel level to the user level

NLM_F_ROOT - this type of query returns a certain table, inside an entity

NLM_F_MATCH - the query returns all matches found.

NLM_F_ATOMIC - returns an atomic slice of a certain table

NLM_F_DUMP - analog NLM_F_ROOT | NLM_F_MATCH



Additional flags:



NLM_F_REPLACE - replace existing similar object

NLM_F_EXCL - do not replace if such an object already exists

NLM_F_CREATE - create an object if it does not exist

NLM_F_APPEND - add an object to the list to an existing one



To identify clients (at the kernel level and at the user level), there is a special address structure - nladdr:



 struct sockaddr_nl { sa_family_t nl_family; //   -  AF_NETLINK unsigned short nl_pad; //     pid_t nl_pid; //   __u32 nl_groups; //   / }; 




nl_pid is a unique socket address. For clients in the kernel, it is always zero. For clients at the user level, it is equal to the identifier of the process owning the socket. Each identifier must be unique, so here you may encounter a problem when you try to create multiple netlink sockets in a multi-threaded application: when you create a new socket, the error “Operation not permitted” will be returned. To bypass this restriction, nl_pid is followed to assign the value of this expression:

pthread_self() << 16 | getpid();

Assigning an identifier value should be done before bind () is called for a socket.

You can also assign a null value to an identifier. In this case, the core will generate the unique identifiers, but the first socket created in the application will always be assigned the value of the identifier of this application.



nl_groups is a bitmask, each bit of which represents a netlink group number. When bind () is called for a netlink socket, you must specify the bit mask of the group that the application wants to listen to in this context. Different groups can be combined using logical or.

Primary groups are defined in the netlink header file.

An example of some of them:



RTMGRP_LINK - this group receives notifications about changes in network interfaces (the interface has been deleted, added, lowered, raised)

RTMGRP_IPV4_IFADDR - this group receives notification of changes in IPv4 interface addresses (the address has been added or deleted)

RTMGRP_IPV6_IFADDR - this group receives notification of changes in IPv6 interface addresses (the address has been added or deleted)

RTMGRP_IPV4_ROUTE - this group receives notifications of changes in the routing table for IPv4 addresses

RTMGRP_IPV6_ROUTE - this group receives notifications of changes in the routing table for IPv6 addresses



After the nlmsghdr header structure, the pointer to the data block is always located. It can be accessed using macros, which will be discussed later.



Netlink macros


The most useful macros in this case are:

NLMSG_ALIGN - Rounds the size of a netlink message to the nearest larger value aligned along the boundary.

NLMSG_LENGTH — Accepts the size of the data field (payload) as a parameter and returns the size-aligned value for writing in the nlmsg_len field of the nlmsghdr header.

NLMSG_SPACE - Returns the size that data of the specified length will take in the netlink packet.

NLMSG_DATA — Returns a pointer to the data associated with the nlmsghdr header sent.

NLMSG_NEXT - Returns the next part of a message consisting of many parts. The macro accepts the following nlmsghdr header in a message consisting of many parts. The calling application should check for the NLMSG_DONE flag in the current nlmsghdr header — the function does not return NULL when the message processing is completed. The second parameter sets the size of the rest of the message buffer. The macro reduces this value by the size of the message header.

NLMSG_OK - Returns true if the message was not truncated and it was successfully disassembled.

NLMSG_PAYLOAD - Returns the size of the data (payload) associated with the nlmsghdr header.



From theory to practice.



Well then. I think that I have already managed to get bored with the boring theory :) Maybe something seemed confusing or not understandable - I will try to chew everything in good examples, there really is nothing complicated.

Below is the promised application that will receive notifications of changes in network interfaces and the routing table.

In the example introduced a number of new structures:



 struct iovec { void *iov_base; //   __kernel_size_t iov_len; //   }; 


This structure serves as a repository of payload data transmitted over netlink sockets. The iov_base field is assigned a pointer to a byte array. It is in this byte array that the message data will be written.



 struct msghdr { void *msg_name; //   ( ) int msg_namelen; //   struct iovec *msg_iov; //     __kernel_size_t msg_iovlen; //    void *msg_control; //    ,      __kernel_size_t msg_controllen; //     unsigned msg_flags; //   }; 


This structure is directly passed through the socket. It contains a pointer to a block of useful data, the number of data blocks, as well as a number of additional flags and fields that came, for the most part, from the BSD platform.



 struct ifinfomsg { unsigned char ifi_family; //  (AF_UNSPEC) unsigned short ifi_type; //   int ifi_index; //   unsigned int ifi_flags; //   unsigned int ifi_change; //  ,           0xFFFFFFFF }; 


This structure is used to represent the network device, its family, type, index, and flags.



 struct ifaddrmsg { unsigned char ifa_family; //   (AF_INET  AF_INET6) unsigned char ifa_prefixlen; //    (  ) unsigned char ifa_flags; //   unsigned char ifa_scope; //   int ifa_index; //  ,     ifinfomsg }; 


This structure is used to represent the network address assigned to the network interface.



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


This structure is used to store any connection parameter or address.



Source code monitor


 #include <errno.h> #include <stdio.h> #include <memory.h> #include <net/if.h> #include <arpa/inet.h> #include <sys/socket.h> #include <linux/rtnetlink.h> //   ,     netlink    //       rtattr void parseRtattr(struct rtattr *tb[], int max, struct rtattr *rta, int len) { memset(tb, 0, sizeof(struct rtattr *) * (max + 1)); while (RTA_OK(rta, len)) { //     if (rta->rta_type <= max) { tb[rta->rta_type] = rta; //  } rta = RTA_NEXT(rta,len); //    } } int main() { int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); //    if (fd < 0) { printf("  netlink : %s", (char*)strerror(errno)); return 1; } struct sockaddr_nl local; //   char buf[8192]; //   struct iovec iov; //   iov.iov_base = buf; //  buf      iov iov.iov_len = sizeof(buf); //    memset(&local, 0, sizeof(local)); //   local.nl_family = AF_NETLINK; //    local.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV4_ROUTE; //    local.nl_pid = getpid(); //       //   netlink -    struct msghdr msg; { msg.msg_name = &local; //   -    msg.msg_namelen = sizeof(local); //   msg.msg_iov = &iov; //     msg.msg_iovlen = 1; //    } if (bind(fd, (struct sockaddr*)&local, sizeof(local)) < 0) { //    printf("   netlink : %s", (char*)strerror(errno)); close(fd); return 1; } //       while (1) { ssize_t status = recvmsg(fd, &msg, MSG_DONTWAIT); //   bind()        //   if (status < 0) { if (errno == EINTR || errno == EAGAIN) { usleep(250000); continue; } printf("   netlink: %s", (char*)strerror(errno)); continue; } if (msg.msg_namelen != sizeof(local)) { //  ,   :) printf("   "); continue; } //    struct nlmsghdr *h; //     for (h = (struct nlmsghdr*)buf; status >= (ssize_t)sizeof(*h); ) { //     int len = h->nlmsg_len; //    int l = len - sizeof(*h); //    char *ifName; //   if ((l < 0) || (len > status)) { printf("  : %i", len); continue; } //    if ((h->nlmsg_type == RTM_NEWROUTE) || (h->nlmsg_type == RTM_DELROUTE)) { //     -   printf("    \n"); } else { //        char *ifUpp; //   char *ifRunn; //   struct ifinfomsg *ifi; //   ,      struct rtattr *tb[IFLA_MAX + 1]; //   , IFLA_MAX   rtnetlink.h ifi = (struct ifinfomsg*) NLMSG_DATA(h); //          parseRtattr(tb, IFLA_MAX, IFLA_RTA(ifi), h->nlmsg_len); //     if (tb[IFLA_IFNAME]) { //   ,    ifName = (char*)RTA_DATA(tb[IFLA_IFNAME]); //   } if (ifi->ifi_flags & IFF_UP) { //    UP   ifUpp = (char*)"UP"; } else { ifUpp = (char*)"DOWN"; } if (ifi->ifi_flags & IFF_RUNNING) { //    RUNNING   ifRunn = (char*)"RUNNING"; } else { ifRunn = (char*)"NOT RUNNING"; } char ifAddress[256]; //    struct ifaddrmsg *ifa; //         struct rtattr *tba[IFA_MAX+1]; //    ifa = (struct ifaddrmsg*)NLMSG_DATA(h); //     parseRtattr(tba, IFA_MAX, IFA_RTA(ifa), h->nlmsg_len); //     if (tba[IFA_LOCAL]) { //      inet_ntop(AF_INET, RTA_DATA(tba[IFA_LOCAL]), ifAddress, sizeof(ifAddress)); //  IP  } switch (h->nlmsg_type) { //   case RTM_DELADDR: printf("    %s\n", ifName); break; case RTM_DELLINK: printf("   %s\n", ifName); break; case RTM_NEWLINK: printf("  %s,   %s %s\n", ifName, ifUpp, ifRunn); break; case RTM_NEWADDR: printf("     %s: %s\n", ifName, ifAddress); break; } } status -= NLMSG_ALIGN(len); //     (    -   ,   :)) h = (struct nlmsghdr*)((char*)h + NLMSG_ALIGN(len)); //   } usleep(250000); //  ,       } close(fd); //    return 0; } 




Compile the program:

gcc monitor.c -o monitor



And the result of the work:





Explanation of the code.

After starting the program, we create a netlink socket and check the success of its creation. Next, the required variables are declared and the local address structure is populated. Here we indicate the groups of messages we want to subscribe to: RTMGRP_LINK, RTMGRP_IPV4_IFADDR, RTMGRP_IPV4_ROUTE.

We also declare the structure of the message and associate with it one data block.

After that, binding to the socket occurs using bind (). After that we become subscribed to messages for the specified groups. You can receive messages through a socket.

This is followed by an endless loop of receiving messages from the socket. Because A received data block can have several headers and their associated data — we begin to iterate through all received data using netlink macros.

Each new message is located at the pointer struct nlmsghdr * h.

Now you can parse the message itself. We look at the nlmsg_type field and find out what kind of message came to us. If it is associated with the routing table, type the message and go to the next message. And if not - we begin to understand in detail.

Arrays of rtattr options are declared, where all the necessary data will be added. The auxiliary function parseRtattr is responsible for obtaining this data. It uses netlink macros and fills the specified array with all attributes from the ifinfomsg or ifaddrmsg structure data block.

After we receive arrays filled with attributes, we can work with these values, analyze them, print.

Each attribute is accessed by its index. All indices are defined in netlink header files and commented.

In this case, we use the following indices:

IFLA_IFNAME is an attribute index with the name of the interface.

IFA_LOCAL - attribute index with local IP address.

After all this, we have complete information about what happened and we can print information on the screen.



That's all. I really hope that this material will be useful to someone.

If there are enough willing (more than one person :)) - I can write a sequel and consider, for example, interaction with the kernel module or the implementation of working with IPv6.



When writing an article used:

1. tools.ietf.org/html/rfc3549

2. www.linuxjournal.com/article/7356



Thanks for attention.

Source: https://habr.com/ru/post/121254/



All Articles