stop_tunnel
Stop specific or all local tunnels running on the current machine. Use this to terminate tunnels identified by name or halt all active ones without affecting other systems.
Instructions
Stops a running tunnel or all local tunnels.
This tool will:
Stop a specific tunnel identified by name (if provided)
Stop all local tunnels (if no name is provided)
Only affects tunnels running on the current machine
Will not affect tunnels running on other machines
After stopping tunnels, you can use 'list_tunnels' to confirm they've been terminated.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | No | Optional name of a specific tunnel to stop. If not provided, all local tunnels will be stopped. |
Implementation Reference
- src/index.ts:240-389 (handler)Primary implementation of the stop_tunnel tool: registers the tool, defines input schema, and provides the handler logic that either stops a specific tunnel using closeSpecificTunnel or all tunnels using killTunnelProcesses, while managing the activeTunnels state.// Define 'stop_tunnel' tool server.tool( "stop_tunnel", `Stops a running tunnel or all local tunnels. This tool will: - Stop a specific tunnel identified by name (if provided) - Stop all local tunnels (if no name is provided) - Only affects tunnels running on the current machine - Will not affect tunnels running on other machines After stopping tunnels, you can use 'list_tunnels' to confirm they've been terminated.`, { name: z .string() .optional() .describe( "Optional name of a specific tunnel to stop. If not provided, all local tunnels will be stopped.", ), }, async ({ name }) => { try { // If name is provided, stop specific tunnel if (name) { if (!activeTunnels.has(name)) { return { content: [ { type: "text", text: `No active tunnel found with name "${name}".`, }, ], }; } const tunnelInfo = activeTunnels.get(name); // Check if this is a remote tunnel if (tunnelInfo?.isRemote) { return { content: [ { type: "text", text: `Tunnel "${name}" is running on a different host (${tunnelInfo.hostId}) and cannot be stopped from this instance. Please stop it from the originating host.`, }, ], }; } log(`==== STOPPING SPECIFIC TUNNEL: ${name} ====`); log( `Tunnel details: URL=${tunnelInfo?.url}, PID=${tunnelInfo?.pid || "unknown"}, Public URL=${tunnelInfo?.publicUrl || "unknown"}`, ); debugLog( `Stopping specific tunnel: ${name} (PID: ${tunnelInfo?.pid || "unknown"})`, ); let killResults: string[] = []; if (tunnelInfo?.url) { const result = await closeSpecificTunnel( tunnelInfo.url, tunnelInfo.pid, ); killResults = result.split("\n"); } // Remove from active tunnels log(`Removing tunnel ${name} from registry`); activeTunnels.delete(name); // Save updated tunnels to file saveTunnels(); const killResultsText = killResults.join("\n"); log(`Kill results:\n${killResultsText}`); log(`==== FINISHED STOPPING TUNNEL: ${name} ====`); return { content: [ { type: "text", text: `Tunnel "${name}" stopped.\n\nDetails:\n${killResultsText}`, }, ], }; } // No name provided, stop all local tunnels else { log(`==== STOPPING ALL LOCAL TUNNELS ====`); // Use killTunnelProcesses to forcefully kill all untun processes const results = killTunnelProcesses(); // Get the count of local tunnels before cleaning const entries = Array.from(activeTunnels.entries()); const localTunnels = entries.filter((entry) => { // Type assertion for entry const [_, info] = entry as [string, TunnelInfo]; return !info.isRemote; }); const localCount = localTunnels.length; if (localCount === 0) { return { content: [ { type: "text", text: "No local tunnels to stop. Remote tunnels are not affected.", }, ], }; } // Remove all local tunnels from the registry for (const [tunnelName, _] of localTunnels) { activeTunnels.delete(tunnelName); } // Save updated tunnels to file saveTunnels(); log(`==== FINISHED STOPPING ALL TUNNELS ====`); return { content: [ { type: "text", text: `Stopped ${localCount} local tunnels. Remote tunnels were not affected.\n\nDetails:\n${results}`, }, ], }; } } catch (error) { const err = error as Error; const errorMsg = `Error stopping tunnel(s): ${err.message || error}`; log(errorMsg); debugLog(errorMsg); return { content: [ { type: "text", text: errorMsg, }, ], }; } }, );
- src/lib/processes.ts:64-189 (helper)Helper function killTunnelProcesses: Force-kills all tunnel-related processes (cloudflared launched by untun and untun itself), used when no specific name is provided to stop all local tunnels.export const killTunnelProcesses = (): string => { log("Forcefully killing all tunnel processes"); debugLog("Forcefully killing all tunnel processes"); const results: string[] = []; // Find all cloudflared processes const cloudflaredDetails = getCloudflaredDetails(); // Filter to get only cloudflared processes that were launched by untun // They typically have 'node-untun' in the path and/or '--url http://' in the command const untunCloudflaredProcesses = cloudflaredDetails.filter((proc) => { return ( (proc.command.includes("node-untun") || proc.command.includes("--url http") || proc.command.match(/tunnel --url/i)) && !proc.command.includes("--token") ); // Exclude processes using an authentication token }); if (untunCloudflaredProcesses.length > 0) { results.push( `Found ${untunCloudflaredProcesses.length} untun-managed cloudflared processes`, ); // Log the processes we found for debugging untunCloudflaredProcesses.forEach((proc) => { results.push( `Will terminate: PID ${proc.pid} (${proc.command.slice(0, 50)}...)`, ); }); // Try to kill each PID with SIGTERM first untunCloudflaredProcesses.forEach((proc) => { const killed = killPid(proc.pid, "TERM"); results.push( `Kill TERM PID ${proc.pid}: ${killed ? "Success" : "Failed"}`, ); }); // Wait a moment for processes to terminate try { execSync("sleep 0.5"); } catch (e) {} // Find which ones remain and use SIGKILL const remainingDetails = getCloudflaredDetails(); const remainingPids = untunCloudflaredProcesses .map((proc) => proc.pid) .filter((pid) => remainingDetails.some((p) => p.pid === pid)); if (remainingPids.length > 0) { results.push( `${remainingPids.length} cloudflared processes still running after SIGTERM`, ); // Force kill each remaining process remainingPids.forEach((pid) => { const killed = killPid(pid, "KILL"); results.push(`Kill KILL PID ${pid}: ${killed ? "Success" : "Failed"}`); }); } } else { results.push("No untun-managed cloudflared processes found to kill"); } // Also try to kill untun processes directly try { const untunPids = findPidsByPattern("untun tunnel"); if (untunPids.length > 0) { results.push(`Found ${untunPids.length} untun tunnel processes`); untunPids.forEach((pid) => { const killed = killPid(pid, "TERM"); results.push( `Kill TERM untun PID ${pid}: ${killed ? "Success" : "Failed"}`, ); }); // Force kill if TERM didn't work try { execSync("sleep 0.3"); const remainingUntunPids = findPidsByPattern("untun tunnel"); remainingUntunPids.forEach((pid) => { const killed = killPid(pid, "KILL"); results.push( `Kill KILL untun PID ${pid}: ${killed ? "Success" : "Failed"}`, ); }); } catch (e) {} } else { results.push("No untun processes found"); } } catch (e) { const error = e as Error; results.push(`Error killing untun processes: ${error.message}`); } // Final check to see if our cloudflared processes are gone try { const finalUntunCloudflared = getCloudflaredDetails().filter((proc) => { return ( (proc.command.includes("node-untun") || proc.command.includes("--url http") || proc.command.match(/tunnel --url/i)) && !proc.command.includes("--token") ); }); if (finalUntunCloudflared.length > 0) { const details = finalUntunCloudflared .map((p) => `PID ${p.pid}: ${p.command.slice(0, 30)}...`) .join("\n"); results.push( `WARNING: ${finalUntunCloudflared.length} untun cloudflared processes still running:\n${details}`, ); } else { results.push("All untun cloudflared processes successfully terminated"); } } catch (e) { // No processes found is good const error = e as Error; results.push(`Error in final check: ${error.message}`); } return results.join("\n"); };
- src/lib/processes.ts:194-328 (helper)Helper function closeSpecificTunnel: Closes a specific tunnel by killing its untun process (by PID) and associated cloudflared child process(es), used for named tunnel stops.export const closeSpecificTunnel = async ( url: string, pid?: number, ): Promise<string> => { const results: string[] = []; log(`Attempting to close specific tunnel for URL: ${url}, PID: ${pid}`); // Function to kill a specific process const killProcess = (targetPid: string, signal = "TERM"): boolean => { try { log(`Killing specific PID ${targetPid} with signal ${signal}`); execSync(`kill -${signal} ${targetPid}`, { stdio: "ignore" }); results.push(`✓ Killed process ${targetPid} with ${signal}`); return true; } catch (e) { const error = e as Error; results.push(`✗ Failed to kill process ${targetPid}: ${error.message}`); return false; } }; // Kill the main untun process if PID is provided let mainProcessKilled = false; if (pid) { try { // Check if process exists first try { execSync(`ps -p ${pid}`); mainProcessKilled = killProcess(pid.toString()); } catch (e) { results.push(`Process ${pid} not found (might have exited already)`); } } catch (error) { const err = error as Error; results.push(`Error checking/killing main process: ${err.message}`); } // Only look for cloudflared processes with the specific parent PID // This ensures we only kill the exact cloudflared process associated with this tunnel try { // Find the child processes of the main process const pgrep = `pgrep -P ${pid}`; let childPids: string[] = []; try { const childPidsOutput = execSync(pgrep).toString().trim(); if (childPidsOutput) { childPids = childPidsOutput.split("\n").filter(Boolean); } } catch (e) { // No child processes found, which might be normal results.push(`No child processes found for PID ${pid}`); } // If we found child processes, check which ones are cloudflared if (childPids.length > 0) { results.push( `Found ${childPids.length} child processes for PID ${pid}`, ); for (const childPid of childPids) { try { // Check if this process is a cloudflared process const psCmd = `ps -p ${childPid} -o command=`; const processCmd = execSync(psCmd).toString().trim(); if (processCmd.includes("cloudflared")) { results.push(`Found cloudflared child process: ${childPid}`); killProcess(childPid); } } catch (e) { // Process might have exited already } } } return results.join("\n"); } catch (error) { const err = error as Error; results.push(`Error finding/killing child processes: ${err.message}`); } } else { // If no PID is provided, fall back to URL-based matching but with a warning results.push( "Warning: No PID provided, trying to match by URL which may affect other tunnels", ); try { // Clean the URL for matching (remove protocol, only use host:port) const cleanUrl = url.replace(/^https?:\/\//, ""); // Find cloudflared processes that match this URL const cmd = `ps aux | grep cloudflared | grep "${cleanUrl}" | grep -v grep`; let matchingProcesses: Array<{ pid: string; cmd: string }> = []; try { const output = execSync(cmd).toString().trim(); if (output) { matchingProcesses = output .split("\n") .filter(Boolean) .map((line) => { const parts = line.trim().split(/\s+/); return { pid: parts[1], cmd: parts.slice(10).join(" ") }; }); } } catch (e) { // If grep returns non-zero (no matches), that's fine results.push("No matching cloudflared processes found"); } // Kill each matching cloudflared process if (matchingProcesses.length > 0) { results.push( `Found ${matchingProcesses.length} cloudflared processes for URL ${cleanUrl}`, ); for (const proc of matchingProcesses) { results.push( `Attempting to kill cloudflared process ${proc.pid} (${proc.cmd.slice(0, 30)}...)`, ); killProcess(proc.pid); } } } catch (error) { const err = error as Error; results.push( `Error finding/killing cloudflared processes: ${err.message}`, ); } } return results.join("\n"); };
- src/lib/tunnels.ts:13-38 (helper)Helper saveTunnels: Persists the activeTunnels state to file after modifications during tunnel stops.export const saveTunnels = (): void => { try { // Convert Map to a serializable object const tunnelsData = Array.from(activeTunnels.entries()).map( ([name, info]) => { // Create a serializable version of the tunnel info return { name, url: info.url, publicUrl: info.publicUrl, created: info.created.toISOString(), pid: info.pid, hostId: os.hostname(), // Add hostname to identify the host }; }, ); fs.writeFileSync(tunnelStoragePath, JSON.stringify(tunnelsData, null, 2)); log(`Saved ${tunnelsData.length} tunnel(s) to ${tunnelStoragePath}`); debugLog(`Saved tunnels to storage file`); } catch (error) { const err = error as Error; log(`Error saving tunnels to file: ${err.message}`); debugLog(`Error saving tunnels: ${err.message}`); } };
- src/lib/tunnels.ts:43-94 (helper)Helper loadTunnels: Restores activeTunnels state from file, called on server start.export const loadTunnels = (): void => { try { if (!fs.existsSync(tunnelStoragePath)) { log(`No tunnel storage file found at ${tunnelStoragePath}`); return; } const data = fs.readFileSync(tunnelStoragePath, "utf8"); const tunnelsData = JSON.parse(data); // Process each tunnel tunnelsData.forEach((tunnelData: any) => { // Skip if tunnel is already loaded if (activeTunnels.has(tunnelData.name)) return; // Check if this is a remote tunnel or a local one const isRemoteTunnel = tunnelData.hostId !== os.hostname(); // Create a tunnel object with appropriate properties based on type activeTunnels.set(tunnelData.name, { url: tunnelData.url, publicUrl: tunnelData.publicUrl, created: new Date(tunnelData.created), pid: tunnelData.pid, hostId: tunnelData.hostId, isRemote: isRemoteTunnel, tunnel: { // Remote tunnels cannot be closed from this instance // Local tunnels can be closed via processes close: async () => { if (isRemoteTunnel) { log( `Cannot close remote tunnel "${tunnelData.name}" from this host`, ); return false; } else { log(`Closing local tunnel "${tunnelData.name}"`); return closeSpecificTunnel(tunnelData.url, tunnelData.pid); } }, }, }); }); log(`Loaded ${tunnelsData.length} tunnel(s) from storage file`); debugLog(`Loaded tunnels from storage file`); } catch (error) { const err = error as Error; log(`Error loading tunnels from file: ${err.message}`); debugLog(`Error loading tunnels: ${err.message}`); } };