diff --git a/CHANGELOG.md b/CHANGELOG.md index 128a93c..fa32364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/)规范。 \ No newline at end of file +版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)规范。 diff --git a/build-windows.sh b/build-windows.sh new file mode 100755 index 0000000..f070b7e --- /dev/null +++ b/build-windows.sh @@ -0,0 +1,5 @@ +CGO_ENABLED=1 \ +GOOS=windows \ +GOARCH=amd64 \ +CC=gcc \ +go build -o dns-server.exe main.go diff --git a/config.json b/config.json index 70da260..3373ce1 100644 --- a/config.json +++ b/config.json @@ -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, diff --git a/config/config.go b/config/config.go index c95db85..28961c4 100644 --- a/config/config.go +++ b/config/config.go @@ -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 { diff --git a/dns/server.go b/dns/server.go index 431e64f..d6374b0 100644 --- a/dns/server.go +++ b/dns/server.go @@ -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 { diff --git a/download.sh b/download.sh new file mode 100755 index 0000000..9b10210 --- /dev/null +++ b/download.sh @@ -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" diff --git a/http/server.go b/http/server.go index 8c175b6..29833dd 100644 --- a/http/server.go +++ b/http/server.go @@ -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 != "" { // 验证屏蔽方法是否有效 diff --git a/main.go b/main.go index 20bfef7..960a439 100644 --- a/main.go +++ b/main.go @@ -42,7 +42,6 @@ func createDefaultConfig(configFile string) error { "1.1.1.1:53" ], "timeout": 5000, - "statsFile": "./data/stats.json", "saveInterval": 300, "cacheTTL": 30, "enableDNSSEC": true, @@ -56,7 +55,6 @@ func createDefaultConfig(configFile string) error { "password": "admin" }, "shield": { - "localRulesFile": "data/rules.txt", "blacklists": [ { "name": "AdGuard DNS filter", @@ -80,15 +78,11 @@ func createDefaultConfig(configFile string) error { } ], "updateInterval": 3600, - "hostsFile": "data/hosts.txt", "blockMethod": "NXDOMAIN", "customBlockIP": "", - "statsFile": "./data/shield_stats.json", - "statsSaveInterval": 60, - "remoteRulesCacheDir": "./data/remote_rules" + "statsSaveInterval": 60 }, "log": { - "file": "logs/dns-server.log", "level": "debug", "maxSize": 100, "maxBackups": 10, @@ -109,12 +103,12 @@ func createRequiredFiles(cfg *config.Config) error { } // 创建远程规则缓存文件夹 - if err := os.MkdirAll(cfg.Shield.RemoteRulesCacheDir, 0755); err != nil { + if err := os.MkdirAll("data/remote_rules", 0755); err != nil { return fmt.Errorf("创建远程规则缓存文件夹失败: %w", err) } // 创建日志文件夹 - logDir := filepath.Dir(cfg.Log.File) + logDir := filepath.Dir("logs/dns-server.log") if logDir != "." { if err := os.MkdirAll(logDir, 0755); err != nil { return fmt.Errorf("创建日志文件夹失败: %w", err) @@ -122,29 +116,29 @@ func createRequiredFiles(cfg *config.Config) error { } // 创建自定义规则文件 - if _, err := os.Stat(cfg.Shield.LocalRulesFile); os.IsNotExist(err) { - if err := os.WriteFile(cfg.Shield.LocalRulesFile, []byte("# 自定义规则文件\n# 格式:域名\n# 例如:example.com\n"), 0644); err != nil { + if _, err := os.Stat("data/rules.txt"); os.IsNotExist(err) { + if err := os.WriteFile("data/rules.txt", []byte("# 自定义规则文件\n# 格式:域名\n# 例如:example.com\n"), 0644); err != nil { return fmt.Errorf("创建自定义规则文件失败: %w", err) } } // 创建Hosts文件 - if _, err := os.Stat(cfg.Shield.HostsFile); os.IsNotExist(err) { - if err := os.WriteFile(cfg.Shield.HostsFile, []byte("# Hosts文件\n# 格式:IP 域名\n# 例如:127.0.0.1 localhost\n"), 0644); err != nil { + if _, err := os.Stat("data/hosts.txt"); os.IsNotExist(err) { + if err := os.WriteFile("data/hosts.txt", []byte("# Hosts文件\n# 格式:IP 域名\n# 例如:127.0.0.1 localhost\n"), 0644); err != nil { return fmt.Errorf("创建Hosts文件失败: %w", err) } } // 创建统计数据文件 - if _, err := os.Stat(cfg.DNS.StatsFile); os.IsNotExist(err) { - if err := os.WriteFile(cfg.DNS.StatsFile, []byte("{}"), 0644); err != nil { + if _, err := os.Stat("data/stats.json"); os.IsNotExist(err) { + if err := os.WriteFile("data/stats.json", []byte("{}"), 0644); err != nil { return fmt.Errorf("创建统计数据文件失败: %w", err) } } // 创建Shield统计数据文件 - if _, err := os.Stat(cfg.Shield.StatsFile); os.IsNotExist(err) { - if err := os.WriteFile(cfg.Shield.StatsFile, []byte("{}"), 0644); err != nil { + if _, err := os.Stat("data/shield_stats.json"); os.IsNotExist(err) { + if err := os.WriteFile("data/shield_stats.json", []byte("{}"), 0644); err != nil { return fmt.Errorf("创建Shield统计数据文件失败: %w", err) } } @@ -183,7 +177,7 @@ func main() { log.Println("所需文件和文件夹创建成功") // 初始化日志系统 - if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0, false); err != nil { + if err := logger.InitLogger("logs/dns-server.log", cfg.Log.Level, 0, 0, 0, false); err != nil { log.Fatalf("初始化日志系统失败: %v", err) } defer logger.Close() diff --git a/shield/manager.go b/shield/manager.go index 44eae44..15e82cc 100644 --- a/shield/manager.go +++ b/shield/manager.go @@ -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 { diff --git a/static/css/style.css b/static/css/style.css index 63fde8c..1bdb257 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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; + } } /* 浮窗内容项 */ diff --git a/static/index.html b/static/index.html index 6d2a0cb..9179dab 100644 --- a/static/index.html +++ b/static/index.html @@ -18,9 +18,67 @@
-