Using TUN/TAP in go or how to write VPN

For some reason I needed to write my own VPN… I choosed golang and most of coding was done in 3 hours, next 3 hours was adding some features. But now I want to show how easy is to write some “vpn” (example is unencrypted tunnel, not realy vpn) in go for linux.

P.S.: if you want to see final result of my 6+ hours of work (decentralized, encrypted and so on), please visit github.com/kanocz/lcvpn.

The first thing we need to do - read something about TUN/TAP and find library to work with for go :) 1 minute of googling give us to github.com/songgao/water.

And we almost done :)

We’ll use TUN interface (just IP packets, without ethernet header) - this is little bit simpler to implement. And we’ll use executing of /sbin/ip to set interface parameters (this can be easly done directly from go, but I need to keep example as simple as possible).

First, interface allocation:

iface, err := water.NewTUN(“”)

Than we need to execute some commands in shell like

/sbin/ip link set dev tun0 mtu 1300
/sbin/ip addr add 192.168.9.1024 dev tun0
/sbin/ip set dev tun0 up

Creating UDP socket is similar to example from “net”.

And 2 loops. One for processing incoming (from net) packets:

buf := make([]byte, BUFFERSIZE)
for {
    n, addr, err := lstnConn.ReadFromUDP(buf)
    header, _ := ipv4.ParseHeader(buf[:n])
    fmt.Printf(“Received %d bytes from %v: %+v\n”, n, addr, header)
    if err != nil || n == 0 {
        fmt.Println(“Error: “, err)
        continue
    }
    iface.Write(buf[:n])
}

And other one is for outcoming packets:

packet := make([]byte, BUFFERSIZE)
for {
    plen, err := iface.Read(packet)
    if err != nil {
        break
    }
    header, _ := ipv4.ParseHeader(packet[:plen])
    fmt.Printf(“Sending to remote: %+v (%+v)\n”, header, err)
    lstnConn.WriteToUDP(packet[:plen], remoteAddr)
}

Looks very simple, right? And complete working example is about 100 lines of code:

package main
import (
    “flag”
    “fmt”
    “log”
    “net”
    “os”
    “os/exec”
    “golang.org/x/net/ipv4”
    “github.com/songgao/water”
)

const ( // I use TUN interface, so only plain IP packet, no ethernet header + mtu is set to 1300 BUFFERSIZE = 1500 MTU = “1300” )

var ( localIP = flag.String(“local”, “”, “Local tun interface IP/MASK like 192.168.3.324”) remoteIP = flag.String(“remote”, “”, “Remote server (external) IP like 8.8.8.8”) port = flag.Int(“port”, 4321, “UDP port for communication”) )

func runIP(args …string) { cmd := exec.Command(“/sbin/ip”, args…) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout cmd.Stdin = os.Stdin err := cmd.Run() if nil != err { log.Fatalln(“Error running /sbin/ip:“, err) } }

func main() { flag.Parse() // check if we have anything if “” == *localIP { flag.Usage() log.Fatalln(”\nlocal ip is not specified”) } if “” == *remoteIP { flag.Usage() log.Fatalln(”\nremote server is not specified”) } // create TUN interface iface, err := water.NewTUN(“”) if nil != err { log.Fatalln(“Unable to allocate TUN interface:“, err) } log.Println(“Interface allocated:“, iface.Name()) // set interface parameters runIP(“link”, “set”, “dev”, iface.Name(), “mtu”, MTU) runIP(“addr”, “add”, *localIP, “dev”, iface.Name()) runIP(“link”, “set”, “dev”, iface.Name(), “up”) // reslove remote addr remoteAddr, err := net.ResolveUDPAddr(“udp”, fmt.Sprintf(“%s:%v”, *remoteIP, *port)) if nil != err { log.Fatalln(“Unable to resolve remote addr:“, err) } // listen to local socket lstnAddr, err := net.ResolveUDPAddr(“udp”, fmt.Sprintf(”:%v”, *port)) if nil != err { log.Fatalln(“Unable to get UDP socket:“, err) } lstnConn, err := net.ListenUDP(“udp”, lstnAddr) if nil != err { log.Fatalln(“Unable to listen on UDP socket:“, err) } defer lstnConn.Close() // recv in separate thread go func() { buf := make([]byte, BUFFERSIZE) for { n, addr, err := lstnConn.ReadFromUDP(buf) // just debug header, _ := ipv4.ParseHeader(buf[:n]) fmt.Printf(“Received %d bytes from %v: %+v\n”, n, addr, header) if err != nil || n == 0 { fmt.Println(“Error: “, err) continue } // write to TUN interface iface.Write(buf[:n]) } }() // and one more loop packet := make([]byte, BUFFERSIZE) for { plen, err := iface.Read(packet) if err != nil { break } // debug :) header, _ := ipv4.ParseHeader(packet[:plen]) fmt.Printf(“Sending to remote: %+v (%+v)\n”, header, err) // real send lstnConn.WriteToUDP(packet[:plen], remoteAddr) } }

To run it just exec on one host something like this:

./vpnhowto -local 192.168.9.1124 -remote first.hostname
and on the second:
./vpnhowto -local 192.168.9.924 -remote second.hostname

After start pinging:

.$ ping 192.168.9.11
PING 192.168.9.11 (192.168.9.11) 56(84) bytes of data.
64 bytes from 192.168.9.11: icmp_seq=1 ttl=64 time=56.1 ms
64 bytes from 192.168.9.11: icmp_seq=2 ttl=64 time=28.3 ms
64 bytes from 192.168.9.11: icmp_seq=3 ttl=64 time=38.5 ms

We have output like this:

$ sudo ./vpnhowto -local 192.168.9.924 -remote vpntest2.nsl.cz
2016/02/22 21:47:03 Interface allocated: tun0
Sending to remote: ver=4 hdrlen=20 tos=0x0 totallen=84 id=0xdd3d flags=0x2 fragoff=0x0 ttl=64 proto=1 cksum=0xca06 src=192.168.9.9 dst=192.168.9.11 ()
Received 84 bytes from 46.234.107.240:4321: ver=4 hdrlen=20 tos=0x0 totallen=84 id=0xe6e8 flags=0x0 fragoff=0x0 ttl=64 proto=1 cksum=0x5c src=192.168.9.11 dst=192.168.9.9
Sending to remote: ver=4 hdrlen=20 tos=0x0 totallen=84 id=0xde1a flags=0x2 fragoff=0x0 ttl=64 proto=1 cksum=0xc929 src=192.168.9.9 dst=192.168.9.11 ()
Received 84 bytes from 46.234.107.240:4321: ver=4 hdrlen=20 tos=0x0 totallen=84 id=0xe6e9 flags=0x0 fragoff=0x0 ttl=64 proto=1 cksum=0x5b src=192.168.9.11 dst=192.168.9.9
Sending to remote: ver=4 hdrlen=20 tos=0x0 totallen=84 id=0xdeab flags=0x2 fragoff=0x0 ttl=64 proto=1 cksum=0xc898 src=192.168.9.9 dst=192.168.9.11 ()
Received 84 bytes from 46.234.107.240:4321: ver=4 hdrlen=20 tos=0x0 totallen=84 id=0xe6ea flags=0x0 fragoff=0x0 ttl=64 proto=1 cksum=0x5a src=192.168.9.11 dst=192.168.9.9