#!/bin/bash

#originally spamhaus drop script.
#modified by Hanz Makmur 2015-03-13
#using LCSR DROP file add to iptables
# locking added by daw; 9/14/16
# logger added by daw; 10/2/17
# efficiency improvement: instead of flushing and re-adding entire list,
# only delete expired entries and add new ones -- daw, 10/23/17
# switch to using curl, iptables-{save,restore}; 4/25/18
# syslog "version";, refuse to run if not on public internet; 10/11/18
# check status after iptables commands; syslog potential reason on file retrieval failure; 
# defend against iptables-restore errors; 10/12/18
# begin ipset mod (indicate presence or absence of ipset in SUMN); 10/19/18
# continue ipset mod (maintain LCSRDrop ipset); 10/23/18
# finish ipset mod (if ipset present, remove LCSRDrop chain, don't maintain LCSRDrop chain,
#	and add LOG_AND_DROP chain using LCSRDrop ipset); 11/7/18
# ipset might be in /sbin rather than /usr/sbin; 11/12/18
# insert LOG_AND_DROP chain at beginning rather then end; 11/12/18
# use wget if curl is not available; 12/7/18
# save temp copy of iptables -L for debugging if removing chain in favor of ipset; 12/9/18
# save a copy of ipset restore file if ipset restore fails; 12/11/18
# use timeout for curl/wget if available; 1/17/19
# avoid buggy timeout on Fedora Core 3 (dhcp2.srv.lcsr) ; 1/22/19
# workaround for buggy timeout on Fedora Core 3 (dhcp2.srv.lcsr) ; 1/23/19
# don't assume LOG_AND_DROP chain exists if LCSRDrop ipset does ; 4/21/19
# install ipset if needed and available ; 4/25/19
# add "-exist" to ipset del commands ; 4/25/19
# make sure 0 return status from yum info means ipset is available ; 4/26/19
# "yum info" needs "timeout -s 9" on Fedora Core 3 (dhcp2.srv.lcsr) ; 4/26/19
# avoid flurry of errors due to simultaneous "iptables restart" ; 4/27/19
# drop previous edit -- reschedule or eliminate "iptables restart" ; 5/7/19
# fix apt/apt-get code; 8/12/19
# add a clue when refusing to run; 8/13/19
# remove s from https because old SSL library ; 6/23/21
# iptables might be in /usr/sbin; 9/9/21

# path to iptables
export PATH=$PATH:/sbin	# sbin needed fot apt on klinzhai.rutgers.edu
IPTABLES="/sbin/iptables";
if [ -e /usr/sbin/iptables ]; then IPTABLES=/usr/sbin/iptables ; fi
IPSAVE="/sbin/iptables-save";
IPRESTORE="/sbin/iptables-restore";
if [ -e /usr/sbin/ipset ]; then IPSET=/usr/sbin/ipset ; fi
if [ -e /sbin/ipset ]; then IPSET=/sbin/ipset ; fi
CURL="`which curl`";
WGET="`which wget`";
if [ -e /usr/bin/timeout ]; then TIMEOUT=/usr/bin/timeout ; fi
if [ -e /bin/timeout ]; then TIMEOUT=/bin/timeout ; fi
if [ $TIMEOUT"x" != "x" ]; then 
    TIMEOUT9="$TIMEOUT -s 9 15"
    TIMEOUT="$TIMEOUT 15"
fi

# if timeout is broken here, don't use it
# (switch within command confuses timeout on FC3)
# I found a workaround.  Put command into a file
#$TIMEOUT echo x -x > /dev/null  2>&1
#if [ $? -ne 0 ]; then TIMEOUT="" ; fi
LOGGER="`which logger`";
BASENAME="`which basename`";
BASE="`$BASENAME $0`";
TMP="/tmp/$BASE.$$";
NOIPSETF=/tmp/$BASE.noipset
DQ='"'

