initial commit
This commit is contained in:
1 parent
5b935301e5
commit
85453852d5
105 files changed
+24846
No files matched your search
@@ -0,0 +1,737 @@
|
||||
// Package blocklist compiles heterogeneous block/allow sources (hosts files,
|
||||
// adblock lists, plain domain lists and RPZ policy zones) into a single
|
||||
// immutable Set that answers "what should I do with this name?" in a handful
|
||||
// of map lookups.
|
||||
//
|
||||
// A Set is never mutated after construction; Manager swaps a whole new Set in
|
||||
// atomically when lists are refreshed, so a query either sees the old rules or
|
||||
// the new ones, never a half-applied mixture.
|
||||
package blocklist
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Action is what to do with a matched name.
|
||||
type Action uint8
|
||||
|
||||
const (
|
||||
// ActionBlock uses the manager's default response (nxdomain/zero/...).
|
||||
ActionBlock Action = iota
|
||||
// ActionAllow whitelists the name: blocklists are skipped entirely.
|
||||
ActionAllow
|
||||
// ActionPassthrough resolves the name upstream but ignores blocklists
|
||||
// (RPZ rpz-passthru).
|
||||
ActionPassthrough
|
||||
// ActionDrop answers nothing at all (RPZ rpz-drop).
|
||||
ActionDrop
|
||||
// ActionNXDomain answers NXDOMAIN regardless of the default response.
|
||||
ActionNXDomain
|
||||
// ActionZero answers the configured sinkhole address.
|
||||
ActionZero
|
||||
// ActionCNAME answers a CNAME to Response.
|
||||
ActionCNAME
|
||||
// ActionRedirect answers A/AAAA for Response as an address literal.
|
||||
ActionRedirect
|
||||
)
|
||||
|
||||
func (a Action) String() string {
|
||||
switch a {
|
||||
case ActionBlock:
|
||||
return "block"
|
||||
case ActionAllow:
|
||||
return "allow"
|
||||
case ActionPassthrough:
|
||||
return "passthrough"
|
||||
case ActionDrop:
|
||||
return "drop"
|
||||
case ActionNXDomain:
|
||||
return "nxdomain"
|
||||
case ActionZero:
|
||||
return "zero"
|
||||
case ActionCNAME:
|
||||
return "cname"
|
||||
case ActionRedirect:
|
||||
return "redirect"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// ParseAction maps configuration and UI strings onto an Action.
|
||||
func ParseAction(s string) (Action, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||
case "", "block":
|
||||
return ActionBlock, true
|
||||
case "allow", "whitelist":
|
||||
return ActionAllow, true
|
||||
case "passthrough", "pass":
|
||||
return ActionPassthrough, true
|
||||
case "drop":
|
||||
return ActionDrop, true
|
||||
case "nxdomain":
|
||||
return ActionNXDomain, true
|
||||
case "zero", "sinkhole", "null":
|
||||
return ActionZero, true
|
||||
case "cname":
|
||||
return ActionCNAME, true
|
||||
case "redirect":
|
||||
return ActionRedirect, true
|
||||
}
|
||||
return ActionBlock, false
|
||||
}
|
||||
|
||||
// Entry is one compiled rule. It is shared by every name that maps to it, so
|
||||
// keep it small: the strings are interned per source.
|
||||
//
|
||||
// The hit counter is a pointer so an Entry can be copied by value (the API
|
||||
// returns copies) without copying a mutex or tripping go vet's lock check.
|
||||
type Entry struct {
|
||||
// Name is the rule's pattern, normalised (lowercase, no trailing dot).
|
||||
Name string
|
||||
Action Action
|
||||
// Response is the sinkhole address or CNAME target for the actions that
|
||||
// need one; empty otherwise.
|
||||
Response string
|
||||
// Source is the list or file the rule came from (shown in the UI).
|
||||
Source string
|
||||
// Categories are the list's tags, copied per rule.
|
||||
Categories []string
|
||||
// Priority orders allow-over-block conflicts: higher wins, and ties break
|
||||
// towards the first rule seen.
|
||||
Priority int
|
||||
// hits counts matches since the set was built. A pointer keeps Entry
|
||||
// copyable by value, which the admin API relies on.
|
||||
hits *atomic.Uint64
|
||||
// TTL overrides the manager's response TTL for this rule (RPZ records
|
||||
// carry their own TTL).
|
||||
TTL uint32
|
||||
}
|
||||
|
||||
// Hits returns how many times this rule matched.
|
||||
func (e *Entry) Hits() uint64 {
|
||||
if e == nil || e.hits == nil {
|
||||
return 0
|
||||
}
|
||||
return e.hits.Load()
|
||||
}
|
||||
|
||||
func (e *Entry) markHit() {
|
||||
if e == nil || e.hits == nil {
|
||||
return
|
||||
}
|
||||
e.hits.Add(1)
|
||||
}
|
||||
|
||||
// IsAllow reports whether the rule whitelists the name.
|
||||
func (e *Entry) IsAllow() bool {
|
||||
return e.Action == ActionAllow || e.Action == ActionPassthrough
|
||||
}
|
||||
|
||||
// Rule is a pattern plus the action it implies, as produced by the parsers.
|
||||
type Rule struct {
|
||||
// Name is the pattern; may start with "*." (subdomains only) or
|
||||
// "**." (subdomains and the apex itself).
|
||||
Name string
|
||||
Action Action
|
||||
// Response is an address literal (for redirect) or a target name (cname).
|
||||
Response string
|
||||
// TTL is a per-rule TTL when the source provides one.
|
||||
TTL uint32
|
||||
// Category tags the rule.
|
||||
Category string
|
||||
// Priority disambiguates allow/block conflicts.
|
||||
Priority int
|
||||
}
|
||||
|
||||
// Set is an immutable compiled rule table.
|
||||
//
|
||||
// Three maps cover the pattern space:
|
||||
//
|
||||
// exact[name] "ads.example.com" the name and nothing else
|
||||
// sub[example.com] "*.example.com" strict subdomains
|
||||
// root[example.com] "**.example.com" subdomains and the apex
|
||||
//
|
||||
// Lookup is one hash for the common case (exact hit or "no wildcards at all"),
|
||||
// and at most one hash per label otherwise — no per-query allocation.
|
||||
type Set struct {
|
||||
exact map[string]*Entry
|
||||
sub map[string]*Entry
|
||||
root map[string]*Entry
|
||||
// hasWild lets Match skip the suffix walk for sets of pure exact rules,
|
||||
// which is the common hosts-file case.
|
||||
hasWild bool
|
||||
// meta is per-source bookkeeping for the UI.
|
||||
meta map[string]SourceMeta
|
||||
// counts is the number of compiled rules, per kind.
|
||||
counts RuleCounts
|
||||
// total is the number of rules dropped as duplicates or malformed.
|
||||
duplicates int
|
||||
// builtAt is when this set was compiled.
|
||||
builtAt time.Time
|
||||
// digest covers the compiled patterns, so two instances can compare rules
|
||||
// without shipping the rules around.
|
||||
digest string
|
||||
// sources is the ordered list of source names that contributed.
|
||||
sources []string
|
||||
// maxDepth is the longest pattern label count, used to bound the walk.
|
||||
maxDepth int
|
||||
}
|
||||
|
||||
// RuleCounts breaks down compiled rules by kind.
|
||||
type RuleCounts struct {
|
||||
Exact int
|
||||
Sub int
|
||||
Root int
|
||||
Allow int
|
||||
Block int
|
||||
Passthru int
|
||||
Drop int
|
||||
}
|
||||
|
||||
// SourceMeta describes one contributing source.
|
||||
type SourceMeta struct {
|
||||
Name string
|
||||
Format string
|
||||
Rules int
|
||||
Bytes int64
|
||||
ETag string
|
||||
LastMod string
|
||||
UpdatedAt time.Time
|
||||
Duration time.Duration
|
||||
Err string
|
||||
Enabled bool
|
||||
URL string
|
||||
Path string
|
||||
Stale bool
|
||||
HTTPStatus int
|
||||
}
|
||||
|
||||
// Builder accumulates rules and produces a Set.
|
||||
type Builder struct {
|
||||
exact map[string]*Entry
|
||||
sub map[string]*Entry
|
||||
root map[string]*Entry
|
||||
meta map[string]SourceMeta
|
||||
order []string
|
||||
dup int
|
||||
counts RuleCounts
|
||||
}
|
||||
|
||||
// NewBuilder returns an empty Builder.
|
||||
func NewBuilder() *Builder {
|
||||
return &Builder{
|
||||
exact: make(map[string]*Entry, 1<<12),
|
||||
sub: make(map[string]*Entry, 1<<10),
|
||||
root: make(map[string]*Entry, 1<<10),
|
||||
meta: make(map[string]SourceMeta, 8),
|
||||
}
|
||||
}
|
||||
|
||||
// Add compiles one rule. Names are normalised here, so callers may pass
|
||||
// user-typed domain names.
|
||||
func (b *Builder) Add(r Rule, source string, categories []string) {
|
||||
name, kind := splitPattern(r.Name)
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
prio := r.Priority
|
||||
if prio == 0 {
|
||||
// Allow rules outrank block rules by default so an explicit
|
||||
// whitelist always wins over a list entry.
|
||||
if r.Action == ActionAllow || r.Action == ActionPassthrough {
|
||||
prio = 10
|
||||
} else {
|
||||
prio = 1
|
||||
}
|
||||
}
|
||||
e := &Entry{
|
||||
hits: new(atomic.Uint64),
|
||||
Name: name,
|
||||
Action: r.Action,
|
||||
Response: r.Response,
|
||||
Source: source,
|
||||
Categories: categories,
|
||||
Priority: prio,
|
||||
TTL: r.TTL,
|
||||
}
|
||||
if r.Category != "" {
|
||||
e.Categories = append(e.Categories, r.Category)
|
||||
}
|
||||
var m map[string]*Entry
|
||||
switch kind {
|
||||
case patternExact:
|
||||
m = b.exact
|
||||
case patternSub:
|
||||
m = b.sub
|
||||
case patternRoot:
|
||||
m = b.root
|
||||
}
|
||||
if existing, ok := m[name]; ok {
|
||||
b.dup++
|
||||
// Higher priority wins; on a tie the existing rule stays, so the
|
||||
// order lists appear in the config is meaningful.
|
||||
if prio <= existing.Priority {
|
||||
return
|
||||
}
|
||||
b.uncount(existing)
|
||||
m[name] = e
|
||||
b.countAction(e, 1)
|
||||
return
|
||||
}
|
||||
m[name] = e
|
||||
b.countAction(e, 1)
|
||||
if _, seen := b.meta[source]; !seen {
|
||||
b.order = append(b.order, source)
|
||||
}
|
||||
}
|
||||
|
||||
// metaFor reports whether a source already contributed to this build.
|
||||
func (b *Builder) metaFor(name string) (SourceMeta, bool) {
|
||||
m, ok := b.meta[name]
|
||||
return m, ok
|
||||
}
|
||||
|
||||
func (b *Builder) countAction(e *Entry, delta int) {
|
||||
switch e.Action {
|
||||
case ActionAllow:
|
||||
b.counts.Allow += delta
|
||||
case ActionPassthrough:
|
||||
b.counts.Passthru += delta
|
||||
case ActionDrop:
|
||||
b.counts.Drop += delta
|
||||
default:
|
||||
b.counts.Block += delta
|
||||
}
|
||||
}
|
||||
|
||||
// uncount removes an entry that a higher-priority rule is replacing.
|
||||
func (b *Builder) uncount(e *Entry) {
|
||||
switch e.Action {
|
||||
case ActionAllow:
|
||||
b.counts.Allow--
|
||||
case ActionPassthrough:
|
||||
b.counts.Passthru--
|
||||
case ActionDrop:
|
||||
b.counts.Drop--
|
||||
default:
|
||||
b.counts.Block--
|
||||
}
|
||||
}
|
||||
|
||||
// NoteSource records (or updates) bookkeeping for a contributing source.
|
||||
func (b *Builder) NoteSource(meta SourceMeta) {
|
||||
if _, seen := b.meta[meta.Name]; !seen {
|
||||
b.order = append(b.order, meta.Name)
|
||||
}
|
||||
b.meta[meta.Name] = meta
|
||||
}
|
||||
|
||||
// SourceCount is the number of contributing sources.
|
||||
func (b *Builder) SourceCount() int { return len(b.meta) }
|
||||
|
||||
// Build freezes the accumulated rules into an immutable Set.
|
||||
func (b *Builder) Build() *Set {
|
||||
s := &Set{
|
||||
exact: b.exact,
|
||||
sub: b.sub,
|
||||
root: b.root,
|
||||
meta: b.meta,
|
||||
sources: b.order,
|
||||
builtAt: time.Now(),
|
||||
duplicates: b.dup,
|
||||
hasWild: len(b.sub) > 0 || len(b.root) > 0,
|
||||
}
|
||||
s.counts = b.counts
|
||||
s.counts.Exact = len(b.exact)
|
||||
s.counts.Sub = len(b.sub)
|
||||
s.counts.Root = len(b.root)
|
||||
s.digest = b.digest()
|
||||
for k := range b.exact {
|
||||
if d := labelCount(k); d > s.maxDepth {
|
||||
s.maxDepth = d
|
||||
}
|
||||
}
|
||||
for k := range b.sub {
|
||||
if d := labelCount(k); d > s.maxDepth {
|
||||
s.maxDepth = d
|
||||
}
|
||||
}
|
||||
for k := range b.root {
|
||||
if d := labelCount(k); d > s.maxDepth {
|
||||
s.maxDepth = d
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// digest hashes every compiled pattern together with its action. The map walk
|
||||
// makes it order-independent, but stable for a given rule set, which is exactly
|
||||
// what cluster convergence checking needs.
|
||||
func (b *Builder) digest() string {
|
||||
h := sha256.New()
|
||||
var buf [3]byte
|
||||
write := func(kind byte, name string, e *Entry) {
|
||||
buf[0] = kind
|
||||
h.Write(buf[:1])
|
||||
io.WriteString(h, name)
|
||||
buf[1] = byte(e.Action)
|
||||
binary.LittleEndian.PutUint16(buf[1:], uint16(e.Priority))
|
||||
h.Write(buf[:3])
|
||||
}
|
||||
for name, e := range b.exact {
|
||||
write(0, name, e)
|
||||
}
|
||||
for name, e := range b.sub {
|
||||
write(1, name, e)
|
||||
}
|
||||
for name, e := range b.root {
|
||||
write(2, name, e)
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)[:8])
|
||||
}
|
||||
|
||||
// Digest returns the compiled content's digest.
|
||||
func (s *Set) Digest() string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return s.digest
|
||||
}
|
||||
|
||||
type patternKind uint8
|
||||
|
||||
const (
|
||||
patternExact patternKind = iota
|
||||
patternSub
|
||||
patternRoot
|
||||
)
|
||||
|
||||
// splitPattern normalises a rule name and reports which map it belongs in.
|
||||
//
|
||||
// "Example.COM." -> exact "example.com"
|
||||
// "*.example.com" -> sub "example.com"
|
||||
// "**.example.com" -> root "example.com"
|
||||
// "." (the whole internet) is dropped: it is never a useful rule.
|
||||
func splitPattern(in string) (string, patternKind) {
|
||||
name := strings.ToLower(strings.TrimSpace(in))
|
||||
if name == "" {
|
||||
return "", patternExact
|
||||
}
|
||||
kind := patternExact
|
||||
switch {
|
||||
case strings.HasPrefix(name, "**."):
|
||||
kind = patternRoot
|
||||
name = name[3:]
|
||||
case strings.HasPrefix(name, "*."):
|
||||
kind = patternSub
|
||||
name = name[2:]
|
||||
case strings.HasPrefix(name, "."):
|
||||
// A leading dot in hosts/adblock convention means "and subdomains".
|
||||
kind = patternRoot
|
||||
name = name[1:]
|
||||
}
|
||||
name = strings.TrimSuffix(name, ".")
|
||||
name = strings.Trim(name, ".")
|
||||
if name == "" || name == "." {
|
||||
return "", patternExact
|
||||
}
|
||||
// A name with an interior "*" is not something we can match; treat the
|
||||
// first label as a fixed prefix and drop the rest, which degrades to
|
||||
// "closest sane behaviour" instead of silently not matching.
|
||||
if i := strings.IndexByte(name, '*'); i >= 0 {
|
||||
name = name[:i]
|
||||
name = strings.TrimSuffix(name, ".")
|
||||
if name == "" {
|
||||
return "", patternExact
|
||||
}
|
||||
}
|
||||
return name, kind
|
||||
}
|
||||
|
||||
// NormalizeName canonicalises a queried name for matching: lowercased, no
|
||||
// trailing dot. It is allocation-free for the common case.
|
||||
func NormalizeName(name string) string {
|
||||
if len(name) > 0 && name[len(name)-1] == '.' {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
// Match returns the rule that applies to name, or nil. name may be fqdn or
|
||||
// relative; the caller is responsible for lowercasing only if it has not
|
||||
// already.
|
||||
func (s *Set) Match(name string) *Entry {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
name = NormalizeName(name)
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
if e, ok := s.exact[name]; ok {
|
||||
e.markHit()
|
||||
return e
|
||||
}
|
||||
if !s.hasWild {
|
||||
return nil
|
||||
}
|
||||
// The apex pattern "**.x" also covers the name itself.
|
||||
if e, ok := s.root[name]; ok {
|
||||
e.markHit()
|
||||
return e
|
||||
}
|
||||
// Walk strictly shorter suffixes, most specific first.
|
||||
for off := labelEnd(name); off < len(name); off = labelEnd(name[off:]) + off {
|
||||
suffix := name[off:]
|
||||
if e, ok := s.root[suffix]; ok {
|
||||
e.markHit()
|
||||
return e
|
||||
}
|
||||
if e, ok := s.sub[suffix]; ok {
|
||||
e.markHit()
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// labelEnd returns the index just past the first label of s, or len(s) when s
|
||||
// has no dot.
|
||||
func labelEnd(s string) int {
|
||||
if i := strings.IndexByte(s, '.'); i >= 0 {
|
||||
return i + 1
|
||||
}
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func labelCount(s string) int {
|
||||
n := 1
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '.' {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Len reports the total number of compiled rules.
|
||||
func (s *Set) Len() int {
|
||||
if s == nil {
|
||||
return 0
|
||||
}
|
||||
return s.counts.Exact + s.counts.Sub + s.counts.Root
|
||||
}
|
||||
|
||||
// Counts returns the per-kind rule breakdown.
|
||||
func (s *Set) Counts() RuleCounts {
|
||||
if s == nil {
|
||||
return RuleCounts{}
|
||||
}
|
||||
return s.counts
|
||||
}
|
||||
|
||||
// HasWildcards reports whether any subdomain rules exist.
|
||||
func (s *Set) HasWildcards() bool { return s != nil && s.hasWild }
|
||||
|
||||
// Duplicates is the number of rules dropped because a higher- or equal-priority
|
||||
// rule already claimed the name.
|
||||
func (s *Set) Duplicates() int { return s.DuplicatesN() }
|
||||
|
||||
// DuplicatesN is Duplicates with a nil guard.
|
||||
func (s *Set) DuplicatesN() int {
|
||||
if s == nil {
|
||||
return 0
|
||||
}
|
||||
return s.duplicates
|
||||
}
|
||||
|
||||
// BuiltAt is when the set was compiled.
|
||||
func (s *Set) BuiltAt() time.Time {
|
||||
if s == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return s.builtAt
|
||||
}
|
||||
|
||||
// Sources lists the contributing source names in order.
|
||||
func (s *Set) Sources() []string {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.sources
|
||||
}
|
||||
|
||||
// SourceMeta returns bookkeeping for one source.
|
||||
func (s *Set) SourceMeta(name string) (SourceMeta, bool) {
|
||||
if s == nil {
|
||||
return SourceMeta{}, false
|
||||
}
|
||||
m, ok := s.meta[name]
|
||||
return m, ok
|
||||
}
|
||||
|
||||
// AllSourceMeta returns the per-source bookkeeping map.
|
||||
func (s *Set) AllSourceMeta() map[string]SourceMeta {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.meta
|
||||
}
|
||||
|
||||
// PatternKind says which of the three maps a rule lives in.
|
||||
type PatternKind uint8
|
||||
|
||||
const (
|
||||
// PatternExact is "ads.example.com": the name and nothing else.
|
||||
PatternExact PatternKind = iota
|
||||
// PatternSub is "*.example.com": strict subdomains.
|
||||
PatternSub
|
||||
// PatternRoot is "**.example.com": subdomains and the apex.
|
||||
PatternRoot
|
||||
)
|
||||
|
||||
// String renders the kind for the UI.
|
||||
func (k PatternKind) String() string {
|
||||
switch k {
|
||||
case PatternSub:
|
||||
return "subdomain"
|
||||
case PatternRoot:
|
||||
return "domain"
|
||||
default:
|
||||
return "exact"
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern renders a pattern of this kind for a stored name.
|
||||
func (k PatternKind) Pattern(name string) string {
|
||||
switch k {
|
||||
case PatternSub:
|
||||
return "*." + name
|
||||
case PatternRoot:
|
||||
return "**." + name
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
// Each visits every rule. It exists for the admin API's rule browser; the DNS
|
||||
// hot path uses Match and never pays this cost.
|
||||
func (s *Set) Each(fn func(pattern string, e *Entry, kind PatternKind) bool) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
for name, e := range s.exact {
|
||||
if !fn(name, e, PatternExact) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for name, e := range s.sub {
|
||||
if !fn(name, e, PatternSub) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for name, e := range s.root {
|
||||
if !fn(name, e, PatternRoot) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MatchPattern returns the kind of rule that matched a name, and the stored
|
||||
// pattern (without its wildcard prefix). It reports PatternExact when only an
|
||||
// exact rule matched.
|
||||
func (s *Set) MatchPattern(name string) PatternKind {
|
||||
if s == nil {
|
||||
return PatternExact
|
||||
}
|
||||
name = NormalizeName(name)
|
||||
if _, ok := s.exact[name]; ok {
|
||||
return PatternExact
|
||||
}
|
||||
if _, ok := s.root[name]; ok {
|
||||
return PatternRoot
|
||||
}
|
||||
for off := labelEnd(name); off < len(name); off = labelEnd(name[off:]) + off {
|
||||
suffix := name[off:]
|
||||
if _, ok := s.root[suffix]; ok {
|
||||
return PatternRoot
|
||||
}
|
||||
if _, ok := s.sub[suffix]; ok {
|
||||
return PatternSub
|
||||
}
|
||||
}
|
||||
return PatternExact
|
||||
}
|
||||
|
||||
// Lookup returns the exact rule for a name without walking wildcards. It is
|
||||
// used by the admin API to explain a match.
|
||||
func (s *Set) Lookup(name string) *Entry {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.exact[NormalizeName(name)]
|
||||
}
|
||||
|
||||
// Stats is a JSON-friendly snapshot for the API and UI.
|
||||
type Stats struct {
|
||||
Total int `json:"total"`
|
||||
Exact int `json:"exact"`
|
||||
Subdomains int `json:"subdomain_rules"`
|
||||
RootRules int `json:"root_rules"`
|
||||
Allow int `json:"allow"`
|
||||
Block int `json:"block"`
|
||||
Passthru int `json:"passthrough"`
|
||||
Drop int `json:"drop"`
|
||||
Duplicates int `json:"duplicates"`
|
||||
Sources int `json:"sources"`
|
||||
BuiltAt time.Time `json:"built_at"`
|
||||
PerSource map[string]SourceMeta `json:"sources_detail"`
|
||||
}
|
||||
|
||||
// Stats summarises the set for the API.
|
||||
func (s *Set) Stats() Stats {
|
||||
if s == nil {
|
||||
return Stats{PerSource: map[string]SourceMeta{}}
|
||||
}
|
||||
return Stats{
|
||||
Total: s.Len(),
|
||||
Exact: s.counts.Exact,
|
||||
Subdomains: s.counts.Sub,
|
||||
RootRules: s.counts.Root,
|
||||
Allow: s.counts.Allow,
|
||||
Block: s.counts.Block,
|
||||
Passthru: s.counts.Passthru,
|
||||
Drop: s.counts.Drop,
|
||||
Duplicates: s.duplicates,
|
||||
Sources: len(s.meta),
|
||||
BuiltAt: s.builtAt,
|
||||
PerSource: s.meta,
|
||||
}
|
||||
}
|
||||
|
||||
// Holder is a swappable Set, so hot paths can grab a pointer once per query
|
||||
// and be immune to a concurrent refresh.
|
||||
type Holder struct {
|
||||
cur atomic.Pointer[Set]
|
||||
}
|
||||
|
||||
// NewHolder wraps an initial set (may be nil).
|
||||
func NewHolder(s *Set) *Holder {
|
||||
h := &Holder{}
|
||||
if s != nil {
|
||||
h.cur.Store(s)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Get returns the current set (nil before the first build).
|
||||
func (h *Holder) Get() *Set { return h.cur.Load() }
|
||||
|
||||
// Set installs a new set.
|
||||
func (h *Holder) Set(s *Set) { h.cur.Store(s) }
|
||||
Reference in new issue
Block a user