#!/bin/sh -e

# Dynamic Multipoint VPN setup script for Alpine Linux
# Copyright (c) 2017-2025 Kaarle Ritvanen
# See LICENSE file for license details


. /usr/lib/libalpine.sh

if [ -z "$1" ]; then
	echo "Usage: $0 <pfx_file>" >&2
	exit 1
fi


ATTRS=$(/usr/libexec/dmvpn-pfx-decode "$1")
eval $ATTRS

for attr in GRE_IPV4_ADDRESS HUBS VPNC_TYPE; do
	eval "[ \"\$$attr\" ]" || die "attribute not defined: $attr"
done


NFLOG_GROUP=
if [ $VPNC_TYPE = hub ]; then
	CONF_FILE=/etc/dmvpn.conf
	[ -e $CONF_FILE ] && . $CONF_FILE

	get_param() {
		eval "[ \"\$$1\" ]" && return
		resp=
		while ! expr "$resp" : '[1-9][0-9]\?[0-9]\?$' > /dev/null; do
			ask "$2" $3
		done
		eval $1=$resp
		echo $1=$resp >> $CONF_FILE
	}

	get_param NFLOG_GROUP "NFLOG group" 1
	get_param SITE_PREFIX_LEN_IPV4 "DMVPN site IPv4 prefix length" 16
	[ "$GRE_IPV6_ADDRESS" ] && \
		get_param SITE_PREFIX_LEN_IPV6 "DMVPN site IPv6 prefix length" \
			48
fi


GRE_MODULE=nf_conntrack_proto_gre
PMTU_SYSCTL=net.ipv4.ip_forward_use_pmtu


get_attr() {
	sed -E "s/^[^ ].* $1 ([^ ]+)( .*)?\$/\\1/;ta;d;:a"
}

get_local_routes() {
	ip route list table local $* | sed "s/^local //;ta;d;:a"
}

get_local_route_attr() {
	local attr=$1
	shift
	get_local_routes $* | get_attr $attr
}

append_to_iface_config() {
	cat >> /etc/network/interfaces
}

is_active() {
	rc-service $1 status > /dev/null
}

enable_service() {
	if is_active $1; then
		rc-service $1 restart
	else
		rc-update add $1
		rc-service $1 start
	fi
}

enable_firewall() {
	augtool -s <<EOF
set /files/etc/conf.d/$1/IPFORWARD yes
set /files/etc/conf.d/$1/SAVE_ON_STOP no
EOF
	enable_service $1
}

