Files
dot/blacklist.go
aixiao f5bf77927d feat(blacklist): 实现基于Trie树的高性能黑名单匹配系统
重构黑名单匹配算法,采用Trie前缀树数据结构替换原有的后缀匹配,
将百万级域名匹配复杂度从O(n)降至O(L),显著提升性能。

同时优化黑名单文件加载机制,支持hosts格式和通配符匹配,
并实现文件修改自动重载功能,提升系统的灵活性和实用性。

refactor: 重构README文档结构和内容展示

更新项目介绍文档,优化整体布局结构,添加项目徽章标识,
精简功能特性描述,改进快速开始指南,提供更清晰的使用说明。

chore(deps): 更新项目依赖库至最新版本

升级github.com/miekg/dns至v1.1.72版本,
更新golang.org/x/net至v0.52.0版本,
升级golang.org/x/sync至v0.20.0版本,
以及其他相关依赖库的版本更新。
2026-03-20 14:39:29 +08:00

152 lines
2.9 KiB
Go

package main
import (
"bufio"
"context"
"log"
"net"
"os"
"strings"
"sync/atomic"
"time"
"github.com/miekg/dns"
)
type BlacklistTrie struct {
root *trieNode
}
type trieNode struct {
children map[string]*trieNode
isEnd bool
rule string
}
func newTrie() *BlacklistTrie {
return &BlacklistTrie{root: &trieNode{children: make(map[string]*trieNode)}}
}
func (t *BlacklistTrie) Add(domain string) {
name := strings.TrimSuffix(strings.ToLower(domain), ".")
parts := strings.Split(name, ".")
curr := t.root
for i := len(parts) - 1; i >= 0; i-- {
p := parts[i]
if _, ok := curr.children[p]; !ok {
curr.children[p] = &trieNode{children: make(map[string]*trieNode)}
}
curr = curr.children[p]
}
curr.isEnd = true
curr.rule = name
}
func (t *BlacklistTrie) Match(domain string) (string, bool) {
name := strings.TrimSuffix(strings.ToLower(domain), ".")
parts := strings.Split(name, ".")
curr := t.root
for i := len(parts) - 1; i >= 0; i-- {
p := parts[i]
next, ok := curr.children[p]
if !ok {
return "", false
}
curr = next
if curr.isEnd {
return curr.rule, true
}
}
return "", false
}
func initBlacklist(ctx context.Context, path, rcodeStr string) (*atomic.Pointer[BlacklistTrie], int) {
var holder atomic.Pointer[BlacklistTrie]
var lastMod time.Time
rcode := parseRcode(rcodeStr)
load := func() {
if path == "" {
return
}
info, err := os.Stat(path)
if err != nil {
log.Printf("[blacklist] Stat error: %v", err)
return
}
// 如果文件没动,不加载
if !info.ModTime().After(lastMod) {
return
}
f, err := os.Open(path)
if err != nil {
return
}
defer f.Close()
trie := newTrie()
scanner := bufio.NewScanner(f)
count := 0
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || line[0] == '#' || line[0] == ';' {
continue
}
fields := strings.Fields(line)
domain := fields[0]
if net.ParseIP(domain) != nil && len(fields) > 1 {
domain = fields[1]
}
trie.Add(domain)
count++
}
holder.Store(trie)
lastMod = info.ModTime()
log.Printf("[blacklist] Loaded %d rules (updated: %v)", count, lastMod.Format(time.RFC3339))
}
load()
if path != "" {
go func() {
// 缩短检查间隔,但因为有文件时间校验,所以不费 CPU
ticker := time.NewTicker(5 * time.Minute)
for {
select {
case <-ticker.C:
load()
case <-ctx.Done():
return
}
}
}()
}
return &holder, rcode
}
func parseRcode(s string) int {
switch strings.ToUpper(s) {
case "NXDOMAIN":
return dns.RcodeNameError
case "SERVFAIL":
return dns.RcodeServerFailure
default:
return dns.RcodeRefused
}
}
func makeBlockedMsg(rcode int, rule string) *dns.Msg {
m := new(dns.Msg)
m.Rcode = rcode
m.RecursionAvailable = true
o := new(dns.OPT)
o.Hdr.Name = "."
o.Hdr.Rrtype = dns.TypeOPT
o.Option = append(o.Option, &dns.EDNS0_EDE{InfoCode: 15, ExtraText: "Blocked by policy"})
m.Extra = append(m.Extra, o)
return m
}