#!/bin/bash

#TODO
# - Sharing rule() (and some other helper functions) between the files
# - ip_in_net: Do a real check (converting to binary ip)

FIREHOL_LIB="/lib/firehol/firehol"

PATH="${PATH}:/bin:/usr/bin:/sbin:/usr/sbin"

# Get all helper functions:

if test -x "$FIREHOL_LIB" ; then
	. $FIREHOL_LIB
else
# We cannot proceed since we lack all the _CMD definitions
	echo "FATAL ERROR: $FIREHOL_LIB does not exist or is not executable!"
	exit 1
fi
# Make sure we clean up the temporary directory when the script exists
trap " [ -d \"$FIREHOL_DIR\" ] && /bin/rm -rf -- \"$FIREHOL_DIR\"" 0 1 2 3 13 15

##### Helper functions

# require commands for wizard mode
require_cmd ip
require_cmd netstat
require_cmd egrep
require_cmd date
require_cmd hostname	

#Ask the questions:
wizard_ask() {
	local prompt="${1}"; shift
	local def="${1}"; shift
		
	echo
		
	while [ 1 = 1 ]
	do
		printf >&2 "%s [%s] > " "${prompt}" "${def}"
		read
			
		local ans="${REPLY}"
			
		test -z "${ans}" && ans="${def}"
			
		local c=0
		while [ $c -le $# ]
		do
			eval local t="\${${c}}"
				
			test "${ans}" = "${t}" && break
			c=$[c + 1]
		done
			
		test $c -le $# && return $c
			
		printf >&2 "*** '${ans}' is not a valid answer. Pick one of "
		printf >&2 "%s " "$@"
		echo >&2 
		echo >&2 
	done
		
	return 0
}

#Check if an ip is a network:
ip_in_net() {
	local ip="${1}"; shift
	local net="${1}"; shift
		
	if [ -z "${ip}" -o -z "${net}" ]
	then
		return 1
	fi
		
	test "${net}" = "default" && net="0.0.0.0/0"
		
	set -- `echo ${ip} | ${TR_CMD} './' '  '`
	local i1=${1}
	local i2=${2}
	local i3=${3}
	local i4=${4}
		
	set -- `echo ${net} | ${TR_CMD} './' '  '`
	local n1=${1}
	local n2=${2}
	local n3=${3}
	local n4=${4}
	local n5=${5:-32}

	local i=$[i1*256*256*256 + i2*256*256 + i3*256 + i4]
	local n=$[n1*256*256*256 + n2*256*256 + n3*256 + n4]

#		echo "IP : '${i1}' . '${i2}' . '${i3}' . '${i4}'"
#		echo "NET: '${n1}' . '${n2}' . '${n3}' . '${n4}' / '${n5}'"
		
		local d=1
		local c=${n5}
		while [ $c -lt 32 ]
		do
			c=$[c + 1]
			d=$[d * 2]
		done
		
		local nm=$[n + d - 1]


	printf "### DEBUG: Is ${ip} part of network ${net}? "

	if [ ${i} -ge ${n} -a ${i} -le ${nm} ]
	then	
		echo "yes"
		return 0
	else
		echo "no"
		return 1
	fi
}


#Check if an IP is also a net:
ip_is_net() {
	local ip="${1}"; shift
	local net="${1}"; shift


	#We need a value for both $ip and $net:		
	if [ -z "${ip}" -o -z "${net}" ]
	then
		return 1
	fi

	test "${net}" = "default" && net="0.0.0.0/0"
	
	set -- `echo ${ip} | ${TR_CMD} './' '  '`
	local i1=${1}
	local i2=${2}
	local i3=${3}
	local i4=${4}
	local i5=${5:-32}
	
	set -- `echo ${net} | ${TR_CMD} './' '  '`
	local n1=${1}
	local n2=${2}
	local n3=${3}
	local n4=${4}
	local n5=${5:-32}
		
	local i=$[i1*256*256*256 + i2*256*256 + i3*256 + i4]
	local n=$[n1*256*256*256 + n2*256*256 + n3*256 + n4]
		
	if [ ${i} -eq ${n} -a ${i5} -eq ${n5} ]
	then
		return 0
	else
		return 1
	fi
}

# Try to translate IPs in a canonical format:
ip2net() {
	local ip="${1}"; shift

	#No value:
	if [ -z "${ip}" ]
	then
		return 0
	fi

	#IP is "default"		
	if [ "${ip}" = "default" ]
	then
		echo "default"
		return 0
	fi

	#Otherwise split the IP in chunks (and a hostmask, if possible)		
	set -- `echo ${ip} | ${TR_CMD} './' '  '`
	local i1=${1}
	local i2=${2}
	local i3=${3}
	local i4=${4}
	local i5=${5:-32}
		
	if [ "${i5}" = "32" ]
	then
		echo ${i1}.${i2}.${i3}.${i4}
	else
		echo ${i1}.${i2}.${i3}.${i4}/${i5}
	fi
}

# Get some ips and prepare them for later use: 
ips2net() {	
	(
		if [ "A${1}" = "A-" ]
		then
			while read ip
			do
				ip2net ${ip}
			done
		else
			while [ ! -z "${1}" ]
			do
				ip2net ${1}
				shift 
			done
		fi
	) | ${SORT_CMD} | ${UNIQ_CMD} | ${TR_CMD} "\n" " "
}

#Create rules based on the data of an interface:
helpme_iface() {
	local route="${1}"; shift
	local i="${1}"; shift
	local iface="${1}"; shift
	local ifip="${1}"; shift
	local ifnets="${1}"; shift
	local ifreason="${1}"; shift
		
	# one argument left: ifnets_excluded
		
	if [ "${route}" = "route" ]
	then
		found_interfaces[$i]="${iface}"
		found_ips[$i]="${ifip}"
		found_nets[$i]="${ifnets}"
		found_excludes[$i]="${1}"
	fi
		
	if [ "${ifnets}" = "default" ]
	then
		ifnets="not \"\${UNROUTABLE_IPS} ${1}\""
	else
		ifnets="\"${ifnets}\""
	fi
		
	# output the interface
	echo
	echo "# Interface No $i."
	echo "# The purpose of this interface is to control the traffic"
	if [ ! -z "${ifreason}" ]
	then
		echo "# ${ifreason}."
	else
		echo "# on the ${iface} interface with IP ${ifip} (net: ${ifnets})."
	fi
	
	echo "# TODO: Change \"interface${i}\" to something with meaning to you."
	echo "# TODO: Check the optional rule parameters (src/dst)."
	echo "# TODO: Remove 'dst ${ifip}' if this is dynamically assigned."
	echo "interface ${iface} interface${i} src ${ifnets} dst ${ifip}"
	echo
	echo "	# The default policy is DROP. You can be more polite with REJECT."
	echo "	# Prefer to be polite on your own clients to prevent timeouts."
	echo "	policy drop"
	echo
	echo "	# If you don't trust the clients behind ${iface} (net ${ifnets}),"
	echo "	# add something like this."
	echo "	# > protection strong"
	echo
	echo "	# Here are the services listening on ${iface}."
	echo "	# TODO: Normally, you will have to remove those not needed."
		
	(
		local x=
		local ports=
		for x in `${NETSTAT_CMD} -an | ${EGREP_CMD} "^tcp" | ${GREP_CMD} "0.0.0.0:*" | ${EGREP_CMD} " (${ifip}|0.0.0.0):[0-9]+" | ${CUT_CMD} -d ':' -f 2 | ${CUT_CMD} -d ' ' -f 1 | ${SORT_CMD} -n | ${UNIQ_CMD}`
		do
			if [ -f "tcp/${x}" ]
			then
				echo "	`${CAT_CMD} tcp/${x}` accept"
			else
				ports="${ports} tcp/${x}"
			fi
		done
			
		for x in `${NETSTAT_CMD} -an | ${EGREP_CMD} "^udp" | ${GREP_CMD} "0.0.0.0:*" | ${EGREP_CMD} " (${ifip}|0.0.0.0):[0-9]+" | ${CUT_CMD} -d ':' -f 2 | ${CUT_CMD} -d ' ' -f 1 | ${SORT_CMD} -n | ${UNIQ_CMD}`
		do
			if [ -f "udp/${x}" ]
			then
				echo "	`${CAT_CMD} udp/${x}` accept"
			else
				ports="${ports} udp/${x}"
			fi
		done
			
		echo "	server ICMP accept"
		
		echo "${ports}" | ${TR_CMD} " " "\n" | ${SORT_CMD} -n | ${UNIQ_CMD} | ${TR_CMD} "\n" " " >unknown.ports
	) | ${SORT_CMD} | ${UNIQ_CMD}
		
	echo
	echo "	# The following ${iface} server ports are not known by FireHOL:"
	echo "	# `${CAT_CMD} unknown.ports`"
	echo "	# TODO: If you need any of them, you should define new services."
	echo "	#       (see Adding Services at the web site - http://firehol.sf.net)."
	echo
		
	echo "	# The following means that this machine can REQUEST anything via ${iface}."
	echo "	# TODO: On production servers, avoid this and allow only the"
	echo "	#       client services you really need."
	echo "	client all accept"
	echo
}


#Prepare the directory:
cd "${FIREHOL_DIR}"
"${MKDIR_CMD}" ports
"${MKDIR_CMD}" keys
cd ports
"${MKDIR_CMD}" tcp
"${MKDIR_CMD}" udp

#Inform the user:

"${CAT_CMD}" >&2 <<EOF
$Id: firehol.sh,v 1.231 2004/11/01 00:13:00 ktsaou Exp $

(C) Copyright 2003, Costa Tsaousis <costa@tsaousis.gr>
FireHOL is distributed under GPL.
Home Page: http://firehol.sourceforge.net

--------------------------------------------------------------------------------
FireHOL controls your firewall. You should want to get updates quickly.
Subscribe (at the home page) to get notified of new releases.
--------------------------------------------------------------------------------

FireHOL will now try to figure out its configuration file on this system.
Please have all the services and network interfaces on this system running.

Your running firewall will not be stopped or altered.

You can re-run the same command with output redirection to get the config
to a file. Example:

EOF

echo >&2 "${FIREHOL_FILE} helpme >/tmp/firehol.conf"
echo >&2 
echo >&2 
		
echo >&2 
echo >&2 "Building list of known services."
echo >&2 "Please wait..."


#Get the services, kill whitespace:
${CAT_CMD} /etc/services | ${SED_CMD} -r 's/[[:blank:]]+/ /g' >services


for c in `echo ${!server_*} | ${TR_CMD} ' ' '\n' | ${GREP_CMD} "_ports$"`
do
	serv=`echo $c | ${SED_CMD} "s/server_//" | ${SED_CMD} "s/_ports//"`
		
	eval "ret=\${$c}"
	for x in ${ret}
	do
		proto=`echo $x | ${CUT_CMD} -d '/' -f 1`
		port=`echo $x | ${CUT_CMD} -d '/' -f 2`
			
		test ! -d "${proto}" && continue
			
		nport=`${EGREP_CMD} "^${port}[[:space:]][0-9]+/${proto}" services | ${CUT_CMD} -d ' ' -f 2 | ${CUT_CMD} -d '/' -f 1`
		test -z "${nport}" && nport="${port}"
			
		echo "server ${serv}" >"${proto}/${nport}"
	done
done

echo "server ftp" >tcp/21
echo "server nfs" >udp/2049
	
echo "client amanda" >udp/10080
	
echo "server dhcp" >udp/67
echo "server dhcp" >tcp/67
	
echo "client dhcp" >udp/68
echo "client dhcp" >tcp/68
	
echo "server emule" >tcp/4662
	
echo "server pptp" >tcp/1723
	
echo "server samba" >udp/137
echo "server samba" >udp/138
echo "server samba" >tcp/139


#Now let's start the game:
wizard_ask "Press RETURN to start." "continue" "continue"
echo >&2 
echo >&2 "--- snip --- snip --- snip --- snip ---"
echo >&2 

#Create the head of the config-file:
echo "#!${FIREHOL_FILE}"
echo "# ------------------------------------------------------------------------------"
echo "# This feature is under construction -- use it with care."
echo "#             *** NEVER USE THIS CONFIG AS-IS ***"
echo "# "

${CAT_CMD} <<EOF
# $Id: firehol.sh,v 1.231 2004/11/01 00:13:00 ktsaou Exp $
# (C) Copyright 2003, Costa Tsaousis <costa@tsaousis.gr>
# FireHOL is distributed under GPL.
# Home Page: http://firehol.sourceforge.net
# 
# ------------------------------------------------------------------------------
# FireHOL controls your firewall. You should want to get updates quickly.
# Subscribe (at the home page) to get notified of new releases.
# ------------------------------------------------------------------------------
#
EOF

echo "# This config will have the same effect as NO PROTECTION!"
echo "# Everything that found to be running, is allowed."
echo "# "
echo "# Date: `${DATE_CMD}` on host `${HOSTNAME_CMD}`"
echo "# "
echo "# The TODOs bellow, are YOUR to-dos!"
echo

# globals for routing
set -a found_interfaces=
set -a found_ips=
set -a found_nets=
set -a found_excludes=


#Get all interfaces and the default routes:
interfaces=`${IP_CMD} link show | ${EGREP_CMD} "^[0-9A-Za-z]+:" | ${CUT_CMD} -d ':' -f 2 | ${SED_CMD} "s/^ //" | ${GREP_CMD} -v "^lo$" | ${SORT_CMD} | ${UNIQ_CMD} | ${TR_CMD} "\n" " "`
gw_if=`${IP_CMD} route show | ${GREP_CMD} "^default" | ${SED_CMD} "s/dev /dev:/g" | ${TR_CMD} " " "\n" | ${GREP_CMD} "^dev:" | ${CUT_CMD} -d ':' -f 2`
gw_ip=`${IP_CMD} route show | ${GREP_CMD} "^default" | ${SED_CMD} "s/via /via:/g" | ${TR_CMD} " " "\n" | ${GREP_CMD} "^via:" | ${CUT_CMD} -d ':' -f 2 | ips2net -`
	
i=0
for iface in ${interfaces}
do
	echo "### DEBUG: Processing interface '${iface}'"
	ips=`${IP_CMD} addr show dev ${iface} | ${SED_CMD} "s/  / /g" | ${SED_CMD} "s/  / /g" | ${SED_CMD} "s/  / /g" | ${GREP_CMD} "^ inet " | ${CUT_CMD} -d ' ' -f 3 | ${CUT_CMD} -d '/' -f 1 | ips2net -`
	peer=`${IP_CMD} addr show dev ${iface} | ${SED_CMD} "s/  / /g" | ${SED_CMD} "s/  / /g" | ${SED_CMD} "s/  / /g" | ${SED_CMD} "s/peer /peer:/g" | ${TR_CMD} " " "\n" | ${GREP_CMD} "^peer:" | ${CUT_CMD} -d ':' -f 2 | ips2net -`
	nets=`${IP_CMD} route show dev ${iface} | ${CUT_CMD} -d ' ' -f 1 | ips2net -`
		
	if [ -z "${ips}" -o -z "${nets}" ]
	then
		echo
		echo "# Ignoring interface '${iface}' because does not have an IP or route."
		echo
		continue
	fi

	for ip in ${ips}
	do
		echo "### DEBUG: Processing IP ${ip} of interface '${iface}'"
		
		ifreason=""
		
		# find all the networks this IP can access directly
		# or through its peer
		netcount=0
		ifnets=
		ofnets=
		for net in ${nets}
		do
			test "${net}" = "default" && continue
			
			found=1
			ip_in_net ${ip} ${net}
			found=$?
			
			if [ ${found} -gt 0 -a ! -z "${peer}" ]
			then
				ip_in_net ${peer} ${net}
				found=$?
			fi
				
			if [ ${found} -eq 0 ]
			then
				# Add it to ifnets
				f=0; ff=0
				while [ $f -lt $netcount ]
				do
					if ip_in_net ${net} ${ifnets[$f]}
					then
						# Already satisfied
						ff=1
					elif ip_in_net ${ifnets[$f]} ${net}
					then
						# New one is superset of old
						ff=1
						ifnets[$f]=${net}
					fi
						
					f=$[f + 1]
				done
					
				if [ $ff -eq 0 ]
				then
					# Add it
					netcount=$[netcount + 1]
					ifnets=(${net} ${ifnets[@]})
				fi
			else
				ofnets=(${net} ${ofnets[@]})
			fi
		done
			
		# find all the networks this IP can access through gateways
		if [ ! -z "${ofnets[*]}" ]
		then
			for net in ${ofnets[@]}
			do
				test "${net}" = "default" && continue
				
				nn=`echo "${net}" | ${CUT_CMD} -d "/" -f 1`
				gw=`${IP_CMD} route show ${nn} dev ${iface} | ${EGREP_CMD} "^${nn}[[:space:]]+via[[:space:]][0-9\.]+" | ${CUT_CMD} -d ' ' -f 3 | ips2net -`
				test -z "${gw}" && continue
					
				for nn in ${ifnets[@]}
				do
					test "${nn}" = "default" && continue
					
					if ip_in_net ${gw} ${nn}
					then
						echo "### DEBUG: Route ${net} is accessed through ${gw}"
						
						# Add it to ifnets
						f=0; ff=0
						while [ $f -lt $netcount ]
						do
							if ip_in_net ${net} ${ifnets[$f]}
							then
								# Already satisfied
								ff=1
							elif ip_in_net ${ifnets[$f]} ${net}
							then
								# New one is superset of old
								ff=1
								ifnets[$f]=${net}
							fi
								
							f=$[f + 1]
						done
						
						if [ $ff -eq 0 ]
						then
							# Add it
							netcount=$[netcount + 1]
							ifnets=(${net} ${ifnets[@]})
						fi
						break
					fi
				done
			done
		fi
		
		# Don't produce an interface if this is just a peer that is also the default gw
		def_ignore_ifnets=0
		if (test ${netcount} -eq 1 -a "${gw_if}" = "${iface}" && ip_is_net "${peer}" "${ifnets[*]}" && ip_is_net "${gw_ip}" "${peer}")
		then
			echo "### DEBUG: Skipping ${iface} peer ${ifnets[*]} only interface (default gateway)."
			echo
			def_ignore_ifnets=1
		else
			i=$[i + 1]
			helpme_iface route $i "${iface}" "${ip}" "${ifnets[*]}" "${ifreason}"
		fi
		
		# Is this interface the default gateway too?
		if [ "${gw_if}" = "${iface}" ]
		then
			for nn in ${ifnets[@]}
			do
				if ip_in_net "${gw_ip}" ${nn}
				then
					echo "### DEBUG: Default gateway ${gw_ip} is part of network ${nn}"
					
					i=$[i + 1]
					helpme_iface route $i "${iface}" "${ip}" "default" "from/to unknown networks behind the default gateway ${gw_ip}" "`test ${def_ignore_ifnets} -eq 0 && echo "${ifnets[*]}"`"
					
					break
				fi
			done
		fi
	done
done
		
echo
echo "# The above $i interfaces were found active at this moment."
echo "# Add more interfaces that can potentially be activated in the future."
echo "# FireHOL will not complain if you setup a firewall on an interface that is"
echo "# not active when you activate the firewall."
echo "# If you don't setup an interface, FireHOL will drop all traffic from or to"
echo "# this interface, if and when it becomes available."
echo "# Also, if an interface name dynamically changes (i.e. ppp0 may become ppp1)"
echo "# you can use the plus (+) character to match all of them (i.e. ppp+)."
echo
	
if [ "1" = "`${CAT_CMD} /proc/sys/net/ipv4/ip_forward`" ]
then
	x=0
	i=0
	while [ $i -lt ${#found_interfaces[*]} ]
	do
		i=$[i + 1]
		
		inface="${found_interfaces[$i]}"
		src="${found_nets[$i]}"
			
		case "${src}" in
			"default")
				src="not \"\${UNROUTABLE_IPS} ${found_excludes[$i]}\""
				;;
				
			*)
				src="\"${src}\""
				;;
		esac
			
		j=0
		while [ $j -lt ${#found_interfaces[*]} ]
		do
			j=$[j + 1]
			
			test $j -eq $i && continue
			
			outface="${found_interfaces[$j]}"
			dst="${found_nets[$j]}"
			dst_ip="${found_ips[$j]}"
			
			case "${dst}" in
				"default")
					dst="not \"\${UNROUTABLE_IPS} ${found_excludes[$j]}\""
					;;
					
				*)
					dst="\"${dst}\""
					;;
			esac
				
			# Make sure we are not routing to the same subnet
			test "${inface}" = "${outface}" -a "${src}" = "${dst}" && continue
				
			# Make sure this is not a duplicate router
			key="`echo ${inface}/${src}-${outface}/${dst} | ${TR_CMD} "/ \\\$\\\"{}" "______"`"
			test -f "${FIREHOL_DIR}/keys/${key}" && continue
			${TOUCH_CMD} "${FIREHOL_DIR}/keys/${key}"
				
			x=$[x + 1]
				
			echo
			echo "# Router No ${x}."
			echo "# Clients on ${inface} (from ${src}) accessing servers on ${outface} (to ${dst})."
			echo "# TODO: Change \"router${x}\" to something with meaning to you."
			echo "# TODO: Check the optional rule parameters (src/dst)."
			echo "router router${x} inface ${inface} outface ${outface} src ${src} dst ${dst}"
			echo 
			echo "	# If you don't trust the clients on ${inface} (from ${src}), or"
			echo "	# if you want to protect the servers on ${outface} (to ${dst}),"
			echo "	# uncomment the following line."
			echo "	# > protection strong"
			echo
			echo "	# To NAT client requests on the output of ${outface}, add this."
			echo "	# > masquerade"
			
			echo "	# Alternatively, you can SNAT them by placing this at the top of this config:"
			echo "	# > snat to ${dst_ip} outface ${outface} src ${src} dst ${dst}"
			echo "	# SNAT commands can be enhanced using 'proto', 'sport', 'dport', etc in order to"
			echo "	# NAT only some specific traffic."
			echo
			echo "	# TODO: This will allow all traffic to pass."
			echo "	# If you remove it, no REQUEST will pass matching this traffic."
			echo "	route all accept"
			echo
		done
	done
		
	if [ ${x} -eq 0 ]
	then
		echo
		echo
		echo "# No router statements have been produced, because your server"
		echo "# does not seem to need any."
		echo
	fi
else
	echo
	echo
	echo "# No router statements have been produced, because your server"
	echo "# is not configured for forwarding traffic."
	echo
fi

exit 0
