fix(tunnel): skip virtual interfaces to prevent false netchange watchdog

- Add VIRTUAL_IFACE_REGEX to filter utun/awdl/bridge from network fingerprint
- Trust cloudflared/tailscale while process is alive, never kill on force restart

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-06 15:43:41 +07:00
parent c785051360
commit 293cf40455
3 changed files with 10 additions and 21 deletions

View File

@@ -44,4 +44,5 @@ export {
NETWORK_SETTLE_MS,
WATCHDOG_INTERVAL_MS,
NETWORK_CHECK_INTERVAL_MS,
VIRTUAL_IFACE_REGEX,
} from "./shared/watchdogConfig.js";

View File

@@ -3,3 +3,6 @@ export const RESTART_COOLDOWN_MS = 120000;
export const NETWORK_SETTLE_MS = 2500;
export const WATCHDOG_INTERVAL_MS = 60000;
export const NETWORK_CHECK_INTERVAL_MS = 5000;
// Skip virtual/transient interfaces (tailscale utun, AirDrop awdl, bridges) that flap and cause false netchange
export const VIRTUAL_IFACE_REGEX = /^(utun|awdl|llw|anpi|bridge|gif|stf|ipsec|ap|tun|tap|vmnet|veth|docker)/i;

View File

@@ -9,11 +9,9 @@ import {
getTunnelService, getTailscaleService, setTunnelUnexpectedExitCallback,
killCloudflared, isCloudflaredRunning, ensureCloudflared,
isTailscaleRunning, isTailscaleRunningStrict,
loadState,
checkInternet,
probeCloudflareAlive, probeTailscaleAlive,
RESTART_COOLDOWN_MS, NETWORK_SETTLE_MS,
WATCHDOG_INTERVAL_MS, NETWORK_CHECK_INTERVAL_MS,
WATCHDOG_INTERVAL_MS, NETWORK_CHECK_INTERVAL_MS, VIRTUAL_IFACE_REGEX,
} from "@/lib/tunnel";
import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks, restoreToolDNS, removeAllDNSEntriesSync } from "@/mitm/manager";
import { syncToJson as syncMitmAliasCache } from "@/lib/mitmAliasCache";
@@ -144,23 +142,9 @@ async function safeRestartTunnel(reason) {
const force = FORCE_RESTART_REASONS.test(reason);
// Watchdog: process alive = trust it (cloudflared self-retries via --retries 99).
// Avoids killing a healthy tunnel on transient HTTP probe failures (app busy / slow DNS).
if (!force && isCloudflaredRunning()) return;
// Force reasons (netchange/sleep/online): process may be up but routing stale → probe to confirm
if (force && isCloudflaredRunning()) {
const state = loadState();
const publicUrl = state?.shortId ? `https://r${state.shortId}.abc-tunnel.us` : null;
const directUrl = state?.tunnelUrl || null;
if (publicUrl && directUrl) {
const [publicOk, directOk] = await Promise.all([
probeCloudflareAlive(publicUrl),
probeCloudflareAlive(directUrl),
]);
if (publicOk && directOk) return;
}
}
// Process alive = trust cloudflared (self-reconnects via --retries 99, keeps same URL).
// Killing a live process on network change drops the tunnel and rotates the quick-tunnel URL.
if (isCloudflaredRunning()) return;
if (!force && Date.now() - svc.lastRestartAt < RESTART_COOLDOWN_MS) {
console.log(`[Tunnel] degraded but cooldown active, skip (${reason})`);
@@ -187,7 +171,7 @@ async function safeRestartTailscale(reason) {
if (svc.cancelToken.cancelled) return;
if (svc.spawnInProgress) return;
// Tailscale daemon is OS-level with built-in reconnect; trust it when running.
// Tailscale daemon is OS-level with built-in reconnect; trust it when running (even on netchange).
// Startup uses strict probe — cached state is cold after process/dev reload.
const running = reason === "startup" ? isTailscaleRunningStrict() : isTailscaleRunning();
if (running) return;
@@ -227,6 +211,7 @@ function getNetworkFingerprint() {
const active = [];
for (const [name, addrs] of Object.entries(interfaces)) {
if (!addrs) continue;
if (VIRTUAL_IFACE_REGEX.test(name)) continue;
for (const addr of addrs) {
if (!addr.internal && addr.family === "IPv4") {
active.push(`${name}:${addr.address}`);