#!/bin/sh /etc/rc.common

START=90
STOP=10

USE_PROCD=1
PROG=/usr/lib/ipsec/charon

. $IPKG_INSTROOT/lib/functions.sh
. $IPKG_INSTROOT/lib/functions/network.sh

STRONGSWAN_CONF_FILE=/etc/strongswan.conf
STRONGSWAN_VAR_CONF_FILE=/var/ipsec/strongswan.conf

SWANCTL_CONF_FILE=/etc/swanctl/swanctl.conf
SWANCTL_VAR_CONF_FILE=/var/swanctl/swanctl.conf

WAIT_FOR_INTF=0

CONFIG_FAIL=0

time2seconds() {
	local timestring="$1"
	local multiplier number suffix

	suffix="${timestring//[0-9 ]}"
	number="${timestring%%$suffix}"
	[ "$number$suffix" != "$timestring" ] && return 1
	case "$suffix" in
	""|s)
		multiplier=1 ;;
	m)
		multiplier=60 ;;
	h)
		multiplier=3600 ;;
	d)
		multiplier=86400 ;;
	*)
		return 1 ;;
	esac
	echo $(( number * multiplier ))
}

seconds2time() {
	local seconds="$1"

	if [ $seconds -eq 0 ]; then
		echo "0s"
	elif [ $((seconds % 86400)) -eq 0 ]; then
		echo "$((seconds / 86400))d"
	elif [ $((seconds % 3600)) -eq 0 ]; then
		echo "$((seconds / 3600))h"
	elif [ $((seconds % 60)) -eq 0 ]; then
		echo "$((seconds / 60))m"
	else
		echo "${seconds}s"
	fi
}

file_reset() {
	: > "$1"
}

xappend() {
	local file="$1"
	local indent="$2"
	shift 2

	for cmd in "$@"; do
		echo "$indent$cmd" >> "$file"
	done
}

swan_reset() {
	file_reset "$STRONGSWAN_VAR_CONF_FILE"
}

swan_xappend() {
	xappend "$STRONGSWAN_VAR_CONF_FILE" "$@"
}

swan_xappend0() {
	swan_xappend "" "$@"
}

swan_xappend1() {
	swan_xappend "  " "$@"
}

swan_xappend2() {
	swan_xappend "    " "$@"
}

swan_xappend3() {
	swan_xappend "      " "$@"
}

swan_xappend4() {
	swan_xappend "        " "$@"
}

swanctl_reset() {
	file_reset "$SWANCTL_VAR_CONF_FILE"
}

swanctl_xappend() {
	xappend "$SWANCTL_VAR_CONF_FILE" "$@"
}

swanctl_xappend0() {
	swanctl_xappend "" "$@"
}

swanctl_xappend1() {
	swanctl_xappend "  " "$@"
}

swanctl_xappend2() {
	swanctl_xappend "    " "$@"
}

swanctl_xappend3() {
	swanctl_xappend "      " "$@"
}

swanctl_xappend4() {
	swanctl_xappend "        " "$@"
}

warning() {
	echo "WARNING: $@" >&2
}

fatal() {
	echo "ERROR: $@" >&2
	CONFIG_FAIL=1
}

append_var() {
	local var="$2" value="$1" delim="${3:- }"
	append "$var" "$value" "$delim"
}

is_aead() {
	local cipher="$1"

	case "$cipher" in
	aes*gcm*|aes*ccm*|aes*gmac*)
		return 0 ;;
	chacha20poly1305)
		return 0 ;;
	esac

	return 1
}

config_esp_proposal() {
	local conf="$1"

	local encryption_algorithm
	local hash_algorithm
	local dh_group

	config_get encryption_algorithm "$conf" encryption_algorithm
	config_get hash_algorithm "$conf" hash_algorithm
	config_get dh_group "$conf" dh_group

	# check for AEAD and clobber hash_algorithm if set
	if is_aead "$encryption_algorithm" && [ -n "$hash_algorithm" ]; then
		fatal "Can't have $hash_algorithm with $encryption_algorithm"
		hash_algorithm=
	fi

	[ -n "$encryption_algorithm" ] && \
		crypto="${crypto:+${crypto},}${encryption_algorithm}${hash_algorithm:+-${hash_algorithm}}${dh_group:+-${dh_group}}"
}

