Snippet for Fun

TIL: Creating TUN Device in Go

Posted at — Jul 26, 2020

TUN/TAP are common used virtual network interfaces which powered by the kernel. We can create a “tunnel” and ask the kernel to redirect some packets to our user space application. To use it in Linux, we can open the device file /dev/net/tun with account with CAP_NET_ADMIN capability, and use ioctl to register a network device to the tunnel. Wireguard and OpenVPN use it to create a bridge and routing between network endpoints.

In Go, we can use syscall to create and register the TUN device:

package main

import (
	"encoding/binary"
	"fmt"
	"net"
	"os"
	"unsafe"

	"golang.org/x/sys/unix"
)

const (
	tunDevice = "/dev/net/tun"
	ifReqSize = unix.IFNAMSIZ + 64
)

func checkErr(err error) {
	if err == nil {
		return
	}

	panic(err)
}

// https://www.kernel.org/doc/Documentation/networking/tuntap.txt
// setup: ip link set up wg1 && ip addr add 10.0.1.2/24 dev wg1
// test: ping -c1 -b 10.0.1.255
func main() {
	nfd, err := unix.Open(tunDevice, os.O_RDWR, 0)
	checkErr(err)

	var ifr [ifReqSize]byte
	var flags uint16 = unix.IFF_TUN | unix.IFF_NO_PI
	name := []byte("wg1")
	copy(ifr[:], name)
	*(*uint16)(unsafe.Pointer(&ifr[unix.IFNAMSIZ])) = flags
	fmt.Println(string(ifr[:unix.IFNAMSIZ]))

	_, _, errno := unix.Syscall(
		unix.SYS_IOCTL,
		uintptr(nfd),
		uintptr(unix.TUNSETIFF),
		uintptr(unsafe.Pointer(&ifr[0])),
	)
	if errno != 0 {
		checkErr(fmt.Errorf("ioctl errno: %d", errno))
	}

	err = unix.SetNonblock(nfd, true)

	fd := os.NewFile(uintptr(nfd), tunDevice)
	checkErr(err)

	for {
		buf := make([]byte, 1500)
		_, err := fd.Read(buf)
		if err != nil {
			fmt.Printf("read error: %v\n", err)
			continue
		}

		fmt.Println("received packet")

		switch buf[0] & 0xF0 {
		case 0x40:
			fmt.Println("received ipv4")
			fmt.Printf("Length: %d\n", binary.BigEndian.Uint16(buf[2:4]))
			fmt.Printf("Protocol: %d (1=ICMP, 6=TCP, 17=UDP)\n", buf[9])
			fmt.Printf("Source IP: %s\n", net.IP(buf[12:16]))
			fmt.Printf("Destination IP: %s\n", net.IP(buf[16:20]))
		case 0x60:
			fmt.Println("received ipv6")
			fmt.Printf("Length: %d\n", binary.BigEndian.Uint16(buf[4:6]))
			fmt.Printf("Protocol: %d (1=ICMP, 6=TCP, 17=UDP)\n", buf[7])
			fmt.Printf("Source IP: %s\n", net.IP(buf[8:24]))
			fmt.Printf("Destination IP: %s\n", net.IP(buf[24:40]))
		}
	}
}

To test it:

  1. run the program with sudo in a Linux machine
  2. create and bind the tun device with ip command
$ ip link set up wg1
$ ip addr add 10.0.1.2/24 dev wg1
  1. send packet to this device’s IP by sending broadcast ICMP:
$ ping -c1 -b 10.0.1.255
  1. check output from the program

NOTE: it won’t work if we send packet directly to the device IP in the same machine, since the kernel figures out for us that we don’t need to really route the packets through the devices.

Read more