blob: 797721eec9d175a39ed512579fd402fa365effa9 [file] [log] [blame] [raw]
#!/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"
[ -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=
replace_sysctl_conf=
replace_resolv_conf=
add_login_logo=1
fallback_name_server=
time_zone=
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="
eval "peer_${current_peer}_metric_offset=0"
current_peer_ip6addr_index=0
continue
;;
\[*\]*)
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_config_update_url)
eval "$key=\"\$value\""
;;
.set_hostname|.run_dns_server|.replace_sysctl_conf|.replace_resolv_conf|.add_login_logo)
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
;;
wireguard.private_key)
eval "wg_$key=\"\$value\""
;;
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))
;;
peer.metric_offset)
eval "peer_${current_peer}_$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
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() {
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=
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")
;;
*)
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
append_unique_line "source /etc/profile" /etc/zsh/zlogin
}
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() {
make_sure_nonempty_file_ends_with_new_line /etc/ssh/sshd_config
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
}
configure_pam_sshd() {
[ -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
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() {
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
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
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
}
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.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 "Warning: Missing BSD ping source code"
return
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
}
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 no_default_route {
if net = 0.0.0.0/0 then reject;
accept;
}
protocol device {
}
protocol direct {
ipv4;
ipv6;
interface "rncn-*";
interface "lo" 10.100.254.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 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
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;
include "ipv$ip_vers-export-to-ospf-filter";
import filter no_default_route;
};
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 20;
wait 40;
dead 80;
cost 20; # ospf-v$v-backbone-interface-cost:$interface
};
EOT
done
cat << EOT
};
}
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
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.pingstat.sh" "$PROGRAM_DIR/src/rncn-link-status-monitor.calc_metric.sh"
cat << EOT
set -e
wait_time=60
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\""
eval "peer_metric_offset=\"\$peer_${peer}_metric_offset\""
printf ' calc_metric "%s" $wait_time %s & ping_%s_pid=$!\n' "$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-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
wait_time=3600
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 = 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
}
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 udp -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
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
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"
}
install_bifsocket() {
local gnu_system_type="`dpkg-architecture --query DEB_HOST_MULTIARCH`" || true
for obj in bifsocket bifcsocket; 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
}
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
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
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
configure_zsh
apt-get install --no-upgrade --no-install-recommends --yes wireguard-tools bird2 iptables net-tools sudo jq bc locales bind9-dnsutils bind9-host
apt-get install --no-upgrade --yes git
apt-get install --no-upgrade --yes gcc
configure_locales
install_bsd_ping
configure_sshd
configure_pam_sshd
create_rncn_unprivileged_user
configure_sudo_pam
configure_sudoers
configure_lookback_interface
configure_iptables
configure_time_zone
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
install_bifsocket
write_login_logo
write_no_vm_drop_caches_service_file
write_iperf3_service_file
;;
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
check_super_user
systemctl daemon-reload || exit
for peer in $peers; do
eval "interface=\"rncn-\$peer_${peer}_name\""
systemctl enable --now wg-quick@$interface
done
[ -f /etc/systemd/system/iptables-legacy.service ] || [ -f /etc/systemd/system/iptables-nft.service ] || systemctl enable --now iptables
systemctl enable --now rncn-bird-link-status-monitor bird
[ -n "$ra_list" ] && systemctl enable --now rnrn-bird-ra-updater
[ -n "$run_dns_server" ] && systemctl enable --now rncn-dns rncn-dns-updater
systemctl enable --now no-vm-drop-caches
true
;;
*)
printf "Unknown subcommand '%s'\\n" "$1" 1>&2
exit 255
;;
esac