iter_esp_proposal() {
	local conf="$1"
	local var="$2"

	local crypto=""

	config_list_foreach "$conf" crypto_proposal config_esp_proposal

	export -n "$var=$crypto"
}

config_ike_proposal() {
	local conf="$1"

	local encryption_algorithm
	local hash_algorithm
	local dh_group
	local prf_algorithm

	config_get encryption_algorithm "$conf" encryption_algorithm
	config_get hash_algorithm "$conf" hash_algorithm
	config_get dh_group "$conf" dh_group
	config_get prf_algorithm "$conf" prf_algorithm

	# check for AEAD and clobber hash_algorithm if set
	if is_aead "$encryption_algorithm" && [ -n "$hash_algorithm" ]; then
		fatal "Can't have $hash_algorithm with $encryption_algorithm"
		hash_algorithm=
	fi

	[ -n "$encryption_algorithm" ] && \
		crypto="${crypto:+${crypto},}${encryption_algorithm}${hash_algorithm:+-${hash_algorithm}}${prf_algorithm:+-${prf_algorithm}}${dh_group:+-${dh_group}}"
}

iter_ike_proposal() {
	local conf="$1"
	local var="$2"

	local crypto=""

	config_list_foreach "$conf" crypto_proposal config_ike_proposal

	export -n "$var=$crypto"
}

