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:
@@ -44,4 +44,5 @@ export {
|
|||||||
NETWORK_SETTLE_MS,
|
NETWORK_SETTLE_MS,
|
||||||
WATCHDOG_INTERVAL_MS,
|
WATCHDOG_INTERVAL_MS,
|
||||||
NETWORK_CHECK_INTERVAL_MS,
|
NETWORK_CHECK_INTERVAL_MS,
|
||||||
|
VIRTUAL_IFACE_REGEX,
|
||||||
} from "./shared/watchdogConfig.js";
|
} from "./shared/watchdogConfig.js";
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ export const RESTART_COOLDOWN_MS = 120000;
|
|||||||
export const NETWORK_SETTLE_MS = 2500;
|
export const NETWORK_SETTLE_MS = 2500;
|
||||||
export const WATCHDOG_INTERVAL_MS = 60000;
|
export const WATCHDOG_INTERVAL_MS = 60000;
|
||||||
export const NETWORK_CHECK_INTERVAL_MS = 5000;
|
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;
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ import {
|
|||||||
getTunnelService, getTailscaleService, setTunnelUnexpectedExitCallback,
|
getTunnelService, getTailscaleService, setTunnelUnexpectedExitCallback,
|
||||||
killCloudflared, isCloudflaredRunning, ensureCloudflared,
|
killCloudflared, isCloudflaredRunning, ensureCloudflared,
|
||||||
isTailscaleRunning, isTailscaleRunningStrict,
|
isTailscaleRunning, isTailscaleRunningStrict,
|
||||||
loadState,
|
|
||||||
checkInternet,
|
checkInternet,
|
||||||
probeCloudflareAlive, probeTailscaleAlive,
|
|
||||||
RESTART_COOLDOWN_MS, NETWORK_SETTLE_MS,
|
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";
|
} from "@/lib/tunnel";
|
||||||
import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks, restoreToolDNS, removeAllDNSEntriesSync } from "@/mitm/manager";
|
import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks, restoreToolDNS, removeAllDNSEntriesSync } from "@/mitm/manager";
|
||||||
import { syncToJson as syncMitmAliasCache } from "@/lib/mitmAliasCache";
|
import { syncToJson as syncMitmAliasCache } from "@/lib/mitmAliasCache";
|
||||||
@@ -144,23 +142,9 @@ async function safeRestartTunnel(reason) {
|
|||||||
|
|
||||||
const force = FORCE_RESTART_REASONS.test(reason);
|
const force = FORCE_RESTART_REASONS.test(reason);
|
||||||
|
|
||||||
// Watchdog: process alive = trust it (cloudflared self-retries via --retries 99).
|
// Process alive = trust cloudflared (self-reconnects via --retries 99, keeps same URL).
|
||||||
// Avoids killing a healthy tunnel on transient HTTP probe failures (app busy / slow DNS).
|
// Killing a live process on network change drops the tunnel and rotates the quick-tunnel URL.
|
||||||
if (!force && isCloudflaredRunning()) return;
|
if (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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!force && Date.now() - svc.lastRestartAt < RESTART_COOLDOWN_MS) {
|
if (!force && Date.now() - svc.lastRestartAt < RESTART_COOLDOWN_MS) {
|
||||||
console.log(`[Tunnel] degraded but cooldown active, skip (${reason})`);
|
console.log(`[Tunnel] degraded but cooldown active, skip (${reason})`);
|
||||||
@@ -187,7 +171,7 @@ async function safeRestartTailscale(reason) {
|
|||||||
if (svc.cancelToken.cancelled) return;
|
if (svc.cancelToken.cancelled) return;
|
||||||
if (svc.spawnInProgress) 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.
|
// Startup uses strict probe — cached state is cold after process/dev reload.
|
||||||
const running = reason === "startup" ? isTailscaleRunningStrict() : isTailscaleRunning();
|
const running = reason === "startup" ? isTailscaleRunningStrict() : isTailscaleRunning();
|
||||||
if (running) return;
|
if (running) return;
|
||||||
@@ -227,6 +211,7 @@ function getNetworkFingerprint() {
|
|||||||
const active = [];
|
const active = [];
|
||||||
for (const [name, addrs] of Object.entries(interfaces)) {
|
for (const [name, addrs] of Object.entries(interfaces)) {
|
||||||
if (!addrs) continue;
|
if (!addrs) continue;
|
||||||
|
if (VIRTUAL_IFACE_REGEX.test(name)) continue;
|
||||||
for (const addr of addrs) {
|
for (const addr of addrs) {
|
||||||
if (!addr.internal && addr.family === "IPv4") {
|
if (!addr.internal && addr.family === "IPv4") {
|
||||||
active.push(`${name}:${addr.address}`);
|
active.push(`${name}:${addr.address}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user