Repository navigation
fix(dnssec): only validate public-upstream answers (#2126) - #2128
Conversation
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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
ResponseTypeCONDITIONALresponses and explicitly clear the AD flag. - Add regression coverage ensuring conditional unsigned answers pass through (AD cleared) while equivalent
RESOLVEDunsigned 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.
…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).
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 ([#​2120](0xERR0R/blocky#2120)) ([#​2127](0xERR0R/blocky#2127)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`fc353a0`](0xERR0R/blocky@fc353a0): fix(dnssec): only validate public-upstream answers ([#​2126](0xERR0R/blocky#2126)) ([#​2128](0xERR0R/blocky#2128)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`53cfbda`](0xERR0R/blocky@53cfbda): fix(querylog): add loong64 to unsupported list ([#​2123](0xERR0R/blocky#2123)) ([@​rtmongold](https://github.com/rtmongold)) ##### Build and dependencies - [`cad2480`](0xERR0R/blocky@cad2480): build(deps): bump actions/checkout from 6 to 7 ([#​2124](0xERR0R/blocky#2124)) ([@​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 ([#​2125](0xERR0R/blocky#2125)) ([@​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-->
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 —
classifyUnsignedResponsenow 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: trueevery private lookup routed through aconditionalmapping now returns 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 forResponseTypeCONDITIONALand clear the AD flag — mirroringRebindingProtectionResolver, 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 —
RESOLVEDandCACHED— which remain fully validated.ResponseTypeCONDITIONALis 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) andSYNTHESIZED(DNS64) remain validated.Tests
CONDITIONALanswer passes through (no SERVFAIL, AD cleared) under the default root anchor.RESOLVEDanswer still SERVFAILs — proving validation remains active and the carve-out is strictly response-type scoped.go test ./resolver/...green;gofmt/go vetclean.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.1SERVFAILs 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
RESOLVEDandCACHED(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 —SPECIALandSYNTHESIZEDare now exempted too. The security argument is unchanged:RTypeis assigned by blocky's own resolvers and is never attacker-controlled, andCACHEDstays 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.