#!/bin/sh

# Copyright 2015-2024 Rivoreo

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


PROGRAM_DIR="${0%/*}"
[ "$PROGRAM_DIR" = "$0" ] && PROGRAM_DIR="$PWD"
RNCN_STATE_FILE="$PROGRAM_DIR/.nodename"
RNCN_UNPRIVILEGED_USER=rncn-noaccess
GEOIP_MMDB_URL=https://cdn.libz.so/gh/JohnnySun/geoip/release/Country.mmdb
DEFAULT_FALLBACK_NAME_SERVER=8.8.4.4

# Override Debian default PATH
PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin
[ -h /bin ] || PATH="$PATH:/bin"
[ -h /sbin ] || PATH="$PATH:/sbin"
export PATH="$PATH:/usr/games"

parse_key_value() {
	case "$1" in
		*=*)
			key="${1%%=*}"
			key="${key%%[ 	]*}"
			value="${1#*=}"
			local trim_value
			while trim_value="${value#[ 	]}" && [ "$trim_value" != "$value" ]; do value="$trim_value"; done
			;;
		*\ *|*\	*)
			key="${1%%[ 	]*}"
			value="${1#*[ 	]}"
			;;
		*)
			false
			;;
	esac
}

parse_boolean() {
	local value
	eval "value=\"\$$1\""
	case "$value" in
		[Oo][Ff][Ff]|[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|0|"")
			eval "$1="
			;;
		[Oo][Nn]|[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|1)
			eval "$1=1"
			;;
		*)
			false
			;;
	esac
}

print_peer_in_tunnel_address() {
	local ORIG_IFS="$IFS"
	IFS=.
	set -- $1
	IFS="$ORIG_IFS"
	[ $# = 4 ] || return
	printf "%s.%s.%s." $1 $2 $3
	echo $(($4+($4%2?-1:1)))
}

print_peer_in_tunnel_inet6_address() {
	local last_number="${1##*:}"
	if [ -n "$last_number" ]; then
		last_number="0x$last_number"
	else
		last_number=0
	fi
	printf '%s:%x\n' "${1%:*}" $((last_number+(last_number%2?-1:1)))
}

# Usage: load_config <config-dir>
load_config() {
	node_address=
	set_hostname=
	ipv4_default_router=
	ipv6_default_router=
	run_dns_server=
	replace_sysctl_conf=
	replace_resolv_conf=
	fallback_name_server=
	dns_config_update_url=
	wg_private_key=
	peers=
	ra_list=
	ipv4_ra_exclusion=
	ipv6_ra_exclusion=
	local_wan_ipv4_route_targets=
	local_wan_ipv6_route_targets=
	local section=
	local current_peer= current_peer_ip6addr_index
	local key value line nlines=0
	while read -r line; do
		nlines=$((nlines+1))
		case "$line" in
			""|\;*|\#*)
				continue
				;;
			\[peer\ *\]*)
				section=peer
				peer_name="${line#\[peer }"
				peer_name="${peer_name%%\]*}"
				current_peer="`printf %s \"$peer_name\" | sed -E 's/[.-]/_/g'`"
				peers="$peers $current_peer"
				eval "peer_${current_peer}_name=\"\$peer_name\""
				eval "peer_${current_peer}_public_key="
				eval "peer_${current_peer}_bind_port="
				eval "peer_${current_peer}_remote="
				eval "peer_${current_peer}_mtu="
				eval "peer_${current_peer}_ip4_addr="
				eval "peer_${current_peer}_ip6_addr_0="
				current_peer_ip6addr_index=0
				continue
				;;
			\[*\]*)
				section="${line%%\]*}"
				section="${section#\[}"
				current_peer=
				continue
				;;
		esac
		case "$section" in
			""|peer)
				if ! parse_key_value "$line"; then
					printf "Parsing error at line %s, missing value?\\n" $nlines 1>&2
					#continue
					return 1
				fi
				case "$section.$key" in
					.node_address|.ipv4_default_router|.ipv6_default_router|.wg_private_key|.fallback_name_server|.dns_config_update_url)
						eval "$key=\"\$value\""
						;;
					.set_hostname|.run_dns_server|.replace_sysctl_conf|.replace_resolv_conf)
						eval "$key=\"\$value\""
						if ! parse_boolean "$key"; then
							printf "Expect a boolean value for %s at line %s, got unrecognizable '%s'\\n" "$key" $nlines "$value" 1>&2
							return 1
						fi
						;;
					peer.public_key|peer.bind_port|peer.remote|peer.mtu)
						eval "peer_${current_peer}_$key=\"\$value\""
						;;
					peer.ip4_addr)
						eval "peer_${current_peer}_$key=\"\$value\""
						if ! value="`print_peer_in_tunnel_address \"$value\"`"; then
							printf "Invalid IPv4 address at line %s\\n" $nlines 1>&2
							return 1
						fi
						eval "peer_${current_peer}_remote_ip4_addr=\"\$value\""
						;;
					peer.ip6_addr)
						eval "peer_${current_peer}_ip6_addr_$current_peer_ip6addr_index=\"\$value\""
						current_peer_ip6addr_index=$((current_peer_ip6addr_index+1))
						;;
				esac
				;;
			ra-list)
				ra_list="$ra_list$line
