diff --git a/app/jobs/nameserver_record_validation_job.rb b/app/jobs/nameserver_record_validation_job.rb index a1787e9d7..7dd2211a4 100644 --- a/app/jobs/nameserver_record_validation_job.rb +++ b/app/jobs/nameserver_record_validation_job.rb @@ -12,7 +12,7 @@ class NameserverRecordValidationJob < ApplicationJob domains.each do |domain| domain.nameservers.each do |nameserver| - next if nameserver.nameserver_failed_validation? || nameserver.validated? + next if nameserver.failed_validation? || nameserver.validated? result = NameserverValidator.run(domain_name: domain.name, nameserver: nameserver) @@ -38,7 +38,7 @@ class NameserverRecordValidationJob < ApplicationJob return logger.info 'Domain not has nameservers' if domain.nameservers.empty? domain.nameservers.each do |nameserver| - next if nameserver.nameserver_failed_validation? + next if nameserver.failed_validation? result = NameserverValidator.run(domain_name: domain.name, nameserver: nameserver) @@ -73,6 +73,8 @@ class NameserverRecordValidationJob < ApplicationJob nameserver.failed_validation_reason = reason nameserver.save + + failed_log(text: reason, nameserver: nameserver, domain: nameserver.domain) if nameserver.failed_validation? end def parse_result(result, nameserver) @@ -95,27 +97,26 @@ class NameserverRecordValidationJob < ApplicationJob 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) + def failed_log(text:, nameserver:, domain:) + inform_to_tech_contact(domain: domain, nameserver: nameserver, text: text) inform_to_registrar(text: text, nameserver: nameserver) false end - def inform_to_tech_contact(text) - "NEED TO DO!" - text + def inform_to_tech_contact(domain:, nameserver:, text: nil) + # ContactNotification.notify_tech_contact(domain: domain, nameserver: nameserver, reason: 'nameserver') end - def inform_to_registrar(text:, nameserver:) - # nameserver.domain.registrar.notifications.create!(text: text) - "NEED TO DO!" + def inform_to_registrar(nameserver:, text: nil) + text = "Host record #{nameserver.hostname} of a domain #{nameserver.domain} is invalid. + Please fix or contact the registrant. Problem with nameserver #{nameserver} - #{nameserver.failed_validation_reason}" + logger.info text + # ContactNotification.notify_registrar(domain: nameserver.domain, text: text) end def logger diff --git a/app/jobs/validate_dnssec_job.rb b/app/jobs/validate_dnssec_job.rb new file mode 100644 index 000000000..623513768 --- /dev/null +++ b/app/jobs/validate_dnssec_job.rb @@ -0,0 +1,145 @@ +class ValidateDnssecJob < ApplicationJob + discard_on StandardError + + def perform(domain_name: nil) + unless domain_name.nil? + domain = Domain.find_by(name: domain_name) + + return logger.info "This domain not contain any dnskeys" if domain.dnskeys.empty? + + return logger.info "No domain found" if domain.nil? + + return logger.info "No related nameservers for this domain" if domain.nameservers.empty? + + iterate_nameservers(domain) + else + domain_list = Domain.all.reject { |d| d.dnskeys.empty? } + + domain_list.each do |d| + if d.nameservers.empty? + logger.info "#{d.name} has no nameserver" + + next + end + + iterate_nameservers(d) + end + end + rescue StandardError => e + logger.error e.message + raise e + end + + private + + def iterate_nameservers(domain) + domain.nameservers.each do |n| + next unless n.validated? + + validate(hostname: n.hostname, domain: domain) + + notify_contacts(domain) + logger.info "----------------------------" + end + end + + def notify_contacts(domain) + flag = domain.dnskeys.any? { |k| k.validation_datetime.present? } + + return if flag + + text = "DNSKEY record of a domain #{domain.name} is invalid. Please fix or contact the registrant." + logger.info text + # ContactNotification.notify_registrar(domain: domain, text: text) + # ContactNotification.notify_tech_contact(domain: domain, reason: 'dnssec') + end + + def validate(hostname:, domain:, type: 'DNSKEY', klass: 'IN') + resolver = prepare_validator(hostname) + answer = resolver.query(domain.name, type, klass) + + return logger.info "no any data for #{domain.name} | hostname - #{hostname}" if answer.nil? + + logger.info "-----------" + logger.info "data for domain name - #{domain.name} | hostname - #{hostname}" + logger.info "-----------" + + response_container = parse_response(answer) + + compare_dnssec_data(response_container: response_container, domain: domain) + rescue Exception => e + logger.error "#{e.message} - domain name: #{domain.name} - hostname: #{hostname}" + nil + end + + def compare_dnssec_data(response_container:, domain:) + domain.dnskeys.each do |key| + next unless key.flags.to_s == '257' + next if key.validation_datetime.present? + + flag = make_magic(response_container: response_container, dnskey: key) + text = "#{key.flags} - #{key.protocol} - #{key.alg} - #{key.public_key}" + + if flag + key.validation_datetime = Time.zone.now + key.save + + logger.info text + " ------->> succesfully!" + else + logger.info text + " ------->> not found in zone!" + end + end + end + + def make_magic(response_container:, dnskey:) + response_container.any? do |r| + r[:flags].to_s == dnskey.flags.to_s && + r[:protocol].to_s == dnskey.protocol.to_s && + r[:alg].to_s == dnskey.alg.to_s && + r[:public_key] == dnskey.public_key + end + end + + def parse_response(answer) + response_container = [] + + answer.each_answer do |a| + + a_string = a.to_s + a_string = a_string.gsub /\t/, ' ' + a_string = a_string.split(' ') + + next unless a_string[4] == '257' + + protocol = a.protocol + alg = a.algorithm.code + + response_container << { + flags: a_string[4], + protocol: protocol, + alg: alg, + public_key: a_string[8] + } + end + + response_container + end + + def prepare_validator(nameserver) + inner_resolver = Dnsruby::Resolver.new + timeouts = ENV['nameserver_validation_timeout'] || 4 + inner_resolver.do_validation = true + inner_resolver.dnssec = true + inner_resolver.nameserver = nameserver + inner_resolver.packet_timeout = timeouts.to_i + inner_resolver.query_timeout = timeouts.to_i + resolver = Dnsruby::Recursor.new(inner_resolver) + resolver.dnssec = true + + resolver + end + + def logger + @logger ||= Rails.logger + end +end diff --git a/app/mailers/contact_inform_mailer.rb b/app/mailers/contact_inform_mailer.rb new file mode 100644 index 000000000..bf5037cbf --- /dev/null +++ b/app/mailers/contact_inform_mailer.rb @@ -0,0 +1,27 @@ +class ContactInformMailer < ApplicationMailer + helper_method :address_processing + + def notify_dnssec(contact:, domain:) + @contact = contact + @domain = domain + + subject = "Domeeni #{@domain.name} DNSSEC kirjed ei ole korrektsed / The DNSKEY records of the domain #{@domain.name} are invalid" + + mail(to: contact.email, subject: subject) + end + + def notify_nameserver(contact:, domain:, nameserver:) + @contact = contact + @domain = domain + @nameserver = nameserver + + subject = "Domeeni #{@domain.name} nimeserveri kirjed ei ole korrektsed / The host records of the domain #{@domain.name} are invalid" + mail(to: contact.email, subject: subject) + end + + private + + def address_processing + Contact.address_processing? + end +end diff --git a/app/models/nameserver.rb b/app/models/nameserver.rb index 20d4656c6..ababd84cf 100644 --- a/app/models/nameserver.rb +++ b/app/models/nameserver.rb @@ -55,7 +55,7 @@ class Nameserver < ApplicationRecord } end - def nameserver_failed_validation? + def failed_validation? return false if validation_counter.nil? validation_counter >= NameserverValidator::VALID_NAMESERVER_COUNT_THRESHOLD diff --git a/app/services/contact_notification.rb b/app/services/contact_notification.rb new file mode 100644 index 000000000..e9b00c575 --- /dev/null +++ b/app/services/contact_notification.rb @@ -0,0 +1,24 @@ +module ContactNotification + extend self + + def notify_registrar(domain:, text:) + domain.registrar.notifications.create(text: text) + end + + def notify_tech_contact(domain:, nameserver: nil, reason: nil) + case reason + when 'dnssec' + domain.tech_contacts.each do |tech| + contact = Contact.find(tech.id) + + ContactInformMailer.notify_dnssec(contact: contact, domain: domain).deliver_now + end + when 'nameserver' + domain.tech_contacts.each do |tech| + contact = Contact.find(tech.id) + + ContactInformMailer.notify_nameserver(contact: contact, domain: domain, nameserver: nameserver).deliver_now + end + end + end +end diff --git a/app/services/nameserver_validator.rb b/app/services/nameserver_validator.rb index 410448472..c45003cd3 100644 --- a/app/services/nameserver_validator.rb +++ b/app/services/nameserver_validator.rb @@ -18,7 +18,7 @@ module NameserverValidator # 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: false, reason: 'glup record' } unless result_response[:result] return result_response end diff --git a/app/views/mailers/contact_inform_mailer/notify_dnssec.html.erb b/app/views/mailers/contact_inform_mailer/notify_dnssec.html.erb new file mode 100644 index 000000000..4b506b9e8 --- /dev/null +++ b/app/views/mailers/contact_inform_mailer/notify_dnssec.html.erb @@ -0,0 +1,40 @@ +

