nesser 0.0.1

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