// device.go 面向终端用户的客户端设备 Peer 管理。 // // 与管理端的 AddPeer/RemovePeer 不同,这里登记的 Peer 全部由客户端 // 自行生成并保管私钥(外部 Peer),服务端只保存公钥;隧道运行中通过 // wg set 增量下发/移除,不重启接口、不中断其他 Peer 的现有连接。 package wireguard import ( "fmt" "net" "os" "path/filepath" "vpn-platform/backend/internal/model" "vpn-platform/backend/internal/service" "vpn-platform/backend/internal/store" ) // IsWireGuardKey 校验 base64 编码的 32 字节 WireGuard 密钥格式。 func IsWireGuardKey(key string) bool { return isWireGuardKey(key) } // InterfaceName 返回隧道对应的内核接口名(与 Create/Start 使用的名字一致)。 func InterfaceName(t *model.Tunnel) string { return interfaceName(t) } // RegisterDevicePeer 把客户端设备公钥登记为外部 Peer 并立即生效。 // 返回为该 Peer 生成的 PresharedKey(需随连接参数下发给设备)。 func (s *Service) RegisterDevicePeer(t *model.Tunnel, id, name, publicKey, clientAddress 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 } peers = append(peers, peer{ ID: id, Name: name, PublicKey: publicKey, ClientAddress: clientAddress, Enabled: true, Managed: false, }) if err := validatePeerInput(peers); err != nil { return "", err } peerSecrets, err := s.persistPeers(t, cfg, secrets, peers) if err != nil { return "", err } preshared := peerSecrets[id].PresharedKey if t.State == "running" { if err := s.runtimeAddPeer(t, publicKey, clientAddress, preshared); err != nil { // wg set 可能只执行了部分操作;确保运行接口和持久配置都回到登记前。 if _, cleanupErr := service.Run("wg", "set", interfaceName(t), "peer", publicKey, "remove"); cleanupErr != nil { return "", fmt.Errorf("运行时下发失败: %w;清理运行 Peer 失败: %v", err, cleanupErr) } if cleanupErr := s.removePersistedDevicePeer(t, id); cleanupErr != nil { return "", fmt.Errorf("运行时下发失败: %w;清理持久 Peer 失败: %v", err, cleanupErr) } return "", fmt.Errorf("运行时下发失败,已回滚设备登记: %w", err) } } return preshared, nil } // RemoveDevicePeer 撤销设备 Peer:从配置与运行时同时移除。 // 与管理端一致,不允许移除最后一个 Peer(否则隧道无法重启)。 func (s *Service) RemoveDevicePeer(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 } var removed *peer next := make([]peer, 0, len(peers)) for i, p := range peers { if p.ID == peerID { removed = &peers[i] continue } next = append(next, p) } if removed == nil { return fmt.Errorf("Peer 不存在: %s", peerID) } if len(next) == 0 { return fmt.Errorf("至少保留一个 Peer,无法移除;可先停用整条专线") } secrets, err := store.TunnelSecrets(t) if err != nil { return err } // 撤销先作用于运行中的接口:若失败,则不改磁盘配置或设备状态。 if t.State == "running" { if _, err := service.Run("wg", "set", interfaceName(t), "peer", removed.PublicKey, "remove"); err != nil { return fmt.Errorf("运行中移除 Peer 失败,未更改持久配置: %w", err) } } if _, err := s.persistPeers(t, cfg, secrets, next); err != nil { if t.State == "running" { oldSecrets, _ := store.TunnelSecrets(t) oldPSK := peerSecretsForID(oldSecrets, peerID) if restoreErr := s.runtimeAddPeer(t, removed.PublicKey, removed.ClientAddress, oldPSK); restoreErr != nil { return fmt.Errorf("保存撤销配置失败: %w;恢复运行 Peer 也失败: %v", err, restoreErr) } } return err } return nil } func peerSecretsForID(secrets map[string]interface{}, id string) string { entries, _ := secrets["peer_secrets"].(map[string]interface{}) entry, _ := entries[id].(map[string]interface{}) psk, _ := entry["preshared_key"].(string) return psk } // 登记出错后,仅移除已写入磁盘的设备,不要求再对不存在的运行接口操作。 func (s *Service) removePersistedDevicePeer(t *model.Tunnel, id 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)) for _, p := range peers { if p.ID != id { next = append(next, p) } } secrets, err := store.TunnelSecrets(t) if err != nil { return err } _, err = s.persistPeers(t, cfg, secrets, next) return err } // persistPeers 归一化 Peer 列表(补齐 PSK 等)、重写服务端 conf 并落库。 // 与 Create 的持久化尾部一致,但不做端口/密钥初始化——调用前配置必须已初始化。 // 返回归一化后的逐 Peer 密钥(含新生成的 PresharedKey)。 func (s *Service) persistPeers(t *model.Tunnel, cfg, secrets map[string]interface{}, peers []peer) (map[string]peerSecret, error) { cfg["peers"] = peersToConfig(peers) normalized, peerSecrets, err := buildPeers(cfg, secrets) if err != nil { return nil, err } cfg["peers"] = peersToConfig(normalized) secrets["peer_secrets"] = peerSecretsToConfig(peerSecrets) serverPrivate := service.Str(secrets, "server_private_key") if serverPrivate == "" { return nil, fmt.Errorf("专线尚未初始化服务端密钥,请先在管理端保存一次配置") } serverConf := renderServerConfig(cfg, serverPrivate, normalized, peerSecrets) confPath := InstanceConfPath(t) previous, readErr := os.ReadFile(confPath) previousConfig, previousSecrets := t.Config, t.Secrets if _, err := service.WriteConfig(t, interfaceName(t)+".conf", serverConf); err != nil { return nil, err } public, sec := service.MergeSecrets(cfg, "server_private_key") for k, v := range secrets { sec[k] = v } if err := service.SaveTunnelConfig(t, public, sec); err != nil { t.Config, t.Secrets = previousConfig, previousSecrets if readErr == nil { _ = os.WriteFile(confPath, previous, 0o600) } return nil, err } return peerSecrets, nil } // runtimeAddPeer 用 wg set 把新 Peer 增量下发到运行中的接口,避免重启。 func (s *Service) runtimeAddPeer(t *model.Tunnel, publicKey, clientAddress, presharedKey string) error { iface := interfaceName(t) args := []string{"set", iface, "peer", publicKey} var pskFile string if presharedKey != "" { dir := service.InstanceDir(t) if err := os.MkdirAll(dir, 0o700); err != nil { return err } f, err := os.CreateTemp(dir, ".psk-*") if err != nil { return err } pskFile = f.Name() if _, err := f.WriteString(presharedKey); err != nil { f.Close() os.Remove(pskFile) return err } f.Close() _ = os.Chmod(pskFile, 0o600) defer os.Remove(pskFile) args = append(args, "preshared-key", pskFile) } args = append(args, "allowed-ips", clientAddress) _, err := service.Run("wg", args...) return err } // UsedClientAddresses 返回服务端自身地址与全部 Peer 已占用的 IPv4/IPv6 地址。 func (s *Service) UsedClientAddresses(t *model.Tunnel) (map[string]bool, error) { cfg, err := store.TunnelConfig(t) if err != nil { return nil, err } used := map[string]bool{} if ip, _, err := net.ParseCIDR(service.Str(cfg, "address")); err == nil && ip != nil { used[ip.String()] = true } peers, err := parsePeers(cfg) if err != nil { return nil, err } for _, p := range peers { if ip, _, err := net.ParseCIDR(p.ClientAddress); err == nil && ip != nil { used[ip.String()] = true } } return used, nil } // DeviceConnectionParams 返回下发给指定设备 Peer 的连接参数。 // 不含任何客户端私钥(客户端私钥只存在于设备上),PSK 经客户端认证的 // HTTPS 通道下发,用于抗量子的一次性对称加固。 func (s *Service) DeviceConnectionParams(t *model.Tunnel, peerID string) (map[string]interface{}, error) { cfg, err := store.TunnelConfig(t) if err != nil { return nil, err } secrets, err := store.TunnelSecrets(t) if err != nil { return nil, err } peers, err := parsePeers(cfg) if err != nil { return nil, err } for _, p := range peers { if p.ID != peerID { continue } allowed := p.AllowedIPs if allowed == "" { allowed = service.StrOr(cfg, "allowed_ips", "0.0.0.0/0, ::/0") } out := map[string]interface{}{ "peer_id": p.ID, "client_address": p.ClientAddress, "server_public_key": service.Str(cfg, "server_public_key"), "preshared_key": parsePeerSecrets(secrets)[p.ID].PresharedKey, "endpoint": service.StrOr(cfg, "public_endpoint", fmt.Sprintf("your-server-ip:%d", int(service.Num(cfg, "listen_port", defaultPort)))), "listen_port": int(service.Num(cfg, "listen_port", defaultPort)), "allowed_ips": allowed, "latency_probe": service.Str(cfg, "latency_probe"), "dns": service.StrOr(cfg, "dns", service.DefaultDNS()), "mtu": int(service.Num(cfg, "mtu", 0)), "keepalive": int(service.Num(cfg, "keepalive", 25)), } return out, nil } return nil, fmt.Errorf("设备 Peer 不存在: %s", peerID) } // InstanceConfPath 暴露实例服务端配置路径(测试与诊断用)。 func InstanceConfPath(t *model.Tunnel) string { return filepath.Join(service.InstanceDir(t), interfaceName(t)+".conf") }