Files
bare-operating-system/packages/bare-os-booter/lib/bare-os-bind-fallback.js
T
Raven Scott 80485468f0 feat(booter): ephemeral port fallback on EADDRINUSE
Retry listen(0)/bind(0) when the preferred port is taken, gated by
BARE_OS_BIND_FALLBACK (default on; set 0/false for strict bind).

- Add lib/bare-os-bind-fallback.js and use it from bare-os-www-initd and
  bare-openssh (including privilege-path + holesail actual port).
- POSIX socket bridge in index.js: TCP listen and UDP bind fallbacks.
- bare-holesail + bare-holesail-managed: retry Holesail ready(); persist
  resolved listen port in managed state when it changes.
- Add test.bare-os-bind-fallback.js.
2026-04-23 20:39:50 -04:00

101 lines
2.7 KiB
JavaScript

/**
* Ephemeral TCP/UDP fallback when bind/listen hits EADDRINUSE (see BARE_OS_BIND_FALLBACK).
*/
export const BARE_OS_BIND_FALLBACK_ENV = 'BARE_OS_BIND_FALLBACK'
/**
* When unset or truthy, retry with port `0` after EADDRINUSE. Set `0` / `false` for strict bind.
* @param {Record<string, string | undefined> | null | undefined} env
*/
export function bindFallbackEnabled(env) {
if (!env || typeof env !== 'object') return true
const v = env[BARE_OS_BIND_FALLBACK_ENV]
if (v === '0' || v === 'false') return false
return true
}
/** @param {unknown} err */
export function isAddrInUse(err) {
const o = err && typeof err === 'object' ? err : null
const code = o && (o.code || o.errno)
if (code === 'EADDRINUSE') return true
const msg = String((o && /** @type {{ message?: string }} */ (o).message) || err || '').toLowerCase()
return msg.includes('eaddrinuse') || msg.includes('address already in use')
}
/**
* @param {{ address?: () => unknown }} server
* @param {number} fallback
*/
export function readListeningTcpPort(server, fallback) {
try {
const a =
server.address && typeof server.address === 'function'
? server.address()
: null
if (a && typeof a === 'object' && Number.isFinite(/** @type {{ port?: number }} */ (a).port))
return Number(/** @type {{ port?: number }} */ (a).port)
} catch {
/* ignore */
}
return fallback
}
/**
* `listen(port, host, cb)` + `once('error')` / `off('error')` (net.Server / http.Server).
* @param {{ listen: Function, address?: Function, once: Function, off: Function }} server
* @param {number} preferredPort
* @param {string} host
* @param {Record<string, string | undefined> | null | undefined} env
* @returns {Promise<{ port: number }>}
*/
export async function listenTcpWithFallback(server, preferredPort, host, env) {
const tryOnce = (port) =>
new Promise((resolve, reject) => {
const onErr = (e) => {
try {
server.off('error', onErr)
} catch {
/* ignore */
}
reject(e)
}
server.once('error', onErr)
try {
server.listen(port, host, () => {
try {
server.off('error', onErr)
} catch {
/* ignore */
}
resolve(null)
})
} catch (e) {
try {
server.off('error', onErr)
} catch {
/* ignore */
}
reject(e)
}
})
try {
await tryOnce(preferredPort)
} catch (e) {
if (
bindFallbackEnabled(env) &&
isAddrInUse(e) &&
preferredPort !== 0
) {
await tryOnce(0)
} else {
throw e
}
}
const port = readListeningTcpPort(server, preferredPort)
return { port }
}