feat: implement VPN platform management and real protocol services

This commit is contained in:
Logan.Li committed 2026-07-22 21:49:13 +08:00
commit 58c036c34c
45 files changed
+9244

No files matched your search

@@ -0,0 +1,676 @@
// Package wireguard manages real WireGuard interfaces through wg-quick and wg.
// Every tunnel owns one interface and any number of peers. Private material is
// retained in Tunnel.Secrets; the JSON configuration only contains public keys.
package wireguard
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/crypto/curve25519"
"vpn-platform/backend/internal/core"
"vpn-platform/backend/internal/model"
"vpn-platform/backend/internal/service"
"vpn-platform/backend/internal/store"
)
const (
defaultPort = 51820
maxListenPort = 60999
handshakeAlive = 3 * time.Minute
)
var (
peerIDPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$`)
ifacePattern = regexp.MustCompile(`^[a-zA-Z0-9_.-]{1,15}$`)
portMu sync.Mutex
)
type Service struct{}
// peer is the public, persisted description of a WireGuard peer. private_key
// and preshared_key deliberately never appear here.
type peer struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
PublicKey string `json:"public_key"`
ClientAddress string `json:"client_address"`
AllowedIPs string `json:"allowed_ips,omitempty"`
Enabled bool `json:"enabled"`
Managed bool `json:"managed"`
}
type peerSecret struct {
PrivateKey string `json:"private_key,omitempty"`
PresharedKey string `json:"preshared_key,omitempty"`
}
func New() *Service { return &Service{} }
func (s *Service) Name() string { return "wireguard" }
func (s *Service) DisplayName() string { return "WireGuard" }
func (s *Service) Available() (bool, string) {
if p, ok := service.LookBin("wg-quick"); ok {
if _, wgOK := service.LookBin("wg"); wgOK {
return true, p
}
}
return false, "未检测到 wireguard-tools(需要 wg 与 wg-quick)"
}
// genKey generates a WireGuard-compatible Curve25519 private/public key pair.
func genKey() (priv, pub string, err error) {
var sk [32]byte
if _, err = rand.Read(sk[:]); err != nil {
return
}
sk[0] &= 248
sk[31] = (sk[31] & 127) | 64
pk, err := curve25519.X25519(sk[:], curve25519.Basepoint)
if err != nil {
return
}
priv = base64.StdEncoding.EncodeToString(sk[:])
pub = base64.StdEncoding.EncodeToString(pk)
return
}
func publicKey(privateKey string) (string, error) {
raw, err := base64.StdEncoding.DecodeString(privateKey)
if err != nil || len(raw) != curve25519.ScalarSize {
return "", fmt.Errorf("WireGuard 私钥格式非法")
}
pub, err := curve25519.X25519(raw, curve25519.Basepoint)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(pub), nil
}
func isWireGuardKey(key string) bool {
raw, err := base64.StdEncoding.DecodeString(key)
return err == nil && len(raw) == curve25519.PointSize
}
func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{}, error) {
if err := service.MustValidate(cfg, "address"); err != nil {
return nil, err
}
out := map[string]interface{}{}
for k, v := range cfg {
out[k] = v
}
if !validCIDR(service.Str(out, "address")) {
return nil, fmt.Errorf("address 必须是合法 CIDR")
}
port := int(service.Num(out, "listen_port", 0))
if port < 0 || port > 65535 || (port > 0 && port < 1024) {
return nil, fmt.Errorf("listen_port 必须是 1024-65535 之间的 UDP 端口")
}
// Port zero means "allocate a free WireGuard port when this tunnel is created".
if port > 0 {
out["listen_port"] = port
}
if service.Str(out, "allowed_ips") == "" {
out["allowed_ips"] = "0.0.0.0/0, ::/0"
}
if err := validateCIDRList(service.Str(out, "allowed_ips")); err != nil {
return nil, fmt.Errorf("allowed_ips: %w", err)
}
if service.Str(out, "dns") == "" {
out["dns"] = "223.5.5.5"
}
if service.Num(out, "keepalive", 0) == 0 {
out["keepalive"] = 25
}
keepalive := int(service.Num(out, "keepalive", 25))
if keepalive < 0 || keepalive > 65535 {
return nil, fmt.Errorf("keepalive 必须是 0-65535")
}
if egress := service.Str(out, "egress_interface"); egress != "" && !ifacePattern.MatchString(egress) {
return nil, fmt.Errorf("egress_interface 包含非法字符")
}
if service.Bool(out, "enable_nat", false) && service.Str(out, "egress_interface") == "" {
return nil, fmt.Errorf("启用 NAT 时必须填写 egress_interface")
}
if _, supplied := out["peers"]; supplied {
peers, err := parsePeers(out)
if err != nil {
return nil, err
}
if err := validatePeerInput(peers); err != nil {
return nil, err
}
} else if addr := service.Str(out, "client_address"); addr != "" && !validCIDR(addr) {
return nil, fmt.Errorf("client_address 必须是合法 CIDR")
}
return out, nil
}
func (s *Service) Create(t *model.Tunnel) error {
// Config allocation and persistence need to be serialised: otherwise two
// concurrent create requests can both observe the same currently-free port.
portMu.Lock()
defer portMu.Unlock()
cfg, err := store.TunnelConfig(t)
if err != nil {
return err
}
secrets, err := store.TunnelSecrets(t)
if err != nil {
return err
}
if int(service.Num(cfg, "listen_port", 0)) == 0 {
port, err := allocatePort(t.ID)
if err != nil {
return err
}
cfg["listen_port"] = port
} else if err := ensurePortAvailable(t.ID, int(service.Num(cfg, "listen_port", 0))); err != nil {
return err
}
cfg["interface_name"] = interfaceName(t)
serverPrivate, _ := secrets["server_private_key"].(string)
if serverPrivate == "" {
var serverPublic string
serverPrivate, serverPublic, err = genKey()
if err != nil {
return err
}
secrets["server_private_key"] = serverPrivate
cfg["server_public_key"] = serverPublic
} else {
serverPublic, err := publicKey(serverPrivate)
if err != nil {
return err
}
cfg["server_public_key"] = serverPublic
}
peers, peerSecrets, err := buildPeers(cfg, secrets)
if err != nil {
return err
}
if len(peers) == 0 {
return fmt.Errorf("至少需要一个 WireGuard Peer")
}
cfg["peers"] = peersToConfig(peers)
// Legacy single-client fields must not leak into Config or conflict with peers.
delete(cfg, "client_address")
delete(cfg, "client_public_key")
delete(secrets, "client_private_key")
delete(secrets, "preshared_key")
secrets["peer_secrets"] = peerSecretsToConfig(peerSecrets)
serverConf := renderServerConfig(cfg, serverPrivate, peers, peerSecrets)
if _, err := service.WriteConfig(t, interfaceName(t)+".conf", serverConf); err != nil {
return err
}
// client.conf remains a convenience export for the first platform-managed peer.
if clientConf, err := generateClientConfig(cfg, peers, peerSecrets, ""); err == nil {
if _, err := service.WriteConfig(t, "client.conf", clientConf); err != nil {
return err
}
}
public, sec := service.MergeSecrets(cfg, "server_private_key")
for k, v := range secrets {
sec[k] = v
}
return service.SaveTunnelConfig(t, public, sec)
}
// GenerateClientConfig exports the first managed peer for backward compatible
// downloads. Use GeneratePeerClientConfig when the caller has selected a peer.
func (s *Service) GenerateClientConfig(t *model.Tunnel) (string, error) {
cfg, err := store.TunnelConfig(t)
if err != nil {
return "", err
}
peers, err := parsePeers(cfg)
if err != nil {
return "", err
}
secrets, err := store.TunnelSecrets(t)
if err != nil {
return "", err
}
return generateClientConfig(cfg, peers, parsePeerSecrets(secrets), "")
}
// GeneratePeerClientConfig generates one device's configuration without exposing
// any other peer's key material.
func (s *Service) GeneratePeerClientConfig(t *model.Tunnel, peerID string) (string, error) {
cfg, err := store.TunnelConfig(t)
if err != nil {
return "", err
}
secrets, err := store.TunnelSecrets(t)
if err != nil {
return "", err
}
peers, err := parsePeers(cfg)
if err != nil {
return "", err
}
peerSecrets := parsePeerSecrets(secrets)
return generateClientConfig(cfg, peers, peerSecrets, peerID)
}
func generateClientConfig(cfg map[string]interface{}, peers []peer, peerSecrets map[string]peerSecret, requestedID string) (string, error) {
for _, p := range peers {
if requestedID != "" && p.ID != requestedID {
continue
}
if requestedID == "" && (!p.Managed || !p.Enabled) {
continue
}
sec := peerSecrets[p.ID]
if !p.Managed || sec.PrivateKey == "" {
return "", fmt.Errorf("Peer %s 为外部设备,平台未保存其私钥", p.ID)
}
endpoint := service.StrOr(cfg, "public_endpoint", fmt.Sprintf("your-server-ip:%d", int(service.Num(cfg, "listen_port", defaultPort))))
allowed := p.AllowedIPs
if allowed == "" {
allowed = service.StrOr(cfg, "allowed_ips", "0.0.0.0/0, ::/0")
}
return fmt.Sprintf(`[Interface]
PrivateKey = %s
Address = %s
DNS = %s
[Peer]
PublicKey = %s
PresharedKey = %s
Endpoint = %s
AllowedIPs = %s
PersistentKeepalive = %d
`, sec.PrivateKey, p.ClientAddress, service.StrOr(cfg, "dns", "223.5.5.5"), service.Str(cfg, "server_public_key"), sec.PresharedKey, endpoint, allowed, int(service.Num(cfg, "keepalive", 25))), nil
}
if requestedID != "" {
return "", fmt.Errorf("WireGuard Peer 不存在: %s", requestedID)
}
return "", fmt.Errorf("没有可下载配置的托管 Peer;外部 Peer 请在其设备上配置")
}
func (s *Service) Start(t *model.Tunnel) error {
if available, _ := s.Available(); !available {
return fmt.Errorf("无法启动 WireGuard:未检测到 wg 和 wg-quick,请先安装 wireguard-tools")
}
if _, err := service.Run("wg", "show", interfaceName(t)); err == nil {
return nil
}
conf := fmt.Sprintf("%s/%s.conf", service.InstanceDir(t), interfaceName(t))
if _, err := service.Run("wg-quick", "up", conf); err != nil {
return err
}
return nil
}
func (s *Service) Stop(t *model.Tunnel) error {
if _, ok := service.LookBin("wg-quick"); !ok {
return fmt.Errorf("无法停止 WireGuard:未检测到 wg-quick")
}
if _, ok := service.LookBin("wg"); ok {
if _, err := service.Run("wg", "show", interfaceName(t)); err != nil {
return nil // already stopped; do not claim a fake process was stopped
}
}
conf := fmt.Sprintf("%s/%s.conf", service.InstanceDir(t), interfaceName(t))
_, err := service.Run("wg-quick", "down", conf)
return err
}
func (s *Service) Status(t *model.Tunnel) (core.Status, error) {
bin, ok := service.LookBin("wg")
if !ok {
return service.NowStatus(false, "undeployed", "配置已生成,但未检测到 wireguard-tools(wg),尚未部署"), nil
}
dump, err := service.Run(bin, "show", interfaceName(t), "dump")
if err != nil {
return service.NowStatus(false, "stopped", "WireGuard 接口未启动"), nil
}
cfg, err := store.TunnelConfig(t)
if err != nil {
return core.Status{}, err
}
peers, err := parsePeers(cfg)
if err != nil {
return core.Status{}, err
}
byKey := make(map[string]peer, len(peers))
for _, p := range peers {
byKey[p.PublicKey] = p
}
st := service.NowStatus(true, "running", "WireGuard 接口运行中")
connected := 0
for _, line := range strings.Split(strings.TrimSpace(dump), "\n") {
fields := strings.Split(line, "\t")
if len(fields) < 8 { // the first interface row has five columns
continue
}
p, known := byKey[fields[0]]
if !known {
// Preserve visibility for a manually added peer instead of silently
// attributing it to a configured client.
p = peer{ID: "unknown-" + fields[0][:min(8, len(fields[0]))], PublicKey: fields[0], Enabled: true}
}
handshake, _ := strconv.ParseInt(fields[4], 10, 64)
rx, _ := strconv.ParseInt(fields[5], 10, 64)
tx, _ := strconv.ParseInt(fields[6], 10, 64)
alive := handshake > 0 && time.Since(time.Unix(handshake, 0)) <= handshakeAlive
if alive {
connected++
}
st.RxBytes += rx
st.TxBytes += tx
st.Peers = append(st.Peers, core.PeerStatus{
ID: p.ID, Name: p.Name, PublicKey: fields[0], Endpoint: fields[2], AllowedIPs: fields[3],
LatestHandshakeAt: handshake, Connected: alive, RxBytes: rx, TxBytes: tx,
})
}
sort.Slice(st.Peers, func(i, j int) bool { return st.Peers[i].ID < st.Peers[j].ID })
st.Detail = fmt.Sprintf("WireGuard 接口 %s 运行中,%d/%d 个 Peer 最近已握手", interfaceName(t), connected, len(st.Peers))
return st, nil
}
func (s *Service) Delete(t *model.Tunnel) error { return s.Stop(t) }
// interfaceName is deterministic, <= IFNAMSIZ-1 (15) and does not depend on
// the user supplied tunnel name. This prevents cross-tunnel interface clashes.
func interfaceName(t *model.Tunnel) string { return "wg" + strconv.FormatUint(uint64(t.ID), 36) }
func allocatePort(tunnelID uint) (int, error) {
for port := defaultPort; port <= maxListenPort; port++ {
if ensurePortAvailable(tunnelID, port) == nil {
return port, nil
}
}
return 0, fmt.Errorf("没有可用的 WireGuard UDP 端口(%d-%d)", defaultPort, maxListenPort)
}
func ensurePortAvailable(tunnelID uint, port int) error {
var tunnels []model.Tunnel
if store.DB == nil {
return nil
}
if err := store.DB.Where("protocol = ? AND id <> ?", "wireguard", tunnelID).Find(&tunnels).Error; err != nil {
return err
}
for _, other := range tunnels {
cfg, err := store.TunnelConfig(&other)
if err == nil && int(service.Num(cfg, "listen_port", defaultPort)) == port {
return fmt.Errorf("WireGuard UDP 端口 %d 已被专线 %q 占用", port, other.Name)
}
}
if !hostUDPPortAvailable(port) {
return fmt.Errorf("WireGuard UDP 端口 %d 已被本机其他服务占用", port)
}
return nil
}
func hostUDPPortAvailable(port int) bool {
listener, err := net.ListenUDP("udp4", &net.UDPAddr{Port: port})
if err != nil {
return false
}
return listener.Close() == nil
}
func buildPeers(cfg, secrets map[string]interface{}) ([]peer, map[string]peerSecret, error) {
peers, err := parsePeers(cfg)
if err != nil {
return nil, nil, err
}
// Compatibility migration: old one-client configs become client-1.
if len(peers) == 0 {
address := service.StrOr(cfg, "client_address", "10.8.0.2/32")
if !validCIDR(address) {
return nil, nil, fmt.Errorf("client_address 必须是合法 CIDR")
}
peers = []peer{{ID: "client-1", Name: "客户端 1", PublicKey: service.Str(cfg, "client_public_key"), ClientAddress: address, AllowedIPs: service.Str(cfg, "allowed_ips"), Enabled: true, Managed: true}}
}
oldSecrets := parsePeerSecrets(secrets)
if legacyPrivate := service.Str(secrets, "client_private_key"); legacyPrivate != "" {
oldSecrets["client-1"] = peerSecret{PrivateKey: legacyPrivate, PresharedKey: service.Str(secrets, "preshared_key")}
}
seenIDs, seenKeys, seenAddresses := map[string]bool{}, map[string]bool{}, map[string]bool{}
for i := range peers {
p := &peers[i]
if p.ID == "" {
p.ID = fmt.Sprintf("client-%d", i+1)
}
if !peerIDPattern.MatchString(p.ID) {
return nil, nil, fmt.Errorf("Peer ID %q 包含非法字符", p.ID)
}
if seenIDs[p.ID] {
return nil, nil, fmt.Errorf("Peer ID 重复: %s", p.ID)
}
seenIDs[p.ID] = true
if p.ClientAddress == "" || !validCIDR(p.ClientAddress) {
return nil, nil, fmt.Errorf("Peer %s 的 client_address 必须是合法 CIDR", p.ID)
}
if seenAddresses[p.ClientAddress] {
return nil, nil, fmt.Errorf("Peer client_address 重复: %s", p.ClientAddress)
}
seenAddresses[p.ClientAddress] = true
if p.AllowedIPs == "" {
p.AllowedIPs = service.StrOr(cfg, "allowed_ips", "0.0.0.0/0, ::/0")
}
if err := validateCIDRList(p.AllowedIPs); err != nil {
return nil, nil, fmt.Errorf("Peer %s allowed_ips: %w", p.ID, err)
}
if strings.ContainsAny(p.Name, "\r\n") {
return nil, nil, fmt.Errorf("Peer %s 的 name 不能包含换行符", p.ID)
}
sec := oldSecrets[p.ID]
// An unchanged public key with a stored private key is still a platform
// managed peer even if a client omitted the derived `managed` flag when
// submitting an update. A changed public key deliberately becomes an
// external peer and must not retain the old device private key/PSK.
if p.PublicKey != "" && sec.PrivateKey != "" {
existingPublic, err := publicKey(sec.PrivateKey)
if err != nil {
return nil, nil, fmt.Errorf("Peer %s: %w", p.ID, err)
}
if existingPublic == p.PublicKey {
p.Managed = true
} else {
p.Managed = false
sec.PrivateKey = ""
sec.PresharedKey = ""
}
}
if p.PublicKey == "" {
if sec.PrivateKey != "" {
pub, err := publicKey(sec.PrivateKey)
if err != nil {
return nil, nil, fmt.Errorf("Peer %s: %w", p.ID, err)
}
p.PublicKey, p.Managed = pub, true
} else {
priv, pub, err := genKey()
if err != nil {
return nil, nil, err
}
p.PublicKey, sec.PrivateKey, p.Managed = pub, priv, true
}
}
if !isWireGuardKey(p.PublicKey) {
return nil, nil, fmt.Errorf("Peer %s 的 public_key 不是 WireGuard 公钥", p.ID)
}
if seenKeys[p.PublicKey] {
return nil, nil, fmt.Errorf("Peer public_key 重复: %s", p.ID)
}
seenKeys[p.PublicKey] = true
if sec.PresharedKey == "" {
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
return nil, nil, err
}
sec.PresharedKey = base64.StdEncoding.EncodeToString(raw)
}
if !isWireGuardKey(sec.PresharedKey) {
return nil, nil, fmt.Errorf("Peer %s 的 preshared_key 格式非法", p.ID)
}
oldSecrets[p.ID] = sec
}
return peers, oldSecrets, nil
}
func parsePeers(cfg map[string]interface{}) ([]peer, error) {
raw, exists := cfg["peers"]
if !exists || raw == nil {
return nil, nil
}
items, ok := raw.([]interface{})
if !ok {
// JSON retrieved from store uses []interface{}, but accepting []map makes
// service-level callers and tests practical as well.
if maps, mapsOK := raw.([]map[string]interface{}); mapsOK {
items = make([]interface{}, len(maps))
for i := range maps {
items[i] = maps[i]
}
} else {
return nil, fmt.Errorf("peers 必须是数组")
}
}
peers := make([]peer, 0, len(items))
for _, item := range items {
m, ok := item.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("peers 项必须是对象")
}
p := peer{ID: service.Str(m, "id"), Name: service.Str(m, "name"), PublicKey: service.Str(m, "public_key"), ClientAddress: service.Str(m, "client_address"), AllowedIPs: service.Str(m, "allowed_ips"), Managed: service.Bool(m, "managed", false), Enabled: true}
if _, set := m["enabled"]; set {
p.Enabled = service.Bool(m, "enabled", true)
}
peers = append(peers, p)
}
return peers, nil
}
func validatePeerInput(peers []peer) error {
seenIDs, seenKeys, seenAddresses := map[string]bool{}, map[string]bool{}, map[string]bool{}
for i := range peers {
p := peers[i]
if p.ID == "" {
p.ID = fmt.Sprintf("client-%d", i+1)
}
if !peerIDPattern.MatchString(p.ID) {
return fmt.Errorf("Peer ID %q 包含非法字符", p.ID)
}
if seenIDs[p.ID] {
return fmt.Errorf("Peer ID 重复: %s", p.ID)
}
seenIDs[p.ID] = true
if !validCIDR(p.ClientAddress) {
return fmt.Errorf("Peer %s 的 client_address 必须是合法 CIDR", p.ID)
}
if seenAddresses[p.ClientAddress] {
return fmt.Errorf("Peer client_address 重复: %s", p.ClientAddress)
}
seenAddresses[p.ClientAddress] = true
if p.PublicKey != "" {
if !isWireGuardKey(p.PublicKey) {
return fmt.Errorf("Peer %s 的 public_key 不是 WireGuard 公钥", p.ID)
}
if seenKeys[p.PublicKey] {
return fmt.Errorf("Peer public_key 重复: %s", p.ID)
}
seenKeys[p.PublicKey] = true
}
if p.AllowedIPs != "" {
if err := validateCIDRList(p.AllowedIPs); err != nil {
return fmt.Errorf("Peer %s allowed_ips: %w", p.ID, err)
}
}
if strings.ContainsAny(p.Name, "\r\n") {
return fmt.Errorf("Peer %s 的 name 不能包含换行符", p.ID)
}
}
return nil
}
func peersToConfig(peers []peer) []map[string]interface{} {
out := make([]map[string]interface{}, 0, len(peers))
for _, p := range peers {
out = append(out, map[string]interface{}{"id": p.ID, "name": p.Name, "public_key": p.PublicKey, "client_address": p.ClientAddress, "allowed_ips": p.AllowedIPs, "enabled": p.Enabled, "managed": p.Managed})
}
return out
}
func parsePeerSecrets(secrets map[string]interface{}) map[string]peerSecret {
out := map[string]peerSecret{}
raw, ok := secrets["peer_secrets"].(map[string]interface{})
if !ok {
return out
}
for id, value := range raw {
m, ok := value.(map[string]interface{})
if !ok {
continue
}
out[id] = peerSecret{PrivateKey: service.Str(m, "private_key"), PresharedKey: service.Str(m, "preshared_key")}
}
return out
}
func peerSecretsToConfig(secrets map[string]peerSecret) map[string]interface{} {
out := make(map[string]interface{}, len(secrets))
for id, sec := range secrets {
out[id] = map[string]interface{}{"private_key": sec.PrivateKey, "preshared_key": sec.PresharedKey}
}
return out
}
func renderServerConfig(cfg map[string]interface{}, serverPrivate string, peers []peer, secrets map[string]peerSecret) string {
var b strings.Builder
fmt.Fprintf(&b, "[Interface]\nPrivateKey = %s\nAddress = %s\nListenPort = %d\n", serverPrivate, service.Str(cfg, "address"), int(service.Num(cfg, "listen_port", defaultPort)))
if service.Bool(cfg, "enable_nat", false) {
egress := service.Str(cfg, "egress_interface")
fmt.Fprintf(&b, "PostUp = iptables -A FORWARD -i %%i -j ACCEPT; iptables -A FORWARD -o %%i -j ACCEPT; iptables -t nat -A POSTROUTING -o %s -j MASQUERADE\n", egress)
fmt.Fprintf(&b, "PostDown = iptables -D FORWARD -i %%i -j ACCEPT; iptables -D FORWARD -o %%i -j ACCEPT; iptables -t nat -D POSTROUTING -o %s -j MASQUERADE\n", egress)
}
for _, p := range peers {
if !p.Enabled {
continue
}
fmt.Fprintf(&b, "\n[Peer]\n# %s (%s)\nPublicKey = %s\nPresharedKey = %s\nAllowedIPs = %s\n", p.Name, p.ID, p.PublicKey, secrets[p.ID].PresharedKey, p.ClientAddress)
}
return b.String()
}
func validCIDR(value string) bool {
_, _, err := net.ParseCIDR(strings.TrimSpace(value))
return err == nil
}
func validateCIDRList(value string) error {
for _, entry := range strings.Split(value, ",") {
if !validCIDR(entry) {
return fmt.Errorf("%q 不是 CIDR", strings.TrimSpace(entry))
}
}
return nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
@@ -0,0 +1,127 @@
package wireguard
import (
"encoding/json"
"os"
"strconv"
"strings"
"testing"
"vpn-platform/backend/internal/model"
"vpn-platform/backend/internal/store"
)
func TestBuildPeersMigratesLegacySingleClient(t *testing.T) {
cfg := map[string]interface{}{
"address": "10.8.0.1/24",
"client_address": "10.8.0.2/32",
"allowed_ips": "0.0.0.0/0, ::/0",
}
peers, secrets, err := buildPeers(cfg, map[string]interface{}{})
if err != nil {
t.Fatalf("buildPeers: %v", err)
}
if len(peers) != 1 || peers[0].ID != "client-1" || !peers[0].Managed {
t.Fatalf("legacy peer migration = %#v", peers)
}
if !isWireGuardKey(peers[0].PublicKey) || !isWireGuardKey(secrets["client-1"].PrivateKey) || !isWireGuardKey(secrets["client-1"].PresharedKey) {
t.Fatal("generated peer keys are not WireGuard keys")
}
cfg["server_public_key"] = "server-public-key"
cfg["listen_port"] = 51820
client, err := generateClientConfig(cfg, peers, secrets, "client-1")
if err != nil {
t.Fatalf("generateClientConfig: %v", err)
}
if !strings.Contains(client, "Address = 10.8.0.2/32") || !strings.Contains(client, "Endpoint = your-server-ip:51820") {
t.Fatalf("unexpected client config:\n%s", client)
}
}
func TestBuildPeersKeepsExternalPeerPrivateKeyOutOfPlatform(t *testing.T) {
_, externalPublic, err := genKey()
if err != nil {
t.Fatal(err)
}
cfg := map[string]interface{}{
"allowed_ips": "10.0.0.0/8",
"peers": []interface{}{map[string]interface{}{
"id": "branch-office",
"name": "Branch office",
"public_key": externalPublic,
"client_address": "10.8.0.10/32",
"enabled": true,
}},
}
peers, secrets, err := buildPeers(cfg, map[string]interface{}{})
if err != nil {
t.Fatalf("buildPeers: %v", err)
}
if peers[0].Managed || secrets["branch-office"].PrivateKey != "" {
t.Fatalf("external peer must not gain a platform-held private key: %#v %#v", peers[0], secrets["branch-office"])
}
if _, err := generateClientConfig(cfg, peers, secrets, "branch-office"); err == nil {
t.Fatal("external peer configuration should not be downloadable")
}
}
func TestValidateRejectsUnsafeNATAndInvalidPeerAddress(t *testing.T) {
s := New()
if _, err := s.Validate(map[string]interface{}{"address": "10.8.0.1/24", "enable_nat": true}); err == nil {
t.Fatal("NAT without explicit egress interface was accepted")
}
_, err := s.Validate(map[string]interface{}{
"address": "10.8.0.1/24",
"peers": []interface{}{map[string]interface{}{"id": "bad", "client_address": "not-a-cidr"}},
})
if err == nil {
t.Fatal("invalid peer address was accepted")
}
}
func TestInterfaceNameFitsLinuxLimit(t *testing.T) {
name := interfaceName(&model.Tunnel{ID: ^uint(0)})
if len(name) > 15 {
t.Fatalf("interface name %q exceeds IFNAMSIZ-1", name)
}
}
func TestCreatePersistsOnlyPublicPeerData(t *testing.T) {
dataDir := t.TempDir()
t.Setenv("VPN_DATA_DIR", dataDir)
if err := store.Init(dataDir); err != nil {
t.Fatalf("store.Init: %v", err)
}
tunnel := &model.Tunnel{
Name: "wg-test",
Protocol: "wireguard",
Config: `{"address":"10.60.0.1/24","client_address":"10.60.0.2/32"}`,
}
if err := store.DB.Create(tunnel).Error; err != nil {
t.Fatal(err)
}
if err := New().Create(tunnel); err != nil {
t.Fatalf("Create: %v", err)
}
if strings.Contains(tunnel.Config, "private_key") || strings.Contains(tunnel.Config, "preshared_key") {
t.Fatalf("private material leaked into Config: %s", tunnel.Config)
}
var config map[string]interface{}
if err := json.Unmarshal([]byte(tunnel.Config), &config); err != nil {
t.Fatal(err)
}
if config["interface_name"] != interfaceName(tunnel) || int(config["listen_port"].(float64)) < defaultPort {
t.Fatalf("missing interface/allocated port: %#v", config)
}
conf, err := New().GenerateClientConfig(tunnel)
if err != nil || !strings.Contains(conf, "PrivateKey = ") {
t.Fatalf("client configuration unavailable: %v\n%s", err, conf)
}
if _, err := os.Stat(serviceConfigPath(tunnel)); err != nil {
t.Fatalf("server config not written: %v", err)
}
}
func serviceConfigPath(tunnel *model.Tunnel) string {
return os.Getenv("VPN_DATA_DIR") + "/instances/" + strconv.Itoa(int(tunnel.ID)) + "-" + tunnel.Protocol + "/" + interfaceName(tunnel) + ".conf"
}