class DHCP::Packet

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.

Attributes

flags[R]
hlen[R]
hops[R]
htype[R]
htype_name[R]
logs[RW]
op[R]
options[R]
optlist[R]
secs[RW]
type[R]
type_name[R]
xid[RW]

Public Class Methods

new(opt={}) click to toggle source
# File lib/dhcp/packet.rb, line 11
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)

  ## While parsing, record any warnings here:
  @logs = []

  ## 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

Public Instance Methods

_find_htype(htype) click to toggle source
# File lib/dhcp/packet.rb, line 156
def _find_htype(htype)
  HTYPE.each do |name, ht|
    if ht[0] == htype
      return name
    end
  end
  return nil
end
_parse(msg) click to toggle source
# File lib/dhcp/packet.rb, line 165
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.get < 1 || opt.get > 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) click to toggle source

Append a single DHCP option to a packet.

It is recommended that when creating a DHCP packet from scratch, use #include_opts(opts) 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.

# File lib/dhcp/packet.rb, line 107
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!() click to toggle source
# File lib/dhcp/packet.rb, line 321
def broadcast!
  @flags |= 0x8000
end
broadcast?() click to toggle source

Broadcast flag:

# File lib/dhcp/packet.rb, line 318
def broadcast?
  @flags & 0x8000 != 0
end
chaddr() click to toggle source

Hardware address (ethernet MAC style):

# File lib/dhcp/packet.rb, line 326
def chaddr
  @chaddr[0,@hlen].each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join(':')
end
chaddr=(addr) click to toggle source
# File lib/dhcp/packet.rb, line 332
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
ciaddr() click to toggle source

IP accessors:

# File lib/dhcp/packet.rb, line 338
def ciaddr
  IPAddress::IPv4::parse_data(@ciaddr).to_s
end
ciaddr=(ip) click to toggle source
# File lib/dhcp/packet.rb, line 341
def ciaddr=(ip)
  @ciaddr = IPAddress::IPv4.new(ip).data
end
file() click to toggle source
# File lib/dhcp/packet.rb, line 129
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
file=(name) click to toggle source
# File lib/dhcp/packet.rb, line 93
def file=(name)
  ## Callers do NOT include the C-style null/zero termination character.
  ## Boot file name (optional) as C-style null/zero terminated string (may instead contain options)
  raise 'Invalid boot file name string.' unless @sname.size < 128
  @file = name
end
get_option(opt) click to toggle source

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

# File lib/dhcp/packet.rb, line 212
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
giaddr() click to toggle source
# File lib/dhcp/packet.rb, line 359
def giaddr
  IPAddress::IPv4::parse_data(@giaddr).to_s
end
giaddr=(ip) click to toggle source
# File lib/dhcp/packet.rb, line 362
def giaddr=(ip)
  @giaddr = IPAddress::IPv4.new(ip).data
end
htype=(htype) click to toggle source
# File lib/dhcp/packet.rb, line 310
def htype=(htype)
  @htype_name = _find_htype(htype)
  raise "Invalid/unsupported hardware type #{htype}" if @htype_name.nil?
  @hlen = HTYPE[@htype_name][1]
  @htype = HTYPE[@htype_name][0]
end
include_opts(opts) click to toggle source

Add one or more options to a DHCP packet. One may pass an array of options to be included, or a single option. This implementation currently DOES NOT support duplicate options yet. An END option will be appended at the end of the packet's options list.

# File lib/dhcp/packet.rb, line 141
def include_opts(opts)
  opts = [opts] unless opts.is_a?(Array)
  list     = @optlist
  @options = ''
  @optlist = []
  list.each do |o|
    ## This implementation currently doesn't support duplicate options yet:
    raise "Duplicate option in packet." unless opts.find_index{|i| o.name == i.name}.nil?
    ## Skip/ignore the end option:
    @optlist << o unless o.name == :end
  end
  opts.each{|o| append_opt(o) unless o.name == :end}
  @optlist << Opt.new(255, :end)
end
initialize_copy(orig) click to toggle source

Both clone and dup will call this:

# File lib/dhcp/packet.rb, line 78
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) click to toggle source
# File lib/dhcp/packet.rb, line 219
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?
          @logs << "WARNING: Unsupported option #{opt} (#{len} bytes)"
        else
          o.data = val unless len == 0
          @optlist << o
        end
      else
        ## See above TODO note...
        @logs << "WARNING: Duplicate option #{opt} (#{o.name}) of #{len} bytes skipped/ignored"
      end
    end
  end
end
raw_chaddr() click to toggle source
# File lib/dhcp/packet.rb, line 329
def raw_chaddr
  @chaddr
end
set_type(opt) click to toggle source
# File lib/dhcp/packet.rb, line 200
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
siaddr() click to toggle source
# File lib/dhcp/packet.rb, line 352
def siaddr
  IPAddress::IPv4::parse_data(@siaddr).to_s
end
siaddr=(ip) click to toggle source
# File lib/dhcp/packet.rb, line 355
def siaddr=(ip)
  @siaddr = IPAddress::IPv4.new(ip).data
end
sname() click to toggle source
# File lib/dhcp/packet.rb, line 117
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) click to toggle source
# File lib/dhcp/packet.rb, line 125
def sname=(val)
  @sname=val
end
to_packet() click to toggle source
# File lib/dhcp/packet.rb, line 256
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_s() click to toggle source
# File lib/dhcp/packet.rb, line 269
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
yiaddr() click to toggle source
# File lib/dhcp/packet.rb, line 345
def yiaddr
  IPAddress::IPv4::parse_data(@yiaddr).to_s
end
yiaddr=(ip) click to toggle source
# File lib/dhcp/packet.rb, line 348
def yiaddr=(ip)
  @yiaddr = IPAddress::IPv4.new(ip).data
end