#!/bin/sh /etc/rc.common # # Copyright (C) 2017 openwrt-ssr # Copyright (C) 2017 yushi studio # Copyright (C) 2018 lean # Copyright (C) 2020 Mattraks # # This is free software, licensed under the GNU General Public License v3. # See /LICENSE for more information. # START=95 STOP=15 SERVICE_DAEMONIZE=1 NAME=shadowsocksr LOCK_FILE=/var/lock/ssrplus.lock LOG_FILE=/var/log/ssrplus.log TMP_PATH=/var/etc/ssrplus TMP_BIN_PATH=$TMP_PATH/bin PERSIST_DIR="/usr/share/nftables.d/ruleset-post" PERSIST_FILE="$PERSIST_DIR/99-shadowsocksr.nft" BACKUP_DIR="/etc/ssrplus/ssrplus-persist" BACKUP_FILE="$BACKUP_DIR/99-shadowsocksr.save" CLASH_CONFIG_DIR="/etc/ssrplus/clash" CLASH_API_PORT="16756" CLASH_OUTBOUND_MARK="255" CLASH_YAML_HELPER="/usr/share/shadowsocksr/clash_yaml.lua" # 设置 DNSMASQ_CONF_DIR 和 TMP_DNSMASQ_PATH if [ -f /etc/openwrt_release ]; then # 获取默认的 DNSMASQ 配置 ID DEFAULT_DNSMASQ_CFGID="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')" # 从 conf-dir 行中提取配置目录路径 if [ -f "/tmp/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID" ]; then DNSMASQ_CONF_DIR="$(awk -F '=' '/^conf-dir=/ {print $2}' "/tmp/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID")" else DNSMASQ_CONF_DIR="/tmp/dnsmasq.d" fi # 设置 TMP_DNSMASQ_PATH,并去除路径末尾的斜杠 TMP_DNSMASQ_PATH="${DNSMASQ_CONF_DIR%*/}/dnsmasq-ssrplus.d" fi chain_config_file= #generate shadowtls chain proxy config file tcp_config_file= udp_config_file= shunt_config_file= local_config_file= shunt_dns_config_file= tmp_local_port= DNS2TCP_FILTER_AAAA_SUPPORTED= ARG_UDP= ARG_UDP_RULES= dns_port="5335" #dns port china_dns_port="5333" #china_dns_port tmp_dns_port="300" #dns2socks temporary port tmp_udp_port="301" #udp temporary port tmp_udp_local_port="302" #udp socks temporary port tmp_shunt_port="303" #shunt temporary port tmp_shunt_local_port="304" #shunt socks temporary port tmp_shunt_dns_port="305" #shunt dns2socks temporary port tmp_tcp_local_port="306" #tcp socks temporary port server_count=0 redir_tcp=0 redir_udp=0 local_enable=0 http_enable=0 kcp_enable_flag=0 pdnsd_enable_flag=0 USE_TABLES="" HAS_NFT=0 HAS_IPSET=0 HAS_IPT=0 HAS_FW3=0 HAS_FW4=0 DNSMASQ_IPSET=0 DNSMASQ_NFTSET=0 switch_server=$1 CRON_FILE=/etc/crontabs/root EXTRA_COMMANDS='reset clash_cache' EXTRA_HELP=" reset Reset to default settings clash_cache Download and cache Clash node config" #extra_command "reset" "Reset to default settings" PS="/bin/busybox ps" ps_list() { if $PS -w >/dev/null 2>&1; then $PS -w else $PS fi } 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_set_by_name() { uci set $NAME.$1.$2=$3 2>/dev/null uci commit $NAME } uci_set_by_type() { uci set $NAME.@$1[0].$2=$3 2>/dev/null uci commit $NAME } uci_get_by_cfgid() { local key="$NAME.@$1[0]" [ -n "$2" ] && key="$key.$2" local ret=$(uci show "$key" 2>/dev/null | awk -F '[.=]' 'NR==1 {print $2}') echo ${ret:=$3} } get_filter_aaaa() { local ret="$(uci get $NAME.@global[0].filter_aaaa 2>/dev/null)" [ -z "$ret" ] && ret="$(uci get $NAME.@global[0].mosdns_ipv6 2>/dev/null)" echo "${ret:=1}" } supports_dns2tcp_filter_aaaa() { local dns2tcp_bin="${1:-$(first_type dns2tcp)}" [ -n "$dns2tcp_bin" ] || return 1 if [ -n "$DNS2TCP_FILTER_AAAA_SUPPORTED" ]; then [ "$DNS2TCP_FILTER_AAAA_SUPPORTED" = "1" ] return $? fi if "$dns2tcp_bin" -h 2>&1 | grep -Eq '(^|[[:space:]])-A([[:space:]]|$)'; then DNS2TCP_FILTER_AAAA_SUPPORTED=1 return 0 fi DNS2TCP_FILTER_AAAA_SUPPORTED=0 return 1 } start_dns2tcp() { local dnsserver="$1" local dns2tcp_bin="$(first_type dns2tcp)" if [ "$(get_filter_aaaa)" = "1" ]; then if supports_dns2tcp_filter_aaaa "$dns2tcp_bin"; then ln_start_bin "$dns2tcp_bin" dns2tcp -L "127.0.0.1#$dns_port" -R "${dnsserver/:/#}" -A else echolog "警告:当前 dns2tcp 不支持 AAAA 过滤参数(-A),已改为不带该参数启动。请升级 dns2tcp 版本。" ln_start_bin "$dns2tcp_bin" dns2tcp -L "127.0.0.1#$dns_port" -R "${dnsserver/:/#}" fi else ln_start_bin "$dns2tcp_bin" dns2tcp -L "127.0.0.1#$dns_port" -R "${dnsserver/:/#}" fi } supports_builtin_dns() { case "$1" in clash|tuic|v2ray|ss) return 0 ;; *) return 1 ;; esac } get_builtin_dns_fallback_mode() { if is_finded "dns2tcp"; then echo "1" elif is_finded "chinadns-ng"; then echo "6" elif is_finded "mosdns"; then echo "4" else echo "0" fi } get_dns2tcp_fallback_mode() { if is_finded "dns2tcp"; then echo "1" else echo "0" fi } force_dns2tcp_fallback() { local reason="$1" local fallback_mode="$(get_dns2tcp_fallback_mode)" if [ "$fallback_mode" = "1" ]; then echolog "提示:${reason},强制回退到 dns2tcp 模式。" echo "1" return 0 fi echolog "错误:${reason},且未安装 dns2tcp,启动终止。" return 1 } is_builtin_dns_active() { local global_server_type="$(uci_get_by_name "$GLOBAL_SERVER" type)" case "$global_server_type" in clash|tuic|ss) ps_list | grep -v "grep" | grep -q "ssr-retcp" ;; v2ray) ps_list | grep -v "grep" | grep -q "$TMP_PATH/.*ssr-retcp\\.json" ;; *) return 1 ;; esac } get_default_node_local_port() { uci_get_by_type global default_node_local_port 1234 } get_configured_threads() { local threads if [ "$(uci_get_by_type global threads 0)" = "0" ]; then threads=$(grep -c '^processor' /proc/cpuinfo 2>/dev/null) else threads=$(uci_get_by_type global threads 1) fi case "$threads" in ''|*[!0-9]*) threads=1 ;; esac [ "$threads" -lt 1 ] && threads=1 echo "$threads" } get_clash_api_secret() { echo "${1}_ssrplus_clash" } get_clash_workdir() { echo "$TMP_PATH/clash-$1" } get_clash_cache_file() { echo "$CLASH_CONFIG_DIR/$1.yaml" } get_clash_state_file() { echo "$CLASH_CONFIG_DIR/$1.cache.db" } get_tuic_workdir() { echo "$TMP_PATH/tuic-$1" } get_tuic_runtime_file() { echo "$(get_tuic_workdir "$1")/config.yaml" } get_ss_mihomo_workdir() { echo "$TMP_PATH/ss-$1" } get_ss_mihomo_runtime_file() { echo "$(get_ss_mihomo_workdir "$1")/config.yaml" } get_ss_server_mihomo_workdir() { echo "$TMP_PATH/ss-server-$1" } get_ss_server_mihomo_runtime_file() { echo "$(get_ss_server_mihomo_workdir "$1")/config.yaml" } link_mihomo_geodata() { local workdir="$1" [ -n "$workdir" ] || return 0 [ -s /usr/share/shadowsocksr/Country.mmdb ] && ln -sf /usr/share/shadowsocksr/Country.mmdb "$workdir/Country.mmdb" [ -s /usr/share/shadowsocksr/Country.mmdb ] && ln -sf /usr/share/shadowsocksr/Country.mmdb "$workdir/geoip.metadb" [ ! -e "$workdir/Country.mmdb" ] && [ -s /etc/openclash/Country.mmdb ] && ln -sf /etc/openclash/Country.mmdb "$workdir/Country.mmdb" [ ! -e "$workdir/geoip.metadb" ] && [ -s /etc/openclash/Country.mmdb ] && ln -sf /etc/openclash/Country.mmdb "$workdir/geoip.metadb" if [ -s /usr/share/v2ray/geosite.dat ]; then ln -sf /usr/share/v2ray/geosite.dat "$workdir/GeoSite.dat" ln -sf /usr/share/v2ray/geosite.dat "$workdir/geosite.dat" else [ -s /etc/openclash/GeoSite.dat ] && ln -sf /etc/openclash/GeoSite.dat "$workdir/GeoSite.dat" [ -s /etc/openclash/geosite.dat ] && ln -sf /etc/openclash/geosite.dat "$workdir/geosite.dat" fi if [ -s /usr/share/v2ray/geoip.dat ]; then ln -sf /usr/share/v2ray/geoip.dat "$workdir/geoip.dat" else [ -s /etc/openclash/geoip.dat ] && ln -sf /etc/openclash/geoip.dat "$workdir/geoip.dat" fi } prepare_tuic_runtime_config() { local sid="$1" local local_port="$2" local socks_port="$3" local instance_key="${4:-$sid}" local run_mode="${5:-redir}" local runtime_file="$(get_tuic_runtime_file "$instance_key")" local workdir="$(get_tuic_workdir "$instance_key")" [ -n "$sid" ] || return 1 [ -n "$local_port" ] || return 1 mkdir -p "$workdir" if ! /usr/bin/lua "$CLASH_YAML_HELPER" tuic "$sid" "$runtime_file" "$local_port" "$socks_port" "$run_mode" >/dev/null 2>&1; then echolog "TUIC 节点运行配置生成失败:$sid" return 1 fi # Reuse existing local geodata files so Mihomo does not have to fetch them. link_mihomo_geodata "$workdir" } prepare_ss_mihomo_runtime_config() { local sid="$1" local local_port="$2" local socks_port="$3" local instance_key="${4:-$sid}" local run_mode="${5:-redir}" local runtime_file="$(get_ss_mihomo_runtime_file "$instance_key")" local workdir="$(get_ss_mihomo_workdir "$instance_key")" [ -n "$sid" ] || return 1 [ -n "$local_port" ] || return 1 mkdir -p "$workdir" if ! /usr/bin/lua "$CLASH_YAML_HELPER" ss "$sid" "$runtime_file" "$local_port" "$socks_port" "$run_mode" >/dev/null 2>&1; then echolog "Shadowsocks 单节点 Mihomo 运行配置生成失败:$sid" return 1 fi link_mihomo_geodata "$workdir" } prepare_ss_server_mihomo_runtime_config() { local sid="$1" local runtime_file="$(get_ss_server_mihomo_runtime_file "$sid")" local workdir="$(get_ss_server_mihomo_workdir "$sid")" [ -n "$sid" ] || return 1 mkdir -p "$workdir" if ! /usr/bin/lua "$CLASH_YAML_HELPER" ss_server "$sid" "$runtime_file" >/dev/null 2>&1; then echolog "Shadowsocks 服务端 Mihomo 配置生成失败:$sid" return 1 fi } download_clash_config() { local sid="$1" local clash_url="$(uci_get_by_name "$sid" clash_url)" local clash_path="$(uci_get_by_name "$sid" clash_path)" local clash_user_agent="$(uci_get_by_name "$sid" clash_user_agent clash)" local cache_file="$(get_clash_cache_file "$sid")" local tmp_file="$TMP_PATH/clash-$sid.download.yaml" if [ -n "$clash_path" ] && [ -s "$clash_path" ]; then mkdir -p "$CLASH_CONFIG_DIR" cp -f "$clash_path" "$cache_file" echolog "Clash 本地配置加载成功:$clash_path" return 0 fi [ -n "$clash_url" ] || { echolog "Clash 节点未配置订阅 URL/本地配置,无法启动。" return 1 } mkdir -p "$CLASH_CONFIG_DIR" if curl -fsSL --connect-timeout 15 --retry 2 --insecure -A "$clash_user_agent" "$clash_url" -o "$tmp_file"; then if /usr/bin/lua "$CLASH_YAML_HELPER" validate "$tmp_file" >/dev/null 2>&1; then mv -f "$tmp_file" "$cache_file" echolog "Clash 配置下载成功:$clash_url" return 0 else echolog "Clash 配置校验失败:$clash_url" rm -f "$tmp_file" fi else echolog "Clash 配置下载失败:$clash_url" fi if [ -s "$cache_file" ]; then echolog "使用缓存的 Clash 配置继续启动。" return 0 fi return 1 } clash_cache() { local sid="$2" [ -n "$sid" ] || sid="$1" [ -n "$sid" ] || return 1 [ "$(uci_get_by_name "$sid" type)" = "clash" ] || return 1 mkdir -p "$TMP_PATH" "$CLASH_CONFIG_DIR" download_clash_config "$sid" } filter_clash_yaml_proxies() { local yaml_file="$1" local filter_words="$(uci_get_by_type server_subscribe filter_words '过期时间/剩余流量')" local names_file="$TMP_PATH/clash-proxy-names.$$" local removed_file="$TMP_PATH/clash-proxy-removed.$$" local removed_count=0 [ -n "$yaml_file" ] && [ -s "$yaml_file" ] || return 0 [ -n "$filter_words" ] || return 0 if ! /usr/bin/lua "$CLASH_YAML_HELPER" validate "$yaml_file" >/dev/null 2>&1; then return 0 fi removed_count="$(/usr/bin/lua "$CLASH_YAML_HELPER" filter "$yaml_file" "$filter_words" 2>/dev/null || echo 0)" if [ -n "$removed_count" ] && [ "$removed_count" -gt 0 ] 2>/dev/null; then echolog "Clash YAML 共过滤节点数量: $removed_count" fi rm -f "$names_file" "$removed_file" } prepare_clash_runtime_config() { local sid="$1" local local_port="$2" local socks_port="$3" local cache_file="$(get_clash_cache_file "$sid")" local workdir="$(get_clash_workdir "$sid")" local raw_file="$workdir/raw.yaml" local overlay_file="$workdir/overlay.yaml" local runtime_file="$workdir/config.yaml" local state_file="$(get_clash_state_file "$sid")" local merge_stats="" local filled_groups=0 local stripped_script_rules=0 local clash_secret="$(get_clash_api_secret "$sid")" local dns_ipv4_only="$(get_filter_aaaa)" local dns_mode="$(uci_get_by_type global pdnsd_enable 0)" [ -s "$cache_file" ] || return 1 mkdir -p "$workdir" "$CLASH_CONFIG_DIR" cp -f "$cache_file" "$raw_file" filter_clash_yaml_proxies "$raw_file" cat >"$overlay_file" <<-EOF redir-port: $local_port tproxy-port: $local_port external-controller: 127.0.0.1:$CLASH_API_PORT secret: $clash_secret allow-lan: true bind-address: 0.0.0.0 routing-mark: $CLASH_OUTBOUND_MARK profile: store-selected: true tun: enable: false dns: enable: $( [ "$dns_mode" = "7" ] && echo "true" || echo "false" ) enhanced-mode: redir-host listen: 127.0.0.1:$dns_port ipv6: $( [ "$dns_ipv4_only" = "1" ] && echo "false" || echo "true" ) EOF if [ -n "$socks_port" ] && [ "$socks_port" != "0" ]; then echo "socks-port: $socks_port" >>"$overlay_file" fi merge_stats="$(/usr/bin/lua "$CLASH_YAML_HELPER" merge "$raw_file" "$overlay_file" "$runtime_file" 2>/dev/null)" || return 1 filled_groups="$(printf '%s\n' "$merge_stats" | sed -n 's/.*filled_groups=\([0-9][0-9]*\).*/\1/p' | tail -n1)" stripped_script_rules="$(printf '%s\n' "$merge_stats" | sed -n 's/.*stripped_script_rules=\([0-9][0-9]*\).*/\1/p' | tail -n1)" [ -n "$filled_groups" ] || filled_groups=0 [ -n "$stripped_script_rules" ] || stripped_script_rules=0 if [ "$filled_groups" -gt 0 ] 2>/dev/null; then echolog "Clash YAML 兼容修复:共为 $filled_groups 个空代理组回填 DIRECT。" fi if [ "$stripped_script_rules" -gt 0 ] 2>/dev/null; then echolog "Clash YAML 兼容修复:共移除 $stripped_script_rules 条不兼容的 SCRIPT 规则。" fi local client_policy_stats="" local client_rules=0 client_policy_stats="$(/usr/bin/lua "$CLASH_YAML_HELPER" append_client_policy_rules "$runtime_file" "$sid" 2>/dev/null)" || true client_rules="$(printf '%s\n' "$client_policy_stats" | sed -n 's/.*client_rules=\([0-9][0-9]*\).*/\1/p' | tail -n1)" [ -n "$client_rules" ] || client_rules=0 if [ "$client_rules" -gt 0 ] 2>/dev/null; then echolog "Clash YAML 自定义客户端代理规则已注入:$client_rules 条。" fi # Persist Mihomo's store-selected cache across service restarts. if [ -f "$workdir/cache.db" ] && [ ! -L "$workdir/cache.db" ]; then if [ ! -e "$state_file" ]; then mv -f "$workdir/cache.db" "$state_file" else rm -f "$workdir/cache.db" fi fi rm -f "$workdir/cache.db" ln -sf "$state_file" "$workdir/cache.db" # Reuse existing local geodata files so Mihomo does not have to fetch them # during startup in environments where DNS or outbound access is not ready yet. link_mihomo_geodata "$workdir" } resolve_global_clash_socks_port() { local socks_enabled=$(uci_get_by_type socks5_proxy enabled 0) local socks_server=$(uci_get_by_type socks5_proxy server nil) local socks_port=$(uci_get_by_type socks5_proxy local_port 1080) local http_enabled=$(uci_get_by_type http_proxy enabled 0) local http_server=$(uci_get_by_type http_proxy server nil) local http_port="$socks_port" local resolved_port="" [ "$socks_server" = "same" ] && socks_server="$GLOBAL_SERVER" [ "$http_server" = "same" ] && http_server="$GLOBAL_SERVER" if [ "$socks_enabled" = "1" ] && [ "$socks_server" = "$GLOBAL_SERVER" ]; then resolved_port="$socks_port" fi if [ "$http_enabled" = "1" ] && [ "$http_server" = "$GLOBAL_SERVER" ]; then if [ -n "$resolved_port" ] && [ "$resolved_port" != "$http_port" ]; then echolog "Global_HTTP: Mihomo/Clash 仅支持单个 socks-port,HTTP 代理将复用全局 SOCKS5 端口 $resolved_port。" else resolved_port="$http_port" fi fi echo "$resolved_port" } supports_builtin_socks() { case "$1" in ss|clash|tuic|v2ray) return 0 ;; *) return 1 ;; esac } supports_builtin_http() { case "$1" in clash) return 0 ;; *) return 1 ;; esac } get_host_ip() { local host=$(uci_get_by_name $1 server) [ -n "$host" ] || { echo "" return } host="${host#\[}" host="${host%\]}" local ip="" if echo "$host" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then ip="$host" elif ! echo "$host" | grep -q ':'; then ip=$(resolveip -4 -t 3 "$host" | awk 'NR==1{print}') [ -z "$ip" ] && ip=$(curl -sSL "http://119.29.29.29/d?dn=$host" | awk -F ';' '{print $1}') fi [ -z "$ip" ] || uci_set_by_name $1 ip $ip [ -n "$ip" ] || ip="" local chinadns="$(uci_get_by_type global chinadns_forward)" if [ -n "$chinadns" ] && [ "$ip" != "$host" ]; then grep -q "$host" "$TMP_DNSMASQ_PATH/chinadns_fixed_server.conf" 2>"/dev/null" || \ echo -e "address=/$host/$ip" >> "$TMP_DNSMASQ_PATH/chinadns_fixed_server.conf" fi echo $ip } clean_log() { local logsnum=$(cat $LOG_FILE 2>/dev/null | wc -l) [ "$logsnum" -gt 1000 ] && { echo "$(date "+%Y-%m-%d %H:%M:%S") 日志文件过长,清空处理!" >$LOG_FILE } } echolog() { local d="$(date "+%Y-%m-%d %H:%M:%S")" echo -e "$d: $*" >>$LOG_FILE } add_cron() { touch $CRON_FILE sed -i '/ssrplus.log/d' $CRON_FILE if [ "$(uci_get_by_type server_subscribe auto_update 0)" -eq 1 ]; then if [ "$(uci_get_by_type server_subscribe config_auto_update_mode 0)" = "1" ]; then echo "* * * * * /usr/share/shadowsocksr/ssrplusupdate.sh loop >>$LOG_FILE 2>&1" >>$CRON_FILE else echo "$(uci_get_by_type server_subscribe auto_update_min_time) $(uci_get_by_type server_subscribe auto_update_day_time) * * $(uci_get_by_type server_subscribe auto_update_week_time) /usr/share/shadowsocksr/ssrplusupdate.sh >$LOG_FILE" >>$CRON_FILE fi fi crontab $CRON_FILE } del_cron() { touch $CRON_FILE sed -i '/ssrplus.log/d' $CRON_FILE crontab $CRON_FILE clean_log } set_lock() { exec 1000>"$LOCK_FILE" flock -xn 1000 } unset_lock() { flock -u 1000 rm -rf "$LOCK_FILE" } unlock() { failcount=1 while [ "$failcount" -le 10 ]; do if [ -f "$LOCK_FILE" ]; then let "failcount++" sleep 1s [ "$failcount" -ge 10 ] && unset_lock else break fi done } _exit() { local rc=$1 unset_lock exit ${rc} } first_type() { local candidate for candidate in "/bin/${1}" "/usr/bin/${1}" "/usr/libexec/${1}" "${TMP_BIN_PATH}/${1}"; do [ -x "$candidate" ] && { echo "$candidate" return 0 } done command -v "${1}" 2>/dev/null | head -n1 } is_finded() { [ -n "$(first_type "$1")" ] } has_ss_rust_client_binary() { [ -n "$(first_type sslocal)" ] } has_ss_rust_server_binary() { [ -n "$(first_type ssserver)" ] } has_mihomo_binary() { local mihomo_bin="$(first_type mihomo)" [ -x "$mihomo_bin" ] } use_mihomo_for_ss_rust_client() { has_ss_rust_client_binary && return 1 has_mihomo_binary } use_mihomo_for_ss_rust_server() { has_ss_rust_server_binary && return 1 has_mihomo_binary } ln_start_bin() { local file_func=${1} local ln_name=${2} shift 2 if [ "${file_func%%/*}" != "${file_func}" ]; then local ln_path="${TMP_BIN_PATH}/${ln_name}" local src_real="$(readlink -f "${file_func}" 2>/dev/null || echo "${file_func}")" local dst_real="$(readlink -f "${ln_path}" 2>/dev/null || true)" if [ ! -L "${ln_path}" ] || [ "${dst_real}" != "${src_real}" ]; then rm -f "${ln_path}" ln -s "${file_func}" "${ln_path}" >/dev/null 2>&1 fi file_func="${ln_path}" [ -x "${file_func}" ] || echolog "$(readlink ${file_func}) 没有执行权限,无法启动:${file_func} $*" fi #echo "${file_func} $*" >&2 [ -x "${file_func}" ] || { echolog "找不到 ${file_func},无法启动..." echolog "-----------end------------" _exit 2 } ulimit -n 1000000 ${file_func:-echolog " - ${ln_name}"} "$@" >/dev/null 2>&1 & } check_run_environment() { local dnsmasq_info=$(dnsmasq -v 2>/dev/null) local dnsmasq_ver=$(echo "$dnsmasq_info" | sed -n '1s/.*version \([0-9.]*\).*/\1/p') DNSMASQ_IPSET=0; [[ "$dnsmasq_info" == *" ipset"* ]] && DNSMASQ_IPSET=1 DNSMASQ_NFTSET=0; [[ "$dnsmasq_info" == *" nftset"* ]] && DNSMASQ_NFTSET=1 HAS_IPT=0; { command -v iptables-legacy || command -v iptables; } >/dev/null && HAS_IPT=1 HAS_IPSET=$(command -v ipset >/dev/null && echo 1 || echo 0) HAS_FW3=$(command -v fw3 >/dev/null && echo 1 || echo 0) HAS_FW4=$(command -v fw4 >/dev/null && echo 1 || echo 0) HAS_NFT=$(command -v nft >/dev/null && echo 1 || echo 0) # 重置 USE_TABLES USE_TABLES="" if [ "$HAS_FW4" -eq 1 ]; then if [ "$DNSMASQ_NFTSET" -eq 1 ] && [ "$HAS_NFT" -eq 1 ]; then USE_TABLES="nftables" else echolog "警告:fw4 已检测到,但 nftables 环境不完整。(has_nft:$HAS_NFT/dnsmasq_nftset:$DNSMASQ_NFTSET)" fi fi if [ -z "$USE_TABLES" ] && [ "$HAS_FW3" -eq 1 ]; then if [ "$HAS_IPSET" -eq 1 ] && [ "$HAS_IPT" -eq 1 ] && [ "$DNSMASQ_IPSET" -eq 1 ]; then USE_TABLES="iptables" else echolog "警告:fw3 已检测到,但 iptables 环境不完整。(has_ipset:$HAS_IPSET/has_ipt:$HAS_IPT/dnsmasq_ipset:$DNSMASQ_IPSET)" fi fi if [ -n "$USE_TABLES" ]; then local dep_list local file_path="/usr/lib/opkg/info" local file_ext=".control" [ -d "/lib/apk/packages" ] && { file_path="/lib/apk/packages"; file_ext=".list"; } if [ "$USE_TABLES" = "iptables" ]; then dep_list="iptables-mod-tproxy iptables-mod-socket iptables-mod-iprange iptables-mod-conntrack-extra kmod-ipt-nat" else dep_list="kmod-nft-socket kmod-nft-tproxy kmod-nft-nat" local v_num=$(echo "$dnsmasq_ver" | tr -cd '0-9') if [ "${v_num:-0}" -lt 290 ]; then echolog "提示:Dnsmasq ($dnsmasq_ver) 低于 2.90,建议升级以增强稳定性。" fi fi local pkg for pkg in $dep_list; do if [ ! -s "${file_path}/${pkg}${file_ext}" ]; then echolog "警告:${USE_TABLES} 透明代理缺失基础依赖 ${pkg}!" fi done else echolog "警告:不满足任何透明代理系统环境。" fi } normalize_run_mode() { local run_mode="$(uci_get_by_type global run_mode router)" if [ "$run_mode" = "oversea" ]; then echolog "提示:Oversea Mode 已移除,自动切换到 IP Route Mode。" uci_set_by_type global run_mode router run_mode="router" fi echo "${run_mode:-router}" } normalize_xray_protocol_nodes() { local changed=0 _migrate_xray_protocol_node() { local section="$1" local type="$(uci_get_by_name "$section" type)" if [ "$type" = "hysteria2" ]; then echolog "提示:检测到原生 Hysteria2 节点,自动迁移为 Xray(Hysteria2)。" uci -q set "$NAME.$section.type=v2ray" uci -q set "$NAME.$section.v2ray_protocol=hysteria2" changed=1 elif [ "$type" = "trojan" ]; then echolog "提示:检测到原生 Trojan 节点,自动迁移为 Xray(Trojan)。" uci -q set "$NAME.$section.type=v2ray" uci -q set "$NAME.$section.v2ray_protocol=trojan" changed=1 fi } config_load "$NAME" config_foreach _migrate_xray_protocol_node servers local subscribe_sid="$(uci_get_by_cfgid server_subscribe)" if [ -n "$subscribe_sid" ] && uci -q get "$NAME.$subscribe_sid.xray_hy2_type" >/dev/null; then uci -q delete "$NAME.$subscribe_sid.xray_hy2_type" changed=1 fi if [ -n "$subscribe_sid" ] && uci -q get "$NAME.$subscribe_sid.xray_tj_type" >/dev/null; then uci -q delete "$NAME.$subscribe_sid.xray_tj_type" changed=1 fi if [ "$changed" = "1" ]; then uci commit "$NAME" fi } add_dns_into_ipset() { case "$1" in gfw) ipset add gfwlist ${2%:*} 2>/dev/null ;; *) ipset add ss_spec_wan_ac ${2%:*} nomatch 2>/dev/null ;; esac } start_dns() { local ssrplus_dns="$(uci_get_by_type global pdnsd_enable 0)" local global_server_type="$(uci_get_by_name "$GLOBAL_SERVER" type)" local builtin_dns_enabled=0 local dnsserver="$(uci_get_by_type global tunnel_forward 8.8.4.4:53)" if [ "$ssrplus_dns" = "5" ]; then ssrplus_dns="$(force_dns2tcp_fallback "旧版 DNSPROXY 模式已移除")" || return 1 uci_set_by_type global pdnsd_enable "$ssrplus_dns" elif [ "$ssrplus_dns" = "6" ]; then dnsserver="$(uci_get_by_type global chinadns_ng_tunnel_forward 8.8.4.4:53)" fi local run_mode="$(normalize_run_mode)" if [ "$ssrplus_dns" = "4" ] && ! is_finded "mosdns"; then ssrplus_dns="$(force_dns2tcp_fallback "当前 DNS 模式 MosDNS 不可用")" || return 1 uci_set_by_type global pdnsd_enable "$ssrplus_dns" fi if [ "$ssrplus_dns" = "7" ]; then if supports_builtin_dns "$global_server_type"; then builtin_dns_enabled=1 else ssrplus_dns="$(force_dns2tcp_fallback "当前主节点不支持模块内置 DNS")" || return 1 uci_set_by_type global pdnsd_enable "$ssrplus_dns" fi fi if [ "$builtin_dns_enabled" = "1" ]; then pdnsd_enable_flag=7 elif [ "$ssrplus_dns" != "0" ]; then if [ "$HAS_IPSET" -eq 1 ]; then if [ -n "$dnsserver" ]; then add_dns_into_ipset $run_mode $dnsserver fi fi case "$ssrplus_dns" in 1) start_dns2tcp "$dnsserver" pdnsd_enable_flag=1 ;; 4) local filter_aaaa="$(get_filter_aaaa)" local mosdns_dnsserver="$(uci_get_by_type global tunnel_forward_mosdns)" output=$(for i in $(echo $mosdns_dnsserver | sed "s/,/ /g"); do dnsserver=${i%:*} dnsserver=${i##*/} if [ "$HAS_IPSET" -eq 1 ]; then add_dns_into_ipset $run_mode $dnsserver fi echo " - addr: $i" echo " enable_pipeline: true" done) awk -v line=14 -v text="$output" 'NR == line+1 {print text} 1' /etc/ssrplus/mosdns-config.yaml | sed "s/DNS_PORT/$dns_port/g" | sed "s/\(concurrent:\).*/\1 $(echo "$mosdns_dnsserver" | sed 's/,/ /g' | wc -w)/g"> $TMP_PATH/mosdns-config.yaml if [ "$filter_aaaa" == "0" ]; then sed -i "s/DNS_MODE/main_sequence_with_IPv6/g" $TMP_PATH/mosdns-config.yaml else sed -i "s/DNS_MODE/main_sequence_disable_IPv6/g" $TMP_PATH/mosdns-config.yaml fi ln_start_bin $(first_type mosdns) mosdns start -c $TMP_PATH/mosdns-config.yaml pdnsd_enable_flag=4 ;; 6) local chinadns_ng_proto="$(uci_get_by_type global chinadns_ng_proto)" local chinadns_ng_dns="" # 遍历每个 DNS 服务器 IFS=',' # 设置分隔符为逗号 for chinadns_ng_server in $dnsserver; do # 处理单个服务器地址 local chinadns_ng_ip="${chinadns_ng_server%%:*}" local chinadns_ng_port="${chinadns_ng_server##*:}" [ "$chinadns_ng_ip" = "$chinadns_ng_port" ] && chinadns_ng_port="53" chinadns_ng_tls_port="853" # 根据协议类型格式化服务器地址 case "$chinadns_ng_proto" in "none") chinadns_ng_server="${chinadns_ng_ip}#${chinadns_ng_port}" ;; "tls") chinadns_ng_server="${chinadns_ng_proto}://${chinadns_ng_ip}#${chinadns_ng_tls_port}" ;; *) chinadns_ng_server="${chinadns_ng_proto}://${chinadns_ng_ip}#${chinadns_ng_port}" ;; esac # 添加到参数列表 chinadns_ng_dns="${chinadns_ng_dns} -t ${chinadns_ng_server}" done unset IFS # 恢复默认分隔符 dnsserver="$chinadns_ng_dns" ln_start_bin $(first_type chinadns-ng) chinadns-ng -b 127.0.0.1 -l $tmp_dns_port -l $dns_port -p 3 -d gfw $dnsserver -N --filter-qtype 64,65 -f -r --cache 4096 --cache-stale 86400 --cache-refresh 20 echolog "ChinaDNS-NG query and cache Started!" pdnsd_enable_flag=6 ;; esac if [ "$run_mode" = "router" ]; then local chinadns="$(uci_get_by_type global chinadns_forward)" if [ -n "$chinadns" ]; then local wandns="$(ifstatus wan | jsonfilter -e '@["dns-server"][0]' || echo "119.29.29.29")" case "$chinadns" in "wan") chinadns="$wandns" ;; "wan_114") chinadns="$wandns,114.114.114.114" ;; esac ln_start_bin $(first_type chinadns-ng) chinadns-ng -l $china_dns_port -4 china -p 3 -c ${chinadns/:/#} -t 127.0.0.1#$dns_port -N -f -r cat <<-EOF >> "$TMP_DNSMASQ_PATH/chinadns_fixed_server.conf" no-poll no-resolv server=127.0.0.1#$china_dns_port EOF fi fi fi if [ "$(uci_get_by_type global apple_optimization 1)" == "1" ]; then local new_appledns="$(uci_get_by_type global apple_dns)" if [ -n "$new_appledns" ]; then sed -i 's/[[:space:]]//g' /etc/ssrplus/applechina.conf #去除所有空白字符 local old_appledns=$(grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' /etc/ssrplus/applechina.conf | sort -u) if [ -n "$old_appledns" ] && [ "$old_appledns" != "$new_appledns" ]; then sed -i "s,$(printf '%s' "$old_appledns"),$(printf '%s' "$new_appledns"),g" /etc/ssrplus/applechina.conf fi fi cp -f /etc/ssrplus/applechina.conf $TMP_DNSMASQ_PATH/ fi } gen_service_file() { #1-server.type 2-cfgname 3-file_path local fastopen if [ $(uci_get_by_name $2 fast_open) == "1" ]; then fastopen="true" else fastopen="false" fi case $1 in ssr) cat <<-EOF >$3 { "server": "0.0.0.0", "server_ipv6": "::", "server_port": $(uci_get_by_name $2 server_port), "mode": "tcp_and_udp", "password": "$(uci_get_by_name $2 password)", "timeout": $(uci_get_by_name $2 timeout 60), "method": "$(uci_get_by_name $2 encrypt_method)", "protocol": "$(uci_get_by_name $2 protocol)", "protocol_param": "$(uci_get_by_name $2 protocol_param)", "obfs": "$(uci_get_by_name $2 obfs)", "obfs_param": "$(uci_get_by_name $2 obfs_param)", "fast_open": $fastopen } EOF ;; ss) cat <<-EOF >$3 { "server": "0.0.0.0", "server_ipv6": "::", "server_port": $(uci_get_by_name $2 server_port), "mode": "tcp_and_udp", "password": "$(uci_get_by_name $2 password)", "timeout": $(uci_get_by_name $2 timeout 60), "method": "$(uci_get_by_name $2 encrypt_method_ss)", "protocol": "socks", "fast_open": $fastopen } EOF ;; esac } get_name() { case "$1" in ss) echo "ShadowSocks" ;; ssr) echo "ShadowsocksR" ;; esac } gen_config_file() { #server1 type2 code3 local_port4 socks_port5 chain6 threads5 case "$3" in 1) config_file=$tcp_config_file chain_config_file=$(echo ${config_file}|sed 's/ssrplus\//ssrplus\/chain-/') ;; 2) config_file=$udp_config_file chain_config_file=$(echo ${config_file}|sed 's/ssrplus\//ssrplus\/chain-/') ;; 3) if [ -n "$tmp_local_port" ]; then local tmp_port=$tmp_local_port else local tmp_port=$tmp_shunt_local_port fi config_file=$shunt_config_file chain_config_file=$(echo ${config_file}|sed 's/ssrplus\//ssrplus\/chain-/') ;; 4) local ss_protocol="socks" config_file=$local_config_file chain_config_file=$(echo ${config_file}|sed 's/ssrplus\//ssrplus\/chain-/') ;; esac case "$2" in ss | ssr) lua /usr/share/shadowsocksr/gen_config.lua $1 $mode $4 ${ss_protocol:-redir} >$config_file if [ "$3" == "3" ]; then lua /usr/share/shadowsocksr/gen_config.lua $1 $mode $tmp_port socks >$shunt_dns_config_file fi ;; v2ray) lua /usr/share/shadowsocksr/gen_config.lua $1 $mode $4 $5 >$config_file ;; trojan) case "$3" in 1) lua /usr/share/shadowsocksr/gen_config.lua $1 nat $4 >$config_file ;; 2) lua /usr/share/shadowsocksr/gen_config.lua $1 client $4 >$config_file ;; 3) lua /usr/share/shadowsocksr/gen_config.lua $1 nat $4 >$config_file lua /usr/share/shadowsocksr/gen_config.lua $1 client $tmp_port >$shunt_dns_config_file ;; 4) lua /usr/share/shadowsocksr/gen_config.lua $1 client $4 >$config_file ;; esac ;; naiveproxy) case "$3" in 1) lua /usr/share/shadowsocksr/gen_config.lua $1 redir $4 >$config_file ;; 3) lua /usr/share/shadowsocksr/gen_config.lua $1 redir $4 >$config_file lua /usr/share/shadowsocksr/gen_config.lua $1 socks $tmp_port >$shunt_dns_config_file ;; 4) lua /usr/share/shadowsocksr/gen_config.lua $1 socks $4 >$config_file ;; esac ;; hysteria2) lua /usr/share/shadowsocksr/gen_config.lua $1 $mode $4 $5 >$config_file ;; tuic) case "$3" in 1|2|4) lua /usr/share/shadowsocksr/gen_config.lua $1 $mode $4 >$config_file ;; 3) [ -z "$6" ] && lua /usr/share/shadowsocksr/gen_config.lua $1 $mode $4 >$shunt_dns_config_file || lua /usr/share/shadowsocksr/gen_config.lua $1 $mode $4 >$config_file ;; esac ;; shadowtls) case "$3" in 1|2|4) [ -z "$6" ] && lua /usr/share/shadowsocksr/gen_config.lua $1 $type $4 >$chain_config_file || lua /usr/share/shadowsocksr/gen_config.lua $1 $mode $4 $5 $6 >$config_file ;; 3) lua /usr/share/shadowsocksr/gen_config.lua $1 $type $4 >$chain_config_file lua /usr/share/shadowsocksr/gen_config.lua $1 $mode $4 $5 $6 >$config_file ;; esac ;; esac sed -i 's/\\//g' $TMP_PATH/*-ssr-*.json #>/dev/null > 2>&1 } start_udp() { local udp_relay_server_type=$(uci_get_by_name $UDP_RELAY_SERVER type) local threads=$(get_configured_threads) local type=$udp_relay_server_type if [ "$udp_relay_server_type" = "ss-rust" ]; then type="ss" fi redir_udp=1 case "$type" in ss | ssr) if [ "$udp_relay_server_type" = "ss" ]; then redir_udp=0 ARG_UDP="" echolog "UDP TPROXY Relay:Mihomo 单节点由主实例处理。" elif [ "$udp_relay_server_type" = "ss-rust" ] && use_mihomo_for_ss_rust_client; then redir_udp=0 ARG_UDP="" echolog "UDP TPROXY Relay:ss-rust 主程序不存在,已回退到 Mihomo 主实例处理。" else gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_port if [ "$udp_relay_server_type" = "ssr" ]; then ss_program="$(first_type ${type}-redir)" elif [ "$udp_relay_server_type" = "ss-rust" ] || [ "$udp_relay_server_type" = "ss" ]; then ss_program="$(first_type ${type}local)" fi echolog "$(get_name $type) program is: $ss_program" old_ss_program=$(readlink -f "$TMP_PATH/bin/${type}-redir" 2>/dev/null) if [ "$old_ss_program" != "$ss_program" ]; then rm -rf "$TMP_PATH/bin/${type}-redir" fi ln_start_bin $ss_program ${type}-redir -c $udp_config_file echolog "UDP TPROXY Relay:$(get_name $type) Started!" fi ;; v2ray) gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_port ln_start_bin $(first_type xray) v2ray run -c $udp_config_file echolog "UDP TPROXY Relay:$($(first_type xray) version | head -1) Started!" ;; trojan) #client gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_local_port ln_start_bin $(first_type trojan) $type --config $udp_config_file ln_start_bin $(first_type ipt2socks) ipt2socks -U -b 0.0.0.0 -4 -s 127.0.0.1 -p $tmp_udp_local_port -l $tmp_udp_port echolog "UDP TPROXY Relay:$($(first_type trojan) --version 2>&1 | head -1) Started!" ;; naiveproxy) echolog "NaïveProxy UDP TPROXY Relay not supported!" redir_udp=0 ARG_UDP="" ;; tuic) # TUIC now uses Mihomo directly and should follow the native path. redir_udp=0 ARG_UDP="" echolog "TUIC UDP relay is handled by the main Mihomo instance." ;; shadowtls) gen_config_file $UDP_RELAY_SERVER $type 2 ${tmp_udp_local_port} gen_config_file $UDP_RELAY_SERVER $type 2 ${tmp_udp_local_port} 0 chain ln_start_bin $(first_type shadow-tls) shadow-tls config --config $chain_config_file local chain_type=$(uci_get_by_name $UDP_RELAY_SERVER chain_type) case ${chain_type} in vmess) ln_start_bin $(first_type xray) v2ray run -c $udp_config_file echolog "UDP TPROXY Relay:shadow-tls chain-to $($(first_type xray) --version) Started!" ;; sslocal) ln_start_bin $(first_type sslocal) sslocal -c $udp_config_file echolog "UDP TPROXY Relay:shadow-tls chain-to $($(first_type sslocal) --version) Started!" ;; esac ;; socks5) if [ "$(uci_get_by_name $UDP_RELAY_SERVER auth_enable 0)" == "1" ]; then local auth="-a $(uci_get_by_name $UDP_RELAY_SERVER username) -k $(uci_get_by_name $UDP_RELAY_SERVER password)" fi for i in $(seq 1 $threads); do ln_start_bin $(first_type ipt2socks) ipt2socks -U -r -b 0.0.0.0 -4 \ -s $(uci_get_by_name $UDP_RELAY_SERVER server) \ -p $(uci_get_by_name $UDP_RELAY_SERVER server_port) \ -l $tmp_udp_port $auth done echolog "UDP TPROXY Relay:Socks5 via IPT2Socks $threads Threads Started!" ;; esac } start_local() { [ "$LOCAL_SERVER" = "nil" ] && return 1 local local_port="${1:-$(uci_get_by_type socks5_proxy local_port)}" local local_server_type=$(uci_get_by_name $LOCAL_SERVER type) local probe_instance_key="$(printf '%s' "${SSR_SWITCH_PROBE_INSTANCE_KEY:-}" | tr -cd '0-9A-Za-z._-')" local type=$local_server_type if [ "$local_server_type" = "ss-rust" ]; then type="ss" fi case "$type" in ss | ssr) if [ "$local_server_type" = "ssr" ]; then gen_config_file $LOCAL_SERVER $type 4 $local_port ss_program="$(first_type ${type}-local)" echolog "$(get_name $type) program is: $ss_program" old_ss_program=$(readlink -f "$TMP_PATH/bin/${type}-local" 2>/dev/null) if [ "$old_ss_program" != "$ss_program" ]; then rm -rf "$TMP_PATH/bin/${type}-local" fi ln_start_bin $ss_program ${type}-local -c $local_config_file echolog "Global_Socks5:$(get_name $type) Started!" elif [ "$local_server_type" = "ss" ] || { [ "$local_server_type" = "ss-rust" ] && use_mihomo_for_ss_rust_client; }; then local mihomo_bin="$(first_type mihomo)" local instance_key="${LOCAL_SERVER}-local" [ -n "$probe_instance_key" ] && instance_key="$probe_instance_key" [ -x "$mihomo_bin" ] || { echolog "Global_Socks5:Mihomo 内核不存在,请确认 /usr/bin/mihomo 或 /usr/libexec/mihomo 可执行。" return 1 } prepare_ss_mihomo_runtime_config "$LOCAL_SERVER" "$local_port" "$local_port" "$instance_key" "socks" || return 1 local ss_workdir="$(get_ss_mihomo_workdir "$instance_key")" ln_start_bin "$mihomo_bin" ss-local -d "$ss_workdir" -f "$ss_workdir/config.yaml" if [ "$local_server_type" = "ss-rust" ]; then echolog "Global_Socks5:ss-rust 主程序不存在,已回退到 Mihomo (Shadowsocks) 启动。" else echolog "Global_Socks5:Mihomo (Shadowsocks) Started!" fi else gen_config_file $LOCAL_SERVER $type 4 $local_port ss_program="$(first_type sslocal)" echolog "ShadowSocks program is: $ss_program" old_ss_program=$(readlink -f "$TMP_PATH/bin/${type}-local" 2>/dev/null) if [ "$old_ss_program" != "$ss_program" ]; then rm -rf "$TMP_PATH/bin/${type}-local" fi ln_start_bin $ss_program ${type}-local -c $local_config_file echolog "Global_Socks5:Shadowsocks-rust Started!" fi ;; v2ray) if [ "$_local" == "2" ]; then gen_config_file $LOCAL_SERVER $type 4 0 $local_port ln_start_bin $(first_type xray) v2ray run -c $local_config_file fi echolog "Global_Socks5:$($(first_type xray) version | head -1) Started!" ;; trojan) #client gen_config_file $LOCAL_SERVER $type 4 $local_port ln_start_bin $(first_type trojan) $type --config $local_config_file echolog "Global_Socks5:$($(first_type trojan) --version 2>&1 | head -1) Started!" ;; naiveproxy) gen_config_file $LOCAL_SERVER $type 4 $local_port ln_start_bin $(first_type naive) naive --config $local_config_file echolog "Global_Socks5:$($(first_type naive) --version | head -1) Started!" ;; tuic) local mihomo_bin="$(first_type mihomo)" local instance_key="${LOCAL_SERVER}-local" [ -n "$probe_instance_key" ] && instance_key="$probe_instance_key" [ -x "$mihomo_bin" ] || { echolog "Global_Socks5:Mihomo 内核不存在,请确认 /usr/bin/mihomo 或 /usr/libexec/mihomo 可执行。" return 1 } prepare_tuic_runtime_config "$LOCAL_SERVER" "$local_port" "$local_port" "$instance_key" "socks" || return 1 local tuic_workdir="$(get_tuic_workdir "$instance_key")" ln_start_bin "$mihomo_bin" tuic-local -d "$tuic_workdir" -f "$tuic_workdir/config.yaml" echolog "Global_Socks5:Mihomo (TUIC) Started!" ;; shadowtls) #respective config for global socks and main node if [ "$_local" == "2" ]; then gen_config_file $LOCAL_SERVER $type 4 "10${tmp_tcp_local_port}" gen_config_file $LOCAL_SERVER $type 4 0 $local_port chain/"10${tmp_tcp_local_port}" ln_start_bin $(first_type shadow-tls) shadow-tls config --config $chain_local_config_file local chain_type=$(uci_get_by_name $LOCAL_SERVER chain_type) case ${chain_type} in vmess) ln_start_bin $(first_type xray) v2ray run -c $local_config_file echolog "Global Socks5 Proxy:shadow-tls chain-to$($(first_type xray) --version) Started!" ;; sslocal) ln_start_bin $(first_type sslocal) sslocal -c $local_config_file echolog "Global Socks5 Proxy:shadow-tls chain-to$($(first_type sslocal) --version) Started!" ;; esac fi ;; clash) echolog "Global_Socks5:Clash total node is only supported when using Same as Global Server." return 1 ;; socks5) echolog "Global_Socks5:Socks5 transparent-only node cannot be used as generic Socks outbound." return 1 ;; *) local listenip='-i 0.0.0.0' [ -e /proc/sys/net/ipv6 ] && listenip='-i ::' ln_start_bin $(first_type microsocks) microsocks $listenip -p $local_port tcp-udp-ssr-local echolog "Global_Socks5:$type Started!" ;; esac local_enable=1 return 0 } start_local_with_port() { start_local "$1" } gen_3proxy_config() { local cfg_file="$1" local listen_port="$2" local relay_socks_port="$3" local auth_mode="$4" local auth_user="$5" local auth_pass="$6" cat <<-EOF >"$cfg_file" daemon pidfile $TMP_PATH/3proxy.pid nserver 8.8.8.8 nscache 65536 timeouts 1 5 30 60 180 1800 15 60 authcache ip 60 internal 0.0.0.0 external 0.0.0.0 flush EOF if [ "$auth_mode" = "password" ] && [ -n "$auth_user" ]; then cat <<-EOF >>"$cfg_file" auth strong users $auth_user:CL:$auth_pass allow $auth_user EOF else cat <<-'EOF' >>"$cfg_file" auth none allow * EOF fi cat <<-EOF >>"$cfg_file" parent 1000 socks5 127.0.0.1 $relay_socks_port proxy -n -a -p$listen_port flush EOF } start_http_proxy() { [ "$HTTP_SERVER" = "nil" ] && return 1 local http_local_port=$(uci_get_by_type http_proxy local_port 3128) local http_auth_mode=$(uci_get_by_type http_proxy http_auth none) local http_user=$(uci_get_by_type http_proxy http_user) local http_pass=$(uci_get_by_type http_proxy http_pass) local http_server_type=$(uci_get_by_name $HTTP_SERVER type) local need_local_start=1 local prev_local_server="$LOCAL_SERVER" local prev_local_config_file="$local_config_file" local prev_local_flag="$local_enable" local prev_local_mode="$_local" local http_config_file="$TMP_PATH/3proxy-ssr-http.cfg" local global_socks_enabled=$(uci_get_by_type socks5_proxy enabled 0) local global_socks_port=$(uci_get_by_type socks5_proxy local_port 1080) local global_socks_server=$(uci_get_by_type socks5_proxy server nil) local resolved_socks_server="$global_socks_server" local http_socks_port="$global_socks_port" if [ -z "$http_socks_port" ] || [ "$http_socks_port" = "0" ]; then http_socks_port=1080 fi [ "$http_local_port" = "$http_socks_port" ] && { echolog "Global_HTTP: HTTP 监听端口与上游 SOCKS5 端口不能相同。" return 1 } if [ "$resolved_socks_server" = "same" ]; then resolved_socks_server="$GLOBAL_SERVER" fi if [ "$global_socks_enabled" = "1" ] && [ "$http_socks_port" = "$global_socks_port" ] && [ "$resolved_socks_server" != "$HTTP_SERVER" ]; then echolog "Global_HTTP: Relay SOCKS5 Port 已被全局 SOCKS5 代理占用,请改用其他端口。" return 1 fi if [ "$http_auth_mode" = "password" ] && [ -z "$http_user" ]; then echolog "Global_HTTP: HTTP 认证模式为 password 时必须填写用户名。" return 1 fi case "$http_server_type" in socks5) echolog "Global_HTTP: 当前节点类型不支持作为 3proxy 的上游 SOCKS5 出口。" return 1 ;; esac if [ "$http_server_type" = "clash" ]; then if [ "$HTTP_SERVER" != "$GLOBAL_SERVER" ]; then echolog "Global_HTTP: Clash 节点仅支持选择 Same as Global Server 进行复用。" return 1 fi if [ "$http_socks_port" != "$global_socks_port" ] && [ "$global_socks_enabled" = "1" ] && [ "$resolved_socks_server" = "$HTTP_SERVER" ]; then echolog "Global_HTTP: Mihomo/Clash 仅支持单个 socks-port,请让 HTTP 代理与全局 SOCKS5 使用相同的上游 SOCKS5 端口。" return 1 fi fi if supports_builtin_http "$http_server_type" && [ "$HTTP_SERVER" = "$GLOBAL_SERVER" ]; then need_local_start=0 elif [ "$global_socks_enabled" = "1" ] && [ "$resolved_socks_server" = "$HTTP_SERVER" ] && [ "$global_socks_port" = "$http_socks_port" ]; then need_local_start=0 else LOCAL_SERVER="$HTTP_SERVER" local_config_file="$TMP_PATH/tcp-http-ssr-local.json" _local="2" local_enable=0 start_local_with_port "$http_socks_port" || { LOCAL_SERVER="$prev_local_server" local_config_file="$prev_local_config_file" local_enable="$prev_local_flag" _local="$prev_local_mode" return 1 } LOCAL_SERVER="$prev_local_server" local_config_file="$prev_local_config_file" local_enable="$prev_local_flag" _local="$prev_local_mode" fi [ "$need_local_start" = "0" ] && echolog "Global_HTTP: Reuse existing local Socks5 upstream on port $http_socks_port." gen_3proxy_config "$http_config_file" "$http_local_port" "$http_socks_port" "$http_auth_mode" "$http_user" "$http_pass" ln_start_bin $(first_type 3proxy) 3proxy "$http_config_file" echolog "Global_HTTP:3proxy HTTP/HTTPS Proxy Started!" http_enable=1 return 0 } get_udp_relay_mode() { local node="$1" local type local proto local socks_ver type=$(uci_get_by_name "$node" type) [ "$type" = "ss-rust" ] && type="ss" case "$type" in ss|ssr|hysteria2) echo "native" ;; clash|tuic) echo "native" ;; trojan|shadowtls) echo "split" ;; socks5) echo "split" ;; naiveproxy) echo "disabled" ;; v2ray) proto=$(uci_get_by_name "$node" v2ray_protocol) case "$proto" in http) echo "disabled" ;; socks) socks_ver=$(uci_get_by_name "$node" socks_ver 5) [ "$socks_ver" = "5" ] && echo "native" || echo "disabled" ;; *) echo "native" ;; esac ;; *) echo "disabled" ;; esac } Start_Run() { local threads=$(get_configured_threads) local global_server_type=$(uci_get_by_name $GLOBAL_SERVER type) if [ "$(uci_get_by_name $GLOBAL_SERVER kcp_enable 0)" == "1" ] && [ "$global_server_type" != "ss" ]; then [ ! -f "/usr/bin/kcptun-client" ] && return 1 local kcp_str=$(/usr/bin/kcptun-client -v | grep kcptun | wc -l) [ "0" == "$kcp_str" ] && return 1 local kcp_server=$(uci_get_by_name $GLOBAL_SERVER server) local kcp_port=$(uci_get_by_name $GLOBAL_SERVER kcp_port) local server_port=$(uci_get_by_name $GLOBAL_SERVER server_port) local password=$(uci_get_by_name $GLOBAL_SERVER kcp_password) local kcp_param=$(uci_get_by_name $GLOBAL_SERVER kcp_param) [ "$password" != "" ] && password="--key "$password service_start /usr/bin/kcptun-client -r $kcp_server:$kcp_port -l :$server_port $password $kcp_param kcp_enable_flag=1 ARG_UDP="" fi if [ "$_local" == "1" ]; then local socks_port=$(uci_get_by_type socks5_proxy local_port) tcp_config_file=$TMP_PATH/local-ssr-retcp.json [ "$mode" == "tcp,udp" ] && tcp_config_file=$TMP_PATH/local-udp-ssr-retcp.json #[ "$mode" == "tcp,udp" ] && { # if [ "$USE_TABLES" = "nftables" ]; then # # nftables / fw4 # tcp_config_file=$TMP_PATH/local-nft-ssr-retcp.json # elif [ "$USE_TABLES" = "iptables" ]; then # # iptables / fw3 # tcp_config_file=$TMP_PATH/local-udp-ssr-retcp.json # fi #} fi local tcp_port=$(get_default_node_local_port) local type=$global_server_type if [ "$global_server_type" = "ss-rust" ]; then type="ss" fi case "$type" in ss | ssr) if [ "$global_server_type" = "ssr" ]; then gen_config_file $GLOBAL_SERVER $type 1 $tcp_port ss_program="$(first_type ${type}-redir)" echolog "$(get_name $type) program is: $ss_program" old_ss_program=$(readlink -f "$TMP_PATH/bin/${type}-redir" 2>/dev/null) if [ "$old_ss_program" != "$ss_program" ]; then rm -rf "$TMP_PATH/bin/${type}-redir" fi for i in $(seq 1 $threads); do ln_start_bin $ss_program ${type}-redir -c $tcp_config_file done echolog "Main node:$(get_name $type) $threads Threads Started!" elif [ "$global_server_type" = "ss" ] || { [ "$global_server_type" = "ss-rust" ] && use_mihomo_for_ss_rust_client; }; then local mihomo_bin="$(first_type mihomo)" local ss_socks_port=0 [ -x "$mihomo_bin" ] || { echolog "Main node:Mihomo 内核不存在,请确认 /usr/bin/mihomo 或 /usr/libexec/mihomo 可执行。" return 1 } if [ "$_local" = "1" ] && [ "$LOCAL_SERVER" = "$GLOBAL_SERVER" ]; then ss_socks_port=$(uci_get_by_type socks5_proxy local_port 1080) fi prepare_ss_mihomo_runtime_config "$GLOBAL_SERVER" "$tcp_port" "$ss_socks_port" "$GLOBAL_SERVER" "redir" || return 1 local ss_workdir="$(get_ss_mihomo_workdir "$GLOBAL_SERVER")" ln_start_bin "$mihomo_bin" ssr-retcp -d "$ss_workdir" -f "$ss_workdir/config.yaml" if [ "$global_server_type" = "ss-rust" ]; then echolog "Main node:ss-rust 主程序不存在,已回退到 Mihomo (Shadowsocks) 启动。" else echolog "Main node:Mihomo (Shadowsocks) Started!" fi else gen_config_file $GLOBAL_SERVER $type 1 $tcp_port ss_program="$(first_type sslocal)" echolog "ShadowSocks program is: $ss_program" old_ss_program=$(readlink -f "$TMP_PATH/bin/${type}-redir" 2>/dev/null) if [ "$old_ss_program" != "$ss_program" ]; then rm -rf "$TMP_PATH/bin/${type}-redir" fi ln_start_bin $ss_program ${type}-redir -c $tcp_config_file echolog "Main node:Shadowsocks-rust Started!" fi ;; v2ray) gen_config_file $GLOBAL_SERVER $type 1 $tcp_port $socks_port ln_start_bin $(first_type xray) v2ray run -c $tcp_config_file echolog "Main node:$($(first_type xray) version | head -1) Started!" ;; trojan) gen_config_file $GLOBAL_SERVER $type 1 $tcp_port for i in $(seq 1 $threads); do ln_start_bin $(first_type $type) $type --config $tcp_config_file done echolog "Main node:$($(first_type $type) --version 2>&1 | head -1) , $threads Threads Started!" ;; naiveproxy) gen_config_file $GLOBAL_SERVER $type 1 $tcp_port ln_start_bin $(first_type naive) naive $tcp_config_file echolog "Main node:$($(first_type naive) --version 2>&1 | head -1) , $threads Threads Started!" ;; tuic) local mihomo_bin="$(first_type mihomo)" local tuic_socks_port=0 [ -x "$mihomo_bin" ] || { echolog "Main node:Mihomo 内核不存在,请确认 /usr/bin/mihomo 或 /usr/libexec/mihomo 可执行。" return 1 } if [ "$_local" = "1" ] && [ "$LOCAL_SERVER" = "$GLOBAL_SERVER" ]; then tuic_socks_port=$(uci_get_by_type socks5_proxy local_port 1080) fi prepare_tuic_runtime_config "$GLOBAL_SERVER" "$tcp_port" "$tuic_socks_port" "$GLOBAL_SERVER" "redir" || return 1 local tuic_workdir="$(get_tuic_workdir "$GLOBAL_SERVER")" ln_start_bin "$mihomo_bin" ssr-retcp -d "$tuic_workdir" -f "$tuic_workdir/config.yaml" echolog "Main node:Mihomo (TUIC) Started!" ;; shadowtls) if [ -z "$socks_port" ]; then gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" 0 chain else gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" $socks_port chain fi local chain_type=$(uci_get_by_name $GLOBAL_SERVER chain_type) case ${chain_type} in vmess) ln_start_bin $(first_type shadow-tls) shadow-tls config --config $chain_config_file ln_start_bin $(first_type xray) v2ray run -c $tcp_config_file echolog "Mian node:shadow-tls chain-to $($(first_type xray) --version) Started!" ;; sslocal) ln_start_bin $(first_type shadow-tls) shadow-tls config --config $chain_config_file ln_start_bin $(first_type sslocal) sslocal -c $tcp_config_file echolog "Main node:shadow-tls chain-to $($(first_type sslocal) --version) Started!" ;; esac ;; clash) local clash_socks_port=0 local mihomo_bin="$(first_type mihomo)" clash_socks_port="$(resolve_global_clash_socks_port)" if [ -z "$clash_socks_port" ] && [ "$_local" == "1" ] && [ "$LOCAL_SERVER" = "$GLOBAL_SERVER" ]; then clash_socks_port=$(uci_get_by_type socks5_proxy local_port 1080) fi [ -x "$mihomo_bin" ] || { echolog "Main node:Mihomo 内核不存在,请确认 /usr/bin/mihomo 或 /usr/libexec/mihomo 可执行。" return 1 } download_clash_config "$GLOBAL_SERVER" || { echolog "Main node:Clash 配置不可用,启动中止。" return 1 } prepare_clash_runtime_config "$GLOBAL_SERVER" "$tcp_port" "$clash_socks_port" || { echolog "Main node:Clash 运行配置生成失败,启动中止。" return 1 } local clash_workdir="$(get_clash_workdir "$GLOBAL_SERVER")" ln_start_bin "$mihomo_bin" ssr-retcp -d "$clash_workdir" -f "$clash_workdir/config.yaml" echolog "Main node:Mihomo (Clash) Started!" ;; socks5) local ipt2socks_bin="$(first_type ipt2socks)" local auth_opts="" [ -x "$ipt2socks_bin" ] || { echolog "Main node:Socks5 缺少 ipt2socks,无法启动透明代理。" return 1 } if [ "$(uci_get_by_name $GLOBAL_SERVER auth_enable 0)" = "1" ]; then auth_opts="-a $(uci_get_by_name $GLOBAL_SERVER username) -k $(uci_get_by_name $GLOBAL_SERVER password)" fi for i in $(seq 1 $threads); do ln_start_bin "$ipt2socks_bin" ipt2socks \ -T -R -r -b 0.0.0.0 -4 \ -s "$(uci_get_by_name $GLOBAL_SERVER server)" \ -p "$(uci_get_by_name $GLOBAL_SERVER server_port)" \ -l "$tcp_port" $auth_opts done echolog "Main node:Socks5 via IPT2Socks $threads Threads Started!" ;; esac redir_tcp=1 return 0 } load_config() { if [ -z "$switch_server" ]; then GLOBAL_SERVER=$(uci_get_by_type global global_server nil) else GLOBAL_SERVER=$switch_server fi if [ "$(uci_get_by_type socks5_proxy enabled 0)" == "1" ]; then uci -q set "$NAME.@socks5_proxy[0].server=same" else uci -q set "$NAME.@socks5_proxy[0].server=nil" fi if [ "$(uci_get_by_type http_proxy enabled 0)" == "1" ]; then uci -q set "$NAME.@http_proxy[0].server=same" else uci -q set "$NAME.@http_proxy[0].server=nil" fi uci -q commit "$NAME" if [ "$(uci_get_by_type socks5_proxy enabled 0)" == "1" ]; then # 只有开启 全局socks 才需要取值 LOCAL_SERVER=$(uci_get_by_type socks5_proxy server nil) else # 没有开启 设置为 nil LOCAL_SERVER=nil fi if [ "$(uci_get_by_type http_proxy enabled 0)" == "1" ]; then HTTP_SERVER=$(uci_get_by_type http_proxy server nil) else HTTP_SERVER=nil fi if [ "$GLOBAL_SERVER" == "nil" ]; then [ "$HTTP_SERVER" = "same" ] && HTTP_SERVER=nil mode="tcp,udp" _local="2" local_config_file=$TMP_PATH/tcp-udp-ssr-local.json start_local return 1 fi UDP_RELAY_SERVER=$GLOBAL_SERVER local udp_mode=$(get_udp_relay_mode "$GLOBAL_SERVER") tcp_config_file=$TMP_PATH/tcp-only-ssr-retcp.json case "$udp_mode" in disabled) echolog "提示:当前节点类型不提供 UDP 透明代理,也不会启用任何 UDP 规则,仅启用 TCP 透明代理。" mode="tcp" ARG_UDP="" ARG_UDP_RULES="-y" udp_config_file="" ;; native) mode="tcp,udp" tcp_config_file=$TMP_PATH/tcp-udp-ssr-retcp.json ARG_UDP="-u" ARG_UDP_RULES="" ;; split) mode="udp" udp_config_file=$TMP_PATH/udp-only-ssr-reudp.json ARG_UDP="-U" ARG_UDP_RULES="" start_udp mode="tcp" ;; esac case "$LOCAL_SERVER" in nil) _local="0" ;; "$GLOBAL_SERVER"|same) _local="1" LOCAL_SERVER=$GLOBAL_SERVER local_config_file=$TMP_PATH/tcp-udp-ssr-local.json if ! supports_builtin_socks "$(uci_get_by_name $GLOBAL_SERVER type)"; then start_local fi local_enable=0 ;; *) _local="2" local_config_file=$TMP_PATH/tcp-udp-ssr-local.json start_local ;; esac [ "$HTTP_SERVER" = "same" ] && HTTP_SERVER=$GLOBAL_SERVER return 0 } check_server() { ENABLE_SERVER=$(uci_get_by_type global global_server nil) if [ "$ENABLE_SERVER" == "nil" ]; then return 1 else local STYPE=$(uci_get_by_name $ENABLE_SERVER type nil) if [ "$STYPE" == "nil" ]; then local CFGID=$(uci_get_by_cfgid servers type nil) if [ "$CFGID" == "nil" ]; then uci_set_by_type global global_server 'nil' else uci_set_by_type global global_server $CFGID fi /etc/init.d/shadowsocksr restart fi fi } start_server() { [ "$(uci_get_by_type server_global enable_server 0)" == "0" ] && return 0 server_service() { [ "$(uci_get_by_name $1 enable 0)" == "0" ] && return 1 let server_count=server_count+1 if [ "$server_count" == "1" ]; then if [ "$USE_TABLES" = "nftables" ]; then # nftables / fw4 if nft list table inet fw4 >/dev/null 2>&1; then if ! nft list chain inet fw4 SSR-SERVER-RULE >/dev/null 2>&1; then nft add chain inet fw4 SSR-SERVER-RULE 2>/dev/null fi if ! nft list chain inet fw4 input 2>/dev/null | grep -q "jump SSR-SERVER-RULE"; then nft insert rule inet fw4 input jump SSR-SERVER-RULE comment \"SSR Server Input Hook\" 2>/dev/null fi nft flush chain inet fw4 SSR-SERVER-RULE 2>/dev/null fi elif [ "$USE_TABLES" = "iptables" ]; then # iptables / fw3 if ! (iptables-save -t filter | grep -q "SSR-SERVER-RULE"); then iptables -N SSR-SERVER-RULE iptables -t filter -I INPUT -j SSR-SERVER-RULE fi fi fi local node_type=$(uci_get_by_name $1 type) local type=$node_type if [ "$node_type" = "ss-rust" ]; then type="ss" fi case "$type" in ss | ssr) if [ "$node_type" = "ss" ] || { [ "$node_type" = "ss-rust" ] && use_mihomo_for_ss_rust_server; }; then local mihomo_bin="$(first_type mihomo)" [ -x "$mihomo_bin" ] || { echolog "Server:Mihomo 内核不存在,请确认 /usr/bin/mihomo 或 /usr/libexec/mihomo 可执行。" return 1 } prepare_ss_server_mihomo_runtime_config "$1" || return 1 local ss_server_workdir="$(get_ss_server_mihomo_workdir "$1")" ln_start_bin "$mihomo_bin" ss-server-$server_count -d "$ss_server_workdir" -f "$ss_server_workdir/config.yaml" if [ "$node_type" = "ss-rust" ]; then echolog "Server:ss-rust 主程序不存在,已回退到 Mihomo Shadowsocks Server$server_count 启动。" else echolog "Server:Mihomo Shadowsocks Server$server_count Started!" fi else gen_service_file ${type} $1 $TMP_PATH/ssr-server$server_count.json if [ "$node_type" = "ssr" ]; then ss_program="$(first_type ${type}-server)" elif [ "$node_type" = "ss-rust" ] || [ "$node_type" = "ss" ]; then ss_program="$(first_type ${type}server)" fi old_ss_program=$(readlink -f "$TMP_PATH/bin/${type}-server" 2>/dev/null) if [ "$old_ss_program" != "$ss_program" ]; then rm -rf "$TMP_PATH/bin/${type}-server" fi ln_start_bin $ss_program ${type}-server -c $TMP_PATH/ssr-server$server_count.json echolog "Server: $(get_name ${type}) Server$server_count Started!" fi ;; socks5) local listenip='-i 0.0.0.0' [ -e /proc/sys/net/ipv6 ] && listenip='-i ::' local username=$(uci_get_by_name $1 username) local password=$(uci_get_by_name $1 password) local auth_opts="" if [ -n "$username" ]; then auth_opts="-u $username" [ -n "$password" ] && auth_opts="$auth_opts -P $password" fi ln_start_bin $(first_type microsocks) microsocks $listenip -p $(uci_get_by_name $1 server_port) -1 $auth_opts ssr-server$server_count echolog "Server:Socks5 Server$server_count Started!" ;; esac server_port=$(uci_get_by_name $1 server_port) if [ "$USE_TABLES" = "nftables" ]; then # nftables / fw4 nft add rule inet fw4 SSR-SERVER-RULE tcp dport $server_port accept nft add rule inet fw4 SSR-SERVER-RULE udp dport $server_port accept elif [ "$USE_TABLES" = "iptables" ]; then # iptables / fw3 iptables -t filter -A SSR-SERVER-RULE -p tcp --dport $server_port -j ACCEPT iptables -t filter -A SSR-SERVER-RULE -p udp --dport $server_port -j ACCEPT fi return 0 } gen_serv_include() { local FWI=$(uci get firewall.shadowsocksr.path 2>/dev/null) [ -n "$FWI" ] || return 0 if [ ! -f $FWI ]; then echo '#!/bin/sh' >$FWI fi if [ "$USE_TABLES" = "nftables" ]; then # nftables / fw4 cat <<-'EOF' >>"$FWI" # 确保表存在 if nft list table inet fw4 >/dev/null 2>&1; then # 如果不存在 SSR-SERVER-RULE 链,则创建 if ! nft list chain inet fw4 SSR-SERVER-RULE >/dev/null 2>&1; then nft add chain inet fw4 SSR-SERVER-RULE 2>/dev/null fi # 从 input 链跳转到 SSR-SERVER-RULE(如果未添加) if ! nft list chain inet fw4 input | grep -q 'jump SSR-SERVER-RULE'; then nft insert rule inet fw4 input jump SSR-SERVER-RULE comment \"SSR Server Input Hook\" 2>/dev/null fi # 已存在则清空链 nft flush chain inet fw4 SSR-SERVER-RULE 2>/dev/null fi EOF elif [ "$USE_TABLES" = "iptables" ]; then # iptables / fw3 extract_rules() { echo "*filter" iptables-save -t filter | grep SSR-SERVER-RULE | sed -e "s/^-A INPUT/-I INPUT/" echo 'COMMIT' } cat <<-EOF >>$FWI iptables-save -c | grep -v "SSR-SERVER" | iptables-restore -c iptables-restore -n <<-EOT $(extract_rules) EOT EOF fi } config_load $NAME config_foreach server_service server_config gen_serv_include return 0 } start_switch() { if [ "$(uci_get_by_name $GLOBAL_SERVER type)" = "clash" ] || [ "$(uci_get_by_name $GLOBAL_SERVER type)" = "tuic" ]; then echolog "提示:Mihomo 托管节点不启用 SSR Plus 自动切换逻辑。" return 0 fi if [ "$(uci_get_by_type global enable_switch 0)" == "1" ]; then if [ -z "$switch_server" ]; then local switch_time=$(uci_get_by_type global switch_time)s local switch_timeout=$(uci_get_by_type global switch_timeout) service_start /usr/bin/ssr-switch start $switch_time $switch_timeout fi fi } start_monitor() { if [ $(uci_get_by_type global monitor_enable 1) == "1" ]; then let total_count=server_count+redir_tcp+redir_udp+kcp_enable_flag+local_enable+pdnsd_enable_flag if [ $total_count -gt 0 ]; then service_start /usr/bin/ssr-monitor $server_count $redir_tcp $redir_udp $kcp_enable_flag $local_enable $pdnsd_enable_flag fi fi } start_xhttp_addr() { local xhttp_addr_file="/etc/ssrplus/xhttp_address.txt" local tmp_file="/tmp/.xhttp_addr.tmp" # 收集所有节点的 download_address 值,去掉空行并去重排序 { for sec in "$GLOBAL_SERVER" "$UDP_RELAY_SERVER"; do local addr addr=$(uci_get_by_name "$sec" download_address) [ -n "$addr" ] && echo "$addr" done } | grep -v '^$' | sort -u > "$tmp_file" # 如果没有 download_address 地址,删除旧文件并退出 if [ ! -s "$tmp_file" ]; then [ -f "$xhttp_addr_file" ] && rm -f "$xhttp_addr_file" rm -f "$tmp_file" return 0 fi # 比较 MD5 判断 download_address 地址是否有变化 local md5_new md5_old md5_new=$(md5sum "$tmp_file" | awk '{print $1}') if [ -f "$xhttp_addr_file" ]; then md5_old=$(md5sum "$xhttp_addr_file" | awk '{print $1}') else md5_old="" fi # MD5 不同更新 download_address 地址文件 if [ "$md5_new" != "$md5_old" ]; then mv -f "$tmp_file" "$xhttp_addr_file" logger -t ssrplus-xhttp "download_address.txt updated" else rm -f "$tmp_file" fi } start_rules() { local server=$(get_host_ip $GLOBAL_SERVER) local local_port=$(get_default_node_local_port) local lan_ac_ips=$(uci_get_by_type access_control lan_ac_ips) local lan_ac_mode=$(uci_get_by_type access_control lan_ac_mode) if [ "$kcp_enable_flag" == "0" -a "$redir_udp" == "1" ]; then local udp_server=$(get_host_ip $UDP_RELAY_SERVER) local udp_local_port=$tmp_udp_port fi if [ -n "$lan_ac_ips" ]; then case "$lan_ac_mode" in w | W | b | B) local ac_ips="$lan_ac_mode$lan_ac_ips" ;; esac fi gfwmode() { case "$(normalize_run_mode)" in gfw) echo "-g" ;; router) echo "-r" ;; all) echo "-z" ;; esac } if [ "$(uci_get_by_type global dports)" == "3" ]; then local custom_ports=$(uci_get_by_name $GLOBAL_SERVER custom_ports) # custom_ports 存储了用户自定义的端口 if [ -n "$custom_ports" ]; then local proxyport="-m multiport --dports $custom_ports" fi else if [ "$(uci_get_by_type global dports 1)" == "2" ]; then local proxyport="-m multiport --dports 22,53,587,465,995,993,143,80,443,853,9418" fi fi get_arg_out() { case "$(uci_get_by_type access_control router_proxy 1)" in 1) echo "-o" ;; 2) echo "-O" ;; esac } if [ "$USE_TABLES" = "nftables" ]; then ARG_A="-A" # Restore nft persistence rules if [ -f "$BACKUP_FILE" ]; then mkdir -p "$PERSIST_DIR" mv "$BACKUP_FILE" "$PERSIST_FILE" rm -rf "$BACKUP_DIR" fi elif [ "$USE_TABLES" = "iptables" ]; then ARG_A="" fi /usr/share/shadowsocksr/gfw2ipset.sh /usr/bin/ssr-rules \ -s "$server" \ -l "$local_port" \ -S "$udp_server" \ -L "$udp_local_port" \ -a "$ac_ips" \ -i "/etc/ssrplus/china_ssr.txt" \ -b "$(uci_get_by_type access_control wan_bp_ips)" \ -w "$(uci_get_by_type access_control wan_fw_ips)" \ -B "$(uci_get_by_type access_control lan_bp_ips)" \ -p "$(uci_get_by_type access_control lan_fp_ips)" \ -G "$(uci_get_by_type access_control lan_gm_ips)" \ -m "$(uci_get_by_type access_control Interface)" \ -D "$proxyport" \ $(get_arg_out) $(gfwmode) $ARG_UDP $ARG_UDP_RULES $ARG_A return $? } start() { set_lock echolog "----------start------------" mkdir -p /var/run /var/lock /var/log $DNSMASQ_CONF_DIR $TMP_BIN_PATH $TMP_DNSMASQ_PATH $CLASH_CONFIG_DIR echo "conf-dir=${TMP_DNSMASQ_PATH}" >"$DNSMASQ_CONF_DIR/dnsmasq-ssrplus.conf" check_run_environment normalize_run_mode >/dev/null normalize_xray_protocol_nodes if load_config; then Start_Run start_http_proxy start_xhttp_addr start_rules start_dns # Restore ipsets after rules creation if [ "$HAS_IPSET" -eq 1 ]; then for setname in gfwlist china blacklist whitelist; do [ "$setname" = "gfwlist" ] && [ "$run_mode" != "gfw" ] && continue if [ -f "/tmp/ssrplus_save/${setname}.save" ]; then ipset restore -! < "/tmp/ssrplus_save/${setname}.save" 2>/dev/null fi done fi add_cron start_switch else echolog "未启动主节点,禁止连接的域名正在加载。" cat /etc/ssrplus/deny.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/address=\/&\//" >$TMP_DNSMASQ_PATH/denylist.conf echolog "禁止连接的域名加载完毕。" if [ "$(uci_get_by_type global adblock 0)" == "1" ]; then echolog "未启动主节点,广告过滤正在加载。" cp -f /etc/ssrplus/ad.conf $TMP_DNSMASQ_PATH/ if [ -f "$TMP_DNSMASQ_PATH/ad.conf" ]; then # Optimize: Batch filter using grep instead of looping sed for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list; do if [ -s "$list_file" ]; then # Clean list file (remove comments and empty lines) grep -vE '^\s*#|^\s*$' "$list_file" > "${list_file}.clean" if [ -s "${list_file}.clean" ]; then grep -v -F -f "${list_file}.clean" "$TMP_DNSMASQ_PATH/ad.conf" > "$TMP_DNSMASQ_PATH/ad.conf.tmp" mv "$TMP_DNSMASQ_PATH/ad.conf.tmp" "$TMP_DNSMASQ_PATH/ad.conf" fi rm -f "${list_file}.clean" fi done fi echolog "广告过滤加载完毕。" fi start_http_proxy fi /etc/init.d/dnsmasq restart >/dev/null 2>&1 check_server if [ "$USE_TABLES" = "nftables" ]; then local CURRENT_SERVER="$(uci_get_by_type global global_server nil)" if [ "$CURRENT_SERVER" != "nil" ]; then uci set shadowsocksr.@global[0].old_global_server="$CURRENT_SERVER" uci commit shadowsocksr fi fi start_server start_monitor clean_log echolog "-----------end------------" unset_lock } boot() { echolog "boot!" mkdir -p /var/run /var/lock /var/log $DNSMASQ_CONF_DIR $TMP_BIN_PATH $TMP_DNSMASQ_PATH start } stop() { unlock set_lock check_run_environment # Save ipsets before stopping to persist transparent proxy state if [ "$HAS_IPSET" -eq 1 ]; then mkdir -p /tmp/ssrplus_save local run_mode="$(uci_get_by_type global run_mode)" if [ "$run_mode" = "gfw" ]; then ipset save gfwlist > /tmp/ssrplus_save/gfwlist.save 2>/dev/null fi for setname in china blacklist whitelist; do ipset save $setname > /tmp/ssrplus_save/$setname.save 2>/dev/null done fi if [ "$USE_TABLES" = "nftables" ]; then # Save nft rules before stopping to persist transparent proxy state if [ -f "$PERSIST_FILE" ]; then mkdir -p "$BACKUP_DIR" mv "$PERSIST_FILE" "$BACKUP_FILE" fi /usr/bin/ssr-rules -K local OLD_SERVER="$(uci_get_by_type global old_global_server nil)" local NEW_SERVER="$(uci_get_by_type global global_server nil)" if [ "$OLD_SERVER" != "nil" ] && [ "$NEW_SERVER" != "nil" ] && [ "$OLD_SERVER" != "$NEW_SERVER" ]; then /usr/bin/ssr-rules -X fi uci delete shadowsocksr.@global[0].old_global_server 2>/dev/null uci commit shadowsocksr fi /usr/bin/ssr-rules -f local srulecount=0 if [ "$USE_TABLES" = "nftables" ]; then # nftables / fw4 #local srulecount=$(nft list ruleset 2>/dev/null | grep -c 'SSR-SERVER-RULE') if nft list chain inet fw4 SSR-SERVER-RULE >/dev/null 2>&1; then srulecount=$(nft list chain inet fw4 SSR-SERVER-RULE | grep SSR-SERVER-RULE | wc -l) fi elif [ "$USE_TABLES" = "iptables" ]; then # iptables / fw3 srulecount=$(iptables -L | grep SSR-SERVER-RULE | wc -l) fi if [ $srulecount -gt 0 ]; then if [ "$USE_TABLES" = "nftables" ]; then # nftables / fw4 if nft list table inet fw4 >/dev/null 2>&1; then if nft list chain inet fw4 SSR-SERVER-RULE >/dev/null 2>&1; then for handle in $(nft --handle list chain inet fw4 input 2>/dev/null | \ grep 'jump SSR-SERVER-RULE' | awk '{for(i=1;i<=NF;i++) if($i=="handle") print $(i+1)}'); do nft delete rule inet fw4 input handle $handle 2>/dev/null || true done nft flush chain inet fw4 SSR-SERVER-RULE 2>/dev/null || true nft delete chain inet fw4 SSR-SERVER-RULE 2>/dev/null || true fi fi elif [ "$USE_TABLES" = "iptables" ]; then # iptables / fw3 if iptables-save -t filter | grep -q "SSR-SERVER-RULE"; then logger -t ssr-rules "Flushing and deleting SSR-SERVER-RULE chain (iptables)" iptables -F SSR-SERVER-RULE 2>/dev/null || true iptables -t filter -D INPUT -j SSR-SERVER-RULE 2>/dev/null || true iptables -X SSR-SERVER-RULE 2>/dev/null || true fi fi fi if [ -z "$switch_server" ]; then ps_list | grep -v "grep" | grep ssr-switch | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 & rm -f /var/lock/ssr-switch.lock killall -q -9 kcptun-client fi ps_list | grep -v "grep" | grep ssr-monitor | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 & ps_list | grep -v "grep" | grep ssr-rules | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 & ps_list | grep -v "grep" | grep "sleep 0000" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 & ( \ # Graceful kill first, so programs have the chance to stop its subprocesses ps_list | grep -v "grep" | grep "$TMP_PATH" | awk '{print $1}' | xargs kill >/dev/null 2>&1 ; \ sleep 3s; \ # Force kill hanged programs ps_list | grep -v "grep" | grep "$TMP_PATH" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 ; \ ) killall -q -9 v2ray-plugin obfs-local xray-plugin shadow-tls rm -f /var/lock/ssr-monitor.lock if [ "$(uci -q get "dhcp.@dnsmasq[0]._unused_ssrp_changed")" = "1" ]; then uci -q del "dhcp.@dnsmasq[0].noresolv" uci -q del_list "dhcp.@dnsmasq[0].server"="127.0.0.1#$china_dns_port" uci -q rename "dhcp.@dnsmasq[0]._orig_noresolv"="noresolv" uci -q rename "dhcp.@dnsmasq[0]._orig_server"="server" uci -q del "dhcp.@dnsmasq[0]._unused_ssrp_changed" uci -q commit "dhcp" fi if [ -f "$DNSMASQ_CONF_DIR/dnsmasq-ssrplus.conf" ]; then rm -rf $DNSMASQ_CONF_DIR/dnsmasq-ssrplus.conf \ $TMP_DNSMASQ_PATH \ $TMP_PATH/*-ssr-*.json \ $TMP_PATH/3proxy* \ $TMP_PATH/clash-* \ $TMP_PATH/ssr-server*.json \ $TMP_PATH/*-config-*.json /etc/init.d/dnsmasq restart >/dev/null 2>&1 fi del_cron unset_lock } reset() { stop set_lock rm -rf /etc/config/shadowsocksr $LOG_FILE [ -f "/etc/ssrplus/xhttp_address.txt" ] && rm -f /etc/ssrplus/xhttp_address.txt touch /etc/config/shadowsocksr $LOG_FILE cp /usr/share/shadowsocksr/shadowsocksr.config /etc/config/shadowsocksr unset_lock }