feat(blocking): log the matched rule in the block reason - #2091
Conversation
When a query is blocked, the response reason now names the specific rule that matched per group, e.g. "BLOCKED CNAME (ads: *.docler.com)", instead of only the group "BLOCKED CNAME (ads)". The rule rides in the existing response reason, so it surfaces in the query log, the debug log, and the EDE EXTRA-TEXT returned to clients when ede.enable is set — no query-log schema change. The matched rule is threaded up the match stack: - trie.HasParentOf -> ([]labels, bool); built only on the matching path so a miss does not allocate, and returned in entry order - stringcache findMatch (was contains) -> (rule, ok); wildcard re-prepends "*." so the reported rule matches the configured entry - grouped/chained cache Contains and lists.Matcher.Match -> map[group]rule, allocated lazily to keep the no-match path allocation-free - resolver formats the reason once via formatBlockReason, sorted by group for deterministic output Fresh implementation superseding the stalled #1489: resolves the API-shape deadlock, fixes the non-deterministic multi-group ordering, adds the missing rule-value tests at every layer, deduplicates the reason formatting, and corrects wildcard rule reconstruction (dropped TLD label and missing "*." prefix).
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2091 +/- ##
==========================================
- Coverage 86.12% 86.10% -0.02%
==========================================
Files 113 113
Lines 8532 8551 +19
==========================================
+ Hits 7348 7363 +15
- Misses 952 954 +2
- Partials 232 234 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR enhances Blocky’s blocking diagnostics by including the specific matched rule (per list group) directly in the existing responseReason string for blocked responses, making it easier to identify the exact denylist entry responsible without changing the query-log schema.
Changes:
- Thread matched-rule information through the matching stack (trie → caches → grouped/chained caches → list matcher → blocking resolver), changing return types to carry
group -> rule. - Add a deterministic
formatBlockReasonhelper (sorted by group) and update resolver logic + tests to assert the new reason formatting and EDE extra text behavior. - Update documentation to describe the richer
responseReasonand EDE extra text contents for blocked queries.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
trie/trie.go |
Changes HasParentOf to return matched labels for reconstructing the stored entry on match. |
trie/trie_test.go |
Updates trie tests for new return signature and adds reconstruction assertions. |
cache/stringcache/string_caches.go |
Reworks leaf caches to return (rule, ok) on match; wildcard cache reconstructs and reports *. rule. |
cache/stringcache/string_caches_test.go |
Updates cache tests to assert returned rule values (string/regex/wildcard). |
cache/stringcache/string_caches_benchmark_test.go |
Updates benchmarks to use findMatch instead of boolean-only matching. |
cache/stringcache/in_memory_grouped_cache.go |
Changes grouped-cache matching to return map[group]rule with lazy allocation on matches. |
cache/stringcache/in_memory_grouped_cache_test.go |
Updates grouped-cache tests to assert group -> rule results for string/regex/wildcard caches. |
cache/stringcache/grouped_cache_interface.go |
Updates GroupedStringCache.Contains signature to return map[group]rule. |
cache/stringcache/chained_grouped_cache.go |
Aggregates matches across chained caches into a map[group]rule (lazy allocation). |
cache/stringcache/chained_grouped_cache_test.go |
Updates chained-cache tests for the new map[group]rule return shape. |
lists/list_cache.go |
Updates Matcher.Match to return map[group]rule and forwards results from grouped cache. |
lists/list_cache_test.go |
Updates list-cache tests to validate group presence using map keys. |
resolver/blocking_resolver.go |
Switches matching to map[group]rule and formats block reasons via formatBlockReason (sorted groups). |
resolver/blocking_resolver_test.go |
Updates resolver expectations for new reason strings and adds formatter determinism tests. |
resolver/ede_resolver_test.go |
Asserts blocked EDE extra text includes the matched rule in the reason string. |
docs/configuration.md |
Documents that blocked responseReason and EDE extra text include matched group: rule details. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Follow-up fixes from code review of the matched-rule block reason: - Cap EDE extra text (maxEDETextLength) so an unbounded matched rule (e.g. a long regex) can't bloat the OPT record and push the answer out of a size-limited UDP response during dns.Msg.Truncate; the full reason is still kept in the query log. - Report regex rules wrapped in their '/.../' delimiters so the reported rule round-trips to the configured entry instead of looking like a plain-string rule (raised by Copilot review). - Reuse the slices.Sorted(maps.Keys(...)) idiom in formatBlockReason and return a clean "BLOCKED[ TYPE]" when there are no matches. - Add trie.JoinTLD as the inverse of SplitTLD so the wildcard cache no longer hard-codes the trie's label separator. - Collapse parent.hasParentOf into polymorphic dispatch: removes the duplicated return and the unreachable (uncoverable) fallthrough; trie package back to 100% coverage. - Document the chained-cache one-rule-per-group (last-write-wins) choice.
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/0xerr0r/blocky](https://github.com/0xERR0R/blocky) | minor | `v0.31.0` → `v0.32.0` | --- ### Release Notes <details> <summary>0xERR0R/blocky (ghcr.io/0xerr0r/blocky)</summary> ### [`v0.32.0`](https://github.com/0xERR0R/blocky/releases/tag/v0.32.0) [Compare Source](0xERR0R/blocky@v0.31.0...v0.32.0) #### Changelog ##### Features - [`c851293`](0xERR0R/blocky@c851293): feat(blocking): log the matched rule in the block reason ([#​2091](0xERR0R/blocky#2091)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`87be127`](0xERR0R/blocky@87be127): feat(cache): Shard the result cache to remove the single-lock read ceiling ([#​2097](0xERR0R/blocky#2097)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`7c6da15`](0xERR0R/blocky@7c6da15): feat(config): structurally merge config folder files ([#​1827](0xERR0R/blocky#1827)) ([#​2112](0xERR0R/blocky#2112)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`e7958e0`](0xERR0R/blocky@e7958e0): feat(lists): opt-in on-disk download cache with conditional revalidation ([#​2087](0xERR0R/blocky#2087)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`b82199b`](0xERR0R/blocky@b82199b): feat(querylog): ignore domains (exact/wildcard/regex) in query log ([#​2084](0xERR0R/blocky#2084)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`bee2d8b`](0xERR0R/blocky@bee2d8b): feat(resolver): UDP-first plain-DNS upstream and EDNS0 buffer floor ([#​2100](0xERR0R/blocky#2100)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`0b70e5c`](0xERR0R/blocky@0b70e5c): feat(resolver): add DNS rebinding protection ([#​2111](0xERR0R/blocky#2111)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`1b8e08a`](0xERR0R/blocky@1b8e08a): feat(resolver): pool DoT connections and enable TLS session resumption ([#​2098](0xERR0R/blocky#2098)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`4018d9e`](0xERR0R/blocky@4018d9e): feat: in-memory statistics subsystem with /api/stats REST endpoint ([#​2093](0xERR0R/blocky#2093)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`7abca44`](0xERR0R/blocky@7abca44): feat: support PROXY protocol on proxied DoT/DoH listeners ([#​2094](0xERR0R/blocky#2094)) ([@​kastakhov](https://github.com/kastakhov)) ##### Bug fixes - [`2496d12`](0xERR0R/blocky@2496d12): fix(dnssec): close DNSSEC validation bypass & cache-scope pollution (GHSA-x845-2f78-7v36) ([#​2119](0xERR0R/blocky#2119)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`06555e0`](0xERR0R/blocky@06555e0): fix(metrics): bound reason label cardinality for blocked responses ([#​2114](0xERR0R/blocky#2114)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`e0ea9b3`](0xERR0R/blocky@e0ea9b3): fix(resolver): eliminate recursive RLock deadlock in blocking group resolution ([#​2106](0xERR0R/blocky#2106)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`80f742e`](0xERR0R/blocky@80f742e): fix(server): answer browser CORS preflights for custom headers and Private Network Access ([#​2109](0xERR0R/blocky#2109)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`9702c8f`](0xERR0R/blocky@9702c8f): fix(stats): populate allow/denylist counts in /api/stats at startup ([#​2113](0xERR0R/blocky#2113)) ([@​0xERR0R](https://github.com/0xERR0R)) ##### Build and dependencies - [`821e378`](0xERR0R/blocky@821e378): build(deps): bump codecov/codecov-action from 6 to 7 ([#​2089](0xERR0R/blocky#2089)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`d63f84b`](0xERR0R/blocky@d63f84b): build(deps): bump github.com/0xERR0R/expiration-cache from 0.1.0 to 0.2.0 ([#​2095](0xERR0R/blocky#2095)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`81f2b2a`](0xERR0R/blocky@81f2b2a): build(deps): bump github.com/onsi/ginkgo/v2 from 2.29.0 to 2.30.0 ([#​2108](0xERR0R/blocky#2108)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`9c5abdf`](0xERR0R/blocky@9c5abdf): build(deps): bump github.com/onsi/ginkgo/v2 from 2.30.0 to 2.31.0 ([#​2116](0xERR0R/blocky#2116)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`6c0ec5f`](0xERR0R/blocky@6c0ec5f): build(deps): bump github.com/onsi/gomega from 1.41.0 to 1.42.0 ([#​2115](0xERR0R/blocky#2115)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`4beb9fe`](0xERR0R/blocky@4beb9fe): build(deps): bump github.com/quic-go/quic-go from 0.59.1 to 0.60.0 ([#​2090](0xERR0R/blocky#2090)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`8fe2202`](0xERR0R/blocky@8fe2202): build(deps): bump golang.org/x/net from 0.55.0 to 0.56.0 ([#​2101](0xERR0R/blocky#2101)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`52fa6a4`](0xERR0R/blocky@52fa6a4): build(deps): bump golang.org/x/sys from 0.45.0 to 0.46.0 ([#​2096](0xERR0R/blocky#2096)) ([@​dependabot](https://github.com/dependabot)\[bot]) ##### Misc - [`1c9f717`](0xERR0R/blocky@1c9f717): ci(e2e): speed up e2e job via Ginkgo proc oversubscription ([#​2085](0xERR0R/blocky#2085)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`8d49922`](0xERR0R/blocky@8d49922): ci: cross-compile multi-arch docker image instead of QEMU emulation ([#​2081](0xERR0R/blocky#2081)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`355a9a1`](0xERR0R/blocky@355a9a1): docs(grafana): redesign Grafana dashboard and update integration guide ([#​2110](0xERR0R/blocky#2110)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`c791e80`](0xERR0R/blocky@c791e80): perf(e2e): probe blocky healthcheck at 250ms during start period ([#​2086](0xERR0R/blocky#2086)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`a0fb375`](0xERR0R/blocky@a0fb375): perf(lists): speed up blocklist loading (cache build + parsing) ([#​2083](0xERR0R/blocky#2083)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`316b073`](0xERR0R/blocky@316b073): perf(resolver): pre-classify client groups to cut per-query allocations ([#​2103](0xERR0R/blocky#2103)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`d502806`](0xERR0R/blocky@d502806): perf(server): let Truncate decide response compression ([#​2102](0xERR0R/blocky#2102)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`8b32512`](0xERR0R/blocky@8b32512): test(e2e): de-flake PROXY protocol and upstream init.strategy specs ([#​2099](0xERR0R/blocky#2099)) ([@​0xERR0R](https://github.com/0xERR0R)) *** #### ❤️ Support Blocky Blocky is free and open source, developed in my spare time — no telemetry, no ads, no hidden filtering. If it's useful to you, please consider supporting its continued development: [GitHub Sponsors](https://github.com/sponsors/0xERR0R) · [thanks.dev](https://thanks.dev/u/gh/0xERR0R) · [Liberapay](https://liberapay.com/spx01) · [Ko-fi](https://ko-fi.com/0xerr0r) · [PayPal](https://paypal.me/spx01) Thank you to everyone supporting Blocky! 🙏 </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMjIuMSIsInVwZGF0ZWRJblZlciI6IjQzLjIyMi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Closes #1458. Fresh implementation superseding the stalled #1489.
What
When a query is blocked, the response reason now names the specific rule that matched per group:
instead of only the group (
BLOCKED CNAME (ads)). With large aggregated lists this turns a manual list-bisection into a one-line log read.Because the rule rides in the existing response reason, it surfaces everywhere that field already flows — the file/DB/console query log, the debug log, and the EDE EXTRA-TEXT returned to clients when
ede.enableis set. No query-log schema change, and the feature is always on.How
The matched rule is threaded up the existing match stack; only return types change:
trie.HasParentOfbool([]labels, bool)stringcacheleafcontains→findMatchbool(rule, ok)GroupedStringCache.Contains(+ chained)[]stringmap[group]rulelists.Matcher.Match[]stringmap[group]ruleBlockingResolver.matches[]stringmap[group]ruleThe reason is formatted once via a new
formatBlockReasonhelper, sorted by group for deterministic output.Performance
The rule is produced only on a match. Both the grouped-cache maps and the trie's label slice are built only on the matching path, so the common no-match path stays allocation-free. Wildcard benchmark (all-hits worst case): +8% bytes vs
main; the miss path (production-common) does not allocate.Relative to #1489
Same idea, but this resolves the issues that stalled it:
findMatch (rule, ok)(idiomatic comma-ok, name no longer implies a pure boolean)BLOCKED (...)formatting → single helper.separator → trie returns labels, caller joins*.docler.comis now reported correctly (Log blocking rule in resolver #1489 dropped the TLD label and the*.prefix, reporting e.g.doclerinstead)Tests
go test ./...(all packages) ✓ ·gofmt✓ ·go vet✓ ·golangci-lint0 issues ✓