diff --git a/dhcp/gendhcp.pl b/dhcp/gendhcp.pl new file mode 100755 index 0000000..4591887 --- /dev/null +++ b/dhcp/gendhcp.pl @@ -0,0 +1,640 @@ +#! /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