[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 <locnh@uniultra.xyz>
This commit is contained in:
Loc Nguyen Huu
2026-03-23 09:23:14 +07:00
committed by GitHub
parent 4d7ddbfffe
commit 8c0b4a3e84
6 changed files with 66 additions and 20 deletions

1
.gitignore vendored
View File

@@ -66,5 +66,4 @@ package-lock.json
README1.md README1.md
deploy.sh deploy.sh
ecosystem.config.* ecosystem.config.*
start.sh

View File

@@ -22,6 +22,10 @@ COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/open-sse ./open-sse 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 RUN mkdir -p /app/data

View File

@@ -1,7 +1,7 @@
const fs = require("fs"); const fs = require("fs");
const crypto = require("crypto"); const crypto = require("crypto");
const { exec } = require("child_process"); const { exec } = require("child_process");
const { execWithPassword } = require("../dns/dnsConfig.js"); const { execWithPassword, isSudoAvailable } = require("../dns/dnsConfig.js");
const { log, err } = require("../logger"); const { log, err } = require("../logger");
const IS_WIN = process.platform === "win32"; const IS_WIN = process.platform === "win32";
@@ -151,6 +151,10 @@ function checkCertInstalledLinux() {
} }
async function installCertLinux(sudoPassword, certPath) { 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`; const destFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`;
// Try update-ca-certificates (Debian/Ubuntu), fallback to update-ca-trust (Fedora/RHEL) // 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)`; 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) { async function uninstallCertLinux(sudoPassword) {
if (!isSudoAvailable()) {
return;
}
const destFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`; 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)`; const cmd = `rm -f "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
try { try {

View File

@@ -1,4 +1,4 @@
const { exec, spawn } = require("child_process"); const { exec, spawn, execSync } = require("child_process");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const os = require("os"); 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) { function execWithPassword(command, password) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = spawn("sudo", ["-S", "sh", "-c", command], { const useSudo = isSudoAvailable();
stdio: ["pipe", "pipe", "pipe"] const child = useSudo
}); ? spawn("sudo", ["-S", "sh", "-c", command], { stdio: ["pipe", "pipe", "pipe"] })
: spawn("sh", ["-c", command], { stdio: ["ignore", "pipe", "pipe"] });
let stdout = ""; let stdout = "";
let stderr = ""; let stderr = "";
@@ -78,8 +91,10 @@ function execWithPassword(command, password) {
else reject(new Error(stderr || `Exit code ${code}`)); else reject(new Error(stderr || `Exit code ${code}`));
}); });
child.stdin.write(`${password}\n`); if (useSudo) {
child.stdin.end(); child.stdin.write(`${password}\n`);
child.stdin.end();
}
}); });
} }
@@ -212,6 +227,7 @@ module.exports = {
removeDNSEntry, removeDNSEntry,
removeAllDNSEntries, removeAllDNSEntries,
execWithPassword, execWithPassword,
isSudoAvailable,
executeElevatedPowerShell, executeElevatedPowerShell,
checkDNSEntry, checkDNSEntry,
checkAllDNSStatus, checkAllDNSStatus,

View File

@@ -5,9 +5,10 @@ const os = require("os");
const net = require("net"); const net = require("net");
const https = require("https"); const https = require("https");
const crypto = require("crypto"); 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_WIN = process.platform === "win32";
const IS_MAC = process.platform === "darwin";
const { generateCert } = require("./cert/generate"); const { generateCert } = require("./cert/generate");
const { installCert, uninstallCert } = require("./cert/install"); const { installCert, uninstallCert } = require("./cert/install");
const { isCertExpired } = require("./cert/rootCA"); 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 // Step 1.5: Auto-install Root CA if not trusted yet
const { checkCertInstalled } = require("./cert/install"); const { checkCertInstalled } = require("./cert/install");
const rootCATrusted = await checkCertInstalled(rootCACertPath); const rootCATrusted = await checkCertInstalled(rootCACertPath);
const linuxNoSystemTrust = !IS_WIN && !IS_MAC && !isSudoAvailable();
if (!rootCATrusted) { if (!rootCATrusted) {
log("🔐 Cert: not trusted → installing..."); log("🔐 Cert: not trusted → installing...");
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword(); const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
if (!password && !IS_WIN) { if (linuxNoSystemTrust) {
throw new Error("Sudo password required to install Root CA certificate"); log(`🔐 Cert: skipping system trust (no sudo). Install ${rootCACertPath} as a trusted CA on machines that use this proxy.`);
} } else {
try { if (!password && !IS_WIN) {
await installCert(password, rootCACertPath); throw new Error("Sudo password required to install Root CA certificate");
log("🔐 Cert: ✅ trusted"); }
} catch (e) { try {
throw new Error(`Failed to trust certificate: ${e.message}`); await installCert(password, rootCACertPath);
log("🔐 Cert: ✅ trusted");
} catch (e) {
throw new Error(`Failed to trust certificate: ${e.message}`);
}
} }
} else { } else {
log("🔐 Cert: already trusted ✅"); log("🔐 Cert: already trusted ✅");
@@ -443,8 +449,7 @@ async function startServer(apiKey, sudoPassword) {
); );
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { }); if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
} else { } else if (isSudoAvailable()) {
// Non-Windows: Root CA already installed in Step 1.5, just spawn server
const inlineCmd = `ROUTER_API_KEY='${apiKey}' NODE_ENV='production' '${process.execPath}' '${SERVER_PATH}'`; const inlineCmd = `ROUTER_API_KEY='${apiKey}' NODE_ENV='production' '${process.execPath}' '${SERVER_PATH}'`;
serverProcess = spawn( serverProcess = spawn(
"sudo", ["-S", "-E", "sh", "-c", inlineCmd], "sudo", ["-S", "-E", "sh", "-c", inlineCmd],
@@ -452,6 +457,13 @@ async function startServer(apiKey, sudoPassword) {
); );
serverProcess.stdin.write(`${sudoPassword}\n`); serverProcess.stdin.write(`${sudoPassword}\n`);
serverProcess.stdin.end(); 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) { if (serverProcess) {
@@ -590,6 +602,10 @@ async function trustCert(sudoPassword) {
const rootCACertPath = path.join(MITM_DIR, "rootCA.crt"); 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."); if (!fs.existsSync(rootCACertPath)) throw new Error("Root CA not found. Start server first to generate it.");
const { installCert } = require("./cert/install"); 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(); const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
if (!password && !IS_WIN) throw new Error("Sudo password required to trust certificate"); if (!password && !IS_WIN) throw new Error("Sudo password required to trust certificate");
await installCert(password, rootCACertPath); await installCert(password, rootCACertPath);

4
start.sh Executable file
View File

@@ -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