"
				;;
			ipv4-ra-exclusion)
				ipv4_ra_exclusion="$ipv4_ra_exclusion$line
"
				;;
			ipv6-ra-exclusion)
				ipv6_ra_exclusion="$ipv6_ra_exclusion$line
"
				;;
			wan-ipv4-route-targets)
				local_wan_ipv4_route_targets="$local_wan_ipv4_route_targets$line
"
				;;
			wan-ipv6-route-targets)
				local_wan_ipv6_route_targets="$local_wan_ipv6_route_targets$line
"
				;;
		esac
	done < "$1/main.cfg"
	for key in node_address ipv4_default_router wg_private_key; do
		eval "[ -n \"\$$key\" ]" && continue
		printf "Missing %s\\n" "$key" 1>&2
		return 1
	done
	[ -z "$peers" ] && echo "Warning: no tunnel peer defined" 1>&2
	if [ -n "$ra_list" ]; then
		bird_conf_include_ipv4_ra='	include "ipv4-ra";'
		bird_conf_include_ipv6_ra='	include "ipv6-ra";'
	else
		bird_conf_include_ipv4_ra=
		bird_conf_include_ipv6_ra=
	fi
	for f in "$1/clips"/*; do
		[ -f "$f" ] || continue
		eval "clip_${f##*/}=\"\`cat \\\"\$f\\\"\`\""
	done
}

get_default_route_interface() {
	local line
	# Skip header
	read -r line
	while read -r line; do
		set -- $line
		if [ "$2" = 00000000 ]; then
			default_route_interface="$1"
			return
		fi
	done
	false
} < /proc/net/route

modify_etc_profile() {
	if [ -h /etc/profile ]; then
		echo "/etc/profile is a symbolic link, this is unexpected" 1>&2
		return 1
	fi
	if [ ! -f /etc/profile ]; then
		echo "/etc/profile didn't exist as a regular file, this is unexpected" 1>&2
		return 1
	fi
	grep --fixed-strings --line-regexp --quiet -- "export SYSTEMD_PAGER=" /etc/profile && return
	make_sure_nonempty_file_ends_with_new_line /etc/profile
	local new_profile IFS= line if_statement=
	new_profile="`mktemp`" || return
	while read -r line; do case "$line" in
		"if [ "*" -eq 0 ]; then")
			if_statement="$line"
			;;
		"  PATH="*)
			if [ -z "$if_statement" ]; then
				echo "Warning: Unexpected format in /etc/profile"
				rm -f "$new_profile"
				append_unique_line "export PATH=$PATH" /etc/profile
				append_unique_line "export SYSTEMD_PAGER=" /etc/profile
				append_unique_line "export SYSTEMD_URLIFY=off" /etc/profile
				return
			fi
			;;
		else)
			[ -z "$if_statement" ] && echo else
			;;
		fi)
			if [ -n "$if_statement" ]; then
				if_statement=
				cat << EOT
export PATH=$PATH
export SYSTEMD_PAGER=
export SYSTEMD_URLIFY=off

if [ "\${LANG%%.*}" = zh_CN ]; then
	# Workaround for bug 939445
	export LANG="zh_TW\${LANG#zh_CN}"
	[ -z "\$LANGUAGE" ] && export LANGUAGE=zh_CN:zh_TW:en_US:en_GB
elif [ -z "\$LANG" ]; then case "\$TERM" in
	linux)
		export LANG=C.UTF-8
		;;
	*)
		#export LANG=zh_TW.UTF-8
		export LANG=en_GB.UTF-8
		;;
esac fi
EOT
			else
				echo fi
			fi
			;;
		"export PATH")
			;;
		*)
			[ -n "$if_statement" ] && printf %s\\n "$if_statement"
			printf %s\\n "$line"
			;;
	esac done < /etc/profile > "$new_profile"
	cat "$new_profile" > /etc/profile
	rm -f "$new_profile"
}

