#!/usr/bin/perl

# based on http://www.perlmonks.org/index.pl?node_id=325248

use strict;
use warnings;

use IO::Socket::INET;
use Net::DHCP::Packet;
use Net::DHCP::Constants;
use Data::Dump qw/dump/;

die "need to run $0 as root like this\nsudo $0\n" unless $< == 0;

my $debug = shift @ARGV;

our ( $server_ip, $file, $next_file );

my $sock = IO::Socket::INET->new(
	LocalPort	=> 67,
#	LocalAddr	=> 'localhost',
#	LocalAddr	=> '10.0.0.100',
	LocalAddr	=> '0.0.0.0',
	Proto		=> 'udp',
	ReuseAddr	=> 1,
#	PeerPort	=> getservbyname('bootpc', 'udp'),
	Broadcast	=> 1,
	Type		=> SOCK_DGRAM,
) or die "Failed to bind to socket: $@";

my $_ip = 10;
my $_mac2ip;

sub client_ip {
	my ( $mac ) = @_;

	my $ip = $_mac2ip->{$mac};
	return $ip if $ip;

	$ip = $server_ip;
	$ip =~ s{\.\d+$}{.$_ip};
	$_mac2ip->{$mac} = $ip;

	$_ip++;
	if ( $_ip == 100 ) {
		warn "IP roll-over to 10\n";
		$_ip = 10;
	}

	return $ip;
}

while (1) {

	require "config.pl"; # refresh config

	print "waiting for DHCP requests on ",$sock->sockhost,":",$sock->sockport,"\n";

	my $buf;
	$sock->recv($buf, 1024);
	print "<< peer:",$sock->peerhost,":",$sock->peerport,"\n";

	if (defined $buf) {

		my $dhcp;

		eval { $dhcp = Net::DHCP::Packet->new($buf); };
		die "can't use request", dump( $buf ) if $@;

		if ( $debug ) {
			warn "recv: ", $dhcp->toString, "\n\n";
		}

		my $mac = substr($dhcp->chaddr(),0,$dhcp->hlen()*2);
		my $ip = client_ip($mac);

		if ( ! $file ) {
			if ( $dhcp->getOptionValue(DHO_USER_CLASS()) ne 'gPXE' ) {
				$file = 'undionly.kpxe';
			} else {
				$file = $next_file;
			}
		}

		my $packet = new Net::DHCP::Packet(
			Op		=> BOOTREPLY(),
			Hops	=> $dhcp->hops(),
			Xid		=> $dhcp->xid(),
			Flags	=> $dhcp->flags(),
			Ciaddr	=> $dhcp->ciaddr(),
			Yiaddr	=> $ip,
			Siaddr	=> $server_ip,
			Giaddr	=> $dhcp->giaddr(),
			Chaddr	=> $dhcp->chaddr(),
			File	=> $file,
#			DHO_DHCP_MESSAGE_TYPE() => DHCPACK(),
        	DHO_SUBNET_MASK() => '255.0.0.0',
		);

		warn ">> $mac == $ip server $server_ip\n";
		
		warn "## ",$packet->toString(),"\n" if $debug;

		my $reply = IO::Socket::INET->new(
			LocalAddr => $server_ip,
			LocalPort => 67,
			Proto => "udp",
			Broadcast => 1,
			PeerAddr => '255.255.255.255',
			PeerPort => 68,
			Reuse => 1,
		) or die "socket: $@";

		my $buff = $packet->serialize();
		$reply->send( $buff, 0 ) or die "Error sending: $!\n";

#		system("arp -s $ip $mac"),

	} else {
		print "No bootp request.\n";
	}

}
