#!/bin/bash
###############################################################################
#                                                                             #
# IPFire.org - A linux based firewall                                         #
# Copyright (C) 2007-2022  IPFire Team  <info@ipfire.org>                     #
#                                                                             #
# This program is free software: you can redistribute it and/or modify        #
# it under the terms of the GNU General Public License as published by        #
# the Free Software Foundation, either version 3 of the License, or           #
# (at your option) any later version.                                         #
#                                                                             #
# This program is distributed in the hope that it will be useful,             #
# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
# GNU General Public License for more details.                                #
#                                                                             #
# You should have received a copy of the GNU General Public License           #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
#                                                                             #
###############################################################################

shopt -s nullglob

VPN_CONFIG="/var/ipfire/vpn/config"

ROUTE_TABLE="220"
ROUTE_TABLE_PRIO="128"

eval $(/usr/local/bin/readhash /var/ipfire/ethernet/settings)
eval $(/usr/local/bin/readhash /var/ipfire/vpn/settings)

# Get RED interface name
if [ -r "/var/ipfire/red/iface" ]; then
	RED_INTF="$(</var/ipfire/red/iface)"
else
	RED_INTF="red0"
fi

VARS=(
	id status name lefthost type ctype psk local local_id leftsubnets
	remote_id remote rightsubnets x3 x4 x5 x6 x7 x8 x9 x10 x11 x12
	x13 x14 x15 x16 x17 x18 x19 proto x20 x21 x22
	route x23 mode interface_mode interface_address interface_mtu rest
)

log() {
	logger -t ipsec "$@"
}

resolve_hostname() {
	local hostname="${1}"

	dig +short A "${hostname}" | tail -n1
}

ip_encode() {
	local address="${1}"

	local int=0
	for field in ${address//./ }; do
		int=$(( $(( ${int} << 8 )) | ${field} ))
	done

	echo ${int}
}

function ip_in_subnet() {
	local address="${1}"
	local subnet="${2}"

	local netmask="${subnet#*/}"

	# Convert netmask to prefix if necessary
	case "${netmask}" in
		[0-9]+)
			;;
		*)
			netmask="$(netmask2prefix "${netmask}")"
			;;
	esac

	local vlsm=$(( -1 << $(( 32 - ${netmask} )) ))

	[ "$(( $(ip_encode "${address}") & ${vlsm} ))" -eq "$(( $(ip_encode "${subnet%/*}") & ${vlsm} ))" ]
}

netmask2prefix() {
	local netmask="${1}"
	local mask="$(ip_encode "${netmask}")"

	local cidr=0
	local x="$(( 128 << 24 ))" # 0x80000000

	while [ $(( ${x} & ${mask} )) -ne 0 ]; do
		[ ${mask} -eq ${x} ] && mask=0 || mask=$(( ${mask} << 1 ))
		cidr=$(( ${cidr} + 1 ))
	done

	echo "${cidr}"
}

main() {
	# Register local variables
	local "${VARS[@]}"
	local action

	local interfaces=()

	# Flush IPsec routes
	ip route flush table "${ROUTE_TABLE}" >/dev/null 2>&1

	# Remove lookups
	ip rule del lookup "${ROUTE_TABLE}" >/dev/null 2>&1

	# We are done when IPsec is not enabled
	if [ "${ENABLED}" = "on" ]; then
		# Enable route table lookup
		ip rule add lookup "${ROUTE_TABLE}" prio "${ROUTE_TABLE_PRIO}"

		while IFS="," read -r "${VARS[@]}"; do
			# Check if the connection is enabled
			[ "${status}" = "on" ] || continue

			# Check if this a net-to-net connection
			[ "${type}" = "net" ] || continue

			# Determine the interface name
			case "${interface_mode}" in
				gre|vti)
					local intf="${interface_mode}${id}"
					;;
				*)
					# Install routes
					local address

					local _address
					for _address in ${GREEN_ADDRESS} ${BLUE_ADDRESS} ${ORANGE_ADDRESS}; do
						local leftsubnet
						for leftsubnet in ${leftsubnets//\|/ }; do
							if ip_in_subnet "${_address}" "${leftsubnet}"; then
								address="${_address}"
								break
							fi
						done

						# End loop when address is set
						[ -n "${address}" ] && break
					done

					local rightsubnet
					for rightsubnet in ${rightsubnets//\|/ }; do
						# Ignore default
						case "${rightsubnet}" in
							0.0.0.0/*)
								continue
								;;
						esac

						log "Creating route to ${rightsubnet} (via ${address} and ${RED_INTF})"
						ip route add table "${ROUTE_TABLE}" "${rightsubnet}" proto static \
							dev "${RED_INTF}" src "${address}"
					done

					# No interface processing required
					continue
					;;
			esac

			# Add the interface to the list of all interfaces
			interfaces+=( "${intf}" )

			# Compat for older connections
			if [ "${local}" = "off" ]; then
				if [ "${VPN_IP}" = "%defaultroute" ]; then
					local=""
				else
					local="${VPN_IP}"
				fi
			fi

			# Handle %defaultroute
			if [ -z "${local}" ]; then
				if [ -r "/var/ipfire/red/local-ipaddress" ]; then
					local="$(</var/ipfire/red/local-ipaddress)"

				elif [ "${RED_TYPE}" = "STATIC" -a -n "${RED_ADDRESS}" ]; then
					local="${RED_ADDRESS}"
				fi
			fi

			# Resolve any hostnames
			if [[ ! ${remote} =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
				remote="$(resolve_hostname "${remote}")"
			fi

			local args=(
				"local" "${local}"
				"remote" "${remote}"
			)

			case "${interface_mode}" in
				gre)
					# Add TTL
					args+=( "ttl" "255" )
					;;

				vti)
					# Add key for VTI
					args+=( "key" "${id}" )
					;;
			esac

			# Update the settings when the interface already exists
			if [ -d "/sys/class/net/${intf}" ]; then
				ip link change dev "${intf}" \
					type "${interface_mode}" "${args[@]}" &>/dev/null

			# Create a new interface and bring it up
			else
				log "Creating interface ${intf}"
				if ! ip link add name "${intf}" type "${interface_mode}" "${args[@]}"; then
					log "Could not create interface ${intf}"
					continue
				fi
			fi

			# Add an IP address
			ip addr flush dev "${intf}"
			ip addr add "${interface_address}" dev "${intf}"

			# Disable IPsec policy lookup for VTI
			if [ "${interface_mode}" = "vti" ]; then
				sysctl -qw "net.ipv4.conf.${intf}.disable_policy=1"
			fi

			# Set MTU
			ip link set dev "${intf}" mtu "${interface_mtu}"

			# Bring up the interface
			ip link set dev "${intf}" up
		done < "${VPN_CONFIG}"
	fi

	# Delete all other interfaces
	local intf
	for intf in /sys/class/net/gre[0-9]* /sys/class/net/vti[0-9]*; do
		intf="$(basename "${intf}")"

		# Ignore a couple of interfaces that cannot be deleted
		case "${intf}" in
			gre0|gretap0)
				continue
				;;
		esac

		# Check if interface is on the list
		local i found="false"
		for i in ${interfaces[@]}; do
			if [ "${intf}" = "${i}" ]; then
				found="true"
				break
			fi
		done

		# Nothing to do if interface was found
		${found} && continue

		# Delete the interface
		log "Deleting interface ${intf}"
		ip link del "${intf}" &>/dev/null
	done

	# (Re-)Apply all static routes
	/etc/init.d/static-routes start
}

main || exit $?
