更新Swaggers API
This commit is contained in:
21
CHANGELOG.md
Normal file
21
CHANGELOG.md
Normal 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/)规范。
|
||||
12
config.json
12
config.json
@@ -3,11 +3,17 @@
|
||||
"port": 53,
|
||||
"upstreamDNS": [
|
||||
"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,
|
||||
"statsFile": "data/stats.json",
|
||||
"saveInterval": 300
|
||||
"saveInterval": 300,
|
||||
"cacheTTL": 30,
|
||||
"enableDNSSEC": true
|
||||
},
|
||||
"http": {
|
||||
"port": 8080,
|
||||
@@ -35,7 +41,7 @@
|
||||
"name": "CHN-anti-AD",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt",
|
||||
"enabled": true,
|
||||
"lastUpdateTime": "2025-11-28T15:26:24.833Z"
|
||||
"lastUpdateTime": "2025-12-16T08:50:10.180Z"
|
||||
},
|
||||
{
|
||||
"name": "My GitHub Rules",
|
||||
|
||||
@@ -9,10 +9,12 @@ import (
|
||||
type DNSConfig struct {
|
||||
Port int `json:"port"`
|
||||
UpstreamDNS []string `json:"upstreamDNS"`
|
||||
DNSSECUpstreamDNS []string `json:"dnssecUpstreamDNS"` // 用于DNSSEC查询的专用服务器
|
||||
Timeout int `json:"timeout"`
|
||||
StatsFile string `json:"statsFile"` // 统计数据持久化文件
|
||||
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
||||
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟)
|
||||
EnableDNSSEC bool `json:"enableDNSSEC"` // 是否启用DNSSEC支持
|
||||
}
|
||||
|
||||
// HTTPConfig HTTP控制台配置
|
||||
@@ -93,6 +95,12 @@ func LoadConfig(path string) (*Config, error) {
|
||||
if config.DNS.CacheTTL == 0 {
|
||||
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 {
|
||||
config.HTTP.Port = 8080
|
||||
}
|
||||
|
||||
59
dns/cache.go
59
dns/cache.go
@@ -11,6 +11,7 @@ import (
|
||||
type DNSCacheItem struct {
|
||||
Response *dns.Msg // DNS响应消息
|
||||
Expiry time.Time // 过期时间
|
||||
HasDNSSEC bool // 是否包含DNSSEC记录
|
||||
}
|
||||
|
||||
// DNSCache DNS缓存结构
|
||||
@@ -38,6 +39,63 @@ func cacheKey(qName string, qType uint16) string {
|
||||
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 设置缓存项
|
||||
func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.Duration) {
|
||||
if ttl <= 0 {
|
||||
@@ -48,6 +106,7 @@ func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.D
|
||||
item := &DNSCacheItem{
|
||||
Response: response.Copy(), // 复制响应以避免外部修改
|
||||
Expiry: time.Now().Add(ttl),
|
||||
HasDNSSEC: hasDNSSECRecords(response), // 检查并设置DNSSEC标志
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
|
||||
491
dns/server.go
491
dns/server.go
@@ -23,11 +23,11 @@ import (
|
||||
)
|
||||
|
||||
// BlockedDomain 屏蔽域名统计
|
||||
|
||||
type BlockedDomain struct {
|
||||
Domain string
|
||||
Count int64
|
||||
LastSeen time.Time
|
||||
DNSSEC bool // 是否使用了DNSSEC
|
||||
}
|
||||
|
||||
// ClientStats 客户端统计
|
||||
@@ -57,6 +57,8 @@ type QueryLog struct {
|
||||
BlockRule string // 屏蔽规则(如果被屏蔽)
|
||||
BlockType string // 屏蔽类型(如果被屏蔽)
|
||||
FromCache bool // 是否来自缓存
|
||||
DNSSEC bool // 是否使用了DNSSEC
|
||||
EDNS bool // 是否使用了EDNS
|
||||
}
|
||||
|
||||
// StatsData 用于持久化的统计数据结构
|
||||
@@ -111,6 +113,9 @@ type Server struct {
|
||||
|
||||
// DNS查询缓存
|
||||
dnsCache *DNSCache // DNS响应缓存
|
||||
|
||||
// 域名DNSSEC状态映射表
|
||||
domainDNSSECStatus map[string]bool // 域名到DNSSEC状态的映射
|
||||
}
|
||||
|
||||
// Stats DNS服务器统计信息
|
||||
@@ -125,6 +130,10 @@ type Stats struct {
|
||||
QueryTypes map[string]int64 // 查询类型统计
|
||||
SourceIPs map[string]bool // 活跃来源IP
|
||||
CpuUsage float64 // CPU使用率(%)
|
||||
DNSSECQueries int64 // DNSSEC查询总数
|
||||
DNSSECSuccess int64 // DNSSEC验证成功数
|
||||
DNSSECFailed int64 // DNSSEC验证失败数
|
||||
DNSSECEnabled bool // 是否启用了DNSSEC
|
||||
}
|
||||
|
||||
// NewServer 创建DNS服务器实例
|
||||
@@ -141,6 +150,7 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
|
||||
resolver: &dns.Client{
|
||||
Net: "udp",
|
||||
Timeout: time.Duration(config.Timeout) * time.Millisecond,
|
||||
UDPSize: 4096, // 增加UDP缓冲区大小,支持更大的DNSSEC响应
|
||||
},
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
@@ -155,6 +165,10 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
|
||||
QueryTypes: make(map[string]int64),
|
||||
SourceIPs: make(map[string]bool),
|
||||
CpuUsage: 0,
|
||||
DNSSECQueries: 0,
|
||||
DNSSECSuccess: 0,
|
||||
DNSSECFailed: 0,
|
||||
DNSSECEnabled: config.EnableDNSSEC,
|
||||
},
|
||||
blockedDomains: 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小时
|
||||
// DNS查询缓存初始化
|
||||
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)
|
||||
w.WriteMsg(response)
|
||||
|
||||
// 计算响应时间
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
// 缓存命中,响应时间设为0ms
|
||||
responseTime := int64(0)
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.TotalResponseTime += responseTime
|
||||
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
|
||||
}
|
||||
|
||||
// 检查hosts文件是否有匹配
|
||||
if ip, exists := s.shieldManager.GetHostsIP(domain); exists {
|
||||
s.handleHostsResponse(w, r, ip)
|
||||
// 计算响应时间
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
// 缓存命中,响应时间设为0ms
|
||||
responseTime := int64(0)
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.TotalResponseTime += responseTime
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 检查缓存中是否有响应(增强版缓存查询)
|
||||
if cachedResponse, found := s.dnsCache.Get(r.Question[0].Name, qType); found {
|
||||
// 检查缓存中是否有响应(优先查找带DNSSEC的缓存项)
|
||||
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.Id = r.Id // 更新ID以匹配请求
|
||||
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)
|
||||
|
||||
// 计算响应时间
|
||||
@@ -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)
|
||||
logger.Debug("从缓存返回DNS响应", "domain", domain, "type", queryType)
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true, cachedDNSSEC, true)
|
||||
logger.Debug("从缓存返回DNS响应", "domain", domain, "type", queryType, "dnssec", cachedDNSSEC)
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存未命中,转发到上游DNS服务器
|
||||
response, _ := s.forwardDNSRequestWithCache(r, domain)
|
||||
response, rtt := s.forwardDNSRequestWithCache(r, domain)
|
||||
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)
|
||||
}
|
||||
|
||||
// 计算响应时间
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
// 使用上游服务器的实际响应时间(转换为毫秒)
|
||||
responseTime := int64(rtt.Milliseconds())
|
||||
// 如果rtt为0(查询失败),则使用本地计算的时间
|
||||
if responseTime == 0 {
|
||||
responseTime = time.Since(startTime).Milliseconds()
|
||||
}
|
||||
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.TotalResponseTime += responseTime
|
||||
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 {
|
||||
// 创建响应副本以避免后续修改影响缓存
|
||||
@@ -426,11 +537,11 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
// 设置合理的TTL,不超过默认的30分钟
|
||||
defaultCacheTTL := 30 * time.Minute
|
||||
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文件匹配的响应
|
||||
@@ -537,22 +648,191 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain
|
||||
// forwardDNSRequestWithCache 转发DNS请求到上游服务器并返回响应
|
||||
func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg, time.Duration) {
|
||||
// 尝试所有上游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 {
|
||||
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
|
||||
|
||||
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.updateStats(func(stats *Stats) {
|
||||
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 更新解析域名统计
|
||||
func (s *Server) updateResolvedDomainStats(domain string) {
|
||||
s.resolvedDomainsMutex.Lock()
|
||||
@@ -643,6 +1081,7 @@ func (s *Server) updateResolvedDomainStats(domain string) {
|
||||
Domain: domain,
|
||||
Count: 1,
|
||||
LastSeen: time.Now(),
|
||||
DNSSEC: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -655,7 +1094,7 @@ func (s *Server) updateStats(update func(*Stats)) {
|
||||
}
|
||||
|
||||
// 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地理位置
|
||||
location := s.getIpGeolocation(clientIP)
|
||||
|
||||
@@ -671,6 +1110,8 @@ func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime in
|
||||
BlockRule: blockRule,
|
||||
BlockType: blockType,
|
||||
FromCache: fromCache,
|
||||
DNSSEC: dnssec,
|
||||
EDNS: edns,
|
||||
}
|
||||
|
||||
// 添加到日志列表
|
||||
@@ -720,6 +1161,10 @@ func (s *Server) GetStats() *Stats {
|
||||
QueryTypes: queryTypesCopy,
|
||||
SourceIPs: sourceIPsCopy,
|
||||
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()
|
||||
if statsData.Stats != nil {
|
||||
s.stats = statsData.Stats
|
||||
// 确保使用当前配置中的EnableDNSSEC值,覆盖从文件加载的值
|
||||
s.stats.DNSSECEnabled = s.config.EnableDNSSEC
|
||||
}
|
||||
s.statsMutex.Unlock()
|
||||
|
||||
|
||||
@@ -219,6 +219,12 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
// 格式化平均响应时间为两位小数
|
||||
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{}{
|
||||
"dns": map[string]interface{}{
|
||||
@@ -232,12 +238,21 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
"QueryTypes": dnsStats.QueryTypes,
|
||||
"SourceIPs": dnsStats.SourceIPs,
|
||||
"CpuUsage": dnsStats.CpuUsage,
|
||||
"DNSSECQueries": dnsStats.DNSSECQueries,
|
||||
"DNSSECSuccess": dnsStats.DNSSECSuccess,
|
||||
"DNSSECFailed": dnsStats.DNSSECFailed,
|
||||
"DNSSECEnabled": dnsStats.DNSSECEnabled,
|
||||
},
|
||||
"shield": shieldStats,
|
||||
"topQueryType": topQueryType,
|
||||
"activeIPs": activeIPCount,
|
||||
"avgResponseTime": formattedResponseTime,
|
||||
"cpuUsage": dnsStats.CpuUsage,
|
||||
"dnssecEnabled": dnsStats.DNSSECEnabled,
|
||||
"dnssecQueries": dnsStats.DNSSECQueries,
|
||||
"dnssecSuccess": dnsStats.DNSSECSuccess,
|
||||
"dnssecFailed": dnsStats.DNSSECFailed,
|
||||
"dnssecUsage": float64(int(dnssecUsage*100)) / 100, // 保留两位小数
|
||||
"time": time.Now(),
|
||||
}
|
||||
|
||||
@@ -355,6 +370,12 @@ func (s *Server) buildStatsData() map[string]interface{} {
|
||||
// 格式化平均响应时间
|
||||
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{}{
|
||||
"dns": map[string]interface{}{
|
||||
"Queries": dnsStats.Queries,
|
||||
@@ -367,12 +388,21 @@ func (s *Server) buildStatsData() map[string]interface{} {
|
||||
"QueryTypes": dnsStats.QueryTypes,
|
||||
"SourceIPs": dnsStats.SourceIPs,
|
||||
"CpuUsage": dnsStats.CpuUsage,
|
||||
"DNSSECQueries": dnsStats.DNSSECQueries,
|
||||
"DNSSECSuccess": dnsStats.DNSSECSuccess,
|
||||
"DNSSECFailed": dnsStats.DNSSECFailed,
|
||||
"DNSSECEnabled": dnsStats.DNSSECEnabled,
|
||||
},
|
||||
"shield": shieldStats,
|
||||
"topQueryType": topQueryType,
|
||||
"activeIPs": activeIPCount,
|
||||
"avgResponseTime": formattedResponseTime,
|
||||
"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)
|
||||
dnssecStatusMap := make(map[string]bool)
|
||||
|
||||
for _, domain := range blockedDomains {
|
||||
domainMap[domain.Domain] += domain.Count
|
||||
dnssecStatusMap[domain.Domain] = domain.DNSSEC
|
||||
}
|
||||
for _, domain := range resolvedDomains {
|
||||
domainMap[domain.Domain] += domain.Count
|
||||
dnssecStatusMap[domain.Domain] = domain.DNSSEC
|
||||
}
|
||||
|
||||
// 转换为切片并排序
|
||||
domainList := make([]map[string]interface{}, 0, len(domainMap))
|
||||
for domain, count := range domainMap {
|
||||
dnssec, hasDNSSEC := dnssecStatusMap[domain]
|
||||
domainList = append(domainList, map[string]interface{}{
|
||||
"domain": domain,
|
||||
"count": count,
|
||||
"dnssec": hasDNSSEC && dnssec,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
8670
server.log
Normal file
8670
server.log
Normal file
File diff suppressed because it is too large
Load Diff
1
server.pid
Normal file
1
server.pid
Normal file
@@ -0,0 +1 @@
|
||||
132708
|
||||
176
start-dns-server.sh
Executable file
176
start-dns-server.sh
Executable 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-server(PID: ${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 "$@"
|
||||
|
||||
@@ -33,6 +33,203 @@
|
||||
}
|
||||
],
|
||||
"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": {
|
||||
"get": {
|
||||
"summary": "获取系统统计信息",
|
||||
@@ -58,7 +255,11 @@
|
||||
"TotalResponseTime": {"type": "number", "description": "总响应时间(毫秒)"},
|
||||
"QueryTypes": {"type": "object", "description": "查询类型统计"},
|
||||
"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统计信息"},
|
||||
@@ -66,6 +267,11 @@
|
||||
"activeIPs": {"type": "integer", "description": "活跃IP数量"},
|
||||
"avgResponseTime": {"type": "number", "description": "平均响应时间(毫秒)"},
|
||||
"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": "统计时间"}
|
||||
}
|
||||
},
|
||||
@@ -82,13 +288,22 @@
|
||||
"TotalResponseTime": 15625,
|
||||
"QueryTypes": {"A": 850, "AAAA": 250, "CNAME": 150},
|
||||
"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": {},
|
||||
"topQueryType": "A",
|
||||
"activeIPs": 2,
|
||||
"avgResponseTime": 12.5,
|
||||
"cpuUsage": 0.15,
|
||||
"dnssecEnabled": true,
|
||||
"dnssecQueries": 500,
|
||||
"dnssecSuccess": 480,
|
||||
"dnssecFailed": 20,
|
||||
"dnssecUsage": 40.0,
|
||||
"time": "2023-07-15T14:30:45Z"
|
||||
}
|
||||
}
|
||||
@@ -274,20 +489,16 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hour": {"type": "string", "description": "小时"},
|
||||
"queries": {"type": "integer", "description": "查询次数"},
|
||||
"blocked": {"type": "integer", "description": "被阻止次数"}
|
||||
}
|
||||
"labels": {"type": "array", "items": {"type": "string"}, "description": "小时标签"},
|
||||
"data": {"type": "array", "items": {"type": "integer"}, "description": "查询次数数据"}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{"hour": "00", "queries": 120, "blocked": 20},
|
||||
{"hour": "01", "queries": 90, "blocked": 15}
|
||||
]
|
||||
"example": {
|
||||
"labels": ["15:00", "16:00", "17:00"],
|
||||
"data": [120, 90, 150]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,20 +516,16 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {"type": "string", "description": "日期"},
|
||||
"queries": {"type": "integer", "description": "查询次数"},
|
||||
"blocked": {"type": "integer", "description": "被阻止次数"}
|
||||
}
|
||||
"labels": {"type": "array", "items": {"type": "string"}, "description": "日期标签"},
|
||||
"data": {"type": "array", "items": {"type": "integer"}, "description": "查询次数数据"}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{"date": "2023-07-09", "queries": 2500, "blocked": 450},
|
||||
{"date": "2023-07-10", "queries": 2700, "blocked": 480}
|
||||
]
|
||||
"example": {
|
||||
"labels": ["01-02", "01-03", "01-04"],
|
||||
"data": [2500, 2700, 2300]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,20 +543,16 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {"type": "string", "description": "日期"},
|
||||
"queries": {"type": "integer", "description": "查询次数"},
|
||||
"blocked": {"type": "integer", "description": "被阻止次数"}
|
||||
}
|
||||
"labels": {"type": "array", "items": {"type": "string"}, "description": "日期标签"},
|
||||
"data": {"type": "array", "items": {"type": "integer"}, "description": "查询次数数据"}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{"date": "2023-06-15", "queries": 2500, "blocked": 450},
|
||||
{"date": "2023-06-16", "queries": 2700, "blocked": 480}
|
||||
]
|
||||
"example": {
|
||||
"labels": ["01-01", "01-02", "01-03"],
|
||||
"data": [2500, 2700, 2300]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,30 +590,157 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/shield": {
|
||||
"/logs/stats": {
|
||||
"get": {
|
||||
"summary": "获取Shield配置",
|
||||
"description": "获取Shield的完整配置信息,包括启用状态、规则等。",
|
||||
"tags": ["shield"],
|
||||
"summary": "获取日志统计信息",
|
||||
"description": "获取DNS查询日志的统计信息。",
|
||||
"tags": ["logs"],
|
||||
"responses": {
|
||||
"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": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "description": "是否启用Shield"},
|
||||
"rulesCount": {"type": "integer", "description": "规则数量"},
|
||||
"lastUpdate": {"type": "string", "description": "最后更新时间"},
|
||||
"blacklists": {"type": "array", "description": "黑名单列表", "items": {"type": "object"}}
|
||||
"count": {"type": "integer", "description": "日志总数"}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"enabled": true,
|
||||
"rulesCount": 5000,
|
||||
"lastUpdate": "2023-07-15T10:00:00Z",
|
||||
"blacklists": []
|
||||
"count": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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": {
|
||||
"summary": "更新Shield配置",
|
||||
"description": "更新Shield的全局配置信息。",
|
||||
"summary": "添加屏蔽规则",
|
||||
"description": "添加新的屏蔽规则到Shield。",
|
||||
"tags": ["shield"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -442,20 +772,18 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "description": "是否启用Shield"},
|
||||
"updateInterval": {"type": "integer", "description": "更新间隔(秒)"}
|
||||
"rule": {"type": "string", "description": "屏蔽规则"}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"enabled": true,
|
||||
"updateInterval": 3600
|
||||
"rule": "example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功更新配置",
|
||||
"description": "成功添加规则",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -492,19 +820,35 @@
|
||||
"error": {"type": "string", "description": "错误信息"}
|
||||
}
|
||||
},
|
||||
"example": {"error": "更新配置失败"}
|
||||
"example": {"error": "添加规则失败"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"summary": "重启Shield",
|
||||
"description": "重新加载和应用Shield规则。",
|
||||
"delete": {
|
||||
"summary": "删除屏蔽规则",
|
||||
"description": "从Shield中删除指定的屏蔽规则。",
|
||||
"tags": ["shield"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rule": {"type": "string", "description": "要删除的屏蔽规则"}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"rule": "example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功重启Shield",
|
||||
"description": "成功删除规则",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -517,6 +861,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {"type": "string", "description": "错误信息"}
|
||||
}
|
||||
},
|
||||
"example": {"error": "参数格式错误"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"content": {
|
||||
@@ -527,7 +885,46 @@
|
||||
"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": {
|
||||
"get": {
|
||||
"summary": "获取服务器状态",
|
||||
"description": "获取DNS服务器的状态信息。",
|
||||
"description": "获取DNS服务器的状态信息,包括查询统计、运行时间等。",
|
||||
"tags": ["server"],
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -1150,14 +1547,32 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {"type": "string", "description": "服务器状态"},
|
||||
"uptime": {"type": "integer", "description": "运行时间(秒)"},
|
||||
"version": {"type": "string", "description": "服务器版本"}
|
||||
"queries": {"type": "integer", "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": {
|
||||
"status": "running",
|
||||
"uptime": 3600,
|
||||
"version": "1.0.0"
|
||||
"queries": 1250,
|
||||
"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": {
|
||||
"get": {
|
||||
"summary": "获取服务器配置",
|
||||
"description": "获取DNS服务器的配置信息。",
|
||||
"description": "获取DNS服务器的配置信息,包括Shield配置。",
|
||||
"tags": ["server"],
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -1178,13 +1593,30 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"port": {"type": "integer", "description": "服务器端口"},
|
||||
"logLevel": {"type": "string", "description": "日志级别"}
|
||||
"shield": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"blockMethod": {"type": "string", "description": "屏蔽方法"},
|
||||
"customBlockIP": {"type": "string", "description": "自定义屏蔽IP"},
|
||||
"blacklists": {"type": "array", "description": "黑名单列表", "items": {"type": "object"}},
|
||||
"updateInterval": {"type": "integer", "description": "更新间隔(秒)"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"port": 53,
|
||||
"logLevel": "info"
|
||||
"shield": {
|
||||
"blockMethod": "NXDOMAIN",
|
||||
"customBlockIP": "",
|
||||
"blacklists": [
|
||||
{
|
||||
"name": "AdGuard DNS filter",
|
||||
"url": "https://example.com/ads.txt",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"updateInterval": 3600
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1193,7 +1625,7 @@
|
||||
},
|
||||
"post": {
|
||||
"summary": "更新服务器配置",
|
||||
"description": "更新DNS服务器的配置信息。",
|
||||
"description": "更新DNS服务器的配置信息,包括Shield配置。",
|
||||
"tags": ["server"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -1202,13 +1634,30 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"port": {"type": "integer", "description": "服务器端口"},
|
||||
"logLevel": {"type": "string", "description": "日志级别"}
|
||||
"shield": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"blockMethod": {"type": "string", "description": "屏蔽方法"},
|
||||
"customBlockIP": {"type": "string", "description": "自定义屏蔽IP"},
|
||||
"blacklists": {"type": "array", "description": "黑名单列表", "items": {"type": "object"}},
|
||||
"updateInterval": {"type": "integer", "description": "更新间隔(秒)"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"port": 53,
|
||||
"logLevel": "info"
|
||||
"shield": {
|
||||
"blockMethod": "NXDOMAIN",
|
||||
"customBlockIP": "",
|
||||
"blacklists": [
|
||||
{
|
||||
"name": "AdGuard DNS filter",
|
||||
"url": "https://example.com/ads.txt",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"updateInterval": 3600
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1221,10 +1670,14 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"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": "错误信息"}
|
||||
}
|
||||
},
|
||||
"example": {"error": "参数格式错误"}
|
||||
"example": {
|
||||
"error": "无效的请求体"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1252,7 +1707,9 @@
|
||||
"error": {"type": "string", "description": "错误信息"}
|
||||
}
|
||||
},
|
||||
"example": {"error": "更新配置失败"}
|
||||
"example": {
|
||||
"error": "保存配置失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1298,6 +1755,10 @@
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "auth",
|
||||
"description": "认证相关API"
|
||||
},
|
||||
{
|
||||
"name": "stats",
|
||||
"description": "统计相关API"
|
||||
@@ -1309,6 +1770,10 @@
|
||||
{
|
||||
"name": "server",
|
||||
"description": "服务器相关API"
|
||||
},
|
||||
{
|
||||
"name": "logs",
|
||||
"description": "日志相关API"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -334,6 +334,42 @@
|
||||
</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>
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ let dashboardWsReconnectTimer = null;
|
||||
let statCardCharts = {};
|
||||
// 存储统计卡片历史数据
|
||||
let statCardHistoryData = {};
|
||||
// 存储仪表盘历史数据,用于计算趋势
|
||||
window.dashboardHistoryData = window.dashboardHistoryData || {
|
||||
prevResponseTime: null,
|
||||
prevActiveIPs: null,
|
||||
prevTopQueryTypeCount: null
|
||||
};
|
||||
|
||||
// 引入颜色配置文件
|
||||
const COLOR_CONFIG = window.COLOR_CONFIG || {};
|
||||
@@ -192,14 +198,42 @@ function processRealTimeData(stats) {
|
||||
|
||||
if (document.getElementById('top-query-type')) {
|
||||
const queryType = stats.topQueryType || '---';
|
||||
document.getElementById('top-query-type').textContent = queryType;
|
||||
|
||||
const queryPercentElem = document.getElementById('query-type-percentage');
|
||||
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')) {
|
||||
@@ -984,6 +1018,60 @@ function updateStatsCards(stats) {
|
||||
animateValue('error-queries', errorQueries);
|
||||
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 queryTypePercentageElement = document.getElementById('query-type-percentage');
|
||||
@@ -1223,7 +1311,7 @@ function updateTopDomainsTable(domains) {
|
||||
<div class="flex-1 min-w-0">
|
||||
<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="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>
|
||||
<span class="ml-4 flex-shrink-0 font-semibold text-success">${formatNumber(domain.count)}</span>
|
||||
|
||||
@@ -399,7 +399,7 @@ function updateLogsTable(logs) {
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<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 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>
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user