Initial commit

This commit is contained in:
2026-06-30 15:14:37 +08:00
commit 15dab96872
311 changed files with 95639 additions and 0 deletions
+452
View File
@@ -0,0 +1,452 @@
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):
}
}
}