Skip to content

fix(resolver): scope allowlist-only mode to the whole client - #2249

Merged
0xERR0R merged 3 commits into
mainfrom
fix/allowlist-only-client-scope-2207
Sep 5, 2026
Merged

0xERR0R merged 3 commits into
mainfrom
fix/allowlist-only-client-scope-2207

Conversation

@0xERR0R

@0xERR0R 0xERR0R commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Fixes #2207

Problem

A group holding only allowlist entries (no denylists key) put the whole client into exclusive "allow nothing else" mode, even when that client was also assigned ordinary denylist groups. The flag was ORed across the client's groups and the branch returned before the denylist was ever consulted:

allowlistOnlyAllowed := r.hasAllowlistOnlyAllowed(groupsToCheck) // any group -> true
...
if allowlistOnlyAllowed {
    return true, r.handleBlocked(..., "BLOCKED (ALLOWLIST ONLY)", ""), nil
}
if matches := r.matches(groupsToCheck, r.denylistMatcher, domain); ... // never reached

So the config from the issue — a shared ads denylist plus a small client-specific allowlist group — blocked everything for that client:

macan   wikipedia.org        -> BLOCKED (ALLOWLIST ONLY)
macan   google.com           -> BLOCKED (ALLOWLIST ONLY)
macan   doubleclick.net      -> BLOCKED (ALLOWLIST ONLY)   <- attribution lost
default wikipedia.org        -> RESOLVED
default doubleclick.net      -> BLOCKED (ads: doubleclick.net)

Note the third line: denylist hits also lost their attribution, since the exclusive branch returned before the match was known. The query log and the reason metric label saw BLOCKED (ALLOWLIST ONLY) with an empty reason label rather than the group and rule that matched.

The reporter's workaround was a dummy .invalid domain in a denylist for that group. It works because determineAllowlistOnlyGroups tests key presence in cfg.Denylists, not whether the group denies anything — undiscoverable from the docs.

Fix

Exclusive mode now requires that every group assigned to the client be allowlist-only. hasAllowlistOnlyAllowed is gone; groupsToCheckForClient returns the flag alongside the active groups.

Exclusivity is derived from the client's configured groups, before the schedule and disable-API filtering. Deriving it from the active groups would introduce a new bug: deactivating the denylist group would leave only the allowlist group active and promote its exceptions into a whitelist, so disabling blocking would block more. There is a test pinning that.

This keeps the pure allow-only use case (a client whose groups are all allowlist-only) working exactly as before — every pre-existing allowlist-only spec passes unchanged.

Docs

The documented behaviour was wrong on two counts, both corrected in docs/configuration.md and docs/config.yml:

  • Scope — it read as a per-group rule; exclusive mode is a property of the client.
  • "only domains from this list" — the permitted set is the union of the allowlists across all of the client's groups. (Verified: a domain allowlisted only in the ads group resolves for a client whose exclusive mode came from a different group.)

The docs now also state that allowlist precedence holds across a client's groups, which is the supported way to add client-specific exceptions to a shared denylist without duplicating it. Added schedule rule 5 noting exclusivity follows configuration, not currently-active groups.

Tests

Written test-first; the failing cases were observed before the fix.

Unit (resolver/blocking_resolver_test.go) — 5 new specs, 3 of which failed against the old code:

  • resolves a domain on neither list
  • still blocks a denylisted domain, naming the group that blocked it
  • allowlist group excepts a domain the other group denies
  • clients without the allowlist-only group unaffected
  • denylist group disabled via API -> still not exclusive mode

The mixed case had no coverage at all before this: every existing allowlist-only spec used clients whose groups were all allowlist-only, which is why this shipped.

E2E (e2e/blocking_test.go) — 3 new specs over two list servers. Against a pre-fix image the first one reproduces the report end to end:

Expected ... unrelated.com. 21600 IN A 0.0.0.0
to contain domain 'unrelated.com.', type 'A', answer '1.2.3.4'

Also replaced the stale note claiming allow-only mode could not be tested here.

