优化修复

This commit is contained in:
Alex Yang
2026-01-02 02:43:10 +08:00
parent 2524336bab
commit 8889c875a9
14 changed files with 2335 additions and 51009 deletions

View File

@@ -1,6 +1,10 @@
# Changelog
所有对本项目的显著更改都将记录在此文件中。
## [1.2.6] - 2025-12-30
### 新增
- 实现查询日志详情的域名信息显示功能
## [1.2.5] - 2025-12-26
### 新增
- 增加了对IPv6的支持配置项默认关闭

View File

@@ -2,7 +2,7 @@
"dns": {
"port": 53,
"upstreamDNS": [
"10.35.10.200"
"223.5.5.5"
],
"dnssecUpstreamDNS": [
"117.50.10.10",
@@ -15,6 +15,8 @@
"cacheTTL": 10,
"enableDNSSEC": true,
"queryMode": "parallel",
"queryTimeout": 500,
"enableFastReturn": true,
"domainSpecificDNS": {
"addr.arpa": [
"10.35.10.200:53"
@@ -77,7 +79,7 @@
"name": "My GitHub Rules",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
"enabled": true,
"lastUpdateTime": "2025-12-26T11:32:28.843Z"
"lastUpdateTime": "2025-12-31T07:39:47.585Z"
},
{
"name": "CNList",

View File

@@ -21,6 +21,8 @@ type DNSConfig struct {
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间分钟
EnableDNSSEC bool `json:"enableDNSSEC"` // 是否启用DNSSEC支持
QueryMode string `json:"queryMode"` // 查询模式:"loadbalance"(负载均衡)、"parallel"(并行请求)、"fastest-ip"最快的IP地址
QueryTimeout int `json:"queryTimeout"` // 查询超时时间(毫秒)
EnableFastReturn bool `json:"enableFastReturn"` // 是否启用快速返回机制
DomainSpecificDNS DomainSpecificDNS `json:"domainSpecificDNS"` // 域名特定DNS服务器配置
NoDNSSECDomains []string `json:"noDNSSECDomains"` // 不验证DNSSEC的域名模式列表
EnableIPv6 bool `json:"enableIPv6"` // 是否启用IPv6解析AAAA记录
@@ -108,6 +110,14 @@ func LoadConfig(path string) (*Config, error) {
if config.DNS.QueryMode == "" {
config.DNS.QueryMode = "parallel" // 默认使用并行请求模式
}
// 查询超时默认配置(毫秒)
if config.DNS.QueryTimeout == 0 {
config.DNS.QueryTimeout = 500 // 默认超时时间为500ms
}
// 快速返回机制默认配置
if config.DNS.EnableFastReturn == false {
config.DNS.EnableFastReturn = true // 默认启用快速返回机制
}
// 域名特定DNS服务器配置默认值
if config.DNS.DomainSpecificDNS == nil {
config.DNS.DomainSpecificDNS = make(DomainSpecificDNS) // 默认为空映射

71
debug-json.js Normal file
View File

@@ -0,0 +1,71 @@
// 调试JSON解析问题
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, 'domain-info', 'domains', 'domain-info.json');
console.log(`正在调试JSON文件: ${filePath}`);
console.log('=' .repeat(50));
// 1. 检查文件大小和内容
const stats = fs.statSync(filePath);
console.log(`文件大小: ${stats.size} 字节`);
const data = fs.readFileSync(filePath, 'utf8');
console.log(`文件内容长度: ${data.length} 字符`);
console.log('=' .repeat(50));
// 2. 查找网易公司的位置和结束位置
const neteaseStart = data.indexOf('"网易"');
console.log(`网易公司开始位置: ${neteaseStart}`);
// 尝试找到网易公司的结束位置
let neteaseEnd = neteaseStart + 1000;
const neteaseEndStr = '},\n\t\t"';
neteaseEnd = data.indexOf(neteaseEndStr, neteaseStart);
console.log(`网易公司结束位置: ${neteaseEnd}`);
// 3. 查看网易公司结束后的内容
if (neteaseEnd !== -1) {
const nextCompany = data.substring(neteaseEnd, neteaseEnd + 50);
console.log(`网易公司结束后的内容: ${nextCompany}`);
}
console.log('=' .repeat(50));
// 4. 解析JSON并查看结果
try {
const parsedData = JSON.parse(data);
console.log('JSON解析成功!');
if ('domains' in parsedData) {
const companies = Object.keys(parsedData.domains);
console.log(`解析出的公司数量: ${companies.length}`);
console.log(`解析出的公司: ${companies.join(', ')}`);
// 查看网易公司的完整数据
if (companies.includes('网易')) {
console.log('\n网易公司的数据结构:');
console.log(JSON.stringify(parsedData.domains['网易'], null, 2));
}
}
} catch (error) {
console.error('JSON解析错误:', error);
// 显示错误位置附近的内容
if (error instanceof SyntaxError && error.offset) {
const errorPos = error.offset;
const context = data.substring(Math.max(0, errorPos - 50), errorPos + 50);
console.log(`错误位置附近的内容: ${context}`);
}
}
console.log('=' .repeat(50));
// 5. 搜索其他公司
const companiesToCheck = ['搜狗', '高德地图', '奇虎360', '百度'];
for (const company of companiesToCheck) {
const position = data.indexOf(`"${company}"`);
console.log(`${company}在文件中的位置: ${position}`);
if (position !== -1) {
const context = data.substring(Math.max(0, position - 10), position + 20);
console.log(` 上下文: ${context}`);
}
}

BIN
dns-server Executable file

Binary file not shown.

View File

@@ -19,6 +19,9 @@ type DNSCache struct {
cache map[string]*DNSCacheItem // 缓存映射表
mutex sync.RWMutex // 读写锁,保护缓存
defaultTTL time.Duration // 默认缓存TTL
maxSize int // 最大缓存条目数
// 使用链表结构来跟踪缓存条目的访问顺序用于LRU淘汰
accessList []string // 记录访问顺序,最新访问的放在最后
}
// NewDNSCache 创建新的DNS缓存实例
@@ -26,6 +29,8 @@ func NewDNSCache(defaultTTL time.Duration) *DNSCache {
cache := &DNSCache{
cache: make(map[string]*DNSCacheItem),
defaultTTL: defaultTTL,
maxSize: 10000, // 默认最大缓存10000条记录
accessList: make([]string, 0, 10000),
}
// 启动缓存清理协程
@@ -110,32 +115,70 @@ func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.D
}
c.mutex.Lock()
defer c.mutex.Unlock()
// 如果条目已存在,先从访问列表中移除
for i, k := range c.accessList {
if k == key {
// 移除旧位置
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
break
}
}
// 将新条目添加到访问列表末尾
c.accessList = append(c.accessList, key)
c.cache[key] = item
c.mutex.Unlock()
// 检查是否超过最大大小限制,如果超过则移除最久未使用的条目
if len(c.cache) > c.maxSize {
// 最久未使用的条目是访问列表的第一个
oldestKey := c.accessList[0]
// 从缓存和访问列表中移除
delete(c.cache, oldestKey)
c.accessList = c.accessList[1:]
}
}
// Get 获取缓存项
func (c *DNSCache) Get(qName string, qType uint16) (*dns.Msg, bool) {
key := cacheKey(qName, qType)
c.mutex.RLock()
c.mutex.Lock()
defer c.mutex.Unlock()
item, found := c.cache[key]
if !found {
c.mutex.RUnlock()
return nil, false
}
// 检查是否过期
if time.Now().After(item.Expiry) {
c.mutex.RUnlock()
// 过期了,删除缓存项(在写锁中)
c.delete(key)
// 过期了,删除缓存项
delete(c.cache, key)
// 从访问列表中移除
for i, k := range c.accessList {
if k == key {
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
break
}
}
return nil, false
}
// 将访问的条目移动到访问列表末尾(标记为最近使用)
for i, k := range c.accessList {
if k == key {
// 移除旧位置
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
// 添加到末尾
c.accessList = append(c.accessList, key)
break
}
}
// 返回缓存的响应副本
response := item.Response.Copy()
c.mutex.RUnlock()
return response, true
}
@@ -143,14 +186,24 @@ func (c *DNSCache) Get(qName string, qType uint16) (*dns.Msg, bool) {
// delete 删除缓存项
func (c *DNSCache) delete(key string) {
c.mutex.Lock()
defer c.mutex.Unlock()
// 从缓存中删除
delete(c.cache, key)
c.mutex.Unlock()
// 从访问列表中移除
for i, k := range c.accessList {
if k == key {
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
break
}
}
}
// Clear 清空缓存
func (c *DNSCache) Clear() {
c.mutex.Lock()
c.cache = make(map[string]*DNSCacheItem)
c.accessList = make([]string, 0, c.maxSize) // 重置访问列表
c.mutex.Unlock()
}
@@ -178,9 +231,23 @@ func (c *DNSCache) cleanupExpired() {
c.mutex.Lock()
defer c.mutex.Unlock()
// 收集所有过期的键
var expiredKeys []string
for key, item := range c.cache {
if now.After(item.Expiry) {
delete(c.cache, key)
expiredKeys = append(expiredKeys, key)
}
}
// 删除过期的缓存项
for _, key := range expiredKeys {
delete(c.cache, key)
// 从访问列表中移除
for i, k := range c.accessList {
if k == key {
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
break
}
}
}
}

View File

@@ -269,6 +269,9 @@ func (s *Server) Start() error {
// 启动自动保存功能
go s.startAutoSave()
// 启动IP地理位置缓存清理协程
go s.startIPGeolocationCacheCleanup()
// 启动UDP服务
go func() {
logger.Info(fmt.Sprintf("DNS UDP服务器启动监听端口: %d", s.config.Port))
@@ -762,6 +765,185 @@ type serverResponse struct {
error error
}
// recordKey 用于唯一标识DNS记录的结构体
type recordKey struct {
name string
rtype uint16
class uint16
data string
}
// getRecordKey 获取DNS记录的唯一标识
func getRecordKey(rr dns.RR) recordKey {
// 对于同一域名的同一类型记录只保留一个选择最长TTL
// 所以对于A、AAAA、CNAME等记录只使用name、rtype、class作为键
// 对于MX记录还需要考虑Preference字段
// 对于TXT记录需要考虑实际文本内容
// 对于NS记录需要考虑目标服务器
switch rr.Header().Rrtype {
case dns.TypeA, dns.TypeAAAA, dns.TypeCNAME, dns.TypePTR:
// 对于A、AAAA、CNAME、PTR记录同一域名只保留一个
return recordKey{
name: rr.Header().Name,
rtype: rr.Header().Rrtype,
class: rr.Header().Class,
data: "",
}
case dns.TypeMX:
// 对于MX记录同一域名的同一Preference只保留一个
if mx, ok := rr.(*dns.MX); ok {
return recordKey{
name: rr.Header().Name,
rtype: rr.Header().Rrtype,
class: rr.Header().Class,
data: fmt.Sprintf("%d", mx.Preference),
}
}
case dns.TypeTXT:
// 对于TXT记录需要考虑实际文本内容
if txt, ok := rr.(*dns.TXT); ok {
return recordKey{
name: rr.Header().Name,
rtype: rr.Header().Rrtype,
class: rr.Header().Class,
data: strings.Join(txt.Txt, " "),
}
}
case dns.TypeNS:
// 对于NS记录需要考虑目标服务器
if ns, ok := rr.(*dns.NS); ok {
return recordKey{
name: rr.Header().Name,
rtype: rr.Header().Rrtype,
class: rr.Header().Class,
data: ns.Ns,
}
}
case dns.TypeSOA:
// 对于SOA记录同一域名只保留一个
return recordKey{
name: rr.Header().Name,
rtype: rr.Header().Rrtype,
class: rr.Header().Class,
data: "",
}
}
// 对于其他类型使用原始rr.String()但移除TTL部分
parts := strings.Split(rr.String(), " ")
if len(parts) >= 5 {
// 跳过TTL字段第3个字段
data := strings.Join(append(parts[:2], parts[3:]...), " ")
return recordKey{
name: rr.Header().Name,
rtype: rr.Header().Rrtype,
class: rr.Header().Class,
data: data,
}
}
return recordKey{
name: rr.Header().Name,
rtype: rr.Header().Rrtype,
class: rr.Header().Class,
data: rr.String(),
}
}
// mergeResponses 合并多个DNS响应
func mergeResponses(responses []*dns.Msg) *dns.Msg {
if len(responses) == 0 {
return nil
}
// 如果只有一个响应,直接返回,避免不必要的合并操作
if len(responses) == 1 {
return responses[0].Copy()
}
// 使用第一个响应作为基础
mergedResponse := responses[0].Copy()
mergedResponse.Answer = []dns.RR{}
mergedResponse.Ns = []dns.RR{}
mergedResponse.Extra = []dns.RR{}
// 使用map存储唯一记录选择最长TTL
// 预分配map容量减少扩容开销
answerMap := make(map[recordKey]dns.RR, len(responses[0].Answer)*len(responses))
nsMap := make(map[recordKey]dns.RR, len(responses[0].Ns)*len(responses))
extraMap := make(map[recordKey]dns.RR, len(responses[0].Extra)*len(responses))
for _, resp := range responses {
if resp == nil {
continue
}
// 合并Answer部分
for _, rr := range resp.Answer {
key := getRecordKey(rr)
if existing, exists := answerMap[key]; exists {
// 如果存在相同记录选择TTL更长的
if rr.Header().Ttl > existing.Header().Ttl {
answerMap[key] = rr
}
} else {
answerMap[key] = rr
}
}
// 合并Ns部分
for _, rr := range resp.Ns {
key := getRecordKey(rr)
if existing, exists := nsMap[key]; exists {
// 如果存在相同记录选择TTL更长的
if rr.Header().Ttl > existing.Header().Ttl {
nsMap[key] = rr
}
} else {
nsMap[key] = rr
}
}
// 合并Extra部分
for _, rr := range resp.Extra {
// 跳过OPT记录避免重复
if rr.Header().Rrtype == dns.TypeOPT {
continue
}
key := getRecordKey(rr)
if existing, exists := extraMap[key]; exists {
// 如果存在相同记录选择TTL更长的
if rr.Header().Ttl > existing.Header().Ttl {
extraMap[key] = rr
}
} else {
extraMap[key] = rr
}
}
}
// 预分配切片容量,减少扩容开销
mergedResponse.Answer = make([]dns.RR, 0, len(answerMap))
mergedResponse.Ns = make([]dns.RR, 0, len(nsMap))
mergedResponse.Extra = make([]dns.RR, 0, len(extraMap))
// 将map转换回切片
for _, rr := range answerMap {
mergedResponse.Answer = append(mergedResponse.Answer, rr)
}
for _, rr := range nsMap {
mergedResponse.Ns = append(mergedResponse.Ns, rr)
}
for _, rr := range extraMap {
mergedResponse.Extra = append(mergedResponse.Extra, rr)
}
return mergedResponse
}
// forwardDNSRequestWithCache 转发DNS请求到上游服务器并返回响应
func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg, time.Duration, string, string) {
// 始终支持EDNS
@@ -856,10 +1038,13 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
var usedDNSServer string
var usedDNSSECServer string
// 使用配置中的超时时间
defaultTimeout := time.Duration(s.config.QueryTimeout) * time.Millisecond
// 根据查询模式处理请求
switch s.config.QueryMode {
case "parallel":
// 并行请求模式 - 优化版:添加超时处理和快速响应返回
// 并行请求模式 - 收集所有响应并合并
responses := make(chan serverResponse, len(selectedUpstreamDNS))
var wg sync.WaitGroup
@@ -881,7 +1066,12 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
close(responses)
}()
// 处理所有响应,实现快速响应返回
// 收集所有有效响应
var validResponses []*dns.Msg
var totalRtt time.Duration
var responseCount int
// 处理所有响应
for resp := range responses {
if resp.error == nil && resp.response != nil {
// 更新服务器统计信息
@@ -890,33 +1080,17 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
// 检查是否包含DNSSEC记录
containsDNSSEC := s.hasDNSSECRecords(resp.response)
// 如果启用了DNSSEC且响应包含DNSSEC记录验证DNSSEC签名
// 但如果域名匹配不验证DNSSEC的模式则跳过验证
if s.config.EnableDNSSEC && containsDNSSEC && !noDNSSEC {
// 验证DNSSEC记录
signatureValid := s.verifyDNSSEC(resp.response)
// 设置AD标志Authenticated Data
resp.response.AuthenticatedData = signatureValid
if signatureValid {
// 更新DNSSEC验证成功计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECSuccess++
})
} else {
// 更新DNSSEC验证失败计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECFailed++
})
}
} else if noDNSSEC {
// 对于不验证DNSSEC的域名始终设置AD标志为false
// 对于不验证DNSSEC的域名始终设置AD标志为false
if noDNSSEC {
resp.response.AuthenticatedData = false
}
// 只对将要返回的响应进行DNSSEC验证减少开销
// 这里只设置containsDNSSEC标志实际验证在确定返回响应后进行
if containsDNSSEC && s.config.EnableDNSSEC && !noDNSSEC {
// 暂时不验证,只标记
}
// 检查当前服务器是否是DNSSEC专用服务器
for _, dnssecServer := range dnssecServers {
if dnssecServer == resp.server {
@@ -925,111 +1099,43 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
}
}
// 检查当前服务器是否是用户配置的上游DNS服务器
isUserUpstream := false
for _, userServer := range s.config.UpstreamDNS {
if userServer == resp.server {
isUserUpstream = true
break
}
}
// 收集有效响应
if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError {
validResponses = append(validResponses, resp.response)
totalRtt += resp.rtt
responseCount++
// 处理响应优先选择用户配置的主DNS服务器
if resp.response.Rcode == dns.RcodeSuccess {
// 成功响应,优先使用
if isUserUpstream {
// 用户配置的主DNS服务器响应直接设置为最佳响应
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
hasDNSSECResponse = containsDNSSEC
// 记录使用的服务器
if usedDNSServer == "" {
usedDNSServer = resp.server
logger.Debug("使用用户配置的上游服务器响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
// 快速返回用户配置的主DNS服务器响应立即返回
continue
} else if containsDNSSEC {
// 非用户配置服务器但有DNSSEC记录
if !hasBestResponse || !isUserUpstream {
// 如果还没有最佳响应,或者当前最佳响应不是用户配置的服务器,则更新
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
hasDNSSECResponse = true
usedDNSServer = resp.server
logger.Debug("找到带DNSSEC的最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
// 快速返回找到带DNSSEC的响应立即返回
continue
}
} else {
// 非用户配置服务器没有DNSSEC记录
if !hasBestResponse {
// 如果还没有最佳响应,设置为最佳响应
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
logger.Debug("找到最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
// 快速返回:第一次找到成功响应,立即返回
continue
}
}
} else if resp.response.Rcode == dns.RcodeNameError {
// NXDOMAIN响应
if !hasBestResponse || bestResponse.Rcode == dns.RcodeNameError {
// 如果还没有最佳响应或者最佳响应也是NXDOMAIN
if isUserUpstream {
// 用户配置的服务器,直接使用
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
logger.Debug("使用用户配置的上游服务器NXDOMAIN响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
// 快速返回用户配置的服务器NXDOMAIN响应立即返回
continue
} else if !hasBestResponse || resp.rtt < bestRtt {
// 非用户配置服务器,选择更快的响应
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
logger.Debug("找到NXDOMAIN最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
// 快速返回找到NXDOMAIN响应立即返回
continue
}
}
}
// 更新备选响应,确保总有一个可用的响应
if resp.response != nil {
if !hasBackup {
// 第一次保存备选响应
backupResponse = resp.response
backupRtt = resp.rtt
hasBackup = true
} else {
// 后续响应,优先保存用户配置的服务器响应作为备选
if isUserUpstream {
} else {
// 更新备选响应,确保总有一个可用的响应
if resp.response != nil {
if !hasBackup {
// 第一次保存备选响应
backupResponse = resp.response
backupRtt = resp.rtt
hasBackup = true
}
}
}
// 即使响应不是成功或NXDOMAIN也保存为最佳响应如果还没有的话
// 确保总有一个响应返回给客户端
if !hasBestResponse {
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
logger.Debug("使用非成功响应作为最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt, "rcode", resp.response.Rcode)
}
} else {
// 更新服务器统计信息(失败)
s.updateServerStats(resp.server, false, 0)
}
}
// 合并所有有效响应
if len(validResponses) > 0 {
bestResponse = mergeResponses(validResponses)
if responseCount > 0 {
bestRtt = totalRtt / time.Duration(responseCount)
}
hasBestResponse = true
logger.Debug("合并所有响应返回", "domain", domain, "responseCount", len(validResponses))
}
case "loadbalance":
// 负载均衡模式 - 使用加权随机选择算法
// 1. 尝试所有可用的服务器,直到找到一个能正常工作的
@@ -1289,8 +1395,14 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
}
default:
// 默认使用并行请求模式 - 添加超时处理和快速响应返回
// 默认使用并行请求模式 - 实现快速返回和超时机制
responses := make(chan serverResponse, len(selectedUpstreamDNS))
resultChan := make(chan struct {
response *dns.Msg
rtt time.Duration
usedServer string
usedDnssecServer string
}, 1)
var wg sync.WaitGroup
// 向所有上游服务器并行发送请求
@@ -1299,113 +1411,287 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
go func(server string) {
defer wg.Done()
// 发送请求并获取响应
response, rtt, err := s.resolver.Exchange(r, normalizeDNSServerAddress(server))
// 创建带有超时的resolver
client := &dns.Client{
Net: s.resolver.Net,
UDPSize: s.resolver.UDPSize,
Timeout: defaultTimeout,
}
// 发送请求并获取响应,确保服务器地址包含端口号
response, rtt, err := client.Exchange(r, normalizeDNSServerAddress(server))
responses <- serverResponse{response, rtt, server, err}
}(upstream)
}
// 等待所有请求完成
// 处理响应的协程
go func() {
var fastestResponse *dns.Msg
var fastestRtt time.Duration = defaultTimeout
var fastestServer string
var fastestDnssecServer string
var fastestHasDnssec bool
var validResponses []*dns.Msg
// 等待所有请求完成或超时
timer := time.NewTimer(defaultTimeout)
defer timer.Stop()
// 处理所有响应
for {
select {
case resp, ok := <-responses:
if !ok {
// 所有响应都已处理
goto doneProcessing
}
if resp.error == nil && resp.response != nil {
// 更新服务器统计信息
s.updateServerStats(resp.server, true, resp.rtt)
// 检查是否包含DNSSEC记录
containsDNSSEC := s.hasDNSSECRecords(resp.response)
// 如果启用了DNSSEC且响应包含DNSSEC记录验证DNSSEC签名
// 但如果域名匹配不验证DNSSEC的模式则跳过验证
if s.config.EnableDNSSEC && containsDNSSEC && !noDNSSEC {
// 验证DNSSEC记录
signatureValid := s.verifyDNSSEC(resp.response)
// 设置AD标志Authenticated Data
resp.response.AuthenticatedData = signatureValid
if signatureValid {
// 更新DNSSEC验证成功计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECSuccess++
})
} else {
// 更新DNSSEC验证失败计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECFailed++
})
}
} else if noDNSSEC {
// 对于不验证DNSSEC的域名始终设置AD标志为false
resp.response.AuthenticatedData = false
}
// 检查当前服务器是否是DNSSEC专用服务器
dnssecServerForResponse := ""
for _, dnssecServer := range dnssecServers {
if dnssecServer == resp.server {
dnssecServerForResponse = resp.server
break
}
}
// 如果响应成功或为NXDOMAIN
if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError {
// 添加到有效响应列表,用于后续合并
validResponses = append(validResponses, resp.response)
// 快速返回逻辑:找到第一个有效响应或更快的响应
if resp.response.Rcode == dns.RcodeSuccess {
// 优先选择带有DNSSEC的响应
if containsDNSSEC {
// 如果这是第一个DNSSEC响应或者比当前最快的DNSSEC响应更快
if !fastestHasDnssec || resp.rtt < fastestRtt {
fastestResponse = resp.response
fastestRtt = resp.rtt
fastestServer = resp.server
fastestDnssecServer = dnssecServerForResponse
fastestHasDnssec = true
// 只对将要返回的响应进行DNSSEC验证
if s.config.EnableDNSSEC && containsDNSSEC && !noDNSSEC {
// 验证DNSSEC记录
signatureValid := s.verifyDNSSEC(fastestResponse)
// 设置AD标志Authenticated Data
fastestResponse.AuthenticatedData = signatureValid
if signatureValid {
// 更新DNSSEC验证成功计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECSuccess++
})
} else {
// 更新DNSSEC验证失败计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECFailed++
})
}
}
// 发送结果,快速返回
resultChan <- struct {
response *dns.Msg
rtt time.Duration
usedServer string
usedDnssecServer string
}{fastestResponse, fastestRtt, fastestServer, fastestDnssecServer}
}
} else {
// 非DNSSEC响应只有在还没有找到DNSSEC响应且当前响应更快时才更新
if !fastestHasDnssec && resp.rtt < fastestRtt {
fastestResponse = resp.response
fastestRtt = resp.rtt
fastestServer = resp.server
fastestDnssecServer = dnssecServerForResponse
// 检查是否包含DNSSEC记录
respContainsDNSSEC := s.hasDNSSECRecords(fastestResponse)
// 只对将要返回的响应进行DNSSEC验证
if s.config.EnableDNSSEC && respContainsDNSSEC && !noDNSSEC {
// 验证DNSSEC记录
signatureValid := s.verifyDNSSEC(fastestResponse)
// 设置AD标志Authenticated Data
fastestResponse.AuthenticatedData = signatureValid
if signatureValid {
// 更新DNSSEC验证成功计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECSuccess++
})
} else {
// 更新DNSSEC验证失败计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECFailed++
})
}
}
// 发送结果,快速返回
resultChan <- struct {
response *dns.Msg
rtt time.Duration
usedServer string
usedDnssecServer string
}{fastestResponse, fastestRtt, fastestServer, fastestDnssecServer}
}
}
} else if resp.response.Rcode == dns.RcodeNameError {
// NXDOMAIN响应只有在还没有找到响应或当前响应更快时才更新
if !fastestHasDnssec && resp.rtt < fastestRtt {
fastestResponse = resp.response
fastestRtt = resp.rtt
fastestServer = resp.server
fastestDnssecServer = dnssecServerForResponse
// 检查是否包含DNSSEC记录
respContainsDNSSEC := s.hasDNSSECRecords(fastestResponse)
// 只对将要返回的响应进行DNSSEC验证
if s.config.EnableDNSSEC && respContainsDNSSEC && !noDNSSEC {
// 验证DNSSEC记录
signatureValid := s.verifyDNSSEC(fastestResponse)
// 设置AD标志Authenticated Data
fastestResponse.AuthenticatedData = signatureValid
if signatureValid {
// 更新DNSSEC验证成功计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECSuccess++
})
} else {
// 更新DNSSEC验证失败计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECFailed++
})
}
}
// 发送结果,快速返回
resultChan <- struct {
response *dns.Msg
rtt time.Duration
usedServer string
usedDnssecServer string
}{fastestResponse, fastestRtt, fastestServer, fastestDnssecServer}
}
}
} else {
// 更新备选响应,确保总有一个可用的响应
if resp.response != nil {
if !hasBackup {
// 第一次保存备选响应
backupResponse = resp.response
backupRtt = resp.rtt
hasBackup = true
}
}
}
} else {
// 更新服务器统计信息(失败)
s.updateServerStats(resp.server, false, 0)
}
case <-timer.C:
// 超时,停止等待更多响应
goto doneProcessing
}
}
doneProcessing:
// 合并所有有效响应,用于缓存
if len(validResponses) > 1 {
mergedResponse := mergeResponses(validResponses)
if mergedResponse != nil {
// 只在合并后的响应比最快响应更好时才使用
mergedHasDnssec := s.hasDNSSECRecords(mergedResponse)
if mergedHasDnssec && !fastestHasDnssec {
// 合并后的响应有DNSSEC而最快响应没有使用合并后的响应
fastestResponse = mergedResponse
// 使用最快的Rtt作为合并响应的Rtt
fastestHasDnssec = true
}
}
}
// 如果还没有发送结果,发送最快的响应
if fastestResponse != nil {
resultChan <- struct {
response *dns.Msg
rtt time.Duration
usedServer string
usedDnssecServer string
}{fastestResponse, fastestRtt, fastestServer, fastestDnssecServer}
}
close(resultChan)
}()
// 等待所有请求完成(不阻塞主流程)
go func() {
wg.Wait()
close(responses)
}()
// 处理所有响应,实现快速响应返回
for resp := range responses {
if resp.error == nil && resp.response != nil {
// 检查是否包含DNSSEC记录
containsDNSSEC := s.hasDNSSECRecords(resp.response)
// 如果启用了DNSSEC且响应包含DNSSEC记录验证DNSSEC签名
// 但如果域名匹配不验证DNSSEC的模式则跳过验证
if s.config.EnableDNSSEC && containsDNSSEC && !noDNSSEC {
// 验证DNSSEC记录
signatureValid := s.verifyDNSSEC(resp.response)
// 设置AD标志Authenticated Data
resp.response.AuthenticatedData = signatureValid
if signatureValid {
// 更新DNSSEC验证成功计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECSuccess++
})
} else {
// 更新DNSSEC验证失败计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
stats.DNSSECFailed++
})
}
} else if noDNSSEC {
// 对于不验证DNSSEC的域名始终设置AD标志为false
resp.response.AuthenticatedData = false
}
// 如果响应成功或为NXDOMAIN根据DNSSEC状态选择最佳响应
if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError {
if resp.response.Rcode == dns.RcodeSuccess {
// 优先选择带有DNSSEC记录的响应
if containsDNSSEC {
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
hasDNSSECResponse = true
usedDNSServer = resp.server
// 如果当前使用的服务器是DNSSEC专用服务器同时设置usedDNSSECServer
for _, dnssecServer := range dnssecServers {
if dnssecServer == resp.server {
usedDNSSECServer = resp.server
break
}
}
logger.Debug("找到带DNSSEC的最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
// 快速返回找到带DNSSEC的响应立即返回
continue
} else if !hasBestResponse {
// 没有带DNSSEC的响应时保存第一个成功响应
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
// 如果当前使用的服务器是DNSSEC专用服务器同时设置usedDNSSECServer
for _, dnssecServer := range dnssecServers {
if dnssecServer == resp.server {
usedDNSSECServer = resp.server
break
}
}
logger.Debug("找到最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
// 快速返回:第一次找到成功响应,立即返回
continue
}
} else if resp.response.Rcode == dns.RcodeNameError {
// 处理NXDOMAIN响应
// 如果还没有最佳响应或者最佳响应也是NXDOMAIN优先选择更快的NXDOMAIN响应
if !hasBestResponse || bestResponse.Rcode == dns.RcodeNameError {
// 如果还没有最佳响应,或者当前响应更快,更新最佳响应
if !hasBestResponse || resp.rtt < bestRtt {
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
logger.Debug("找到NXDOMAIN最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
// 快速返回找到NXDOMAIN响应立即返回
continue
}
}
}
// 保存为备选响应
if !hasBackup {
backupResponse = resp.response
backupRtt = resp.rtt
hasBackup = true
}
}
}
// 等待结果或超时
select {
case result := <-resultChan:
// 快速返回结果
bestResponse = result.response
bestRtt = result.rtt
usedDNSServer = result.usedServer
usedDNSSECServer = result.usedDnssecServer
hasBestResponse = true
hasDNSSECResponse = s.hasDNSSECRecords(result.response)
logger.Debug("快速返回DNS响应", "domain", domain, "server", result.usedServer, "rtt", result.rtt, "dnssec", hasDNSSECResponse)
case <-time.After(defaultTimeout):
// 超时,使用备选响应
logger.Debug("并行请求超时", "domain", domain, "timeout", defaultTimeout)
}
}
@@ -1430,7 +1716,13 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
}, 1)
go func() {
response, rtt, err := s.resolver.Exchange(r, normalizeDNSServerAddress(selectedDnssecServer))
// 创建带有超时的resolver
client := &dns.Client{
Net: s.resolver.Net,
UDPSize: s.resolver.UDPSize,
Timeout: defaultTimeout,
}
response, rtt, err := client.Exchange(r, normalizeDNSServerAddress(selectedDnssecServer))
resultChan <- struct {
response *dns.Msg
rtt time.Duration
@@ -1442,9 +1734,15 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
var rtt time.Duration
var err error
// 直接获取结果,不使用上下文超时
result := <-resultChan
response, rtt, err = result.response, result.rtt, result.err
// 使用超时获取结果
select {
case result := <-resultChan:
response, rtt, err = result.response, result.rtt, result.err
case <-time.After(defaultTimeout):
// 超时,不再等待
logger.Debug("DNSSEC专用服务器请求超时", "domain", domain, "server", selectedDnssecServer, "timeout", defaultTimeout)
return bestResponse, bestRtt, usedDNSServer, usedDNSSECServer
}
if err == nil && response != nil {
// 更新服务器统计信息
@@ -2840,6 +3138,40 @@ func (s *Server) startCpuUsageMonitor() {
}
}
// startIPGeolocationCacheCleanup 启动IP地理位置缓存清理协程
func (s *Server) startIPGeolocationCacheCleanup() {
ticker := time.NewTicker(time.Hour) // 每小时清理一次
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.cleanupExpiredIPGeolocationCache()
case <-s.ctx.Done():
return
}
}
}
// cleanupExpiredIPGeolocationCache 清理过期的IP地理位置缓存
func (s *Server) cleanupExpiredIPGeolocationCache() {
now := time.Now()
s.ipGeolocationCacheMutex.Lock()
defer s.ipGeolocationCacheMutex.Unlock()
var deletedCount int
for ip, geo := range s.ipGeolocationCache {
if now.After(geo.Expiry) {
delete(s.ipGeolocationCache, ip)
deletedCount++
}
}
if deletedCount > 0 {
logger.Info("清理过期的IP地理位置缓存", "deleted", deletedCount, "remaining", len(s.ipGeolocationCache))
}
}
// getSystemCpuUsage 获取系统CPU使用率
func getSystemCpuUsage(prevIdle, prevTotal *uint64) (float64, error) {
// 读取/proc/stat文件获取CPU统计信息

23
package-lock.json generated Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "dns-server-console",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"detect-file-encoding-and-language": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/detect-file-encoding-and-language/-/detect-file-encoding-and-language-2.4.0.tgz",
"integrity": "sha512-moFSAumrGlLCNU5jnaHyCzRUJJu0BCZunfL08iMbnDAgvNnxZad7+WZ26U2dsrIbGChlDPLKmEyEb2tEPUJFkw=="
},
"json-stream-parser": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stream-parser/-/json-stream-parser-1.3.0.tgz",
"integrity": "sha512-AF3Dm4h7lSvRRMIXJsp6pOVrVl9GeHCw5xORxaPZlJN0PdBM/dyx0qQZEK+CI4UGH9/PX3tphWdqrtvQ1txMzw=="
},
"json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
}
}
}

