TUN/TAP Demystified

SHARE
May 21, 2016

Opening the Device

Before you can do anything, you need to open the device. As I mentioned in the overview, this is as as simple as opening the /dev/net/tun device file:

int tapfd = open("/dev/net/tun", O_RDWR);
if (tapfd < 0) {
  perror("open");
  return; // Or otherwise handle the error.
}

This is nothing you’re not familiar with; no special flags are needed, or anything of that sort. Once you have the tap file descriptor (tapfd in the example), you can move on to setting the mode and options.

Setting the Mode

The mode and options are set with a simple ioctl() call:

#include <linux/if.h>
#include <linux/if_tun.h>

// ...
  struct ifreq ifr;

  // Set up the ioctl request
  memset(&ifr, 0, sizeof(ifr));
  ifr.ifr_flags = IFF_TAP | IFF_NO_PI;

  // Call the ioctl
  int err = ioctl(tapfd, TUNSETIFF, (void *)&ifr);
  if (err < 0) {
    perror("ioctl");
    return; // Or otherwise handle the error.
  }
// ...

And that’s it. Once this is done, you’re ready to use your shiny new tun/tap device.

Sending and Receiving

After the ioctl() call, your file descriptor can be used just like any other. Send and receive are analogous to write and read, respectively:

// Write a packet to the network.
//
// Returns 0 on success, or a negative error number on error.
int send_data(int tapfd, char *packet, int len) {
  int wlen;

  wlen = write(tapfd, packet, len);
  if (wlen < 0) {
    perror("write");
  }

  return wlen;
}

// Read a packet from the network.
//
// Returns the packet size on successful read, or a negative error number on error.
int receive_data(int tapfd, char *packet, int bufsize) {
  int len;

  len = read(tapfd, packet, bufsize);
  if (len < 0) {
    perror("read");
  }

  return len;
}

It’s so simple that the functions are only adding error handling. They just wrap read and write. Simple, yes? Any operation that works on a non-seekable stream (poll() and select(), for example) should work just fine on the TUN/TAP file descriptor.

Routing Traffic

Okay, so you have an application that opens a tap device, and you’re not seeing any traffic. The far end of your tunnel is the same as any other network interface under Linux, so all the standard network principles apply.

For example, let’s say that you’ve got an application that implements an IPv4 stack. It has the IP address 10.0.0.42, and the TUN/TAP device it allocated is called tap0. Your host system is 10.0.0.3. The simplest way for your host system to talk to your tap device is with a host route:

# route add -host 10.0.0.42 tap0

If you now ping (or send other traffic to) 10.0.0.42, it will hit the routing table, see that route, and get routed over the tap interface. Piece of cake for simple applications. Unfortunately, this doesn’t do anything for you if you want your application to talk to the rest of the network (unless you go overboard and set up your Linux box as a router, of course).

So what if you want that?

There are two main solutions to this problem:

  • NAT - If you’re dealing with IP networking, you’re almost certainly familiar with NAT. The iptables facility can enable you to map traffic from an IP (or alias) on your host machine directly to the IP used by the application that owns the tap interface. This can also get complicated, however.
  • Bridging - You can use the Linux bridge facility to put your application directly on the network the host is attached to.

My preference is for the latter solution. The nice thing about it is, once you have it set up you can run as many tap devices as you want on the same bridge. That means you only have to do it once.

To accomplish this, you might get on your system console and do something like this (note that you’ll need the bridge-utils package for brctl, at least under RedHat and friends):

# brctl addbr bridge0
# ifconfig bridge0
# brctl addif bridge0 eth0

Once done, your system will work like it normally does – but now you’ll be able to add other interfaces (including TUN/TAP devices) to the bridge. They will see any and all network traffic that appears there (say, from eth0). Makes running multiple instances easy, right?

How to set this up at startup is beyond the scope of this article. See your platform’s documentation for more information.