The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Net::DHCP::Packet.pm
# Version 0.0
# Author: F. van Dun
#
# Some information about a DHCP packet from RFC 1497:
#	FIELD      OCTETS       DESCRIPTION
#   -----      ------       -----------
#
#   op            1  Message op code / message type.
#                    1 = BOOTREQUEST, 2 = BOOTREPLY
#   htype         1  Hardware address type, see ARP section in "Assigned
#                    Numbers" RFC; e.g., '1' = 10mb ethernet.
#   hlen          1  Hardware address length (e.g.  '6' for 10mb
#                    ethernet).
#   hops          1  Client sets to zero, optionally used by relay agents
#                    when booting via a relay agent.
#   xid           4  Transaction ID, a random number chosen by the
#                    client, used by the client and server to associate
#                    messages and responses between a client and a
#                    server.
#   secs          2  Filled in by client, seconds elapsed since client
#                    began address acquisition or renewal process.
#   flags         2  Flags (see figure 2).
#   ciaddr        4  Client IP address; only filled in if client is in
#                    BOUND, RENEW or REBINDING state and can respond
#                    to ARP requests.
#   yiaddr        4  'your' (client) IP address.
#   siaddr        4  IP address of next server to use in bootstrap;
#                    returned in DHCPOFFER, DHCPACK by server.
#   giaddr        4  Relay agent IP address, used in booting via a
#                    relay agent.
#   chaddr       16  Client hardware address.
#   sname        64  Optional server host name, null terminated string.
#   file        128  Boot file name, null terminated string; "generic"
#                    name or null in DHCPDISCOVER, fully qualified
#                    directory-path name in DHCPOFFER.
#   options     var  Optional parameters field.  See the options
#                    documents for a list of defined options.
#
#           Table 1:  Description of fields in a DHCP message
#
# Difference between bootp and DHCP:
#   The first four octets of the 'options' field of the DHCP message
#   contain the (decimal) values 99, 130, 83 and 99, respectively (this
#
#   is the same magic cookie as is defined in RFC 1497).  The remainder
#   of the 'options' field consists a list of tagged parameters that are
#   called "options".  All of the "vendor extensions" listed in RFC 1497
#   are also DHCP options.  A separate document gives the complete set of
#   options defined for use with DHCP [2].

package Net::DHCP::Packet;
use Socket;
use strict;
use Carp;
use Net::DHCP::Options;
use vars qw($VERSION);
$VERSION=0.1;

=pod

=head1 NAME

Net::DHCP::Packet - Object methods to create a DHCP packet.

=head1 SYNOPSIS

    use Net::DHCP::Packet;
	use strict;

	my $p = new Net::DHCP::Packet('Chaddr' => '0??BCDEF', 
				'Xid' => hex(0x9F0FD),
				'Ciaddr' => '0.0.0.0',
				'Siaddr' => '0.0.0.0', 'Hops' => 0);

=head1 DESCRIPTION

Represents a DHCP packet as specified in RFC 1533, RFC 2132.

=head2 CONSTRUCTORS

=item new

=item discover

=item request

=item decline

=item release


=head2 METHODS

=cut

# Opcode
sub BOOTREQUEST() 	{ 0x1 }
sub BOOTREPLY() 	{ 0x2 }

sub randomstring($) {
	my $len = shift;
	my $c;
	while ($len--) {
		$c .= chr(int (rand(255)));
	}
	return $c;
}

sub mac2str($$) {
	my $nibbles = shift(@_) * 2;
	my $str = unpack("H$nibbles", shift);
	return $str;
}

sub str2mac($) {
	pack('H*', shift);
}

=pod

=item op(BYTE opcode)

	Sets the opcode in the BOOTP type.
	Without argument, returns the BOOTP type.
	Argument is either:
	Net::DHCP::Packet::BOOTREQUEST() 	{ pack('C',0x1) }
	Net::DHCP::Packet::BOOTREPLY() 	{ pack('C',0x2) }

=cut
sub op {
	my ($self, $arg) = @_;
	$self->{op} = $arg unless (!defined ($arg));
	return $self->{op};
}
=pod

