#!/usr/bin/perl
## >>---------------------------------------------------------------------------------

use strict;

use Sys::Hostname;
use Socket;
use Net::DHCP::Packet;
use Net::DHCP::Constants;
use POSIX qw(setsid strftime);

use IO::Socket::INET;

# sample logger
sub logger{
    my $str = shift;
    print STDOUT strftime "[%d/%b/%Y:%H:%M:%S] ", localtime;
    print STDOUT "$str\n";
}

logger("Starting dhcpd");

my $DAEMON = 0;	# run as daemon ?

# accept only from selected VENDOR classes (avoids messing existing networks)
my $VENDOR_ACCEPTED = "foo|bar";

# broadcast address
my $bcastaddr = sockaddr_in("68",INADDR_BROADCAST);

# get a flag to force daemon to stop
my $time_to_die = 0;

# generic signal handler to cause daemon to stop
sub signal_handler {
    $time_to_die = 1;
}
$SIG{INT} = $SIG{TERM} = $SIG{HUP} = \&signal_handler;
# trap or ignore $SIG{PIPE}

# Daemon behaviour
# ignore any PIPE signal: standard behaviour is to quit process
$SIG{PIPE} = 'IGNORE';

# open listening socket
my $sock_in = IO::Socket::INET->new(
					LocalPort => 67,
					LocalAddr => "127.0.0.1",
					Proto    => 'udp') || die "Socket creation error: $@\n";

if ($DAEMON) {		# doesn't seem to work very well on cygwin
	logger("Entering Daemon mode");
	chdir '/'                 or die "Can't chdir to /: $!";
	umask 0;

	open STDIN, '/dev/null'   or die "Can't read /dev/null: $!";
	open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
	open STDERR, '>/dev/null' or die "Can't write to /dev/null: $!";

	my $pid = fork;
	exit if $pid;
	die "Couldn't fork: $!" unless defined($pid);
	
	POSIX::setsid() or die "Can't start a new session: $!";
	logger("Now in Daemon mode");
}

logger("Initialization complete");

# main loop
#
# process incoming packets
my $transaction = 0;			# report transaction number
		
	until ($time_to_die) {
		my $buf = undef;
		my $fromaddr;						# address & port from which packet was received
		my $dhcpreq;

		eval {					# catch fatal errors
			logger("Waiting for incoming packet");
			
			# receive packet
			$fromaddr = $sock_in->recv($buf,4096) || logger("recv:$!");
			next if ($!);		# continue loop if an error occured
			$transaction++;					# transaction counter
			
			{
				use bytes;
				my ($port,$addr) = unpack_sockaddr_in($fromaddr);
				my $ipaddr = inet_ntoa($addr);
	    	logger("Got a packet tr=$transaction src=$ipaddr:$port length=".length($buf));
	    }
	
			my $dhcpreq = new Net::DHCP::Packet($buf);
			$dhcpreq->comment($transaction);
		 	
		 	my $messagetype = $dhcpreq->getOptionValue(DHO_DHCP_MESSAGE_TYPE());
	
		 	if ($messagetype eq DHCPDISCOVER()) {
		 			do_discover($dhcpreq);
		 	} elsif ($messagetype eq DHCPREQUEST()) {
		 			do_request($dhcpreq);
		 	} elsif ($messagetype eq DHCPINFORM()) {
		 		
		 	} else {
		 		logger("Packet dropped");
		 		# bad messagetype, we drop it
		 	}
		};					# end of 'eval' blocks
		if ($@) {
			logger("Caught error in main loop:$@");
		}
	 	
	}
	logger("Exiting dhcpd");

#=======================================================================
sub do_discover($) {
	my ($dhcpreq) = @_;
	my $sock_out;
	my ($calc_ip, $calc_router, $calc_mask);
	
	# calculate address
	$calc_ip = "12.34.56.78";
	
	my $vendor = $dhcpreq->getOptionValue(DHO_VENDOR_CLASS_IDENTIFIER());
  if ($vendor !~ $VENDOR_ACCEPTED) {
    logger("DISCOVER rejected, unsupported VENDOR class");
    return;   # dropping packet
  }
	
	
	my $dhcpresp = new Net::DHCP::Packet(
											Comment => $dhcpreq->comment(),
											Op => BOOTREPLY(),
											Hops => $dhcpreq->hops(),
											Xid => $dhcpreq->xid(),
											Flags => $dhcpreq->flags(),
											Ciaddr => $dhcpreq->ciaddr(),
											Yiaddr => $calc_ip,
											Siaddr => $dhcpreq->siaddr(),
											Giaddr => $dhcpreq->giaddr(),
											Chaddr => $dhcpreq->chaddr(),
											DHO_DHCP_MESSAGE_TYPE() => DHCPOFFER(),
											);

	logger("Sending response");
	
	# Socket object keeps track of whom sent last packet
	# so we don't need to specify target address
	logger("Sending OFFER tr=".$dhcpresp->comment());
	$sock_in->send($dhcpresp->serialize()) || die "Error sending OFFER:$!\n";
	# TODO: you have to choose between sending back to sender or broadcasting to network

}

#=======================================================================
sub do_request($) {
	my ($dhcpreq) = @_;
	my $sock_out;
	my $calc_ip;
	my $dhcpresp;

	$calc_ip = "12.34.56.78";

	my $vendor = $dhcpreq->getOptionValue(DHO_VENDOR_CLASS_IDENTIFIER());
  if ($vendor !~ $VENDOR_ACCEPTED) {
    logger("REQUEST rejected, unsupported VENDOR class");
    return;   # dropping packet
  }
  
	# compare calculated address with requested address
	if ($calc_ip eq $dhcpreq->getOptionValue(DHO_DHCP_REQUESTED_ADDRESS())) {
		# address is correct, we send an ACK
	
		$dhcpresp = new Net::DHCP::Packet(
												Comment => $dhcpreq->comment(),
												Op => BOOTREPLY(),
												Hops => $dhcpreq->hops(),
												Xid => $dhcpreq->xid(),
												Flags => $dhcpreq->flags(),
												Ciaddr => $dhcpreq->ciaddr(),
												Yiaddr => $calc_ip,
												Siaddr => $dhcpreq->siaddr(),
												Giaddr => $dhcpreq->giaddr(),
												Chaddr => $dhcpreq->chaddr(),
												DHO_DHCP_MESSAGE_TYPE() => DHCPACK(),
												);
	} else {
		# bad request, we send a NAK
		$dhcpresp = new Net::DHCP::Packet(
												Comment => $dhcpreq->comment(),
												Op => BOOTREPLY(),
												Hops => $dhcpreq->hops(),
												Xid => $dhcpreq->xid(),
												Flags => $dhcpreq->flags(),
												Ciaddr => $dhcpreq->ciaddr(),
												Yiaddr => "0.0.0.0",
												Siaddr => $dhcpreq->siaddr(),
												Giaddr => $dhcpreq->giaddr(),
												Chaddr => $dhcpreq->chaddr(),
												DHO_DHCP_MESSAGE_TYPE() => DHCPNAK(),
												DHO_DHCP_MESSAGE(), "Bad request...",
												);
	}

	# Socket object keeps track of whom sent last packet
	# so we don't need to specify target address
	logger("Sending ACK/NAK tr=".$dhcpresp->comment());
	$sock_in->send($dhcpresp->serialize()) || die "Error sending ACK/NAK:$!\n";
  # TODO: you have to choose between sending back to sender or broadcasting to network
	
}
