From 6366b687a48becb581d5c8fc264c44cc899b5f00 Mon Sep 17 00:00:00 2001 From: leochien0102 Date: Mon, 27 Jul 2026 13:44:22 +0800 Subject: [PATCH 1/3] luci-app-ssr-plus: wait for internet connectivity before starting at boot At boot the service could start before the WAN link was usable. Node domains then failed to resolve via get_host_ip(), leaving the service "running" while no traffic could actually pass. Add a configurable startup connectivity check that gates the boot path only: - New "startup_check" UCI section, exposed under Advanced Settings - Two probe methods: DNS resolution (default) and ICMP ping. DNS matches what the service actually depends on at startup; ping cannot prove the resolver is ready - Configurable probe servers, resolve domain, retry count and interval - Runs from boot() only, gated by SSR_BOOT. Manual restarts, ssr-monitor restarts and component upgrades are never delayed: the node IP is already persisted in UCI by then, so the check would add nothing - On timeout the service starts anyway, so the check can never block startup indefinitely Also drop the 99-ssrplus-pppoe hotplug script. It slept a fixed 10s after a PPPoE ifup and restarted the service, which this check supersedes with an actual readiness probe that works on any WAN type, including single-arm setups with no WAN interface at all. --- luci-app-ssr-plus/Makefile | 3 - .../model/cbi/shadowsocksr/advanced.lua | 33 ++++++++++ luci-app-ssr-plus/po/templates/ssr-plus.pot | 60 ++++++++++++++++++ luci-app-ssr-plus/po/zh_Hans/ssr-plus.po | 63 +++++++++++++++++++ .../root/etc/hotplug.d/iface/99-ssrplus-pppoe | 22 ------- .../root/etc/init.d/shadowsocksr | 60 ++++++++++++++++++ .../root/etc/uci-defaults/luci-ssr-plus | 11 ++++ .../share/shadowsocksr/shadowsocksr.config | 9 +++ 8 files changed, 236 insertions(+), 25 deletions(-) delete mode 100644 luci-app-ssr-plus/root/etc/hotplug.d/iface/99-ssrplus-pppoe diff --git a/luci-app-ssr-plus/Makefile b/luci-app-ssr-plus/Makefile index 076eb0c444c..f85525f14dd 100644 --- a/luci-app-ssr-plus/Makefile +++ b/luci-app-ssr-plus/Makefile @@ -213,9 +213,6 @@ define Package/$(PKG_NAME)/install chmod 0755 "$(1)/usr/bin/ssr-rules" 2>/dev/null || true; \ chmod 0755 "$(1)/usr/bin/ssr-switch" 2>/dev/null || true; \ fi - if [ -f "$(1)/etc/hotplug.d/iface/99-ssrplus-pppoe" ]; then \ - chmod 0755 "$(1)/etc/hotplug.d/iface/99-ssrplus-pppoe" 2>/dev/null || true; \ - fi endef # call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua index d43a1e8d706..ddb10ca5d28 100644 --- a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua +++ b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua @@ -168,6 +168,39 @@ o.write = function() luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers")) end +-- [[ Startup Internet Check ]]-- +s = m:section(TypedSection, "startup_check", translate("Startup Connectivity Check"), translate("On boot, wait until the internet is reachable before starting the service, so that node domains can be resolved when the WAN link (e.g. PPPoE) comes up late. Manual restarts are never delayed. Disable this if SSR Plus is only used as a LAN-only proxy.")) +s.anonymous = true + +o = s:option(Flag, "enabled", translate("Enable")) +o.default = "1" +o.rmempty = false + +o = s:option(ListValue, "method", translate("Check Method"), translate("DNS resolution matches what the service actually needs at startup; ICMP ping is faster but does not prove that DNS is ready.")) +o:value("dns", translate("DNS resolution")) +o:value("ping", translate("ICMP ping")) +o.default = "dns" +o:depends("enabled", "1") + +o = s:option(DynamicList, "host", translate("Check Servers"), translate("DNS servers to query, or hosts to ping. The check passes as soon as any one of them responds.")) +o.datatype = "ipaddr" +o:depends("enabled", "1") + +o = s:option(Value, "domain", translate("Resolve Domain"), translate("Domain name used for the DNS resolution test.")) +o.datatype = "hostname" +o.default = "www.baidu.com" +o:depends({enabled = "1", method = "dns"}) + +o = s:option(Value, "retry", translate("Max Retries")) +o.datatype = "uinteger" +o.default = 30 +o:depends("enabled", "1") + +o = s:option(Value, "interval", translate("Retry Interval(second)"), translate("Maximum wait is Max Retries multiplied by Retry Interval.")) +o.datatype = "uinteger" +o.default = 2 +o:depends("enabled", "1") + -- [[ SOCKS5 Proxy ]]-- s = m:section(TypedSection, "socks5_proxy", translate("Global SOCKS5 Proxy Server")) s.anonymous = true diff --git a/luci-app-ssr-plus/po/templates/ssr-plus.pot b/luci-app-ssr-plus/po/templates/ssr-plus.pot index 830e57e64a3..b0a18a152d1 100644 --- a/luci-app-ssr-plus/po/templates/ssr-plus.pot +++ b/luci-app-ssr-plus/po/templates/ssr-plus.pot @@ -3362,3 +3362,63 @@ msgstr "" #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1667 msgid "xudpProxyUDP443" msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:171 +msgid "Startup Connectivity Check" +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:171 +msgid "" +"On boot, wait until the internet is reachable before starting the service, " +"so that node domains can be resolved when the WAN link (e.g. PPPoE) comes up " +"late. Manual restarts are never delayed. Disable this if SSR Plus is only " +"used as a LAN-only proxy." +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:178 +msgid "Check Method" +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:178 +msgid "" +"DNS resolution matches what the service actually needs at startup; ICMP ping " +"is faster but does not prove that DNS is ready." +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:179 +msgid "DNS resolution" +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:180 +msgid "ICMP ping" +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:184 +msgid "Check Servers" +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:184 +msgid "" +"DNS servers to query, or hosts to ping. The check passes as soon as any one " +"of them responds." +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:188 +msgid "Resolve Domain" +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:188 +msgid "Domain name used for the DNS resolution test." +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:193 +msgid "Max Retries" +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:198 +msgid "Retry Interval(second)" +msgstr "" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:198 +msgid "Maximum wait is Max Retries multiplied by Retry Interval." +msgstr "" diff --git a/luci-app-ssr-plus/po/zh_Hans/ssr-plus.po b/luci-app-ssr-plus/po/zh_Hans/ssr-plus.po index 5382a3f32a9..7ed901becb9 100644 --- a/luci-app-ssr-plus/po/zh_Hans/ssr-plus.po +++ b/luci-app-ssr-plus/po/zh_Hans/ssr-plus.po @@ -3394,6 +3394,69 @@ msgstr "UDP 最大并发连接数" msgid "xudpProxyUDP443" msgstr "对被代理的 UDP/443 流量处理方式" +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:171 +msgid "Startup Connectivity Check" +msgstr "启动前连通性检查" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:171 +msgid "" +"On boot, wait until the internet is reachable before starting the service, " +"so that node domains can be resolved when the WAN link (e.g. PPPoE) comes up " +"late. Manual restarts are never delayed. Disable this if SSR Plus is only " +"used as a LAN-only proxy." +msgstr "" +"开机时等待互联网可达后再启动服务,避免 WAN 链路(如 PPPoE)拨号较慢时节点域名" +"解析失败。手动重启不会被延迟。若 SSR Plus 仅用作纯内网代理,可关闭此项。" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:178 +msgid "Check Method" +msgstr "检查方式" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:178 +msgid "" +"DNS resolution matches what the service actually needs at startup; ICMP ping " +"is faster but does not prove that DNS is ready." +msgstr "" +"DNS 解析对应服务启动时真正依赖的条件;ICMP ping 更快,但无法证明 DNS 已就绪。" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:179 +msgid "DNS resolution" +msgstr "DNS 解析" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:180 +msgid "ICMP ping" +msgstr "ICMP Ping" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:184 +msgid "Check Servers" +msgstr "检查服务器" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:184 +msgid "" +"DNS servers to query, or hosts to ping. The check passes as soon as any one " +"of them responds." +msgstr "用于查询的 DNS 服务器或 Ping 目标地址。任意一个响应即视为通过。" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:188 +msgid "Resolve Domain" +msgstr "解析测试域名" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:188 +msgid "Domain name used for the DNS resolution test." +msgstr "用于 DNS 解析测试的域名。" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:193 +msgid "Max Retries" +msgstr "最大重试次数" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:198 +msgid "Retry Interval(second)" +msgstr "重试间隔(秒)" + +#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:198 +msgid "Maximum wait is Max Retries multiplied by Retry Interval." +msgstr "最长等待时间为最大重试次数乘以重试间隔。" + #~ msgid "CHECK AND UPDATE" #~ msgstr "检查并更新" diff --git a/luci-app-ssr-plus/root/etc/hotplug.d/iface/99-ssrplus-pppoe b/luci-app-ssr-plus/root/etc/hotplug.d/iface/99-ssrplus-pppoe deleted file mode 100644 index fc969f71110..00000000000 --- a/luci-app-ssr-plus/root/etc/hotplug.d/iface/99-ssrplus-pppoe +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -[ "$ACTION" = "ifup" ] || exit 0 -[ "$INTERFACE" = "wan" ] || exit 0 - -WAN_PROTO="$(uci -q get network.wan.proto 2>/dev/null)" -[ "$WAN_PROTO" = "pppoe" ] || exit 0 - -LOCK_FILE="/var/run/ssrplus-pppoe-hotplug.lock" - -( - if ! mkdir "$LOCK_FILE" 2>/dev/null; then - exit 0 - fi - - trap 'rmdir "$LOCK_FILE" 2>/dev/null' EXIT INT TERM - - sleep 10 - /etc/init.d/shadowsocksr restart >/dev/null 2>&1 -) >/dev/null 2>&1 & - -exit 0 diff --git a/luci-app-ssr-plus/root/etc/init.d/shadowsocksr b/luci-app-ssr-plus/root/etc/init.d/shadowsocksr index 41df5c1c267..f219f4e2531 100755 --- a/luci-app-ssr-plus/root/etc/init.d/shadowsocksr +++ b/luci-app-ssr-plus/root/etc/init.d/shadowsocksr @@ -2920,6 +2920,64 @@ start_rules() { return $? } +startup_check_probe() { + local method="$1" + local host="$2" + local domain="$3" + + case "$method" in + ping) + ping -c1 -W2 "$host" >/dev/null 2>&1 + ;; + *) + if command -v timeout >/dev/null 2>&1; then + timeout 3 nslookup "$domain" "$host" >/dev/null 2>&1 + else + nslookup "$domain" "$host" >/dev/null 2>&1 + fi + ;; + esac +} + +wait_for_internet() { + local method hosts domain max_retries interval retry host + + # Only gate the boot path: a manual restart must not block the LuCI UI. + [ "${SSR_BOOT:-0}" = "1" ] || return 0 + [ "$(uci_get_by_type startup_check enabled 1)" = "1" ] || return 0 + + hosts="$(uci_get_by_type startup_check host "223.5.5.5 119.29.29.29")" + # Unquoted on purpose: word splitting collapses a whitespace-only list to "" + hosts="$(echo $hosts)" + [ -n "$hosts" ] || return 0 + + method="$(uci_get_by_type startup_check method dns)" + domain="$(uci_get_by_type startup_check domain www.baidu.com)" + max_retries="$(uci_get_by_type startup_check retry 30)" + interval="$(uci_get_by_type startup_check interval 2)" + + # Guard against blank/invalid values left behind by the UI + echo "$max_retries" | grep -Eq '^[0-9]+$' || max_retries=30 + echo "$interval" | grep -Eq '^[0-9]+$' || interval=2 + [ "$method" = "ping" ] || [ -n "$domain" ] || domain="www.baidu.com" + + retry=0 + while [ "$retry" -lt "$max_retries" ]; do + for host in $hosts; do + if startup_check_probe "$method" "$host" "$domain"; then + echolog "Internet is reachable via $host ($method, attempt $((retry + 1)))" + return 0 + fi + done + + retry=$((retry + 1)) + echolog "Waiting for internet access... ($retry/$max_retries)" + sleep "$interval" + done + + echolog "Warning: Internet access timeout (${max_retries} retries), starting anyway" +} + start() { local run_mode local CURRENT_SERVER @@ -2929,6 +2987,7 @@ start() { set_lock echolog "----------start------------" + wait_for_internet 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 @@ -3023,6 +3082,7 @@ start() { boot() { echolog "boot!" mkdir -p /var/run /var/lock /var/log "$DNSMASQ_CONF_DIR" "$TMP_BIN_PATH" "$TMP_DNSMASQ_PATH" + SSR_BOOT=1 start } diff --git a/luci-app-ssr-plus/root/etc/uci-defaults/luci-ssr-plus b/luci-app-ssr-plus/root/etc/uci-defaults/luci-ssr-plus index 3e69e08aeb4..d125200c0c3 100755 --- a/luci-app-ssr-plus/root/etc/uci-defaults/luci-ssr-plus +++ b/luci-app-ssr-plus/root/etc/uci-defaults/luci-ssr-plus @@ -68,6 +68,17 @@ if [ -s "/etc/config/shadowsocksr" ]; then uci -q set shadowsocksr.@global_xray_fragment[0].noise='0' fi + if ! uci -q get shadowsocksr.@startup_check[0] > /dev/null; then + uci -q add shadowsocksr startup_check + uci -q set shadowsocksr.@startup_check[0].enabled='1' + uci -q set shadowsocksr.@startup_check[0].method='dns' + uci -q add_list shadowsocksr.@startup_check[0].host='223.5.5.5' + uci -q add_list shadowsocksr.@startup_check[0].host='119.29.29.29' + uci -q set shadowsocksr.@startup_check[0].domain='www.baidu.com' + uci -q set shadowsocksr.@startup_check[0].retry='30' + uci -q set shadowsocksr.@startup_check[0].interval='2' + fi + if ! uci -q get shadowsocksr.@global[0].component_mirror > /dev/null; then uci -q set shadowsocksr.@global[0].component_mirror='direct' fi diff --git a/luci-app-ssr-plus/root/usr/share/shadowsocksr/shadowsocksr.config b/luci-app-ssr-plus/root/usr/share/shadowsocksr/shadowsocksr.config index 819ce7641e4..ab11594b86f 100644 --- a/luci-app-ssr-plus/root/usr/share/shadowsocksr/shadowsocksr.config +++ b/luci-app-ssr-plus/root/usr/share/shadowsocksr/shadowsocksr.config @@ -59,6 +59,15 @@ config global_xray_fragment option fragment '0' option noise '0' +config startup_check + option enabled '1' + option method 'dns' + list host '223.5.5.5' + list host '119.29.29.29' + option domain 'www.baidu.com' + option retry '30' + option interval '2' + config clash_client_group option enabled '0' option remarks '' From a29c9fc2fb134977b8e925d2d988a330f3094f43 Mon Sep 17 00:00:00 2001 From: leochien0102 Date: Mon, 17 Aug 2026 10:45:24 +0800 Subject: [PATCH 2/3] luci-app-ssr-plus: harden the startup connectivity check - Skip the boot wait when no main node is configured: a nil global_server means the service has nothing to resolve at boot - Use ping6 for IPv6 probe hosts, as busybox ping is IPv4-only - Clamp a zero retry interval to 1s so the probe loop never spins - Run the wait after check_run_environment so local environment problems surface before the network wait --- luci-app-ssr-plus/root/etc/init.d/shadowsocksr | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/luci-app-ssr-plus/root/etc/init.d/shadowsocksr b/luci-app-ssr-plus/root/etc/init.d/shadowsocksr index f219f4e2531..16d3cee5f94 100755 --- a/luci-app-ssr-plus/root/etc/init.d/shadowsocksr +++ b/luci-app-ssr-plus/root/etc/init.d/shadowsocksr @@ -2927,7 +2927,11 @@ startup_check_probe() { case "$method" in ping) - ping -c1 -W2 "$host" >/dev/null 2>&1 + if echo "$host" | grep -q ':'; then + ping6 -c1 -W2 "$host" >/dev/null 2>&1 + else + ping -c1 -W2 "$host" >/dev/null 2>&1 + fi ;; *) if command -v timeout >/dev/null 2>&1; then @@ -2945,6 +2949,8 @@ wait_for_internet() { # Only gate the boot path: a manual restart must not block the LuCI UI. [ "${SSR_BOOT:-0}" = "1" ] || return 0 [ "$(uci_get_by_type startup_check enabled 1)" = "1" ] || return 0 + # 未配置主节点相当于未启用服务,没有需要解析的域名,无需等待 + [ "$(uci_get_by_type global global_server nil)" != "nil" ] || return 0 hosts="$(uci_get_by_type startup_check host "223.5.5.5 119.29.29.29")" # Unquoted on purpose: word splitting collapses a whitespace-only list to "" @@ -2959,6 +2965,7 @@ wait_for_internet() { # Guard against blank/invalid values left behind by the UI echo "$max_retries" | grep -Eq '^[0-9]+$' || max_retries=30 echo "$interval" | grep -Eq '^[0-9]+$' || interval=2 + [ "$interval" -lt 1 ] && interval=1 [ "$method" = "ping" ] || [ -n "$domain" ] || domain="www.baidu.com" retry=0 @@ -2987,10 +2994,10 @@ start() { set_lock echolog "----------start------------" - wait_for_internet 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 + wait_for_internet normalize_run_mode >/dev/null normalize_xray_protocol_nodes From 60eee299aca0f298f9e72a5e61c81b19d7544aa3 Mon Sep 17 00:00:00 2001 From: leochien0102 Date: Fri, 14 Aug 2026 09:01:45 +0800 Subject: [PATCH 3/3] luci-app-ssr-plus: fall back to resolveip for the startup DNS probe busybox does not always ship the nslookup applet, so the startup connectivity check could silently fail on minimal builds. Prefer nslookup for a targeted query of the configured server and fall back to resolveip, which ships with netifd and is always present, resolving via the system resolver - the same path get_host_ip() uses at startup. --- luci-app-ssr-plus/root/etc/init.d/shadowsocksr | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/luci-app-ssr-plus/root/etc/init.d/shadowsocksr b/luci-app-ssr-plus/root/etc/init.d/shadowsocksr index 16d3cee5f94..749d1f723dc 100755 --- a/luci-app-ssr-plus/root/etc/init.d/shadowsocksr +++ b/luci-app-ssr-plus/root/etc/init.d/shadowsocksr @@ -2934,10 +2934,19 @@ startup_check_probe() { fi ;; *) - if command -v timeout >/dev/null 2>&1; then - timeout 3 nslookup "$domain" "$host" >/dev/null 2>&1 + # Prefer nslookup, which queries the configured server directly and so + # does not depend on /etc/resolv.conf being populated yet. busybox does + # not always ship the nslookup applet, so fall back to resolveip, which + # ships with netifd and is always present, resolving via the system + # resolver - the same path get_host_ip() relies on at startup. + if command -v nslookup >/dev/null 2>&1; then + if command -v timeout >/dev/null 2>&1; then + timeout 3 nslookup "$domain" "$host" >/dev/null 2>&1 + else + nslookup "$domain" "$host" >/dev/null 2>&1 + fi else - nslookup "$domain" "$host" >/dev/null 2>&1 + resolveip -4 -t 3 "$domain" >/dev/null 2>&1 fi ;; esac