增加查询日志详情界面点击域名列表,显示解析日志的详细信息.
This commit is contained in:
20
CHANGELOG.md
20
CHANGELOG.md
@@ -2,6 +2,24 @@
|
||||
|
||||
所有对本项目的显著更改都将记录在此文件中。
|
||||
|
||||
## [1.2.2] - 2025-12-25
|
||||
|
||||
### 新增
|
||||
- 增加查询日志详情界面点击域名列表,显示解析日志的详细信息。
|
||||
- 增加DNSSEC上游服务器的配置项。
|
||||
|
||||
### 修复
|
||||
- web界面系统设置加载后不获取数据和保存配置不生效的问题。
|
||||
|
||||
|
||||
## [1.2.1] - 2025-12-25
|
||||
|
||||
### 改进
|
||||
- 增加IPv6支持配置项,默认关闭
|
||||
### 修复
|
||||
- 修复了DNS查询超时设置过短导致的"Server failed"错误
|
||||
- 将默认DNS请求超时时间从5毫秒调整为1000毫秒
|
||||
|
||||
## [1.2.0] - 2025-12-24
|
||||
|
||||
### 添加
|
||||
@@ -64,4 +82,4 @@
|
||||
|
||||
本CHANGELOG遵循[Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)格式。
|
||||
|
||||
版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)规范。
|
||||
版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)规范。
|
||||
|
||||
46
config.json
46
config.json
@@ -2,37 +2,27 @@
|
||||
"dns": {
|
||||
"port": 53,
|
||||
"upstreamDNS": [
|
||||
"223.5.5.5:53"
|
||||
"223.5.5.5"
|
||||
],
|
||||
"dnssecUpstreamDNS": [
|
||||
"117.50.10.10:53",
|
||||
"101.226.4.6:53",
|
||||
"218.30.118.6:53",
|
||||
"208.67.220.220:53",
|
||||
"208.67.222.222:53"
|
||||
"117.50.10.10",
|
||||
"101.226.4.6",
|
||||
"218.30.118.6",
|
||||
"208.67.220.220",
|
||||
"208.67.222.222"
|
||||
],
|
||||
"timeout": 5000,
|
||||
"statsFile": "data/stats.json",
|
||||
"saveInterval": 300,
|
||||
"timeout": 5,
|
||||
"saveInterval": 30,
|
||||
"cacheTTL": 10,
|
||||
"enableDNSSEC": true,
|
||||
"queryMode": "loadbalance",
|
||||
"queryMode": "parallel",
|
||||
"domainSpecificDNS": {
|
||||
"addr.arpa": [
|
||||
"10.35.10.200:53"
|
||||
],
|
||||
"akadns": [
|
||||
"4.2.2.1:53"
|
||||
],
|
||||
"akamai": [
|
||||
"4.2.2.1:53"
|
||||
],
|
||||
"amazehome.cn": [
|
||||
"10.35.10.200:53"
|
||||
],
|
||||
"amazehome.xyz": [
|
||||
"10.35.10.200:53"
|
||||
],
|
||||
"microsoft.com": [
|
||||
"4.2.2.1:53"
|
||||
],
|
||||
@@ -41,11 +31,10 @@
|
||||
]
|
||||
},
|
||||
"noDNSSECDomains": [
|
||||
"amazehome.cn",
|
||||
"addr.arpa",
|
||||
"amazehome.xyz",
|
||||
".cn"
|
||||
]
|
||||
],
|
||||
"enableIPv6": false
|
||||
},
|
||||
"http": {
|
||||
"port": 8080,
|
||||
@@ -55,7 +44,6 @@
|
||||
"password": "admin"
|
||||
},
|
||||
"shield": {
|
||||
"localRulesFile": "data/rules.txt",
|
||||
"blacklists": [
|
||||
{
|
||||
"name": "AdGuard DNS filter",
|
||||
@@ -79,7 +67,7 @@
|
||||
"name": "My GitHub Rules",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
|
||||
"enabled": true,
|
||||
"lastUpdateTime": "2025-11-29T17:05:40.283Z"
|
||||
"lastUpdateTime": "2025-12-24T07:11:16.596Z"
|
||||
},
|
||||
{
|
||||
"name": "CNList",
|
||||
@@ -117,7 +105,7 @@
|
||||
"name": "My Gitlab A/T Rules",
|
||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt",
|
||||
"enabled": true,
|
||||
"lastUpdateTime": "2025-12-18T10:38:42.344Z"
|
||||
"lastUpdateTime": "2025-12-24T07:11:07.334Z"
|
||||
},
|
||||
{
|
||||
"name": "My Gitlab Malware List",
|
||||
@@ -141,18 +129,14 @@
|
||||
}
|
||||
],
|
||||
"updateInterval": 3600,
|
||||
"hostsFile": "data/hosts.txt",
|
||||
"blockMethod": "NXDOMAIN",
|
||||
"customBlockIP": "",
|
||||
"statsFile": "./data/shield_stats.json",
|
||||
"statsSaveInterval": 60,
|
||||
"remoteRulesCacheDir": "data/remote_rules"
|
||||
"statsSaveInterval": 60
|
||||
},
|
||||
"log": {
|
||||
"file": "logs/dns-server.log",
|
||||
"level": "debug",
|
||||
"maxSize": 100,
|
||||
"maxBackups": 10,
|
||||
"maxAge": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@ type DNSConfig struct {
|
||||
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支持
|
||||
QueryMode string `json:"queryMode"` // 查询模式:"loadbalance"(负载均衡)、"parallel"(并行请求)、"fastest-ip"(最快的IP地址)
|
||||
DomainSpecificDNS DomainSpecificDNS `json:"domainSpecificDNS"` // 域名特定DNS服务器配置
|
||||
NoDNSSECDomains []string `json:"noDNSSECDomains"` // 不验证DNSSEC的域名模式列表
|
||||
EnableIPv6 bool `json:"enableIPv6"` // 是否启用IPv6解析(AAAA记录)
|
||||
}
|
||||
|
||||
// HTTPConfig HTTP控制台配置
|
||||
@@ -47,20 +47,15 @@ type BlacklistEntry struct {
|
||||
|
||||
// ShieldConfig 屏蔽规则配置
|
||||
type ShieldConfig struct {
|
||||
LocalRulesFile string `json:"localRulesFile"`
|
||||
Blacklists []BlacklistEntry `json:"blacklists"`
|
||||
UpdateInterval int `json:"updateInterval"`
|
||||
HostsFile string `json:"hostsFile"`
|
||||
BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP"
|
||||
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用
|
||||
StatsFile string `json:"statsFile"` // 计数数据持久化文件
|
||||
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
|
||||
RemoteRulesCacheDir string `json:"remoteRulesCacheDir"` // 远程规则缓存目录
|
||||
Blacklists []BlacklistEntry `json:"blacklists"`
|
||||
UpdateInterval int `json:"updateInterval"`
|
||||
BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP"
|
||||
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用
|
||||
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
|
||||
}
|
||||
|
||||
// LogConfig 日志配置
|
||||
type LogConfig struct {
|
||||
File string `json:"file"`
|
||||
Level string `json:"level"`
|
||||
MaxSize int `json:"maxSize"`
|
||||
MaxBackups int `json:"maxBackups"`
|
||||
@@ -98,9 +93,6 @@ func LoadConfig(path string) (*Config, error) {
|
||||
if len(config.DNS.UpstreamDNS) == 0 {
|
||||
config.DNS.UpstreamDNS = []string{"223.5.5.5:53", "223.6.6.6:53"}
|
||||
}
|
||||
if config.DNS.StatsFile == "" {
|
||||
config.DNS.StatsFile = "./data/stats.json" // 默认统计数据文件路径
|
||||
}
|
||||
if config.DNS.SaveInterval == 0 {
|
||||
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
|
||||
}
|
||||
@@ -110,6 +102,8 @@ func LoadConfig(path string) (*Config, error) {
|
||||
}
|
||||
// DNSSEC默认配置
|
||||
config.DNS.EnableDNSSEC = true // 默认启用DNSSEC支持
|
||||
// IPv6默认配置
|
||||
config.DNS.EnableIPv6 = true // 默认启用IPv6解析
|
||||
// DNSSEC专用服务器默认配置
|
||||
if len(config.DNS.DNSSECUpstreamDNS) == 0 {
|
||||
config.DNS.DNSSECUpstreamDNS = []string{"8.8.8.8:53", "1.1.1.1:53"}
|
||||
@@ -142,15 +136,9 @@ func LoadConfig(path string) (*Config, error) {
|
||||
if config.Shield.BlockMethod == "" {
|
||||
config.Shield.BlockMethod = "NXDOMAIN" // 默认屏蔽方法为NXDOMAIN
|
||||
}
|
||||
if config.Shield.StatsFile == "" {
|
||||
config.Shield.StatsFile = "./data/shield_stats.json" // 默认Shield统计数据文件路径
|
||||
}
|
||||
if config.Shield.StatsSaveInterval == 0 {
|
||||
config.Shield.StatsSaveInterval = 300 // 默认5分钟保存一次
|
||||
}
|
||||
if config.Shield.RemoteRulesCacheDir == "" {
|
||||
config.Shield.RemoteRulesCacheDir = "./data/remote_rules" // 默认远程规则缓存目录
|
||||
}
|
||||
|
||||
// 如果黑名单列表为空,添加一些默认的黑名单
|
||||
if len(config.Shield.Blacklists) == 0 {
|
||||
|
||||
209
dns/server.go
209
dns/server.go
@@ -45,22 +45,30 @@ type IPGeolocation struct {
|
||||
Expiry time.Time `json:"expiry"` // 缓存过期时间
|
||||
}
|
||||
|
||||
// DNSAnswer DNS解析记录
|
||||
type DNSAnswer struct {
|
||||
Type string `json:"type"` // 记录类型
|
||||
Value string `json:"value"` // 记录值
|
||||
TTL uint32 `json:"ttl"` // 生存时间
|
||||
}
|
||||
|
||||
// QueryLog 查询日志记录
|
||||
type QueryLog struct {
|
||||
Timestamp time.Time // 查询时间
|
||||
ClientIP string // 客户端IP
|
||||
Location string // IP地理位置(国家 城市)
|
||||
Domain string // 查询域名
|
||||
QueryType string // 查询类型
|
||||
ResponseTime int64 // 响应时间(ms)
|
||||
Result string // 查询结果(allowed, blocked, error)
|
||||
BlockRule string // 屏蔽规则(如果被屏蔽)
|
||||
BlockType string // 屏蔽类型(如果被屏蔽)
|
||||
FromCache bool // 是否来自缓存
|
||||
DNSSEC bool // 是否使用了DNSSEC
|
||||
EDNS bool // 是否使用了EDNS
|
||||
DNSServer string // 使用的DNS服务器
|
||||
DNSSECServer string // 使用的DNSSEC专用服务器
|
||||
Timestamp time.Time // 查询时间
|
||||
ClientIP string // 客户端IP
|
||||
Location string // IP地理位置(国家 城市)
|
||||
Domain string // 查询域名
|
||||
QueryType string // 查询类型
|
||||
ResponseTime int64 // 响应时间(ms)
|
||||
Result string // 查询结果(allowed, blocked, error)
|
||||
BlockRule string // 屏蔽规则(如果被屏蔽)
|
||||
BlockType string // 屏蔽类型(如果被屏蔽)
|
||||
FromCache bool // 是否来自缓存
|
||||
DNSSEC bool // 是否使用了DNSSEC
|
||||
EDNS bool // 是否使用了EDNS
|
||||
DNSServer string // 使用的DNS服务器
|
||||
DNSSECServer string // 使用的DNSSEC专用服务器
|
||||
Answers []DNSAnswer `json:"answers"` // 解析记录
|
||||
}
|
||||
|
||||
// StatsData 用于持久化的统计数据结构
|
||||
@@ -348,6 +356,29 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.QueryTypes[queryType]++
|
||||
})
|
||||
|
||||
// 检查是否是AAAA记录查询且IPv6解析已禁用
|
||||
if qType == dns.TypeAAAA && !s.config.EnableIPv6 {
|
||||
// 返回NXDOMAIN响应(域名不存在)
|
||||
response := new(dns.Msg)
|
||||
response.SetReply(r)
|
||||
response.SetRcode(r, dns.RcodeNameError)
|
||||
w.WriteMsg(response)
|
||||
|
||||
// 更新统计信息
|
||||
responseTime := int64(0)
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.TotalResponseTime += responseTime
|
||||
if stats.Queries > 0 {
|
||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加查询日志
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "error", "", "", false, false, true, "", "", nil)
|
||||
logger.Debug("IPv6解析已禁用,拒绝AAAA记录查询", "domain", domain)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("接收到DNS查询", "domain", domain, "type", queryType, "client", w.RemoteAddr())
|
||||
@@ -370,7 +401,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
})
|
||||
|
||||
// 添加查询日志
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "error", "", "", false, false, true, "", "")
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "error", "", "", false, false, true, "", "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -386,8 +417,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
}
|
||||
})
|
||||
|
||||
// 添加查询日志
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, false, true, "缓存", "无")
|
||||
// 该方法内部未直接调用addQueryLog,而是在handleDNSRequest中处理
|
||||
return
|
||||
}
|
||||
|
||||
@@ -409,7 +439,8 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
})
|
||||
|
||||
// 添加查询日志
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false, false, true, "无", "无")
|
||||
blockedAnswers := []DNSAnswer{}
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false, false, true, "无", "无", blockedAnswers)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -481,8 +512,20 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
})
|
||||
}
|
||||
|
||||
// 从缓存响应中提取解析记录
|
||||
cachedAnswers := []DNSAnswer{}
|
||||
if cachedResponse != nil {
|
||||
for _, rr := range cachedResponse.Answer {
|
||||
cachedAnswers = append(cachedAnswers, DNSAnswer{
|
||||
Type: dns.TypeToString[rr.Header().Rrtype],
|
||||
Value: rr.String(),
|
||||
TTL: rr.Header().Ttl,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 添加查询日志 - 标记为缓存
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true, cachedDNSSEC, true, "缓存", "无")
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true, cachedDNSSEC, true, "缓存", "无", cachedAnswers)
|
||||
logger.Debug("从缓存返回DNS响应", "domain", domain, "type", queryType, "dnssec", cachedDNSSEC)
|
||||
return
|
||||
}
|
||||
@@ -566,8 +609,20 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
logger.Debug("DNS响应已缓存", "domain", domain, "type", queryType, "ttl", defaultCacheTTL, "dnssec", responseDNSSEC)
|
||||
}
|
||||
|
||||
// 从响应中提取解析记录
|
||||
responseAnswers := []DNSAnswer{}
|
||||
if response != nil {
|
||||
for _, rr := range response.Answer {
|
||||
responseAnswers = append(responseAnswers, DNSAnswer{
|
||||
Type: dns.TypeToString[rr.Header().Rrtype],
|
||||
Value: rr.String(),
|
||||
TTL: rr.Header().Ttl,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 添加查询日志 - 标记为实时
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, responseDNSSEC, true, dnsServer, dnssecServer)
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, responseDNSSEC, true, dnsServer, dnssecServer, responseAnswers)
|
||||
}
|
||||
|
||||
// handleHostsResponse 处理hosts文件匹配的响应
|
||||
@@ -731,14 +786,33 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
||||
|
||||
// 2. 如果没有匹配的域名特定配置
|
||||
if !domainMatched {
|
||||
// 如果启用了DNSSEC且有配置DNSSEC专用服务器,并且域名不匹配NoDNSSECDomains,则使用DNSSEC专用服务器
|
||||
// 创建一个新的切片来存储最终的上游服务器列表
|
||||
var finalUpstreamDNS []string
|
||||
|
||||
// 首先添加用户配置的上游DNS服务器
|
||||
finalUpstreamDNS = append(finalUpstreamDNS, s.config.UpstreamDNS...)
|
||||
logger.Debug("使用用户配置的上游DNS服务器", "servers", finalUpstreamDNS)
|
||||
|
||||
// 如果启用了DNSSEC且有配置DNSSEC专用服务器,并且域名不匹配NoDNSSECDomains,则将DNSSEC专用服务器添加到列表中
|
||||
if s.config.EnableDNSSEC && len(s.config.DNSSECUpstreamDNS) > 0 && !noDNSSEC {
|
||||
selectedUpstreamDNS = s.config.DNSSECUpstreamDNS
|
||||
logger.Debug("使用DNSSEC专用服务器", "servers", selectedUpstreamDNS)
|
||||
} else {
|
||||
// 否则使用默认的上游DNS服务器
|
||||
selectedUpstreamDNS = s.config.UpstreamDNS
|
||||
// 合并DNSSEC专用服务器到上游服务器列表,避免重复
|
||||
for _, dnssecServer := range s.config.DNSSECUpstreamDNS {
|
||||
hasDuplicate := false
|
||||
for _, upstream := range finalUpstreamDNS {
|
||||
if upstream == dnssecServer {
|
||||
hasDuplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasDuplicate {
|
||||
finalUpstreamDNS = append(finalUpstreamDNS, dnssecServer)
|
||||
}
|
||||
}
|
||||
logger.Debug("合并DNSSEC专用服务器到上游服务器列表", "servers", finalUpstreamDNS)
|
||||
}
|
||||
|
||||
// 使用最终合并后的服务器列表
|
||||
selectedUpstreamDNS = finalUpstreamDNS
|
||||
}
|
||||
|
||||
// 1. 首先尝试所有配置的上游DNS服务器
|
||||
@@ -837,8 +911,25 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
||||
|
||||
if resp.response.Rcode == dns.RcodeSuccess {
|
||||
// 处理成功响应
|
||||
// 优先选择带有DNSSEC记录的响应
|
||||
if containsDNSSEC {
|
||||
// 检查当前服务器是否是用户配置的上游DNS服务器(优先使用用户配置的服务器)
|
||||
isUserUpstream := false
|
||||
for _, userServer := range s.config.UpstreamDNS {
|
||||
if userServer == resp.server {
|
||||
isUserUpstream = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 优先选择用户配置的上游DNS服务器的响应
|
||||
if isUserUpstream {
|
||||
bestResponse = resp.response
|
||||
bestRtt = resp.rtt
|
||||
hasBestResponse = true
|
||||
hasDNSSECResponse = containsDNSSEC
|
||||
usedDNSServer = resp.server
|
||||
logger.Debug("使用用户配置的上游服务器响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
|
||||
} else if containsDNSSEC {
|
||||
// 如果不是用户配置的服务器,优先选择带有DNSSEC记录的响应
|
||||
bestResponse = resp.response
|
||||
bestRtt = resp.rtt
|
||||
hasBestResponse = true
|
||||
@@ -882,9 +973,32 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
||||
|
||||
case "loadbalance":
|
||||
// 负载均衡模式 - 使用加权随机选择算法
|
||||
// 1. 选择一个加权随机服务器
|
||||
selectedServer := s.selectWeightedRandomServer(selectedUpstreamDNS)
|
||||
if selectedServer != "" {
|
||||
// 1. 尝试所有可用的服务器,直到找到一个能正常工作的
|
||||
var triedServers []string
|
||||
for len(triedServers) < len(selectedUpstreamDNS) {
|
||||
// 从剩余的服务器中选择一个加权随机服务器
|
||||
var availableServers []string
|
||||
for _, server := range selectedUpstreamDNS {
|
||||
found := false
|
||||
for _, tried := range triedServers {
|
||||
if server == tried {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
availableServers = append(availableServers, server)
|
||||
}
|
||||
}
|
||||
|
||||
selectedServer := s.selectWeightedRandomServer(availableServers)
|
||||
if selectedServer == "" {
|
||||
break
|
||||
}
|
||||
|
||||
triedServers = append(triedServers, selectedServer)
|
||||
logger.Debug("在负载均衡模式下选择服务器", "domain", domain, "server", selectedServer, "triedServers", triedServers)
|
||||
|
||||
// 设置超时上下文
|
||||
timeoutCtx, cancel := context.WithTimeout(s.ctx, time.Duration(s.config.Timeout)*time.Millisecond)
|
||||
defer cancel()
|
||||
@@ -997,10 +1111,12 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
||||
backupRtt = rtt
|
||||
hasBackup = true
|
||||
}
|
||||
break // 找到有效响应,退出循环
|
||||
}
|
||||
} else {
|
||||
// 更新服务器统计信息(失败)
|
||||
s.updateServerStats(selectedServer, false, 0)
|
||||
logger.Debug("服务器请求失败,尝试下一个", "domain", domain, "server", selectedServer, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1967,7 +2083,7 @@ func (s *Server) updateStats(update func(*Stats)) {
|
||||
}
|
||||
|
||||
// addQueryLog 添加查询日志
|
||||
func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string, fromCache, dnssec, edns bool, dnsServer, dnssecServer string) {
|
||||
func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string, fromCache, dnssec, edns bool, dnsServer, dnssecServer string, answers []DNSAnswer) {
|
||||
// 获取IP地理位置
|
||||
location := s.getIpGeolocation(clientIP)
|
||||
|
||||
@@ -1987,6 +2103,7 @@ func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime in
|
||||
EDNS: edns,
|
||||
DNSServer: dnsServer,
|
||||
DNSSECServer: dnssecServer,
|
||||
Answers: answers,
|
||||
}
|
||||
|
||||
// 添加到日志列表
|
||||
@@ -2441,12 +2558,8 @@ func (s *Server) fetchIpGeolocationFromAPI(ip string) (map[string]interface{}, e
|
||||
|
||||
// loadStatsData 从文件加载统计数据
|
||||
func (s *Server) loadStatsData() {
|
||||
if s.config.StatsFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
data, err := ioutil.ReadFile(s.config.StatsFile)
|
||||
data, err := ioutil.ReadFile("data/stats.json")
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logger.Error("读取统计数据文件失败", "error", err)
|
||||
@@ -2515,14 +2628,10 @@ func (s *Server) loadStatsData() {
|
||||
|
||||
// loadQueryLogs 从文件加载查询日志
|
||||
func (s *Server) loadQueryLogs() {
|
||||
if s.config.StatsFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取绝对路径
|
||||
statsFilePath, err := filepath.Abs(s.config.StatsFile)
|
||||
statsFilePath, err := filepath.Abs("data/stats.json")
|
||||
if err != nil {
|
||||
logger.Error("获取统计文件绝对路径失败", "path", s.config.StatsFile, "error", err)
|
||||
logger.Error("获取统计文件绝对路径失败", "path", "data/stats.json", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2564,14 +2673,10 @@ func (s *Server) loadQueryLogs() {
|
||||
|
||||
// saveStatsData 保存统计数据到文件
|
||||
func (s *Server) saveStatsData() {
|
||||
if s.config.StatsFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取绝对路径以避免工作目录问题
|
||||
statsFilePath, err := filepath.Abs(s.config.StatsFile)
|
||||
statsFilePath, err := filepath.Abs("data/stats.json")
|
||||
if err != nil {
|
||||
logger.Error("获取统计文件绝对路径失败", "path", s.config.StatsFile, "error", err)
|
||||
logger.Error("获取统计文件绝对路径失败", "path", "data/stats.json", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2754,15 +2859,15 @@ func getSystemCpuUsage(prevIdle, prevTotal *uint64) (float64, error) {
|
||||
|
||||
// startAutoSave 启动自动保存功能
|
||||
func (s *Server) startAutoSave() {
|
||||
if s.config.StatsFile == "" || s.config.SaveInterval <= 0 {
|
||||
if s.config.SaveInterval <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置定时器
|
||||
// 初始化定时器
|
||||
s.saveTicker = time.NewTicker(time.Duration(s.config.SaveInterval) * time.Second)
|
||||
defer s.saveTicker.Stop()
|
||||
|
||||
logger.Info("启动统计数据自动保存功能", "interval", s.config.SaveInterval, "file", s.config.StatsFile)
|
||||
logger.Info("启动统计数据自动保存功能", "interval", s.config.SaveInterval, "file", "data/stats.json")
|
||||
|
||||
// 定期保存数据
|
||||
for {
|
||||
|
||||
@@ -751,7 +751,7 @@ 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
|
||||
@@ -1233,18 +1233,40 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||
case http.MethodGet:
|
||||
// 返回当前配置(包括黑名单配置)
|
||||
config := map[string]interface{}{
|
||||
"shield": map[string]interface{}{
|
||||
"Shield": map[string]interface{}{
|
||||
"blockMethod": s.globalConfig.Shield.BlockMethod,
|
||||
"customBlockIP": s.globalConfig.Shield.CustomBlockIP,
|
||||
"blacklists": s.globalConfig.Shield.Blacklists,
|
||||
"updateInterval": s.globalConfig.Shield.UpdateInterval,
|
||||
},
|
||||
"DNSServer": map[string]interface{}{
|
||||
"port": s.globalConfig.DNS.Port,
|
||||
"UpstreamServers": s.globalConfig.DNS.UpstreamDNS,
|
||||
"DNSSECUpstreamServers": s.globalConfig.DNS.DNSSECUpstreamDNS,
|
||||
"timeout": s.globalConfig.DNS.Timeout,
|
||||
"saveInterval": s.globalConfig.DNS.SaveInterval,
|
||||
"enableIPv6": s.globalConfig.DNS.EnableIPv6,
|
||||
},
|
||||
"HTTPServer": map[string]interface{}{
|
||||
"port": s.globalConfig.HTTP.Port,
|
||||
},
|
||||
}
|
||||
json.NewEncoder(w).Encode(config)
|
||||
|
||||
case http.MethodPost:
|
||||
// 更新配置
|
||||
var req struct {
|
||||
DNSServer struct {
|
||||
Port int `json:"port"`
|
||||
UpstreamServers []string `json:"upstreamServers"`
|
||||
DnssecUpstreamServers []string `json:"dnssecUpstreamServers"`
|
||||
Timeout int `json:"timeout"`
|
||||
SaveInterval int `json:"saveInterval"`
|
||||
EnableIPv6 bool `json:"enableIPv6"`
|
||||
} `json:"dnsserver"`
|
||||
HTTPServer struct {
|
||||
Port int `json:"port"`
|
||||
} `json:"httpserver"`
|
||||
Shield struct {
|
||||
BlockMethod string `json:"blockMethod"`
|
||||
CustomBlockIP string `json:"customBlockIP"`
|
||||
@@ -1258,6 +1280,29 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新DNS配置
|
||||
if req.DNSServer.Port > 0 {
|
||||
s.globalConfig.DNS.Port = req.DNSServer.Port
|
||||
}
|
||||
if len(req.DNSServer.UpstreamServers) > 0 {
|
||||
s.globalConfig.DNS.UpstreamDNS = req.DNSServer.UpstreamServers
|
||||
}
|
||||
if len(req.DNSServer.DnssecUpstreamServers) > 0 {
|
||||
s.globalConfig.DNS.DNSSECUpstreamDNS = req.DNSServer.DnssecUpstreamServers
|
||||
}
|
||||
if req.DNSServer.Timeout > 0 {
|
||||
s.globalConfig.DNS.Timeout = req.DNSServer.Timeout
|
||||
}
|
||||
if req.DNSServer.SaveInterval > 0 {
|
||||
s.globalConfig.DNS.SaveInterval = req.DNSServer.SaveInterval
|
||||
}
|
||||
s.globalConfig.DNS.EnableIPv6 = req.DNSServer.EnableIPv6
|
||||
|
||||
// 更新HTTP配置
|
||||
if req.HTTPServer.Port > 0 {
|
||||
s.globalConfig.HTTP.Port = req.HTTPServer.Port
|
||||
}
|
||||
|
||||
// 更新屏蔽配置
|
||||
if req.Shield.BlockMethod != "" {
|
||||
// 验证屏蔽方法是否有效
|
||||
|
||||
30
main.go
30
main.go
@@ -42,7 +42,6 @@ func createDefaultConfig(configFile string) error {
|
||||
"1.1.1.1:53"
|
||||
],
|
||||
"timeout": 5000,
|
||||
"statsFile": "./data/stats.json",
|
||||
"saveInterval": 300,
|
||||
"cacheTTL": 30,
|
||||
"enableDNSSEC": true,
|
||||
@@ -56,7 +55,6 @@ func createDefaultConfig(configFile string) error {
|
||||
"password": "admin"
|
||||
},
|
||||
"shield": {
|
||||
"localRulesFile": "data/rules.txt",
|
||||
"blacklists": [
|
||||
{
|
||||
"name": "AdGuard DNS filter",
|
||||
@@ -80,15 +78,11 @@ func createDefaultConfig(configFile string) error {
|
||||
}
|
||||
],
|
||||
"updateInterval": 3600,
|
||||
"hostsFile": "data/hosts.txt",
|
||||
"blockMethod": "NXDOMAIN",
|
||||
"customBlockIP": "",
|
||||
"statsFile": "./data/shield_stats.json",
|
||||
"statsSaveInterval": 60,
|
||||
"remoteRulesCacheDir": "./data/remote_rules"
|
||||
"statsSaveInterval": 60
|
||||
},
|
||||
"log": {
|
||||
"file": "logs/dns-server.log",
|
||||
"level": "debug",
|
||||
"maxSize": 100,
|
||||
"maxBackups": 10,
|
||||
@@ -109,12 +103,12 @@ func createRequiredFiles(cfg *config.Config) error {
|
||||
}
|
||||
|
||||
// 创建远程规则缓存文件夹
|
||||
if err := os.MkdirAll(cfg.Shield.RemoteRulesCacheDir, 0755); err != nil {
|
||||
if err := os.MkdirAll("data/remote_rules", 0755); err != nil {
|
||||
return fmt.Errorf("创建远程规则缓存文件夹失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建日志文件夹
|
||||
logDir := filepath.Dir(cfg.Log.File)
|
||||
logDir := filepath.Dir("logs/dns-server.log")
|
||||
if logDir != "." {
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建日志文件夹失败: %w", err)
|
||||
@@ -122,29 +116,29 @@ func createRequiredFiles(cfg *config.Config) error {
|
||||
}
|
||||
|
||||
// 创建自定义规则文件
|
||||
if _, err := os.Stat(cfg.Shield.LocalRulesFile); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(cfg.Shield.LocalRulesFile, []byte("# 自定义规则文件\n# 格式:域名\n# 例如:example.com\n"), 0644); err != nil {
|
||||
if _, err := os.Stat("data/rules.txt"); os.IsNotExist(err) {
|
||||
if err := os.WriteFile("data/rules.txt", []byte("# 自定义规则文件\n# 格式:域名\n# 例如:example.com\n"), 0644); err != nil {
|
||||
return fmt.Errorf("创建自定义规则文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建Hosts文件
|
||||
if _, err := os.Stat(cfg.Shield.HostsFile); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(cfg.Shield.HostsFile, []byte("# Hosts文件\n# 格式:IP 域名\n# 例如:127.0.0.1 localhost\n"), 0644); err != nil {
|
||||
if _, err := os.Stat("data/hosts.txt"); os.IsNotExist(err) {
|
||||
if err := os.WriteFile("data/hosts.txt", []byte("# Hosts文件\n# 格式:IP 域名\n# 例如:127.0.0.1 localhost\n"), 0644); err != nil {
|
||||
return fmt.Errorf("创建Hosts文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建统计数据文件
|
||||
if _, err := os.Stat(cfg.DNS.StatsFile); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(cfg.DNS.StatsFile, []byte("{}"), 0644); err != nil {
|
||||
if _, err := os.Stat("data/stats.json"); os.IsNotExist(err) {
|
||||
if err := os.WriteFile("data/stats.json", []byte("{}"), 0644); err != nil {
|
||||
return fmt.Errorf("创建统计数据文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建Shield统计数据文件
|
||||
if _, err := os.Stat(cfg.Shield.StatsFile); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(cfg.Shield.StatsFile, []byte("{}"), 0644); err != nil {
|
||||
if _, err := os.Stat("data/shield_stats.json"); os.IsNotExist(err) {
|
||||
if err := os.WriteFile("data/shield_stats.json", []byte("{}"), 0644); err != nil {
|
||||
return fmt.Errorf("创建Shield统计数据文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -183,7 +177,7 @@ func main() {
|
||||
log.Println("所需文件和文件夹创建成功")
|
||||
|
||||
// 初始化日志系统
|
||||
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0, false); err != nil {
|
||||
if err := logger.InitLogger("logs/dns-server.log", cfg.Log.Level, 0, 0, 0, false); err != nil {
|
||||
log.Fatalf("初始化日志系统失败: %v", err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
3235
server.log
3235
server.log
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
549846
|
||||
@@ -134,11 +134,7 @@ func (m *ShieldManager) LoadRules() error {
|
||||
|
||||
// loadLocalRules 加载自定义规则文件
|
||||
func (m *ShieldManager) loadLocalRules() error {
|
||||
if m.config.LocalRulesFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(m.config.LocalRulesFile)
|
||||
file, err := os.Open("data/rules.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -183,7 +179,7 @@ func (m *ShieldManager) getCacheFilePath(url string) string {
|
||||
// 简单处理,移除特殊字符,确保文件名合法
|
||||
hash = strings.ReplaceAll(hash, "/", "_")
|
||||
hash = strings.ReplaceAll(hash, "\\", "_")
|
||||
return filepath.Join(m.config.RemoteRulesCacheDir, hash+".rules")
|
||||
return filepath.Join("data/remote_rules", hash+".rules")
|
||||
}
|
||||
|
||||
// shouldUpdateCache 检查缓存是否需要更新
|
||||
@@ -298,7 +294,7 @@ func (m *ShieldManager) loadCachedRules(filePath string, source string) error {
|
||||
// saveRemoteRulesToCache 保存远程规则到缓存文件
|
||||
func (m *ShieldManager) saveRemoteRulesToCache(filePath string, data []byte) error {
|
||||
// 确保缓存目录存在
|
||||
if err := os.MkdirAll(m.config.RemoteRulesCacheDir, 0755); err != nil {
|
||||
if err := os.MkdirAll("data/remote_rules", 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -308,11 +304,7 @@ func (m *ShieldManager) saveRemoteRulesToCache(filePath string, data []byte) err
|
||||
|
||||
// loadHosts 加载hosts文件
|
||||
func (m *ShieldManager) loadHosts() error {
|
||||
if m.config.HostsFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(m.config.HostsFile)
|
||||
file, err := os.Open("data/hosts.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -585,7 +577,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
||||
|
||||
// 检查子域名排除规则
|
||||
parts := strings.Split(domain, ".")
|
||||
|
||||
|
||||
// 3. 先检查本地子域名排除规则
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
subdomain := strings.Join(parts[i:], ".")
|
||||
@@ -653,7 +645,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
||||
|
||||
// 检查子域名匹配(AdGuardHome风格)
|
||||
// 从最长的子域名开始匹配,确保优先级正确
|
||||
|
||||
|
||||
// 9. 先检查本地子域名阻止规则
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
subdomain := strings.Join(parts[i:], ".")
|
||||
@@ -827,11 +819,9 @@ func (m *ShieldManager) AddRule(rule string) error {
|
||||
m.parseRule(rule, true, "自定义规则")
|
||||
|
||||
// 持久化保存规则到文件
|
||||
if m.config.LocalRulesFile != "" {
|
||||
if err := m.saveRulesToFile(); err != nil {
|
||||
logger.Error("保存规则到文件失败", "error", err)
|
||||
return err
|
||||
}
|
||||
if err := m.saveRulesToFile(); err != nil {
|
||||
logger.Error("保存规则到文件失败", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -996,7 +986,7 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
||||
}
|
||||
|
||||
// 如果有规则被删除,持久化保存更改
|
||||
if removed && m.config.LocalRulesFile != "" {
|
||||
if removed {
|
||||
if err := m.saveRulesToFile(); err != nil {
|
||||
logger.Error("保存规则到文件失败", "error", err)
|
||||
return err
|
||||
@@ -1087,7 +1077,7 @@ func (m *ShieldManager) saveRulesToFile() error {
|
||||
|
||||
// 写入文件
|
||||
content := strings.Join(rules, "\n")
|
||||
return ioutil.WriteFile(m.config.LocalRulesFile, []byte(content), 0644)
|
||||
return ioutil.WriteFile("data/rules.txt", []byte(content), 0644)
|
||||
}
|
||||
|
||||
// AddHostsEntry 添加hosts条目
|
||||
@@ -1098,11 +1088,9 @@ func (m *ShieldManager) AddHostsEntry(ip, domain string) error {
|
||||
m.hostsMap[domain] = ip
|
||||
|
||||
// 持久化保存到hosts文件
|
||||
if m.config.HostsFile != "" {
|
||||
if err := m.saveHostsToFile(); err != nil {
|
||||
logger.Error("保存hosts到文件失败", "error", err)
|
||||
return err
|
||||
}
|
||||
if err := m.saveHostsToFile(); err != nil {
|
||||
logger.Error("保存hosts到文件失败", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1117,11 +1105,9 @@ func (m *ShieldManager) RemoveHostsEntry(domain string) error {
|
||||
delete(m.hostsMap, domain)
|
||||
|
||||
// 持久化保存到hosts文件
|
||||
if m.config.HostsFile != "" {
|
||||
if err := m.saveHostsToFile(); err != nil {
|
||||
logger.Error("保存hosts到文件失败", "error", err)
|
||||
return err
|
||||
}
|
||||
if err := m.saveHostsToFile(); err != nil {
|
||||
logger.Error("保存hosts到文件失败", "error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1151,7 +1137,7 @@ func (m *ShieldManager) saveHostsToFile() error {
|
||||
|
||||
// 写入文件
|
||||
content := strings.Join(lines, "\n")
|
||||
return ioutil.WriteFile(m.config.HostsFile, []byte(content), 0644)
|
||||
return ioutil.WriteFile("data/hosts.txt", []byte(content), 0644)
|
||||
}
|
||||
|
||||
// GetStats 获取规则统计信息
|
||||
@@ -1171,15 +1157,10 @@ func (m *ShieldManager) GetStats() map[string]interface{} {
|
||||
|
||||
// loadStatsData 从文件加载计数数据
|
||||
func (m *ShieldManager) loadStatsData() {
|
||||
if m.config.StatsFile == "" {
|
||||
logger.Info("Shield统计文件路径未配置,跳过加载")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取绝对路径以避免工作目录问题
|
||||
statsFilePath, err := filepath.Abs(m.config.StatsFile)
|
||||
statsFilePath, err := filepath.Abs("data/shield_stats.json")
|
||||
if err != nil {
|
||||
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
|
||||
logger.Error("获取Shield统计文件绝对路径失败", "path", "data/shield_stats.json", "error", err)
|
||||
return
|
||||
}
|
||||
logger.Debug("尝试加载Shield统计数据", "file", statsFilePath)
|
||||
@@ -1271,15 +1252,10 @@ func (m *ShieldManager) loadStatsData() {
|
||||
|
||||
// saveStatsData 保存计数数据到文件
|
||||
func (m *ShieldManager) saveStatsData() {
|
||||
if m.config.StatsFile == "" {
|
||||
logger.Debug("Shield统计文件路径未配置,跳过保存")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取绝对路径以避免工作目录问题
|
||||
statsFilePath, err := filepath.Abs(m.config.StatsFile)
|
||||
statsFilePath, err := filepath.Abs("data/shield_stats.json")
|
||||
if err != nil {
|
||||
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
|
||||
logger.Error("获取Shield统计文件绝对路径失败", "path", "data/shield_stats.json", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1337,14 +1313,14 @@ func (m *ShieldManager) saveStatsData() {
|
||||
|
||||
// startAutoSaveStats 启动计数数据自动保存功能
|
||||
func (m *ShieldManager) startAutoSaveStats() {
|
||||
if m.config.StatsFile == "" || m.config.StatsSaveInterval <= 0 {
|
||||
if m.config.StatsSaveInterval <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Duration(m.config.StatsSaveInterval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
logger.Info("启动Shield计数数据自动保存功能", "interval", m.config.StatsSaveInterval, "file", m.config.StatsFile)
|
||||
logger.Info("启动Shield计数数据自动保存功能", "interval", m.config.StatsSaveInterval, "file", "data/shield_stats.json")
|
||||
|
||||
// 定期保存数据
|
||||
for {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"blockedDomainsCount": {},
|
||||
"resolvedDomainsCount": {},
|
||||
"lastSaved": "2025-11-29T02:08:50.6341349+08:00"
|
||||
}
|
||||
@@ -1122,7 +1122,23 @@ tr:hover {
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 搜索框样式优化 */
|
||||
#logs-search {
|
||||
/* 确保搜索框在所有设备上都有合适的宽度 */
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 在移动设备上进一步优化搜索框 */
|
||||
@media (max-width: 768px) {
|
||||
/* 确保搜索框在移动设备上占满宽度 */
|
||||
#logs-search {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 浮窗内容项 */
|
||||
|
||||
@@ -1050,13 +1050,19 @@
|
||||
<input type="text" id="dns-upstream-servers" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8.8.8.8, 1.1.1.1">
|
||||
</div>
|
||||
<div>
|
||||
<label for="dns-stats-file" class="block text-sm font-medium text-gray-700 mb-1">统计文件路径</label>
|
||||
<input type="text" id="dns-stats-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./stats.json">
|
||||
<label for="dns-dnssec-upstream-servers" class="block text-sm font-medium text-gray-700 mb-1">DNSSEC上游DNS服务器 (逗号分隔)</label>
|
||||
<input type="text" id="dns-dnssec-upstream-servers" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8.8.8.8, 1.1.1.1">
|
||||
</div>
|
||||
<div>
|
||||
<label for="dns-save-interval" class="block text-sm font-medium text-gray-700 mb-1">保存间隔 (秒)</label>
|
||||
<input type="number" id="dns-save-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="300">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="dns-enable-ipv6" class="rounded text-primary focus:ring-primary">
|
||||
<span class="text-sm font-medium text-gray-700">启用IPv6解析(AAAA记录)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1074,15 +1080,6 @@
|
||||
<!-- 屏蔽配置 -->
|
||||
<div class="mb-8">
|
||||
<h4 class="text-md font-medium mb-4">屏蔽配置</h4>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="shield-local-rules-file" class="block text-sm font-medium text-gray-700 mb-1">自定义规则文件</label>
|
||||
<input type="text" id="shield-local-rules-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./rules.txt">
|
||||
</div>
|
||||
<div>
|
||||
<label for="shield-hosts-file" class="block text-sm font-medium text-gray-700 mb-1">Hosts文件</label>
|
||||
<input type="text" id="shield-hosts-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="/etc/hosts">
|
||||
</div>
|
||||
<div>
|
||||
<label for="shield-update-interval" class="block text-sm font-medium text-gray-700 mb-1">更新间隔 (秒)</label>
|
||||
<input type="number" id="shield-update-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="3600">
|
||||
@@ -1117,15 +1114,29 @@
|
||||
</div>
|
||||
|
||||
<!-- 修改密码模态框 -->
|
||||
<!-- 日志详情模态框 -->
|
||||
<div id="log-detail-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-semibold">日志详情</h3>
|
||||
<button id="close-log-modal-btn" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||
<i class="fa fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="log-detail-content">
|
||||
<!-- 日志详情内容将通过JS动态填充 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="change-password-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-semibold">修改密码</h3>
|
||||
<button id="close-modal-btn" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||
<i class="fa fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="change-password-form">
|
||||
<div class="mb-4">
|
||||
<label for="current-password" class="block text-sm font-medium text-gray-700 mb-1">当前密码</label>
|
||||
|
||||
@@ -61,18 +61,20 @@ function populateConfigForm(config) {
|
||||
// DNS配置 - 使用函数安全设置值,避免 || 操作符可能的错误处理
|
||||
setElementValue('dns-port', getSafeValue(dnsServerConfig.Port, 53));
|
||||
setElementValue('dns-upstream-servers', getSafeArray(dnsServerConfig.UpstreamServers).join(', '));
|
||||
setElementValue('dns-dnssec-upstream-servers', getSafeArray(dnsServerConfig.DNSSECUpstreamServers).join(', '));
|
||||
setElementValue('dns-timeout', getSafeValue(dnsServerConfig.Timeout, 5));
|
||||
setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
|
||||
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 300));
|
||||
|
||||
//setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
|
||||
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 30));
|
||||
//setElementValue('dns-cache-ttl', getSafeValue(dnsServerConfig.CacheTTL, 10));
|
||||
setElementValue('dns-enable-ipv6', getSafeValue(dnsServerConfig.EnableIPv6, false));
|
||||
// HTTP配置
|
||||
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
|
||||
|
||||
// 屏蔽配置
|
||||
setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
|
||||
//setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
|
||||
setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
|
||||
setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
|
||||
// 使用服务器端接受的屏蔽方法值,默认使用NXDOMAIN
|
||||
//setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
|
||||
// 使用服务器端接受的屏蔽方法值,默认使用NXDOMAIN, 可选值: NXDOMAIN, NULL, REFUSED
|
||||
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, 'NXDOMAIN'));
|
||||
}
|
||||
|
||||
@@ -80,7 +82,11 @@ function populateConfigForm(config) {
|
||||
function setElementValue(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element && element.tagName === 'INPUT') {
|
||||
element.value = value;
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = value;
|
||||
} else {
|
||||
element.value = value;
|
||||
}
|
||||
} else if (!element) {
|
||||
console.warn(`Element with id "${elementId}" not found for setting value: ${value}`);
|
||||
}
|
||||
@@ -163,6 +169,12 @@ function collectFormData() {
|
||||
upstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
|
||||
[];
|
||||
|
||||
// 安全获取DNSSEC上游服务器列表
|
||||
const dnssecUpstreamServersText = getElementValue('dns-dnssec-upstream-servers');
|
||||
const dnssecUpstreamServers = dnssecUpstreamServersText ?
|
||||
dnssecUpstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
|
||||
[];
|
||||
|
||||
// 安全获取并转换整数值
|
||||
const timeoutValue = getElementValue('dns-timeout');
|
||||
const timeout = timeoutValue ? parseInt(timeoutValue, 10) : 5;
|
||||
@@ -174,21 +186,20 @@ function collectFormData() {
|
||||
const updateInterval = updateIntervalValue ? parseInt(updateIntervalValue, 10) : 3600;
|
||||
|
||||
return {
|
||||
DNSServer: {
|
||||
Port: dnsPort,
|
||||
UpstreamServers: upstreamServers,
|
||||
Timeout: timeout,
|
||||
StatsFile: getElementValue('dns-stats-file') || './data/stats.json',
|
||||
SaveInterval: saveInterval
|
||||
dnsserver: {
|
||||
port: dnsPort,
|
||||
upstreamServers: upstreamServers,
|
||||
dnssecUpstreamServers: dnssecUpstreamServers,
|
||||
timeout: timeout,
|
||||
saveInterval: saveInterval,
|
||||
enableIPv6: getElementValue('dns-enable-ipv6')
|
||||
},
|
||||
HTTPServer: {
|
||||
Port: httpPort
|
||||
httpserver: {
|
||||
port: httpPort
|
||||
},
|
||||
Shield: {
|
||||
LocalRulesFile: getElementValue('shield-local-rules-file') || './data/rules.txt',
|
||||
UpdateInterval: updateInterval,
|
||||
HostsFile: getElementValue('shield-hosts-file') || './data/hosts.txt',
|
||||
BlockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
|
||||
shield: {
|
||||
updateInterval: updateInterval,
|
||||
blockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -197,6 +208,9 @@ function collectFormData() {
|
||||
function getElementValue(elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element && element.tagName === 'INPUT') {
|
||||
if (element.type === 'checkbox') {
|
||||
return element.checked;
|
||||
}
|
||||
return element.value;
|
||||
}
|
||||
return ''; // 默认返回空字符串
|
||||
|
||||
0
static/js/guide.js
Normal file
0
static/js/guide.js
Normal file
@@ -107,6 +107,9 @@ function initLogsPage() {
|
||||
// 绑定事件
|
||||
bindLogsEvents();
|
||||
|
||||
// 初始化日志详情弹窗
|
||||
initLogDetailModal();
|
||||
|
||||
// 建立WebSocket连接,用于实时更新统计数据和图表
|
||||
connectLogsWebSocket();
|
||||
|
||||
@@ -425,6 +428,9 @@ async function updateLogsTable(logs) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测是否为移动设备
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
// 填充表格
|
||||
for (const log of logs) {
|
||||
const row = document.createElement('tr');
|
||||
@@ -491,7 +497,7 @@ async function updateLogsTable(logs) {
|
||||
const trackerInfo = await isDomainInTrackerDatabase(log.Domain);
|
||||
const isTracker = trackerInfo !== null;
|
||||
|
||||
// 构建行内容 - 两行显示,时间列显示时间和日期,请求列显示域名和类型状态
|
||||
// 构建行内容 - 根据设备类型决定显示内容
|
||||
// 添加缓存状态显示
|
||||
const cacheStatusClass = log.FromCache ? 'text-primary' : 'text-gray-500';
|
||||
const cacheStatusText = log.FromCache ? '缓存' : '非缓存';
|
||||
@@ -510,36 +516,59 @@ async function updateLogsTable(logs) {
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4">
|
||||
<div class="text-sm font-medium">${formattedTime}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="font-medium">${log.ClientIP}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${log.Location || '未知 未知'}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="font-medium flex items-center relative">
|
||||
${log.DNSSEC ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||||
<div class="tracker-icon-container relative">
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||
${trackerTooltip}
|
||||
if (isMobile) {
|
||||
// 移动设备只显示时间和请求信息
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4">
|
||||
<div class="text-sm font-medium">${formattedTime}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm" colspan="5">
|
||||
<div class="font-medium flex items-center relative">
|
||||
${log.DNSSEC ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||||
<div class="tracker-icon-container relative">
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
${log.Domain}
|
||||
</div>
|
||||
${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>${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>
|
||||
<div class="text-xs text-gray-500 mt-1">DNS 服务器: ${log.DNSServer || '无'}, DNSSEC专用: ${log.DNSSECServer || '无'}</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>
|
||||
<td class="py-3 px-4 text-sm text-center">
|
||||
${isBlocked ?
|
||||
`<button class="unblock-btn px-3 py-1 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors text-xs" data-domain="${log.Domain}">放行</button>` :
|
||||
`<button class="block-btn px-3 py-1 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-xs" data-domain="${log.Domain}">拦截</button>`
|
||||
}
|
||||
</td>
|
||||
`;
|
||||
<div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span></div>
|
||||
<div class="text-xs text-gray-500 mt-1">客户端: ${log.ClientIP}</div>
|
||||
</td>
|
||||
`;
|
||||
} else {
|
||||
// 桌面设备显示完整信息
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4">
|
||||
<div class="text-sm font-medium">${formattedTime}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="font-medium">${log.ClientIP}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${log.Location || '未知 未知'}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="font-medium flex items-center relative">
|
||||
${log.DNSSEC ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||||
<div class="tracker-icon-container relative">
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
${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>${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>
|
||||
<div class="text-xs text-gray-500 mt-1">DNS 服务器: ${log.DNSServer || '无'}, DNSSEC专用: ${log.DNSSECServer || '无'}</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>
|
||||
<td class="py-3 px-4 text-sm text-center">
|
||||
${isBlocked ?
|
||||
`<button class="unblock-btn px-3 py-1 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors text-xs" data-domain="${log.Domain}">放行</button>` :
|
||||
`<button class="block-btn px-3 py-1 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-xs" data-domain="${log.Domain}">拦截</button>`
|
||||
}
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
|
||||
// 添加跟踪器图标悬停事件
|
||||
if (isTracker) {
|
||||
@@ -576,6 +605,16 @@ async function updateLogsTable(logs) {
|
||||
unblockDomain(domain);
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定日志详情点击事件
|
||||
row.addEventListener('click', (e) => {
|
||||
// 如果点击的是按钮,不触发详情弹窗
|
||||
if (e.target.closest('button')) {
|
||||
return;
|
||||
}
|
||||
console.log('Row clicked, log object:', log);
|
||||
showLogDetailModal(log);
|
||||
});
|
||||
|
||||
tableBody.appendChild(row);
|
||||
}
|
||||
@@ -932,6 +971,663 @@ async function unblockDomain(domain) {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示日志详情弹窗
|
||||
async function showLogDetailModal(log) {
|
||||
console.log('showLogDetailModal called with log:', JSON.stringify(log, null, 2)); // 输出完整的log对象
|
||||
|
||||
// 确保log对象存在
|
||||
if (!log) {
|
||||
console.error('No log data provided!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 简化版本,直接创建一个新的模态框
|
||||
const modalContainer = document.createElement('div');
|
||||
modalContainer.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center';
|
||||
modalContainer.style.zIndex = '9999'; // 确保z-index足够高
|
||||
|
||||
// 创建模态框内容
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.className = 'bg-white rounded-lg shadow-xl p-6 w-full max-w-md';
|
||||
|
||||
// 添加关闭按钮
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.innerHTML = '<i class="fa fa-times text-xl"></i>';
|
||||
closeButton.className = 'text-gray-500 hover:text-gray-700 focus:outline-none ml-auto';
|
||||
closeButton.onclick = function() {
|
||||
document.body.removeChild(modalContainer);
|
||||
};
|
||||
|
||||
// 创建标题栏
|
||||
const titleBar = document.createElement('div');
|
||||
titleBar.className = 'flex justify-between items-center mb-4';
|
||||
|
||||
// 添加标题
|
||||
const title = document.createElement('h3');
|
||||
title.className = 'text-xl font-semibold';
|
||||
title.textContent = '日志详情';
|
||||
|
||||
// 将标题和关闭按钮添加到标题栏
|
||||
titleBar.appendChild(title);
|
||||
titleBar.appendChild(closeButton);
|
||||
|
||||
// 创建详情内容
|
||||
const details = document.createElement('div');
|
||||
details.className = 'space-y-4';
|
||||
|
||||
// 安全获取log属性,提供默认值
|
||||
const timestamp = log.Timestamp ? new Date(log.Timestamp) : null;
|
||||
const dateStr = timestamp ? timestamp.toLocaleDateString() : '未知';
|
||||
const timeStr = timestamp ? timestamp.toLocaleTimeString() : '未知';
|
||||
const domain = log.Domain || '未知';
|
||||
const queryType = log.QueryType || '未知';
|
||||
const result = log.Result || '未知';
|
||||
const responseTime = log.ResponseTime || '未知';
|
||||
const clientIP = log.ClientIP || '未知';
|
||||
const location = log.Location || '未知';
|
||||
const fromCache = log.FromCache || false;
|
||||
const dnssec = log.DNSSEC || false;
|
||||
const edns = log.EDNS || false;
|
||||
const dnsServer = log.DNSServer || '无';
|
||||
const dnssecServer = log.DNSSECServer || '无';
|
||||
const blockRule = log.BlockRule || '无';
|
||||
|
||||
// 检查域名是否在跟踪器数据库中
|
||||
const trackerInfo = await isDomainInTrackerDatabase(log.Domain);
|
||||
const isTracker = trackerInfo !== null;
|
||||
|
||||
// 获取DNS响应内容(如果有)
|
||||
const dnsResponse = log.Response || '无';
|
||||
// 添加调试信息,查看log对象结构
|
||||
console.log('=== DNS日志对象结构 ===');
|
||||
console.log('log对象:', log);
|
||||
console.log('log字段列表:', Object.keys(log));
|
||||
console.log('Answers字段值:', log.Answers);
|
||||
console.log('answers字段值:', log.answers);
|
||||
console.log('Response字段值:', log.Response);
|
||||
console.log('Answer字段值:', log.Answer);
|
||||
// 处理Answers字段,确保正确解析
|
||||
let dnsAnswers = log.Answers || log.answers || [];
|
||||
|
||||
// 添加更多调试信息
|
||||
console.log('=== 解析记录提取调试信息 ===');
|
||||
console.log('日志对象:', log);
|
||||
console.log('日志字段:', Object.keys(log));
|
||||
|
||||
// 检查所有可能的解析记录字段
|
||||
const potentialFields = ['Answers', 'answers', 'Answer', 'answer', 'Records', 'records', 'Response'];
|
||||
potentialFields.forEach(field => {
|
||||
console.log(`${field}:`, log[field]);
|
||||
});
|
||||
|
||||
// 关键修复:如果Answers是字符串类型的JSON数组,强制解析
|
||||
if (typeof dnsAnswers === 'string') {
|
||||
// 先检查是否是有效的JSON数组格式
|
||||
if (dnsAnswers.startsWith('[') && dnsAnswers.endsWith(']')) {
|
||||
try {
|
||||
dnsAnswers = JSON.parse(dnsAnswers);
|
||||
} catch (e) {
|
||||
console.error('解析Answers JSON数组失败:', e);
|
||||
dnsAnswers = [];
|
||||
}
|
||||
} else {
|
||||
// 如果不是数组格式,尝试解析为单个对象
|
||||
try {
|
||||
dnsAnswers = JSON.parse(dnsAnswers);
|
||||
// 如果解析后是单个对象,转换为数组
|
||||
if (typeof dnsAnswers === 'object' && !Array.isArray(dnsAnswers)) {
|
||||
dnsAnswers = [dnsAnswers];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析Answers JSON失败:', e);
|
||||
dnsAnswers = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('处理后的dnsAnswers:', dnsAnswers);
|
||||
|
||||
// 添加基本信息,使用安全获取的值
|
||||
details.innerHTML = `
|
||||
<!-- 第1组:基本信息 -->
|
||||
<div class="text-xs">
|
||||
<div class="font-medium text-gray-700 mb-2">基本信息</div>
|
||||
<div class="grid grid-cols-2 gap-y-2 gap-x-4">
|
||||
<div>
|
||||
<div class="text-gray-500">日期:</div>
|
||||
<div class="text-gray-800">${dateStr}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">时间:</div>
|
||||
<div class="text-gray-800">${timeStr}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">状态:</div>
|
||||
<div class="${result === 'blocked' ? 'text-red-600' : result === 'allowed' ? 'text-green-600' : 'text-gray-500'}">
|
||||
${result === 'blocked' ? '已拦截' : result === 'allowed' ? '允许' : result}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">域名:</div>
|
||||
<div class="text-gray-800">${domain}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">类型:</div>
|
||||
<div class="text-gray-800">${queryType}</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<div class="text-gray-500">DNS特性:</div>
|
||||
<div class="text-gray-800">
|
||||
${dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>DNSSEC ' : ''}
|
||||
${edns ? '<i class="fa fa-exchange text-blue-500 mr-1" title="EDNS已启用"></i>EDNS' : ''}
|
||||
${!dnssec && !edns ? '无' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<div class="text-gray-500">跟踪器信息:</div>
|
||||
<div class="text-gray-800">
|
||||
${isTracker ? `
|
||||
<div class="flex items-center">
|
||||
<i class="fa fa-eye text-red-500 mr-1"></i>
|
||||
<span>${trackerInfo.name} (${trackersDatabase.categories[trackerInfo.categoryId] || '未知'})</span>
|
||||
</div>
|
||||
` : '无'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<div class="text-gray-500">解析记录:</div>
|
||||
<div class="text-gray-800 whitespace-pre-wrap break-all text-left">
|
||||
${result === 'blocked' ? '无' : (() => {
|
||||
// 尝试从不同字段获取解析记录
|
||||
let records = '';
|
||||
|
||||
// 1. 尝试使用Answers数组 - 始终优先使用Answers
|
||||
if (dnsAnswers && Array.isArray(dnsAnswers) && dnsAnswers.length > 0) {
|
||||
records = dnsAnswers.map(answer => {
|
||||
// 处理不同格式的answer对象
|
||||
const type = answer.type || answer.Type || '未知';
|
||||
let value = answer.value || answer.Value || answer.data || answer.Data || '未知';
|
||||
const ttl = answer.TTL || answer.ttl || answer.expires || '未知';
|
||||
|
||||
// 增强的记录值提取逻辑:处理多种DNS记录格式
|
||||
if (typeof value === 'string') {
|
||||
// 如果value是完整的DNS记录字符串,提取出实际值
|
||||
if (value.includes('\t') || value.includes('\\t')) {
|
||||
// 处理实际制表符或转义的制表符
|
||||
const parts = value.replace(/\\t/g, '\t').split('\t');
|
||||
// 兼容不同长度的记录格式
|
||||
if (parts.length >= 4) {
|
||||
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
|
||||
value = parts[parts.length - 1].trim();
|
||||
} else if (parts.length >= 2) {
|
||||
// 对于其他格式,使用最后一个字段
|
||||
value = parts[parts.length - 1].trim();
|
||||
}
|
||||
}
|
||||
// 如果value是JSON字符串,尝试解析
|
||||
else if (value.startsWith('{') && value.endsWith('}')) {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
value = parsedValue.data || parsedValue.value || value;
|
||||
// 解析后的值也需要trim
|
||||
if (typeof value === 'string') {
|
||||
value = value.trim();
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,保持原值但trim
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
// 对于其他所有字符串类型的值,直接trim
|
||||
else {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
}).join('\n').trim();
|
||||
}
|
||||
// 2. 尝试解析字符串类型的Answers - 增强的容错处理
|
||||
else if (typeof dnsAnswers === 'string') {
|
||||
try {
|
||||
const parsedAnswers = JSON.parse(dnsAnswers);
|
||||
if (Array.isArray(parsedAnswers)) {
|
||||
records = parsedAnswers.map(answer => {
|
||||
const type = answer.type || answer.Type || '未知';
|
||||
let value = answer.value || answer.Value || answer.data || answer.Data || '未知';
|
||||
const ttl = answer.TTL || answer.ttl || answer.expires || '未知';
|
||||
|
||||
// 增强的记录值提取逻辑:处理多种DNS记录格式
|
||||
if (typeof value === 'string') {
|
||||
// 如果value是完整的DNS记录字符串,提取出实际值
|
||||
if (value.includes('\t') || value.includes('\\t')) {
|
||||
// 处理实际制表符或转义的制表符
|
||||
const parts = value.replace(/\\t/g, '\t').split('\t');
|
||||
// 兼容不同长度的记录格式
|
||||
if (parts.length >= 4) {
|
||||
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
|
||||
value = parts[parts.length - 1].trim();
|
||||
} else if (parts.length >= 2) {
|
||||
// 对于其他格式,使用最后一个字段
|
||||
value = parts[parts.length - 1].trim();
|
||||
}
|
||||
}
|
||||
// 对于其他所有字符串类型的值,直接trim
|
||||
else {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
}).join('\n').trim();
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,继续尝试其他字段
|
||||
}
|
||||
}
|
||||
// 3. 尝试从log.Answer字段获取(单数形式)
|
||||
if (!records && log.Answer) {
|
||||
if (Array.isArray(log.Answer)) {
|
||||
records = log.Answer.map(answer => {
|
||||
const type = answer.type || answer.Type || '未知';
|
||||
let value = answer.value || answer.Value || answer.data || answer.Data || '未知';
|
||||
const ttl = answer.TTL || answer.ttl || answer.expires || '未知';
|
||||
|
||||
// 增强的记录值提取逻辑:处理多种DNS记录格式
|
||||
if (typeof value === 'string') {
|
||||
// 如果value是完整的DNS记录字符串,提取出实际值
|
||||
if (value.includes('\t') || value.includes('\\t')) {
|
||||
// 处理实际制表符或转义的制表符
|
||||
const parts = value.replace(/\\t/g, '\t').split('\t');
|
||||
// 兼容不同长度的记录格式
|
||||
if (parts.length >= 4) {
|
||||
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
|
||||
value = parts[parts.length - 1].trim();
|
||||
} else if (parts.length >= 2) {
|
||||
// 对于其他格式,使用最后一个字段
|
||||
value = parts[parts.length - 1].trim();
|
||||
}
|
||||
}
|
||||
// 对于其他所有字符串类型的值,直接trim
|
||||
else {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
}).join('\n').trim();
|
||||
} else if (typeof log.Answer === 'object') {
|
||||
// 单个answer对象
|
||||
const type = log.Answer.type || log.Answer.Type || '未知';
|
||||
let value = log.Answer.value || log.Answer.Value || log.Answer.data || log.Answer.Data || '未知';
|
||||
const ttl = log.Answer.TTL || log.Answer.ttl || log.Answer.expires || '未知';
|
||||
|
||||
// 增强的记录值提取逻辑:处理多种DNS记录格式
|
||||
if (typeof value === 'string') {
|
||||
// 如果value是完整的DNS记录字符串,提取出实际值
|
||||
if (value.includes('\t') || value.includes('\\t')) {
|
||||
// 处理实际制表符或转义的制表符
|
||||
const parts = value.replace(/\\t/g, '\t').split('\t');
|
||||
// 兼容不同长度的记录格式
|
||||
if (parts.length >= 4) {
|
||||
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
|
||||
value = parts[parts.length - 1].trim();
|
||||
} else if (parts.length >= 2) {
|
||||
// 对于其他格式,使用最后一个字段
|
||||
value = parts[parts.length - 1].trim();
|
||||
}
|
||||
}
|
||||
// 对于其他所有字符串类型的值,直接trim
|
||||
else {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
records = `${type}: ${value} (ttl=${ttl})`;
|
||||
} else if (typeof log.Answer === 'string') {
|
||||
// 字符串类型的Answer - 处理每行缩进
|
||||
records = log.Answer.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
|
||||
}
|
||||
}
|
||||
// 4. 尝试从log.answer字段获取(小写单数形式)
|
||||
if (!records && log.answer) {
|
||||
if (Array.isArray(log.answer)) {
|
||||
records = log.answer.map(answer => {
|
||||
const type = answer.type || answer.Type || '未知';
|
||||
let value = answer.value || answer.Value || answer.data || answer.Data || '未知';
|
||||
const ttl = answer.TTL || answer.ttl || answer.expires || '未知';
|
||||
|
||||
// 增强的记录值提取逻辑:处理多种DNS记录格式
|
||||
if (typeof value === 'string') {
|
||||
// 如果value是完整的DNS记录字符串,提取出实际值
|
||||
if (value.includes('\t') || value.includes('\\t')) {
|
||||
// 处理实际制表符或转义的制表符
|
||||
const parts = value.replace(/\\t/g, '\t').split('\t');
|
||||
// 兼容不同长度的记录格式
|
||||
if (parts.length >= 4) {
|
||||
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
|
||||
value = parts[parts.length - 1].trim();
|
||||
} else if (parts.length >= 2) {
|
||||
// 对于其他格式,使用最后一个字段
|
||||
value = parts[parts.length - 1].trim();
|
||||
}
|
||||
}
|
||||
// 对于其他所有字符串类型的值,直接trim
|
||||
else {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
}).join('\n').trim();
|
||||
} else if (typeof log.answer === 'object') {
|
||||
// 单个answer对象
|
||||
const type = log.answer.type || log.answer.Type || '未知';
|
||||
let value = log.answer.value || log.answer.Value || log.answer.data || log.answer.Data || '未知';
|
||||
const ttl = log.answer.TTL || log.answer.ttl || log.answer.expires || '未知';
|
||||
|
||||
// 增强的记录值提取逻辑:处理多种DNS记录格式
|
||||
if (typeof value === 'string') {
|
||||
// 如果value是完整的DNS记录字符串,提取出实际值
|
||||
if (value.includes('\t') || value.includes('\\t')) {
|
||||
// 处理实际制表符或转义的制表符
|
||||
const parts = value.replace(/\\t/g, '\t').split('\t');
|
||||
// 兼容不同长度的记录格式
|
||||
if (parts.length >= 4) {
|
||||
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
|
||||
value = parts[parts.length - 1].trim();
|
||||
} else if (parts.length >= 2) {
|
||||
// 对于其他格式,使用最后一个字段
|
||||
value = parts[parts.length - 1].trim();
|
||||
}
|
||||
}
|
||||
// 对于其他所有字符串类型的值,直接trim
|
||||
else {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
records = `${type}: ${value} (ttl=${ttl})`;
|
||||
} else if (typeof log.answer === 'string') {
|
||||
// 字符串类型的answer - 处理每行缩进
|
||||
records = log.answer.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
|
||||
}
|
||||
}
|
||||
// 5. 尝试从log.Records字段获取
|
||||
if (!records && log.Records) {
|
||||
if (Array.isArray(log.Records)) {
|
||||
records = log.Records.map(record => {
|
||||
const type = record.type || record.Type || '未知';
|
||||
let value = record.value || record.Value || record.data || record.Data || '未知';
|
||||
const ttl = record.TTL || record.ttl || record.expires || '未知';
|
||||
|
||||
// 增强的记录值提取逻辑:处理多种DNS记录格式
|
||||
if (typeof value === 'string') {
|
||||
// 如果value是完整的DNS记录字符串,提取出实际值
|
||||
if (value.includes('\t') || value.includes('\\t')) {
|
||||
// 处理实际制表符或转义的制表符
|
||||
const parts = value.replace(/\\t/g, '\t').split('\t');
|
||||
// 兼容不同长度的记录格式
|
||||
if (parts.length >= 4) {
|
||||
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
|
||||
value = parts[parts.length - 1].trim();
|
||||
} else if (parts.length >= 2) {
|
||||
// 对于其他格式,使用最后一个字段
|
||||
value = parts[parts.length - 1].trim();
|
||||
}
|
||||
}
|
||||
// 对于其他所有字符串类型的值,直接trim
|
||||
else {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
}).join('\n').trim();
|
||||
} else if (typeof log.Records === 'string') {
|
||||
// 字符串类型的Records - 处理每行缩进
|
||||
records = log.Records.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
|
||||
}
|
||||
}
|
||||
// 6. 尝试从log.records字段获取(小写形式)
|
||||
if (!records && log.records) {
|
||||
if (Array.isArray(log.records)) {
|
||||
records = log.records.map(record => {
|
||||
const type = record.type || record.Type || '未知';
|
||||
let value = record.value || record.Value || record.data || record.Data || '未知';
|
||||
const ttl = record.TTL || record.ttl || record.expires || '未知';
|
||||
|
||||
// 增强的记录值提取逻辑:处理多种DNS记录格式
|
||||
if (typeof value === 'string') {
|
||||
// 如果value是完整的DNS记录字符串,提取出实际值
|
||||
if (value.includes('\t') || value.includes('\\t')) {
|
||||
// 处理实际制表符或转义的制表符
|
||||
const parts = value.replace(/\\t/g, '\t').split('\t');
|
||||
// 兼容不同长度的记录格式
|
||||
if (parts.length >= 4) {
|
||||
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
|
||||
value = parts[parts.length - 1].trim();
|
||||
} else if (parts.length >= 2) {
|
||||
// 对于其他格式,使用最后一个字段
|
||||
value = parts[parts.length - 1].trim();
|
||||
}
|
||||
}
|
||||
// 对于其他所有字符串类型的值,直接trim
|
||||
else {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
}).join('\n').trim();
|
||||
} else if (typeof log.records === 'string') {
|
||||
// 字符串类型的records - 处理每行缩进
|
||||
records = log.records.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
|
||||
}
|
||||
}
|
||||
// 7. 尝试从Response字段获取(兼容旧格式)
|
||||
if (!records && dnsResponse && dnsResponse !== '无') {
|
||||
// 如果Response是JSON字符串,尝试解析
|
||||
if (dnsResponse.startsWith('[') && dnsResponse.endsWith(']')) {
|
||||
try {
|
||||
const parsedResponse = JSON.parse(dnsResponse);
|
||||
if (Array.isArray(parsedResponse)) {
|
||||
records = parsedResponse.map(item => {
|
||||
const type = item.type || item.Type || '未知';
|
||||
let value = item.value || item.Value || item.data || item.Data || '未知';
|
||||
const ttl = item.TTL || item.ttl || item.expires || '未知';
|
||||
|
||||
// 增强的记录值提取逻辑:处理多种DNS记录格式
|
||||
if (typeof value === 'string') {
|
||||
// 如果value是完整的DNS记录字符串,提取出实际值
|
||||
if (value.includes('\t') || value.includes('\\t')) {
|
||||
// 处理实际制表符或转义的制表符
|
||||
const parts = value.replace(/\\t/g, '\t').split('\t');
|
||||
// 兼容不同长度的记录格式
|
||||
if (parts.length >= 4) {
|
||||
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
|
||||
value = parts[parts.length - 1].trim();
|
||||
} else if (parts.length >= 2) {
|
||||
// 对于其他格式,使用最后一个字段
|
||||
value = parts[parts.length - 1].trim();
|
||||
}
|
||||
}
|
||||
// 如果value是JSON字符串,尝试解析
|
||||
else if (value.startsWith('{') && value.endsWith('}')) {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
value = parsedValue.data || parsedValue.value || value;
|
||||
// 解析后的值也需要trim
|
||||
if (typeof value === 'string') {
|
||||
value = value.trim();
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,保持原值但trim
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
// 对于其他所有字符串类型的值,直接trim
|
||||
else {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
}).join('\n').trim();
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,直接显示Response内容 - 处理每行缩进
|
||||
records = dnsResponse.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
|
||||
}
|
||||
} else {
|
||||
// Response不是JSON数组 - 处理每行缩进
|
||||
records = dnsResponse.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
|
||||
}
|
||||
}
|
||||
// 8. 如果还是没有解析记录,显示友好提示
|
||||
if (!records) {
|
||||
records = '无解析记录';
|
||||
}
|
||||
|
||||
return records;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<div class="text-gray-500">DNS服务器:</div>
|
||||
<div class="text-gray-800">${dnsServer}</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<div class="text-gray-500">DNSSEC专用服务器:</div>
|
||||
<div class="text-gray-800">${dnssecServer}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="border-t border-gray-200 my-3"></div>
|
||||
|
||||
<!-- 第2组:响应细节 -->
|
||||
<div class="text-xs">
|
||||
<div class="font-medium text-gray-700 mb-2">响应细节</div>
|
||||
<div class="grid grid-cols-2 gap-y-2 gap-x-4">
|
||||
<div>
|
||||
<div class="text-gray-500">响应时间:</div>
|
||||
<div class="text-gray-800">${responseTime}毫秒</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">规则:</div>
|
||||
<div class="text-gray-800">${blockRule}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">响应代码:</div>
|
||||
<div class="text-gray-800">无</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">缓存状态:</div>
|
||||
<div class="${fromCache ? 'text-primary' : 'text-gray-500'}">
|
||||
${fromCache ? '缓存' : '非缓存'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="border-t border-gray-200 my-3"></div>
|
||||
|
||||
<!-- 第3组:客户端详情 -->
|
||||
<div class="text-xs">
|
||||
<div class="font-medium text-gray-700 mb-2">客户端详情</div>
|
||||
<div>
|
||||
<div class="text-gray-500">IP地址:</div>
|
||||
<div class="text-gray-800">${clientIP} (${location})</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 组装模态框
|
||||
modalContent.appendChild(titleBar);
|
||||
modalContent.appendChild(details);
|
||||
modalContainer.appendChild(modalContent);
|
||||
|
||||
// 添加到页面
|
||||
document.body.appendChild(modalContainer);
|
||||
|
||||
// 点击外部关闭
|
||||
modalContainer.addEventListener('click', function(e) {
|
||||
if (e.target === modalContainer) {
|
||||
document.body.removeChild(modalContainer);
|
||||
}
|
||||
});
|
||||
|
||||
// ESC键关闭
|
||||
document.addEventListener('keydown', function handleEsc(e) {
|
||||
if (e.key === 'Escape') {
|
||||
document.body.removeChild(modalContainer);
|
||||
document.removeEventListener('keydown', handleEsc);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in showLogDetailModal:', error);
|
||||
|
||||
// 显示错误提示
|
||||
const errorModal = document.createElement('div');
|
||||
errorModal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center';
|
||||
errorModal.style.zIndex = '9999';
|
||||
|
||||
const errorContent = document.createElement('div');
|
||||
errorContent.className = 'bg-white rounded-lg shadow-xl p-6 w-full max-w-md';
|
||||
|
||||
errorContent.innerHTML = `
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-semibold">错误</h3>
|
||||
<button onclick="document.body.removeChild(errorModal)" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||
<i class="fa fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-red-600">
|
||||
加载日志详情失败: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
|
||||
errorModal.appendChild(errorContent);
|
||||
document.body.appendChild(errorModal);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭日志详情弹窗
|
||||
function closeLogDetailModal() {
|
||||
const modal = document.getElementById('log-detail-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 初始化日志详情弹窗事件
|
||||
function initLogDetailModal() {
|
||||
// 关闭按钮事件
|
||||
const closeBtn = document.getElementById('close-log-modal-btn');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', closeLogDetailModal);
|
||||
}
|
||||
|
||||
// 点击模态框外部关闭
|
||||
const modal = document.getElementById('log-detail-modal');
|
||||
if (modal) {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeLogDetailModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ESC键关闭
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeLogDetailModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 定期更新日志统计数据(备用方案)
|
||||
setInterval(() => {
|
||||
// 只有在查询日志页面时才更新
|
||||
|
||||
52
temp_config.json
Normal file
52
temp_config.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"dns": {
|
||||
"port": 5353,
|
||||
"upstreamDNS": [
|
||||
"223.5.5.5:53",
|
||||
"223.6.6.6:53",
|
||||
"117.50.10.10:53",
|
||||
"10.35.10.200:53"
|
||||
],
|
||||
"dnssecUpstreamDNS": [
|
||||
"117.50.10.10:53",
|
||||
"101.226.4.6:53",
|
||||
"218.30.118.6:53",
|
||||
"208.67.220.220:53",
|
||||
"208.67.222.222:53"
|
||||
],
|
||||
"timeout": 5000,
|
||||
"statsFile": "data/stats.json",
|
||||
"saveInterval": 300,
|
||||
"cacheTTL": 30,
|
||||
"enableDNSSEC": true,
|
||||
"queryMode": "parallel",
|
||||
"domainSpecificDNS": {
|
||||
"amazehome.xyz": ["10.35.10.200:53"]
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"port": 8081,
|
||||
"host": "0.0.0.0",
|
||||
"enableAPI": true,
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
},
|
||||
"shield": {
|
||||
"localRulesFile": "data/rules.txt",
|
||||
"blacklists": [],
|
||||
"updateInterval": 3600,
|
||||
"hostsFile": "data/hosts.txt",
|
||||
"blockMethod": "NXDOMAIN",
|
||||
"customBlockIP": "",
|
||||
"statsFile": "./data/shield_stats.json",
|
||||
"statsSaveInterval": 60,
|
||||
"remoteRulesCacheDir": "data/remote_rules"
|
||||
},
|
||||
"log": {
|
||||
"file": "logs/dns-server-5353.log",
|
||||
"level": "debug",
|
||||
"maxSize": 100,
|
||||
"maxBackups": 10,
|
||||
"maxAge": 30
|
||||
}
|
||||
}
|
||||
@@ -3102,13 +3102,13 @@
|
||||
"companyId": null
|
||||
},
|
||||
"baidu_ads": {
|
||||
"name": "Baidu Ads",
|
||||
"name": "百度",
|
||||
"categoryId": 4,
|
||||
"url": "http://www.baidu.com/",
|
||||
"companyId": "baidu"
|
||||
},
|
||||
"baidu_static": {
|
||||
"name": "Baidu Static",
|
||||
"name": "百度统计",
|
||||
"categoryId": 8,
|
||||
"url": "https://www.baidu.com/",
|
||||
"companyId": "baidu"
|
||||
|
||||
Reference in New Issue
Block a user