Class: MockDnsServer::SerialHistory

Inherits:
Object
  • Object
show all
Defined in:
lib/mock_dns_server/serial_history.rb

Overview

Manages RR additions and deletions for multiple serials, and builds responses to AXFR and IXFR requests.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(zone, start_serial, initial_records = [], ixfr_response_uses_axfr_style = :never) ⇒ SerialHistory

Creates the instance.

:never (default) - always return IXFR-style, but
    if the requested serial is not known by the server
    (i.e. if it is *not* the serial of one of the transactions in the history),
    then return 'transfer failed' rcode
:always - always return AXFR-style
:auto - if the requested serial is known by the server (i.e. if it is
     the serial of one of the transactions in the history,
     or is the initial serial of the history), then return an IXFR list;
     otherwise return an AXFR list.
Note that even when an AXFR-style list is returned, it is still an IXFR
response -- that is, the IXFR question from the query is copied into the response.

Parameters:

  • zone
  • start_serial

    the serial of the data set provided in the initial_records

  • initial_records (defaults to: [])

    the starting data

  • ixfr_response_uses_axfr_style (defaults to: :never)

    when to respond to an IXFR request with an AXFR-style IXFR, rather than an IXFR list of changes. Regardless of this option, if the requested serial >= the last known serial of this history, a response with a single SOA record containing the highest known serial will be sent. The following options apply to any other case, and are:



33
34
35
36
37
38
39
# File 'lib/mock_dns_server/serial_history.rb', line 33

def initialize(zone, start_serial, initial_records = [], ixfr_response_uses_axfr_style = :never)
  @zone = zone
  @low_serial = SerialNumber.object(start_serial)
  @initial_records = initial_records
  self.ixfr_response_uses_axfr_style = ixfr_response_uses_axfr_style
  @txns = ThreadSafe::Hash.new  # txns is an abbreviation of transactions
end

Instance Attribute Details

#ixfr_response_uses_axfr_styleObject

Returns the value of attribute ixfr_response_uses_axfr_style.



10
11
12
# File 'lib/mock_dns_server/serial_history.rb', line 10

def ixfr_response_uses_axfr_style
  @ixfr_response_uses_axfr_style
end

#low_serialObject

Returns the value of attribute low_serial.



10
11
12
# File 'lib/mock_dns_server/serial_history.rb', line 10

def low_serial
  @low_serial
end

#zoneObject

Returns the value of attribute zone.



10
11
12
# File 'lib/mock_dns_server/serial_history.rb', line 10

def zone
  @zone
end

Instance Method Details

#axfr_recordsObject



162
163
164
# File 'lib/mock_dns_server/serial_history.rb', line 162

def axfr_records
  [high_serial_soa_rr, current_data, high_serial_soa_rr].flatten
end

#current_dataObject



154
155
156
# File 'lib/mock_dns_server/serial_history.rb', line 154

def current_data
  data_at_serial(:current)
end

#data_at_serial(serial) ⇒ Object

Returns a snapshot array of the data as of a given serial number.

Returns:

  • a snapshot array of the data as of a given serial number



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/mock_dns_server/serial_history.rb', line 129

def data_at_serial(serial)

  serial = high_serial if serial == :current
  serial = SerialNumber.object(serial)

  if serial.nil? || serial > high_serial || serial < low_serial
    raise "Serial must be in range #{low_serial} to #{high_serial} inclusive."
  end
  data = @initial_records.clone

  txn_serials.each do |key|
    txn = @txns[key]
    break if txn.serial > serial
    txn.deletions.each do |d|
      data.reject! { |rr| rr_equivalent(rr, d) }
    end
    txn.additions.each do |a|
      data.reject! { |rr| rr_equivalent(rr, a) }
      data << a
    end
  end

  data
end

#high_serialObject



87
88
89
# File 'lib/mock_dns_server/serial_history.rb', line 87

def high_serial
  txn_serials.empty? ? low_serial : txn_serials.last
end

#high_serial_soa_rrObject



158
159
160
# File 'lib/mock_dns_server/serial_history.rb', line 158

def high_serial_soa_rr
  MessageBuilder.soa_answer(name: zone, serial: high_serial)
end

#is_tracked_serial(serial) ⇒ Object



230
231
232
233
# File 'lib/mock_dns_server/serial_history.rb', line 230