get_config_cmds() {
	local p=$1
	shift
	while [ $# -gt 0 ]; do
		[ "$1" ] && echo $p $1
		shift
	done
}

get_nhrp_config() {
	(
		IFS=$'\n'
		get_config_cmds "$1 nhrp" \
			"network-id 1" \
			shortcut \
			"registration no-unique" \
			"${NFLOG_GROUP:+redirect}" \
			$(get_config_cmds "nhs dynamic nbma" $HUBS)
	)
}

get_route_map_config() {
	cat <<EOF
	route-map RTT-$1 permit 10
		set metric $2rtt
	exit
EOF
}

get_prefix_list_config() {
	local af=$1
	local addr_len=$2
	local net_max_len=$3
	shift 3
	get_config_cmds "no $af prefix-list" dmvpn no-hosts
	local prefix
	for prefix; do
		cat <<EOF
		$af prefix-list dmvpn permit $prefix le $addr_len
		$af prefix-list no-hosts permit $prefix le $net_max_len
EOF
	done
}

get_af_config_cmds() {
	get_config_cmds "$@"
	if [ "$IPV6_PREFIXES" ]; then
		echo "address-family ipv6"
		get_config_cmds "$@"
		echo exit
	fi
}

get_peer_config() {
	local group=$1
	local prefixes=$2
	local map=RTT-$3
	local af_extra=$4
	shift 4
	get_config_cmds "neighbor $group" \
		peer-group \
		"ebgp-multihop 1" \
		disable-connected-check \
		"timers 10 30" \
		"$@"
	get_af_config_cmds "neighbor $group" \
		activate \
		"next-hop-self all" \
		"soft-reconfiguration inbound" \
		"prefix-list $prefixes out" \
		"route-map $map in" \
		${af_extra:+"$af_extra"}
}

get_spoke_config() {
	local group=spoke-$1
	local af_extra=$2
	shift 2
	get_peer_config $group no-hosts SET "$af_extra" "$@" \
		passive \
		"advertisement-interval 1"
}

get_quagga_config() {
	cat <<EOF
	configure terminal
		nhrp event socket /var/run/nhrp-events.sock
		${NFLOG_GROUP:+nhrp nflog-group $NFLOG_GROUP}
		interface $GRE_IFACE
			tunnel protection vici profile dmvpn
EOF
	get_nhrp_config ip
	[ "$GRE_IPV6_ADDRESS" ] && get_nhrp_config ipv6
	cat <<EOF
		exit
		no router bgp $AS_NUMBER
		router bgp $AS_NUMBER
			bgp router-id $GRE_IPV4_ADDRESS
EOF
	get_config_cmds network $IPV4_PREFIXES
	if [ "$IPV6_PREFIXES" ]; then
		echo address-family ipv6
		get_config_cmds network $IPV6_PREFIXES
		echo exit
	fi
	echo exit
	if [ $VPNC_TYPE = hub ]; then
		get_route_map_config ADD +
		get_route_map_config SET

		get_prefix_list_config ip 32 30 $IPV4_PREFIXES
		get_prefix_list_config ipv6 128 64 $IPV6_PREFIXES

		echo "router bgp $AS_NUMBER"
		get_af_config_cmds redistribute kernel nhrp
		get_peer_config hubs dmvpn ADD "" \
			"remote-as $AS_NUMBER" \
			"timers connect 10"
		get_spoke_config ebgp "attribute-unchanged med"
		get_spoke_config ibgp route-reflector-client \
			"remote-as $AS_NUMBER"
		echo exit
	fi
	cat <<EOF
	exit
	write memory
EOF
}


GRE_IFACE=$(get_local_route_attr dev $GRE_IPV4_ADDRESS)

if [ -z "$GRE_IFACE" ]; then
	HUB_ROUTE=$(for h in $HUBS; do
		if expr ${h##*.} : '[a-zA-Z][a-zA-Z0-9]*$' \
			> /dev/null; then

			host $h | sed -E 's/^.+ has address //;ta;d;:a'
		else
			echo $h
		fi
	done | while read addr; do
		if [ -z "$(get_local_routes $addr)" ]; then
			ip route get $addr
			break
		fi
	done)

	ask_which interface "shall be used for GRE transport" \
		"$(ls /sys/class/net)" $(echo "$HUB_ROUTE" | get_attr dev)
	[ "$resp" = done ] && \
		die "Cannot proceed without GRE interface $GRE_IPV4_ADDRESS"
	TRANSPORT_IFACE=$resp

	TRANSPORT_ADDR=
	TRANSPORT_IFACE_ADDRS=$(get_local_route_attr src \
		dev $TRANSPORT_IFACE | sort -u)
	if [ $(echo "$TRANSPORT_IFACE_ADDRS" | wc -l) -gt 1 ]; then
		ask_which "IP address" "shall be used for GRE transport"\
			"$TRANSPORT_IFACE_ADDRS" \
			$(echo "$HUB_ROUTE" | get_attr src)
		[ "$resp" = done ] || TRANSPORT_ADDR=$resp
	fi

	i=1
	while [ -d /sys/class/net/gre$i ]; do
		: $(( i++ ))
	done

	ask "GRE tunnel interface" gre$i
	GRE_IFACE=$resp

	echo "$PMTU_SYSCTL = 1" > /etc/sysctl.d/dmvpn.conf
	sysctl -w $PMTU_SYSCTL=1

	append_to_iface_config <<EOF

auto $GRE_IFACE
iface $GRE_IFACE inet static
	address $GRE_IPV4_ADDRESS
	netmask 255.255.255.255
	tunnel-mode gre
	tunnel-dev $TRANSPORT_IFACE
	tunnel-ttl 64
EOF
	if [ "$TRANSPORT_ADDR" ]; then
		append_to_iface_config <<EOF
	tunnel-local $TRANSPORT_ADDR
EOF
	fi

	if [ "$GRE_IPV6_ADDRESS" ]; then
		append_to_iface_config <<EOF
iface $GRE_IFACE inet6 static
	address $GRE_IPV6_ADDRESS
	netmask 128
EOF
	fi

	ifup $GRE_IFACE
fi


augtool -s "set /files/etc/conf.d/nhrpd/rc_need '\"charon nhrp-events\"'"

for serv in charon zebra; do
	is_active $serv && rc-service $serv stop
done

for serv in bgpd nhrpd zebra; do
	file=/etc/quagga/$serv.conf
	touch $file
	chown quagga $file
done

enable_service nhrpd

vtysh -c "$(get_quagga_config)"


if [ -f /etc/iptables/awall-save -o "$NFLOG_GROUP" ]; then
	apk add awall
	echo "{ \"variable\": { \"dmvpn_gre_iface\": \"$GRE_IFACE\" } }" > \
		/etc/awall/dmvpn-config.json

	if [ "$NFLOG_GROUP" ]; then
		cat > /etc/nhrp-events.conf <<EOF
max-prefix-length:
  ip: $SITE_PREFIX_LEN_IPV4
EOF
		[ "$SITE_PREFIX_LEN_IPV6" ] && \
			cat >> /etc/nhrp-events.conf <<EOF
  ipv6: $SITE_PREFIX_LEN_IPV6
EOF

		(
			PREFIX="set /files/etc/awall/dmvpn-config.json/dict/entry/dict/entry"
			cat <<EOF
$PREFIX[2] dmvpn_nflog_group
$PREFIX[2]/number $NFLOG_GROUP
$PREFIX[3] dmvpn_site_mask
$PREFIX[3]/dict/entry inet
$PREFIX[3]/dict/entry/number $SITE_PREFIX_LEN_IPV4
EOF
			[ "$SITE_PREFIX_LEN_IPV6" ] && cat <<EOF
$PREFIX[3]/dict/entry[2] inet6
$PREFIX[3]/dict/entry[2]/number $SITE_PREFIX_LEN_IPV6
EOF
		) | augtool -s
		awall enable dmvpn-hub
	else
		awall enable dmvpn
	fi

	awall translate

	if modprobe $GRE_MODULE 2>/dev/null; then
		echo $GRE_MODULE > /etc/modules-load.d/dmvpn.conf
	fi

	enable_firewall iptables
	[ -f /etc/iptables/rules6-save -o "$SITE_PREFIX_LEN_IPV6" ] && \
		enable_firewall ip6tables
fi
