Files
opsx-vpn/backend/internal/service/wireguard/wireguard.go
T

813 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"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 := service.CloneConfig(cfg)
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"] = service.DefaultDNS()
}
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 包含非法字符")
}
// 启用 NAT 但未指定出口时,自动探测默认路由网卡;探测失败再要求手填。
if service.Bool(out, "enable_nat", false) && service.Str(out, "egress_interface") == "" {
iface, err := service.DefaultRouteInterface()
if err != nil {
return nil, fmt.Errorf("启用 NAT 时无法自动探测出口网卡,请填写 egress_interface: %w", err)
}
out["egress_interface"] = iface
}
if mtu := int(service.Num(out, "mtu", 0)); mtu != 0 && (mtu < 576 || mtu > 9000) {
return nil, fmt.Errorf("mtu 必须在 576-9000 之间")
}
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")
}
mtuLine := ""
if mtu := int(service.Num(cfg, "mtu", 0)); mtu > 0 {
mtuLine = fmt.Sprintf("MTU = %d\n", mtu)
}
return fmt.Sprintf(`[Interface]
PrivateKey = %s
Address = %s
DNS = %s
%s
[Peer]
PublicKey = %s
PresharedKey = %s
Endpoint = %s
AllowedIPs = %s
PersistentKeepalive = %d
`, sec.PrivateKey, p.ClientAddress, service.StrOr(cfg, "dns", service.DefaultDNS()), mtuLine, 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
}
// 内核计数器在接口重启后归零;用 traffic-state 做增量,再叠到库中的累计值。
prev := loadTrafficState(t)
next := map[string][2]int64{}
st := service.NowStatus(true, "running", "WireGuard 接口运行中")
var deltaRx, deltaTx int64
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++
}
old := prev[fields[0]]
dRx, dTx := rx-old[0], tx-old[1]
if dRx < 0 {
dRx = rx
}
if dTx < 0 {
dTx = tx
}
deltaRx += dRx
deltaTx += dTx
next[fields[0]] = [2]int64{rx, 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,
})
}
saveTrafficState(t, next)
st.RxBytes = t.RxBytes + deltaRx
st.TxBytes = t.TxBytes + deltaTx
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) }
// AddPeer appends a managed or external peer and rewrites configuration.
func (s *Service) AddPeer(t *model.Tunnel, input map[string]interface{}) (peer, error) {
cfg, err := store.TunnelConfig(t)
if err != nil {
return peer{}, err
}
peers, err := parsePeers(cfg)
if err != nil {
return peer{}, err
}
p := peer{
ID: service.Str(input, "id"),
Name: service.Str(input, "name"),
PublicKey: service.Str(input, "public_key"),
ClientAddress: service.Str(input, "client_address"),
AllowedIPs: service.Str(input, "allowed_ips"),
Enabled: true,
Managed: service.Str(input, "public_key") == "",
}
if p.ID == "" {
p.ID = fmt.Sprintf("client-%d", len(peers)+1)
}
if p.Name == "" {
p.Name = p.ID
}
if p.ClientAddress == "" {
return peer{}, fmt.Errorf("client_address 必填")
}
peers = append(peers, p)
cfg["peers"] = peersToConfig(peers)
raw, _ := json.Marshal(cfg)
t.Config = string(raw)
if err := s.Create(t); err != nil {
return peer{}, err
}
// Return the persisted peer (with generated public key when managed).
cfg, _ = store.TunnelConfig(t)
peers, _ = parsePeers(cfg)
for _, item := range peers {
if item.ID == p.ID {
return item, nil
}
}
return p, nil
}
// RemovePeer deletes one peer by id and rewrites configuration.
func (s *Service) RemovePeer(t *model.Tunnel, peerID string) error {
cfg, err := store.TunnelConfig(t)
if err != nil {
return err
}
peers, err := parsePeers(cfg)
if err != nil {
return err
}
next := make([]peer, 0, len(peers))
found := false
for _, p := range peers {
if p.ID == peerID {
found = true
continue
}
next = append(next, p)
}
if !found {
return fmt.Errorf("Peer 不存在: %s", peerID)
}
if len(next) == 0 {
return fmt.Errorf("至少保留一个 Peer")
}
cfg["peers"] = peersToConfig(next)
// Drop removed peer secrets so keys are not retained after revocation.
secrets, _ := store.TunnelSecrets(t)
if raw, ok := secrets["peer_secrets"].(map[string]interface{}); ok {
delete(raw, peerID)
secrets["peer_secrets"] = raw
}
return service.SaveTunnelConfig(t, cfg, secrets)
}
func loadTrafficState(t *model.Tunnel) map[string][2]int64 {
out := map[string][2]int64{}
raw, err := os.ReadFile(filepath.Join(service.InstanceDir(t), "wg-traffic-state.json"))
if err != nil {
return out
}
var m map[string][]int64
if json.Unmarshal(raw, &m) != nil {
return out
}
for k, v := range m {
if len(v) >= 2 {
out[k] = [2]int64{v[0], v[1]}
}
}
return out
}
func saveTrafficState(t *model.Tunnel, state map[string][2]int64) {
m := make(map[string][]int64, len(state))
for k, v := range state {
m[k] = []int64{v[0], v[1]}
}
raw, _ := json.Marshal(m)
_ = os.WriteFile(filepath.Join(service.InstanceDir(t), "wg-traffic-state.json"), raw, 0o600)
}
// 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 mtu := int(service.Num(cfg, "mtu", 0)); mtu > 0 {
fmt.Fprintf(&b, "MTU = %d\n", mtu)
}
if service.Bool(cfg, "enable_nat", false) {
egress := service.Str(cfg, "egress_interface")
source := service.Str(cfg, "address")
if _, subnet, err := net.ParseCIDR(source); err == nil {
source = subnet.String()
}
fmt.Fprintf(&b, "PostUp = iptables -A FORWARD -i %%i -j ACCEPT; iptables -A FORWARD -o %%i -j ACCEPT; iptables -t nat -A POSTROUTING -s %s -o %s -j MASQUERADE\n", source, egress)
fmt.Fprintf(&b, "PostDown = iptables -D FORWARD -i %%i -j ACCEPT; iptables -D FORWARD -o %%i -j ACCEPT; iptables -t nat -D POSTROUTING -s %s -o %s -j MASQUERADE\n", source, 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
}