create_rncn_unprivileged_user() {
	adduser --system --gecos "Unprivileged user for running RNCN tasks" --no-create-home "$RNCN_UNPRIVILEGED_USER" && return
	adduser --system --comment "Unprivileged user for running RNCN tasks" --no-create-home "$RNCN_UNPRIVILEGED_USER"
}

# Silence log messages from pam_unix.so for sudo(1) to $RNCN_UNPRIVILEGED_USER
configure_sudo_pam() {
	[ -f /etc/pam.d/sudo ] || return 0
	local IFS=
	local pass=
	rm -f /etc/pam.d/sudo.new
	while read -r line; do
		case "$line" in
			@include*)
				[ -z "$pass" ] && printf 'session [success=done default=ignore] pam_succeed_if.so quiet ruser = root user = %s\n' "$RNCN_UNPRIVILEGED_USER"
				pass=1
				;;
			*/limits.conf*)
				continue
				;;
			session*pam_limits.so*)
				continue
				;;
			session*pam_env.so*envfile=/etc/default/locale*)
				continue
				;;
		esac
		printf %s\\n "$line"
	done < /etc/pam.d/sudo > /etc/pam.d/sudo.new
	mv -f /etc/pam.d/sudo.new /etc/pam.d/sudo
}

configure_sudoers() {
	local sudoers=/etc/sudoers new_sudoers
	append_or_set_words() {
		if [ -n "$new_value" ]; then
			eval "$1="
			append_value="$new_value"
		elif [ -z "$append_value" ]; then
			return
		fi
		while new_value="${append_value#[ 	]}" && [ "$new_value" != "$append_value" ]
		do append_value="$new_value"
		done
		while new_value="${append_value%[ 	]}" && [ "$new_value" != "$append_value" ]
		do append_value="$new_value"
		done
		append_value="${append_value#\"}"
		append_value="${append_value%\"}"
		eval "$1=\"\$$1
\$append_value\""
	}
	new_sudoers="`mktemp`"
	trap 'rm -f "$new_sudoers"' EXIT
	chmod 600 "$new_sudoers"
	local payload="Defaults	!set_utmp"
	printf %s\\n "$payload" | while read -r line; do
		grep --fixed-strings --line-regexp --quiet "$line" "$sudoers" || exit
	done && payload=
	local i=0
	local last_defaults_line=
	local has_env_reset=
	local has_always_set_home=
	local env_keep=
	local line
	while read -r line; do
		i=$((i+1))
		case "$line" in
			Defaults[\ \	]*)
				append_value=
				new_value=
				last_defaults_line=$i
				option_name="${line#Defaults[ 	]}"
				option_name=${option_name#${option_name%%[\!a-z]*}}
				if [ "${option_name%%+=*}" != "$option_name" ]; then
					append_value="${option_name#*+=}"
					option_name=${option_name%%+=*}
				elif [ "${option_name%%=*}" != "$option_name" ]; then
					new_value="${option_name#*=}"
					option_name=${option_name%%=*}
				fi
				option_name=${option_name%${option_name##*[a-z]}}
				case $option_name in
					env_reset|always_set_home|use_pty)
						eval "has_$option_name=$i"
						;;
					!env_reset|!always_set_home|!use_pty)
						eval "has_${option_name#!}="
						;;
					env_keep)
						append_or_set_words env_keep
						;;
				esac
				;;
		esac
	done < "$sudoers"
	for i in env_reset always_set_home; do
		eval "value=\$has_$i"
		[ -n "$value" ] && continue
		echo "Defaults $i" | visudo -cq -f - || continue
		payload="Defaults	$i
$payload"
	done
	local e add_env_keep=
	for e in SYSTEMD_PAGER SYSTEMD_URLIFY SSH_CLIENT SSH_CONNECTION; do
		for k in $env_keep; do [ $k = $e ] && continue 2; done
		[ -n "$add_env_keep" ] && add_env_keep="$add_env_keep $e" || add_env_keep="$e"
	done
	[ -n "$add_env_keep" ] && payload="$payload
Defaults	env_keep += \"$add_env_keep\""
	[ -n "$has_use_pty" ] && payload="$payload
