多项更新优化

This commit is contained in:
Alex Yang
2025-12-26 09:02:59 +08:00
parent 356310ae75
commit b48dc4ed27
18 changed files with 1178 additions and 348 deletions

View File

@@ -1,6 +1,65 @@
# Changelog
所有对本项目的显著更改都将记录在此文件中。
## [1.2.5] - 2025-12-26
### 新增
- 增加了对IPv6的支持配置项默认关闭
- 增加跟踪器状态显示匹配tracker/trackers.json数据库
- 全局UX改进包括但不限于
- 增加了页面滚动时,菜单栏和顶部标题栏保持固定的功能;
- 优化了页面适应窗口大小的改变,确保在所有设备上都能正确显示;
- 增加点击解析记录后弹窗日志详情的UI/UX使用现代化设计和动画效果
- 增加了查询日志详情界面的滚动条,方便查看长日志。
### 改进
- 新增API接口用于查询解析日志详情
- 支持EDNS在web界面查询日志详情的请求列表区域增加了EDNS标记显示。
### 修复
- 修复DNS服务器地址缺少端口号导致的Server Failed问题
- 修复查询日志详情接口返回的日志格式错误问题,现在返回的日志包含完整的解析记录和解析时间;
- 修复查询日志详情接口返回的日志中解析记录中缺少IP地址、类型、DNSSEC验证状态等信息的问题
- web界面系统设置加载后不获取数据和保存配置不生效的问题
- 修复了DNS查询超时设置过短导致的"Server failed"错误。
### 下一版本改进
- 增加了对DNSSEC的支持配置项默认关闭
## [1.2.4] - 2025-12-25
### 改进
- 修复DNS解析记录显示现在显示完整格式"A: 104.26.24.30 (ttl=193)" 而不仅仅是IP地址
- 移除了查询日志列表中的"屏蔽规则"列,但在详情弹窗中仍保留
- 在弹窗日志详情中,只有被屏蔽或者有自定义规则时才显示规则信息
- 改进了日志详情弹窗的UI/UX使用现代化设计和动画效果
- 移除了右上角的服务器状态卡片CPU、查询统计等
- 实现了页面滚动时,菜单栏和顶部标题栏保持固定
- 优化了页面适应窗口大小的改变,确保在所有设备上都能正确显示
### 修复
- 修复了移动端侧边栏在打开时遮挡页面内容的问题
- 修复了侧边栏布局分离了桌面端和移动端侧边栏使用CSS媒体查询控制显示
## [1.2.3] - 2025-12-25
### 修复
- 修复DNS服务器地址缺少端口号导致的Server Failed问题
- 添加normalizeDNSServerAddress函数确保DNS服务器地址始终包含端口号默认添加53端口
- 修改所有resolver.Exchange()调用,确保传递的服务器地址包含端口号
- 优化DNSSEC服务器合并逻辑确保DNSSEC服务器地址也包含端口号
## [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 +123,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/)规范。

5
build-windows.sh Executable file
View File

@@ -0,0 +1,5 @@
CGO_ENABLED=1 \
GOOS=windows \
GOARCH=amd64 \
CC=gcc \
go build -o dns-server.exe main.go

View File

