fix(mitm): clean up privileged hosts entries on exit when possible#5808
Conversation
installCleanupHandlers() only killed the spawned MITM child on SIGINT/SIGTERM and always left /etc/hosts spoofed for a manual Repair (Gap 7), even though a sudo password is often already cached for the session (getCachedPassword()). The extracted handleExitCleanup() now best-effort reverts every managed host via the same removeDNSEntry()/removeDNSEntries(collectManagedHosts()) pair stopMitm() already uses when a password is cached, and falls back to flagging _orphanedStateDetected exactly as before when none is available. Co-authored-by: manhdzzz <manhzack@gmail.com> Inspired-by: decolua/9router#2216
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Code Review
This pull request extracts the exit-cleanup logic from installCleanupHandlers into a testable handleExitCleanup function and adds corresponding unit tests to verify hosts cleanup behavior. Feedback on these changes highlights two key issues: first, because handleExitCleanup is asynchronous, calling it synchronously inside the signal handler can lead to the process exiting before cleanup completes or hanging; second, setting the in-memory _orphanedStateDetected flag right before process exit is a no-op since the state is lost upon termination.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const onSignal = (signal: string) => { | ||
| try { | ||
| if (serverProcess && !serverProcess.killed) serverProcess.kill("SIGTERM"); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| log.warn( | ||
| { signal }, | ||
| "MITM parent received signal — child terminated; run Repair if DNS/CA/proxy were applied." | ||
| ); | ||
| void handleExitCleanup(signal); | ||
| }; |
There was a problem hiding this comment.
Since handleExitCleanup is asynchronous, simply calling void handleExitCleanup(signal) means the cleanup process runs concurrently with the process exit sequence. If there are other signal listeners that call process.exit() synchronously, the process will terminate before the asynchronous cleanup (which spawns a child process to revert /etc/hosts) can complete. Conversely, if there are no other listeners and the application has persistent handles (like an HTTP server or database pool), registering a signal listener without calling process.exit() or re-sending the signal will cause the process to hang indefinitely on Ctrl+C.
By making onSignal asynchronous, awaiting handleExitCleanup, and then re-sending the signal to the process if no other listeners remain, we ensure that:
- The cleanup is fully completed before the default exit behavior is triggered.
- The process does not hang when there are no other signal listeners.
- The correct exit status/signal is preserved.
const onSignal = async (signal: string) => {
await handleExitCleanup(signal);
if (process.listenerCount(signal) === 0) {
process.kill(process.pid, signal);
}
};|
|
||
| const sudoPassword = deps.getCachedPassword(); | ||
| if (!sudoPassword) { | ||
| _orphanedStateDetected = true; |
There was a problem hiding this comment.
Setting the in-memory _orphanedStateDetected = true right before the process exits is a no-op because this state is lost when the process terminates. On the next application startup, _orphanedStateDetected will reset to false.
Furthermore, if the child process exits cleanly, it deletes the PID_FILE, meaning the stale PID file check in getMitmStatus() won't detect the orphaned state either, even though /etc/hosts remains spoofed.
To fix this robustly, consider updating getMitmStatus() (outside this diff) to also flag orphanedStateDetected if !running && dnsConfigured is true. This naturally detects orphaned DNS entries across restarts without relying on transient in-memory state or the presence of a stale PID file.
…r-2216-mitm-hosts-cleanup
…MITM hosts cleanup bullet
…r-2216-mitm-hosts-cleanup
Summary
installCleanupHandlers()only killed the spawned MITM child process onSIGINT/SIGTERMand always left the privileged/etc/hostsentries in place, requiring a manual dashboard Repair — even when a sudo password was already cached for the current session./etc/hostsentry when a cached password is available, and falls back to the existing orphaned-state flag (manual Repair) when it is not.Changes
handleExitCleanup(signal, depsOverride?)frominstallCleanupHandlers()insrc/mitm/manager.tsso the shutdown behavior is directly unit-testable without sending a real OS signal to the test process.SIGINT/SIGTERM, after terminating the spawned MITM child:getCachedPassword()returns a cached sudo password, best-effort calls the sameremoveDNSEntry()+removeDNSEntries(collectManagedHosts())pairstopMitm()already uses, reverting every managed host._orphanedStateDetectedexactly as before (dashboard Repair still available).sudo -S/execFileWithPasswordpath (src/mitm/systemCommands.ts) — not a directfs.writeFileSyncon/etc/hosts— so Hard Rule deps: bump qs from 6.14.1 to 6.14.2 #13 (no shell string-interpolation) and the project's existing privilege model are preserved.Attribution
Thanks to @manhdzzz for the original implementation.
Testing
tests/unit/mitm-hosts-cleanup-on-exit.test.tsremoveDNSEntry/removeDNSEntriesare invoked with the cached password and the managed host set, andorphanedStateDetectedstaysfalse.removeDNSEntry/removeDNSEntriesare not invoked andorphanedStateDetectedbecomestrue.node --import tsx/esm --test tests/unit/mitm-hosts-cleanup-on-exit.test.ts— 2/2 passing.mitm-manager-repair,mitm-manager-cleanup-symmetry,mitm-diagnostics,mitm-sudo-gate-822,mitm-sudo-graceful-degrade,mitm-upstream-ca-wiring,mitm-system-commands-prefix-3641,dns-config-generic.npm run typecheck:core— clean.npx eslint src/mitm/manager.ts tests/unit/mitm-hosts-cleanup-on-exit.test.ts— clean.