#!/bin/sh

# Copyright 2015-2025 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"

[ -z "$CC" ] && CC=gcc

check_super_user() {
	[ "`id -u`" = 0 ] && return
	echo "This action must be run with super user" 1>&2
	exit 1
}

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=
	dns_server_port=53
	replace_sysctl_conf=
	replace_resolv_conf=
	add_login_logo=1
	run_sniproxy=
	fallback_name_server=
	time_zone=
	dns_config_update_url=
	mon_initial_ping_duration=60
	mon_ping_duration=3600
	mon_minimal_ping_wait=3600
	wg_private_key=
	wg_use_peer_resolver=
	wg_periodic_rebind_port=
	peers=
	interfaces=
	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 current_interface=
	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}_tunnel_type=wireguard"
				eval "peer_${current_peer}_public_key="
				eval "peer_${current_peer}_bind="
				eval "peer_${current_peer}_bind_port="
				eval "peer_${current_peer}_initial_bind_port="
				eval "peer_${current_peer}_self_name_in_peer="
				eval "peer_${current_peer}_remote="
				eval "peer_${current_peer}_mtu="
				eval "peer_${current_peer}_ip4_addr="
				eval "peer_${current_peer}_ip6_addr_0="
				eval "peer_${current_peer}_metric_offset=0"
				current_peer_ip6addr_index=0
				continue
				;;
			\[interface\ *\]*)
				section=interface
				interface_name="${line#\[interface }"
				interface_name="${interface_name%%\]*}"
				current_interface="`printf %s \"$interface_name\" | sed -E 's/[.-]/_/g'`"
				interfaces="$interfaces $current_interface"
				eval "interface_${current_interface}_name=\"\$interface_name\""
				eval "interface_${current_interface}_ospf_if_type="
				eval "interface_${current_interface}_metric_offset=0"
				;;
			\[*\]*)
				section="${line%%\]*}"
				section="${section#\[}"
				current_peer=
				continue
				;;
		esac
		case "$section" in
			""|wireguard|peer)
				if ! parse_key_value "$line"; then
					printf "Parsing error at line %s, missing value?\\n" $nlines 1>&2
					return 1
				fi
				case "$section.$key" in
					.node_address|.ipv4_default_router|.ipv6_default_router|.fallback_name_server|.time_zone|.dns_server_port|.dns_config_update_url)
						eval "$key=\"\$value\""
						;;
					.set_hostname|.run_dns_server|.replace_sysctl_conf|.replace_resolv_conf|.add_login_logo|.run_sniproxy)
						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
						;;
					monitor.initial_ping_duration|monitor.ping_duration|monitor.minimal_ping_wait)
						eval "mon_$key=\"\$value\""
						;;
					wireguard.private_key|wireguard.use_peer_resolver|wireguard.periodic_rebind_port)
						eval "wg_$key=\"\$value\""
						;;
					peer.tunnel_type|peer.public_key|peer.bind|peer.bind_port|peer.initial_bind_port|peer.self_name_in_peer|peer.remote|peer.mtu|peer.metric_offset)
						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))
						;;
					interface.ospf_if_type|interface.metric_offset)
						eval "interface_${current_interface}_$key=\"\$value\""
						;;
				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
	local peer bind_port initial_bind_port self_name_in_peer
	for peer in $peers; do
		eval "bind_port=\"\$peer_${peer}_bind_port\""
		eval "initial_bind_port=\"\$peer_${peer}_initial_bind_port\""
		eval "self_name_in_peer=\"\$peer_${peer}_self_name_in_peer\""
		if [ -n "$bind_port" ] && [ -n "$initial_bind_port" ]; then
			printf 'Both bind_port and initial_bind_port set for peer %s\n' "$peer" 1>&2
			return 1
		fi
		if [ -n "$initial_bind_port" ] && [ -z "$self_name_in_peer" ]; then
			printf 'Missing self_name_in_peer for peer %s for initial_bind_port to work\n' "$peer" 1>&2
			return 1
		fi
	done
	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 ] && [ "$7" = 0 ]; then
			default_route_interface="$1"
			return
		fi
	done
	false
} < /proc/net/route

make_sure_nonempty_file_ends_with_new_line() {
	[ -f "$1" ] || return
	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"
}

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= for_do_statement=
	new_profile="`mktemp`" || return
	while read -r line; do case "$line" in
		'if [ "${PS1-}" ]; then'|"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)
			case "$if_statement" in
				'if [ "${PS1-}" ]; then')
					cat << EOT
if [ -n "\${PS1-}" ]; then
	# Interactive shell
	[ -n "\${BASH-}" ] && [ "\$BASH" != "/bin/sh" ] && [ -f /etc/bash.bashrc ] && . /etc/bash.bashrc
