UI/UX 优化: - 登录页分屏布局 + 可访问性修复 + 内联错误提示 - 侧栏菜单分组(专线运维/资源管理/系统) + 图标修正 + 子路由高亮 - Dashboard 统计卡渐变图标 + 空状态引导 + 统计卡可点击 - 创建向导: 依赖检测门控 + 高级折叠 + 必填校验 + WireGuard推荐标 - 专线列表: 空状态CTA + 操作列「更多」下拉 + 状态筛选 + SSE日志流 - 协议品牌SVG图标(ProtocolIcon组件)替换emoji - 设计令牌系统(tokens.css)统一颜色/间距/圆角/阴影 功能增强: - 在线会话管理(sessionmgr): 跨协议会话同步 + 踢下线 + 连接审计日志 - 流量配额引擎(trafficmgr): 配额检查 + 超额动作(block/warn/throttle) + 周期重置 - 异常流量告警: 突发流量检测(>90MB/15s) + 审计记录 - 节点SSH凭证管理: 密码/私钥认证 + 即时测试连接 + AES-GCM加密存储 - 协议二进制管理(binmgr): FRP/OpenVPN GitHub下载 + 系统包安装引导(apt/yum/dnf) - 右上角修改密码弹窗 + MFA独立验证页 + 密码强度提示 - 用户管理: 批量导入 + LDAP浏览导入 + 平台用户导入VPN拨号用户 - 404路由 + IPSec对接页可达性修复 - MFA备用码持久化弹窗(替代Toast) - 轮询可见性守卫 + ECharts resize修复 后端: - 3张新表: VPNSession/ConnectionLog/TrafficPolicy - monitor增强: 会话同步+配额检查+异常告警+到期审计 - changePassword统一8位+新旧不同校验 - openvpn-otp文案专业化(移除抄袭感) - SSE日志流(token降级query参数)
822 lines
26 KiB
Go
822 lines
26 KiB
Go
// 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 := 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 包含非法字符")
|
||
}
|
||
// 启用 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", "223.5.5.5"), 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
|
||
_ = service.SaveTunnelConfig(t, cfg, secrets)
|
||
}
|
||
raw, _ := json.Marshal(cfg)
|
||
t.Config = string(raw)
|
||
return s.Create(t)
|
||
}
|
||
|
||
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")
|
||
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
|
||
}
|