summaryrefslogtreecommitdiff
path: root/lib/net/dhcp/core.rb
blob: bb42e5230c3b637b3d239f5a0765d1a3a5eafb58 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
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
=begin
**
** core.rb
** 02/OCT/2007
** ETD-Software
**  - Daniel Martin Gomez <etd[-at-]nomejortu.com>
**
** Desc:
**   This file provides a set of classes to work with the DHCP protocol. They 
** provide low level access to all the fields and internals of the protocol.
** 
** See the provided rdoc comments for further information.
**
** Version:
**  v1.0 [02/October/2007]: first released
**  v1.1 [31/October/2007]: file moved under /dhcp/ directory and renamed to
**                          core.rb. Now it only contains core classes to 
**                          encapsulate DHCP messages
**
** License:
**   Please see dhcp.rb or LICENSE.txt for copyright and licensing information.
**
=end

module DHCP
  # -------------------------------------------------------------- dhcp messages
  class Message
    attr_accessor :op, :htype, :hlen, :hops
    attr_accessor :xid
    attr_accessor :secs, :flags
    attr_accessor :ciaddr, :yiaddr, :siaddr, :giaddr, :chaddr
    attr_accessor :options

    alias == eql?
    
    def Message.from_udp_payload(data)
      values = data.unpack('C4Nn2N4C16C192NC*')

      params = {
        :op => values.shift,
        :htype => values.shift,
        :hlen => values.shift,
        :hops => values.shift,
      
        :xid => values.shift,
        :secs => values.shift,
        :flags => values.shift,
        :ciaddr => values.shift,
        :yiaddr => values.shift,
        :siaddr => values.shift,
        :giaddr => values.shift,
        :chaddr => values.slice!(0..15)
      }
      
      # sname and file
      not_used = values.slice!(0..191)
      
      return nil unless ($DHCP_MAGIC == values.shift)
      
      #default message class
      msg_class = Message
      #default option class
      opt_class = Option
      
      params[:options] = []
      
      next_opt = values.shift
      while(next_opt != $DHCP_END)
        p = {
          :type => next_opt,
          :len => values.shift
        }
        p[:payload] = values.slice!(0..p[:len]-1)
        
        # check what is the type of dhcp option
        opt_class = $DHCP_MSG_OPTIONS[p[:type]]
        if(opt_class.nil?)
          puts '-------------------- please further investigate!!'
          puts p[:type]
          puts '-------------------- /'
          opt_class == Option
        end
        if (opt_class == MessageTypeOption)
          msg_class = $DHCP_MSG_CLASSES[p[:payload].first]
        end
        params[:options] << opt_class.new(p)
        next_opt = values.shift
      end
      
      if(msg_class.nil?)
        puts '-------------------- please further investigate!!'
        p params[:options]
        puts '-------------------- /'
        opt_class == Option
      end
      msg_class.new(params)
    end
  
    def initialize(params = {})
    
      # message operation and options. We need at least an operation and a 
      # MessageTypeOption to create a DHCP message!!
      if (([:op, :options]  & params.keys).size != 2)
        raise ArgumentError('you need to specify at least values for :op and :options') 
      end
      
      self.op = params[:op]
      
      self.options = params[:options]
      found = false
      self.options.each do |opt|
        next unless opt.class == MessageTypeOption
        found = true
      end
      raise ArgumentError(':options must include a MessageTypeOption') unless found
    
      #hardware type and length of the hardware address
      self.htype = params.fetch(:htype, $DHCP_HTYPE_ETHERNET)
      self.hlen = params.fetch(:hlen, $DHCP_HLEN_ETHERNET)
    
      # client sets to zero. relay agents may modify
      self.hops = params.fetch(:hops, 0x00)
    
      # initialize a random transaction ID
      self.xid  = params.fetch(:xid, rand(2**32))
    
      # seconds elapsed, flags
      self.secs = params.fetch(:secs, 0x0000)
      self.flags = params.fetch(:flags, 0x0000)
    
      # client, you, next server  and relay agent addresses
      self.ciaddr = params.fetch(:ciaddr, 0x00000000)
      self.yiaddr = params.fetch(:yiaddr, 0x00000000)
      self.siaddr = params.fetch(:siaddr, 0x00000000)
      self.giaddr = params.fetch(:giaddr, 0x00000000)
    
      if (params.key?(:chaddr))
        self.chaddr = params[:chaddr]
        raise 'chaddr field should be of 16 bytes' unless self.chaddr.size == 16
      else
        mac = `/sbin/ifconfig | grep HWaddr | cut -c39- | head -1`.chomp.strip.gsub(/:/,'')
        self.chaddr = [mac].pack('H*').unpack('CCCCCC')
        self.chaddr += [0x00]*(16-self.chaddr.size)
      end
    
      
    end
  
    def pack()
      out = [
      self.op, self.htype, self.hlen, self.hops, 
      self.xid, 
      self.secs, self.flags, 
      self.ciaddr, 
      self.yiaddr, 
      self.siaddr, 
      self.giaddr
      ].pack('C4Nn2N4')

      if self.chaddr.size >= 16
        out << self.chaddr.pack('C16')
      else
        out << (self.chaddr + [0x00]*(16-self.chaddr.size)).pack('C16')
      end
      
      # sname and file
      out << ([0x00]*192).pack('C192')
    
      out << [$DHCP_MAGIC].pack('N')
      self.options.each do |option|
        out << option.pack
      end
      out << [$DHCP_END].pack('C')
      
      # add padding up to 300 bytes
      if out.size < 300
        out << ([$DHCP_PAD]*(300-out.size)).pack('C*')
      end
      return out
    end
    
    def eql?(obj)
      # objects must be of the same class
      return false unless (self.class == obj.class)
      
      vars1 = self.instance_variables
      
      # first make sure that the :options var is equal
      opt1 = self.instance_variable_get('@options')
      opt2 = obj.instance_variable_get('@options')
      
      return false unless opt1.eql?(opt2)
      vars1.delete('@options')
      
      # check all the other instance vairables
      vars1.each do |var|
        return false unless (self.instance_variable_get(var) == obj.instance_variable_get(var))
      end
      
      return true
    end
    
    
    
    def to_s
      out = "DHCP Message\r\n"
      out << "\tFIELDS:\r\n"
      out << "\t\tTransaction ID = #{self.xid}\r\n"
      out << "\t\tClient IP address = #{[self.ciaddr].pack('N').unpack('C4').join('.')}\r\n"
      out << "\t\tYour IP address = #{[self.yiaddr].pack('N').unpack('C4').join('.')}\r\n"      
      out << "\t\tNext server IP address = #{[self.siaddr].pack('N').unpack('C4').join('.')}\r\n"
      out << "\t\tRelay agent IP address = #{[self.giaddr].pack('N').unpack('C4').join('.')}\r\n"      
      out << "\t\tHardware address = #{self.chaddr.slice(0..(self.hlen-1)).collect do |b| b.to_s(16).upcase.rjust(2,'0') end.join(':')}\r\n"
      out << "\tOPT:\r\n"
      self.options.each do |opt|
        out << "\t\t #{opt.to_s}\r\n"
      end
      return out
    end
  end

  # Client broadcast to locate available servers.
  class Discover < Message
    def initialize(params={})
      params[:op] = $DHCP_OP_REQUEST
      # if an :options field is provided, we use it, otherwise, a default is set
      params[:options] = params.fetch(:options, [MessageTypeOption.new, ParameterRequestListOption.new])
      super(params)
    end
  end

  # Server to client in response to DHCPDISCOVER with offer of configuration
  # parameters.
  #
  # By default an ACK message will contain a Server Identifier (0.0.0.0) and
  # a Domain Name ('nomejortu.com') option.
  class Offer < Message
    def initialize(params={})
      params[:op] = $DHCP_OP_REPLY
      params[:options] = params.fetch(:options, [
      MessageTypeOption.new({:payload=>$DHCP_MSG_OFFER}), 
      ServerIdentifierOption.new,
      DomainNameOption.new
      ])
      super(params)
    end        
  end
  
  # Client message to servers either (a) requesting offered parameters from one 
  # server and implicitly declining offers from all others, (b) confirming
  # correctness of previously allocated address after, e.g., system reboot, or 
  # (c) extending the lease on a particular network address.  
  class Request < Message
    def initialize(params={})
      params[:op] = $DHCP_OP_REQUEST
      params[:options] = params.fetch(:options, [MessageTypeOption.new({:payload=>$DHCP_MSG_REQUEST}), ParameterRequestListOption.new])
      super(params)
    end
  end

  #
  #   DHCPDECLINE  -  Client to server indicating network address is already
  #                   in use.
  
  # Server to client with configuration parameters, including committed network
  # address.
  #
  # By default an ACK message will contain a Server Identifier (0.0.0.0) and
  # a Domain Name ('nomejortu.com') option.
  class ACK < Message
    def initialize(params={})
      params[:op] = $DHCP_OP_REPLY
      params[:options] = params.fetch(:options, [
      MessageTypeOption.new({:payload=>$DHCP_MSG_ACK}), 
      ServerIdentifierOption.new,
      DomainNameOption.new
      ])
      super(params)
    end    
  end
  
  #   DHCPNAK      -  Server to client indicating client's notion of network
  #                   address is incorrect (e.g., client has moved to new
  #                   subnet) or client's lease as expired

  # Client to server relinquishing network address and cancelling remaining
  # lease.
  #
  # By default an ACK message will contain a Server Identifier (0.0.0.0)
  class Release < Message
    def initialize(params={})
      params[:op] = $DHCP_OP_REQUEST
      params[:options] = params.fetch(:options, [
      MessageTypeOption.new({:payload=>$DHCP_MSG_RELEASE}), 
      ServerIdentifierOption.new
      ])
      super(params)
    end    
  end
  
  # Client to server, asking only for local configuration parameters; client 
  # already has externally configured network address.  
  class Inform < Message
    def initialize(params={})
      params[:op] = $DHCP_OP_REQUEST
      params[:options] = params.fetch(:options, [MessageTypeOption.new({:payload=>$DHCP_MSG_INFORM}), ParameterRequestListOption.new])
      super(params)
    end
  end

  # ------------------------------------ map from values of fields to class names

  $DHCP_MSG_CLASSES = {
    $DHCP_MSG_DISCOVER  => Discover,
    $DHCP_MSG_OFFER     => Offer,
    $DHCP_MSG_REQUEST   => Request,
    #    $DHCP_MSG_DECLINE=     0x04
    $DHCP_MSG_ACK       => ACK,
    #    $DHCP_MSG_NACK=        0x06
    $DHCP_MSG_RELEASE   => Release,
    $DHCP_MSG_INFORM    => Inform
  }

  
end