docs(readme): 重构 README 文档结构与内容以提升可读性

- 更新项目标题图标并优化描述语句
- 重新组织特性列表为表格形式,增加 ECS 剥离、黑名单过滤等功能说明
- 补充快速开始章节,细化源码构建与 Docker 使用方式
- 调整参数说明表,新增黑名单相关配置项及缓存条目限制
- 增加缓存机制详解、黑名单功能使用示例与架构图
- 更新开发依赖信息与推荐编译参数
- 修正作者信息展示格式并添加仓库链接

feat(cache): 改进缓存键生成逻辑与 EDNS 元数据处理

- 使用 dns.CanonicalName 规范化域名避免重复缓存键
- 缓存条目中保存 EDNS 扩展信息(version, rcode, EDE)
- 修复缓存读取函数返回值,传递完整缓存元数据
- 调整 TTL 计算优先级,仅在必要时检查 Extra 区域
- 黑名单匹配提前拦截请求,跳过上游查询
- 启动日志中显示黑名单规则数量与返回码设置
```
This commit is contained in:
2025-10-15 14:19:55 +08:00
parent 916a7c8127
commit 4060e83686
5 changed files with 404 additions and 90 deletions

188
blacklist.go Normal file
View File

@@ -0,0 +1,188 @@
package main
import (
"bufio"
"context"
"log"
"os"
"sort"
"strings"
"time"
"github.com/miekg/dns"
)
// -------- Blacklist helpers (独立文件) --------
// 将输入域名规范化为 canonical FQDN去空格、去 *. 前缀、统一大小写/尾点)
func canonicalFQDN(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
s = strings.TrimPrefix(s, "*.")
return dns.CanonicalName(s)
}
func uniqueStrings(in []string) []string {
seen := make(map[string]struct{}, len(in))
out := make([]string, 0, len(in))
for _, s := range in {
if s == "" {
continue
}
if _, ok := seen[s]; ok {
continue
}
seen[s] = struct{}{}
out = append(out, s)
}
return out
}
// 支持 # / ; 注释;每行一个域名;支持以 "*.example.com" 书写
func loadBlacklistFile(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
sc := bufio.NewScanner(f)
var rules []string
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" {
continue
}
if i := strings.IndexAny(line, "#;"); i >= 0 {
line = strings.TrimSpace(line[:i])
}
if r := canonicalFQDN(line); r != "" {
rules = append(rules, r)
}
}
if err := sc.Err(); err != nil {
return nil, err
}
return uniqueStrings(rules), nil
}
// 自动重载黑名单
func startBlacklistReloader(ctx context.Context, path string, interval time.Duration, current **suffixMatcher) {
if path == "" {
return
}
go func() {
var lastMod time.Time
for {
select {
case <-ctx.Done():
return
case <-time.After(interval):
fi, err := os.Stat(path)
if err != nil {
log.Printf("[blacklist] reload check failed: %v", err)
continue
}
modTime := fi.ModTime()
if modTime.After(lastMod) {
rules, err := loadBlacklistFile(path)
if err != nil {
log.Printf("[blacklist] reload failed: %v", err)
continue
}
*current = newSuffixMatcher(rules)
lastMod = modTime
log.Printf("[blacklist] reloaded %d rules (modified %s)", len(rules), modTime.Format(time.RFC3339))
}
}
}
}()
}
// 后缀匹配器rules 已 canonical按长度降序排列更精确的规则优先
type suffixMatcher struct {
rules []string
}
func newSuffixMatcher(rules []string) *suffixMatcher {
rs := uniqueStrings(rules)
sort.Slice(rs, func(i, j int) bool { return len(rs[i]) > len(rs[j]) })
return &suffixMatcher{rules: rs}
}
// 命中则返回匹配的规则
func (m *suffixMatcher) match(name string) (string, bool) {
if m == nil || len(m.rules) == 0 {
return "", false
}
name = dns.CanonicalName(name)
for _, r := range m.rules {
if name == r || strings.HasSuffix(name, "."+r) {
return r, true
}
}
return "", false
}
// 解析 RCODE 文本到常量
func parseRcode(s string) int {
switch strings.ToUpper(strings.TrimSpace(s)) {
case "REFUSED", "":
return dns.RcodeRefused
case "NXDOMAIN":
return dns.RcodeNameError
case "SERVFAIL":
return dns.RcodeServerFailure
default:
log.Printf("[blacklist] unknown rcode %q, fallback REFUSED", s)
return dns.RcodeRefused
}
}
// 构造“伪上游”阻断响应;带 EDE=Blocked(15) 说明命中规则
func makeBlockedUpstream(rcode int, rule string) *dns.Msg {
m := new(dns.Msg)
m.RecursionAvailable = true
m.AuthenticatedData = false
m.Rcode = rcode
opt := &dns.OPT{}
opt.Hdr.Name = "."
opt.Hdr.Rrtype = dns.TypeOPT
ede := &dns.EDNS0_EDE{InfoCode: 15, ExtraText: "blocked by policy: " + rule}
opt.Option = append(opt.Option, ede)
m.Extra = append(m.Extra, opt)
return m
}
func initBlacklist(ctx context.Context, listStr, filePath, rcodeStr string) (*suffixMatcher, int) {
var rules []string
if v := strings.TrimSpace(listStr); v != "" {
for _, s := range strings.Split(v, ",") {
if r := canonicalFQDN(s); r != "" {
rules = append(rules, r)
}
}
}
if file := strings.TrimSpace(filePath); file != "" {
fileRules, err := loadBlacklistFile(file)
if err != nil {
log.Fatalf("[fatal] failed to load blacklist-file %q: %v", file, err)
}
rules = append(rules, fileRules...)
}
bl := newSuffixMatcher(rules)
blRcode := parseRcode(rcodeStr)
if filePath != "" {
startBlacklistReloader(ctx, filePath, 30*time.Second, &bl)
}
log.Printf("[blacklist] loaded %d rules (file=%v, rcode=%s)",
len(bl.rules), filePath != "", strings.ToUpper(rcodeStr))
return bl, blRcode
}