Class: DHCP::Packet

Inherits:
Object
  • Object
show all
Defined in:
lib/dhcp/packet.rb

Overview

Class representing a DHCP packet (a request or a response) for creating said packets, or for parsing them from a UDP DHCP packet data payload.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opt = {}) ⇒ Packet

Returns a new instance of Packet.



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
# File 'lib/dhcp/packet.rb', line 12

def initialize(opt={})
  data = nil
  if opt.is_a?(String)
    data = opt
    opt = {}
  end
  ## 1: Operation (BOOTREQUEST=1/BOOTREPLY=2)
  @op = opt[:op]
  raise "Invalid/unsupported operation type #{@op}" unless @op.nil? || @op == BOOTREQUEST || @op == BOOTREPLY
  @htype_name = :htype_10mb_ethernet  ## Only supported type currently...
  @htype   = HTYPE[@htype_name][0] ## 1: Hardware address type
  @hlen    = HTYPE[@htype_name][1] ## 1: Hardware address length
  @hops    = 0                     ## 1: Client sets to zero, relays may increment
  @xid     = opt[:xid]   || 0      ## 4: Client picks random 32-bit XID (session ID of sorts)
  @secs    = opt[:secs]  || 0      ## 4: Seconds elapsed since client started transaction
  @flags   = opt[:flats] || 0      ## 2: Leftmost bit is the 'BROADCAST' flag (if set) - Others are zero (reserved for future use)

  ## 4: "Client IP"  -- Only set by client if client state is BOUND/RENEW/REBINDING and client can respond to ARP requests
  @ciaddr = IPAddress::IPv4.new(opt[:ciaddr] || '0.0.0.0').data

  ## 4: "Your IP"    -- Server assigns IP to client
  @yiaddr = IPAddress::IPv4.new(opt[:yiaddr] || '0.0.0.0').data

  ## 4: "Server IP"  -- IP of server to use in NEXT step of client bootstrap process
  @siaddr = IPAddress::IPv4.new(opt[:siaddr] || '0.0.0.0').data

  ## 4: "Gateway IP" -- Relay agent will set this to itself and modify replies
  @giaddr = IPAddress::IPv4.new(opt[:giaddr] || '0.0.0.0').data

  ## 16: Client hardware address (see htype and hlen)
  @chaddr = (opt[:chaddr] || ('00' * @hlen)).gsub(%r{[ :._-]},'').downcase
  raise 'Invalid client hardware address.' unless @chaddr.size == @hlen*2 && %r{\A[a-f0-9]{2}+\Z}.match(@chaddr)
  @chaddr = @chaddr.scan(%r{..}m).map{|b| b.to_i(16).chr}.join

  ## 64: Server host name (optional) as C-style null/zero terminated string (may instead contain options)
  ## If provided by caller, do NOT include the C-style null/zero termination character.
  @sname = opt[:sname] || ''
  raise 'Invalid server host name string.' unless @sname.size < 64

  ## 128: Boot file name (optional) as C-style null/zero terminated string (may instead contain options)
  ## If provided by caller, do NOT include the C-style null/zero termination character.
  @file = opt[:file] || ''
  raise 'Invalid boot file name string.' unless @sname.size < 128

  ## variable: Options - Up to 312 bytes in a 576-byte DHCP message - First four bytes are MAGIC
  @options = ''  ## Preserve any parsed packet's original binary option data - NOT set for non-parsed generated packets
  @optlist = []

  @type      = nil
  @type_name = 'UNKNOWN'
  if opt[:type]
    include_opt(DHCP.make_opt_name(:dhcp_message_type, opt[:type].is_a?(String) ? DHCP::MSG_STR_TO_TYPE[opt[:type]] : opt[:type]))
  end

  ## Default to BOOTREQUEST when generating a blank (invalid) packet:
  @op = BOOTREQUEST if @op.nil?

  ## If a packet was provided, parse it:
  _parse(data) unless data.nil?
end

Instance Attribute Details

#flagsObject (readonly)

Returns the value of attribute flags.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def flags
  @flags
end

#hlenObject (readonly)

Returns the value of attribute hlen.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def hlen
  @hlen
end

#hopsObject (readonly)

Returns the value of attribute hops.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def hops
  @hops
end

#htypeObject

Returns the value of attribute htype.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def htype
  @htype
end

#htype_nameObject (readonly)

Returns the value of attribute htype_name.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def htype_name
  @htype_name
end

#opObject (readonly)

Returns the value of attribute op.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def op
  @op
end

#optionsObject (readonly)

Returns the value of attribute options.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def options
  @options
end

#optlistObject (readonly)

Returns the value of attribute optlist.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def optlist
  @optlist
end

#secsObject

Returns the value of attribute secs.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def secs
  @secs
end

#typeObject (readonly)

Returns the value of attribute type.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def type
  @type
end

#type_nameObject (readonly)

Returns the value of attribute type_name.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def type_name
  @type_name
end

#xidObject

Returns the value of attribute xid.



72
73
74
# File 'lib/dhcp/packet.rb', line 72

def xid
  @xid
end

Instance Method Details

#_find_htype(htype) ⇒ Object



141
142
143
144
145
146
147
148
# File 'lib/dhcp/packet.rb', line 141

def _find_htype(htype)
  HTYPE.each do |name, htype|
    if htype[0] == @htype
      return name
    end
  end
  return nil
end

#_parse(msg) ⇒ Object



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
# File 'lib/dhcp/packet.rb', line 150

def _parse(msg)
  raise "Packet is too short (#{msg.size} < 241)" if (msg.size < 241)
  @op    = msg[0,1].ord
  raise 'Invalid OP (expected BOOTREQUEST or BOOTREPLY)' if @op != BOOTREQUEST && @op != BOOTREPLY
  self.htype = msg[1,1].ord  ## This will do sanity checking and raise an exception on unsupported HTYPE
  raise "Invalid hardware address length #{msg[2,1].ord} (expected #{@hlen})" if msg[2,1].ord != @hlen
  @hops   = msg[3,1].ord
  @xid    = msg[4,4].unpack('N')[0]
  @secs   = msg[8,2].unpack('n')[0]
  @flags  = msg[10,2].unpack('n')[0]
  @ciaddr = msg[12,4]
  @yiaddr = msg[16,4]
  @siaddr = msg[20,4]
  @giaddr = msg[24,4]
  @chaddr = msg[28,16]
  @sname  = msg[44,64]
  @file   = msg[108,128]
  magic   = msg[236,4]
  raise "Invalid DHCP OPTION MAGIC #{magic.each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join(':')} !=  #{MAGIC.each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join(':')}" if magic != MAGIC
  @options = msg[240,msg.size-240]
  @optlist = []
  parse_opts(@options)
  opt = get_option(:option_overload)
  unless opt.nil?
    ## RFC 2131: If "option overload" present, parse FILE field first, then SNAME (depending on overload value)
    parse_opts(@file)  if opt.get == 1 || opt.get == 3
    parse_opts(@sname) if opt.get == 2 || opt.get == 3
    raise "Invalid option overload value" if opt.val > 1 || opt.val > 3
  end
  opt = get_option(:dhcp_message_type)
  raise "Not a valid DHCP packet (may be BOOTP): Missing DHCP MESSAGE TYPE" if opt.nil?
  set_type(opt)
  self
end

#append_opt(opt) ⇒ Object

It is recommended that when creating a DHCP packet from scratch, use include_opt(opt) instead so that the “end” option will be correctly added or moved to the end. append_opt(opt) will not automatically add an “end” nor will it move an existing “end” option, possibly resulting in an invalid DHCP packet if not used carefully.



96
97
98
99
100
101
102
103
104
# File 'lib/dhcp/packet.rb', line 96

def append_opt(opt)
  if opt.name == :dhcp_message_type
    unless @type.nil?
      raise "DHCP message type ALREADY SET in packet"
    end
    set_type(opt)
  end
  @optlist << opt
end

#broadcast!Object



306
307
308
# File 'lib/dhcp/packet.rb', line 306

def broadcast!
  @flags |= 0x8000
end

#broadcast?Boolean

Broadcast flag:

Returns:

  • (Boolean)


303
304
305
# File 'lib/dhcp/packet.rb', line 303

def broadcast?
  @flags & 0x8000 != 0
end

#chaddrObject

Hardware address (ethernet MAC style):



311
312
313
# File 'lib/dhcp/packet.rb', line 311

def chaddr
  @chaddr[0,@hlen].each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join(':')
end

#chaddr=(addr) ⇒ Object



317
318
319
320
# File 'lib/dhcp/packet.rb', line 317

def chaddr=(addr)
  raise "Invalid hardware address" if addr.size - @hlen + 1 != @hlen * 2 || !/^(?:[a-fA-F0-9]{2}[ \.:_\-])*[a-fA-F0-9]{2}$/.match(addr)
  @chaddr = addr.split(/[ .:_-]/).map{|b| b.to_i(16).chr}.join
end

#ciaddrObject

IP accessors:



323
324
325
# File 'lib/dhcp/packet.rb', line 323

def ciaddr
  IPAddress::IPv4::parse_data(@ciaddr).to_s
end

#ciaddr=(ip) ⇒ Object



326
327
328
# File 'lib/dhcp/packet.rb', line 326

def ciaddr=(ip)
  @ciaddr = IPAddress::IPv4.new(ip).data
end

#fileObject



118
119
120
121
122
123
124
# File 'lib/dhcp/packet.rb', line 118

def file
  ## If the option overload is value 1 or 3, look for a :bootfile_name option:
  opt = get_option(:option_overload)
  return @file if opt.nil? || opt.get == 2
  opt = get_option(:bootfile_name)
  return opt.nil? ? '' : opt.get
end

#get_option(opt) ⇒ Object

Look through a packet’s options for the option in question:



197
198
199
200
201
202
# File 'lib/dhcp/packet.rb', line 197

def get_option(opt)
  @optlist.each do |o|
    return o if (opt.is_a?(Symbol) && o.name == opt) || (opt.is_a?(Fixnum) && o.opt == opt)
  end
  nil
end

#giaddrObject



344
345
346
# File 'lib/dhcp/packet.rb', line 344

def giaddr
  IPAddress::IPv4::parse_data(@giaddr).to_s
end

#giaddr=(ip) ⇒ Object



347
348
349
# File 'lib/dhcp/packet.rb', line 347

def giaddr=(ip)
  @giaddr = IPAddress::IPv4.new(ip).data
end

#include_opt(opt) ⇒ Object

This is the best way to add an option to a DHCP packet:



127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/dhcp/packet.rb', line 127

def include_opt(opt)
  list     = @optlist
  @options = ''
  @optlist = []
  list.each do |o|
    ## This implementation currently doesn't support duplicate options yet:
    raise "Duplicate option in packet." if o.name == opt.name
    ## Skip/ignore the end option:
    @optlist << o unless o.name == :end
  end
  append_opt(opt)
  @optlist << Opt.new(255, :end)
end

#initialize_copy(orig) ⇒ Object

Both #clone and #dup will call this:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/dhcp/packet.rb', line 76

def initialize_copy(orig)
  self.ciaddr = orig.ciaddr
  self.yiaddr = orig.yiaddr
  self.siaddr = orig.siaddr
  self.giaddr = orig.giaddr
  @chaddr  = orig.raw_chaddr.dup
  @file    = orig.file.dup
  @sname   = orig.sname.dup
  @options = orig.options.dup
  @optlist = []
  orig.optlist.each do |opt|
    @optlist << opt.dup
  end
end

#parse_opts(opts) ⇒ Object



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
# File 'lib/dhcp/packet.rb', line 204

def parse_opts(opts)
  msg = opts.dup
  while msg.size > 0
    opt = msg[0,1].ord
    if opt == 0
      ## Don't add padding options to our list...
      msg[0,1] = ''
    elsif opt == 255 
      ## Options end...  Assume all the rest is padding (if any)
      @optlist << Opt.new(255, :end)
      msg = ''
    else
      ## TODO: If an option value can't fit within a single option,
      ## it may span several and the values should be merged.  We
      ## don't support this yet for parsing.
      raise "Options end too soon" if msg.size == 1
      len = msg[1,1].ord
      raise "Options end too abruptly (expected #{len} more bytes, but found only #{msg.size - 2})" if msg.size < len + 2
      val = msg[2,len]
      msg[0,len+2] = ''
      o = get_option(opt)
      if o.nil?
        o = DHCP::make_opt(opt)
        if o.nil?
          puts "WARNING: Ignoring unsupported option #{opt} (#{len} bytes)"
        else
          o.data = val unless len == 0
          @optlist << o
        end
      else
        ## See above TODO note...
        puts "WARNING: Duplicate option #{opt} (#{o.name}) of #{len} bytes skipped/ignored"
      end
    end
  end
end

#raw_chaddrObject



314
315
316
# File 'lib/dhcp/packet.rb', line 314

def raw_chaddr
  @chaddr
end

#set_type(opt) ⇒ Object



185
186
187
188
189
190
191
192
193
194
# File 'lib/dhcp/packet.rb', line 185

def set_type(opt)
  @type = opt.get
  if DHCP::MSG_TYPE_TO_OP.key?(@type)
    @type_name = DHCP::MSG_TYPE_TO_STR[@type]
    @op = DHCP::MSG_TYPE_TO_OP[@type] if @op.nil?
    raise "Invalid OP #{@op} for #{@type_name}" unless @op == DHCP::MSG_TYPE_TO_OP[@type]
  else
    raise "Invalid or unsupported DHCP MESSAGE TYPE"
  end
end

#siaddrObject



337
338
339
# File 'lib/dhcp/packet.rb', line 337

def siaddr
  IPAddress::IPv4::parse_data(@siaddr).to_s
end

#siaddr=(ip) ⇒ Object



340
341
342
# File 'lib/dhcp/packet.rb', line 340

def siaddr=(ip)
  @siaddr = IPAddress::IPv4.new(ip).data
end

#snameObject



106
107
108
109
110
111
112
# File 'lib/dhcp/packet.rb', line 106

def sname
  ## If the option overload is value 2 or 3, look for a :tftp_server_name option:
  opt = get_option(:option_overload)
  return @sname if opt.nil? || opt.get == 1
  opt = get_option(:tftp_server_name)
  return opt.nil? ? '' : opt.get
end

#sname=(val) ⇒ Object



114
115
116
# File 'lib/dhcp/packet.rb', line 114

def sname=(val)
  @sname=val
end

#to_packetObject



241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/dhcp/packet.rb', line 241

def to_packet
  packet =
    @op.chr + @htype.chr + @hlen.chr + @hops.chr +
    [@xid, @secs, @flags].pack('Nnn') +
    @ciaddr + @yiaddr + @siaddr + @giaddr +
    @chaddr + (0.chr * (16-@chaddr.size)) +
    @sname  + (0.chr * (64-@sname.size)) +
    @file   + (0.chr * (128-@file.size)) +
    MAGIC +
    @optlist.map{|x| x.to_opt}.join
  packet + (packet.size < 300 ? 0.chr * (300 - packet.size) : '')  ## Pad to minimum of 300 bytes -  Minimum BOOTP/DHCP packet size (RFC 951) - Some devices will drop packets smaller than this.
end

#to_sObject



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
# File 'lib/dhcp/packet.rb', line 254

def to_s
  str = "op=#{@op} "
  case @op
  when BOOTREQUEST
    str += '(BOOTREQUEST)'
  when BOOTREPLY
    str += '(BOOTREPLY)'
  else
    str += '(UNKNOWN)'
  end
  str += "\n"

  str += "htype=#{@htype} "
  found = false
  HTYPE.each do |name, htype|
    if htype[0] == @htype
      found = true
      str += name.to_s.upcase + "\n" + 'hlen=' + htype[1].to_s + "\n"
      str += "*** INVALID HLEN #{@hlen} != #{htype[1]} ***\n" if @hlen != htype[1]
      break
    end
  end
  str += "UNKNOWN\nhlen=" + @hlen.to_s + "\n"  unless found
  str += "hops=#{@hops}\n"
  str += "xid=#{@xid} (0x" + [@xid].pack('N').each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join + ")\n"
  str += "secs=#{@secs}\n"
  str += "flags=#{@flags} (" + (broadcast? ? 'BROADCAST' : 'NON-BROADCAST') + ")\n"
  str += 'ciaddr=' + ciaddr + "\n"
  str += 'yiaddr=' + yiaddr + "\n"
  str += 'siaddr=' + siaddr + "\n"
  str += 'giaddr=' + giaddr + "\n"
  str += 'chaddr=' + chaddr + "\n"
  str += "sname='#{@sname.sub(/\x00.*$/,'')}' (#{@sname.sub(/\x00.*$/,'').size})\n"
  str += "file='#{@file.sub(/\x00.*$/,'')}' (#{@file.sub(/\x00.*$/,'').size})\n"
  str += 'MAGIC: (0x' + MAGIC.each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join + ")\n"
  str += "OPTIONS(#{@optlist.size}) = [\n  "
  str += @optlist.map{|x| x.to_s}.join(",\n  ") + "\n]\n"
  str += "DHCP_PACKET_TYPE='#{@type_name}' (#{@type}) " unless @type.nil?
  str
end

#yiaddrObject



330
331
332
# File 'lib/dhcp/packet.rb', line 330

def yiaddr
  IPAddress::IPv4::parse_data(@yiaddr).to_s
end

#yiaddr=(ip) ⇒ Object



333
334
335
# File 'lib/dhcp/packet.rb', line 333

def yiaddr=(ip)
  @yiaddr = IPAddress::IPv4.new(ip).data
end