From 1f8bd6b97fa12dd974acaf933d9f611694566905 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Sun, 21 Dec 2025 21:18:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=9F=A5=E8=AF=A2=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E5=A2=9E=E5=8A=A0=E6=93=8D=E4=BD=9C=E5=88=97,?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B4=A3=E5=A2=9E=E5=8A=A0=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E8=A7=84=E5=88=99=E4=B8=8D=E7=94=9F=E6=95=88=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98,=E4=BF=AE=E5=A4=8D=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E8=A7=84=E5=88=99=E4=BC=98=E5=85=88=E7=BA=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 31 ++ README.md | 4 +- build.sh | 5 + config.json | 51 +- config/config.go | 38 +- dns/server.go | 1053 ++++++++++++++++++++++++++++++++++++---- http/server.go | 6 + main.go | 15 +- shield/manager.go | 170 +++++-- static/api/js/index.js | 16 +- static/index.html | 9 +- static/js/logs.js | 172 +++++++ static/js/shield.js | 14 +- temp_config.json | 52 ++ 14 files changed, 1467 insertions(+), 169 deletions(-) create mode 100755 build.sh create mode 100644 temp_config.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b3a933..fee0a06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ 所有对本项目的显著更改都将记录在此文件中。 +## [1.1.4] - 2025-12-21 + +### 修复 +- 修复规则优先级问题:确保自定义规则优先于远程规则 +- 修复添加自定义规则后需要重启服务器的问题:通过在添加或删除规则后清空DNS缓存实现 + +## [1.1.3] - 2025-12-19 + +### 移除 +- 移除search domain功能,不再支持自动添加域名前缀进行查询 +- 移除DNSConfig结构体中的PrefixDomain字段 +- 移除配置文件中的prefixDomain配置项 + +## [1.1.2] - 2025-12-19 + +### 添加 +- 添加不验证DNSSEC的域名功能,支持通过配置文件指定需要跳过DNSSEC验证的域名模式 +- 在DNSConfig结构体中增加NoDNSSECDomains字段,用于存储不验证DNSSEC的域名模式列表 + +### 修改 +- 在forwardDNSRequestWithCache函数中添加域名匹配逻辑,检查域名是否包含不验证DNSSEC的模式 +- 在所有查询模式(parallel、loadbalance、fastest-ip、default)中实现跳过DNSSEC验证的功能 + +## [1.1.1] - 2025-12-19 + +### 修改 +- 修复NXDOMAIN响应传播逻辑,确保上游DNS服务器返回的NXDOMAIN响应能正确传递给客户端 +- 优化loadbalance、fastest-ip和parallel查询模式下的NXDOMAIN响应选择机制 +- 确保不存在的域名能被正确识别并返回NXDOMAIN状态码 +- 修复服务器绑定地址配置,确保IPv4兼容性 + ## [1.0.0] - 2025-12-16 ### 添加 diff --git a/README.md b/README.md index 4090699..701b6e6 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ - 支持域名规则和正则表达式规则 - 支持规则例外 - 支持远程规则列表 -- 支持本地规则管理 +- 支持自定义规则管理 ### 3. 查询日志记录和统计 - 实时记录DNS查询日志 @@ -116,7 +116,7 @@ http://localhost:8080 1. 登录Web控制台 2. 点击左侧菜单中的"屏蔽管理" -3. 在"本地规则管理"中添加或删除规则 +3. 在"自定义规则管理"中添加或删除规则 4. 在"远程黑名单管理"中添加或删除远程规则列表 ### 查看查询日志 diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..6048767 --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +CGO_ENABLED=1 \ +GOOS=linux \ +GOARCH=amd64 \ +CC=gcc \ +go build -ldflags "-linkmode external -extldflags '-static -pthread'" -o dns-server main.go diff --git a/config.json b/config.json index 0ef6850..cb705b5 100644 --- a/config.json +++ b/config.json @@ -2,18 +2,47 @@ "dns": { "port": 53, "upstreamDNS": [ - "223.5.5.5:53", - "223.6.6.6:53", - "117.50.10.10:53" + "223.5.5.5:53" ], "dnssecUpstreamDNS": [ - "1.1.1.1:53" + "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 + "cacheTTL": 10, + "enableDNSSEC": true, + "queryMode": "loadbalance", + "domainSpecificDNS": { + "addr.arpa": [ + "10.35.10.200:53" + ], + "akadns": [ + "4.2.2.1:53" + ], + "akamai": [ + "4.2.2.1:53" + ], + "amazehome.cn": [ + "10.35.10.200:53" + ], + "amazehome.xyz": [ + "10.35.10.200:53" + ], + "microsoft.com": [ + "4.2.2.1:53" + ] + }, + "noDNSSECDomains": [ + "amazehome.cn", + "addr.arpa", + "amazehome.xyz", + ".cn" + ] }, "http": { "port": 8080, @@ -29,7 +58,7 @@ "name": "AdGuard DNS filter", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt", "enabled": true, - "lastUpdateTime": "2025-11-28T16:13:03.564Z" + "lastUpdateTime": "2025-12-21T10:46:36.629Z" }, { "name": "Adaway Default Blocklist", @@ -62,13 +91,14 @@ { "name": "Hate \u0026 Junk", "url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt", - "enabled": true + "enabled": true, + "lastUpdateTime": "2025-12-21T10:46:43.522Z" }, { "name": "My Gitlab Hosts", "url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt", "enabled": true, - "lastUpdateTime": "2025-11-29T17:11:28.130Z" + "lastUpdateTime": "2025-12-18T10:39:39.333Z" }, { "name": "Anti Remote Requests", @@ -83,7 +113,8 @@ { "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 + "enabled": true, + "lastUpdateTime": "2025-12-18T10:38:42.344Z" }, { "name": "My Gitlab Malware List", diff --git a/config/config.go b/config/config.go index a96255b..c95db85 100644 --- a/config/config.go +++ b/config/config.go @@ -5,16 +5,26 @@ import ( "io/ioutil" ) +// DomainSpecificDNS 域名特定DNS服务器配置 +// 格式:{"domainMatch": ["dns1", "dns2"]} +// domainMatch: 域名匹配字符串,当域名中包含该字符串时使用对应的DNS服务器 +// dns1, dns2: 用于解析匹配域名的DNS服务器列表 + +type DomainSpecificDNS map[string][]string + // DNSConfig DNS配置 type DNSConfig struct { - Port int `json:"port"` - UpstreamDNS []string `json:"upstreamDNS"` - DNSSECUpstreamDNS []string `json:"dnssecUpstreamDNS"` // 用于DNSSEC查询的专用服务器 - Timeout int `json:"timeout"` - StatsFile string `json:"statsFile"` // 统计数据持久化文件 - SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒) - CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟) - EnableDNSSEC bool `json:"enableDNSSEC"` // 是否启用DNSSEC支持 + Port int `json:"port"` + UpstreamDNS []string `json:"upstreamDNS"` + DNSSECUpstreamDNS []string `json:"dnssecUpstreamDNS"` // 用于DNSSEC查询的专用服务器 + Timeout int `json:"timeout"` + StatsFile string `json:"statsFile"` // 统计数据持久化文件 + SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒) + CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟) + EnableDNSSEC bool `json:"enableDNSSEC"` // 是否启用DNSSEC支持 + QueryMode string `json:"queryMode"` // 查询模式:"loadbalance"(负载均衡)、"parallel"(并行请求)、"fastest-ip"(最快的IP地址) + DomainSpecificDNS DomainSpecificDNS `json:"domainSpecificDNS"` // 域名特定DNS服务器配置 + NoDNSSECDomains []string `json:"noDNSSECDomains"` // 不验证DNSSEC的域名模式列表 } // HTTPConfig HTTP控制台配置 @@ -82,6 +92,9 @@ func LoadConfig(path string) (*Config, error) { if config.DNS.Port == 0 { config.DNS.Port = 53 } + if config.DNS.Timeout == 0 { + config.DNS.Timeout = 5000 // 默认超时时间为5000毫秒 + } if len(config.DNS.UpstreamDNS) == 0 { config.DNS.UpstreamDNS = []string{"223.5.5.5:53", "223.6.6.6:53"} } @@ -101,6 +114,15 @@ func LoadConfig(path string) (*Config, error) { if len(config.DNS.DNSSECUpstreamDNS) == 0 { config.DNS.DNSSECUpstreamDNS = []string{"8.8.8.8:53", "1.1.1.1:53"} } + // 查询模式默认配置 + if config.DNS.QueryMode == "" { + config.DNS.QueryMode = "parallel" // 默认使用并行请求模式 + } + // 域名特定DNS服务器配置默认值 + if config.DNS.DomainSpecificDNS == nil { + config.DNS.DomainSpecificDNS = make(DomainSpecificDNS) // 默认为空映射 + } + if config.HTTP.Port == 0 { config.HTTP.Port = 8080 } diff --git a/dns/server.go b/dns/server.go index 09f3b92..431e64f 100644 --- a/dns/server.go +++ b/dns/server.go @@ -59,6 +59,8 @@ type QueryLog struct { FromCache bool // 是否来自缓存 DNSSEC bool // 是否使用了DNSSEC EDNS bool // 是否使用了EDNS + DNSServer string // 使用的DNS服务器 + DNSSECServer string // 使用的DNSSEC专用服务器 } // StatsData 用于持久化的统计数据结构 @@ -73,6 +75,15 @@ type StatsData struct { LastSaved time.Time `json:"lastSaved"` } +// ServerStats 服务器统计信息 +type ServerStats struct { + SuccessCount int64 // 成功查询次数 + FailureCount int64 // 失败查询次数 + LastResponse time.Time // 最后响应时间 + ResponseTime time.Duration // 平均响应时间 + ConnectionSpeed time.Duration // TCP连接速度 +} + // Server DNS服务器 type Server struct { config *config.DNSConfig @@ -112,10 +123,14 @@ type Server struct { ipGeolocationCacheTTL time.Duration // 缓存有效期 // DNS查询缓存 - dnsCache *DNSCache // DNS响应缓存 + DnsCache *DNSCache // DNS响应缓存 // 域名DNSSEC状态映射表 domainDNSSECStatus map[string]bool // 域名到DNSSEC状态的映射 + + // 上游服务器状态跟踪 + serverStats map[string]*ServerStats // 服务器地址到状态的映射 + serverStatsMutex sync.RWMutex // 保护服务器状态的互斥锁 } // Stats DNS服务器统计信息 @@ -184,9 +199,11 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie ipGeolocationCache: make(map[string]*IPGeolocation), ipGeolocationCacheTTL: 24 * time.Hour, // 缓存有效期24小时 // DNS查询缓存初始化 - dnsCache: NewDNSCache(cacheTTL), + DnsCache: NewDNSCache(cacheTTL), // 初始化域名DNSSEC状态映射表 domainDNSSECStatus: make(map[string]bool), + // 初始化服务器状态跟踪 + serverStats: make(map[string]*ServerStats), } // 加载已保存的统计数据 @@ -215,14 +232,14 @@ func (s *Server) Start() error { s.startTime = time.Now() s.server = &dns.Server{ - Addr: fmt.Sprintf(":%d", s.config.Port), + Addr: fmt.Sprintf("0.0.0.0:%d", s.config.Port), Net: "udp", Handler: dns.HandlerFunc(s.handleDNSRequest), } // 保存TCP服务器实例,以便在Stop方法中关闭 s.tcpServer = &dns.Server{ - Addr: fmt.Sprintf(":%d", s.config.Port), + Addr: fmt.Sprintf("0.0.0.0:%d", s.config.Port), Net: "tcp", Handler: dns.HandlerFunc(s.handleDNSRequest), } @@ -339,7 +356,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { if r.RecursionDesired == false { response := new(dns.Msg) response.SetReply(r) - response.RecursionAvailable = true + // 不再硬编码RecursionAvailable,使用默认值或上游返回的值 response.SetRcode(r, dns.RcodeRefused) w.WriteMsg(response) @@ -353,7 +370,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, "", "") return } @@ -370,7 +387,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { }) // 添加查询日志 - s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, false, true) + s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, false, true, "缓存", "无") return } @@ -392,7 +409,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { }) // 添加查询日志 - s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false, false, true) + s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false, false, true, "无", "无") return } @@ -402,7 +419,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { var cachedDNSSEC bool // 1. 首先检查是否有普通缓存项 - if tempResponse, tempFound := s.dnsCache.Get(r.Question[0].Name, qType); tempFound { + if tempResponse, tempFound := s.DnsCache.Get(r.Question[0].Name, qType); tempFound { cachedResponse = tempResponse found = tempFound cachedDNSSEC = s.hasDNSSECRecords(tempResponse) @@ -465,13 +482,22 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { } // 添加查询日志 - 标记为缓存 - s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true, cachedDNSSEC, true) + s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true, cachedDNSSEC, true, "缓存", "无") logger.Debug("从缓存返回DNS响应", "domain", domain, "type", queryType, "dnssec", cachedDNSSEC) return } - // 缓存未命中,转发到上游DNS服务器 - response, rtt := s.forwardDNSRequestWithCache(r, domain) + // 缓存未命中,处理DNS请求 + var response *dns.Msg + var rtt time.Duration + var queryAttempts []string + var dnsServer string + var dnssecServer string + + // 直接查询原始域名 + queryAttempts = append(queryAttempts, domain) + response, rtt, dnsServer, dnssecServer = s.forwardDNSRequestWithCache(r, domain) + if response != nil { // 如果客户端请求包含EDNS记录,确保响应也包含EDNS if opt := r.IsEdns0(); opt != nil { @@ -536,19 +562,19 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { responseCopy := response.Copy() // 设置合理的TTL,不超过默认的30分钟 defaultCacheTTL := 30 * time.Minute - s.dnsCache.Set(r.Question[0].Name, qType, responseCopy, defaultCacheTTL) + s.DnsCache.Set(r.Question[0].Name, qType, responseCopy, defaultCacheTTL) logger.Debug("DNS响应已缓存", "domain", domain, "type", queryType, "ttl", defaultCacheTTL, "dnssec", responseDNSSEC) } // 添加查询日志 - 标记为实时 - s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, responseDNSSEC, true) + s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false, responseDNSSEC, true, dnsServer, dnssecServer) } // handleHostsResponse 处理hosts文件匹配的响应 func (s *Server) handleHostsResponse(w dns.ResponseWriter, r *dns.Msg, ip string) { response := new(dns.Msg) response.SetReply(r) - response.RecursionAvailable = true + // 不再硬编码RecursionAvailable,使用默认值或上游返回的值 if len(r.Question) > 0 { q := r.Question[0] @@ -588,7 +614,7 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain response := new(dns.Msg) response.SetReply(r) - response.RecursionAvailable = true + // 不再硬编码RecursionAvailable,使用默认值或上游返回的值 // 获取屏蔽方法配置 blockMethod := "NXDOMAIN" // 默认值 @@ -645,21 +671,31 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain } // forwardDNSRequest 转发DNS请求到上游服务器 -// forwardDNSRequestWithCache 转发DNS请求到上游服务器并返回响应 -func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg, time.Duration) { - // 尝试所有上游DNS服务器 - var bestResponse *dns.Msg - var bestRtt time.Duration - var hasBestResponse bool - var hasDNSSECResponse bool - var backupResponse *dns.Msg - var backupRtt time.Duration - var hasBackup bool +// serverResponse 用于存储服务器响应的结构体 +type serverResponse struct { + response *dns.Msg + rtt time.Duration + server string + error error +} +// forwardDNSRequestWithCache 转发DNS请求到上游服务器并返回响应 +func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg, time.Duration, string, string) { // 始终支持EDNS var udpSize uint16 = 4096 var doFlag bool = s.config.EnableDNSSEC + // 检查域名是否匹配不验证DNSSEC的模式 + noDNSSEC := false + for _, pattern := range s.config.NoDNSSECDomains { + if strings.Contains(domain, pattern) { + noDNSSEC = true + doFlag = false + logger.Debug("域名匹配到不验证DNSSEC的模式", "domain", domain, "pattern", pattern) + break + } + } + // 检查客户端请求是否包含EDNS记录 if opt := r.IsEdns0(); opt != nil { // 保留客户端的UDP缓冲区大小 @@ -679,67 +715,553 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg // DNSSEC专用服务器列表,从配置中获取 dnssecServers := s.config.DNSSECUpstreamDNS + // 选择合适的上游DNS服务器列表 + // 1. 首先检查是否有域名特定的DNS服务器配置 + var selectedUpstreamDNS []string + var domainMatched bool + + for matchStr, dnsServers := range s.config.DomainSpecificDNS { + if strings.Contains(domain, matchStr) { + selectedUpstreamDNS = dnsServers + domainMatched = true + logger.Debug("域名匹配到特定DNS服务器配置", "domain", domain, "matchStr", matchStr, "dnsServers", dnsServers) + break + } + } + + // 2. 如果没有匹配的域名特定配置 + if !domainMatched { + // 如果启用了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 + } + } + // 1. 首先尝试所有配置的上游DNS服务器 - for _, upstream := range s.config.UpstreamDNS { - response, rtt, err := s.resolver.Exchange(r, upstream) - if err == nil && response != nil { - // 设置递归可用标志 - response.RecursionAvailable = true + var bestResponse *dns.Msg + var bestRtt time.Duration + var hasBestResponse bool + var hasDNSSECResponse bool + var backupResponse *dns.Msg + var backupRtt time.Duration + var hasBackup bool + var usedDNSServer string + var usedDNSSECServer string - // 检查是否包含DNSSEC记录 - containsDNSSEC := s.hasDNSSECRecords(response) + // 根据查询模式处理请求 + switch s.config.QueryMode { + case "parallel": + // 并行请求模式 - 优化版:添加超时处理和快速响应返回 + responses := make(chan serverResponse, len(selectedUpstreamDNS)) + var wg sync.WaitGroup - // 如果启用了DNSSEC且响应包含DNSSEC记录,验证DNSSEC签名 - if s.config.EnableDNSSEC && containsDNSSEC { - // 验证DNSSEC记录 - signatureValid := s.verifyDNSSEC(response) + // 超时上下文 + timeoutCtx, cancel := context.WithTimeout(s.ctx, time.Duration(s.config.Timeout)*time.Millisecond) + defer cancel() - // 设置AD标志(Authenticated Data) - response.AuthenticatedData = signatureValid + // 向所有上游服务器并行发送请求 + for _, upstream := range selectedUpstreamDNS { + wg.Add(1) + go func(server string) { + defer wg.Done() - if signatureValid { - // 更新DNSSEC验证成功计数 - s.updateStats(func(stats *Stats) { - stats.DNSSECQueries++ - stats.DNSSECSuccess++ - }) - } else { - // 更新DNSSEC验证失败计数 - s.updateStats(func(stats *Stats) { - stats.DNSSECQueries++ - stats.DNSSECFailed++ - }) + // 发送请求并获取响应 + response, rtt, err := s.resolver.Exchange(r, server) + + select { + case responses <- serverResponse{response, rtt, server, err}: + // 成功发送响应 + case <-timeoutCtx.Done(): + // 超时,忽略此响应 + logger.Debug("并行请求超时", "server", server, "domain", domain) + return } + }(upstream) + } + + // 等待所有请求完成或超时 + go func() { + wg.Wait() + close(responses) + }() + + // 处理所有响应 + for resp := range responses { + if resp.error == nil && resp.response != nil { + // 更新服务器统计信息 + s.updateServerStats(resp.server, true, resp.rtt) + + // 检查是否包含DNSSEC记录 + containsDNSSEC := s.hasDNSSECRecords(resp.response) + + // 如果启用了DNSSEC且响应包含DNSSEC记录,验证DNSSEC签名 + // 但如果域名匹配不验证DNSSEC的模式,则跳过验证 + if s.config.EnableDNSSEC && containsDNSSEC && !noDNSSEC { + // 验证DNSSEC记录 + signatureValid := s.verifyDNSSEC(resp.response) + + // 设置AD标志(Authenticated Data) + resp.response.AuthenticatedData = signatureValid + + if signatureValid { + // 更新DNSSEC验证成功计数 + s.updateStats(func(stats *Stats) { + stats.DNSSECQueries++ + stats.DNSSECSuccess++ + }) + } else { + // 更新DNSSEC验证失败计数 + s.updateStats(func(stats *Stats) { + stats.DNSSECQueries++ + stats.DNSSECFailed++ + }) + } + } else if noDNSSEC { + // 对于不验证DNSSEC的域名,始终设置AD标志为false + 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 + } + } + + if resp.response.Rcode == dns.RcodeSuccess { + // 处理成功响应 + // 优先选择带有DNSSEC记录的响应 + if containsDNSSEC { + 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的响应时,保存第一个成功响应 + 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) + } + } + } + // 保存为备选响应 + if !hasBackup { + backupResponse = resp.response + backupRtt = resp.rtt + hasBackup = true + } + } + } else { + // 更新服务器统计信息(失败) + s.updateServerStats(resp.server, false, 0) + } + } + + case "loadbalance": + // 负载均衡模式 - 使用加权随机选择算法 + // 1. 选择一个加权随机服务器 + selectedServer := s.selectWeightedRandomServer(selectedUpstreamDNS) + if selectedServer != "" { + // 设置超时上下文 + timeoutCtx, cancel := context.WithTimeout(s.ctx, time.Duration(s.config.Timeout)*time.Millisecond) + defer cancel() + + // 使用带超时的方式执行Exchange + resultChan := make(chan struct { + response *dns.Msg + rtt time.Duration + err error + }, 1) + + go func() { + response, rtt, err := s.resolver.Exchange(r, selectedServer) + resultChan <- struct { + response *dns.Msg + rtt time.Duration + err error + }{response, rtt, err} + }() + + var response *dns.Msg + var rtt time.Duration + var err error + + select { + case result := <-resultChan: + response, rtt, err = result.response, result.rtt, result.err + case <-timeoutCtx.Done(): + err = timeoutCtx.Err() } - // 如果响应成功,根据DNSSEC状态选择最佳响应 - if response.Rcode == dns.RcodeSuccess { - // 优先选择带有DNSSEC记录的响应 - if containsDNSSEC { - bestResponse = response - bestRtt = rtt - hasBestResponse = true - hasDNSSECResponse = true - logger.Debug("找到带DNSSEC的最佳响应", "domain", domain, "server", upstream, "rtt", rtt) - } else if !hasBestResponse { - // 没有带DNSSEC的响应时,保存第一个成功响应 - bestResponse = response - bestRtt = rtt - hasBestResponse = true - logger.Debug("找到最佳响应", "domain", domain, "server", upstream, "rtt", rtt) + if err == nil && response != nil { + // 更新服务器统计信息 + s.updateServerStats(selectedServer, true, rtt) + + // 检查是否包含DNSSEC记录 + containsDNSSEC := s.hasDNSSECRecords(response) + + // 如果启用了DNSSEC且响应包含DNSSEC记录,验证DNSSEC签名 + // 但如果域名匹配不验证DNSSEC的模式,则跳过验证 + if s.config.EnableDNSSEC && containsDNSSEC && !noDNSSEC { + // 验证DNSSEC记录 + signatureValid := s.verifyDNSSEC(response) + + // 设置AD标志(Authenticated Data) + response.AuthenticatedData = signatureValid + + if signatureValid { + // 更新DNSSEC验证成功计数 + s.updateStats(func(stats *Stats) { + stats.DNSSECQueries++ + stats.DNSSECSuccess++ + }) + } else { + // 更新DNSSEC验证失败计数 + s.updateStats(func(stats *Stats) { + stats.DNSSECQueries++ + stats.DNSSECFailed++ + }) + } + } else if noDNSSEC { + // 对于不验证DNSSEC的域名,始终设置AD标志为false + response.AuthenticatedData = false } - // 保存为备选响应 - if !hasBackup { - backupResponse = response - backupRtt = rtt - hasBackup = true + + // 如果响应成功或为NXDOMAIN,根据DNSSEC状态选择最佳响应 + if response.Rcode == dns.RcodeSuccess || response.Rcode == dns.RcodeNameError { + if response.Rcode == dns.RcodeSuccess { + // 优先选择带有DNSSEC记录的响应 + if containsDNSSEC { + bestResponse = response + bestRtt = rtt + hasBestResponse = true + hasDNSSECResponse = true + usedDNSServer = selectedServer + // 如果当前使用的服务器是DNSSEC专用服务器,同时设置usedDNSSECServer + for _, dnssecServer := range dnssecServers { + if dnssecServer == selectedServer { + usedDNSSECServer = selectedServer + break + } + } + logger.Debug("找到带DNSSEC的最佳响应", "domain", domain, "server", selectedServer, "rtt", rtt) + } else { + // 没有带DNSSEC的响应时,保存成功响应 + bestResponse = response + bestRtt = rtt + hasBestResponse = true + usedDNSServer = selectedServer + // 如果当前使用的服务器是DNSSEC专用服务器,同时设置usedDNSSECServer + for _, dnssecServer := range dnssecServers { + if dnssecServer == selectedServer { + usedDNSSECServer = selectedServer + break + } + } + logger.Debug("找到最佳响应", "domain", domain, "server", selectedServer, "rtt", rtt) + } + } else if response.Rcode == dns.RcodeNameError { + // 处理NXDOMAIN响应 + bestResponse = response + bestRtt = rtt + hasBestResponse = true + usedDNSServer = selectedServer + logger.Debug("找到NXDOMAIN响应", "domain", domain, "server", selectedServer, "rtt", rtt) + } + // 保存为备选响应 + if !hasBackup { + backupResponse = response + backupRtt = rtt + hasBackup = true + } + } + } else { + // 更新服务器统计信息(失败) + s.updateServerStats(selectedServer, false, 0) + } + } + + case "fastest-ip": + // 最快的IP地址模式 - 使用TCP连接速度测量选择最快服务器 + // 1. 选择最快的服务器 + fastestServer := s.selectFastestServer(selectedUpstreamDNS) + if fastestServer != "" { + // 设置超时上下文 + timeoutCtx, cancel := context.WithTimeout(s.ctx, time.Duration(s.config.Timeout)*time.Millisecond) + defer cancel() + + // 使用带超时的方式执行Exchange + resultChan := make(chan struct { + response *dns.Msg + rtt time.Duration + err error + }, 1) + + go func() { + resp, r, e := s.resolver.Exchange(r, fastestServer) + resultChan <- struct { + response *dns.Msg + rtt time.Duration + err error + }{resp, r, e} + }() + + var response *dns.Msg + var rtt time.Duration + var err error + + select { + case result := <-resultChan: + response, rtt, err = result.response, result.rtt, result.err + case <-timeoutCtx.Done(): + err = timeoutCtx.Err() + } + if err == nil && response != nil { + // 更新服务器统计信息 + s.updateServerStats(fastestServer, true, rtt) + + // 检查是否包含DNSSEC记录 + containsDNSSEC := s.hasDNSSECRecords(response) + + // 如果启用了DNSSEC且响应包含DNSSEC记录,验证DNSSEC签名 + // 但如果域名匹配不验证DNSSEC的模式,则跳过验证 + if s.config.EnableDNSSEC && containsDNSSEC && !noDNSSEC { + // 验证DNSSEC记录 + signatureValid := s.verifyDNSSEC(response) + + // 设置AD标志(Authenticated Data) + response.AuthenticatedData = signatureValid + + if signatureValid { + // 更新DNSSEC验证成功计数 + s.updateStats(func(stats *Stats) { + stats.DNSSECQueries++ + stats.DNSSECSuccess++ + }) + } else { + // 更新DNSSEC验证失败计数 + s.updateStats(func(stats *Stats) { + stats.DNSSECQueries++ + stats.DNSSECFailed++ + }) + } + } else if noDNSSEC { + // 对于不验证DNSSEC的域名,始终设置AD标志为false + response.AuthenticatedData = false + } + + // 如果响应成功或为NXDOMAIN,根据DNSSEC状态选择最佳响应 + if response.Rcode == dns.RcodeSuccess || response.Rcode == dns.RcodeNameError { + if response.Rcode == dns.RcodeSuccess { + // 优先选择带有DNSSEC记录的响应 + if containsDNSSEC { + bestResponse = response + bestRtt = rtt + hasBestResponse = true + hasDNSSECResponse = true + usedDNSServer = fastestServer + // 如果当前使用的服务器是DNSSEC专用服务器,同时设置usedDNSSECServer + for _, dnssecServer := range dnssecServers { + if dnssecServer == fastestServer { + usedDNSSECServer = fastestServer + break + } + } + logger.Debug("找到带DNSSEC的最佳响应", "domain", domain, "server", fastestServer, "rtt", rtt) + } else { + // 没有带DNSSEC的响应时,保存成功响应 + bestResponse = response + bestRtt = rtt + hasBestResponse = true + usedDNSServer = fastestServer + // 如果当前使用的服务器是DNSSEC专用服务器,同时设置usedDNSSECServer + for _, dnssecServer := range dnssecServers { + if dnssecServer == fastestServer { + usedDNSSECServer = fastestServer + break + } + } + logger.Debug("找到最佳响应", "domain", domain, "server", fastestServer, "rtt", rtt) + } + } else if response.Rcode == dns.RcodeNameError { + // 处理NXDOMAIN响应 + bestResponse = response + bestRtt = rtt + hasBestResponse = true + usedDNSServer = fastestServer + logger.Debug("找到NXDOMAIN响应", "domain", domain, "server", fastestServer, "rtt", rtt) + } + // 保存为备选响应 + if !hasBackup { + backupResponse = response + backupRtt = rtt + hasBackup = true + } + } + } else { + // 更新服务器统计信息(失败) + s.updateServerStats(fastestServer, false, 0) + } + } + + default: + // 默认使用并行请求模式 - 添加超时处理和快速响应返回 + responses := make(chan serverResponse, len(selectedUpstreamDNS)) + var wg sync.WaitGroup + + // 超时上下文 + timeoutCtx, cancel := context.WithTimeout(s.ctx, time.Duration(s.config.Timeout)*time.Millisecond) + defer cancel() + + // 向所有上游服务器并行发送请求 + for _, upstream := range selectedUpstreamDNS { + wg.Add(1) + go func(server string) { + defer wg.Done() + + // 发送请求并获取响应 + response, rtt, err := s.resolver.Exchange(r, server) + + select { + case responses <- serverResponse{response, rtt, server, err}: + // 成功发送响应 + case <-timeoutCtx.Done(): + // 超时,忽略此响应 + logger.Debug("并行请求超时", "server", server, "domain", domain) + return + } + }(upstream) + } + + // 等待所有请求完成或超时 + go func() { + wg.Wait() + close(responses) + }() + + // 等待上下文超时,防止泄漏 + go func() { + <-timeoutCtx.Done() + }() + + // 处理所有响应 + for resp := range responses { + if resp.error == nil && resp.response != nil { + + // 检查是否包含DNSSEC记录 + containsDNSSEC := s.hasDNSSECRecords(resp.response) + + // 如果启用了DNSSEC且响应包含DNSSEC记录,验证DNSSEC签名 + // 但如果域名匹配不验证DNSSEC的模式,则跳过验证 + if s.config.EnableDNSSEC && containsDNSSEC && !noDNSSEC { + // 验证DNSSEC记录 + signatureValid := s.verifyDNSSEC(resp.response) + + // 设置AD标志(Authenticated Data) + resp.response.AuthenticatedData = signatureValid + + if signatureValid { + // 更新DNSSEC验证成功计数 + s.updateStats(func(stats *Stats) { + stats.DNSSECQueries++ + stats.DNSSECSuccess++ + }) + } else { + // 更新DNSSEC验证失败计数 + s.updateStats(func(stats *Stats) { + stats.DNSSECQueries++ + stats.DNSSECFailed++ + }) + } + } else if noDNSSEC { + // 对于不验证DNSSEC的域名,始终设置AD标志为false + resp.response.AuthenticatedData = false + } + + // 如果响应成功或为NXDOMAIN,根据DNSSEC状态选择最佳响应 + if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError { + if resp.response.Rcode == dns.RcodeSuccess { + // 优先选择带有DNSSEC记录的响应 + if containsDNSSEC { + bestResponse = resp.response + bestRtt = resp.rtt + hasBestResponse = true + hasDNSSECResponse = true + usedDNSServer = resp.server + // 如果当前使用的服务器是DNSSEC专用服务器,同时设置usedDNSSECServer + for _, dnssecServer := range dnssecServers { + if dnssecServer == resp.server { + usedDNSSECServer = resp.server + break + } + } + logger.Debug("找到带DNSSEC的最佳响应", "domain", domain, "server", resp.server, "rtt", resp.rtt) + } else if !hasBestResponse { + // 没有带DNSSEC的响应时,保存第一个成功响应 + bestResponse = resp.response + bestRtt = resp.rtt + hasBestResponse = true + usedDNSServer = resp.server + // 如果当前使用的服务器是DNSSEC专用服务器,同时设置usedDNSSECServer + for _, dnssecServer := range dnssecServers { + if dnssecServer == resp.server { + usedDNSSECServer = resp.server + break + } + } + 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) + } + } + } + // 保存为备选响应 + if !hasBackup { + backupResponse = resp.response + backupRtt = resp.rtt + hasBackup = true + } } } } } // 2. 当启用DNSSEC且没有找到带DNSSEC的响应时,向DNSSEC专用服务器发送请求 - if s.config.EnableDNSSEC && !hasDNSSECResponse { + // 但如果域名匹配了domainSpecificDNS配置或NoDNSSECDomains,则不使用DNSSEC专用服务器,只使用指定的DNS服务器 + if s.config.EnableDNSSEC && !hasDNSSECResponse && !domainMatched && !noDNSSEC { logger.Debug("向DNSSEC专用服务器发送请求", "domain", domain) // 增加DNSSEC查询计数 @@ -747,16 +1269,51 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg stats.DNSSECQueries++ }) - for _, dnssecServer := range dnssecServers { - response, rtt, err := s.resolver.Exchange(r, dnssecServer) + // 无论查询模式是什么,DNSSEC验证都只使用加权随机选择一个服务器 + selectedDnssecServer := s.selectWeightedRandomServer(dnssecServers) + if selectedDnssecServer != "" { + // 设置超时上下文 + timeoutCtx, cancel := context.WithTimeout(s.ctx, time.Duration(s.config.Timeout)*time.Millisecond) + defer cancel() + + // 使用带超时的方式执行Exchange + resultChan := make(chan struct { + response *dns.Msg + rtt time.Duration + err error + }, 1) + + go func() { + response, rtt, err := s.resolver.Exchange(r, selectedDnssecServer) + resultChan <- struct { + response *dns.Msg + rtt time.Duration + err error + }{response, rtt, err} + }() + + var response *dns.Msg + var rtt time.Duration + var err error + + select { + case result := <-resultChan: + response, rtt, err = result.response, result.rtt, result.err + case <-timeoutCtx.Done(): + err = timeoutCtx.Err() + } + if err == nil && response != nil { - // 设置递归可用标志 - response.RecursionAvailable = true + // 更新服务器统计信息 + s.updateServerStats(selectedDnssecServer, true, rtt) // 检查是否包含DNSSEC记录 containsDNSSEC := s.hasDNSSECRecords(response) if response.Rcode == dns.RcodeSuccess { + // 无论响应是否包含DNSSEC记录,只要使用了DNSSEC专用服务器,就设置usedDNSSECServer + usedDNSSECServer = selectedDnssecServer + // 验证DNSSEC记录 signatureValid := s.verifyDNSSEC(response) @@ -777,12 +1334,11 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg // 优先使用DNSSEC专用服务器的响应,尤其是带有DNSSEC记录的 if containsDNSSEC { - // 即使之前有最佳响应,也优先使用DNSSEC专用服务器的DNSSEC响应 bestResponse = response bestRtt = rtt hasBestResponse = true hasDNSSECResponse = true - logger.Debug("DNSSEC专用服务器返回带DNSSEC的响应,优先使用", "domain", domain, "server", dnssecServer, "rtt", rtt) + logger.Debug("DNSSEC专用服务器返回带DNSSEC的响应,优先使用", "domain", domain, "server", selectedDnssecServer, "rtt", rtt) } // 注意:如果DNSSEC专用服务器返回的响应不包含DNSSEC记录, // 我们不会覆盖之前从upstreamDNS获取的响应, @@ -795,6 +1351,9 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg hasBackup = true } } + } else { + // 更新服务器统计信息(失败) + s.updateServerStats(selectedDnssecServer, false, 0) } } } @@ -804,9 +1363,85 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg // 检查最佳响应是否包含DNSSEC记录 bestHasDNSSEC := s.hasDNSSECRecords(bestResponse) - // 如果启用了DNSSEC且最佳响应不包含DNSSEC记录,使用upstreamDNS的解析结果 - if s.config.EnableDNSSEC && !bestHasDNSSEC { - logger.Debug("最佳响应不包含DNSSEC记录,使用upstreamDNS的解析结果", "domain", domain) + // 如果启用了DNSSEC且最佳响应不包含DNSSEC记录,尝试使用本地解析(使用upstreamDNS服务器) + // 但如果域名匹配了domainSpecificDNS配置,则不执行此逻辑,只使用指定的DNS服务器 + if s.config.EnableDNSSEC && !bestHasDNSSEC && !domainMatched { + logger.Debug("最佳响应不包含DNSSEC记录,尝试使用本地解析(upstreamDNS)", "domain", domain) + // 选择一个upstreamDNS服务器进行解析(使用加权随机算法) + localServer := s.selectWeightedRandomServer(s.config.UpstreamDNS) + if localServer != "" { + // 设置超时上下文 + timeoutCtx, cancel := context.WithTimeout(s.ctx, time.Duration(s.config.Timeout)*time.Millisecond) + defer cancel() + + // 使用带超时的方式执行Exchange + resultChan := make(chan struct { + response *dns.Msg + rtt time.Duration + err error + }, 1) + + go func() { + resp, r, e := s.resolver.Exchange(r, localServer) + resultChan <- struct { + response *dns.Msg + rtt time.Duration + err error + }{resp, r, e} + }() + + var localResponse *dns.Msg + var rtt time.Duration + var err error + + select { + case result := <-resultChan: + localResponse, rtt, err = result.response, result.rtt, result.err + case <-timeoutCtx.Done(): + err = timeoutCtx.Err() + } + if err == nil && localResponse != nil { + // 更新服务器统计信息 + s.updateServerStats(localServer, true, rtt) + + // 检查是否包含DNSSEC记录 + localHasDNSSEC := s.hasDNSSECRecords(localResponse) + + // 验证DNSSEC记录(如果存在),但不影响最终响应 + if localHasDNSSEC { + signatureValid := s.verifyDNSSEC(localResponse) + localResponse.AuthenticatedData = signatureValid + + if signatureValid { + s.updateStats(func(stats *Stats) { + stats.DNSSECQueries++ + stats.DNSSECSuccess++ + }) + } else { + s.updateStats(func(stats *Stats) { + stats.DNSSECQueries++ + stats.DNSSECFailed++ + }) + } + } + + // 记录解析域名统计 + s.updateResolvedDomainStats(domain) + + // 更新域名的DNSSEC状态 + s.updateDomainDNSSECStatus(domain, localHasDNSSEC) + + s.updateStats(func(stats *Stats) { + stats.Allowed++ + }) + + logger.Debug("使用本地解析结果(upstreamDNS)", "domain", domain, "server", localServer, "rtt", rtt) + return localResponse, rtt, localServer, "" + } else { + // 更新服务器统计信息(失败) + s.updateServerStats(localServer, false, 0) + } + } } // 记录解析域名统计 @@ -815,12 +1450,14 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg // 更新域名的DNSSEC状态 if bestHasDNSSEC { s.updateDomainDNSSECStatus(domain, true) + } else { + s.updateDomainDNSSECStatus(domain, false) } s.updateStats(func(stats *Stats) { stats.Allowed++ }) - return bestResponse, bestRtt + return bestResponse, bestRtt, usedDNSServer, usedDNSSECServer } // 如果有备选响应,返回该响应 @@ -828,29 +1465,29 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg logger.Debug("使用备选响应,没有找到更好的结果", "domain", domain) // 记录解析域名统计 s.updateResolvedDomainStats(domain) - + // 更新统计信息 s.updateStats(func(stats *Stats) { stats.Allowed++ }) - return backupResponse, backupRtt + return backupResponse, backupRtt, "", "" } // 所有上游服务器都失败,返回服务器失败错误 response := new(dns.Msg) response.SetReply(r) - response.RecursionAvailable = true + response.SetRcode(r, dns.RcodeServerFailure) logger.Error("DNS查询失败", "domain", domain) s.updateStats(func(stats *Stats) { stats.Errors++ }) - return response, 0 + return response, 0, "", "" } // forwardDNSRequest 转发DNS请求到上游服务器 func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain string) { - response, _ := s.forwardDNSRequestWithCache(r, domain) + response, _, _, _ := s.forwardDNSRequestWithCache(r, domain) w.WriteMsg(response) } @@ -1006,11 +1643,17 @@ func (s *Server) verifyDNSSEC(response *dns.Msg) bool { // 验证所有RRSIG记录 signatureValid := true + // 用于记录已经警告过的DNSKEY tag,避免重复警告 + warnedKeyTags := make(map[uint16]bool) for _, rrsig := range rrsigs { // 查找对应的DNSKEY dnskey, exists := dnskeys[rrsig.KeyTag] if !exists { - logger.Warn("DNSSEC验证失败:找不到对应的DNSKEY", "key_tag", rrsig.KeyTag) + // 仅当该key_tag尚未警告过时才记录警告 + if !warnedKeyTags[rrsig.KeyTag] { + logger.Warn("DNSSEC验证失败:找不到对应的DNSKEY", "key_tag", rrsig.KeyTag) + warnedKeyTags[rrsig.KeyTag] = true + } signatureValid = false continue } @@ -1086,6 +1729,236 @@ func (s *Server) updateResolvedDomainStats(domain string) { } } +// getServerStats 获取服务器统计信息,如果不存在则创建 +func (s *Server) getServerStats(server string) *ServerStats { + s.serverStatsMutex.RLock() + stats, exists := s.serverStats[server] + s.serverStatsMutex.RUnlock() + + if !exists { + // 创建新的服务器统计信息 + stats = &ServerStats{ + SuccessCount: 0, + FailureCount: 0, + LastResponse: time.Now(), + ResponseTime: 0, + ConnectionSpeed: 0, + } + + // 加锁更新服务器统计信息 + s.serverStatsMutex.Lock() + s.serverStats[server] = stats + s.serverStatsMutex.Unlock() + } + + return stats +} + +// updateServerStats 更新服务器统计信息 +func (s *Server) updateServerStats(server string, success bool, rtt time.Duration) { + stats := s.getServerStats(server) + + s.serverStatsMutex.Lock() + defer s.serverStatsMutex.Unlock() + + // 更新统计信息 + stats.LastResponse = time.Now() + + if success { + stats.SuccessCount++ + // 更新平均响应时间(简单移动平均) + // 将所有值转换为纳秒进行计算,然后再转换回Duration + if stats.SuccessCount == 1 { + // 第一次成功,直接使用当前响应时间 + stats.ResponseTime = rtt + } else { + // 使用纳秒进行计算以避免类型不匹配 + prevTotal := stats.ResponseTime.Nanoseconds() * (stats.SuccessCount - 1) + newTotal := prevTotal + rtt.Nanoseconds() + stats.ResponseTime = time.Duration(newTotal / stats.SuccessCount) + } + } else { + stats.FailureCount++ + } +} + +// selectWeightedRandomServer 加权随机选择服务器 +func (s *Server) selectWeightedRandomServer(servers []string) string { + if len(servers) == 0 { + return "" + } + + if len(servers) == 1 { + return servers[0] + } + + // 计算每个服务器的权重 + type serverWeight struct { + server string + weight int64 + } + + var totalWeight int64 + weights := make([]serverWeight, 0, len(servers)) + + // 获取所有服务器的平均响应时间,用于归一化 + var totalResponseTime time.Duration + validServers := 0 + + for _, server := range servers { + stats := s.getServerStats(server) + if stats.ResponseTime > 0 { + totalResponseTime += stats.ResponseTime + validServers++ + } + } + + // 计算平均响应时间基准值 + var avgResponseTime time.Duration + if validServers > 0 { + avgResponseTime = totalResponseTime / time.Duration(validServers) + } else { + avgResponseTime = 1 * time.Second // 默认基准值 + } + + for _, server := range servers { + stats := s.getServerStats(server) + + // 计算基础权重:成功次数 - 失败次数 * 2(失败权重更高) + // 确保权重至少为1 + baseWeight := stats.SuccessCount - stats.FailureCount*2 + if baseWeight < 1 { + baseWeight = 1 + } + + // 计算响应时间调整因子:响应时间越短,因子越高 + // 如果没有响应时间数据,使用默认值1 + var responseFactor float64 = 1.0 + if stats.ResponseTime > 0 { + // 使用平均响应时间作为基准,计算调整因子 + // 响应时间越短,因子越高,最高为2.0,最低为0.5 + responseFactor = float64(avgResponseTime) / float64(stats.ResponseTime) + // 限制调整因子的范围,避免权重波动过大 + if responseFactor > 2.0 { + responseFactor = 2.0 + } else if responseFactor < 0.5 { + responseFactor = 0.5 + } + } + + // 综合计算最终权重,四舍五入到整数 + finalWeight := int64(float64(baseWeight) * responseFactor) + // 确保最终权重至少为1 + if finalWeight < 1 { + finalWeight = 1 + } + + weights = append(weights, serverWeight{server, finalWeight}) + totalWeight += finalWeight + } + + // 随机选择一个权重 + random := time.Now().UnixNano() % totalWeight + if random < 0 { + random += totalWeight + } + + // 选择对应的服务器 + var currentWeight int64 + for _, sw := range weights { + currentWeight += sw.weight + if random < currentWeight { + return sw.server + } + } + + // 兜底返回第一个服务器 + return servers[0] +} + +// measureServerSpeed 测量服务器TCP连接速度 +func (s *Server) measureServerSpeed(server string) time.Duration { + // 提取服务器地址和端口 + addr := server + if !strings.Contains(server, ":") { + addr = server + ":53" + } + + // 测量TCP连接时间 + startTime := time.Now() + conn, err := net.DialTimeout("tcp", addr, 2*time.Second) + if err != nil { + // 连接失败,返回最大持续时间 + return 2 * time.Second + } + defer conn.Close() + + // 计算连接建立时间 + connTime := time.Since(startTime) + + // 更新服务器连接速度 + stats := s.getServerStats(server) + s.serverStatsMutex.Lock() + // 使用指数移动平均更新连接速度 + stats.ConnectionSpeed = (stats.ConnectionSpeed*3 + connTime) / 4 + s.serverStatsMutex.Unlock() + + return connTime +} + +// selectFastestServer 选择连接速度最快的服务器 +func (s *Server) selectFastestServer(servers []string) string { + if len(servers) == 0 { + return "" + } + + if len(servers) == 1 { + return servers[0] + } + + // 并行测量所有服务器的速度 + type speedResult struct { + server string + speed time.Duration + } + + results := make(chan speedResult, len(servers)) + var wg sync.WaitGroup + + for _, server := range servers { + wg.Add(1) + go func(srv string) { + defer wg.Done() + speed := s.measureServerSpeed(srv) + results <- speedResult{srv, speed} + }(server) + } + + // 等待所有测量完成 + go func() { + wg.Wait() + close(results) + }() + + // 找出最快的服务器 + var fastestServer string + var fastestSpeed time.Duration = 2 * time.Second + + for result := range results { + if result.speed < fastestSpeed { + fastestSpeed = result.speed + fastestServer = result.server + } + } + + // 如果没有找到最快服务器(理论上不会发生),返回第一个服务器 + if fastestServer == "" { + fastestServer = servers[0] + } + + return fastestServer +} + // updateStats 更新统计信息 func (s *Server) updateStats(update func(*Stats)) { s.statsMutex.Lock() @@ -1094,7 +1967,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) { +func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string, fromCache, dnssec, edns bool, dnsServer, dnssecServer string) { // 获取IP地理位置 location := s.getIpGeolocation(clientIP) @@ -1112,6 +1985,8 @@ func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime in FromCache: fromCache, DNSSEC: dnssec, EDNS: edns, + DNSServer: dnsServer, + DNSSECServer: dnssecServer, } // 添加到日志列表 diff --git a/http/server.go b/http/server.go index 669e0d0..d1f0dc9 100644 --- a/http/server.go +++ b/http/server.go @@ -832,6 +832,9 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { return } + // 清空DNS缓存,使新规则立即生效 + s.dnsServer.DnsCache.Clear() + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) return case http.MethodDelete: @@ -850,6 +853,9 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { return } + // 清空DNS缓存,使规则变更立即生效 + s.dnsServer.DnsCache.Clear() + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) return case http.MethodPut: diff --git a/main.go b/main.go index 6f2a81b..20bfef7 100644 --- a/main.go +++ b/main.go @@ -37,9 +37,16 @@ func createDefaultConfig(configFile string) error { "223.5.5.5:53", "223.6.6.6:53" ], + "dnssecUpstreamDNS": [ + "8.8.8.8:53", + "1.1.1.1:53" + ], "timeout": 5000, "statsFile": "./data/stats.json", - "saveInterval": 300 + "saveInterval": 300, + "cacheTTL": 30, + "enableDNSSEC": true, + "queryMode": "parallel" }, "http": { "port": 8080, @@ -114,10 +121,10 @@ 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 { - return fmt.Errorf("创建本地规则文件失败: %w", err) + if err := os.WriteFile(cfg.Shield.LocalRulesFile, []byte("# 自定义规则文件\n# 格式:域名\n# 例如:example.com\n"), 0644); err != nil { + return fmt.Errorf("创建自定义规则文件失败: %w", err) } } diff --git a/shield/manager.go b/shield/manager.go index 18c0463..44eae44 100644 --- a/shield/manager.go +++ b/shield/manager.go @@ -30,7 +30,7 @@ type ShieldStatsData struct { type regexRule struct { pattern *regexp.Regexp original string - isLocal bool // 是否为本地规则 + isLocal bool // 是否为自定义规则 source string // 规则来源 } @@ -39,8 +39,8 @@ type ShieldManager struct { config *config.ShieldConfig domainRules map[string]bool domainExceptions map[string]bool - domainRulesIsLocal map[string]bool // 标记域名规则是否为本地规则 - domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为本地规则 + domainRulesIsLocal map[string]bool // 标记域名规则是否为自定义规则 + domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为自定义规则 domainRulesSource map[string]string // 标记域名规则来源 domainExceptionsSource map[string]string // 标记域名排除规则来源 domainRulesOriginal map[string]string // 存储域名规则的原始字符串 @@ -54,7 +54,7 @@ type ShieldManager struct { updateCtx context.Context updateCancel context.CancelFunc updateRunning bool - localRulesCount int // 本地规则数量 + localRulesCount int // 自定义规则数量 remoteRulesCount int // 远程规则数量 } @@ -109,9 +109,9 @@ func (m *ShieldManager) LoadRules() error { m.remoteRulesCount = 0 // 保留计数数据,不随规则重新加载而清空 - // 加载本地规则文件 + // 加载自定义规则文件 if err := m.loadLocalRules(); err != nil { - logger.Error("加载本地规则失败", "error", err) + logger.Error("加载自定义规则失败", "error", err) // 继续执行,不返回错误 } @@ -132,7 +132,7 @@ func (m *ShieldManager) LoadRules() error { return nil } -// loadLocalRules 加载本地规则文件 +// loadLocalRules 加载自定义规则文件 func (m *ShieldManager) loadLocalRules() error { if m.config.LocalRulesFile == "" { return nil @@ -144,7 +144,7 @@ func (m *ShieldManager) loadLocalRules() error { } defer file.Close() - // 记录加载前的规则数量,用于计算本地规则数量 + // 记录加载前的规则数量,用于计算自定义规则数量 beforeDomainRules := len(m.domainRules) beforeRegexRules := len(m.regexRules) @@ -154,10 +154,10 @@ func (m *ShieldManager) loadLocalRules() error { if line == "" || strings.HasPrefix(line, "#") { continue } - m.parseRule(line, true, "本地规则") // 本地规则,isLocal=true,来源为"本地规则" + m.parseRule(line, true, "自定义规则") // 自定义规则,isLocal=true,来源为"自定义规则" } - // 更新本地规则计数 + // 更新自定义规则计数 m.localRulesCount = (len(m.domainRules) - beforeDomainRules) + (len(m.regexRules) - beforeRegexRules) return scanner.Err() @@ -445,10 +445,16 @@ func (m *ShieldManager) parseRuleOptions(optionsStr string) map[string]string { // addDomainRule 添加域名规则,支持是否为阻止规则 func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string, original string) { if block { - // 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖 + // 如果是远程规则,检查是否已经存在自定义规则(阻止或排除),如果存在则不覆盖 if !isLocal { + // 检查是否存在自定义阻止规则 if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] { - // 已经存在本地规则,不覆盖 + // 已经存在自定义规则,不覆盖 + return + } + // 检查是否存在自定义排除规则 + if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] { + // 已经存在自定义规则,不覆盖 return } } @@ -458,10 +464,16 @@ func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, s m.domainRulesOriginal[domain] = original } else { // 添加到排除规则 - // 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖 + // 如果是远程规则,检查是否已经存在自定义规则(阻止或排除),如果存在则不覆盖 if !isLocal { + // 检查是否存在自定义阻止规则 + if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] { + // 已经存在自定义规则,不覆盖 + return + } + // 检查是否存在自定义排除规则 if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] { - // 已经存在本地规则,不覆盖 + // 已经存在自定义规则,不覆盖 return } } @@ -481,11 +493,19 @@ func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block b source: source, } if block { - // 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加 + // 如果是远程规则,检查是否已经存在任何自定义规则,如果存在则不添加 if !isLocal { + // 检查是否存在自定义阻止规则 for _, existingRule := range m.regexRules { - if existingRule.original == original && existingRule.isLocal { - // 已经存在相同的本地规则,不添加 + if existingRule.pattern.String() == re.String() && existingRule.isLocal { + // 已经存在相同的自定义阻止规则,不添加 + return + } + } + // 检查是否存在自定义排除规则 + for _, existingRule := range m.regexExceptions { + if existingRule.pattern.String() == re.String() && existingRule.isLocal { + // 已经存在相同的自定义排除规则,不添加 return } } @@ -493,11 +513,19 @@ func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block b m.regexRules = append(m.regexRules, rule) } else { // 添加到排除规则 - // 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加 + // 如果是远程规则,检查是否已经存在任何自定义规则,如果存在则不添加 if !isLocal { + // 检查是否存在自定义阻止规则 + for _, existingRule := range m.regexRules { + if existingRule.pattern.String() == re.String() && existingRule.isLocal { + // 已经存在相同的自定义阻止规则,不添加 + return + } + } + // 检查是否存在自定义排除规则 for _, existingRule := range m.regexExceptions { - if existingRule.original == original && existingRule.isLocal { - // 已经存在相同的本地规则,不添加 + if existingRule.pattern.String() == re.String() && existingRule.isLocal { + // 已经存在相同的自定义排除规则,不添加 return } } @@ -537,8 +565,17 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf result["hostsIP"] = hostsIP // 检查排除规则(优先级最高) - // 检查域名排除规则 - if m.domainExceptions[domain] { + // 1. 先检查本地域名排除规则 + if m.domainExceptions[domain] && m.domainExceptionsIsLocal[domain] { + result["excluded"] = true + result["excludeRule"] = m.domainExceptionsOriginal[domain] + result["excludeRuleType"] = "exact_domain" + result["blocksource"] = m.domainExceptionsSource[domain] + return result + } + + // 2. 再检查远程域名排除规则 + if m.domainExceptions[domain] && !m.domainExceptionsIsLocal[domain] { result["excluded"] = true result["excludeRule"] = m.domainExceptionsOriginal[domain] result["excludeRuleType"] = "exact_domain" @@ -548,9 +585,11 @@ 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:], ".") - if m.domainExceptions[subdomain] { + if m.domainExceptions[subdomain] && m.domainExceptionsIsLocal[subdomain] { result["excluded"] = true result["excludeRule"] = m.domainExceptionsOriginal[subdomain] result["excludeRuleType"] = "subdomain" @@ -559,9 +598,32 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf } } - // 检查正则表达式排除规则 + // 4. 再检查远程子域名排除规则 + for i := 0; i < len(parts)-1; i++ { + subdomain := strings.Join(parts[i:], ".") + if m.domainExceptions[subdomain] && !m.domainExceptionsIsLocal[subdomain] { + result["excluded"] = true + result["excludeRule"] = m.domainExceptionsOriginal[subdomain] + result["excludeRuleType"] = "subdomain" + result["blocksource"] = m.domainExceptionsSource[subdomain] + return result + } + } + + // 5. 先检查本地正则表达式排除规则 for _, re := range m.regexExceptions { - if re.pattern.MatchString(domain) { + if re.isLocal && re.pattern.MatchString(domain) { + result["excluded"] = true + result["excludeRule"] = re.original + result["excludeRuleType"] = "regex" + result["blocksource"] = re.source + return result + } + } + + // 6. 再检查远程正则表达式排除规则 + for _, re := range m.regexExceptions { + if !re.isLocal && re.pattern.MatchString(domain) { result["excluded"] = true result["excludeRule"] = re.original result["excludeRuleType"] = "regex" @@ -571,8 +633,17 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf } // 检查阻止规则 - 先检查精确域名匹配,再检查子域名匹配 - // 检查精确域名匹配 - if m.domainRules[domain] { + // 7. 先检查本地域名阻止规则 + if m.domainRules[domain] && m.domainRulesIsLocal[domain] { + result["blocked"] = true + result["blockRule"] = m.domainRulesOriginal[domain] + result["blockRuleType"] = "exact_domain" + result["blocksource"] = m.domainRulesSource[domain] + return result + } + + // 8. 再检查远程域名阻止规则 + if m.domainRules[domain] && !m.domainRulesIsLocal[domain] { result["blocked"] = true result["blockRule"] = m.domainRulesOriginal[domain] result["blockRuleType"] = "exact_domain" @@ -582,9 +653,11 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf // 检查子域名匹配(AdGuardHome风格) // 从最长的子域名开始匹配,确保优先级正确 + + // 9. 先检查本地子域名阻止规则 for i := 0; i < len(parts)-1; i++ { subdomain := strings.Join(parts[i:], ".") - if m.domainRules[subdomain] { + if m.domainRules[subdomain] && m.domainRulesIsLocal[subdomain] { result["blocked"] = true result["blockRule"] = m.domainRulesOriginal[subdomain] result["blockRuleType"] = "subdomain" @@ -593,9 +666,32 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf } } - // 检查正则表达式匹配 + // 10. 再检查远程子域名阻止规则 + for i := 0; i < len(parts)-1; i++ { + subdomain := strings.Join(parts[i:], ".") + if m.domainRules[subdomain] && !m.domainRulesIsLocal[subdomain] { + result["blocked"] = true + result["blockRule"] = m.domainRulesOriginal[subdomain] + result["blockRuleType"] = "subdomain" + result["blocksource"] = m.domainRulesSource[subdomain] + return result + } + } + + // 11. 先检查本地正则表达式阻止规则 for _, re := range m.regexRules { - if re.pattern.MatchString(domain) { + if re.isLocal && re.pattern.MatchString(domain) { + result["blocked"] = true + result["blockRule"] = re.original + result["blockRuleType"] = "regex" + result["blocksource"] = re.source + return result + } + } + + // 12. 再检查远程正则表达式阻止规则 + for _, re := range m.regexRules { + if !re.isLocal && re.pattern.MatchString(domain) { result["blocked"] = true result["blockRule"] = re.original result["blockRuleType"] = "regex" @@ -722,13 +818,13 @@ func (m *ShieldManager) GetHostsIP(domain string) (string, bool) { return ip, exists } -// AddRule 添加屏蔽规则,用户添加的规则是本地规则 +// AddRule 添加屏蔽规则,用户添加的规则是自定义规则 func (m *ShieldManager) AddRule(rule string) error { m.rulesMutex.Lock() defer m.rulesMutex.Unlock() - // 解析并添加规则到内存,isLocal=true表示本地规则,来源为"本地规则" - m.parseRule(rule, true, "本地规则") + // 解析并添加规则到内存,isLocal=true表示自定义规则,来源为"自定义规则" + m.parseRule(rule, true, "自定义规则") // 持久化保存规则到文件 if m.config.LocalRulesFile != "" { @@ -957,7 +1053,7 @@ func (m *ShieldManager) StopAutoUpdate() { logger.Info("规则自动更新已停止") } -// saveRulesToFile 保存规则到文件,只保存本地规则 +// saveRulesToFile 保存规则到文件,只保存自定义规则 func (m *ShieldManager) saveRulesToFile() error { var rules []string @@ -1293,12 +1389,12 @@ func (m *ShieldManager) GetHostsCount() int { return len(m.hostsMap) } -// GetLocalRules 获取仅本地规则 +// GetLocalRules 获取仅自定义规则 func (m *ShieldManager) GetLocalRules() map[string]interface{} { m.rulesMutex.RLock() defer m.rulesMutex.RUnlock() - // 转换map和slice为字符串列表,只包含本地规则 + // 转换map和slice为字符串列表,只包含自定义规则 domainRulesList := make([]string, 0) for domain, isLocal := range m.domainRulesIsLocal { if isLocal && m.domainRules[domain] { @@ -1329,7 +1425,7 @@ func (m *ShieldManager) GetLocalRules() map[string]interface{} { } } - // 计算本地规则数量 + // 计算自定义规则数量 localDomainRulesCount := 0 for _, isLocal := range m.domainRulesIsLocal { if isLocal { diff --git a/static/api/js/index.js b/static/api/js/index.js index ea876cc..94fa52e 100644 --- a/static/api/js/index.js +++ b/static/api/js/index.js @@ -1138,12 +1138,12 @@ }, "/shield/localrules": { "get": { - "summary": "获取本地规则", - "description": "获取Shield的本地规则列表。", + "summary": "获取自定义规则", + "description": "获取Shield的自定义规则列表。" "tags": ["shield"], "responses": { "200": { - "description": "成功获取本地规则", + "description": "成功获取自定义规则", "content": { "application/json": { "schema": { @@ -1166,8 +1166,8 @@ } }, "post": { - "summary": "添加本地规则", - "description": "添加新的本地规则。", + "summary": "添加自定义规则", + "description": "添加新的自定义规则。", "tags": ["shield"], "requestBody": { "required": true, @@ -1205,8 +1205,8 @@ } }, "delete": { - "summary": "删除本地规则", - "description": "删除指定ID的本地规则。", + "summary": "删除自定义规则", + "description": "删除指定ID的自定义规则。", "tags": ["shield"], "parameters": [ { @@ -1500,7 +1500,7 @@ "blocked": true, "blockRule": "example.com", "blockRuleType": "exact_domain", - "blocksource": "本地规则", + "blocksource": "自定义规则", "excluded": false, "excludeRule": "", "excludeRuleType": "", diff --git a/static/index.html b/static/index.html index 363371e..f36953f 100644 --- a/static/index.html +++ b/static/index.html @@ -630,9 +630,9 @@ - +
-