Defaults	!use_pty"
	if [ -n "$last_defaults_line" ]; then
		i=0
		while read -r line; do
			i=$((i+1))
			[ -n "$has_use_pty" ] && [ $i = $has_use_pty ] && printf \#
			printf %s\\n "$line"
			if [ $i = $last_defaults_line ]; then
				echo
				printf %s\\n "$payload"
			fi
		done
	else
		cat
		printf %s\\n "$payload"
	fi < "$sudoers" > "$new_sudoers"
	if ! visudo -c -f "$new_sudoers"; then
		echo "Error configuring sudoers" 1>&2
		rm -f "$new_sudoers"
		return 1
	fi
	cat "$new_sudoers" > "$sudoers"
	rm -f "$new_sudoers"
	trap - EXIT
}

configure_lookback_interface() {
	grep -Eq "^iface lo:.+ inet static" /etc/network/interfaces && return
	cat >> /etc/network/interfaces << EOT
auto lo:rncn
iface lo:rncn inet static
	address $node_address/32
EOT
	ifup lo:rncn || true
	[ -z "$run_dns_server" ] && return
	cat >> /etc/network/interfaces << EOT
auto lo:dns1
iface lo:dns1 inet static
	address 10.10.10.10/32
auto lo:dns2
iface lo:dns2 inet static
	address 10.0.0.10/32
EOT
}

configure_iptables() {
	[ -f /etc/systemd/system/iptables.service ] || cp "$PROGRAM_DIR/systemd-units/iptables.service" /etc/systemd/system/ || return
	[ -d /etc/iptables ] || mkdir /etc/iptables || return
	[ -f /etc/iptables/iptables.rules ] || cat > /etc/iptables/iptables.rules << EOT
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A OUTPUT -o $default_route_interface -d 10.10.10.10 -p tcp -m udp --dport 53 -j DNAT --to-destination ${fallback_name_server:-$DEFAULT_FALLBACK_NAME_SERVER}
-A POSTROUTING -o $default_route_interface -s 10.100.0.0/16 -j MASQUERADE
-A POSTROUTING -o CloudflareWARP -s 10.100.0.0/16 -j MASQUERADE
COMMIT
EOT
}

make_sure_nonempty_file_ends_with_new_line() {
	local len
	len=`wc -c < "$1"` || return
	[ "$len" -lt 1 ] && return
	[ "`dd bs=1 \"if=$1\" skip=$((len-1)) 2> /dev/null | wc -l`" -eq 1 ] && return
	echo >> "$1"
}

append_unique_line() {
	grep --fixed-strings --line-regexp --quiet -- "$1" "$2" && return
	printf %s\\n "$1" >> "$2"
}

configure_locales() {
	[ -h /etc/default/locale ] || rm -f /etc/default/locale
	grep --fixed-strings --line-regexp --quiet zh_TW.GB18030 /etc/locale.gen && return
	sed -i -E 's/^# +((en_GB|zh_CN|zh_TW)\.UTF\-8|zh_CN\.GB18030) /\1 /' /etc/locale.gen
	make_sure_nonempty_file_ends_with_new_line /etc/locale.gen
	append_unique_line "en_GB.UTF-8 UTF-8" /etc/locale.gen
	append_unique_line "zh_CN.UTF-8 UTF-8" /etc/locale.gen
	append_unique_line "zh_TW.UTF-8 UTF-8" /etc/locale.gen
	append_unique_line "zh_CN.GB18030 GB18030" /etc/locale.gen
	append_unique_line "zh_TW.GB18030 GB18030" /etc/locale.gen
	locale-gen || true
}

install_and_apply_sysctl_config() {
	[ -z "$replace_sysctl_conf" ] && return
	cat > /etc/sysctl.conf << EOT
###################################################################
# Functions previously found in netbase
#

# Uncomment the next two lines to enable Spoof protection (reverse-path filter)
# Turn on Source Address Verification in all interfaces to
# prevent some spoofing attacks
#net.ipv4.conf.default.rp_filter=1
#net.ipv4.conf.all.rp_filter=1

# Uncomment the next line to enable TCP/IP SYN cookies
# See http://lwn.net/Articles/277146/
# Note: This may impact IPv6 TCP sessions too
#net.ipv4.tcp_syncookies=1

# Uncomment the next line to enable packet forwarding for IPv4
net.ipv4.ip_forward=1

# Uncomment the next line to enable packet forwarding for IPv6
#  Enabling this option disables Stateless Address Autoconfiguration
#  based on Router Advertisements for this host
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.default.forwarding = 1

###################################################################
# Additional settings - these settings can improve the network
# security of the host and prevent against some network attacks
# including spoofing attacks and man in the middle attacks through
# redirection. Some network environments, however, require that these
# settings are disabled so review and enable them as needed.
#
# Do not accept ICMP redirects (prevent MITM attacks)
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# _or_
# Accept ICMP redirects only for gateways listed in our default
# gateway list (enabled by default)
# net.ipv4.conf.all.secure_redirects = 1
#
# Do not send ICMP redirects (we are not a router)
net.ipv4.conf.all.send_redirects = 0
#
# Do not accept IP source route packets (we are not a router)
#net.ipv4.conf.all.accept_source_route = 0
#net.ipv6.conf.all.accept_source_route = 0
#
# Log Martian Packets
#net.ipv4.conf.all.log_martians = 1
#

###################################################################
# Magic system request Key
# 0=disable, 1=enable all, >1 bitmask of sysrq functions
# See https://www.kernel.org/doc/html/latest/admin-guide/sysrq.html
# for what other values do
#kernel.sysrq=438

net.core.default_qdisc = fq
net.core.rmem_max = 67108848
net.core.wmem_max = 67108848
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_rmem = 16384 16777216 536870912
net.ipv4.tcp_wmem = 16384 16777216 536870912
net.ipv4.tcp_adv_win_scale = -2
net.ipv4.tcp_sack = 1
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_fastopen = 3
net.ipv6.conf.all.accept_ra=2
EOT
	sysctl -p /etc/sysctl.conf || true
}

