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

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

#$title$ IPv6 address
#$check$ global-unicast IPv6 address matches VLAN designation
#$ref$ KB:IPv6AddressPlan
#$author$ Rafal Rzeczkowski
#$version$ 0.6.0

level_check long

#CHANGELOG
#0.5.0	initial
#0.5.1	exclude xen bridges connected directly to "switchport access vlan x"
#0.5.2	exclude RFC4193 Unique Local IPv6 Unicast Addresses from analysis
#0.5.2	exclude RFC4193 Unique Local IPv6 Unicast Addresses from analysis
#0.5.3	exclude Hurricane Electric tunnel broker prefix 2001:470::/32
#0.5.4	restyle according to https://kb.clearcable.ca/KB/ProgrammingStyleStandards
#0.5.5	ignore interfaces with a bad address plan outside of CCN control
#0.6.0	parse output from "bridge link show" instead of brctl to map VLANs to bridges

declare -r -i HOP_LIMIT_DEFAULT=64

#$ bridge link show
#7: vlan200@bond0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master xenbr0 state forwarding priority 32 cost 4

declare -A VLAN
bridge link show | while read -r id dev status mtu_keyword mtu master_keyword bridge_name state_keyword state junk; do
	if [[ $mtu_keyword = 'mtu' ]] && [[ $master_keyword = 'master' ]] &&
		[[ $state_keyword = 'state' ]]; then
		dev=${dev%:}
	else
		continue
	fi

	if [[ $dev = ${dev#vlan} ]]; then
		continue
	else
		vlan_id=${dev%@*}
		vlan_id=${vlan_id#vlan}
		VLAN[$bridge_name]=$vlan_id
	fi
done


#$ ip -oneline -family inet6 address show
#1: lo    inet6 ::1/128 scope host \       valid_lft forever preferred_lft forever
#2: eth0    inet6 fe80::214:4fff:fee7:58c2/64 scope link \       valid_lft forever preferred_lft forever
#8: xenbr22    inet6 2620:120:8000:16:214:4fff:fee7:58c2/64 scope global mngtmpaddr dynamic \       valid_lft 86389sec preferred_lft 14389sec
#8: xenbr22    inet6 fe80::214:4fff:fee7:58c2/64 scope link \       valid_lft forever preferred_lft forever
#9: xenbr23    inet6 fe80::214:4fff:fee7:58c2/64 scope link \       valid_lft forever preferred_lft forever

ok=0
ip -oneline -family inet6 address show | while read id dev family local_address scope_keyword scope; do
	if [[ "$local_address" =~ ^(fe80:|f[cd]|2001:470:) ]]; then
		# Link-local address
		# Hurricane Electric tunnel broker prefix
		# RFC4193 Unique Local IPv6 Unicast Addresses
		continue
	fi

	hop_limit=$(</proc/sys/net/ipv6/conf/$dev/hop_limit)
	if [[ $hop_limit -eq $HOP_LIMIT_DEFAULT ]]; then
		standard_address_plan=1
	else
		standard_address_plan=0
	fi
	INTERFACE=$dev
	INTERFACE_ID=$INTERFACE
	while [[ "$INTERFACE_ID" != "${INTERFACE_ID##[a-z-]}" ]]; do
		INTERFACE_ID="${INTERFACE_ID##[a-z-]}"
	done
	INTERFACE_NAME=${INTERFACE%%$INTERFACE_ID}
	INTERFACE_NAME=${INTERFACE_NAME#xen}
	if [[ "$INTERFACE_NAME" = 'br' || "$INTERFACE_NAME" = 'vlan' ]]; then
		case $INTERFACE_NAME in
			br)
				if [[ -z "${VLAN[$dev]+abc}" ]]; then
					continue
				fi
				VLAN_ID=${VLAN[$dev]}
				;;
			vlan)
				VLAN_ID=$INTERFACE_ID
				;;
		esac
		IFS='/' read -ra IFADDR <<< "$local_address"
		IFS=':' read -ra PREFIX <<< $IFADDR
		ADDR_VLAN=$(( 16#${PREFIX[3]} ))

		if [[ $ADDR_VLAN -eq $VLAN_ID ]]; then
			echodebug "$dev\t$VLAN_ID\t$local_address\tOK"
			ok=$(( ok + 1 ))
		else
			if [[ $standard_address_plan -eq 0 ]]; then
				echodebug "$dev\t$VLAN_ID\t$local_address\tBAD"
			else
				fail warning "$local_address does not belong on VLAN $VLAN_ID"
				helpmsg 'refer to KB for VLAN ID to IPv6 address mapping'
				exit
			fi
		fi
	fi
done

if [[ $ok -gt 0 ]]; then
	ok
else
	unknown
fi
