281 lines
9.3 KiB
Go
281 lines
9.3 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,
|
|
"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")
|
|
}
|