diff --git a/scripts/core/clashctl.sh b/scripts/core/clashctl.sh index 8dd31bc..75c7b1e 100644 --- a/scripts/core/clashctl.sh +++ b/scripts/core/clashctl.sh @@ -4916,6 +4916,8 @@ cmd_tun_on() { local verify_result local container_mode risk_reason + guard_sudo_on_user_install "on" || return 1 + prepare container_mode="$(tun_container_mode 2>/dev/null || echo unknown)" @@ -4944,12 +4946,21 @@ cmd_tun_on() { esac if ! can_manage_tun_safely; then - echo - echo "❗ Tun 模式无法开启" - echo "🚨 原因:当前环境不满足基础 Tun 条件" - echo "👉 下一步:clashctl tun doctor" - echo - return 1 + # Fallback: check if the running mihomo process already has CAP_NET_ADMIN + # (covers the case where setcap was applied after the binary check fails + # due to getcap being unavailable, or the process received the capability + # through another mechanism). + local _fb_backend + _fb_backend="$(runtime_backend 2>/dev/null || echo unknown)" + if ! tun_process_has_cap_net_admin "$_fb_backend" 2>/dev/null; then + echo + echo "❗ Tun 模式无法开启" + echo "🚨 原因:当前环境不满足基础 Tun 条件" + echo "💡 若已通过 setcap 授权 mihomo,请确认 getcap 已安装并重试" + echo "👉 下一步:clashctl tun doctor" + echo + return 1 + fi fi case "$(tun_kernel_support_level 2>/dev/null || echo unknown)" in @@ -4996,6 +5007,8 @@ cmd_tun_on() { cmd_tun_off() { local verify_result + guard_sudo_on_user_install "off" || return 1 + prepare if ! sync_tun_target_state "off" "false"; then diff --git a/scripts/core/common.sh b/scripts/core/common.sh index 73e22b5..96fb59d 100644 --- a/scripts/core/common.sh +++ b/scripts/core/common.sh @@ -2030,6 +2030,14 @@ has_ip_command() { command -v ip >/dev/null 2>&1 } +kernel_binary_has_cap_net_admin() { + local _bin + _bin="$(runtime_kernel_bin 2>/dev/null || true)" + [ -n "${_bin:-}" ] && [ -x "${_bin}" ] || return 1 + command -v getcap >/dev/null 2>&1 || return 1 + getcap "$_bin" 2>/dev/null | grep -q 'cap_net_admin' +} + can_manage_tun_safely() { if ! tun_device_exists; then return 1 @@ -2039,10 +2047,37 @@ can_manage_tun_safely() { return 0 fi + # Current shell has CAP_NET_ADMIN if has_cap_net_admin; then return 0 fi + # Kernel binary has file capability cap_net_admin (setcap) + if kernel_binary_has_cap_net_admin; then + return 0 + fi + + return 1 +} + +# Guard: refuse to run a tun action as root via sudo when the installation +# was done in user scope. Writing runtime files as root corrupts their +# ownership and breaks subsequent systemctl --user operations. +guard_sudo_on_user_install() { + local _action="${1:-on}" + is_root_user || return 0 + [ -n "${SUDO_USER:-}" ] || return 0 + + local _stored_scope + _stored_scope="$(install_env_scope 2>/dev/null || true)" + [ "${_stored_scope:-}" = "user" ] || return 0 + + echo + echo "❗ 操作被拒绝:user 安装模式不支持以 sudo 运行" + echo "🚨 原因:sudo 会将 runtime 文件写成 root:root,导致 systemctl --user 无法访问" + echo "👤 请以安装用户(${SUDO_USER})直接执行:" + echo " clashctl tun ${_action}" + echo return 1 }