config_child() {
	# Generic ipsec conn section shared by tunnel and transport
	local conf="$1"
	local mode="$2"

	local hw_offload
	local interface
	local ipcomp
	local priority
	local local_subnet
	local local_nat
	local updown
	local firewall
	local remote_subnet
	local lifetime
	local dpdaction
	local closeaction
	local startaction
	local if_id
	local rekeytime
	local rekeybytes
	local lifebytes
	local rekeypackets
	local lifepackets
	local replay_window

	config_get startaction "$conf" startaction "route"
	config_get local_nat "$conf" local_nat ""
	config_get updown "$conf" updown ""
	config_get firewall "$conf" firewall ""
	config_get lifetime "$conf" lifetime ""
	config_get dpdaction "$conf" dpdaction "none"
	config_get closeaction "$conf" closeaction "none"
	config_get if_id "$conf" if_id ""
	config_get rekeytime "$conf" rekeytime ""
	config_get_bool ipcomp "$conf" ipcomp 0
	config_get interface "$conf" interface ""
	config_get hw_offload "$conf" hw_offload ""
	config_get priority "$conf" priority ""
	config_get rekeybytes "$conf" rekeybytes ""
	config_get lifebytes "$conf" lifebytes ""
	config_get rekeypackets "$conf" rekeypackets ""
	config_get lifepackets "$conf" lifepackets ""
	config_get replay_window "$conf" replay_window ""

	config_list_foreach "$conf" local_subnet append_var local_subnet ","
	config_list_foreach "$conf" remote_subnet append_var remote_subnet ","

	local esp_proposal
	iter_esp_proposal "$conf" esp_proposal

	# translate from ipsec to swanctl
	case "$startaction" in
	add)
		startaction="none" ;;
	route)
		startaction="trap" ;;
	start|none|trap)
		# already using new syntax
		;;
	*)
		fatal "Startaction $startaction unknown"
		startaction=
		;;
	esac

	case "$closeaction" in
	none|clear)
		closeaction="none" ;;
	hold)
		closeaction="trap" ;;
	restart)
		closeaction="start" ;;
	trap|start)
		# already using new syntax
		;;
	*)
		fatal "Closeaction $closeaction unknown"
		closeaction=
		;;
	esac

	[ -n "$closeaction" -a "$closeaction" != "none" ] && warning "Closeaction $closeaction can cause instability"

	case "$dpdaction" in
	none)
		dpddelay="0s"
		dpdaction=
		;;
	clear)
		;;
	hold)
		dpdaction="trap" ;;
	restart)
		dpdaction="start" ;;
	trap|start)
		# already using new syntax
		;;
	*)
		fatal "Dpdaction $dpdaction unknown"
		dpdaction=
		;;
	esac

	case "$hw_offload" in
	yes|no|auto|"")
		;;
	*)
		fatal "hw_offload value $hw_offload invalid"
		hw_offload=""
		;;
	esac

	[ -n "$local_nat" ] && local_subnet="$local_nat"

	swanctl_xappend3 "$conf {"

	[ -n "$local_subnet" ] && swanctl_xappend4 "local_ts = $local_subnet"
	[ -n "$remote_subnet" ] && swanctl_xappend4 "remote_ts = $remote_subnet"

	[ -n "$hw_offload" ] && swanctl_xappend4 "hw_offload = $hw_offload"
	[ $ipcomp -eq 1 ] && swanctl_xappend4 "ipcomp = 1"
	[ -n "$interface" ] && swanctl_xappend4 "interface = $interface"
	[ -n "$priority" ] && swanctl_xappend4 "priority = $priority"
	[ -n "$if_id" ] && swanctl_xappend4 "if_id_in = $if_id" "if_id_out = $if_id"
	[ -n "$startaction" -a "$startaction" != "none" ] && swanctl_xappend4 "start_action = $startaction"
	[ -n "$closeaction" -a "$closeaction" != "none" ] && swanctl_xappend4 "close_action = $closeaction"
	swanctl_xappend4 "esp_proposals = $esp_proposal"
	swanctl_xappend4 "mode = $mode"

	if [ -n "$lifetime" ]; then
		swanctl_xappend4 "life_time = $lifetime"
	elif [ -n "$rekeytime" ]; then
		swanctl_xappend4 "life_time = $(seconds2time $(((110 * $(time2seconds $rekeytime)) / 100)))"
	fi
	[ -n "$rekeytime" ] && swanctl_xappend4 "rekey_time = $rekeytime"
	if [ -n "$lifebytes" ]; then
		swanctl_xappend4 "life_bytes = $lifebytes"
	elif [ -n "$rekeybytes" ]; then
		swanctl_xappend4 "life_bytes = $(((110 * rekeybytes) / 100))"
	fi
	[ -n "$rekeybytes" ] && swanctl_xappend4 "rekey_bytes = $rekeybytes"
	if [ -n "$lifepackets" ]; then
		swanctl_xappend4 "life_packets = $lifepackets"
	elif [ -n "$rekeypackets" ]; then
		swanctl_xappend4 "life_packets = $(((110 * rekeypackets) / 100))"
	fi
	[ -n "$rekeypackets" ] && swanctl_xappend4 "rekey_packets = $rekeypackets"
	[ -n "$inactivity" ] && swanctl_xappend4 "inactivity = $inactivity"

	[ -n "$updown" ] && swanctl_xappend4 "updown = $updown"
	[ -n "$dpdaction" ] && swanctl_xappend4 "dpd_action = $dpdaction"
	[ -n "$replay_window" ] && swanctl_xappend4 "replay_window = $replay_window"

        swanctl_xappend3 "}"
}

config_tunnel() {
	config_child "$1" "tunnel"
}

config_transport() {
	config_child "$1" "transport"
}

