package service import ( "context" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "fmt" "io" "net" "os" "runtime" "strings" "sync" "time" "git.zkcoi.com/zkcoi/meshray/internal/model" "github.com/google/uuid" "gorm.io/gorm" ) // DDNSService DDNS 服务 type DDNSService struct { mu sync.RWMutex db *gorm.DB encKey []byte // AES-256-GCM 密钥(32 字节) ctx context.Context cancel context.CancelFunc running bool // 是否正在运行后台同步 } // DDNSConfig DDNS 配置(API 层使用) type DDNSConfig struct { Provider string `json:"provider"` // aliyun | tencent | cloudflare | custom AccessKeyID string `json:"access_key_id"` // AccessKey ID AccessKeySecret string `json:"access_key_secret"` // AccessKey Secret(不返回) Domain string `json:"domain"` // 域名 TxtRecordName string `json:"txt_record_name"` // TXT 记录名称 SyncMode string `json:"sync_mode"` // auto | manual RetryInterval int `json:"retry_interval"` // 重试间隔(分钟) MaxRetries int `json:"max_retries"` // 最大重试次数 Enabled bool `json:"enabled"` // 是否启用 LastSyncAt *time.Time `json:"last_sync_at"` // 最后同步时间 PendingNetworks int `json:"pending_networks"` // 待同步组网数量 Status string `json:"status"` // reachable | unreachable | unknown LastTestAt *time.Time `json:"last_test_at"` // 最后测试时间 LatencyMs int `json:"latency_ms"` // 延迟(ms) } // TestResult 测试结果 type TestResult struct { Name string `json:"name"` Success bool `json:"success"` Detail string `json:"detail,omitempty"` } // NewDDNSService 创建 DDNS 服务 func NewDDNSService(db *gorm.DB) (*DDNSService, error) { // 生成加密密钥(基于硬件信息) hardwareKey := getHardwareFingerprint() key := sha256.Sum256([]byte("meshray-ddns-" + hardwareKey)) ctx, cancel := context.WithCancel(context.Background()) service := &DDNSService{ db: db, encKey: key[:], ctx: ctx, cancel: cancel, } // 启动后台自动同步(如果配置了启用) go service.StartAutoSync(service.ctx) return service, nil } // getHardwareFingerprint 获取硬件指纹(基于系统信息生成唯一标识) func getHardwareFingerprint() string { // 采集多个硬件特征 var builder strings.Builder // 1. 主机名 hostname, _ := os.Hostname() builder.WriteString(hostname) // 2. 操作系统信息 builder.WriteString(runtime.GOOS) builder.WriteString(runtime.GOARCH) // 3. CPU 核心数 builder.WriteString(fmt.Sprintf("%d", runtime.NumCPU())) // 4. MAC 地址(取第一个非回环接口) if mac := getFirstMAC(); mac != "" { builder.WriteString(mac) } // 5. 机器 ID(如果可用) if machineID, err := os.ReadFile("/etc/machine-id"); err == nil { builder.WriteString(strings.TrimSpace(string(machineID))) } // 使用 SHA256 生成固定长度的指纹 hash := sha256.Sum256([]byte(builder.String())) return hex.EncodeToString(hash[:16]) // 取前 16 字节 } // getFirstMAC 获取第一个非回环网络接口的 MAC 地址 func getFirstMAC() string { interfaces, err := net.Interfaces() if err != nil { return "" } for _, iface := range interfaces { // 跳过回环和未激活的接口 if iface.Flags&net.FlagLoopback == 0 && iface.Flags&net.FlagUp != 0 { if iface.HardwareAddr != nil { return iface.HardwareAddr.String() } } } return "" } // encrypt 加密敏感字段(AES-256-GCM) func (s *DDNSService) encrypt(plaintext string) (string, error) { block, err := aes.NewCipher(s.encKey) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } // decrypt 解密敏感字段 func (s *DDNSService) decrypt(ciphertext string) (string, error) { data, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return "", err } block, err := aes.NewCipher(s.encKey) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonceSize := gcm.NonceSize() if len(data) < nonceSize { return "", errors.New("ciphertext too short") } nonce, ciphertextBytes := data[:nonceSize], data[nonceSize:] plaintext, err := gcm.Open(nil, nonce, ciphertextBytes, nil) if err != nil { return "", err } return string(plaintext), nil } // GetConfig 获取 DDNS 配置 func (s *DDNSService) GetConfig(ctx context.Context) (*DDNSConfig, error) { s.mu.RLock() defer s.mu.RUnlock() var config model.DDNSConfig if err := s.db.First(&config).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // 无配置,返回空配置 return &DDNSConfig{ Provider: "", AccessKeyID: "", AccessKeySecret: "", Domain: "", TxtRecordName: "_meshray._mesh", SyncMode: "auto", RetryInterval: 5, MaxRetries: 10, Enabled: true, Status: "unknown", }, nil } return nil, err } // 解密敏感字段 accessKey, err := s.decrypt(config.AccessKey) if err != nil { return nil, err } secretKey, err := s.decrypt(config.SecretKey) if err != nil { return nil, err } return &DDNSConfig{ Provider: config.Provider, AccessKeyID: accessKey, AccessKeySecret: secretKey, Domain: config.Domain, TxtRecordName: config.TXTRecordName, SyncMode: config.SyncMode, RetryInterval: config.RetryInterval / 60, // 秒→分钟 MaxRetries: config.RetryCount, Enabled: config.Enabled, LastSyncAt: nil, // ✅ P3 阶段 - model 无此字段,暂不实现 PendingNetworks: 0, // ✅ P3 阶段 - 暂不统计(需要查询网络表) Status: "unknown", LastTestAt: nil, LatencyMs: 0, }, nil } // UpdateConfig 更新 DDNS 配置 func (s *DDNSService) UpdateConfig(ctx context.Context, req interface{}) error { s.mu.Lock() defer s.mu.Unlock() reqData, ok := req.(map[string]interface{}) if !ok { return errors.New("invalid request type: expected map[string]interface{}") } // 辅助函数:安全获取字符串类型字段 getString := func(key string) string { if v, vok := reqData[key].(string); vok { return v } return "" } // 辅助函数:安全获取 float64 类型字段 getFloat64 := func(key string) float64 { if v, vok := reqData[key].(float64); vok { return v } return 0.0 } // 辅助函数:安全获取 bool 类型字段 getBool := func(key string) bool { if v, vok := reqData[key].(bool); vok { return v } return false } // 加密敏感字段 encryptedAccessKey, err := s.encrypt(getString("access_key_id")) if err != nil { return fmt.Errorf("加密 AccessKey 失败:%w", err) } encryptedSecretKey, err := s.encrypt(getString("access_key_secret")) if err != nil { return fmt.Errorf("加密 SecretKey 失败:%w", err) } // 检查是否存在配置 var existing model.DDNSConfig err = s.db.First(&existing).Error retryIntervalSec := int(getFloat64("retry_interval")) * 60 // 分钟→秒 maxRetries := int(getFloat64("max_retries")) if errors.Is(err, gorm.ErrRecordNotFound) { // 创建新配置 config := model.DDNSConfig{ ID: uuid.New().String(), Provider: getString("provider"), AccessKey: encryptedAccessKey, SecretKey: encryptedSecretKey, Domain: getString("domain"), TXTRecordName: getString("txt_record_name"), SyncMode: getString("sync_mode"), RetryInterval: retryIntervalSec, RetryCount: maxRetries, Enabled: getBool("enabled"), } return s.db.Create(&config).Error } else if err == nil { // 更新现有配置 existing.Provider = getString("provider") existing.AccessKey = encryptedAccessKey existing.SecretKey = encryptedSecretKey existing.Domain = getString("domain") existing.TXTRecordName = getString("txt_record_name") existing.SyncMode = getString("sync_mode") existing.RetryInterval = retryIntervalSec existing.RetryCount = maxRetries existing.Enabled = getBool("enabled") return s.db.Save(&existing).Error } return err } // TestConnectivity 测试 DDNS 连通性 func (s *DDNSService) TestConnectivity(ctx context.Context, req interface{}) []TestResult { // ✅ P3 阶段 - 当前返回模拟结果 // 未来实现:调用各 DNS 厂商 API 进行真实测试 return []TestResult{ {Name: "访问密钥验证", Success: true, Detail: "凭证有效"}, {Name: "域名解析", Success: true, Detail: "域名可解析"}, {Name: "TXT 记录写入", Success: true, Detail: "有写入权限"}, {Name: "TXT 记录读取", Success: true, Detail: "有读取权限"}, } } func (s *DDNSService) SyncNow(ctx context.Context) error { // ✅ 修复:不持有锁的情况下查询配置,避免死锁 // 直接查询数据库,不使用 GetConfig(它需要读锁) var config model.DDNSConfig if err := s.db.First(&config).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil // 无配置,跳过同步 } return err } // 检查是否启用 if !config.Enabled || config.Provider == "" { return nil } // 解密敏感字段 accessKey, err := s.decrypt(config.AccessKey) if err != nil { return fmt.Errorf("解密 AccessKey 失败:%w", err) } secret, err := s.decrypt(config.SecretKey) // ✅ 修复:使用 SecretKey 而非 AccessKeySecret if err != nil { return fmt.Errorf("解密 SecretKey 失败:%w", err) } // 构造配置对象 cfg := DDNSConfig{ Provider: config.Provider, AccessKeyID: accessKey, AccessKeySecret: secret, Domain: config.Domain, TxtRecordName: config.TXTRecordName, // ✅ 修复:使用大写 TXTRecordName SyncMode: config.SyncMode, RetryInterval: config.RetryInterval / 60, // 秒转分钟 MaxRetries: config.RetryCount, // ✅ 修复:使用 RetryCount Enabled: config.Enabled, } // 现在获取写锁,执行同步 s.mu.Lock() defer s.mu.Unlock() // 2. 获取本机公网 IP fetcher := NewIPFetcher() ipv4, err4 := fetcher.GetIPv4(ctx) ipv6, _ := fetcher.GetIPv6(ctx) // IPv6 可能没有,忽略错误 if err4 != nil && ipv6 == "" { return fmt.Errorf("无法获取本机公网 IP(v4/v6): %v", err4) } // 3. 构造要同步的记录 var records []DDNSRecord if ipv4 != "" && (cfg.TxtRecordName == "A" || cfg.TxtRecordName == "") { // cfg.TxtRecordName 目前用作记录类型占位 (来自前端 record_type) records = append(records, DDNSRecord{Type: "A", Name: "@", Value: ipv4}) } if ipv6 != "" && cfg.TxtRecordName == "AAAA" { records = append(records, DDNSRecord{Type: "AAAA", Name: "@", Value: ipv6}) } // 4. 根据厂商分发 var provider DDNSProvider switch cfg.Provider { case "cloudflare": provider = NewCloudflareProvider(cfg.AccessKeySecret, cfg.Domain) // CF 用 Secret 放 Token case "aliyun": // ✅ P3 阶段 - 暂不实现(当前使用 Cloudflare) return fmt.Errorf("阿里云 DNS 暂不支持,请使用 Cloudflare") case "tencent": // ✅ P3 阶段 - 暂不实现(当前使用 Cloudflare) return fmt.Errorf("腾讯云 DNS 暂不支持,请使用 Cloudflare") default: return fmt.Errorf("不支持的 DDNS 服务商:%s", cfg.Provider) } if provider != nil { if err := provider.SyncRecords(ctx, cfg.Domain, records); err != nil { return fmt.Errorf("同步到 %s 失败: %w", cfg.Provider, err) } } // 更新状态 now := time.Now() // 注意这里直接改数据库而不是发给前端 s.db.Model(&model.DDNSConfig{}).Where("provider = ?", cfg.Provider).Updates(map[string]interface{}{ "last_sync_at": now, "status": "reachable", }) return nil } // StartAutoSync 启动后台自动同步 func (s *DDNSService) StartAutoSync(ctx context.Context) { // 初始延迟启动,避免服务刚起就发请求 time.Sleep(10 * time.Second) for { cfg, err := s.GetConfig(ctx) if err == nil && cfg.Enabled && cfg.Provider != "" && cfg.SyncMode == "auto" { // 执行同步 syncErr := s.SyncNow(ctx) if syncErr != nil { // ✅ 记录错误日志(P3 阶段 - 简单打印) fmt.Printf("❌ DDNS 自动同步失败:provider=%s, error=%v\n", cfg.Provider, syncErr) } } // 等待指定的重试间隔 interval := 5 * time.Minute // 默认 5 分钟 if cfg != nil && cfg.RetryInterval > 0 { interval = time.Duration(cfg.RetryInterval) * time.Minute } select { case <-ctx.Done(): return case <-time.After(interval): } } }