diff --git a/app/interactions/actions/email_check.rb b/app/interactions/actions/email_check.rb index b358db08d..4b026ec2e 100644 --- a/app/interactions/actions/email_check.rb +++ b/app/interactions/actions/email_check.rb @@ -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 diff --git a/app/jobs/nameserver_record_validation_job.rb b/app/jobs/nameserver_record_validation_job.rb new file mode 100644 index 000000000..a1787e9d7 --- /dev/null +++ b/app/jobs/nameserver_record_validation_job.rb @@ -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 diff --git a/app/models/nameserver.rb b/app/models/nameserver.rb index 75255eb89..20d4656c6 100644 --- a/app/models/nameserver.rb +++ b/app/models/nameserver.rb @@ -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 diff --git a/app/services/nameserver_validator.rb b/app/services/nameserver_validator.rb new file mode 100644 index 000000000..410448472 --- /dev/null +++ b/app/services/nameserver_validator.rb @@ -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 diff --git a/config/application.yml.sample b/config/application.yml.sample index 5fdb72055..02b230ff4 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -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' diff --git a/db/migrate/20220106123143_add_validation_fields_to_nameserver.rb b/db/migrate/20220106123143_add_validation_fields_to_nameserver.rb new file mode 100644 index 000000000..82d9ec794 --- /dev/null +++ b/db/migrate/20220106123143_add_validation_fields_to_nameserver.rb @@ -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 diff --git a/db/structure.sql b/db/structure.sql index 4d539a7a6..9afc1742f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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'); diff --git a/test/integration/epp/domain/update/base_test.rb b/test/integration/epp/domain/update/base_test.rb index 580d5c603..d021b496d 100644 --- a/test/integration/epp/domain/update/base_test.rb +++ b/test/integration/epp/domain/update/base_test.rb @@ -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 diff --git a/test/integration/epp/domain/update/rem_dns_test.rb b/test/integration/epp/domain/update/rem_dns_test.rb index fd46338e7..6e079b126 100644 --- a/test/integration/epp/domain/update/rem_dns_test.rb +++ b/test/integration/epp/domain/update/rem_dns_test.rb @@ -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 diff --git a/test/jobs/nameserver_record_validation_job_test.rb b/test/jobs/nameserver_record_validation_job_test.rb new file mode 100644 index 000000000..a8d625ca1 --- /dev/null +++ b/test/jobs/nameserver_record_validation_job_test.rb @@ -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