重构黑名单匹配算法,采用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版本, 以及其他相关依赖库的版本更新。
152 lines
2.9 KiB
Go
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
|
|
}
|