=item htype(BYTE hardware address type)

	Ex. '1' = 10mb ethernet

=cut
sub htype {
	my ($self, $arg) = @_;
	$self->{htype} = $arg unless (!defined ($arg));
	return $self->{htype};
}
=pod

=item hlen(BYTE hardware address length)

	For most NIC's, the MAC address has 6 bytes.

=cut
sub hlen {
	my ($self, $arg) = @_;
	$self->{hlen} = $arg unless (!defined ($arg));
	return $self->{hlen};
}
=pod

=item hops(BYTE number of hops)

	This field is incremented by each encountered DHCP relay agent.	
	
=cut
sub hops {
	my ($self, $arg) = @_;
	$self->{hops} = $arg unless (!defined ($arg));
	return $self->{hops};
}
=pod

=item xid(C4 transaction id)

	4 byte transaction id.

=cut
sub xid {
	my ($self, $arg) = @_;
	$self->{xid} = $arg unless (!defined ($arg));
	return $self->{xid};
}
=pod

=item secs(SHORT elapsed boot time)

	2 bytes for elapsed boot time.

=cut
sub secs {
	my ($self, $arg) = @_;
	$self->{secs} = $arg unless (!defined ($arg));	# unsigned Short. Exactly 16 bits.
	return $self->{secs};
}
=pod

=item flags(SHORT)

	2 bytes. 
	0x0000 = No broadcasts.
	0x1000 = Broadcasts.

=cut
sub flags {
	my ($self, $arg) = @_;
	$self->{flags} = $arg unless (!defined ($arg));
	return $self->{flags};
}
=pod

=item ciaddr(IP address)

	IP address is an ascci string like '10.24.50.3'.
	
=cut
sub ciaddr {
	my ($self, $arg) = @_;
	$self->{ciaddr} = $arg unless (!defined($arg));
	return $self->{ciaddr};
}
=pod

=item yiaddr(IP address)

=cut
sub yiaddr {
	my ($self, $arg) = @_;
	$self->{yiaddr} = $arg unless (!defined($arg));
	return $self->{yiaddr};
}
=pod

=item siaddr(IP address)

=cut
sub siaddr {
	my ($self, $arg) = @_;
	$self->{siaddr} = $arg unless(!defined($arg));
	return $self->{siaddr};
}
=pod

=item giaddr(IP address)

=cut
sub giaddr {
	my ($self, $arg) = @_;
	$self->{giaddr} = $arg unless (!defined($arg));
	return $self->{giaddr};
}
=pod

=item chaddr(MAC address)
	Hexadecimal string represenatation.
	Example: "0010A706DFFF" for 6 bytes mac address.
	
=cut
sub chaddr {
	my ($self, $arg) = @_;
	$self->{chaddr} = $arg unless (!defined ($arg));
	return $self->{chaddr};
}

=pod

=item sname(C64 servername)

	Optional 64 bytes null terminated string with server host name.

=cut
sub sname {
	my ($self, $arg) = @_;
	$self->{sname} = $arg unless (!defined ($arg));
	return $self->{sname};
}
=pod

=item file(C128 bootfilename)
	
	Optional

=cut
sub file {
	my ($self, $arg) = @_;
	$self->{file} = $arg unless (!defined ($arg));
	return $self->{file};
}

=pod

=item options(REF Net::DHCP::Options)

		Argument is reference to a Net::DHCP::Options object.
		Without argument, returns the Options object.

=cut
sub options {
	my ($self, $arg) = @_;
	$self->{options} = $arg unless (!defined ($arg));
	return $self->{options};
}
=pod

=item addOption($type, $value)

=cut
sub addOption {
	my ($self, $type, $value) = @_;
	$self->{options}->setOption($type,$value);
}

=item getOption($type)

=cut
sub getOption {
	my ($self, $type) = @_;
	$self->{options}->getOption(chr($type));
}
=pod

=item new(%ARGS)

	The hash %ARGS  can contain any of these keys:
	Op, Htype, Hlen, Hops, Xid, Secs, Flags, Ciaddr, Yiaddr, Siaddr, 
	Giaddr, Chaddr, Sname, File
	
