mock_dns_server 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|