Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/record_store/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module RecordStore
VERSION = '8.0.10'.freeze
VERSION = '8.0.11'.freeze
end
32 changes: 31 additions & 1 deletion lib/record_store/zone.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
Expand All @@ -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 }

Expand Down
1 change: 1 addition & 0 deletions record_store.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
11 changes: 4 additions & 7 deletions test/cli/info_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 6 additions & 5 deletions test/zone_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down