=cut

sub new {
	my ($class, %args)	= @_;
	
	my $self = {};
	bless $self, $class;
	$self->op($args{Op} || BOOTREQUEST());
	$self->htype($args{Htype} || 1);	# 10mb ethernet
	$self->hlen($args{Hlen} || 6);		# Use 6 bytes MAC by default
	$self->hops($args{Hops} || 0);
	$self->xid($args{Xid} || randomstring(4));
	$self->secs($args{Secs} || 0);
	$self->flags($args{Flags} || 0);
	$self->ciaddr($args{Ciaddr} || 0);
	$self->yiaddr($args{Yiaddr} || 0);
	$self->siaddr($args{Siaddr} || 0);
	$self->giaddr($args{Giaddr} || 0);
	$self->chaddr($args{Chaddr} || randomstring(ord($self->hlen()) ) );
	$self->sname($args{Sname} || chr(0));
	$self->file($args{File} || chr(0));
	$self->{options} = new Net::DHCP::Options();
	return $self;
}
=pod

=item discover

	DHCP discover packet

=cut
sub discover {
	my ($class, %args)	= @_;
	
	my $self =$class->new(%args);
	$self->addOption(Net::DHCP::Options::MESSAGE_TYPE(),Net::DHCP::Options::DISCOVER());	
	$self->addOption(Net::DHCP::Options::CLIENT_ID(), str2mac( '01' . $self->{chaddr} ) );
	$self->addOption(Net::DHCP::Options::REQUEST_IP(), inet_aton($args{Request_ip}) ) if ( $args{Request_ip} );
	$self->addOption(Net::DHCP::Options::HOSTNAME(), $args{Hostname}) if ( $args{Hostname} );
	$self->addOption(Net::DHCP::Options::CLASS_ID(),'MSFT 5.0');
	return $self;
}
=pod

=item request

	DHCP request packet
	
=cut
sub request {
	my ($class, %args)	= @_;
	
	my $self =$class->new(%args);
	$self->addOption(Net::DHCP::Options::MESSAGE_TYPE(),Net::DHCP::Options::REQUEST());	
	$self->addOption(Net::DHCP::Options::CLIENT_ID(), str2mac( '01' . $self->{chaddr} ));
	$self->addOption(Net::DHCP::Options::HOSTNAME(), $args{Hostname} )  if ( $args{Hostname} );
	$self->addOption(Net::DHCP::Options::SERVER_IP(), inet_aton($args{Server_ip}) ) if ( $args{Server_ip} );
	$self->addOption(Net::DHCP::Options::CLASS_ID(),'MSFT 5.0');
	return $self;
}
=pod

=item decline

	DHCP decline packet

=cut
sub decline {
	my ($class, %args)	= @_;
	
	my $self =$class->new(%args);
	$self->addOption(Net::DHCP::Options::MESSAGE_TYPE(),Net::DHCP::Options::DECLINE());	
	$self->addOption(Net::DHCP::Options::CLASS_ID(),'MSFT 5.0');
	return $self;
}
=pod

=item release

	DHCP release packet

=cut

sub release {
	my ($class, %args)	= @_;
	
	my $self =$class->new(%args);
	$self->addOption(Net::DHCP::Options::MESSAGE_TYPE(),Net::DHCP::Options::RELEASE());	
	$self->addOption(Net::DHCP::Options::CLIENT_ID(), str2mac( '01' . $self->{chaddr})) ;
	$self->addOption(Net::DHCP::Options::SERVER_IP(), inet_aton($args{Server_ip}) ) if ( $args{Server_ip} );
	$self->addOption(Net::DHCP::Options::HOSTNAME(), $args{Hostname} ) if ( $args{Hostname} );
	$self->addOption(Net::DHCP::Options::CLASS_ID(),'MSFT 5.0');
	return $self;
}

=pod
=item inform

	DHCP inform packet

=cut