SUM=`which sum`
# use SUMSEP to indicate whether using ipset
SUMSEP='-'
if [ $IPSET"x" != "x" ]; then SUMSEP="+"; fi
SUMN=`$SUM $0 | sed "s;  *;$SUMSEP;"`
BASEDATE=`grep '; *[0-9]*/[0-9]*/[0-9]* *$' $0 | tail -1 | awk '{print $NF}'`
VFILE=/tmp/$BASE.version

# list of known IPs
URL="http://report.rutgers.edu/DROP/attackers";

# save local copy here
FILE="/tmp/attackers.drop"

# iptables custom chain
CHAIN="LCSRDrop";

# ipset custom set
SETNAME="LCSRDrop";

# lockfile
LOCKF=/tmp/lcsrdrop.lock

# create temp lockfile
( echo $$ > $LOCKF.$$ ) > /dev/null 2>&1

# put that in real lockfile place of that doesn't exist already
if [ ! -f $LOCKF ]; then 
    /bin/mv $LOCKF.$$ $LOCKF > /dev/null 2>&1
fi

# see who now has the lock
if [ -f $LOCKF ]; then
    if [ -r $LOCKF ]; then
	LPID=`cat $LOCKF`
    else
	LPID=1			# if we cannot read file, use init's pid
    fi
else
    LPID=1			# if we cannot create file, use init's pid
fi

# if it's not us, try removing lockfile if that process no longer exists
# and clean up after self
if [ $$ != "$LPID" ]; then
    /bin/ps -p $LPID > /dev/null
    if [ $? -ne 0 ]; then
	/bin/rm -f $LOCKF > /dev/null 2>&1
    fi
    /bin/rm -f $LOCKF.$$
    exit 1
fi

# Check if version changed.  If so, log it
if [ ! -e $VFILE ]; then touch $VFILE ; fi
echo "$SUMN ($BASEDATE)" > $VFILE.new
diff $VFILE $VFILE.new > /dev/null
if [ $? -ne 0 ]; then
    LOGVERSION=1
else
    LOGVERSION=0
fi

# If the date has changed, log the version as well
DN=`/bin/ls -l $VFILE $VFILE.new | awk '{print $6,$7}' | sort -u | wc -l`
if [ $DN -ne 1 ]; then LOGVERSION=1 ; fi
/bin/mv -f $VFILE.new $VFILE

if [ $LOGVERSION -eq 1 ]; then
    $LOGGER -t$BASE -pdaemon.err "SUMN=$SUMN ($BASEDATE)"
    echo `date` "SUMN=$SUMN ($BASEDATE)"
fi

/sbin/ip addr show | egrep "128\.6\.|165\.230\." > /dev/null
if [ $? -ne 0 ] && [ ! -e $0.run-in-private-IP-space ]; then
#    $LOGGER -t$BASE -pdaemon.err "refusing to run in private IP space"
    IP=`ifconfig -a | grep 'inet 172\.' | sed -e 's;.* inet ;;' -e 's; .*;;'`
    $LOGGER -t$BASE -pdaemon.err "refusing to run in private IP space ($IP)"
    echo $BASE need not be run on Rutgers private IP space
    exit 0
fi

# make sure ipset is installed if possible
if [ $IPSET"x" == "x" -a ! -e $NOIPSETF ]; then
    if [ -e /usr/bin/apt ]; then APT=/usr/bin/apt ; fi
    if [ -e /bin/apt ]; then APT=/bin/apt ; fi
#    if [ -e /usr/bin/apt-get ]; then APTGET=/usr/bin/apt-get ; fi
#    if [ -e /bin/apt-get ]; then APTGET=/bin/apt ; fi
    if [ -e /usr/bin/yum ]; then YUM=/usr/bin/yum ; fi
    if [ -e /bin/yum ]; then YUM=/bin/yum ; fi
    if [ $APT"x" == "x" -a $YUM"x" == "x" ]; then
	$LOGGER -t$BASE -pdaemon.err "Neither apt nor yum found"
    fi
