mirror of
https://github.com/internetee/registry.git
synced 2025-08-15 22:13:54 +02:00
DNS validator
This commit is contained in:
parent
bde53d73d0
commit
53510cad43
2 changed files with 1118 additions and 0 deletions
644
app/services/dns_validator.rb
Normal file
644
app/services/dns_validator.rb
Normal file
|
@ -0,0 +1,644 @@
|
||||||
|
require 'resolv'
|
||||||
|
require 'dnsruby'
|
||||||
|
|
||||||
|
class DNSValidator
|
||||||
|
include Dnsruby
|
||||||
|
|
||||||
|
attr_reader :domain, :results
|
||||||
|
|
||||||
|
def initialize(domain)
|
||||||
|
@domain = domain
|
||||||
|
@results = {
|
||||||
|
nameservers: {},
|
||||||
|
dns_records: {},
|
||||||
|
dnssec: {},
|
||||||
|
csync: {},
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.validate(domain)
|
||||||
|
new(domain).validate
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate
|
||||||
|
Rails.logger.info "Starting DNS validation for domain: #{domain.name}"
|
||||||
|
|
||||||
|
validate_nameservers
|
||||||
|
validate_dns_records
|
||||||
|
check_dnssec_sync_records
|
||||||
|
check_csync_records
|
||||||
|
apply_enforcement_actions
|
||||||
|
|
||||||
|
Rails.logger.info "DNS validation completed for domain: #{domain.name}"
|
||||||
|
@results
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "DNS validation failed for #{domain.name}: #{e.message}"
|
||||||
|
@results[:errors] << "Validation failed: #{e.message}"
|
||||||
|
@results
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_nameservers
|
||||||
|
Rails.logger.info "Validating nameservers for domain: #{domain.name}"
|
||||||
|
|
||||||
|
domain.nameservers.each do |nameserver|
|
||||||
|
result = validate_single_nameserver(nameserver)
|
||||||
|
@results[:nameservers][nameserver.hostname] = result
|
||||||
|
|
||||||
|
if result[:valid]
|
||||||
|
# Update nameserver validation status
|
||||||
|
nameserver.update_columns(
|
||||||
|
validation_datetime: Time.current,
|
||||||
|
validation_counter: 0,
|
||||||
|
failed_validation_reason: nil
|
||||||
|
)
|
||||||
|
else
|
||||||
|
@results[:errors] << "Nameserver #{nameserver.hostname} failed validation: #{result[:reason]}"
|
||||||
|
|
||||||
|
# Update failure counter
|
||||||
|
counter = (nameserver.validation_counter || 0) + 1
|
||||||
|
nameserver.update_columns(
|
||||||
|
validation_datetime: Time.current,
|
||||||
|
validation_counter: counter,
|
||||||
|
failed_validation_reason: result[:reason]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_single_nameserver(nameserver)
|
||||||
|
result = {
|
||||||
|
hostname: nameserver.hostname,
|
||||||
|
valid: false,
|
||||||
|
authoritative: false,
|
||||||
|
ns_records: [],
|
||||||
|
reason: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
begin
|
||||||
|
resolver = create_resolver(nameserver.hostname)
|
||||||
|
|
||||||
|
# Query SOA to check if nameserver is authoritative for this domain
|
||||||
|
soa_response = resolver.query(domain.name, 'SOA')
|
||||||
|
|
||||||
|
if soa_response.answer.empty?
|
||||||
|
result[:reason] = 'No SOA record found'
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check for CNAME at domain apex (invalid)
|
||||||
|
if soa_response.answer.any? { |a| a.type == 'CNAME' }
|
||||||
|
result[:reason] = 'Domain has CNAME record at apex (invalid)'
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check SOA record
|
||||||
|
soa_record = soa_response.answer.find { |a| a.type == 'SOA' }
|
||||||
|
if soa_record
|
||||||
|
result[:authoritative] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Query NS records to verify this nameserver is listed
|
||||||
|
ns_response = resolver.query(domain.name, 'NS')
|
||||||
|
result[:ns_records] = ns_response.answer.map { |a| a.nsdname.to_s if a.type == 'NS' }.compact
|
||||||
|
|
||||||
|
# Check if this nameserver is in the NS records
|
||||||
|
if result[:ns_records].any? { |ns| ns.downcase == nameserver.hostname.downcase }
|
||||||
|
result[:valid] = true
|
||||||
|
else
|
||||||
|
result[:reason] = 'Nameserver not listed in zone NS records'
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue Dnsruby::NXDomain
|
||||||
|
result[:reason] = 'Domain not found'
|
||||||
|
rescue Dnsruby::Refused
|
||||||
|
result[:reason] = 'Query refused'
|
||||||
|
rescue StandardError => e
|
||||||
|
result[:reason] = "Query failed: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
# Story 2: Resolve and Validate DNS Records
|
||||||
|
def validate_dns_records
|
||||||
|
Rails.logger.info "Validating DNS records for domain: #{domain.name}"
|
||||||
|
|
||||||
|
@results[:dns_records] = {
|
||||||
|
a_records: [],
|
||||||
|
aaaa_records: [],
|
||||||
|
cname_records: []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only check records from valid nameservers
|
||||||
|
valid_nameservers = domain.nameservers.select do |ns|
|
||||||
|
@results[:nameservers][ns.hostname]&.dig(:valid)
|
||||||
|
end
|
||||||
|
|
||||||
|
if valid_nameservers.empty?
|
||||||
|
@results[:warnings] << "No valid nameservers found for DNS record validation"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
valid_nameservers.each do |nameserver|
|
||||||
|
validate_records_from_nameserver(nameserver)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check for glue records if needed
|
||||||
|
validate_glue_records
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_records_from_nameserver(nameserver)
|
||||||
|
resolver = create_resolver(nameserver.hostname)
|
||||||
|
|
||||||
|
# Validate A records
|
||||||
|
begin
|
||||||
|
a_response = resolver.query(domain.name, 'A')
|
||||||
|
a_response.answer.each do |record|
|
||||||
|
next unless record.type == 'A'
|
||||||
|
@results[:dns_records][:a_records] << {
|
||||||
|
address: record.address.to_s,
|
||||||
|
ttl: record.ttl,
|
||||||
|
nameserver: nameserver.hostname
|
||||||
|
}
|
||||||
|
end
|
||||||
|
rescue Dnsruby::NXDomain
|
||||||
|
# No A records
|
||||||
|
rescue StandardError => e
|
||||||
|
@results[:warnings] << "Failed to query A records from #{nameserver.hostname}: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate AAAA records
|
||||||
|
begin
|
||||||
|
aaaa_response = resolver.query(domain.name, 'AAAA')
|
||||||
|
aaaa_response.answer.each do |record|
|
||||||
|
next unless record.type == 'AAAA'
|
||||||
|
@results[:dns_records][:aaaa_records] << {
|
||||||
|
address: record.address.to_s,
|
||||||
|
ttl: record.ttl,
|
||||||
|
nameserver: nameserver.hostname
|
||||||
|
}
|
||||||
|
end
|
||||||
|
rescue Dnsruby::NXDomain
|
||||||
|
# No AAAA records
|
||||||
|
rescue StandardError => e
|
||||||
|
@results[:warnings] << "Failed to query AAAA records from #{nameserver.hostname}: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check for CNAME records (should not exist at apex)
|
||||||
|
begin
|
||||||
|
cname_response = resolver.query(domain.name, 'CNAME')
|
||||||
|
cname_response.answer.each do |record|
|
||||||
|
next unless record.type == 'CNAME'
|
||||||
|
@results[:dns_records][:cname_records] << {
|
||||||
|
target: record.cname.to_s,
|
||||||
|
ttl: record.ttl,
|
||||||
|
nameserver: nameserver.hostname
|
||||||
|
}
|
||||||
|
@results[:errors] << "CNAME record found at domain apex (invalid DNS configuration)"
|
||||||
|
end
|
||||||
|
rescue Dnsruby::NXDomain
|
||||||
|
# No CNAME records (good)
|
||||||
|
rescue StandardError => e
|
||||||
|
@results[:warnings] << "Failed to query CNAME records from #{nameserver.hostname}: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_glue_records
|
||||||
|
domain.nameservers.each do |nameserver|
|
||||||
|
# Check if nameserver is in-bailiwick (subdomain of the domain)
|
||||||
|
next unless nameserver.hostname.end_with?(".#{domain.name}")
|
||||||
|
|
||||||
|
# Validate IPv4 glue records
|
||||||
|
nameserver.ipv4.each do |ip|
|
||||||
|
if valid_ipv4?(ip)
|
||||||
|
@results[:dns_records][:a_records] << {
|
||||||
|
address: ip,
|
||||||
|
ttl: 0,
|
||||||
|
nameserver: 'glue',
|
||||||
|
type: 'glue_record'
|
||||||
|
}
|
||||||
|
else
|
||||||
|
@results[:errors] << "Invalid IPv4 glue record for #{nameserver.hostname}: #{ip}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate IPv6 glue records
|
||||||
|
nameserver.ipv6.each do |ip|
|
||||||
|
if valid_ipv6?(ip)
|
||||||
|
@results[:dns_records][:aaaa_records] << {
|
||||||
|
address: ip,
|
||||||
|
ttl: 0,
|
||||||
|
nameserver: 'glue',
|
||||||
|
type: 'glue_record'
|
||||||
|
}
|
||||||
|
else
|
||||||
|
@results[:errors] << "Invalid IPv6 glue record for #{nameserver.hostname}: #{ip}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Story 4: Check CDS/CDNSKEY records for DNSSEC synchronization
|
||||||
|
def check_dnssec_sync_records
|
||||||
|
Rails.logger.info "Checking DNSSEC synchronization records for domain: #{domain.name}"
|
||||||
|
|
||||||
|
@results[:dnssec] = {
|
||||||
|
cds_records: [],
|
||||||
|
cdnskey_records: [],
|
||||||
|
ds_updates_needed: []
|
||||||
|
}
|
||||||
|
|
||||||
|
return unless domain.dnskeys.any? # Only check if DNSSEC is enabled
|
||||||
|
|
||||||
|
domain.nameservers.each do |nameserver|
|
||||||
|
check_cds_records(nameserver)
|
||||||
|
check_cdnskey_records(nameserver)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_cds_records(nameserver)
|
||||||
|
begin
|
||||||
|
resolver = create_resolver(nameserver.hostname)
|
||||||
|
response = resolver.query(domain.name, 'CDS')
|
||||||
|
|
||||||
|
response.answer.each do |record|
|
||||||
|
next unless record.type == 'CDS'
|
||||||
|
|
||||||
|
cds_data = {
|
||||||
|
key_tag: record.key_tag,
|
||||||
|
algorithm: record.algorithm,
|
||||||
|
digest_type: record.digest_type,
|
||||||
|
digest: record.digest,
|
||||||
|
nameserver: nameserver.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
@results[:dnssec][:cds_records] << cds_data
|
||||||
|
|
||||||
|
# Check if DS record update is needed
|
||||||
|
if record.algorithm == 0
|
||||||
|
@results[:dnssec][:ds_updates_needed] << {
|
||||||
|
action: 'remove_ds',
|
||||||
|
reason: 'CDS record with algorithm 0 indicates DS removal'
|
||||||
|
}
|
||||||
|
else
|
||||||
|
# Check if we need to update DS records
|
||||||
|
existing_ds = domain.dnskeys.find_by(ds_key_tag: record.key_tag)
|
||||||
|
if !existing_ds || existing_ds.ds_digest != record.digest
|
||||||
|
@results[:dnssec][:ds_updates_needed] << {
|
||||||
|
action: 'update_ds',
|
||||||
|
cds_data: cds_data,
|
||||||
|
reason: 'CDS record indicates DS record update needed'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue Dnsruby::NXDomain
|
||||||
|
# No CDS records
|
||||||
|
rescue StandardError => e
|
||||||
|
@results[:warnings] << "Failed to query CDS records from #{nameserver.hostname}: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_cdnskey_records(nameserver)
|
||||||
|
begin
|
||||||
|
resolver = create_resolver(nameserver.hostname)
|
||||||
|
response = resolver.query(domain.name, 'CDNSKEY')
|
||||||
|
|
||||||
|
response.answer.each do |record|
|
||||||
|
next unless record.type == 'CDNSKEY'
|
||||||
|
|
||||||
|
cdnskey_data = {
|
||||||
|
flags: record.flags,
|
||||||
|
protocol: record.protocol,
|
||||||
|
algorithm: record.algorithm,
|
||||||
|
public_key: Base64.strict_encode64(record.key),
|
||||||
|
nameserver: nameserver.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
@results[:dnssec][:cdnskey_records] << cdnskey_data
|
||||||
|
|
||||||
|
# Check if this DNSKEY exists in our database
|
||||||
|
existing_key = domain.dnskeys.find_by(
|
||||||
|
flags: record.flags,
|
||||||
|
protocol: record.protocol,
|
||||||
|
alg: record.algorithm,
|
||||||
|
public_key: cdnskey_data[:public_key]
|
||||||
|
)
|
||||||
|
|
||||||
|
unless existing_key
|
||||||
|
@results[:dnssec][:ds_updates_needed] << {
|
||||||
|
action: 'add_dnskey',
|
||||||
|
cdnskey_data: cdnskey_data,
|
||||||
|
reason: 'CDNSKEY record indicates new DNSKEY should be added'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue Dnsruby::NXDomain
|
||||||
|
# No CDNSKEY records
|
||||||
|
rescue StandardError => e
|
||||||
|
@results[:warnings] << "Failed to query CDNSKEY records from #{nameserver.hostname}: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Story 5: Check CSYNC records for delegation synchronization
|
||||||
|
def check_csync_records
|
||||||
|
Rails.logger.info "Checking CSYNC records for domain: #{domain.name}"
|
||||||
|
|
||||||
|
@results[:csync] = {
|
||||||
|
csync_records: [],
|
||||||
|
delegation_updates_needed: []
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.nameservers.each do |nameserver|
|
||||||
|
check_single_csync_record(nameserver)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_single_csync_record(nameserver)
|
||||||
|
begin
|
||||||
|
resolver = create_resolver(nameserver.hostname)
|
||||||
|
response = resolver.query(domain.name, 'CSYNC')
|
||||||
|
|
||||||
|
response.answer.each do |record|
|
||||||
|
next unless record.type == 'CSYNC'
|
||||||
|
|
||||||
|
csync_data = {
|
||||||
|
serial: record.serial,
|
||||||
|
flags: record.flags,
|
||||||
|
type_bitmap: parse_type_bitmap(record.types),
|
||||||
|
nameserver: nameserver.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
@results[:csync][:csync_records] << csync_data
|
||||||
|
|
||||||
|
# Check what needs to be synchronized
|
||||||
|
if csync_data[:type_bitmap].include?('NS')
|
||||||
|
check_ns_sync_needed(nameserver)
|
||||||
|
end
|
||||||
|
|
||||||
|
if csync_data[:type_bitmap].include?('A')
|
||||||
|
check_a_sync_needed(nameserver)
|
||||||
|
end
|
||||||
|
|
||||||
|
if csync_data[:type_bitmap].include?('AAAA')
|
||||||
|
check_aaaa_sync_needed(nameserver)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue Dnsruby::NXDomain
|
||||||
|
# No CSYNC records
|
||||||
|
rescue StandardError => e
|
||||||
|
@results[:warnings] << "Failed to query CSYNC records from #{nameserver.hostname}: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_ns_sync_needed(nameserver)
|
||||||
|
# Get NS records from child zone
|
||||||
|
child_ns_records = @results[:nameservers].values
|
||||||
|
.select { |ns| ns[:valid] }
|
||||||
|
.flat_map { |ns| ns[:ns_records] }
|
||||||
|
.uniq
|
||||||
|
|
||||||
|
# Compare with current nameservers
|
||||||
|
current_ns = domain.nameservers.map(&:hostname)
|
||||||
|
|
||||||
|
to_add = child_ns_records - current_ns
|
||||||
|
to_remove = current_ns - child_ns_records
|
||||||
|
|
||||||
|
if to_add.any? || to_remove.any?
|
||||||
|
@results[:csync][:delegation_updates_needed] << {
|
||||||
|
type: 'ns_records',
|
||||||
|
add: to_add,
|
||||||
|
remove: to_remove,
|
||||||
|
reason: 'CSYNC indicates NS record synchronization needed'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_a_sync_needed(nameserver)
|
||||||
|
# Check A record synchronization for in-bailiwick nameservers
|
||||||
|
domain.nameservers.each do |ns|
|
||||||
|
next unless ns.hostname.end_with?(".#{domain.name}")
|
||||||
|
|
||||||
|
# Query A records from child zone
|
||||||
|
child_a_records = query_a_records_for_host(ns.hostname)
|
||||||
|
|
||||||
|
if child_a_records != ns.ipv4
|
||||||
|
@results[:csync][:delegation_updates_needed] << {
|
||||||
|
type: 'a_records',
|
||||||
|
hostname: ns.hostname,
|
||||||
|
current_ips: ns.ipv4,
|
||||||
|
child_ips: child_a_records,
|
||||||
|
reason: 'CSYNC indicates A record glue synchronization needed'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_aaaa_sync_needed(nameserver)
|
||||||
|
# Check AAAA record synchronization for in-bailiwick nameservers
|
||||||
|
domain.nameservers.each do |ns|
|
||||||
|
next unless ns.hostname.end_with?(".#{domain.name}")
|
||||||
|
|
||||||
|
# Query AAAA records from child zone
|
||||||
|
child_aaaa_records = query_aaaa_records_for_host(ns.hostname)
|
||||||
|
|
||||||
|
if child_aaaa_records != ns.ipv6
|
||||||
|
@results[:csync][:delegation_updates_needed] << {
|
||||||
|
type: 'aaaa_records',
|
||||||
|
hostname: ns.hostname,
|
||||||
|
current_ips: ns.ipv6,
|
||||||
|
child_ips: child_aaaa_records,
|
||||||
|
reason: 'CSYNC indicates AAAA record glue synchronization needed'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Story 6: Apply enforcement actions based on validation results
|
||||||
|
def apply_enforcement_actions
|
||||||
|
Rails.logger.info "Applying enforcement actions for domain: #{domain.name}"
|
||||||
|
|
||||||
|
# Handle failed nameservers
|
||||||
|
domain.nameservers.each do |nameserver|
|
||||||
|
if nameserver.validation_counter && nameserver.validation_counter >= 3
|
||||||
|
@results[:warnings] << "Nameserver #{nameserver.hostname} has failed validation #{nameserver.validation_counter} times"
|
||||||
|
|
||||||
|
# Auto-remove if configured and we have enough nameservers
|
||||||
|
if should_auto_remove_nameserver? && domain.nameservers.count > 2
|
||||||
|
Rails.logger.info "Auto-removing failed nameserver: #{nameserver.hostname}"
|
||||||
|
|
||||||
|
# Notify registrar
|
||||||
|
create_notification(
|
||||||
|
"Nameserver #{nameserver.hostname} was automatically removed from domain #{domain.name} due to repeated validation failures"
|
||||||
|
)
|
||||||
|
|
||||||
|
nameserver.destroy
|
||||||
|
@results[:warnings] << "Automatically removed nameserver #{nameserver.hostname}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Apply DNSSEC updates if needed
|
||||||
|
@results[:dnssec][:ds_updates_needed].each do |update|
|
||||||
|
case update[:action]
|
||||||
|
when 'update_ds'
|
||||||
|
update_ds_record(update[:cds_data])
|
||||||
|
when 'remove_ds'
|
||||||
|
remove_ds_records
|
||||||
|
when 'add_dnskey'
|
||||||
|
add_dnskey(update[:cdnskey_data])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Apply delegation updates if needed
|
||||||
|
@results[:csync][:delegation_updates_needed].each do |update|
|
||||||
|
case update[:type]
|
||||||
|
when 'ns_records'
|
||||||
|
update_ns_records(update)
|
||||||
|
when 'a_records', 'aaaa_records'
|
||||||
|
update_glue_records(update)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Send notifications if there are errors
|
||||||
|
if @results[:errors].any?
|
||||||
|
create_notification(
|
||||||
|
"DNS validation errors found for domain #{domain.name}: #{@results[:errors].join(', ')}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper methods
|
||||||
|
def create_resolver(nameserver_ip)
|
||||||
|
resolver = Dnsruby::Resolver.new
|
||||||
|
resolver.nameserver = nameserver_ip
|
||||||
|
resolver.query_timeout = 5
|
||||||
|
resolver.retry_times = 2
|
||||||
|
resolver.recurse = 0 # Non-recursive queries
|
||||||
|
resolver.do_caching = false
|
||||||
|
resolver
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_ipv4?(ip)
|
||||||
|
ip.match?(Nameserver::IPV4_REGEXP)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_ipv6?(ip)
|
||||||
|
ip.match?(Nameserver::IPV6_REGEXP)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_type_bitmap(types)
|
||||||
|
types.is_a?(Array) ? types : [types].compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_a_records_for_host(hostname)
|
||||||
|
resolver = Dnsruby::Resolver.new
|
||||||
|
response = resolver.query(hostname, 'A')
|
||||||
|
response.answer.select { |r| r.type == 'A' }.map { |r| r.address.to_s }
|
||||||
|
rescue StandardError
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_aaaa_records_for_host(hostname)
|
||||||
|
resolver = Dnsruby::Resolver.new
|
||||||
|
response = resolver.query(hostname, 'AAAA')
|
||||||
|
response.answer.select { |r| r.type == 'AAAA' }.map { |r| r.address.to_s }
|
||||||
|
rescue StandardError
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
def should_auto_remove_nameserver?
|
||||||
|
# Check if auto-removal is enabled (you can add this setting)
|
||||||
|
false # Disabled by default for safety
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_notification(text)
|
||||||
|
begin
|
||||||
|
domain.registrar.notifications.create!(
|
||||||
|
text: text,
|
||||||
|
attached_obj_type: domain.class.to_s,
|
||||||
|
attached_obj_id: domain.id
|
||||||
|
)
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.warn "Failed to create notification: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_ds_record(cds_data)
|
||||||
|
dnskey = domain.dnskeys.find_or_initialize_by(ds_key_tag: cds_data[:key_tag])
|
||||||
|
dnskey.ds_alg = cds_data[:algorithm]
|
||||||
|
dnskey.ds_digest_type = cds_data[:digest_type]
|
||||||
|
dnskey.ds_digest = cds_data[:digest]
|
||||||
|
|
||||||
|
if dnskey.save
|
||||||
|
Rails.logger.info "Updated DS record for #{domain.name}"
|
||||||
|
else
|
||||||
|
Rails.logger.error "Failed to update DS record: #{dnskey.errors.full_messages.join(', ')}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_ds_records
|
||||||
|
domain.dnskeys.update_all(
|
||||||
|
ds_digest: nil,
|
||||||
|
ds_alg: nil,
|
||||||
|
ds_digest_type: nil,
|
||||||
|
ds_key_tag: nil
|
||||||
|
)
|
||||||
|
Rails.logger.info "Removed DS records for #{domain.name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_dnskey(cdnskey_data)
|
||||||
|
dnskey = domain.dnskeys.build(
|
||||||
|
flags: cdnskey_data[:flags],
|
||||||
|
protocol: cdnskey_data[:protocol],
|
||||||
|
alg: cdnskey_data[:algorithm],
|
||||||
|
public_key: cdnskey_data[:public_key]
|
||||||
|
)
|
||||||
|
|
||||||
|
if dnskey.save
|
||||||
|
Rails.logger.info "Added DNSKEY for #{domain.name}"
|
||||||
|
else
|
||||||
|
Rails.logger.error "Failed to add DNSKEY: #{dnskey.errors.full_messages.join(', ')}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_ns_records(update)
|
||||||
|
# Add new nameservers
|
||||||
|
update[:add].each do |hostname|
|
||||||
|
domain.nameservers.find_or_create_by(hostname: hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Remove old nameservers
|
||||||
|
update[:remove].each do |hostname|
|
||||||
|
domain.nameservers.where(hostname: hostname).destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
Rails.logger.info "Updated nameservers for #{domain.name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_glue_records(update)
|
||||||
|
nameserver = domain.nameservers.find_by(hostname: update[:hostname])
|
||||||
|
return unless nameserver
|
||||||
|
|
||||||
|
if update[:type] == 'a_records'
|
||||||
|
nameserver.ipv4 = update[:child_ips]
|
||||||
|
else
|
||||||
|
nameserver.ipv6 = update[:child_ips]
|
||||||
|
end
|
||||||
|
|
||||||
|
nameserver.save
|
||||||
|
Rails.logger.info "Updated glue records for #{nameserver.hostname}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Class methods for easy usage
|
||||||
|
class << self
|
||||||
|
def validate_domain(domain)
|
||||||
|
validator = new(domain)
|
||||||
|
validator.validate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
474
test/services/dns_validator_test.rb
Normal file
474
test/services/dns_validator_test.rb
Normal file
|
@ -0,0 +1,474 @@
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class DNSValidatorTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
|
setup do
|
||||||
|
@domain = domains(:shop)
|
||||||
|
@nameserver1 = nameservers(:shop_ns1)
|
||||||
|
@nameserver2 = nameservers(:shop_ns2)
|
||||||
|
@dnskey = dnskeys(:one)
|
||||||
|
|
||||||
|
# Ensure domain has fresh timestamps for validation
|
||||||
|
@domain.update(created_at: 1.day.ago)
|
||||||
|
@domain.reload
|
||||||
|
|
||||||
|
# Associate dnskey with domain for DNSSEC tests
|
||||||
|
@dnskey.update(domain: @domain) if @dnskey
|
||||||
|
end
|
||||||
|
|
||||||
|
# Basic functionality tests
|
||||||
|
|
||||||
|
test 'initializes with correct structure' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
assert_equal @domain, validator.domain
|
||||||
|
assert_instance_of Hash, validator.results
|
||||||
|
|
||||||
|
expected_keys = [:nameservers, :dns_records, :dnssec, :csync, :errors, :warnings]
|
||||||
|
expected_keys.each do |key|
|
||||||
|
assert_includes validator.results.keys, key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'class method validate creates instance' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
# Mock validate method to avoid DNS calls
|
||||||
|
validator.define_singleton_method(:validate) { { test: 'result' } }
|
||||||
|
|
||||||
|
# Temporarily override new method
|
||||||
|
original_new = DNSValidator.method(:new)
|
||||||
|
DNSValidator.define_singleton_method(:new) { |domain| validator }
|
||||||
|
|
||||||
|
result = DNSValidator.validate(@domain)
|
||||||
|
|
||||||
|
assert_equal({ test: 'result' }, result)
|
||||||
|
ensure
|
||||||
|
# Restore original new method
|
||||||
|
DNSValidator.define_singleton_method(:new, original_new)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Story 1: Nameserver Validation Tests
|
||||||
|
|
||||||
|
test 'validates nameservers successfully' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
# Mock successful nameserver validation
|
||||||
|
validator.define_singleton_method(:validate_single_nameserver) do |ns|
|
||||||
|
{ valid: true, authoritative: true, ns_records: [ns.hostname], reason: nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
validator.send(:validate_nameservers)
|
||||||
|
|
||||||
|
@nameserver1.reload
|
||||||
|
assert_not_nil @nameserver1.validation_datetime
|
||||||
|
assert_equal 0, @nameserver1.validation_counter
|
||||||
|
assert_nil @nameserver1.failed_validation_reason
|
||||||
|
|
||||||
|
assert validator.results[:nameservers][@nameserver1.hostname][:valid]
|
||||||
|
assert_empty validator.results[:errors]
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'handles failed nameserver validation' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
# Mock failed nameserver validation
|
||||||
|
validator.define_singleton_method(:validate_single_nameserver) do |ns|
|
||||||
|
{ valid: false, authoritative: false, ns_records: [], reason: 'No SOA record found' }
|
||||||
|
end
|
||||||
|
|
||||||
|
validator.send(:validate_nameservers)
|
||||||
|
|
||||||
|
@nameserver1.reload
|
||||||
|
assert_not_nil @nameserver1.validation_datetime
|
||||||
|
assert_equal 1, @nameserver1.validation_counter
|
||||||
|
assert_equal 'No SOA record found', @nameserver1.failed_validation_reason
|
||||||
|
|
||||||
|
assert_not validator.results[:nameservers][@nameserver1.hostname][:valid]
|
||||||
|
assert_includes validator.results[:errors], "Nameserver #{@nameserver1.hostname} failed validation: No SOA record found"
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'validates single nameserver with mocked DNS' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
# Create mock resolver
|
||||||
|
resolver = create_mock_resolver
|
||||||
|
|
||||||
|
# Create references to helper methods in test context
|
||||||
|
test_context = self
|
||||||
|
nameserver_hostname = @nameserver1.hostname
|
||||||
|
|
||||||
|
# Mock SOA response (authoritative)
|
||||||
|
soa_response = create_mock_dns_response([create_mock_soa_record])
|
||||||
|
ns_response = create_mock_dns_response([create_mock_ns_record(nameserver_hostname)])
|
||||||
|
|
||||||
|
resolver.define_singleton_method(:query) do |domain, type|
|
||||||
|
if type == 'SOA'
|
||||||
|
soa_response
|
||||||
|
elsif type == 'NS'
|
||||||
|
ns_response
|
||||||
|
else
|
||||||
|
test_context.create_mock_dns_response([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
validator.define_singleton_method(:create_resolver) { |ip| resolver }
|
||||||
|
|
||||||
|
result = validator.send(:validate_single_nameserver, @nameserver1)
|
||||||
|
|
||||||
|
assert result[:valid]
|
||||||
|
assert result[:authoritative]
|
||||||
|
assert_includes result[:ns_records], @nameserver1.hostname.downcase
|
||||||
|
assert_nil result[:reason]
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'detects nameserver not in NS records' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
resolver = create_mock_resolver
|
||||||
|
|
||||||
|
# Pre-create responses
|
||||||
|
test_context = self
|
||||||
|
soa_response = create_mock_dns_response([create_mock_soa_record])
|
||||||
|
ns_response = create_mock_dns_response([create_mock_ns_record('other.nameserver.com')])
|
||||||
|
|
||||||
|
resolver.define_singleton_method(:query) do |domain, type|
|
||||||
|
if type == 'SOA'
|
||||||
|
soa_response
|
||||||
|
elsif type == 'NS'
|
||||||
|
ns_response
|
||||||
|
else
|
||||||
|
test_context.create_mock_dns_response([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
validator.define_singleton_method(:create_resolver) { |ip| resolver }
|
||||||
|
|
||||||
|
result = validator.send(:validate_single_nameserver, @nameserver1)
|
||||||
|
|
||||||
|
assert_not result[:valid]
|
||||||
|
assert_equal 'Nameserver not listed in zone NS records', result[:reason]
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'detects CNAME at apex' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
resolver = create_mock_resolver
|
||||||
|
|
||||||
|
# Pre-create CNAME response
|
||||||
|
test_context = self
|
||||||
|
cname_response = create_mock_dns_response([create_mock_cname_record('example.com')])
|
||||||
|
|
||||||
|
# Mock CNAME response for SOA query
|
||||||
|
resolver.define_singleton_method(:query) do |domain, type|
|
||||||
|
cname_response
|
||||||
|
end
|
||||||
|
|
||||||
|
validator.define_singleton_method(:create_resolver) { |ip| resolver }
|
||||||
|
|
||||||
|
result = validator.send(:validate_single_nameserver, @nameserver1)
|
||||||
|
|
||||||
|
assert_not result[:valid]
|
||||||
|
assert_equal 'Domain has CNAME record at apex (invalid)', result[:reason]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Story 2: DNS Records Validation Tests
|
||||||
|
|
||||||
|
test 'validates DNS records from valid nameservers' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
# Set up nameserver validation results
|
||||||
|
validator.instance_variable_set(:@results, {
|
||||||
|
nameservers: { @nameserver1.hostname => { valid: true } },
|
||||||
|
dns_records: { a_records: [], aaaa_records: [], cname_records: [] },
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
})
|
||||||
|
|
||||||
|
# Pre-create responses
|
||||||
|
test_context = self
|
||||||
|
a_response = create_mock_dns_response([create_mock_a_record('192.0.2.1')])
|
||||||
|
aaaa_response = create_mock_dns_response([create_mock_aaaa_record('2001:db8::1')])
|
||||||
|
empty_response = create_mock_dns_response([])
|
||||||
|
|
||||||
|
resolver = create_mock_resolver
|
||||||
|
resolver.define_singleton_method(:query) do |domain, type|
|
||||||
|
case type
|
||||||
|
when 'A'
|
||||||
|
a_response
|
||||||
|
when 'AAAA'
|
||||||
|
aaaa_response
|
||||||
|
when 'CNAME'
|
||||||
|
empty_response
|
||||||
|
else
|
||||||
|
empty_response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
validator.define_singleton_method(:create_resolver) { |ip| resolver }
|
||||||
|
|
||||||
|
validator.send(:validate_dns_records)
|
||||||
|
|
||||||
|
assert_not_empty validator.results[:dns_records][:a_records]
|
||||||
|
assert_not_empty validator.results[:dns_records][:aaaa_records]
|
||||||
|
|
||||||
|
a_record = validator.results[:dns_records][:a_records].first
|
||||||
|
assert_equal '192.0.2.1', a_record[:address]
|
||||||
|
assert_equal @nameserver1.hostname, a_record[:nameserver]
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'skips DNS validation when no valid nameservers' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
# Set up scenario with no valid nameservers
|
||||||
|
validator.instance_variable_set(:@results, {
|
||||||
|
nameservers: { @nameserver1.hostname => { valid: false } },
|
||||||
|
dns_records: { a_records: [], aaaa_records: [], cname_records: [] },
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
})
|
||||||
|
|
||||||
|
validator.send(:validate_dns_records)
|
||||||
|
|
||||||
|
assert_includes validator.results[:warnings], 'No valid nameservers found for DNS record validation'
|
||||||
|
assert_empty validator.results[:dns_records][:a_records]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Story 4: DNSSEC Tests
|
||||||
|
|
||||||
|
test 'processes CDS records for DNSSEC sync' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
resolver = create_mock_resolver
|
||||||
|
|
||||||
|
# Pre-create responses
|
||||||
|
test_context = self
|
||||||
|
cds_response = create_mock_dns_response([create_mock_cds_record(12345, 7, 1, 'ABC123')])
|
||||||
|
empty_response = create_mock_dns_response([])
|
||||||
|
|
||||||
|
resolver.define_singleton_method(:query) do |domain, type|
|
||||||
|
case type
|
||||||
|
when 'CDS'
|
||||||
|
cds_response
|
||||||
|
when 'CDNSKEY'
|
||||||
|
empty_response
|
||||||
|
else
|
||||||
|
empty_response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
validator.define_singleton_method(:create_resolver) { |ip| resolver }
|
||||||
|
|
||||||
|
validator.send(:check_dnssec_sync_records)
|
||||||
|
|
||||||
|
assert_not_empty validator.results[:dnssec][:cds_records]
|
||||||
|
|
||||||
|
cds_record = validator.results[:dnssec][:cds_records].first
|
||||||
|
assert_equal 12345, cds_record[:key_tag]
|
||||||
|
assert_equal 7, cds_record[:algorithm]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Story 6: Enforcement Actions Tests
|
||||||
|
|
||||||
|
test 'removes failed nameservers after threshold' do
|
||||||
|
@nameserver1.update!(validation_counter: 3, failed_validation_reason: 'Failed validation')
|
||||||
|
|
||||||
|
# Ensure domain has enough nameservers
|
||||||
|
@domain.nameservers.create!(hostname: 'backup.ns.com')
|
||||||
|
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
# Initialize complete results structure
|
||||||
|
validator.instance_variable_set(:@results, {
|
||||||
|
nameservers: {},
|
||||||
|
dns_records: {},
|
||||||
|
dnssec: { ds_updates_needed: [] },
|
||||||
|
csync: { delegation_updates_needed: [] },
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
})
|
||||||
|
|
||||||
|
validator.define_singleton_method(:should_auto_remove_nameserver?) { true }
|
||||||
|
validator.define_singleton_method(:create_notification) { |text| nil }
|
||||||
|
|
||||||
|
initial_count = @domain.nameservers.count
|
||||||
|
validator.send(:apply_enforcement_actions)
|
||||||
|
|
||||||
|
assert_equal initial_count - 1, @domain.nameservers.count
|
||||||
|
assert_not @domain.nameservers.exists?(@nameserver1.id)
|
||||||
|
assert_includes validator.results[:warnings], "Automatically removed nameserver #{@nameserver1.hostname}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'does not remove nameserver if insufficient nameservers' do
|
||||||
|
@nameserver1.update!(validation_counter: 3, failed_validation_reason: 'Failed validation')
|
||||||
|
@nameserver2.destroy # Only one nameserver left
|
||||||
|
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
validator.instance_variable_set(:@results, {
|
||||||
|
nameservers: {},
|
||||||
|
dns_records: {},
|
||||||
|
dnssec: { ds_updates_needed: [] },
|
||||||
|
csync: { delegation_updates_needed: [] },
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
})
|
||||||
|
|
||||||
|
validator.define_singleton_method(:should_auto_remove_nameserver?) { true }
|
||||||
|
|
||||||
|
initial_count = @domain.nameservers.count
|
||||||
|
validator.send(:apply_enforcement_actions)
|
||||||
|
|
||||||
|
assert_equal initial_count, @domain.nameservers.count
|
||||||
|
assert @domain.nameservers.exists?(@nameserver1.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Integration Tests
|
||||||
|
|
||||||
|
test 'full validation workflow' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
# Mock all validation methods to avoid DNS calls
|
||||||
|
validator.define_singleton_method(:validate_nameservers) { nil }
|
||||||
|
validator.define_singleton_method(:validate_dns_records) { nil }
|
||||||
|
validator.define_singleton_method(:check_dnssec_sync_records) { nil }
|
||||||
|
validator.define_singleton_method(:check_csync_records) { nil }
|
||||||
|
validator.define_singleton_method(:apply_enforcement_actions) { nil }
|
||||||
|
|
||||||
|
results = validator.validate
|
||||||
|
|
||||||
|
assert_instance_of Hash, results
|
||||||
|
assert_includes results.keys, :nameservers
|
||||||
|
assert_includes results.keys, :dns_records
|
||||||
|
assert_includes results.keys, :dnssec
|
||||||
|
assert_includes results.keys, :csync
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'handles validation exceptions gracefully' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
validator.define_singleton_method(:validate_nameservers) do
|
||||||
|
raise StandardError.new('DNS timeout')
|
||||||
|
end
|
||||||
|
|
||||||
|
results = validator.validate
|
||||||
|
|
||||||
|
assert_includes results[:errors], 'Validation failed: DNS timeout'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper method tests
|
||||||
|
|
||||||
|
test 'helper methods work correctly' do
|
||||||
|
validator = DNSValidator.new(@domain)
|
||||||
|
|
||||||
|
# Test parse_type_bitmap
|
||||||
|
assert_equal ['NS', 'A'], validator.send(:parse_type_bitmap, ['NS', 'A'])
|
||||||
|
assert_equal ['NS'], validator.send(:parse_type_bitmap, 'NS')
|
||||||
|
assert_equal [], validator.send(:parse_type_bitmap, nil)
|
||||||
|
|
||||||
|
# Test should_auto_remove_nameserver? (default is false)
|
||||||
|
assert_not validator.send(:should_auto_remove_nameserver?)
|
||||||
|
|
||||||
|
# Test create_resolver
|
||||||
|
resolver = validator.send(:create_resolver, '192.0.2.1')
|
||||||
|
assert_instance_of Dnsruby::Resolver, resolver
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'handles domains without nameservers' do
|
||||||
|
domain_without_ns = Domain.new(
|
||||||
|
name: 'empty.test',
|
||||||
|
registrar: @domain.registrar,
|
||||||
|
registrant: @domain.registrant
|
||||||
|
)
|
||||||
|
|
||||||
|
validator = DNSValidator.new(domain_without_ns)
|
||||||
|
|
||||||
|
assert_nothing_raised do
|
||||||
|
validator.send(:validate_nameservers)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_empty validator.results[:nameservers]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Simple mock helpers using basic Ruby objects
|
||||||
|
|
||||||
|
def create_mock_resolver
|
||||||
|
resolver = Object.new
|
||||||
|
resolver.define_singleton_method(:nameserver=) { |value| }
|
||||||
|
resolver.define_singleton_method(:query_timeout=) { |value| }
|
||||||
|
resolver.define_singleton_method(:retry_times=) { |value| }
|
||||||
|
resolver.define_singleton_method(:recurse=) { |value| }
|
||||||
|
resolver.define_singleton_method(:do_caching=) { |value| }
|
||||||
|
resolver
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_mock_dns_response(records)
|
||||||
|
response = Object.new
|
||||||
|
response.define_singleton_method(:answer) { records }
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_mock_soa_record(serial = 123456)
|
||||||
|
record = Object.new
|
||||||
|
record.define_singleton_method(:type) { 'SOA' }
|
||||||
|
record.define_singleton_method(:serial) { serial }
|
||||||
|
record.define_singleton_method(:instance_variable_defined?) { |var| var == '@serial' }
|
||||||
|
record
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_mock_ns_record(hostname)
|
||||||
|
record = Object.new
|
||||||
|
record.define_singleton_method(:type) { 'NS' }
|
||||||
|
|
||||||
|
nsdname = Object.new
|
||||||
|
nsdname.define_singleton_method(:to_s) { hostname }
|
||||||
|
record.define_singleton_method(:nsdname) { nsdname }
|
||||||
|
|
||||||
|
record
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_mock_a_record(address)
|
||||||
|
record = Object.new
|
||||||
|
record.define_singleton_method(:type) { 'A' }
|
||||||
|
record.define_singleton_method(:ttl) { 3600 }
|
||||||
|
|
||||||
|
addr = Object.new
|
||||||
|
addr.define_singleton_method(:to_s) { address }
|
||||||
|
record.define_singleton_method(:address) { addr }
|
||||||
|
|
||||||
|
record
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_mock_aaaa_record(address)
|
||||||
|
record = Object.new
|
||||||
|
record.define_singleton_method(:type) { 'AAAA' }
|
||||||
|
record.define_singleton_method(:ttl) { 3600 }
|
||||||
|
|
||||||
|
addr = Object.new
|
||||||
|
addr.define_singleton_method(:to_s) { address }
|
||||||
|
record.define_singleton_method(:address) { addr }
|
||||||
|
|
||||||
|
record
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_mock_cname_record(target)
|
||||||
|
record = Object.new
|
||||||
|
record.define_singleton_method(:type) { 'CNAME' }
|
||||||
|
record.define_singleton_method(:ttl) { 3600 }
|
||||||
|
|
||||||
|
cname = Object.new
|
||||||
|
cname.define_singleton_method(:to_s) { target }
|
||||||
|
record.define_singleton_method(:cname) { cname }
|
||||||
|
|
||||||
|
record
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_mock_cds_record(key_tag, algorithm, digest_type, digest)
|
||||||
|
record = Object.new
|
||||||
|
record.define_singleton_method(:type) { 'CDS' }
|
||||||
|
record.define_singleton_method(:key_tag) { key_tag }
|
||||||
|
record.define_singleton_method(:algorithm) { algorithm }
|
||||||
|
record.define_singleton_method(:digest_type) { digest_type }
|
||||||
|
record.define_singleton_method(:digest) { digest }
|
||||||
|
record
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Add a link
Reference in a new issue