fi
EOT
					;;
				"")
					echo fi
					;;
				*)
					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
			esac
			if_statement=
			;;
		"export PATH")
			;;
		*"for i in /etc/profile.d/*.sh"*"; do")
			for_do_statement="$line"
			printf %s\\n "$line"
			;;
		'    if [ -r $i ]; then'|'    if [ -r "$i" ]; then')
			if [ -n "$for_do_statement" ]; then
				echo '    if [ -f "$i" ] && [ -r "$i" ]; then'
			else
				printf %s\\n "$line"
			fi
			;;
		*done)
			for_do_statement=
			printf %s\\n "$line"
			;;
		*)
			case "$if_statement" in
				'if [ "${PS1-}" ]; then')
					;;
				"")
					printf %s\\n "$line"
					;;
				*)
					printf '%s\n%s\n' "$if_statement" "$line"
					;;
			esac
			;;
	esac done < /etc/profile > "$new_profile"
	cat "$new_profile" > /etc/profile
	rm -f "$new_profile"
}

configure_zsh() {
	[ -d /etc/zsh ] || return 0
	make_sure_nonempty_file_ends_with_new_line /etc/zsh/zlogin || true
	append_unique_line "source /etc/profile" /etc/zsh/zlogin
}

unconfigure_pam_limits() {
	if { [ -h /etc/security/limits.conf ] && [ ! -c /etc/security/limits.conf ]; } || [ -f /etc/security/limits.conf ]; then
		ln -sfn /dev/null /etc/security/limits.conf || return
	fi
	if [ -d /etc/security/limits.d ] && ! rmdir /etc/security/limits.d; then
		rm -rfv /etc/security/limits.d
	fi
}

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"
}

configure_sshd() {
	if ! make_sure_nonempty_file_ends_with_new_line /etc/ssh/sshd_config; then
		echo "Missing /etc/ssh/sshd_config" 1>&2
		return 1
	fi
	local sshd_config_sed_flags=
	grep -Eq "^[[:space:]]*#?UseDNS[[:space:]]+yes" /etc/ssh/sshd_config && sshd_config_sed_flags="-e 's/^([[:space:]]*)#?(UseDNS[[:space:]]+).+/\\1\\2no/'" || true
	grep -Eq "^[[:space:]]*#?PasswordAuthentication[[:space:]]+no" /etc/ssh/sshd_config && sshd_config_sed_flags="-e 's/^([[:space:]]*)#?(PasswordAuthentication[[:space:]]+).+/\\1\\2yes/'" || true
	if grep -Eq "^[[:space:]]*#?PrintMotd[[:space:]]+" /etc/ssh/sshd_config; then
		sshd_config_sed_flags="-e 's/^([[:space:]]*)#?(PrintMotd[[:space:]]+).+/\\1\\2yes/'"
	else
		append_unique_line "PrintMotd yes" /etc/ssh/sshd_config
	fi
	[ -z "$sshd_config_sed_flags" ] && return
	rm -f /etc/ssh/sshd_config.new || return
	eval "sed -E $sshd_config_sed_flags /etc/ssh/sshd_config" > /etc/ssh/sshd_config.new || return
	mv -f /etc/ssh/sshd_config.new /etc/ssh/sshd_config
}

# Avoid Debian bug #1008715, and don't set process limits via PAM for SSH sessions
configure_sshd_pam() {
	[ -f /etc/pam.d/sshd ] || return 0
	rm -f /etc/pam.d/sshd.new || return
	sed -E '/[[:space:]]pam_(motd|limits)\.so/d' /etc/pam.d/sshd > /etc/pam.d/sshd.new || return
	if cmp -s /etc/pam.d/sshd.new /etc/pam.d/sshd; then
		rm -f /etc/pam.d/sshd.new
	else
		mv -f /etc/pam.d/sshd.new /etc/pam.d/sshd
	fi
}

