#!/bin/bash
# vim: cindent:shiftwidth=4:tabstop=4:smarttab:textwidth=100

#$title$ Internet Protocol (v4/v6)
#$check$ loopback interface status, missing/multiple default routes
#$ref$ KB:InterfaceBonding
#$author$ Rafal Rzeczkowski
#$version$ 0.8.1

set -o posix
set -o errexit
set -o pipefail
set -o nounset
#set -o xtrace

level_check short

#CHANGELOG
#0.5.0	initial
#0.5.1	also check loopback interface status
#0.5.2	metadata headers, help messages
#0.5.3	IPv6 stack analysis
#0.5.4	detect deprecated IPv6 addresses
#0.5.5	invert IPv6 detection logic to reduce indent level
#0.5.6	ignore IPv6 tunnel interfaces when checking deprecated status
#0.5.7	ensure that IPv6 tunnel addresses are deprecated
#0.5.8	restyle according to https://kb.clearcable.ca/KB/ProgrammingStyleStandards
#0.5.9	ignore IPv6 loopback interfaces with LVS addresses when checking deprecated status
#0.6.0	allow (and TEST!) multiple IPv6 default routes
#0.6.1	skip IPv4 tests if no addresses are present
#0.6.2	move to short level_check
#0.6.3	ignore deprecated IPv6 addresses managed by keepalived (with "nodad" flag)
#0.7.0	cache DNS lookup results for the IPv6 router test
#0.7.1	accept IPv6 default route via OpenVPN
#0.7.2	parse multiple IPv6 default routers output syntax from iproute2 v6
#0.7.3	probe IPv4 activation status based on route presence
#0.8.0	relax IPv4 routing requirements when IPv6 connectivity is present
#0.8.1	improve detection of IPv4 nexthop

declare -r ICMP6_EXTERNAL_TARGET_NAME='ip6-route.test.clearcable.net.'
declare -r ICMP_EXTERNAL_TARGET_NAME='ip4-route.test.clearcable.net.'

function validate_loopback () {
	iface_lo_state=$(ip -oneline link show dev lo | awk '{
		for (i=0;i<=NF;i++)
			if ($i=="state") {
				next_field=i+1
				print $next_field
			}
		}')

	case "$iface_lo_state" in
		UNKNOWN)
			echodebug 'loopback interface state is OK'
			;;
		DOWN)
			fail critical "loopback interface state is $iface_lo_state"
			helpmsg 'verify that systemd-networkd.service is running'
			exit
			;;
		*)
			exit 1
			;;
	esac
}

function is_ipv6_enabled() {
	local disable_ipv6=$(</proc/sys/net/ipv6/conf/all/disable_ipv6)
	if [[ $disable_ipv6 -eq 0 ]]; then
		echodebug 'kernel IPv6 support is enabled'
		return 0
	else
		echodebug 'IPv6 support disabled via proc filesystem'
		return 1
	fi
}

