#! /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 <<END_USAGE;

Usage: $0 <flag> <value>

\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
