rubydns 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,62 @@
1
+ = RubyDNS
2
+
3
+ Author:: Samuel Williams (http://www.oriontransfer.co.nz/)
4
+ Copyright:: Copyright (C) 2009 Samuel Williams
5
+ License:: GPLv3
6
+
7
+ RubyDNS is a simple programmatic DSL (domain specific language) for configuring and running a DNS server. RubyDNS provides a daemon that runs a DNS server which can process DNS requests depending on specific policy. Rule selection is based on pattern matching, and results can be hard-coded, computed, fetched from a remote DNS server, fetched from a local cache, etc.
8
+
9
+ RubyDNS provides a full daemon server using RExec. You can either use the built in daemon, customize it to your needs, or specify a full daemon implementation.
10
+
11
+ RubyDNS is not designed to be high-performance and uses a thread-per-request model. This is designed to make it as easy as possible to achieve concurrent performance. This is also due to the fact that many other APIs work best this way (unfortunately).
12
+
13
+ == Basic Example
14
+
15
+ This is copied from <tt>test/example1.rb</tt>. It has been simplified slightly.
16
+
17
+ require 'rubygems'
18
+ require 'rubydns'
19
+
20
+ $R = Resolv::DNS.new
21
+
22
+ RubyDNS::run_server do
23
+ # For this exact address record, return an IP address
24
+ match("dev.mydomain.org", :A) do |transaction|
25
+ transaction.respond!("10.0.0.80")
26
+ end
27
+
28
+ match(/^test([0-9]+).mydomain.org$/, :A) do |match_data, transaction|
29
+ offset = match_data[1].to_i
30
+
31
+ if offset > 0 && offset < 10
32
+ logger.info "Responding with address #{"10.0.0." + (90 + offset).to_s}..."
33
+ transaction.respond!("10.0.0." + (90 + offset).to_s)
34
+ else
35
+ logger.info "Address out of range: #{offset}!"
36
+ false
37
+ end
38
+ end
39
+
40
+ # Default DNS handler
41
+ otherwise do |transaction|
42
+ logger.info "Passing DNS request upstream..."
43
+ transaction.passthrough!($R)
44
+ end
45
+ end
46
+
47
+ After starting this server you can test it using dig:
48
+
49
+ dig @localhost test1.mydomain.org
50
+ dig @localhost dev.mydomain.org
51
+ dig @localhost google.com
52
+
53
+
54
+ == Todo
55
+
56
+ * Better support for logging output
57
+ * Support for TCP connections
58
+ * Support for more features of DNS such as zone transfer
59
+ * Support reverse records more easily?
60
+
61
+
62
+
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright (c) 2009 Samuel Williams. Released under the GNU GPLv3.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+
18
+ # Pulls down DNS data from old-dns
19
+ # rd-dns-check -s old-dns.mydomain.com -d mydomain.com. -f old-dns.yml
20
+
21
+ # Check data against old-dns
22
+ # rd-dns-check -s old-dns.mydomain.com -d mydomain.com. -c old-dns.yml
23
+
24
+ # Check data against new DNS server
25
+ # rd-dns-check -s 10.0.0.36 -d mydomain.com. -c old-dns.yml
26
+
27
+ require 'yaml'
28
+ require 'optparse'
29
+ require 'set'
30
+
31
+ class DNSRecord
32
+ def initialize(arr)
33
+ @record = arr
34
+ normalize
35
+ end
36
+
37
+ def normalize
38
+ @record[0] = @record[0].downcase
39
+ @record[1] = @record[1].upcase
40
+ @record[2] = @record[2].upcase
41
+ @record[3] = @record[3].downcase
42
+ end
43
+
44
+ def hostname
45
+ @record[0]
46
+ end
47
+
48
+ def klass
49
+ @record[1]
50
+ end
51
+
52
+ def type
53
+ @record[2]
54
+ end
55
+
56
+ def value
57
+ @record[3]
58
+ end
59
+
60
+ def is_address?
61
+ ["A", "AAAA"].include?(type)
62
+ end
63
+
64
+ def is_cname?
65
+ return type == "CNAME"
66
+ end
67
+
68
+ def to_s
69
+ "#{hostname.ljust(50)} #{klass.rjust(4)} #{type.rjust(5)} #{value}"
70
+ end
71
+
72
+ def key
73
+ "#{hostname}:#{klass}:#{type}".downcase
74
+ end
75
+
76
+ def to_a
77
+ @record
78
+ end
79
+
80
+ def == other
81
+ return @record == other.to_a
82
+ end
83
+ end
84
+
85
+ def dig(dns_server, cmd, exclude = ["TXT", "HINFO", "SOA", "NS"])
86
+ records = []
87
+
88
+ IO.popen("dig @#{dns_server} +nottlid +nocmd +noall +answer " + cmd) do |p|
89
+ p.each do |line|
90
+ r = line.chomp.split(/\s/, 4)
91
+
92
+ next if exclude.include?(r[2])
93
+
94
+ records << DNSRecord.new(r)
95
+ end
96
+ end
97
+
98
+ return records
99
+ end
100
+
101
+ def retrieve_records(dns_server, dns_root)
102
+ return dig(dns_server, "#{dns_root} AXFR")
103
+ end
104
+
105
+ def resolve_hostname(dns_server, hostname)
106
+ return dig(dns_server, "#{hostname} A").first
107
+ end
108
+
109
+ def resolve_address(dns_server, address)
110
+ return dig(dns_server, "-x #{address}").first
111
+ end
112
+
113
+ def print_summary(records, errors, okay, &block)
114
+ puts "[ Summary ]".center(72, "=")
115
+ puts "Checked #{records.size} record(s). #{errors} errors."
116
+ if errors == 0
117
+ puts "Everything seemed okay."
118
+ else
119
+ puts "The following records are okay:"
120
+ okay.each do |r|
121
+ if block_given?
122
+ yield r
123
+ else
124
+ puts "".rjust(12) + r.to_s
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ # Resolve hostnames to IP address "A" or "AAAA" records.
131
+ # Works through CNAME records in order to find out the final
132
+ # address if possible. Checks for loops in CNAME records.
133
+ def resolve_addresses(records)
134
+ addresses = {}
135
+ cnames = {}
136
+
137
+ # Extract all hostname -> ip address mappings
138
+ records.each do |r|
139
+ if r.is_address?
140
+ addresses[r.hostname] = r
141
+ elsif r.is_cname?
142
+ cnames[r.hostname] = r
143
+ end
144
+ end
145
+
146
+ cnames.each do |hostname, r|
147
+ q = r
148
+ trail = []
149
+ failed = false
150
+
151
+ # Keep track of CNAME records to avoid loops
152
+ while q.is_cname?
153
+ trail << q
154
+ q = cnames[q.value] || addresses[q.value]
155
+
156
+ # Q could be nil at this point, which means there was no address record
157
+ # Q could be already part of the trail, which means there was a loop
158
+ if q == nil || trail.include?(q)
159
+ failed = true
160
+ break
161
+ end
162
+ end
163
+
164
+ if failed
165
+ q = trail.last
166
+ puts "*** Warning: CNAME record #{hostname} does not point to actual address!"
167
+ trail.each_with_index do |r, idx|
168
+ puts idx.to_s.rjust(10) + ": " + r.to_s
169
+ end
170
+ end
171
+
172
+ addresses[r.hostname] = q
173
+ end
174
+
175
+ return addresses, cnames
176
+ end
177
+
178
+ def check_reverse(records, dns_server)
179
+ errors = 0
180
+ okay = []
181
+
182
+ puts "[ Checking Reverse Lookups ]".center(72, "=")
183
+
184
+ records.each do |r|
185
+ next unless r.is_address?
186
+
187
+ sr = resolve_address(dns_server, r.value)
188
+
189
+ if sr == nil
190
+ puts "*** Could not resolve host"
191
+ puts "".rjust(12) + r.to_s
192
+ errors += 1
193
+ elsif r.hostname != sr.value
194
+ puts "*** Hostname does not match"
195
+ puts "Primary: ".rjust(12) + r.to_s
196
+ puts "Secondary: ".rjust(12) + sr.to_s
197
+ errors += 1
198
+ else
199
+ okay << [r, sr]
200
+ end
201
+ end
202
+
203
+ print_summary(records, errors, okay) do |r|
204
+ puts "Primary:".rjust(12) + r[0].to_s
205
+ puts "Secondary:".rjust(12) + r[1].to_s
206
+ end
207
+ end
208
+
209
+ def ping_records(records)
210
+ addresses, cnames = resolve_addresses(records)
211
+
212
+ errors = 0
213
+ okay = []
214
+
215
+ puts "[ Pinging Records ]".center(72, "=")
216
+
217
+ addresses.each do |hostname, r|
218
+ ping = "ping -c 5 -t 5 -i 1 -o #{r.value} > /dev/null"
219
+
220
+ system(ping)
221
+
222
+ if $?.exitstatus == 0
223
+ okay << r
224
+ else
225
+ puts "*** Could not ping host #{hostname.dump}: #{ping.dump}"
226
+ puts "".rjust(12) + r.to_s
227
+ errors += 1
228
+ end
229
+ end
230
+
231
+ print_summary(records, errors, okay)
232
+ end
233
+
234
+ def query_records(primary, secondary_server)
235
+ addresses, cnames = resolve_addresses(primary)
236
+
237
+ okay = []
238
+ errors = 0
239
+
240
+ primary.each do |r|
241
+ sr = resolve_hostname(secondary_server, r.hostname)
242
+
243
+ if sr == nil
244
+ puts "*** Could not resolve hostname #{r.hostname.dump}"
245
+ puts "Primary: ".rjust(12) + r.to_s
246
+
247
+ rsr = resolve_address(secondary_server, (addresses[r.value] || r).value)
248
+ puts "Address: ".rjust(12) + rsr.to_s if rsr
249
+
250
+ errors += 1
251
+ elsif sr.value != r.value
252
+ ra = addresses[r.value] if r.is_cname?
253
+ sra = addresses[sr.value] if sr.is_cname?
254
+
255
+ if (sra || sr).value != (ra || r).value
256
+ puts "*** IP Address does not match"
257
+ puts "Primary: ".rjust(12) + r.to_s
258
+ puts "Resolved: ".rjust(12) + ra.to_s if ra
259
+ puts "Secondary: ".rjust(12) + sr.to_s
260
+ puts "Resolved: ".rjust(12) + sra.to_s if sra
261
+ errors += 1
262
+ end
263
+ else
264
+ okay << r
265
+ end
266
+ end
267
+
268
+ print_summary(primary, errors, okay)
269
+ end
270
+
271
+ def check_records(primary, secondary)
272
+ s = {}
273
+ okay = []
274
+ errors = 0
275
+
276
+ secondary.each do |r|
277
+ s[r.key] = r
278
+ end
279
+
280
+ puts "[ Checking Records ]".center(72, "=")
281
+
282
+ primary.each do |r|
283
+ sr = s[r.key]
284
+
285
+ if sr == nil
286
+ puts "*** Could not find record"
287
+ puts "Primary: ".rjust(12) + r.to_s
288
+ errors += 1
289
+ elsif sr != r
290
+ puts "*** Records are different"
291
+ puts "Primary: ".rjust(12) + r.to_s
292
+ puts "Secondary: ".rjust(12) + sr.to_s
293
+ errors += 1
294
+ else
295
+ okay << r
296
+ end
297
+ end
298
+
299
+ print_summary(primary, errors, okay)
300
+ end
301
+
302
+ OPTIONS = {
303
+ :DNSServer => nil,
304
+ :DNSRoot => ".",
305
+ }
306
+
307
+ ARGV.options do |o|
308
+ script_name = File.basename($0)
309
+
310
+ o.set_summary_indent(' ')
311
+ o.banner = "Usage: #{script_name} [options]"
312
+ o.define_head "This script is designed to test and check DNS servers."
313
+
314
+ o.on("-s ns.my.domain.", "--server ns.my.domain.", String, "The DNS server to query.") { |host| OPTIONS[:DNSServer] = host }
315
+ o.on("-d my.domain.", "--domain my.domain.", String, "The DNS zone to transfer/test.") { |host| OPTIONS[:DNSRoot] = host }
316
+
317
+ o.on("-f output.yml", "--fetch output.yml", String, "Pull down a list of hosts. Filters TXT and HINFO records. DNS transfers must be enabled.") { |f|
318
+ records = retrieve_records(OPTIONS[:DNSServer], OPTIONS[:DNSRoot])
319
+
320
+ output = (f ? File.open(f, "w") : STDOUT)
321
+
322
+ output.write(YAML::dump(records))
323
+
324
+ puts "#{records.size} record(s) retrieved."
325
+ }
326
+
327
+ o.on("-c input.yml", "--check input.yml", String, "Check that the DNS server returns results as specified by the file.") { |f|
328
+ input = (f ? File.open(f) : STDIN)
329
+
330
+ master_records = YAML::load(input.read)
331
+ secondary_records = retrieve_records(OPTIONS[:DNSServer], OPTIONS[:DNSRoot])
332
+
333
+ check_records(master_records, secondary_records)
334
+ }
335
+
336
+ o.on("-q input.yml", "--query input.yml", String, "Query the remote DNS server with all hostnames in the given file, and checks the IP addresses are consistent.") { |f|
337
+ input = (f ? File.open(f) : STDIN)
338
+
339
+ master_records = YAML::load(input.read)
340
+
341
+ query_records(master_records, OPTIONS[:DNSServer])
342
+ }
343
+
344
+ o.on("-p input.yml", "--ping input.yml", String, "Ping all hosts to check if they are available or not.") { |f|
345
+ input = (f ? File.open(f) : STDIN)
346
+
347
+ master_records = YAML::load(input.read)
348
+
349
+ ping_records(master_records)
350
+ }
351
+
352
+ o.on("-r input.yml", "--reverse input.yml", String, "Check that all address records have appropriate reverse entries.") { |f|
353
+ input = (f ? File.open(f) : STDIN)
354
+
355
+ master_records = YAML::load(input.read)
356
+
357
+ check_reverse(master_records, OPTIONS[:DNSServer])
358
+ }
359
+
360
+ o.separator ""
361
+ o.separator "Help and Copyright information"
362
+
363
+ o.on_tail("--copy", "Display copyright information") {
364
+ puts "#{script_name}. Copyright (c) 2008 Samuel Williams. Released under the GPLv3."
365
+ puts "See http://www.oriontransfer.co.nz/ for more information."
366
+ exit
367
+ }
368
+
369
+ o.on_tail("-h", "--help", "Show this help message.") { puts o; exit }
370
+ end.parse!
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright (c) 2009 Samuel Williams. Released under the GNU GPLv3.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require 'rubydns/version'
18
+
19
+ require 'resolv'
20
+ require 'optparse'
21
+
22
+ OPTIONS = {
23
+ :Domains => [],
24
+ :Nameservers => [],
25
+ :Timeout => 0.5,
26
+
27
+ :Threads => 10,
28
+ :Requests => 20
29
+ }
30
+
31
+ ARGV.options do |o|
32
+ script_name = File.basename($0)
33
+
34
+ o.banner = "Usage: #{script_name} [options] nameserver [nameserver]"
35
+
36
+ o.on("-d [path]", String, "Specify a file that contains a list of domains") do |path|
37
+ OPTIONS[:Domains] += File.readlines(path).collect { |name| name.strip.downcase }
38
+ end
39
+
40
+ o.on("-t [timeout]", Float, "Queries that take longer than this will be printed") do |timeout|
41
+ OPTIONS[:Timeout] = timeout.to_f
42
+ end
43
+
44
+ o.on("--threads [count]", Integer, "Number of threads to resolve names concurrently") do |count|
45
+ OPTIONS[:Threads] = count.to_i
46
+ end
47
+
48
+ o.on("--requests [count]", Integer, "Number of requests to perform per thread") do |count|
49
+ OPTIONS[:Requests] = count.to_i
50
+ end
51
+
52
+ o.on_tail("--copy", "Display copyright information") do
53
+ puts "#{script_name} v#{RubyDNS::VERSION::STRING}. Copyright (c) 2009 Samuel Williams. Released under the GPLv3."
54
+ puts "See http://www.oriontransfer.co.nz/ for more information."
55
+
56
+ exit
57
+ end
58
+
59
+ o.on_tail("-h", "--help", "Show this help message.") { puts o; exit }
60
+ end.parse!
61
+
62
+ OPTIONS[:Nameservers] = ARGV
63
+
64
+ if OPTIONS[:Nameservers].size > 0
65
+ $R = Resolv::DNS.new(:nameserver => ARGV)
66
+ else
67
+ $R = Resolv::DNS.new
68
+ end
69
+
70
+ $TG = ThreadGroup.new
71
+ $M = Mutex.new
72
+ $STATUS = {}
73
+ $TOTAL = [0.0, 0]
74
+
75
+ if OPTIONS[:Domains].size == 0
76
+ OPTIONS[:Domains] += ["www.google.com", "www.amazon.com", "www.apple.com", "www.microsoft.com"]
77
+ OPTIONS[:Domains] += ["www.rubygems.org", "www.ruby-lang.org", "www.slashdot.org", "www.lucidsystems.org"]
78
+ OPTIONS[:Domains] += ["www.facebook.com", "www.twitter.com", "www.myspace.com", "www.youtube.com"]
79
+ OPTIONS[:Domains] += ["www.oriontransfer.co.nz", "www.digg.com"]
80
+ end
81
+
82
+ def random_domain
83
+ d = OPTIONS[:Domains]
84
+
85
+ d[rand(d.size - 1)]
86
+ end
87
+
88
+ puts "Starting test with #{OPTIONS[:Domains].size} domains..."
89
+ puts "Using nameservers: " + OPTIONS[:Nameservers].join(", ")
90
+ puts "Only long running queries will be printed..."
91
+
92
+ def resolve_domain
93
+ s = Time.now
94
+ result = nil
95
+ name = random_domain
96
+
97
+ begin
98
+ result = [$R.getaddress(name)]
99
+ rescue Resolv::ResolvError
100
+ $M.synchronize do
101
+ puts "Name #{name} failed to resolve!"
102
+ $STATUS[name] ||= []
103
+ $STATUS[name] << :failure
104
+
105
+ if $STATUS[name].include?(:success)
106
+ puts "Name #{name} has had previous successes!"
107
+ end
108
+ end
109
+
110
+ return
111
+ end
112
+
113
+ result.unshift(name)
114
+ result.unshift(Time.now - s)
115
+
116
+ $M.synchronize do
117
+ $TOTAL[0] += result[0]
118
+ $TOTAL[1] += 1
119
+
120
+ if result[0] > OPTIONS[:Timeout]
121
+ puts "\t\t%0.2fs: %s => %s" % result
122
+ end
123
+
124
+ $STATUS[name] ||= []
125
+ $STATUS[name] << :success
126
+
127
+ if $STATUS[name].include?(:failure)
128
+ puts "Name #{name} has had previous failures!"
129
+ end
130
+ end
131
+ end
132
+
133
+ puts "Starting threads..."
134
+ Thread.abort_on_exception = true
135
+
136
+ OPTIONS[:Threads].times do
137
+ th = Thread.new do
138
+ OPTIONS[:Requests].times do
139
+ resolve_domain
140
+ end
141
+ end
142
+
143
+ $TG.add th
144
+ end
145
+
146
+ $TG.list.each { |thr| thr.join }
147
+
148
+ $STATUS.each do |name, results|
149
+ if results.include?(:failure)
150
+ puts "Name #{name} failed at least once!"
151
+ end
152
+ end
153
+
154
+ puts
155
+ puts "Requests: #{$TOTAL[1]} Average time: #{$TOTAL[0] / $TOTAL[1]}"