config_pool() {
	local conf="$1"

	local addrs
	local dns
	local nbns
	local dhcp
	local netmask
	local server
	local subnet
	local split_include
	local split_exclude

	config_get addrs "$conf" addrs
	config_list_foreach "$conf" dns append_var dns ","
	config_list_foreach "$conf" nbns append_var nbns ","
	config_list_foreach "$conf" dhcp append_var dhcp ","
	config_list_foreach "$conf" netmask append_var netmask ","
	config_list_foreach "$conf" server append_var server ","
	config_list_foreach "$conf" subnet append_var subnet ","
	config_list_foreach "$conf" split_include append_var split_include ","
	config_list_foreach "$conf" split_exclude append_var split_exclude ","

	swanctl_xappend1 "$conf {"
	[ -n "$addrs" ] && swanctl_xappend2 "addrs = $addrs"
	[ -n "$dns" ] && swanctl_xappend2 "dns = $dns"
	[ -n "$nbns" ] && swanctl_xappend2 "nbns = $nbns"
	[ -n "$dhcp" ] && swanctl_xappend2 "dhcp = $dhcp"
	[ -n "$netmask" ] && swanctl_xappend2 "netmask = $netmask"
	[ -n "$server" ] && swanctl_xappend2 "server = $server"
	[ -n "$subnet" ] && swanctl_xappend2 "subnet = $subnet"
	[ -n "$split_include" ] && swanctl_xappend2 "split_include = $split_include"
	[ -n "$split_exclude" ] && swanctl_xappend2 "split_exclude = $split_exclude"
	swanctl_xappend1 "}"
}