write_bird_conf_template() {
	cat << EOT
log syslog all;
#log "/var/log/bird.log" { debug, trace, info, remote, warning, error, auth, fatal, bug };

router id $node_address;

protocol device {
}

protocol direct {
	ipv4;
	ipv6;
	interface "rncn-*";
	interface "lo" 10.100.254.0/24;
$clip_bird_direct_interfaces
}

# The Kernel protocol is not a real routing protocol. Instead of communicating
# with other routers in the network, it performs synchronization of BIRD
# routing tables with the OS kernel. One instance per table.
# This doesn't include on-link routes by default, use 'direct' protocol for
# that.
protocol kernel {
	ipv4 {
	      import all;
	      export all;
	};
	# Learn alien routes from the kernel, excluding kernel maintained
	# entries.
	learn;
}

# Another instance for IPv6, skipping default options
protocol kernel {
	ipv6 { export all; };
}

# Static routes (Again, there can be multiple instances, for different address
# families and to disable/enable various groups of static routes on the fly).
protocol static {
	ipv4;			# Again, IPv4 channel with default options

$bird_conf_include_ipv4_ra
	# RNCN DNS
	route 10.10.10.10/32 via "lo";
	route 10.0.0.10/32 via "lo";
	# END OF RNCN DNS
EOT
	local target
	for target in $local_wan_ipv4_route_targets; do
		printf '	route %s via %s;\n' "$target" "$ipv4_default_router"
	done
	cat << EOT
}
EOT

	if [ -n "$ipv6_default_router" ]; then
		cat << EOT
protocol static {
	ipv6;
$bird_conf_include_ipv6_ra
EOT
		for target in $local_wan_ipv6_route_targets; do
			printf '	route %s via %s;\n' "$target" "$ipv6_default_router"
		done
		echo \}
	fi
	local v ip_vers peer interface
	for v in 2 3; do
		case $v in
			2) ip_vers=4; ;;
			3) ip_vers=6; ;;
		esac
		cat << EOT

protocol ospf v$v {
	ipv$ip_vers {
		export all;
		import all;
	};
	ecmp yes;
	area 0 {
EOT
		for peer in $peers; do
			eval "interface=\"rncn-\$peer_${peer}_name\""
			cat << EOT
		interface "$interface" {
			type pointopoint;
			authentication none;
			hello 5;
			wait 10;
			dead 40;
			cost 20; # ospf-v$v-backbone-interface-cost:$interface
		};
EOT
		done
		cat << EOT
	};
}
EOT
	done
} > /etc/bird/bird.conf.template

