#!/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$ DNS enumeration of all inet/inet6 addresses currently assigned to this node
#$check$ hostname is sane; all addresses have valid reverse/forward entries in DNS (or at least "getent hosts")
#$ref$ /etc/bind/zones/*.{in-addr,ip6}.arpa (or /etc/hosts)
#$author$ Rafal Rzeczkowski
#$version$ 0.8.6

level_check long

#CHANGELOG
#0.50	initial
#0.51	do not fail if there are no IPv4 or IPv6 address to process
#0.52	ignore IPv4 addresses on DHCP-configured systems
#0.53	use GNU C Library resolve functions to support /etc/hosts
#0.54	more resilient parsing of 'ip address show' output
#0.55	ignore comments in the /etc/network/interfaces file
#0.56	exclude 6to4 IPv6 local address
#0.57	check own hostname (needed for sudo, BackupPC backups)
#0.58	skip HE IPv6 tunnel endpoint addresses
#0.59	skip OpenVPN IPv4 tunnel endpoint addresses
#0.60	issue query twice as a workaround for high-latency connections
#0.61	undo 0.60 (no help)
#0.62	add helpmsg
#0.63	skip checking of HE IPv6 addresses in RFC1918 systems
#0.64	skip broken TekSavvy IPv6 (with DHCPv6)
#0.65	added hostname checks
#0.66	added metadata headers
#0.67	unified address skip checks
#0.68	NAT64 exclusions
#0.69	tighter match for clearcable.net customer codes
#0.70	PPTP VPN exclusions
#0.71	remove warning about zz.clearcable.net hostnames
#0.72	restyle according to https://kb.clearcable.ca/KB/ProgrammingStyleStandards
#0.8.0	support enumeration of alternate network namespaces
#0.8.1	skip checking of TEST-NET-1 addresses
#0.8.2	detect the RFC1918 environment via the BCFG2_GROUPS API
#0.8.3	skip IPv4 addresses on Point-to-Point Links
#0.8.4	generalize DHCPv4 client detection
#0.8.5	skip first OpenVPN tunnel interface
#0.8.6	skip TEST-NET-2, TEST-NET-3, and "IPv6 to IPv4 relay" blocks
#0.9.0	reduce severity of check to caution except on mailX VMs

if [[ -z "$HOSTNAME" ]]; then
	fail warning 'no hostname set'
	helpmsg 'ensure that /etc/hostname contains the system FQDN'
	exit
elif ! [[ "$HOSTNAME" =~ ^[a-z0-9.-]{,255}$ ]]; then
	fail warning "entire hostname {$HOSTNAME} syntax is invalid"
	helpmsg 'refer to RFC 952 and RFC 1123'
	exit
elif ! [[ "$HOSTNAME." =~ ^(([a-z][a-z0-9-]*|[0-9][a-z0-9-]*[a-z]+[a-z0-9-]*)[.]){2,}$ ]]; then
	fail warning 'hostname label syntax is invalid'
	helpmsg 'refer to RFC 952 and RFC 1123'
	exit
else
	echodebug "HOST name $HOSTNAME passes basic syntax checks"
fi

if getent hosts "$HOSTNAME" >/dev/null; then
	echodebug "HOST name $HOSTNAME found in DNS"
else
	fail warning "unable to resolve local hostname {$HOSTNAME}"
	helpmsg "add A and AAAA records for $HOSTNAME to DNS"
	exit
fi

DHCPv4=0
if [[ -d '/etc/systemd/network' ]]; then
	if grep --quiet --extended-regexp --recursive --regexp='DHCP=(yes|ipv4)' '/etc/systemd/network'; then
		DHCPv4=1
	fi
elif [[ -f '/etc/network/interfaces' ]]; then
	if grep --quiet --extended-regexp '^[^#]+ inet dhcp[[:space:]]*' '/etc/network/interfaces'; then
		DHCPv4=1
	fi
fi

declare -a IP_ADDRESSES=()
if [[ $DHCPv4 -eq 0 ]]; then
	IP_ADDRESSES=($(ip -oneline -family inet address show|
		awk '{print $4}'))
else
	echodebug 'DHCPv4 detected: ignoring IPv4 addresses'
fi

IP_ADDRESSES+=($(ip -oneline -family inet6 address show|
	awk '{print $4}'))

if [[ -s /etc/tayga.conf ]]; then
	TAYGA_LAN=$(awk '{if ($1=="ipv4-addr"){print $2}}' /etc/tayga.conf)
else
	TAYGA_LAN=''
fi

if [[ -f /usr/local/etc/captive-portal.conf ]]; then
	FENCE_LAN=$(awk -F'=' '{if ($1=="CAPTIVEPORTAL_LAN_ADDR"){print $2}}' /usr/local/etc/captive-portal.conf | tr -d '"')
else
	FENCE_LAN=''
fi

