// domainrules.go:域名规则 trie(后缀匹配 direct/pool:xxx/deny)。 // 规则从 SQLite rules 表加载,Reload() 热载;gateway policy 与 // proxymanager 共享同一表(design-arch §5.3)。 package policy import ( "strings" "sync" "onesvm.com/onesvm/browser-server/internal/store" ) // Action 域名路由动作。 const ( ActionDirect = "direct" ActionDeny = "deny" // ActionPool 前缀 pool:(如 pool:us-vless)。 ActionPoolPrefix = "pool:" ) // node trie 节点:按域名标签倒序匹配(com → example → api)。 type node struct { children map[string]*node action string // 命中节点上的动作(最长后缀优先) } func newNode() *node { return &node{children: make(map[string]*node)} } // DomainTrie 并发安全域名规则 trie。 type DomainTrie struct { mu sync.RWMutex root *node ver int64 // 加载代次(观测热载生效) } // NewDomainTrie 空 trie。 func NewDomainTrie() *DomainTrie { return &DomainTrie{root: newNode()} } // LoadFromStore 从 rules 表全量加载(Reload 热载路径)。 // 非 suffix 类型跳过(glob 由调用方另行处理,首版以 suffix 为主)。 func (t *DomainTrie) LoadFromStore(db *store.DB) (int, error) { rules, err := db.RulesAll() if err != nil { return 0, err } root := newNode() n := 0 for _, r := range rules { if r.MatchType != "suffix" { continue } t.insert(root, r.Value, r.Action) n++ } t.mu.Lock() t.root = root t.ver++ t.mu.Unlock() return n, nil } // Insert 单条内存插入(value 如 .blocked.com,action 如 deny)。 // 供无 SQLite 场景(proxymanager 单测 / CLI 即时规则)使用; // 与 LoadFromStore 同一 insert 路径,不破坏热载代次语义。 func (t *DomainTrie) Insert(value, action string) { t.mu.Lock() t.insert(t.root, value, action) t.ver++ t.mu.Unlock() } // insert 插入一条后缀规则(value 形如 .onesvm.com 或 onesvm.com)。 func (t *DomainTrie) insert(root *node, value, action string) { labels := splitLabels(value) cur := root for i := len(labels) - 1; i >= 0; i-- { lk := labels[i] next, ok := cur.children[lk] if !ok { next = newNode() cur.children[lk] = next } cur = next } cur.action = action } // splitLabels 域名转小写标签切片(去前导点)。 func splitLabels(domain string) []string { d := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(domain), ".")) if d == "" { return nil } return strings.Split(d, ".") } // Lookup 最长后缀匹配。返回 (action, 是否命中)。 // example.com 匹配规则 .example.com 与 .com;未命中返回 ("", false)。 func (t *DomainTrie) Lookup(host string) (string, bool) { labels := splitLabels(host) if labels == nil { return "", false } t.mu.RLock() defer t.mu.RUnlock() cur := t.root action := "" found := false for i := len(labels) - 1; i >= 0; i-- { next, ok := cur.children[labels[i]] if !ok { break } if next.action != "" { action = next.action found = true } cur = next } return action, found } // IsDenied 命中 deny 的便捷判定。 func (t *DomainTrie) IsDenied(host string) bool { a, ok := t.Lookup(host) return ok && a == ActionDeny } // Generation 当前加载代次(热载观测)。 func (t *DomainTrie) Generation() int64 { t.mu.RLock() defer t.mu.RUnlock() return t.ver }