sub inform {
	my ($class, %args)	= @_;
	
	my $self =$class->new(%args);
	$self->addOption(Net::DHCP::Options::MESSAGE_TYPE(),Net::DHCP::Options::INFORM());	
	$self->addOption(Net::DHCP::Options::CLIENT_ID(), str2mac( '01' . $self->{chaddr})) ;
	$self->addOption(Net::DHCP::Options::SERVER_IP(), inet_aton($args{Server_ip}) ) if ( $args{Server_ip} );
	$self->addOption(Net::DHCP::Options::HOSTNAME(), $args{Hostname} ) if ( $args{Hostname} );
	$self->addOption(Net::DHCP::Options::CLASS_ID(),'MSFT 5.0');
	return $self;
}

=pod

=item serialize

	Converts a Net::DHCP::Packet to a string, ready to put on the network.

=cut
sub serialize {
	my ($self) = @_;
	my $bytes = undef;
	$bytes .= pack('C',$self->op());
	$bytes .= pack('C',$self->htype());
	$bytes .= pack('C',$self->hlen());
	$bytes .= pack('C',$self->hops());
	$bytes .= $self->xid();
	$bytes .= pack('S',$self->secs());
	$bytes .= pack('S',$self->flags());
	$bytes .= inet_aton($self->ciaddr());
	$bytes .= inet_aton($self->yiaddr());
	$bytes .= inet_aton($self->siaddr());
	$bytes .= inet_aton($self->giaddr());
	$bytes .= pack('C16', unpack('C16', str2mac($self->chaddr()) ) );
	$bytes .= pack('C64', unpack('C64',$self->sname()));
	$bytes .= pack('C128', unpack('C128',$self->file()));
	$bytes .= $self->{options}->serialize();
	return $bytes;
}
=pod

=item marshall(string)

	The inverse of serialize. Converts a string, presumably a 
	received UDP packet, into a Net::DHCP::Packet.

=cut
sub marshall {
	use bytes;
	my ($self,$bytes) = @_;
	my $pos = 0;
	$self->{op} = unpack('C',substr($bytes,$pos++,1));
	$self->{htype} = unpack('C',substr($bytes,$pos++,1));
	$self->{hlen} = unpack('C',substr($bytes,$pos++,1));
	$self->{hops} = unpack('C',substr($bytes,$pos++,1));
	$self->{xid} = substr($bytes,$pos,4); $pos+=4;
	$self->{secs} = substr($bytes,$pos,2); $pos+=2;
	$self->{flags} = substr($bytes,$pos,2); $pos+=2;
	$self->{ciaddr} = inet_ntoa(substr($bytes,$pos,4)); $pos+=4;
	$self->{yiaddr} = inet_ntoa(substr($bytes,$pos,4)); $pos+=4;
	$self->{siaddr} = inet_ntoa(substr($bytes,$pos,4)); $pos+=4;
	$self->{giaddr} = inet_ntoa(substr($bytes,$pos,4)); $pos+=4;
	$self->{chaddr} = mac2str($self->{hlen},substr($bytes,$pos,16)); $pos+=16;	
	$self->{sname} = substr($bytes,$pos,64); $pos+=64;	
	$self->{sname} = substr($bytes,$pos,128); $pos+=128;	
	$self->{options} = new Net::DHCP::Options()->marshall(substr($bytes,$pos));
	return $self;
}
=pod

=item toString()

	Returns a textual representation of the packet, for debugging.

=cut
sub toString {
	my ($self) = @_;
	my $s = "";
	while ( my ($key, $value) = each (%$self) ) {
		next if ($key eq 'options');
		$s .= sprintf("%s = %s\n",$key,$value);
	}
	$s .= sprintf("options =\n %s\n", $self->{options}->toString());
	return $s;
}

=pod

=head1 AUTHOR

F. van Dun

=head1 BUGS

I only ran some simple tests on Windows 2000 with a W2K DHCP server and 
a USR DHCP server.
Not yet tested on Unix platform.

=head1 COPYRIGHT

This is free software. It can be distributed and/or modified under the same terms as
Perl itself.

=head1 SEE ALSO

perl(1), Net::DHCP::Options.

=cut

1;