#!/bin/sh /etc/rc.common # # Copyright (C) 2014-2017 Jian Chang # 2018-2023 honwen # # This is free software, licensed under the GNU General Public License v3. # See /LICENSE for more information. # START=90 STOP=15 NAME=shadowsocks EXTRA_COMMANDS="rules healthcheck" EXTRA_HELP=" rules Start IPTABLES inject\n healthcheck Check if service health" CRON_FILE=/etc/crontabs/root WATCHDOG_ENDPOINT='accounts.gstatic.com' # WATCHDOG_ENDPOINT='cp.cloudflare.com' DNSMASQDIR=$(sed -n 's+conf-dir=++p' /var/etc/dnsmasq.conf.* 2>/dev/null) [ "V$DNSMASQDIR" = "V" ] && DNSMASQDIR=/var/dnsmasq.d uci_get_by_name() { local ret=$(uci get $NAME.$1.$2 2>/dev/null) echo ${ret:=$3} } uci_get_by_type() { local ret=$(uci get $NAME.@$1[0].$2 2>/dev/null) echo ${ret:=$3} } uci_bool_by_name() { case "$(uci_get_by_name $1 $2)" in 1 | on | true | yes | enabled) return 0 ;; esac return 1 } uci_bool_by_type() { case "$(uci_get_by_type $1 $2)" in 1 | on | true | yes | enabled) return 0 ;; esac return 1 } validate_server() { [ "$(uci get $NAME.$1 2>/dev/null)" = "servers" ] } has_valid_server() { for server in $@; do validate_server $server && return 0 done return 1 } get_arg_udp() { local server=$(uci_get_by_type transparent_proxy udp_relay_server) [ "$server" = "same" ] || validate_server $server && echo "-u" } get_arg_out() { case "$(uci_get_by_type access_control self_proxy 1)" in 1) echo "-o" ;; 2) echo "-O" ;; esac } get_arg_tnd() { uci_bool_by_type $1 no_delay && echo "--tcp-no-delay" uci_bool_by_type $1 fast_open && echo "--tcp-fast-open" } get_server_ips() { echo $(uci_get_by_name $1 server) } get_lan_hosts() { uci_bool_by_name $1 enable && echo "$(uci_get_by_name $1 type),$(uci_get_by_name $1 host)" } get_plugin_config() { local plugin=$(uci_get_by_name $1 plugin) local plugin_opts=$(uci_get_by_name $1 plugin_opts) if [ -n "$plugin" ]; then echo $plugin >>/var/run/ss-plugin echo -e "\n \"plugin\": \"$plugin\"," if [ -n "$plugin_opts" ]; then echo " \"plugin_opts\": \"$plugin_opts\"," fi fi } get_crypto_config() { local key=$(uci_get_by_name $1 key) local password=$(uci_get_by_name $1 password) if [ -n "$key" ]; then echo "\"key\": \"$key\"," elif [ -n "$password" ]; then echo "\"password\": \"$password\"," else logger -st $NAME -p3 "The password or key is not set." fi } get_mode_weight_config() { local tcp_weight=$(uci_get_by_name $1 tcp_weight 1) local udp_weight=$(uci_get_by_name $1 udp_weight 1) [ "V$tcp_weight" = "V0" -a "V$udp_weight" = "V0" ] && return # failsafe with no config echo if [ "V$tcp_weight" = "V0" ]; then echo " \"mode\": \"udp_only\"," echo " \"udp_weight\": $udp_weight," elif [ "V$udp_weight" = "V0" ]; then echo " \"mode\": \"tcp_only\"," echo " \"tcp_weight\": $tcp_weight," else echo " \"tcp_weight\": $tcp_weight," echo " \"udp_weight\": $udp_weight," fi } get_udp_config() { udp_max_associations=$(uci_get_by_type general udp_max_associations 512) echo -e "\n \"udp_timeout\": $(uci_get_by_type general udp_timeout 300)," if [ $udp_max_associations -gt 0 ]; then echo " \"udp_max_associations\": ${udp_max_associations}," fi } gen_server_config() { validate_server $1 && cat <<-EOF { "address": "$(uci_get_by_name $1 server)", "port": $(uci_get_by_name $1 server_port), "method": "$(uci_get_by_name $1 encrypt_method)", $(get_crypto_config $1)$(get_plugin_config $1)$(get_mode_weight_config $1) "timeout": $(uci_get_by_name $1 timeout 60) }, EOF } gen_config_file() { local config_file=/var/etc/$NAME.$2.json local protocol=$2 local mode='tcp_and_udp' case "$protocol" in http) mode='tcp_only' ;; redir) mode='tcp_only' ;; redir-U) protocol='redir' ;; redir-u) protocol='redir' mode='udp_only' ;; esac cat <<-EOF | sed 's+},__$+}+g' >$config_file { "servers": [ $(for server in $1; do gen_server_config $server; done)__ ], "locals": [{ "mode": "$mode", "protocol": "$protocol", "local_address": "0.0.0.0", "local_port": $3 }],$(get_udp_config) "outbound_fwmark": 255, "nofile": 51200 } EOF echo $config_file } ss_cleandns() { uci_bool_by_type general no_dns_injection && return 0 rm -f $DNSMASQDIR/ss.conf $DNSMASQDIR/ss-servers.conf 2>/dev/null ([ -x /etc/init.d/dnsmasq-extra ] && /etc/init.d/dnsmasq reload || /etc/init.d/dnsmasq restart) >/dev/null 2>&1 } ss_injectdns() { uci_bool_by_type general no_dns_injection && return 0 echo >&2 "# Info: $NAME dnsmasq injecting..." mkdir -p $DNSMASQDIR DNSconf=$DNSMASQDIR/ss-servers.conf tmpDNSconf=$DNSMASQDIR/ss.conf echo "all-servers" >$tmpDNSconf config_load $NAME for server in $(config_foreach get_server_ips servers | sort -u | grep -v '[0-9]$'); do cat <<-EOF >>$tmpDNSconf ipset=/${server}/ss_spec_dst_sp server=/${server}/# server=/${server}/223.5.5.5 server=/${server}/119.29.29.29 server=/${server}/114.114.114.114 server=/${server}/80.80.80.80 server=/${server}/208.67.222.222#443 server=/${server}/208.67.220.220#5353 EOF done (grep -q 'no-resolv' /etc/dnsmasq.conf /etc/dnsmasq.d/* /var/dnsmasq.d/* /var/etc/dnsmasq.conf.* 2 >/dev/null) && sed "/\/#$/d" -i $tmpDNSconf sort -u $tmpDNSconf | sed '/\/\//d; /\/127.0.0.1\//d' >$DNSconf rm -f $tmpDNSconf echo "server=/${WATCHDOG_ENDPOINT}/127.0.0.1#$(uci_get_by_type port_forward local_port 5300)" >>$DNSconf [ -x /etc/init.d/dnsmasq-extra ] || { echo "server=/${WATCHDOG_ENDPOINT}/208.67.222.222#443" >>$DNSconf echo "server=/${WATCHDOG_ENDPOINT}/114.114.115.115" >>$DNSconf echo "server=/${WATCHDOG_ENDPOINT}/80.80.80.80" >>$DNSconf } DNSPROBE_DOMAIN='t.cn' cat <<-EOF >>$DNSconf server=/$DNSPROBE_DOMAIN/223.5.5.5 server=/$DNSPROBE_DOMAIN/119.29.29.29 server=/$DNSPROBE_DOMAIN/114.114.114.114 EOF /etc/init.d/dnsmasq reload >/dev/null 2>&1 # wait-for-dns, timeout 10s if which wait4x >/dev/null 2>&1; then # use [wait4x] wait4x http http://$DNSPROBE_DOMAIN --no-redirect -q elif which wait-for >/dev/null 2>&1; then # use [wait-for] wait-for -t=10s http --url=http://$DNSPROBE_DOMAIN >/dev/null 2>&1 else # use [ping] for _ in $(seq 10); do if ping -4 -q -c 1 -s 0 -W 1 -w 1 $DNSPROBE_DOMAIN >/dev/null 2>&1; then break; fi; done fi echo >&2 "# Info: $NAME dnsmasq injected." } start_rules() { config_load $NAME /usr/bin/ss-rules \ -s "$(config_foreach get_server_ips servers | sort -u)" \ -l "$(uci_get_by_type transparent_proxy local_port 1234)" \ -B "$(uci_get_by_type access_control wan_bp_list)" \ -b "$(uci_get_by_type access_control wan_bp_ips)" \ -W "$(uci_get_by_type access_control wan_fw_list)" \ -w "$(uci_get_by_type access_control wan_fw_ips)" \ -I "$(uci_get_by_type access_control lan_ifaces)" \ -d "$(uci_get_by_type access_control lan_target)" \ -a "$(config_foreach get_lan_hosts lan_hosts)" \ -e "$(uci_get_by_type access_control ipt_ext)" \ $(get_arg_out) $(get_arg_udp) } rules() { pidof sslocal >/dev/null || return 0 start_rules || /usr/bin/ss-rules -f } start_redir() { has_valid_server $1 || return 0 cd /var/run/ssservice sslocal -d $(get_arg_tnd transparent_proxy) \ -c=$(gen_config_file "$1" "redir$2" $(uci_get_by_type transparent_proxy local_port 1234)) \ --daemonize-pid=/var/run/ss-redir$2.pid cd - >/dev/null for _ in $(seq 10); do if pgrep -f sslocal >/dev/null; then break; else sleep 1; fi; done } ss_redir() { command -v sslocal >/dev/null 2>&1 || return 1 local main_server=$(uci_get_by_type transparent_proxy main_server) has_valid_server "$main_server" || return 1 local udp_relay_server=$(uci_get_by_type transparent_proxy udp_relay_server) if [ "$udp_relay_server" = "same" ]; then start_redir "$main_server" -U else start_redir "$main_server" start_redir "$udp_relay_server" -u fi } start_local() { has_valid_server $1 || return 0 cd /var/run/ssservice sslocal -d $(get_arg_tnd ${2}_proxy) \ -c=$(gen_config_file "$1" "$2" $(uci_get_by_type ${2}_proxy local_port 1080)) \ --daemonize-pid=/var/run/ss-local-$2.pid cd - >/dev/null } ss_local() { command -v sslocal >/dev/null 2>&1 || return 0 start_local "$(uci_get_by_type http_proxy server)" "http" start_local "$(uci_get_by_type socks_proxy server)" "socks" } start_tunnel() { has_valid_server $1 || return 0 cd /var/run/ssservice local config=$(gen_config_file "$1" "tunnel" $(uci_get_by_type port_forward local_port 5300)) uci_get_by_type port_forward destination '8.8.4.4:53' | sed 's+:+ +g' | while read addr port; do sed -i $config \ -e "/local_address/i\ \"forward_address\": \"$addr\"," \ -e "/local_address/i\ \"forward_port\": $port," done sslocal -d $(get_arg_tnd port_forward) \ -c=$config --daemonize-pid=/var/run/ss-tunnel.pid cd - >/dev/null } ss_tunnel() { command -v sslocal >/dev/null 2>&1 || return 0 start_tunnel "$(uci_get_by_type port_forward server)" } start() { pidof sslocal >/dev/null && return 0 mkdir -p /var/run/ssservice /var/etc local t0=$(date '+%s') echo >&2 "# Info: starting..." has_valid_server $(uci_get_by_type transparent_proxy main_server) && ss_injectdns ss_redir && rules ss_local ss_tunnel has_valid_server $(uci_get_by_type transparent_proxy main_server) && add_cron echo >&2 "# Info: started. CostTime: $(($(date '+%s') - $t0))s" } boot() { echo 'exit 0' >/var/etc/$NAME.include sysctl -w net.ipv4.tcp_fastopen=3 local delay=$(uci_get_by_type general startup_delay 0) (sleep $delay && start >/dev/null 2>&1) & return 0 } kill_all() { for it in $@; do kill -9 $(pgrep -f $it) >/dev/null 2>&1 done } stop() { /usr/bin/ss-rules -f kill_all sslocal if [ -f /var/run/ss-plugin ]; then kill_all $(sort -u /var/run/ss-plugin) rm -f /var/run/ss-plugin fi rm -rf /var/run/ssservice ss_cleandns del_cron } add_cron() { [ -f $CRON_FILE ] || return 0 uci_bool_by_type transparent_proxy no_healthcheck && return 0 sed -i '/shadowsocks_healthcheck/d' $CRON_FILE echo '0 */3 * * * rm -f /var/log/shadowsocks_healthcheck.log 2>&1' >>$CRON_FILE echo '* * * * * /etc/init.d/shadowsocks healthcheck >> /var/log/shadowsocks_healthcheck.log 2>&1' >>$CRON_FILE /etc/init.d/cron restart } del_cron() { [ -f $CRON_FILE ] || return 0 uci_bool_by_type transparent_proxy no_healthcheck && return 0 sed -i '/shadowsocks_healthcheck/d' $CRON_FILE /etc/init.d/cron restart } healthcheck() { command -v sslocal >/dev/null 2>&1 || return 1 has_valid_server $(uci_get_by_type transparent_proxy main_server) || return 1 uci_bool_by_type transparent_proxy no_healthcheck && return 0 LOGTIME=$(date "+%Y-%m-%d %H:%M:%S") pgrep -f "sslocal" >/dev/null 2>&1 || { echo "[${LOGTIME}] Problem decteted, restarting ${NAME}..." stop >/dev/null 2>&1 start >/dev/null 2>&1 return 0 } iptables -n -t nat -L PREROUTING | grep -q '_SPEC_LAN_DG' || { echo "[${LOGTIME}] Problem decteted, restarting ${NAME}..." stop >/dev/null 2>&1 start >/dev/null 2>&1 return 0 } cat_connect() { target="$1" retry=${2:-1} timeout=5 [ $retry -lt 1 ] && return 1 if which wait4x >/dev/null 2>&1; then # use [wait4x] wait4x http $target || cat_connect $target $((retry - 1)) elif which wait-for >/dev/null 2>&1; then # use [wait-for] wait-for -t=10s http --url=$target >/dev/null 2>&1 || cat_connect $target $((retry - 1)) else # use [curl] ret_code=$(curl -s --connect-timeout $timeout "$target" -w %{http_code} -o /dev/null | tail -n1) # echo -n "[ $retry $ret_code ] " [ "x$ret_code" = "x200" -o "x$ret_code" = "x204" ] && return 0 || sleep 1 && cat_connect $target $((retry - 1)) fi } TRPORT=$(uci_get_by_type transparent_proxy local_port 1234) GOOGLE=$(ping -4 -q -c 1 -s 0 -W 1 -w 1 ${WATCHDOG_ENDPOINT} 2>/dev/null | sed '1{s/[^(]*(//;s/).*//;q}') DNSPOD=119.29.29.98 #DNSPOD HTTPDNS (Inside GFW) if [ "Z$GOOGLE" = "Z" ]; then iptables -t nat -I OUTPUT -p tcp -d $DNSPOD -j RETURN cat_connect "http://${DNSPOD}/d" if [ "Z$?" = "Z0" ]; then echo "[${LOGTIME}] Problem-DNS decteted, restarting ${NAME}..." [ -x /etc/init.d/dnsmasq-extra ] && /etc/init.d/dnsmasq-extra restart || /etc/init.d/dnsmasq restart stop >/dev/null 2>&1 start >/dev/null 2>&1 else echo '['$LOGTIME'] Network Problem. Do nothing.' fi iptables -t nat -D OUTPUT -p tcp -d $DNSPOD -j RETURN return 0 fi iptables -t nat -I OUTPUT -p tcp -d $GOOGLE -j REDIRECT --to-port $TRPORT iptables -t nat -I OUTPUT -p tcp -d $DNSPOD -j RETURN cat_connect "http://${GOOGLE}/generate_204" 3 if [ "Z$?" = "Z0" ]; then echo "[${LOGTIME}] ${NAME} No Problem." else # cat_connect "http://wifi.vivo.com.cn/generate_204" # cat_connect "http://www.qualcomm.cn/generate_204" cat_connect "http://${DNSPOD}/d" if [ "Z$?" = "Z0" ]; then echo "[${LOGTIME}] Problem decteted, restarting ${NAME}..." [ -x /etc/init.d/haproxy-tcp ] && /etc/init.d/haproxy-tcp restart stop >/dev/null 2>&1 start >/dev/null 2>&1 else echo '['$LOGTIME'] Network Problem. Do nothing.' fi fi iptables -t nat -D OUTPUT -p tcp -d $GOOGLE -j REDIRECT --to-port $TRPORT iptables -t nat -D OUTPUT -p tcp -d $DNSPOD -j RETURN return 0 }