feat: implement VPN platform management and real protocol services
This commit is contained in:
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
|
||||
}
|
||||
Reference in new issue
Block a user