config_remote() {
	local conf="$1"

	local enabled
	local gateway
	local local_sourceip
	local local_ip
	local remote_gateway
	local pre_shared_key
	local auth_method
	local keyingtries
	local dpddelay
	local inactivity
	local keyexchange
	local fragmentation
	local mobike
	local local_cert
	local local_key
	local ca_cert
	local rekeytime
	local remote_ca_certs
	local pools

	config_get_bool enabled "$conf" enabled 0
	[ $enabled -eq 0 ] && return

	config_get gateway "$conf" gateway
	config_get pre_shared_key "$conf" pre_shared_key
	config_get auth_method "$conf" authentication_method
	config_get local_identifier "$conf" local_identifier ""
	config_get remote_identifier "$conf" remote_identifier ""
	config_get local_ip "$conf" local_ip "%any"
	config_get keyingtries "$conf" keyingtries "3"
	config_get dpddelay "$conf" dpddelay "30s"
	config_get inactivity "$conf" inactivity
	config_get keyexchange "$conf" keyexchange "ikev2"
	config_get fragmentation "$conf" fragmentation "yes"
	config_get_bool mobike "$conf" mobike 1
	config_get local_cert "$conf" local_cert ""
	config_get local_key "$conf" local_key ""
	config_get ca_cert "$conf" ca_cert ""
	config_get rekeytime "$conf" rekeytime
	config_get overtime "$conf" overtime

	config_list_foreach "$conf" local_sourceip append_var local_sourceip ","
	config_list_foreach "$conf" remote_ca_certs append_var remote_ca_certs ","
	config_list_foreach "$conf" pools append_var pools ","

	case "$fragmentation" in
	0)
		fragmentation="no" ;;
	1)
		fragmentation="yes" ;;
	yes|accept|force|no)
		# already using new syntax
		;;
	*)
		fatal "Fragmentation $fragmentation not supported"
		fragmentation=
		;;
	esac

	[ "$gateway" = "any" ] && remote_gateway="%any" || remote_gateway="$gateway"

	if [ -n "$local_key" ]; then
		[ "$(dirname "$local_key")" != "." ] && \
		   fatal "local_key $local_key can't be pathname"
		[ -f "/etc/swanctl/private/$local_key" ] || \
		   fatal "local_key $local_key not found"
	fi

	local ike_proposal
	iter_ike_proposal "$conf" ike_proposal

	[ -n "$firewall" ] && fatal "Firewall not supported"

	if [ "$auth_method" = pubkey ]; then
		if [ -n "$ca_cert" ]; then
			[ "$(dirname "$ca_cert")" != "." ] && \
			    fatal "ca_cert $ca_cert can't be pathname"
			[ -f "/etc/swanctl/x509ca/$ca_cert" ] || \
			    fatal "ca_cert $ca_cert not found"
		fi

		if [ -n "$local_cert" ]; then
			[ "$(dirname "$local_cert")" != "." ] && \
			    fatal "local_cert $local_cert can't be pathname"
			[ -f "/etc/swanctl/x509/$local_cert" ] || \
			    fatal "local_cert $local_cert not found"
		fi
	fi

	swanctl_xappend0 "# config for $conf"
	swanctl_xappend0 "connections {"
	swanctl_xappend1 "$conf {"
	swanctl_xappend2 "local_addrs = $local_ip"
	swanctl_xappend2 "remote_addrs = $remote_gateway"

	[ -n "$local_sourceip" ] && swanctl_xappend2 "vips = $local_sourceip"
	[ -n "$fragmentation" ] && swanctl_xappend2 "fragmentation = $fragmentation"
	[ -n "$pools" ] && swanctl_xappend2 "pools = $pools"

	swanctl_xappend2 "local {"
	swanctl_xappend3 "auth = $auth_method"

	[ -n "$local_identifier" ] && swanctl_xappend3 "id = \"$local_identifier\""
	[ "$auth_method" = pubkey ] && [ -n "$local_cert" ] && \
	    swanctl_xappend3 "certs = $local_cert"
	swanctl_xappend2 "}"

	swanctl_xappend2 "remote {"
	swanctl_xappend3 "auth = $auth_method"
	[ -n "$remote_identifier" ] && swanctl_xappend3 "id = \"$remote_identifier\""
	[ -n "$remote_ca_certs" ] && swanctl_xappend3 "cacerts = \"$remote_ca_certs\""
	swanctl_xappend2 "}"

	swanctl_xappend2 "children {"

	config_list_foreach "$conf" tunnel config_tunnel

	config_list_foreach "$conf" transport config_transport

	swanctl_xappend2 "}"

	case "$keyexchange" in
	ike)
		;;
	ikev1)
		swanctl_xappend2 "version = 1" ;;
	ikev2)
		swanctl_xappend2 "version = 2" ;;
	*)
		fatal "Keyexchange $keyexchange not supported"
		keyexchange=
		;;
	esac

	[ $mobike -eq 1 ] && swanctl_xappend2 "mobike = yes" || swanctl_xappend2 "mobike = no"

	if [ -n "$rekeytime" ]; then
		swanctl_xappend2 "rekey_time = $rekeytime"

		if [ -z "$overtime" ]; then
			overtime=$(seconds2time $(($(time2seconds $rekeytime) / 10)))
		fi
	fi
	[ -n "$overtime" ] && swanctl_xappend2 "over_time = $overtime"

	swanctl_xappend2 "proposals = $ike_proposal"
	[ -n "$dpddelay" ] && swanctl_xappend2 "dpd_delay = $dpddelay"
	[ "$keyingtries" = "%forever" ] && swanctl_xappend2 "keyingtries = 0" || swanctl_xappend2 "keyingtries = $keyingtries"

	swanctl_xappend1 "}"
	swanctl_xappend0 "}"

	if [ "$auth_method" = pubkey ]; then
		swanctl_xappend0 ""

		if [ -n "$ca_cert" ]; then
			swanctl_xappend0 "authorities {"
			swanctl_xappend1 "$conf {"
			swanctl_xappend2 "cacert = $ca_cert"
			swanctl_xappend1 "}"
			swanctl_xappend0 "}"
		fi

	elif [ "$auth_method" = psk ]; then
		swanctl_xappend0 ""

		swanctl_xappend0 "secrets {"
		swanctl_xappend1 "ike-$conf {"
		swanctl_xappend2 "secret = $pre_shared_key"
		if [ -n "$local_identifier" ]; then
			swanctl_xappend2 "id1 = $local_identifier"
			if [ -n "$remote_identifier" ]; then
				swanctl_xappend2 "id2 = $remote_identifier"
			fi
		fi
		swanctl_xappend1 "}"
		swanctl_xappend0 "}"
	else
		fatal "AuthenticationMode $auth_mode not supported"
	fi

	swanctl_xappend0 "pools {"
	config_list_foreach "$conf" pools config_pool
	swanctl_xappend0 "}"

	swanctl_xappend0 ""
}

