Merge pull request #2233 from internetee/2202-2-nameserver-record-validation

added nameserver validator
This commit is contained in:
Timo Võhmar 2022-01-07 15:39:30 +02:00 committed by GitHub
commit d4fa99de73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 334 additions and 6 deletions

View file

@ -82,7 +82,8 @@ module Actions
dns_servers = ENV['dnssec_resolver_ips'].to_s.split(',').map(&:strip)
Resolv::DNS.open({ nameserver: dns_servers }) do |dns|
dns.timeouts = ENV['a_and_aaaa_validation_timeout'].to_i || 1
timeouts = ENV['a_and_aaaa_validation_timeout'] || '1'
dns.timeouts = timeouts.to_i
ress = nil
case value

View file

@ -0,0 +1,124 @@
# frozen_string_literal: true
require 'resolv'
class NameserverRecordValidationJob < ApplicationJob
include Dnsruby
def perform(domain_name: nil)
if domain_name.nil?
domains = Domain.all.select { |domain| domain.created_at < Time.zone.now - NameserverValidator::VALIDATION_DOMAIN_PERIOD }
.select { |domain| domain.nameservers.exists? }
domains.each do |domain|
domain.nameservers.each do |nameserver|
next if nameserver.nameserver_failed_validation? || nameserver.validated?
result = NameserverValidator.run(domain_name: domain.name, nameserver: nameserver)
if result[:result]
add_nameserver_to_succesfully(nameserver)
true
else
parse_result(result, nameserver)
false
end
end
end
else
domain = Domain.find_by(name: domain_name)
return logger.info 'Domain not found' if domain.nil?
if domain.created_at > Time.zone.now - NameserverValidator::VALIDATION_DOMAIN_PERIOD
return logger.info "It should take #{NameserverValidator::VALIDATION_DOMAIN_PERIOD} hours after the domain was created"
end
return logger.info 'Domain not has nameservers' if domain.nameservers.empty?
domain.nameservers.each do |nameserver|
next if nameserver.nameserver_failed_validation?
result = NameserverValidator.run(domain_name: domain.name, nameserver: nameserver)
if result[:result]
add_nameserver_to_succesfully(nameserver)
true
else
parse_result(result, nameserver)
false
end
end
end
end
private
def add_nameserver_to_succesfully(nameserver)
nameserver.validation_counter = nil
nameserver.failed_validation_reason = nil
nameserver.validation_datetime = Time.zone.now
nameserver.save
end
def add_nameserver_to_failed(nameserver:, reason:)
if nameserver.validation_counter.nil?
nameserver.validation_counter = 1
else
nameserver.validation_counter = nameserver.validation_counter + 1
end
nameserver.failed_validation_reason = reason
nameserver.save
end
def parse_result(result, nameserver)
domain = Domain.find(nameserver.domain_id)
text = ""
case result[:reason]
when 'answer'
text = "No any answer comes from **#{nameserver.hostname}**. Nameserver not exist"
when 'serial'
text = "Serial number for nameserver hostname **#{nameserver.hostname}** doesn't present. SOA validation failed."
when 'not found'
text = "Seems nameserver hostname **#{nameserver.hostname}** doesn't exist"
when 'exception'
text = "Something went wrong, exception reason: **#{result[:error_info]}**"
when 'domain'
text = "#{domain} zone is not in nameserver**#{nameserver.hostname}**"
when 'glup record'
text = "Hostname #{nameserver.hostname} didn't resovle by glue record to #{domain}"
end
logger.info text
failed_log(text: text, nameserver: nameserver)
add_nameserver_to_failed(nameserver: nameserver, reason: text)
false
end
def failed_log(text:, nameserver:)
inform_to_tech_contact(text)
inform_to_registrar(text: text, nameserver: nameserver)
false
end
def inform_to_tech_contact(text)
"NEED TO DO!"
text
end
def inform_to_registrar(text:, nameserver:)
# nameserver.domain.registrar.notifications.create!(text: text)
"NEED TO DO!"
end
def logger
@logger ||= Rails.logger
end
end

View file

@ -34,6 +34,8 @@ class Nameserver < ApplicationRecord
delegate :name, to: :domain, prefix: true
scope :non_validated, -> { where(validation_datetime: nil) }
self.ignored_columns = %w[legacy_domain_id]
def epp_code_map
@ -53,6 +55,18 @@ class Nameserver < ApplicationRecord
}
end
def nameserver_failed_validation?
return false if validation_counter.nil?
validation_counter >= NameserverValidator::VALID_NAMESERVER_COUNT_THRESHOLD
end
def validated?
return false if validation_datetime.nil?
validation_datetime + NameserverValidator::VALIDATION_NAMESERVER_PERIOD > Time.zone.now
end
def to_s
hostname
end

