proxy_mobile_teardown
Deactivates Android proxy target, stops transparent and explicit listeners, and generates iptables removal script to restore NetworkManager management.
Instructions
Reverse proxy_mobile_setup: deactivate the Android target if any, stop the transparent + explicit listeners, and emit a sudo-runnable script that removes the iptables rules and restores NetworkManager management.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| ap_iface | No | AP/USB interface name. Auto-detected via cdc_ncm if omitted. | |
| ap_subnet | No | Subnet the AP serves (must match setup). | 192.168.4.0/24 |
| egress_iface | No | Host's egress iface (must match setup). | |
| block_quic | No | Whether the QUIC DROP rule was set up (so we know to remove it). | |
| android_target_id | No | Android target ID to deactivate (from proxy_mobile_setup response). | |
| stop_proxy | No | Also stop the explicit proxy (default: keep it running for continued use). |
Implementation Reference
- src/tools/mobile.ts:328-392 (handler)The handler for proxy_mobile_teardown: deactivates Android target, stops transparent/explicit listeners, emits a teardown sudo script that removes iptables rules and restores NetworkManager.
server.tool( "proxy_mobile_teardown", "Reverse proxy_mobile_setup: deactivate the Android target if any, stop the transparent + explicit listeners, and emit a sudo-runnable script that removes the iptables rules and restores NetworkManager management.", { ap_iface: z.string().optional().describe("AP/USB interface name. Auto-detected via cdc_ncm if omitted."), ap_subnet: z.string().optional().default("192.168.4.0/24").describe("Subnet the AP serves (must match setup)."), egress_iface: z.string().optional().describe("Host's egress iface (must match setup)."), block_quic: z.boolean().optional().default(true).describe("Whether the QUIC DROP rule was set up (so we know to remove it)."), android_target_id: z.string().optional().describe("Android target ID to deactivate (from proxy_mobile_setup response)."), stop_proxy: z.boolean().optional().default(false).describe("Also stop the explicit proxy (default: keep it running for continued use)."), }, async ({ ap_iface, ap_subnet, egress_iface, block_quic, android_target_id, stop_proxy }) => { try { let apIface = ap_iface; if (!apIface) { const detected = detectApIface(); if (!detected) { return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: "Could not auto-detect AP iface for teardown. Pass ap_iface explicitly.", }) }] }; } apIface = detected.iface; } const egressIface = egress_iface ?? detectEgressIface(); if (android_target_id) { await interceptorManager.deactivate("android-adb", android_target_id).catch(() => {}); } if (proxyManager.isTransparentRunning()) { await proxyManager.stopTransparent().catch(() => {}); } if (stop_proxy && proxyManager.isRunning()) { await proxyManager.stop().catch(() => {}); } const scriptPath = writeScript( buildTeardownScript({ apIface, apSubnet: ap_subnet, egressIface, blockQuic: block_quic, }), "mobile-teardown", ); return { content: [{ type: "text", text: JSON.stringify({ status: "success", ap_iface: apIface, egress_iface: egressIface, android_target_deactivated: !!android_target_id, transparent_stopped: true, explicit_stopped: stop_proxy, sudo_script: scriptPath, sudo_command: `sudo bash ${scriptPath}`, }), }], }; } catch (e) { return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] }; } }, - src/tools/mobile.ts:331-337 (schema)Zod schema defining the input parameters for proxy_mobile_teardown.
{ ap_iface: z.string().optional().describe("AP/USB interface name. Auto-detected via cdc_ncm if omitted."), ap_subnet: z.string().optional().default("192.168.4.0/24").describe("Subnet the AP serves (must match setup)."), egress_iface: z.string().optional().describe("Host's egress iface (must match setup)."), block_quic: z.boolean().optional().default(true).describe("Whether the QUIC DROP rule was set up (so we know to remove it)."), android_target_id: z.string().optional().describe("Android target ID to deactivate (from proxy_mobile_setup response)."), stop_proxy: z.boolean().optional().default(false).describe("Also stop the explicit proxy (default: keep it running for continued use)."), - src/index.ts:73-73 (registration)Registration of all mobile tools (including proxy_mobile_teardown) on the MCP server.
registerMobileTools(server); - src/tools/mobile.ts:189-430 (registration)Registration function that calls server.tool() to register proxy_mobile_teardown (and setup/detect_iface) on the MCP server.
export function registerMobileTools(server: McpServer): void { server.tool( "proxy_mobile_setup", "One-command mobile capture: start explicit + transparent listeners, optionally inject the CA on an Android device, and emit a sudo-runnable script that wires iptables/sysctl/nmcli on the AP iface. Designed to pair with the proxy-ap-card firmware (ESP32-S3 rogue AP over USB-NCM).", { ap_iface: z.string().optional().describe("AP/USB interface name. Auto-detected via cdc_ncm driver if omitted."), ap_address: z.string().optional().default("192.168.99.2/24").describe("Laptop-side address on the AP iface (default: 192.168.99.2/24, matches proxy-ap-card firmware)."), ap_subnet: z.string().optional().default("192.168.4.0/24").describe("Subnet the AP serves to clients (default: 192.168.4.0/24, matches proxy-ap-card firmware)."), egress_iface: z.string().optional().describe("Host's internet-facing iface. Auto-detected from /proc/net/route if omitted."), explicit_port: z.number().optional().default(8080).describe("Port for the explicit HTTP proxy (default: 8080)."), transparent_port: z.number().optional().default(8443).describe("Port for the transparent HTTPS listener (default: 8443)."), block_quic: z.boolean().optional().default(true).describe("Drop UDP/443 on the AP iface so apps fall back to TCP/TLS (capturable). Default: true."), upstream_proxy_url: z.string().optional().describe("Optional upstream proxy URL (socks5://user:pass@host:port or http://...). Sets the global upstream for BOTH listeners."), android_serial: z.string().optional().describe("ADB serial of an Android device to inject the CA on. If omitted, no cert injection is attempted."), inject_cert: z.boolean().optional().default(true).describe("Inject the CA into the Android device's system store. Ignored if android_serial is omitted."), }, async ({ ap_iface, ap_address, ap_subnet, egress_iface, explicit_port, transparent_port, block_quic, upstream_proxy_url, android_serial, inject_cert, }) => { try { // 1. Resolve AP iface. let apIface = ap_iface; let ifaceReason = "user-provided"; if (!apIface) { const detected = detectApIface(); if (!detected) { return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: "Could not auto-detect an AP interface. Plug in the proxy-ap-card (or another cdc_ncm USB NCM device) and retry, or pass ap_iface explicitly. Known interfaces:", interfaces: readdirSync("/sys/class/net").filter((n) => n !== "lo"), }) }] }; } apIface = detected.iface; ifaceReason = detected.reason; } // 2. Resolve egress. const egressIface = egress_iface ?? detectEgressIface(); // 3. Explicit proxy. let explicitPortUsed = explicit_port; if (!proxyManager.isRunning()) { const started = await proxyManager.start(explicit_port); explicitPortUsed = started.port; } else { explicitPortUsed = proxyManager.getPort() ?? explicit_port; } // 4. Transparent listener. let transparentPortUsed = transparent_port; if (!proxyManager.isTransparentRunning()) { const startedTp = await proxyManager.startTransparent(transparent_port); transparentPortUsed = startedTp.port; } else { transparentPortUsed = proxyManager.getTransparentPort() ?? transparent_port; } // 5. Upstream (optional). let upstreamSet = false; if (upstream_proxy_url) { await proxyManager.setGlobalUpstream({ proxyUrl: upstream_proxy_url }); upstreamSet = true; } // 6. Android CA injection (optional). let certInjected = false; let androidTargetId: string | null = null; if (android_serial && inject_cert) { const cert = proxyManager.getCert(); if (!cert) throw new Error("Proxy started but no cert was generated — this is a bug."); const result = await interceptorManager.activate("android-adb", { proxyPort: explicitPortUsed, certPem: cert.cert, certFingerprint: cert.fingerprint, serial: android_serial, injectCert: true, setupTunnel: false, // AP + iptables handle routing; no reverse tunnel needed setWifiProxy: false, // same reason }); certInjected = true; androidTargetId = result.targetId; } // 7. Emit sudo script. const scriptPath = writeScript( buildSetupScript({ apIface, apAddress: ap_address, apSubnet: ap_subnet, egressIface, explicitPort: explicitPortUsed, transparentPort: transparentPortUsed, blockQuic: block_quic, }), "mobile-setup", ); return { content: [{ type: "text", text: JSON.stringify({ status: "success", ap_iface: apIface, ap_iface_reason: ifaceReason, ap_address, ap_subnet, egress_iface: egressIface, explicit_port: explicitPortUsed, transparent_port: transparentPortUsed, block_quic, upstream_set: upstreamSet, cert_injected: certInjected, android_target_id: androidTargetId, sudo_script: scriptPath, sudo_command: `sudo bash ${scriptPath}`, next_steps: [ `Run: sudo bash ${scriptPath}`, "Connect the target device to the proxy-ap WiFi network (credentials live in the proxy-ap-card firmware).", "Call proxy_list_traffic — transparent HTTPS exchanges appear with source=\"transparent\", HTTP with source=\"explicit\".", "When done, call proxy_mobile_teardown and run the emitted sudo script.", ], }), }], }; } catch (e) { return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] }; } }, ); server.tool( "proxy_mobile_teardown", "Reverse proxy_mobile_setup: deactivate the Android target if any, stop the transparent + explicit listeners, and emit a sudo-runnable script that removes the iptables rules and restores NetworkManager management.", { ap_iface: z.string().optional().describe("AP/USB interface name. Auto-detected via cdc_ncm if omitted."), ap_subnet: z.string().optional().default("192.168.4.0/24").describe("Subnet the AP serves (must match setup)."), egress_iface: z.string().optional().describe("Host's egress iface (must match setup)."), block_quic: z.boolean().optional().default(true).describe("Whether the QUIC DROP rule was set up (so we know to remove it)."), android_target_id: z.string().optional().describe("Android target ID to deactivate (from proxy_mobile_setup response)."), stop_proxy: z.boolean().optional().default(false).describe("Also stop the explicit proxy (default: keep it running for continued use)."), }, async ({ ap_iface, ap_subnet, egress_iface, block_quic, android_target_id, stop_proxy }) => { try { let apIface = ap_iface; if (!apIface) { const detected = detectApIface(); if (!detected) { return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: "Could not auto-detect AP iface for teardown. Pass ap_iface explicitly.", }) }] }; } apIface = detected.iface; } const egressIface = egress_iface ?? detectEgressIface(); if (android_target_id) { await interceptorManager.deactivate("android-adb", android_target_id).catch(() => {}); } if (proxyManager.isTransparentRunning()) { await proxyManager.stopTransparent().catch(() => {}); } if (stop_proxy && proxyManager.isRunning()) { await proxyManager.stop().catch(() => {}); } const scriptPath = writeScript( buildTeardownScript({ apIface, apSubnet: ap_subnet, egressIface, blockQuic: block_quic, }), "mobile-teardown", ); return { content: [{ type: "text", text: JSON.stringify({ status: "success", ap_iface: apIface, egress_iface: egressIface, android_target_deactivated: !!android_target_id, transparent_stopped: true, explicit_stopped: stop_proxy, sudo_script: scriptPath, sudo_command: `sudo bash ${scriptPath}`, }), }], }; } catch (e) { return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] }; } }, ); server.tool( "proxy_mobile_detect_iface", "Auto-detect the USB-NCM interface the proxy-ap-card presents as (via the cdc_ncm driver). Returns null + iface list if none found.", {}, async () => { try { const detected = detectApIface(); if (!detected) { return { content: [{ type: "text", text: JSON.stringify({ status: "success", found: false, interfaces: readdirSync("/sys/class/net").filter((n) => n !== "lo"), }), }], }; } return { content: [{ type: "text", text: JSON.stringify({ status: "success", found: true, iface: detected.iface, reason: detected.reason, }), }], }; } catch (e) { return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] }; } }, ); } - src/tools/mobile.ts:151-179 (helper)Helper that builds the sudo bash teardown script content to remove iptables rules, disable IP forwarding, flush the AP address, and re-enable NetworkManager management.
function buildTeardownScript(opts: { apIface: string; apSubnet: string; egressIface: string; blockQuic: boolean; }): string { return `#!/bin/bash # proxy-mcp mobile capture — remove host-side network rules. set -u AP_IFACE="${opts.apIface}" AP_SUBNET="${opts.apSubnet}" EGRESS_IFACE="${opts.egressIface}" iptables -t nat -D PREROUTING -i "\$AP_IFACE" -j PROXY_MCP_PREROUTING 2>/dev/null || true iptables -t nat -F PROXY_MCP_PREROUTING 2>/dev/null || true iptables -t nat -X PROXY_MCP_PREROUTING 2>/dev/null || true ${opts.blockQuic ? `iptables -D FORWARD -i "\$AP_IFACE" -p udp --dport 443 -j DROP 2>/dev/null || true\n` : ""}iptables -t nat -D POSTROUTING -s "\$AP_SUBNET" -o "\$EGRESS_IFACE" -j MASQUERADE 2>/dev/null || true sysctl -w net.ipv4.ip_forward=0 >/dev/null ip addr flush dev "\$AP_IFACE" 2>/dev/null || true nmcli device set "\$AP_IFACE" managed yes 2>/dev/null || true echo "Teardown complete." `; }