write_rncn_link_status_monitor_sh() {
	cat << EOT
#!/bin/sh

export LANG=C

EOT
	cat "$PROGRAM_DIR/src/rncn-link-status-monitor.calc_metric.sh"
	cat << EOT

set -e

while true; do
EOT
	local peer peer_in_tunnel_addr interface
	for peer in $peers; do
		#eval "my_in_tunnel_addr=\"\$peer_${peer}_ip4_addr\""
		#peer_in_tunnel_addr="`print_peer_in_tunnel_addr \"$my_in_tunnel_addr\"`"
		eval "peer_in_tunnel_addr=\"\$peer_${peer}_remote_ip4_addr\""
		printf '	calc_metric "%s" & ping_%s_pid=$!\n' "$peer_in_tunnel_addr" $peer
	done
	printf "	wait"
	for peer in $peers; do printf ' $ping_%s_pid' $peer; done
	echo
	for peer in $peers; do
		eval "peer_in_tunnel_addr=\"\$peer_${peer}_remote_ip4_addr\""
		printf '	%s_cost="`cat /tmp/cost.%s`" && rm -f /tmp/cost.%s || exit\n' $peer "$peer_in_tunnel_addr" "$peer_in_tunnel_addr"
	done
	cat << EOT
	if [ -f /etc/bird/bird.conf ]; then
		rm -f /etc/bird/bird.conf.old
		mv /etc/bird/bird.conf /etc/bird/bird.conf.old
	elif [ -e /etc/bird/bird.conf ]; then
		rm -f /etc/bird/bird.conf
	fi
	sed -E \\
EOT
	for peer in $peers; do
		eval "interface=\"rncn-\$peer_${peer}_name\""
		printf '		-e "s/	cost [0-9]+; (# ?ospf-v[23]-backbone-interface-cost:%s)$/	cost $%s_cost; \\\\1/" \\\n' $interface $peer
	done
	cat << EOT
		/etc/bird/bird.conf.template > /etc/bird/bird.conf
	birdc configure > /dev/null
done
EOT
} > /etc/bird/rncn-link-status-monitor.sh

write_ra_list() {
	[ -z "$ra_list" ] && return
	printf %s "$ra_list" > /etc/bird/ra.list
	[ -n "$ipv4_ra_exclusion" ] && printf %s "$ipv4_ra_exclusion" > /etc/bird/ipv4-ra-exclusion
	[ -n "$ipv6_ra_exclusion" ] && printf %s "$ipv6_ra_exclusion" > /etc/bird/ipv6-ra-exclusion
	true
}

write_ra_updater() {
	cat << EOT
#!/bin/sh

LOCAL_IP4_ROUTER='$ipv4_default_router'
LOCAL_IP6_ROUTER='$ipv6_default_router'
EOT
	cat "$PROGRAM_DIR/src/rncn-ra-updater.sh"
} > /etc/bird/rncn-ra-updater.sh

write_wireguard_configuration_files() {
	[ -d /etc/wireguard ] || mkdir /etc/wireguard || return
	local peer interface public_key bind_port remote mtu ip4_addr ip6_addr first_ip6_addr i
	for peer in $peers; do
		eval "interface=\"rncn-\$peer_${peer}_name\""
		eval "public_key=\"\$peer_${peer}_public_key\""
		eval "bind_port=\"\$peer_${peer}_bind_port\""
		eval "remote=\"\$peer_${peer}_remote\""
		eval "mtu=\"\$peer_${peer}_mtu\""
		eval "ip4_addr=\"\$peer_${peer}_ip4_addr\""
		rm -f "/etc/wireguard/$interface.conf"
		{
			cat << EOT
[Interface]
SaveConfig = false
PrivateKey = $wg_private_key
EOT
			[ -n "$bind_port" ] && printf 'ListenPort = %s\n' "$bind_port"
			[ -n "$mtu" ] && printf 'MTU = %s\n' "$mtu"
			cat << EOT
Table = off
FwMark = 0xca6c
Address = $ip4_addr/31
EOT
			first_ip6_addr=
			i=0
			while
				eval "ip6_addr=\"\$peer_${peer}_ip6_addr_$i\""
				[ -n "$ip6_addr" ]
			do
				printf 'Address = %s/127\n' "$ip6_addr"
				[ -z "$first_ip6_addr" ] && first_ip6_addr="$ip6_addr"
				i=$((i+1))
			done
			[ -n "$first_ip6_addr" ] && cat << EOT
PostUp = ip6tables -t nat -A POSTROUTING -s `print_peer_in_tunnel_inet6_address $first_ip6_addr` -o eth0 -j MASQUERADE
PostUp = ip6tables -t nat -A POSTROUTING -o %i -s fd00::/8 -d fd00::/8 -j ACCEPT
PostUp = ip6tables -t nat -A POSTROUTING -o %i -s fd00::/8 -d fc00::/8 -j ACCEPT
PostUp = ip6tables -t nat -A POSTROUTING -o %i -s fc00::/8 -d fd00::/8 -j ACCEPT
PostUp = ip6tables -t nat -A POSTROUTING -o %i -s fc00::/8 -d fc00::/8 -j ACCEPT
PostUp = ip6tables -t nat -A POSTROUTING -o %i -j MASQUERADE
PostUp = route -6 add ::/0 dev %i metric 2048
PostDown = ip6tables -t nat -D POSTROUTING -s `print_peer_in_tunnel_inet6_address $first_ip6_addr` -o eth0 -j MASQUERADE
PostDown = ip6tables -t nat -D POSTROUTING -o %i -s fd00::/8 -d fd00::/8 -j ACCEPT
PostDown = ip6tables -t nat -D POSTROUTING -o %i -s fd00::/8 -d fc00::/8 -j ACCEPT
PostDown = ip6tables -t nat -D POSTROUTING -o %i -s fc00::/8 -d fd00::/8 -j ACCEPT
PostDown = ip6tables -t nat -D POSTROUTING -o %i -s fc00::/8 -d fc00::/8 -j ACCEPT
PostDown = ip6tables -t nat -D POSTROUTING -o %i -j MASQUERADE
EOT
			cat << EOT

[Peer]
PublicKey = $public_key
EOT
			[ -n "$remote" ] && cat << EOT
Endpoint = $remote
PersistentKeepAlive = 50
EOT
			echo "AllowedIPs = 0.0.0.0/0, ::/0"
		} > "/etc/wireguard/$interface.conf"
		chmod 640 "/etc/wireguard/$interface.conf"
	done
}