Lugupeetud domeeni <%= @domain.name %> tehniline kontakt,

+ +

+Eesti Interneti Sihtasutusele (EIS) juhib tähelepanu, et domeeni <%= @domain.name %> DNSKEY kirjed on puudulikud ning domeeniga seotud teenuse ei pruugi korrektselt toimida. +

+

+Andmete parandamiseks vaadake palun üle domeeni nimeserverite seaditused või pöörduge palun oma registripidaja <%= @domain.registrar.name %> või nimeserveri teenuse pakkuja poole.

+ +

+Lisaküsimuste korral võtke palun ühendust oma registripidajaga: +

+<%= @domain.registrar.name %>
+Email: <%= @domain.registrar.email %>
+Telefon:<%= @domain.registrar.phone %>
+Veebileht:<%= @domain.registrar.website %>
+ +Lugupidamisega
+Eesti Interneti Sihtasutus
+
+--- +
+ +

Dear technical contact of <%= @domain.name %> domain,

+ +

+Estonian Internet Foundation points out that the DNSKEY record(s) for the domain <%= @domain.name %> are invalid and the service related to the domain may not work correctly. +

+

+Please check the DNSKEY records of the domain or contact your registrar <%= @domain.registrar.name%> or your name server service provider to correct this information. +

+

+Should you have additional questions, please contact your registrar: +

+<%= @domain.registrar.name %>
+Email: <%= @domain.registrar.email %>
+Phone: <%= @domain.registrar.phone %>
+Website: <%= @domain.registrar.website %>
+ +Best Regards,
+Estonian Internet Foundation diff --git a/app/views/mailers/contact_inform_mailer/notify_dnssec.text.erb b/app/views/mailers/contact_inform_mailer/notify_dnssec.text.erb new file mode 100644 index 000000000..09b243854 --- /dev/null +++ b/app/views/mailers/contact_inform_mailer/notify_dnssec.text.erb @@ -0,0 +1,29 @@ +Lugupeetud domeeni <%= @domain.name %> tehniline kontakt, + +Eesti Interneti Sihtasutusele (EIS) juhib tähelepanu, et domeeni <%= @domain.name %> DNSKEY kirjed on puudulikud ning domeeniga seotud teenuse ei pruugi korrektselt toimida. +Andmete parandamiseks vaadake palun üle domeeni nimeserverite seaditused või pöörduge palun oma registripidaja <%= @domain.registrar.name %> või nimeserveri teenuse pakkuja poole. + +Lisaküsimuste korral võtke palun ühendust oma registripidajaga: +<%= @domain.registrar.name%> +Email: <%= @domain.registrar.email %> +Telefon: <%= @domain.registrar.phone %> +Veebileht: <%= @domain.registrar.website %> + +Lugupidamisega +Eesti Interneti Sihtasutus + +--- + +Dear technical contact of <%= @domain.name %> domain, + +Estonian Internet Foundation points out that the DNSKEY record(s) for the domain <%= @domain.name %> are invalid and the service related to the domain may not work correctly. +Please check the DNSKEY records of the domain or contact your registrar <%= @domain.registrar.name%> or your name server service provider to correct this information. + +Should you have additional questions, please contact your registrar: +<%= @domain.registrar.name%> +Email: <%= @domain.registrar.email %> +Phone: <%= @domain.registrar.phone %> +Website: <%= @domain.registrar.website %> + +Best Regards, +Estonian Internet Foundation diff --git a/app/views/mailers/contact_inform_mailer/notify_nameserver.html.erb b/app/views/mailers/contact_inform_mailer/notify_nameserver.html.erb new file mode 100644 index 000000000..c9df4f6f0 --- /dev/null +++ b/app/views/mailers/contact_inform_mailer/notify_nameserver.html.erb @@ -0,0 +1,47 @@ +

Lugupeetud domeeni <%= @domain.name %> tehniline kontakt

+ +

+Eesti Interneti Sihtasutusele (EIS) juhib tähelepanu, et domeeni <%= @domain.name %> nimeserverite seaded on puudulikud ning domeeniga seotud teenuse ei pruugi korrektselt toimida.

+

+Andmete parandamiseks vaadake palun üle domeeni nimeserverite seaditused või pöörduge palun oma registripidaja <%= @domain.registrar.name%> või nimeserveri teenuse pakkuja poole. +

+ +Viga nimesereriga <%= @nameserver %> - <%= @nameserver.failed_validation_reason %> + +

+Lisaküsimuste korral võtke palun ühendust oma registripidajaga: +

+ +<%= @domain.registrar.name %>
+Email: <%= @domain.registrar.email %>
+Telefon: <%= @domain.registrar.phone %>
+Veebileht: <%= @domain.registrar.website %>
+ +Lugupidamisega
+Eesti Interneti Sihtasutus
+
+--- +
+ +

Dear technical contact of <%= @domain.name %> domain,

+ +

+ Estonian Internet Foundation points out that the settings for the name servers of the domain <%= @domain.name %> are incomplete and the service related to the domain may not work correctly. +

+

+Please check the name server settings of the domain or contact your registrar <%= @domain.registrar.name%> or your name server service provider to correct this information. +

+ +Problem with nameserver <%= @nameserver %> - <%= @nameserver.failed_validation_reason %>" + +

+Should you have additional questions, please contact your registrar: +

+ +<%= @domain.registrar.name%>
+Email: <%= @domain.registrar.email %>
+Phone: <%= @domain.registrar.phone %>
+Website: <%= @domain.registrar.website %>
+ +Best Regards,
+Estonian Internet Foundation
diff --git a/app/views/mailers/contact_inform_mailer/notify_nameserver.text.erb b/app/views/mailers/contact_inform_mailer/notify_nameserver.text.erb new file mode 100644 index 000000000..3a6734402 --- /dev/null +++ b/app/views/mailers/contact_inform_mailer/notify_nameserver.text.erb @@ -0,0 +1,29 @@ +Lugupeetud domeeni <%= @domain.name %> tehniline kontakt + +Eesti Interneti Sihtasutusele (EIS) juhib tähelepanu, et domeeni <%= @domain.name %> nimeserverite seaded on puudulikud ning domeeniga seotud teenuse ei pruugi korrektselt toimida. +Andmete parandamiseks vaadake palun üle domeeni nimeserverite seaditused või pöörduge palun oma registripidaja <%= @domain.registrar.name%> või nimeserveri teenuse pakkuja poole. + +Lisaküsimuste korral võtke palun ühendust oma registripidajaga: +<%= @domain.registrar.name%> +Email: <%= @domain.registrar.email %> +Telefon: <%= @domain.registrar.phone %> +Veebileht: <%= @domain.registrar.website %> + +Lugupidamisega +Eesti Interneti Sihtasutus + +--- + +Dear technical contact of <%= @domain.name %> domain, + +Estonian Internet Foundation points out that the settings for the name servers of the domain <%= @domain.name %> are incomplete and the service related to the domain may not work correctly. +Please check the name server settings of the domain or contact your registrar <%= @domain.registrar.name%> or your name server service provider to correct this information. + +Should you have additional questions, please contact your registrar: +<%= @domain.registrar.name%> +Email: <%= @domain.registrar.email %> +Phone: <%= @domain.registrar.phone %> +Website: <%= @domain.registrar.website %> + +Best Regards, +Estonian Internet Foundation diff --git a/config/locales/en.yml b/config/locales/en.yml index 31947350d..2f7e8a0aa 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -666,6 +666,7 @@ en: user_not_authenticated: "user not authenticated" actions: Actions contact_has_been_archived: 'Contact with code %{contact_code} has been archieved because it has been orphaned for longer than %{orphan_months} months.' + dns_policy_violation: "Data management policy violation: DNSKEY does not match or not found in the authoritative nameservers" number: currency: diff --git a/db/migrate/20211231113934_add_validation_datetime_to_dnskey.rb b/db/migrate/20211231113934_add_validation_datetime_to_dnskey.rb new file mode 100644 index 000000000..a0ad82332 --- /dev/null +++ b/db/migrate/20211231113934_add_validation_datetime_to_dnskey.rb @@ -0,0 +1,5 @@ +class AddValidationDatetimeToDnskey < ActiveRecord::Migration[6.1] + def change + add_column :dnskeys, :validation_datetime, :datetime + end +end diff --git a/db/structure.sql b/db/structure.sql index 9afc1742f..4403facfb 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -5398,3 +5398,4 @@ INSERT INTO "schema_migrations" (version) VALUES ('20220106123143'); + diff --git a/test/integration/api/registrant/registrant_api_verifications_test.rb b/test/integration/api/registrant/registrant_api_verifications_test.rb index 821d0dee0..612006eb9 100644 --- a/test/integration/api/registrant/registrant_api_verifications_test.rb +++ b/test/integration/api/registrant/registrant_api_verifications_test.rb @@ -15,7 +15,6 @@ class RegistrantApiVerificationsTest < ApplicationIntegrationTest @domain.update!(statuses: [DomainStatus::PENDING_UPDATE], registrant_verification_asked_at: Time.zone.now - 1.day, registrant_verification_token: @token) - end def test_fetches_registrant_change_request diff --git a/test/jobs/nameserver_record_validation_job_test.rb b/test/jobs/nameserver_record_validation_job_test.rb index a8d625ca1..1f39c6d33 100644 --- a/test/jobs/nameserver_record_validation_job_test.rb +++ b/test/jobs/nameserver_record_validation_job_test.rb @@ -93,6 +93,6 @@ class NameserverRecordValidationJobTest < ActiveSupport::TestCase 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? + assert @nameserver.failed_validation? end end diff --git a/test/jobs/validate_dnssec_job_test.rb b/test/jobs/validate_dnssec_job_test.rb new file mode 100644 index 000000000..cd207e03a --- /dev/null +++ b/test/jobs/validate_dnssec_job_test.rb @@ -0,0 +1,71 @@ +require 'test_helper' + +class ZoneAnswer + def initialize(valid_response: true) + @answer = [] + + algorithm = OpenStruct.new(code: 13) + + answer = OpenStruct.new + answer.data = "some0 some1 some2 257 some4 some5 some6 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ== some" + answer.flags = 257 + answer.protocol = 3 + answer.protocol = 7 unless valid_response + answer.algorithm = algorithm + + @answer << answer + end + + def each_answer + @answer.each {|rec| + yield rec + } + end +end + +class ValidateDnssecJobTest < ActiveJob::TestCase + setup do + @domain = domains(:shop) + @dnskey = dnskeys(:one) + end + + def test_job_should_set_validation_datetime_if_validation_is_valid + @domain.nameservers.each do |n| + n.update(validation_datetime: Time.zone.now - 1.minute) + end + @domain.dnskeys << @dnskey + @domain.save + + @domain.reload + + mock_zone_data = ZoneAnswer.new + + Spy.on_instance_method(ValidateDnssecJob, :prepare_validator).and_return(Dnsruby::Resolver.new) + Spy.on_instance_method(Dnsruby::Resolver, :query).and_return(mock_zone_data) + + ValidateDnssecJob.perform_now(domain_name: @domain.name) + + @domain.reload + assert_not_nil @domain.dnskeys.first.validation_datetime + end + + def test_job_should_not_set_validation_datetime_if_validation_is_invalid + @domain.nameservers.each do |n| + n.update(validation_datetime: Time.zone.now - 1.minute) + end + @domain.dnskeys << @dnskey + @domain.save + + @domain.reload + + mock_zone_data = ZoneAnswer.new(valid_response: false) + + Spy.on_instance_method(ValidateDnssecJob, :prepare_validator).and_return(Dnsruby::Resolver.new) + Spy.on_instance_method(Dnsruby::Resolver, :query).and_return(mock_zone_data) + + ValidateDnssecJob.perform_now(domain_name: @domain.name) + + @domain.reload + assert_nil @domain.dnskeys.first.validation_datetime + end +end diff --git a/test/services/contact_notification_test.rb b/test/services/contact_notification_test.rb new file mode 100644 index 000000000..ec3d975e6 --- /dev/null +++ b/test/services/contact_notification_test.rb @@ -0,0 +1,29 @@ +require 'test_helper' + +class ContactNotificationTest < ActionMailer::TestCase + + setup do + @domain = domains(:shop) + @nameserver = nameservers(:shop_ns1) + @text = 'text' + end + + def test_notify_registrar + assert_difference -> { @domain.registrar.notifications.count } do + ContactNotification.notify_registrar(domain: @domain, text: @text) + end + end + + def test_notify_tech_contacts_that_nameserver_is_broken + ContactNotification.notify_tech_contact(domain: @domain, reason: 'nameserver', nameserver: @nameserver) + assert_equal @domain.tech_contacts.count, 2 + assert_emails 2 + end + + def test_notify_tech_contacts_that_dnssec_is_broken + ContactNotification.notify_tech_contact(domain: @domain, reason: 'dnssec') + assert_equal @domain.tech_contacts.count, 2 + assert_emails 2 + end +end +