Skip to content

fix(dnssec): only validate public-upstream answers (#2126) - #2128

Merged
0xERR0R merged 2 commits into
mainfrom
fix/dnssec-conditional-bypass
Jun 20, 2026
Merged

0xERR0R merged 2 commits into
mainfrom
fix/dnssec-conditional-bypass

Conversation

@0xERR0R

@0xERR0R 0xERR0R commented Jun 19, 2026 •

Copy link
Copy Markdown
Owner

What

Exempt conditional-upstream answers (ResponseTypeCONDITIONAL) from DNSSEC validation, clearing the AD flag instead of validating them.

Fixes #2126.

Why

Since v0.32.0 (the GHSA-x845-2f78-7v36 hardening, #2119), a response carrying no RRSIGs is no longer automatically treated as Insecure — classifyUnsignedResponse now checks the zone's security status, and because the whole namespace sits under the default IANA root trust anchor, the fail-closed logic classifies any unsigned answer without an authenticated insecure-delegation proof as Bogus → SERVFAIL.

Conditional-upstream answers serve private/split-horizon zones from an operator-trusted resolver. They are inherently unsigned and have no chain of trust in the public DNS hierarchy, so with dnssec.validate: true every private lookup routed through a conditional mapping now returns SERVFAIL:

No RRSIG for myhost. but zone is secure - treating unsigned answer as bogus
DNSSEC validation failed for myhost. - returning SERVFAIL

This worked before 0.32.0, where an unsigned answer resolved to Insecure (AD cleared) and passed through.

How

In DNSSECResolver.Resolve, skip validation for ResponseTypeCONDITIONAL and clear the AD flag — mirroring RebindingProtectionResolver, which already exempts trusted-local answers by response type. The DNSSEC resolver sits above the conditional resolver in the chain, so it observes these answers on the way back up.

Security

The GHSA-x845 findings concern answers from the public DNS hierarchy — RESOLVED and CACHED — which remain fully validated. ResponseTypeCONDITIONAL is assigned only by blocky's own conditional resolver for operator-configured mappings and can never be attacker-assigned, so the carve-out does not reopen the unsigned-answer bypass. The change is deliberately scoped to this single response type to stay as close as possible to the hardened 0.32 behaviour; SPECIAL (SUDN) and SYNTHESIZED (DNS64) remain validated.

Tests

  • Regression test: an unsigned CONDITIONAL answer passes through (no SERVFAIL, AD cleared) under the default root anchor.
  • Companion test: an equivalent unsigned RESOLVED answer still SERVFAILs — proving validation remains active and the carve-out is strictly response-type scoped.
  • go test ./resolver/... green; gofmt/go vet clean.

Update — generalized to a response-type whitelist (89185b0)

Follow-up review found the CONDITIONAL-only carve-out above left two siblings of the same SERVFAIL class open, since both also reach the DNSSEC resolver from below in the chain and are inherently unsigned under the default root anchor:

  • SPECIAL (SUDN) — e.g. localhost A → 127.0.0.1 SERVFAILs under the default config (SUDN is on by default + dnssec.validate: true) — the same precondition as DNSSEC blocks trusted conditional resolvers #2126 itself.
  • SYNTHESIZED (DNS64) — synthesized AAAA for signed zones SERVFAILs when DNS64 is enabled.

The carve-out is now the rebinding resolver's exact response-type whitelist: validate only RESOLVED and CACHED (the public chain of trust / GHSA-x845 attack surface) and skip + clear AD for every other trusted-local or synthesized type. This supersedes the "deliberately scoped to this single response type" note above — SPECIAL and SYNTHESIZED are now exempted too. The security argument is unchanged: RType is assigned by blocky's own resolvers and is never attacker-controlled, and CACHED stays validated so re-validation on every cache hit (a GHSA finding) is preserved.

Tests are now table-driven: the skip table {CONDITIONAL, SPECIAL, SYNTHESIZED} asserts pass-through + AD cleared + no DS/DNSKEY sub-queries issued; the validate table {RESOLVED, CACHED} asserts unsigned answers still SERVFAIL.

Known residual: a DNS64-synthesized answer is cacheable and re-served as CACHED, so it is re-validated and SERVFAILs on cache hits — inherent to DNS64 + server-side DNSSEC validation (RFC 6147 §5.5), left for a separate change.

Conditional-upstream answers serve private/split-horizon zones from an
operator-trusted resolver. They are inherently unsigned and have no chain
of trust in the public DNS hierarchy, so the post-GHSA-x845 unsigned-answer
handling classified them bogus (the whole namespace sits under the default
root trust anchor) and turned every private lookup into SERVFAIL.

Skip DNSSEC validation for ResponseTypeCONDITIONAL and clear the AD flag,
mirroring the rebinding resolver's response-type carve-out for trusted-local
answers. Public-upstream answers (RESOLVED/CACHED) - the GHSA-x845 attack
surface - remain fully validated, so the advisory findings are preserved.
Copilot AI review requested due to automatic review settings June 19, 2026 20:26
@0xERR0R 0xERR0R added the 🐞 bug Something isn't working label Jun 19, 2026
@0xERR0R 0xERR0R added this to the v0.33.0 milestone Jun 19, 2026
@codecov

codecov Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.65%. Comparing base (d3a1fe5) to head (89185b0).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2128      +/-   ##
==========================================
- Coverage   87.67%   87.65%   -0.02%     
==========================================
  Files         122      122              
  Lines        9449     9455       +6     
==========================================
+ Hits         8284     8288       +4     
- Misses        923      925       +2     
  Partials      242      242              

☔ 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 adjusts Blocky’s DNSSEC resolver behavior to treat responses produced by the conditional-upstream resolver (ResponseTypeCONDITIONAL) as operator-trusted/local and therefore exempt them from DNSSEC validation, clearing the AD bit instead of potentially failing closed with SERVFAIL under the default root trust anchor. This restores pre-0.32.0 behavior for split-horizon/private zones while keeping DNSSEC validation strict for public upstream-derived answers.

Changes:

  • Skip DNSSEC validation for ResponseTypeCONDITIONAL responses and explicitly clear the AD flag.
  • Add regression coverage ensuring conditional unsigned answers pass through (AD cleared) while equivalent RESOLVED unsigned answers still SERVFAIL.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
resolver/dnssec_resolver.go Adds a response-type scoped carve-out to bypass DNSSEC validation for conditional-upstream answers and clears AD.
resolver/dnssec_resolver_test.go Adds tests covering the conditional carve-out and confirming validation remains enforced for RESOLVED unsigned answers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread resolver/dnssec_resolver_test.go Outdated
…elist

The #2126 fix (b1db60e) skipped DNSSEC validation only for
ResponseTypeCONDITIONAL. That blacklist left two siblings of the same
post-GHSA-x845 SERVFAIL class open, since both reach the DNSSEC resolver
from below in the chain and are inherently unsigned under the default root
trust anchor:

  - SPECIAL (SUDN): e.g. `localhost A -> 127.0.0.1` now SERVFAILs under the
    default config (SUDN default-on + validate-on) - the same precondition
    as the bug #2126 fixed.
  - SYNTHESIZED (DNS64): synthesized AAAA for signed zones SERVFAILs when
    DNS64 is enabled.

Replace the CONDITIONAL-only check with the rebinding resolver's exact
response-type whitelist: validate only RESOLVED and CACHED - the public
chain of trust and the GHSA-x845 attack surface - and skip + clear AD for
every other (trusted-local / synthesized) type. RType is assigned by
blocky's own resolvers, never by the upstream answer, so a poisoned public
answer cannot mislabel itself out of validation; CACHED stays validated so
re-validation on every cache hit (a GHSA finding) is preserved.

Tests are table-driven: the skip table {CONDITIONAL, SPECIAL, SYNTHESIZED}
asserts pass-through + AD cleared + no DS/DNSKEY sub-queries issued; the
validate table {RESOLVED, CACHED} asserts unsigned answers still SERVFAIL.

Known residual: a DNS64-synthesized answer is cacheable and re-served as
CACHED, so it is re-validated and SERVFAILs on cache hits - inherent to
DNS64 + server-side DNSSEC validation (RFC 6147 sec 5.5).
@0xERR0R 0xERR0R changed the title fix(dnssec): don't validate conditional-upstream answers (#2126) fix(dnssec): only validate public-upstream answers (#2126) Jun 20, 2026
@0xERR0R
0xERR0R merged commit fc353a0 into main Jun 20, 2026
23 checks passed
@0xERR0R
0xERR0R deleted the fix/dnssec-conditional-bypass branch June 20, 2026 08:13
dadezzz pushed a commit to dadezzz/infra_docker-compose that referenced this pull request Jun 21, 2026
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/0xerr0r/blocky](https://github.com/0xERR0R/blocky) | patch | `v0.32.0` → `v0.32.1` |

---

### Release Notes

<details>
<summary>0xERR0R/blocky (ghcr.io/0xerr0r/blocky)</summary>

### [`v0.32.1`](https://github.com/0xERR0R/blocky/releases/tag/v0.32.1)

[Compare Source](0xERR0R/blocky@v0.32.0...v0.32.1)

#### Changelog

##### Bug fixes

- [`d3a1fe5`](0xERR0R/blocky@d3a1fe5): fix(dnssec): don't cache transient Indeterminate validation results ([#&#8203;2120](0xERR0R/blocky#2120)) ([#&#8203;2127](0xERR0R/blocky#2127)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`fc353a0`](0xERR0R/blocky@fc353a0): fix(dnssec): only validate public-upstream answers ([#&#8203;2126](0xERR0R/blocky#2126)) ([#&#8203;2128](0xERR0R/blocky#2128)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`53cfbda`](0xERR0R/blocky@53cfbda): fix(querylog): add loong64 to unsupported list ([#&#8203;2123](0xERR0R/blocky#2123)) ([@&#8203;rtmongold](https://github.com/rtmongold))

##### Build and dependencies

- [`cad2480`](0xERR0R/blocky@cad2480): build(deps): bump actions/checkout from 6 to 7 ([#&#8203;2124](0xERR0R/blocky#2124)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`6ff7db7`](0xERR0R/blocky@6ff7db7): build(deps): bump github.com/moby/moby/api from 1.54.2 to 1.55.0 ([#&#8203;2125](0xERR0R/blocky#2125)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])

***

#### ❤️ 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

🐞 bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DNSSEC blocks trusted conditional resolvers

2 participants