From 8c0b4a3e84887deaba0571c5998a64700c233f60 Mon Sep 17 00:00:00 2001 From: Loc Nguyen Huu Date: Mon, 23 Mar 2026 09:23:14 +0700 Subject: [PATCH] [fix] fix mitm for docker and enhance dockerfile (#381) * [fix] macos * chore: clean up .gitignore by removing unnecessary start.sh entry --------- Co-authored-by: lokinh --- .gitignore | 1 - Dockerfile | 4 ++++ src/mitm/cert/install.js | 9 ++++++++- src/mitm/dns/dnsConfig.js | 30 +++++++++++++++++++++++------- src/mitm/manager.js | 38 +++++++++++++++++++++++++++----------- start.sh | 4 ++++ 6 files changed, 66 insertions(+), 20 deletions(-) create mode 100755 start.sh diff --git a/.gitignore b/.gitignore index 78ff5bc4..75f91bd3 100644 --- a/.gitignore +++ b/.gitignore @@ -66,5 +66,4 @@ package-lock.json README1.md deploy.sh ecosystem.config.* -start.sh diff --git a/Dockerfile b/Dockerfile index b5bcae14..31923ce1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,10 @@ COPY --from=builder /app/public ./public COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/open-sse ./open-sse +# Next file tracing can omit sibling files; MITM runs server.js as a separate process. +COPY --from=builder /app/src/mitm ./src/mitm +# Standalone node_modules may omit deps only required by the MITM child process. +COPY --from=builder /app/node_modules/node-forge ./node_modules/node-forge RUN mkdir -p /app/data diff --git a/src/mitm/cert/install.js b/src/mitm/cert/install.js index 52159874..e5e4d0c5 100644 --- a/src/mitm/cert/install.js +++ b/src/mitm/cert/install.js @@ -1,7 +1,7 @@ const fs = require("fs"); const crypto = require("crypto"); const { exec } = require("child_process"); -const { execWithPassword } = require("../dns/dnsConfig.js"); +const { execWithPassword, isSudoAvailable } = require("../dns/dnsConfig.js"); const { log, err } = require("../logger"); const IS_WIN = process.platform === "win32"; @@ -151,6 +151,10 @@ function checkCertInstalledLinux() { } async function installCertLinux(sudoPassword, certPath) { + if (!isSudoAvailable()) { + log(`🔐 Cert: cannot install to system store without sudo — trust this file on clients: ${certPath}`); + return; + } const destFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`; // Try update-ca-certificates (Debian/Ubuntu), fallback to update-ca-trust (Fedora/RHEL) const cmd = `cp "${certPath}" "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`; @@ -163,6 +167,9 @@ async function installCertLinux(sudoPassword, certPath) { } async function uninstallCertLinux(sudoPassword) { + if (!isSudoAvailable()) { + return; + } const destFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`; const cmd = `rm -f "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`; try { diff --git a/src/mitm/dns/dnsConfig.js b/src/mitm/dns/dnsConfig.js index 30508961..408755fa 100644 --- a/src/mitm/dns/dnsConfig.js +++ b/src/mitm/dns/dnsConfig.js @@ -1,4 +1,4 @@ -const { exec, spawn } = require("child_process"); +const { exec, spawn, execSync } = require("child_process"); const fs = require("fs"); const path = require("path"); const os = require("os"); @@ -59,14 +59,27 @@ function executeElevatedPowerShell(psScriptPath, timeoutMs = 30000) { }); } +/** True when `sudo` exists (e.g. missing on minimal Docker images like Alpine). */ +function isSudoAvailable() { + if (IS_WIN) return false; + try { + execSync("command -v sudo", { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + /** - * Execute command with sudo password via stdin (macOS/Linux only) + * Execute command with sudo password via stdin (macOS/Linux only). + * Without sudo in PATH (containers), runs via sh — same user, no elevation. */ function execWithPassword(command, password) { return new Promise((resolve, reject) => { - const child = spawn("sudo", ["-S", "sh", "-c", command], { - stdio: ["pipe", "pipe", "pipe"] - }); + const useSudo = isSudoAvailable(); + const child = useSudo + ? spawn("sudo", ["-S", "sh", "-c", command], { stdio: ["pipe", "pipe", "pipe"] }) + : spawn("sh", ["-c", command], { stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; @@ -78,8 +91,10 @@ function execWithPassword(command, password) { else reject(new Error(stderr || `Exit code ${code}`)); }); - child.stdin.write(`${password}\n`); - child.stdin.end(); + if (useSudo) { + child.stdin.write(`${password}\n`); + child.stdin.end(); + } }); } @@ -212,6 +227,7 @@ module.exports = { removeDNSEntry, removeAllDNSEntries, execWithPassword, + isSudoAvailable, executeElevatedPowerShell, checkDNSEntry, checkAllDNSStatus, diff --git a/src/mitm/manager.js b/src/mitm/manager.js index 1d96b73f..ed585b97 100644 --- a/src/mitm/manager.js +++ b/src/mitm/manager.js @@ -5,9 +5,10 @@ const os = require("os"); const net = require("net"); const https = require("https"); const crypto = require("crypto"); -const { addDNSEntry, removeDNSEntry, removeAllDNSEntries, checkAllDNSStatus, TOOL_HOSTS } = require("./dns/dnsConfig"); +const { addDNSEntry, removeDNSEntry, removeAllDNSEntries, checkAllDNSStatus, TOOL_HOSTS, isSudoAvailable } = require("./dns/dnsConfig"); const IS_WIN = process.platform === "win32"; +const IS_MAC = process.platform === "darwin"; const { generateCert } = require("./cert/generate"); const { installCert, uninstallCert } = require("./cert/install"); const { isCertExpired } = require("./cert/rootCA"); @@ -404,17 +405,22 @@ async function startServer(apiKey, sudoPassword) { // Step 1.5: Auto-install Root CA if not trusted yet const { checkCertInstalled } = require("./cert/install"); const rootCATrusted = await checkCertInstalled(rootCACertPath); + const linuxNoSystemTrust = !IS_WIN && !IS_MAC && !isSudoAvailable(); if (!rootCATrusted) { log("🔐 Cert: not trusted → installing..."); const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword(); - if (!password && !IS_WIN) { - throw new Error("Sudo password required to install Root CA certificate"); - } - try { - await installCert(password, rootCACertPath); - log("🔐 Cert: ✅ trusted"); - } catch (e) { - throw new Error(`Failed to trust certificate: ${e.message}`); + if (linuxNoSystemTrust) { + log(`🔐 Cert: skipping system trust (no sudo). Install ${rootCACertPath} as a trusted CA on machines that use this proxy.`); + } else { + if (!password && !IS_WIN) { + throw new Error("Sudo password required to install Root CA certificate"); + } + try { + await installCert(password, rootCACertPath); + log("🔐 Cert: ✅ trusted"); + } catch (e) { + throw new Error(`Failed to trust certificate: ${e.message}`); + } } } else { log("🔐 Cert: already trusted ✅"); @@ -443,8 +449,7 @@ async function startServer(apiKey, sudoPassword) { ); if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { }); - } else { - // Non-Windows: Root CA already installed in Step 1.5, just spawn server + } else if (isSudoAvailable()) { const inlineCmd = `ROUTER_API_KEY='${apiKey}' NODE_ENV='production' '${process.execPath}' '${SERVER_PATH}'`; serverProcess = spawn( "sudo", ["-S", "-E", "sh", "-c", inlineCmd], @@ -452,6 +457,13 @@ async function startServer(apiKey, sudoPassword) { ); serverProcess.stdin.write(`${sudoPassword}\n`); serverProcess.stdin.end(); + } else { + // Docker/minimal images: no sudo — same as Windows-style direct spawn + serverProcess = spawn(process.execPath, [SERVER_PATH], { + detached: false, + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, ROUTER_API_KEY: apiKey, NODE_ENV: "production" }, + }); } if (serverProcess) { @@ -590,6 +602,10 @@ async function trustCert(sudoPassword) { const rootCACertPath = path.join(MITM_DIR, "rootCA.crt"); if (!fs.existsSync(rootCACertPath)) throw new Error("Root CA not found. Start server first to generate it."); const { installCert } = require("./cert/install"); + if (!IS_WIN && !IS_MAC && !isSudoAvailable()) { + log(`🔐 Cert: system trust unavailable (no sudo). Use file: ${rootCACertPath}`); + return; + } const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword(); if (!password && !IS_WIN) throw new Error("Sudo password required to trust certificate"); await installCert(password, rootCACertPath); diff --git a/start.sh b/start.sh new file mode 100755 index 00000000..0e752296 --- /dev/null +++ b/start.sh @@ -0,0 +1,4 @@ +docker stop 9router +docker rm 9router +docker build -t 9router . +docker run -d --name 9router -p 20128:20128 --env-file .env -v 9router-data:/app/data 9router \ No newline at end of file