diff --git a/CHANGELOG.md b/CHANGELOG.md index 899e5b3d..5a526f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG +## 8.0.11 +- Modified Zone#fetch_authority to handle SOA answer sections from authoritative servers + ## 8.0.10 - Fix Cloudflare provider to skip empty changesets - Add TXT record denormalization for DNSimple provider diff --git a/lib/record_store/version.rb b/lib/record_store/version.rb index a25d6029..bee5e28e 100644 --- a/lib/record_store/version.rb +++ b/lib/record_store/version.rb @@ -1,3 +1,3 @@ module RecordStore - VERSION = '8.0.10'.freeze + VERSION = '8.0.11'.freeze end diff --git a/lib/record_store/zone.rb b/lib/record_store/zone.rb index 850a8513..a55bf91b 100644 --- a/lib/record_store/zone.rb +++ b/lib/record_store/zone.rb @@ -143,6 +143,12 @@ def write(**write_options) def fetch_authority(nameserver = ROOT_SERVERS.sample) authority = fetch_soa(nameserver) do |reply, _name| + # If we get a valid SOA answer, the nameserver is authoritative, so fetch NS records directly + if reply.answer.any? && reply.answer.first[0].to_s == unrooted_name + break fetch_ns_records(nameserver) + end + + # For NXDOMAIN or referral responses, continue with authority section break if reply.answer.any? raise "No authority found (#{name})" if reply.authority.none? @@ -151,7 +157,16 @@ def fetch_authority(nameserver = ROOT_SERVERS.sample) end # candidate DNS name is returned instead when NXDomain or other error - return if unrooted_name.casecmp?(Array(authority).first.to_s) + # In this case, query the parent domain's NS records + if authority.is_a?(Array) && authority.first.is_a?(Resolv::DNS::Name) && + unrooted_name.casecmp?(authority.first.to_s) + # Extract parent domain from unrooted_name (e.g., "sub.example.com" -> "example.com") + parts = unrooted_name.split('.') + return if parts.length <= 1 # No parent domain available (TLD) + + parent_domain = parts[1..-1].join('.') + '.' + return fetch_ns_records_for_domain(parent_domain, nameserver) + end authority end @@ -164,6 +179,21 @@ def fetch_soa(nameserver, &block) end end + def fetch_ns_records(nameserver) + fetch_ns_records_for_domain(name, nameserver) + end + + def fetch_ns_records_for_domain(domain, nameserver) + Resolv::DNS.open(nameserver: nameserver) do |resolv| + resources = resolv.getresources(domain, Resolv::DNS::Resource::IN::NS) + return nil if resources.empty? + + resources.map.with_index do |ns, index| + Record::NS.new(ttl: ns.ttl, fqdn: domain, nsdname: ns.name.to_s, record_id: index) + end + end + end + def resolve_authority(authority) nameservers = authority.map { |a| a.last.name.to_s } diff --git a/record_store.gemspec b/record_store.gemspec index 48d78eb4..b478df3a 100644 --- a/record_store.gemspec +++ b/record_store.gemspec @@ -28,6 +28,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'activemodel', '>= 4.2' spec.add_runtime_dependency 'activesupport', '>= 4.2' + spec.add_runtime_dependency 'csv' spec.add_runtime_dependency 'ejson' spec.add_runtime_dependency 'thor', '>= 1.4.0' diff --git a/test/cli/info_test.rb b/test/cli/info_test.rb index 151acc48..4290b240 100644 --- a/test/cli/info_test.rb +++ b/test/cli/info_test.rb @@ -35,13 +35,10 @@ def test_lists_providers def test_lists_authoritative_nameservers RecordStore::CLI.start(%w(info)) - authority = <<~AUTHORITY - Authoritative nameservers: - - [NSRecord] example.com. 172800 IN NS a.iana-servers.net. - - [NSRecord] example.com. 172800 IN NS b.iana-servers.net. - AUTHORITY - - assert_includes($stdout.string, authority) + output = $stdout.string + assert_includes(output, "Authoritative nameservers:") + assert_includes(output, "hera.ns.cloudflare.com.") + assert_includes(output, "elliott.ns.cloudflare.com.") end end end diff --git a/test/zone_test.rb b/test/zone_test.rb index 3068eadb..437d3ccb 100644 --- a/test/zone_test.rb +++ b/test/zone_test.rb @@ -678,11 +678,12 @@ def implicit_record_injection_does_not_occur_if_no_implicit_records_templates_ar def test_fetch_authority zone = Zone.new(name: 'example.com') nameservers = zone.fetch_authority - expected = [ - Record::NS.new(fqdn: 'example.com', ttl: 172_800, nsdname: 'a.iana-servers.net.'), - Record::NS.new(fqdn: 'example.com', ttl: 172_800, nsdname: 'b.iana-servers.net.'), - ] - assert_equal(expected, nameservers) + + assert_equal(2, nameservers.count) + nsdnames = nameservers.map(&:nsdname).sort + assert_equal(['elliott.ns.cloudflare.com.', 'hera.ns.cloudflare.com.'], nsdnames) + assert_equal('example.com.', nameservers.first.fqdn) + assert(nameservers.first.ttl > 0) end def test_fetch_authority_handles_unreachable_host