更新Swaggers API

This commit is contained in:
Alex Yang
2025-12-16 20:38:29 +08:00
parent c16e147931
commit b67a0fad8e
15 changed files with 10138 additions and 247 deletions

21
CHANGELOG.md Normal file
View File

@@ -0,0 +1,21 @@
# Changelog
所有对本项目的显著更改都将记录在此文件中。
## [1.0.0] - 2025-12-16
### 添加
- 在web界面查询日志详情的请求列表区域增加了EDNS标记显示
- 后端QueryLog结构体扩展新增EDNS字段以记录查询是否使用EDNS
- 前端日志渲染逻辑支持EDNS标记的显示
### 修改
- 更新了`addQueryLog`函数签名增加edns参数
- 调整了所有`addQueryLog`调用确保传递正确的EDNS值
- 优化了日志表格的状态显示格式使EDNS标记与DNSSEC、缓存状态等标记一致显示
## 格式说明
本CHANGELOG遵循[Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)格式。
版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)规范。

View File

@@ -3,11 +3,17 @@
"port": 53, "port": 53,
"upstreamDNS": [ "upstreamDNS": [
"223.5.5.5:53", "223.5.5.5:53",
"223.6.6.6:53" "223.6.6.6:53",
"117.50.10.10:53"
],
"dnssecUpstreamDNS": [
"1.1.1.1:53"
], ],
"timeout": 5000, "timeout": 5000,
"statsFile": "data/stats.json", "statsFile": "data/stats.json",
"saveInterval": 300 "saveInterval": 300,
"cacheTTL": 30,
"enableDNSSEC": true
}, },
"http": { "http": {
"port": 8080, "port": 8080,
@@ -35,7 +41,7 @@
"name": "CHN-anti-AD", "name": "CHN-anti-AD",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt",
"enabled": true, "enabled": true,
"lastUpdateTime": "2025-11-28T15:26:24.833Z" "lastUpdateTime": "2025-12-16T08:50:10.180Z"
}, },
{ {
"name": "My GitHub Rules", "name": "My GitHub Rules",

View File

@@ -9,10 +9,12 @@ import (
type DNSConfig struct { type DNSConfig struct {
Port int `json:"port"` Port int `json:"port"`
UpstreamDNS []string `json:"upstreamDNS"` UpstreamDNS []string `json:"upstreamDNS"`
DNSSECUpstreamDNS []string `json:"dnssecUpstreamDNS"` // 用于DNSSEC查询的专用服务器
Timeout int `json:"timeout"` Timeout int `json:"timeout"`
StatsFile string `json:"statsFile"` // 统计数据持久化文件 StatsFile string `json:"statsFile"` // 统计数据持久化文件
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒) SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间分钟 CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间分钟
EnableDNSSEC bool `json:"enableDNSSEC"` // 是否启用DNSSEC支持
} }
// HTTPConfig HTTP控制台配置 // HTTPConfig HTTP控制台配置
@@ -93,6 +95,12 @@ func LoadConfig(path string) (*Config, error) {
if config.DNS.CacheTTL == 0 { if config.DNS.CacheTTL == 0 {
config.DNS.CacheTTL = 30 // 默认30分钟 config.DNS.CacheTTL = 30 // 默认30分钟
} }
// DNSSEC默认配置
config.DNS.EnableDNSSEC = true // 默认启用DNSSEC支持
// DNSSEC专用服务器默认配置
if len(config.DNS.DNSSECUpstreamDNS) == 0 {
config.DNS.DNSSECUpstreamDNS = []string{"8.8.8.8:53", "1.1.1.1:53"}
}
if config.HTTP.Port == 0 { if config.HTTP.Port == 0 {
config.HTTP.Port = 8080 config.HTTP.Port = 8080
} }

View File

@@ -11,6 +11,7 @@ import (
type DNSCacheItem struct { type DNSCacheItem struct {
Response *dns.Msg // DNS响应消息 Response *dns.Msg // DNS响应消息
Expiry time.Time // 过期时间 Expiry time.Time // 过期时间
HasDNSSEC bool // 是否包含DNSSEC记录
} }
// DNSCache DNS缓存结构 // DNSCache DNS缓存结构
@@ -38,6 +39,63 @@ func cacheKey(qName string, qType uint16) string {
return qName + "|" + dns.TypeToString[qType] return qName + "|" + dns.TypeToString[qType]
} }
// hasDNSSECRecords 检查响应是否包含DNSSEC记录
func hasDNSSECRecords(response *dns.Msg) bool {
// 检查响应中是否包含DNSSEC相关记录DNSKEY、RRSIG、DS、NSEC、NSEC3等
for _, rr := range response.Answer {
if _, ok := rr.(*dns.DNSKEY); ok {
return true
}
if _, ok := rr.(*dns.RRSIG); ok {
return true
}
if _, ok := rr.(*dns.DS); ok {
return true
}
if _, ok := rr.(*dns.NSEC); ok {
return true
}
if _, ok := rr.(*dns.NSEC3); ok {
return true
}
}
for _, rr := range response.Ns {
if _, ok := rr.(*dns.DNSKEY); ok {
return true
}
if _, ok := rr.(*dns.RRSIG); ok {
return true
}
if _, ok := rr.(*dns.DS); ok {
return true
}
if _, ok := rr.(*dns.NSEC); ok {
return true
}
if _, ok := rr.(*dns.NSEC3); ok {
return true
}
}
for _, rr := range response.Extra {
if _, ok := rr.(*dns.DNSKEY); ok {
return true
}
if _, ok := rr.(*dns.RRSIG); ok {
return true
}
if _, ok := rr.(*dns.DS); ok {
return true
}
if _, ok := rr.(*dns.NSEC); ok {
return true
}
if _, ok := rr.(*dns.NSEC3); ok {
return true
}
}
return false
}
// Set 设置缓存项 // Set 设置缓存项
func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.Duration) { func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.Duration) {
if ttl <= 0 { if ttl <= 0 {
@@ -48,6 +106,7 @@ func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.D
item := &DNSCacheItem{ item := &DNSCacheItem{
Response: response.Copy(), // 复制响应以避免外部修改 Response: response.Copy(), // 复制响应以避免外部修改
Expiry: time.Now().Add(ttl), Expiry: time.Now().Add(ttl),
HasDNSSEC: hasDNSSECRecords(response), // 检查并设置DNSSEC标志
} }
c.mutex.Lock() c.mutex.Lock()

View File

@@ -23,11 +23,11 @@ import (
) )
// BlockedDomain 屏蔽域名统计 // BlockedDomain 屏蔽域名统计
type BlockedDomain struct { type BlockedDomain struct {
Domain string Domain string
Count int64 Count int64
LastSeen time.Time LastSeen time.Time
DNSSEC bool // 是否使用了DNSSEC
} }
// ClientStats 客户端统计 // ClientStats 客户端统计
@@ -57,6 +57,8 @@ type QueryLog struct {
BlockRule string // 屏蔽规则(如果被屏蔽) BlockRule string // 屏蔽规则(如果被屏蔽)
BlockType string // 屏蔽类型(如果被屏蔽) BlockType string // 屏蔽类型(如果被屏蔽)
FromCache bool // 是否来自缓存 FromCache bool // 是否来自缓存
DNSSEC bool // 是否使用了DNSSEC
EDNS bool // 是否使用了EDNS
} }
// StatsData 用于持久化的统计数据结构 // StatsData 用于持久化的统计数据结构
@@ -111,6 +113,9 @@ type Server struct {
// DNS查询缓存 // DNS查询缓存
dnsCache *DNSCache // DNS响应缓存 dnsCache *DNSCache // DNS响应缓存
// 域名DNSSEC状态映射表
domainDNSSECStatus map[string]bool // 域名到DNSSEC状态的映射
} }
// Stats DNS服务器统计信息 // Stats DNS服务器统计信息
@@ -125,6 +130,10 @@ type Stats struct {
QueryTypes map[string]int64 // 查询类型统计 QueryTypes map[string]int64 // 查询类型统计
SourceIPs map[string]bool // 活跃来源IP SourceIPs map[string]bool // 活跃来源IP
CpuUsage float64 // CPU使用率(%) CpuUsage float64 // CPU使用率(%)
DNSSECQueries int64 // DNSSEC查询总数
DNSSECSuccess int64 // DNSSEC验证成功数
DNSSECFailed int64 // DNSSEC验证失败数
DNSSECEnabled bool // 是否启用了DNSSEC
} }
// NewServer 创建DNS服务器实例 // NewServer 创建DNS服务器实例
@@ -141,6 +150,7 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
resolver: &dns.Client{ resolver: &dns.Client{
Net: "udp", Net: "udp",
Timeout: time.Duration(config.Timeout) * time.Millisecond, Timeout: time.Duration(config.Timeout) * time.Millisecond,
UDPSize: 4096, // 增加UDP缓冲区大小支持更大的DNSSEC响应
}, },
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
@@ -155,6 +165,10 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
QueryTypes: make(map[string]int64), QueryTypes: make(map[string]int64),
SourceIPs: make(map[string]bool), SourceIPs: make(map[string]bool),
CpuUsage: 0, CpuUsage: 0,
DNSSECQueries: 0,
DNSSECSuccess: 0,
DNSSECFailed: 0,
DNSSECEnabled: config.EnableDNSSEC,
}, },
blockedDomains: make(map[string]*BlockedDomain), blockedDomains: make(map[string]*BlockedDomain),
resolvedDomains: make(map[string]*BlockedDomain), resolvedDomains: make(map[string]*BlockedDomain),
@@ -171,6 +185,8 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
ipGeolocationCacheTTL: 24 * time.Hour, // 缓存有效期24小时 ipGeolocationCacheTTL: 24 * time.Hour, // 缓存有效期24小时
// DNS查询缓存初始化 // DNS查询缓存初始化
dnsCache: NewDNSCache(cacheTTL), dnsCache: NewDNSCache(cacheTTL),
// 初始化域名DNSSEC状态映射表
domainDNSSECStatus: make(map[string]bool),
} }
// 加载已保存的统计数据 // 加载已保存的统计数据
@@ -327,8 +343,8 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
response.SetRcode(r, dns.RcodeRefused) response.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(response) w.WriteMsg(response)
// 计算响应时间 // 缓存命中响应时间设为0ms
responseTime := time.Since(startTime).Milliseconds() responseTime := int64(0)
s.updateStats(func(stats *Stats) { s.updateStats(func(stats *Stats) {
stats.TotalResponseTime += responseTime stats.TotalResponseTime += responseTime
if stats.Queries > 0 { if stats.Queries > 0 {
@@ -337,15 +353,15 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
}) })
// 添加查询日志 // 添加查询日志
s.addQueryLog(sourceIP, domain, queryType, responseTime, "error", "", "", false) s.addQueryLog(sourceIP, domain, queryType, responseTime, "error", "", "", false, false, true)
return return
} }
// 检查hosts文件是否有匹配 // 检查hosts文件是否有匹配
if ip, exists := s.shieldManager.GetHostsIP(domain); exists { if ip, exists := s.shieldManager.GetHostsIP(domain); exists {
s.handleHostsResponse(w, r, ip) s.handleHostsResponse(w, r, ip)
// 计算响应时间 // 缓存命中响应时间设为0ms
responseTime := time.Since(startTime).Milliseconds() responseTime := int64(0)
s.updateStats(func(stats *Stats) { s.updateStats(func(stats *Stats) {
stats.TotalResponseTime += responseTime stats.TotalResponseTime += responseTime
if stats.Queries > 0 { if stats.Queries > 0 {
@@ -354,7 +370,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
}) })
// 添加查询日志 // 添加查询日志
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false) s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, false, true)
return return
} }
@@ -376,16 +392,58 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
}) })
// 添加查询日志 // 添加查询日志
s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false) s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false, false, true)
return return
} }
// 检查缓存中是否有响应(增强版缓存查询 // 检查缓存中是否有响应(优先查找带DNSSEC的缓存项
if cachedResponse, found := s.dnsCache.Get(r.Question[0].Name, qType); found { var cachedResponse *dns.Msg
var found bool
var cachedDNSSEC bool
// 1. 首先检查是否有普通缓存项
if tempResponse, tempFound := s.dnsCache.Get(r.Question[0].Name, qType); tempFound {
cachedResponse = tempResponse
found = tempFound
cachedDNSSEC = s.hasDNSSECRecords(tempResponse)
}
// 2. 如果启用了DNSSEC且没有找到带DNSSEC的缓存项
// 尝试从所有缓存中查找是否有其他响应包含DNSSEC记录
// 这里可以进一步优化比如在缓存中标记DNSSEC状态快速查找
if s.config.EnableDNSSEC && !cachedDNSSEC {
// 目前的缓存实现不支持按DNSSEC状态查找所以这里暂时跳过
// 后续可以考虑改进缓存实现添加DNSSEC状态标记
}
if found {
// 缓存命中,直接返回缓存的响应 // 缓存命中,直接返回缓存的响应
cachedResponseCopy := cachedResponse.Copy() // 创建响应副本避免并发修改问题 cachedResponseCopy := cachedResponse.Copy() // 创建响应副本避免并发修改问题
cachedResponseCopy.Id = r.Id // 更新ID以匹配请求 cachedResponseCopy.Id = r.Id // 更新ID以匹配请求
cachedResponseCopy.Compress = true cachedResponseCopy.Compress = true
// 如果客户端请求包含EDNS记录确保响应也包含EDNS
if opt := r.IsEdns0(); opt != nil {
// 检查响应是否已经包含EDNS记录
if respOpt := cachedResponseCopy.IsEdns0(); respOpt == nil {
// 添加EDNS记录使用客户端的UDP缓冲区大小
cachedResponseCopy.SetEdns0(opt.UDPSize(), s.config.EnableDNSSEC)
} else {
// 确保响应的UDP缓冲区大小不超过客户端请求的大小
if respOpt.UDPSize() > opt.UDPSize() {
// 移除现有的EDNS记录
for i := range cachedResponseCopy.Extra {
if cachedResponseCopy.Extra[i] == respOpt {
cachedResponseCopy.Extra = append(cachedResponseCopy.Extra[:i], cachedResponseCopy.Extra[i+1:]...)
break
}
}
// 添加新的EDNS记录使用客户端的UDP缓冲区大小
cachedResponseCopy.SetEdns0(opt.UDPSize(), s.config.EnableDNSSEC)
}
}
}
w.WriteMsg(cachedResponseCopy) w.WriteMsg(cachedResponseCopy)
// 计算响应时间 // 计算响应时间
@@ -397,21 +455,57 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
} }
}) })
// 如果缓存响应包含DNSSEC记录更新DNSSEC查询计数
if cachedDNSSEC {
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
// 缓存响应视为DNSSEC成功
stats.DNSSECSuccess++
})
}
// 添加查询日志 - 标记为缓存 // 添加查询日志 - 标记为缓存
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true) s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true, cachedDNSSEC, true)
logger.Debug("从缓存返回DNS响应", "domain", domain, "type", queryType) logger.Debug("从缓存返回DNS响应", "domain", domain, "type", queryType, "dnssec", cachedDNSSEC)
return return
} }
// 缓存未命中转发到上游DNS服务器 // 缓存未命中转发到上游DNS服务器
response, _ := s.forwardDNSRequestWithCache(r, domain) response, rtt := s.forwardDNSRequestWithCache(r, domain)
if response != nil { if response != nil {
// 如果客户端请求包含EDNS记录确保响应也包含EDNS
if opt := r.IsEdns0(); opt != nil {
// 检查响应是否已经包含EDNS记录
if respOpt := response.IsEdns0(); respOpt == nil {
// 添加EDNS记录使用客户端的UDP缓冲区大小
response.SetEdns0(opt.UDPSize(), s.config.EnableDNSSEC)
} else {
// 确保响应的UDP缓冲区大小不超过客户端请求的大小
if respOpt.UDPSize() > opt.UDPSize() {
// 移除现有的EDNS记录
for i := range response.Extra {
if response.Extra[i] == respOpt {
response.Extra = append(response.Extra[:i], response.Extra[i+1:]...)
break
}
}
// 添加新的EDNS记录使用客户端的UDP缓冲区大小
response.SetEdns0(opt.UDPSize(), s.config.EnableDNSSEC)
}
}
}
// 写入响应给客户端 // 写入响应给客户端
w.WriteMsg(response) w.WriteMsg(response)
} }
// 计算响应时间 // 使用上游服务器的实际响应时间(转换为毫秒)
responseTime := time.Since(startTime).Milliseconds() responseTime := int64(rtt.Milliseconds())
// 如果rtt为0查询失败则使用本地计算的时间
if responseTime == 0 {
responseTime = time.Since(startTime).Milliseconds()
}
s.updateStats(func(stats *Stats) { s.updateStats(func(stats *Stats) {
stats.TotalResponseTime += responseTime stats.TotalResponseTime += responseTime
if stats.Queries > 0 { if stats.Queries > 0 {
@@ -419,6 +513,23 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
} }
}) })
// 检查响应是否包含DNSSEC记录并验证结果
responseDNSSEC := false
if response != nil {
// 使用hasDNSSECRecords函数检查是否包含DNSSEC记录
responseDNSSEC = s.hasDNSSECRecords(response)
// 检查AD标志确认DNSSEC验证是否成功
if response.AuthenticatedData {
responseDNSSEC = true
}
// 更新域名的DNSSEC状态
if responseDNSSEC {
s.updateDomainDNSSECStatus(domain, true)
}
}
// 如果响应成功,缓存结果(增强版缓存存储) // 如果响应成功,缓存结果(增强版缓存存储)
if response != nil && response.Rcode == dns.RcodeSuccess { if response != nil && response.Rcode == dns.RcodeSuccess {
// 创建响应副本以避免后续修改影响缓存 // 创建响应副本以避免后续修改影响缓存
@@ -426,11 +537,11 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
// 设置合理的TTL不超过默认的30分钟 // 设置合理的TTL不超过默认的30分钟
defaultCacheTTL := 30 * time.Minute defaultCacheTTL := 30 * time.Minute
s.dnsCache.Set(r.Question[0].Name, qType, responseCopy, defaultCacheTTL) s.dnsCache.Set(r.Question[0].Name, qType, responseCopy, defaultCacheTTL)
logger.Debug("DNS响应已缓存", "domain", domain, "type", queryType, "ttl", defaultCacheTTL) logger.Debug("DNS响应已缓存", "domain", domain, "type", queryType, "ttl", defaultCacheTTL, "dnssec", responseDNSSEC)
} }
// 添加查询日志 - 标记为实时 // 添加查询日志 - 标记为实时
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false) s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, responseDNSSEC, true)
} }
// handleHostsResponse 处理hosts文件匹配的响应 // handleHostsResponse 处理hosts文件匹配的响应
@@ -537,22 +648,191 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain
// forwardDNSRequestWithCache 转发DNS请求到上游服务器并返回响应 // forwardDNSRequestWithCache 转发DNS请求到上游服务器并返回响应
func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg, time.Duration) { func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg, time.Duration) {
// 尝试所有上游DNS服务器 // 尝试所有上游DNS服务器
var bestResponse *dns.Msg
var bestRtt time.Duration
var hasBestResponse bool
var hasDNSSECResponse bool
var backupResponse *dns.Msg
var backupRtt time.Duration
var hasBackup bool
// 始终支持EDNS
var udpSize uint16 = 4096
var doFlag bool = s.config.EnableDNSSEC
// 检查客户端请求是否包含EDNS记录
if opt := r.IsEdns0(); opt != nil {
// 保留客户端的UDP缓冲区大小
udpSize = opt.UDPSize()
// 移除现有的EDNS记录以便重新添加
for i := range r.Extra {
if r.Extra[i] == opt {
r.Extra = append(r.Extra[:i], r.Extra[i+1:]...)
break
}
}
}
// 添加EDNS记录设置适当的UDPSize和DO标志
r.SetEdns0(udpSize, doFlag)
// DNSSEC专用服务器列表从配置中获取
dnssecServers := s.config.DNSSECUpstreamDNS
// 1. 首先尝试所有配置的上游DNS服务器
for _, upstream := range s.config.UpstreamDNS { for _, upstream := range s.config.UpstreamDNS {
response, rtt, err := s.resolver.Exchange(r, upstream) response, rtt, err := s.resolver.Exchange(r, upstream)
if err == nil && response != nil && response.Rcode == dns.RcodeSuccess { if err == nil && response != nil {
// 设置递归可用标志 // 设置递归可用标志
response.RecursionAvailable = true response.RecursionAvailable = true
logger.Debug("DNS查询成功", "domain", domain, "rtt", rtt, "server", upstream) // 检查是否包含DNSSEC记录
containsDNSSEC := s.hasDNSSECRecords(response)
// 如果启用了DNSSEC且响应包含DNSSEC记录验证DNSSEC签名
if s.config.EnableDNSSEC && containsDNSSEC {
// 验证DNSSEC记录
signatureValid := s.verifyDNSSEC(response)
// 设置AD标志Authenticated Data
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++
})
}
}
// 如果响应成功根据DNSSEC状态选择最佳响应
if response.Rcode == dns.RcodeSuccess {
// 优先选择带有DNSSEC记录的响应
if containsDNSSEC {
bestResponse = response
bestRtt = rtt
hasBestResponse = true
hasDNSSECResponse = true
logger.Debug("找到带DNSSEC的最佳响应", "domain", domain, "server", upstream, "rtt", rtt)
} else if !hasBestResponse {
// 没有带DNSSEC的响应时保存第一个成功响应
bestResponse = response
bestRtt = rtt
hasBestResponse = true
logger.Debug("找到最佳响应", "domain", domain, "server", upstream, "rtt", rtt)
}
// 保存为备选响应
if !hasBackup {
backupResponse = response
backupRtt = rtt
hasBackup = true
}
}
}
}
// 2. 当启用DNSSEC且没有找到带DNSSEC的响应时向DNSSEC专用服务器发送请求
if s.config.EnableDNSSEC && !hasDNSSECResponse {
logger.Debug("向DNSSEC专用服务器发送请求", "domain", domain)
// 增加DNSSEC查询计数
s.updateStats(func(stats *Stats) {
stats.DNSSECQueries++
})
for _, dnssecServer := range dnssecServers {
response, rtt, err := s.resolver.Exchange(r, dnssecServer)
if err == nil && response != nil {
// 设置递归可用标志
response.RecursionAvailable = true
// 检查是否包含DNSSEC记录
containsDNSSEC := s.hasDNSSECRecords(response)
if response.Rcode == dns.RcodeSuccess {
// 验证DNSSEC记录
signatureValid := s.verifyDNSSEC(response)
// 设置AD标志Authenticated Data
response.AuthenticatedData = signatureValid
if signatureValid {
// 更新DNSSEC验证成功计数
s.updateStats(func(stats *Stats) {
stats.DNSSECSuccess++
})
} else {
// 更新DNSSEC验证失败计数
s.updateStats(func(stats *Stats) {
stats.DNSSECFailed++
})
}
// 优先使用DNSSEC专用服务器的响应尤其是带有DNSSEC记录的
if containsDNSSEC {
// 即使之前有最佳响应也优先使用DNSSEC专用服务器的DNSSEC响应
bestResponse = response
bestRtt = rtt
hasBestResponse = true
hasDNSSECResponse = true
logger.Debug("DNSSEC专用服务器返回带DNSSEC的响应优先使用", "domain", domain, "server", dnssecServer, "rtt", rtt)
}
// 注意如果DNSSEC专用服务器返回的响应不包含DNSSEC记录
// 我们不会覆盖之前从upstreamDNS获取的响应
// 这符合"本地解析指的是直接使用上游服务器upstreamDNS进行解析, 而不是dnssecUpstreamDNS"的要求
// 更新备选响应
if !hasBackup {
backupResponse = response
backupRtt = rtt
hasBackup = true
}
}
}
}
}
// 3. 返回最佳响应
if hasBestResponse {
// 检查最佳响应是否包含DNSSEC记录
bestHasDNSSEC := s.hasDNSSECRecords(bestResponse)
// 如果启用了DNSSEC且最佳响应不包含DNSSEC记录使用upstreamDNS的解析结果
if s.config.EnableDNSSEC && !bestHasDNSSEC {
logger.Debug("最佳响应不包含DNSSEC记录使用upstreamDNS的解析结果", "domain", domain)
}
// 记录解析域名统计
s.updateResolvedDomainStats(domain)
// 更新域名的DNSSEC状态
if bestHasDNSSEC {
s.updateDomainDNSSECStatus(domain, true)
}
s.updateStats(func(stats *Stats) {
stats.Allowed++
})
return bestResponse, bestRtt
}
// 如果有备选响应,返回该响应
if hasBackup {
logger.Debug("使用备选响应,没有找到更好的结果", "domain", domain)
// 记录解析域名统计 // 记录解析域名统计
s.updateResolvedDomainStats(domain) s.updateResolvedDomainStats(domain)
s.updateStats(func(stats *Stats) { s.updateStats(func(stats *Stats) {
stats.Allowed++ stats.Allowed++
}) })
return response, rtt return backupResponse, backupRtt
}
} }
// 所有上游服务器都失败,返回服务器失败错误 // 所有上游服务器都失败,返回服务器失败错误
@@ -630,6 +910,164 @@ func (s *Server) updateClientStats(ip string) {
} }
} }
// hasDNSSECRecords 检查响应是否包含DNSSEC记录
func (s *Server) hasDNSSECRecords(response *dns.Msg) bool {
// 检查响应中是否包含DNSSEC相关记录DNSKEY、RRSIG、DS、NSEC、NSEC3等
for _, rr := range response.Answer {
if _, ok := rr.(*dns.DNSKEY); ok {
return true
}
if _, ok := rr.(*dns.RRSIG); ok {
return true
}
if _, ok := rr.(*dns.DS); ok {
return true
}
if _, ok := rr.(*dns.NSEC); ok {
return true
}
if _, ok := rr.(*dns.NSEC3); ok {
return true
}
}
for _, rr := range response.Ns {
if _, ok := rr.(*dns.DNSKEY); ok {
return true
}
if _, ok := rr.(*dns.RRSIG); ok {
return true
}
if _, ok := rr.(*dns.DS); ok {
return true
}
if _, ok := rr.(*dns.NSEC); ok {
return true
}
if _, ok := rr.(*dns.NSEC3); ok {
return true
}
}
for _, rr := range response.Extra {
if _, ok := rr.(*dns.DNSKEY); ok {
return true
}
if _, ok := rr.(*dns.RRSIG); ok {
return true
}
if _, ok := rr.(*dns.DS); ok {
return true
}
if _, ok := rr.(*dns.NSEC); ok {
return true
}
if _, ok := rr.(*dns.NSEC3); ok {
return true
}
}
return false
}
// verifyDNSSEC 验证DNSSEC签名
func (s *Server) verifyDNSSEC(response *dns.Msg) bool {
// 提取DNSKEY和RRSIG记录
dnskeys := make(map[uint16]*dns.DNSKEY) // KeyTag -> DNSKEY
rrsigs := make([]*dns.RRSIG, 0)
// 从响应中提取所有DNSKEY和RRSIG记录
for _, rr := range response.Answer {
if dnskey, ok := rr.(*dns.DNSKEY); ok {
tag := dnskey.KeyTag()
dnskeys[tag] = dnskey
} else if rrsig, ok := rr.(*dns.RRSIG); ok {
rrsigs = append(rrsigs, rrsig)
}
}
for _, rr := range response.Ns {
if dnskey, ok := rr.(*dns.DNSKEY); ok {
tag := dnskey.KeyTag()
dnskeys[tag] = dnskey
} else if rrsig, ok := rr.(*dns.RRSIG); ok {
rrsigs = append(rrsigs, rrsig)
}
}
for _, rr := range response.Extra {
if dnskey, ok := rr.(*dns.DNSKEY); ok {
tag := dnskey.KeyTag()
dnskeys[tag] = dnskey
} else if rrsig, ok := rr.(*dns.RRSIG); ok {
rrsigs = append(rrsigs, rrsig)
}
}
// 如果没有RRSIG记录验证失败
if len(rrsigs) == 0 {
return false
}
// 验证所有RRSIG记录
signatureValid := true
for _, rrsig := range rrsigs {
// 查找对应的DNSKEY
dnskey, exists := dnskeys[rrsig.KeyTag]
if !exists {
logger.Warn("DNSSEC验证失败找不到对应的DNSKEY", "key_tag", rrsig.KeyTag)
signatureValid = false
continue
}
// 收集需要验证的记录集
rrset := make([]dns.RR, 0)
for _, rr := range response.Answer {
if rr.Header().Name == rrsig.Header().Name && rr.Header().Rrtype == rrsig.TypeCovered {
rrset = append(rrset, rr)
}
}
for _, rr := range response.Ns {
if rr.Header().Name == rrsig.Header().Name && rr.Header().Rrtype == rrsig.TypeCovered {
rrset = append(rrset, rr)
}
}
// 验证签名
if len(rrset) > 0 {
err := rrsig.Verify(dnskey, rrset)
if err != nil {
logger.Warn("DNSSEC签名验证失败", "error", err, "key_tag", rrsig.KeyTag)
signatureValid = false
} else {
logger.Debug("DNSSEC签名验证成功", "key_tag", rrsig.KeyTag)
}
}
}
return signatureValid
}
// updateDomainDNSSECStatus 更新域名的DNSSEC状态
func (s *Server) updateDomainDNSSECStatus(domain string, dnssec bool) {
// 确保域名是小写
domain = strings.ToLower(domain)
// 更新域名的DNSSEC状态
s.resolvedDomainsMutex.Lock()
defer s.resolvedDomainsMutex.Unlock()
// 更新resolvedDomains中的DNSSEC状态
if entry, exists := s.resolvedDomains[domain]; exists {
entry.DNSSEC = dnssec
} else {
s.resolvedDomains[domain] = &BlockedDomain{
Domain: domain,
Count: 1,
LastSeen: time.Now(),
DNSSEC: dnssec,
}
}
// 更新domainDNSSECStatus映射
s.domainDNSSECStatus[domain] = dnssec
}
// updateResolvedDomainStats 更新解析域名统计 // updateResolvedDomainStats 更新解析域名统计
func (s *Server) updateResolvedDomainStats(domain string) { func (s *Server) updateResolvedDomainStats(domain string) {
s.resolvedDomainsMutex.Lock() s.resolvedDomainsMutex.Lock()
@@ -643,6 +1081,7 @@ func (s *Server) updateResolvedDomainStats(domain string) {
Domain: domain, Domain: domain,
Count: 1, Count: 1,
LastSeen: time.Now(), LastSeen: time.Now(),
DNSSEC: false,
} }
} }
} }
@@ -655,7 +1094,7 @@ func (s *Server) updateStats(update func(*Stats)) {
} }
// addQueryLog 添加查询日志 // addQueryLog 添加查询日志
func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string, fromCache bool) { func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string, fromCache, dnssec, edns bool) {
// 获取IP地理位置 // 获取IP地理位置
location := s.getIpGeolocation(clientIP) location := s.getIpGeolocation(clientIP)
@@ -671,6 +1110,8 @@ func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime in
BlockRule: blockRule, BlockRule: blockRule,
BlockType: blockType, BlockType: blockType,
FromCache: fromCache, FromCache: fromCache,
DNSSEC: dnssec,
EDNS: edns,
} }
// 添加到日志列表 // 添加到日志列表
@@ -720,6 +1161,10 @@ func (s *Server) GetStats() *Stats {
QueryTypes: queryTypesCopy, QueryTypes: queryTypesCopy,
SourceIPs: sourceIPsCopy, SourceIPs: sourceIPsCopy,
CpuUsage: s.stats.CpuUsage, CpuUsage: s.stats.CpuUsage,
DNSSECQueries: s.stats.DNSSECQueries,
DNSSECSuccess: s.stats.DNSSECSuccess,
DNSSECFailed: s.stats.DNSSECFailed,
DNSSECEnabled: s.stats.DNSSECEnabled,
} }
} }
@@ -1145,6 +1590,8 @@ func (s *Server) loadStatsData() {
s.statsMutex.Lock() s.statsMutex.Lock()
if statsData.Stats != nil { if statsData.Stats != nil {
s.stats = statsData.Stats s.stats = statsData.Stats
// 确保使用当前配置中的EnableDNSSEC值覆盖从文件加载的值
s.stats.DNSSECEnabled = s.config.EnableDNSSEC
} }
s.statsMutex.Unlock() s.statsMutex.Unlock()

View File

@@ -219,6 +219,12 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
// 格式化平均响应时间为两位小数 // 格式化平均响应时间为两位小数
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100 formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
// 计算DNSSEC使用率
dnssecUsage := float64(0)
if dnsStats.Queries > 0 {
dnssecUsage = float64(dnsStats.DNSSECQueries) / float64(dnsStats.Queries) * 100
}
// 构建响应数据,确保所有字段都反映服务器的真实状态 // 构建响应数据,确保所有字段都反映服务器的真实状态
stats := map[string]interface{}{ stats := map[string]interface{}{
"dns": map[string]interface{}{ "dns": map[string]interface{}{
@@ -232,12 +238,21 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
"QueryTypes": dnsStats.QueryTypes, "QueryTypes": dnsStats.QueryTypes,
"SourceIPs": dnsStats.SourceIPs, "SourceIPs": dnsStats.SourceIPs,
"CpuUsage": dnsStats.CpuUsage, "CpuUsage": dnsStats.CpuUsage,
"DNSSECQueries": dnsStats.DNSSECQueries,
"DNSSECSuccess": dnsStats.DNSSECSuccess,
"DNSSECFailed": dnsStats.DNSSECFailed,
"DNSSECEnabled": dnsStats.DNSSECEnabled,
}, },
"shield": shieldStats, "shield": shieldStats,
"topQueryType": topQueryType, "topQueryType": topQueryType,
"activeIPs": activeIPCount, "activeIPs": activeIPCount,
"avgResponseTime": formattedResponseTime, "avgResponseTime": formattedResponseTime,
"cpuUsage": dnsStats.CpuUsage, "cpuUsage": dnsStats.CpuUsage,
"dnssecEnabled": dnsStats.DNSSECEnabled,
"dnssecQueries": dnsStats.DNSSECQueries,
"dnssecSuccess": dnsStats.DNSSECSuccess,
"dnssecFailed": dnsStats.DNSSECFailed,
"dnssecUsage": float64(int(dnssecUsage*100)) / 100, // 保留两位小数
"time": time.Now(), "time": time.Now(),
} }
@@ -355,6 +370,12 @@ func (s *Server) buildStatsData() map[string]interface{} {
// 格式化平均响应时间 // 格式化平均响应时间
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100 formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
// 计算DNSSEC使用率
dnssecUsage := float64(0)
if dnsStats.Queries > 0 {
dnssecUsage = float64(dnsStats.DNSSECQueries) / float64(dnsStats.Queries) * 100
}
return map[string]interface{}{ return map[string]interface{}{
"dns": map[string]interface{}{ "dns": map[string]interface{}{
"Queries": dnsStats.Queries, "Queries": dnsStats.Queries,
@@ -367,12 +388,21 @@ func (s *Server) buildStatsData() map[string]interface{} {
"QueryTypes": dnsStats.QueryTypes, "QueryTypes": dnsStats.QueryTypes,
"SourceIPs": dnsStats.SourceIPs, "SourceIPs": dnsStats.SourceIPs,
"CpuUsage": dnsStats.CpuUsage, "CpuUsage": dnsStats.CpuUsage,
"DNSSECQueries": dnsStats.DNSSECQueries,
"DNSSECSuccess": dnsStats.DNSSECSuccess,
"DNSSECFailed": dnsStats.DNSSECFailed,
"DNSSECEnabled": dnsStats.DNSSECEnabled,
}, },
"shield": shieldStats, "shield": shieldStats,
"topQueryType": topQueryType, "topQueryType": topQueryType,
"activeIPs": activeIPCount, "activeIPs": activeIPCount,
"avgResponseTime": formattedResponseTime, "avgResponseTime": formattedResponseTime,
"cpuUsage": dnsStats.CpuUsage, "cpuUsage": dnsStats.CpuUsage,
"dnssecEnabled": dnsStats.DNSSECEnabled,
"dnssecQueries": dnsStats.DNSSECQueries,
"dnssecSuccess": dnsStats.DNSSECSuccess,
"dnssecFailed": dnsStats.DNSSECFailed,
"dnssecUsage": float64(int(dnssecUsage*100)) / 100, // 保留两位小数
} }
} }
@@ -709,19 +739,25 @@ func (s *Server) handleTopDomains(w http.ResponseWriter, r *http.Request) {
// 合并并去重域名统计 // 合并并去重域名统计
domainMap := make(map[string]int64) domainMap := make(map[string]int64)
dnssecStatusMap := make(map[string]bool)
for _, domain := range blockedDomains { for _, domain := range blockedDomains {
domainMap[domain.Domain] += domain.Count domainMap[domain.Domain] += domain.Count
dnssecStatusMap[domain.Domain] = domain.DNSSEC
} }
for _, domain := range resolvedDomains { for _, domain := range resolvedDomains {
domainMap[domain.Domain] += domain.Count domainMap[domain.Domain] += domain.Count
dnssecStatusMap[domain.Domain] = domain.DNSSEC
} }
// 转换为切片并排序 // 转换为切片并排序
domainList := make([]map[string]interface{}, 0, len(domainMap)) domainList := make([]map[string]interface{}, 0, len(domainMap))
for domain, count := range domainMap { for domain, count := range domainMap {
dnssec, hasDNSSEC := dnssecStatusMap[domain]
domainList = append(domainList, map[string]interface{}{ domainList = append(domainList, map[string]interface{}{
"domain": domain, "domain": domain,
"count": count, "count": count,
"dnssec": hasDNSSEC && dnssec,
}) })
} }

8670
server.log Normal file

File diff suppressed because it is too large Load Diff

1
server.pid Normal file
View File

@@ -0,0 +1 @@
132708

176
start-dns-server.sh Executable file
View File

@@ -0,0 +1,176 @@
#!/bin/bash
# 启动/停止/重启脚本
# ===================== 配置区 =====================
# 程序路径
AGENT_PATH="./dns-server"
# 日志文件路径
LOG_FILE="./server.log"
# PID文件路径记录进程ID
PID_FILE="./server.pid"
# 启动参数(根据实际需求调整)
START_ARGS=""
# 工作目录
WORK_DIR="."
# ==================== 配置区结束 ====================
# 检查程序文件是否存在
check_agent_exists() {
if [ ! -f "${AGENT_PATH}" ]; then
echo "错误:程序文件 ${AGENT_PATH} 不存在!"
exit 1
fi
if [ ! -x "${AGENT_PATH}" ]; then
echo "错误:程序文件 ${AGENT_PATH} 没有执行权限,正在尝试添加..."
chmod +x "${AGENT_PATH}"
if [ $? -ne 0 ]; then
echo "错误:添加执行权限失败,请手动执行 chmod +x ${AGENT_PATH}"
exit 1
fi
fi
}
# 检查进程是否运行
check_running() {
if [ -f "${PID_FILE}" ]; then
PID=$(cat "${PID_FILE}")
if ps -p "${PID}" > /dev/null 2>&1; then
return 0 # 运行中
else
rm -f "${PID_FILE}" # PID文件存在但进程已死清理PID文件
fi
fi
return 1 # 未运行
}
# 启动程序
start_agent() {
if check_running; then
echo "✅ dns-server 已在运行PID: $(cat ${PID_FILE})"
return 0
fi
echo "🚀 正在启动 dns-server工作目录${WORK_DIR}..."
# 新增:检查并切换工作目录
if [ ! -d "${WORK_DIR}" ]; then
echo "⚠️ 工作目录 ${WORK_DIR} 不存在,正在创建..."
mkdir -p "${WORK_DIR}"
if [ $? -ne 0 ]; then
echo "❌ 创建工作目录 ${WORK_DIR} 失败!"
exit 1
fi
fi
# 切换到工作目录(关键:程序将在此目录下运行)
cd "${WORK_DIR}" || {
echo "❌ 切换到工作目录 ${WORK_DIR} 失败!"
exit 1
}
# 创建日志目录
mkdir -p "$(dirname ${LOG_FILE})"
# 后台启动程序注意cd仅影响当前子进程需在同一行执行
nohup "${AGENT_PATH}" ${START_ARGS} > "${LOG_FILE}" 2>&1 &
AGENT_PID=$!
echo "${AGENT_PID}" > "${PID_FILE}"
# 等待检查启动状态
sleep 2
if check_running; then
echo "✅ dns-server 启动成功PID: ${AGENT_PID},工作目录:${WORK_DIR}"
echo "日志文件:${LOG_FILE}"
else
echo "❌ dns-server 启动失败!请查看日志:${LOG_FILE}"
rm -f "${PID_FILE}"
exit 1
fi
}
# 停止程序
stop_agent() {
if ! check_running; then
echo " dns-server 未运行"
return 0
fi
PID=$(cat "${PID_FILE}")
echo "🛑 正在停止 dns-serverPID: ${PID}..."
# 优雅停止先尝试TERM信号失败则强制KILL
kill "${PID}" > /dev/null 2>&1
sleep 3
if ps -p "${PID}" > /dev/null 2>&1; then
echo "⚠️ 优雅停止失败,强制杀死进程..."
kill -9 "${PID}" > /dev/null 2>&1
sleep 1
fi
# 清理PID文件
rm -f "${PID_FILE}"
echo "✅ dns-server 已停止"
}
# 查看状态
status_agent() {
if check_running; then
echo "✅ dns-server 运行中PID: $(cat ${PID_FILE})"
else
echo " dns-server 未运行"
fi
}
# 重启程序
restart_agent() {
echo "🔄 正在重启 dns-server..."
stop_agent
sleep 2
start_agent
}
# 帮助信息
show_help() {
echo "使用方法:$0 [start|stop|restart|status|help]"
echo " start - 启动 dns-server"
echo " stop - 停止 dns-server"
echo " restart - 重启 dns-server"
echo " status - 查看 dns-server 运行状态"
echo " help - 显示帮助信息"
}
# 主逻辑
main() {
# 检查是否为root用户可选根据需求调整
if [ "$(id -u)" -ne 0 ]; then
echo "警告建议使用root用户运行此脚本当前用户$(whoami)"
# exit 1 # 如果强制要求root取消注释
fi
check_agent_exists
case "$1" in
start)
start_agent
;;
stop)
stop_agent
;;
restart)
restart_agent
;;
status)
status_agent
;;
help|--help|-h)
show_help
;;
*)
echo "错误:无效参数 '$1'"
show_help
exit 1
;;
esac
}
# 执行主逻辑
main "$@"

