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.10⁄24 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.3⁄24”)
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.11⁄24 -remote first.hostname
and on the second:
./vpnhowto -local 192.168.9.9⁄24 -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.9⁄24 -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