@@ -245,7 +217,7 @@ export default function MitmToolCard({
)}
{/* Start / Stop DNS button */}
-
+
{dnsActive ? (
+ )}
+
+ {/* Warning below button */}
+ {warning && (
+
+ warning
+ {warning}
+
)}
@@ -288,14 +266,14 @@ export default function MitmToolCard({
onChange={(e) => setSudoPassword(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && !loading) handleConfirmPassword(); }}
/>
- {message && (
+ {modalError && (
error
- {message.text}
+ {modalError}
)}
- { setShowPasswordModal(false); setSudoPassword(""); setMessage(null); }} disabled={loading}>
+ { setShowPasswordModal(false); setSudoPassword(""); setModalError(null); }} disabled={loading}>
Cancel
diff --git a/src/app/api/cli-tools/antigravity-mitm/route.js b/src/app/api/cli-tools/antigravity-mitm/route.js
index 55eaa162..45dbcf95 100644
--- a/src/app/api/cli-tools/antigravity-mitm/route.js
+++ b/src/app/api/cli-tools/antigravity-mitm/route.js
@@ -43,7 +43,7 @@ export async function GET() {
certExists: status.certExists || false,
certTrusted: status.certTrusted || false,
dnsStatus: status.dnsStatus || {},
- hasCachedPassword: !!getCachedPassword(),
+ hasCachedPassword: !!getCachedPassword() || !!(await loadEncryptedPassword()),
isAdmin: checkIsAdmin(),
});
} catch (error) {
diff --git a/src/app/api/tunnel/enable/route.js b/src/app/api/tunnel/enable/route.js
index 1b2fdc4a..2a2e08d3 100644
--- a/src/app/api/tunnel/enable/route.js
+++ b/src/app/api/tunnel/enable/route.js
@@ -1,9 +1,13 @@
import { NextResponse } from "next/server";
import { enableTunnel } from "@/lib/tunnel/tunnelManager";
+const DNS_WARMUP_DELAY_MS = 8000;
+
export async function POST() {
try {
const result = await enableTunnel();
+ // Wait for DNS warmup to propagate at Cloudflare edge after tunnel registered
+ await new Promise((r) => setTimeout(r, DNS_WARMUP_DELAY_MS));
return NextResponse.json(result);
} catch (error) {
console.error("Tunnel enable error:", error);
diff --git a/src/lib/tunnel/cloudflared.js b/src/lib/tunnel/cloudflared.js
index 61ae3e51..7a639c15 100644
--- a/src/lib/tunnel/cloudflared.js
+++ b/src/lib/tunnel/cloudflared.js
@@ -241,15 +241,26 @@ export async function spawnQuickTunnel(localPort, onUrlUpdate) {
reject(new Error("Quick tunnel timed out"));
}, 90000);
+ let lastUrl = null;
+
const handleLog = (data) => {
const msg = data.toString();
const tunnelUrl = getQuickTunnelUrlFromLog(msg);
- if (tunnelUrl && !resolved) {
+ if (!tunnelUrl) return;
+
+ if (!resolved) {
+ // First URL — resolve the promise, do NOT call onUrlUpdate (caller handles initial register)
resolved = true;
+ lastUrl = tunnelUrl;
clearTimeout(timeout);
cleanup();
resolve({ child, tunnelUrl });
- // Notify caller of URL (for re-registration on URL change)
+ return;
+ }
+
+ // URL changed after initial connect — notify caller to re-register
+ if (tunnelUrl !== lastUrl) {
+ lastUrl = tunnelUrl;
if (onUrlUpdate) onUrlUpdate(tunnelUrl);
}
};
diff --git a/src/lib/tunnel/tunnelManager.js b/src/lib/tunnel/tunnelManager.js
index 0f9c25b6..58ce87ca 100644
--- a/src/lib/tunnel/tunnelManager.js
+++ b/src/lib/tunnel/tunnelManager.js
@@ -12,6 +12,16 @@ const MAX_RECONNECT_ATTEMPTS = RECONNECT_DELAYS_MS.length;
let isReconnecting = false;
let exitHandlerRegistered = false;
+let reconnectTimeoutId = null;
+let manualDisabled = false;
+
+export function isTunnelManuallyDisabled() {
+ return manualDisabled;
+}
+
+export function isTunnelReconnecting() {
+ return isReconnecting;
+}
function generateShortId() {
let result = "";
@@ -43,6 +53,7 @@ async function registerTunnelUrl(shortId, tunnelUrl) {
}
export async function enableTunnel(localPort = 20128) {
+ manualDisabled = false;
if (isCloudflaredRunning()) {
const existing = loadState();
if (existing?.tunnelUrl) {
@@ -56,15 +67,18 @@ export async function enableTunnel(localPort = 20128) {
const existing = loadState();
const shortId = existing?.shortId || generateShortId();
- // Spawn quick tunnel, parse URL from cloudflared output
- const { tunnelUrl } = await spawnQuickTunnel(localPort, async (url) => {
- // Called on URL change (restart) - re-register new URL
+ // onUrlUpdate: only called when URL changes AFTER initial connect (not on first resolve)
+ const onUrlUpdate = async (url) => {
+ if (manualDisabled) return;
await registerTunnelUrl(shortId, url);
saveState({ shortId, machineId, tunnelUrl: url });
await updateSettings({ tunnelEnabled: true, tunnelUrl: url });
- });
+ };
- // Register initial URL
+ // Spawn quick tunnel — resolve returns initial URL, onUrlUpdate handles subsequent changes
+ const { tunnelUrl } = await spawnQuickTunnel(localPort, onUrlUpdate);
+
+ // Register initial URL (exactly once)
await registerTunnelUrl(shortId, tunnelUrl);
saveState({ shortId, machineId, tunnelUrl });
await updateSettings({ tunnelEnabled: true, tunnelUrl });
@@ -82,15 +96,19 @@ export async function enableTunnel(localPort = 20128) {
}
async function scheduleReconnect(attempt) {
- if (isReconnecting) return;
+ if (isReconnecting || manualDisabled) return;
isReconnecting = true;
const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
console.log(`[Tunnel] Reconnecting in ${delay / 1000}s (attempt ${attempt + 1})...`);
- await new Promise((r) => setTimeout(r, delay));
+ await new Promise((r) => { reconnectTimeoutId = setTimeout(r, delay); });
try {
+ if (manualDisabled) {
+ isReconnecting = false;
+ return;
+ }
const settings = await getSettings();
if (!settings.tunnelEnabled) {
isReconnecting = false;
@@ -109,8 +127,17 @@ async function scheduleReconnect(attempt) {
}
export async function disableTunnel() {
+ // Block any reconnect attempts before killing the process
+ manualDisabled = true;
+ isReconnecting = true;
+ if (reconnectTimeoutId) {
+ clearTimeout(reconnectTimeoutId);
+ reconnectTimeoutId = null;
+ }
+ setUnexpectedExitHandler(null);
+ exitHandlerRegistered = false;
+
killCloudflared();
- exitHandlerRegistered = false; // Reset handler flag when tunnel disabled
const state = loadState();
if (state) {
@@ -119,6 +146,9 @@ export async function disableTunnel() {
await updateSettings({ tunnelEnabled: false, tunnelUrl: "" });
+ // Unblock reconnect lock — manualDisabled stays true to block Watchdog/NetworkMonitor
+ isReconnecting = false;
+
return { success: true };
}
diff --git a/src/mitm/manager.js b/src/mitm/manager.js
index 571099f0..1d96b73f 100644
--- a/src/mitm/manager.js
+++ b/src/mitm/manager.js
@@ -146,6 +146,15 @@ async function saveMitmSettings(enabled, password) {
}
}
+async function clearEncryptedPassword() {
+ if (!_updateSettings) return;
+ try {
+ await _updateSettings({ mitmSudoEncrypted: null });
+ } catch (e) {
+ err(`Failed to clear encrypted password: ${e.message}`);
+ }
+}
+
async function loadEncryptedPassword() {
if (!_getSettings) return null;
try {
@@ -467,6 +476,7 @@ async function startServer(apiKey, sudoPassword) {
// Detect wrong/missing password — clear cache and stop retry loop
if (!IS_WIN && (msg.includes("incorrect password") || msg.includes("no password was provided"))) {
setCachedPassword(null);
+ clearEncryptedPassword();
mitmIsRestarting = true; // prevent scheduleMitmRestart from firing
}
});
@@ -603,5 +613,6 @@ module.exports = {
getCachedPassword,
setCachedPassword,
loadEncryptedPassword,
+ clearEncryptedPassword,
initDbHooks,
};
diff --git a/src/shared/services/initializeApp.js b/src/shared/services/initializeApp.js
index f315ac46..42c665fa 100644
--- a/src/shared/services/initializeApp.js
+++ b/src/shared/services/initializeApp.js
@@ -1,5 +1,5 @@
import { cleanupProviderConnections, getSettings, updateSettings, getApiKeys } from "@/lib/localDb";
-import { enableTunnel } from "@/lib/tunnel/tunnelManager";
+import { enableTunnel, isTunnelManuallyDisabled, isTunnelReconnecting } from "@/lib/tunnel/tunnelManager";
import { killCloudflared, isCloudflaredRunning, ensureCloudflared } from "@/lib/tunnel/cloudflared";
import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks } from "@/mitm/manager";
import { fileURLToPath } from "url";
@@ -135,12 +135,20 @@ function startWatchdog() {
if (g.watchdogInterval) return;
g.watchdogInterval = setInterval(async () => {
try {
+ if (isTunnelManuallyDisabled()) return;
+ if (isTunnelReconnecting()) return;
+ if (g.tunnelRestartInProgress) return;
const settings = await getSettings();
if (!settings.tunnelEnabled) return;
if (isCloudflaredRunning()) return;
console.log("[Watchdog] Tunnel process is down, attempting recovery...");
- await enableTunnel();
- console.log("[Watchdog] Tunnel recovered");
+ g.tunnelRestartInProgress = true;
+ try {
+ await enableTunnel();
+ console.log("[Watchdog] Tunnel recovered");
+ } finally {
+ g.tunnelRestartInProgress = false;
+ }
} catch (err) {
console.log("[Watchdog] Recovery failed:", err.message);
}
@@ -173,6 +181,7 @@ function startNetworkMonitor() {
g.networkMonitorInterval = setInterval(async () => {
try {
+ if (isTunnelManuallyDisabled()) return;
const settings = await getSettings();
if (!settings.tunnelEnabled) return;
@@ -190,6 +199,7 @@ function startNetworkMonitor() {
// Skip if restart already in progress or restarted recently
if (g.tunnelRestartInProgress) return;
+ if (isTunnelReconnecting()) return;
if (now - g.lastTunnelRestartAt < NETWORK_RESTART_COOLDOWN_MS) return;
const reason = wasSleep && networkChanged ? "sleep/wake + network change"