Design and Prototype of a Distributed VPN Using Go and KCP
This article describes the motivation, requirements, solution research, and detailed design of a lightweight distributed VPN built with Go, employing KCP as the transport protocol, and includes prototype source code for both hub and peer components.
Introduction: The author, a budget‑conscious user, seeks to interconnect domestic cloud hosts and foreign VPS across different regions with minimal latency and high throughput.
Requirements: Connect servers located in various provinces or countries while keeping delay low and bandwidth high.
Solution research: Evaluated three approaches – traditional VPN (high overhead and does not improve poor network conditions), kcptun (based on KCP but suffers from cumbersome per‑port mapping), and other alternatives such as proxies.
KCP description: KCP is a fast, reliable protocol that can reduce average latency by 30‑40% at the cost of 10‑20% extra bandwidth, requiring the user to provide the underlying UDP transport and timing callbacks.
Design: The system is implemented in Go, using KCP as the transport layer. It consists of two main components – a hub server and peer clients. The hub server maintains a list of peers, handles heart‑beat validation, updates peer information, and forwards packets. Each peer client registers with the hub, creates a TUN device, reads packets from the TUN interface, resolves destination IPs via the hub’s peer map, and sends data over UDP/KCP.
Prototype code – Hub server (fastvpn_hub.go): package main import ( "log" "net" "os" "syscall" "time" // "github.com/kavu/go_reuseport" kcp "github.com/xtaci/kcp-go" ) func main() { var err error var conn *net.UDPConn if len(os.Args) < 2 { os.Exit(-1) } raddr := os.Args[1] localAddr, _ := net.ResolveUDPAddr("udp", ":8887") serverAddr, _ := net.ResolveUDPAddr("udp", raddr) conn, err = net.DialUDP("udp", localAddr, serverAddr) if err != nil { log.Println("dial udp err") log.Println(err) } _, err = conn.Write([]byte("hello")) if err != nil { log.Println("udp write error") log.Println(err) } buf := make([]byte, 1024) n, _, err := conn.ReadFromUDP(buf) log.Println("udp read data: ", string(buf[:n])) conn.Close() // heartbeat(localAddr, serverAddr) // omitted for brevity klisten, err := kcp.Listen(":8887") if err != nil { log.Println("listen error") log.Println(err) } log.Println("accept start") for { conn, err := klisten.Accept() if err != nil { log.Println("accept error") log.Println(err) continue } go func() { for { buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { log.Println("read error.") log.Println(err) } log.Println("receive data: ", string(buf[:n])) _, err = conn.Write([]byte("hello")) if err != nil { log.Println("kcp write error") log.Println(err) } log.Println("remote addr: ", conn.RemoteAddr()) time.Sleep(3 * time.Second) } }() } }
Prototype code – Peer client (peerclient.go): package main import ( "encoding/json" "flag" "fmt" "log" "net" "os" "os/exec" "strings" // "fastvpn/common" "github.com/songgao/water" "golang.org/x/net/ipv4" ) const ( BUFFERSIZE = 1500 MTU = "1300" ) var ( hubServer = flag.String("hub", "", "server addr like 192.168.11.100:8796") local = flag.String("local", "", "local ip like 172.16.97.101") listen = flag.String("listen", ":6222", "udp for bind") port = flag.String("port", "9999", "local port like 9999") peerMap = make(map[string]string) ) func checkFatalErr(err error, msg string) { if err != nil { log.Println(msg) log.Fatal(err) } } func runIP(args ...string) { cmd := exec.Command("/sbin/ip", args...) cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout if err := cmd.Run(); err != nil { log.Fatal("Error running /sbin/ip:", err) } } func main() { flag.Parse() if *hubServer == "" { flag.Usage() log.Fatal("\nhub Server is not specified") } if *local == "" { flag.Usage() log.Fatal("\nlocal ip is not specified") } hubAddr, err := net.ResolveUDPAddr("udp", *hubServer) checkFatalErr(err, "Unable to resolve server UDP socket") listenAddr, err := net.ResolveUDPAddr("udp", *listen) checkFatalErr(err, "Unable to resolve local UDP socket") config := water.Config{DeviceType: water.TUN} iface, err := water.New(config) checkFatalErr(err, "Unable to allocate TUN interface: ") log.Println("Interface allocated: ", iface.Name()) runIP("link", "set", "dev", iface.Name(), "mtu", MTU) runIP("addr", "add", *local, "dev", iface.Name()) runIP("link", "set", "dev", iface.Name(), "up") conn, err := net.ListenUDP("udp", listenAddr) checkFatalErr(err, "Unable to connect server") defer conn.Close() privateIP := strings.Split(*local, "/")[0] conn.WriteToUDP([]byte(privateIP), hubAddr) go func() { buf := make([]byte, BUFFERSIZE) for { n, addr, err := conn.ReadFromUDP(buf) if addr.String() == hubAddr.String() { log.Println("receive data from server:") if err := json.Unmarshal(buf[:n], &peerMap); err != nil { log.Println("peermap unmarshal error") log.Println(err) } } else { log.Println("receive data from peer:") } if err != nil || n == 0 { fmt.Println("Error: ", err) continue } log.Println(string(buf[:n])) iface.Write(buf[:n]) } }() packet := make([]byte, BUFFERSIZE) for { plen, err := iface.Read(packet) if err != nil { break } header, _ := ipv4.ParseHeader(packet[:plen]) dstIP := header.Dst.String() realDest, ok := peerMap[dstIP] if ok { realDestAddr, err := net.ResolveUDPAddr("udp", realDest) if err != nil { log.Println("resolve real dest ip error") log.Println(err) continue } fmt.Printf("Sending to remote: %+v (%v)\n", header, err) conn.WriteTo(packet[:plen], realDestAddr) } } }
Conclusion: The prototype demonstrates basic data forwarding between hub and peers using KCP over UDP, but lacks NAT traversal, encryption, and full validation; further enhancements are required.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.