#! /bin/sh
#
# dhcpgen, (c) 2006, xtom (tichy@klfree.net)
# 
# version: 1.2 (RC4)
# development: dmitrij <fryc@klfree.net>
# home: https://twiki.klfree.net/twiki/bin/view/SpravaSite.DhcpGen
#
##########################################################################

CONFIGFILE=/etc/default/dhcpgen
PIDFILE=/var/run/dhcpgen
TMP=/tmp/
SLEEP=5
IS_SLEEP=120

NETS=""

PERM_DIR=/etc/dnsmasq
DHCP_DIR=/var/etc/dnsmasq

PERM_RANGES=${PERM_DIR}/ranges.conf
PERM_HOSTS=${PERM_DIR}/ethers.conf
PERM_ETHERS=${PERM_DIR}/ethers

DHCP_RANGES=${DHCP_DIR}/ranges.conf
DHCP_HOSTS=${DHCP_DIR}/ethers.conf
DHCP_ETHERS=${DHCP_DIR}/ethers
DHCP_LEASES=${DHCP_DIR}/leases
DHCP_IPNEIGH=${DHCP_DIR}/ipneigh
DHCP_INFO=${DHCP_DIR}/info

MAP_ETHERS=${DHCP_DIR}/ethers.map
MAP_ETHERS_UPD=${DHCP_DIR}/ethers.map.upd
MAP_LEASES=${DHCP_DIR}/leases.map
MAP_LEASES_UPD=${DHCP_DIR}/leases.map.upd

DNSMASQ_LEASES=/var/lib/misc/dnsmasq.leases

CKSUM=${DHCP_DIR}/cksum

DO_CKSUM="cksum ${DHCP_RANGES} ${DHCP_HOSTS} ${DHCP_ETHERS} ${DHCP_LEASES}";

URLBASE="https://is.klfree.net/export.php"

URL_MACS="$URLBASE?export=bans&net="

if wget --help | grep -q -i no-check-certificate; then
	WGET="wget --no-check-certificate --quiet"
else
	WGET="wget --quiet"
fi

DEBUG=1
DEBUG_FILE=${TMP}/dhcpgen-debug

##########################################################################

#maps of registered MAC-IPs, comma separated
MAPS_MACIP="";

#maps of leased IPs, comma separated
MAPS_LEASES="";

#maps of registered IPs without MAC alias
MAPS_NOMAC="";

#devices where force iwpriv mac-filter, comma separated
IWPRIV_DEVS="";

##########################################################################

print_help() {
cat <<HELP
dhcpgen start|stop|restart|status|load|update|reload

start   - starts a daemon process, periodically updates dhcp table
loop    - starts interactively, periodically updates dhcp table
stop    - stops the daemon
restart - restarts a daemon process
status  - shows state
save    - saves current dhcp table as initial (for next boot)
update  - updates current dhcp table
reload  - gets inital dhcp table
version - prints version of dhcpgen-related files
HELP
}

save() {
    echo "Saving ranges ${DHCP_RANGES} into ${PERM_RANGES}..." &&
    cat ${DHCP_RANGES} > ${PERM_RANGES} &&
    echo "Saving hosts ${DHCP_HOSTS} into ${PERM_HOSTS}..." &&
    cat ${DHCP_HOSTS} > ${PERM_HOSTS} &&
    echo "Saving ethers ${DHCP_ETHERS} into ${PERM_ETHERS}..." &&
    cat ${DHCP_ETHERS} > ${PERM_ETHERS} &&
    echo "done." || { echo "failed."; return 1; }
}

upd_ranges() {
    echo "Updating ranges into ${DHCP_RANGES}..." &&
    echo $NETS | tr ',' '\n' | sed -r 's#^(.*)$#inet \1#'  | tr './' ' ' | awk '
    BEGIN { 
      while (getline < "'"${DHCP_ETHERS}"'")
      {
         iplist[$1] = 1;
      }
      printf "#Generated by dhcpgen\n"; 
    }
    function ip2str(ip)
    {
      return sprintf("%d.%d.%d.%d", ip/256^3%256,ip/256^2%256,ip/256%256,ip%256);
    }
    { 
    	ip=(($2*256+$3)*256+$4)*256+$5; b=$6;
        if (ip > 0) {
	mask=0; brd=0; net=0;
	for (i=0; i<32; i++)
	{
	   if (i >= 32-b)
	   {
	      mask = mask + 2^i;
	      brd = brd + (ip % 2^(i+1) - ip % 2^i);
	      net = net + (ip % 2^(i+1) - ip % 2^i);
	   } else
	   {
	      brd = brd + 2^i;
	   }
	}
	printf "dhcp-range=%s,static,%s,%s,1h\n", ip2str(net+1), ip2str(mask), ip2str(brd);
	firstj=0;
	for (j=net+2; j <= brd; j++)
	{
	   if (j == brd || iplist[ip2str(j)] == 1)
	   {
	      if (firstj > 0)
	      {
	         printf "dhcp-range=%s,%s,%s,%s,5m\n", ip2str(firstj), ip2str(j-1), ip2str(mask), ip2str(brd); 
		 firstj = 0;
	      }
	   } else
	   {
	      if (firstj == 0) firstj = j;
	   }
	}
	}
    }' > ${DHCP_RANGES} && 
    echo "done." || { echo "failed."; return 1; }
}

