Page MenuHomeGRNET

No OneTemporary

File Metadata

Created
Sun, May 17, 10:58 AM
diff --git a/app/models/record.rb b/app/models/record.rb
index 6109ccb..46af7ad 100644
--- a/app/models/record.rb
+++ b/app/models/record.rb
@@ -1,203 +1,292 @@
require 'ipaddr'
require_dependency 'drop_privileges_validator'
class Record < ActiveRecord::Base
belongs_to :domain
# Powerdns inserts empty records on slave zones,
# we want to hide them
#
# http://mailman.powerdns.com/pipermail/pdns-users/2013-December/010389.html
default_scope { where.not(type: nil) }
# List all supported DNS RR types.
def self.record_types
[
'A', 'AAAA', 'CNAME',
'MX',
'TXT', 'SPF', 'SRV', 'SSHFP',
'SOA', 'NS',
'PTR', 'NAPTR'
]
end
# List types usually used in forward zones.
def self.forward_records
record_types - ['SOA', 'PTR']
end
# List types usually used in reverse zones.
def self.reverse_records
['PTR', 'CNAME', 'TXT', 'NS', 'NAPTR']
end
# List types usually used in enum zones.
def self.enum_records
['NAPTR', 'CNAME', 'TXT', 'NS']
end
# List types that can be touched by a simple user.
def self.allowed_record_types
record_types - WebDNS.settings[:prohibit_records_types]
end
validates :name, presence: true
validates :type, inclusion: { in: record_types }
# http://mark.lindsey.name/2009/03/never-use-dns-ttl-of-zero-0.html
validates_numericality_of :ttl,
allow_nil: true, # Default pdns TTL
only_integer: true,
greater_than: 0,
less_than_or_equal_to: 2_147_483_647
# Don't allow the following actions on drop privileges mode
validate :no_touching_for_slave_zones, if: -> { domain.slave? }
validates_drop_privileges :type,
message: 'You cannot touch that record!',
unless: -> { Record.allowed_record_types.include?(type) }
validates_drop_privileges :name,
message: 'You cannot touch top level NS records!',
if: -> { type == 'NS' && domain_record? }
before_validation :guess_reverse_name
before_validation :set_name
after_save :update_zone_serial
after_destroy :update_zone_serial
+ before_create :generate_classless_delegations, unless: -> { domain.slave? }
+ before_destroy :delete_classless_delegations, unless: -> { domain.slave? }
+
# Smart sort a list of records.
#
# Order by:
# * Top level records
# * Record name
# * SOA
# * NS
# * Friendly type
# * Priority
# * Content
#
# records - The list of records to order.
#
# Returns the list sorted.
def self.smart_order(records)
records.sort_by { |r|
[
r.domain_record? ? 0 : 1, # Zone records
+ r.classless_delegated? ? 1 : 0,
r.name,
r.type == 'SOA' ? 0 : 1,
r.type == 'NS' ? 0 : 1,
record_types.index(r.type), # Friendly type
r.prio,
r.content
]
}
end
# Get the a short name for the record (without the zone suffix).
#
# Returns a string.
def short
return '' if name == domain.name
return '' if name.blank?
File.basename(name, ".#{domain.name}")
end
# Returns true if this is a zone record.
def domain_record?
name.blank? || name == domain.name
end
# Find out if the record is edittable.
#
# by - Editable by :user or :admin.
#
# Returns true if the record is editable.
def editable?(by = :user)
return false if domain.slave?
+ return false if classless_delegated?
case by
when :user
return false unless Record.allowed_record_types.include?(type)
return false if type == 'NS' && domain_record?
end
true
end
# Find out this record type supports priorities.
#
# We set this to false by default, record types that support priorities.
# shoule override this.
#
# Returns true this record type support priorities.
def supports_prio?
false
end
# Make sure rails generates record specific urls for all record types.
#
# Overrides default rails STI behavior.
def self.model_name
return super if self == Record
Record.model_name
end
# Generate the usual admin friendly DNS record line.
#
# Returns a string.
def to_dns
[name, ttl, 'IN', type, supports_prio? ? prio : nil, content].compact.join(' ')
end
# Generate a shorter version of the DNS record line.
#
# Returns a string.
def to_short_dns
[name, 'IN', type].join(' ')
end
+ def classless_delegated?
+ return false if not type == 'CNAME'
+ return false if not domain.name.end_with?('.in-addr.arpa')
+
+ network, mask = parse_delegation(content)
+ return false if network.nil?
+
+ octet = name.split('.').first.to_i
+ return true if octet >= network
+ return true if octet <= network + 2 ^ (32 - mask) - 1 # max
+
+ false
+ end
+
+ def classless_delegation?
+ return true if classless_delegation
+
+ false
+ end
+
private
# Validations
def no_touching_for_slave_zones
# Allow automatic SOA creation for slave zones
# powerdns needs a valid serial to compare it with master
return if type == 'SOA' && validation_context == :create
errors.add(:type, 'This is a slave zone!')
end
# Hooks
def guess_reverse_name
return if not type == 'PTR'
return if not domain.reverse?
return if name.blank?
reverse = IPAddr.new(name).reverse
self.name = reverse if reverse.end_with?(domain.name)
rescue IPAddr::InvalidAddressError # rubycop:disable HandleExceptions
end
# Powerdns expects full domain names
def set_name
self.name = domain.name if name.blank?
self.name = "#{name}.#{domain.name}" if not name.end_with?(domain.name)
end
def remove_terminating_dot
self.content = content.gsub(/\.+\Z/, '')
end
def update_zone_serial
# SOA records handle serial themselves
return true if type == 'SOA'
return true if !domain
domain.soa.bump_serial!
end
+ def classless_delegation
+ return if not type == 'NS'
+ return if not domain.name.end_with?('.in-addr.arpa')
+
+ network, mask = parse_delegation(name)
+ return if network.nil?
+
+ range = IPAddr.new("0.0.0.#{network}/#{mask}").to_range
+ return if !range.first.to_s.end_with?(".#{network}")
+
+ range.map { |ip|
+ octet = ip.to_s.split('.').last
+ "#{octet}.#{domain.name}"
+ }
+ end
+
+ def parse_delegation(value)
+ first, _rest = value.split('.', 2)
+ return if !first['/']
+
+ network, mask = first.split('/', 2).map { |i| Integer(i).abs }
+ return if [network, mask].join('/') != first
+ return if mask <= 24
+ return if mask > 31
+ return if network > 255
+
+ [network, mask]
+ rescue ArgumentError # Not an integer
+ end
+
+ def delete_classless_delegations
+ rnames = classless_delegation
+ return unless rnames
+
+ # Check if we have another NS for the same delegation
+ return if domain.records.where(type: 'NS', name: name)
+ .where.not(id: id).exists?
+
+ # Delete all CNAMEs
+ domain.records.where(name: rnames,
+ type: 'CNAME',
+ content: name).delete_all
+ end
+
+ def generate_classless_delegations
+ rnames = classless_delegation
+ return unless rnames
+
+ # Make sure no record exists for a delegated domain
+ if domain.records.where(name: rnames)
+ .where.not(content: name).exists?
+
+ errors.add(:name, 'Records already exist for the delegated octets!')
+ return false
+ end
+
+ rnames.each { |rname|
+ CNAME.find_or_create_by!(
+ domain: domain,
+ name: rname,
+ content: name
+ )
+ }
+ end
end
diff --git a/app/views/domains/show.html.erb b/app/views/domains/show.html.erb
index d3d4f43..fad58c2 100644
--- a/app/views/domains/show.html.erb
+++ b/app/views/domains/show.html.erb
@@ -1,50 +1,54 @@
<% content_for :more_breadcrumbs do %>
<li>
<%= link_to_edit edit_domain_path(@domain) %>
</li>
<% end if admin? %>
<table class="table table-striped table-hover">
<thead>
<tr>
<th colspan="6">Records</th>
<th colspan="3"><%= 'Controls' if !@domain.slave? %></th>
</tr>
</thead>
<tbody>
<% Record.smart_order(@domain.records).each do |record| %>
<tr class="<%= record.disabled? ? 'warning' : '' %>">
<td><%= record.name %></td>
<td><%= record.ttl %></td>
<td>IN</td>
<td><%= record.type %></td>
<td><%= record.supports_prio? ? record.prio : '' %></td>
<td><%= record.content %></td>
- <% if can_edit?(record) %>
+ <% if record.classless_delegation? %>
+ <td/>
+ <td/>
+ <td><%= link_to_destroy [@domain, record], method: :delete, data: { confirm: 'Are you sure?' } %></td>
+ <% elsif can_edit?(record) %>
<td>
<% if record.disabled? %>
<%= link_to_enable enable_domain_record_path(@domain, record), method: :put %>
<% else %>
<%= link_to_disable disable_domain_record_path(@domain, record), method: :put %>
<% end %>
</td>
<td><%= link_to_edit edit_domain_record_path(@domain, record) if can_edit?(record) %></td>
<td><%= link_to_destroy [@domain, record], method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% else %>
<td/>
<td/>
<td/>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<p>
<%= link_to 'Add Record', '#new_record', class: 'btn btn-primary', onclick: '$("#new_record_wrapper").toggleClass("hidden");' %>
</p>
<div class="jumbotron hidden" id="new_record_wrapper">
<%= render 'records/form' %>
</div>
diff --git a/test/factories/ns.rb b/test/factories/ns.rb
index 3d30273..0f3e81e 100644
--- a/test/factories/ns.rb
+++ b/test/factories/ns.rb
@@ -1,7 +1,13 @@
FactoryGirl.define do
factory :ns, class: NS do
domain
name ''
content { generate(:domain) }
end
+
+ factory :cd_ns, class: NS do
+ association :domain, factory: :v4_arpa_domain
+ name '192/29'
+ content 'ns1.example.com'
+ end
end
diff --git a/test/models/ns_test.rb b/test/models/ns_test.rb
index dab9721..36ee587 100644
--- a/test/models/ns_test.rb
+++ b/test/models/ns_test.rb
@@ -1,37 +1,115 @@
require 'test_helper'
class NSTest < ActiveSupport::TestCase
setup do
@record = build(:ns)
end
test 'save' do
@record.save
assert_empty @record.errors
end
test 'chop terminating dot' do
@record.content = 'with-dot.example.com.'
@record.save!
@record.reload
assert_equal 'with-dot.example.com', @record.content
end
test 'drop privileges on zone NS records' do
@record.drop_privileges = true
@record.save
assert_not_empty @record.errors[:name]
end
test 'doesnt drop privileges on non zone NS records' do
@record.name = 'other'
@record.drop_privileges = true
@record.save
assert_empty @record.errors[:name]
end
+
+ class ClasslessDelegation < ActiveSupport::TestCase
+ setup do
+ @record = build(:cd_ns, name: '192/29')
+ @domain = @record.domain
+ end
+
+ test 'creates delegation' do
+ assert @record.save
+ assert @record.classless_delegation?
+
+ octets = [192, 193, 194, 195, 196, 197, 198, 199]
+
+ assert_equal octets.size, @domain.records.where(type: 'CNAME').count
+
+ octets.each { |octet|
+ cname = @domain.records.where(type: 'CNAME', name: "#{octet}.#{@domain.name}").first
+
+ assert cname, "#{octet} delegation"
+ assert_not cname.editable?, "#{octet} editable"
+ assert cname.classless_delegated?, "#{octet} classless_delegated"
+ }
+ end
+
+ test 'delete delegation' do
+ assert @record.save
+
+ @record.destroy
+ assert_equal 0, @domain.records.where(type: 'CNAME').count, 'zero delegations'
+ end
+
+ [
+ '0/24', # too big
+ '0/32', # too small
+ '191/29',
+ '-192/29',
+ '192/-29',
+ '192/29/29',
+ '192',
+ ].each { |del|
+ test "invalid delegation #{del}" do
+ @record.name = del
+ @record.save!
+ assert_not @record.classless_delegation?
+
+ assert_equal 0, @domain.records.where(type: 'CNAME').count, "0 CNAMEs for #{del}"
+ end
+ }
+
+ test 'add errors when a record already exists' do
+ create(:v4_ptr, domain: @domain, name: '194')
+
+ assert_not @record.save, 'delegation with records should fail'
+ assert_not_empty @record.errors[:name]
+ end
+
+ test 'allow second delegation with a different NS' do
+ @record.save!
+
+ assert_no_difference "@domain.records.where(type: 'CNAME').count" do
+ NS.create!(domain: @domain, name: @record.name, content: 'ns-other.example.com')
+ end
+ end
+
+ test 'drop records after all NS are deleted' do
+ @record.save!
+ second = NS.create!(domain: @domain, name: @record.name, content: 'ns-other.example.com')
+
+ # CNAMEs are not deleted
+ assert_no_difference '@domain.records.where(type: "CNAME").count' do
+ second.destroy
+ end
+
+ # Deleting the last NS delegation should delete all CNAMEs
+ @record.destroy
+ assert_equal 0, @domain.records.where(type: 'CNAME').count, 'zero delegations'
+ end
+ end
end

Event Timeline