def is_tracked_serial(serial)
  serial = SerialNumber.object(serial)
  serials.include?(serial)
end

#ixfr_records(base_serial = nil) ⇒ Object

Returns an array of RR’s that can be used to populate an IXFR response.

Returns:

  • an array of RR’s that can be used to populate an IXFR response.



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/mock_dns_server/serial_history.rb', line 181

def ixfr_records(base_serial = nil)
  base_serial = SerialNumber.object(base_serial)

  records = []
  records << high_serial_soa_rr

  serials = @txns.keys

  # Note that the serials in the data structure are the 'to' serials,
  # whereas the serial of this request will be the 'from' serial.
  # To compensate for this, we take the first serial *after* the
  # occurrence of base_serial in the array of serials, thus the +1 below.
  index_minus_one = serials.find_index(base_serial)
  index_is_index_other_than_last_index = index_minus_one && index_minus_one < serials.size - 1

  base_serial_index = index_is_index_other_than_last_index ? index_minus_one + 1 : 0

  serials_to_process = serials[base_serial_index..-1]
  serials_to_process.each do |serial|
    txn = @txns[serial]
    txn_records = txn.ixfr_records(previous_serial(serial))
    txn_records.each { |rec| records << rec }
  end

  records << high_serial_soa_rr
  records
end

#ixfr_response_style(serial) ⇒ Object

When handling an IXFR request, use the following logic:

if the serial number requested >= the current serial number (highest_serial), return a single SOA record (at the current serial number).

Otherwise, given the current value of ixfr_response_uses_axfr_style:

:always - always return an AXFR-style IXFR response

:never (default) - if we have that serial in our history, return an IXFR response,

else return a Transfer Failed error message

:auto - if we have that serial in our history, return an IXFR response,

else return an AXFR style response.

Returns:

  • the type of response appropriate to this serial and request



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/mock_dns_server/serial_history.rb', line 259

def ixfr_response_style(serial)
  serial = SerialNumber.object(serial)

  if serial >= high_serial
    :single_soa
  else
    case ixfr_response_uses_axfr_style
      when :never
        is_tracked_serial(serial) ? :ixfr : :xfer_failed
      when :auto
        is_tracked_serial(serial) ? :ixfr : :axfr_style_ixfr
      when :always
        :axfr_style_ixfr
    end
  end
end

#next_serial_valueObject

Returns the next serial value that could be added to the history, i.e. the successor to the highest serial we now have.



238
239
240
# File 'lib/mock_dns_server/serial_history.rb', line 238

def next_serial_value
  SerialNumber.next_serial_value(high_serial.to_i)
end

#previous_serial(serial) ⇒ Object

Finds the serial previous to that of this transaction. else the serial of the previous transaction

Returns:

  • If txn is the first txn, returns start_serial of the history



170
171
172
173
174
175
176
# File 'lib/mock_dns_server/serial_history.rb', line 170

def previous_serial(serial)
  serial = SerialNumber.object(serial)
  return nil if serial <= low_serial || serial > high_serial

  txn_index = txn_serials.find_index(serial)
  txn_index > 0 ? txn_serials[txn_index - 1] : @low_serial
end

#rr_compare(rr1, rr2) ⇒ Object

Although Dnsruby has a <=> operator on RR’s, we need a comparison that looks only at the type, name, and rdata (and not the TTL, for example), for purposes of detecting records that need be deleted.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/mock_dns_server/serial_history.rb', line 98

def rr_compare(rr1, rr2)

  rrs = [rr1, rr2]

  name1, name2 = rrs.map { |rr| rr.name.to_s.downcase }
  if name1 != name2
    return name1 > name2 ? 1 : -1
  end

  type1, type2 = rrs.map { |rr| rr.type.to_s.downcase }
  if type1 != type2
    return type1 > type2 ? 1 : -1
  end

  rdata1, rdata2 = rrs.map(&:rdata)
  if rdata1 != rdata2
    rdata1 > rdata2 ? 1 : -1
  else
    0
  end
end

#rr_equivalent(rr1, rr2) ⇒ Object



121
122
123
# File 'lib/mock_dns_server/serial_history.rb', line 121

def rr_equivalent(rr1, rr2)
  rr_compare(rr1, rr2) == 0
end

#serial_additions(serial) ⇒ Object



62
63
64
65
# File 'lib/mock_dns_server/serial_history.rb', line 62

