- go.mod: require git.zkcoi.com/zkcoi/meshray/core, replace => ../Meshray - internal/ctr/corebind: EnhancedBind + registry + adapter (wg-go integration isolated) - cmd/mr-wg migrated from old core/cmd/meshray-core - core/ removed; CHANGELOG updated; fix checkdb vet warning
622 lines
17 KiB
Go
622 lines
17 KiB
Go
package ctr
|
||
|
||
import (
|
||
"fmt"
|
||
"net"
|
||
"os/exec"
|
||
"runtime"
|
||
"strings"
|
||
"sync"
|
||
|
||
contract "git.zkcoi.com/zkcoi/meshray-contract"
|
||
"go.uber.org/zap"
|
||
"golang.zx2c4.com/wireguard/device"
|
||
"golang.zx2c4.com/wireguard/tun"
|
||
"golang.zx2c4.com/wireguard/wgctrl"
|
||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||
)
|
||
|
||
// WGManager WireGuard 管理器(P2 阶段骨架)
|
||
type WGManager struct {
|
||
devices map[string]*WGDevice // network_id -> device
|
||
mu sync.RWMutex
|
||
logger *zap.Logger
|
||
wgMode string // "kernel" | "userspace"(当前运行模式)
|
||
}
|
||
|
||
// WGDevice WireGuard 设备
|
||
type WGDevice struct {
|
||
NetworkID string
|
||
Name string // wg0, wg1, ...
|
||
Config *DeviceConfig
|
||
Peers []PeerInfo
|
||
Running bool
|
||
// 用户态模式下使用的资源引用
|
||
tunDevice tun.Device // TUN 设备引用(用户态模式)
|
||
wgDevice *device.Device // WireGuard 设备引用(用户态模式)
|
||
}
|
||
|
||
// NewWGManager 创建 WireGuard 管理器(固定使用用户态)
|
||
func NewWGManager(logger *zap.Logger) *WGManager {
|
||
logger.Info("使用 wireguard-go 用户态模式(跨平台统一)")
|
||
|
||
return &WGManager{
|
||
devices: make(map[string]*WGDevice),
|
||
logger: logger,
|
||
wgMode: "userspace", // 固定为用户态
|
||
}
|
||
}
|
||
|
||
// CreateDevice 创建 WireGuard 设备
|
||
// meshMode: "native" 使用 wireguard-go 默认 Bind 直连;"enhanced" 使用 meshray-core 注册的自定义 Bind 接管收发
|
||
func (m *WGManager) CreateDevice(networkID string, subnet string, listenPort int, meshMode string) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
// 检查是否已存在
|
||
if _, ok := m.devices[networkID]; ok {
|
||
return fmt.Errorf("网络 %s 的设备已存在", networkID)
|
||
}
|
||
|
||
deviceName := fmt.Sprintf("wg%s", networkID)
|
||
|
||
m.logger.Info("开始创建 WireGuard 设备",
|
||
zap.String("network_id", networkID),
|
||
zap.String("device", deviceName),
|
||
zap.String("subnet", subnet),
|
||
zap.Int("listen_port", listenPort),
|
||
zap.String("mesh_mode", meshMode))
|
||
|
||
// 生成密钥对
|
||
privateKey, err := wgtypes.GeneratePrivateKey()
|
||
if err != nil {
|
||
return fmt.Errorf("生成私钥失败:%w", err)
|
||
}
|
||
publicKey := privateKey.PublicKey()
|
||
|
||
m.logger.Debug("生成 WG 密钥对",
|
||
zap.String("public_key", publicKey.String()))
|
||
|
||
// 使用用户态模式启动 wireguard-go 进程,并获取资源引用
|
||
tunDev, wgDev, err := m.startUserModeWGProcessWithRefs(deviceName, privateKey, listenPort, networkID, meshMode)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 4. ✅ Core 模块使用回环地址拦截,无需配置 TUN 设备 IP
|
||
// WireGuard 用户态设备会自动管理自己的网络接口
|
||
m.logger.Info("WireGuard 用户态设备已创建,使用回环地址拦截模式",
|
||
zap.String("device", deviceName))
|
||
|
||
// 5. 启动设备(带回滚)
|
||
if err := m.bringUpDevice(deviceName); err != nil {
|
||
m.cleanupDevice(deviceName) // 回滚
|
||
return fmt.Errorf("启动设备失败:%w", err)
|
||
}
|
||
|
||
// 7. 记录到内存
|
||
device := &WGDevice{
|
||
NetworkID: networkID,
|
||
Name: deviceName,
|
||
Config: &DeviceConfig{
|
||
PrivateKey: privateKey.String(),
|
||
PublicKey: publicKey.String(),
|
||
ListenPort: listenPort,
|
||
Subnet: subnet,
|
||
},
|
||
Running: true,
|
||
}
|
||
|
||
// ⚠️ 关键:保存用户态模式资源引用
|
||
device.tunDevice = tunDev
|
||
device.wgDevice = wgDev
|
||
m.logger.Debug("已保存用户态模式资源引用",
|
||
zap.String("device", deviceName))
|
||
|
||
m.devices[networkID] = device
|
||
|
||
m.logger.Info("WireGuard 设备创建成功(用户态模式)",
|
||
zap.String("network_id", networkID),
|
||
zap.String("public_key", publicKey.String()))
|
||
|
||
return nil
|
||
}
|
||
|
||
// AddPeer 添加 Peer
|
||
func (m *WGManager) AddPeer(networkID string, publicKey, allowedIP string) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
device, ok := m.devices[networkID]
|
||
if !ok {
|
||
return fmt.Errorf("网络 %s 的设备不存在", networkID)
|
||
}
|
||
|
||
m.logger.Info("添加 Peer",
|
||
zap.String("network_id", networkID),
|
||
zap.String("public_key", truncatePublicKey(publicKey)+"..."),
|
||
zap.String("allowed_ip", allowedIP))
|
||
|
||
// 1. 解析公钥
|
||
peerKey, err := wgtypes.ParseKey(publicKey)
|
||
if err != nil {
|
||
return fmt.Errorf("解析公钥失败:%w", err)
|
||
}
|
||
|
||
// 2. 解析 AllowedIP
|
||
_, ipNet, err := net.ParseCIDR(allowedIP)
|
||
if err != nil {
|
||
return fmt.Errorf("解析 AllowedIP 失败:%w", err)
|
||
}
|
||
|
||
// 3. 连接 wgctrl 并配置
|
||
client, err := wgctrl.New()
|
||
if err != nil {
|
||
return fmt.Errorf("wgctrl 连接失败:%w", err)
|
||
}
|
||
defer client.Close()
|
||
|
||
// 4. 配置 Peer
|
||
config := wgtypes.Config{
|
||
Peers: []wgtypes.PeerConfig{
|
||
{
|
||
PublicKey: peerKey,
|
||
ReplaceAllowedIPs: true,
|
||
AllowedIPs: []net.IPNet{*ipNet},
|
||
},
|
||
},
|
||
}
|
||
|
||
if err := client.ConfigureDevice(device.Name, config); err != nil {
|
||
return fmt.Errorf("配置 Peer 失败:%w", err)
|
||
}
|
||
|
||
m.logger.Info("Peer 配置成功",
|
||
zap.String("network_id", networkID),
|
||
zap.String("public_key", truncatePublicKey(publicKey)+"..."),
|
||
zap.String("allowed_ip", allowedIP))
|
||
|
||
// 5. 同时更新内存(保持向后兼容)
|
||
peer := PeerInfo{
|
||
PublicKey: publicKey,
|
||
AllowedIPs: []string{allowedIP},
|
||
}
|
||
device.Peers = append(device.Peers, peer)
|
||
|
||
return nil
|
||
}
|
||
|
||
// RemovePeer 移除 Peer
|
||
func (m *WGManager) RemovePeer(networkID string, publicKey string) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
device, ok := m.devices[networkID]
|
||
if !ok {
|
||
return fmt.Errorf("网络 %s 的设备不存在", networkID)
|
||
}
|
||
|
||
m.logger.Info("移除 Peer",
|
||
zap.String("network_id", networkID),
|
||
zap.String("public_key", truncatePublicKey(publicKey)+"..."))
|
||
|
||
// 1. 解析公钥
|
||
peerKey, err := wgtypes.ParseKey(publicKey)
|
||
if err != nil {
|
||
return fmt.Errorf("解析公钥失败:%w", err)
|
||
}
|
||
|
||
// 2. 连接 wgctrl 并移除
|
||
client, err := wgctrl.New()
|
||
if err != nil {
|
||
return fmt.Errorf("wgctrl 连接失败:%w", err)
|
||
}
|
||
defer client.Close()
|
||
|
||
// 3. 配置移除 Peer(使用 Remove: true)
|
||
config := wgtypes.Config{
|
||
Peers: []wgtypes.PeerConfig{
|
||
{
|
||
PublicKey: peerKey,
|
||
Remove: true,
|
||
},
|
||
},
|
||
}
|
||
|
||
if err := client.ConfigureDevice(device.Name, config); err != nil {
|
||
return fmt.Errorf("移除 Peer 失败:%w", err)
|
||
}
|
||
|
||
m.logger.Info("Peer 移除成功",
|
||
zap.String("network_id", networkID),
|
||
zap.String("public_key", truncatePublicKey(publicKey)+"..."))
|
||
|
||
// 4. 同时从内存删除(保持向后兼容)
|
||
newPeers := []PeerInfo{}
|
||
for _, p := range device.Peers {
|
||
if p.PublicKey != publicKey {
|
||
newPeers = append(newPeers, p)
|
||
}
|
||
}
|
||
device.Peers = newPeers
|
||
|
||
return nil
|
||
}
|
||
|
||
// DeleteDevice 删除 WireGuard 设备
|
||
func (m *WGManager) DeleteDevice(networkID string) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
device, ok := m.devices[networkID]
|
||
if !ok {
|
||
return fmt.Errorf("网络 %s 的设备不存在", networkID)
|
||
}
|
||
|
||
m.logger.Info("删除 WireGuard 设备",
|
||
zap.String("network_id", networkID),
|
||
zap.String("device", device.Name))
|
||
|
||
// 1. 清理真实设备(内核态或用户态)
|
||
m.cleanupDevice(device.Name)
|
||
|
||
// 2. 从内存删除
|
||
delete(m.devices, networkID)
|
||
|
||
m.logger.Info("WireGuard 设备已删除",
|
||
zap.String("network_id", networkID))
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetStatus 获取设备状态
|
||
func (m *WGManager) GetStatus(networkID string) (*WGStatus, error) {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
|
||
device, ok := m.devices[networkID]
|
||
if !ok {
|
||
return nil, fmt.Errorf("网络 %s 的设备不存在", networkID)
|
||
}
|
||
|
||
status := &WGStatus{
|
||
DeviceName: device.Name,
|
||
Running: device.Running,
|
||
PeerCount: len(device.Peers),
|
||
Subnet: device.Config.Subnet,
|
||
}
|
||
|
||
return status, nil
|
||
}
|
||
|
||
// GetWGMode 获取当前 WG 模式
|
||
func (m *WGManager) GetWGMode() string {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
return m.wgMode
|
||
}
|
||
|
||
// Stop 停止管理器并释放所有资源
|
||
func (m *WGManager) Stop() error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
m.logger.Info("正在停止 WireGuard 管理器...")
|
||
|
||
// 关闭所有设备
|
||
for networkID, device := range m.devices {
|
||
m.logger.Info("正在关闭设备",
|
||
zap.String("network_id", networkID),
|
||
zap.String("device_name", device.Name))
|
||
|
||
// 如果是用户态模式,关闭相关资源
|
||
if device.wgDevice != nil {
|
||
m.logger.Debug("关闭用户态 WireGuard 设备",
|
||
zap.String("device", device.Name))
|
||
device.wgDevice.Close()
|
||
}
|
||
|
||
if device.tunDevice != nil {
|
||
m.logger.Debug("关闭 TUN 设备",
|
||
zap.String("device", device.Name))
|
||
device.tunDevice.Close()
|
||
}
|
||
|
||
// 清理内核态设备(忽略返回值)
|
||
m.cleanupDevice(device.Name)
|
||
|
||
delete(m.devices, networkID)
|
||
}
|
||
|
||
m.logger.Info("WireGuard 管理器已停止")
|
||
return nil
|
||
}
|
||
|
||
// WGDevice WireGuard 设备(结构已在上面定义)
|
||
|
||
// DeviceConfig 设备配置
|
||
type DeviceConfig struct {
|
||
PrivateKey string
|
||
PublicKey string
|
||
ListenPort int
|
||
Subnet string
|
||
}
|
||
|
||
// PeerInfo Peer 信息
|
||
type PeerInfo struct {
|
||
PublicKey string
|
||
Endpoint string
|
||
AllowedIPs []string
|
||
}
|
||
|
||
// WGStatus WireGuard 状态
|
||
type WGStatus struct {
|
||
DeviceName string `json:"device_name"`
|
||
Running bool `json:"running"`
|
||
PeerCount int `json:"peer_count"`
|
||
Subnet string `json:"subnet"`
|
||
}
|
||
|
||
// ListPeers 获取设备下所有 Peer 信息
|
||
// 除内存记录外,还会从 wgctrl 读取对端真实 Endpoint,作为增强模式 P2P 建连的候选地址来源
|
||
func (m *WGManager) ListPeers(networkID string) ([]PeerInfo, error) {
|
||
m.mu.RLock()
|
||
device, ok := m.devices[networkID]
|
||
if !ok {
|
||
m.mu.RUnlock()
|
||
return nil, fmt.Errorf("网络 %s 的设备不存在", networkID)
|
||
}
|
||
// 复制内存中的 peer 列表,避免返回内部共享引用
|
||
peers := make([]PeerInfo, len(device.Peers))
|
||
copy(peers, device.Peers)
|
||
m.mu.RUnlock()
|
||
|
||
// 从 wgctrl 读取真实 Endpoint(若已配置)
|
||
client, err := wgctrl.New()
|
||
if err == nil {
|
||
defer client.Close()
|
||
if dev, err := client.Device(device.Name); err == nil {
|
||
for i := range peers {
|
||
for _, wp := range dev.Peers {
|
||
if wp.PublicKey.String() == peers[i].PublicKey {
|
||
if wp.Endpoint != nil {
|
||
peers[i].Endpoint = wp.Endpoint.String()
|
||
}
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return peers, nil
|
||
}
|
||
|
||
// UpdatePeerEndpoint 更新 Peer 的 Endpoint
|
||
// 注意:增强模式(meshray-core 接管收发)下不应再改写 peer Endpoint 为回环端口,
|
||
// 否则 WG 入站源地址匹配失败;此方法仅用于设置对端真实地址或原生模式。
|
||
func (m *WGManager) UpdatePeerEndpoint(networkID string, publicKey string, newEndpoint string) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
device, ok := m.devices[networkID]
|
||
if !ok {
|
||
return fmt.Errorf("网络 %s 的设备不存在", networkID)
|
||
}
|
||
|
||
m.logger.Info("更新 Peer Endpoint",
|
||
zap.String("network_id", networkID),
|
||
zap.String("public_key", truncatePublicKey(publicKey)+"..."),
|
||
zap.String("new_endpoint", newEndpoint))
|
||
|
||
// 解析公钥
|
||
peerKey, err := wgtypes.ParseKey(publicKey)
|
||
if err != nil {
|
||
return fmt.Errorf("解析公钥失败:%w", err)
|
||
}
|
||
|
||
// 解析 Endpoint 地址
|
||
var udpAddr *net.UDPAddr
|
||
if newEndpoint != "" {
|
||
addr, err := net.ResolveUDPAddr("udp", newEndpoint)
|
||
if err != nil {
|
||
return fmt.Errorf("解析 Endpoint 地址 %s 失败:%w", newEndpoint, err)
|
||
}
|
||
udpAddr = addr
|
||
}
|
||
|
||
// 连接 wgctrl 并配置
|
||
client, err := wgctrl.New()
|
||
if err != nil {
|
||
return fmt.Errorf("wgctrl 连接失败:%w", err)
|
||
}
|
||
defer client.Close()
|
||
|
||
// 配置更新 Peer
|
||
config := wgtypes.Config{
|
||
Peers: []wgtypes.PeerConfig{
|
||
{
|
||
PublicKey: peerKey,
|
||
UpdateOnly: true,
|
||
Endpoint: udpAddr,
|
||
},
|
||
},
|
||
}
|
||
|
||
if err := client.ConfigureDevice(device.Name, config); err != nil {
|
||
return fmt.Errorf("更新 Peer 失败:%w", err)
|
||
}
|
||
|
||
// 同时更新内存
|
||
for i, p := range device.Peers {
|
||
if p.PublicKey == publicKey {
|
||
device.Peers[i].Endpoint = newEndpoint
|
||
break
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// startUserModeWGProcess 用户态模式启动 wireguard-go(保存资源引用)
|
||
func (m *WGManager) startUserModeWGProcess(deviceName string, privateKey wgtypes.Key, listenPort int) error {
|
||
tunDev, wgDev, err := m.startUserModeWGProcessWithRefs(deviceName, privateKey, listenPort, "", "native")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// 兼容旧接口,忽略返回值
|
||
_ = tunDev
|
||
_ = wgDev
|
||
return nil
|
||
}
|
||
|
||
// startUserModeWGProcessWithRefs 用户态模式启动 wireguard-go,返回资源引用
|
||
// meshMode 决定 Bind 来源:native 用 wireguard-go 默认 Bind;enhanced 用 meshray-core 注册的自定义 Bind
|
||
func (m *WGManager) startUserModeWGProcessWithRefs(deviceName string, privateKey wgtypes.Key, listenPort int, networkID string, meshMode string) (tun.Device, *device.Device, error) {
|
||
m.logger.Info("使用用户态模式启动 WireGuard",
|
||
zap.String("device", deviceName),
|
||
zap.Int("port", listenPort),
|
||
zap.String("mesh_mode", meshMode))
|
||
|
||
// 1. 创建 TUN 设备并保存引用
|
||
tunDevice, err := tun.CreateTUN(deviceName, 1420)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("创建 TUN 设备失败:%w", err)
|
||
}
|
||
|
||
// 2. 创建 UDP bind:根据组网模式选择
|
||
// - 原生模式:wireguard-go 默认 Bind(标准直连,无需 meshray-core)
|
||
// - 增强模式:优先使用 meshray-core 注册的自定义 Bind(接管收发,根治 Endpoint 旁路缺陷);
|
||
// 若默认构建未引入 meshray-core,则返回错误,增强模式不可用。
|
||
bindFactory, err := contract.GetBindFactory(meshMode == "enhanced")
|
||
if err != nil {
|
||
tunDevice.Close()
|
||
return nil, nil, fmt.Errorf("获取 Bind 工厂失败(增强模式需 meshray-core):%w", err)
|
||
}
|
||
bind, err := bindFactory(listenPort, networkID, m.logger)
|
||
if err != nil {
|
||
tunDevice.Close()
|
||
return nil, nil, fmt.Errorf("创建 Bind 失败:%w", err)
|
||
}
|
||
|
||
// 3. 创建 WireGuard device 并保存引用
|
||
logger := &device.Logger{
|
||
Verbosef: func(format string, args ...interface{}) {
|
||
m.logger.Debug(fmt.Sprintf(format, args...))
|
||
},
|
||
Errorf: func(format string, args ...interface{}) {
|
||
m.logger.Error(fmt.Sprintf(format, args...))
|
||
},
|
||
}
|
||
|
||
wgDevice := device.NewDevice(tunDevice, bind, logger)
|
||
|
||
// 4. 配置设备
|
||
config := fmt.Sprintf("private_key=%s\nlisten_port=%d\n",
|
||
privateKey.String(), listenPort)
|
||
|
||
if err := wgDevice.IpcSet(config); err != nil {
|
||
wgDevice.Close()
|
||
return nil, nil, fmt.Errorf("配置 WireGuard 设备失败:%w", err)
|
||
}
|
||
|
||
// 5. 启动设备
|
||
if err := wgDevice.Up(); err != nil {
|
||
wgDevice.Close()
|
||
return nil, nil, fmt.Errorf("启动 WireGuard 设备失败:%w", err)
|
||
}
|
||
|
||
m.logger.Info("用户态 WireGuard 启动成功",
|
||
zap.String("device", deviceName),
|
||
zap.Int("port", listenPort))
|
||
|
||
// 6. 返回资源引用
|
||
return tunDevice, wgDevice, nil
|
||
}
|
||
|
||
// truncatePublicKey 截断公钥用于日志显示(避免数组越界)
|
||
func truncatePublicKey(publicKey string) string {
|
||
if len(publicKey) <= 8 {
|
||
return publicKey
|
||
}
|
||
return publicKey[:8]
|
||
}
|
||
|
||
// bringUpDevice 启动设备(跨平台实现)
|
||
func (m *WGManager) bringUpDevice(deviceName string) error {
|
||
m.logger.Info("启动设备",
|
||
zap.String("device", deviceName))
|
||
|
||
var cmd *exec.Cmd
|
||
var output []byte
|
||
var err error
|
||
|
||
switch runtime.GOOS {
|
||
case "linux":
|
||
// Linux: ip link set up device
|
||
cmd = exec.Command("ip", "link", "set", "up", deviceName)
|
||
output, err = cmd.CombinedOutput()
|
||
case "windows":
|
||
// Windows: 用户态模式下 wireguard-go 会自动管理设备状态
|
||
// 这里不需要额外操作
|
||
m.logger.Debug("Windows 平台用户态模式无需手动启动设备")
|
||
return nil
|
||
case "darwin":
|
||
// macOS: ifconfig device up
|
||
cmd = exec.Command("ifconfig", deviceName, "up")
|
||
output, err = cmd.CombinedOutput()
|
||
default:
|
||
return fmt.Errorf("不支持的操作系统:%s", runtime.GOOS)
|
||
}
|
||
|
||
if err != nil {
|
||
return fmt.Errorf("启动设备失败 (%s): %w", string(output), err)
|
||
}
|
||
|
||
m.logger.Info("设备已启动",
|
||
zap.String("device", deviceName))
|
||
|
||
return nil
|
||
}
|
||
|
||
// cleanupDevice 清理残留设备(回滚用,跨平台实现)
|
||
func (m *WGManager) cleanupDevice(deviceName string) {
|
||
m.logger.Warn("清理残留设备",
|
||
zap.String("device", deviceName))
|
||
|
||
var cmd *exec.Cmd
|
||
var output []byte
|
||
|
||
switch runtime.GOOS {
|
||
case "linux":
|
||
// Linux: ip link delete device
|
||
cmd = exec.Command("ip", "link", "delete", deviceName)
|
||
output, _ = cmd.CombinedOutput()
|
||
case "windows":
|
||
// Windows: 无法通过命令行直接删除,需要用户手动操作
|
||
m.logger.Warn("Windows 平台需要通过 WireGuard 客户端删除设备",
|
||
zap.String("device", deviceName))
|
||
return
|
||
case "darwin":
|
||
// macOS: ifconfig device down delete
|
||
cmd = exec.Command("ifconfig", deviceName, "down")
|
||
_, _ = cmd.CombinedOutput()
|
||
// macOS 可能需要额外步骤,这里简化处理
|
||
default:
|
||
m.logger.Warn("不支持的操作系统,跳过清理",
|
||
zap.String("os", runtime.GOOS))
|
||
return
|
||
}
|
||
|
||
// 检查是否是"设备不存在"错误
|
||
if strings.Contains(string(output), "does not exist") ||
|
||
strings.Contains(string(output), "cannot find device") {
|
||
m.logger.Debug("设备不存在,无需清理",
|
||
zap.String("device", deviceName))
|
||
return
|
||
}
|
||
|
||
m.logger.Debug("设备清理完成",
|
||
zap.String("device", deviceName))
|
||
}
|