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

282 lines
9.4 KiB
Go

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