#
# Copyright 2018 devolo AG
#
SCRIPT=/usr/sbin/wifi_guest.sh

_rm_cron_script(){
    crontab -l | grep -v "$1" | sort -u | crontab -
}

_rm_and_add_cron_script(){
    (crontab -l | grep -v "$1"; [ -n "$2" ] && echo "$2") | sort -u | crontab -
}


set_delos_guestwifi() {
	#local EBLOG=--log

	# determine new guest interfaces (from tmpfile written by a wifi hook)
	[ -f /tmp/guestwifi-ifs ] && . /tmp/guestwifi-ifs || GUEST_IFS=
	# determine new GW addresses (from tmpfile written by a dhcp hook)
	[ -f /tmp/guestwifi-ipv4 ] && . /tmp/guestwifi-ipv4 || GW4_IP= GW4_MAC=
	[ -f /tmp/guestwifi-ipv6 ] && . /tmp/guestwifi-ipv6 || GW6_IP= GW6_MAC=

	# read saved interfaces and GW addresses of the installed rules
	[ -f /var/run/guest-wifi-restricted ] && . /var/run/guest-wifi-restricted ||
		CUR_GUEST_IFS= CUR_GW4_IP= CUR_GW4_MAC= CUR_GW6_IP= CUR_GW6_MAC=

	# check if new parameters are same as last used
	[ "$GUEST_IFS/$GW4_IP/$GW4_MAC/$GW6_IP/$GW6_MAC" = "$CUR_GUEST_IFS/$CUR_GW4_IP/$CUR_GW4_MAC/$CUR_GW6_IP/$CUR_GW6_MAC" ] &&
		return
	# reinstall rules only if changed

	# throw away current user-defined chains
	for iface in $CUR_GUEST_IFS; do
		ebtables -D INPUT -i $iface -j i-guestwifi $EBLOG 2>/dev/null
		ebtables -D OUTPUT -o $iface -j o-guestwifi $EBLOG 2>/dev/null
		ebtables -D FORWARD -i $iface -j fi-guestwifi $EBLOG 2>/dev/null
		ebtables -D FORWARD -o $iface -j fo-guestwifi $EBLOG 2>/dev/null
	done
	ebtables -X i-guestwifi 2>/dev/null
	ebtables -X o-guestwifi 2>/dev/null
	ebtables -X fi-guestwifi 2>/dev/null
	ebtables -X fo-guestwifi 2>/dev/null

	# but don't install new rules when there are no restricted interfaces
	[ -z "$GUEST_IFS" ] && {
		rm -f /var/run/guest-wifi-restricted
		return
	}

	# use user-defined chains to group rules for easier deletion
	ebtables -N i-guestwifi -P RETURN
	ebtables -N o-guestwifi -P RETURN
	ebtables -N fi-guestwifi -P RETURN
	ebtables -N fo-guestwifi -P RETURN
	for iface in $GUEST_IFS; do
		ebtables -A INPUT -i $iface -j i-guestwifi $EBLOG 2>/dev/null
		ebtables -A OUTPUT -o $iface -j o-guestwifi $EBLOG 2>/dev/null
		ebtables -A FORWARD -i $iface -j fi-guestwifi $EBLOG 2>/dev/null
		ebtables -A FORWARD -o $iface -j fo-guestwifi $EBLOG 2>/dev/null
	done

	# save current rule parameters for later
	cat >/var/run/guest-wifi-restricted <<-EOF
	CUR_GUEST_IFS="$GUEST_IFS"
	CUR_GW4_IP="$GW4_IP"
	CUR_GW4_MAC="$GW4_MAC"
	CUR_GW6_IP="$GW6_IP"
	CUR_GW6_MAC="$GW6_MAC"
	EOF

	# actual rules
	# We return but don't accept so that more chains after ours are considered

	#allow EAPOL frames destined to the adapter itself (needed for wifi authentication)
	ebtables -A i-guestwifi -p 0x888e -j RETURN $EBLOG

	#allow ARP frames destinated to the adapter itself
	ebtables -A i-guestwifi -p ARP -j RETURN $EBLOG

	#deny any other frame destined to the adapter itself
	# TODO use DROP policy instead of using this rule as the last one everywhere!?
	ebtables -A i-guestwifi -j DROP $EBLOG


	#allow EAPOL frames originating from the adapter itself (needed for wifi authentication)
	ebtables -A o-guestwifi -p 0x888e -j RETURN $EBLOG

	#allow ARP frames originationg from the adapter itself
	ebtables -A o-guestwifi -p ARP -j RETURN $EBLOG

	#deny any frame originating from the adapter itself being destined to a guest account station
	ebtables -A o-guestwifi -j DROP $EBLOG


	# some IP-based restrictions to strengthen security
	# FIXME what if no MAC? leave it out and use IP only?
	[ -n "$GW4_IP" ] && {
		#allow DHCP, DNS and ICMP unicast packets destined to the current default gateway
		ebtables -A fi-guestwifi -d $GW4_MAC -p IPv4 --ip-dst $GW4_IP --ip-proto tcp --ip-dport 53 -j RETURN $EBLOG
		ebtables -A fi-guestwifi -d $GW4_MAC -p IPv4 --ip-dst $GW4_IP --ip-proto udp --ip-dport 53 -j RETURN $EBLOG
		ebtables -A fi-guestwifi -d $GW4_MAC -p IPv4 --ip-dst $GW4_IP --ip-proto udp --ip-dport 67 -j RETURN $EBLOG
		ebtables -A fi-guestwifi -d $GW4_MAC -p IPv4 --ip-dst $GW4_IP --ip-proto icmp -j RETURN $EBLOG

		#deny any other IP unicast packets destined to the current default gateway
		ebtables -A fi-guestwifi -d $GW4_MAC -p IPv4 --ip-dst $GW4_IP -j DROP $EBLOG
	}

	#allow DHCP broadcast packets originating from a guest account station
	ebtables -A fi-guestwifi -p IPv4 --ip-dst 255.255.255.255 --ip-proto udp --ip-dport 67 -j RETURN $EBLOG

	#deny any other IP broadcast packets originating from a guest account station
	ebtables -A fi-guestwifi -p IPv4 --ip-dst 255.255.255.255 -j DROP $EBLOG

	[ -n "$GW4_IP" ] && {
		#allow DHCP, DNS and ICMP packets originating from the current default gateway
		ebtables -A fo-guestwifi -s $GW4_MAC -p IPv4 --ip-src $GW4_IP --ip-proto tcp --ip-sport 53 -j RETURN $EBLOG
		ebtables -A fo-guestwifi -s $GW4_MAC -p IPv4 --ip-src $GW4_IP --ip-proto udp --ip-sport 53 -j RETURN $EBLOG
		ebtables -A fo-guestwifi -s $GW4_MAC -p IPv4 --ip-src $GW4_IP --ip-proto udp --ip-sport 67 -j RETURN $EBLOG
		ebtables -A fo-guestwifi -s $GW4_MAC -p IPv4 --ip-src $GW4_IP --ip-proto icmp -j RETURN $EBLOG

		#deny any other IP packets originating from the current default gateway
		ebtables -A fo-guestwifi -s $GW4_MAC -p IPv4 --ip-src $GW4_IP -j DROP $EBLOG
	}

	#if GW6_IP isn't known then deny all IPv6 frames originating from or destined to a guest account station
	[ -z "$GW6_IP" ] && {
		ebtables -A fo-guestwifi -p IPv6 -j DROP $EBLOG
		ebtables -A fi-guestwifi -p IPv6 -j DROP $EBLOG
	}

	# some IPv6-based restrictions to strengthen security
	[ -n "$GW6_IP" ] && {
		#allow DNS packets destined to the current default gateway
		ebtables -A fi-guestwifi -d $GW6_MAC -p IPv6 --ip6-dst $GW6_IP --ip6-proto tcp --ip6-dport 53 -j RETURN $EBLOG
		ebtables -A fi-guestwifi -d $GW6_MAC -p IPv6 --ip6-dst $GW6_IP --ip6-proto udp --ip6-dport 53 -j RETURN $EBLOG

		#allow DHCPv6 packets (using multicast)
		ebtables -A fi-guestwifi -p IPv6 --ip6-proto udp --ip6-sport 546 --ip6-dport 547 -j RETURN $EBLOG

		#allow ipv6-icmp type router-solicitation, router-advertisement, neighbour-solicitation, neighbour-advertisement
		ebtables -A fi-guestwifi -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type router-solicitation -j RETURN $EBLOG
		ebtables -A fi-guestwifi -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type router-advertisement -j RETURN $EBLOG
		ebtables -A fi-guestwifi -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type neighbour-solicitation -j RETURN $EBLOG
		ebtables -A fi-guestwifi -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type neighbour-advertisement -j RETURN $EBLOG

		#deny any other IPv6 packets destined to the current default gateway
		ebtables -A fi-guestwifi -d $GW6_MAC -p IPv6 --ip6-dst $GW6_IP -j DROP $EBLOG
	}

	[ -n "$GW6_IP" ] && {
		#allow DNS packets originating from the current default gateway
		ebtables -A fo-guestwifi -s $GW6_MAC -p IPv6 --ip6-src $GW6_IP --ip6-proto tcp --ip6-sport 53 -j RETURN $EBLOG
		ebtables -A fo-guestwifi -s $GW6_MAC -p IPv6 --ip6-src $GW6_IP --ip6-proto udp --ip6-sport 53 -j RETURN $EBLOG

		#allow DHCPv6 reply packets
		ebtables -A fo-guestwifi -p IPv6 --ip6-proto udp --ip6-sport 547 --ip6-dport 546 -j RETURN $EBLOG

		#allow ipv6-icmp type router-solicitation, router-advertisement, neighbour-solicitation, neighbour-advertisement
		ebtables -A fo-guestwifi -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type router-solicitation -j RETURN $EBLOG
		ebtables -A fo-guestwifi -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type router-advertisement -j RETURN $EBLOG
		ebtables -A fo-guestwifi -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type neighbour-solicitation -j RETURN $EBLOG
		ebtables -A fo-guestwifi -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type neighbour-advertisement -j RETURN $EBLOG

		#deny any other IP packets originating from the current default gateway
		ebtables -A fo-guestwifi -s $GW6_MAC -p IPv6 --ip6-src $GW6_IP -j DROP $EBLOG
	}

	#allow all broadcast frames transporting plausible protocols for Internet usage (e.g. no MMEs)
	ebtables -A fi-guestwifi -p ARP -d FF:FF:FF:FF:FF:FF -j RETURN $EBLOG
	ebtables -A fi-guestwifi -p IPv4 -d FF:FF:FF:FF:FF:FF -j RETURN $EBLOG

	#allow all frames destined to the current default gateway (and thus, most likely to the Internet)
	[ -n "$GW4_MAC" ] && {
		ebtables -A fi-guestwifi -d $GW4_MAC -j RETURN $EBLOG
	}
	[ -n "$GW6_MAC" ] && {
		ebtables -A fi-guestwifi -d $GW6_MAC -j RETURN $EBLOG
	}

	#deny all frames originating from a guest account station, that have not been allowed already
	ebtables -A fi-guestwifi -j DROP $EBLOG

	#allow all frames originating from the current default gateway destined to a guest account station
	[ -n "$GW4_MAC" ] && {
		ebtables -A fo-guestwifi -s $GW4_MAC -j RETURN $EBLOG
	}
	[ -n "$GW6_MAC" ] && {
		ebtables -A fo-guestwifi -s $GW6_MAC -j RETURN $EBLOG
	}

	#deny all still unallowed frames destined to a guest account station
	ebtables -A fo-guestwifi -j DROP $EBLOG
}