write_resolv_conf() {
	[ -z "$replace_resolv_conf" ] && return
	rm -f /etc/resolv.conf
	{
		echo "nameserver 10.10.10.10"
		if [ -n "$fallback_name_server" ]; then
			printf 'nameserver %s\n' "$fallback_name_server"
		fi
	} > /etc/resolv.conf || return
	[ -n "$fallback_name_server" ] && return
	if ! iptables -t nat --list-rules | grep -Eq '^-A OUTPUT -d 10\.10\.10\.10/32 -o .+ -p udp -m udp --dport 53 -j DNAT --to-destination '; then
		iptables -t nat -A OUTPUT -o "$default_route_interface" -d 10.10.10.10 -p tcp -m udp --dport 53 -j DNAT --to-destination "$DEFAULT_FALLBACK_NAME_SERVER"
	fi
}

install_rncn_dns() {
	[ -z "$run_dns_server" ] && return
	[ -d "$PROGRAM_DIR/dns" ] || mkdir "$PROGRAM_DIR/dns" || return
	rm -f "$PROGRAM_DIR/dns/Country.mmdb.new"
	wget "$GEOIP_MMDB_URL" --output-document "$PROGRAM_DIR/dns/Country.mmdb.new"
	rm -f "$PROGRAM_DIR/dns/Country.mmdb"
	mv "$PROGRAM_DIR/dns/Country.mmdb.new" "$PROGRAM_DIR/dns/Country.mmdb"
	if [ ! -f "$PROGRAM_DIR/dns/mihomo" ]; then
		local go_arch
		case "`uname -m`" in
			i?86)
				go_arch=386
				;;
			x86_64)
				go_arch=amd64
				;;
			armv7*l*)
				go_arch=armv7
				;;
			aarch64)
				go_arch=arm64
				;;
			riscv64)
				go_arch=riscv64
				;;
			*)
				echo "Unsupported machine type" 1>&2
				return 1
				;;
		esac
		wget https://github.com/MetaCubeX/mihomo/releases/download/v1.18.6/mihomo-linux-$go_arch-v1.18.6.gz -O - | gzip -d > "$PROGRAM_DIR/dns/mihomo"
		chmod 755 "$PROGRAM_DIR/dns/mihomo"
	fi 
	rm -f /etc/systemd/system/rncn-dns.service
	cat > /etc/systemd/system/rncn-dns.service << EOT
[Unit]
Description=RNCN DNS
After=network-online.target

[Service]
Type=simple
ExecStartPre=-/sbin/ifup lo:dns1 lo:dns2
ExecStart="$PROGRAM_DIR/dns/mihomo" -d "$PROGRAM_DIR/dns"
ExecStopPost=-/sbin/ifdown lo:dns1 lo:dns2
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
EOT
	[ -z "$dns_config_update_url" ] && return
	rm -f "$PROGRAM_DIR/dns/rncn-dns-updater.sh"
	{
		printf '#!/bin/sh\n\n'
		cat "$PROGRAM_DIR/src/rncn-dns-updater.functions.sh"
		cat << EOT

while true; do
	check_and_update_config '$dns_config_update_url' '$PROGRAM_DIR/dns/config.yaml' rncn-dns
	sleep 600
done
EOT
	} > "$PROGRAM_DIR/dns/rncn-dns-updater.sh"
	rm -f /etc/systemd/system/rncn-dns-updater.service
	cat > /etc/systemd/system/rncn-dns-updater.service << EOT
