#! /usr/bin/perl # # File: gendhcp.p # Author: Darin Davis, Copyright 1998 # Date: 19 January 1998 # Update: 28 October 1998 # Desc: gendhcp.p is designed to test the ability of a DHCP server # to respond to a large number of DHCP client requests submitted # in rapid succession. Each DISCOVER message contains a unique: # # - Hardware address (CHADDR), beginning with '0fffffff' # and ending with a unique 16-bit number (starting by # default with 1). # - Client ID, identical to the hardware address # - XID, beginning with 'eeee' and ending with the same # ending number as the hardware address # - Hostname, beginning with 'dhcp-client-' and ending # with the same ending number as the hardware address # except in decimal rather than hex # # The script logic can be outlined as follows: # # 1 - Blast out all the DISCOVER msgs and optionally wait a # user-specified number of seconds to give the server a chance # to respond. # # 2 - Send REQUESTS for all the OFFERs in my input socket. # # 3 - Make note of any ACKs I receive. # # 4 - If I haven't received all the OFFERs, then resend DISCOVERs # for each DISCOVER as yet unOFFERed and optionally wait a # user-specified number of seconds to give the server a chance # to respond and go to step 2. # # 5 - If I have received all the OFFERs but not all the ACKs, then # resend a DISCOVER for each ACK I am missing and go to step 2. # Note that this is a departure from the RFC which suggests the # client should reREQUEST not reDISCOVER. But the implementation # was simpler this way. # # When the reDISCOVER is performed, the same XID is used. # This can cause problems for later versions of the QIP DHCP # server which have a configuration parameter "CheckTransactionID". # This parameter defaults to TRUE, meaning that the server will # not issue an address for a previously detected Transaction ID # (XID)/MAC address tuple. To suppress this behavior, set the # "CheckTransactionID" policy to FALSE by putting the following # into the dhcpd.pcy file: # # CheckTransactionID=0 # # Thanks to Gary DiFazio for providing this information about QIP. # # There are no timeouts in the script. Only the optional wait. # # gendhcp.p simulates the four packet exchange DISCOVER-OFFER- # REQUEST-ACK, and maintains two associative arrays, one for # outstanding OFFERs and one for outstanding ACKs. The arrays # are indexed by the CHADDR of the DISCOVER packet. gendhcp.p # first blasts out all the DISCOVER messages, noting the CHADDRs # in the Offers and Acks arrays, and then goes into a loop waiting # for responses. If it receives an OFFER, it sends a REQUEST and # deletes the CHADDR from the Offers array. If it receives an ACK, # it deletes the CHADDR from the Acks array. If there are no # responses from a server to read but there are still outstanding # OFFERs or ACKs, it reDISCOVERS everything in the Offers array (or # if empty, the Acks array). It continues this process until all # ACKs have been received. # # Some DHCP servers send their OFFER messages to the IP broadcast # address. Others (such as NTS IPserver) address the OFFER to the # IP address which it is offering. Because the OFFER is not # addressed to the machine on which you run gendhcp.p, gendhcp.p # can't receive the offer. So, to test a server of the latter # variety, use the '-r' option. This puts gendhcp.p in "relay # agent" mode. It puts your machine's address in the GIADDR field, # convincing the unsuspecting DHCP server that your machine is # actually a router. So, the DHCP server addressees the OFFER to # your machine. # # The author used tcpdump (on the same machine as gendhcp.p) in # attempt to measure the effectiveness of the script. tcpdump # didn't capture all the packets sent and received. Therefore, # you would do well to use a standalone Sniffer-like device for # any tracing. # # gendhcp.p is intended for testing a single DHCP server. If # multiple DHCP servers are active, gendhcp.p will REQUEST only # one IP address for a given hardware address (just as a normal # DHCP client would). # # Developed for Linux with modifications for Solaris. # # Usage: See &usage(). ######################## ### GLOBAL CONSTANTS ### ######################## # define which OS this is if (system("test `uname` = SunOS") == 0) { $OS = "SunOS"; } else { $OS = "Linux" } #print "OS = $OS\n"; $AF_INET = 2; # define address family if ($OS eq "Linux") { # Linux-specific definitions $SOCK_DGRAM = 2; # define socket type $SO_BROADCAST = 6; $SOL_SOCKET = 1; } else { # Solaris-specific definitions $SOCK_DGRAM = 1; # define socket type $SO_BROADCAST = 32; $SOL_SOCKET = 65535; } $SOCKADDR_FMT = 'S n a4 x8'; # set pack format $DHCP_SERVER_PORT = 67; $DHCP_CLIENT_PORT = 68; $MAX_LEN = 1024; # max length of datagram $FLAGS = 0; # flags for recv() $DISC_MAX = 65000; # maximum number of discover msgs # to be generated $INADDR_ANY = pack("C4", split(/\./, "0.0.0.0")); $SO_RCVBUF = 8; $OPT_VAL = 1; $TRUE = 1 == 1; $DISC_MSG = 1; $OFFER_MSG = 2; $RQST_MSG = 3; $ACK_MSG = 5; $DELAY_MAX = 10; # limit on how long to wait before # reading server replies ######################## ### GLOBAL VARIABLES ### ######################## $Server_hostname = "255.255.255.255"; # default name of server host $Server_pport; # packed remote port number @Server_ipaddr; # server's IP address @Client_ipaddr; # my IP address *CS; # descriptor of socket bound to # local DHCP client port *SS; # descriptor of socket bound to # local DHCP server port $Protocol_name; # name of UDP protocol $Protocol_aliases; # aliases of UDP $Protocol_number; # UDP's protocol number $Addr_family; # client's address family $Discover_count = 1; # number of discover msgs to send $Datagram; # server's reply message $Suppress_request = ! $TRUE; # DO send requests $Offer_cnt = 0; # number of OFFERs received $Ack_cnt = 0; # number of ACKs received $Relay = ! $TRUE; # boolean flag: should I act like a # BOOTP relay agent (by listening # for offers on the BOOTPS port)? $Haddr = 1; # initial hardware address suffix $Rc; # Return Code for system calls %Offers; # outstanding (unreceived) OFFER msgs %Acks; # outstanding (unreceived) ACK msgs @Start_times; # user, system and elapsed start times @End_times; # user, system and elapsed end times $Delay = 0; # how long to wait before trying to # read replies from server $Retry = 0; # how many times we have to reDISCOVER $Last_ack_cnt = 0; # last count of ACKs received $Progress = ! $TRUE; # boolean flag: should I show the # number of ACKs received? ################### ### SUBROUTINES ### ################### ##################################### # defines functions needed to set # socket to non-blocking using fcntl ##################################### sub F_SETFL {4;} if ($OS eq "Linux") { # Linux-specific definition sub O_NONBLOCK {04000;} } else { # Solaris-specific definition sub O_NONBLOCK {0x80;} } ###################################### # creates socket and binds to # DHCP client port # GLOBAL VARS: Server_hostname, # Server_pport, Client_ipaddr, CS, SS ###################################### sub setup_socket { local($client_hostname, # name of this host $client_pport # packed local port number ); chop($client_hostname = `hostname`); # get my hostname # get UDP's protocol number ($Protocol_name,$Protocol_aliases,$Protocol_number) = getprotobyname('udp'); # get my IP address ($Protocol_name,$Protocol_aliases,$type,$len,@Client_ipaddr) = gethostbyname($client_hostname); # get server's IP address ($Protocol_name,$Protocol_aliases,$type,$len,@Server_ipaddr) = gethostbyname($Server_hostname); # define fully-specified port addrs $client_pport = pack($SOCKADDR_FMT, $AF_INET, $DHCP_CLIENT_PORT, $INADDR_ANY); $Server_pport = pack($SOCKADDR_FMT, $AF_INET, $DHCP_SERVER_PORT, $Server_ipaddr[0]); # create UDP socket socket(CS, $AF_INET, $SOCK_DGRAM, $Protocol_number) || die("$0: cannot create CS socket ($!)"); # set socket to non-blocking ($Rc = fcntl(CS, &F_SETFL, &O_NONBLOCK)) || printf("error (%d) setting CS to non-blocking\n", $Rc); # enable send/recv broadcasts $Rc = setsockopt(CS, $SOL_SOCKET, $SO_BROADCAST, $OPT_VAL); if (! defined($Rc)) { print "error calling setsockopt(SO_BROADCAST) for CS\n"; } # bind to the socket bind(CS, $client_pport) || die("$0: cannot bind CS socket to DHCP client port ($!)"); # setup extra socket in case we # have to act as the relay agent if ($Relay) { $client_pport = pack($SOCKADDR_FMT, $AF_INET, $DHCP_SERVER_PORT, $INADDR_ANY); # create UDP socket socket(SS, $AF_INET, $SOCK_DGRAM, $Protocol_number) || die("$0: cannot create SS socket ($!)"); # set socket to non-blocking ($Rc = fcntl(SS, &F_SETFL, &O_NONBLOCK)) || printf("error (%d) setting SS to non-blocking\n", $Rc); # enable send/recv broadcasts $Rc = setsockopt(SS, $SOL_SOCKET, $SO_BROADCAST, $OPT_VAL); if (! defined($Rc)) { print "error calling setsockopt(SO_BROADCAST) for SS\n"; } # bind to the socket bind(SS, $client_pport) || die("$0: cannot bind SS socket to DHCP server port ($!)"); } } ##################################### # builds and sends $discover_count # number of DHCP DISCOVER messages # GLOBAL VARS: CS, FLAGS, Server_pport # Haddr ##################################### sub send_discovers { local($discover_count) = @_; local($xid, $msg, $hostname, $hnlen, $tmp); $xid = $Haddr; # initialize incremental variables # build and send DISCOVER messages while ($discover_count-- > 0) { $msg = sprintf("0fffffff%04x", $Haddr); $Offers{$msg} = $TRUE; # this DISCOVER msg has an outstanding # OFFER msg $Acks{$msg} = $TRUE; # this DISCOVER msg has an outstanding # ACK msg # op, htype, hlen, hops $msg = pack("H2" x 4, "01", "01", "06", "00"); # xid, secs, flags $msg .= pack("H4 n H8", "eeee", $xid++, 0); # ciaddr, yiaddr, siaddr, giaddr if ($Relay) { $msg .= pack("H8 H8 H8 C4", 0, 0, 0, unpack("C4", $Client_ipaddr[0])); } else { $msg .= pack("H8 H8 H8 H8", 0, 0, 0, 0); } # chaddr $msg .= pack("H8 n H20", "0fffffff", $Haddr, 0); $msg .= pack("H384", "0"); # sname, file $msg .= pack("H8", "63825363"); # magic cookie $msg .= pack("H6", "350101"); # message type DISCOVER # client ID = chaddr $msg .= pack("H4 H8 n", "3d06", "0fffffff", $Haddr); ### create unique hostname ### $tmp = sprintf("dhcp-client-%05d", $Haddr++); $hostname = ""; while (length($tmp) > 0) { # convert string to hex $hostname .= sprintf("%02x", ord($tmp)); $tmp =~ s/^.//; } $hnlen = length($hostname); # hostname length $msg .= pack("H2 C H$hnlen", "0c", $hnlen/2, "$hostname"); $msg .= pack("H2", "ff"); # end of options # send msg send(CS, $msg, $FLAGS, $Server_pport); } } ##################################### # resends DHCP DISCOVER messages # for each HW addr in %Offers # GLOBAL VARS: CS, FLAGS, Server_pport # Haddr, Retry, Delay # RETURNS: number of DISCOVERs sent ##################################### sub rediscover { local($xid, $msg, $cnt, $hostname, $hnlen, $tmp); $cnt = 0; $Retry++; # build and send DISCOVER messages foreach $Haddr (keys %Offers) { $xid = $Haddr; $xid =~ s/^0fffffff//; # behead MAC addr prefix # op, htype, hlen, hops $msg = pack("H2" x 4, "01", "01", "06", "00"); # xid, secs, flags $msg .= pack("H4 n H8", "eeee", $xid, 0); # ciaddr, yiaddr, siaddr, giaddr if ($Relay) { $msg .= pack("H8 H8 H8 C4", 0, 0, 0, unpack("C4", $Client_ipaddr[0])); } else { $msg .= pack("H8 H8 H8 H8", 0, 0, 0, 0); } # chaddr $msg .= pack("H12 H20", $Haddr, 0); $msg .= pack("H384", "0"); # sname, file $msg .= pack("H8", "63825363"); # magic cookie $msg .= pack("H6", "350101"); # message type DISCOVER # client ID = chaddr $msg .= pack("H4 H12", "3d06", $Haddr); ### create unique hostname ### $tmp = sprintf("dhcp-client-%05d", $xid); $hostname = ""; while (length($tmp) > 0) { # convert string to hex $hostname .= sprintf("%02x", ord($tmp)); $tmp =~ s/^.//; } $hnlen = length($hostname); # hostname length $msg .= pack("H2 C H$hnlen", "0c", $hnlen/2, "$hostname"); $msg .= pack("H2", "ff"); # end of options # send msg send(CS, $msg, $FLAGS, $Server_pport); $cnt++; # incr count of msgs sent } if ($Delay > 0) { sleep($Delay); } # give server a chance to respond return($cnt); } ################################### # builds and sends a single DHCP # REQUEST message (if the message # from the server is an OFFER not # an ACK) # GLOBAL VARS: CS and almost every # other "local" var used herein # RETURNS: DHCP msg type ################################### sub send_request { local($datagram) = @_; local($server_id, $msg_type, $tmp_chaddr, $client_id, $tmp, $hostname, $hnlen); $msg_type = 0; # init local vars $server_id = ""; $DHCP_STRUCT = "H2 H2 H2 H2 H8 H4 H4 H8 H8 H8 H8 H32 H384 H8 H*"; ($op, $htype, $hlen, $hops, $xid, $secs, $flags, $ciaddr, $yiaddr, $siaddr, $giaddr, $chaddr, $sname_file, $cookie, $options) = unpack($DHCP_STRUCT, $datagram); $tmp_opt = $options; $tmp_chaddr = substr($chaddr, 0, 12); # trunc trailing zeros # determine whether this is an ACK # or OFFER by searching through # options while (length($tmp_opt) > 3) { # ASSUMES no pad options! $option = substr($tmp_opt, 0, 2); # extract option $length = substr($tmp_opt, 2, 2); # extract length # extract value $value = substr($tmp_opt, 4, 2 * hex($length)); # behead leading option $tmp_opt = substr($tmp_opt, 4 + 2 * hex($length)); if ($option eq "35") { # if this is DHCP_Msg_Type option $msg_type = hex($value); if ($msg_type == $ACK_MSG) { # if this is an ACK # if I haven't yet seen this addr's ACK if (defined($Acks{$tmp_chaddr})) { delete($Acks{$tmp_chaddr}); # remove addr from ACK list $Ack_cnt++; # increment number of ACKs recv'd } return($msg_type); # stop processing } if ($msg_type != $OFFER_MSG) { # if this is NOT an OFFER return($msg_type); # then stop processing } } if ($option eq "36") { # if this is Server_ID option $server_id = $value; } } if ($msg_type != $OFFER_MSG) { # there was no DHCP_Msg_Type option return($msg_type); # so stop processing } # this addr no longer has an # outstanding OFFER if (defined($Offers{$tmp_chaddr})) { delete($Offers{$tmp_chaddr}); # remove addr from list $Offer_cnt++; # increment number of OFFERs recv'd } else { return($msg_type); } $rqst_msg = "350103"; # build REQUEST message type $addr_rqst = "3204$yiaddr"; # build address request if ($server_id) { $srvr_id = "3604$server_id"; # server identifier } else { $srvr_id = "3604$siaddr"; } $client_id = "3d06$tmp_chaddr"; # client identifier ### create unique hostname ### $tmp_chaddr =~ s/^0fffffff//; # behead prefix $tmp = sprintf("dhcp-client-%05d", hex($tmp_chaddr)); $hostname = ""; while (length($tmp) > 0) { # convert string to hex $hostname .= sprintf("%02x", ord($tmp)); $tmp =~ s/^.//; } $hnlen = length($hostname); # hostname length $hostname = sprintf("0c%02x$hostname", $hnlen/2); $end_opt = "ff"; # end of options $datagram = pack($DHCP_STRUCT, "01", $htype, $hlen, $hops, $xid, $secs, $flags, $ciaddr, "00000000", "00000000", $giaddr, $chaddr, $sname_file, $cookie, "$rqst_msg$addr_rqst$srvr_id$client_id$hostname$end_opt"); # send REQUEST msg send(CS, $datagram, $FLAGS, $Server_pport); return($msg_type); } ######################### # prints program usage ######################### sub usage { print < \tFlags\tValues \t-a\tstarting hardware Address (default: 1) \t-d\tnumber of Discover messages to send (default: 1) \t-h, -?\tshow Help \t-p\tshow Progress (number of ACKs received) \t-r\tact like a Relay agent \t-R\tsuppress sending REQUEST messages \t-s\tServer hostname or IP address (default: broadcast) \t-w\tWait time (in seconds) between retries (default: 0) END_USAGE exit; } ################# ### MAIN CODE ### ################# $| = 1; # flush writes # ensure we're root so we can # bind to priviledged sockets chomp($USER = `whoami`); if ($USER ne "root") { print "\n\t*** You must be root to run this program. ***\n"; &usage; } # process arguments while($#ARGV >= 0) { ARG_SWITCH: { if ($ARGV[0] =~ /^(-h|-\?)$/) { &usage; } # show progress if ($ARGV[0] =~ /^-p$/) { shift(@ARGV); # discard flag $Progress = $TRUE; last ARG_SWITCH; } # don't send request messages if ($ARGV[0] =~ /^-R$/) { shift(@ARGV); # discard flag $Suppress_request = $TRUE; last ARG_SWITCH; } # act as a relay agent if ($ARGV[0] =~ /^-r$/) { shift(@ARGV); # discard flag $Relay = $TRUE; last ARG_SWITCH; } # user specified server if ($ARGV[0] =~ /^-s$/) { shift(@ARGV); # discard flag $Server_hostname = $ARGV[0]; shift(@ARGV); # discard server hostname last ARG_SWITCH; } # user specified DISCOVER count if ($ARGV[0] =~ /^-d$/) { shift(@ARGV); # discard flag $Discover_count = $ARGV[0]; if ($Discover_count < 1 || $Discover_count > $DISC_MAX) { print "Discover count must be between 1 and $DISC_MAX.\n"; exit; } shift(@ARGV); # discard server hostname last ARG_SWITCH; } # user specified Wait time if ($ARGV[0] =~ /^-w$/) { shift(@ARGV); # discard flag $Delay = $ARGV[0]; if ($Delay < 0 || $Delay > $DELAY_MAX) { print "Wait time be between 0 and $DELAY_MAX.\n"; exit; } shift(@ARGV); # discard Wait time last ARG_SWITCH; } # user specified starting HW addr if ($ARGV[0] =~ /^-a$/) { shift(@ARGV); # discard flag $Haddr = $ARGV[0]; if ($Haddr < 1 || $Haddr > $DISC_MAX) { print "Starting hardware address must be between 1 and $DISC_MAX.\n"; exit; } shift(@ARGV); # discard server hostname last ARG_SWITCH; } print "Unknown flag '$ARGV[0]'.\n"; exit; } } # create socket capable of broadcasts &setup_socket($Server_hostname); # get the start user, system, and... ($Start_times[0], $Start_times[1], $junk, $junk) = times; $Start_times[2] = time; # ...absolute times # blast out all the DISCOVER msgs print "Sending $Discover_count DISCOVER messages...\n"; &send_discovers($Discover_count); if ($Delay > 0) { sleep($Delay); } # give server a chance to respond if ($Suppress_request) { exit; } # should I process the OFFER msgs? # yes print "Waiting for $Discover_count ACKs...\n"; while ($Ack_cnt < $Discover_count) { # if I'm supposed to report progress # then do so if ($Progress) { if (($Ack_cnt % 10) == 0 && $Ack_cnt > $Last_ack_cnt) { print "$Ack_cnt ACKs...\n"; $Last_ack_cnt = $Ack_cnt; } } # receive the OFFER/ACK msg if ($Relay) { $Rc = recv(SS, $Datagram, $MAX_LEN, $FLAGS); } else { $Rc = recv(CS, $Datagram, $MAX_LEN, $FLAGS); } if (! defined($Rc)) { # if no more incoming messages # to process if ($Offer_cnt < $Discover_count) { &rediscover; } else { # we've received all the OFFERs if ($Ack_cnt < $Discover_count) { # if we're missing some ACKs %Offers = %Acks; # then reDISCOVER (rather than # reREQUEST as advised in RFC2131; # this choice is for the sake of # simplicity in this program) $Offer_cnt = $Ack_cnt; &rediscover; } } } else { # I have a reply to process &send_request($Datagram); # send the REQUEST msg } } ### print statistics ### print "Received $Ack_cnt Acks.\n"; # get the ending user, system, and... ($End_times[0], $End_times[1], $junk, $junk) = times; $End_times[2] = time; # ...absolute times printf("\nTimes: %.2f user, %.2f system, %d elapsed\n", $End_times[0] - $Start_times[0], $End_times[1] - $Start_times[1], $End_times[2] - $Start_times[2]); print "Number of reDISCOVERies: $Retry\n"; close(CS); # close client socket if ($Relay) { close(SS); } # close server socket