#    if [ $APT"x" != "x" -a $APTGET"x" != "x" ]; then
    if [ $APT"x" != "x" ]; then
	$APT show ipset > /dev/null 2>&1
	if [ $? -eq 0 ]; then
#	    echo `date +%T` "Installing ipset using apt-get"
#	    $LOGGER -t$BASE -pdaemon.err "Installing ipset using apt-get"
#	    $APTGET install -y ipset
	    echo `date +%T` "Installing ipset using apt"
	    $LOGGER -t$BASE -pdaemon.err "Installing ipset using apt"
# Avoid sometimes exiting install with status 100
	    $APT update
	    STATUS=$?
	    if [ $STATUS -ne 0 ]; then
		$LOGGER -t$BASE -pdaemon.err "$APT update returned status $STATUS"
	    fi
	    $APT install -y ipset
	    STATUS=$?
	    if [ $STATUS -ne 0 ]; then
#		$LOGGER -t$BASE -pdaemon.err "$APTGET install -y ipset returned status $STATUS"
		$LOGGER -t$BASE -pdaemon.err "$APT install -y ipset returned status $STATUS"
	    else
		if [ -e /usr/sbin/ipset ]; then IPSET=/usr/sbin/ipset ; fi
		if [ -e /sbin/ipset ]; then IPSET=/sbin/ipset ; fi
	    fi
	else
	    touch $NOIPSETF
	fi
    fi
    if [ $YUM"x" != "x" ]; then
	$TIMEOUT9 $YUM info ipset > $TMP 2>&1
	if [ $? -eq 0 ]; then
	    grep ipset $TMP /dev/null 2>&1
	    if [ $? -eq 0 ]; then
		echo `date +%T` "Installing ipset using yum"
		$LOGGER -t$BASE -pdaemon.err "Installing ipset using yum"
		$YUM install -y ipset
		STATUS=$?
		if [ $STATUS -ne 0 ]; then
		    $LOGGER -t$BASE -pdaemon.err "$YUM install -y ipset returned status $STATUS"
		else
		    if [ -e /usr/sbin/ipset ]; then IPSET=/usr/sbin/ipset ; fi
		    if [ -e /sbin/ipset ]; then IPSET=/sbin/ipset ; fi
		fi
	    else
		touch $NOIPSETF
	    fi
	else
	    touch $NOIPSETF
	fi
    fi
fi

# check to see if the chain already exists
$IPTABLES -L $CHAIN -n > /dev/null 2>&1
ILCSTATUS=$?
if [ $ILCSTATUS -ne 0 ]; then
# only create chain if ipset not present
    if [ $IPSET"x" == "x" ]; then
	echo `date +%T` "Chain $CHAIN not detected. Creating new chain...."

	# create a new chain set
	$IPTABLES -N $CHAIN
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -N $CHAIN returned status $STATUS"
	fi

	# tie chain to input rules so it runs
	$IPTABLES -A INPUT -j $CHAIN
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -A INPUT -j $CHAIN returned status $STATUS"
	fi

	# don't allow this traffic through
	$IPTABLES -A FORWARD -j $CHAIN
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -A FORWARD -j $CHAIN returned status $STATUS"
	fi
    fi
else
# chain exists -- remove it if ipset present
    if [ $IPSET"x" != "x" ]; then 
# Save an old copy of this for debugging, just in case...
	echo `date +%T` "Saving old copy of $IPSET -L -n"
	$LOGGER -t$BASE -pdaemon.err "Saving old copy of $IPSET -L -n"
	$IPTABLES -L -n > /tmp/$BASE.iptables-L-n 2>&1
	echo `date +%T` "$IPSET exists.  Removing chain $CHAIN...."
	$IPTABLES -D FORWARD -j $CHAIN
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -D FORWARD -j $CHAIN returned status $STATUS"
	fi

	$IPTABLES -D INPUT -j $CHAIN
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -D INPUT -j $CHAIN returned status $STATUS"
	fi

	$IPTABLES -F $CHAIN
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -F $CHAIN returned status $STATUS"
	fi

	$IPTABLES -X $CHAIN
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -X $CHAIN returned status $STATUS"
	fi
    fi
