require 'socket' # one dhcp packet class Dhcp MAGIC_COOKIE = 0x63825363 OP = { 1 => 'REQUEST', 2 => 'REPLY', } TYPE = { 1 => 'DISCOVER', 2 => 'OFFER', 3 => 'REQUEST', 4 => 'DECLINE', 5 => 'ACK', 6 => 'NACK', 7 => 'RELEASE', 8 => 'INFORM', } OPTION = %w[PAD SUBNETMASK TIMEOFFSET ROUTER TIMESERVER NAMESERVER DNS LOGSERV QUOTESSERV LPRSERV IMPSERV RESSERV HOSTNAME BOOTFILESIZE DUMPFILE DOMAINNAME SWAPSERV ROOTPATH EXTENPATH IPFORWARD SRCROUTE POLICYFILTER MAXASMSIZE IPTTL MTUTIMEOUT MTUTABLE MTUSIZE LOCALSUBNETS BROADCASTADDR DOMASKDISCOV MASKSUPPLY DOROUTEDISC ROUTERSOLICIT STATICROUTE TRAILERENCAP ARPTIMEOUT ETHERENCAP TCPTTL TCPKEEPALIVE TCPALIVEGARBAGE NISDOMAIN NISSERVERS NISTIMESERV VENDSPECIFIC NBNS NBDD NBTCPIP NBTCPSCOPE XFONT XDISPLAYMGR DISCOVERADDR LEASETIME OPTIONOVERLOAD MESSAGETYPE SERVIDENT PARAMREQUEST MESSAGE MAXMSGSIZE RENEWTIME REBINDTIME CLASSSID CLIENTID x x NISPLUSDOMAIN NISPLUSSERVERS x x MOBILEIPAGENT SMTPSERVER POP3SERVER NNTPSERVER WWWSERVER FINGERSERVER IRCSERVER STSERVER STDASERVER] class Option attr_accessor :type, :v def initialize(type, val) @type, @val = type, val @type = OPTION.index(@type.to_s) || @type if OPTION[@type] == 'MESSAGETYPE' @val = TYPE[@val[0]] || @val[0] end end def encode case type when 255: type.chr when OPTION.index('MESSAGETYPE'): type.chr + (TYPE.index(@val) || @val).chr else type.chr + @val end end def inspect "" end end Fields = [:op, :htype, :hlen, :hops, :xid, :secs, :flags, :ciaddr, :yiaddr, :siaddr, :giaddr, :chaddr, :sname, :file] attr_accessor(*Fields) attr_accessor :options def initialize(*vals) Fields.each { |f| instance_variable_set("@#{f}", vals.shift) } [@chaddr, @sname, @file].each { |s| s.sub!(/\0*$/, '') } if vals.shift == MAGIC_COOKIE @options = [] while opt = parse_option(vals) @options << opt end end end def parse_option(opts) type = opts.shift case type = OPTION.index(type) || type when 'PAD': Option.new(type, nil) # pad when 255 # end else len = opts.shift o = Option.new(type, opts[0, len].pack('C*')) opts[0, len] = [] o end end class Zero def self.to_str ; '' end def self.to_int ; 0 end end def encode str = Fields.map { |f| instance_variable_get("@#{f}") || Zero }.pack('C4Nn2N4a16a64a128') str += [MAGIC_COOKIE].pack('N') @options.to_a.each { |op| str += op.encode } str end def self.decode(str) new(*str.unpack('C4Nn2N4a16a64a128NC*')) end def inspect fds = Fields + ['options'] fds = fds.map { |f| "#{f}=#{instance_variable_get("@#{f}").inspect}" } "#<#{self.class}:#{'0x%08x' % object_id} #{fds.join(' ')}>" end end class Dhcpd def initialize(addr=0, port=67) @lfd = UDPSocket.open @lfd.bind(addr, port) end def mainloop loop do puts 'wait cx' if $VERBOSE nil while not IO.select([@lfd], nil, nil, 1) handle_newclient end end def handle_newclient packet, from = @lfd.recvfrom(516) p = Dhcp.decode(packet) @c_ip, @c_p = from[3], from[1] puts "#{Time.now.strftime('%H:%M:%S')} rq from #@c_ip:#@c_p - #{p.inspect}" if $VERBOSE # respond @ofd = @lfd # populate the arp cache with the newly assigned address, and respond using it arpreq = [ifindex=0, flags=0, maclen=6, ipaddr, hwaddr, pad=''].pack('LLL16C16C242C') @ofd.ioctl(SIOCSARP, arpreq) @ofd.send(resp, c_ip, c_p) @c_ip = @c_p = nil end end if __FILE__ == $0 if not ARGV.delete('-c') # addr port Dhcpd.new(*ARGV).mainloop else # TODO broadcast eth srv = ARGV.shift eth = [ARGV.shift.delete(':')].pack('H*') rq = Dhcp.eth(eth, 'DISCOVER').add_option('PARAMREQUEST', "\001\034\002\003\017\006w\f,/\032y") end end