# Silence log messages from pam_unix.so for sudo(1) to $RNCN_UNPRIVILEGED_USER
configure_sudo_pam() {
	[ -f /etc/pam.d/sudo ] || return 0
	grep --fixed-strings --line-regexp --quiet "session [success=done default=ignore] pam_succeed_if.so quiet ruser = root user = $RNCN_UNPRIVILEGED_USER" /etc/pam.d/sudo && return
	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_systemd_link() {
	[ -d /lib/systemd/network ] || return 0
	[ -d /etc/systemd/network ] || mkdir /etc/systemd/network || return
	if
		grep --extended-regexp --files-with-matches \
			-e '^[[:space:]]*NamePolicy[[:space:]]*=[[:space:]]*mac($|[[:space:]])' \
			-e '^[[:space:]]*AlternativeNamesPolicy[[:space:]]*=' \
			/lib/systemd/network/*.link \
				2> /dev/null | {
			fail() {
				kill $$
				exit 1
			}
			changed=
			while read -r f; do
				override_f="/etc${f#/lib}"
				[ -c "$override_f" ] && continue
				[ -f "$override_f" ] && continue
				rm -f "$override_f" || fail
				ln -sf /dev/null "$override_f" || fail
				changed=1
				[ "$override_f" != /etc/systemd/network/99-default.link ] && continue
				[ -f /etc/systemd/network/70-default.link ] && continue
				rm -f /etc/systemd/network/70-default.link || fail
				cat > /etc/systemd/network/70-default.link << EOT
[Match]
OriginalName=*

[Link]
NamePolicy=keep kernel database onboard slot path
MACAddressPolicy=persistent
EOT
			done
			[ -n "$changed" ]
		}
	then
		update-initramfs -u
	fi
}

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
	# Still configuring 10.0.0.10 for bird(8)
	cat >> /etc/network/interfaces << EOT
iface lo:dns1 inet static
	address 10.10.10.10/32
iface lo:dns2 inet static
	address 10.0.0.10/32
EOT
}

configure_iptables() {
	for f in /etc/systemd/system/iptables.service /etc/systemd/system/iptables-legacy.service /etc/systemd/system/iptables-nft.service ""; do
		[ -f "$f" ] && break
		[ -n "$f" ] || cp "$PROGRAM_DIR/systemd-units/iptables.service" /etc/systemd/system/ || return
	done
	[ -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 udp -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
-A POSTROUTING -o rncn-+ -s 10.100.0.0/16 -d 10.10.10.10 -p udp -m udp --dport 53 -j MASQUERADE
COMMIT
*mangle
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A FORWARD -o rncn-+ -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
-A PREROUTING -i $default_route_interface -p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG SYN -j CONNMARK --set-mark 22
-A OUTPUT -p tcp -m connmark --mark 22 -j MARK --set-mark 22
-A OUTPUT -p tcp -m tcp --dport 25 -j MARK --set-mark 22
-A OUTPUT -p tcp -m tcp --dport 465 -j MARK --set-mark 22
COMMIT
EOT
}

configure_ip6tables() {
	[ -z "$ipv6_default_router" ] && return
	if [ ! -f /etc/systemd/system/ip6tables.service ]; then
		rm -f /etc/systemd/system/ip6tables.service || return
		cp "$PROGRAM_DIR/systemd-units/ip6tables.service" /etc/systemd/system/ || return
	fi
	[ -d /etc/iptables ] || mkdir /etc/iptables || return
	[ -f /etc/iptables/ip6tables.rules ] || cat > /etc/iptables/ip6tables.rules << EOT
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -o $default_route_interface -s fd00:0db8::/32 -j MASQUERADE
-A POSTROUTING -o CloudflareWARP -s fd00:0db8::/32 -j MASQUERADE
COMMIT
*mangle
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A FORWARD -o rncn-+ -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
COMMIT
EOT
}

configure_locales() {
	[ -h /etc/default/locale ] || rm -f /etc/default/locale
	grep --fixed-strings --line-regexp --quiet "zh_TW.GB18030 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 || true
	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
}

configure_time_zone() {
	[ -z "$time_zone" ] && return
	if [ ! -f "/usr/share/zoneinfo/$time_zone" ]; then
		printf 'Time zone %s not available\n' "$time_zone" 1>&2
		return 1
	fi
	ln -snf "/usr/share/zoneinfo/$time_zone" /etc/localtime
}

configure_ssh_wgcfg() {
	if [ ! -f "$PROGRAM_DIR/config/id_ecdsa_wgcfg" ]; then
		printf "Warning: %s doesn't exist, skip configuring wgcfg\\n" "$PROGRAM_DIR/config/id_ecdsa_wgcfg" 1>&2
		return
	fi
	if ! adduser --system --gecos "WireGuard configuration over SSH" --ingroup nogroup --home /var/lib/wgcfg --shell /usr/local/sbin/sshwgcfg wgcfg; then
		adduser --system --comment "WireGuard configuration over SSH" --ingroup nogroup --home /var/lib/wgcfg --shell /usr/local/sbin/sshwgcfg wgcfg || true
	fi
	usermod --non-unique --uid 0 wgcfg
	if [ ! -f /usr/local/sbin/sshwgcfg ] || [ "$PROGRAM_DIR/src/sshwgcfg" -nt /usr/local/sbin/sshwgcfg ]; then
		rm -f /usr/local/sbin/sshwgcfg
		cp "$PROGRAM_DIR/src/sshwgcfg" /usr/local/sbin/
	fi
	chmod 750 /usr/local/sbin/sshwgcfg
	if ! grep --fixed-strings --line-regexp --ignore-case --quiet "match user wgcfg" /etc/ssh/sshd_config; then
		cat >> /etc/ssh/sshd_config << EOT

#Match user wgcfg localaddress $node_address
#	PermitRootLogin without-password
Match user wgcfg
	PermitRootLogin without-password
	X11Forwarding no
	AllowStreamLocalForwarding no
        AllowTcpForwarding no
        PermitTTY no
EOT
		service ssh restart
	fi
	[ -d /var/lib/wgcfg/.ssh ] || mkdir -p /var/lib/wgcfg/.ssh || return
	chmod go-rw "$PROGRAM_DIR/config/id_ecdsa_wgcfg"
	ssh-keygen -y -f "$PROGRAM_DIR/config/id_ecdsa_wgcfg" > /var/lib/wgcfg/.ssh/authorized_keys
}

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.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
net.ipv6.conf.$default_route_interface.accept_ra=2

net.core.rps_sock_flow_entries = 32768
net.core.wmem_default=16384
net.core.rmem_default=262144
net.core.rmem_max=268435456
net.core.wmem_max=268435456
net.ipv4.tcp_rmem=8192 262144 268435456
net.ipv4.tcp_wmem=4096 16384 268435456

net.ipv4.route.max_size=2147483647
net.ipv4.route.gc_thresh=-1
net.ipv4.route.gc_interval=60
net.ipv4.route.gc_timeout=300
net.ipv4.route.gc_elasticity=8

net.ipv6.route.max_size=2147483647
net.ipv6.route.gc_thresh=-1
net.ipv6.route.gc_interval=60
net.ipv6.route.gc_timeout=300
net.ipv6.route.gc_elasticity=8
EOT
	sysctl -p /etc/sysctl.conf || true
}

install_bsd_ping() {
	if [ ! -f "$PROGRAM_DIR/src/bsdping/build.sh" ] || [ ! -f "$PROGRAM_DIR/src/bsdping/ping.c" ]; then
		echo "Missing BSD ping source code" 1>&2
		return 1
	fi
	[ -f /usr/local/bin/ping.bsd ] && [ /usr/local/bin/ping.bsd -nt "$PROGRAM_DIR/src/bsdping/ping.c" ] && return
	sh "$PROGRAM_DIR/src/bsdping/build.sh" -o /usr/local/bin/ping.bsd || return
	chown root:root /usr/local/bin/ping.bsd || return
	setcap cap_net_raw=+ep /usr/local/bin/ping.bsd || chmod 4755 /usr/local/bin/ping.bsd || return
	[ -d /usr/local/share/man/man8 ] || mkdir -p /usr/local/share/man/man8 || return
	cp -p -- "$PROGRAM_DIR/src/bsdping/ping.8" /usr/local/share/man/man8/ping.bsd.8 || return
	[ -h /usr/local/bin/ping4.bsd ] || ln -s ping.bsd /usr/local/bin/ping4.bsd || true
}

install_sniproxy() {
	[ -z "$run_sniproxy" ] && return
	apt-get install --no-upgrade --no-install-recommends --yes sniproxy || return
	# sniproxy.service that distributed in Debian had a bug, where it is
	# missing option '-f' to sniproxy(8) to keep it running in foreground:
	# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1032295
	# While the bug has been fixed in trixie/sid, it still presents in
	# latest stable and oldstable releases. Install own copy of
	# sniproxy.service to avoid the bug.
	rm -f /etc/systemd/system/sniproxy.service || return
	cp "$PROGRAM_DIR/systemd-units/sniproxy.service" /etc/systemd/system/
	if ! grep -Fqs LD_PRELOAD=bifcsocket.so /etc/default/sniproxy; then
		rm -f /etc/default/sniproxy || return
		cat > /etc/default/sniproxy << EOT
# Additional options that are passed to the Daemon.
#DAEMON_ARGS="-c /etc/sniproxy.conf"

# Bind connect (client) sockets to the specified interface.
LD_PRELOAD=bifcsocket.so
BIND_INTERFACE=$default_route_interface
#BIND_INTERFACE=CloudflareWARP
EOT
	fi
	if ! grep -q "^# sniproxy configuration for RNCN" /etc/sniproxy.conf; then
		rm -f /etc/sniproxy.conf || return
		cat > /etc/sniproxy.conf << EOT
# sniproxy configuration for RNCN

user daemon

error_log {
	# Log to the daemon syslog facility
	syslog daemon
	# Alternatively we could log to file
	#filename /var/log/sniproxy/sniproxy.log
	# Control the verbosity of the log
	priority notice
}

resolver {
	nameserver ${fallback_name_server:-$DEFAULT_FALLBACK_NAME_SERVER}
}

listener $node_address:443 {
	proto tls
	access_log {
		filename /var/log/sniproxy/https_access.log
		priority notice
	}
}

# The default table for listeners without a specified table
table {
	.* *
}
EOT
	fi
	[ -d /var/log/sniproxy ] || mkdir /var/log/sniproxy || return
	chown daemon /var/log/sniproxy
}

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;

filter import_from_ospf_filter {
	if net = 0.0.0.0/0 then reject;
$clip_import_from_ospf_filter
	accept;
}

protocol device {
}

protocol direct {
	ipv4;
	ipv6;
	interface "rncn-*";
	interface "lo" 10.100.254.0/24;
	interface "lo" 10.100.253.0/24;
	interface "lo" 10.10.10.10;
	interface "lo" 10.0.0.10;
$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 filter {
			if krt_metric > 32 then reject;
$clip_import_from_kernel_ipv4_routing_table_filter
			accept;
		};
		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
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 ip_vers in 4 6; do
		cat << EOT

protocol ospf v3 {
	ipv$ip_vers {
		#export all;
		include "ipv$ip_vers-export-to-ospf-filter";
		import filter import_from_ospf_filter;
	};
	ecmp yes;
	merge external yes;
	area 0 {
EOT
		for peer in $peers; do
			eval "interface=\"rncn-\$peer_${peer}_name\""
			cat << EOT
		interface "$interface" {
			type pointopoint;
			authentication none;
			hello 20;
			wait 40;
			dead 80;
			cost 20; # ospf-backbone-interface-cost:$interface
		};
EOT
		done
		for interface in $interfaces; do
			eval "ospf_if_type=\"\$interface_${interface}_ospf_if_type\""
			eval "metric_offset=\"\$interface_${interface}_metric_offset\""
			eval "interface=\"\$interface_${interface}_name\""
			printf '		interface "%s" {\n' "$interface"
			[ -n "$ospf_if_type" ] && printf '			type %s;\n' "$ospf_if_type"
			cat << EOT
			authentication none;
			hello 20;
			wait 40;
			dead 80;
			cost $((1+metric_offset));
		};
EOT
		done
		cat << EOT
$clip_bird_ospf_default_area
	};
}
EOT
		[ -e /etc/bird/ipv$ip_vers-export-to-ospf-filter ] || echo "export filter {ospf_metric1 = 0; accept;};" > /etc/bird/ipv$ip_vers-export-to-ospf-filter
	done
} > /etc/bird/bird.conf.template

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

PING_PAYLOAD_SIZE=104
export PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin
export LANG=C
EOT
	cat "$PROGRAM_DIR/src/rncn-link-status-monitor.calc_metric.sh"
	cat << EOT

set -e

wait_time=$mon_initial_ping_duration
while true; do
	sleep $mon_minimal_ping_wait & sleep_pid=\$!
EOT
	local peer my_in_tunnel_addr peer_in_tunnel_addr interface
	for peer in $peers; do
		eval "my_in_tunnel_addr=\"\$peer_${peer}_ip4_addr\""
		eval "peer_in_tunnel_addr=\"\$peer_${peer}_remote_ip4_addr\""
		eval "peer_metric_offset=\"\$peer_${peer}_metric_offset\""
		printf '	calc_metric "%s" "%s" $wait_time %s & ping_%s_pid=$!\n' \
			"$my_in_tunnel_addr" "$peer_in_tunnel_addr" "$peer_metric_offset" $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-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
	wait \$sleep_pid
	wait_time=$mon_ping_duration
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'
NAME_SERVER=${fallback_name_server:-$DEFAULT_FALLBACK_NAME_SERVER}
EOT
	cat "$PROGRAM_DIR/src/rncn-ra-updater.sh"
} > /etc/bird/rncn-ra-updater.sh

write_ifupdown_cloned_interfaces() {
	local need_cloning=
	local peer tunnel_type
	for peer in $peers; do
		eval "tunnel_type=\"\$peer_${peer}_tunnel_type\""
		case "$tunnel_type" in
			gre|gre-fou)
				need_cloning=1
				break
				;;
		esac
	done
	[ -z "$need_cloning" ] && return
	[ -d /etc/network/interfaces.d ] || mkdir /etc/network/interfaces.d || return
	if ! grep -Eq '^[[:space:]]*source[[:space:]]+(/etc/network/)?interfaces\.d/\*$' /etc/network/interfaces; then
		make_sure_nonempty_file_ends_with_new_line /etc/network/interfaces || true
		echo "source /etc/network/interfaces.d/*" >> /etc/network/interfaces || return
	fi
	local interface bind remote mtu ip4_addr ip6_addr i
	rm -f /etc/network/interfaces.d/rncn-tunnels || return
	for peer in $peers; do
		eval "tunnel_type=\"\$peer_${peer}_tunnel_type\""
		eval "interface=\"rncn-\$peer_${peer}_name\""
		eval "bind=\"\$peer_${peer}_bind\""
		eval "remote=\"\$peer_${peer}_remote\""
		eval "mtu=\"\$peer_${peer}_mtu\""
		eval "ip4_addr=\"\$peer_${peer}_ip4_addr\""
		case "$tunnel_type" in
			gre)
				if [ -z "$bind" ] || [ -z "$remote" ]; then
					printf "Both 'bind' and 'remote' must be specified for peer %s that uses GRE tunnel\\n" "$peer" 1>&2
					return 1
				fi
				if [ "${bind%:*}" != "$bind" ] || [ "${remote%:*}" != "$remote" ]; then
					printf 'Incorrect configuration for peer %s: GRE tunnel cannot have port number\n' "$peer" 1>&2
					return 1
				fi
				cat << EOT

auto $interface
iface $interface inet static
	pre-up ip tunnel add $interface mode gre local $bind remote $remote
	post-down ip tunnel del $interface
EOT
				;;
			gre-fou)
				if [ -z "$bind" ] || [ -z "$remote" ]; then
					printf "Both 'bind' and 'remote' must be specified for peer %s that uses GRE/FOU tunnel\\n" "$peer" 1>&2
					return 1
				fi
				if [ "${bind%:*}" = "$bind" ] || [ "${remote%:*}" = "$remote" ]; then
					printf 'Incorrect configuration for peer %s: GRE/FOU tunnel addresses must have port numbers\n' "$peer" 1>&2
					return 1
				fi
				cat << EOT

auto $interface
iface $interface inet static
	pre-up ip link add $interface type gre local ${bind%:*} remote ${remote%:*} encap fou encap-sport ${bind##*:} encap-dport ${remote##*:}
	pre-up ip fou add port ${bind##*:} local ${bind%:*} peer ${remote%:*} peer_port ${remote##*:} ipproto 47
	post-down ip fou del port ${bind##*:} local ${bind%:*} peer ${remote%:*} peer_port ${remote##*:} ipproto 47
	post-down ip link del $interface
EOT
				;;
			*)
				continue
				;;
		esac
		[ -n "$mtu" ] && printf '	pre-up ifconfig %s mtu %s\n' "$interface" "$mtu"
		printf '	address %s/31\n' "$ip4_addr"
		cat << EOT
	up echo 0 > /proc/sys/net/ipv4/conf/$interface/send_redirects
	up echo 0 > /proc/sys/net/ipv4/conf/$interface/accept_redirects
	address $ip4_addr/31
EOT
		i=0
		while
			eval "ip6_addr=\"\$peer_${peer}_ip6_addr_$i\""
			[ -n "$ip6_addr" ]
		do
			printf 'iface %s inet6 static\n' "$interface"
			if [ $i = 0 ]; then
				cat << EOT
	up echo 0 > /proc/sys/net/ipv6/conf/$interface/accept_redirects
	up echo 1 > /proc/sys/net/ipv6/conf/$interface/forwarding
EOT
			fi
			printf '	address %s/127\n' "$ip6_addr"
			i=$((i+1))
		done
	done > /etc/network/interfaces.d/rncn-tunnels
}

write_wireguard_configuration_files() {
	[ -d /etc/wireguard ] || mkdir /etc/wireguard || return
	local peer interface public_key bind_port initial_bind_port self_name_in_peer 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 "initial_bind_port=\"\$peer_${peer}_initial_bind_port\""
		eval "self_name_in_peer=\"\$peer_${peer}_self_name_in_peer\""
		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
			if [ -n "$initial_bind_port" ]; then
				cat << EOT
ListenPort = $initial_bind_port
#RNCN#DynamicBindPort = true
#RNCN#RemotePeerName = $self_name_in_peer
EOT
			elif [ -n "$bind_port" ]; then
				printf 'ListenPort = %s\n' "$bind_port"
			fi
			[ -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
			cat << EOT
PostUp = echo 0 > /proc/sys/net/ipv4/conf/%i/send_redirects
PostUp = echo 0 > /proc/sys/net/ipv4/conf/%i/accept_redirects
EOT
			[ -n "$first_ip6_addr" ] && cat << EOT
PostUp = echo 0 > /proc/sys/net/ipv6/conf/%i/accept_redirects
PostUp = echo 1 > /proc/sys/net/ipv6/conf/%i/forwarding
PostUp = ip6tables -t nat -A POSTROUTING -s `print_peer_in_tunnel_inet6_address $first_ip6_addr` -o $default_route_interface -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 $default_route_interface -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
}

install_wireguard_peer_resolver() {
	[ -z "$wg_use_peer_resolver" ] && return
	cat > /etc/systemd/system/wireguard-peer-resolver.service << EOT
[Unit]
Description = WireGuard peer name resolver
After = wg-quick.target network-online.target

[Service]
Type = simple
ExecStart = /bin/sh "$PROGRAM_DIR/src/wireguard-peer-resolver.sh"

[Install]
WantedBy=multi-user.target
EOT
}

install_wireguard_periodic_rebind_port() {
	[ -z "$wg_periodic_rebind_port" ] && return
	cat > /etc/systemd/system/wireguard-periodic-rebind-port.service << EOT
[Unit]
Description = WireGuard periodic rebind port
After = wg-quick.target network-online.target

[Service]
Type = simple
ExecStart = /bin/sh "$PROGRAM_DIR/src/wireguard-periodic-rebind-port.sh"

[Install]
WantedBy=multi-user.target
EOT
}

write_resolv_conf() {
	[ -z "$replace_resolv_conf" ] && return
	rm -f /etc/resolv.conf
	echo "nameserver 10.10.10.10" > /etc/resolv.conf || 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 udp -m udp --dport 53 -j DNAT --to-destination "${fallback_name_server:-$DEFAULT_FALLBACK_NAME_SERVER}"
	fi
}

install_rncn_dns() {
	[ -z "$run_dns_server" ] && return
	[ -d "$PROGRAM_DIR/dns" ] || mkdir "$PROGRAM_DIR/dns" || return
	#apt-get install --no-upgrade --no-install-recommends --yes cpp || return
	local now
	now="`date +%s`" || return
	if ! mtim="`date -r \"$PROGRAM_DIR/dns/Country.mmdb\" +%s 2> /dev/null`" || [ $((now-mtim)) -gt 86400 ]; then
		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"
	fi
	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
ExecStartPre=-/sbin/iptables -t nat -A PREROUTING -d 10.0.0.10 -p udp -m udp --dport $dns_server_port -j DNAT --to-destination 10.10.10.10
ExecStartPre=-/sbin/iptables -t nat -A OUTPUT -o lo -d 10.0.0.10 -p udp -m udp --dport $dns_server_port -j DNAT --to-destination 10.10.10.10
ExecStart="$PROGRAM_DIR/dns/mihomo" -d "$PROGRAM_DIR/dns"
ExecStopPost=-/sbin/iptables -t nat -D PREROUTING -d 10.0.0.10 -p udp -m udp --dport $dns_server_port -j DNAT --to-destination 10.10.10.10
ExecStopPost=-/sbin/iptables -t nat -D OUTPUT -o lo -d 10.0.0.10 -p udp -m udp --dport $dns_server_port -j DNAT --to-destination 10.10.10.10
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_server_port' '$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
	[ -f "$PROGRAM_DIR/dns/config.yaml" ] && return
	. "$PROGRAM_DIR/src/rncn-dns-updater.functions.sh"
	check_and_update_config "$dns_server_port" "$dns_config_update_url" "$PROGRAM_DIR/dns/config.yaml"
}

install_preload_objects() {
	local gnu_system_type="`dpkg-architecture --query DEB_HOST_MULTIARCH`" || true
	for obj in bifsocket bifcsocket noinet6; do
		f="/usr/lib/$gnu_system_type/$obj.so"
		[ -f "$f" ] && continue
		rm -f "$f"
		$CC -Os -fno-common $CFLAGS -Wall -fPIC $LDFLAGS --shared "$PROGRAM_DIR/src/$obj.c" -l dl -o "$f" || return
	done
}

write_ssh_known_hosts() {
	[ -f "$PROGRAM_DIR/config/ssh_known_hosts" ] || return 0
	set -f
	make_sure_nonempty_file_ends_with_new_line /etc/ssh/ssh_known_hosts || true
	while read line; do
		line="${line%%\#*}"
		[ -z "$line" ] && continue
		set -- $line
		[ $# -lt 3 ] && continue
		cut -d " " -f 1,2,3 /etc/ssh/ssh_known_hosts | grep --fixed-strings --line-regexp --quiet "$1 $2 $3" && continue
		printf %s\\n "$*"
	done < "$PROGRAM_DIR/config/ssh_known_hosts" >> /etc/ssh/ssh_known_hosts
	set +f
}

LOGO_DATA="2,4 0,8 2,11 0,3 2,5 0,3 2,5 0,4 2,9 0,2 2,5 0,3 2,5
1,3 2,3 0,5 1,2 2,3 1,5 2,3 0 1,2 2,5 0 1,2 2,3 0,4 2,3 1,5 2,3 1,2 2,5 0 1,2 2,3
0,2 1,3 2,3 0,4 1 2,3 0,4 1 2,3 0,2 1 2,6 0 1 2,3 0,3 2,3 0,5 1,3 0,2 1 2,6 0 1 2,3
0,4 1,3 2,3 0,2 1 2,10 0,3 1 2,3 1 2,3 1 2,3 0,2 1 2,3 0,10 1 2,3 1 2,3 1 2,3
0,5 2,3 1 0,3 1 2,3 1,3 2,3 0,4 1 2,3 1,2 2,6 0,2 1 2,3 0,10 1 2,3 1,2 2,6
0,3 2,3 1 0,5 1 2,3 0,2 1,2 2,3 0,3 1 2,3 0 1,2 2,5 0,2 1,2 2,3 0,5 2,3 0 1 2,3 0 1,2 2,5
2,4 1 0,7 2,5 0,2 1,2 2,4 0 2,5 0 1,2 2,5 0,2 1,2 2,9 0,2 2,5 0 1,2 2,5 0 2,10
1,3 0,8 1,5 0,4 1,4 0 1,5 0,3 1,5 0,4 1,9 0,2 1,5 0,3 1,5 0 1,10"

write_login_logo() {
	[ -z "$add_login_logo" ] && return
	grep -q ^terminal_supports_utf_8 /etc/profile && return
	{
		cat << EOT

terminal_supports_utf_8() {
	case "\$TERM" in
		"")
			false
			;;
		ansi)
			false
			;;
		linux*)
			false
			;;
		sun*)
			false
			;;
		dtterm)
			false
			;;
		*)
			true
			;;
	esac
}

if [ -z "\$SUDO_USER" ] && window_size="\`stty size 0>&2 2> /dev/null\`" && columns="\${window_size#* }" && [ "\$columns" != "\$window_size" ]; then
	if [ "\$columns" -ge 78 ]; then
		_locale="\${LC_CTYPE-\$LANG}"
		_locale="\${_locale%%@*}"
		_charset="\${_locale#*.}"
		if [ "\$_charset" != "\$_locale" ] && [ "\$_charset" = UTF-8 ] && terminal_supports_utf_8; then
EOT
		printf %s "			printf '%s\\n'"
		printf %s\\n "$LOGO_DATA" | while read line; do
			printf ' \\\n				"'
			for draw in $line; do
				color=${draw%,*}
				[ $draw = $color ] && count=1 || count=${draw#*,}
				case $color in
					0)
						c=\ 
						;;
					1)
						c=░
						;;
					2)
						c=█
						;;
					*)
						echo e!
						exit 1
						;;
				esac
				while [ $count -gt 0 ]; do
					printf %s "$c"
					count=$((count-1))
				done
			done
			printf \"
		done
		printf '\n		else\n'
		printf %s\\n "$LOGO_DATA" | while read line; do
			printf "			printf '"
			for draw in $line; do
				color=${draw%,*}
				[ $draw = $color ] && count=1 || count=${draw#*,}
				case $color in
					0)
						printf '\\033[0m'
						;;
					1)
						printf '\\033[100m'
						;;
					2)
						printf '\\033[103m'
						;;
					*)
						echo e!
						exit 1
						;;
				esac
				while [ $count -gt 0 ]; do
					printf \ 
					count=$((count-1))
				done
			done
			printf "\\\\033[0m\\\\n'\\n"
		done
		cat << EOT
		fi
		unset _locale _charset
		printf '%s\\n' "" \\
			"             Welcome to RNCN.NET Network Infrastructure Service" \\
EOT
		printf '			"'
		local padding_len=$(((78-${#node_name}+1)/2))
		while [ $padding_len -gt 0 ]; do
			printf \ 
			padding_len=$((padding_len-1))
		done
		cat << EOT
$node_name"
	else
		printf 'Welcome to RNCN node:\\n%s\\n' "$node_name"
	fi
fi 1>&2
unset window_size
EOT
	} >> /etc/profile
}

write_no_vm_drop_caches_service_file() {
	[ -f /etc/systemd/system/no-vm-drop-caches.service ] && return
	rm -f /etc/systemd/system/no-vm-drop-caches.service
	cp "$PROGRAM_DIR/systemd-units/no-vm-drop-caches.service" /etc/systemd/system/
}

# Just write a systemd unit file, the actual program could be missing
write_iperf3_service_file() {
	[ -f /etc/systemd/system/iperf3.service ] && return
	rm -f /etc/systemd/system/iperf3.service
	cat > /etc/systemd/system/iperf3.service << EOT
[Unit]
Description=iperf3 server
Documentation=man:iperf3(1)
After=network.target auditd.service

[Service]
Type=simple
Restart=always
RestartSec=15
User=iperf3
ExecStart=/usr/bin/iperf3 --version4 --bind $node_address --server --interval 0
SuccessExitStatus=1

[Install]
WantedBy=multi-user.target
EOT
}


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
		if [ ! -f "$PROGRAM_DIR/src/bsdping/build.sh" ] || [ ! -f "$PROGRAM_DIR/src/bsdping/ping.c" ]; then
			echo "Warning: Missing BSD ping source code"
			echo "You may need to run 'git submodule update --init' to fetch it"
		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
		check_super_user
		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 || true
		append_unique_line "set enable-bracketed-paste off" /etc/inputrc
		make_sure_nonempty_file_ends_with_new_line /etc/bash.bashrc || true
		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
		configure_zsh
		apt-get install --no-upgrade --no-install-recommends --yes openssh-server readline-common wireguard-tools bird2 iptables net-tools sudo jq bc locales bind9-dnsutils bind9-host dpkg-dev gcc libc6-dev wget irqbalance
		unconfigure_pam_limits
		configure_locales
		install_bsd_ping
		install_sniproxy
		configure_sshd
		configure_sshd_pam
		create_rncn_unprivileged_user
		configure_sudo_pam
		configure_sudoers
		configure_systemd_link
		configure_lookback_interface
		configure_iptables
		configure_ip6tables
		configure_time_zone
		configure_ssh_wgcfg
		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/bird.service" /etc/systemd/system/
		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_ifupdown_cloned_interfaces
		write_wireguard_configuration_files
		install_wireguard_peer_resolver
		install_wireguard_periodic_rebind_port
		install_rncn_dns
		install_preload_objects
		write_ssh_known_hosts
		write_login_logo
		write_no_vm_drop_caches_service_file
		write_iperf3_service_file
		;;
	enable-services)
		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
		check_super_user
		systemctl daemon-reload || exit
		for peer in $peers; do
			eval "[ \"\$peer_${peer}_tunnel_type\" != wireguard ]" && continue
			eval "interface=\"rncn-\$peer_${peer}_name\""
			systemctl enable --now wg-quick@$interface
		done
		[ -n "$wg_use_peer_resolver" ] && systemctl enable --now wireguard-peer-resolver
		[ -n "$wg_periodic_rebind_port" ] && systemctl enable --now wireguard-periodic-rebind-port
		[ -f /etc/systemd/system/iptables-legacy.service ] || [ -f /etc/systemd/system/iptables-nft.service ] || systemctl enable --now iptables
		[ -n "$ipv6_default_router" ] && systemctl enable --now ip6tables
		systemctl enable --now rncn-bird-link-status-monitor bird
		[ -n "$ra_list" ] && systemctl enable --now rncn-bird-ra-updater
		if [ -n "$run_dns_server" ]; then
			systemctl enable --now rncn-dns
			[ -n "$dns_config_update_url" ] && systemctl enable --now rncn-dns-updater
		fi
		[ -n "$run_sniproxy" ] && systemctl enable --now sniproxy
		systemctl enable --now no-vm-drop-caches
		true
		;;
	*)
		printf "Unknown subcommand '%s'\\n" "$1" 1>&2
		exit 255
		;;
esac
