Files
Meshray-Manager/docs/DDNS_Usage 前缀定义策略.md
T
2026-06-30 15:14:37 +08:00

602 lines
16 KiB
Markdown
Raw 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.
# DDNS Usage 前缀定义策略
**设计时间**: 2026-03-26
**核心问题**: 如何定义和管理 TXT 记录前缀
---
## 🎯 问题背景
### 当前需求
```
同一个 DDNS 配置(如 example.com)需要支持多个组网同步:
├─ 组网 A → _meshray._mesh.network-a.example.com
├─ 组网 B → _meshray._mesh.network-b.example.com
└─ 组网 C → _custom.prefix.network-c.example.com
问题:前缀 (_meshray._mesh) 如何定义?谁来决定?
```
---
## ✅ 三种设计方案
### 方案一:系统预设固定前缀(推荐)⭐
**设计思路**:
```
系统内置标准前缀,用户不可自定义
├─ MeshSeed 同步专用:_meshray._mesh
├─ 未来扩展 1: _meshray.config (配置同步)
└─ 未来扩展 2: _meshray.device (设备注册)
```
**优点**:
- ✅ 标准化,避免混乱
- ✅ 安全性高(防止恶意前缀)
- ✅ 实现简单
**缺点**:
- ❌ 灵活性较低
- ❌ 无法适配特殊场景
**数据库设计**:
```go
// DDNSUsage 模型 - 前缀字段枚举化
type DDNSUsage struct {
ID string `gorm:"primaryKey;type:varchar(36)"`
ServiceID string `gorm:"type:varchar(36);index"`
// ✅ 方案 A:预设前缀类型(枚举)
PrefixType string `gorm:"type:varchar(32);not null"`
/*
可选值:
- "MESHSEED_SYNC" → 对应 "_meshray._mesh"
- "CONFIG_SYNC" → 对应 "_meshray.config"
- "DEVICE_REG" → 对应 "_meshray.device"
*/
RecordPrefix string `gorm:"-"` // 计算字段,不存储
FullDomain string // 自动生成
NetworkID *uint64 `gorm:"type:bigint;index"`
Enabled bool `gorm:"default:true"`
}
// 方法:获取实际前缀
func (u *DDNSUsage) GetRecordPrefix() string {
switch u.PrefixType {
case "MESHSEED_SYNC":
return "_meshray._mesh"
case "CONFIG_SYNC":
return "_meshray.config"
case "DEVICE_REG":
return "_meshray.device"
default:
panic("未知的前缀类型:" + u.PrefixType)
}
}
```
**前端实现**:
```vue
<!-- 创建 Usage 时只能选择预设类型 -->
<el-form-item label="用途类型" prop="prefix_type">
<el-select v-model="formData.prefix_type" placeholder="请选择">
<el-option
label="MeshSeed 同步 (_meshray._mesh)"
value="MESHSEED_SYNC"
/>
<el-option
label="配置同步 (_meshray.config)"
value="CONFIG_SYNC"
disabled
/>
<el-option
label="设备注册 (_meshray.device)"
value="DEVICE_REG"
disabled
/>
</el-select>
<div class="form-tip">
<el-icon><InfoFilled /></el-icon>
当前仅支持 MeshSeed 同步其他功能开发中
</div>
</el-form-item>
```
---
### 方案二:管理员自定义前缀(灵活)🔧
**设计思路**:
```
管理员创建 Usage 时自由填写前缀
├─ 示例 1: _meshray._mesh
├─ 示例 2: _custom.prefix
└─ 示例 3: anything.you.want
```
**优点**:
- ✅ 灵活性极高
- ✅ 适配各种场景
**缺点**:
- ❌ 容易冲突(需要验证唯一性)
- ❌ 安全性风险(可能输入恶意前缀)
- ❌ 用户学习成本高
**数据库设计**:
```go
type DDNSUsage struct {
ID string `gorm:"primaryKey;type:varchar(36)"`
ServiceID string `gorm:"type:varchar(36);index"`
// ✅ 方案 B:完全自定义前缀
RecordPrefix string `gorm:"type:varchar(255);not null"`
/*
示例:
- "_meshray._mesh"
- "_custom.test"
- "anything"
*/
// 格式验证
validator func(string) error
NetworkID *uint64 `gorm:"type:bigint;index"`
Enabled bool `gorm:"default:true"`
}
// 验证函数
func validateRecordPrefix(prefix string) error {
if prefix == "" {
return fmt.Errorf("前缀不能为空")
}
// DNS 标签规则
if len(prefix) > 253 {
return fmt.Errorf("前缀过长(最大 253 字符)")
}
// 只能包含字母、数字、连字符、点
matched, _ := regexp.MatchString(`^[a-zA-Z0-9._-]+$`, prefix)
if !matched {
return fmt.Errorf("前缀只能包含字母、数字、点、下划线和连字符")
}
// 不能以特殊字符开头
if strings.HasPrefix(prefix, "_") && !strings.HasPrefix(prefix, "_meshray") {
return fmt.Errorf("下划线前缀仅限系统使用")
}
return nil
}
```
**前端实现**:
```vue
<el-form-item label="TXT 记录前缀" prop="record_prefix">
<el-input
v-model="formData.record_prefix"
placeholder="_meshray._mesh"
maxlength="253"
/>
<div class="form-tip">
<el-icon><InfoFilled /></el-icon>
完整记录名{{ formData.record_prefix }}.网络名称域名
</div>
<div class="form-tip">
<el-icon><WarningFilled /></el-icon>
只能包含字母数字下划线和连字符
</div>
</el-form-item>
```
---
### 方案三:混合模式(最佳实践)🏆
**设计思路**:
```
系统预设 + 管理员自定义组合
├─ 预设类型:快速选择,安全可靠
└─ 自定义:高级选项,满足特殊需求
```
**数据库设计**:
```go
type DDNSUsage struct {
ID string `gorm:"primaryKey;type:varchar(36)"`
ServiceID string `gorm:"type:varchar(36);index"`
// ✅ 方案 C:混合模式
PrefixMode string `gorm:"type:varchar(16);not null;default:'preset'"`
/*
- "preset" → 使用预设类型
- "custom" → 使用自定义前缀
*/
PrefixType string `gorm:"type:varchar(32)"` // preset 模式下使用
RecordPrefix string `gorm:"type:varchar(255)"` // custom 模式下使用
NetworkID *uint64 `gorm:"type:bigint;index"`
Enabled bool `gorm:"default:true"`
}
// 方法:获取实际前缀
func (u *DDNSUsage) GetRecordPrefix() string {
if u.PrefixMode == "preset" {
return u.GetPresetPrefix()
} else {
return u.RecordPrefix
}
}
func (u *DDNSUsage) GetPresetPrefix() string {
switch u.PrefixType {
case "MESHSEED_SYNC":
return "_meshray._mesh"
case "CONFIG_SYNC":
return "_meshray.config"
default:
return "_meshray._mesh" // 默认回退
}
}
```
**前端实现**:
```vue
<el-form-item label="前缀模式" prop="prefix_mode">
<el-radio-group v-model="formData.prefix_mode">
<el-radio label="preset">系统预设</el-radio>
<el-radio label="custom">自定义</el-radio>
</el-radio-group>
</el-form-item>
<!-- 预设模式 -->
<el-form-item v-if="formData.prefix_mode === 'preset'" label="预设类型">
<el-select v-model="formData.prefix_type" placeholder="请选择">
<el-option
label="✨ MeshSeed 同步 (_meshray._mesh)"
value="MESHSEED_SYNC"
/>
<el-option
label="🔧 配置同步 (_meshray.config)"
value="CONFIG_SYNC"
disabled
/>
</el-select>
</el-form-item>
<!-- 自定义模式 -->
<el-form-item v-else label="TXT 记录前缀">
<el-input
v-model="formData.record_prefix"
placeholder="例如:my.custom.prefix"
maxlength="253"
/>
<div class="form-tip">
<el-icon><InfoFilled /></el-icon>
需符合 DNS 命名规范
</div>
</el-form-item>
```
---
## 🎯 推荐方案:混合模式
### 理由
1. **兼顾安全与灵活**
- 默认使用预设,避免错误
- 允许高级用户自定义
2. **渐进式扩展**
- 初期只有 MeshSeed 同步
- 后续可增加其他用途
3. **用户体验好**
- 普通用户选预设即可
- 专家用户可以深度定制
---
## 🔧 完整实现(混合模式)
### 后端验证逻辑
```go
// internal/service/ddns_usage.go
// CreateUsage 创建 DDNS 使用方式
func (s *DDNSService) CreateUsage(ctx context.Context, req CreateUsageRequest) (*model.DDNSUsage, error) {
// 1. 验证 DDNS 服务存在
var service model.ExternalService
if err := s.db.First(&service, req.ServiceID).Error; err != nil {
return nil, fmt.Errorf("DDNS 服务不存在")
}
// 2. 根据模式验证前缀
var recordPrefix string
if req.PrefixMode == "preset" {
// 预设模式:验证类型合法性
switch req.PrefixType {
case "MESHSEED_SYNC":
recordPrefix = "_meshray._mesh"
default:
return nil, fmt.Errorf("未知的预设类型")
}
} else if req.PrefixMode == "custom" {
// 自定义模式:严格验证格式
if err := validateCustomPrefix(req.RecordPrefix); err != nil {
return nil, err
}
recordPrefix = req.RecordPrefix
} else {
return nil, fmt.Errorf("无效的前缀模式")
}
// 3. 解析域名
var config map[string]interface{}
json.Unmarshal([]byte(service.Config), &config)
domain, _ := config["domain"].(string)
if domain == "" {
return nil, fmt.Errorf("DDNS 配置缺少域名")
}
// 4. 创建 Usage
usage := &model.DDNSUsage{
ID: generateUUID(),
ServiceID: req.ServiceID,
PrefixMode: req.PrefixMode,
PrefixType: req.PrefixType,
RecordPrefix: recordPrefix,
FullDomain: fmt.Sprintf("%s.%s", recordPrefix, domain),
Enabled: true,
}
// 5. 检查是否重复(同一域名 + 前缀组合)
var count int64
s.db.Model(&model.DDNSUsage{}).
Where("service_id = ? AND record_prefix = ? AND network_id IS NOT NULL",
req.ServiceID, recordPrefix).
Count(&count)
if count > 0 {
return nil, fmt.Errorf("该前缀已被其他组网占用")
}
// 6. 保存
if err := s.db.Create(usage).Error; err != nil {
return nil, fmt.Errorf("创建失败:%w", err)
}
return usage, nil
}
// 自定义前缀验证
func validateCustomPrefix(prefix string) error {
if prefix == "" {
return fmt.Errorf("前缀不能为空")
}
if len(prefix) > 253 {
return fmt.Errorf("前缀过长")
}
// DNS 标签规范
if !regexp.MustCompile(`^[a-zA-Z0-9._-]+$`).MatchString(prefix) {
return fmt.Errorf("前缀只能包含字母、数字、点、下划线和连字符")
}
// 保留前缀检查
if strings.HasPrefix(prefix, "_meshray.") && prefix != "_meshray._mesh" {
return fmt.Errorf("_meshray.* 前缀为系统保留")
}
return nil
}
```
---
### 前端完整表单
```vue
<!-- CreateUsageDialog.vue -->
<template>
<el-dialog title="创建 DDNS 使用方式" v-model="visible">
<el-form :model="form" label-width="120px">
<!-- 选择 DDNS 配置 -->
<el-form-item label="DDNS 服务" required>
<el-select v-model="form.service_id" filterable style="width: 100%">
<el-option
v-for="svc in ddnsServices"
:key="svc.id"
:label="`${svc.name} (${svc.config.domain})`"
:value="svc.id"
/>
</el-select>
</el-form-item>
<!-- 前缀模式 -->
<el-form-item label="前缀模式" required>
<el-radio-group v-model="form.prefix_mode">
<el-radio label="preset">
系统预设
<span class="radio-desc">推荐使用安全可靠</span>
</el-radio>
<el-radio label="custom">
🔧 自定义
<span class="radio-desc">高级选项需谨慎填写</span>
</el-radio>
</el-radio-group>
</el-form-item>
<!-- 预设类型 -->
<el-form-item v-if="form.prefix_mode === 'preset'" label="预设类型" required>
<el-select v-model="form.prefix_type" style="width: 100%">
<el-option
label="✨ MeshSeed 同步 (_meshray._mesh)"
value="MESHSEED_SYNC"
>
<div style="display: flex; justify-content: space-between;">
<span>MeshSeed 同步</span>
<el-tag size="small" type="success">推荐</el-tag>
</div>
<div class="option-desc">用于组网配置加密同步到 DNS TXT 记录</div>
</el-option>
<el-option
label="🔧 配置同步 (_meshray.config)"
value="CONFIG_SYNC"
disabled
>
<div class="option-desc">即将支持敬请期待</div>
</el-option>
</el-select>
</el-form-item>
<!-- 自定义前缀 -->
<el-form-item v-else label="TXT 记录前缀" required>
<el-input
v-model="form.record_prefix"
placeholder="例如:my.custom.prefix"
maxlength="253"
show-word-limit
/>
<div class="form-tip">
<el-icon><InfoFilled /></el-icon>
完整记录名{{ form.record_prefix }}.网络名称域名
</div>
<div class="form-tip">
<el-icon><WarningFilled /></el-icon>
只能包含字母数字下划线和连字符
</div>
<div class="form-tip">
<el-icon><WarningFilled /></el-icon>
_meshray.* 前缀为系统保留不能使用
</div>
</el-form-item>
<!-- 用途说明 -->
<el-form-item label="用途说明">
<el-input
v-model="form.description"
type="textarea"
:rows="2"
placeholder="描述这个用途,如:办公网络 MeshSeed 同步"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">创建</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
const form = ref({
service_id: '',
prefix_mode: 'preset',
prefix_type: 'MESHSEED_SYNC',
record_prefix: '',
description: ''
})
const handleSubmit = async () => {
try {
await createDDNSUsage(form.value)
ElMessage.success('创建成功')
emit('success')
visible.value = false
} catch (error: any) {
ElMessage.error('创建失败:' + error.message)
}
}
</script>
```
---
## 📊 实际应用示例
### 场景 1: 标准企业用户(使用预设)
```
公司 IT 管理员配置:
1. 添加 DDNS 服务
└─ Cloudflare + mesh.company.com
2. 创建 Usage(预设模式)
└─ 类型:MeshSeed 同步 (_meshray._mesh)
3. 创建组网 A
└─ 选择 Usage → _meshray._mesh.office.mesh.company.com
4. 创建组网 B
└─ 选择 Usage → _meshray._mesh.dev.mesh.company.com
结果:
✅ 自动分配不同子域名
✅ 不会冲突
✅ 管理简单
```
---
### 场景 2: 高级用户(自定义前缀)
```
技术专家配置:
1. 添加 DDNS 服务
└─ Cloudflare + example.com
2. 创建 Usage(自定义模式)
└─ 前缀:prod.meshray.sync
3. 创建生产环境组网
└─ 选择 Usage → prod.meshray.sync.prod-net.example.com
4. 创建第二个 Usage
└─ 前缀:test.meshray.sync
5. 创建测试环境组网
└─ 选择 Usage → test.meshray.sync.test-net.example.com
结果:
✅ 环境隔离清晰
✅ 命名规范自主定义
✅ 灵活性极高
```
---
## ✅ 最终建议
**采用混合模式**:
1. **默认引导用户使用预设** (90% 场景)
- MeshSeed 同步专用前缀:`_meshray._mesh`
- 安全、标准、无需思考
2. **提供自定义入口** (10% 高级场景)
- 严格验证格式
- 保留系统前缀
- 防止冲突
3. **未来扩展预留**
- `_meshray.config` - 配置同步
- `_meshray.device` - 设备注册
- `_meshray.log` - 日志投递
这样既保证了大多数用户的易用性,又满足了高级用户的灵活性需求!🎯
需要我立即开始实现吗?