# (C) Copyright 2005-2007, Axis Communications AB, LUND, SWEDEN
# This file is released under the GPL v2.

RC_GOT_IPv6=y

local run_dir=/var/run/init.d

[ -d $run_dir ] || mkdir -p $run_dir ||
	error "$0: Could not create run-directory!"

[ -r $RCLIB/sh/rc-dhcp6.sh ] && . $RCLIB/sh/rc-dhcp6.sh

#---------------------------------------------------------------------
# IPv6 configuration functions
#     net6_config
#     net6_stop
#     net6_need_restart
#     net6_get_enabled
#     net6_set_enabled
#
# Comments:
# 1. Setting accept_ra to 0 should remove the RA addresses. But since
#    this does not work:
#     - All addresses are flushed when disabling IPv6.
#     - The interface is restarted when the RA setting is changed and
#       IPv6 is still enabled.
#
# 2. Setting autoconf to 0 should remove the autoconfigured link local
#    address.  But since this does not work:
#    - All addresses are flushed when disabling IPv6.
#    - The interface is restarted when enabling IPv6.
#---------------------------------------------------------------------

# Due to a kernel bug, two icmpv6 packets are sent upon `ip link set up`
# even if IPv6 is disabled and autoconf or router advertisement are disabled
# in /proc/sys/net/ipv6/conf/. When the interface is up, the parameters are
# eventually taken into account. net6_pre_config and net6_post_config
# drop those two packets.

# The interface is brought up AFTER calling this function. The packets dropping
# can fail but should never stop the script (simply log the incident).
net6_pre_config() {
	[ "$INET6_ENABLED" != no ] ||
		! exists ip6tables ||
		ip6tables -A OUTPUT -p icmpv6 -j DROP ||
		logger "net6.sh: ip6tables incident in net6_pre_config"
}

# Delete the icmpv6 rule introduced by net6_pre_config()
net6_post_config() {
	[ "$INET6_ENABLED" != no ] ||
		! exists ip6tables ||
		ip6tables -D OUTPUT -p icmpv6 -j DROP ||
		logger "net6.sh: ip6tables incident in net6_post_config"
}

# Configure the interface for IPv6 using parameters from global env.
# variables.
# The interface is brought up BEFORE calling this function.
net6_config() {
	local _iface _prevcfg _net6conf _man_addrs _man_gw _maxw _w _incw

	[ $# -gt 0 ] || {
		echo "net6_config: invalid number of arguments: $#" >&2
		net6_post_config
		return 1
	}

	_iface=$1
	_prevcfg=$run_dir/net.$_iface.conf6
	_net6conf=/proc/sys/net/ipv6/conf
	_maxw=5000000			# micro secs
	_w=0
	_incw=250000			# micro secs

	[ ! -r $_prevcfg ] || . $_prevcfg
	rm -f $_prevcfg

	if [ "$INET6_ENABLED" = yes ]; then
		information "IPv6 on $_iface"

		# Enable use of RA, if so configured
		if [ "$INET6_RA" = yes ]; then
			information "Automatic IPv6 config using RA on $_iface"
			[ ! -w $_net6conf/$1/accept_ra ] ||
			echo 1 > $_net6conf/$_iface/accept_ra
		else
			[ ! -w $_net6conf/$1/accept_ra ] ||
			echo 0 > $_net6conf/$_iface/accept_ra
		fi
		[ ! -w $_net6conf/$_iface/autoconf ] ||
		echo 2 > $_net6conf/$_iface/autoconf

		# Manual IPv6 addresses
		local _man_addrs=$(manual_ipv6_addr_configuration \
					"$INET6_IP" "$PREV_INET6_IP")
		[ "$_man_addrs" ] &&
			information "Manual IPv6 addresses on $_iface: $_man_addrs"
		echo "PREV_INET6_IP=\"$_man_addrs\"" > $_prevcfg

		# Wait for link local addresses
		while [ $_w -lt $_maxw ]; do
			ip -6 address show scope link dev $_iface | \
				grep -q inet6 && break
			usleep $_incw
			_w=$(($_w + $_incw))
		done
		if [ $_w -lt $_maxw ]; then
			echo "net6_config: scope link address ok after" \
			     "$(($_w / 1000000))s" >&2
		else
			echo "net6_config: scope link address not found after" \
			     "$(($_maxw / 1000000))s" >&2
		fi

		# Manual IPv6 gateway
		local _man_gw=$(manual_ipv6_gw_configuration \
					"$INET6_GATEWAY" "$PREV_INET6_GATEWAY")
		[ "$_man_gw" ] &&
			information "Manual IPv6 default gateway on $_iface: $_man_gw"
		echo "PREV_INET6_GATEWAY=$_man_gw" >> $_prevcfg

		# Start DHCPv6 according to configuration
		if [ "$RC_GOT_DHCP6" ] && [ "$PREV_INET6_DHCPV6" != "$INET6_DHCPV6" ]; then
			stop_dhcp6 $_iface
			case "$INET6_DHCPV6" in
				auto|AUTO)
					start_dhcp6 $_iface
					;;
				stateful|STATEFUL)
					start_dhcp6 $_iface -fs
					;;
				stateless|STATELESS)
					start_dhcp6 $_iface -fo
					;;
				*)
					information "Wrong setting to start DHCPv6: $dhcp_enb"
					;;
			esac
		fi
		echo "PREV_INET6_DHCPV6=$INET6_DHCPV6" >> $_prevcfg
	else
		information "no IPv6 on $_iface"

		[ ! -w $_net6conf/$_iface/accept_ra ] ||
			echo 0 >$_net6conf/$_iface/accept_ra
		[ ! -w $_net6conf/$_iface/autoconf ] ||
			echo 0 >$_net6conf/$_iface/autoconf

		# Remove previous manual addresses
		if [ "$PREV_INET6_IP" ]; then
			local _ipv6_addr

			for _ipv6_addr in $PREV_INET6_IP; do
				ip -f inet6 address delete $_ipv6_addr \
					dev $_iface
			done
		fi
		[ "$PREV_INET6_GATEWAY" ] &&
			ip -f inet6 route del default via $PREV_INET6_GATEWAY \
				dev $_iface

		[ "$RC_GOT_DHCP6" != y ] || stop_dhcp6 $_iface

		net_flush_ipv6_addresses $_iface
	fi

	net6_post_config
}

