blocklist

package
v1.0.126 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package blocklist is the host blocklist engine: O(labels) exact/wildcard matching with a never-block exceptions list, block/allow modes, sidecar persistence (.mode/.manual/.exceptions/.sources), per-feed attribution, and hosts-format line normalization. Extracted from package main's store.go per ADR-0002 (store.go decomposition, Phase A). The matcher (IsBlocked) sits on the proxy/SOCKS5 per-request hot path and moved VERBATIM — same RWMutex, same probe sequence. package main keeps the process-wide `bl` singleton and every admin/cluster surface behind type aliases.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NormalizeLine

func NormalizeLine(raw string) (string, bool)

NormalizeLine extracts the blockable host from one feed or blocklist-file line. It handles plain domain lists, /etc/hosts format ("0.0.0.0 domain" / "127.0.0.1 domain"), inline comments, accidental schemes, and trailing paths/ports. Returns ok=false for lines that carry no blockable host (comments, hosts-file boilerplate, unspecified/loopback IPs). Wildcard entries ("*.example.com") pass through untouched.

Types

type Entry

type Entry struct {
	Host   string `json:"host"`
	Source string `json:"source"`         // "manual" or "feed"
	Feed   string `json:"feed,omitempty"` // feed URL that imported this entry, when known
}

Entry is a single blocklist host with its origin.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store holds two separate maps for O(1) host lookups:

  • exact: e.g. "ads.example.com"
  • wildcards: keyed by dot-prefix, e.g. ".example.com" (from "*.example.com")

IsBlocked walks the host's own dot-labels to probe the wildcards map, so lookup cost is O(labels) ≈ O(1) for real-world domain names, regardless of how many wildcard rules are loaded. All methods are safe for concurrent use.

func New

func New() *Store

New returns an empty Store with initialized maps and the default "block" mode — the same shape as the pre-extraction package-main literal.

func (*Store) Add

func (b *Store) Add(host string)

Add inserts host ("*.example.com" → wildcard, otherwise exact) without persisting — the feed-sync path batches its own Save.

func (*Store) AddException

func (b *Store) AddException(host string)

AddException marks host as permanently exempt from blocking. Feed syncs will still add the host to the blocklist, but IsBlocked will always return false for it.

func (*Store) AddManual

func (b *Store) AddManual(host string)

AddManual adds a host and marks it as manually managed by an admin. Unlike Add (used by the feed syncer), this persists both the source attribution (the .manual sidecar via saveManual) AND the enforcement state (the main blocklist file via Save). The dual save makes the call self-durable so a caller path that bails before its own deferred Save (e.g. the apiBlocklist POST handler returning early on an invalid wildcard mid-loop, ui_policy.go) cannot leave manual entries in memory + sidecar but missing from the main file — which would not survive restart, because Load reads the main file into b.exact / b.wildcards (the maps IsBlocked consults) and the .manual sidecar only restores attribution metadata.

For bulk admin requests, prefer AddManualBulk: it does one save for N hosts instead of N saves, avoiding the O(hosts × blocklist-size) rewrite when the main file is large (e.g. a feed-backed blocklist with hundreds of thousands of entries). Codex P2 review on PR #283.

func (*Store) AddManualBulk

func (b *Store) AddManualBulk(hosts []string) int

AddManualBulk adds multiple hosts as manually-managed admin entries under a single write lock with a single saveManual + single Save call, instead of one save per host. Use this for bulk admin requests; the per-host normalization and dedupe match AddManual exactly (lowercase + trim, empty hosts skipped, "*." → wildcard, otherwise exact). The caller is responsible for any validation (length cap, wildcard format) before invoking this method — invalid entries reaching here are silently accepted, mirroring AddManual.

Returns the number of unique normalized entries actually stored by THIS call — i.e. hosts whose admin attribution (b.manual) went from false to true. Within-batch duplicates count once; cross-call repeats of an already-attributed host count as 0. This matches the caller's expectation that "added: N" in the API response and audit line reflects net new admin entries, not raw non-empty input count. A zero return means no on-disk write happens, so calling with an empty or all-blank slice (or an all-duplicates slice) is a cheap no-op.

Added per the Codex P2 review on PR #283: with per-call Save() inside AddManual, a bulk POST of N hosts to a feed-backed blocklist rewrote the entire main file N times (O(hosts × blocklist-size) disk work). This save-once path preserves the durability guarantee while keeping bulk cost O(blocklist-size).

func (*Store) ClearAll

func (b *Store) ClearAll()

ClearAll removes all blocklist entries (exact, wildcard, manual) but preserves exceptions and mode. Used by config import "replace" mode.

func (*Store) Count

func (b *Store) Count() int

Count returns the number of entries (exact + wildcard).

func (*Store) CountByFeedSource

func (b *Store) CountByFeedSource(feedURL string) int

