mock_dns_server 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +24 -0
- data/README.md +127 -0
- data/RELEASE_NOTES.md +3 -0
- data/Rakefile +19 -0
- data/bin/show_dig_request +41 -0
- data/lib/mock_dns_server.rb +12 -0
- data/lib/mock_dns_server/action_factory.rb +84 -0
- data/lib/mock_dns_server/conditional_action.rb +42 -0
- data/lib/mock_dns_server/conditional_action_factory.rb +53 -0
- data/lib/mock_dns_server/conditional_actions.rb +73 -0
- data/lib/mock_dns_server/dnsruby_monkey_patch.rb +19 -0
- data/lib/mock_dns_server/history.rb +84 -0
- data/lib/mock_dns_server/history_inspections.rb +58 -0
- data/lib/mock_dns_server/ip_address_dispenser.rb +34 -0
- data/lib/mock_dns_server/message_builder.rb +199 -0
- data/lib/mock_dns_server/message_helper.rb +86 -0
- data/lib/mock_dns_server/message_transformer.rb +74 -0
- data/lib/mock_dns_server/predicate_factory.rb +108 -0
- data/lib/mock_dns_server/serial_history.rb +385 -0
- data/lib/mock_dns_server/serial_number.rb +129 -0
- data/lib/mock_dns_server/serial_transaction.rb +46 -0
- data/lib/mock_dns_server/server.rb +422 -0
- data/lib/mock_dns_server/server_context.rb +57 -0
- data/lib/mock_dns_server/server_thread.rb +13 -0
- data/lib/mock_dns_server/version.rb +3 -0
- data/mock_dns_server.gemspec +32 -0
- data/spec/mock_dns_server/conditions_factory_spec.rb +58 -0
- data/spec/mock_dns_server/history_inspections_spec.rb +84 -0
- data/spec/mock_dns_server/history_spec.rb +65 -0
- data/spec/mock_dns_server/ip_address_dispenser_spec.rb +30 -0
- data/spec/mock_dns_server/message_builder_spec.rb +18 -0
- data/spec/mock_dns_server/predicate_factory_spec.rb +147 -0
- data/spec/mock_dns_server/serial_history_spec.rb +385 -0
- data/spec/mock_dns_server/serial_number_spec.rb +119 -0
- data/spec/mock_dns_server/serial_transaction_spec.rb +37 -0
- data/spec/mock_dns_server/server_context_spec.rb +20 -0
- data/spec/mock_dns_server/server_spec.rb +411 -0
- data/spec/mock_dns_server/socket_research_spec.rb +59 -0
- data/spec/spec_helper.rb +44 -0
- data/todo.txt +0 -0
- metadata +212 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
|
2
|
+
module MockDnsServer
|
3
|
+
|
4
|
+
module MessageHelper
|
5
|
+
|
6
|
+
MESSAGE_LENGTH_PACK_UNPACK_FORMAT = 'n'
|
7
|
+
|
8
|
+
# If the string can convert to a Dnsruby::Message without throwing an exception,
|
9
|
+
# return the Dnsruby::Message instance; else, return the original string.
|
10
|
+
def self.to_dns_message(object)
|
11
|
+
case object
|
12
|
+
when String
|
13
|
+
begin
|
14
|
+
Dnsruby::Message.decode(object)
|
15
|
+
rescue
|
16
|
+
object
|
17
|
+
end
|
18
|
+
when Dnsruby::Message
|
19
|
+
object
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def self.convertible_to_dnsruby_message?(object)
|
25
|
+
to_dns_message(object).is_a?(Dnsruby::Message)
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
# Builds a string for a TCP client to send to a DNS server
|
31
|
+
#
|
32
|
+
# @param message, either a DNS message or a string
|
33
|
+
# @return if message is a Dnsruby::Message, returns the wire_data prepended with the 2-byte size field
|
34
|
+
# else returns the message unchanged
|
35
|
+
def self.tcp_message_package_for_write(message)
|
36
|
+
message = message.encode if message.is_a?(Dnsruby::Message)
|
37
|
+
size_field = [message.size].pack(MESSAGE_LENGTH_PACK_UNPACK_FORMAT)
|
38
|
+
size_field + message
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def self.udp_message_package_for_write(object)
|
43
|
+
case object
|
44
|
+
when Dnsruby::Message
|
45
|
+
object.encode
|
46
|
+
when String
|
47
|
+
object
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# Reads a message from a TCP connection. First gets the 2 byte length, then reads the payload.
|
53
|
+
# Attempts to convert the payload into a Dnsruby::Message.
|
54
|
+
def self.read_tcp_message(socket)
|
55
|
+
|
56
|
+
message_len_str = socket.read(2)
|
57
|
+
raise "Unable to read from socket; read returned nil" if message_len_str.nil?
|
58
|
+
message_len = message_len_str.unpack(MESSAGE_LENGTH_PACK_UNPACK_FORMAT).first
|
59
|
+
|
60
|
+
bytes_not_yet_read = message_len
|
61
|
+
message_wire_data = ''
|
62
|
+
|
63
|
+
while bytes_not_yet_read > 0
|
64
|
+
str = socket.read(bytes_not_yet_read)
|
65
|
+
bytes_not_yet_read -= str.size
|
66
|
+
message_wire_data << str
|
67
|
+
end
|
68
|
+
|
69
|
+
message = MessageHelper.to_dns_message(message_wire_data)
|
70
|
+
message
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
# Sends a UDP message and returns the response, using a temporary socket.
|
75
|
+
def self.send_udp_and_get_response(message, host, port)
|
76
|
+
socket = UDPSocket.new
|
77
|
+
message = message.encode if message.is_a?(Dnsruby::Message)
|
78
|
+
socket.send(message, 0, host, port)
|
79
|
+
_, _, _ = IO.select([socket], nil, nil)
|
80
|
+
response_data, _ = socket.recvfrom(10_000)
|
81
|
+
response = to_dns_message(response_data)
|
82
|
+
socket.close
|
83
|
+
response
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module MockDnsServer
|
2
|
+
|
3
|
+
# Lambdas that transform a message into something else, usually a message component such as domain or qtype.
|
4
|
+
class MessageTransformer
|
5
|
+
|
6
|
+
attr_reader :message
|
7
|
+
|
8
|
+
# Initialize the transformer with a message.
|
9
|
+
# @param dns_message can be either a Dnsruby::Message instance or binary wire data
|
10
|
+
def initialize(dns_message)
|
11
|
+
self.message = dns_message
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def message=(dns_message)
|
16
|
+
@message = dns_message.is_a?(String) ? Dnsruby::Message.decode(dns_message) : dns_message
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# A SOA record is usually in the answer section, but in the case of IXFR requests
|
21
|
+
# it will be in the authority section.
|
22
|
+
#
|
23
|
+
# @location defaults to :answer, can override w/:authority
|
24
|
+
def serial(location = :answer)
|
25
|
+
return nil if message.nil?
|
26
|
+
|
27
|
+
target_section = message.send(location == :answer ? :answer : :authority)
|
28
|
+
return nil if target_section.nil?
|
29
|
+
|
30
|
+
soa_answer = target_section.detect { |record| record.is_a?(Dnsruby::RR::IN::SOA) }
|
31
|
+
soa_answer ? soa_answer.serial : nil
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# @return the message's qtype as a String
|
36
|
+
def qtype
|
37
|
+
dnsruby_type_instance = question_attr(:qtype)
|
38
|
+
Dnsruby::Types.to_string(dnsruby_type_instance)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# @return the message's qname as a String
|
43
|
+
def qname
|
44
|
+
question_attr(:qname)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# @return the message's qclass as a String
|
49
|
+
def qclass
|
50
|
+
question_attr(:qclass)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def question_attr(symbol)
|
55
|
+
question = first_question
|
56
|
+
question ? question.send(symbol).to_s : nil
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
def first_question
|
61
|
+
has_question = message &&
|
62
|
+
message.question &&
|
63
|
+
message.question.first &&
|
64
|
+
message.question.first.is_a?(Dnsruby::Question)
|
65
|
+
|
66
|
+
has_question ? message.question.first : nil
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def answer_count(answer_type)
|
71
|
+
message.answer.select { |a| a.rr_type.to_s == answer_type}.count
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'mock_dns_server/message_transformer'
|
2
|
+
|
3
|
+
module MockDnsServer
|
4
|
+
|
5
|
+
# Each method returns a predicate in the form of a lambda or proc that takes an
|
6
|
+
# incoming object (usually a Dnsruby::Message) object as a parameter and returns
|
7
|
+
# (as predicates do) true or false.
|
8
|
+
class PredicateFactory
|
9
|
+
|
10
|
+
# shorthand for the MessageTransformers instance
|
11
|
+
def mt(message)
|
12
|
+
MessageTransformer.new(message)
|
13
|
+
end
|
14
|
+
|
15
|
+
# TODO: Case insensitive?
|
16
|
+
# TODO: Add sender to signature for tcp/udp, etc.?
|
17
|
+
|
18
|
+
def all(*predicates)
|
19
|
+
->(message, protocol = nil) do
|
20
|
+
predicates.all? { |p| p.call(message, protocol) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def any(*predicates)
|
25
|
+
->(message, protocol = nil) do
|
26
|
+
predicates.any? { |p| p.call(message, protocol) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def none(*predicates)
|
31
|
+
->(message, protocol = nil) do
|
32
|
+
predicates.none? { |p| p.call(message, protocol) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def dns
|
37
|
+
->(message, _ = nil) { message.is_a?(Dnsruby::Message) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def soa
|
41
|
+
qtype('SOA')
|
42
|
+
end
|
43
|
+
|
44
|
+
def ixfr
|
45
|
+
qtype('IXFR')
|
46
|
+
end
|
47
|
+
|
48
|
+
def axfr
|
49
|
+
qtype('AXFR')
|
50
|
+
end
|
51
|
+
|
52
|
+
def xfr
|
53
|
+
any(axfr, ixfr)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns true for messages relating to data from the zone load.
|
57
|
+
def zone_load
|
58
|
+
any(xfr, soa)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Convenience method for testing for a specific qtype and qname.
|
62
|
+
def qtype_and_qname(qtype, qname)
|
63
|
+
all(qtype(qtype), qname(qname))
|
64
|
+
end
|
65
|
+
|
66
|
+
# Convenience method for testing a request of qtype 'A' with the given qname.
|
67
|
+
def a_request(qname)
|
68
|
+
qtype_and_qname('A', qname)
|
69
|
+
end
|
70
|
+
|
71
|
+
def qtype(qtype)
|
72
|
+
->(message, _ = nil) do
|
73
|
+
dns.(message) && eq_case_insensitive(mt(message).qtype, qtype)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def qclass(qclass)
|
78
|
+
->(message, _ = nil) { dns.(message) && eq_case_insensitive(mt(message).qclass, qclass) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def qname(qname)
|
82
|
+
->(message, _ = nil) { dns.(message) && eq_case_insensitive(mt(message).qname, qname) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def from_tcp
|
86
|
+
->(_, protocol) { protocol == :tcp }
|
87
|
+
end
|
88
|
+
|
89
|
+
def from_udp
|
90
|
+
->(_, protocol) { protocol == :udp }
|
91
|
+
end
|
92
|
+
|
93
|
+
def always_true
|
94
|
+
->(_, _) { true }
|
95
|
+
end
|
96
|
+
|
97
|
+
def always_false
|
98
|
+
->(_, _) { false }
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def eq_case_insensitive(s1, s2)
|
104
|
+
s1.downcase == s2.downcase
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,385 @@
|
|
1
|
+
require 'thread_safe'
|
2
|
+
require 'mock_dns_server/serial_transaction'
|
3
|
+
|
4
|
+
module MockDnsServer
|
5
|
+
|
6
|
+
# Manages RR additions and deletions for multiple serials,
|
7
|
+
# and builds responses to AXFR and IXFR requests.
|
8
|
+
class SerialHistory
|
9
|
+
|
10
|
+
attr_accessor :zone, :low_serial, :ixfr_response_uses_axfr_style
|
11
|
+
|
12
|
+
# Creates the instance.
|
13
|
+
# @param zone
|
14
|
+
# @param start_serial the serial of the data set provided in the initial_records
|
15
|
+
# @param initial_records the starting data
|
16
|
+
# @param ixfr_response_uses_axfr_style when to respond to an IXFR request with an AXFR-style IXFR,
|
17
|
+
# rather than an IXFR list of changes. Regardless of this option,
|
18
|
+
# if the requested serial >= the last known serial of this history,
|
19
|
+
# a response with a single SOA record containing the highest known serial will be sent.
|
20
|
+
# The following options apply to any other case, and are:
|
21
|
+
#
|
22
|
+
# :never (default) - always return IXFR-style, but
|
23
|
+
# if the requested serial is not known by the server
|
24
|
+
# (i.e. if it is *not* the serial of one of the transactions in the history),
|
25
|
+
# then return 'transfer failed' rcode
|
26
|
+
# :always - always return AXFR-style
|
27
|
+
# :auto - if the requested serial is known by the server (i.e. if it is
|
28
|
+
# the serial of one of the transactions in the history,
|
29
|
+
# or is the initial serial of the history), then return an IXFR list;
|
30
|
+
# otherwise return an AXFR list.
|
31
|
+
# Note that even when an AXFR-style list is returned, it is still an IXFR
|
32
|
+
# response -- that is, the IXFR question from the query is copied into the response.
|
33
|
+
def initialize(zone, start_serial, initial_records = [], ixfr_response_uses_axfr_style = :never)
|
34
|
+
@zone = zone
|
35
|
+
@low_serial = SerialNumber.object(start_serial)
|
36
|
+
@initial_records = initial_records
|
37
|
+
self.ixfr_response_uses_axfr_style = ixfr_response_uses_axfr_style
|
38
|
+
@txns = ThreadSafe::Hash.new # txns is an abbreviation of transactions
|
39
|
+
end
|
40
|
+
|
41
|
+
def ixfr_response_uses_axfr_style=(mode)
|
42
|
+
|
43
|
+
validate_input = ->() do
|
44
|
+
valid_modes = [:never, :always, :auto]
|
45
|
+
unless valid_modes.include?(mode)
|
46
|
+
valid_modes_as_string = valid_modes.map(&:inspect).join(', ')
|
47
|
+
raise "ixfr_response_uses_axfr_style mode must be one of the following: #{valid_modes_as_string}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
validate_input.()
|
52
|
+
@ixfr_response_uses_axfr_style = mode
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_serial_additions(serial, additions)
|
56
|
+
serial = SerialNumber.object(serial)
|
57
|
+
additions = Array(additions)
|
58
|
+
serial_transaction(serial).additions = additions
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def serial_additions(serial)
|
63
|
+
serial = SerialNumber.object(serial)
|
64
|
+
@txns[serial] ? @txns[serial].additions : nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_serial_deletions(serial, deletions)
|
68
|
+
serial = SerialNumber.object(serial)
|
69
|
+
deletions = Array(deletions)
|
70
|
+
serial_transaction(serial).deletions = deletions
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def serial_deletions(serial)
|
75
|
+
serial = SerialNumber.object(serial)
|
76
|
+
@txns[serial] ? @txns[serial].deletions : nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def txn_serials
|
80
|
+
@txns.keys
|
81
|
+
end
|
82
|
+
|
83
|
+
def serials
|
84
|
+
[low_serial] + txn_serials
|
85
|
+
end
|
86
|
+
|
87
|
+
def high_serial
|
88
|
+
txn_serials.empty? ? low_serial : txn_serials.last
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_s
|
92
|
+
"#{self.class.name}: zone: #{zone}, initial serial: #{low_serial}, high_serial: #{high_serial}, records:\n#{ixfr_records}\n"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Although Dnsruby has a <=> operator on RR's, we need a comparison that looks only
|
96
|
+
# at the type, name, and rdata (and not the TTL, for example), for purposes of
|
97
|
+
# detecting records that need be deleted.
|
98
|
+
def rr_compare(rr1, rr2)
|
99
|
+
|
100
|
+
rrs = [rr1, rr2]
|
101
|
+
|
102
|
+
name1, name2 = rrs.map { |rr| rr.name.to_s.downcase }
|
103
|
+
if name1 != name2
|
104
|
+
return name1 > name2 ? 1 : -1
|
105
|
+
end
|
106
|
+
|
107
|
+
type1, type2 = rrs.map { |rr| rr.type.to_s.downcase }
|
108
|
+
if type1 != type2
|
109
|
+
return type1 > type2 ? 1 : -1
|
110
|
+
end
|
111
|
+
|
112
|
+
rdata1, rdata2 = rrs.map(&:rdata)
|
113
|
+
if rdata1 != rdata2
|
114
|
+
rdata1 > rdata2 ? 1 : -1
|
115
|
+
else
|
116
|
+
0
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
def rr_equivalent(rr1, rr2)
|
122
|
+
rr_compare(rr1, rr2) == 0
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
# @return a snapshot array of the data as of a given serial number
|
127
|
+
# @serial if a number, must be in the range of known serials
|
128
|
+
# if :current, the highest known serial will be used
|
129
|
+
def data_at_serial(serial)
|
130
|
+
|
131
|
+
serial = high_serial if serial == :current
|
132
|
+
serial = SerialNumber.object(serial)
|
133
|
+
|
134
|
+
if serial.nil? || serial > high_serial || serial < low_serial
|
135
|
+
raise "Serial must be in range #{low_serial} to #{high_serial} inclusive."
|
136
|
+
end
|
137
|
+
data = @initial_records.clone
|
138
|
+
|
139
|
+
txn_serials.each do |key|
|
140
|
+
txn = @txns[key]
|
141
|
+
break if txn.serial > serial
|
142
|
+
txn.deletions.each do |d|
|
143
|
+
data.reject! { |rr| rr_equivalent(rr, d) }
|
144
|
+
end
|
145
|
+
txn.additions.each do |a|
|
146
|
+
data.reject! { |rr| rr_equivalent(rr, a) }
|
147
|
+
data << a
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
data
|
152
|
+
end
|
153
|
+
|
154
|
+
def current_data
|
155
|
+
data_at_serial(:current)
|
156
|
+
end
|
157
|
+
|
158
|
+
def high_serial_soa_rr
|
159
|
+
MessageBuilder.soa_answer(name: zone, serial: high_serial)
|
160
|
+
end
|
161
|
+
|
162
|
+
def axfr_records
|
163
|
+
[high_serial_soa_rr, current_data, high_serial_soa_rr].flatten
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
# Finds the serial previous to that of this transaction.
|
168
|
+
# @return If txn is the first txn, returns start_serial of the history
|
169
|
+
# else the serial of the previous transaction
|
170
|
+
def previous_serial(serial)
|
171
|
+
serial = SerialNumber.object(serial)
|
172
|
+
return nil if serial <= low_serial || serial > high_serial
|
173
|
+
|
174
|
+
txn_index = txn_serials.find_index(serial)
|
175
|
+
txn_index > 0 ? txn_serials[txn_index - 1] : @low_serial
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
# @return an array of RR's that can be used to populate an IXFR response.
|
180
|
+
# @base_serial the serial from which to start when building the list of changes
|
181
|
+
def ixfr_records(base_serial = nil)
|
182
|
+
base_serial = SerialNumber.object(base_serial)
|
183
|
+
|
184
|
+
records = []
|
185
|
+
records << high_serial_soa_rr
|
186
|
+
|
187
|
+
serials = @txns.keys
|
188
|
+
|
189
|
+
# Note that the serials in the data structure are the 'to' serials,
|
190
|
+
# whereas the serial of this request will be the 'from' serial.
|
191
|
+
# To compensate for this, we take the first serial *after* the
|
192
|
+
# occurrence of base_serial in the array of serials, thus the +1 below.
|
193
|
+
index_minus_one = serials.find_index(base_serial)
|
194
|
+
index_is_index_other_than_last_index = index_minus_one && index_minus_one < serials.size - 1
|
195
|
+
|
196
|
+
base_serial_index = index_is_index_other_than_last_index ? index_minus_one + 1 : 0
|
197
|
+
|
198
|
+
serials_to_process = serials[base_serial_index..-1]
|
199
|
+
serials_to_process.each do |serial|
|
200
|
+
txn = @txns[serial]
|
201
|
+
txn_records = txn.ixfr_records(previous_serial(serial))
|
202
|
+
txn_records.each { |rec| records << rec }
|
203
|
+
end
|
204
|
+
|
205
|
+
records << high_serial_soa_rr
|
206
|
+
records
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
# Determines whether a given record array is AXFR- or IXFR-style.
|
211
|
+
# @param records array of IXFR or AXFR records
|
212
|
+
# @return :ixfr, :axfr, :error
|
213
|
+
def xfr_array_type(records)
|
214
|
+
begin
|
215
|
+
for num_consecutive_soas in (0..records.size)
|
216
|
+
break unless records[num_consecutive_soas].is_a?(Dnsruby::RR::SOA)
|
217
|
+
end
|
218
|
+
case num_consecutive_soas
|
219
|
+
when nil; :error
|
220
|
+
when 0; :error
|
221
|
+
when 1; :axfr
|
222
|
+
else; :ixfr
|
223
|
+
end
|
224
|
+
rescue => e
|
225
|
+
:error
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
|
230
|
+
def is_tracked_serial(serial)
|
231
|
+
serial = SerialNumber.object(serial)
|
232
|
+
serials.include?(serial)
|
233
|
+
end
|
234
|
+
|
235
|
+
|
236
|
+
# Returns the next serial value that could be added to the history,
|
237
|
+
# i.e. the successor to the highest serial we now have.
|
238
|
+
def next_serial_value
|
239
|
+
SerialNumber.next_serial_value(high_serial.to_i)
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
# When handling an IXFR request, use the following logic:
|
244
|
+
#
|
245
|
+
# if the serial number requested >= the current serial number (highest_serial),
|
246
|
+
# return a single SOA record (at the current serial number).
|
247
|
+
#
|
248
|
+
# Otherwise, given the current value of ixfr_response_uses_axfr_style:
|
249
|
+
#
|
250
|
+
# :always - always return an AXFR-style IXFR response
|
251
|
+
#
|
252
|
+
# :never (default) - if we have that serial in our history, return an IXFR response,
|
253
|
+
# else return a Transfer Failed error message
|
254
|
+
#
|
255
|
+
# :auto - if we have that serial in our history, return an IXFR response,
|
256
|
+
# else return an AXFR style response.
|
257
|
+
#
|
258
|
+
# @return the type of response appropriate to this serial and request
|
259
|
+
def ixfr_response_style(serial)
|
260
|
+
serial = SerialNumber.object(serial)
|
261
|
+
|
262
|
+
if serial >= high_serial
|
263
|
+
:single_soa
|
264
|
+
else
|
265
|
+
case ixfr_response_uses_axfr_style
|
266
|
+
when :never
|
267
|
+
is_tracked_serial(serial) ? :ixfr : :xfer_failed
|
268
|
+
when :auto
|
269
|
+
is_tracked_serial(serial) ? :ixfr : :axfr_style_ixfr
|
270
|
+
when :always
|
271
|
+
:axfr_style_ixfr
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
|
277
|
+
# Creates a response message based on the type and serial of the incoming message.
|
278
|
+
# @param incoming_message an AXFR or IXFR request
|
279
|
+
# @return a Dnsruby message containing the response, either or AXFR or IXFR
|
280
|
+
def xfr_response(incoming_message)
|
281
|
+
|
282
|
+
mt = MessageTransformer.new(incoming_message)
|
283
|
+
query_zone = mt.qname
|
284
|
+
query_type = mt.qtype.downcase.to_sym # :axfr or :ixfr
|
285
|
+
query_serial = mt.serial(:authority) # ixfr requests only, else will be nil
|
286
|
+
|
287
|
+
validate_inputs = ->() {
|
288
|
+
if query_zone.downcase != zone.downcase
|
289
|
+
raise "Query zone (#{query_zone}) differs from history zone (#{zone})."
|
290
|
+
end
|
291
|
+
|
292
|
+
unless [:axfr, :ixfr].include?(query_type)
|
293
|
+
raise "Invalid qtype (#{query_type}), must be AXFR or IXFR."
|
294
|
+
end
|
295
|
+
|
296
|
+
if query_type == :ixfr && query_serial.nil?
|
297
|
+
raise 'IXFR request did not specify serial in authority section.'
|
298
|
+
end
|
299
|
+
}
|
300
|
+
|
301
|
+
build_standard_response = ->(rrs = nil) do
|
302
|
+
response = Dnsruby::Message.new
|
303
|
+
response.header.qr = true
|
304
|
+
response.header.aa = true
|
305
|
+
rrs.each { |record| response.add_answer!(record) } if rrs
|
306
|
+
incoming_message.question.each { |q| response.add_question(q) }
|
307
|
+
response
|
308
|
+
end
|
309
|
+
|
310
|
+
build_error_response = ->() {
|
311
|
+
response = build_standard_response.()
|
312
|
+
response.header.rcode = Dnsruby::RCode::REFUSED
|
313
|
+
response
|
314
|
+
}
|
315
|
+
|
316
|
+
build_single_soa_response = ->() {
|
317
|
+
build_standard_response.([high_serial_soa_rr])
|
318
|
+
}
|
319
|
+
|
320
|
+
validate_inputs.()
|
321
|
+
xfr_response = nil
|
322
|
+
|
323
|
+
case query_type
|
324
|
+
|
325
|
+
when :axfr
|
326
|
+
xfr_response = build_standard_response.(axfr_records)
|
327
|
+
when :ixfr
|
328
|
+
response_style = ixfr_response_style(query_serial)
|
329
|
+
|
330
|
+
case response_style
|
331
|
+
when :axfr_style_ixfr
|
332
|
+
xfr_response = build_standard_response.(axfr_records)
|
333
|
+
when :ixfr
|
334
|
+
xfr_response = build_standard_response.(ixfr_records(query_serial))
|
335
|
+
when :single_soa
|
336
|
+
xfr_response = build_single_soa_response.()
|
337
|
+
when :error
|
338
|
+
xfr_response = build_error_response.()
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
xfr_response
|
343
|
+
end
|
344
|
+
|
345
|
+
private
|
346
|
+
|
347
|
+
# Checks to see that a new serial whose transactions will be added to the history
|
348
|
+
# has a valid serial value in the context of the data already there.
|
349
|
+
# Raises an error if the serial is bad, else does nothing.
|
350
|
+
def check_new_serial(new_serial)
|
351
|
+
if new_serial < low_serial
|
352
|
+
raise "New serial of #{new_serial} must not be lower than initial serial of #{low_serial}."
|
353
|
+
elsif new_serial < high_serial
|
354
|
+
raise "New serial of #{new_serial} must not be lower than highest preexisting serial of #{high_serial}."
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
|
359
|
+
# Returns the SerialTransaction instance associated with this serial value,
|
360
|
+
# creating it if it does not already exist.
|
361
|
+
def serial_transaction(serial)
|
362
|
+
unless @txns[serial]
|
363
|
+
check_new_serial(serial)
|
364
|
+
@txns[serial] ||= SerialTransaction.new(zone, serial)
|
365
|
+
|
366
|
+
# As long as we prohibit adding serials out of order, there is no need for this:
|
367
|
+
# recreate_hash
|
368
|
+
end
|
369
|
+
|
370
|
+
@txns[serial]
|
371
|
+
end
|
372
|
+
|
373
|
+
|
374
|
+
# Recreates the hash so that its keys are in ascending order.
|
375
|
+
# Currently (12/18/2013) this is redundant, since serials must be added
|
376
|
+
# in ascending order.
|
377
|
+
#def recreate_hash
|
378
|
+
# keys = @txns.keys
|
379
|
+
# new_hash = ThreadSafe::Hash.new
|
380
|
+
# keys.sort.each { |key| new_hash[key] = @txns[key] }
|
381
|
+
# @txns = new_hash
|
382
|
+
#end
|
383
|
+
|
384
|
+
end
|
385
|
+
end
|