#!/usr/bin/perl

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

use strict;
use warnings;

use autodie;

use IO::Socket::INET;
use File::Slurp;
use Data::Dump qw/dump/;
use Net::Ping;

use lib 'lib';
use Net::DHCP::Packet;
use Net::DHCP::Constants 0.67;
die "need to run $0 as root like this\nsudo $0\n" unless $< == 0;

my $debug = shift @ARGV;

our ( $file, $gpxe_file );
our ( $ip_from, $ip_to ) = ( 10, 100 );

our $server_ip = readlink 'conf/server.ip' if -l 'conf/server.ip';

if ( ! $server_ip ) {
	$server_ip = `/sbin/ifconfig`;
	$server_ip =~ s/^.+?addr:([\d\.]+).*$/$1/gs;
	warn "auto-configure server ip to $server_ip\n";
} else {
	warn "server ip $server_ip\n";
}

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 $addr = $ip_from;

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

	my $conf = "conf/$server_ip";
	mkdir $conf unless -e $conf;

	if ( -e "$conf/mac/$mac" ) {
		my $ip = read_file "$conf/mac/$mac";
		print "$mac old $ip\n";
		return $ip;
	}

	mkdir $_ foreach grep { ! -e $_ } map { "$conf/$_" } ( 'ip', 'mac' );

	my $p = Net::Ping->new;

	my $prefix = $server_ip;
	$prefix =~ s{\.\d+$}{.};
	my $ip = $prefix . $addr;
	while ( -e "conf/ip/$ip" || $p->ping( $ip ) ) {
		$ip = $prefix . $addr++;
		die "all addresses allocated!" if $addr == $ip_to;
	}

	write_file "$conf/mac/$mac", $ip;
	if ( -l "$conf/ip/$ip" && readlink "$conf/ip/$ip" ne "$conf/mac/$mac") {
		unlink     "$conf/ip/$ip";
		symlink    "$conf/mac/$mac", "$conf/ip/$ip";
		warn "$mac IP changed to $ip";
	}

	print "$mac NEW $ip\n";

	return $ip;
}

my $transaction = 0; # FIXME predictible transaction numbers

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 "<< ",$sock->peerhost,":",$sock->peerport,"\n";

	if (defined $buf) {

		my $dhcp = Net::DHCP::Packet->new($buf);
		$dhcp->comment( $transaction++ );

		warn "recv: ", $dhcp->toString, "\n\n";

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

		if ( $user_class eq 'gPXE' ) {
			$file = $gpxe_file;
		} elsif ( ! $file ) {
			$file = 'undionly.kpxe';
		}

		my $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.255.255.0',
		};

		my $messagetype = $dhcp->getOptionValue(DHO_DHCP_MESSAGE_TYPE());

		if ($messagetype eq DHCPDISCOVER()) {
			warn "DHCP DISCOVER";
			$packet->{Comment} = $dhcp->comment();
			$packet->{DHO_DHCP_MESSAGE_TYPE()} = DHCPOFFER();
		} elsif ($messagetype eq DHCPREQUEST()) {
			my $requested_ip = $dhcp->getOptionValue(DHO_DHCP_REQUESTED_ADDRESS());
			warn "DHCP REQUEST $requested_ip";
			if ( $ip eq $requested_ip ) {
				$packet->{DHO_DHCP_MESSAGE_TYPE()} = DHCPACK();
				$packet->{DHO_DHCP_LEASE_TIME()}   = 100;
			} else {
				$packet->{DHO_DHCP_MESSAGE_TYPE()} = DHCPNAK();
				$packet->{DHO_DHCP_MESSAGE()} = "Bad request, expected $ip";
			}
		} elsif ($messagetype eq DHCPINFORM()) {
			warn "DHCP INFORM ignored";
		} else {
			warn "$messagetype igored (bootp?)";
		}


		warn ">> $mac == $ip server: $server_ip", $file ? " file: $file\n" : "\n";

		$packet = new Net::DHCP::Packet( %$packet );
		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";
	}

}