fi;

# Create LCSRDrop ipset if it does not exist

if [ $IPSET"x" != "x" ]; then 
    $IPSET list $SETNAME > /dev/null 2>&1
    if [ $? -ne 0 ]; then
	echo `date +%T` "set $SETNAME not detected. Creating new set...."

	# create new ipset set
	$IPSET create $SETNAME iphash
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPSET create $SETNAME iphash returned status $STATUS"
	fi
    fi

# Make sure LOG_AND_DROP chain exists

#    $IPTABLES -L LOG_AND_DROP -n > /dev/null 2>&1
# On report.cs at 0700, this sometimes does not detect LOG_AND_DROP because of simultaneous "iptables restart"
# No simple way to avoid this other than rescheduling (or removing)  "iptables restart"
    $IPTABLES -L LOG_AND_DROP -n > $TMP 2>&1
    if [ $? -ne 0 ]; then
	echo "  error from $IPTABLES -L LOG_AND_DROP -n:"
	sed 's;.;    &;' $TMP

	echo `date +%T` "Chain LOG_AND_DROP not detected. Creating new chain...."

	# create LOG_AND_DROP chain
	$IPTABLES -N LOG_AND_DROP
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -N LOG_AND_DROP returned status $STATUS"
	fi

	$IPTABLES -A LOG_AND_DROP -m limit --limit 10/min -j LOG --log-prefix "LCSRDropped: "
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -A LOG_AND_DROP -m limit --limit 10/min -j LOG --log-prefix ${DQ}LCSRDropped: $DQ returned status $STATUS"
	fi

	$IPTABLES -A LOG_AND_DROP -j REJECT
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -A LOG_AND_DROP -j REJECT returned status $STATUS"
	fi

#	$IPTABLES -A INPUT -m set --match-set $SETNAME src -j LOG_AND_DROP
# Put this at the beginning rather than the end
	$IPTABLES -I INPUT -m set --match-set $SETNAME src -j LOG_AND_DROP
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPTABLES -I INPUT -m set --match-set $SETNAME src -j LOG_AND_DROP returned status $STATUS"
	fi
    fi
fi

# make sure $FILE does not exist
if [ -e $FILE ]; then
    /bin/mv -f $FILE $FILE.old
fi;

# get a copy of the drop list
echo `date +%T` "Retrieving copy of attackers data...."
if [ $CURL"x" != "x" ]; then
#    $TIMEOUT $CURL -k -s $URL -o $FILE
# Put command into file so buggy timeout won't see it under FC3
    echo $CURL -k -s $URL -o $FILE > $TMP
    $TIMEOUT /bin/sh $TMP
    CSTATUS=$?
else
# if curl doesn't exist, use wget
    $TIMEOUT $WGET -q --no-check-certificate -O $FILE $URL
    CSTATUS=$?
fi

# make sure it has nothing but IPs
grep -v '^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$' $FILE > /dev/null
CJSTATUS=$?

# check to see if curl succeeded and did not get junk
if [ $CSTATUS -eq 0 -a $CJSTATUS -eq 1 ]; then

    /bin/rm -f $FILE.old

else
    # use old data
    /bin/mv -f $FILE $FILE.fail
    /bin/mv -f $FILE.old $FILE

    STRING=` grep '<title>' $FILE.fail | sed -e 's;.*<title>;;' -e 's:<.*:; :'`
    
    echo `date +%T` "Retrieval of attackers data failed. Reusing old data...."
    $LOGGER -t$BASE -pdaemon.err "Retrieval of attackers data failed ($STRING$CSTATUS,$CJSTATUS). Reusing old data...."