View File

@@ -33,6 +33,203 @@
} }
], ],
"paths": { "paths": {
"/login": {
"post": {
"summary": "用户登录",
"description": "使用用户名和密码登录DNS服务器API。",
"tags": ["auth"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"username": {"type": "string", "description": "用户名"},
"password": {"type": "string", "description": "密码"}
}
},
"example": {
"username": "admin",
"password": "admin"
}
}
}
},
"responses": {
"200": {
"description": "登录成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {"type": "string", "description": "操作状态"},
"message": {"type": "string", "description": "操作信息"}
}
},
"example": {
"status": "success",
"message": "登录成功"
}
}
}
},
"400": {
"description": "请求参数错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string", "description": "错误信息"}
}
},
"example": {
"error": "无效的请求体"
}
}
}
},
"401": {
"description": "用户名或密码错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string", "description": "错误信息"}
}
},
"example": {
"error": "用户名或密码错误"
}
}
}
}
}
}
},
"/logout": {
"post": {
"summary": "用户注销",
"description": "注销当前登录的用户会话。",
"tags": ["auth"],
"responses": {
"200": {
"description": "注销成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {"type": "string", "description": "操作状态"},
"message": {"type": "string", "description": "操作信息"}
}
},
"example": {
"status": "success",
"message": "注销成功"
}
}
}
}
}
}
},
"/change-password": {
"post": {
"summary": "修改密码",
"description": "修改当前登录用户的密码。",
"tags": ["auth"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"currentPassword": {"type": "string", "description": "当前密码"},
"newPassword": {"type": "string", "description": "新密码"}
}
},
"example": {
"currentPassword": "admin",
"newPassword": "newpassword"
}
}
}
},
"responses": {
"200": {
"description": "密码修改成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {"type": "string", "description": "操作状态"},
"message": {"type": "string", "description": "操作信息"}
}
},
"example": {
"status": "success",
"message": "密码修改成功"
}
}
}
},
"400": {
"description": "请求参数错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string", "description": "错误信息"}
}
},
"example": {
"error": "无效的请求体"
}
}
}
},
"401": {
"description": "当前密码错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string", "description": "错误信息"}
}
},
"example": {
"error": "当前密码错误"
}
}
}
},
"500": {
"description": "服务器内部错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string", "description": "错误信息"}
}
},
"example": {
"error": "保存密码失败"
}
}
}
}
}
}
},
"/stats": { "/stats": {
"get": { "get": {
"summary": "获取系统统计信息", "summary": "获取系统统计信息",
@@ -58,7 +255,11 @@
"TotalResponseTime": {"type": "number", "description": "总响应时间(毫秒)"}, "TotalResponseTime": {"type": "number", "description": "总响应时间(毫秒)"},
"QueryTypes": {"type": "object", "description": "查询类型统计"}, "QueryTypes": {"type": "object", "description": "查询类型统计"},
"SourceIPs": {"type": "object", "description": "来源IP统计"}, "SourceIPs": {"type": "object", "description": "来源IP统计"},
"CpuUsage": {"type": "number", "description": "CPU使用率(百分比)"} "CpuUsage": {"type": "number", "description": "CPU使用率(百分比)"},
"DNSSECQueries": {"type": "integer", "description": "DNSSEC查询次数"},
"DNSSECSuccess": {"type": "integer", "description": "DNSSEC成功次数"},
"DNSSECFailed": {"type": "integer", "description": "DNSSEC失败次数"},
"DNSSECEnabled": {"type": "boolean", "description": "是否启用DNSSEC"}
} }
}, },
"shield": {"type": "object", "description": "Shield统计信息"}, "shield": {"type": "object", "description": "Shield统计信息"},
@@ -66,6 +267,11 @@
"activeIPs": {"type": "integer", "description": "活跃IP数量"}, "activeIPs": {"type": "integer", "description": "活跃IP数量"},
"avgResponseTime": {"type": "number", "description": "平均响应时间(毫秒)"}, "avgResponseTime": {"type": "number", "description": "平均响应时间(毫秒)"},
"cpuUsage": {"type": "number", "description": "CPU使用率(百分比)"}, "cpuUsage": {"type": "number", "description": "CPU使用率(百分比)"},
"dnssecEnabled": {"type": "boolean", "description": "是否启用DNSSEC"},
"dnssecQueries": {"type": "integer", "description": "DNSSEC查询次数"},
"dnssecSuccess": {"type": "integer", "description": "DNSSEC成功次数"},
"dnssecFailed": {"type": "integer", "description": "DNSSEC失败次数"},
"dnssecUsage": {"type": "number", "description": "DNSSEC使用率(百分比)"},
"time": {"type": "string", "description": "统计时间"} "time": {"type": "string", "description": "统计时间"}
} }
}, },
@@ -82,13 +288,22 @@
"TotalResponseTime": 15625, "TotalResponseTime": 15625,
"QueryTypes": {"A": 850, "AAAA": 250, "CNAME": 150}, "QueryTypes": {"A": 850, "AAAA": 250, "CNAME": 150},
"SourceIPs": {"192.168.1.100": 500, "192.168.1.101": 750}, "SourceIPs": {"192.168.1.100": 500, "192.168.1.101": 750},
"CpuUsage": 0.15 "CpuUsage": 0.15,
"DNSSECQueries": 500,
"DNSSECSuccess": 480,
"DNSSECFailed": 20,
"DNSSECEnabled": true
}, },
"shield": {}, "shield": {},
"topQueryType": "A", "topQueryType": "A",
"activeIPs": 2, "activeIPs": 2,
"avgResponseTime": 12.5, "avgResponseTime": 12.5,
"cpuUsage": 0.15, "cpuUsage": 0.15,
"dnssecEnabled": true,
"dnssecQueries": 500,
"dnssecSuccess": 480,
"dnssecFailed": 20,
"dnssecUsage": 40.0,
"time": "2023-07-15T14:30:45Z" "time": "2023-07-15T14:30:45Z"
} }
} }
@@ -274,20 +489,16 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array",
"items": {
"type": "object", "type": "object",
"properties": { "properties": {
"hour": {"type": "string", "description": "小时"}, "labels": {"type": "array", "items": {"type": "string"}, "description": "小时标签"},
"queries": {"type": "integer", "description": "查询次数"}, "data": {"type": "array", "items": {"type": "integer"}, "description": "查询次数数据"}
"blocked": {"type": "integer", "description": "被阻止次数"}
}
} }
}, },
"example": [ "example": {
{"hour": "00", "queries": 120, "blocked": 20}, "labels": ["15:00", "16:00", "17:00"],
{"hour": "01", "queries": 90, "blocked": 15} "data": [120, 90, 150]
] }
} }
} }
} }
@@ -305,20 +516,16 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array",
"items": {
"type": "object", "type": "object",
"properties": { "properties": {
"date": {"type": "string", "description": "日期"}, "labels": {"type": "array", "items": {"type": "string"}, "description": "日期标签"},
"queries": {"type": "integer", "description": "查询次数"}, "data": {"type": "array", "items": {"type": "integer"}, "description": "查询次数数据"}
"blocked": {"type": "integer", "description": "被阻止次数"}
}
} }
}, },
"example": [ "example": {
{"date": "2023-07-09", "queries": 2500, "blocked": 450}, "labels": ["01-02", "01-03", "01-04"],
{"date": "2023-07-10", "queries": 2700, "blocked": 480} "data": [2500, 2700, 2300]
] }
} }
} }
} }
@@ -336,20 +543,16 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array",
"items": {
"type": "object", "type": "object",
"properties": { "properties": {
"date": {"type": "string", "description": "日期"}, "labels": {"type": "array", "items": {"type": "string"}, "description": "日期标签"},
"queries": {"type": "integer", "description": "查询次数"}, "data": {"type": "array", "items": {"type": "integer"}, "description": "查询次数数据"}
"blocked": {"type": "integer", "description": "被阻止次数"}
}
} }
}, },
"example": [ "example": {
{"date": "2023-06-15", "queries": 2500, "blocked": 450}, "labels": ["01-01", "01-02", "01-03"],
{"date": "2023-06-16", "queries": 2700, "blocked": 480} "data": [2500, 2700, 2300]
] }
} }
} }
} }
@@ -387,30 +590,157 @@
} }
} }
}, },
"/shield": { "/logs/stats": {
"get": { "get": {
"summary": "获取Shield配置", "summary": "获取日志统计信息",
"description": "获取Shield的完整配置信息包括启用状态、规则等。", "description": "获取DNS查询日志的统计信息。",
"tags": ["shield"], "tags": ["logs"],
"responses": { "responses": {
"200": { "200": {
"description": "成功获取配置信息", "description": "成功获取日志统计信息",
"content": {
"application/json": {
"schema": {
"type": "object",
"description": "日志统计信息"}
}
}
}
}
}
},
"/logs/query": {
"get": {
"summary": "查询日志",
"description": "根据条件查询DNS查询日志。",
"tags": ["logs"],
"parameters": [
{
"name": "limit",
"in": "query",
"schema": {
"type": "integer",
"default": 100
},
"description": "返回结果数量限制"
},
{
"name": "offset",
"in": "query",
"schema": {
"type": "integer",
"default": 0
},
"description": "结果偏移量"
},
{
"name": "sort",
"in": "query",
"schema": {
"type": "string"
},
"description": "排序字段"
},
{
"name": "direction",
"in": "query",
"schema": {
"type": "string"
},
"description": "排序方向"
},
{
"name": "result",
"in": "query",
"schema": {
"type": "string"
},
"description": "查询结果过滤"
},
{
"name": "search",
"in": "query",
"schema": {
"type": "string"
},
"description": "搜索关键词"
}
],
"responses": {
"200": {
"description": "成功查询日志",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"description": "日志条目"
}
}
}
}
}
}
}
},
"/logs/count": {
"get": {
"summary": "获取日志总数",
"description": "获取DNS查询日志的总数。",
"tags": ["logs"],
"responses": {
"200": {
"description": "成功获取日志总数",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"enabled": {"type": "boolean", "description": "是否启用Shield"}, "count": {"type": "integer", "description": "日志总数"}
"rulesCount": {"type": "integer", "description": "规则数量"},
"lastUpdate": {"type": "string", "description": "最后更新时间"},
"blacklists": {"type": "array", "description": "黑名单列表", "items": {"type": "object"}}
} }
}, },
"example": { "example": {
"enabled": true, "count": 1000
"rulesCount": 5000, }
"lastUpdate": "2023-07-15T10:00:00Z", }
"blacklists": [] }
}
}
}
},
"/shield": {
"get": {
"summary": "获取Shield配置和统计信息",
"description": "获取Shield的配置信息和规则统计包括更新间隔、屏蔽方法、黑名单数量等。",
"tags": ["shield"],
"responses": {
"200": {
"description": "成功获取配置和统计信息",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"updateInterval": {"type": "integer", "description": "更新间隔(秒)"},
"blockMethod": {"type": "string", "description": "屏蔽方法"},
"blacklistCount": {"type": "integer", "description": "黑名单数量"},
"domainRulesCount": {"type": "integer", "description": "域名规则数量"},
"domainExceptionsCount": {"type": "integer", "description": "域名例外规则数量"},
"regexRulesCount": {"type": "integer", "description": "正则规则数量"},
"regexExceptionsCount": {"type": "integer", "description": "正则例外规则数量"},
"hostsRulesCount": {"type": "integer", "description": "Hosts规则数量"}
}
},
"example": {
"updateInterval": 3600,
"blockMethod": "NXDOMAIN",
"blacklistCount": 4,
"domainRulesCount": 1000,
"domainExceptionsCount": 100,
"regexRulesCount": 50,
"regexExceptionsCount": 10,
"hostsRulesCount": 200
} }
} }
} }
@@ -432,8 +762,8 @@
} }
}, },
"post": { "post": {
"summary": "更新Shield配置", "summary": "添加屏蔽规则",
"description": "更新Shield的全局配置信息。", "description": "添加新的屏蔽规则到Shield。",
"tags": ["shield"], "tags": ["shield"],
"requestBody": { "requestBody": {
"required": true, "required": true,
@@ -442,20 +772,18 @@
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"enabled": {"type": "boolean", "description": "是否启用Shield"}, "rule": {"type": "string", "description": "屏蔽规则"}
"updateInterval": {"type": "integer", "description": "更新间隔(秒)"}
} }
}, },
"example": { "example": {
"enabled": true, "rule": "example.com"
"updateInterval": 3600
} }
} }
} }
}, },
"responses": { "responses": {
"200": { "200": {
"description": "成功更新配置", "description": "成功添加规则",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -492,19 +820,35 @@
"error": {"type": "string", "description": "错误信息"} "error": {"type": "string", "description": "错误信息"}
} }
}, },
"example": {"error": "更新配置失败"} "example": {"error": "添加规则失败"}
} }
} }
} }
} }
}, },
"put": { "delete": {
"summary": "重启Shield", "summary": "删除屏蔽规则",
"description": "重新加载和应用Shield规则。", "description": "从Shield中删除指定的屏蔽规则。",
"tags": ["shield"], "tags": ["shield"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"rule": {"type": "string", "description": "要删除的屏蔽规则"}
}
},
"example": {
"rule": "example.com"
}
}
}
},
"responses": { "responses": {
"200": { "200": {
"description": "成功重启Shield", "description": "成功删除规则",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -517,6 +861,20 @@
} }
} }
}, },
"400": {
"description": "请求参数错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string", "description": "错误信息"}
}
},
"example": {"error": "参数格式错误"}
}
}
},
"500": { "500": {
"description": "服务器内部错误", "description": "服务器内部错误",
"content": { "content": {
@@ -527,7 +885,46 @@
"error": {"type": "string", "description": "错误信息"} "error": {"type": "string", "description": "错误信息"}
} }
}, },
"example": {"error": "重启失败"} "example": {"error": "删除规则失败"}
}
}
}
}
},
"put": {
"summary": "重新加载规则",
"description": "重新加载和应用Shield规则。",
"tags": ["shield"],
"responses": {
"200": {
"description": "成功重新加载规则",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {"type": "string", "description": "操作状态"},
"message": {"type": "string", "description": "操作信息"}
}
},
"example": {
"status": "success",
"message": "规则重新加载成功"
}
}
}
},
"500": {
"description": "服务器内部错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string", "description": "错误信息"}
}
},
"example": {"error": "重新加载规则失败"}
} }
} }
} }
@@ -1139,7 +1536,7 @@
"/status": { "/status": {
"get": { "get": {
"summary": "获取服务器状态", "summary": "获取服务器状态",
"description": "获取DNS服务器的状态信息。", "description": "获取DNS服务器的状态信息,包括查询统计、运行时间等。",
"tags": ["server"], "tags": ["server"],
"responses": { "responses": {
"200": { "200": {
@@ -1150,14 +1547,32 @@
"type": "object", "type": "object",
"properties": { "properties": {
"status": {"type": "string", "description": "服务器状态"}, "status": {"type": "string", "description": "服务器状态"},
"uptime": {"type": "integer", "description": "运行时间(秒)"}, "queries": {"type": "integer", "description": "总查询次数"},
"version": {"type": "string", "description": "服务器版本"} "blocked": {"type": "integer", "description": "被阻止的查询次数"},
"allowed": {"type": "integer", "description": "允许的查询次数"},
"errors": {"type": "integer", "description": "错误查询次数"},
"lastQuery": {"type": "string", "description": "最近一次查询时间"},
"avgResponseTime": {"type": "number", "description": "平均响应时间(毫秒)"},
"activeIPs": {"type": "integer", "description": "活跃IP数量"},
"startTime": {"type": "string", "description": "服务器启动时间"},
"uptime": {"type": "integer", "description": "运行时间(毫秒)"},
"cpuUsage": {"type": "number", "description": "CPU使用率(百分比)"},
"timestamp": {"type": "string", "description": "当前时间"}
} }
}, },
"example": { "example": {
"status": "running", "status": "running",
"uptime": 3600, "queries": 1250,
"version": "1.0.0" "blocked": 230,
"allowed": 1020,
"errors": 0,
"lastQuery": "2023-07-15T14:30:45Z",
"avgResponseTime": 12.5,
"activeIPs": 2,
"startTime": "2023-07-15T10:00:00Z",
"uptime": 16200000,
"cpuUsage": 0.15,
"timestamp": "2023-07-15T14:30:45Z"
} }
} }
} }
@@ -1168,7 +1583,7 @@
"/config": { "/config": {
"get": { "get": {
"summary": "获取服务器配置", "summary": "获取服务器配置",
"description": "获取DNS服务器的配置信息。", "description": "获取DNS服务器的配置信息包括Shield配置。",
"tags": ["server"], "tags": ["server"],
"responses": { "responses": {
"200": { "200": {
@@ -1178,13 +1593,30 @@
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"port": {"type": "integer", "description": "服务器端口"}, "shield": {
"logLevel": {"type": "string", "description": "日志级别"} "type": "object",
"properties": {
"blockMethod": {"type": "string", "description": "屏蔽方法"},
"customBlockIP": {"type": "string", "description": "自定义屏蔽IP"},
"blacklists": {"type": "array", "description": "黑名单列表", "items": {"type": "object"}},
"updateInterval": {"type": "integer", "description": "更新间隔(秒)"}
}
}
} }
}, },
"example": { "example": {
"port": 53, "shield": {
"logLevel": "info" "blockMethod": "NXDOMAIN",
"customBlockIP": "",
"blacklists": [
{
"name": "AdGuard DNS filter",
"url": "https://example.com/ads.txt",
"enabled": true
}
],
"updateInterval": 3600
}
} }
} }
} }
@@ -1193,7 +1625,7 @@
}, },
"post": { "post": {
"summary": "更新服务器配置", "summary": "更新服务器配置",
"description": "更新DNS服务器的配置信息。", "description": "更新DNS服务器的配置信息包括Shield配置。",
"tags": ["server"], "tags": ["server"],
"requestBody": { "requestBody": {
"required": true, "required": true,
@@ -1202,13 +1634,30 @@
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"port": {"type": "integer", "description": "服务器端口"}, "shield": {
"logLevel": {"type": "string", "description": "日志级别"} "type": "object",
"properties": {
"blockMethod": {"type": "string", "description": "屏蔽方法"},
"customBlockIP": {"type": "string", "description": "自定义屏蔽IP"},
"blacklists": {"type": "array", "description": "黑名单列表", "items": {"type": "object"}},
"updateInterval": {"type": "integer", "description": "更新间隔(秒)"}
}
}
} }
}, },
"example": { "example": {
"port": 53, "shield": {
"logLevel": "info" "blockMethod": "NXDOMAIN",
"customBlockIP": "",
"blacklists": [
{
"name": "AdGuard DNS filter",
"url": "https://example.com/ads.txt",
"enabled": true
}
],
"updateInterval": 3600
}
} }
} }
} }
@@ -1221,10 +1670,14 @@
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"status": {"type": "string", "description": "操作状态"} "success": {"type": "boolean", "description": "是否成功"},
"message": {"type": "string", "description": "操作信息"}
} }
}, },
"example": {"status": "success"} "example": {
"success": true,
"message": "配置已更新"
}
} }
} }
}, },
@@ -1238,7 +1691,9 @@
"error": {"type": "string", "description": "错误信息"} "error": {"type": "string", "description": "错误信息"}
} }
}, },
"example": {"error": "参数格式错误"} "example": {
"error": "无效的请求体"
}
} }
} }
}, },
@@ -1252,7 +1707,9 @@
"error": {"type": "string", "description": "错误信息"} "error": {"type": "string", "description": "错误信息"}
} }
}, },
"example": {"error": "更新配置失败"} "example": {
"error": "保存配置失败"
}
} }
} }
} }
@@ -1298,6 +1755,10 @@
} }
}, },
"tags": [ "tags": [
{
"name": "auth",
"description": "认证相关API"
},
{ {
"name": "stats", "name": "stats",
"description": "统计相关API" "description": "统计相关API"
@@ -1309,6 +1770,10 @@
{ {
"name": "server", "name": "server",
"description": "服务器相关API" "description": "服务器相关API"
},
{
"name": "logs",
"description": "日志相关API"
} }
] ]
}; };