@@ -5,18 +5,17 @@
"223.5.5.5: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"
"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"
@@ -45,7 +44,8 @@
"addr.arpa",
"amazehome.xyz",
".cn"
]
],
"enableIPv6": false
},
"http": {
"port": 8080,
@@ -55,7 +55,6 @@
"password": "admin"
},
"shield": {
"localRulesFile": "data/rules.txt",
"blacklists": [
{
"name": "AdGuard DNS filter",
@@ -79,7 +78,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 +116,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,15 +140,11 @@
}
],
"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,

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

@@ -22,6 +22,17 @@ import (
"github.com/miekg/dns"
)
// 确保DNS服务器地址包含端口号默认添加53端口
func normalizeDNSServerAddress(address string) string {
// 检查地址是否已经包含端口号
if _, _, err := net.SplitHostPort(address); err != nil {
// 如果没有端口号添加默认的53端口
return net.JoinHostPort(address, "53")
}
// 已经有端口号,直接返回
return address
}
// BlockedDomain 屏蔽域名统计
type BlockedDomain struct {
Domain string
@@ -45,22 +56,31 @@ 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 `json:"timestamp"` // 查询时间
ClientIP string `json:"clientIP"` // 客户端IP
Location string `json:"location"` // IP地理位置国家 城市)
Domain string `json:"domain"` // 查询域名
QueryType string `json:"queryType"` // 查询类型
ResponseTime int64 `json:"responseTime"` // 响应时间(ms)
Result string `json:"result"` // 查询结果allowed, blocked, error
BlockRule string `json:"blockRule"` // 屏蔽规则(如果被屏蔽)
BlockType string `json:"blockType"` // 屏蔽类型(如果被屏蔽)
FromCache bool `json:"fromCache"` // 是否来自缓存
DNSSEC bool `json:"dnssec"` // 是否使用了DNSSEC
EDNS bool `json:"edns"` // 是否使用了EDNS
DNSServer string `json:"dnsServer"` // 使用的DNS服务器
DNSSECServer string `json:"dnssecServer"` // 使用的DNSSEC专用服务器
Answers []DNSAnswer `json:"answers"` // 解析记录
ResponseCode int `json:"responseCode"` // DNS响应代码
}
// StatsData 用于持久化的统计数据结构
@@ -348,6 +368,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, dns.RcodeNameError)
logger.Debug("IPv6解析已禁用拒绝AAAA记录查询", "domain", domain)
return
}
}
logger.Debug("接收到DNS查询", "domain", domain, "type", queryType, "client", w.RemoteAddr())
@@ -370,7 +413,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, dns.RcodeRefused)
return
}
@@ -386,8 +429,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
}
})
// 添加查询日志
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, false, true, "缓存", "无")
// 该方法内部未直接调用addQueryLog而是在handleDNSRequest中处理
return
}
@@ -408,8 +450,16 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
}
})
// 添加查询日志
s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false, false, true, "无", "无")
// 添加查询日志 - 被屏蔽域名
blockedAnswers := []DNSAnswer{}
// 根据屏蔽方法确定响应代码
blockedRcode := dns.RcodeNameError // 默认NXDOMAIN
if blockMethod := s.shieldConfig.BlockMethod; blockMethod == "refused" {
blockedRcode = dns.RcodeRefused
} else if blockMethod == "emptyIP" || blockMethod == "customIP" {
blockedRcode = dns.RcodeSuccess
}
s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false, false, true, "无", "无", blockedAnswers, blockedRcode)
return
}
@@ -481,8 +531,25 @@ 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, "缓存", "无")
// 从缓存响应中获取响应代码
cacheRcode := dns.RcodeSuccess // 默认成功
if cachedResponse != nil {
cacheRcode = cachedResponse.Rcode
}
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true, cachedDNSSEC, true, "缓存", "无", cachedAnswers, cacheRcode)
logger.Debug("从缓存返回DNS响应", "domain", domain, "type", queryType, "dnssec", cachedDNSSEC)
return
}
@@ -566,8 +633,25 @@ 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)
// 从响应中获取响应代码
realRcode := dns.RcodeSuccess // 默认成功
if response != nil {
realRcode = response.Rcode
}
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, responseDNSSEC, true, dnsServer, dnssecServer, responseAnswers, realRcode)
}
// handleHostsResponse 处理hosts文件匹配的响应
@@ -731,14 +815,35 @@ 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
// 确保DNSSEC服务器地址包含端口号
normalizedDnssecServer := normalizeDNSServerAddress(dnssecServer)
for _, upstream := range finalUpstreamDNS {
if upstream == normalizedDnssecServer {
hasDuplicate = true
break
}
}
if !hasDuplicate {
finalUpstreamDNS = append(finalUpstreamDNS, normalizedDnssecServer)
}
}
logger.Debug("合并DNSSEC专用服务器到上游服务器列表", "servers", finalUpstreamDNS)
}
// 使用最终合并后的服务器列表
selectedUpstreamDNS = finalUpstreamDNS
}
// 1. 首先尝试所有配置的上游DNS服务器
@@ -769,8 +874,8 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
go func(server string) {
defer wg.Done()
// 发送请求并获取响应
response, rtt, err := s.resolver.Exchange(r, server)
// 发送请求并获取响应,确保服务器地址包含端口号
response, rtt, err := s.resolver.Exchange(r, normalizeDNSServerAddress(server))
select {
case responses <- serverResponse{response, rtt, server, err}:
@@ -825,55 +930,103 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
resp.response.AuthenticatedData = false
}
// 如果响应成功或为NXDOMAIN根据DNSSEC状态选择最佳响应
if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError {
// 检查当前使用的服务器是否是DNSSEC专用服务器
for _, dnssecServer := range dnssecServers {
if dnssecServer == resp.server {
usedDNSSECServer = resp.server
break
}
// 检查当前服务器是否是DNSSEC专用服务器
for _, dnssecServer := range dnssecServers {
if dnssecServer == resp.server {
usedDNSSECServer = resp.server
break
}
}
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 resp.response.Rcode == dns.RcodeSuccess {
// 成功响应,优先使用
if isUserUpstream {
// 用户配置的主DNS服务器响应直接设置为最佳响应
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记录
if !hasBestResponse || !isUserUpstream {
// 如果还没有最佳响应,或者当前最佳响应不是用户配置的服务器,则更新
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
hasDNSSECResponse = true
usedDNSServer = resp.server
logger.Debug("找到带DNSSEC的最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
} else if !hasBestResponse {
// 没有带DNSSEC的响应时保存第一个成功响应
}
} else {
// 非用户配置服务器没有DNSSEC记录
if !hasBestResponse {
// 如果还没有最佳响应,设置为最佳响应
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
logger.Debug("找到最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
}
} else if resp.response.Rcode == dns.RcodeNameError {
// 处理NXDOMAIN响应
// 如果还没有最佳响应或者最佳响应也是NXDOMAIN优先选择更快的NXDOMAIN响应
if !hasBestResponse || bestResponse.Rcode == dns.RcodeNameError {
// 如果还没有最佳响应,或者当前响应更快,更新最佳响应
if !hasBestResponse || resp.rtt < bestRtt {
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
logger.Debug("找到NXDOMAIN最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
}
}
} else if resp.response.Rcode == dns.RcodeNameError {
// NXDOMAIN响应
if !hasBestResponse || bestResponse.Rcode == dns.RcodeNameError {
// 如果还没有最佳响应,或者最佳响应也是NXDOMAIN
if isUserUpstream {
// 用户配置的服务器,直接使用
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
logger.Debug("使用用户配置的上游服务器NXDOMAIN响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
} else if !hasBestResponse || resp.rtt < bestRtt {
// 非用户配置服务器,选择更快的响应
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
logger.Debug("找到NXDOMAIN最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt)
}
}
// 保存为备选响应
}
// 更新备选响应,确保总有一个可用的响应
if resp.response != nil {
if !hasBackup {
// 第一次保存备选响应
backupResponse = resp.response
backupRtt = resp.rtt
hasBackup = true
} else {
// 后续响应,优先保存用户配置的服务器响应作为备选
if isUserUpstream {
backupResponse = resp.response
backupRtt = resp.rtt
}
}
}
// 即使响应不是成功或NXDOMAIN也保存为最佳响应如果还没有的话
// 确保总有一个响应返回给客户端
if !hasBestResponse {
bestResponse = resp.response
bestRtt = resp.rtt
hasBestResponse = true
usedDNSServer = resp.server
logger.Debug("使用非成功响应作为最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt, "rcode", resp.response.Rcode)
}
} else {
// 更新服务器统计信息(失败)
s.updateServerStats(resp.server, false, 0)
@@ -882,9 +1035,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()
@@ -897,7 +1073,7 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
}, 1)
go func() {
response, rtt, err := s.resolver.Exchange(r, selectedServer)
response, rtt, err := s.resolver.Exchange(r, normalizeDNSServerAddress(selectedServer))
resultChan <- struct {
response *dns.Msg
rtt time.Duration
@@ -997,10 +1173,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)
}
}
@@ -1021,7 +1199,7 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
}, 1)
go func() {
resp, r, e := s.resolver.Exchange(r, fastestServer)
resp, r, e := s.resolver.Exchange(r, normalizeDNSServerAddress(fastestServer))
resultChan <- struct {
response *dns.Msg
rtt time.Duration
@@ -1143,7 +1321,7 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
defer wg.Done()
// 发送请求并获取响应
response, rtt, err := s.resolver.Exchange(r, server)
response, rtt, err := s.resolver.Exchange(r, normalizeDNSServerAddress(server))
select {
case responses <- serverResponse{response, rtt, server, err}:
@@ -1284,7 +1462,7 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
}, 1)
go func() {
response, rtt, err := s.resolver.Exchange(r, selectedDnssecServer)
response, rtt, err := s.resolver.Exchange(r, normalizeDNSServerAddress(selectedDnssecServer))
resultChan <- struct {
response *dns.Msg
rtt time.Duration
@@ -1382,7 +1560,7 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
}, 1)
go func() {
resp, r, e := s.resolver.Exchange(r, localServer)
resp, r, e := s.resolver.Exchange(r, normalizeDNSServerAddress(localServer))
resultChan <- struct {
response *dns.Msg
rtt time.Duration
@@ -1967,7 +2145,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, responseCode int) {
// 获取IP地理位置
location := s.getIpGeolocation(clientIP)
@@ -1987,6 +2165,8 @@ func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime in
EDNS: edns,
DNSServer: dnsServer,
DNSSECServer: dnssecServer,
Answers: answers,
ResponseCode: responseCode,
}
// 添加到日志列表
@@ -2441,12 +2621,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 +2691,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 +2736,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 +2922,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 {

12
download.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
set -e -f -u -x
# This script syncs companies DB that we bundle with AdGuard Home. The source
# for this database is https://github.com/AdguardTeam/companiesdb.
#
trackers_url='https://raw.githubusercontent.com/AdguardTeam/companiesdb/main/dist/trackers.json'
output='./trackers.json'
readonly trackers_url output
curl -o "$output" -v "$trackers_url"

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()

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

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

@@ -18,9 +18,67 @@
<body class="bg-gray-50 text-dark font-sans">
<div class="flex h-screen overflow-hidden">
<!-- 侧边栏 -->
<aside id="sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 md:relative md:translate-x-0 -translate-x-full shadow-lg">
<aside id="sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 hidden md:flex">
<!-- Logo -->
<div class="flex items-center justify-center h-16 border-b border-gray-200">
<i class="fa fa-server text-3xl text-primary mr-3"></i>
<h1 class="text-xl font-bold text-primary">DNS 控制台</h1>
</div>
<!-- 菜单 -->
<nav class="flex-1 overflow-y-auto p-4">
<ul class="space-y-1">
<li>
<a href="#dashboard" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all sidebar-item-active">
<i class="fa fa-tachometer mr-3 text-lg"></i>
<span>仪表盘</span>
</a>
</li>
<li>
<a href="#shield" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
<i class="fa fa-shield mr-3 text-lg"></i>
<span>屏蔽管理</span>
</a>
</li>
<li>
<a href="#hosts" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
<i class="fa fa-file-text mr-3 text-lg"></i>
<span>Hosts管理</span>
</a>
</li>
<li>
<a href="#query" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
<i class="fa fa-search mr-3 text-lg"></i>
<span>DNS屏蔽查询</span>
</a>
</li>
<li>
<a href="#logs" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
<i class="fa fa-file-text-o mr-3 text-lg"></i>
<span>查询日志</span>
</a>
</li>
<li>
<a href="#config" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
<i class="fa fa-cog mr-3 text-lg"></i>
<span>系统设置</span>
</a>
</li>
</ul>
</nav>
<!-- 底部信息 -->
<div class="p-4 border-t border-gray-200 text-center text-gray-500 text-sm">
<p>DNS服务器 v1.2.0</p>
<p class="mt-1" id="uptime">正常运行中</p>
</div>
</aside>
<!-- 移动端侧边栏 -->
<aside id="mobile-sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 -translate-x-full md:hidden">
<!-- 移动端关闭按钮 -->
<div class="absolute top-4 right-4 md:hidden">
<div class="absolute top-4 right-4">
<button id="close-sidebar" class="p-2 text-gray-500 hover:text-gray-700 focus:outline-none">
<i class="fa fa-times text-xl"></i>
</button>
@@ -85,9 +143,9 @@
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-40 hidden md:hidden"></div>
<!-- 主内容区 -->
<main class="flex-1 overflow-y-auto">
<main class="flex-1 flex flex-col md:ml-64">
<!-- 顶部导航栏 -->
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6">
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 sticky top-0 z-30">
<div class="flex items-center">
<button id="toggle-sidebar" class="block md:hidden text-gray-500 hover:text-gray-700 focus:outline-none">
<i class="fa fa-bars text-xl"></i>
@@ -96,55 +154,6 @@
</div>
<div class="flex items-center space-x-4">
<!-- 服务器状态组件 -->
<div class="relative bg-white rounded-lg shadow-md px-3 py-2 flex items-center space-x-2 server-status-widget md:min-w-[300px] sm:min-w-[250px] min-w-[180px]" id="server-status-widget">
<div class="flex flex-col">
<div class="flex items-center">
<span class="text-xs font-medium text-gray-500">CPU</span>
<span id="server-cpu-value" class="ml-2 text-sm font-semibold">0%</span>
</div>
<div class="w-16 h-1 bg-gray-100 rounded-full mt-1">
<div id="server-cpu-bar" class="h-full bg-warning rounded-full" style="width: 0%"></div>
</div>
</div>
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
<div class="flex flex-col">
<div class="flex items-center">
<span class="text-xs font-medium text-gray-500">查询</span>
<span id="server-queries-value" class="ml-2 text-sm font-semibold">0</span>
</div>
<div class="w-16 h-1 bg-gray-100 rounded-full mt-1">
<div id="server-queries-bar" class="h-full bg-primary rounded-full" style="width: 0%"></div>
</div>
</div>
<!-- 额外指标区域 - 初始隐藏,只在非首页显示 -->
<div id="server-additional-stats" class="hidden md:flex items-center">
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
<div class="flex flex-col">
<div class="flex items-center">
<span class="text-xs font-medium text-gray-500">总量</span>
<span id="server-total-queries" class="ml-2 text-sm font-semibold">0</span>
</div>
</div>
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
<div class="flex flex-col">
<div class="flex items-center">
<span class="text-xs font-medium text-gray-500">屏蔽</span>
<span id="server-blocked-queries" class="ml-2 text-sm font-semibold">0</span>
</div>
</div>
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
<div class="flex flex-col">
<div class="flex items-center">
<span class="text-xs font-medium text-gray-500">正常</span>
<span id="server-allowed-queries" class="ml-2 text-sm font-semibold">0</span>
</div>
</div>
</div>
<div class="absolute top-1 right-1">
<span id="server-status-indicator" class="inline-block w-2 h-2 bg-success rounded-full"></span>
</div>
</div>
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
<i class="fa fa-bell text-lg"></i>
@@ -170,7 +179,7 @@
</header>
<!-- 页面内容 -->
<div class="p-6">
<div class="p-6 overflow-y-auto flex-1">
<!-- 仪表盘部分 -->
<div id="dashboard-content" class="space-y-6">
<!-- 统计卡片 -->
@@ -975,13 +984,12 @@
</div>
</th>
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">响应时间</th>
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">屏蔽规则</th>
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">操作</th>
</tr>
</thead>
<tbody id="logs-table-body">
<tr>
<td colspan="6" class="py-8 text-center text-gray-500 border-b border-gray-100">
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
<div>暂无查询日志</div>
</td>
@@ -1050,13 +1058,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 +1088,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 +1122,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>
@@ -1155,7 +1174,6 @@
<script src="js/main.js"></script>
<script src="js/api.js"></script>
<script src="js/dashboard.js"></script>
<script src="js/server-status.js"></script>
<script src="js/shield.js"></script>
<script src="js/hosts.js"></script>
<script src="js/query.js"></script>

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 ''; // 默认返回空字符串

View File

@@ -2964,33 +2964,10 @@ window.addEventListener('hashchange', handleHashChange);
// 初始化hash路由 - 确保在页面加载时就能被调用
initHashRoute();
// 侧边栏切换
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('-translate-x-full');
}
// 响应式处理
function handleResponsive() {
const toggleBtn = document.getElementById('toggle-sidebar');
const sidebar = document.getElementById('sidebar');
toggleBtn.addEventListener('click', toggleSidebar);
// 初始状态处理
function updateSidebarState() {
if (window.innerWidth < 1024) {
sidebar.classList.add('-translate-x-full');
} else {
sidebar.classList.remove('-translate-x-full');
}
}
updateSidebarState();
// 窗口大小改变时处理
window.addEventListener('resize', () => {
updateSidebarState();
// 更新所有图表大小
if (dnsRequestsChart) {

0
static/js/guide.js Normal file
View File

View File

@@ -107,13 +107,25 @@ function initLogsPage() {
// 绑定事件
bindLogsEvents();
// 初始化日志详情弹窗
initLogDetailModal();
// 建立WebSocket连接用于实时更新统计数据和图表
connectLogsWebSocket();
// 窗口大小改变时重新加载日志表格
window.addEventListener('resize', handleWindowResize);
// 在页面卸载时清理资源
window.addEventListener('beforeunload', cleanupLogsResources);
}
// 处理窗口大小改变
function handleWindowResize() {
// 重新加载日志表格,以适应新的屏幕尺寸
loadLogs();
}
// 清理资源
function cleanupLogsResources() {
// 清除WebSocket连接
@@ -127,6 +139,9 @@ function cleanupLogsResources() {
clearTimeout(logsWsReconnectTimer);
logsWsReconnectTimer = null;
}
// 清除窗口大小改变事件监听器
window.removeEventListener('resize', handleWindowResize);
}
// 绑定事件
@@ -425,13 +440,16 @@ async function updateLogsTable(logs) {
return;
}
// 检测是否为移动设备
const isMobile = window.innerWidth <= 768;
// 填充表格
for (const log of logs) {
const row = document.createElement('tr');
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
// 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
const time = new Date(log.Timestamp);
const time = new Date(log.timestamp);
const formattedDate = time.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
@@ -445,13 +463,13 @@ async function updateLogsTable(logs) {
// 根据结果添加不同的背景色
let rowClass = '';
switch (log.Result) {
switch (log.result) {
case 'blocked':
rowClass = 'bg-red-50'; // 淡红色填充
break;
case 'allowed':
// 检查是否是规则允许项目
if (log.BlockRule && log.BlockRule.includes('allow')) {
if (log.blockRule && log.blockRule.includes('allow')) {
rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充
} else {
rowClass = ''; // 允许的不填充
@@ -469,7 +487,7 @@ async function updateLogsTable(logs) {
// 添加被屏蔽或允许显示,并增加颜色
let statusText = '';
let statusClass = '';
switch (log.Result) {
switch (log.result) {
case 'blocked':
statusText = '被屏蔽';
statusClass = 'text-danger';
@@ -488,16 +506,16 @@ async function updateLogsTable(logs) {
}
// 检查域名是否在跟踪器数据库中
const trackerInfo = await isDomainInTrackerDatabase(log.Domain);
const trackerInfo = await isDomainInTrackerDatabase(log.domain);
const isTracker = trackerInfo !== null;
// 构建行内容 - 两行显示,时间列显示时间和日期,请求列显示域名和类型状态
// 构建行内容 - 根据设备类型决定显示内容
// 添加缓存状态显示
const cacheStatusClass = log.FromCache ? 'text-primary' : 'text-gray-500';
const cacheStatusText = log.FromCache ? '缓存' : '非缓存';
const cacheStatusClass = log.fromCache ? 'text-primary' : 'text-gray-500';
const cacheStatusText = log.fromCache ? '缓存' : '非缓存';
// 检查域名是否被拦截
const isBlocked = log.Result === 'blocked';
const isBlocked = log.result === 'blocked';
// 构建跟踪器浮窗内容
const trackerTooltip = isTracker ? `
@@ -510,36 +528,58 @@ 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="4">
<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-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 +616,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 +982,467 @@ async function unblockDomain(domain) {
}
}
// 独立的DNS记录格式化函数
function formatDNSRecords(log, result) {
if (result === 'blocked') return '无';
let records = '';
const sources = [
log.answers,
log.answer,
log.Records,
log.records,
log.response
];
for (const source of sources) {
if (records) break;
if (!source || source === '无') continue;
// 处理数组类型
if (Array.isArray(source)) {
records = source.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 || '未知';
// 增强的记录值提取逻辑
if (typeof value === 'string') {
value = value.trim();
// 处理制表符分隔的格式
if (value.includes('\t') || value.includes('\\t')) {
const parts = value.replace(/\\t/g, '\t').split('\t');
if (parts.length >= 4) {
value = parts[parts.length - 1].trim();
}
}
// 处理JSON格式
else if (value.startsWith('{') && value.endsWith('}')) {
try {
const parsed = JSON.parse(value);
value = parsed.data || parsed.value || value;
} catch (e) {}
}
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
}
// 处理字符串类型
else if (typeof source === 'string') {
// 尝试解析为JSON数组
if (source.startsWith('[') && source.endsWith(']')) {
try {
const parsed = JSON.parse(source);
if (Array.isArray(parsed)) {
records = parsed.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 || '未知';
if (typeof value === 'string') {
value = value.trim();
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
}
} catch (e) {
// 解析失败,尝试直接格式化
records = formatDNSString(source);
}
} else {
// 直接格式化字符串
records = formatDNSString(source);
}
}
}
return records || '无解析记录';
}
// 格式化DNS字符串记录
function formatDNSString(str) {
// 处理可能的转义字符并分割行
const recordLines = str.split(/\r?\n/).map(line => line.replace(/^\s+/, '')).filter(line => line.trim() !== '');
return recordLines.map(line => {
// 检查是否已经是标准格式
if (line.includes(':') && line.includes('(')) {
return line;
}
// 尝试解析为标准DNS格式
const parts = line.split(/\s+/);
if (parts.length >= 5) {
const type = parts[3];
const value = parts.slice(4).join(' ');
const ttl = parts[1];
return `${type}: ${value} (ttl=${ttl})`;
}
// 无法解析,返回原始行但移除前导空格
return line.replace(/^\s+/, '');
}).join('\n');
}
// 显示日志详情弹窗
async function showLogDetailModal(log) {
console.log('showLogDetailModal called with log:', JSON.stringify(log, null, 2)); // 输出完整的log对象
if (!log) {
console.error('No log data provided!');
return;
}
try {
// 安全获取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 dnsRecords = formatDNSRecords(log, result);
// 创建模态框容器
const modalContainer = document.createElement('div');
modalContainer.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 animate-fade-in';
modalContainer.style.zIndex = '9999';
// 创建模态框内容
const modalContent = document.createElement('div');
modalContent.className = 'bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto animate-slide-in';
// 创建标题栏
const header = document.createElement('div');
header.className = 'sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center';
const title = document.createElement('h3');
title.className = 'text-xl font-semibold text-gray-900';
title.textContent = '日志详情';
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 transition-colors';
closeButton.onclick = () => closeModal();
header.appendChild(title);
header.appendChild(closeButton);
// 创建内容区域
const content = document.createElement('div');
content.className = 'p-6 space-y-6';
// 基本信息部分
const basicInfo = document.createElement('div');
basicInfo.className = 'space-y-4';
const basicInfoTitle = document.createElement('h4');
basicInfoTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider';
basicInfoTitle.textContent = '基本信息';
const basicInfoGrid = document.createElement('div');
basicInfoGrid.className = 'grid grid-cols-1 md:grid-cols-2 gap-4';
// 添加基本信息项
basicInfoGrid.innerHTML = `
<div class="space-y-1">
<div class="text-xs text-gray-500">日期</div>
<div class="text-sm font-medium text-gray-900">${dateStr}</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">时间</div>
<div class="text-sm font-medium text-gray-900">${timeStr}</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">状态</div>
<div class="text-sm font-medium ${result === 'blocked' ? 'text-red-600' : result === 'allowed' ? 'text-green-600' : 'text-gray-500'}">
${result === 'blocked' ? '已拦截' : result === 'allowed' ? '允许' : result}
</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">域名</div>
<div class="text-sm font-medium text-gray-900 break-all">${domain}</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">类型</div>
<div class="text-sm font-medium text-gray-900">${queryType}</div>
</div>
`;
// DNS特性
const dnsFeatures = document.createElement('div');
dnsFeatures.className = 'col-span-1 md:col-span-2 space-y-1';
dnsFeatures.innerHTML = `
<div class="text-xs text-gray-500">DNS特性</div>
<div class="text-sm font-medium text-gray-900">
${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>
`;
// 跟踪器信息
const trackerDiv = document.createElement('div');
trackerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
trackerDiv.innerHTML = `
<div class="text-xs text-gray-500">跟踪器信息</div>
<div class="text-sm font-medium text-gray-900">
${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>
`;
// 解析记录
const recordsDiv = document.createElement('div');
recordsDiv.className = 'col-span-1 md:col-span-2 space-y-1';
recordsDiv.innerHTML = `
<div class="text-xs text-gray-500">解析记录</div>
<div class="text-sm font-medium text-gray-900 whitespace-pre-wrap break-all bg-gray-50 p-3 rounded-md border border-gray-200">${dnsRecords}</div>
`;
// DNS服务器
const dnsServerDiv = document.createElement('div');
dnsServerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
dnsServerDiv.innerHTML = `
<div class="text-xs text-gray-500">DNS服务器</div>
<div class="text-sm font-medium text-gray-900">${dnsServer}</div>
`;
// DNSSEC专用服务器
const dnssecServerDiv = document.createElement('div');
dnssecServerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
dnssecServerDiv.innerHTML = `
<div class="text-xs text-gray-500">DNSSEC专用服务器</div>
<div class="text-sm font-medium text-gray-900">${dnssecServer}</div>
`;
basicInfoGrid.appendChild(dnsFeatures);
basicInfoGrid.appendChild(trackerDiv);
basicInfoGrid.appendChild(recordsDiv);
basicInfoGrid.appendChild(dnsServerDiv);
basicInfoGrid.appendChild(dnssecServerDiv);
basicInfo.appendChild(basicInfoTitle);
basicInfo.appendChild(basicInfoGrid);
// 响应细节部分
const responseDetails = document.createElement('div');
responseDetails.className = 'space-y-4 pt-4 border-t border-gray-200';
const responseDetailsTitle = document.createElement('h4');
responseDetailsTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider';
responseDetailsTitle.textContent = '响应细节';
// 准备响应细节内容,根据条件添加规则信息
let responseDetailsHTML = `
<div class="space-y-1">
<div class="text-xs text-gray-500">响应时间</div>
<div class="text-sm font-medium text-gray-900">${responseTime}毫秒</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">响应代码</div>
<div class="text-sm font-medium text-gray-900">${getResponseCodeText(log.responseCode)}</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">缓存状态</div>
<div class="text-sm font-medium ${fromCache ? 'text-primary' : 'text-gray-500'}">
${fromCache ? '缓存' : '非缓存'}
</div>
</div>
`;
// 只有被屏蔽或者有自定义规则时才显示规则信息
if (result === 'blocked' || (blockRule && blockRule !== '无' && blockRule !== '-')) {
responseDetailsHTML += `
<div class="space-y-1">
<div class="text-xs text-gray-500">规则</div>
<div class="text-sm font-medium text-gray-900">${blockRule || '-'}</div>
</div>
`;
}
const responseGrid = document.createElement('div');
responseGrid.className = 'grid grid-cols-1 md:grid-cols-2 gap-4';
responseGrid.innerHTML = responseDetailsHTML;
responseDetails.appendChild(responseDetailsTitle);
responseDetails.appendChild(responseGrid);
// 客户端详情部分
const clientDetails = document.createElement('div');
clientDetails.className = 'space-y-4 pt-4 border-t border-gray-200';
const clientDetailsTitle = document.createElement('h4');
clientDetailsTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider';
clientDetailsTitle.textContent = '客户端详情';
const clientIPDiv = document.createElement('div');
clientIPDiv.className = 'space-y-1';
clientIPDiv.innerHTML = `
<div class="text-xs text-gray-500">IP地址</div>
<div class="text-sm font-medium text-gray-900">${clientIP} (${location})</div>
`;
clientDetails.appendChild(clientDetailsTitle);
clientDetails.appendChild(clientIPDiv);
// 组装内容
content.appendChild(basicInfo);
content.appendChild(responseDetails);
content.appendChild(clientDetails);
// 组装模态框
modalContent.appendChild(header);
modalContent.appendChild(content);
modalContainer.appendChild(modalContent);
// 添加到页面
document.body.appendChild(modalContainer);
// 关闭模态框函数
function closeModal() {
modalContainer.classList.add('animate-fade-out');
modalContent.classList.add('animate-slide-out');
// 等待动画结束后移除元素
setTimeout(() => {
document.body.removeChild(modalContainer);
}, 300);
}
// 点击外部关闭
modalContainer.addEventListener('click', (e) => {
if (e.target === modalContainer) {
closeModal();
}
});
// ESC键关闭
const handleEsc = (e) => {
if (e.key === 'Escape') {
closeModal();
document.removeEventListener('keydown', handleEsc);
}
};
document.addEventListener('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 p-4 animate-fade-in';
errorModal.style.zIndex = '9999';
const errorContent = document.createElement('div');
errorContent.className = 'bg-white rounded-xl shadow-2xl p-6 w-full max-w-md animate-slide-in';
errorContent.innerHTML = `
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-gray-900">错误</h3>
<button onclick="closeErrorModal()" class="text-gray-500 hover:text-gray-700 focus:outline-none transition-colors">
<i class="fa fa-times text-xl"></i>
</button>
</div>
<div class="text-red-600 text-sm">
加载日志详情失败: ${error.message}
</div>
`;
errorModal.appendChild(errorContent);
document.body.appendChild(errorModal);
// 关闭错误模态框函数
function closeErrorModal() {
errorModal.classList.add('animate-fade-out');
errorContent.classList.add('animate-slide-out');
// 等待动画结束后移除元素
setTimeout(() => {
document.body.removeChild(errorModal);
}, 300);
}
// ESC键关闭错误模态框
const handleErrorEsc = (e) => {
if (e.key === 'Escape') {
closeErrorModal();
document.removeEventListener('keydown', handleErrorEsc);
}
};
document.addEventListener('keydown', handleErrorEsc);
}
}
// 关闭日志详情弹窗
// 获取响应代码文本
function getResponseCodeText(rcode) {
const rcodeMap = {
0: 'NOERROR',
1: 'FORMERR',
2: 'SERVFAIL',
3: 'NXDOMAIN',
4: 'NOTIMP',
5: 'REFUSED',
6: 'YXDOMAIN',
7: 'YXRRSET',
8: 'NXRRSET',
9: 'NOTAUTH',
10: 'NOTZONE'
};
return rcodeMap[rcode] || `UNKNOWN(${rcode})`;
}
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(() => {
// 只有在查询日志页面时才更新

View File

@@ -28,7 +28,7 @@ function setupNavigation() {
// 移动端侧边栏切换
const toggleSidebar = document.getElementById('toggle-sidebar');
const closeSidebarBtn = document.getElementById('close-sidebar');
const sidebar = document.getElementById('sidebar');
const sidebar = document.getElementById('mobile-sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
// 打开侧边栏函数

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"