Adding Networking Support to ELK: Part 3

Again I’m posting this as a post-in-progress. I was hoping to finish this before real life intrudes tomorrow. Things are going well but tomorrow looks like a real stretch goal.

In Part 1 of this post I wrote about some design goals for ELK networking. In Part 2, I described some of the implementation details that evolved as I integrated the LwIP TCP/IP stack into ELK. In this post, I’ll describe adding an Ethernet driver to ELK’s network implementation.

As in Part 2, I’m writing this as development is progressing, so be please patient if I tend to go off on tangents.

LwIP comes with a skeleton example Ethernet driver in src/netif/ethernetif.c. My ARM development environment is currently QEMU emulating the Versatile Express A9 board, which emulates the LAN 91C111 Ethernet controller. Eventually, I’d like to get ELK networking running on the Raspberry Pi, but since it uses a USB based Ethernet controller (the smsc9512), that will require a bit more work.

OK, enough with Google. Time to get to work. I’ll start or by making a copy of ethernetif.c which I have called lan91c111.c. Now I’ll start filling in the details.

I’ve made some basic changes to the driver and now I’ll add it to the list of ELK source files in sources.mk. I know the first build is going to spit out a bunch of errors, and sure enough it does. I’ll add a few “#if RICH” lines to mark places I have to add functionality. I got through the “#if RICH” part when I realized that I’m missing the whole infrastructure for socket ioctl() calls. I realized that when I started about how to initialize Ethernet devices. So I’m side tracking to add ioctl() support to the network interfaces.

As an aside, it turns out I had to make another change to the LwIP sources. One of the ioctl() calls supported on sockets is named IP_HDRINCL. It turns out that LwIP uses that name at the lower levels to represent a packet that already contains an address. I changed all instances of IP_HDRINCL to IP_HDRINCLUDED in LwIP to get around the naming conflict.

There are two interfaces that LwIP supports to configure Ethernet devices, basically because LwIP has two modes of operation as I mentioned in Part 1. The core of LwIP is meant to be used by a single (or the only) thread. That is the first mode, and is usually reserved for smaller systems where all network activity is done by a single thread. The second mode has a network thread which has a message based interface to all the other threads in the system. ELK uses the second mode, so we have to use the netifapi functions, defined in the lwip/netifapi.h file. I had to to enable this API in the ELK specific lwipopts.h file.

The ioctl() calls for sockets are documented in the netdevice(7) man page. We’ll see how well the LwIP API maps to Linux’s idea of network configuration.

There are a set of ioctl() calls defined that are used to configure network devices as described in netdevice(7). I’ve implemented a fairly large subset of socket ioctl() calls to prepare for actually adding the Ethernet driver. Here is an example of some of them:

/* ELK running as a VM enabled OS.
 */
#define _GNU_SOURCE
#include <sys/ioctl.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>

#include <sys/cdefs.h>
#include <stdio.h>

#include "command.h"

int main(int argc, char **argv)
{
  setprogname("elk");
  printf("%s started. Type \"help\" for a list of commands.\n", getprogname());

  int sfd = socket(AF_INET, SOCK_STREAM /*|SOCK_NONBLOCK */, 0);
  if (sfd < 0) {
    printf("socket(AF_INET) failed: %s\n", strerror(errno));
  }

  int s;
  struct ifreq ifreq;
  struct in_addr in_addr;

  // Set the device name for subsequent calls.
  strcpy(ifreq.ifr_name, "ln01");

  // Set the interface IP{ address.
  inet_aton("192.124.43.4", &in_addr);
  ifreq.ifr_addr.sa_family = AF_INET;
  memcpy(ifreq.ifr_addr.sa_data, &in_addr, sizeof(in_addr));
  s = ioctl(sfd, SIOCSIFADDR, &ifreq);
  if (s < 0) {
    printf("ioctl(SIOCSIFADDR) failed: %s\n", strerror(errno));
  }

  // Set ine interface netmask.
  inet_aton("255.255.255.0", &in_addr);
  ifreq.ifr_netmask.sa_family = AF_INET;
  memcpy(ifreq.ifr_netmask.sa_data, &in_addr, sizeof(in_addr));
  s = ioctl(sfd, SIOCSIFNETMASK, &ifreq);
  if (s < 0) {
    printf("ioctl(SIOCSIFNETMASK) failed: %s\n", strerror(errno));
  }

  // Set the interface MAC address.
  ifreq.ifr_hwaddr.sa_family = ARPHRD_ETHER;
  ifreq.ifr_hwaddr.sa_data[0] = 0x01;
  ifreq.ifr_hwaddr.sa_data[1] = 0x02;
  ifreq.ifr_hwaddr.sa_data[2] = 0x03;
  ifreq.ifr_hwaddr.sa_data[3] = 0x04;
  ifreq.ifr_hwaddr.sa_data[4] = 0x05;
  ifreq.ifr_hwaddr.sa_data[5] = 0x06;
  s = ioctl(sfd, SIOCSIFHWADDR, &ifreq);
  if (s < 0) {
    printf("ioctl(SIOCSIFHWADDR) failed: %s\n", strerror(errno));
  }
  printf("Try the command 'inetif'\n");

  // Enter the kernel command processor.
  do_commands(getprogname());
}

Where is what the compiled example produces:

[~/ellcc/examples/elk] dev% make run
Compiling main.c
Linking elk
Running elk
enter 'control-A x' to exit QEMU
audio: Could not init `oss' audio driver
elk started. Type "help" for a list of commands.
Try the command 'inetif'
elk % inetif
0 lo: active flags=137 (0x01)  mtu 0
        inet 127.0.0.1  netmask 255.0.0.0  broadcast 127.255.255.255
1 ln01: active flags=66 (0x32)  mtu 1500
        inet 192.124.43.4  netmask 255.255.255.0  broadcast 192.124.43.255
        ether 01:02:03:04:05:06
elk % 

ln01 is my stubbed driver. It looks like the ioctl() calls have worked. lwip_network.c is the source file implementing ELK's LwIP socket interface. Now it's time to get the driver to do something.

As I started to flesh out the driver I realized that the vexpress-a9 board that QEMU is emulating uses the LAN9118 Ethernet controller, not the SMC91C111 that I thought. That got me thinking. I really don't want to reinvent the wheel each time for all the different Ethernet controllers out there and it is always better to find something that exists and adapt it rather than to write it from scratch. With that in mind, I decided what it would take to use Ethernet drivers borrowed from NetBSD for ELK. As I see it, the first step is to get a NetBSD driver to compile in the ELK environment with as few source changes as possible. Step two is to make some glue code to make the NetBSD driver fit into the LwIP environment.

Yikes! I just spent a harrowing half a day trying to isolate the NetBSD driver for the LAN9118 so that it could "drop in" as a LwIP driver. I went down the path of stubbing out all the include files that were directly and indirectly included and compiling the source file, over and over, adding definitions to the include files as needed. It turns out that the driver has lots of dependencies on the NetBSD kernel, which is not surprising. I've decided to try a different route, but I did stumble on this interesting post about rump kernels, which are basically doing what ELK is doing, but with NetBSD as the basis.

I like the direction this new approach is taking. I'm using an idea from the LwIP wiki to create a driver framework. The driver skeleton has been fleshed out in ethernetif_driver.c and low level support for the LAN9118 is taking shape in lan9118.c. I hope I find some time over the next few days to fill in the reast of the details.

Leave a Reply

Your email address will not be published.