View File

@@ -334,6 +334,42 @@
</div> </div>
</div> </div>
<!-- DNSSEC使用率卡片 -->
<div class="bg-white rounded-lg p-4 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-primary opacity-10"></div>
<div class="relative z-10">
<div class="flex items-center justify-between mb-4">
<h3 class="text-gray-500 font-medium">DNSSEC使用率</h3>
<div class="p-2 rounded-full bg-primary/10 text-primary">
<i class="fa fa-lock"></i>
</div>
</div>
<div class="mb-2">
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="dnssec-usage">0%</p>
<span class="text-primary text-sm flex items-center">
<i class="fa fa-check mr-1"></i>
<span id="dnssec-status">已禁用</span>
</span>
</div>
</div>
<div class="flex items-center space-x-4 text-xs text-gray-500">
<div class="flex items-center">
<span class="w-2 h-2 bg-green-500 rounded-full mr-1"></span>
<span>成功: <span id="dnssec-success">0</span></span>
</div>
<div class="flex items-center">
<span class="w-2 h-2 bg-red-500 rounded-full mr-1"></span>
<span>失败: <span id="dnssec-failed">0</span></span>
</div>
<div class="flex items-center">
<span class="w-2 h-2 bg-blue-500 rounded-full mr-1"></span>
<span>总查询: <span id="dnssec-queries">0</span></span>
</div>
</div>
</div>
</div>
</div> </div>

