Using rtnetlink for detection of NICs status changes in C

I had a task once, where I had to update a configuration when a change was detected in the network interfaces. While the major part of the task was the configuration part, it’s interesting to see how we can detect networking changes in C, and it’s also a nice Netlink exercise.

Netlink is a socket-based IPC between the kernel and user-space, using packets communication (a datagram-oriented service). Netlink communication in user-space is done with the standard and familiar sockets API, similar to unix-domain sockets and INET sockets (See Netlink’s man page).

Netlink has a lot of families, which are sort of sub-protocols. Each family is designed for communication with a different kernel service. It also has a generic family, for user-customized communication.

What I’m going to show here is the usage of “RTNL” – Routing Netlink. This Netlink family is used for managing the routing services of the kernel. We can send commands to change routing, get routing information or “subscribe” for routing notification as (RTNL man page).

The complete code for this post can be downloaded from github.

Netlink messages

An incoming buffer from a netlink socket can contain one or more netlink messages. Each message consists of a header and payload. It looks something like this:

This is the header structure, I think it is pretty self-documenting:

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. */
};

The message payload itself depends on the type, as we’ll soon see with RT messages.

Netlink API offers convenient macros to iterate the messages, extract the payload, and such. You should use them instead of doing the casting yourself, since netlink uses strict alignments, which the macros already take into account.

More on netlink can be found in it’s man page. Or here, if you want to go deeper into Netlink.

RT Netlink

If we are receiving RT Netlink messages, nlmsg_type will indicate which routing message it is, and the payload will match the type. RT Netlink payload look something like this:

The “Message data” is a struct (as I said, it depends on the netlink message type), and the attributes struct looks like this:

struct rtattr {
    unsigned short rta_len;    /* Length of option */
    unsigned short rta_type;   /* Type of option */
    /* Data follows */
};

And here, again, the attribute’s data depends on the attribute’s type.

In our example, we wait for “RTM_NEWLINK” netlink message type, which payload is this:

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 */
};
  • We will be interested only in messages that inform a change in network interfaces, that is, only messages with non-zero ifi_change.
  • ifi_flags will indicate if the device was taken up, or down.
  • In order to know which interface has changed its state, we’ll look for an attribute with rta_type “IFLA_IFNAME”, and read its payload, which type is char*.

Taking it into practice

Opening a netlink socket with RT netlink family is shown in the man page, and looks like this:

fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);

Binding the socket to a specific “Netlink address” – Here we specify which RT groups we want to “subscribe” to. We can subscribe to more than one by OR’ing them together. Here we subscribe only to “RTMGRP_LINK”:

sa.nl_family = AF_NETLINK;
sa.nl_groups = RTMGRP_LINK;
err = bind(fd, (struct sockaddr *) &sa, sizeof(sa));

Waiting for messages and receiving data is done with the regular sockets API. In my code I used “poll” and “recv”, but any other API will do. You can see this part in the code itself.

When we get the message buffer, we need to iterate the netlink messages it contains by iterating the header:

for (nh = (struct nlmsghdr *)buff; NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) {
    .
    .
    .

And for each message we look for nlmsg_type “RTM_NEWLINK” with change indication:

    ifi = NLMSG_DATA(nh);
    if (nh->nlmsg_type != RTM_NEWLINK || !ifi->ifi_change)
        continue;

If we do find such a message, we iterate its attributes in order to find the interface’s name:

char *if_name = NULL;

for ( ; RTA_OK(rta, rta_len) ; rta = RTA_NEXT(rta, rta_len)) {
    if (rta->rta_type == IFLA_IFNAME) {
        if_name = (char*)RTA_DATA(rta);
        break;
    }
}

Where rta and rta_len is taken from the netlink message:

struct rtattr *rta = IFLA_RTA(ifi);
int rta_len = IFLA_PAYLOAD(nh);

Again, you can check out the code itself for the complete picture, I believe it’s clear enough.

Amnon.

Amnon

Advantures in Embeddedland · Post

Update Revert to draft Preview Close

ComposeHTML

Normal
Labels
C, C++, devices, linux, netlink, network interfaces, networking, nic, programming, rtnetlink, sockets, unix
Published on

5/20/16, 10:36 PM

Israel Daylight Time

Permalink
Location
Search Description
Options

Amnon

Advantures in Embeddedland · Post

Update Revert to draft Preview Close

ComposeHTML

Normal
Labels
C, C++, devices, linux, netlink, network interfaces, networking, nic, programming, rtnetlink, sockets, unix
Published on

5/20/16, 10:36 PM

Israel Daylight Time

Permalink
Location
Search Description
Options

Amnon

Advantures in Embeddedland · Post

Update Revert to draft Preview Close

ComposeHTML

Normal
Labels
C, C++, devices, linux, netlink, network interfaces, networking, nic, programming, rtnetlink, sockets, unix
Published on

5/20/16, 10:36 PM

Israel Daylight Time

Permalink
Location
Search Description
Options

Amnon

Advantures in Embeddedland · Post

Update Revert to draft Preview Close

ComposeHTML

Normal
Labels
C, C++, devices, linux, netlink, network interfaces, networking, nic, programming, rtnetlink, sockets, unix
Published on

5/20/16, 10:36 PM

Israel Daylight Time

Permalink
Location
Search Description
Options

Setting up Ubuntu VM with Virtual Box

Well, my laptop finally gave in. It was a quite old Dell laptop, previously belonged to my wife. When its hard drive failed, we bought her a new laptop, and I fixed the old one and installed Ubuntu. With Intel core duo processor, 2GB RAM, and seven years of service, it was OK for my needs (VIM, gcc and some internet), and now it’s dead…

So I bought this brand new laptop that runs Windows. Even as a Linux developer I must admit that Windows is a far better desktop OS than any Linux distribution out there. Linux has its advantages in other areas…

With this new computer running Windows, I had to  install a VM, so I can continue developing at home. A college recommended using Oracle’s VirtualBox, so I’m giving it a try. Installing it was not trouble free, so I’m summarizing my work here.

    1. Download VirtualBox from here. I myself installed Ubuntu 16.04, downloaded from here. You can, of course install any OS you’d like.

      Important: By default, VirtualBox saves its VM files under your user folder, and it doesn’t like strange character in its file path. If you have a username with non-english characters, you might encounter issues along the way, some of which might be critical (That is, you might not be able to complete the installation).

    2. If you want to to install a 64Bit OS, you need to enable CPU virtualization. This is done from UEFI (previously known as BIOS), So you’ll have to restart your computer and enter the UEFI settings before Windows comes up.
      Pay extra attention to what you’re doing here. Changes you make to the UEFI (BIOS) might be harmful to your computer and are at your own risk!

      ufei.png

    3. Open VirtualBox, click “New” and select your desired OS. Give it a nice name, and click “Next”.

      VirtualBox01.png

    4. Give it enough RAM to suit your needs:

      VirtualBox02.png

    5. Next you need to create a hard drive. VirtualBox, for some reason, recommends 8GB, this is not enough for anything. Changing the hard drive’s size when you already have data on it, is a pain, so I recommend a size of 80GB. This is enough for the OS (for Ubuntu, at least), and if you need more storage (doubtedly) in the future, you can always add more hard drives.

      You can choose the hard drive to be allocated dynamically, and this way you won’t waste space on your real hard drive (as opposed to choosing fixed size, which allocated the whole amount from the first byte you use). Although, with the drives sizes you have today, it’s hardly a consideration.

      VirtualBox03.png

    6. This is the only step I’m not really sure of – Choosing the chipset.

      There are two types of chipsets you can use – PIIX3 or ICH9. The thing is, the guest OS must support the chipset. VirtualBox, by default, sets this to PIIX3, and it really should work. But it didn’t. For me, it worked only with ICH9, so I guess Ubuntu 16.04 doesn’t support PIIX3 (Again, not sure of this).
      You can try either option, as I said – it should work, just remember that if you encounter any critical issue (ie, OS doesn’t start), this might be the source to the problem.
      VirtualBox04.png

    7. If you want to work in full screen mode, with proper resolution, this is the first step you need to do – Giving the video card enough memory. I don’t really care what the minimum is, I give it the maximum possible. this might not be a good advice, but I didn’t find any serious performance issues while running my VM.
      VirtualBox05.png
    8. Now you need to mount the Ubuntu installation disk image:

      VirtualBoxMountingDisk.png

    9. Now we can start our VM, so when you’re ready, click on the green arrow.
      You will see the Ubuntu installation screen. Choose “Install Ubuntu” and follow the guide, it is really straight-forward. When the installation ends, it displays a “Restart” button. Click it. (For some reason, first time boot didn’t work for me, I had to restart the VM again. Just mentioning in case it happens to you too)

      VirtualBoxUbuntu.png

    10. For the second step of enabling full-mode, while your installation is executing, you need to download VirtualBox’s “Guest additions”. This is an image file (.iso), that autoinstall once mounted.
      This file is actually hard to find. There is one downloadable version from this page (where you download VirtualBox from). But all the images themselves can be found here (according to your VirtualBox version).
    11. The installation ended, and YES! We have a working Ubuntu 16.04 VM!

      VirtualBoxUbuntuVM.png

    12. Installing the guest additions, in VirtualBox VM’s window click on “Devices” and “Insert guest additions CD image”:

      VirtualBoxGuestAdditions01.png

      Choose your image file and confirm the execution. When the installation is done, restart your VM.

    13. Last thing to do (if you want to work in full screen mode) is to set the resolution. Check your resolution on Windows, and set the exact settings in Ubuntu.
      Click on the “Settings” icon, and choose “Displays”, then set your correct screen resolution.

      VirtualBoxUbuntuDisplaySettings.png

      Press right-ctrl+F, and you’re in full screen mode!


That’s it. Can’t say it was short, but hopefully by now you have an Ubuntu VM working.

Hope you find this guide helpful.

Amnon.