Unix Domain Sockets

“Unix domain sockets” is a powerful socket-based IPC (inter-process communication) mechanism, it uses the same-old sockets API, and is simple to use… well, it has many features, so getting deep in it might be a bit complicated, but the basic¬†use of Unix domain sockets it quite simple.

The man page is excellent, and it has a very good code example, almost as good as mine ūüôā

All the code for this post can be found in github.

Unix-domain server

Basically, we create a “server” by opening a socket (either stream or datagram) and linking it to an address, which is actually a filename in our filesystem*. Then we listen for clients and wait for incoming data. Exactly as we do with network sockets.

* There are other binding options, but I’m going to review the filesystem one¬†here.

In my example code, my listener waits for a client (using “accept”), and when a client connect, it handles the connection until the client closes (not optimal, because a client can starve other clients, but this is only an example).
In order to terminate¬†the listener, it handles “Ctrl-C” – when this signal is received, it exits gracefully.

So, this is what we need to do:

  • Step 1: Create a socket and bind it to an address (a filename)
    1. Creating the socket:
       int sock = socket(AF_UNIX, SOCK_STREAM, 0);

      We are using the “AF_UNIX” family, which is the Unix-domain family, with SOCK_STREAM. We can also use SOCK_DGRAM if we want¬†a datagram socket.

    2. Binding the socket:
       res = bind_address(sock, path);

      Where¬†“sock” is the result from the call to “socket()”, and “path” is the filename that represents the socket. This filename mustn’t exist, therefore, we call “unlink(path)” before calling bind, to be sure that the filename is cleared.

    3. Listen:
       res = listen(sock, MAX_CLIENTS);

      “MAX_CLIENTS” is the length of the clients queue – We’re handling one client at a time.

  • Step 2: In a loop, wait for clients
     while (!quit) {
         int client_socket = accept(sock, NULL, NULL);
         if (client_socket < 0) {          if (errno != EINTR)              continue;          LOGE("Failed accepting client (%d: %s)", errno, strerror(errno));          break;      }      quit = handle_client(client_socket) > 0 ? 1 : 0;
         if (quit) 
             LOGI("Got termination command, exiting");
    
         close(client_socket);
    }
    

    Notes:
    Our socket is blocking, as we didn’t define it otherwise, so “accept()” will block until a client tries to connect.
    We exit only if accept fails on EINTR because we don’t want just any client connection error to kill our service.
    handle_client handles the input/output from/to the client. I defined it, arbitrarily, so that if it returns any positive value, then the listener exits.

    Regarding listener ending, I think it is important to define a SIGTERM handler, so that if the program needs to exit, it will do so gracefully (closing sockets and unlinking the address).

  • Step 3: When a client¬†connects, receive data from it and send back result.
    This is really trivial, and it happens in “handle_client()”. We read the input data like so:

     len = read(sock, buff, MAX_BUFFER_LEN-1);

    Where “sock” is the client socket’s file descriptor that we received from “accept()”.
    Now we do all our processing on the input, and when we want to send data back to the client, we use “write”:

     res = write(sock, out_buff, outbuff_len);

    We can repeat reading/writing as long as the client is connected, and when we decide to terminate the connection, we close the client socket:

     close(client_socket);

Easy enough.

Unix-domain client

Well, this is even easier than creating a server.

  • Step 1: Create a new Unix-domain socket and connect to the server
    1. Creating a new client socket is exactly the same as for the server
    2. Connecting:
      struct sockaddr_un address;
      
      memset(&address, 0x00, sizeof(address));
      address.sun_family = AF_UNIX;
      strncpy(address.sun_path, socket_name, SOCKET_NAME_MAX_LEN);
      
      res = connect(sock, (struct sockaddr *)&address, sizeof(address));
  • Step 2: Read/Write data
    Again, exactly the same as explained above
  • Step 3: Disconnect
    Dito – Use close.

Execution example

Listener

$ ./listener /tmp/sock1
[LISTENER] INFO  Starting "Unix Domain Sockets" demo listener
[LISTENER] INFO  * Starting communication with a client *
[LISTENER] INFO  Incoming message: Test this string
[LISTENER] INFO  Client socket was closed by peer
[LISTENER] INFO  * Starting communication with a client *
[LISTENER] INFO  Incoming message: Take another one
[LISTENER] INFO  Client socket was closed by peer
^C[LISTENER] INFO  Terminating service

Client

$ ./test_client /tmp/sock1 "Test this string"
INFO  Starting "Unix Domain Sockets" demo client
INFO  Got back: Got your message
$ ./test_client /tmp/sock1 "Take another one"
INFO  Starting "Unix Domain Sockets" demo client
INFO  Got back: Got your message

Just what we wanted!
Until next time,
Amnon.

Detecting defective tarballs (from a list of files)

There’s this build system, where tar files are downloaded into a specific folder, and then extracted, compiled and whatever. There are several thread, each one downloads, extracts, compiles and installs, and suddenly, there is no more space left on the hard drive. After a little cleanup for extra space, trying to rebuild fails, because some threads were in the middle of downloading when stopped violently, so a few tar files were corrupted – We need to delete them before going on.

Now, there are tens of files, checking them one by one will take a long time, and will be very tedious, better clean and rebuild, even if it takes a day ūüôā

However, we can use a bash script for detecting defected tarballs!

First, how can we tell if a tar file is defected?

Tar’s “-t” option gives us a list of the stored files. It’s pretty quick, and when the file is corrupted, it fails, like so:

$ tar -tf not_a_tar_file.tar.gz 
tar: This does not look like a tar archive

gzip: stdin: not in gzip format
tar: Child returned status 1
tar: Error is not recoverable: exiting now

$ echo $?
2

The bash variable “$?” gives us the result of the last execution. “Pass” is (obviously) zero, and any other value is “Fail”. So we can use this information.

Improving this for a single file, we’ll send the output, including stderr, to /dev/null, using a¬†redirection: “&> /dev/null”

Then, all we have to do is to put it into a loop, add some command line input, and we get this short script:

#!/bin/bash

if [ -z "$1" ] ; then
    DIR=`pwd`
else
    ls $1 &> /dev/null
    if [ "$?" -ne "0" ] ; then
        echo "No such path: "$1
        exit 1
    fi

    DIR=$1
fi

FILES_LIST=`ls $DIR`
for FILE in $FILES_LIST ; do
    FULLNAME=$DIR/$FILE 
    tar -tf $FULLNAME &> /dev/null
    if [ "$?" -eq "0" ] ; then
        echo $FULLNAME" is OK" 
    else
        echo "[!]" $FULLNAME "is defiective"
    fi
done

And its execution looks like this:

$ ./verify_my_tarballs.sh ./list

./list/backup.tar.gz is OK
[!] ./list/not_a_tar_file.tar.gz is defiective
./list/one_tar.tar.gz is OK
./list/onther_one.tar.gz is OK
./list/project1.tar.gz is OK

Notice that this script is good only for tar files. We can probably expand it to handle zip files and other archive file types, by using the same concept

* No tar files were harmed during the making of this post.

Amnon.

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.