fi;

# Maintain LCSRDrop ipset if ipset program exists

if [ $IPSET"x" != "x" ]; then 
    $IPSET list $SETNAME > $TMP.ipset 2>&1
    STATUS=$?
    if [ $STATUS -ne 0 ]; then
	echo `date +%T` "$IPSET list $SETNAME returned status $STATUS"
	$LOGGER -t$BASE -pdaemon.err "$IPSET list $SETNAME returned status $STATUS"
    fi
    grep '^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$' $TMP.ipset | sort -u > $TMP.old
    sort -u $FILE > $TMP.new
    comm -23 $TMP.{old,new} > $TMP.remove
    comm -13 $TMP.{old,new} > $TMP.add
    sed "s;.;del -exist $SETNAME &;" $TMP.remove > $TMP.restore
    sed "s;.;add -exist $SETNAME &;" $TMP.add   >> $TMP.restore
    if [ -s $TMP.restore ]; then
	echo `date +%T` "restoring new $SETNAME ipset...."
	$IPSET restore < $TMP.restore
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    $LOGGER -t$BASE -pdaemon.err "$IPSET restore < $TMP.restore returned status $STATUS"
	    echo `date +%T` "Saving copy of failed restore file"
	    $LOGGER -t$BASE -pdaemon.err "Saving copy of failed restore file"
	    /bin/mv $TMP.restore /tmp/$BASE.ipset.restore.failed
	fi
    else
	echo `date +%T` "No changes $SETNAME ipset...."
    fi
fi

# Old code maintains LCSRDrop chain if ipset not present

if [ $IPSET"x" == "x" ]; then 
    # save current iptables rules with/without LCSRDRop
    $IPSAVE > $TMP.old
    SSTATUS=$?
    if [ $SSTATUS -ne 0 ]; then
	$LOGGER -t$BASE -pdaemon.err "$IPSAVE > $TMP.old returned status $SSTATUS"
    fi
    $IPSAVE | grep -v '^-A LCSRDrop' > $TMP
    STATUS=$?
    if [ $STATUS -ne 0 ]; then
	$LOGGER -t$BASE -pdaemon.err "$IPSAVE | grep -v '^-A LCSRDrop' > $TMP returned status $STATUS"
    fi

# build new save file to load

    CLINE=`grep -n "COMMIT" $TMP | tail -1 | sed 's;:.*;;'`
    (( CLINEm1=$CLINE-1 ))
    
    head -$CLINEm1 $TMP					 > $TMP.new
    sed 's;.*;-A LCSRDrop -s &/32 -j DROP;'	$FILE		>> $TMP.new
    tail --lines=+$CLINE $TMP				>> $TMP.new

    diff $TMP.{old,new} > /dev/null

    if [ $? -ne 0 ]; then

	echo `date +%T` "restoring new iptables rules...."
	# "restore" new iptables rules
	$IPRESTORE < $TMP.new
	STATUS=$?
	if [ $STATUS -ne 0 ]; then
	    echo `date +%T` "restore failed"
	    $LOGGER -t$BASE -pdaemon.err "$IPRESTORE < $TMP.new returned status $STATUS"
	    if [ $SSTATUS -eq 0 ]; then
		echo "	restoring to saved state"
		$LOGGER -t$BASE -pdaemon.err "Restoring to saved state"
		$IPRESTORE < $TMP.old
		STATUS=$?
		if [ $STATUS -ne 0 ]; then
		    echo `date +%T` "that restore failed too"
		    $LOGGER -t$BASE -pdaemon.err "$IPRESTORE < $TMP.old returned status $STATUS"
		fi
	    fi
	fi

    else

	echo `date +%T` "No changes in iptables rules...."

    fi;
fi

# remove lockfile (and possible leftover temp lockfile) and temp files
/bin/rm -f $LOCKF $LOCKF.$$ $TMP*