View file

@ -0,0 +1,77 @@
module NameserverValidator
include Dnsruby
extend self
VALIDATION_NAMESERVER_PERIOD = 1.year.freeze
VALIDATION_DOMAIN_PERIOD = 8.hours.freeze
VALID_NAMESERVER_COUNT_THRESHOLD = 3
def run(domain_name:, nameserver:)
result_response = validate(domain_name: domain_name, hostname: nameserver.hostname)
unless result_response[:result] && result_response[:reason] == :exception
if result_response[:error_info].to_s.include? "Nameserver invalid!"
if nameserver.ipv4.present?
result_response = validate(domain_name: domain_name, hostname: nameserver.ipv4)
# elsif nameserver.ipv6.present?
# result_response = validate(domain_name: domain_name, hostname: nameserver.ipv6)
end
return { result: false, reason: 'glup record' } if result.answer.empty? if result_response[:result]
return result_response
end
end
result_response
end
private
def validate(domain_name:, hostname:)
resolver = setup_resolver(hostname)
result = resolver.query(domain_name, 'SOA', 'IN')
return { result: false, reason: 'answer' } if result.answer.empty?
decision = result.answer.all? do |a|
a.serial.present?
end
return { result: false, reason: 'serial' } unless decision
logger.info "Serial number - #{result.answer[0].serial.to_s} of #{hostname} - domain name: #{domain_name}"
{ result: true, reason: '' }
rescue Dnsruby::Refused => e
logger.error e.message
logger.error "failed #{hostname} validation of #{domain_name} domain name. Domain not found"
return { result: false, reason: 'domain', error_info: e }
rescue Dnsruby::NXDomain => e
logger.error e.message
logger.error "failed #{hostname} validation of #{domain_name} domain name. Domain not found"
return { result: false, reason: 'domain', error_info: e }
rescue StandardError => e
logger.error e.message
logger.error "failed #{hostname} validation of #{domain_name} domain name"
return { result: false, reason: 'exception', error_info: e }
end
def setup_resolver(hostname)
resolver = Dnsruby::Resolver.new
timeouts = ENV['nameserver_validation_timeout'] || 4
resolver.query_timeout = timeouts.to_i
resolver.retry_times = 3
resolver.recurse = 0 # Send out non-recursive queries
# disable caching otherwise SOA is cached from first nameserver queried
resolver.do_caching = false
resolver.nameserver = hostname
resolver
end
def logger
@logger ||= Rails.logger
end
end

View file

@ -233,4 +233,5 @@ registry_demo_registrar_results_url: 'http://registry.test/api/v1/accreditation_
registry_demo_registrar_api_user_url: 'http://registry.test/api/v1/accreditation_center/show_api_user'
registry_demo_accredited_users_url: 'http://registry.test/api/v1/accreditation_center/list_accreditated_api_users'
a_and_aaaa_validation_timeout: '1'
nameserver_validation_timeout: '1'

View file

@ -0,0 +1,7 @@
class AddValidationFieldsToNameserver < ActiveRecord::Migration[6.1]
def change
add_column :nameservers, :validation_datetime, :datetime
add_column :nameservers, :validation_counter, :integer
add_column :nameservers, :failed_validation_reason, :string
end
end

View file

@ -824,7 +824,8 @@ CREATE TABLE public.dnskeys (
creator_str character varying,
updator_str character varying,
legacy_domain_id integer,
updated_at timestamp without time zone
updated_at timestamp without time zone,
validation_datetime timestamp without time zone
);
@ -2177,7 +2178,10 @@ CREATE TABLE public.nameservers (
creator_str character varying,
updator_str character varying,
legacy_domain_id integer,
hostname_puny character varying
hostname_puny character varying,
validation_datetime timestamp without time zone,
validation_counter integer,
failed_validation_reason character varying
);
@ -5389,6 +5393,8 @@ INSERT INTO "schema_migrations" (version) VALUES
('20211124084308'),
('20211125181033'),
('20211125184334'),
('20211126085139');
('20211126085139'),
('20211231113934'),
('20220106123143');

View file