View File

@@ -1,26 +0,0 @@
{
"name": "dns-server-console",
"version": "1.0.0",
"description": "DNS服务器Web控制台",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"chart.js": "^4.4.8",
"detect-file-encoding-and-language": "^2.4.0",
"font-awesome": "^4.7.0",
"json-stream-parser": "^1.3.0",
"json5": "^2.2.3",
"tailwindcss": "^3.3.3"
},
"devDependencies": {},
"keywords": [
"dns",
"server",
"console",
"web"
],
"author": "",
"license": "ISC"
}

File diff suppressed because it is too large Load Diff

View File

@@ -165,12 +165,16 @@ function processRealTimeData(stats) {
let trendIcon = '---';
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
// 存储当前值用于下次计算趋势
const prevResponseTime = window.dashboardHistoryData.prevResponseTime || stats.avgResponseTime;
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
// 计算变化百分比
if (prevResponseTime > 0) {
// 首次加载时初始化历史数据,不计算趋势
if (prevResponseTime === null) {
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
responsePercent = '0.0%';
trendIcon = '•';
trendClass = 'text-gray-500';
} else {
// 计算变化百分比
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
@@ -185,6 +189,9 @@ function processRealTimeData(stats) {
trendIcon = '•';
trendClass = 'text-gray-500';
}
// 更新历史数据
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
}
}
@@ -208,26 +215,35 @@ function processRealTimeData(stats) {
let trendIcon = '---';
if (stats.topQueryTypeCount !== undefined && stats.topQueryTypeCount !== null) {
// 存储当前值用于下次计算趋势
const prevTopQueryTypeCount = window.dashboardHistoryData.prevTopQueryTypeCount || stats.topQueryTypeCount;
window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
const prevTopQueryTypeCount = window.dashboardHistoryData.prevTopQueryTypeCount;
// 计算变化百分比
if (prevTopQueryTypeCount > 0) {
const changePercent = ((stats.topQueryTypeCount - prevTopQueryTypeCount) / prevTopQueryTypeCount) * 100;
queryPercent = Math.abs(changePercent).toFixed(1) + '%';
// 设置趋势图标和颜色
if (changePercent > 0) {
trendIcon = '↑';
trendClass = 'text-primary';
} else if (changePercent < 0) {
trendIcon = '';
trendClass = 'text-secondary';
} else {
trendIcon = '•';
trendClass = 'text-gray-500';
// 首次加载时初始化历史数据,不计算趋势
if (prevTopQueryTypeCount === null) {
window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
queryPercent = '0.0%';
trendIcon = '•';
trendClass = 'text-gray-500';
} else {
// 计算变化百分比
if (prevTopQueryTypeCount > 0) {
const changePercent = ((stats.topQueryTypeCount - prevTopQueryTypeCount) / prevTopQueryTypeCount) * 100;
queryPercent = Math.abs(changePercent).toFixed(1) + '%';
// 设置趋势图标和颜色
if (changePercent > 0) {
trendIcon = '';
trendClass = 'text-primary';
} else if (changePercent < 0) {
trendIcon = '↓';
trendClass = 'text-secondary';
} else {
trendIcon = '•';
trendClass = 'text-gray-500';
}
}
// 更新历史数据
window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
}
}
@@ -245,23 +261,33 @@ function processRealTimeData(stats) {
let trendIcon = '---';
if (stats.activeIPs !== undefined) {
const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs || stats.activeIPs;
window.dashboardHistoryData.prevActiveIPs = stats.activeIPs;
const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs;
if (prevActiveIPs > 0) {
const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100;
ipsPercent = Math.abs(changePercent).toFixed(1) + '%';
if (changePercent > 0) {
trendIcon = '';
trendClass = 'text-primary';
} else if (changePercent < 0) {
trendIcon = '↓';
trendClass = 'text-secondary';
} else {
trendIcon = '•';
trendClass = 'text-gray-500';
// 首次加载时初始化历史数据,不计算趋势
if (prevActiveIPs === null) {
window.dashboardHistoryData.prevActiveIPs = stats.activeIPs;
ipsPercent = '0.0%';
trendIcon = '•';
trendClass = 'text-gray-500';
} else {
if (prevActiveIPs > 0) {
const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100;
ipsPercent = Math.abs(changePercent).toFixed(1) + '%';
if (changePercent > 0) {
trendIcon = '';
trendClass = 'text-primary';
} else if (changePercent < 0) {
trendIcon = '↓';
trendClass = 'text-secondary';
} else {
trendIcon = '•';
trendClass = 'text-gray-500';
}
}
// 更新历史数据
window.dashboardHistoryData.prevActiveIPs = stats.activeIPs;
}
}

View File

@@ -275,17 +275,22 @@ async function getDomainInfo(domain) {
function isDomainMatch(urlValue, targetDomain, categoryId) {
console.log(' 开始匹配URL:', urlValue, '目标域名:', targetDomain, '类别ID:', categoryId);
// 规范化目标域名,去除末尾的点
const normalizedTargetDomain = targetDomain.replace(/\.$/, '').toLowerCase();
try {
// 尝试将URL值解析为完整URL
console.log(' 尝试解析URL为完整URL');
const url = new URL(urlValue);
const hostname = url.hostname.toLowerCase();
console.log(' 解析成功,主机名:', hostname);
let hostname = url.hostname.toLowerCase();
// 规范化主机名,去除末尾的点
hostname = hostname.replace(/\.$/, '');
console.log(' 解析成功,主机名:', hostname, '规范化目标域名:', normalizedTargetDomain);
// 根据类别ID选择匹配方式
if (categoryId === 2) {
// CDN类别使用域名后缀匹配
if (targetDomain.endsWith('.' + hostname) || targetDomain === hostname) {
if (normalizedTargetDomain.endsWith('.' + hostname) || normalizedTargetDomain === hostname) {
console.log(' CDN域名后缀匹配成功');
return true;
} else {
@@ -294,7 +299,7 @@ function isDomainMatch(urlValue, targetDomain, categoryId) {
}
} else {
// 其他类别,使用完整域名匹配
if (hostname === targetDomain) {
if (hostname === normalizedTargetDomain) {
console.log(' 完整域名匹配成功');
return true;
} else {
@@ -305,13 +310,15 @@ function isDomainMatch(urlValue, targetDomain, categoryId) {
} catch (e) {
console.log(' 解析URL失败将其视为纯域名处理错误信息:', e.message);
// 如果是纯域名而不是完整URL
const urlDomain = urlValue.toLowerCase();
console.log(' 处理为纯域名:', urlDomain);
let urlDomain = urlValue.toLowerCase();
// 规范化纯域名,去除末尾的点
urlDomain = urlDomain.replace(/\.$/, '');
console.log(' 处理为纯域名:', urlDomain, '规范化目标域名:', normalizedTargetDomain);
// 根据类别ID选择匹配方式
if (categoryId === 2) {
// CDN类别使用域名后缀匹配
if (targetDomain.endsWith('.' + urlDomain) || targetDomain === urlDomain) {
if (normalizedTargetDomain.endsWith('.' + urlDomain) || normalizedTargetDomain === urlDomain) {
console.log(' CDN域名后缀匹配成功');
return true;
} else {
@@ -320,7 +327,7 @@ function isDomainMatch(urlValue, targetDomain, categoryId) {
}
} else {
// 其他类别,使用完整域名匹配
if (urlDomain === targetDomain) {
if (urlDomain === normalizedTargetDomain) {
console.log(' 完整域名匹配成功');
return true;
} else {
@@ -1485,20 +1492,20 @@ async function showLogDetailModal(log) {
domainInfoDiv.className = 'col-span-1 md:col-span-2 space-y-1';
domainInfoDiv.innerHTML = `
<div class="text-xs text-gray-500">域名信息</div>
<div class="text-sm font-medium text-gray-900 p-3 bg-gray-50 rounded-md border border-gray-200">
<div class="text-sm font-medium text-gray-900 p-3 bg-gray-50 rounded-md border border-gray-200 w-full">
${domainInfo ? `
<div class="flex items-center mb-2">
${domainInfo.icon ? `<img src="${domainInfo.icon}" alt="${domainInfo.name}" class="w-6 h-6 mr-2 rounded-sm" onerror="this.style.display='none'" />` : ''}
<span class="text-base font-semibold">${domainInfo.name || '未知'}</span>
<span class="text-base font-semibold flex-grow">${domainInfo.name || '未知'}</span>
</div>
<div class="ml-8 mt-1">
<div class="flex items-center mb-1">
<span class="text-gray-500 w-16">类别:</span>
<span>${domainInfo.categoryName || '未知'}</span>
<div class="mt-1">
<div class="flex items-center mb-1 flex-wrap">
<span class="text-gray-500 mr-2">类别</span>
<span class="flex-grow">${domainInfo.categoryName || '未知'}</span>
</div>
<div class="flex items-center">
<span class="text-gray-500 w-16">所属公司:</span>
<span>${domainInfo.company || '未知'}</span>
<div class="flex items-center flex-wrap">
<span class="text-gray-500 mr-2">所属单位:</span>
<span class="flex-grow">${domainInfo.company || '未知'}</span>
</div>
</div>
` : '无'}
@@ -1615,16 +1622,59 @@ async function showLogDetailModal(log) {
clientDetails.appendChild(clientDetailsTitle);
clientDetails.appendChild(clientIPDiv);
// 操作按钮区域
const actionButtons = document.createElement('div');
actionButtons.className = 'pt-4 border-t border-gray-200 flex justify-end space-x-2';
// 根据域名状态显示不同的操作按钮
if (result === 'blocked') {
// 被拦截时显示放行按钮
actionButtons.innerHTML = `
<button class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors text-sm" id="unblock-domain-btn">
<i class="fa fa-unlock-alt mr-1"></i>放行
</button>
`;
} else {
// 未被拦截时显示拦截按钮
actionButtons.innerHTML = `
<button class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-sm" id="block-domain-btn">
<i class="fa fa-lock mr-1"></i>拦截
</button>
`;
}
// 组装内容
content.appendChild(basicInfo);
content.appendChild(responseDetails);
content.appendChild(clientDetails);
content.appendChild(actionButtons);
// 组装模态框
modalContent.appendChild(header);
modalContent.appendChild(content);
modalContainer.appendChild(modalContent);
// 绑定操作按钮事件
if (result === 'blocked') {
const unblockBtn = modalContent.querySelector('#unblock-domain-btn');
if (unblockBtn) {
unblockBtn.addEventListener('click', async () => {
await unblockDomain(domain);
closeModal();
loadLogs(); // 刷新日志列表
});
}
} else {
const blockBtn = modalContent.querySelector('#block-domain-btn');
if (blockBtn) {
blockBtn.addEventListener('click', async () => {
await blockDomain(domain);
closeModal();
loadLogs(); // 刷新日志列表
});
}
}
// 添加到页面
document.body.appendChild(modalContainer);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff