| #!/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 |
| ifup lo:dns1 lo:dns2 || true |
| } |
| |
| 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 |
| ExecStart="$PROGRAM_DIR/dns/mihomo" -d "$PROGRAM_DIR/dns" |
| 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 |