Verification

  • ./resolver suite: 713/713 pass
  • full e2e suite: 163/163 pass
  • golangci-lint (v2.12.2): 0 issues
  • gofmt clean

https://claude.ai/code/session_01QyEFFhrY3j9QtGzR2wUJr9

An allowlist-only group (allowlist entries, no denylist key) put a client into
exclusive "allow nothing else" mode even when the client was also assigned
ordinary denylist groups: the flag was ORed across the client's groups and the
branch returned before the denylist was ever consulted. A client that combined
a shared denylist group with a small client-specific allowlist group therefore
had every unlisted domain blocked, and its denylist hits lost their attribution
-- reported as "BLOCKED (ALLOWLIST ONLY)" with an empty reason label instead of
naming the group and rule that matched.

Exclusive mode now requires that *every* group assigned to the client be
allowlist-only. It is derived from the client's configured groups rather than
the currently active ones, so deactivating the denylist group -- via a schedule
or the disable API -- cannot promote the remaining allowlists into a whitelist.
Disabling blocking must never block more.

The documented behaviour was wrong on two counts and is corrected: exclusive
mode is a property of the client, not of a single group, and the domains it
permits are the union of the allowlists across all of the client's groups.

Fixes #2207

Claude-Session: https://claude.ai/code/session_01QyEFFhrY3j9QtGzR2wUJr9
Copilot AI lite review requested due to automatic review settings September 5, 2026 13:53
@0xERR0R 0xERR0R added the 🐞 bug Something isn't working label Sep 5, 2026
@0xERR0R 0xERR0R added this to the v0.35.0 milestone Sep 5, 2026

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.

🟡 Changes recommended

A remaining call site (resolver/blocking_resolver_concurrency_test.go) still calls groupsToCheckForClient as a single-return function and will not compile until updated.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes a blocking resolver edge case where assigning a client both (1) a denylist group and (2) an allowlist-only group incorrectly forced the entire client into “exclusive allow” mode, preventing normal denylist evaluation and losing denylist attribution.

Changes:

  • Scope “exclusive allowlist-only” mode to apply only when all configured groups for the client are allowlist-only.
  • Propagate the client-level allowlist-only flag from groupsToCheckForClient into handleDenylist to avoid OR-ing allow-only behavior across mixed group sets.
  • Add unit + e2e coverage and update docs to reflect correct client-scoped behavior and precedence rules.
File summaries
File Description
resolver/blocking_resolver.go Changes allowlist-only determination to be client-scoped (all groups) and threads it into denylist handling.
resolver/blocking_resolver_test.go Adds unit tests for mixed allowlist-only + denylist group behavior and attribution.
resolver/blocking_resolver_bench_test.go Updates benchmarks for the new groupsToCheckForClient return signature.
e2e/blocking_test.go Adds e2e coverage for the mixed-group case and updates outdated commentary.
docs/configuration.md Corrects documentation to describe client-scoped exclusivity and cross-group allowlist precedence.
docs/config.yml Updates sample config comments for client-scoped exclusive allow mode.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// groupsToCheckForClient returns the groups which should be checked for the client's
// request, and whether those groups form an exclusive allowlist (block everything not
// allowlisted).
func (r *BlockingResolver) groupsToCheckForClient(request *model.Request) ([]string, bool) {
@codecov

codecov Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.03%. Comparing base (769d908) to head (bf4bd81).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2249      +/-   ##
==========================================
+ Coverage   88.00%   88.03%   +0.02%     
==========================================
  Files         126      126              
  Lines        9940     9939       -1     
==========================================
+ Hits         8748     8750       +2     
+ Misses        926      924       -2     
+ Partials      266      265       -1     

☔ 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.

@0xERR0R
0xERR0R enabled auto-merge (squash) September 5, 2026 14:05
@0xERR0R
0xERR0R merged commit 344de86 into main Sep 5, 2026
23 checks passed
@0xERR0R
0xERR0R deleted the fix/allowlist-only-client-scope-2207 branch September 5, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client-specific allowlist additions require a dummy denylist entry to avoid "exclusive allow-only" mode

2 participants