Files
zkcoi e9ca2f7d70 fix: rename manager module to git.zkcoi.com/zkcoi/Meshray-Manager
- go.mod module path matches repo zkcoi/Meshray-Manager (case-distinct from zkcoi/Meshray/core)
- rewrite internal imports meshray/{internal,web,pkg} -> Meshray-Manager/... (core refs kept)
- sync README.md / install.sh repo URLs; add CHANGELOG entry
2026-07-15 16:25:46 +08:00

358 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"fmt"
"time"
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/dnsprovider"
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/model"
"github.com/libdns/libdns"
"go.uber.org/zap"
"gorm.io/gorm"
)
// DDNSOperationService DDNS 操作服务(全功能模式)
type DDNSOperationService struct {
logger *zap.Logger
db *gorm.DB
}
// NewDDNSOperationService 创建 DDNS 操作服务
func NewDDNSOperationService(logger *zap.Logger, db *gorm.DB) *DDNSOperationService {
return &DDNSOperationService{
logger: logger,
db: db,
}
}
// CreateDNSRecord 创建 DNS 记录(全功能模式)
func (s *DDNSOperationService) CreateDNSRecord(config *model.Service, recordType string, name string, value string, ttl int) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. 获取关联的 DDNS 配置
ddnsConfig, err := s.getDDNSConfig(config.DDNSConfigID)
if err != nil {
return fmt.Errorf("获取 DDNS 配置失败:%w", err)
}
// 2. 创建 DNS Provider
providerConfig := dnsprovider.ProviderConfig{
Provider: dnsprovider.ProviderType(ddnsConfig.Provider),
Domain: ddnsConfig.Domain,
APIToken: ddnsConfig.Token,
AccessKeyID: ddnsConfig.AuthUsername,
AccessKeySecret: ddnsConfig.AuthPassword,
SecretId: ddnsConfig.AuthUsername,
SecretKey: ddnsConfig.AuthPassword,
}
provider, err := dnsprovider.NewDNSProvider(providerConfig)
if err != nil {
return fmt.Errorf("创建 DNS Provider 失败:%w", err)
}
// 3. 构建 DNS 记录
dnsRecord := &dnsprovider.DNSRecord{
Type: dnsprovider.RecordType(recordType),
Name: name,
Value: value,
TTL: ttl,
}
// 4. 添加 DNS 记录
zone := ddnsConfig.Domain
libdnsRecord := dnsRecord.ToLibdnsRecord()
s.logger.Info("开始创建 DNS 记录",
zap.String("type", recordType),
zap.String("name", name),
zap.String("value", value),
zap.String("domain", zone))
_, err = provider.AppendRecords(ctx, zone, []libdns.Record{libdnsRecord})
if err != nil {
return fmt.Errorf("添加 DNS 记录失败:%w", err)
}
s.logger.Info("DNS 记录创建成功",
zap.String("type", recordType),
zap.String("name", name),
zap.String("domain", zone))
return nil
}
// UpdateDNSRecord 更新 DNS 记录(全功能模式)
func (s *DDNSOperationService) UpdateDNSRecord(config *model.Service, recordType string, name string, value string, ttl int) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. 获取关联的 DDNS 配置
ddnsConfig, err := s.getDDNSConfig(config.DDNSConfigID)
if err != nil {
return fmt.Errorf("获取 DDNS 配置失败:%w", err)
}
// 2. 创建 DNS Provider
providerConfig := dnsprovider.ProviderConfig{
Provider: dnsprovider.ProviderType(ddnsConfig.Provider),
Domain: ddnsConfig.Domain,
APIToken: ddnsConfig.Token,
AccessKeyID: ddnsConfig.AuthUsername,
AccessKeySecret: ddnsConfig.AuthPassword,
SecretId: ddnsConfig.AuthUsername,
SecretKey: ddnsConfig.AuthPassword,
}
provider, err := dnsprovider.NewDNSProvider(providerConfig)
if err != nil {
return fmt.Errorf("创建 DNS Provider 失败:%w", err)
}
// 3. 构建新的 DNS 记录
newRecord := &dnsprovider.DNSRecord{
Type: dnsprovider.RecordType(recordType),
Name: name,
Value: value,
TTL: ttl,
}
// 4. 使用 SetRecords 覆盖现有记录(会自动删除旧记录并创建新记录)
zone := ddnsConfig.Domain
libdnsRecord := newRecord.ToLibdnsRecord()
s.logger.Info("开始更新 DNS 记录",
zap.String("type", recordType),
zap.String("name", name),
zap.String("value", value),
zap.String("domain", zone))
_, err = provider.SetRecords(ctx, zone, []libdns.Record{libdnsRecord})
if err != nil {
return fmt.Errorf("更新 DNS 记录失败:%w", err)
}
s.logger.Info("DNS 记录更新成功",
zap.String("type", recordType),
zap.String("name", name),
zap.String("domain", zone))
return nil
}
// DeleteDNSRecord 删除 DNS 记录(全功能模式)
func (s *DDNSOperationService) DeleteDNSRecord(config *model.Service, recordType string, name string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. 获取关联的 DDNS 配置
ddnsConfig, err := s.getDDNSConfig(config.DDNSConfigID)
if err != nil {
return fmt.Errorf("获取 DDNS 配置失败:%w", err)
}
// 2. 创建 DNS Provider
providerConfig := dnsprovider.ProviderConfig{
Provider: dnsprovider.ProviderType(ddnsConfig.Provider),
Domain: ddnsConfig.Domain,
APIToken: ddnsConfig.Token,
AccessKeyID: ddnsConfig.AuthUsername,
AccessKeySecret: ddnsConfig.AuthPassword,
SecretId: ddnsConfig.AuthUsername,
SecretKey: ddnsConfig.AuthPassword,
}
provider, err := dnsprovider.NewDNSProvider(providerConfig)
if err != nil {
return fmt.Errorf("创建 DNS Provider 失败:%w", err)
}
// 3. 先获取所有记录
records, err := provider.GetRecords(ctx, ddnsConfig.Domain)
if err != nil {
return fmt.Errorf("获取 DNS 记录失败:%w", err)
}
// 4. 找到要删除的记录
var targetRecord *libdns.Record
for _, rec := range records {
if rec.Type == recordType && rec.Name == name {
targetRecord = &rec
break
}
}
if targetRecord == nil {
s.logger.Warn("DNS 记录不存在,跳过删除",
zap.String("type", recordType),
zap.String("name", name))
return nil
}
// 5. 删除记录
s.logger.Info("开始删除 DNS 记录",
zap.String("type", recordType),
zap.String("name", name),
zap.String("domain", ddnsConfig.Domain))
_, err = provider.DeleteRecords(ctx, ddnsConfig.Domain, []libdns.Record{*targetRecord})
if err != nil {
return fmt.Errorf("删除 DNS 记录失败:%w", err)
}
s.logger.Info("DNS 记录删除成功",
zap.String("type", recordType),
zap.String("name", name),
zap.String("domain", ddnsConfig.Domain))
return nil
}
// getDDNSConfig 获取关联的 DDNS 配置
func (s *DDNSOperationService) getDDNSConfig(configID string) (*model.Service, error) {
// 从 Service 表中查询 ID=configID 且 Type=DDNS 的记录
var ddnsService model.Service
if err := s.db.Where("id = ? AND type = 'DDNS'", configID).First(&ddnsService).Error; err != nil {
return nil, fmt.Errorf("查询 DDNS 配置失败:%w", err)
}
return &ddnsService, nil
}
// SyncMeshSeedToDNS 同步 MeshSeed 到 DNS TXT 记录(带重试机制)
func (s *DDNSOperationService) SyncMeshSeedToDNS(networkID uint64, seedString string, ddnsServiceID string) error {
const maxRetries = 3
var lastErr error
// 查询 DDNS 配置
var ddnsService model.Service
if err := s.db.First(&ddnsService, ddnsServiceID).Error; err != nil {
return fmt.Errorf("查询 DDNS 服务失败:%w", err)
}
// 查询网络获取前缀
var network model.Network
if err := s.db.First(&network, networkID).Error; err != nil {
return fmt.Errorf("查询网络失败:%w", err)
}
// 重试逻辑(指数退避)
for attempt := 1; attempt <= maxRetries; attempt++ {
lastErr = s.doSyncMeshSeedToDNS(&network, &ddnsService, seedString)
if lastErr == nil {
// 成功,更新状态
s.updateSyncStatus(networkID, "success", "")
s.logger.Info("MeshSeed 同步到 DNS 成功",
zap.Uint64("network_id", networkID),
zap.Int("attempt", attempt))
return nil
}
// 失败,记录日志
s.logger.Warn("MeshSeed 同步失败",
zap.Uint64("network_id", networkID),
zap.Int("attempt", attempt),
zap.Error(lastErr))
// 等待后重试(指数退避:1s, 2s, 4s...
if attempt < maxRetries {
waitTime := time.Duration(1<<uint(attempt-1)) * time.Second
time.Sleep(waitTime)
}
}
// 全部失败,更新状态
s.updateSyncStatus(networkID, "failed", fmt.Sprintf("重试%d次失败:%v", maxRetries, lastErr))
return fmt.Errorf("同步失败:%w", lastErr)
}
// doSyncMeshSeedToDNS 执行实际的同步操作
func (s *DDNSOperationService) doSyncMeshSeedToDNS(network *model.Network, ddnsService *model.Service, seedString string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. 创建 DNS Provider
providerConfig := dnsprovider.ProviderConfig{
Provider: dnsprovider.ProviderType(ddnsService.Provider),
Domain: ddnsService.Domain,
APIToken: ddnsService.Token,
AccessKeyID: ddnsService.AuthUsername,
AccessKeySecret: ddnsService.AuthPassword,
SecretId: ddnsService.AuthUsername,
SecretKey: ddnsService.AuthPassword,
}
provider, err := dnsprovider.NewDNSProvider(providerConfig)
if err != nil {
return fmt.Errorf("创建 DNS Provider 失败:%w", err)
}
// 2. 构建 TXT 记录名称
txtRecordName := network.DDNSPrefix
if txtRecordName == "" {
return fmt.Errorf("DDNS 前缀为空")
}
// 3. 加密 MeshSeedAES-256-GCM
encryptedSeed, err := s.encryptMeshSeed(seedString, network.ID)
if err != nil {
return fmt.Errorf("加密 MeshSeed 失败:%w", err)
}
s.logger.Info("开始同步 MeshSeed 到 DNS",
zap.String("record_name", txtRecordName),
zap.String("domain", ddnsService.Domain))
// 4. 创建/更新 TXT 记录
zone := ddnsService.Domain
libdnsRecord := libdns.Record{
Type: "TXT",
Name: txtRecordName,
Value: encryptedSeed,
TTL: time.Duration(600) * time.Second,
}
// 先尝试删除旧记录(如果存在)
_, err = provider.DeleteRecords(ctx, zone, []libdns.Record{libdnsRecord})
if err != nil {
s.logger.Debug("删除旧记录失败(可能不存在)",
zap.String("name", txtRecordName),
zap.Error(err))
}
// 添加新记录
_, err = provider.AppendRecords(ctx, zone, []libdns.Record{libdnsRecord})
if err != nil {
return fmt.Errorf("添加 DNS 记录失败:%w", err)
}
s.logger.Info("MeshSeed 同步到 DNS 成功",
zap.String("record_name", txtRecordName),
zap.String("domain", ddnsService.Domain))
return nil
}
// encryptMeshSeed 加密 MeshSeedAES-256-GCM
func (s *DDNSOperationService) encryptMeshSeed(plaintext string, networkID uint64) (string, error) {
// TODO: 实现 AES-256-GCM 加密
// 密钥派生:SHA256("meshray-ddns" + Network.Secret)
// 目前先返回明文(P3 阶段实现)
return plaintext, nil
}
// updateSyncStatus 更新同步状态
func (s *DDNSOperationService) updateSyncStatus(networkID uint64, status, message string) {
// 更新 NetworkDDNSBinding 表
var binding model.NetworkDDNSBinding
if err := s.db.Where("network_id = ?", networkID).First(&binding).Error; err == nil {
binding.Status = status
now := time.Now()
binding.LastSyncAt = &now
binding.SyncMessage = message
s.db.Save(&binding)
}
}