function is_ipv6_active() {
	local ipv6_addresses=($(ip -oneline -family inet6 address show |
		awk '{ if ($4!~/^(fe80)?::/) print $4 }'))

	if [[ ${#ipv6_addresses[@]} -gt 0 ]]; then
		local address_plural=$(plural_text ${#ipv6_addresses[@]} 'address')
		echodebug "found ${#ipv6_addresses[@]} active IPv6 $address_plural: ${ipv6_addresses[@]}"
		return 0
	else
		echodebug 'IPv6 stack is inactive since there are no valid addresses present'
		return 1
	fi
}

function is_ipv4_active() {
	local ipv4_routes=$(ip -oneline -family inet route show)
	if [[ -n "$ipv4_routes" ]]; then
		return 0
	else
		echodebug 'IPv4 stack is inactive since there are no routes present'
		return 1
	fi
}

function get_icmp6_external_target() {
	local icmp6_external_target

	if icmp6_external_target=$(getent hosts $ICMP6_EXTERNAL_TARGET_NAME); then
		icmp6_external_target=${icmp6_external_target%% *}
		state_save <<< $icmp6_external_target
	else
		icmp6_external_target=$(state_load)
		if [[ -z "$icmp6_external_target" ]]; then
			unknown "unable to determine the address of ICMP6_EXTERNAL_TARGET ($ICMP6_EXTERNAL_TARGET_NAME)"
			exit
		fi
	fi
	echo $icmp6_external_target
}

function get_inet6_default_nexthop() {
	# "default via fe80::aec:f5ff:fe60:1ddb dev eth0 proto ra metric 1024 expires 1735sec hoplimit 64 pref medium"
	# "default dev tun0 metric 1024 pref medium"
	# "default proto ra metric 1024 expires 1617sec mtu 9178 pref medium
	#		nexthop via fe80::aec:f5ff:fe60:ec3 dev enp1s0 weight 1
	#		nexthop via fe80::aec:f5ff:fe60:1ddb dev enp1s0 weight 1"
	ip -family inet6 route show default | awk '{
	if ( $1=="default" ) {
		if ( $2=="via" && $4=="dev" )
			print $3"%"$5
		else if ( $2=="dev" && $4=="metric" )
			print "ff02::2%"$3
		else if ( $2=="proto" && $4=="metric" )
			nexthop=1
	} else if ( nexthop && $1=="nexthop") {
		if ( $2=="via" && $4=="dev" )
			print $3"%"$5
	}
}'
}

function get_inet_default_nexthop() {
	# default via 192.168.1.1 dev eno1 proto dhcp src 192.168.1.177 metric 1024
	# default via 207.210.46.1 dev enX0 proto static
	# default dev ppp0 scope link
	# 206.248.155.244 dev ppp0 proto kernel scope link src 69.165.220.178
	ip -oneline -family inet route show | awk '{
	if ( $1=="default") {
		if ( $2=="via" && $4=="dev" )
			print $3
		else if ( $2=="dev" )
			peer_dev=$3
	} else if ( $1!="default" && $2=="dev" && $3==peer_dev )
		print $1
}'
}

function check_inet6_nexthop() {
	local nexthop=$1

	nexthop_address=${nexthop%%%*}
	nexthop_device=${nexthop##*%}
	if [[ $nexthop_address = 'ff02::2' ]]; then
		return
	fi

	# nexthop must be explicitly tested for reachability first
	# otherwise a static route added to unreachable gateway will be /ignored/
	# due to FAILED nud state in ip-neighbour
	if fping6 $nexthop >/dev/null; then
		echodebug "ICMPv6 reachability of $nexthop is OK"
	else
		case $nexthop_device in
			ppp*) echodebug "ICMPv6 reachability of $nexthop fails on dev $nexthop_device";;
			*)
				fail warning "next-hop router $nexthop fails to respond to ICMPv6 probes"
				helpmsg 'router address advertised in RA is bogus'
				exit
				;;
		esac
	fi

	ip -family inet6 route add $icmp6_external_target via $nexthop_address dev $nexthop_device
	##ip -family inet6 route get $icmp6_external_target
	if fping6 $icmp6_external_target >/dev/null; then
		ip -family inet6 route delete $icmp6_external_target
		echodebug "ICMPv6 connectivity via $nexthop to $icmp6_external_target is OK"
	else
		ip -family inet6 route delete $icmp6_external_target
		fail warning "next-hop router $nexthop fails to route ICMPv6 to $icmp6_external_target"
		helpmsg 'rogue IPv6 routers on the vLAN?'
		exit
	fi
}

function validate_inet6_default_nexthop() {
	inet6_default_nexthop=($(get_inet6_default_nexthop))

	if [[ ${#inet6_default_nexthop[@]} -eq 0 ]]; then
		fail critical 'missing IPv6 default next-hop router'
		helpmsg 'RA disabled on the subnet/vLAN router; autoconf disabled in sysctl net.ipv6.conf'
		exit
	fi

	router_plural=$(plural_text ${#inet6_default_nexthop[@]} 'router')
	echodebug "IPv6 next-hop $router_plural: ${inet6_default_nexthop[@]}"

	icmp6_external_target=$(get_icmp6_external_target)

	local nexthop
	for nexthop in ${inet6_default_nexthop[@]}; do
		check_inet6_nexthop $nexthop
	done
}

function validate_deprecated_address() {
	# deprecated addresses: valid_lft > 0, but preferred_lft < 0
	if ! ip -oneline -family inet6 address show |
		awk '{
		for (i=7;i<NF;i++){
			if ($i=="deprecated" && $2!="6in4" && $2!="lo" && $7!="nodad")
				exit 1
		}
	}'; then
		fail warning 'deprecated IPv6 address detected (preferred_lft<0)'
		helpmsg 'IPv6 Router Advertisements stopped or previous prefix no longer advertised'
		exit
	fi
}

function validate_6in4_tunnel_address() {
   # this address *must* be deprecated to avoid selecting it for outbound requests
	if ! ip -oneline -family inet6 address show | awk '{
		if ($2 == "6in4") {
			tunnel=1
			if ($7 == "deprecated")
				deprecated=1
		}
	}
	END {
		if (tunnel && !deprecated)
			exit 1
	}'; then
		fail warning 'non-deprecated IPv6 address detected on 6in4 tunnel interface'
		helpmsg 'set preferred_lft=0 for the IPv6 tunnel address'
		exit
	fi
}

function validate_inet_default_nexthop() {
	inet_default_nexthop=($(get_inet_default_nexthop))

	if [[ ${#inet_default_nexthop[@]} -eq 0 ]]; then
		if [[ $IPV6_STACK_OK -gt 0 ]]; then
			echodebug 'no IPv4 Internet connectivity, but IPv6 stack is OK'
		else
			fail critical 'missing IPv4 default next-hop router'
			helpmsg 'DHCP client failure; {gateway} field not defined in {/etc/network/interfaces} Internet access stanza'
			exit
		fi
	elif [[ ${#inet_default_nexthop[@]} -gt 1 ]]; then
		fail warning "multiple IPv4 default next-hop routers: ${inet_default_nexthop[@]}"
		helpmsg 'do not use multiple IPv4 gateways; the configuration is unlikely to work without additional hacks in /etc/iproute2/rt_tables'
		exit
	else
		local nexthop=${inet_default_nexthop[0]}
		icmp_external_target=$(dig +short $ICMP_EXTERNAL_TARGET_NAME A)
		if fping $icmp_external_target >/dev/null; then
			echodebug "ICMP connectivity via $nexthop to $icmp_external_target is OK"
		else
			fail warning "next-hop router $nexthop fails to route ICMP to $icmp_external_target"
			exit
		fi
	fi
}

validate_loopback
declare -i IPV6_STACK_OK=0
if is_ipv6_enabled && is_ipv6_active; then
	validate_inet6_default_nexthop
	validate_deprecated_address
	validate_6in4_tunnel_address
	echodebug 'IPv6 stack validated'
	IPV6_STACK_OK=1
fi
if is_ipv4_active; then
	validate_inet_default_nexthop
	echodebug 'IPv4 stack validated'
fi
ok