upd_ethers() {
    echo "Updating ethers into ${DHCP_ETHERS}..." &&
    ${WGET} "${URL_MACS}${NETS}" -O-  \
        | tr '<' '\n' \
        | grep '^ip .*mac=' \
        | sed "
        s/^.*ipv4='\([^']*\)'.*status='\([^']*\)'.*active='\([^']*\)'.*mac='\([^']*\)'.*$/\1;\4;\4;\2;\3/
       " | awk '
       BEGIN { FS=";"; }
       {
          mc = tolower($2);
          ma = tolower($3);
          st = $4;
          ac = $5;
          if (! match(mc, "^[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]$")) mc = "*";
          if (! match(ma, "^[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]$")) ma = "*";
          printf "%s %s %s %s %s %s\n", tolower($1), mc, ma, "'"$DEV"'", st, ac;
       }' | sort > ${DHCP_ETHERS} &&
    echo "done." || { echo "failed."; return 1; }
}

upd_hosts() {
    echo "Updating hosts into ${DHCP_HOSTS}..." &&
    cat ${DHCP_ETHERS} | awk '
    BEGIN { printf "#Generated by dhcpgen\n"; }
    {
    	if (match($2, "^[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]$") && match($1, "^[0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]*$"))
	{
    		printf "dhcp-host=%s,%s\n", $2, $1;
	}
    }
    ' > ${DHCP_HOSTS} &&
    echo "done." || { echo "failed."; return 1; }
}

upd_leases() {
    echo "Updating DHCP leases..." && 
    cat ${DNSMASQ_LEASES} | awk '
    { printf "%s %s\n", $3, $2; }
    ' > ${DHCP_LEASES} &&
    echo "done." || { echo "failed."; return 1; }
}

upd_ipneigh() {
    echo "Updating ip neigh..." && 
    ip neigh | grep lladdr | awk '{ printf "%s %s\n", $1, $5; }
    ' > ${DHCP_IPNEIGH} &&
    echo "done." || { echo "failed."; return 1; }
}

upd_info() {
    echo "Updating info..." &&
    TF=${DHCP_INFO}.tmp &&
    touch ${DHCP_INFO} && 
    awk '
    BEGIN {
        printf "#IP             mac               mac_alias         since  eths lease neigh\n";

	nr=0;
    	while (getline < "'"${DHCP_INFO}"'")
	{
	    nr++;
	    if (nr > 1 && NF > 1) { 
	       s = sprintf("%-15s %17s %17s", $1, $2, $3);
	       list[s] = 1;
	       since[s] = $4 + 1;
	       eths[s] = $5;
	       leases[s] = $6;
	       neighs[s] = $7;
	       if (list[ip2s[$1]] != 1 || since[i2s[$1]] > since[s])
	          ip2s[$1] = s;
	    }
	}
    	while (getline < "'"${DHCP_ETHERS}"'")
	{
	    if (NF > 1) { 
	       s = sprintf("%-15s %17s %17s", $1, $2, $3);
	       list[s] = 1; eths[s]++;
	    }
	}
    	while (getline < "'"${DHCP_LEASES}"'")
	{
	    if (NF > 1) { 
	    	ips[$1]++; macs[$1] = $2;
	    }
	}
    	while (getline < "'"${DHCP_IPNEIGH}"'")
	{
	    if (NF > 1) { 
	    	ips[$1]--; maca[$1] = $2;
	    }
	}
	for (ip in ips)
	{
	    if (ips[ip] == 1)
	    {
	       if (list[ip2s[ip]] == 1)
	       {
	          s = ip2s[ip];
	       } else
	       {
	          s = sprintf("%-15s %17s %17s", ip, macs[ip], "*");
		  list[s] = 1;
	       }
	       since[s] = 0;
	       leases[s]++;
	    } else
	    if (ips[ip] == -1)
	    {
	       if (list[ip2s[ip]] == 1)
	       {
	          s = ip2s[ip];
	       } else
	       {
	          s = sprintf("%-15s %17s %17s", ip, "*", maca[ip]);
		  list[s] = 1;
	       }
	       since[s] = 0;
	       neighs[s]++;
	    } else
	    {
	       s = sprintf("%-15s %17s %17s", ip, macs[ip], maca[ip]);
	       list[s] = 1;
	       since[s] = 0;
	       neighs[s]++;
	       leases[s]++;
	    }
	}
        for (s in list)
	{
	    if (s != "")
	    {
	       printf "%s %5d %5d %5d %5d\n", s, since[s], eths[s], leases[s], neighs[s];
	    }
	}
    }
    ' < /dev/null | sort > ${TF} &&
    mv ${TF} ${DHCP_INFO} &&
    echo "done." || { echo "failed."; return 1; }
}

upd_map_files() {
    echo "Updating map files..." &&
    TF=/tmp/tmp-$$ &&
    cat ${DHCP_ETHERS} | awk '{ if (NF>1 && $6=="Y") { printf "%s %s %s\n", $1, $3, $4; } }
    ' > ${TF} &&
    awk '
      BEGIN {
         while (getline < "'"${MAP_ETHERS}"'")
	 {
	    list[$0] = -1;
	 }
         while (getline < "'"${TF}"'")
	 {
	    list[$0]++;
	 }
	 for (s in list)
	 {
	    if (list[s] == -1)
	    {
	       printf "DEL %s\n", s;
	    }
	 }
	 for (s in list)
	 {
	    if (list[s] == 1) 
	    {
	       printf "ADD %s\n", s;
	    }
	 }
      }
    ' < /dev/null > $MAP_ETHERS_UPD && cat $TF > $MAP_ETHERS &&
    rm -f $TF &&
    echo "done." || echo "failed.";
    cat ${DHCP_LEASES} | awk '{ print $1; }' > ${TF} &&
    if uptime | grep -q 'min,'; then
        cat ${DHCP_ETHERS} | awk '{ if ($6 == "Y") print $1; }' >> ${TF};
    fi &&
    awk '
      BEGIN {
         while (getline < "'"${MAP_LEASES}"'")
	 {
	    list[$0] = -1;
	 }
         while (getline < "'"${TF}"'")
	 {
	    list[$0]++;
	 }
	 for (s in list)
	 {
	    if (list[s] == -1)
	    {
	       printf "DEL %s\n", s;
	    }
	 }
	 for (s in list)
	 {
	    if (list[s] == 1) 
	    {
	       printf "ADD %s\n", s;
	    }
	 }
      }
    ' < /dev/null > $MAP_LEASES_UPD && cat $TF > $MAP_LEASES &&
    rm -f $TF &&
    echo "done." || echo "failed.";
}

flush_maps() {
    echo "Flushing maps...";
    echo -ne "${MAPS_MACIP},${MAPS_LEASES},${MAPS_NOMAC}" | tr ',' '\n' | sed 's/ *//g
    ' | grep -v '^$' | while read MAP; do
	echo "- Flushing ipset ${MAP}";
	ipset -F ${MAP};
    done;
    cat /dev/null > ${MAP_ETHERS};
    cat /dev/null > ${MAP_LEASES};
    echo "done.";
}

upd_maps_regmacip() {
    echo "Updating maps of registered MAC-IPs..." &&
    echo -ne "${MAPS_MACIP}" | tr ',' '\n' | sed 's/ *//g
    ' | grep -v '^$' | while read MAP; do
	echo "- Updating ipset ${MAP}";
	cat ${MAP_ETHERS_UPD} | while read OP IP MAC DEV X; do
	  if [ "$MAC" != "*" ]; then
	     if [ "$OP" == "ADD" ]; then
		 ipset -A ${MAP} "${IP}%${MAC}" >/dev/null 2>/dev/null;
	     else
		 ipset -D ${MAP} "${IP}" >/dev/null 2>/dev/null;
	     fi;
	  fi;
	done;
    done;
    echo "done.";
}

upd_maps_leased() {
    echo "Updating maps of leased MAC-IPs..." &&
    echo -ne "${MAPS_LEASES}" | tr ',' '\n' | sed 's/ *//g
    ' | grep -v '^$' | while read MAP; do
	echo "- Updating ipset ${MAP}";
	
	cat ${MAP_LEASES_UPD} | while read OP IP X; do
	     if [ "$OP" == "ADD" ]; then
		 ipset -A ${MAP} "${IP}" >/dev/null 2>/dev/null;
	     else
		 ipset -D ${MAP} "${IP}" >/dev/null 2>/dev/null;
	     fi;
	done;
    done;
    echo "done.";
}

upd_maps_nomac() {
    echo "Updating maps of IPs without MACs..." &&
    echo -ne "${MAPS_NOMAC}" | tr ',' '\n' | sed 's/ *//g
    ' | grep -v '^$' | while read MAP; do
	echo "- Updating ipset ${MAP}";
	cat ${MAP_ETHERS_UPD} | sort -r | while read OP IP MAC X; do
	 if [ "${MAC}" == "*" ]; then 
	   if [ "$OP" == "ADD" ]; then
		 ipset -A ${MAP} "${IP}" >/dev/null 2>/dev/null;
	   else
		 ipset -D ${MAP} "${IP}" >/dev/null 2>/dev/null;
	   fi;
	 fi;
	done;
    done;
    echo "done.";
}

flush_iwpriv() {
  echo "Flushing iwpriv MAC settings..." &&
  echo -ne "${IWPRIV_DEVS}" | tr ',' '\n' | sed 's/ *//g
    ' | grep -v '^$' | while read DEV; do
       echo "Flushing iwpriv for ${DEV}...";
       iwpriv ${DEV} maccmd 3;    #clear list
       iwpriv ${DEV} maccmd 2;    #make blacklist
  done;
  echo "done.";
}

upd_iwpriv() {
  echo "Updating iwpriv MAC settings..." &&
  echo -ne "${IWPRIV_DEVS}" | tr ',' '\n' | sed 's/ *//g
    ' | grep -v '^$' | while read DEV; do
    echo "Updating iwpriv for ${DEV}..." &&
    cat ${MAP_ETHERS_UPD} | grep -- "${DEV}" | while read OP IP MAC XDEV X; do
       if [ "${DEV}" == "${XDEV}" ]; then
         if [ "$OP" == "ADD" ]; then
    	   iwpriv ${DEV} addmac ${MAC};
	 else
    	   iwpriv ${DEV} delmac ${MAC};
         fi;
       fi;
    done;
  done;
  echo "done.";
}

update() {
    echo "Update current dhcp table (USEIS=$1)..." &&
    [ $1 -eq 1 ] && upd_ethers;
    upd_hosts;
    upd_ranges;
    upd_leases;
    upd_ipneigh;
    upd_info;
    ACK=`cat ${CKSUM} 2>/dev/null`;
    BCK=`${DO_CKSUM}`;
    if [ "$ACK" != "$BCK" ]; then
       upd_map_files;
       upd_maps_regmacip;
       upd_maps_leased;
       upd_maps_nomac;
       upd_iwpriv;
       /etc/init.d/dnsmasq force-reload;
       echo -ne "$BCK" > ${CKSUM};
    fi;
    echo "done.";
}

reload() {
    echo "Reloading initial dhcp table..." &&
    [ -f ${PERM_RANGES} ] && cat ${PERM_RANGES} > ${DHCP_RANGES} &&
    [ -f ${PERM_ETHERS} ] && cat ${PERM_ETHERS} > ${DHCP_ETHERS} &&
    [ -f ${PERM_HOSTS} ] && cat ${PERM_HOSTS} > ${DHCP_HOSTS} &&
    echo "done." || { echo "failed."; return 1; }
}

control_loop() {
    echo "Starting dhcpgen control loop...";
    echo $$ > ${PIDFILE};
    setup-iptables restart;
    reload;
    flush_maps;
    flush_iwpriv;
    echo > $CKSUM;
    IS_STAMP=0;
    while true; do
        TDIF=$[`date +"%s"` - $IS_STAMP];
        if [ $TDIF -ge $IS_SLEEP ]; then
            update 1;
            IS_STAMP=`date +"%s"`;
        else
            update 0;
        fi;
        sleep ${SLEEP};
    done;
}

debug() {
    [ $DEBUG == 1 ] && echo "$1" >>${DEBUG_FILE}
}

[ -r $CONFIGFILE ] || {
    echo "Config file is missing. Aborting."
    exit 1
}

. $CONFIGFILE


case "$1" in
loop)
    control_loop
    ;;

load)
    load
    ;;

update)
    update 1
    ;;

reload)
    reload
    ;;

save)
    save
    ;;

start)
    echo "Starting dhcpgen control..."
    start-stop-daemon --background --start --quiet --pidfile ${PIDFILE} --exec /etc/init.d/dhcpgen loop
    echo "."
    ;;
    
stop)
    echo "Stopping dhcpgen control loop..."
    start-stop-daemon --signal TERM --stop --quiet --pidfile ${PIDFILE} && rm ${PIDFILE}
    echo "."
    ;;

restart)
    /etc/init.d/dhcpgen stop;
    /etc/init.d/dhcpgen start;
    ;;
    
status)
    if [ -r ${PIDFILE} ] ; then
	echo "Running..."
    else
	echo "Stopped."
    fi
    ;;

version)
    echo -n "Script version: "
    grep "# version:" /etc/init.d/dhcpgen | head -n 1 | sed -r "s/# version: //"
    echo -n "Configuration file version: "
    grep "# version:" /etc/default/dhcpgen | head -n 1 | sed -r "s/# version: //"
    ;;

*)
    print_help
    ;;
esac

