From d95661f295108772b8551fe3eb16b02fdc55df1a Mon Sep 17 00:00:00 2001 From: Arvin <62139570+wnlen@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:41:03 +0800 Subject: [PATCH] Fix TUN policy-routing status detection Unify TUN status classification around policy-routing evidence and wait for the controller before post-restart verification. Add a regression check for Issue #303. --- scripts/core/clashctl.sh | 69 +++++++++++---- .../dev/check-tun-policy-routing-status.sh | 86 +++++++++++++++++++ 2 files changed, 140 insertions(+), 15 deletions(-) create mode 100755 scripts/dev/check-tun-policy-routing-status.sh diff --git a/scripts/core/clashctl.sh b/scripts/core/clashctl.sh index 93a0c7f..cad2087 100644 --- a/scripts/core/clashctl.sh +++ b/scripts/core/clashctl.sh @@ -621,11 +621,14 @@ status_tun_effective_status() { return 0 fi - result="$(tun_effective_check 2>/dev/null || true)" + result="$(tun_current_effective_result 2>/dev/null || true)" case "${result:-unknown}" in ok) echo "effective" ;; + policy-routing-likely-effective) + echo "likely-effective" + ;; *) echo "ineffective" ;; @@ -638,6 +641,7 @@ status_tun_effective_text() { case "$s" in effective) echo "🐱 已生效" ;; + likely-effective) echo "🟡 很可能已生效" ;; *) echo "❗ 未生效" ;; esac } @@ -2001,7 +2005,7 @@ print_tun_off_feedback() { ui_blank } -tun_on_verify_result() { +tun_resolve_effective_result() { local result auto_redirect result="${1:-unknown}" @@ -2018,13 +2022,31 @@ tun_on_verify_result() { auto_redirect="$(runtime_config_tun_auto_redirect 2>/dev/null || echo false)" if [ "${auto_redirect:-false}" = "true" ] && tun_has_policy_routing_evidence 2>/dev/null; then - echo "policy-routing-likely-effective" + if tun_log_tun_source_line >/dev/null 2>&1; then + echo "ok" + else + echo "policy-routing-likely-effective" + fi return 0 fi echo "$result" } +tun_current_effective_result() { + local result + + result="$(tun_effective_check 2>/dev/null || true)" + [ -n "${result:-}" ] || result="unknown" + tun_resolve_effective_result "$result" +} + +# Backward-compatible helper retained for callers that already captured the +# low-level Tun result. +tun_on_verify_result() { + tun_resolve_effective_result "${1:-unknown}" +} + status_subscription_health_summary() { local active health fail_count auto_disabled @@ -2146,8 +2168,17 @@ status_risk_reason_lines() { fi fi - if [ "$tun_enabled" = "true" ] && [ "$tun_effective" != "effective" ]; then - echo "• Tun 未生效" + if [ "$tun_enabled" = "true" ]; then + case "$tun_effective" in + effective) + ;; + likely-effective) + echo "• Tun policy routing 已安装,尚未观察到明确 Tun 流量" + ;; + *) + echo "• Tun 未生效" + ;; + esac fi if [ "$tun_container_mode" = "container-risky" ]; then @@ -5343,11 +5374,17 @@ cmd_tun_status() { echo "🚨 环境检查:当前不满足基础开启条件" fi - if [ "$effective_status" = "effective" ]; then - echo "🐱 已生效" - else - echo "❗ 未生效" - fi + case "$effective_status" in + effective) + echo "🐱 已生效" + ;; + likely-effective) + echo "🟡 很可能已生效(已检测到 Tun 与 policy routing,尚未观察到明确 Tun 流量)" + ;; + *) + echo "❗ 未生效" + ;; + esac if [ "$enabled" = "true" ]; then echo "👉 下一步:clash tun doctor" @@ -5425,11 +5462,13 @@ cmd_tun_on() { if status_is_running; then service_restart + if ! wait_runtime_controller_ready 15; then + ui_warn "控制器未在预期时间内就绪,将保留 Tun 开启状态并报告当前验证结果" + fi fi - verify_result="$(tun_effective_check 2>/dev/null || true)" + verify_result="$(tun_current_effective_result 2>/dev/null || true)" [ -n "${verify_result:-}" ] || verify_result="unknown" - verify_result="$(tun_on_verify_result "$verify_result")" case "$verify_result" in ok) @@ -5715,7 +5754,7 @@ doctor_tun_checks() { echo "【生效验证】" if [ "$(tun_enabled 2>/dev/null || echo false)" = "true" ]; then - effective_result="$(tun_effective_check 2>/dev/null || true)" + effective_result="$(tun_current_effective_result 2>/dev/null || true)" primary_reason="$(tun_doctor_primary_reason "$effective_result" "$backend" "$route_takeover")" || { rc=$? tun_doctor_report_failure "tun_doctor_primary_reason" "$rc" "derive primary reason" @@ -6476,7 +6515,7 @@ tun_recommendation_lines() { fi if [ "$enabled" = "true" ]; then - effective_result="$(tun_effective_check 2>/dev/null || true)" + effective_result="$(tun_current_effective_result 2>/dev/null || true)" [ -n "${effective_result:-}" ] || effective_result="unknown" [ -n "${primary_reason:-}" ] || primary_reason="$(tun_doctor_primary_reason "$effective_result" "$backend" "$route_takeover")" @@ -6625,7 +6664,7 @@ tun_problem_lines() { fi if [ "$enabled" = "true" ]; then - effective_result="$(tun_effective_check 2>/dev/null || true)" + effective_result="$(tun_current_effective_result 2>/dev/null || true)" if [ "$effective_result" != "ok" ]; then if tun_has_policy_routing_evidence 2>/dev/null; then if tun_log_tun_source_line >/dev/null 2>&1; then diff --git a/scripts/dev/check-tun-policy-routing-status.sh b/scripts/dev/check-tun-policy-routing-status.sh new file mode 100755 index 0000000..871f20b --- /dev/null +++ b/scripts/dev/check-tun-policy-routing-status.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +source_clashctl_for_tests() { + set -- "" + # Source the real functions; suppress the no-arg usage printed by the command dispatcher. + source "$PROJECT_DIR/scripts/core/clashctl.sh" >/dev/null +} + +source_clashctl_for_tests + +assert_equal() { + local name="$1" + local expected="$2" + local actual="$3" + + if [ "$actual" != "$expected" ]; then + echo "not ok - $name: got '$actual', expected '$expected'" >&2 + return 1 + fi + + echo "ok - $name" +} + +# Reproduce Issue #303: policy routing sends both direct-looking probes through +# Tun, so comparing their public IPs alone reports traffic-same-as-host. +tun_enabled() { printf 'true\n'; } +runtime_config_tun_enabled() { printf 'true\n'; } +status_is_running() { return 0; } +proxy_controller_reachable() { return 0; } +tun_public_ip_without_proxy_env() { printf '203.0.113.10\n'; } +tun_public_ip_with_current_route() { printf '203.0.113.10\n'; } +runtime_config_tun_auto_redirect() { printf 'true\n'; } +tun_has_policy_routing_evidence() { return 0; } +tun_log_tun_source_line() { printf '[TCP] 28.0.0.1:1234 --> example.com:443\n'; } + +assert_equal \ + "accepts policy routing with observed Tun traffic" \ + "ok" \ + "$(tun_current_effective_result)" + +# The same classifier must drive both status commands. Without observed traffic, +# keep the result explicit instead of presenting a false negative. +tun_log_tun_source_line() { return 1; } +assert_equal \ + "reports policy routing without traffic as likely effective" \ + "policy-routing-likely-effective" \ + "$(tun_current_effective_result)" +assert_equal \ + "maps likely policy routing to a distinct status" \ + "likely-effective" \ + "$(status_tun_effective_status)" + +# Reproduce the restart race: verification must happen only after waiting for +# the controller following service_restart. +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +events_file="$tmp_dir/events" +: > "$events_file" + +record_event() { + printf '%s\n' "$1" >> "$events_file" +} + +guard_sudo_on_user_install() { return 0; } +prepare() { :; } +tun_container_mode() { printf 'host\n'; } +tun_container_risk_reason() { :; } +can_manage_tun_safely() { return 0; } +tun_kernel_support_level() { printf 'full\n'; } +sync_tun_target_state() { return 0; } +service_restart() { record_event restart; } +wait_runtime_controller_ready() { record_event wait; return 0; } +tun_current_effective_result() { record_event verify; printf 'ok\n'; } +mark_tun_last_action() { :; } +mark_tun_last_verification() { :; } +print_tun_on_feedback() { :; } + +cmd_tun_on +assert_equal \ + "waits for controller before Tun verification" \ + "restart,wait,verify" \ + "$(paste -sd, "$events_file")"