602 lines
16 KiB
Markdown
602 lines
16 KiB
Markdown
# 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` - 日志投递
|
||
|
||
这样既保证了大多数用户的易用性,又满足了高级用户的灵活性需求!🎯
|
||
|
||
需要我立即开始实现吗?
|