do_preamble() {
	swanctl_xappend0 "# generated by /etc/init.d/swanctl"
}

config_ipsec() {
	local conf="$1"

	local rtinstall_enabled
	local routing_table
	local routing_table_id
	local interface
	local interface_list

	config_get debug "$conf" debug 0
	config_get_bool rtinstall_enabled "$conf" rtinstall_enabled 1
	[ $rtinstall_enabled -eq 1 ] && install_routes=yes || install_routes=no

	# prepare extra charon config option ignore_routing_tables
	for routing_table in $(config_get "$conf" "ignore_routing_tables"); do
		if [ "$routing_table" -ge 0 ] 2>/dev/null; then
			routing_table_id=$routing_table
		else
			routing_table_id=$(sed -n '/[ \t]*[0-9]\+[ \t]\+'$routing_table'[ \t]*$/s/[ \t]*\([0-9]\+\).*/\1/p' /etc/iproute2/rt_tables)
		fi

		[ -n "$routing_table_id" ] && append routing_tables_ignored "$routing_table_id"
	done

	config_list_foreach "$conf" interface append_var interface_list

	if [ -z "$interface_list" ]; then
		WAIT_FOR_INTF=0
	else
		for interface in $interface_list; do
			network_get_device device $interface
			[ -n "$device" ] && append device_list "$device" ","
		done
		[ -n "$device_list" ] && WAIT_FOR_INTF=0 || WAIT_FOR_INTF=1
	fi
}

do_postamble() {
	swan_xappend0 "# generated by /etc/init.d/swanctl"
	swan_xappend0 "charon {"
	swan_xappend1 "install_routes = $install_routes"
	[ -n "$routing_tables_ignored" ] && swan_xappend1 "ignore_routing_tables = $routing_tables_ignored"
	[ -n "$device_list" ] && swan_xappend1 "interfaces_use = $device_list"
	swan_xappend1 "start-scripts {"
	swan_xappend2 "load-all = /usr/sbin/swanctl --load-all --noprompt"
	swan_xappend1 "}"
	swan_xappend1 "syslog {"
	swan_xappend2 "identifier = ipsec"
	swan_xappend2 "daemon {"
	swan_xappend3 "default = $debug"
	swan_xappend2 "}"
	swan_xappend1 "}"
	swan_xappend0 "}"
}

prepare_env() {
	mkdir -p /var/ipsec /var/swanctl

	swan_reset
	swanctl_reset
	do_preamble

	# needed by do_postamble
	local debug install_routes routing_tables_ignored device_list

	config_load ipsec
	config_foreach config_ipsec ipsec
	config_foreach config_remote remote

	do_postamble
}

service_running() {
	swanctl --stats > /dev/null 2>&1
}

reload_service() {
	running && {
		prepare_env
		[ $WAIT_FOR_INTF -eq 0 ] && {
			swanctl --load-all --noprompt
			return
		}
	}

	start
}

stop_service() {
	swan_reset
	swanctl_reset
}

service_triggers() {
	procd_add_reload_trigger "ipsec"
	config load "ipsec"

	config_foreach service_trigger_ipsec ipsec
}

service_trigger_ipsec() {
	local interface interface_list
	config_list_foreach "$1" interface append_var interface_list
	for interface in $interface_list; do
		procd_add_reload_interface_trigger $interface
	done
}

start_service() {
	prepare_env

	[ $WAIT_FOR_INTF -eq 1 ] && return

	if [ $CONFIG_FAIL -ne 0 ]; then
		procd_set_param error "Invalid configuration"
		return
	fi

	procd_open_instance

	procd_set_param command $PROG

	procd_set_param file $SWANCTL_CONF_FILE
	procd_append_param file /etc/swanctl/conf.d/*.conf
	procd_append_param file $STRONGSWAN_CONF_FILE

	procd_set_param respawn

	procd_close_instance
}