delos_wifi_guest_timer() {
	local auto_switch_off=

	config_get_bool auto_switch_off guest_wifi auto_switch_off 0

	if [ "$auto_switch_off" = "1" ]; then
		local starttime=
		local stoptime=
		local interval=

		config_get interval guest_wifi interval '00:30'
		config_get starttime guest_wifi starttime
		if [ -n "$starttime" -a ${interval//:} -gt 0 ]; then
			stoptime=$(($starttime + $(echo $interval | awk -F: '{ print ($1 * 3600) + ($2 * 60) + 60 }')))
			_rm_and_add_cron_script "${SCRIPT}" "$(date -D %s -d ${stoptime} "+%M %H %d %m %a ${SCRIPT} stop")"
		fi
	else
		_rm_cron_script "${SCRIPT}"
	fi
}

delos_wifi_guest_post() {
	local action=$1; shift # enable/disable
	local iface_list
	local ifname

	case "$action" in
	enable)
		check_access() {
			# check guest wifi restriction option
			[ "$(config_get $1 disabled)" != "1" ] &&
			[ "$(config_get $(config_get $1 device) disabled)" != "1" ] &&
			[ "$(config_get $1 dvl_guest)" = "1" ] || return
			local dvl_unrestricted_access
			config_get dvl_unrestricted_access $1 dvl_unrestricted_access 0
			[ "$dvl_unrestricted_access" = "0" ] &&
			ifname=$(uci_get_state wireless $1 ifname 2>/dev/null) &&
				echo $ifname
		}
		iface_list=$(config_foreach check_access wifi-iface | sort)
		iface_list="$(echo $iface_list)"
		delos_wifi_guest_timer
		;;
	disable)
		iface_list=
		;;
	*)
		return
		;;
	esac


	if [ -n "$iface_list" ]; then (
		# subshell for new trap context
		TMP=
		trap '[ -n "$TMP" ] && rm -f "$TMP"; TMP=' EXIT HUP INT QUIT TERM
		TMP=$(mktemp)
		cat >$TMP <<-EOF
		GUEST_IFS="$iface_list"
		EOF
		# if changed:
		cmp -s /tmp/guestwifi-ifs $TMP || {
			mv $TMP /tmp/guestwifi-ifs
			TMP=
			( set_delos_guestwifi )
		}
	) else
		# if removed:
		rm /tmp/guestwifi-ifs 2>/dev/null && {
			( set_delos_guestwifi )
		}
	fi
}

# is DELOS_WIFI_DRIVERS defined at all (empty or non-empty)? (force exit code 0 if empty,
# don't exit current script if not defined)
if (: ${DELOS_WIFI_DRIVERS?}) 2>/dev/null; then
	# we were sourced to register as a plugin
	delos_wifi_driver_guest() {
		type=$1; shift
		type delos_wifi_guest_$type >/dev/null 2>/dev/null && delos_wifi_guest_$type "$@"
	}

	append DELOS_WIFI_DRIVERS delos_wifi_driver_guest
fi