// 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 }