class DHCP::Server

Constants

ALLONES_IP
ZERO_IP

Public Class Methods

new(opt={}) click to toggle source
# File lib/dhcp/server.rb, line 12
def initialize(opt={})
  @interval    = opt[:interval] || 0.5          ## Sleep (seconds) if no data
  @log         = opt[:log]      || Syslog       ## Logging object (should be open already)
  @server_ip   = opt[:ip]       || '0.0.0.0'    ## Listen on this IP
  @server_port = opt[:port]     || 67           ## Listen on this UDP port
  @debug       = opt[:debug]    || false

  ## Bind to UDP server port:
  @log.info("Starting DHCP on [#{@server_ip}]:#{@server_port} server at #{Time.now}")
  @sock = UDPSocket.new
  @sock.do_not_reverse_lookup = true
  @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true) ## Permit sending to broadcast address
  unless @sock.bind(@server_ip, 67)
    raise "Failed to bind"
  end
end

Public Instance Methods

dispatch_packet(data, source_ip, source_port) click to toggle source

Hand off raw UDP packet data here for parsing and dispatch:

# File lib/dhcp/server.rb, line 74
def dispatch_packet(data, source_ip, source_port)
  now = Time.now
  @log.debug("Packet (#{data.size} bytes) from [#{source_ip}]:#{source_port} received at #{now}")
  if data.size < 300
    @log.debug("Ignoring small packet (less than BOOTP minimum size.") if @debug
    return
  end

  packet = nil
  begin
    packet = DHCP::Packet.new(data)
    packet.logs.each do |msg|
      @log.warn("DHCP Packet Parser Log Message: #{msg}")
    end
  rescue => e
    if packet.nil?
      @log.debug("Packet was not created from data: #{data.inspect} (#{data.size})") if @debug
    else
      show_packet(packet) if @debug
    end
    @log.error("Error parsing DHCP packet: #{e}")
    return
  end

  if source_port == 67     ## DHCP relay via an intermediary (relayed)
    ## Quick relay sanity-check on GIADDR:
    if packet.giaddr == ZERO_IP
      @log.debug("Packet from relay (port 67) has no GIADDR address set.  Ignoring.") if @debug
      return
    end

    unless relay_authorized?(source_ip, packet.giaddr)
      @log.debug("Ignoring DHCP packet from unauthorized relay [#{source_ip}] with GIADDR set to [#{packet.giaddr}].") if @debug
      return
    end
  elsif source_port == 68  ## DHCP via directly attached network (no relay)
    ## Quick relay sanity-check on GIADDR:
    if packet.giaddr != ZERO_IP
      @log.debug("Direct (non-relay) packet has set GIADDR to [#{packet.giaddr}] in violation of RFC. Ignoring.") if @debug
      return
    end
  else
    @log.debug("Ignoring packet from UDP port other than 67 (relay) or 68 (direct)") if @debug
    return
  end

  ## Ethernet hardware type sanity check:
  if packet.htype != DHCP::HTYPE[:htype_10mb_ethernet][0] || packet.hlen !=  DHCP::HTYPE[:htype_10mb_ethernet][1]
    @log.debug("Request hardware type or length doesn't match ETHERNET type and length. Ignoring.") if @debug
    return
  end

  if packet.op != DHCP::BOOTREQUEST
    @log.debug("Recived a non-BOOTREQUEST packet.  Ignoring.") if @debug
    return
  end

  ## Dispatch packet:
  case packet.type
  when DHCP::DHCPDISCOVER
    handle_discover(packet, source_ip, source_port)
  when DHCP::DHCPREQUEST
    handle_request(packet, source_ip, source_port)
  when DHCP::DHCPINFORM
    handle_inform(packet, source_ip, source_port)
  when DHCP::DHCPRELEASE
    handle_release(packet, source_ip, source_port)
  when DHCP::DHCPDECLINE
    handle_decline(packet, source_ip, source_port)
  when DHCP::DHCPLEASEQUERY
    handle_leasequery(packet, source_ip, source_port)
  when DHCP::DHCPOFFER, DHCP::DHCPACK, DHCP::DHCPNAK, DHCP::DHCPFORCERENEW, DHCP::DHCPLEASEUNASSIGNED, DHCP::DHCPLEASEACTIVE, DHCP::DHCPLEASEUNKNOWN
    show_packet(packet) if @debug
    @log.debug("Packet type #{packet.type_name} in a BOOTREQUEST is invalid.") if @debug
  else
    show_packet(packet) if @debug
    @log.debug("Invalid, unknown, or unhandled DHCP packet type received.") if @debug
  end