@ -8,7 +8,7 @@ class EppDomainUpdateBaseTest < EppTestCase
@domain = domains(:shop)
@contact = contacts(:john)
@original_registrant_change_verification =
Setting.request_confirmation_on_registrant_change_enabled
Setting.request_confirmation_on_registrant_change_enabled
ActionMailer::Base.deliveries.clear
end

View file

@ -10,7 +10,7 @@ class EppDomainUpdateRemDnsTest < EppTestCase
@dnskey = dnskeys(:one)
@dnskey.update(domain: @domain)
@original_registrant_change_verification =
Setting.request_confirmation_on_registrant_change_enabled
Setting.request_confirmation_on_registrant_change_enabled
ActionMailer::Base.deliveries.clear
end

View file

@ -0,0 +1,98 @@
require 'test_helper'
class NameserverRecordValidationJobTest < ActiveSupport::TestCase
include ActionMailer::TestHelper
setup do
@nameserver = nameservers(:shop_ns1)
@domain = domains(:shop)
@domain.update(created_at: Time.zone.now - 10.hours)
@domain.reload
end
def test_nameserver_should_validate_succesfully_and_set_validation_datetime
mock_dns_response = OpenStruct.new
answer = OpenStruct.new
answer.serial = '12343'
mock_dns_response.answer = [ answer ]
Spy.on_instance_method(NameserverValidator, :setup_resolver).and_return(Dnsruby::Resolver.new)
Spy.on_instance_method(Dnsruby::Resolver, :query).and_return(mock_dns_response)
assert_nil @nameserver.validation_datetime
assert_nil @nameserver.validation_counter
assert_nil @nameserver.failed_validation_reason
NameserverRecordValidationJob.perform_now(domain_name: @domain.name)
@nameserver.reload
assert_not_nil @nameserver.validation_datetime
assert_nil @nameserver.validation_counter
assert_nil @nameserver.failed_validation_reason
end
def test_should_return_failed_validation_with_answer_reason
mock_dns_response = OpenStruct.new
mock_dns_response.answer = [ ]
Spy.on_instance_method(NameserverValidator, :setup_resolver).and_return(Dnsruby::Resolver.new)
Spy.on_instance_method(Dnsruby::Resolver, :query).and_return(mock_dns_response)
assert_nil @nameserver.validation_datetime
assert_nil @nameserver.validation_counter
assert_nil @nameserver.failed_validation_reason
NameserverRecordValidationJob.perform_now(domain_name: @domain.name)
@nameserver.reload
assert_nil @nameserver.validation_datetime
assert @nameserver.validation_counter, 1
assert @nameserver.failed_validation_reason.include? "No any answer comes from **#{@nameserver.hostname}**"
end
def test_should_return_failed_validation_with_serial_reason
mock_dns_response = OpenStruct.new
answer = OpenStruct.new
answer.some_field = '12343'
mock_dns_response.answer = [ answer ]
Spy.on_instance_method(NameserverValidator, :setup_resolver).and_return(Dnsruby::Resolver.new)
Spy.on_instance_method(Dnsruby::Resolver, :query).and_return(mock_dns_response)
assert_nil @nameserver.validation_datetime
assert_nil @nameserver.validation_counter
assert_nil @nameserver.failed_validation_reason
NameserverRecordValidationJob.perform_now(domain_name: @domain.name)
@nameserver.reload
assert_nil @nameserver.validation_datetime
assert @nameserver.validation_counter, 1
assert @nameserver.failed_validation_reason.include? "Serial number for nameserver hostname **#{@nameserver.hostname}** doesn't present. SOA validation failed."
end
def test_after_third_invalid_times_nameserver_should_be_invalid
mock_dns_response = OpenStruct.new
answer = OpenStruct.new
answer.some_field = '12343'
mock_dns_response.answer = [ answer ]
Spy.on_instance_method(NameserverValidator, :setup_resolver).and_return(Dnsruby::Resolver.new)
Spy.on_instance_method(Dnsruby::Resolver, :query).and_return(mock_dns_response)
assert_nil @nameserver.validation_datetime
assert_nil @nameserver.validation_counter
assert_nil @nameserver.failed_validation_reason
3.times do
NameserverRecordValidationJob.perform_now(domain_name: @domain.name)
end
@nameserver.reload
assert_nil @nameserver.validation_datetime
assert @nameserver.validation_counter, 1
assert @nameserver.failed_validation_reason.include? "Serial number for nameserver hostname **#{@nameserver.hostname}** doesn't present. SOA validation failed."
assert @nameserver.nameserver_failed_validation?
end
end