使用 Go 语言构建 VPN:完整教程
引言
虚拟专用网络(VPN)是一种强大的工具,它允许用户在公共网络(如互联网)上创建一个安全的、加密的连接,就像在私有网络中一样。VPN 可以用于保护隐私、绕过地理限制、安全访问远程资源等多种场景。
Go 语言(Golang)以其简洁的语法、强大的并发能力、出色的网络库和高效的性能,成为构建网络应用程序(包括 VPN)的理想选择。本教程将详细指导你如何使用 Go 语言从头开始构建一个基础的点对点 VPN。我们将使用 TUN 设备(网络层虚拟设备)和 TLS 加密来确保通信安全。
本教程的目标:
- 理解 VPN 的基本工作原理。
- 了解 TUN/TAP 虚拟网络设备。
- 学习如何使用 Go 语言创建和配置 TUN 设备。
- 掌握使用 Go 的
crypto/tls
包建立安全的客户端-服务器连接。 - 实现 VPN 客户端和服务器之间的数据包转发。
- 配置操作系统路由以通过 VPN 隧道传输流量。
阅读本教程需要:
- 基本的 Go 语言编程知识。
- 对计算机网络(IP 协议、TCP/UDP、路由)有基本了解。
- 安装 Go 开发环境(建议最新稳定版)。
- 具有管理员权限(sudo)的 Linux 或 macOS 系统(TUN/TAP 的操作和路由配置需要权限)。Windows 上的 TUN/TAP 实现有所不同,本教程主要关注类 Unix 系统,但核心 Go 代码逻辑相似。
openssl
命令行工具(用于生成 TLS 证书)。
免责声明: 本教程旨在教学,构建的 VPN 是一个基础实现。生产环境中的 VPN 需要更复杂的安全措施、错误处理、性能优化和管理功能。请勿在未充分理解其安全限制的情况下将其用于敏感数据传输。
核心概念
在开始编码之前,让我们先理解几个关键概念。
1. TUN/TAP 设备
TUN 和 TAP 是操作系统内核提供的虚拟网络设备。它们允许用户空间的程序(如我们的 Go VPN 应用)直接读写网络数据包。
- TUN (Tunnel): 工作在网络层(IP 层,L3)。它模拟一个点对点的 IP 通道。当操作系统将 IP 数据包路由到 TUN 设备时,这些数据包会被交给连接到该设备的应用程序。反之,应用程序写入 TUN 设备的数据包会被当作来自该虚拟接口的数据包注入操作系统的网络协议栈。这是构建基于 IP 的 VPN 的常用选择。
- TAP (Terminal Access Point): 工作在数据链路层(以太网层,L2)。它模拟一个以太网设备,可以处理以太网帧。这允许构建更复杂的 VPN,例如桥接两个以太网段。
在本教程中,我们将使用 TUN 设备,因为它更适合典型的 IP VPN 场景,并且相对简单。
2. VPN 工作流程(基于 TUN)
我们构建的 VPN 将遵循以下基本流程:
-
客户端:
- 创建一个 TUN 设备。
- 为 TUN 设备分配一个私有 IP 地址(例如
10.0.0.2
)。 - 与 VPN 服务器建立一个安全的(TLS 加密)连接(通常使用 TCP 或 UDP)。
- 配置操作系统路由,将目标流量(例如,访问特定子网或所有互联网流量)指向 TUN 设备。
- 当应用程序发送数据包,且路由表指示其通过 TUN 设备时:
- 操作系统将 IP 数据包写入 TUN 设备。
- VPN 客户端程序从 TUN 设备读取该 IP 数据包。
- 客户端加密数据包,并通过 TLS 连接发送给服务器。
- 当从服务器收到加密数据时:
- 客户端解密数据,得到原始 IP 数据包。
- 客户端将 IP 数据包写入 TUN 设备。
- 操作系统接收该数据包,并根据其目标 IP 地址进行后续处理(例如,发送给本地应用程序)。
-
服务器端:
- 创建一个 TUN 设备。
- 为 TUN 设备分配一个私有 IP 地址(例如
10.0.0.1
,作为 VPN 子网的网关)。 - 监听来自 VPN 客户端的安全连接(TLS)。
- 当从客户端收到加密数据时:
- 服务器解密数据,得到原始 IP 数据包。
- 服务器将 IP 数据包写入 TUN 设备。
- 操作系统接收该数据包。如果数据包的目标是服务器本身或其他 VPN 客户端,则相应处理。如果目标是外部网络(例如互联网),则需要配置 IP 转发(和 NAT)。
- 当操作系统将数据包路由到 TUN 设备(通常是响应 VPN 客户端的请求)时:
- VPN 服务器程序从 TUN 设备读取该 IP 数据包。
- 服务器加密数据包,并通过 TLS 连接发送给相应的客户端。
3. 加密 (TLS)
直接在公共网络上传输原始 IP 数据包是不安全的。我们需要加密 VPN 隧道中的所有流量。TLS (Transport Layer Security) 是广泛使用的标准,提供了身份验证、加密和数据完整性。Go 的 crypto/tls
包使得实现 TLS 客户端和服务器变得相对容易。我们将使用自签名证书进行演示,但在生产环境中应使用受信任的证书颁发机构 (CA) 签发的证书。
4. 路由和 IP 转发
- 客户端路由: 客户端需要知道哪些流量应该通过 VPN 发送。这通过修改操作系统的路由表来完成。例如,可以添加一条规则,将发往特定私有网络(如
192.168.1.0/24
)或所有目标 (0.0.0.0/0
) 的流量路由到 VPN 服务器的 TUN IP 地址(通过客户端的 TUN 接口)。 - 服务器 IP 转发: 如果 VPN 客户端需要通过服务器访问互联网或其他网络,服务器必须启用 IP 转发功能。这允许服务器将从一个网络接口(TUN 设备)接收到的数据包转发到另一个网络接口(物理网卡)。通常还需要配置网络地址转换 (NAT),以便从 VPN 客户端发出的流量看起来像是来自服务器的公共 IP 地址。
环境准备
1. 安装 Go
确保你已经安装了 Go。可以通过在终端运行 go version
来检查。如果未安装,请访问 Go 官方网站 下载并安装。
2. 获取 TUN/TAP 库
我们将使用一个流行的 Go 库来简化与 TUN/TAP 设备的交互。github.com/songgao/water
是一个不错的选择,它提供了跨平台的接口(尽管底层实现和配置仍有平台差异)。
bash
go get github.com/songgao/water
3. 生成 TLS 证书和密钥
我们需要为服务器生成 TLS 证书和私钥,以及为客户端生成证书和私钥(用于双向 TLS 认证,更安全)。这里我们使用 openssl
生成自签名的 CA 和证书。
“`bash
1. 创建自签名 CA
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -subj “/CN=MyVPN CA”
2. 生成服务器证书和密钥,由 CA 签名
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj “/CN=vpn.server.local” # 使用服务器的域名或 IP
openssl x509 -req -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
3. 生成客户端证书和密钥,由 CA 签名
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj “/CN=vpn.client1”
openssl x509 -req -days 3650 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt
清理 CSR 文件(可选)
rm .csr .srl
“`
现在我们有了以下文件:
ca.crt
: CA 证书(客户端和服务器都需要信任它)server.crt
,server.key
: 服务器证书和私钥client.crt
,client.key
: 客户端证书和私钥
将这些文件妥善保管。服务器需要 ca.crt
, server.crt
, server.key
。客户端需要 ca.crt
, client.crt
, client.key
。
VPN 服务器实现 (server.go
)
“`go
package main
import (
“crypto/tls”
“crypto/x509”
“flag”
“io”
“log”
“net”
“os”
“os/exec”
“strings”
“sync”
“time”
"github.com/songgao/water"
)
const (
// MTU (Maximum Transmission Unit) for the TUN interface.
// Needs to be coordinated with the client.
// Adjust based on underlying network and encryption overhead.
mtu = 1400
)
var (
localIP = flag.String(“local”, “10.0.0.1/24”, “Local TUN interface IP/CIDR”)
remoteIP = flag.String(“remote”, “”, “Remote client IP (used for point-to-point config, optional)”) // Usually managed dynamically
listenAddr = flag.String(“listen”, “:8443”, “Listen address for client connections (host:port)”)
certFile = flag.String(“cert”, “server.crt”, “Server certificate file”)
keyFile = flag.String(“key”, “server.key”, “Server private key file”)
caFile = flag.String(“ca”, “ca.crt”, “Client certificate CA file”)
verbose = flag.Bool(“v”, false, “Enable verbose logging”)
)
func main() {
flag.Parse()
log.Println("Starting GoVPN Server...")
// 1. Create TUN Interface
iface, err := createTUN(*localIP)
if err != nil {
log.Fatalf("Failed to create TUN interface: %v", err)
}
defer iface.Close()
log.Printf("TUN interface '%s' created and configured with IP %s", iface.Name(), *localIP)
// 2. Load TLS Configuration
tlsConfig, err := loadTLSConfig(*certFile, *keyFile, *caFile)
if err != nil {
log.Fatalf("Failed to load TLS config: %v", err)
}
log.Println("TLS configuration loaded successfully.")
// 3. Start Listening for Client Connections
listener, err := tls.Listen("tcp", *listenAddr, tlsConfig)
if err != nil {
log.Fatalf("Failed to listen on %s: %v", *listenAddr, err)
}
defer listener.Close()
log.Printf("Listening for client connections on %s", *listenAddr)
// 4. Accept and Handle Client Connections Concurrently
var wg sync.WaitGroup
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Failed to accept connection: %v", err)
continue // Don't stop the server for one failed connection
}
// Handle each client in a separate goroutine
wg.Add(1)
go func(tlsConn net.Conn) {
defer wg.Done()
defer tlsConn.Close()
// Optional: Log client certificate details
if tlsConn, ok := tlsConn.(*tls.Conn); ok {
if err := tlsConn.Handshake(); err != nil {
log.Printf("TLS handshake failed: %v", err)
return
}
state := tlsConn.ConnectionState()
if len(state.PeerCertificates) > 0 {
clientCN := state.PeerCertificates[0].Subject.CommonName
log.Printf("Accepted connection from %s (CN: %s)", tlsConn.RemoteAddr(), clientCN)
} else {
log.Printf("Accepted connection from %s (No client cert presented)", tlsConn.RemoteAddr())
}
}
log.Printf("Handling connection from %s", tlsConn.RemoteAddr())
handleClient(iface, tlsConn)
log.Printf("Connection from %s closed", tlsConn.RemoteAddr())
}(conn)
}
wg.Wait() // Keep main goroutine alive (though listener loop is infinite)
}
// createTUN creates and configures the TUN interface
func createTUN(cidr string) (*water.Interface, error) {
ip, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return nil, err
}
config := water.Config{
DeviceType: water.TUN,
// Optional: Specify interface name if needed
// PlatformSpecificParams: water.PlatformSpecificParams{Name: "govpn0"},
}
iface, err := water.New(config)
if err != nil {
return nil, err
}
log.Printf("TUN Interface Name: %s", iface.Name())
// Configure IP address and bring up the interface (using external commands)
// Note: This part is OS-specific. Below is for Linux.
mask := ipNet.Mask
netmask := net.IP(mask).String() // Correctly get netmask string
// Use ip command (preferred over ifconfig)
cmd := exec.Command("ip", "addr", "add", cidr, "dev", iface.Name())
if err := runCommand(cmd); err != nil {
iface.Close()
return nil, err
}
cmd = exec.Command("ip", "link", "set", "dev", iface.Name(), "up")
if err := runCommand(cmd); err != nil {
iface.Close()
return nil, err
}
// Set MTU
cmd = exec.Command("ip", "link", "set", "dev", iface.Name(), "mtu", string(mtu))
if err := runCommand(cmd); err != nil {
log.Printf("Warning: Failed to set MTU %d on %s: %v", mtu, iface.Name(), err)
// Continue anyway, but MTU mismatch can cause issues.
}
// Optional: Add route for the VPN subnet if needed (usually implicit)
// cmd = exec.Command("ip", "route", "add", ipNet.String(), "dev", iface.Name())
// if err := runCommand(cmd); err != nil {
// iface.Close()
// return nil, err
// }
return iface, nil
}
// runCommand executes a command and logs output/errors
func runCommand(cmd exec.Cmd) error {
log.Printf(“Executing: %s”, strings.Join(cmd.Args, ” “))
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf(“Error executing command: %v\nOutput: %s”, err, string(output))
return err
}
if verbose && len(output) > 0 {
log.Printf(“Command output: %s”, string(output))
}
return nil
}
// loadTLSConfig loads server cert, key, and CA cert for client auth
func loadTLSConfig(certFile, keyFile, caFile string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
caCert, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
log.Fatalf("Failed to append CA certificate")
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert, // Require client cert verification
MinVersion: tls.VersionTLS12, // Use modern TLS versions
// Consider specifying CipherSuites for better security
}, nil
}
// handleClient manages communication between the TUN interface and a single client connection
func handleClient(iface *water.Interface, tlsConn net.Conn) {
var wg sync.WaitGroup
wg.Add(2)
// Goroutine: Read from TUN, write to client
go func() {
defer wg.Done()
// Buffer size should accommodate MTU
buf := make([]byte, mtu+100) // Add some headroom
for {
n, err := iface.Read(buf)
if err != nil {
if err == io.EOF {
log.Printf("TUN interface closed (read)")
} else {
log.Printf("Error reading from TUN: %v", err)
}
tlsConn.Close() // Close connection if TUN read fails
return
}
packet := buf[:n]
if *verbose {
log.Printf("TUN -> Client (%s): %d bytes", tlsConn.RemoteAddr(), n)
// Add packet logging here if needed (e.g., source/dest IP)
}
// Add framing/length prefix if needed, especially over UDP
// For TCP, the stream nature usually handles it, but explicit framing is robust.
// Simple framing: Write length prefix (e.g., 2 bytes) then packet
// Example: lenBytes := make([]byte, 2); binary.BigEndian.PutUint16(lenBytes, uint16(n)); tlsConn.Write(lenBytes)
_, err = tlsConn.Write(packet)
if err != nil {
log.Printf("Error writing to client %s: %v", tlsConn.RemoteAddr(), err)
// Don't close TUN, just this connection
return
}
}
}()
// Goroutine: Read from client, write to TUN
go func() {
defer wg.Done()
buf := make([]byte, mtu+100)
for {
n, err := tlsConn.Read(buf)
if err != nil {
if err == io.EOF {
log.Printf("Client %s disconnected", tlsConn.RemoteAddr())
} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
log.Printf("Client %s read timeout", tlsConn.RemoteAddr())
} else {
log.Printf("Error reading from client %s: %v", tlsConn.RemoteAddr(), err)
}
// Don't close TUN, just signal the other goroutine to stop
// Often closing the connection `tlsConn.Close()` here is sufficient
return
}
packet := buf[:n]
if *verbose {
log.Printf("Client (%s) -> TUN: %d bytes", tlsConn.RemoteAddr(), n)
// Add packet logging here if needed
}
// Consider adding packet validation here (e.g., check source IP)
_, err = iface.Write(packet)
if err != nil {
if err == io.EOF {
log.Printf("TUN interface closed (write)")
} else {
log.Printf("Error writing to TUN: %v", err)
}
tlsConn.Close() // If TUN write fails, likely critical, close connection
return
}
}
}()
// Wait for both goroutines to finish (e.g., connection closed or error)
wg.Wait()
}
// — Server OS Configuration (Manual Steps Required) —
// 1. Enable IP Forwarding:
// sudo sysctl -w net.ipv4.ip_forward=1
// # Make it permanent: Edit /etc/sysctl.conf or /etc/sysctl.d/ and add:
// # net.ipv4.ip_forward=1
// # Then run sudo sysctl -p
//
// 2. Configure NAT (using iptables):
// # Replace eth0 with your server’s public network interface name
// # Replace 10.0.0.0/24 with your VPN subnet CIDR
// sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
// # Optional: Allow forwarding from VPN subnet to the internet
// sudo iptables -A FORWARD -i ${TUN_INTERFACE_NAME} -o eth0 -j ACCEPT
// sudo iptables -A FORWARD -i eth0 -o ${TUN_INTERFACE_NAME} -m state –state RELATED,ESTABLISHED -j ACCEPT
// # Persist iptables rules (e.g., using iptables-persistent package)
“`
服务器代码说明:
- Flags: 定义命令行参数来配置服务器(本地 TUN IP、监听地址、证书路径等)。
createTUN
: 使用water.New
创建 TUN 设备,然后调用ip
命令(Linux 示例)来设置 IP 地址、子网掩码,并启用接口。MTU 设置也很重要,需要与客户端协调。runCommand
: 辅助函数,用于执行外部命令并记录输出/错误。loadTLSConfig
: 加载服务器证书/密钥,并加载 CA 证书。ClientAuth: tls.RequireAndVerifyClientCert
强制要求客户端提供由指定 CA 签名的有效证书。main
:- 解析 flags。
- 调用
createTUN
和loadTLSConfig
。 - 使用
tls.Listen
启动 TLS 服务器。 - 在一个无限循环中接受客户端连接 (
listener.Accept()
)。 - 为每个接受的连接启动一个新的 goroutine (
go handleClient(...)
) 进行处理,实现并发。
handleClient
:- 使用
sync.WaitGroup
管理两个核心 goroutine。 - TUN -> Client Goroutine: 从 TUN 接口 (
iface.Read
) 读取 IP 数据包,然后通过 TLS 连接 (tlsConn.Write
) 将其写入客户端。 - Client -> TUN Goroutine: 从 TLS 连接 (
tlsConn.Read
) 读取加密数据(TLS 库处理解密),然后将解密后的 IP 数据包写入 TUN 接口 (iface.Write
)。 - 错误处理: 包含基本的错误日志记录。如果任一方向的读/写失败,通常会导致该客户端连接关闭。
- 日志:
verbose
标志可以启用更详细的数据包大小日志。
- 使用
- OS 配置注释: 代码末尾包含了非常重要的注释,说明了运行服务器前需要在操作系统级别执行的步骤:启用 IP 转发和配置 NAT(使用
iptables
)。必须手动执行这些步骤!
VPN 客户端实现 (client.go
)
“`go
package main
import (
“crypto/tls”
“crypto/x509”
“flag”
“io”
“log”
“net”
“os”
“os/exec”
“strings”
“sync”
“time”
"github.com/songgao/water"
)
const (
// MTU for the TUN interface, should match the server setting.
mtu = 1400
)
var (
serverAddr = flag.String(“server”, “YOUR_SERVER_IP:8443”, “VPN server address (host:port)”)
localIP = flag.String(“local”, “10.0.0.2/24”, “Local TUN interface IP/CIDR”)
vpnSubnet = flag.String(“subnet”, “10.0.0.0/24”, “VPN subnet CIDR (for routing)”)
certFile = flag.String(“cert”, “client.crt”, “Client certificate file”)
keyFile = flag.String(“key”, “client.key”, “Client private key file”)
caFile = flag.String(“ca”, “ca.crt”, “Server certificate CA file”)
routeAll = flag.Bool(“route-all”, false, “Route all traffic (0.0.0.0/0) through VPN”)
verbose = flag.Bool(“v”, false, “Enable verbose logging”)
)
func main() {
flag.Parse()
if *serverAddr == "YOUR_SERVER_IP:8443" {
log.Fatal("Please specify the server address using -server flag.")
}
log.Println("Starting GoVPN Client...")
// 1. Create TUN Interface
// Note: localIP here defines the client's address within the VPN subnet.
// The subnet mask from localIP isn't directly used for TUN config here,
// but is useful for knowing the network.
iface, err := createTUN(*localIP)
if err != nil {
log.Fatalf("Failed to create TUN interface: %v", err)
}
defer iface.Close()
log.Printf("TUN interface '%s' created and configured with IP %s", iface.Name(), *localIP)
// 2. Load TLS Configuration
tlsConfig, err := loadTLSConfig(*certFile, *keyFile, *caFile)
if err != nil {
log.Fatalf("Failed to load TLS config: %v", err)
}
log.Println("TLS configuration loaded successfully.")
// 3. Connect to Server
log.Printf("Connecting to server %s...", *serverAddr)
conn, err := tls.Dial("tcp", *serverAddr, tlsConfig)
if err != nil {
log.Fatalf("Failed to connect to server: %v", err)
}
defer conn.Close()
log.Println("Connected to server successfully.")
// Verify server certificate details (optional but recommended)
if err := conn.ConnectionState().VerifyHostname(strings.Split(*serverAddr, ":")[0]); err != nil {
// NOTE: This requires the server cert CN or SAN to match the hostname/IP used to connect.
// For self-signed certs with CN like "vpn.server.local", this might fail if connecting via IP.
// In production, use proper certs. For testing, you might need to adjust hostname verification
// or add the server's IP as a SAN in the certificate.
log.Printf("Warning: Server hostname verification failed: %v. Ensure server certificate CN/SAN matches '%s'.", err, strings.Split(*serverAddr, ":")[0])
// If strictly needed: config.InsecureSkipVerify = true (NOT RECOMMENDED)
}
// 4. Configure Routing
originalGateway, err := setupRouting(iface.Name(), *vpnSubnet, *serverAddr, *routeAll)
if err != nil {
log.Printf("Warning: Failed to setup routing automatically: %v. Manual configuration required.", err)
// Continue execution, assuming manual routing or simple testing
} else {
log.Println("Routing configured successfully.")
// Ensure routes are cleaned up on exit
defer cleanupRouting(iface.Name(), *vpnSubnet, *serverAddr, originalGateway, *routeAll)
}
// 5. Start Forwarding Traffic
var wg sync.WaitGroup
wg.Add(2)
// Goroutine: Read from TUN, write to Server
go func() {
defer wg.Done()
buf := make([]byte, mtu+100) // Buffer size
for {
n, err := iface.Read(buf)
if err != nil {
log.Printf("Error reading from TUN: %v", err)
conn.Close() // Close connection if TUN read fails
return
}
packet := buf[:n]
if *verbose {
log.Printf("TUN -> Server: %d bytes", n)
}
_, err = conn.Write(packet)
if err != nil {
log.Printf("Error writing to server: %v", err)
// Don't close TUN, connection error handled by read goroutine
return
}
}
}()
// Goroutine: Read from Server, write to TUN
go func() {
defer wg.Done()
buf := make([]byte, mtu+100)
for {
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
log.Println("Server disconnected.")
} else {
log.Printf("Error reading from server: %v", err)
}
// Signal TUN read goroutine to stop by closing TUN? Or just exit?
// Closing the TUN interface here might be too drastic if reconnect logic is added.
// For now, we let main exit which closes the TUN via defer.
return
}
packet := buf[:n]
if *verbose {
log.Printf("Server -> TUN: %d bytes", n)
}
_, err = iface.Write(packet)
if err != nil {
log.Printf("Error writing to TUN: %v", err)
// If TUN write fails, something is wrong locally, maybe close connection
conn.Close()
return
}
}
}()
// Wait for goroutines to finish (e.g., connection closed or error)
log.Println("VPN tunnel established. Forwarding traffic...")
wg.Wait()
log.Println("VPN connection closed.")
}
// createTUN creates and configures the TUN interface for the client
func createTUN(localCIDR string) (*water.Interface, error) {
ip, _, err := net.ParseCIDR(localCIDR)
if err != nil {
return nil, err
}
config := water.Config{
DeviceType: water.TUN,
}
iface, err := water.New(config)
if err != nil {
return nil, err
}
log.Printf("TUN Interface Name: %s", iface.Name())
// Configure IP address and bring up the interface (OS-specific)
// Linux example:
// Note: We only need the IP here for the client side of the point-to-point link.
// The mask /24 is relevant for routing, not the interface config itself in point-to-point.
// We set the peer (server's VPN IP) to enable direct routing over the TUN.
serverIP := strings.Split(*localIP,"/")[0] // Use the IP part of the CIDR
serverVPNIP := strings.Split(*vpnSubnet,"/")[0] // Assume server is .1 in the subnet
if net.ParseIP(serverVPNIP) == nil {
log.Printf("Warning: Could not parse server VPN IP from subnet %s, assuming .1", *vpnSubnet)
serverVPNIP = strings.Split(ip.Mask(ip.DefaultMask).String(), "/")[0] // Get network address
baseIP := net.ParseIP(serverVPNIP)
baseIP[len(baseIP)-1] = 1 // Set last octet to 1
serverVPNIP = baseIP.String()
}
cmd := exec.Command("ip", "addr", "add", ip.String(), "peer", serverVPNIP, "dev", iface.Name())
if err := runCommand(cmd); err != nil {
iface.Close()
return nil, err
}
cmd = exec.Command("ip", "link", "set", "dev", iface.Name(), "up")
if err := runCommand(cmd); err != nil {
iface.Close()
return nil, err
}
// Set MTU
cmd = exec.Command("ip", "link", "set", "dev", iface.Name(), "mtu", string(mtu))
if err := runCommand(cmd); err != nil {
log.Printf("Warning: Failed to set MTU %d on %s: %v", mtu, iface.Name(), err)
}
return iface, nil
}
// runCommand executes a command and logs output/errors (same as server)
func runCommand(cmd exec.Cmd) error {
log.Printf(“Executing: %s”, strings.Join(cmd.Args, ” “))
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf(“Error executing command: %v\nOutput: %s”, err, string(output))
return err
}
if verbose && len(output) > 0 {
log.Printf(“Command output: %s”, string(output))
}
return nil
}
// loadTLSConfig loads client cert, key, and CA cert for server auth
func loadTLSConfig(certFile, keyFile, caFile string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
caCert, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to append CA certificate")
}
// Get server hostname for SNI and verification
serverHost, _, err := net.SplitHostPort(*serverAddr)
if err != nil {
// Fallback if port is missing (though Dial needs it)
serverHost = *serverAddr
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool, // Trust the CA that signed the server cert
ServerName: serverHost, // Important for SNI and verification
// InsecureSkipVerify: false, // Default is false, DO NOT set to true in production
MinVersion: tls.VersionTLS12,
}, nil
}
// — Client OS Routing Configuration (OS-Specific) —
// getOriginalGateway finds the current default gateway (Linux example)
func getOriginalGateway() (string, error) {
// Crude way: parse ip route show default
cmd := exec.Command(“ip”, “route”, “show”, “default”)
output, err := cmd.Output()
if err != nil {
return “”, fmt.Errorf(“failed to get default route: %v”, err)
}
lines := strings.Split(string(output), “\n”)
if len(lines) > 0 {
fields := strings.Fields(lines[0])
if len(fields) >= 3 && fields[0] == “default” && fields[1] == “via” {
return fields[2], nil
}
}
return “”, fmt.Errorf(“could not parse default gateway from ‘ip route show default'”)
}
// setupRouting configures OS routing to use the VPN tunnel
func setupRouting(ifaceName, vpnSubnet, serverAddr string, routeAll bool) (originalGateway string, err error) {
serverIP := strings.Split(serverAddr, “:”)[0]
if net.ParseIP(serverIP) == nil {
return “”, fmt.Errorf(“invalid server address format: %s”, serverAddr)
}
// Get original gateway *before* changing routes (needed for cleanup)
originalGateway, err = getOriginalGateway()
if err != nil {
log.Printf("Warning: Could not determine original gateway: %v", err)
// Proceed, but cleanup might be incomplete
}
// Add route for the VPN server itself via the original gateway
// This ensures we can still reach the server after changing the default route
if originalGateway != "" {
cmd := exec.Command("ip", "route", "add", serverIP, "via", originalGateway)
if err := runCommand(cmd); err != nil {
// This might fail if the route already exists, log as warning
log.Printf("Warning: Could not add explicit route to server %s via %s: %v", serverIP, originalGateway, err)
}
} else {
log.Println("Skipping explicit server route addition (no original gateway found).")
}
// Determine Server's VPN IP (usually the .1 address in the vpnSubnet)
_, ipNet, err := net.ParseCIDR(vpnSubnet)
if err != nil {
return originalGateway, fmt.Errorf("invalid vpnSubnet CIDR %s: %w", vpnSubnet, err)
}
serverVPNIP := ipNet.IP.Mask(ipNet.Mask) // Get network address
serverVPNIP[len(serverVPNIP)-1] = 1 // Assume .1 for server
serverVPNIPStr := serverVPNIP.String()
if routeAll {
log.Println("Configuring to route all traffic through VPN...")
// Add routes for 0.0.0.0/1 and 128.0.0.0/1 via the VPN tunnel's peer
// This overrides the default route (0.0.0.0/0) but is more specific
cmd := exec.Command("ip", "route", "add", "0.0.0.0/1", "via", serverVPNIPStr, "dev", ifaceName)
if err := runCommand(cmd); err != nil {
return originalGateway, fmt.Errorf("failed to add route 0.0.0.0/1: %w", err)
}
cmd = exec.Command("ip", "route", "add", "128.0.0.0/1", "via", serverVPNIPStr, "dev", ifaceName)
if err := runCommand(cmd); err != nil {
// Attempt cleanup of the first route before failing
cleanupCmd := exec.Command("ip", "route", "del", "0.0.0.0/1")
runCommand(cleanupCmd)
return originalGateway, fmt.Errorf("failed to add route 128.0.0.0/1: %w", err)
}
} else {
// Route only the VPN subnet traffic through the tunnel
log.Printf("Configuring to route VPN subnet %s through VPN...", vpnSubnet)
cmd := exec.Command("ip", "route", "add", vpnSubnet, "via", serverVPNIPStr, "dev", ifaceName)
if err := runCommand(cmd); err != nil {
return originalGateway, fmt.Errorf("failed to add route for VPN subnet %s: %w", vpnSubnet, err)
}
}
return originalGateway, nil // Success
}
// cleanupRouting removes the routes added by setupRouting
func cleanupRouting(ifaceName, vpnSubnet, serverAddr, originalGateway string, routeAll bool) {
log.Println(“Cleaning up routing rules…”)
serverIP := strings.Split(serverAddr, “:”)[0]
if routeAll {
cmd := exec.Command("ip", "route", "del", "0.0.0.0/1")
if err := runCommand(cmd); err != nil {
log.Printf("Warning: Failed to delete route 0.0.0.0/1: %v", err)
}
cmd = exec.Command("ip", "route", "del", "128.0.0.0/1")
if err := runCommand(cmd); err != nil {
log.Printf("Warning: Failed to delete route 128.0.0.0/1: %v", err)
}
} else {
// Delete route for the VPN subnet
cmd := exec.Command("ip", "route", "del", vpnSubnet)
if err := runCommand(cmd); err != nil {
log.Printf("Warning: Failed to delete route for %s: %v", vpnSubnet, err)
}
}
// Delete the explicit route to the VPN server
if originalGateway != "" { // Only if we added it
cmd := exec.Command("ip", "route", "del", serverIP)
if err := runCommand(cmd); err != nil {
log.Printf("Warning: Failed to delete route to server %s: %v", serverIP, err)
}
}
// Note: Restoring the original default gateway is complex if multiple existed
// or if other network changes happened. The deletion of the overriding routes
// (0/1, 128/1) usually allows the original default to become active again.
// For robust cleanup, more sophisticated state tracking or network management tools are needed.
log.Println("Routing cleanup attempted.")
}
“`
客户端代码说明:
- Flags: 定义客户端配置参数(服务器地址、本地 TUN IP、证书路径、是否路由所有流量等)。
createTUN
: 与服务器类似,创建 TUN 设备并配置其 IP 地址。关键区别在于,客户端通常配置为点对点模式,需要指定peer
地址(即服务器的 VPN TUN IP 地址)。loadTLSConfig
: 加载客户端证书/密钥,并加载 CA 证书以验证服务器证书。RootCAs
指定了客户端信任的 CA,ServerName
用于 SNI (Server Name Indication) 和主机名验证。main
:- 解析 flags。
- 创建 TUN 接口。
- 加载 TLS 配置。
- 使用
tls.Dial
连接到服务器。进行基本的主机名验证(对自签名证书可能需要注意)。 - 调用
setupRouting
配置操作系统路由。 - 使用
defer cleanupRouting
确保在程序退出时尝试清理路由规则。 - 启动两个 goroutine (
handleClient
中的逻辑被直接移入 main 的 goroutines) 来处理 TUN 与服务器之间的数据转发,与服务器端的handleClient
逻辑镜像。 - 使用
sync.WaitGroup
等待转发 goroutine 结束。
setupRouting
: 这是客户端的关键部分,也是 OS 依赖性最强的部分。- 获取服务器的公网 IP。
- 获取原始网关: 保存当前的默认网关,以便后续可以添加指向 VPN 服务器公网 IP 的静态路由,防止在修改默认路由后无法连接服务器。
- 添加服务器路由: 添加一条静态路由,确保发往 VPN 服务器公网 IP 的流量仍然通过原始网关,而不是尝试通过尚未完全建立的 VPN 隧道。
- 添加 VPN 路由:
- 如果
routeAll
为true
,则添加两条覆盖默认路由的规则 (0.0.0.0/1
,128.0.0.0/1
),将所有流量(除了到服务器本身的流量)指向服务器的 VPN TUN IP 地址(通过本地 TUN 接口)。这是一种常见的强制所有流量通过 VPN 的方法。 - 如果
routeAll
为false
,则只添加一条规则,将发往 VPN 子网(例如10.0.0.0/24
)的流量指向服务器的 VPN TUN IP。
- 如果
- 使用
ip route add
命令 (Linux 示例)。macOS 使用route add
,Windows 使用route add
,语法不同。
cleanupRouting
: 尝试撤销setupRouting
所做的更改,删除添加的路由。路由清理可能很复杂,尤其是在routeAll
模式下。这个实现比较基础。getOriginalGateway
: 一个简单的辅助函数,尝试解析ip route show default
的输出来找到当前的默认网关(仅适用于 Linux)。
编译和运行
- 编译服务器:
bash
go build -o govpn-server server.go - 编译客户端:
bash
go build -o govpn-client client.go -
准备文件:
- 在服务器上放置
govpn-server
,ca.crt
,server.crt
,server.key
。 - 在客户端上放置
govpn-client
,ca.crt
,client.crt
,client.key
。
- 在服务器上放置
-
运行服务器(需要 root/sudo 权限):
- 启用 IP 转发和配置 NAT(一次性设置):
bash
sudo sysctl -w net.ipv4.ip_forward=1
# 将 eth0 替换为你的实际公网网卡名
sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
# 获取 TUN 接口名(可能需要先运行一次 server 看日志,或指定名称)
# TUN_IFACE=$(./govpn-server -local 10.0.0.1/24 & PID=$!; sleep 1; TUN_IFACE_NAME=$(ip -o tuntap show | cut -d: -f1); kill $PID; echo $TUN_IFACE_NAME)
# 假设 TUN 接口名为 tun0
sudo iptables -A FORWARD -i tun0 -o eth0 -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o tun0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# (确保保存 iptables 规则,如使用 iptables-persistent) - 启动服务器:
bash
sudo ./govpn-server -local 10.0.0.1/24 -listen ":8443" -cert server.crt -key server.key -ca ca.crt -v
- 启用 IP 转发和配置 NAT(一次性设置):
-
运行客户端(需要 root/sudo 权限):
- 启动客户端(路由 VPN 子网):
bash
# 将 YOUR_SERVER_IP 替换为服务器的公网 IP 地址
sudo ./govpn-client -server YOUR_SERVER_IP:8443 -local 10.0.0.2/24 -subnet 10.0.0.0/24 -cert client.crt -key client.key -ca ca.crt -v - 启动客户端(路由所有流量):
bash
sudo ./govpn-client -server YOUR_SERVER_IP:8443 -local 10.0.0.2/24 -subnet 10.0.0.0/24 -cert client.crt -key client.key -ca ca.crt -route-all -v
- 启动客户端(路由 VPN 子网):
-
测试:
- 在客户端上,尝试
ping 10.0.0.1
(服务器的 VPN IP)。 - 如果服务器配置了 NAT 并且客户端设置了
route-all
,尝试访问互联网网站(例如curl ifconfig.me
),应该显示服务器的公网 IP。 - 尝试从客户端访问服务器本地网络上的其他资源(如果服务器防火墙允许)。
- 在客户端上,尝试
安全考虑和局限性
- 证书管理: 本教程使用自签名证书,仅适用于测试。生产环境需要健壮的 PKI(公钥基础设施),包括证书吊销列表 (CRL) 或 OCSP (Online Certificate Status Protocol)。
- 身份验证: TLS 客户端证书提供了基本的身份验证,但可以添加额外的用户/密码或令牌认证层。
- 数据包完整性: TLS 提供了传输层的完整性,但 VPN 协议本身可能需要额外的检查(例如,防止重放攻击,虽然 TLS 也有一定保护)。
- 错误处理和健壮性: 代码中的错误处理是基础的。生产级 VPN 需要更细致地处理网络中断、超时、资源耗尽等情况,并可能需要自动重连逻辑。
- 性能:
- TCP over TCP 问题:将 TCP 流量封装在基于 TCP 的 VPN 隧道(如我们这里实现的)中,可能导致性能下降和连接问题(称为 “TCP Meltdown”)。使用 UDP 作为底层传输协议通常是更好的选择(例如 DTLS 或基于 QUIC 的隧道),但这会增加复杂性(需要处理丢包、重排、拥塞控制)。
- 加解密开销:TLS 加解密会消耗 CPU 资源。Go 的 TLS 实现是高效的,但对于极高吞吐量的场景可能需要硬件加速或优化。
- 上下文切换和拷贝:数据在内核(TUN)、用户空间(Go 程序)和网络套接字之间拷贝会带来开销。
- 并发模型: 当前服务器为每个客户端启动了 2 个 goroutine 进行转发。对于大量客户端,这可能会消耗较多资源。可以考虑更优化的 I/O 模型(例如,使用 epoll/kqueue 配合单个或少量 goroutine 处理多个连接)。
- 平台兼容性: TUN/TAP 的创建和配置、路由命令在不同操作系统(Linux, macOS, Windows, BSD)之间差异很大。
water
库提供了一定的抽象,但 OS 级别的配置仍然需要平台特定的代码或脚本。 - IP 地址分配: 本教程使用静态 IP 地址。真实的 VPN 通常需要动态 IP 地址分配(类似 DHCP)和管理。
- DNS泄漏: 如果客户端配置为路由所有流量,但仍然使用本地 ISP 的 DNS 服务器,则可能发生 DNS 泄漏,暴露用户的浏览活动。需要配置客户端使用 VPN 内部或特定的安全 DNS 服务器。
改进和下一步
- 切换到 UDP: 使用
net.ListenPacket
和conn.WriteTo/ReadFrom
,并结合 DTLS(Go 的标准库没有内置 DTLS 服务器,需要第三方库或自己实现)或自定义加密协议(如 WireGuard 使用的 Noise 协议框架 + ChaCha20Poly1305)。 - 实现动态 IP 分配: 在服务器端维护一个 IP 地址池,并在客户端连接时分配一个未使用的 IP。
- 添加用户认证: 在 TLS 握手之后,实现用户名/密码或其他认证机制。
- 配置文件: 使用 JSON, YAML 或 TOML 文件来管理配置,而不是完全依赖命令行标志。
- Keepalive 机制: 实现心跳包来检测断开的连接,尤其是在使用 UDP 时。
- 压缩: 对隧道内的数据进行压缩(例如使用 zlib 或 snappy)可以节省带宽,但会增加 CPU 开销。
- IPv6 支持: 扩展代码以支持 IPv6 地址和路由。
- 更完善的路由管理: 使用更健壮的库或方法来管理路由,处理复杂的场景和清理。
- Windows 支持: 使用适用于 Windows 的 TUN/TAP 驱动(如 OpenVPN 的
tap-windows
或wintun
),并编写相应的 Go 代码和配置脚本。
结论
通过本教程,我们使用 Go 语言成功构建了一个基础的点对点 TUN VPN。我们学习了如何创建和配置 TUN 接口,如何使用 crypto/tls
建立安全的客户端-服务器连接,以及如何在两者之间转发 IP 数据包。我们还探讨了实现 VPN 所需的关键操作系统配置,如 IP 转发和路由。
虽然这个实现是教学性质的,但它展示了 Go 语言在网络编程方面的强大能力和简洁性。Go 的并发特性、标准库以及活跃的社区生态(如 water
库)使得构建复杂的网络服务变得更加容易。要构建生产级的 VPN,还需要在安全性、健壮性、性能和跨平台兼容性方面进行大量的额外工作,但这个基础项目为你提供了一个坚实的起点。希望本教程能激发你进一步探索网络编程和 Go 语言的兴趣!