fix: 彻底修复 stop 后 TUN 残留导致断网

根因:mihomo 退出后 0.0.0.0/1 + 128.0.0.0/1 黑洞路由未清掉,
所有流量指向已消失的 TUN 网关 198.18.0.1 → 断网。

macOS (clash):
- start 解析 mihomo 真实 PID 写入 PID 文件(原记的是 sudo PID)
- cmd_stop 直接 pkill -TERM/-9 mihomo,不再依赖 sudo PID
- network_restore 无条件删除 0.0.0.0/1 + 128.0.0.0/1 黑洞路由,
  并放宽 TUN 识别到整个 198.18.x fake-IP 段(不再只认 198.18.0.1)
- network_leftovers 检测同步放宽

Windows (clash.ps1):
- NetworkRestore 删除 wintun 网卡上的全部路由(不限网段)+ 清理
  网关落在 198.18.x 的所有路由
- Test-NetworkLeftovers 路由检测放宽到 198.18.x
- CmdStop 强制终止时增加按进程名强杀兜底(防止 PID 文件不准杀不掉)

两版已做语法检查与入口冒烟测试。
This commit is contained in:
2026-08-10 17:29:24 +08:00
parent 061b2bb46e
commit e94e38e39c
2 changed files with 42 additions and 21 deletions
+31 -15
View File
@@ -142,10 +142,10 @@ network_leftovers() {
[[ "$dev" != utun* ]] && continue [[ "$dev" != utun* ]] && continue
local idx="${dev#utun}" local idx="${dev#utun}"
[[ "$idx" != <-> ]] && continue [[ "$idx" != <-> ]] && continue
if ifconfig "$dev" 2>/dev/null | grep -q "inet 198.18.0.1 "; then if ifconfig "$dev" 2>/dev/null | grep -qE "inet 198\.18\."; then
return 0 return 0
fi fi
if netstat -rn -f inet 2>/dev/null | grep -qE "198\.18\.0\.1[[:space:]]+.*[[:space:]]+$dev"; then if netstat -rn -f inet 2>/dev/null | grep -qE "198\.18\.[[:space:]]+.*[[:space:]]+$dev"; then
return 0 return 0
fi fi
(( idx >= 1024 )) && return 0 (( idx >= 1024 )) && return 0
@@ -181,16 +181,19 @@ network_restore() {
rm -f "$PID_FILE" rm -f "$PID_FILE"
fi fi
# 2. 清理 mihomo 残留 TUN(按 fake-IP 网关 198.18.0.1 / 路由特征识别, # 2. 清理 mihomo 残留 TUN / 全量劫持路由
# macOS 上 mihomo 常创建 utun7/utun8 这类低编号,不能只认 >=1024) # 放宽识别:fake-IP 网关只要是 198.18.x 段都算(不限于 198.18.0.1),
# 并无条件删除 0.0.0.0/1 + 128.0.0.0/1 这两条黑洞路由,防止残留导致断网
local tun_list=$(ifconfig -l 2>/dev/null) local tun_list=$(ifconfig -l 2>/dev/null)
local route_devs=$(netstat -rn -f inet 2>/dev/null | awk '$2=="198.18.0.1" && $4 ~ /^utun[0-9]+$/ {print $4}' | sort -u) # fake-IP 网关段(默认 198.18.x,放宽不再只认 198.18.0.1
local fake_gws=$(netstat -rn -f inet 2>/dev/null | awk '$4 ~ /^utun[0-9]+$/ && $2 ~ /^198\.18\./ {print $2}' | sort -u)
local route_devs=$(netstat -rn -f inet 2>/dev/null | awk '$4 ~ /^utun[0-9]+$/ && $2 ~ /^198\.18\./ {print $4}' | sort -u)
local mihomo_tuns="" local mihomo_tuns=""
for dev in ${(s: :)tun_list}; do for dev in ${(s: :)tun_list}; do
[[ "$dev" != utun* ]] && continue [[ "$dev" != utun* ]] && continue
local idx="${dev#utun}" local idx="${dev#utun}"
[[ "$idx" != <-> ]] && continue [[ "$idx" != <-> ]] && continue
if ifconfig "$dev" 2>/dev/null | grep -q "inet 198.18.0.1 "; then if ifconfig "$dev" 2>/dev/null | grep -qE "inet 198\.18\."; then
mihomo_tuns+=" $dev" mihomo_tuns+=" $dev"
continue continue
fi fi
@@ -200,11 +203,19 @@ network_restore() {
fi fi
(( idx >= 1024 )) && mihomo_tuns+=" $dev" (( idx >= 1024 )) && mihomo_tuns+=" $dev"
done done
for dev in ${(s: :)mihomo_tuns}; do
$verbose && echo -n " 清理残留 TUN $dev ... " # 无条件清理全量劫持路由(0.0.0.0/1 + 128.0.0.0/1),几乎必定是 mihomo 创建
for gw in ${(s: :)fake_gws}; do
sudo route -n delete -net 0.0.0.0/1 "$gw" 2>/dev/null
sudo route -n delete -net 128.0.0.0/1 "$gw" 2>/dev/null
sudo route -n delete -net 198.18.0.0/16 "$gw" 2>/dev/null
done
# 兜底:常见默认网关组合,防止识别遗漏留下黑洞
sudo route -n delete -net 0.0.0.0/1 198.18.0.1 2>/dev/null sudo route -n delete -net 0.0.0.0/1 198.18.0.1 2>/dev/null
sudo route -n delete -net 128.0.0.0/1 198.18.0.1 2>/dev/null sudo route -n delete -net 128.0.0.0/1 198.18.0.1 2>/dev/null
sudo route -n delete -net 198.18.0.0/16 198.18.0.1 2>/dev/null
for dev in ${(s: :)mihomo_tuns}; do
$verbose && echo -n " 清理残留 TUN $dev ... "
sudo ifconfig "$dev" down 2>/dev/null sudo ifconfig "$dev" down 2>/dev/null
sudo ifconfig "$dev" destroy 2>/dev/null sudo ifconfig "$dev" destroy 2>/dev/null
$verbose && echo "${GREEN}✓${NC}" $verbose && echo "${GREEN}✓${NC}"
@@ -329,8 +340,11 @@ cmd_start() {
echo "" echo ""
echo -n " 启动中 ... " echo -n " 启动中 ... "
nohup sudo "$MIHOMO_BIN" -d "$CONFIG_DIR" -f "$CONFIG_FILE" > "$LOG_FILE" 2>&1 & nohup sudo "$MIHOMO_BIN" -d "$CONFIG_DIR" -f "$CONFIG_FILE" > "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE" local sudo_pid=$!
sleep 1 sleep 1
# 解析 mihomo 真实 PIDsudo 的子进程),PID 文件记它,stop 才能精确杀
local mihomo_pid=$(pgrep -P "$sudo_pid" 2>/dev/null | head -1)
echo "${mihomo_pid:-$sudo_pid}" > "$PID_FILE"
if is_running; then if is_running; then
ok "已就绪" ok "已就绪"
@@ -366,11 +380,13 @@ cmd_stop() {
local pid=$(cat "$PID_FILE") local pid=$(cat "$PID_FILE")
echo "" echo ""
echo -n " 停止中 ... " echo -n " 停止中 ... "
sudo kill "$pid" 2>/dev/null # 直接给 mihomo 发 SIGTERMPID 文件记的是 sudo,按进程名杀最稳)
sudo pkill -TERM -x mihomo 2>/dev/null
sudo kill "$pid" 2>/dev/null # 兜底:也通知 sudo 进程
for i in {1..10}; do for i in {1..10}; do
if ! pgrep -F "$PID_FILE" &>/dev/null 2>&1; then if ! pgrep -x mihomo >/dev/null 2>&1; then
ok "已停止 (PID: $pid)" ok "已停止"
rm -f "$PID_FILE" rm -f "$PID_FILE"
echo "" echo ""
network_restore false network_restore false
@@ -382,11 +398,11 @@ cmd_stop() {
echo "" echo ""
warn "超时,强制终止..." warn "超时,强制终止..."
sudo kill -9 "$pid" 2>/dev/null sudo pkill -9 -x mihomo 2>/dev/null
sleep 0.5 sleep 0.5
rm -f "$PID_FILE" rm -f "$PID_FILE"
if pgrep -l mihomo >/dev/null 2>&1; then if pgrep -x mihomo >/dev/null 2>&1; then
err "停止失败,残留进程仍在运行" err "停止失败,残留进程仍在运行"
dim "试试: sudo pkill -9 mihomo" dim "试试: sudo pkill -9 mihomo"
return 1 return 1
+11 -6
View File
@@ -168,8 +168,8 @@ function Test-NetworkLeftovers {
Where-Object { $_.InterfaceDescription -like "*Wintun*" -or $_.InterfaceDescription -like "*Mihomo*" } Where-Object { $_.InterfaceDescription -like "*Wintun*" -or $_.InterfaceDescription -like "*Mihomo*" }
if ($adapters) { return $true } if ($adapters) { return $true }
# 3. 残留路由(指向 198.18.0.1 # 3. 残留路由(网关落在 198.18.x fake-IP 段
$routes = Get-NetRoute -ErrorAction SilentlyContinue | Where-Object { $_.NextHop -eq "198.18.0.1" } $routes = Get-NetRoute -ErrorAction SilentlyContinue | Where-Object { $_.NextHop -match '^198\.18\.' }
if ($routes) { return $true } if ($routes) { return $true }
# 4. DNS 劫持(指向 127.0.0.1 或 198.18.x # 4. DNS 劫持(指向 127.0.0.1 或 198.18.x
@@ -196,20 +196,23 @@ function Invoke-NetworkRestore {
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
} }
# 2. 清理残留 wintun 网卡 # 2. 清理残留 wintun 网卡 + 其上的全部路由
$adapters = Get-NetAdapter -ErrorAction SilentlyContinue | $adapters = Get-NetAdapter -ErrorAction SilentlyContinue |
Where-Object { $_.InterfaceDescription -like "*Wintun*" -or $_.InterfaceDescription -like "*Mihomo*" } Where-Object { $_.InterfaceDescription -like "*Wintun*" -or $_.InterfaceDescription -like "*Mihomo*" }
foreach ($adapter in $adapters) { foreach ($adapter in $adapters) {
if ($ShowProgress) { Write-Host -NoNewline " 清理残留 TUN $($adapter.Name) ... " } if ($ShowProgress) { Write-Host -NoNewline " 清理残留 TUN $($adapter.Name) ... " }
# 删除该网卡上的所有路由(不限网段),避免黑洞残留
Get-NetRoute -InterfaceIndex $adapter.InterfaceIndex -ErrorAction SilentlyContinue |
Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue
netsh interface set interface "$($adapter.Name)" admin=disable 2>$null | Out-Null netsh interface set interface "$($adapter.Name)" admin=disable 2>$null | Out-Null
if ($ShowProgress) { Write-Host "${Green}${NC}" } if ($ShowProgress) { Write-Host "${Green}${NC}" }
} }
# 3. 清理残留路由 # 3. 清理残留路由(网关落在 198.18.x fake-IP 段的,无条件删除)
$mihomoRoutes = Get-NetRoute -ErrorAction SilentlyContinue | $mihomoRoutes = Get-NetRoute -ErrorAction SilentlyContinue |
Where-Object { $_.NextHop -eq "198.18.0.1" } Where-Object { $_.NextHop -match '^198\.18\.' }
foreach ($route in $mihomoRoutes) { foreach ($route in $mihomoRoutes) {
Remove-NetRoute -DestinationPrefix $route.DestinationPrefix -NextHop "198.18.0.1" -Confirm:$false -ErrorAction SilentlyContinue Remove-NetRoute -DestinationPrefix $route.DestinationPrefix -NextHop $route.NextHop -Confirm:$false -ErrorAction SilentlyContinue
} }
# 4. 刷新 DNS 缓存 # 4. 刷新 DNS 缓存
@@ -399,6 +402,8 @@ function Invoke-CmdStop {
Write-Host "" Write-Host ""
W-Warn "超时,强制终止..." W-Warn "超时,强制终止..."
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
# 兜底:按进程名强杀,防止 PID 文件不准导致杀不掉
Get-Process -Name mihomo -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 500 Start-Sleep -Milliseconds 500
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue Remove-Item $PidFile -Force -ErrorAction SilentlyContinue