feat(blacklist): 支持国际化域名与 hosts 文件风格黑名单

- 引入 `golang.org/x/net/idna` 实现 Unicode 域名转 ASCII(Punycode)
- 黑名单加载支持通配符格式如 `*.example.com`
- 支持解析 hosts 风格的文件(每行首列为 IP 地址时,其余列为域名)
- 扩展 Scanner 缓冲区至 2MB 以适应大型 hosts 文件
- 注释处理优化,兼容 `#` 和 `;` 分隔符
- 加载后对规则排序并去重,提升匹配效率与一致性

fix(cache): 调整负面响应缓存逻辑与上游查询并发控制

- 明确区分 NXDOMAIN 与 NODATA 并正确处理 SOA 缺失情况
- 查询上游时引入更可靠的并发限制与超时机制
- UDP 截断时自动回退 TCP 查询
- 过滤无效 RCODE(如 SERVFAIL、REFUSED 等),防止污染结果
- 区分“全部失败”与“部分完成但无有效响应”,增强调试日志信息
This commit is contained in:
2025-10-17 17:28:44 +08:00
parent 1ab273e2a8
commit 767ada5e43
5 changed files with 80 additions and 31 deletions

View File

@@ -12,6 +12,7 @@ import (
"time"
"github.com/miekg/dns"
"golang.org/x/net/idna"
)
// -------- Blacklist helpers (独立文件) --------
@@ -22,7 +23,14 @@ func canonicalFQDN(s string) string {
if s == "" {
return ""
}
// 允许黑名单写 "*.example.com";内部匹配用裸后缀
s = strings.TrimPrefix(s, "*.")
// 先把可能的中文/Unicode 域名转成 ASCIIpunycode再规范化
if a, err := idna.Lookup.ToASCII(s); err == nil {
s = a
}
// CanonicalName 会做小写化与尾点规范化
return dns.CanonicalName(s)
}
@@ -43,38 +51,38 @@ func uniqueStrings(in []string) []string {
}
// 支持 # / ; 注释;每行一个域名;支持以 "*.example.com" 书写
// 支持 # / ; 注释;每行一个域名;支持以 "*.example.com" 书写;支持 hosts 风格(首列为 IP
func loadBlacklistFile(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
sc := bufio.NewScanner(f)
// 默认 64KB 容量不够稳妥,这里放大到 2MB兼容一些合并的大 hosts 列表
sc.Buffer(make([]byte, 0, 64*1024), 2*1024*1024)
var rules []string
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" {
continue
}
// 行首注释
if strings.HasPrefix(line, "//") || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
// 行内注释:先 // 再 # ;
if i := strings.Index(line, "//"); i >= 0 {
line = strings.TrimSpace(line[:i])
}
// 去掉注释(# 或 ; 之后的内容)
if i := strings.IndexAny(line, "#;"); i >= 0 {
line = strings.TrimSpace(line[:i])
if line == "" {
continue
}
}
if line == "" {
continue
}
// hosts 风格:第一个字段是 IP则其余每个字段视为域名
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
// hosts 风格:第一个字段是 IP则其余每个字段视为域名
start := 0
if net.ParseIP(fields[0]) != nil {
start = 1
@@ -88,7 +96,10 @@ func loadBlacklistFile(path string) ([]string, error) {
if err := sc.Err(); err != nil {
return nil, err
}
return uniqueStrings(rules), nil
sort.Strings(rules)
rules = uniqueStrings(rules)
return rules, nil
}
// 自动重载黑名单