nesser 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2647db1767dd49b7498c90b61a6db3e42ddd0baa
4
- data.tar.gz: 1dca698d183e7e4415ec2cbc2dd75babbab39919
3
+ metadata.gz: 7c6c93d335d6983d60154b25ed5251f122a30534
4
+ data.tar.gz: 4d8dca606688b56847e0e82712e92b5467173659
5
5
  SHA512:
6
- metadata.gz: 05a2335a136d62fa438592616af81e0a4d1aecf23b8a3c4cfb65e35d024d2af862e08bc62957f54f0db9de85795cdea42a06f46a133e4b2692c4c3f2207b85c1
7
- data.tar.gz: cf8809e713363f6497bf0ecb7caafa9ab4643cbfbb658d926724519b9939c3d7dd6fbe552896f59fbb31b43338733d2f51b9db593fed6fca0a51bb50bce7359c
6
+ metadata.gz: c048c7eccf69dcd6091308ab2d95b485a3706a02749b5b20ef97f15c104dec50eb56ae20f52df64dd2d1f2bba2faf9cd0c5092f07922ffd2491b7431a88c5017
7
+ data.tar.gz: 0d18a1aee601408787d45d87bd3f0477ed89219cf5e7baa08db44115d27a333390647a27ec318311890a2922f4e448cb72f5c58db1267e70dec6ac7dab0e357c
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2013-2017, Ron Bowes
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+ - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+ - Neither the name of the organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9
+
10
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -20,9 +20,160 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
- TODO: Write usage instructions here
23
+ ### Client
24
+
25
+ After installing the gem, using it as a client is pretty straight forward.
26
+ I wrote an example file in [lib/nesser/examples](lib/nesser/examples), but in
27
+ a nutshell, here's the code:
28
+
29
+ ```ruby
30
+ require 'socket'
31
+ require 'nesser'
32
+
33
+ # Create a UDP socket
34
+ s = UDPSocket.new()
35
+
36
+ # Perform a query for 'google.com', of type ANY. I chose this because it has a
37
+ # wide array of varied answers.
38
+ #
39
+ # See packets/constants.rb for a full list of possible request types.
40
+ result = Nesser::Nesser.query(s: s, hostname: 'google.com', type: Nesser::TYPE_ANY)
41
+
42
+ # Print the result
43
+ puts result
44
+ ```
45
+
46
+ You can find all constant definitions in
47
+ [the constants.rb file](lib/nesser/packets/constants.rb)
48
+
49
+ The return from Nesser.query() is an instance of
50
+ [Nesser::Answer](lib/nesser/packets/answer.rb).
51
+
52
+ ### Server
53
+
54
+ Writing a server is a little more complicated, because you have to deal with
55
+ much more of the nitty gritty DNS stuff.
56
+
57
+ To start a server, create an instance of Nesser, and pass in a block that
58
+ handles queries. From [examples/server.rb](lib/nesser/examples/server.rb):
59
+
60
+ ```ruby
61
+ nesser = Nesser::Nesser.new(s: s) do |transaction|
62
+ puts transaction
63
+
64
+ # ...
65
+ end
66
+ nesser.wait()
67
+ ```
68
+
69
+ This is called once per packet received - since many servers send the same
70
+ request several times, that has to be handled by the application. Since the
71
+ function starts a thread and returns instantly, `nesser.wait()` is necessary
72
+ so the program doesn't end until the thread does.
73
+
74
+ The transaction is an instance of [Nesser::Transaction](lib/nesser/transaction.rb).
75
+ Any methods of transaction that end with an exclamation mark
76
+ (`transaction.answer!()`, for example) will send a response and can only be
77
+ called once, after which the transaction is done and shouldn't be used anymore.
78
+
79
+ The request packet can be accessed via `transaction.request`, and the response
80
+ can be directly accessed (or changed, though I don't recommend that) via
81
+ `transaction.response` and `transaction.response=()`. Both are instances of
82
+ [Nesser::Packet](lib/nesser/packets/packet.rb). The response already has the
83
+ appropriate `trn_id` and `flags` and the `question`, so all you have to do is
84
+ add answers and send it off using `transaction.reply!()`.
85
+
86
+ A much easier way to reply to a request is to use one of the two helper
87
+ functions, `transaction.answer!()` or `transaction.error!()`. Each of these
88
+ will take the `transaction.response` packet, with whatever changes have been
89
+ made to it, add to it, and send it off.
90
+
91
+ `transaction.answer!()` takes an optional array of
92
+ [Nesser::Answer](lib/nesser/packets/answer.rb), adds them to the packet, then sends
93
+ it.
94
+
95
+ `transaction.error!()` takes a response code (see
96
+ [constants.rb](lib/nesser/packets/constants.rb) for the list), updates the
97
+ packet with that code, then sends it.
98
+
99
+ You'll rarely need to do anything with the transaction other than inspecting the
100
+ request and using one of those two functions to answer.
101
+
102
+ Here's a full example that replies to requests for test.com with '1.2.3.4' and
103
+ sends a "name not found" error for anything else:
104
+
105
+ ```ruby
106
+ require 'socket'
107
+ require 'nesser'
108
+
109
+ # Create a UDP socket
110
+ s = UDPSocket.new()
111
+
112
+ # Create a new instance of Nesser to handle transactions
113
+ nesser = Nesser::Nesser.new(s: s) do |transaction|
114
+ # We only have an answer for 'test.com' (this is, after all, an example)
115
+ if transaction.request.questions[0].name == 'test.com'
116
+ # Create an A-type resource record pointing to 1.2.3.4. See README.md for
117
+ # details on how to create other record types
118
+ rr = Nesser::A.new(address: '1.2.3.4')
119
+
120
+ # Create an answer. The name will almost always be the same as the original
121
+ # name, but the type and cls don't necessarily have to match the request
122
+ # type (in this case, we don't even check what the request type was).
123
+ #
124
+ # You'll probably want the rr's type to match the type: argument. I'm not
125
+ # sure if it'll work otherwise, but the client it's sent to sure as heck
126
+ # won't know what to do with it. :)
127
+ answer = Nesser::Answer.new(
128
+ name: 'test.com',
129
+ type: Nesser::TYPE_A, # See constants.rb for other options
130
+ cls: Nesser::CLS_IN,
131
+ ttl: 1337,
132
+ rr: rr,
133
+ )
134
+
135
+ # The transaction's functions that end with '!' actually send the message -
136
+ # in this case, answer!() sends an array of the one answre that we created.
137
+ transaction.answer!([answer])
138
+ else
139
+ # Response NXDomain - aka, no such domain name - to everything other than
140
+ # 'test.com'.
141
+ transaction.error!(Nesser::RCODE_NAME_ERROR)
142
+ end
143
+
144
+ # Display the transaction
145
+ puts(transaction)
146
+ end
147
+
148
+ # Since Nesser::Nesser.new() runs in a new thread, we have to basically join
149
+ # the thread to prevent the program from ending
150
+ nesser.wait()
151
+ ```
152
+
153
+ We currently support A, NS, CNAME, SOA, MX, TXT, and AAAA records. We can also
154
+ parse and send unknown types as well. You can find the definitions in
155
+ [rr_types.rb](lib/nesser/packets/rr_types.rb).
156
+
157
+ For quick reference:
158
+
159
+ * `a = Nesser::A.new(address: '1.2.3.4')`
160
+ * `ns = Nesser::NS.new(name: 'google.com')`
161
+ * `cname = Nesser::CNAME.new(name: 'google.com')`
162
+ * `soa = Nesser::SOA.new(primary: 'google.com', responsible: 'test.google.com', serial: 1, refresh: 2, retry_interval: 3, expire: 4, ttl: 5)`
163
+ * `mx = Nesser::MX.new(name: 'mail.google.com', preference: 10)`
164
+ * `txt = Nesser::TXT.new(data: 'hello this is data!')`
165
+ * `aaaa = Nesser::AAAA.new(address: '::1')`
166
+ * `unknown = Nesser::RRUnknown.new(type: 0x1337, data: 'datagoeshere')`
24
167
 
25
168
  ## Contributing
26
169
 
27
170
  Bug reports and pull requests are welcome on GitHub at https://github.com/iagox86/nesser
28
171
 
172
+ Please try to follow my style as much as possible, and update test coverage
173
+ when necessary!
174
+
175
+ ## Version history / changelog
176
+
177
+ * 0.0.1 - Test deploy
178
+ * 0.0.2 - Code complete
179
+ * 0.0.3 - First actual release
@@ -16,9 +16,9 @@
16
16
  # There are two methods for using this library: as a client (to make a query)
17
17
  # or as a server (to listen for queries and respond to them).
18
18
  #
19
- # To make a query, use Nesser.query:
20
- #
21
- # ...TODO...
19
+ # To avoid putting too much narrative in this header, I wrote full usage
20
+ # details in README.md in the root directory. Check that out for full usage
21
+ # details!
22
22
  ##
23
23
 
24
24
  require 'ipaddr'
@@ -35,6 +35,35 @@ module Nesser
35
35
  class Nesser
36
36
  attr_reader :thread
37
37
 
38
+ ##
39
+ # Create a new instance and start listening for requests in a new thread.
40
+ # Returns instantly (use the `wait()` method to pause until the thread is
41
+ # over (pretty much indefinitely).
42
+ #
43
+ # The `s` parameter should be an instance of `UDPSocket` in `socket`. The
44
+ # logger will default to an instance of `Nesser::Logger` if it's not
45
+ # specified, which simply logs to stdout.
46
+ #
47
+ # The other parameters are reasonably self explanatory.
48
+ #
49
+ # When you create an instance, you must also specify a proc:
50
+ #
51
+ # ```ruby
52
+ # Nesser::Nesser.new(s: s) do |transaction|
53
+ # # ...
54
+ # end
55
+ # ```
56
+ #
57
+ # Whenever a valid DNS message comes in, a new `Nesser::Transaction` is
58
+ # created and the proc is called with it (an invalid packet will be printed
59
+ # to the logger and discarded). It's up to the user to answer it using
60
+ # `transaction.answer!()`, `transaction.error!()`, etc. (see README.md for
61
+ # examples).
62
+ #
63
+ # If the handler function throws an Exception (most non-system errors),
64
+ # a SERVFAIL message will be returned automatically and the error will be
65
+ # logged to the logger.
66
+ ##
38
67
  def initialize(s:, logger: nil, host:"0.0.0.0", port:53)
39
68
  @s = s
40
69
  @s.bind(host, port)
@@ -42,48 +71,47 @@ module Nesser
42
71
  @logger = (logger = logger || Logger.new())
43
72
 
44
73
  @thread = Thread.new() do
45
- begin
46
- loop do
47
- # Grab all the data we can off the socket
48
- data = @s.recvfrom(65536)
49
-
50
- begin
51
- # Data is an array where the first element is the actual data, and the second is the host/port
52
- request = Packet.parse(data[0])
53
- rescue DnsException => e
54
- logger.error("Failed to parse the DNS packet: %s" % e.to_s())
55
- next
56
- rescue Exception => e
57
- logger.error("Error: %s" % e.to_s())
58
- logger.info(e.backtrace().join("\n"))
59
- end
74
+ loop do
75
+ # Grab all the data we can off the socket
76
+ data = @s.recvfrom(65536)
77
+
78
+ begin
79
+ # Data is an array where the first element is the actual data, and the second is the host/port
80
+ request = Packet.parse(data[0])
81
+ rescue DnsException => e
82
+ logger.error("Failed to parse the DNS packet: %s" % e.to_s())
83
+ next
84
+ rescue Exception => e
85
+ logger.error("Error: %s" % e.to_s())
86
+ logger.info(e.backtrace().join("\n"))
87
+ end
60
88
 
61
- # Create a transaction object, which we can use to respond
62
- transaction = Transaction.new(
63
- s: @s,
64
- request_packet: request,
65
- host: data[1][3],
66
- port: data[1][1],
67
- )
68
-
69
- begin
70
- proc.call(transaction)
71
- rescue StandardError => e
72
- logger.error("Error thrown while processing the DNS packet: %s" % e.to_s())
73
- logger.info(e.backtrace().join("\n"))
74
-
75
- if transaction.open?()
76
- transaction.error!(RCODE_SERVER_FAILURE)
77
- end
89
+ # Create a transaction object, which we can use to respond
90
+ transaction = Transaction.new(
91
+ s: @s,
92
+ request: request,
93
+ host: data[1][3],
94
+ port: data[1][1],
95
+ )
96
+
97
+ begin
98
+ proc.call(transaction)
99
+ rescue Exception => e
100
+ logger.error("Error thrown while processing the DNS packet: %s" % e.to_s())
101
+ logger.info(e.backtrace().join("\n"))
102
+
103
+ if transaction.open?()
104
+ transaction.error!(RCODE_SERVER_FAILURE)
78
105
  end
79
106
  end
80
- ensure
81
- @s.close()
82
107
  end
83
108
  end
84
109
  end
85
110
 
86
- # Kill the listener
111
+ ##
112
+ # Kill the listener thread (once this is called, the class instance is
113
+ # worthless).
114
+ ##
87
115
  def stop()
88
116
  if(@thread.nil?)
89
117
  @logger.error("Tried to stop a listener that wasn't listening!")
@@ -94,8 +122,10 @@ module Nesser
94
122
  @thread = nil
95
123
  end
96
124
 
97
- # After calling on_request(), this can be called to halt the program's
98
- # execution until the thread is stopped.
125
+ ##
126
+ # Pauses as long as the listener thread is alive (generally, that means
127
+ # indefinitely).
128
+ ##
99
129
  def wait()
100
130
  if(@thread.nil?)
101
131
  @logger.error("Tried to wait on a Nesser instance that wasn't listening!")
@@ -105,7 +135,18 @@ module Nesser
105
135
  @thread.join()
106
136
  end
107
137
 
108
- # Send out a query
138
+ ##
139
+ # Send a query.
140
+ #
141
+ # * `s`: an instance of `UDPSocket` from 'socket'.
142
+ # * `hostname`: the name being queried - eg, 'example.org'.
143
+ # * `server` and `port`: The upstream DNS server to query.
144
+ # * `type` and `cls`: typical DNS values, you can find a list of them in
145
+ # packets/constants.rb.
146
+ # * `timeout`: The number of seconds to wait for a response before giving up.
147
+ #
148
+ # Returns a Nesser::Packet (nesser/packets/packet.rb).
149
+ ##
109
150
  def self.query(s:, hostname:, server: '8.8.8.8', port: 53, type: TYPE_A, cls: CLS_IN, timeout: 3)
110
151
  s = UDPSocket.new()
111
152
 
@@ -11,8 +11,16 @@
11
11
  $LOAD_PATH.unshift File.expand_path('../../../', __FILE__)
12
12
 
13
13
  require 'socket'
14
-
15
14
  require 'nesser'
16
15
 
16
+ # Create a UDP socket
17
17
  s = UDPSocket.new()
18
- puts Nesser::Nesser.query(s: s, hostname: 'google.com', type: Nesser::TYPE_ANY)
18
+
19
+ # Perform a query for 'google.com', of type ANY. I chose this because it has a
20
+ # wide array of varied answers.
21
+ #
22
+ # See packets/constants.rb for a full list of possible request types.
23
+ result = Nesser::Nesser.query(s: s, hostname: 'google.com', type: Nesser::TYPE_ANY)
24
+
25
+ # Print the result
26
+ puts result
@@ -13,21 +13,45 @@ $LOAD_PATH.unshift File.expand_path('../../../', __FILE__)
13
13
  require 'socket'
14
14
  require 'nesser'
15
15
 
16
+ # Create a UDP socket
16
17
  s = UDPSocket.new()
17
18
 
19
+ # Create a new instance of Nesser to handle transactions
18
20
  nesser = Nesser::Nesser.new(s: s) do |transaction|
19
- if transaction.request_packet.questions[0].name == 'test.com'
20
- transaction.answer!([Nesser::Answer.new(
21
+ # We only have an answer for 'test.com' (this is, after all, an example)
22
+ if transaction.request.questions[0].name == 'test.com'
23
+ # Create an A-type resource record pointing to 1.2.3.4. See README.md for
24
+ # details on how to create other record types
25
+ rr = Nesser::A.new(address: '1.2.3.4')
26
+
27
+ # Create an answer. The name will almost always be the same as the original
28
+ # name, but the type and cls don't necessarily have to match the request
29
+ # type (in this case, we don't even check what the request type was).
30
+ #
31
+ # You'll probably want the rr's type to match the type: argument. I'm not
32
+ # sure if it'll work otherwise, but the client it's sent to sure as heck
33
+ # won't know what to do with it. :)
34
+ answer = Nesser::Answer.new(
21
35
  name: 'test.com',
22
- type: Nesser::TYPE_A,
36
+ type: Nesser::TYPE_A, # See constants.rb for other options
23
37
  cls: Nesser::CLS_IN,
24
38
  ttl: 1337,
25
- rr: Nesser::A.new(address: '1.2.3.4')
26
- )])
39
+ rr: rr,
40
+ )
41
+
42
+ # The transaction's functions that end with '!' actually send the message -
43
+ # in this case, answer!() sends an array of the one answre that we created.
44
+ transaction.answer!([answer])
27
45
  else
46
+ # Response NXDomain - aka, no such domain name - to everything other than
47
+ # 'test.com'.
28
48
  transaction.error!(Nesser::RCODE_NAME_ERROR)
29
49
  end
50
+
51
+ # Display the transaction
30
52
  puts(transaction)
31
53
  end
32
54
 
55
+ # Since Nesser::Nesser.new() runs in a new thread, we have to basically join
56
+ # the thread to prevent the program from ending
33
57
  nesser.wait()
@@ -1,12 +1,12 @@
1
1
  # Encoding: ASCII-8BIT
2
2
  ##
3
- # dns_exception.rb
3
+ # logger.rb.rb
4
4
  # Created June 20, 2017
5
5
  # By Ron Bowes
6
6
  #
7
7
  # See LICENSE.md
8
8
  #
9
- # Implements a simple exception class for dns errors.
9
+ # A super simple logger implementation.
10
10
  ##
11
11
 
12
12
  module Nesser
@@ -6,15 +6,26 @@
6
6
  #
7
7
  # See: LICENSE.md
8
8
  #
9
- # A DNS answer. A DNS response packet contains zero or more Answer records
10
- # (defined by the 'ancount' value in the header). An answer contains the
11
- # name of the domain from the question, followed by a resource record.
9
+ # A DNS answer. A DNS packet contains zero or more Answer records (defined in
10
+ # the 'ancount' value in the header). An answer contains the name of the domain
11
+ # followed by a resource record.
12
12
  ##
13
13
 
14
14
  module Nesser
15
15
  class Answer
16
16
  attr_reader :name, :type, :cls, :ttl, :rr
17
17
 
18
+ ##
19
+ # Create an answer.
20
+ #
21
+ # * `name`: Should match the name from the question.
22
+ # * `type`: The type of resource record (eg, TYPE_A, TYPE_NS, etc). You can
23
+ # find a list of types in constants.rb.
24
+ # * `cls`: The DNS class - this will almost certainly be `Nesser::CLS_IN`,
25
+ # since 'IN' means 'Internet'. I'm not familiar with any others.
26
+ # * `ttl`: The time-to-live for the response, in seconds
27
+ # * `rr`: A resource record - you can find these classes in rr_types.rb.
28
+ ##
18
29
  def initialize(name:, type:, cls:, ttl:, rr:)
19
30
  @name = name
20
31
  @type = type
@@ -23,6 +34,11 @@ module Nesser
23
34
  @rr = rr
24
35
  end
25
36
 
37
+ ##
38
+ # Parse an answer from a DNS packet. You won't likely need to use this, but
39
+ # if you do, it's necessary to use a Nesser::Unpacker that's loaded with the
40
+ # full DNS message (due to in-packet pointers).
41
+ ##
26
42
  def self.unpack(unpacker)
27
43
  name = unpacker.unpack_name()
28
44
  type, cls, ttl = unpacker.unpack("nnN")
@@ -55,6 +71,10 @@ module Nesser
55
71
  )
56
72
  end
57
73
 
74
+ ##
75
+ # Pack this into a Nesser::Packer in preparation for being sent over the
76
+ # wire.
77
+ ##
58
78
  def pack(packer)
59
79
  # The name is echoed back
60
80
  packer.pack_name(@name)
@@ -5,6 +5,9 @@
5
5
  # By Ron Bowes
6
6
  #
7
7
  # See: LICENSE.md
8
+ #
9
+ # A bunch of constants used with DNS - some of them for the protocol, others for
10
+ # error checking the outgoing messages.
8
11
  ##
9
12
 
10
13
  module Nesser
@@ -21,6 +24,8 @@ module Nesser
21
24
  MAX_SEGMENT_LENGTH = 63
22
25
  MAX_TOTAL_LENGTH = 253
23
26
 
27
+ # DNS classes - I only define IN (Internet) because I don't even know what to
28
+ # do with others.
24
29
  CLS_IN = 0x0001 # Internet
25
30
  CLSES = {
26
31
  CLS_IN => "IN",
@@ -42,7 +47,6 @@ module Nesser
42
47
  RCODE_NAME_ERROR = 0x0003 # :NXDomain
43
48
  RCODE_NOT_IMPLEMENTED = 0x0004
44
49
  RCODE_REFUSED = 0x0005
45
-
46
50
  RCODES = {
47
51
  RCODE_SUCCESS => ":NoError (RCODE_SUCCESS)",
48
52
  RCODE_FORMAT_ERROR => ":FormErr (RCODE_FORMAT_ERROR)",
@@ -56,7 +60,6 @@ module Nesser
56
60
  OPCODE_QUERY = 0x0000
57
61
  OPCODE_IQUERY = 0x0800
58
62
  OPCODE_STATUS = 0x1000
59
-
60
63
  OPCODES = {
61
64
  OPCODE_QUERY => "OPCODE_QUERY",
62
65
  OPCODE_IQUERY => "OPCODE_IQUERY",
@@ -72,7 +75,6 @@ module Nesser
72
75
  TYPE_TXT = 0x0010
73
76
  TYPE_AAAA = 0x001c
74
77
  TYPE_ANY = 0x00FF
75
-
76
78
  TYPES = {
77
79
  TYPE_A => "A",
78
80
  TYPE_NS => "NS",
@@ -24,11 +24,18 @@ module Nesser
24
24
  @segment_cache = {}
25
25
  end
26
26
 
27
+ ##
28
+ # This is simply a wrapper around String.pack() - it's for packing perfectly
29
+ # ordinary data into a DNS packet.
30
+ ##
27
31
  public
28
32
  def pack(format, *data)
29
33
  @data += data.pack(format)
30
34
  end
31
35
 
36
+ ##
37
+ # Sanity check a name (length, legal characters, etc).
38
+ ##
32
39
  private
33
40
  def validate!(name)
34
41
  if name.chars.detect { |ch| !LEGAL_CHARACTERS.include?(ch) }
@@ -44,9 +51,37 @@ module Nesser
44
51
  end
45
52
  end
46
53
 
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!
54
+ ##
55
+ # This function is sort of the point of this class's existance.
56
+ #
57
+ # You pass in a typical DNS name, such as "google.com". If that name
58
+ # doesn't appear in the packet yet, it's simply encoded with length-
59
+ # prefixed segments - "\x06google\x03com\x00".
60
+ #
61
+ # However, if all or part of the name already exist in the packet, this will
62
+ # save packet space by re-using those segments. For example, let's say that
63
+ # "google.com" exists 0x0c bytes into the packet (which it normally does).
64
+ # In that case, instead of including "\x06google\x03com\x00" a second time,
65
+ # it will simply encode the offset with "\xc0" in front - "\xc0\x0c".
66
+ #
67
+ # Let's say that later in the packet, we have "www.google.com". That string
68
+ # as a whole hasn't appeared yet, but "google.com" appeared at offset 0x0c.
69
+ # It will then be encoded "\x03www\xc0\x0c" - the longest possible segment
70
+ # is encoded.
71
+ #
72
+ # This logic is somewhat complicated, but this function seems to work
73
+ # pretty well. :)
74
+ #
75
+ # * `name`: The name to encode, as a normal dotted string.
76
+ # * `dry_run`: If set to true, don't actually "write" the name. This is
77
+ # unfortunately needed to check the size of something that *would* be
78
+ # encoded, since we occasionally need the length written to the buffer
79
+ # before the name.
80
+ # * `compress`: If set (which it always probably should be), will attempt
81
+ # to do the compression ("\xc0") stuff discussed earlier.
82
+ #
83
+ # Returns the actual length of the name.
84
+ ##
50
85
  public
51
86
  def pack_name(name, dry_run:false, compress:true)
52
87
  length = 0
@@ -89,6 +124,9 @@ module Nesser
89
124
  return length
90
125
  end
91
126
 
127
+ ##
128
+ # Retrieve the buffer as a string.
129
+ ##
92
130
  public
93
131
  def get()
94
132
  return @data
@@ -5,6 +5,9 @@
5
5
  # By Ron Bowes
6
6
  #
7
7
  # See: LICENSE.md
8
+ #
9
+ # An implementation of a DNS packet, including encoding and parsing. This covers
10
+ # the header and questions/answers.
8
11
  ##
9
12
 
10
13
  require 'nesser/dns_exception'
@@ -19,6 +22,25 @@ module Nesser
19
22
  class Packet
20
23
  attr_accessor :trn_id, :qr, :opcode, :flags, :rcode, :questions, :answers
21
24
 
25
+ ##
26
+ # Create a new packet.
27
+ #
28
+ # * `trn_id`: The 16-bit transaction id - should be random for clients, and
29
+ # match the incoming trn_id for servers.
30
+ # * `qr`: QR_QUERY or QR_RESPONSE.
31
+ # * `opcode`: will likely be OPCODE_QUERY.
32
+ # * `flags`: A combination of the flags FLAG_AA, FLAG_TC, FLAG_RD, and
33
+ # FLAG_RA.
34
+ # * `rcode`: A response code - RCODE_SUCCESS for requests or good responses,
35
+ # or an error code (RCODE_NAME_ERROR, RCODE_SERVER_FAILURE, etc) for
36
+ # errors. Find the list in constants.rb.
37
+ # * `questions`: An array (although most implementations only handle exactly
38
+ # one) of questions - Nesser::Question.
39
+ # * `answers`: An array of zero or more ansewrs - Nesser::Answer.
40
+ #
41
+ # We don't support authority or additional records right now (or perhaps
42
+ # ever).
43
+ ##
22
44
  def initialize(trn_id:, qr:, opcode:, flags:, rcode:, questions:[], answers:[])
23
45
  @trn_id = trn_id
24
46
  @qr = qr
@@ -33,6 +55,9 @@ module Nesser
33
55
  @answers = answers
34
56
  end
35
57
 
58
+ ##
59
+ # Add a Nesser::Question.
60
+ ##
36
61
  def add_question(question)
37
62
  if !question.is_a?(Question)
38
63
  raise(DnsException, "Questions must be of type Question!")
@@ -41,6 +66,9 @@ module Nesser
41
66
  @questions << question
42
67
  end
43
68
 
69
+ ##
70
+ # Add a Nesser::Answer.
71
+ ##
44
72
  def add_answer(answer)
45
73
  if !answer.is_a?(Answer)
46
74
  raise(DnsException, "Questions must be of type Question!")
@@ -49,6 +77,12 @@ module Nesser
49
77
  @answers << answer
50
78
  end
51
79
 
80
+ ##
81
+ # Parse an incoming DNS packet. Takes a byte string as an argument, and
82
+ # returns an instance of Nesser::Packet - this class.
83
+ #
84
+ # Raises a DnsException if things go badly.
85
+ ##
52
86
  def self.parse(data)
53
87
  unpacker = Unpacker.new(data)
54
88
  trn_id, full_flags, qdcount, ancount, _, _ = unpacker.unpack("nnnnnn")
@@ -81,6 +115,11 @@ module Nesser
81
115
  return packet
82
116
  end
83
117
 
118
+ ##
119
+ # Convert a query packet to the corresponding answer - the trn_id is copied,
120
+ # the qr is changed to QR_RESPONSE, the opcode and flags are updated, and
121
+ # the question from the query is added.
122
+ ##
84
123
  def answer(answers:[], question:nil)
85
124
  question = question || @questions[0]
86
125
 
@@ -95,6 +134,10 @@ module Nesser
95
134
  )
96
135
  end
97
136
 
137
+ ##
138
+ # Convert a query packet to the corresponding error answer with the given
139
+ # rcode (see constants.rb for a list of rcodes).
140
+ ##
98
141
  def error(rcode:, question:nil)
99
142
  question = question || @questions[0]
100
143
 
@@ -109,6 +152,9 @@ module Nesser
109
152
  )
110
153
  end
111
154
 
155
+ ##
156
+ # Serialize the packet to an array of bytes.
157
+ ##
112
158
  def to_bytes()
113
159
  packer = Packer.new()
114
160
 
@@ -6,20 +6,29 @@
6
6
  #
7
7
  # See: LICENSE.md
8
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.
9
+ # This defines a DNS question. Typically, a single question is sent in both
10
+ # incoming and outgoing packets. Most implementations can't handle any other
11
+ # situation.
12
12
  ##
13
13
  module Nesser
14
14
  class Question
15
15
  attr_reader :name, :type, :cls
16
16
 
17
+ ##
18
+ # Create a question.
19
+ #
20
+ # The name is a typical dotted name, like "google.com". The type and cls
21
+ # are DNS-specific values that can be found in constants.rb.
22
+ ##
17
23
  def initialize(name:, type:, cls:)
18
24
  @name = name
19
25
  @type = type
20
26
  @cls = cls
21
27
  end
22
28
 
29
+ ##
30
+ # Parse a question from a DNS packet.
31
+ ##
23
32
  def self.unpack(unpacker)
24
33
  name = unpacker.unpack_name()
25
34
  type, cls = unpacker.unpack("nn")
@@ -27,6 +36,9 @@ module Nesser
27
36
  return self.new(name: name, type: type, cls: cls)
28
37
  end
29
38
 
39
+ ##
40
+ # Serialize the question.
41
+ ##
30
42
  def pack(packer)
31
43
  packer.pack_name(@name)
32
44
  packer.pack('nn', type, cls)
@@ -5,6 +5,16 @@
5
5
  # By Ron Bowes
6
6
  #
7
7
  # See: LICENSE.md
8
+ #
9
+ # These are implementations of resource records - ie, the records found in a DNS
10
+ # answer that contain, for example, an ip address, a mail exchange, etc.
11
+ #
12
+ # Every one of these classes follows the same paradigm (I guess in Java you'd
13
+ # say they implement the same interface). They can be initialized with
14
+ # type-dependent parameters; they implement `self.unpack()`, which takes a
15
+ # `Nesser::Unpacker` and returns an instance of itself; they implement `pack()`,
16
+ # which serialized itself into a `Nesser::Packer` instance; and they have a
17
+ # `to_s()` function, which stringifies the record in a fairly user-friendly way.
8
18
  ##
9
19
 
10
20
  require 'ipaddr'
@@ -15,6 +25,9 @@ require 'nesser/packets/packer'
15
25
  require 'nesser/packets/unpacker'
16
26
 
17
27
  module Nesser
28
+ ##
29
+ # An A record is a typical IPv4 address - eg, '1.2.3.4'.
30
+ ##
18
31
  class A
19
32
  attr_accessor :address
20
33
 
@@ -54,6 +67,9 @@ module Nesser
54
67
  end
55
68
  end
56
69
 
70
+ ##
71
+ # Nameserver record: eg, 'ns1.google.com'.
72
+ ##
57
73
  class NS
58
74
  attr_accessor :name
59
75
 
@@ -80,6 +96,9 @@ module Nesser
80
96
  end
81
97
  end
82
98
 
99
+ ##
100
+ # Alias record: eg, 'www.google.com'->'google.com'.
101
+ ##
83
102
  class CNAME
84
103
  attr_accessor :name
85
104
 
@@ -105,6 +124,9 @@ module Nesser
105
124
  end
106
125
  end
107
126
 
127
+ ##
128
+ # Statement of authority record.
129
+ ##
108
130
  class SOA
109
131
  attr_accessor :primary, :responsible, :serial, :refresh, :retry_interval, :expire, :ttl
110
132
 
@@ -155,6 +177,9 @@ module Nesser
155
177
  end
156
178
  end
157
179
 
180
+ ##
181
+ # Mail exchange record - eg, 'mail.google.com' 10.
182
+ ##
158
183
  class MX
159
184
  attr_accessor :preference, :name
160
185
 
@@ -188,6 +213,10 @@ module Nesser
188
213
  end
189
214
  end
190
215
 
216
+ ##
217
+ # A TXT record, with is simply binary data (except on some libraries where it
218
+ # can't contain a NUL byte).
219
+ ##
191
220
  class TXT
192
221
  attr_accessor :data
193
222
 
@@ -223,6 +252,9 @@ module Nesser
223
252
  end
224
253
  end
225
254
 
255
+ ##
256
+ # IPv6 record, eg, "::1".
257
+ ##
226
258
  class AAAA
227
259
  attr_accessor :address
228
260
 
@@ -264,6 +296,9 @@ module Nesser
264
296
  end
265
297
  end
266
298
 
299
+ ##
300
+ # An unknown record type.
301
+ ##
267
302
  class RRUnknown
268
303
  attr_reader :type, :data
269
304
  def initialize(type:, data:)
@@ -22,12 +22,21 @@ module Nesser
22
22
  class Unpacker
23
23
  attr_accessor :data, :offset
24
24
 
25
+ ##
26
+ # Create a new unpacker with a string (the string must be the full DNS
27
+ # request, starting at the `trn_id` - otherwise, unpacking compressed
28
+ # fields won't work properly!
29
+ ##
25
30
  public
26
31
  def initialize(data)
27
32
  @data = data
28
33
  @offset = 0
29
34
  end
30
35
 
36
+ ##
37
+ # Does some basic error checking to make sure we didn't run off the end of
38
+ # packet - doesn't catch every case, though.
39
+ ##
31
40
  private
32
41
  def _verify_results(results)
33
42
  # If there's at least one nil included in our results, bad stuff happened
@@ -36,8 +45,11 @@ module Nesser
36
45
  end
37
46
  end
38
47
 
39
- # Unpack from the string, exactly like the normal `String#Unpack` method
40
- # in Ruby, except that an offset into the string is maintained and updated.
48
+ ##
49
+ # Unpack from the string, exactly like the normal `String.unpack()` method
50
+ # in Ruby, except that a pointer offset into the string is maintained and
51
+ # updated (which is required for unpacking names).
52
+ ##
41
53
  public
42
54
  def unpack(format)
43
55
  if @offset >= @data.length
@@ -53,6 +65,13 @@ module Nesser
53
65
  return *results
54
66
  end
55
67
 
68
+ ##
69
+ # Unpack a single element from the buffer and return it (this is a simple
70
+ # little utility function that I wrote to save myself time).
71
+ #
72
+ # The `format` argument works exactly like in `String.unpack()`, but only
73
+ # one element can be given.
74
+ ##
56
75
  public
57
76
  def unpack_one(format)
58
77
  results = unpack(format)
@@ -65,9 +84,10 @@ module Nesser
65
84
  return results.pop()
66
85
  end
67
86
 
68
- # This temporarily changes the offset that we're reading from, runs the
69
- # given block, then changes it back. This is used internally while
70
- # unpacking names.
87
+ ##
88
+ # Temporarily changes the offset that we're reading from, runs the given
89
+ # block, then changes it back. This is used internally while unpacking names.
90
+ ##
71
91
  private
72
92
  def _with_offset(offset)
73
93
  old_offset = @offset
@@ -76,11 +96,26 @@ module Nesser
76
96
  @offset = old_offset
77
97
  end
78
98
 
79
- # Unpack a name from the packet. Names are special, because they're
80
- # encoded as:
81
- # * A series of length-prefixed blocks, each indicating a segment
82
- # * Blocks with a length the starts with two '1' bits (11xxxxx...), which
83
- # contains a pointer to another name elsewhere in the packet
99
+ ##
100
+ # Unpack a name from the packet and convert it into a standard dotted name
101
+ # that we all understand.
102
+ #
103
+ # At the simplest, names are encoded as length-prefixed blocks. For example,
104
+ # "google.com" is encoded as "\x06google\x03com\x00".
105
+ #
106
+ # More complex, however, is that all or part of a name can be replaced with
107
+ # "\xc0" followed by an offset into the packet where the remainder of the
108
+ # name (or the full name) can be found. For example, if
109
+ # "\x06google\x03com\x00" is found at index 0x0c (which it frequently is),
110
+ # then "www.google.com" can be encoded as "\x03www\xc0\x0c". In other words,
111
+ # "www" followed by the rest of the name at offset 0x0c".
112
+ #
113
+ # The point of this class is that that situation is handled as cleanly as
114
+ # possible.
115
+ #
116
+ # The `depth` argument is used internally for recursion, the default value
117
+ # of 0 should be used externally.
118
+ ##
84
119
  public
85
120
  def unpack_name(depth:0)
86
121
  segments = []
@@ -6,27 +6,40 @@
6
6
  #
7
7
  # See: LICENSE.md
8
8
  #
9
- # When a request comes in, a transaction is created and sent to the callback.
10
- # The transaction can be used to respond to the request at any point in the
11
- # future.
9
+ # When a request comes in, a transaction is created to represent it. The
10
+ # transaction can be used to respond to the request at any point in the
11
+ # future - the trn_id and socket and stuff are all set up. Though, keep in
12
+ # mind, most clients will only wait like 3 seconds, so responding at *any*
13
+ # point, while technically true, isn't really a useful distinction.
12
14
  #
13
- # Any methods with a bang ('!') in front will send the response back to the
15
+ # Any methods with a bang ('!') in their name will send the response back to the
14
16
  # requester. Only one bang method can be called, any subsequent calls will
15
17
  # throw an exception.
18
+ #
19
+ # You'll almost always want to use either `transaction.answer!()` or
20
+ # `transaction.error!()`. While it's possible to change and/or add to
21
+ # `transaction.response` and send it with `transaction.reply!()`, that's more
22
+ # complex and generally not needed.
16
23
  ##
24
+
17
25
  module Nesser
18
26
  class Transaction
19
- attr_reader :request_packet, :response_packet, :sent
27
+ attr_reader :request, :sent
28
+ attr_accessor :response
20
29
 
30
+ ##
31
+ # Create a new instance of Transaction. This is used internally - it's
32
+ # unlikely you'll ever need to create an instance.
33
+ ##
21
34
  public
22
- def initialize(s:, request_packet:, host:, port:)
35
+ def initialize(s:, request:, host:, port:)
23
36
  @s = s
24
- @request_packet = request_packet
37
+ @request = request
25
38
  @host = host
26
39
  @port = port
27
40
  @sent = false
28
41
 
29
- @response_packet = request_packet.answer()
42
+ @response = request.answer()
30
43
  end
31
44
 
32
45
  private
@@ -36,25 +49,41 @@ module Nesser
36
49
  end
37
50
  end
38
51
 
52
+ ##
53
+ # Check whether or not the transaction has been sent already.
54
+ ##
39
55
  public
40
56
  def open?()
41
57
  return !@sent
42
58
  end
43
59
 
60
+ ##
61
+ # Reply with zero or more answers, specified in the array.
62
+ #
63
+ # Only one "bang function" can be called per transaction, subsequent calls
64
+ # will throw an exception.
65
+ ##
44
66
  public
45
67
  def answer!(answers=[])
46
68
  answers.each do |answer|
47
- @response_packet.add_answer(answer)
69
+ @response.add_answer(answer)
48
70
  end
49
71
 
50
72
  reply!()
51
73
  end
52
74
 
75
+ ##
76
+ # Reply with an error defined by rcode (you can find a full list in
77
+ # packets/constants.rb).
78
+ #
79
+ # Only one "bang function" can be called per transaction, subsequent calls
80
+ # will throw an exception.
81
+ ##
53
82
  public
54
83
  def error!(rcode)
55
84
  not_sent!()
56
85
 
57
- @response_packet.rcode = rcode
86
+ @response.rcode = rcode
58
87
  reply!()
59
88
  end
60
89
 
@@ -88,11 +117,19 @@ module Nesser
88
117
  # @sent = true
89
118
  # end
90
119
 
91
- private
120
+ ##
121
+ # Reply with the response packet, in whatever state it's in. While this is
122
+ # public and gives you find control over the packet being sent back,
123
+ # realistically answer!() and error!() are probably all you'll need. Only
124
+ # use this is those won't work for whatever reason.
125
+ #
126
+ # Only one "bang function" can be called per transaction, subsequent calls
127
+ # will throw an exception.
128
+ public
92
129
  def reply!()
93
130
  not_sent!()
94
131
 
95
- @s.send(@response_packet.to_bytes(), 0, @host, @port)
132
+ @s.send(@response.to_bytes(), 0, @host, @port)
96
133
  @sent = true
97
134
  end
98
135
 
@@ -102,13 +139,14 @@ module Nesser
102
139
 
103
140
  result << '== Nesser (DNS) Transaction =='
104
141
  result << '-- Request --'
105
- result << @request_packet.to_s()
142
+ result << @request.to_s()
106
143
  if !sent()
107
144
  result << '-- Response [not sent yet] --'
108
145
  else
109
146
  result << '-- Response [sent] --'
110
147
  end
111
- result << @response_packet.to_s()
148
+ result << @response.to_s()
149
+ result << ''
112
150
 
113
151
  return result.join("\n")
114
152
  end
@@ -1,3 +1,3 @@
1
1
  module Nesser
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nesser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - iagox86
@@ -76,6 +76,7 @@ files:
76
76
  - ".gitignore"
77
77
  - ".travis.yml"
78
78
  - Gemfile
79
+ - LICENSE.md
79
80
  - README.md
80
81
  - Rakefile
81
82
  - bin/console