Repository navigation
feat: filter ipv6hint in HTTPS/SVCB RR when AAAA is filtered - #2070
Conversation
When `filtering.queryTypes` contains AAAA, also remove the ipv6hint SvcParam from HTTPS/SVCB (RFC 9460) answers, so clients can't reach IPv6 endpoints advertised in those records and bypass the AAAA filter. Other SvcParams (alpn, ipv4hint, ...) are left untouched. Closes #1706
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2070 +/- ##
==========================================
- Coverage 86.34% 86.34% -0.01%
==========================================
Files 108 108
Lines 8061 8104 +43
==========================================
+ Hits 6960 6997 +37
- Misses 897 901 +4
- Partials 204 206 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR closes #1706 by preventing IPv6 connectivity bypass when filtering.queryTypes includes AAAA: it strips the ipv6hint SvcParam from HTTPS/SVCB answers so modern clients can’t learn IPv6 endpoints via RFC 9460 hints.
Changes:
- Update
FilteringResolverto removeipv6hintfromHTTPSandSVCBanswer RRs whenAAAAis configured as a filtered query type. - Add Ginkgo specs covering
HTTPSandSVCBstripping behavior, plus non-SVCB no-op and “AAAA not filtered” behavior. - Document the behavior in the Filtering section of the configuration docs.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| resolver/filtering_resolver.go | Adds response post-processing to strip ipv6hint from HTTPS/SVCB answers when AAAA filtering is enabled. |
| resolver/filtering_resolver_test.go | Adds unit tests validating ipv6hint stripping and non-regression cases. |
| docs/configuration.md | Documents automatic stripping of ipv6hint when AAAA query filtering is configured. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Stripping ipv6hint mutates the HTTPS/SVCB RRset after the DNSSEC resolver in the chain may have set AD=1 and passed RRSIGs through. Leaving AD=1 / invalid signatures would serve DNSSEC-inconsistent data. When a hint is actually removed, clear AuthenticatedData and drop the RRSIGs covering HTTPS/SVCB, mirroring the DNS64 resolver. Responses with no ipv6hint are left untouched so their DNSSEC status is preserved.
| // removeIPv6Hints strips the ipv6hint SvcParam from any HTTPS/SVCB record in the response. | ||
| // The records are owned by the current request (freshly produced upstream or unpacked from | ||
| // cache), so they are modified in place. | ||
| // | ||
| // Modifying a signed RRset invalidates its DNSSEC signatures, so when a hint is actually | ||
| // removed the AD bit is cleared and the now-invalid RRSIGs covering the modified RRsets are | ||
| // dropped, to avoid serving DNSSEC-inconsistent data (mirrors the DNS64 resolver behavior). | ||
| func removeIPv6Hints(msg *dns.Msg) { | ||
| modified := false | ||
|
|
||
| for _, rr := range msg.Answer { |
There was a problem hiding this comment.
| if !modified { | ||
| return | ||
| } | ||
|
|
||
| msg.AuthenticatedData = false | ||
| msg.Answer = removeSVCBSignatures(msg.Answer) | ||
| } | ||
|
|
||
| // removeSVCBSignatures returns the answers with any RRSIG covering HTTPS or SVCB records removed. | ||
| func removeSVCBSignatures(answers []dns.RR) []dns.RR { | ||
| filtered := make([]dns.RR, 0, len(answers)) | ||
|
|
||
| for _, rr := range answers { | ||
| if sig, ok := rr.(*dns.RRSIG); ok && | ||
| (sig.TypeCovered == dns.TypeHTTPS || sig.TypeCovered == dns.TypeSVCB) { | ||
| continue | ||
| } |
There was a problem hiding this comment.
Fixed in 6c0f991. removeIPv6Hints now records the Rrtype of each record it actually modifies and removeSignaturesCovering only drops RRSIGs whose TypeCovered is in that set, so stripping a hint from an HTTPS RR no longer removes valid SVCB signatures (and vice versa). Added a spec asserting that with HTTPS modified, only the HTTPS-covering RRSIG is dropped while SVCB- and CNAME-covering signatures are kept.
| filtered := make([]dns.SVCBKeyValue, 0, len(*values)) | ||
|
|
||
| for _, kv := range *values { | ||
| if kv.Key() == dns.SVCB_IPV6HINT { | ||
| modified = true | ||
|
|
||
| continue | ||
| } | ||
|
|
||
| filtered = append(filtered, kv) | ||
| } | ||
|
|
||
| *values = filtered | ||
| } |
There was a problem hiding this comment.
Fixed in 6c0f991. A containsIPv6Hint pre-check now skips records that carry no ipv6hint, so the Value slice is only rebuilt and reassigned for records actually being modified.
…cords Address review feedback on the ipv6hint stripping: - Only drop RRSIGs covering record types that were actually modified, so a hint removed from an HTTPS RR no longer invalidates unrelated SVCB signatures. - Skip rebuilding the SvcParam slice for records that carry no ipv6hint. - Narrow the doc comment to the answer section (the only section processed). Also fixes lint: drop unused helper param and unnecessary conversion in tests.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
resolver/filtering_resolver.go:57
removeIPv6Hintsalways allocatesmodifiedTypes := map[uint16]struct{}{}even when there are no HTTPS/SVCB records (or no ipv6hint) in the answer. To reduce allocations on the hot path, consider lazily initializing this map only when a record is actually modified (e.g., keep it nil until the first removal).
func removeIPv6Hints(msg *dns.Msg) {
modifiedTypes := map[uint16]struct{}{}
for _, rr := range msg.Answer {
|
|
||
| // When AAAA lookups are filtered, also drop IPv6 hints from HTTPS/SVCB answers so that | ||
| // clients can't reach the IPv6 endpoints advertised via SvcParams (RFC 9460). | ||
| if resp != nil && resp.Res != nil && r.cfg.QueryTypes.Contains(dns.Type(dns.TypeAAAA)) { |
There was a problem hiding this comment.
Fixed in dd1a3c0. The call is now gated by the request query type (isSVCBQuery), so removeIPv6Hints only runs for HTTPS/SVCB queries. Since those record types can only appear in the answer section of HTTPS/SVCB queries, other traffic (A/AAAA/MX/...) skips the answer scan and the per-request map allocation entirely. Added a spec asserting an A-query response is never scanned.
Gate the ipv6hint stripping behind the request query type. HTTPS/SVCB records can only appear in the answer section of HTTPS/SVCB queries, so A/AAAA/MX and other traffic no longer pays for the answer scan and the per-request map allocation when AAAA filtering is enabled.
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/0xerr0r/blocky](https://github.com/0xERR0R/blocky) | minor | `v0.30.0` → `v0.31.0` | --- ### Release Notes <details> <summary>0xERR0R/blocky (ghcr.io/0xerr0r/blocky)</summary> ### [`v0.31.0`](https://github.com/0xERR0R/blocky/releases/tag/v0.31.0) [Compare Source](0xERR0R/blocky@v0.30.0...v0.31.0) #### Changelog ##### Features - [`132c42f`](0xERR0R/blocky@132c42f): feat(config): validate and log ports.dohPath ([#​2072](0xERR0R/blocky#2072)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`e43b5e5`](0xERR0R/blocky@e43b5e5): feat(querylog): add SQLite query log target ([#​2080](0xERR0R/blocky#2080)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`5d806b6`](0xERR0R/blocky@5d806b6): feat(server): opt-in IP\_FREEBIND for DNS listeners ([#​2078](0xERR0R/blocky#2078)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`f18e52b`](0xERR0R/blocky@f18e52b): feat: capability-aware image that runs under drop:ALL ([#​2071](0xERR0R/blocky#2071)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`bf19d6d`](0xERR0R/blocky@bf19d6d): feat: config JSON schema generation and schema-driven validation ([#​2066](0xERR0R/blocky#2066)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`99ae703`](0xERR0R/blocky@99ae703): feat: filter ipv6hint in HTTPS/SVCB RR when AAAA is filtered ([#​2070](0xERR0R/blocky#2070)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`c44017a`](0xERR0R/blocky@c44017a): feat: load sensitive config values from files ([#​2077](0xERR0R/blocky#2077)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`e6b41db`](0xERR0R/blocky@e6b41db): feat: per-client DNS rate limiting ([#​2063](0xERR0R/blocky#2063)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`f457ec9`](0xERR0R/blocky@f457ec9): feat: read bootstrap nameservers from a resolv.conf file (resolvFile) ([#​2068](0xERR0R/blocky#2068)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`fdcf351`](0xERR0R/blocky@fdcf351): feat: resolve client names from hosts file and custom DNS ([#​1964](0xERR0R/blocky#1964)) ([#​2069](0xERR0R/blocky#2069)) ([@​0xERR0R](https://github.com/0xERR0R)) ##### Bug fixes - [`93cc238`](0xERR0R/blocky@93cc238): fix(caching): sync prefetched entries to Redis ([#​2079](0xERR0R/blocky#2079)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`69b0663`](0xERR0R/blocky@69b0663): fix(config): handle DNS stamp port on hostname field and bootstrap IPs ([#​2065](0xERR0R/blocky#2065)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`31f8df3`](0xERR0R/blocky@31f8df3): fix(querylog): build SQLite target only on modernc-supported platforms ([#​2082](0xERR0R/blocky#2082)) ([@​0xERR0R](https://github.com/0xERR0R)) ##### Build and dependencies - [`a5cdeb2`](0xERR0R/blocky@a5cdeb2): build(deps): bump github.com/go-chi/chi/v5 from 5.2.5 to 5.3.0 ([#​2074](0xERR0R/blocky#2074)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`5a25f67`](0xERR0R/blocky@5a25f67): build(deps): bump github.com/mattn/go-colorable from 0.1.14 to 0.1.15 ([#​2075](0xERR0R/blocky#2075)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`1da222a`](0xERR0R/blocky@1da222a): build(deps): bump github.com/oapi-codegen/runtime from 1.4.0 to 1.4.1 ([#​2064](0xERR0R/blocky#2064)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`6ff1d13`](0xERR0R/blocky@6ff1d13): build(deps): bump golang.org/x/net from 0.54.0 to 0.55.0 ([#​2067](0xERR0R/blocky#2067)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`63f8d9a`](0xERR0R/blocky@63f8d9a): build: upgrade Go to 1.26.2 and golangci-lint to v2.12.2 ([#​2062](0xERR0R/blocky#2062)) ([@​0xERR0R](https://github.com/0xERR0R)) ##### Misc - [`e2de6ae`](0xERR0R/blocky@e2de6ae): docs(redis): document unix socket support and add e2e test ([#​2076](0xERR0R/blocky#2076)) ([@​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:eyJjcmVhdGVkSW5WZXIiOiI0My4yMDUuMyIsInVwZGF0ZWRJblZlciI6IjQzLjIwNS4zIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Closes #1706
Problem
When
filtering.queryTypesincludesAAAA(to keep clients off IPv6), Blocky still returnsHTTPS/SVCBrecords (RFC 9460) that carry anipv6hintSvcParam. Modern clients use those hints to connect directly over IPv6, bypassing the AAAA filter entirely:AdGuard Home strips these hints; Blocky did not.
Change
FilteringResolvernow, only whenAAAAis among the filtered query types, removes theipv6hintSvcParam from everyHTTPSandSVCBanswer record. All other SvcParams (alpn,ipv4hint, ECH, …) are preserved, so HTTP/3/2 negotiation and IPv4 hints keep working; a record left with no hints simply makes the client fall back to anAlookup.Notes
FilteringResolverare never shared with the cache —CachingResolverunpacks a fresh*dns.Msgon every hit and stores a packed copy on miss — so in-place mutation is safe and matches the existingsetTTLInCachedResponsepattern.HTTPS(type 65) andSVCB(type 64); non-SVCB answers are untouched.Tests
resolver/filtering_resolver_test.go: stripsipv6hintfromHTTPSwhile keepingalpn/ipv4hint; strips fromSVCB; leaves non-SVCB answers alone; keeps the hint whenAAAAis not filtered.Docs
docs/configuration.md.