[PR #3477] [MERGED] fix(recursor): require NS owner name to match zone in zone cut detection #3876

Closed
opened 2026-03-16 12:07:34 +03:00 by kerem · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/hickory-dns/hickory-dns/pull/3477
Author: @jackboykin
Created: 3/1/2026
Status: Merged
Merged: 3/9/2026
Merged by: @djc

Base: mainHead: fix/recursor-zone-cut-detection


📝 Commits (2)

  • 52cbd8e fix(recursor): check NS record owner name in zone cut detection
  • 6dbcffa Merge branch 'main' into fix/recursor-zone-cut-detection

📊 Changes

4 files changed (+169 additions, -1 deletions)

View changed files

📝 conformance/e2e-tests/src/recursor/delegation/scenarios.rs (+90 -0)
📝 conformance/test-server/src/handlers.rs (+77 -0)
📝 conformance/test-server/src/main.rs (+1 -0)
📝 crates/resolver/src/recursor/handle.rs (+1 -1)

📄 Description

Found while swapping homelab from unbound to hickory (for RFC 9539 support). Twitter and Spotify were failing on my phone, I investigated thereafter.

Bug

The recursor queries for api.x.com. NS records. The response doesn't include records for that name, but does include NS records for x.com. in the authority section. This is valid per RFC 2308 Section 2.2.

$ dig @a.r06.twtrdns.net. api.x.com. NS +norecurse

;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63143
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 8

;; AUTHORITY SECTION:
x.com.          13999   IN  NS  a.r10.twtrdns.net.
x.com.          13999   IN  NS  b.r10.twtrdns.net.
...

ns_pool_for_name() then concludes that api.x.com. is a valid zone because it got NS records in the response. On a subsequent query it will have api.x.com. as the bailiwick, causing it to think the valid NS records for x.com. are out of scope:

ERROR dropping out of bailiwick record record=x.com. 13999 IN NS a.r10.twtrdns.net. zone=api.x.com.

This produces SERVFAIL/NXDOMAIN for domains delegated through twtrdns.net (twitter.com, abs.twimg.com, etc.) and other servers with the same behavior (e.g. aaplimg.com).

Per RFC 1034 Section 5.3.3, resolvers should validate that a delegation is "closer" to the answer than the current servers. A parent NS record in a NODATA response is further away, not closer, and should not be treated as a zone cut. This matches Unbound's behavior, which uses harden-glue and referral path validation to reject out-of-zone authority records.

Fix

Before, ns_pool_for_name() marked something as a zone cut if it got any NS records back. Now it checks if those NS records are actually for the queried zone.

 let any_ns = response
     .all_sections()
-    .any(|record| record.record_type() == RecordType::NS);
+    .any(|record| record.record_type() == RecordType::NS && record.name() == &zone);

Name's PartialEq uses case-insensitive comparison per RFC 4343.

Test plan

The e2e test uses a CNAME chain to make the bug observable: deep.sub.example.testing. IN CNAME target.example.testing. with an inline A record. When the false zone cut narrows the bailiwick to sub.example.testing., the target.example.testing. IN A record is dropped by the bailiwick filter, causing CNAME following to fail. Without the false zone cut, all records pass the filter.

  • New e2e test parent_ns_in_authority_does_not_prevent_resolution fails without fix (NXDOMAIN), passes with fix (NOERROR)
  • Full conformance suite passes (155/155), e2e tests pass (31/31)
  • hickory-resolver unit tests pass (77 pass, 1 pre-existing failure, 3 ignored)
  • Fix verified against the previously failing services (Twitter, Spotify app) on my homelab recursor

Disclosure

Bug discovered through real-world usage; fix and conformance test developed with assistance from Claude Code (Opus 4.6).


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/hickory-dns/hickory-dns/pull/3477 **Author:** [@jackboykin](https://github.com/jackboykin) **Created:** 3/1/2026 **Status:** ✅ Merged **Merged:** 3/9/2026 **Merged by:** [@djc](https://github.com/djc) **Base:** `main` ← **Head:** `fix/recursor-zone-cut-detection` --- ### 📝 Commits (2) - [`52cbd8e`](https://github.com/hickory-dns/hickory-dns/commit/52cbd8e01613b5d76a2fbb1dd85e4cd81fb57107) fix(recursor): check NS record owner name in zone cut detection - [`6dbcffa`](https://github.com/hickory-dns/hickory-dns/commit/6dbcffae21db2686f0d4b6faf28d99bee7ae101c) Merge branch 'main' into fix/recursor-zone-cut-detection ### 📊 Changes **4 files changed** (+169 additions, -1 deletions) <details> <summary>View changed files</summary> 📝 `conformance/e2e-tests/src/recursor/delegation/scenarios.rs` (+90 -0) 📝 `conformance/test-server/src/handlers.rs` (+77 -0) 📝 `conformance/test-server/src/main.rs` (+1 -0) 📝 `crates/resolver/src/recursor/handle.rs` (+1 -1) </details> ### 📄 Description Found while swapping homelab from unbound to hickory (for RFC 9539 support). Twitter and Spotify were failing on my phone, I investigated thereafter. ## Bug The recursor queries for `api.x.com.` NS records. The response doesn't include records for that name, but does include NS records for `x.com.` in the authority section. This is valid per RFC 2308 Section 2.2. ``` $ dig @a.r06.twtrdns.net. api.x.com. NS +norecurse ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63143 ;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 8 ;; AUTHORITY SECTION: x.com. 13999 IN NS a.r10.twtrdns.net. x.com. 13999 IN NS b.r10.twtrdns.net. ... ``` `ns_pool_for_name()` then concludes that `api.x.com.` is a valid zone because it got NS records in the response. On a subsequent query it will have `api.x.com.` as the bailiwick, causing it to think the valid NS records for `x.com.` are out of scope: ``` ERROR dropping out of bailiwick record record=x.com. 13999 IN NS a.r10.twtrdns.net. zone=api.x.com. ``` This produces SERVFAIL/NXDOMAIN for domains delegated through twtrdns.net (twitter.com, abs.twimg.com, etc.) and other servers with the same behavior (e.g. aaplimg.com). Per RFC 1034 Section 5.3.3, resolvers should validate that a delegation is "closer" to the answer than the current servers. A parent NS record in a NODATA response is further away, not closer, and should not be treated as a zone cut. This matches Unbound's behavior, which uses `harden-glue` and referral path validation to reject out-of-zone authority records. ## Fix Before, `ns_pool_for_name()` marked something as a zone cut if it got any NS records back. Now it checks if those NS records are actually for the queried zone. ```diff let any_ns = response .all_sections() - .any(|record| record.record_type() == RecordType::NS); + .any(|record| record.record_type() == RecordType::NS && record.name() == &zone); ``` `Name`'s `PartialEq` uses case-insensitive comparison per RFC 4343. ## Test plan The e2e test uses a CNAME chain to make the bug observable: `deep.sub.example.testing. IN CNAME target.example.testing.` with an inline A record. When the false zone cut narrows the bailiwick to `sub.example.testing.`, the `target.example.testing. IN A` record is dropped by the bailiwick filter, causing CNAME following to fail. Without the false zone cut, all records pass the filter. - [x] New e2e test `parent_ns_in_authority_does_not_prevent_resolution` fails without fix (NXDOMAIN), passes with fix (NOERROR) - [x] Full conformance suite passes (155/155), e2e tests pass (31/31) - [x] `hickory-resolver` unit tests pass (77 pass, 1 pre-existing failure, 3 ignored) - [x] Fix verified against the previously failing services (Twitter, Spotify app) on my homelab recursor ## Disclosure Bug discovered through real-world usage; fix and conformance test developed with assistance from Claude Code (Opus 4.6). --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
kerem 2026-03-16 12:07:34 +03:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/hickory-dns#3876
No description provided.