增加查询日志详情界面点击域名列表,显示解析日志的详细信息.

This commit is contained in:
Alex Yang
2025-12-25 20:33:07 +08:00
parent 151042e675
commit 01f2152e46
17 changed files with 1138 additions and 3480 deletions

View File

@@ -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/)规范。

View File

@@ -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
}
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
549846

View File

@@ -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 {

View File

@@ -1,5 +0,0 @@
{
"blockedDomainsCount": {},
"resolvedDomainsCount": {},
"lastSaved": "2025-11-29T02:08:50.6341349+08:00"
}

View File

@@ -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;
}
}
/* 浮窗内容项 */

View File

@@ -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>

View File

@@ -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
View File

View 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
View 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
}
}

View File

@@ -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"