本地规则管理

+

自定义规则管理

@@ -976,11 +976,12 @@ 响应时间 屏蔽规则 + 操作 - +
暂无查询日志
@@ -1056,7 +1057,7 @@

屏蔽配置

- +
diff --git a/static/js/logs.js b/static/js/logs.js index 9190e73..4f13ca1 100644 --- a/static/js/logs.js +++ b/static/js/logs.js @@ -282,6 +282,9 @@ function loadLogs() { // 更新日志表格 updateLogsTable(logs); + // 绑定操作按钮事件 + bindActionButtonsEvents(); + // 更新分页信息 updateLogsPagination(); @@ -388,6 +391,9 @@ function updateLogsTable(logs) { const cacheStatusClass = log.FromCache ? 'text-primary' : 'text-gray-500'; const cacheStatusText = log.FromCache ? '缓存' : '非缓存'; + // 检查域名是否被拦截 + const isBlocked = log.Result === 'blocked'; + row.innerHTML = `
${formattedTime}
@@ -400,10 +406,36 @@ function updateLogsTable(logs) {
${log.Domain}
类型: ${log.QueryType}, ${statusText}, ${log.FromCache ? '缓存' : '实时'}${log.DNSSEC ? ', DNSSEC' : ''}${log.EDNS ? ', EDNS' : ''}
+
DNS 服务器: ${log.DNSServer || '无'}, DNSSEC专用: ${log.DNSSECServer || '无'}
${log.ResponseTime}ms ${log.BlockRule || '-'} + + ${isBlocked ? + `` : + `` + } + `; + + // 绑定按钮事件 + const blockBtn = row.querySelector('.block-btn'); + if (blockBtn) { + blockBtn.addEventListener('click', (e) => { + e.preventDefault(); + const domain = e.currentTarget.dataset.domain; + blockDomain(domain); + }); + } + + const unblockBtn = row.querySelector('.unblock-btn'); + if (unblockBtn) { + unblockBtn.addEventListener('click', (e) => { + e.preventDefault(); + const domain = e.currentTarget.dataset.domain; + unblockDomain(domain); + }); + } tableBody.appendChild(row); }); @@ -613,6 +645,146 @@ function updateLogsStatsFromWebSocket(stats) { } } +// 拦截域名 +async function blockDomain(domain) { + try { + console.log(`开始拦截域名: ${domain}`); + + // 创建拦截规则,使用AdBlock Plus格式 + const blockRule = `||${domain}^`; + console.log(`创建的拦截规则: ${blockRule}`); + + // 调用API添加拦截规则 + console.log(`调用API添加拦截规则,路径: /shield, 方法: POST`); + const response = await apiRequest('/shield', 'POST', { rule: blockRule }); + + console.log(`API响应:`, response); + + // 处理不同的响应格式 + if (response && (response.success || response.status === 'success')) { + // 重新加载日志,显示更新后的状态 + loadLogs(); + + // 刷新规则列表 + refreshRulesList(); + + // 显示成功通知 + if (typeof window.showNotification === 'function') { + window.showNotification(`已成功拦截域名: ${domain}`, 'success'); + } + } else { + const errorMsg = response ? (response.message || '添加拦截规则失败') : '添加拦截规则失败: 无效的API响应'; + console.error(`拦截域名失败: ${errorMsg}`); + throw new Error(errorMsg); + } + } catch (error) { + console.error('拦截域名失败:', error); + + // 显示错误通知 + if (typeof window.showNotification === 'function') { + window.showNotification(`拦截域名失败: ${error.message}`, 'danger'); + } + } +} + +// 绑定操作按钮事件 +function bindActionButtonsEvents() { + // 绑定拦截按钮事件 + const blockBtns = document.querySelectorAll('.block-btn'); + blockBtns.forEach(btn => { + btn.addEventListener('click', async (e) => { + e.preventDefault(); + const domain = e.currentTarget.dataset.domain; + await blockDomain(domain); + }); + }); + + // 绑定放行按钮事件 + const unblockBtns = document.querySelectorAll('.unblock-btn'); + unblockBtns.forEach(btn => { + btn.addEventListener('click', async (e) => { + e.preventDefault(); + const domain = e.currentTarget.dataset.domain; + await unblockDomain(domain); + }); + }); +} + +// 刷新规则列表 +async function refreshRulesList() { + try { + // 调用API重新加载规则 + const response = await apiRequest('/shield', 'GET'); + + if (response) { + // 处理规则列表响应 + let allRules = []; + if (response && typeof response === 'object') { + // 合并所有类型的规则到一个数组 + if (Array.isArray(response.domainRules)) allRules = allRules.concat(response.domainRules); + if (Array.isArray(response.domainExceptions)) allRules = allRules.concat(response.domainExceptions); + if (Array.isArray(response.regexRules)) allRules = allRules.concat(response.regexRules); + if (Array.isArray(response.regexExceptions)) allRules = allRules.concat(response.regexExceptions); + } + + // 更新规则列表 + if (window.rules) { + rules = allRules; + filteredRules = [...rules]; + + // 更新规则数量统计 + if (window.updateRulesCount && typeof window.updateRulesCount === 'function') { + window.updateRulesCount(rules.length); + } + } + } + } catch (error) { + console.error('刷新规则列表失败:', error); + } +} + +// 放行域名 +async function unblockDomain(domain) { + try { + console.log(`开始放行域名: ${domain}`); + + // 创建放行规则,使用AdBlock Plus格式 + const allowRule = `@@||${domain}^`; + console.log(`创建的放行规则: ${allowRule}`); + + // 调用API添加放行规则 + console.log(`调用API添加放行规则,路径: /shield, 方法: POST`); + const response = await apiRequest('/shield', 'POST', { rule: allowRule }); + + console.log(`API响应:`, response); + + // 处理不同的响应格式 + if (response && (response.success || response.status === 'success')) { + // 重新加载日志,显示更新后的状态 + loadLogs(); + + // 刷新规则列表 + refreshRulesList(); + + // 显示成功通知 + if (typeof window.showNotification === 'function') { + window.showNotification(`已成功放行域名: ${domain}`, 'success'); + } + } else { + const errorMsg = response ? (response.message || '添加放行规则失败') : '添加放行规则失败: 无效的API响应'; + console.error(`放行域名失败: ${errorMsg}`); + throw new Error(errorMsg); + } + } catch (error) { + console.error('放行域名失败:', error); + + // 显示错误通知 + if (typeof window.showNotification === 'function') { + window.showNotification(`放行域名失败: ${error.message}`, 'danger'); + } + } +} + // 定期更新日志统计数据(备用方案) setInterval(() => { // 只有在查询日志页面时才更新 diff --git a/static/js/shield.js b/static/js/shield.js index 4ece01e..a9ec599 100644 --- a/static/js/shield.js +++ b/static/js/shield.js @@ -231,7 +231,7 @@ async function loadShieldStats() { } } -// 加载本地规则 +// 加载自定义规则 async function loadLocalRules() { try { const response = await fetch('/api/shield/localrules'); @@ -242,7 +242,7 @@ async function loadLocalRules() { const data = await response.json(); - // 更新本地规则数量显示 + // 更新自定义规则数量显示 if (document.getElementById('local-rules-count')) { document.getElementById('local-rules-count').textContent = data.localRulesCount || 0; } @@ -250,7 +250,7 @@ async function loadLocalRules() { // 设置当前规则类型 currentRulesType = 'local'; - // 合并所有本地规则 + // 合并所有自定义规则 let rules = []; // 添加域名规则 if (Array.isArray(data.domainRules)) { @@ -271,8 +271,8 @@ async function loadLocalRules() { updateRulesTable(rules); } catch (error) { - console.error('加载本地规则失败:', error); - showNotification('加载本地规则失败', 'error'); + console.error('加载自定义规则失败:', error); + showNotification('加载自定义规则失败', 'error'); } } @@ -1202,7 +1202,7 @@ let currentRulesType = 'local'; // 设置事件监听器 function setupShieldEventListeners() { - // 本地规则管理事件 + // 自定义规则管理事件 const saveRuleBtn = document.getElementById('save-rule-btn'); if (saveRuleBtn) { saveRuleBtn.addEventListener('click', handleAddRule); @@ -1214,7 +1214,7 @@ function setupShieldEventListeners() { saveBlacklistBtn.addEventListener('click', handleAddBlacklist); } - // 添加切换查看本地规则和远程规则的事件监听 + // 添加切换查看自定义规则和远程规则的事件监听 const viewLocalRulesBtn = document.getElementById('view-local-rules-btn'); if (viewLocalRulesBtn) { viewLocalRulesBtn.addEventListener('click', loadLocalRules); diff --git a/temp_config.json b/temp_config.json new file mode 100644 index 0000000..237b7c2 --- /dev/null +++ b/temp_config.json @@ -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 + } +} \ No newline at end of file