nesser 0.0.1

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.
@@ -0,0 +1,97 @@
1
+ # Encoding: ASCII-8BIT
2
+ ##
3
+ # packer.rb
4
+ # Created June 20, 2017
5
+ # By Ron Bowes
6
+ #
7
+ # See: LICENSE.md
8
+ #
9
+ # DNS has some unusual properties that we have to handle, which is why I
10
+ # wrote this class. It handles building / parsing DNS packets and keeping
11
+ # track of where in the packet we currently are. The advantage, besides
12
+ # simpler unpacking, is that encoded names (with pointers to other parts
13
+ # of the packet) can be trivially handled.
14
+ ##
15
+
16
+ require 'nesser/dns_exception'
17
+ require 'nesser/packets/constants'
18
+
19
+ module Nesser
20
+ class Packer
21
+ public
22
+ def initialize()
23
+ @data = ''
24
+ @segment_cache = {}
25
+ end
26
+
27
+ public
28
+ def pack(format, *data)
29
+ @data += data.pack(format)
30
+ end
31
+
32
+ private
33
+ def validate!(name)
34
+ if name.chars.detect { |ch| !LEGAL_CHARACTERS.include?(ch) }
35
+ raise(FormatException, "DNS name contains illegal characters")
36
+ end
37
+ if name.length > 253
38
+ raise(FormatException, "DNS name can't be longer than 253 characters")
39
+ end
40
+ name.split(/\./).each do |segment|
41
+ if segment.length == 0 || segment.length > 63
42
+ raise(FormatException, "DNS segments must be between 1 and 63 characters!")
43
+ end
44
+ end
45
+ end
46
+
47
+ # Take a name, as a dotted string ("google.com") and return it as length-
48
+ # prefixed segments ("\x06google\x03com\x00"). It also does a pointer
49
+ # (\xc0\xXX) when possible!
50
+ public
51
+ def pack_name(name, dry_run:false, compress:true)
52
+ length = 0
53
+ validate!(name)
54
+
55
+ # `name` becomes nil at the end, unless there's a comma on the end, in
56
+ # which case it's a 0-length string
57
+ while name and name.length() > 0
58
+ if compress && @segment_cache[name]
59
+ # User a pointer if we've already done this
60
+ if not dry_run
61
+ @data += [0xc000 | @segment_cache[name]].pack("n")
62
+ end
63
+
64
+ # If we use break here, we get a bad NUL terminator
65
+ return length + 2
66
+ end
67
+
68
+ # Log where we put this segment
69
+ if not dry_run
70
+ @segment_cache[name] = @data.length
71
+ end
72
+
73
+ # Get the next label
74
+ segment, name = name.split(/\./, 2)
75
+
76
+ # Encode it into the string
77
+ if not dry_run
78
+ @data += [segment.length(), segment].pack("Ca*")
79
+ end
80
+ length += 1 + segment.length()
81
+ end
82
+
83
+ # Always be null terminating
84
+ if not dry_run
85
+ @data += "\0"
86
+ end
87
+ length += 1
88
+
89
+ return length
90
+ end
91
+
92
+ public
93
+ def get()
94
+ return @data
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,186 @@
1
+ # Encoding: ASCII-8BIT
2
+ ##
3
+ # packet.rb
4
+ # Created June 20, 2017
5
+ # By Ron Bowes
6
+ #
7
+ # See: LICENSE.md
8
+ ##
9
+
10
+ require 'nesser/dns_exception'
11
+ require 'nesser/packets/answer'
12
+ require 'nesser/packets/constants'
13
+ require 'nesser/packets/packer'
14
+ require 'nesser/packets/question'
15
+ require 'nesser/packets/rr_types'
16
+ require 'nesser/packets/unpacker'
17
+
18
+ module Nesser
19
+ class Packet
20
+ attr_accessor :trn_id, :qr, :opcode, :flags, :rcode, :questions, :answers
21
+
22
+ def initialize(trn_id:, qr:, opcode:, flags:, rcode:, questions:[], answers:[])
23
+ @trn_id = trn_id
24
+ @qr = qr
25
+ @opcode = opcode
26
+ @flags = flags
27
+ @rcode = rcode
28
+
29
+ questions.each { |q| raise(DnsException, "Questions must be of type Answer!") if !q.is_a?(Question) }
30
+ @questions = questions
31
+
32
+ answers.each { |a| raise(DnsException, "Answers must be of type Answer!") if !a.is_a?(Answer) }
33
+ @answers = answers
34
+ end
35
+
36
+ def add_question(question)
37
+ if !question.is_a?(Question)
38
+ raise(DnsException, "Questions must be of type Question!")
39
+ end
40
+
41
+ @questions << question
42
+ end
43
+
44
+ def add_answer(answer)
45
+ if !answer.is_a?(Answer)
46
+ raise(DnsException, "Questions must be of type Question!")
47
+ end
48
+
49
+ @answers << answer
50
+ end
51
+
52
+ def self.parse(data)
53
+ unpacker = Unpacker.new(data)
54
+ trn_id, full_flags, qdcount, ancount, _, _ = unpacker.unpack("nnnnnn")
55
+
56
+ qr = (full_flags >> 15) & 0x0001
57
+ opcode = (full_flags >> 11) & 0x000F
58
+ flags = (full_flags >> 7) & 0x000F
59
+ rcode = (full_flags >> 0) & 0x000F
60
+
61
+ packet = self.new(
62
+ trn_id: trn_id,
63
+ qr: qr,
64
+ opcode: opcode,
65
+ flags: flags,
66
+ rcode: rcode,
67
+ questions: [],
68
+ answers: [],
69
+ )
70
+
71
+ 0.upto(qdcount - 1) do
72
+ question = Question.unpack(unpacker)
73
+ packet.add_question(question)
74
+ end
75
+
76
+ 0.upto(ancount - 1) do
77
+ answer = Answer.unpack(unpacker)
78
+ packet.add_answer(answer)
79
+ end
80
+
81
+ return packet
82
+ end
83
+
84
+ def answer(answers:[], question:nil)
85
+ question = question || @questions[0]
86
+
87
+ return Packet.new(
88
+ trn_id: @trn_id,
89
+ qr: QR_RESPONSE,
90
+ opcode: OPCODE_QUERY,
91
+ flags: FLAG_RD | FLAG_RA,
92
+ rcode: RCODE_SUCCESS,
93
+ questions: [question],
94
+ answers: answers,
95
+ )
96
+ end
97
+
98
+ def error(rcode:, question:nil)
99
+ question = question || @questions[0]
100
+
101
+ return Packet.new(
102
+ trn_id: @trn_id,
103
+ qr: QR_RESPONSE,
104
+ opcode: OPCODE_QUERY,
105
+ flags: FLAG_RD | FLAG_RA,
106
+ rcode: rcode,
107
+ questions: [question],
108
+ answers: [],
109
+ )
110
+ end
111
+
112
+ def to_bytes()
113
+ packer = Packer.new()
114
+
115
+ full_flags = ((@qr << 15) & 0x8000) |
116
+ ((@opcode << 11) & 0x7800) |
117
+ ((@flags << 7) & 0x0780) |
118
+ ((@rcode << 0) & 0x000F)
119
+
120
+ packer.pack('nnnnnn',
121
+ @trn_id, # trn_id
122
+ full_flags, # qr, opcode, flags, rcode
123
+ @questions.length(), # qdcount
124
+ @answers.length(), # ancount
125
+ 0, # nscount (we don't handle)
126
+ 0, # arcount (we don't handle)
127
+ )
128
+
129
+ questions.each do |q|
130
+ q.pack(packer)
131
+ end
132
+
133
+ answers.each do |a|
134
+ a.pack(packer)
135
+ end
136
+
137
+ return packer.get()
138
+ end
139
+
140
+ def to_s(brief:false)
141
+ if(brief)
142
+ question = @questions[0] || '<unknown>'
143
+
144
+ # Print error packets more clearly
145
+ if(@rcode != RCODE_SUCCESS)
146
+ return "Request for #{question}: error: #{RCODES[@rcode]}"
147
+ end
148
+
149
+ if(@qr == QR_QUERY)
150
+ return "Request for #{question}"
151
+ else
152
+ if(@answers.length == 0)
153
+ return "Response for %s: n/a" % question.to_s
154
+ else
155
+ return "Response for %s: %s (and %d others)" % [
156
+ question.to_s(),
157
+ @answers[0].to_s(),
158
+ @answers.length - 1,
159
+ ]
160
+ end
161
+ end
162
+ end
163
+
164
+ results = []
165
+ results << "DNS %s: id=0x%04x, opcode = %s, flags = %s, rcode = %s, qdcount = 0x%04x, ancount = 0x%04x" % [
166
+ QRS[@qr] || "unknown",
167
+ @trn_id,
168
+ OPCODES[@opcode] || "unknown opcode",
169
+ Nesser::FLAGS(@flags),
170
+ RCODES[@rcode] || "unknown",
171
+ @questions.length,
172
+ @answers.length,
173
+ ]
174
+
175
+ @questions.each do |q|
176
+ results << " Question: %s" % q.to_s()
177
+ end
178
+
179
+ @answers.each do |a|
180
+ results << " Answer: %s" % a.to_s()
181
+ end
182
+
183
+ return results.join("\n")
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,43 @@
1
+ # Encoding: ASCII-8BIT
2
+ ##
3
+ # question.rb
4
+ # Created June 21, 2017
5
+ # By Ron Bowes
6
+ #
7
+ # See: LICENSE.md
8
+ #
9
+ # This defines a DNS question. One question is sent in outgoing packets,
10
+ # and one question is also sent in the response - generally, the same as
11
+ # the question that was asked.
12
+ ##
13
+ module Nesser
14
+ class Question
15
+ attr_reader :name, :type, :cls
16
+
17
+ def initialize(name:, type:, cls:)
18
+ @name = name
19
+ @type = type
20
+ @cls = cls
21
+ end
22
+
23
+ def self.unpack(unpacker)
24
+ name = unpacker.unpack_name()
25
+ type, cls = unpacker.unpack("nn")
26
+
27
+ return self.new(name: name, type: type, cls: cls)
28
+ end
29
+
30
+ def pack(packer)
31
+ packer.pack_name(@name)
32
+ packer.pack('nn', type, cls)
33
+ end
34
+
35
+ def to_s()
36
+ return '%s [%s %s]' % [
37
+ @name,
38
+ TYPES[@type] || '<0x%04x?>' % @type,
39
+ CLSES[@cls] || '<0x%04x?>' % @cls,
40
+ ]
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,290 @@
1
+ # Encoding: ASCII-8BIT
2
+ ##
3
+ # types.rb
4
+ # Created June 20, 2017
5
+ # By Ron Bowes
6
+ #
7
+ # See: LICENSE.md
8
+ ##
9
+
10
+ require 'ipaddr'
11
+
12
+ require 'nesser/packets/constants'
13
+ require 'nesser/dns_exception'
14
+ require 'nesser/packets/packer'
15
+ require 'nesser/packets/unpacker'
16
+
17
+ module Nesser
18
+ class A
19
+ attr_accessor :address
20
+
21
+ def initialize(address:)
22
+ if !address.is_a?(String)
23
+ raise(FormatException, "String required!")
24
+ end
25
+
26
+ begin
27
+ @address = IPAddr.new(address)
28
+ rescue IPAddr::InvalidAddressError => e
29
+ raise(FormatException, "Invalid address: %s" % e)
30
+ end
31
+
32
+ if !@address.ipv4?()
33
+ raise(FormatException, "IPv4 address required!")
34
+ end
35
+ end
36
+
37
+ def self.unpack(unpacker)
38
+ length = unpacker.unpack_one('n')
39
+ if length != 4
40
+ raise(FormatException, "Invalid A record!")
41
+ end
42
+
43
+ data = unpacker.unpack('a4').join()
44
+ return self.new(address: IPAddr.ntop(data))
45
+ end
46
+
47
+ def pack(packer)
48
+ packer.pack('n', 4) # length
49
+ packer.pack('C4', *@address.hton().bytes())
50
+ end
51
+
52
+ def to_s()
53
+ return "#{@address} [A]"
54
+ end
55
+ end
56
+
57
+ class NS
58
+ attr_accessor :name
59
+
60
+ def initialize(name:)
61
+ @name = name
62
+ end
63
+
64
+ def self.unpack(unpacker)
65
+ # We don't really need the name for anything, so just discard it
66
+ unpacker.unpack('n')
67
+
68
+ return self.new(name: unpacker.unpack_name())
69
+ end
70
+
71
+ def pack(packer)
72
+ length = packer.pack_name(@name, dry_run: true)
73
+ packer.pack('n', length)
74
+
75
+ packer.pack_name(@name)
76
+ end
77
+
78
+ def to_s()
79
+ return "#{@name} [NS]"
80
+ end
81
+ end
82
+
83
+ class CNAME
84
+ attr_accessor :name
85
+
86
+ def initialize(name:)
87
+ @name = name
88
+ end
89
+
90
+ def self.unpack(unpacker)
91
+ # We don't really need the name for anything, so just discard it
92
+ unpacker.unpack('n')
93
+
94
+ return self.new(name: unpacker.unpack_name())
95
+ end
96
+
97
+ def pack(packer)
98
+ length = packer.pack_name(@name, dry_run: true)
99
+ packer.pack('n', length)
100
+ packer.pack_name(@name)
101
+ end
102
+
103
+ def to_s()
104
+ return "#{@name} [CNAME]"
105
+ end
106
+ end
107
+
108
+ class SOA
109
+ attr_accessor :primary, :responsible, :serial, :refresh, :retry_interval, :expire, :ttl
110
+
111
+ def initialize(primary:, responsible:, serial:, refresh:, retry_interval:, expire:, ttl:)
112
+ @primary = primary
113
+ @responsible = responsible
114
+ @serial = serial
115
+ @refresh = refresh
116
+ @retry_interval = retry_interval
117
+ @expire = expire
118
+ @ttl = ttl
119
+ end
120
+
121
+ def self.unpack(unpacker)
122
+ length = unpacker.unpack_one('n')
123
+ if length < 22
124
+ raise(FormatException, "Invalid SOA record")
125
+ end
126
+
127
+ primary = unpacker.unpack_name()
128
+ responsible = unpacker.unpack_name()
129
+ serial, refresh, retry_interval, expire, ttl = unpacker.unpack("NNNNN")
130
+
131
+ return self.new(primary: primary, responsible: responsible, serial: serial, refresh: refresh, retry_interval: retry_interval, expire: expire, ttl: ttl)
132
+ end
133
+
134
+ def pack(packer)
135
+ length = packer.pack_name(@primary, dry_run: true) + packer.pack_name(@responsible, dry_run: true, compress: false) + 20
136
+ packer.pack('n', length)
137
+
138
+ packer.pack_name(@primary)
139
+ # It's a pain to calculate the length when both of these can be
140
+ # compressed, so we're just not going to compress the second name
141
+ packer.pack_name(@responsible, compress: false)
142
+ packer.pack("NNNNN", @serial, @refresh, @retry_interval, @expire, @ttl)
143
+ end
144
+
145
+ def to_s()
146
+ return "Primary name server = %s, responsible authority's mailbox: %s, serial number: 0x%08x, refresh interval: 0x%08x, retry interval: 0x%08x, expire limit: 0x%08x, min_ttl: 0x%08x, [SOA]" % [
147
+ @primary,
148
+ @responsible,
149
+ @serial,
150
+ @refresh,
151
+ @retry_interval,
152
+ @expire,
153
+ @ttl,
154
+ ]
155
+ end
156
+ end
157
+
158
+ class MX
159
+ attr_accessor :preference, :name
160
+
161
+ def initialize(name:, preference:)
162
+ @name = name
163
+ @preference = preference
164
+ end
165
+
166
+ def self.unpack(unpacker)
167
+ length = unpacker.unpack_one('n')
168
+ if length < 3
169
+ raise(FormatException, "Invalid MX record")
170
+ end
171
+
172
+ preference = unpacker.unpack_one('n')
173
+ name = unpacker.unpack_name()
174
+
175
+ return self.new(name: name, preference: preference)
176
+ end
177
+
178
+ def pack(packer)
179
+ length = packer.pack_name(@name, dry_run: true) + 2
180
+ packer.pack('n', length)
181
+
182
+ packer.pack('n', @preference)
183
+ packer.pack_name(@name)
184
+ end
185
+
186
+ def to_s()
187
+ return "#{@preference} #{@name} [MX]"
188
+ end
189
+ end
190
+
191
+ class TXT
192
+ attr_accessor :data
193
+
194
+ def initialize(data:)
195
+ @data = data
196
+ end
197
+
198
+ def self.unpack(unpacker)
199
+ length = unpacker.unpack_one('n')
200
+ if length < 1
201
+ raise(FormatException, "Invalid TXT record")
202
+ end
203
+
204
+ len = unpacker.unpack_one("C")
205
+
206
+ if len != length - 1
207
+ raise(FormatException, "Invalid TXT record")
208
+ end
209
+
210
+ data = unpacker.unpack_one("a#{len}")
211
+
212
+ return self.new(data: data)
213
+ end
214
+
215
+ def pack(packer)
216
+ packer.pack('n', @data.length + 1)
217
+
218
+ packer.pack('Ca*', @data.length, @data)
219
+ end
220
+
221
+ def to_s()
222
+ return "#{@data} [TXT]"
223
+ end
224
+ end
225
+
226
+ class AAAA
227
+ attr_accessor :address
228
+
229
+ def initialize(address:)
230
+ if !address.is_a?(String)
231
+ raise(FormatException, "String required!")
232
+ end
233
+
234
+ begin
235
+ @address = IPAddr.new(address)
236
+ rescue IPAddr::InvalidAddressError => e
237
+ raise(FormatException, "Invalid address: %s" % e)
238
+ end
239
+
240
+ if !@address.ipv6?()
241
+ raise(FormatException, "IPv6 address required!")
242
+ end
243
+ end
244
+
245
+ def self.unpack(unpacker)
246
+ length = unpacker.unpack_one('n')
247
+ if length != 16
248
+ raise(FormatException, "Invalid AAAA record")
249
+ end
250
+
251
+ data = unpacker.unpack('a16').join()
252
+ return self.new(address: IPAddr.ntop(data))
253
+ end
254
+
255
+ def pack(packer)
256
+ packer.pack('n', 16)
257
+
258
+ packer.pack('C16', *@address.hton().bytes())
259
+ end
260
+
261
+
262
+ def to_s()
263
+ return "#{@address} [AAAA]"
264
+ end
265
+ end
266
+
267
+ class RRUnknown
268
+ attr_reader :type, :data
269
+ def initialize(type:, data:)
270
+ @type = type
271
+ @data = data
272
+ end
273
+
274
+ def self.unpack(unpacker, type)
275
+ length = unpacker.unpack_one('n')
276
+ data = unpacker.unpack_one("a#{length}")
277
+ return self.new(type: type, data: data)
278
+ end
279
+
280
+ def pack(packer)
281
+ packer.pack('n', @data.length)
282
+
283
+ packer.pack('a*', @data)
284
+ end
285
+
286
+ def to_s()
287
+ return "(Unknown record type 0x%04x: %s)" % [@type, @data]
288
+ end
289
+ end
290
+ end