CountByFeedSource reports how many currently-listed, non-manual entries are attributed to feedURL (the number a cascade delete would remove).

func (*Store) IsBlocked

func (b *Store) IsBlocked(host string) bool

IsBlocked reports whether a request to host should be blocked. In "block" mode (default): listed hosts are blocked. In "allow" mode: only listed hosts are allowed; all others blocked. Exceptions always pass through regardless of mode or list membership.

func (*Store) List

func (b *Store) List() []string

List returns all entries; wildcards are rendered as "*.example.com".

func (*Store) ListExceptions

func (b *Store) ListExceptions() []string

ListExceptions returns a sorted list of all exception hosts.

func (*Store) ListWithSource

func (b *Store) ListWithSource() []Entry

ListWithSource returns all blocklist entries annotated with their origin: "manual" if added by an admin via the UI/API, "feed" if imported from a feed.

func (*Store) Load

func (b *Store) Load(path string) error

Load reads the main blocklist file and its sidecars (.mode, .manual, .exceptions, .sources) from path, which becomes the persistence path.

func (*Store) MergeFromLines

func (b *Store) MergeFromLines(lines []string, source string) int

MergeFromLines adds all valid host entries from lines to the blocklist and saves it. Existing entries are NOT removed — safe to call on a live blocklist. Lines starting with '#' or empty are skipped; /etc/hosts-format lines are normalized to their hostname (see NormalizeLine). source is the feed URL recorded as per-entry attribution ("" = none). Returns the number of newly-added entries.

func (*Store) Mode

func (b *Store) Mode() string

Mode returns the current list mode: "block" (default) or "allow".

func (*Store) Remove

func (b *Store) Remove(host string)

Remove deletes host from the enforcement maps, the manual set, and the feed attribution, persisting the .manual sidecar.

func (*Store) RemoveByFeedSource

func (b *Store) RemoveByFeedSource(feedURL string) int

RemoveByFeedSource removes every entry attributed to feedURL (cascade delete when the admin removes a feed AND opts to purge its imports). Admin-added (manual) entries always survive. Returns the removed count.

func (*Store) RemoveException

func (b *Store) RemoveException(host string)

RemoveException removes an exception, allowing the host to be blocked again.

func (*Store) RemoveUnattributedFeedEntries

func (b *Store) RemoveUnattributedFeedEntries() int

RemoveUnattributedFeedEntries removes every feed-owned entry with no recorded source — the legacy cohort imported before per-feed attribution existed and no longer present in any current feed (a host still carried by a configured feed is re-stamped on every sync, so it never stays unattributed for long). Admin-added entries always survive. Returns the removed count.

func (*Store) ReplaceFeedEntries

func (b *Store) ReplaceFeedEntries(hosts []string)

ReplaceFeedEntries replaces the feed-pushed entries (exact + wildcards) in place, leaving DP-local state (path, mode, manual, exceptions) intact. Used by applyConfigSnapshot to avoid the wholesale-replacement pattern that previously zeroed those local fields and orphaned the persistence path. Per-host parsing mirrors Add: "*.example.com" → wildcard, otherwise → exact.

IMPORTANT: AddManual (below) writes admin-added hosts to BOTH the metadata map (b.manual) AND the enforcement maps (b.exact / b.wildcards). The enforcement maps are what IsBlocked consults; b.manual is just the attribution set. We therefore re-inject every b.manual host into the new enforcement maps before the swap so admin-added blocks survive every cluster sync. Without this re-injection (pre-fix and my first-pass ReplaceFeedEntries had the same defect; flagged by Codex on PR #249), admin manual blocks would silently disappear from enforcement on every snapshot apply.

func (*Store) RestoreFeedSources

func (b *Store) RestoreFeedSources(snap map[string]string)

RestoreFeedSources re-stamps feed attribution onto currently-listed, non-manual entries after a wholesale rebuild. Config rollback and import-replace go through Remove/ClearAll + Add, which would otherwise strand every feed entry as "unknown origin" — making them prey for the unattributed-cleanup operation (Codex P1, PR #447). Attribution already present (e.g. re-stamped by a sync mid-rebuild) is not overwritten.

func (*Store) Save

func (b *Store) Save()

Save persists the main blocklist file and the pruned .sources attribution sidecar (fsynced atomic writes). No-op when no path is set.

func (*Store) SetMode

func (b *Store) SetMode(mode string)

SetMode sets the list mode ("allow"; anything else means "block") and persists it to the .mode sidecar.

func (*Store) SnapshotFeedSources

func (b *Store) SnapshotFeedSources() map[string]string

SnapshotFeedSources returns a copy of the per-entry feed attribution map. Taken before a wholesale rebuild (config rollback / import-replace) so RestoreFeedSources can re-stamp surviving entries afterwards.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL