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:
sudo
in a Linux machineip
command$ ip link set up wg1
$ ip addr add 10.0.1.2/24 dev wg1
$ ping -c1 -b 10.0.1.255
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.
linux/drivers/net/tun.c
: source code of the tun/tap implementation in Linux kernel. Surprisingly, the code is simple compares with other network driver 😄