package service import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/pika-monitor/pika/internal/ddns" "github.com/pika-monitor/pika/internal/models" "github.com/pika-monitor/pika/internal/protocol" "github.com/pika-monitor/pika/internal/repo" "github.com/pika-monitor/pika/internal/websocket" "github.com/go-orz/toolkit/syncx" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" ) // ipCacheData IP 缓存数据 type ipCacheData struct { IPv4 string IPv6 string } type DDNSService struct { logger *zap.Logger ConfigRepo *repo.DDNSConfigRepo // 导出用于 handler 的 PageBuilder recordRepo *repo.DDNSRecordRepo propertyService *PropertyService wsManager *websocket.Manager ipCache *syncx.SafeMap[string, *ipCacheData] // 使用内存缓存存储 IP } func NewDDNSService( logger *zap.Logger, db *gorm.DB, propertyService *PropertyService, wsManager *websocket.Manager, ) *DDNSService { s := &DDNSService{ logger: logger, ConfigRepo: repo.NewDDNSConfigRepo(db), recordRepo: repo.NewDDNSRecordRepo(db), propertyService: propertyService, wsManager: wsManager, ipCache: syncx.NewSafeMap[string, *ipCacheData](), } // 初始化 IP 缓存:从 DNS 服务商查询当前记录 go s.initIPCache() return s } // initIPCache 初始化 IP 缓存:从 DNS 服务商查询所有启用配置的当前 IP 记录 func (s *DDNSService) initIPCache() { ctx := context.Background() // 查询所有启用的 DDNS 配置 configs, err := s.ConfigRepo.FindAllEnabled(ctx) if err != nil { s.logger.Error("初始化 IP 缓存失败:查询配置出错", zap.Error(err)) return } if len(configs) == 0 { s.logger.Info("没有启用的 DDNS 配置,跳过 IP 缓存初始化") return } s.logger.Info("开始初始化 IP 缓存", zap.Int("配置数量", len(configs))) for _, config := range configs { // 创建 DNS 提供商客户端 provider, err := s.createProvider(ctx, &config) if err != nil { s.logger.Error("初始化 IP 缓存失败:创建 DNS 提供商失败", zap.String("agentId", config.AgentID), zap.String("provider", config.Provider), zap.Error(err)) continue } cacheData := &ipCacheData{} // 查询 IPv4 记录 if config.EnableIPv4 && len(config.DomainsIPv4) > 0 { // 使用第一个域名查询 domain := config.DomainsIPv4[0] ipv4, err := provider.GetRecord(ctx, domain, ddns.RecordTypeA) if err != nil { s.logger.Warn("查询 IPv4 记录失败", zap.String("agentId", config.AgentID), zap.String("domain", domain), zap.Error(err)) } else { cacheData.IPv4 = ipv4 s.logger.Info("成功查询 IPv4 记录", zap.String("agentId", config.AgentID), zap.String("domain", domain), zap.String("ipv4", ipv4)) } } // 查询 IPv6 记录 if config.EnableIPv6 && len(config.DomainsIPv6) > 0 { // 使用第一个域名查询 domain := config.DomainsIPv6[0] ipv6, err := provider.GetRecord(ctx, domain, ddns.RecordTypeAAAA) if err != nil { s.logger.Warn("查询 IPv6 记录失败", zap.String("agentId", config.AgentID), zap.String("domain", domain), zap.Error(err)) } else { cacheData.IPv6 = ipv6 s.logger.Info("成功查询 IPv6 记录", zap.String("agentId", config.AgentID), zap.String("domain", domain), zap.String("ipv6", ipv6)) } } // 如果查询到了任何 IP,就放入缓存 if cacheData.IPv4 != "" || cacheData.IPv6 != "" { s.ipCache.Set(config.AgentID, cacheData) } } s.logger.Info("IP 缓存初始化完成") } func (s *DDNSService) clearIPCache(agentID string, reason string) { if agentID == "" { return } s.ipCache.Delete(agentID) s.logger.Debug("清理 IP 缓存", zap.String("agentId", agentID), zap.String("reason", reason)) } // HandleIPReport 处理客户端上报的 IP 地址 func (s *DDNSService) HandleIPReport(ctx context.Context, agentID string, ipData *protocol.DDNSIPReportData) error { // 获取该探针的 DDNS 配置 config, err := s.ConfigRepo.FindEnabledByAgentID(ctx, agentID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // 没有配置或未启用,直接返回 return nil } return fmt.Errorf("获取 DDNS 配置失败: %w", err) } // 获取缓存的 IP cachedIP, _ := s.ipCache.Get(agentID) var oldIPv4, oldIPv6 string if cachedIP != nil { oldIPv4 = cachedIP.IPv4 oldIPv6 = cachedIP.IPv6 } // 检查 IP 是否变化 ipv4Changed := config.EnableIPv4 && ipData.IPv4 != "" && oldIPv4 != ipData.IPv4 ipv6Changed := config.EnableIPv6 && ipData.IPv6 != "" && oldIPv6 != ipData.IPv6 if !ipv4Changed && !ipv6Changed { // IP 没有变化,无需更新 s.logger.Info("IP 未变化,无需更新", zap.String("agentId", agentID), zap.Bool("ipv4Changed", ipv4Changed), zap.Bool("ipv6Changed", ipv6Changed), zap.String("oldIPv4", oldIPv4), zap.String("newIPv4", ipData.IPv4), zap.String("oldIPv6", oldIPv6), zap.String("newIPv6", ipData.IPv6), ) return nil } s.logger.Info("检测到 IP 变化", zap.String("agentId", agentID), zap.Bool("ipv4Changed", ipv4Changed), zap.Bool("ipv6Changed", ipv6Changed), zap.String("oldIPv4", oldIPv4), zap.String("newIPv4", ipData.IPv4), zap.String("oldIPv6", oldIPv6), zap.String("newIPv6", ipData.IPv6), ) // 创建 DNS 提供商客户端 provider, err := s.createProvider(ctx, config) if err != nil { return fmt.Errorf("创建 DNS 提供商失败: %w", err) } // 更新 DNS 记录 // 处理 IPv4 域名 if ipv4Changed { for _, domain := range config.DomainsIPv4 { if err := s.updateRecord(ctx, provider, config, domain, ddns.RecordTypeA, ipData.IPv4, oldIPv4); err != nil { s.logger.Error("更新 IPv4 域名记录失败", zap.String("agentId", agentID), zap.String("domain", domain), zap.Error(err)) } } } // 处理 IPv6 域名 if ipv6Changed { for _, domain := range config.DomainsIPv6 { if err := s.updateRecord(ctx, provider, config, domain, ddns.RecordTypeAAAA, ipData.IPv6, oldIPv6); err != nil { s.logger.Error("更新 IPv6 域名记录失败", zap.String("agentId", agentID), zap.String("domain", domain), zap.Error(err)) } } } // 更新内存缓存 s.ipCache.Set(agentID, &ipCacheData{IPv4: ipData.IPv4, IPv6: ipData.IPv6}) return nil } // updateRecord 更新单条 DNS 记录 func (s *DDNSService) updateRecord( ctx context.Context, provider ddns.Provider, config *models.DDNSConfig, domain, recordType, newIP, oldIP string, ) error { err := provider.UpdateRecord(ctx, domain, recordType, newIP) // 记录更新结果 record := &models.DDNSRecord{ ID: uuid.New().String(), ConfigID: config.ID, AgentID: config.AgentID, Domain: domain, RecordType: recordType, OldIP: oldIP, NewIP: newIP, CreatedAt: time.Now().UnixMilli(), } if err != nil { record.Status = "failed" record.ErrorMessage = err.Error() s.logger.Error("更新 DNS 记录失败", zap.String("domain", domain), zap.String("recordType", recordType), zap.String("newIP", newIP), zap.Error(err)) } else { record.Status = "success" s.logger.Info("DNS 记录更新成功", zap.String("domain", domain), zap.String("recordType", recordType), zap.String("oldIP", oldIP), zap.String("newIP", newIP)) } // 保存更新记录 if saveErr := s.recordRepo.Create(ctx, record); saveErr != nil { s.logger.Error("保存 DDNS 更新记录失败", zap.Error(saveErr)) } return err } // createProvider 创建 DNS 提供商 func (s *DDNSService) createProvider(ctx context.Context, config *models.DDNSConfig) (ddns.Provider, error) { // 从 PropertyService 获取 DNS Provider 配置 dnsProvider, err := s.propertyService.GetDNSProviderByType(ctx, config.Provider) if err != nil { return nil, fmt.Errorf("获取 DNS Provider 配置失败: %w", err) } if !dnsProvider.Enabled { return nil, fmt.Errorf("DNS Provider %s 未启用", config.Provider) } // 将 Config (map[string]interface{}) 转换为 map[string]string providerConfig := make(map[string]string) for k, v := range dnsProvider.Config { if str, ok := v.(string); ok { providerConfig[k] = str } } return ddns.NewProvider(config.Provider, providerConfig) } // GetConfigByAgentID 获取探针的 DDNS 配置 func (s *DDNSService) GetConfigByAgentID(ctx context.Context, agentID string) (*models.DDNSConfig, error) { return s.ConfigRepo.FindByAgentID(ctx, agentID) } // GetDDNSConfig 将数据库配置转换为协议配置(下发给客户端) func (s *DDNSService) GetDDNSConfig(config *models.DDNSConfig) (*protocol.DDNSConfigData, error) { return &protocol.DDNSConfigData{ Enabled: config.Enabled, EnableIPv4: config.EnableIPv4, EnableIPv6: config.EnableIPv6, IPv4GetMethod: config.IPv4GetMethod, IPv6GetMethod: config.IPv6GetMethod, IPv4GetValue: config.IPv4GetValue, IPv6GetValue: config.IPv6GetValue, }, nil } // ListConfigsByAgentID 列出探针的所有 DDNS 配置 func (s *DDNSService) ListConfigsByAgentID(ctx context.Context, agentID string) ([]models.DDNSConfig, error) { return s.ConfigRepo.ListByAgentID(ctx, agentID) } // ListRecords 列出 DDNS 更新记录 func (s *DDNSService) ListRecords(ctx context.Context, configID string, limit int) ([]models.DDNSRecord, error) { return s.recordRepo.ListByConfigID(ctx, configID, limit) } // ListRecordsByAgentID 列出探针的 DDNS 更新记录 func (s *DDNSService) ListRecordsByAgentID(ctx context.Context, agentID string, limit int) ([]models.DDNSRecord, error) { return s.recordRepo.ListByAgentID(ctx, agentID, limit) } // CreateConfig 创建 DDNS 配置 func (s *DDNSService) CreateConfig(ctx context.Context, config *models.DDNSConfig) error { return s.ConfigRepo.Create(ctx, config) } // UpdateConfig 更新 DDNS 配置 func (s *DDNSService) UpdateConfig(ctx context.Context, config *models.DDNSConfig) error { if err := s.ConfigRepo.Save(ctx, config); err != nil { return err } s.clearIPCache(config.AgentID, "config_updated") return nil } func (s *DDNSService) UpdateEnabled(ctx context.Context, id string, enabled bool) error { config, err := s.GetConfig(ctx, id) if err != nil { return err } if err := s.ConfigRepo.UpdateColumnsById(ctx, id, map[string]interface{}{ "enabled": enabled, }); err != nil { return err } config.Enabled = enabled s.clearIPCache(config.AgentID, "enabled_updated") if enabled { if err := s.sendDDNSConfigToAgent(config); err != nil { s.logger.Debug("启用后发送 DDNS 配置失败", zap.String("agentID", config.AgentID), zap.Error(err)) } } return nil } // GetConfig 获取 DDNS 配置 func (s *DDNSService) GetConfig(ctx context.Context, id string) (*models.DDNSConfig, error) { config, err := s.ConfigRepo.FindById(ctx, id) if err != nil { return nil, err } return &config, nil } // DeleteConfig 删除 DDNS 配置 func (s *DDNSService) DeleteConfig(ctx context.Context, id string) error { config, err := s.GetConfig(ctx, id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } // 先删除相关记录 if err := s.recordRepo.DeleteByConfigID(ctx, id); err != nil { return err } // 删除配置 if err := s.ConfigRepo.DeleteById(ctx, id); err != nil { return err } if err == nil { s.clearIPCache(config.AgentID, "config_deleted") } return nil } // Run 启动 DDNS 定时任务 func (s *DDNSService) Run(ctx context.Context) { // DDNS 配置检查 ticker (1 分钟) ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() s.logger.Info("DDNS 定时任务已启动") for { select { case <-ctx.Done(): s.logger.Info("DDNS 定时任务已停止") return case <-ticker.C: s.checkDDNS() } } } // checkDDNS 定时检查并下发启用的 DDNS 配置 func (s *DDNSService) checkDDNS() { ctx := context.Background() // 查询所有启用的 DDNS 配置 configs, err := s.ConfigRepo.FindAllEnabled(ctx) if err != nil { s.logger.Error("查询启用的 DDNS 配置失败", zap.Error(err)) return } enabledAgents := make(map[string]struct{}, len(configs)) for _, config := range configs { if config.AgentID != "" { enabledAgents[config.AgentID] = struct{}{} } } allAgents, err := s.ConfigRepo.ListAgentIDs(ctx) if err != nil { s.logger.Error("查询 DDNS 探针列表失败", zap.Error(err)) } else { for _, agentID := range allAgents { if agentID == "" { continue } if _, ok := enabledAgents[agentID]; !ok { s.clearIPCache(agentID, "disabled_or_missing") } } } // 并发向每个配置对应的在线探针发送 DDNS 配置 for _, config := range configs { agentID := config.AgentID go func(id string) { if err := s.sendDDNSConfigToAgent(&config); err != nil { s.logger.Debug("发送 DDNS 配置失败", zap.String("agentID", id), zap.Error(err)) } }(agentID) } } // sendDDNSConfigToAgent 向指定探针发送 DDNS 配置 func (s *DDNSService) sendDDNSConfigToAgent(config *models.DDNSConfig) error { // 获取探针的 DDNS 配置 configData, err := s.GetDDNSConfig(config) if err != nil { return err } msgData, err := json.Marshal(protocol.OutboundMessage{ Type: protocol.MessageTypeDDNSConfig, Data: configData, }) if err != nil { return err } return s.wsManager.SendToClient(config.AgentID, msgData) } // TriggerUpdate 手动触发 DDNS 更新 // 向探针发送配置消息,触发探针立即获取并上报 IP 地址 func (s *DDNSService) TriggerUpdate(ctx context.Context, configID string) error { // 获取配置 config, err := s.GetConfig(ctx, configID) if err != nil { return fmt.Errorf("获取 DDNS 配置失败: %w", err) } // 检查配置是否启用 if !config.Enabled { return fmt.Errorf("DDNS 配置未启用") } s.clearIPCache(config.AgentID, "manual_trigger") // 向探针发送配置,触发探针立即上报 IP if err := s.sendDDNSConfigToAgent(config); err != nil { return fmt.Errorf("向探针发送配置失败: %w", err) } s.logger.Info("手动触发 DDNS 更新成功", zap.String("configID", configID), zap.String("agentID", config.AgentID), zap.String("name", config.Name)) return nil }