前端页面

This commit is contained in:
robin
2026-02-24 22:43:49 +08:00
parent 2eb32b9f1f
commit 4d275c921d
18 changed files with 611 additions and 485 deletions

View File

@@ -1,23 +0,0 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
)
type AppSettingsResetAESSecretAction struct {
actionutils.ParentAction
}
func (this *AppSettingsResetAESSecretAction) RunPost(params struct {
AppId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
app := pickApp(params.AppId)
resetAESSecret(app)
this.Success()
}

View File

@@ -17,7 +17,6 @@ var appSettingsStore = struct {
func defaultAppSettings(app maps.Map) maps.Map { func defaultAppSettings(app maps.Map) maps.Map {
signSecretPlain := randomPlainSecret("ss") signSecretPlain := randomPlainSecret("ss")
aesSecretPlain := randomPlainSecret("as")
return maps.Map{ return maps.Map{
"appId": app.GetString("appId"), "appId": app.GetString("appId"),
"primaryClusterId": app.GetInt64("clusterId"), "primaryClusterId": app.GetInt64("clusterId"),
@@ -25,9 +24,6 @@ func defaultAppSettings(app maps.Map) maps.Map {
"signSecretPlain": signSecretPlain, "signSecretPlain": signSecretPlain,
"signSecretMasked": maskSecret(signSecretPlain), "signSecretMasked": maskSecret(signSecretPlain),
"signSecretUpdatedAt": "2026-02-20 12:30:00", "signSecretUpdatedAt": "2026-02-20 12:30:00",
"aesSecretPlain": aesSecretPlain,
"aesSecretMasked": maskSecret(aesSecretPlain),
"aesSecretUpdatedAt": "2026-02-12 09:45:00",
"appStatus": app.GetBool("isOn"), "appStatus": app.GetBool("isOn"),
"defaultTTL": 30, "defaultTTL": 30,
"fallbackTimeoutMs": 300, "fallbackTimeoutMs": 300,
@@ -52,9 +48,6 @@ func cloneSettings(settings maps.Map) maps.Map {
"signSecretPlain": settings.GetString("signSecretPlain"), "signSecretPlain": settings.GetString("signSecretPlain"),
"signSecretMasked": settings.GetString("signSecretMasked"), "signSecretMasked": settings.GetString("signSecretMasked"),
"signSecretUpdatedAt": settings.GetString("signSecretUpdatedAt"), "signSecretUpdatedAt": settings.GetString("signSecretUpdatedAt"),
"aesSecretPlain": settings.GetString("aesSecretPlain"),
"aesSecretMasked": settings.GetString("aesSecretMasked"),
"aesSecretUpdatedAt": settings.GetString("aesSecretUpdatedAt"),
"appStatus": settings.GetBool("appStatus"), "appStatus": settings.GetBool("appStatus"),
"defaultTTL": settings.GetInt("defaultTTL"), "defaultTTL": settings.GetInt("defaultTTL"),
"fallbackTimeoutMs": settings.GetInt("fallbackTimeoutMs"), "fallbackTimeoutMs": settings.GetInt("fallbackTimeoutMs"),
@@ -110,16 +103,6 @@ func resetSignSecret(app maps.Map) maps.Map {
return settings return settings
} }
func resetAESSecret(app maps.Map) maps.Map {
settings := loadAppSettings(app)
aesSecretPlain := randomPlainSecret("as")
settings["aesSecretPlain"] = aesSecretPlain
settings["aesSecretMasked"] = maskSecret(aesSecretPlain)
settings["aesSecretUpdatedAt"] = nowDateTime()
saveAppSettings(app.GetInt64("id"), settings)
return settings
}
func nowDateTime() string { func nowDateTime() string {
return time.Now().Format("2006-01-02 15:04:05") return time.Now().Format("2006-01-02 15:04:05")
} }
@@ -182,21 +165,6 @@ func ensureSettingsFields(settings maps.Map) bool {
changed = true changed = true
} }
aesSecretPlain := settings.GetString("aesSecretPlain")
if len(aesSecretPlain) == 0 {
aesSecretPlain = randomPlainSecret("as")
settings["aesSecretPlain"] = aesSecretPlain
changed = true
}
if len(settings.GetString("aesSecretMasked")) == 0 {
settings["aesSecretMasked"] = maskSecret(aesSecretPlain)
changed = true
}
if len(settings.GetString("aesSecretUpdatedAt")) == 0 {
settings["aesSecretUpdatedAt"] = nowDateTime()
changed = true
}
if len(settings.GetString("sniPolicy")) == 0 { if len(settings.GetString("sniPolicy")) == 0 {
settings["sniPolicy"] = "level2" settings["sniPolicy"] = "level2"
changed = true changed = true

View File

@@ -19,7 +19,6 @@ func init() {
GetPost("/app/settings", new(AppSettingsAction)). GetPost("/app/settings", new(AppSettingsAction)).
Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)). Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)).
Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)). Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)).
Post("/app/settings/resetAESSecret", new(AppSettingsResetAESSecretAction)).
Get("/domains", new(DomainsAction)). Get("/domains", new(DomainsAction)).
Get("/customRecords", new(CustomRecordsAction)). Get("/customRecords", new(CustomRecordsAction)).
GetPost("/createPopup", new(CreatePopupAction)). GetPost("/createPopup", new(CreatePopupAction)).

View File

@@ -45,7 +45,7 @@ func (this *ClusterSettingsAction) RunGet(params struct {
cid := strconv.FormatInt(params.ClusterId, 10) cid := strconv.FormatInt(params.ClusterId, 10)
this.Data["leftMenuItems"] = []map[string]interface{}{ this.Data["leftMenuItems"] = []map[string]interface{}{
{"name": "基础设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=basic", "isActive": section == "basic"}, {"name": "基础设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=basic", "isActive": section == "basic"},
{"name": "TLS", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=tls", "isActive": section == "tls"}, {"name": "端口设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=tls", "isActive": section == "tls"},
} }
settings["isDefaultCluster"] = (policies.LoadDefaultClusterID() == cluster.GetInt64("id")) settings["isDefaultCluster"] = (policies.LoadDefaultClusterID() == cluster.GetInt64("id"))
@@ -89,6 +89,7 @@ func (this *ClusterSettingsAction) RunGet(params struct {
} else { } else {
sslPolicy = &sslconfigs.SSLPolicy{ sslPolicy = &sslconfigs.SSLPolicy{
IsOn: true, IsOn: true,
MinVersion: "TLS 1.1",
} }
} }

View File

@@ -3,6 +3,7 @@ package sandbox
import ( import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
) )
type IndexAction struct { type IndexAction struct {
@@ -15,21 +16,29 @@ func (this *IndexAction) Init() {
func (this *IndexAction) RunGet(params struct{}) { func (this *IndexAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent()) httpdnsutils.AddLeftMenu(this.Parent())
this.Data["clusters"] = policies.LoadAvailableDeployClusters()
this.Data["apps"] = []map[string]interface{}{ this.Data["apps"] = []map[string]interface{}{
{ {
"id": int64(1), "id": int64(1),
"name": "主站移动业务", "name": "主站移动业务",
"appId": "ab12xc34s2", "appId": "ab12xc34s2",
"clusterId": int64(1),
"domains": []string{"api.business.com", "www.aliyun.com"},
}, },
{ {
"id": int64(2), "id": int64(2),
"name": "视频网关业务", "name": "支付网关业务",
"appId": "vd8992ksm1", "appId": "vd8992ksm1",
"clusterId": int64(2),
"domains": []string{"payment.business.com"},
}, },
{ {
"id": int64(3), "id": int64(3),
"name": "海外灰度测试", "name": "海外灰度测试",
"appId": "ov7711hkq9", "appId": "ov7711hkq9",
"clusterId": int64(1),
"domains": []string{"global.example.com", "edge.example.com"},
}, },
} }
this.Show() this.Show()

View File

@@ -6,6 +6,7 @@ import (
"strconv" "strconv"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
) )
@@ -15,6 +16,7 @@ type TestAction struct {
func (this *TestAction) RunPost(params struct { func (this *TestAction) RunPost(params struct {
AppId string AppId string
ClusterId int64
Domain string Domain string
ClientIp string ClientIp string
Qtype string Qtype string
@@ -40,7 +42,11 @@ func (this *TestAction) RunPost(params struct {
query.Set("dn", params.Domain) query.Set("dn", params.Domain)
query.Set("cip", params.ClientIp) query.Set("cip", params.ClientIp)
query.Set("qtype", params.Qtype) query.Set("qtype", params.Qtype)
requestURL := "https://api.httpdns.example.com/resolve?" + query.Encode() clusterServiceDomain := policies.LoadClusterGatewayByID(params.ClusterId)
if len(clusterServiceDomain) == 0 {
clusterServiceDomain = "gw.httpdns.example.com"
}
requestURL := "https://" + clusterServiceDomain + "/resolve?" + query.Encode()
this.Data["result"] = maps.Map{ this.Data["result"] = maps.Map{
"code": 0, "code": 0,

View File

@@ -116,24 +116,6 @@
<p class="comment httpdns-note">用于生成鉴权接口的安全密钥。</p> <p class="comment httpdns-note">用于生成鉴权接口的安全密钥。</p>
</td> </td>
</tr> </tr>
<tr>
<td class="title">数据加密 Secret</td>
<td>
<div class="httpdns-secret-line">
<code>{{aesSecretVisible ? settings.aesSecretPlain : settings.aesSecretMasked}}</code>
<a href="" class="httpdns-mini-icon" @click.prevent="aesSecretVisible = !aesSecretVisible"
:title="aesSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon"
:class="aesSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-icon" title="复制数据加密 Secret"
@click.prevent="copySecret(settings.aesSecretPlain, 'AES Secret')"><i
class="copy outline icon"></i></a>
<a href="" class="httpdns-mini-icon" title="重置数据加密 Secret" @click.prevent="resetAESSecret"><i
class="redo icon"></i></a>
</div>
<p class="comment httpdns-note">最近更新:{{settings.aesSecretUpdatedAt}}</p>
<p class="comment httpdns-note">用于解析接口数据加密的密钥。</p>
</td>
</tr>
</table> </table>
<submit-btn></submit-btn> <submit-btn></submit-btn>

View File

@@ -2,14 +2,13 @@ Tea.context(function () {
this.activeSection = this.activeSection || "basic"; this.activeSection = this.activeSection || "basic";
this.success = NotifyReloadSuccess("保存成功"); this.success = NotifyReloadSuccess("保存成功");
this.signSecretVisible = false; this.signSecretVisible = false;
this.aesSecretVisible = false;
this.toggleSignEnabled = function () { this.toggleSignEnabled = function () {
let that = this; let that = this;
let targetIsOn = !this.settings.signEnabled; let targetIsOn = !this.settings.signEnabled;
if (targetIsOn) { if (targetIsOn) {
teaweb.confirm("html:开启后,服务端会对解析请求进行签名鉴权,<span class='red'>未签名、签名无效或过期的请求都解析失败</span>,确认开启吗?", function () { teaweb.confirm("html:开启后,服务端会对解析请求进行签名鉴权,<span class='red'>未签名、签名无效或过期的请求都解析失败</span>,确认开启吗?", function () {
that.$post("/httpdns/apps/app/settings/toggleSignEnabled") that.$post("/httpdns/apps/app/settings/toggleSignEnabled")
.params({ .params({
appId: that.app.id, appId: that.app.id,
@@ -93,20 +92,4 @@ Tea.context(function () {
}); });
}); });
}; };
this.resetAESSecret = function () {
let that = this;
teaweb.confirm("确定要重置 AES Secret 吗?", function () {
that.$post("/httpdns/apps/app/settings/resetAESSecret")
.params({
appId: that.app.id
})
.success(function () {
teaweb.success("AES Secret 已重置", function () {
teaweb.reload();
}); });
});
});
};
});

View File

@@ -8,8 +8,8 @@
return; return;
} }
teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id, { teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id, {
width: "56em", width: "42em",
height: "40em", height: "33em",
title: "新增自定义解析规则" title: "新增自定义解析规则"
}); });
}; };
@@ -19,8 +19,8 @@
return; return;
} }
teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id + "&recordId=" + recordId, { teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id + "&recordId=" + recordId, {
width: "56em", width: "42em",
height: "40em", height: "33em",
title: "编辑自定义解析规则" title: "编辑自定义解析规则"
}); });
}; };

View File

@@ -4,22 +4,27 @@
.httpdns-inline-actions { .httpdns-inline-actions {
margin-top: .6em; margin-top: .6em;
} }
.httpdns-inline-actions .count { .httpdns-inline-actions .count {
color: #8f9aa6; color: #8f9aa6;
margin-left: .4em; margin-left: .4em;
} }
.httpdns-row { .httpdns-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: .5em; gap: .5em;
margin-bottom: .45em; margin-bottom: .45em;
} }
.httpdns-row .field { .httpdns-row .field {
margin: 0 !important; margin: 0 !important;
} }
.httpdns-row .field.flex { .httpdns-row .field.flex {
flex: 1; flex: 1;
} }
.httpdns-line-row { .httpdns-line-row {
display: flex !important; display: flex !important;
align-items: center; align-items: center;
@@ -40,7 +45,8 @@
<input type="hidden" name="recordId" :value="record.id" /> <input type="hidden" name="recordId" :value="record.id" />
<input type="hidden" name="recordItemsJSON" :value="JSON.stringify(recordItems)" /> <input type="hidden" name="recordItemsJSON" :value="JSON.stringify(recordItems)" />
<table class="ui table definition selectable"> <table class="ui table definition selectable small">
<tbody>
<tr> <tr>
<td class="title">域名 *</td> <td class="title">域名 *</td>
<td><strong>{{record.domain}}</strong></td> <td><strong>{{record.domain}}</strong></td>
@@ -56,25 +62,38 @@
<td class="title">线路</td> <td class="title">线路</td>
<td> <td>
<div class="httpdns-line-row"> <div class="httpdns-line-row">
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineScope" v-model="record.lineScope" @change="onLineScopeChange"> <select class="ui dropdown auto-width"
<option value="china">中国大陆</option> style="display:inline-block !important;width:auto !important;margin:0 !important;"
<option value="overseas">港澳台及境外</option> name="lineScope" v-model="record.lineScope" @change="onLineScopeChange">
<option value="china">中国地区</option>
<option value="overseas">境外</option>
</select> </select>
<select class="ui dropdown auto-width"
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineCarrier" v-if="record.lineScope == 'china'" v-model="record.lineCarrier"> style="display:inline-block !important;width:auto !important;margin:0 !important;"
name="lineCarrier" v-if="record.lineScope == 'china'" v-model="record.lineCarrier">
<option v-for="carrier in chinaCarriers" :value="carrier">{{carrier}}</option> <option v-for="carrier in chinaCarriers" :value="carrier">{{carrier}}</option>
</select> </select>
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineRegion" v-if="record.lineScope == 'china'" v-model="record.lineRegion" @change="onChinaRegionChange"> <select class="ui dropdown auto-width"
style="display:inline-block !important;width:auto !important;margin:0 !important;"
name="lineRegion" v-if="record.lineScope == 'china'" v-model="record.lineRegion"
@change="onChinaRegionChange">
<option v-for="region in chinaRegions" :value="region">{{region}}</option> <option v-for="region in chinaRegions" :value="region">{{region}}</option>
</select> </select>
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineProvince" v-if="record.lineScope == 'china'" v-model="record.lineProvince"> <select class="ui dropdown auto-width"
style="display:inline-block !important;width:auto !important;margin:0 !important;"
name="lineProvince" v-if="record.lineScope == 'china'" v-model="record.lineProvince">
<option v-for="province in provinceOptions" :value="province">{{province}}</option> <option v-for="province in provinceOptions" :value="province">{{province}}</option>
</select> </select>
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineContinent" v-if="record.lineScope == 'overseas'" v-model="record.lineContinent" @change="onContinentChange"> <select class="ui dropdown auto-width"
style="display:inline-block !important;width:auto !important;margin:0 !important;"
name="lineContinent" v-if="record.lineScope == 'overseas'" v-model="record.lineContinent"
@change="onContinentChange">
<option v-for="continent in continents" :value="continent">{{continent}}</option> <option v-for="continent in continents" :value="continent">{{continent}}</option>
</select> </select>
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineCountry" v-if="record.lineScope == 'overseas'" v-model="record.lineCountry"> <select class="ui dropdown auto-width"
style="display:inline-block !important;width:auto !important;margin:0 !important;"
name="lineCountry" v-if="record.lineScope == 'overseas'" v-model="record.lineCountry">
<option v-for="country in countryOptions" :value="country">{{country}}</option> <option v-for="country in countryOptions" :value="country">{{country}}</option>
</select> </select>
</div> </div>
@@ -83,12 +102,13 @@
<tr> <tr>
<td class="title">解析记录值 *</td> <td class="title">解析记录值 *</td>
<td> <td>
<div style="float:right;"> <div style="margin-bottom: 0.5em;">
<div class="ui checkbox"> <div class="ui checkbox" style="margin-bottom: 0.5em;">
<input type="checkbox" name="weightEnabled" value="1" v-model="record.weightEnabled" /> <input type="checkbox" name="weightEnabled" value="1" v-model="record.weightEnabled" />
<label>权重调度</label> <label>开启权重调度设置</label>
</div> </div>
</div> </div>
<div class="httpdns-row" v-for="(item, index) in recordItems"> <div class="httpdns-row" v-for="(item, index) in recordItems">
<div class="field"> <div class="field">
<select class="ui dropdown auto-width" v-model="item.type"> <select class="ui dropdown auto-width" v-model="item.type">
@@ -97,12 +117,15 @@
</select> </select>
</div> </div>
<div class="field flex"> <div class="field flex">
<input type="text" placeholder="记录值(与记录类型匹配)" v-model="item.value" /> <input type="text" placeholder="记录值" v-model="item.value" />
</div> </div>
<div class="field" v-if="record.weightEnabled"> <div class="field" v-if="record.weightEnabled">
<input type="text" style="width:8em;" placeholder="权重(1-100)" v-model="item.weight" /> <input type="text" style="width:7em;" placeholder="权重1-100" v-model="item.weight" />
</div>
<div class="field">
<a href="" @click.prevent="removeRecordItem(index)" title="删除"><i
class="icon trash alternate outline"></i></a>
</div> </div>
<a href="" @click.prevent="removeRecordItem(index)" title="删除"><i class="icon trash alternate outline"></i></a>
</div> </div>
<div class="httpdns-inline-actions"> <div class="httpdns-inline-actions">
<a href="" @click.prevent="addRecordItem" :class="{disabled: recordItems.length >= 10}"> <a href="" @click.prevent="addRecordItem" :class="{disabled: recordItems.length >= 10}">
@@ -110,7 +133,6 @@
</a> </a>
<span class="count">{{recordItems.length}}/10</span> <span class="count">{{recordItems.length}}/10</span>
</div> </div>
<p class="comment" v-if="record.weightEnabled">开启后每条记录可配置权重,范围 1-100。</p>
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -123,14 +145,15 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td class="title">规则状态</td> <td class="title">状态</td>
<td> <td>
<div class="ui checkbox"> <div class="ui checkbox">
<input type="checkbox" name="isOn" value="1" v-model="record.isOn" /> <input type="checkbox" name="isOn" value="1" v-model="record.isOn" />
<label>启用</label> <label>启用当前规则</label>
</div> </div>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
<submit-btn></submit-btn> <submit-btn></submit-btn>

View File

@@ -26,11 +26,11 @@
<table class="ui table selectable celled httpdns-apps-table" v-if="apps.length > 0"> <table class="ui table selectable celled httpdns-apps-table" v-if="apps.length > 0">
<colgroup> <colgroup>
<col style="width:34%;" /> <col style="width:32%;" />
<col style="width:28%;" /> <col style="width:26%;" />
<col style="width:12%;" /> <col style="width:12%;" />
<col style="width:10%;" /> <col style="width:10%;" />
<col style="width:16%;" /> <col style="width:20%;" />
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>

View File

@@ -87,14 +87,6 @@
<a href="" class="httpdns-mini-action" title="复制加签 Secret" @click.prevent="copyText(selectedApp.signSecret, '加签 Secret')"><i class="copy outline icon"></i></a> <a href="" class="httpdns-mini-action" title="复制加签 Secret" @click.prevent="copyText(selectedApp.signSecret, '加签 Secret')"><i class="copy outline icon"></i></a>
</td> </td>
</tr> </tr>
<tr>
<td>AES 数据加密 Secret</td>
<td>
<code>{{aesSecretVisible ? selectedApp.aesSecret : selectedApp.aesSecretMasked}}</code>
<a href="" class="httpdns-mini-action" @click.prevent="aesSecretVisible = !aesSecretVisible" :title="aesSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon" :class="aesSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-action" title="复制 AES 数据加密 Secret" @click.prevent="copyText(selectedApp.aesSecret, 'AES Secret')"><i class="copy outline icon"></i></a>
</td>
</tr>
<tr> <tr>
<td>请求验签</td> <td>请求验签</td>
<td> <td>

View File

@@ -3,7 +3,6 @@
this.selectedApp = {} this.selectedApp = {}
this.currentStep = 1 this.currentStep = 1
this.signSecretVisible = false this.signSecretVisible = false
this.aesSecretVisible = false
if (typeof this.apps == "undefined") { if (typeof this.apps == "undefined") {
this.apps = [] this.apps = []
@@ -29,7 +28,6 @@
} }
this.signSecretVisible = false this.signSecretVisible = false
this.aesSecretVisible = false
this.currentStep = 1 this.currentStep = 1
} }

View File

@@ -4,21 +4,31 @@
<div> <div>
<div class="ui grid stackable"> <div class="ui grid stackable">
<!-- Left: 解析配置 --> <!-- Left: 解析测试 -->
<div class="six wide column"> <div class="six wide column">
<div class="ui segment"> <div class="ui segment">
<h4 class="ui header" style="margin-top:0;">解析配置</h4> <h4 class="ui header" style="margin-top:0;">解析测试</h4>
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>目标应用 *</label> <label>目标应用 *</label>
<select class="ui dropdown" name="appId" v-model="request.appId"> <select class="ui dropdown" name="appId" v-model="request.appId" @change="onAppChanged">
<option value="">[请选择应用]</option> <option value="">[请选择应用]</option>
<option v-for="app in apps" :value="app.appId">{{app.name}} ({{app.appId}})</option> <option v-for="app in apps" :value="app.appId">{{app.name}} ({{app.appId}})</option>
</select> </select>
</div> </div>
<div class="field">
<label>所属集群 *</label>
<select class="ui dropdown" name="clusterId" v-model="request.clusterId">
<option value="">[请选择所属集群]</option>
<option v-for="cluster in clusters" :value="String(cluster.id)">{{cluster.name}}</option>
</select>
</div>
<div class="field"> <div class="field">
<label>解析域名 *</label> <label>解析域名 *</label>
<input type="text" v-model="request.domain" placeholder="例如 www.example.com" /> <select class="ui dropdown" name="domain" v-model="request.domain">
<option value="">[请选择域名]</option>
<option v-for="domain in currentDomains" :value="domain">{{domain}}</option>
</select>
</div> </div>
<div class="field"> <div class="field">
<label>模拟客户端 IP</label> <label>模拟客户端 IP</label>

View File

@@ -2,6 +2,7 @@ Tea.context(function () {
this.newRequest = function () { this.newRequest = function () {
return { return {
appId: "", appId: "",
clusterId: "",
domain: "", domain: "",
clientIp: "", clientIp: "",
qtype: "A" qtype: "A"
@@ -20,13 +21,48 @@ Tea.context(function () {
} }
this.isRequesting = false this.isRequesting = false
this.currentDomains = []
if (typeof this.apps == "undefined") { if (typeof this.apps === "undefined") {
this.apps = [] this.apps = []
} }
if (typeof this.clusters === "undefined") {
this.clusters = []
}
this.onAppChanged = function () {
let selectedApp = null
for (let i = 0; i < this.apps.length; i++) {
if (this.apps[i].appId === this.request.appId) {
selectedApp = this.apps[i]
break
}
}
if (selectedApp == null) {
this.currentDomains = []
this.request.domain = ""
this.request.clusterId = ""
return
}
this.currentDomains = Array.isArray(selectedApp.domains) ? selectedApp.domains : []
if (this.currentDomains.length > 0) {
if (this.currentDomains.indexOf(this.request.domain) < 0) {
this.request.domain = this.currentDomains[0]
}
} else {
this.request.domain = ""
}
if (typeof selectedApp.clusterId !== "undefined" && selectedApp.clusterId !== null) {
this.request.clusterId = String(selectedApp.clusterId)
}
}
this.normalizeResultRows = function (data) { this.normalizeResultRows = function (data) {
if (typeof data == "undefined" || data == null) { if (typeof data === "undefined" || data == null) {
return [] return []
} }
@@ -35,10 +71,7 @@ Tea.context(function () {
} }
let rows = [] let rows = []
let ips = [] let ips = Array.isArray(data.ips) ? data.ips : []
if (Array.isArray(data.ips)) {
ips = data.ips
}
let domain = this.request.domain let domain = this.request.domain
let qtype = this.request.qtype let qtype = this.request.qtype
let ttl = data.ttl || 0 let ttl = data.ttl || 0
@@ -55,16 +88,21 @@ Tea.context(function () {
line: line line: line
}) })
}) })
return rows return rows
} }
this.sendTestRequest = function () { this.sendTestRequest = function () {
if (this.request.appId.length == 0) { if (this.request.appId.length === 0) {
teaweb.warn("请选择目标应用") teaweb.warn("Please select target app")
return return
} }
if (this.request.domain.length == 0) { if (this.request.clusterId.length === 0) {
teaweb.warn("请填写解析域名") teaweb.warn("Please select cluster")
return
}
if (this.request.domain.length === 0) {
teaweb.warn("Please select domain")
return return
} }
@@ -87,6 +125,7 @@ Tea.context(function () {
this.resetForm = function () { this.resetForm = function () {
this.request = this.newRequest() this.request = this.newRequest()
this.currentDomains = []
this.response = { this.response = {
hasResult: false, hasResult: false,
code: -1, code: -1,

View File

@@ -1,148 +1,147 @@
# HTTPDNS 主计划V1.2 现行 # HTTPDNS 主计划V1.2 现行设计
## 1. 目标 ## 1. 目标
1. 形成 HTTPDNS 管理端可用闭环:集群、全局配置、应用、域名、自定义解析、日志、解析测试。 1. 构建独立的 HTTPDNS 管理闭环:集群、应用、域名、自定义解析、访问日志、运行日志、解析测试。
2. 保持独立 HTTPDNS 模块设计,不挂载到传统 DNS(53)职责中 2. 方案以当前已确认的页面与交互为准,不保留历史分支设计
3. 文档仅保留当前已采用的设计与页面,不包含未落地方案 3. 优先保证可运维、可灰度、可观测,先落地稳定版本
## 2. 当前范围(仅管理平台 ## 2. 信息架构(菜单
1. 菜单顺序固定为 1. 左侧 HTTPDNS 菜单顺序:
- 集群管理 - 集群管理
- 全局配置
- 应用管理 - 应用管理
- SDK接入引导
- 访问日志 - 访问日志
- 运行日志 - 运行日志
- 解析测试 - 解析测试
2. 现阶段以管理端页面与 Mock 数据联调为主 2. 不再保留独立“全局配置”菜单
3. 不再保留独立“SDK接入引导”菜单。
## 3. 核心设计约束 ## 3. 核心设计约束
1. 应用侧 SNI 防护仅采用“隐匿 SNI”固定策略(不提供 level1/level3 选择)。 1. SNI 防护策略固定为“隐匿 SNI”不提供 level1/level3、mask/empty切换入口)。
2. 服务入口按“集群服务域名”管理,不使用全局单一网关地址 2. 服务入口按“集群服务域名”管理,不使用全局单入口
3. 应用认证采用: 3. 回源协议默认 HTTPS不提供开关。
- 请求验签开关 4. 域名校验默认开启,不在用户侧暴露开关
- 加签 Secret 5. 自定义解析先做精简版:不支持 SDNS 参数匹配。
- AES 数据加密 Secret
4. 自定义解析规则支持 SDNS 参数与解析记录多条配置(各最多 10 条)。
## 4. 页面设计(现状) ## 4. 集群管理
### 4.1 集群管理 ### 4.1 集群列表
1. 集群列表字段: 1. 字段:集群名称、服务域名、节点数、在线节点数、状态、操作。
2. 操作:节点列表、集群设置、删除集群。
### 4.2 节点列表
1. 字段节点名称、IP、CPU、内存、负载、状态、操作。
2. IP 列不展示“[下线]/[宕机]”附加标识。
### 4.3 集群设置
1. 页面布局对齐智能DNS左侧菜单 + 右侧配置区(`left-box with-menu` / `right-box with-menu`)。
2. 配置分组:
- 基础设置
- TLS
3. 基础设置字段:
- 集群名称 - 集群名称
- 服务域名 - 服务域名
- 节点数 - 默认解析 TTL
- 在线节点数
- 状态
- 操作
2. 集群设置字段:
- 集群名称
- 集群服务域名
- 降级超时容忍度(毫秒) - 降级超时容忍度(毫秒)
- 本地内存缓存(秒)
- 节点安装根目录(默认 `/opt/edge-httpdns` - 节点安装根目录(默认 `/opt/edge-httpdns`
- 启用当前集群 - 启用当前集群
3. 节点页提供基础筛选、状态展示、详情与删除。 - 默认集群(勾选)
4. TLS 配置:
- 样式与交互对齐智能DNS TLS页
- 维护并绑定该集群服务域名使用的证书
- 保证节点回源链路为 HTTPS
### 4.2 全局配置 ## 5. 应用管理
1. 用户设置:
- 默认部署集群
- 启用用户域名校验
2. 基础默认:
- SNI 防护配置(文案展示为“隐匿 SNI”
- 全局默认解析 TTL
- 全局降级超时
### 4.3 应用管理 ### 5.1 应用列表
1. 应用列表字段: 1. 字段:应用名称、AppID、绑定域名数、状态、操作。
- 应用名称 2. 操作域名列表、应用设置、SDK集成、删除应用。
- AppID 3. 删除应用页面交互风格对齐“删除集群”页面。
- 绑定域名数(可点击)
- 状态
- 操作(域名管理 / 应用设置)
2. 添加应用:
- 应用名称
- 所属集群
- 所属用户(可选)
3. 应用设置分两块:
- 基础配置AppID、应用启用、SNI 防护配置(隐匿 SNI
- 认证与密钥:请求验签开关、加签 Secret、AES Secret查看/复制/重置/更新时间)
### 4.4 域名管理与自定义解析 ### 5.2 应用设置
1. 域名管理: 1. 页面布局对齐智能DNS设置页风格左侧菜单 + 右侧配置区)。
2. 分组:
- 基础配置
- 认证与密钥
3. 基础配置字段:
- AppID只读
- 主集群
- 备集群(可选)
- 应用启用
- SNI 防护配置(文案展示:隐匿 SNI
4. 认证与密钥字段:
- 请求验签(状态展示 + 独立启停按钮 + 确认提示)
- 加签 Secret查看、复制、重置、最近更新时间
5. 交互约束:
- 强制 HTTPS 传输,不再提供独立“数据加密 Secret”配置项。
- 密钥操作区使用紧凑图标样式,减少视觉噪音。
## 6. 域名管理与自定义解析
### 6.1 域名管理
1. 页面采用框架标准顶部 tab + 面包屑样式。
2. 字段:
- 域名列表 - 域名列表
- 规则策略(显示该域名自定义解析规则数 - 规则策略(仅展示数字,表示该域名规则数,可点击
- 操作(自定义解析、解绑) - 操作(自定义解析、解绑)
2. 自定义解析列表字段: 3. 入口:在域名行操作中直接进入“自定义解析”。
- 线路
### 6.2 自定义解析(精简版)
1. 规则字段:
- 规则名称 - 规则名称
- SDNS 参数 - 线路
- 解析记录 - 解析记录
- TTL - TTL
- 状态 - 状态
- 操作(编辑/启停/删除) 2. 线路联动:
3. 新增/编辑规则弹窗: - 第一层:`中国地区` / `境外`
- 规则名称 - 中国地区:运营商 -> 大区 -> 省份
- 线路联动: - 境外:洲 -> 国家/地区(亚洲内使用中国香港/中国澳门/中国台湾)
- 中国大陆:运营商 -> 大区 -> 省份 3. 解析记录:
- 港澳台及境外:洲 -> 国家/地区(亚洲含中国香港/中国澳门/中国台湾) - 每条规则最多 10 条
- SDNS 参数配置:最多 10 条 - 支持 A / AAAA
- 解析记录值:最多 10 条,支持 A/AAAA可开启权重1-100 - 可开启权重调度
- TTL1-86400 4. 不包含 SDNS 参数配置。
- 规则状态
## 5. SDK接入引导现状 ## 7. 访问日志
1. 仅保留两步: 1. 菜单与页面文案统一为“访问日志”(不再使用“解析日志”)。
- 01 查看配置 2. 页面结构筛选区与列表区分离间距与智能DNS访问日志风格一致。
- 02 开发接入 3. 列字段:集群、节点、域名、类型、概要。
2. 查看配置展示: 4. 概要展示:
- App ID可复制 - 使用单行拼接
- 应用名称 - 按“访问信息 -> 解析结果”顺序
- 集群服务地址(可复制) - 不显示字段名堆叠
- 加签 Secret查看/复制)
- AES 数据加密 Secret查看/复制)
- 请求验签状态
## 6. 日志页面(现状) ## 8. 运行日志
1. 字段与智能DNS运行日志保持一致。
2. 级别样式使用文字着色,不使用大块背景色。
### 6.1 访问日志 ## 9. 解析测试
1. 主列: 1. 去掉“API在线沙盒”标题与右侧等待占位图标区。
- 集群 2. 解析配置区标题统一为“解析测试”。
- 节点 3. 测试参数保留目标应用、所属集群、解析域名、模拟客户端IP、解析类型A/AAAA
- 域名 4. 所属集群使用下拉框选择。
- 类型 5. 解析域名使用下拉框选择(按当前目标应用联动展示)。
- 概要 6. 去掉 SDNS 参数。
2. 概要按单行拼接展示,按“访问信息 -> 解析结果”顺序输出,不使用字段名堆叠 7. 结果区保留核心结果展示,参考阿里云风格的简洁结果布局
### 6.2 运行日志 ## 10. SDK 集成
1. 字段: 1. SDK 集成不作为左侧独立菜单。
- 时间 2. 在“应用管理”操作列进入 SDK 集成页。
- 集群 3. 页面只保留:
- 节点 - SDK 下载
- 级别 - 集成文档
- 类型 4. 卡片与按钮采用紧凑布局,避免按钮区域拥挤。
- 模块
- 详情
- 次数
- 请求ID
## 7. 解析测试(现 ## 11. SDK 与服务端接口(现
1. 左侧配置项: 1. 解析接口:`/resolve`
- 目标应用 - SDK 请求域名解析结果的主接口。
- 解析域名 2. 启动配置接口:`/bootstrap`(规划/联调口径)
- 模拟客户端 IP - 用于 SDK 获取可用服务域名与策略参数替代节点IP调度模式
- 解析类型A/AAAA 3. SDK 地址策略:优先主集群服务域名,不可用时切备集群;失败后按客户端降级策略处理。
- SDNS 参数(最多 10 条)
2. 右侧结果区:
- 解析成功/失败
- Request ID
- 请求URL
- 客户端 IP / 地区 / 线路
- 解析记录表解析域名、解析类型、IP地址、TTL、地区、线路
3. 已去掉无关展示块(如安全策略、验签响应头详情块)。
## 8. 本文不包含的内容 ## 12. 本文明确不包含
1. 不包含 ECH 控制台与 ECH 分阶段交付设计。 1. 不包含全局配置独立页面设计。
2. 不包含 SNI level1/level3、多级切换与 Public SNI 域名池页面设计。 2. 不包含 SDK 接入向导独立菜单设计。
3. 不包含第三方 DNS 凭证复用与相关编排描述 3. 不包含 SNI level1/level3、ECH 控制台、Public SNI 池分级配置
4. 不包含用户平台User页面方案 4. 不包含 SDNS 参数匹配链路
5. 不包含第三方 DNS 依赖与复用方案。

View File

@@ -0,0 +1,254 @@
# HTTPDNS后端开发计划V1.0
## 0. 文档信息
- 目标文件:`EdgeHttpDNS/HTTPDNS后端开发计划.md`
- 交付范围:`EdgeAdmin + EdgeAPI + EdgeNode + SDK对接接口`
- 交付策略:一次性全量交付(非分阶段)
- 核心约束:
- 仅新协议
-`/bootstrap`
- `/resolve` 使用 GET 参数
- 线路匹配仅基于客户端 IP 归属
- 独立 `edgeHTTPDNS*` 数据表
- 新增独立节点角色 `NodeRoleHTTPDNS`
- 访问日志 MySQL + ClickHouse 双写(查询优先 ClickHouse
## 1. 目标与成功标准
1. 将 HTTPDNS 从当前 Admin 侧 mock/store 方案落地为真实后端能力。
2. 打通“配置 -> 下发 -> 解析 -> 日志 -> 查询”闭环。
3. 与当前前端设计严格对齐:
- 菜单:集群管理、应用管理、访问日志、运行日志、解析测试
- SNI 固定为“隐匿 SNI”
- 自定义解析不含 SDNS 参数
4. 成功标准:
- 管理页面均通过 RPC 读取真实数据,无本地 mock 依赖
- `/resolve` 可按应用/域名/线路返回解析结果
- 访问日志与运行日志可查询、可筛选、可分页
- 节点配置与状态可下发和回传
- 主备集群服务域名可在应用设置中配置并生效
- EdgeNode 支持 SNI 与 Host 解耦路由,并可执行 WAF 动态验签与隐匿 SNI 转发
## 2. 架构与边界
### 2.1 服务边界
1. EdgeAdmin仅负责页面动作与 RPC 编排,不存业务状态。
2. EdgeAPI负责数据存储、策略匹配、接口服务、日志汇聚。
3. EdgeNodeHTTPDNS节点负责执行解析、接收策略任务、上报运行日志/访问日志,并执行 SNI/Host 解耦路由、WAF 动态验签、隐匿 SNI 转发。
4. SDK手动配置应用关联主备服务域名调用 `/resolve` 获取结果。
### 2.2 不做项
1. 不做 `/bootstrap` 接口。
2. 不做 ECH / SNI 分级策略。
3. 不做 SDNS 参数匹配。
4. 不做第三方 DNS 依赖复用。
## 3. 公开接口与契约(需新增/调整)
### 3.1 HTTP 解析接口
1. `GET /resolve`
2. 请求参数:
- `appId`(必填)
- `dn`(必填)
- `qtype`可选A/AAAA默认 A
- `cip`(可选)
- `sid``sdk_version``os`(可选,用于日志)
- `nonce``exp``sign`(可选,验签开启时必需)
3. 响应结构(统一):
- `code``message``requestId`
- `data`
- `domain`
- `qtype`
- `ttl`
- `records[]``type`,`ip`,`weight?`,`line?`,`region?`
- `client``ip`,`region`,`carrier`,`country`
- `summary`(命中规则摘要)
4. 错误码最低集合:
- app 无效/禁用
- 域名未绑定
- 验签失败
- 无可用解析记录
- 内部解析失败/超时
### 3.2 管理 RPCEdgeAdmin -> EdgeAPI
1. 新增服务:
- `HTTPDNSClusterService`
- `HTTPDNSNodeService`
- `HTTPDNSAppService`
- `HTTPDNSDomainService`
- `HTTPDNSRuleService`
- `HTTPDNSAccessLogService`
- `HTTPDNSRuntimeLogService`
- `HTTPDNSSandboxService`
2. 最小方法集:
- 集群增删改查、设置默认集群、TLS证书绑定、节点列表/状态
- 应用:增删改查、主备集群设置、启停、验签开关、密钥重置
- 域名:绑定/解绑/列表
- 自定义解析:规则增删改查、启停、排序
- 日志:访问日志分页查询、运行日志分页查询
- 测试:在线解析测试调用(入参包含 appId、clusterId、domain、qtype、clientIp
### 3.3 节点日志上报 RPCEdgeNode -> EdgeAPI
1. `CreateHTTPDNSAccessLogs`(批量)
2. `CreateHTTPDNSRuntimeLogs`(批量)
3. 幂等键:`requestId + nodeId`
4. 支持高吞吐批量提交和失败重试
### 3.4 节点路由与 WAF 策略下发契约EdgeAPI -> EdgeNode
1. 下发内容最小集合:
- `appId/domain/serviceDomain`
- `sniMode`(固定为隐匿 SNI
- `hostRouteMode`SNI 与 Host 解耦)
- `wafVerifyEnabled`
- `wafVerifyPolicy`(验签字段、时效窗口、失败动作)
2. 节点执行口径:
- 入站按 `serviceDomain` 接入,按 Host/业务域名做路由匹配
- TLS 握手与业务 Host 解耦,避免真实业务域名暴露在 SNI
- 命中需要验签的应用时,先执行 WAF 动态验签,再继续解析链路
3. 失败处置:
- 验签失败按策略拒绝并记录运行日志、访问日志错误码
- 路由未命中返回统一错误码并上报审计日志
## 4. 数据模型设计(独立 HTTPDNS 表)
1. `edgeHTTPDNSClusters`
- `id,name,isOn,isDefault,serviceDomain,defaultTTL,fallbackTimeoutMs,installDir,tlsPolicyJSON,createdAt,updatedAt`
2. `edgeHTTPDNSNodes`
- `id,clusterId,name,isOn,isUp,isInstalled,isActive,statusJSON,installStatusJSON,installDir,uniqueId,secret,createdAt,updatedAt`
3. `edgeHTTPDNSApps`
- `id,name,appId,isOn,primaryClusterId,backupClusterId,sniMode(fixed_hide),createdAt,updatedAt`
4. `edgeHTTPDNSAppSecrets`
- `id,appId,signEnabled,signSecretEnc,signUpdatedAt,updatedAt`
5. `edgeHTTPDNSDomains`
- `id,appId,domain,isOn,createdAt,updatedAt`
6. `edgeHTTPDNSCustomRules`
- `id,appId,domainId,ruleName,lineScope,lineCarrier,lineRegion,lineProvince,lineContinent,lineCountry,ttl,isOn,priority,updatedAt`
7. `edgeHTTPDNSCustomRuleRecords`
- `id,ruleId,recordType,recordValue,weight,sort`
8. `edgeHTTPDNSAccessLogs`
- `id,requestId,clusterId,nodeId,appId,domain,qtype,clientIP,clientRegion,carrier,sdkVersion,os,resultIPs,status,errorCode,costMs,createdAt,day`
9. `edgeHTTPDNSRuntimeLogs`
- `id,clusterId,nodeId,level,type,tag,description,count,createdAt,day`
### 4.1 索引与唯一约束
1. 唯一:`edgeHTTPDNSApps.appId`
2. 唯一:`edgeHTTPDNSDomains(appId,domain)`
3. 唯一:`edgeHTTPDNSAccessLogs(requestId,nodeId)`
4. 索引:
- 访问日志:`day,clusterId,nodeId,domain,status,createdAt`
- 规则匹配:`domainId,isOn,priority,lineScope,...`
- 应用查询:`name,appId,isOn`
## 5. 解析引擎实现EdgeAPI
1. 输入校验:`appId/dn/qtype`
2. 应用与域名校验:
- app 存在且启用
- 域名已绑定到 app
3. 线路归属:
- `cip` 优先,其次 remote IP
- 映射字段:运营商/大区/省份/洲/国家
4. 规则匹配:
- 精确匹配 > 半精确 > 默认
- 同级按 `priority` 从小到大
5. 记录返回:
- 权重关闭:返回全部记录
- 权重开启:按权重算法返回单条或子集(固定口径)
6. TTL 取值:
- 命中规则取规则 TTL
- 未命中规则取集群默认 TTL
7. 验签:
- `signEnabled=true` 时必须通过签名校验
- `signEnabled=false` 时跳过签名校验
8. 访问日志:
- 解析结束异步写日志
- 双写 MySQL/ClickHouse
## 6. 节点与任务链路
1.`EdgeCommon/pkg/nodeconfigs` 增加 `NodeRoleHTTPDNS`
2. 在任务系统增加 HTTPDNS 任务类型:
- 配置变更
- 应用变更
- 域名变更
- 规则变更
- 证书变更
- 路由与 WAF 策略变更
3. EdgeNode 增加 HTTPDNS 子服务:
- 接收配置快照
- 执行解析
- 上报运行/访问日志
- 执行 SNI/Host 解耦路由
- 执行 WAF 动态验签
- 执行隐匿 SNI 转发
4. 复用现有安装升级框架,但节点角色、任务通道、日志 tag 独立。
## 7. EdgeAdmin 后端改造
1.`internal/web/actions/default/httpdns/*` 的 store/mock 读写改为 RPC。
2. 删除/停用旧能力路由:
- `/httpdns/policies`
- `/httpdns/guide`
- `/httpdns/ech`
3. 保留必要跳转,避免旧链接 404。
4. `sandbox/test` 改为调用真实解析服务(可保留测试开关)。
5. 解析测试页面交互固定为:
- 配置区标题“解析测试”
- 所属集群使用下拉框
- 解析域名使用下拉框并按目标应用联动
## 8. 安全与审计
1. Secret 持久化使用加密存储(至少密文列),返回时脱敏。
2. 操作审计记录:
- 验签开关启停
- Sign Secret 重置
- 应用主备集群修改
3. 验签失败日志保留 `requestId,errorCode,sourceIP`
4. 防滥用:
- `/resolve` 增加基础限流与异常请求过滤(按 appId + IP 维度)。
5. 节点侧安全执行:
- WAF 动态验签失败必须留存审计日志(含 requestId/appId/sourceIP
- SNI/Host 解耦路由命中结果需可追踪(用于问题回溯)
## 9. 测试与验收
### 9.1 单元测试
1. 规则匹配优先级与线路匹配
2. 验签成功/失败路径
3. 权重返回算法
4. DAO CRUD 与唯一约束
### 9.2 集成测试
1. EdgeAdmin -> RPC -> DB 全链路
2. `/resolve` 各错误码分支
3. 节点日志上报双写MySQL+CH
4. CH 不可用时 MySQL 回退查询
5. EdgeAPI 策略下发 -> EdgeNode 路由/WAF 执行 -> 日志落库全链路
### 9.3 回归测试
1. 智能DNS功能不受影响
2. 菜单与权限不串模块
3. 旧入口跳转正确,无新增 404
### 9.4 验收用例
1. 应用配置主备服务域名后SDK可解析成功
2. 主集群故障时可切备集群
3. 自定义解析按线路返回预期 IP
4. 访问日志筛选与概要展示正确
5. 运行日志级别/字段与智能DNS一致
6. EdgeNode 可在 SNI 与 Host 解耦场景下正确路由到目标应用
7. 开启 WAF 动态验签后,合法请求通过、非法签名请求被拒绝且有审计日志
## 10. 发布与回滚
1. 发布顺序:
- DB migration
- EdgeAPIDAO+RPC+resolve
- EdgeNode角色+上报)
- EdgeAdminRPC切换
2. 开关控制:
- `httpdns.resolve.enabled`
- `httpdns.log.clickhouse.enabled`
3. 回滚策略:
- 关闭 `resolve` 新实现开关
- 访问日志读取切回 MySQL
- Admin 保留只读能力
## 11. 默认值与固定决策
1.`/bootstrap`SDK手动配置主备服务域名。
2. `SNI` 固定“隐匿 SNI”。
3. 自定义解析无 SDNS 参数。
4. 线路仅客户端 IP 归属。
5. 日志双写,查询优先 ClickHouse。
6. 节点角色独立:`NodeRoleHTTPDNS`

View File

@@ -1,114 +0,0 @@
在域名管理中 加一个 自定义解析功能。
功能简介若需为域名提供特定的解析结果可以使用HTTPDNS提供的自定义解析功能。该功能支持通过配置规则来实现对特定域名的自定义解析。
应用场景:灰度测试:假设您的域名是 www.example.com因为业务增长发布了新的服务新的服务IP为1.1.X.X在服务全量发布前您希望对电信_上海访问域名的流量进行特定APP版本号的灰度测试。对于这些流量访问www.example.com时发起的域名解析请求返回1.1.X.X。
流量调度:假设某个汽车企业服务域名是 www.example.com希望DNS解析过程中可以根据特定的业务逻辑返回位于不同区域的服务器的 IP 地址。例如根据汽车常驻地返回不同的服务IP某辆汽车的常驻地区在广州对于该汽车访问 www.example.com 时发起的 DNS 查询请求返回位于广州的服务器的 IP 地址。
策略说明
可以通过某种规则来达到自定义解析的目的。您可以对网络线路进行更精细的配置,并通过配置不同的解析参数,使来自不同运营商和地域的用户流量精准路由至不同的服务地址。
使用方式如下:
在 HTTPDNS 控制台 中,为指定域名创建一条自定义解析规则策略。
客户端通过 SDK 发起 DNS 查询请求时,携带相应的自定义解析参数。
HTTPDNS 服务端接收到请求后,会根据预设的匹配规则,返回最符合业务需求的解析结果。
您可以做以下配置:
基本信息
参数
说明
域名
您希望自定义解析的域名例如www.aliyun.com。
说明
域名选择下拉的数据源来自于接入域名中已经添加的域名,如果想要自定义解析的域名不在下拉列表中,可以到域名列表中添加后,再为该域名添加自定义解析记录。
如果要为某个泛域名的子域名添加自定义解析记录,例如:*.aliyun.com但你想要自定义域名是 a.aliyun.com则需要将 a.aliyun.com 添加到域名列表中,再为该域名添加自定义解析记录。
域名选择下拉字段选不到对应的域名有以下几种情况:
想要添加的域名不在域名列表中,您可以到接入域名中添加对应的域名即可。
想要添加的域名是某个泛域名的子域名,将该子域名添加到域名列表即可。
想要添加的域名已经存在自定义解析记录,需要去自定义解析记录列表管理对应的域名。
线路
可针对运营商和地域进行线路配置。
中国内地线路:按“运营商 > 大区 > 省份”进行配置。
运营商:可以自定义运营商,例如:中国电信。如果运营商设置为默认,表示当前线路覆盖所有运营商。
大区:按照地域划分,例如:东北、华北、华东等,省份归属在对应大区下。如果大区设置为默认,表示当前线路覆盖所有大区。
省份:可以自定义省份,例如:北京、河北,如果省份设置为默认,表示当前线路覆盖所有省份。
海外线路:选择地域为“境外”时生效,按“洲 > 国家或地区”进行配置。
可以选择大洲,例如:亚洲、欧洲、南美洲等;也可以在大洲下选择具体国家或地区,例如:日本、英国等。
如果大洲、国家或地区设置为默认,表示当前线路覆盖所选范围内的全部区域。
说明
在同一个域名下,对于相同地域的用户,线路生效的优先级是:运营商>地理位置>默认。例如,电信-华北-北京>电信-华北-默认>默认-华北-北京>默认-默认-默认。
例如:如果在同一个域名下同时存在两条规则策略,线路分别是电信-华北-北京和电信-华北-默认,那么对于北京的电信用户会使用电信-华北-北京线路的规则策略。
自定义解析规则
一条规则策略最多支持配置 10 条自定义解析规则。
参数
说明
规则名称
说明当前规则的名称可以用来表达规则的用途例如通过SDK版本调度。
规则排序
多个规则之间可以调整顺序,匹配的逻辑是从上往下串行匹配,顺序决定了哪个规则会被优先命中,调整顺序后会按照新的顺序匹配。
SDNS参数配置
用来匹配客户端请求解析接口携带的SDNS参数决定该条规则是否被命中如果匹配成功则返回该条规则中的解析记录值。详细的匹配逻辑请查看规则策略匹配逻辑说明。
参数名称SDNS参数的名称长度限制为 2 ~ 64 个字符。
参数值SDNS参数的值长度限制为 1 64 个字符。
说明
一条规则最多添加 10 个 SDNS参数。
解析接口可以添加SDNS参数具体查看 客户端传递自定义解析参数。
解析记录值
自定义解析的返回值集合,每个记录值代表记录集中的一条解析记录,必填。
记录类型返回解析记录值的类型支持A和AAAA记录。
记录值:返回的记录值
您可以添加多个记录值,在未开启权重的情况下,添加的多个记录值将会合并在一起返回。
还可以按照权重调度只需要打开按照权重调度开关即可。开启权重后可以为每个记录值设置权重权重值设置范围为1-100根据记录值的权重通过负载均衡算法返回一个合适的记录值。
说明
一条规则最多添加 10 个记录值。
TTL
必填自定义解析记录的有效期。有效期越短HTTPDNS SDK 中的解析记录缓存过期就越快。同时HTTPDNS SDK 请求新的解析记录的频率就越高。