- 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
285 lines
7.5 KiB
Go
285 lines
7.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/dnsprovider"
|
|
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/model"
|
|
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/store/sqlite"
|
|
"github.com/google/uuid"
|
|
"github.com/libdns/libdns"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ServiceService 服务管理服务
|
|
type ServiceService struct {
|
|
store *sqlite.Store
|
|
}
|
|
|
|
// NewServiceService 创建服务服务实例
|
|
func NewServiceService(store *sqlite.Store) *ServiceService {
|
|
return &ServiceService{store: store}
|
|
}
|
|
|
|
// ListServices 获取服务列表(支持按类型筛选)
|
|
func (s *ServiceService) ListServices(serverType string) ([]model.Service, error) {
|
|
var services []model.Service
|
|
query := s.store.DB().Order("created_at desc")
|
|
if serverType != "" {
|
|
// 支持大小写不敏感匹配
|
|
query = query.Where("LOWER(type) = LOWER(?)", serverType)
|
|
}
|
|
err := query.Find(&services).Error
|
|
return services, err
|
|
}
|
|
|
|
// GetServiceByID 根据 ID 获取服务
|
|
func (s *ServiceService) GetServiceByID(id string) (*model.Service, error) {
|
|
var service model.Service
|
|
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("服务不存在")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &service, nil
|
|
}
|
|
|
|
// CreateService 创建服务
|
|
func (s *ServiceService) CreateService(req *model.Service) (*model.Service, error) {
|
|
if req.Name == "" {
|
|
return nil, errors.New("服务名称不能为空")
|
|
}
|
|
if req.Type == "" {
|
|
return nil, errors.New("服务类型不能为空")
|
|
}
|
|
|
|
// 强制转换为大写以保持一致性
|
|
req.Type = strings.ToUpper(req.Type)
|
|
|
|
// 校验支持的类型
|
|
allowedTypes := map[string]bool{
|
|
"STUN": true, "TURN": true, "DDNS": true, "TUN": true, "CUSTOM": true,
|
|
}
|
|
if !allowedTypes[req.Type] {
|
|
return nil, fmt.Errorf("不支持的服务类型: %s", req.Type)
|
|
}
|
|
|
|
// 地址和端口校验(除 TUN 外通常需要)
|
|
if req.Type != "TUN" {
|
|
if req.Address == "" {
|
|
return nil, errors.New("服务器地址不能为空")
|
|
}
|
|
if req.Port <= 0 || req.Port > 65535 {
|
|
return nil, errors.New("端口号无效")
|
|
}
|
|
}
|
|
|
|
// DDNS 鉴权校验(基础设施模式)
|
|
if req.Type == "DDNS" && req.ConfigMode == "infrastructure" {
|
|
if req.Provider == "" {
|
|
return nil, errors.New("请选择 DNS 服务商")
|
|
}
|
|
if req.Domain == "" {
|
|
return nil, errors.New("请输入根域名")
|
|
}
|
|
// 根据服务商校验认证信息
|
|
switch req.Provider {
|
|
case "cloudflare":
|
|
if req.Token == "" {
|
|
return nil, errors.New("请输入 API Token")
|
|
}
|
|
case "aliyun":
|
|
if req.AuthUsername == "" || req.AuthPassword == "" {
|
|
return nil, errors.New("请输入 AccessKey ID 和 Secret")
|
|
}
|
|
case "tencent":
|
|
if req.AuthUsername == "" || req.AuthPassword == "" {
|
|
return nil, errors.New("请输入 SecretId 和 SecretKey")
|
|
}
|
|
}
|
|
}
|
|
|
|
// DDNS 全功能模式校验
|
|
if req.Type == "DDNS" && req.ConfigMode == "fullservice" {
|
|
if req.DDNSConfigID == "" {
|
|
return nil, errors.New("请选择 DDNS 配置")
|
|
}
|
|
if req.RecordType == "" {
|
|
return nil, errors.New("请选择记录类型")
|
|
}
|
|
// 根据记录类型校验字段
|
|
switch req.RecordType {
|
|
case "A", "AAAA":
|
|
if req.Subdomain == "" {
|
|
return nil, errors.New("请输入主机记录")
|
|
}
|
|
if req.TargetIP == "" {
|
|
return nil, errors.New("请输入目标 IP")
|
|
}
|
|
if req.Port <= 0 {
|
|
return nil, errors.New("请输入检测端口")
|
|
}
|
|
case "TXT":
|
|
if req.TXTRecordName == "" {
|
|
return nil, errors.New("请输入 TXT 记录名称")
|
|
}
|
|
if req.TXTValue == "" {
|
|
return nil, errors.New("请输入 TXT 记录值")
|
|
}
|
|
case "CNAME":
|
|
if req.CNAMETarget == "" {
|
|
return nil, errors.New("请输入目标域名")
|
|
}
|
|
}
|
|
}
|
|
|
|
// 生成 ID
|
|
if req.ID == "" {
|
|
req.ID = uuid.New().String()
|
|
}
|
|
|
|
// 如果是 DDNS 全功能模式,先创建 DNS 记录
|
|
if req.Type == "DDNS" && req.ConfigMode == "fullservice" {
|
|
// 使用事务确保原子性
|
|
tx := s.store.DB().Begin()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
// 1. 获取关联的 DDNS 配置
|
|
var ddnsConfig model.Service
|
|
if err := tx.Where("id = ?", req.DDNSConfigID).First(&ddnsConfig).Error; err != nil {
|
|
tx.Rollback()
|
|
return nil, fmt.Errorf("获取 DDNS 配置失败:%w", err)
|
|
}
|
|
|
|
// 2. 确定记录类型、名称和值
|
|
var recordType, name, value string
|
|
switch req.RecordType {
|
|
case "A", "AAAA":
|
|
recordType = req.RecordType
|
|
name = req.Subdomain
|
|
value = req.TargetIP
|
|
case "TXT":
|
|
recordType = req.RecordType
|
|
name = req.TXTRecordName
|
|
value = req.TXTValue
|
|
case "CNAME":
|
|
recordType = req.RecordType
|
|
name = req.Subdomain
|
|
value = req.CNAMETarget
|
|
default:
|
|
tx.Rollback()
|
|
return nil, errors.New("不支持的记录类型")
|
|
}
|
|
|
|
// 3. 创建 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 {
|
|
tx.Rollback()
|
|
return nil, fmt.Errorf("创建 DNS Provider 失败:%w", err)
|
|
}
|
|
|
|
// 4. 构建并添加 DNS 记录
|
|
dnsRecord := &dnsprovider.DNSRecord{
|
|
Type: dnsprovider.RecordType(recordType),
|
|
Name: name,
|
|
Value: value,
|
|
TTL: req.TTL,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
_, err = provider.AppendRecords(ctx, ddnsConfig.Domain, []libdns.Record{dnsRecord.ToLibdnsRecord()})
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return nil, fmt.Errorf("创建 DNS 记录失败:%w", err)
|
|
}
|
|
|
|
// 5. DNS 记录创建成功,保存到数据库
|
|
if err := tx.Create(req).Error; err != nil {
|
|
tx.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
tx.Commit()
|
|
return req, nil
|
|
}
|
|
|
|
// 其他情况直接保存
|
|
if err := s.store.DB().Create(req).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
// UpdateService 更新服务
|
|
func (s *ServiceService) UpdateService(id string, updates map[string]interface{}) (*model.Service, error) {
|
|
var service model.Service
|
|
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
|
|
return nil, errors.New("服务不存在")
|
|
}
|
|
|
|
if err := s.store.DB().Model(&service).Updates(updates).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &service, nil
|
|
}
|
|
|
|
// DeleteService 删除服务
|
|
func (s *ServiceService) DeleteService(id string) error {
|
|
var service model.Service
|
|
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
|
|
return errors.New("服务不存在")
|
|
}
|
|
return s.store.DB().Delete(&service).Error
|
|
}
|
|
|
|
// TestServiceConnectivity 测试服务连通性
|
|
func (s *ServiceService) TestServiceConnectivity(id string) (string, error) {
|
|
var service model.Service
|
|
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
|
|
return "", errors.New("服务不存在")
|
|
}
|
|
|
|
addr := net.JoinHostPort(service.Address, strconv.Itoa(service.Port))
|
|
|
|
if service.Type == "STUN" {
|
|
// STUN 使用 UDP 测试
|
|
conn, err := net.DialTimeout("udp", addr, 5*time.Second)
|
|
if err != nil {
|
|
return "unreachable", nil
|
|
}
|
|
conn.Close()
|
|
return "ok", nil
|
|
}
|
|
|
|
// 其他使用 TCP 测试
|
|
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
|
if err != nil {
|
|
return "unreachable", nil
|
|
}
|
|
conn.Close()
|
|
return "ok", nil
|
|
}
|