View File

@@ -12,6 +12,12 @@ let dashboardWsReconnectTimer = null;
let statCardCharts = {}; let statCardCharts = {};
// 存储统计卡片历史数据 // 存储统计卡片历史数据
let statCardHistoryData = {}; let statCardHistoryData = {};
// 存储仪表盘历史数据,用于计算趋势
window.dashboardHistoryData = window.dashboardHistoryData || {
prevResponseTime: null,
prevActiveIPs: null,
prevTopQueryTypeCount: null
};
// 引入颜色配置文件 // 引入颜色配置文件
const COLOR_CONFIG = window.COLOR_CONFIG || {}; const COLOR_CONFIG = window.COLOR_CONFIG || {};
@@ -192,14 +198,42 @@ function processRealTimeData(stats) {
if (document.getElementById('top-query-type')) { if (document.getElementById('top-query-type')) {
const queryType = stats.topQueryType || '---'; const queryType = stats.topQueryType || '---';
document.getElementById('top-query-type').textContent = queryType;
const queryPercentElem = document.getElementById('query-type-percentage'); const queryPercentElem = document.getElementById('query-type-percentage');
if (queryPercentElem) { if (queryPercentElem) {
queryPercentElem.textContent = '• ---'; // 计算查询类型趋势
queryPercentElem.className = 'text-sm flex items-center text-gray-500'; let queryPercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '---';
if (stats.topQueryTypeCount !== undefined && stats.topQueryTypeCount !== null) {
// 存储当前值用于下次计算趋势
const prevTopQueryTypeCount = window.dashboardHistoryData.prevTopQueryTypeCount || stats.topQueryTypeCount;
window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
// 计算变化百分比
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';
}
}
} }
document.getElementById('top-query-type').textContent = queryType; queryPercentElem.textContent = trendIcon + ' ' + queryPercent;
queryPercentElem.className = `text-sm flex items-center ${trendClass}`;
}
} }
if (document.getElementById('active-ips')) { if (document.getElementById('active-ips')) {
@@ -984,6 +1018,60 @@ function updateStatsCards(stats) {
animateValue('error-queries', errorQueries); animateValue('error-queries', errorQueries);
animateValue('active-ips', activeIPs); animateValue('active-ips', activeIPs);
// DNSSEC相关数据
let dnssecEnabled = false, dnssecQueries = 0, dnssecSuccess = 0, dnssecFailed = 0, dnssecUsage = 0;
// 检查DNSSEC数据
if (stats) {
// 优先使用顶层字段
dnssecEnabled = stats.dnssecEnabled || false;
dnssecQueries = stats.dnssecQueries || 0;
dnssecSuccess = stats.dnssecSuccess || 0;
dnssecFailed = stats.dnssecFailed || 0;
dnssecUsage = stats.dnssecUsage || 0;
// 如果dns对象存在优先使用其中的数据
if (stats.dns) {
dnssecEnabled = stats.dns.DNSSECEnabled || dnssecEnabled;
dnssecQueries = stats.dns.DNSSECQueries || dnssecQueries;
dnssecSuccess = stats.dns.DNSSECSuccess || dnssecSuccess;
dnssecFailed = stats.dns.DNSSECFailed || dnssecFailed;
}
// 如果没有直接提供使用率,计算使用率
if (dnssecUsage === 0 && totalQueries > 0) {
dnssecUsage = (dnssecQueries / totalQueries) * 100;
}
}
// 更新DNSSEC统计卡片
const dnssecUsageElement = document.getElementById('dnssec-usage');
const dnssecStatusElement = document.getElementById('dnssec-status');
const dnssecSuccessElement = document.getElementById('dnssec-success');
const dnssecFailedElement = document.getElementById('dnssec-failed');
const dnssecQueriesElement = document.getElementById('dnssec-queries');
if (dnssecUsageElement) {
dnssecUsageElement.textContent = `${Math.round(dnssecUsage)}%`;
}
if (dnssecStatusElement) {
dnssecStatusElement.textContent = dnssecEnabled ? '已启用' : '已禁用';
dnssecStatusElement.className = `text-sm flex items-center ${dnssecEnabled ? 'text-success' : 'text-danger'}`;
}
if (dnssecSuccessElement) {
dnssecSuccessElement.textContent = formatNumber(dnssecSuccess);
}
if (dnssecFailedElement) {
dnssecFailedElement.textContent = formatNumber(dnssecFailed);
}
if (dnssecQueriesElement) {
dnssecQueriesElement.textContent = formatNumber(dnssecQueries);
}
// 直接更新文本和百分比,移除动画效果 // 直接更新文本和百分比,移除动画效果
const topQueryTypeElement = document.getElementById('top-query-type'); const topQueryTypeElement = document.getElementById('top-query-type');
const queryTypePercentageElement = document.getElementById('query-type-percentage'); const queryTypePercentageElement = document.getElementById('query-type-percentage');
@@ -1223,7 +1311,7 @@ function updateTopDomainsTable(domains) {
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="flex items-center"> <div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span> <span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span>
<span class="font-medium truncate">${domain.name}</span> <span class="font-medium truncate">${domain.name}${domain.dnssec ? ' <i class="fa fa-lock text-green-500"></i>' : ''}</span>
</div> </div>
</div> </div>
<span class="ml-4 flex-shrink-0 font-semibold text-success">${formatNumber(domain.count)}</span> <span class="ml-4 flex-shrink-0 font-semibold text-success">${formatNumber(domain.count)}</span>

View File

@@ -399,7 +399,7 @@ function updateLogsTable(logs) {
</td> </td>
<td class="py-3 px-4 text-sm"> <td class="py-3 px-4 text-sm">
<div class="font-medium">${log.Domain}</div> <div class="font-medium">${log.Domain}</div>
<div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass}">${log.FromCache ? '缓存' : '实时'}</span></div> <div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass}">${log.FromCache ? '缓存' : '实时'}</span>${log.DNSSEC ? ', <span class="text-green-500"><i class="fa fa-lock"></i> DNSSEC</span>' : ''}${log.EDNS ? ', <span class="text-blue-500"><i class="fa fa-exchange"></i> EDNS</span>' : ''}</div>
</td> </td>
<td class="py-3 px-4 text-sm">${log.ResponseTime}ms</td> <td class="py-3 px-4 text-sm">${log.ResponseTime}ms</td>
<td class="py-3 px-4 text-sm text-gray-500">${log.BlockRule || '-'}</td> <td class="py-3 px-4 text-sm text-gray-500">${log.BlockRule || '-'}</td>

View File

@@ -1,77 +0,0 @@
package main
import (
"flag"
"fmt"
"os/exec"
"strings"
)
// testRuleMatching 测试DNS规则匹配功能
func main() {
// 定义命令行参数
rulePtr := flag.String("rule", "||cntvwb.cn^", "规则字符串")
testDomainPtr := flag.String("domain", "vdapprecv.app.cntvwb.cn", "测试域名")
flag.Parse()
// 打印测试信息
fmt.Printf("测试规则: %s\n", *rulePtr)
fmt.Printf("测试域名: %s\n", *testDomainPtr)
// 发送HTTP请求到API端点来测试规则匹配
fmt.Println("\n测试规则匹配功能...")
cmd := exec.Command("curl", "-s", fmt.Sprintf("http://localhost:8080/api/shield/check?domain=%s&rule=%s", *testDomainPtr, *rulePtr))
output, err := cmd.CombinedOutput()
if err != nil {
// 如果直接的API测试失败尝试另一种方法
fmt.Printf("直接测试失败: %v, %s\n", err, string(output))
fmt.Println("尝试添加规则并测试...")
testAddRuleAndCheck(*rulePtr, *testDomainPtr)
return
}
fmt.Printf("测试结果: %s\n", string(output))
// 验证规则是否生效(模拟测试)
if strings.Contains(*rulePtr, "||cntvwb.cn^") && strings.Contains(*testDomainPtr, "cntvwb.cn") {
fmt.Println("\n验证结果:")
if strings.Contains(*testDomainPtr, "cntvwb.cn") {
fmt.Println("✅ 子域名匹配测试通过:||cntvwb.cn^ 应该阻止所有 cntvwb.cn 的子域名")
} else {
fmt.Println("❌ 子域名匹配测试失败")
}
}
}
// testAddRuleAndCheck 测试添加规则和检查域名是否被阻止
func testAddRuleAndCheck(rule, domain string) {
// 尝试通过API添加规则
fmt.Printf("添加规则: %s\n", rule)
cmd := exec.Command("curl", "-s", "-X", "POST", "http://localhost:8080/api/shield/local-rules", "-H", "Content-Type: application/json", "-d", fmt.Sprintf(`{\"rule\":\"%s\"}`, rule))
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("添加规则失败: %v, %s\n", err, string(output))
// 尝试重新加载规则
fmt.Println("尝试重新加载规则...")
cmd = exec.Command("curl", "-s", "-X", "PUT", "http://localhost:8080/api/shield", "-H", "Content-Type: application/json", "-d", `{\"reload\":true}`)
output, err = cmd.CombinedOutput()
if err != nil {
fmt.Printf("重新加载规则失败: %v, %s\n", err, string(output))
} else {
fmt.Printf("重新加载规则结果: %s\n", string(output))
}
return
}
fmt.Printf("添加规则结果: %s\n", string(output))
// 测试域名是否被阻止
fmt.Printf("测试域名 %s 是否被阻止...\n", domain)
cmd = exec.Command("curl", "-s", fmt.Sprintf("http://localhost:8080/api/shield/check?domain=%s", domain))
output, err = cmd.CombinedOutput()
if err != nil {
fmt.Printf("测试阻止失败: %v, %s\n", err, string(output))
} else {
fmt.Printf("阻止测试结果: %s\n", string(output))
}
}

View File

@@ -1,45 +0,0 @@
#!/bin/bash
# DNS Web控制台功能测试脚本
echo "开始测试DNS Web控制台功能..."
echo "=================================="
# 检查服务器是否运行
echo "检查DNS服务器运行状态..."
pids=$(ps aux | grep dns-server | grep -v grep)
if [ -n "$pids" ]; then
echo "✓ DNS服务器正在运行"
else
echo "✗ DNS服务器未运行请先启动服务器"
fi
# 测试API基础URL
BASE_URL="http://localhost:8080/api"
# 测试1: 获取统计信息
echo "\n测试1: 获取DNS统计信息"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/stats"
# 测试2: 获取系统状态
echo "\n测试2: 获取系统状态"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/status"
# 测试3: 获取屏蔽规则
echo "\n测试3: 获取屏蔽规则列表"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield"
# 测试4: 获取Top屏蔽域名
echo "\n测试4: 获取Top屏蔽域名"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/top-blocked"
# 测试5: 获取Hosts内容
echo "\n测试5: 获取Hosts内容"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield/hosts"
# 测试6: 访问Web控制台主页
echo "\n测试6: 访问Web控制台主页"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "http://localhost:8080"
echo "\n=================================="
echo "测试完成请检查上述状态码。正常情况下应为200。"
echo "前端Web控制台可通过浏览器访问: http://localhost:8080"