end
handle_decline(packet, source_ip, source_port) click to toggle source

Handle DHCPDECLINE packet:

# File lib/dhcp/server.rb, line 177
def handle_decline(packet, source_ip, source_port)
  show_packet(packet) if @debug
  @log.debug("handle_decline") if @debug
end
handle_discover(packet, source_ip, source_port) click to toggle source

Handle DHCPDISCOVER packet:

# File lib/dhcp/server.rb, line 159
def handle_discover(packet, source_ip, source_port)
  show_packet(packet) if @debug
  @log.debug("handle_discover") if @debug
end
handle_inform(packet, source_ip, source_port) click to toggle source

Handle DHCPINFORM packet:

# File lib/dhcp/server.rb, line 171
def handle_inform(packet, source_ip, source_port)
  show_packet(packet) if @debug
  @log.debug("handle_inform") if @debug
end
handle_leasequery(packet, source_ip, source_port) click to toggle source

Handle DHCPLEASEQUERY packet:

# File lib/dhcp/server.rb, line 183
def handle_leasequery(packet, source_ip, source_port)
  show_packet(packet) if @debug
  @log.debug("handle_leasequery") if @debug
end
handle_release(packet, source_ip, source_port) click to toggle source

Handle DHCPRELEASE packet:

# File lib/dhcp/server.rb, line 189
def handle_release(packet, source_ip, source_port)
  show_packet(packet) if @debug
  @log.debug("handle_release") if @debug
end
handle_request(packet, source_ip, source_port) click to toggle source

Handle DHCPREQUEST packet:

# File lib/dhcp/server.rb, line 165
def handle_request(packet, source_ip, source_port)
  show_packet(packet) if @debug
  @log.debug("handle_request") if @debug
end
relay_authorized?(source_ip, giaddr) click to toggle source
# File lib/dhcp/server.rb, line 154
def relay_authorized?(source_ip, giaddr)
  true
end
run() click to toggle source

Main server event loop (blocking):

# File lib/dhcp/server.rb, line 58
def run
  loop do
    result = run_once
    sleep @interval if !result  ## Sleep if no data was received and no errors occured
  end
end
run_once() click to toggle source

Main server event single-iteration function (non-blocking):

# File lib/dhcp/server.rb, line 38
def run_once
  raise "Cannot run after DHCP server has been shut down!" if @sock.nil?
  r,w,e = IO.select([@sock], nil, [@sock], 0)
  if !r.nil? && r.size == 1
    data, src = @sock.recvfrom_nonblock(1500)
    if data.bytesize < 300
      @log.debug("Ignoring packet smaller than BOOTP minimum size") if @debug
    else
      dispatch_packet(data, IPAddress(src[3]), src[1])
    end
    return true
  end
  if !e.nil? && e.size == 1
    ## TODO: Handle errors...
    raise "Unhandled error on socket"
  end
  return false
end
send_response(packet, dest_ip, dest_port) click to toggle source
# File lib/dhcp/server.rb, line 194
def send_response(packet, dest_ip, dest_port)
  data = packet.to_packet
  dest_ip = ALLONES_IP if dest_ip == ZERO_IP
  if @debug
    @log.debug(">>> SENDING DHCP #{packet.type_name} (#{packet.type}) PACKET to [#{dest_ip}]:#{dest_port}")
    packet.to_s.gsub(/\/,'\\').gsub(/[^\x20-\x7e\n]/){|x| '\x' + x.unpack('H2')[0].upcase}.split("\n").each do |i|
      @log.debug("..." + i)
    end
    @log.debug("<<< END OF PACKET <<<")
  end
  @sock.send(data, 0, dest_ip.to_s, dest_port)
end
show_packet(pk) click to toggle source
# File lib/dhcp/server.rb, line 65
def show_packet(pk)
  @log.debug(">>> PACKET: #{pk.type} '#{pk.type_name}' at #{Time.now} >>>")
  pk.to_s.gsub(/\/,'\\').gsub(/[^\x20-\x7e\n]/){|x| '\x' + x.unpack('H2')[0].upcase}.split("\n").each do |i|
    @log.debug("..." + i)
  end
  @log.debug("<<< END OF PACKET <<<")
end
shutdown() click to toggle source
# File lib/dhcp/server.rb, line 29
def shutdown
  unless @sock.nil?
    @sock.close
    @sock = nil
    @log.info("Stopping DHCP on [#{@server_ip}]:#{@server_port} server at #{Time.now}")
 end
end