if IFS=$'\n' NAMESPACES=($(ip netns)); then
	for ((i=0; i<${#NAMESPACES[@]}; i++)); do
		NETNS=${NAMESPACES[$i]}
		NETNS=${NETNS%% *}
		IP_ADDRESSES+=($(ip netns exec $NETNS ip -oneline address show |
			awk '{print $4}'))
	done
fi

PPTP_VPN_WAN=''
for PID in $(pgrep pppd); do
	while read -r -d $'\0' CMD; do
		PROVIDER=$CMD;
	done < "/proc/$PID/cmdline"
	UNIT=$(awk '{if ($0=="require-mppe-128"){VPN=1}
	if (VPN&&$1=="unit"){print $2}}' "/etc/ppp/peers/$PROVIDER")
	if [[ -n "$UNIT" ]]; then
		PPTP_VPN_WAN=$(ip -oneline -family inet address show dev "ppp$UNIT"|
			awk '{sub("/[0-9]+","",$4);print $4}')
	fi
done

OPENVPN_WAN=''
if [[ -d '/etc/openvpn' ]]; then
	UNIT=0
	OPENVPN_WAN=$(ip -oneline -family inet address show dev "tun$UNIT"|awk '{print $4}')
fi

if [[ -s /etc/dnsmasq.conf ]]; then
	DNSMASQ_ROUTER=$(awk -F'=option:router,' '{if ($1=="#$dhcp-option"){print $2}}' /etc/dnsmasq.conf)
else
	DNSMASQ_ROUTER=''
fi

for A in ${IP_ADDRESSES[@]}; do
	case $A in
		*.*) ATYPE=4;;
		*:*) ATYPE=6;;
	esac
	SKIP_MESSAGE="IPv$ATYPE addr $A skipped"
	case $A in
		127.*|::1/128)
			echodebug "$SKIP_MESSAGE (loopback)"
			continue
			;;
		fe80::*)
			echodebug "$SKIP_MESSAGE (link-local)"
			continue
			;;
		64:ff9b::*)
			echodebug "$SKIP_MESSAGE (RFC6052 Well-Known Prefix)"
			continue
			;;
		$TAYGA_LAN/*)
			echodebug "$SKIP_MESSAGE (NAT64/Tayga LAN)"
			continue
			;;
		$FENCE_LAN/*)
			echodebug "$SKIP_MESSAGE (Captive Portal LAN)"
			continue
			;;
		$PPTP_VPN_WAN)
			echodebug "$SKIP_MESSAGE (PPTP VPN endpoint)"
			continue
			;;
		$OPENVPN_WAN)
			echodebug "$SKIP_MESSAGE (OpenVPN VPN endpoint)"
			continue
			;;
		2001:470:*:?)
			# HE tunnel address sometimes fails to resolve - beyond our control
			echodebug "$SKIP_MESSAGE (HE tunnel endpoint)"
			continue
			;;
		2001:470:*)
			# HE regular address - ignore when on RFC1918
			if is_element_of 'RFC1918' ${BCFG2_GROUPS[@]}; then
				echodebug "$SKIP_MESSAGE (HE address in RFC1918 environment)"
				continue
			fi
			;;
		2607:f2c0:*)
			# TekSavvy with DHCPv6 is broken, e.g.:
			# 2607:f2c0:f00e:c901:20d:b9ff:fe19:a6f4 != node-77b0sibj81rvaa4d52s.ipv6.teksavvy.com.
			echodebug "$SKIP_MESSAGE (TekSavvy broken DHCPv6 address)"
			continue
			;;
		198.1[89].*)
			# OpenVPN tunnel address not tracked in DNS
			echodebug "$SKIP_MESSAGE (OpenVPN tunnel endpoint)"
			continue
			;;
		192.0.2.*)
			# TEST-NET-1: documentation and examples
			echodebug "$SKIP_MESSAGE (TEST-NET-1)"
			continue
			;;
		198.51.100.*)
			# TEST-NET-2: documentation and examples
			echodebug "$SKIP_MESSAGE (TEST-NET-2)"
			continue
			;;
		203.0.113.*)
			# TEST-NET-3: documentation and examples
			echodebug "$SKIP_MESSAGE (TEST-NET-3)"
			continue
			;;
		192.88.99.*)
			# Formerly used for IPv6 to IPv4 relay
			echodebug "$SKIP_MESSAGE (IPv6 to IPv4 relay)"
			continue
			;;
		192.168.*/3?)
			# RFC3021: Using 31-Bit Prefixes on IPv4 Point-to-Point Links
			echodebug "$SKIP_MESSAGE (IPv4 Point-to-Point Link)"
			continue
			;;
		$DNSMASQ_ROUTER/*)
			echodebug "$SKIP_MESSAGE (dnsmasq NAT gateway)"
			continue
			;;
	esac

	A=${A%/*} # remove subnet
	if getent hosts "$A" >/dev/null; then
		HOSTNAME=$(getent hosts "$A" | awk '{print $2}')
		ADDRESS=$(getent ahostsv$ATYPE "$HOSTNAME" | awk '{print $1;exit}')
		if [[ "$ADDRESS" = "$A" ]]; then
			echodebug "IPv$ATYPE addr $A resolves OK in DNS."
		else
			case "$BCFG2_PARCEL" in
				mail)
					fail critical "DNS forward/reverse mismatch for $A"
				;;
				*)
					fail caution "DNS forward/reverse mismatch for $A"
				;;
			esac
			helpmsg "ensure that $A resolves back to $HOSTNAME"
			exit
		fi
	else
		case "$BCFG2_PARCEL" in
			mail)
				fail critical "DNS reverse not found for $A"
			;;
			*)
				fail caution "DNS reverse not found for $A"
			;;
		esac
		helpmsg "ensure that there is a PTR record for $A"
		exit
	fi
done
ok