# Remove all IPv6 configuration
net6_stop() {
	local _iface _prevcfg

	[ $# -gt 0 ] || {
		echo "net6_stop: invalid number of arguments: $#" >&2
		return 1
	}

	_iface=$1

	_prevcfg=$run_dir/net.$_iface.conf6
	rm -f $_prevcfg

	[ "$RC_GOT_DHCP6" != y ] || stop_dhcp6 $_iface
	net_flush_ipv6_addresses $_iface
}

# Check if the IPv6 configuration require a restart of the interface
# (See comments above.)
net6_need_restart() {
	local _net6conf _current_ra _new_ra=0 _current_auto=0

	[ $# -gt 0 ] || {
		echo "net6_need_restart: invalid number of arguments: $#" >&2
		return 1
	}

	_net6conf=/proc/sys/net/ipv6/conf
	_current_ra=$([ -r $_net6conf/$1/accept_ra ] &&
		      cat $_net6conf/$1/accept_ra 2>/dev/null || echo 0)

	[ "$INET6_ENABLED" != yes ] || {
		[ "$INET6_RA" != yes ] || _new_ra=1
		[ ! -f $_net6conf/$1/accept_ra ] ||
			echo $_new_ra > $_net6conf/$1/accept_ra
		[ -r $_net6conf/$_iface/autoconf ] &&
			_current_auto=$(cat $_net6conf/$1/autoconf) ||
			_current_auto=0
	}

	# No restart needed when IPv6 is being disabled
	[ "$INET6_ENABLED" = yes ] || return 1

	# Restart needed when RA is changed or IPv6 is being enabled
	[ "$_new_ra" = "$_current_ra" ] || return 0
	[ $_current_auto -ne 0 ] || return 0
	return 1
}

net6_get_enabled() {
	[ "$INET6_ENABLED" != yes ] || return 0
	return 1
}

net6_set_enabled() {
	INET6_ENABLED=yes
}

#---------------------------------------------------------------------
# Help functions
#---------------------------------------------------------------------

# Return the IPv6 address, including the prefix length. Set default
# lenght if not included.
build_ipv6_address() {
	local _ip _len

	[ $# -gt 0 ] || {
		echo "build_ipv6_address: invalid number of arguments: $#" >&2
		return 1
	}

	_ip=${1%%/*}
	_len=${1#*/}

	[ "$_len" = "$_ip" ] || [ -z "$_len" ] && _len=64
	echo "$_ip/$_len"
}

# Manual IPv6 address configuration
manual_ipv6_addr_configuration() {
	local _new_addrs _prev_addrs _res_addrs _new _new2 _prev _res

	[ $# -gt 1 ] || {
		echo "manual_ipv6_addr_configuration: invalid number of arguments: $#" >&2
		return 1
	}

	_new_addrs=$1
	_prev_addrs=$2
	_res_addrs=

	if [ "$_new_addrs" ]; then
		for _new in $_new_addrs; do
			_new2=$(build_ipv6_address $_new)
			ip -f inet6 address add $_new2 dev $_iface
			_res_addrs="$_res_addrs $_new2"
		done
	fi
	if [ "$_prev_addrs" ]; then
		for _prev in $_prev_addrs; do
			for _res in $_res_addrs; do
				[ "$_res" != "$_prev" ] || _prev=
			done
			[ -z "$_prev" ] ||
				ip -f inet6 address delete $_prev dev $_iface
		done
	fi
	echo "$_res_addrs"
}

# Manual IPv6 gateway configuration
manual_ipv6_gw_configuration() {
	local _new_gw _prev_gw

	[ $# -gt 1 ] || {
		echo "manual_ipv6_gw_configuration: invalid number of arguments: $#" >&2
		return 1
	}

	_new_gw=$1
	_prev_gw=$2

	if [ "$_prev_gw" ]; then
		if [ "$_prev_gw" != "$_new_gw" ]; then
			ip -f inet6 route del default via $_prev_gw dev $_iface
		fi
	fi

	[ -z "$_new_gw" ] ||
		ip -f inet6 route add default via $_new_gw dev $_iface

	echo "$_new_gw"
}
