mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
[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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -66,5 +66,4 @@ package-lock.json
|
|||||||
README1.md
|
README1.md
|
||||||
deploy.sh
|
deploy.sh
|
||||||
ecosystem.config.*
|
ecosystem.config.*
|
||||||
start.sh
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user