[Unit]
Description=RNCN DNS Updater
After=network-online.target rncn-dns.service rncn-media-unlocker.service

[Service]
Type=simple
ExecStart=/bin/sh "$PROGRAM_DIR/dns/rncn-dns-updater.sh"
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
EOT
	. "$PROGRAM_DIR/src/rncn-dns-updater.functions.sh"
	check_and_update_config "$dns_config_update_url" "$PROGRAM_DIR/dns/config.yaml"
}


if [ $# = 0 ]; then
	cat << EOF
Available subcommands:
	init
	install
	enable-services
EOF
	exit 255
fi

case "$1" in
	init)
		if [ $# != 2 ]; then
			printf 'Usage: %s init <node-name>\n' "$0" 1>&2
			exit 255
		fi
		if [ -f "$RNCN_STATE_FILE" ]; then
			echo "This node appears already initiailzed" 1>&2
			exit 1
		fi
		if [ ! -d "$PROGRAM_DIR/config/$2" ]; then
			printf "No configuration found for %s\\n" "$2" 1>&2
			exit 1
		fi
		printf %s\\n "$2" > "$RNCN_STATE_FILE"
		;;
	install)
		if [ ! -f "$RNCN_STATE_FILE" ] || ! read node_name < "$RNCN_STATE_FILE" || [ -z "$node_name" ]; then
			printf "Use '%s init <node-name>' to select a configuration for this node first\\n" "$0" 1>&2
			exit 1
		fi
		if ! load_config "$PROGRAM_DIR/config/$node_name"; then
			echo "Failed to load node configuration" 1>&2
			exit 1
		fi
		apt-get --yes remove cloud-init
		apt-get --yes remove systemd-resolved
		set -e
		if [ -n "$set_hostname" ] && [ "`hostname`" != "$node_name" ] && [ "`hostname -f`" != "$node_name" ]; then
			rm -f /etc/hostname
			printf %s\\n "$node_name" > /etc/hostname
			if grep -Eq '^127\.0\.1\.1[[:space:]]' /etc/hosts; then
				escaped_node_name="`printf %s \"$node_name\" | sed 's/\\./\\\\\\./g'`"
				if ! grep -Eq "^127\\.0\\.1\\.1[[:space:]]$escaped_node_name( |\$)" /etc/hosts; then
					sed -i -E "s/(127\\.0\\.1\\.1[[:space:]])/\\1$escaped_node_name /" /etc/hosts
				fi
			else
				make_sure_nonempty_file_ends_with_new_line /etc/hosts
				printf '127.0.1.1	%s\n' "$node_name" > /etc/hosts
			fi
			hostname --file /etc/hostname
		fi
		if ! get_default_route_interface; then
			echo "Failed to determine default route interface" 1>&2
			exit 1
		fi
		make_sure_nonempty_file_ends_with_new_line /etc/inputrc
		append_unique_line "set enable-bracketed-paste off" /etc/inputrc
		make_sure_nonempty_file_ends_with_new_line /etc/bash.bashrc
		append_unique_line "set +H" /etc/bash.bashrc
		append_unique_line HISTCONTROL=ignoreboth /etc/bash.bashrc
		append_unique_line "shopt -s histappend" /etc/bash.bashrc
		modify_etc_profile
		apt-get install -y wireguard-tools bird2 iptables net-tools sudo git jq bc locales
		configure_locales
		create_rncn_unprivileged_user
		configure_sudo_pam
		configure_sudoers
		configure_lookback_interface
		configure_iptables
		write_resolv_conf
		install_and_apply_sysctl_config
		[ -d /etc/bird ] || mkdir /etc/bird || exit
		write_bird_conf_template
		write_rncn_link_status_monitor_sh
		cp -f "$PROGRAM_DIR/systemd-units/rncn-bird-link-status-monitor.service" /etc/systemd/system/
		write_ra_list
		write_ra_updater
		cp -f "$PROGRAM_DIR/systemd-units/rncn-bird-ra-updater.service" /etc/systemd/system/
		write_wireguard_configuration_files
		install_rncn_dns
		;;
	enable-services)
		if [ ! -f "$RNCN_STATE_FILE" ]; then
			printf "Use '%s init <node-name>' to select a configuration for this node first\\n" "$0" 1>&2
			exit 1
		fi
		# TODO
		;;
	*)
		printf "Unknown subcommand '%s'\\n" "$1" 1>&2
		exit 255
		;;
esac