def serial_additions(serial)
  serial = SerialNumber.object(serial)
  @txns[serial] ? @txns[serial].additions : nil
end

#serial_deletions(serial) ⇒ Object



74
75
76
77
# File 'lib/mock_dns_server/serial_history.rb', line 74

def serial_deletions(serial)
  serial = SerialNumber.object(serial)
  @txns[serial] ? @txns[serial].deletions : nil
end

#serialsObject



83
84
85
# File 'lib/mock_dns_server/serial_history.rb', line 83

def serials
  [low_serial] + txn_serials
end

#set_serial_additions(serial, additions) ⇒ Object



55
56
57
58
59
60
# File 'lib/mock_dns_server/serial_history.rb', line 55

def set_serial_additions(serial, additions)
  serial = SerialNumber.object(serial)
  additions = Array(additions)
  serial_transaction(serial).additions = additions
  self
end

#set_serial_deletions(serial, deletions) ⇒ Object



67
68
69
70
71
72
# File 'lib/mock_dns_server/serial_history.rb', line 67

def set_serial_deletions(serial, deletions)
  serial = SerialNumber.object(serial)
  deletions = Array(deletions)
  serial_transaction(serial).deletions = deletions
  self
end

#to_sObject



91
92
93
# File 'lib/mock_dns_server/serial_history.rb', line 91

def to_s
  "#{self.class.name}: zone: #{zone}, initial serial: #{low_serial}, high_serial: #{high_serial}, records:\n#{ixfr_records}\n"
end

#txn_serialsObject



79
80
81
# File 'lib/mock_dns_server/serial_history.rb', line 79

def txn_serials
  @txns.keys
end

#xfr_array_type(records) ⇒ Object

Determines whether a given record array is AXFR- or IXFR-style.

Parameters:

  • records

    array of IXFR or AXFR records

Returns:

  • :ixfr, :axfr, :error



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/mock_dns_server/serial_history.rb', line 213

def xfr_array_type(records)
  begin
    for num_consecutive_soas in (0..records.size)
      break unless records[num_consecutive_soas].is_a?(Dnsruby::RR::SOA)
    end
    case num_consecutive_soas
      when nil; :error
      when 0;   :error
      when 1;   :axfr
      else;     :ixfr
    end
  rescue => e
    :error
  end
end

#xfr_response(incoming_message) ⇒ Object

Creates a response message based on the type and serial of the incoming message.

Parameters:

  • incoming_message

    an AXFR or IXFR request

Returns:

  • a Dnsruby message containing the response, either or AXFR or IXFR



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/mock_dns_server/serial_history.rb', line 280

def xfr_response(incoming_message)

  mt           = MessageTransformer.new(incoming_message)
  query_zone   = mt.qname
  query_type   = mt.qtype.downcase.to_sym  # :axfr or :ixfr
  query_serial = mt.serial(:authority)  # ixfr requests only, else will be nil

  validate_inputs = ->() {
    if query_zone.downcase != zone.downcase
      raise "Query zone (#{query_zone}) differs from history zone (#{zone})."
    end

    unless [:axfr, :ixfr].include?(query_type)
      raise "Invalid qtype (#{query_type}), must be AXFR or IXFR."
    end

    if query_type == :ixfr && query_serial.nil?
      raise 'IXFR request did not specify serial in authority section.'
    end
  }

  build_standard_response = ->(rrs = nil) do
    response = Dnsruby::Message.new
    response.header.qr = true
    response.header.aa = true
    rrs.each { |record| response.add_answer!(record) } if rrs
    incoming_message.question.each { |q| response.add_question(q) }
    response
  end

  build_error_response = ->() {
    response = build_standard_response.()
    response.header.rcode = Dnsruby::RCode::REFUSED
    response
  }

  build_single_soa_response = ->() {
    build_standard_response.([high_serial_soa_rr])
  }

  validate_inputs.()
  xfr_response = nil

  case query_type

    when :axfr
      xfr_response = build_standard_response.(axfr_records)
    when :ixfr
      response_style = ixfr_response_style(query_serial)

      case response_style
        when :axfr_style_ixfr
          xfr_response = build_standard_response.(axfr_records)
        when :ixfr
          xfr_response = build_standard_response.(ixfr_records(query_serial))
        when :single_soa
          xfr_response = build_single_soa_response.()
        when :error
          xfr_response = build_error_response.()
      end
  end

xfr_response
end