Skip to content

Commit 63bb57f

Browse files
committed
fix: degrade gracefully when port probe gets EPERM/EACCES
Fixes #544. In restricted environments (sandboxes, seccomp, AppArmor), the net.createServer bind probe can fail with EPERM or EACCES even when the port is free. Previously this was treated as "port unavailable," killing onboarding with a false port conflict. Now EPERM/EACCES resolve as ok:true with a warning field, so preflight degrades gracefully instead of blocking.
1 parent 20d0c95 commit 63bb57f

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

bin/lib/preflight.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ async function checkPortAvailable(port, opts) {
7272
pid: null,
7373
reason: `port ${p} is in use (EADDRINUSE)`,
7474
});
75+
} else if (err.code === "EPERM" || err.code === "EACCES") {
76+
// Bind blocked by sandbox/seccomp/AppArmor policy — not a port
77+
// conflict. Degrade gracefully and assume the port is available.
78+
resolve({
79+
ok: true,
80+
warning: `port probe skipped: ${err.code} (${err.message})`,
81+
});
7582
} else {
7683
// Unexpected error — treat port as unavailable
7784
resolve({

test/preflight.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,57 @@ describe("checkPortAvailable", () => {
110110
assert.equal(typeof result.ok, "boolean");
111111
});
112112

113+
// Helper: monkey-patch net.createServer to emit a specific error code on listen.
114+
function mockBindError(code, message) {
115+
const origCreate = net.createServer;
116+
net.createServer = function () {
117+
const srv = origCreate.call(net);
118+
srv.listen = function () {
119+
process.nextTick(() => {
120+
const err = new Error(message);
121+
err.code = code;
122+
srv.emit("error", err);
123+
});
124+
return srv;
125+
};
126+
return srv;
127+
};
128+
return origCreate;
129+
}
130+
131+
it("degrades gracefully when bind is blocked by EPERM", async () => {
132+
const restore = mockBindError("EPERM", "listen EPERM: operation not permitted 127.0.0.1");
133+
try {
134+
const result = await checkPortAvailable(9999, { skipLsof: true });
135+
assert.equal(result.ok, true);
136+
assert.ok(result.warning && result.warning.includes("EPERM"));
137+
} finally {
138+
net.createServer = restore;
139+
}
140+
});
141+
142+
it("degrades gracefully when bind is blocked by EACCES", async () => {
143+
const restore = mockBindError("EACCES", "listen EACCES: permission denied 127.0.0.1");
144+
try {
145+
const result = await checkPortAvailable(9999, { skipLsof: true });
146+
assert.equal(result.ok, true);
147+
assert.ok(result.warning && result.warning.includes("EACCES"));
148+
} finally {
149+
net.createServer = restore;
150+
}
151+
});
152+
153+
it("treats unexpected bind errors as port unavailable", async () => {
154+
const restore = mockBindError("ENOTFOUND", "getaddrinfo ENOTFOUND 127.0.0.1");
155+
try {
156+
const result = await checkPortAvailable(9999, { skipLsof: true });
157+
assert.equal(result.ok, false);
158+
assert.ok(result.reason.includes("port probe failed"));
159+
} finally {
160+
net.createServer = restore;
161+
}
162+
});
163+
113164
it("checks gateway port 8080", async () => {
114165
const freePort = await new Promise((resolve) => {
115166
const srv = net.createServer();

0 commit comments

Comments
 (0)