Skip to content

feat(blocking): log the matched rule in the block reason - #2091

Merged
0xERR0R merged 2 commits into
mainfrom
feat/log-blocking-rule
Jun 8, 2026
Merged

feat(blocking): log the matched rule in the block reason#2091
0xERR0R merged 2 commits into
mainfrom
feat/log-blocking-rule

Conversation

@0xERR0R

@0xERR0R 0xERR0R commented Jun 8, 2026

Copy link
Copy Markdown
Owner

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:

BLOCKED (gr1: domain1.com)
BLOCKED CNAME (ads: *.docler.com)
BLOCKED IP (defaultGroup: 123.145.123.145)
BLOCKED (gr1: a.com, gr2: b.com)        # multiple groups, sorted by 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.enable is 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:

Layer Before After
trie.HasParentOf bool ([]labels, bool)
stringcache leaf containsfindMatch bool (rule, ok)
GroupedStringCache.Contains (+ chained) []string map[group]rule
lists.Matcher.Match []string map[group]rule
BlockingResolver.matches []string map[group]rule

The reason is formatted once via a new formatBlockReason helper, 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:

  • API-shape deadlock → findMatch (rule, ok) (idiomatic comma-ok, name no longer implies a pure boolean)
  • non-deterministic multi-group reason → formatter sorts groups
  • no rule-value tests → positive rule-value assertions added at every layer (trie, leaf caches, grouped/chained, lists, resolver, EDE)
  • duplicated BLOCKED (...) formatting → single helper
  • hot-path allocation → zero-alloc miss path
  • trie hard-coding the . separator → trie returns labels, caller joins
  • + bug fix: *.docler.com is now reported correctly (Log blocking rule in resolver #1489 dropped the TLD label and the *. prefix, reporting e.g. docler instead)

Tests

go test ./... (all packages) ✓ · gofmt ✓ · go vet ✓ · golangci-lint 0 issues ✓

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).
Copilot AI review requested due to automatic review settings June 8, 2026 06:33
@0xERR0R 0xERR0R added the 🔨 enhancement New feature or request label Jun 8, 2026
@0xERR0R 0xERR0R added this to the v0.32.0 milestone Jun 8, 2026
@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.10%. Comparing base (e7958e0) to head (4d58335).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
resolver/blocking_resolver.go 89.47% 1 Missing and 1 partial ⚠️
resolver/ede_resolver.go 75.00% 1 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 formatBlockReason helper (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 responseReason and 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.

Comment thread cache/stringcache/string_caches.go
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.
@0xERR0R
0xERR0R merged commit c851293 into main Jun 8, 2026
22 checks passed
@0xERR0R
0xERR0R deleted the feat/log-blocking-rule branch June 8, 2026 07:19
dadezzz pushed a commit to dadezzz/infra_docker-compose that referenced this pull request Jun 20, 2026
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 ([#&#8203;2091](0xERR0R/blocky#2091)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`87be127`](0xERR0R/blocky@87be127): feat(cache): Shard the result cache to remove the single-lock read ceiling ([#&#8203;2097](0xERR0R/blocky#2097)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`7c6da15`](0xERR0R/blocky@7c6da15): feat(config): structurally merge config folder files ([#&#8203;1827](0xERR0R/blocky#1827)) ([#&#8203;2112](0xERR0R/blocky#2112)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`e7958e0`](0xERR0R/blocky@e7958e0): feat(lists): opt-in on-disk download cache with conditional revalidation ([#&#8203;2087](0xERR0R/blocky#2087)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`b82199b`](0xERR0R/blocky@b82199b): feat(querylog): ignore domains (exact/wildcard/regex) in query log ([#&#8203;2084](0xERR0R/blocky#2084)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`bee2d8b`](0xERR0R/blocky@bee2d8b): feat(resolver): UDP-first plain-DNS upstream and EDNS0 buffer floor ([#&#8203;2100](0xERR0R/blocky#2100)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`0b70e5c`](0xERR0R/blocky@0b70e5c): feat(resolver): add DNS rebinding protection ([#&#8203;2111](0xERR0R/blocky#2111)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`1b8e08a`](0xERR0R/blocky@1b8e08a): feat(resolver): pool DoT connections and enable TLS session resumption ([#&#8203;2098](0xERR0R/blocky#2098)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`4018d9e`](0xERR0R/blocky@4018d9e): feat: in-memory statistics subsystem with /api/stats REST endpoint ([#&#8203;2093](0xERR0R/blocky#2093)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`7abca44`](0xERR0R/blocky@7abca44): feat: support PROXY protocol on proxied DoT/DoH listeners ([#&#8203;2094](0xERR0R/blocky#2094)) ([@&#8203;kastakhov](https://github.com/kastakhov))

##### Bug fixes

- [`2496d12`](0xERR0R/blocky@2496d12): fix(dnssec): close DNSSEC validation bypass & cache-scope pollution (GHSA-x845-2f78-7v36) ([#&#8203;2119](0xERR0R/blocky#2119)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`06555e0`](0xERR0R/blocky@06555e0): fix(metrics): bound reason label cardinality for blocked responses ([#&#8203;2114](0xERR0R/blocky#2114)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`e0ea9b3`](0xERR0R/blocky@e0ea9b3): fix(resolver): eliminate recursive RLock deadlock in blocking group resolution ([#&#8203;2106](0xERR0R/blocky#2106)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`80f742e`](0xERR0R/blocky@80f742e): fix(server): answer browser CORS preflights for custom headers and Private Network Access ([#&#8203;2109](0xERR0R/blocky#2109)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`9702c8f`](0xERR0R/blocky@9702c8f): fix(stats): populate allow/denylist counts in /api/stats at startup ([#&#8203;2113](0xERR0R/blocky#2113)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))

##### Build and dependencies

- [`821e378`](0xERR0R/blocky@821e378): build(deps): bump codecov/codecov-action from 6 to 7 ([#&#8203;2089](0xERR0R/blocky#2089)) ([@&#8203;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 ([#&#8203;2095](0xERR0R/blocky#2095)) ([@&#8203;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 ([#&#8203;2108](0xERR0R/blocky#2108)) ([@&#8203;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 ([#&#8203;2116](0xERR0R/blocky#2116)) ([@&#8203;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 ([#&#8203;2115](0xERR0R/blocky#2115)) ([@&#8203;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 ([#&#8203;2090](0xERR0R/blocky#2090)) ([@&#8203;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 ([#&#8203;2101](0xERR0R/blocky#2101)) ([@&#8203;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 ([#&#8203;2096](0xERR0R/blocky#2096)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])

##### Misc

- [`1c9f717`](0xERR0R/blocky@1c9f717): ci(e2e): speed up e2e job via Ginkgo proc oversubscription ([#&#8203;2085](0xERR0R/blocky#2085)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`8d49922`](0xERR0R/blocky@8d49922): ci: cross-compile multi-arch docker image instead of QEMU emulation ([#&#8203;2081](0xERR0R/blocky#2081)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`355a9a1`](0xERR0R/blocky@355a9a1): docs(grafana): redesign Grafana dashboard and update integration guide ([#&#8203;2110](0xERR0R/blocky#2110)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`c791e80`](0xERR0R/blocky@c791e80): perf(e2e): probe blocky healthcheck at 250ms during start period ([#&#8203;2086](0xERR0R/blocky#2086)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`a0fb375`](0xERR0R/blocky@a0fb375): perf(lists): speed up blocklist loading (cache build + parsing) ([#&#8203;2083](0xERR0R/blocky#2083)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`316b073`](0xERR0R/blocky@316b073): perf(resolver): pre-classify client groups to cut per-query allocations ([#&#8203;2103](0xERR0R/blocky#2103)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`d502806`](0xERR0R/blocky@d502806): perf(server): let Truncate decide response compression ([#&#8203;2102](0xERR0R/blocky#2102)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`8b32512`](0xERR0R/blocky@8b32512): test(e2e): de-flake PROXY protocol and upstream init.strategy specs ([#&#8203;2099](0xERR0R/blocky#2099)) ([@&#8203;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-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show the rule which is the cause of blocked request

2 participants