diff --git a/doc/api/errors.md b/doc/api/errors.md
index 4714df8e8244b9..c61c92cc52e251 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -2972,6 +2972,14 @@ disconnected socket.
A call was made and the UDP subsystem was not running.
+
+
+### `ERR_SOCKET_HANDLE_ADOPTED`
+
+An operation was attempted on a [`BoundHandle`][] that had already been adopted
+by a [`net.Server`][] or [`net.Socket`][]. Once a bound handle is adopted, its
+`address()` and `close()` methods can no longer be used.
+
### `ERR_SOURCE_MAP_CORRUPT`
@@ -4552,6 +4560,7 @@ An error occurred trying to allocate memory. This should never happen.
[`--force-fips`]: cli.md#--force-fips
[`--no-addons`]: cli.md#--no-addons
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
+[`BoundHandle`]: net.md#class-netboundhandle
[`Class: assert.AssertionError`]: assert.md#class-assertassertionerror
[`ERR_INCOMPATIBLE_OPTION_PAIR`]: #err_incompatible_option_pair
[`ERR_INVALID_ARG_TYPE`]: #err_invalid_arg_type
@@ -4595,7 +4604,9 @@ An error occurred trying to allocate memory. This should never happen.
[`http`]: http.md
[`https`]: https.md
[`libuv Error handling`]: https://docs.libuv.org/en/v1.x/errors.html
+[`net.Server`]: net.md#class-netserver
[`net.Socket.write()`]: net.md#socketwritedata-encoding-callback
+[`net.Socket`]: net.md#class-netsocket
[`net`]: net.md
[`new URL(input)`]: url.md#new-urlinput-base
[`new URLPattern(input)`]: url.md#new-urlpatternstring-baseurl-options
diff --git a/doc/api/net.md b/doc/api/net.md
index 9ee3c1497397da..52bae026f3b529 100644
--- a/doc/api/net.md
+++ b/doc/api/net.md
@@ -523,8 +523,12 @@ Start a server listening for connections on a given `handle` that has
already been bound to a port, a Unix domain socket, or a Windows named pipe.
The `handle` object can be either a server, a socket (anything with an
-underlying `_handle` member), or an object with an `fd` member that is a
-valid file descriptor.
+underlying `_handle` member), a [`BoundHandle`][], or an object with an `fd`
+member that is a valid file descriptor.
+
+When `handle` is a [`BoundHandle`][], the server adopts the already-bound
+socket and starts listening on it. Adoption consumes the bound handle (see
+[ownership transfer][`BoundHandle`]).
Listening on a file descriptor is not supported on Windows.
@@ -550,6 +554,10 @@ changes:
* `backlog` {number} Common parameter of [`server.listen()`][]
functions.
* `exclusive` {boolean} **Default:** `false`
+ * `handle` {net.BoundHandle} A pre-bound [`BoundHandle`][]. The server adopts
+ the already-bound socket and listens on it, ignoring `host`, `port`, and
+ `path`. Adoption consumes the bound handle (see
+ [ownership transfer][`BoundHandle`]).
* `host` {string}
* `ipv6Only` {boolean} For TCP servers, setting `ipv6Only` to `true` will
disable dual-stack support, i.e., binding to host `::` won't make
@@ -573,7 +581,8 @@ changes:
functions.
* Returns: {net.Server}
-If `port` is specified, it behaves the same as
+If `handle` is specified, the server adopts that pre-bound socket. Otherwise, if
+`port` is specified, it behaves the same as
[`server.listen([port[, host[, backlog]]][, callback])`][`server.listen(port)`].
Otherwise, if `path` is specified, it behaves the same as
[`server.listen(path[, backlog][, callback])`][`server.listen(path)`].
@@ -769,6 +778,12 @@ changes:
access to specific IP addresses, IP ranges, or IP subnets.
* `fd` {number} If specified, wrap around an existing socket with
the given file descriptor, otherwise a new socket will be created.
+ * `handle` {net.BoundHandle} If specified, wrap around the bound socket from a
+ [`BoundHandle`][]. A subsequent
+ [`socket.connect()`][`socket.connect()`] uses the bound handle as the
+ connection's source binding (honoring the bound local address and port).
+ Adoption consumes the bound handle (see
+ [ownership transfer][`BoundHandle`]).
* `keepAlive` {boolean} If set to `true`, it enables keep-alive functionality on
the socket immediately after the connection is established, similarly on what
is done in [`socket.setKeepAlive()`][]. **Default:** `false`.
@@ -1627,6 +1642,94 @@ This property represents the state of the connection as a string.
* If the stream is readable and not writable, it is `readOnly`.
* If the stream is not readable and writable, it is `writeOnly`.
+## Class: `net.BoundHandle`
+
+
+
+Allows for the synchronous creation of a pre-bound socket, that can be passed
+to `listen()` or `new net.Socket()` later on. For `listen()` this enables
+synchronous port reservation, while for `new net.Socket()`, it allows control
+over the local egress port/IP, via `bind(2)` semantics.
+
+Adoption transfers ownership of the socket; afterwards `address()` and `close()`
+throw [`ERR_SOCKET_HANDLE_ADOPTED`][]. A handle that is never adopted must be
+closed to avoid leaking the socket.
+
+```mjs
+import net from 'node:net';
+
+const bound = new net.BoundHandle();
+const { port } = bound.address();
+console.log(`Reserved port ${port} for server`);
+
+const server = net.createServer();
+server.listen(bound); // Adopt as a server, or pass to new net.Socket() instead.
+```
+
+### `new net.BoundHandle([options])`
+
+
+
+* `options` {Object}
+ * `host` {string} Local address to bind. Must be a numeric IP literal; no DNS
+ resolution is performed. **Default:** `'0.0.0.0'`, or `'::'` when
+ `ipv6Only` is `true`.
+ * `port` {number} Local port. `0` requests an OS-assigned ephemeral port.
+ **Default:** `0`.
+ * `ipv6Only` {boolean} Sets `IPV6_V6ONLY`, disabling dual-stack support so the
+ socket binds IPv6 only. Only meaningful for IPv6 binds. **Default:**
+ `false`.
+ * `reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
+ the same address and port for kernel-level load balancing. Support is
+ platform-dependent. **Default:** `false`.
+
+### `boundHandle.address()`
+
+
+
+* Returns: {Object} An object with `address`, `family`, and `port` properties,
+ as [`server.address()`][] returns.
+
+Returns the bound local address. When bound with `port: 0`, `port` is the
+OS-assigned ephemeral port.
+
+### `boundHandle.fd()`
+
+
+
+* Returns: {integer} The underlying OS file descriptor, or `-1` on platforms
+ that do not expose one for sockets (such as Windows).
+
+Returns the file descriptor of the bound socket. Ownership remains with the
+`BoundHandle`, so the descriptor must not be closed by the caller. The
+descriptor is only available before the handle is adopted; afterwards it belongs
+to the adopting [`net.Server`][] or [`net.Socket`][] and `fd()` throws
+[`ERR_SOCKET_HANDLE_ADOPTED`][].
+
+### `boundHandle.close()`
+
+
+
+Releases the bound socket. Only needed when the handle is never adopted.
+
+### `boundHandle[Symbol.dispose]()`
+
+
+
+Closes the handle if it has not been adopted or closed; otherwise a no-op.
+
## `net.connect()`
Aliases to
@@ -1721,6 +1824,9 @@ and [`socket.connect(options[, connectListener])`][`socket.connect(options)`].
Additional options:
+* `handle` {net.BoundHandle} A pre-bound [`BoundHandle`][] used as the
+ connection's source binding, honoring its local address and port. Adoption
+ consumes the bound handle (see [ownership transfer][`BoundHandle`]).
* `timeout` {number} If set, will be used to call
[`socket.setTimeout(timeout)`][] after the socket is created, but before
it starts the connection.
@@ -2097,6 +2203,8 @@ net.isIPv6('fhqwhgads'); // returns false
[`'error'`]: #event-error_1
[`'listening'`]: #event-listening
[`'timeout'`]: #event-timeout
+[`BoundHandle`]: #class-netboundhandle
+[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
[`EventEmitter`]: events.md#class-eventemitter
[`child_process.fork()`]: child_process.md#child_processforkmodulepath-args-options
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
@@ -2116,6 +2224,7 @@ net.isIPv6('fhqwhgads'); // returns false
[`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: #netgetdefaultautoselectfamilyattempttimeout
[`new net.Socket(options)`]: #new-netsocketoptions
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
+[`server.address()`]: #serveraddress
[`server.close()`]: #serverclosecallback
[`server.dropMaxConnection`]: #serverdropmaxconnection
[`server.listen()`]: #serverlisten
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index f09788538ce8f5..b8b73d74e3c4ce 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1788,6 +1788,8 @@ E('ERR_SOCKET_CONNECTION_TIMEOUT',
E('ERR_SOCKET_DGRAM_IS_CONNECTED', 'Already connected', Error);
E('ERR_SOCKET_DGRAM_NOT_CONNECTED', 'Not connected', Error);
E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running', Error);
+E('ERR_SOCKET_HANDLE_ADOPTED',
+ 'The bound handle has already been adopted by a server or socket', Error);
E('ERR_SOURCE_MAP_CORRUPT', `The source map for '%s' does not exist or is corrupt.`, Error);
E('ERR_SOURCE_MAP_MISSING_SOURCE', `Cannot find '%s' imported from the source map for '%s'`, Error);
E('ERR_SRI_PARSE',
diff --git a/lib/net.js b/lib/net.js
index ee4bc9943e4d52..9d33feca561b2c 100644
--- a/lib/net.js
+++ b/lib/net.js
@@ -118,6 +118,7 @@ const {
ERR_SOCKET_CLOSED,
ERR_SOCKET_CLOSED_BEFORE_CONNECTION,
ERR_SOCKET_CONNECTION_TIMEOUT,
+ ERR_SOCKET_HANDLE_ADOPTED,
},
genericNodeError,
} = require('internal/errors');
@@ -135,6 +136,7 @@ const {
validateFunction,
validateInt32,
validateNumber,
+ validateObject,
validatePort,
validateString,
} = require('internal/validators');
@@ -362,6 +364,122 @@ function closeSocketHandle(self, isException, isCleanupPending = false) {
const kBytesRead = Symbol('kBytesRead');
const kBytesWritten = Symbol('kBytesWritten');
const kSetTOS = Symbol('kSetTOS');
+// Marks a Socket whose handle is an adopted, already-bound BoundHandle.
+const kBoundSource = Symbol('kBoundSource');
+
+// Internal: adopt the underlying handle, transferring ownership to a
+// Server/Socket.
+const kBoundHandleConsume = Symbol('kBoundHandleConsume');
+
+// A role-neutral wrapper over a synchronously bound libuv TCP handle: bound to
+// a local address but neither listening nor connecting until adopted by exactly
+// one Server (server.listen) or Socket (new net.Socket({ handle })). Adoption
+// transfers ownership; an un-adopted handle must be closed by the caller.
+// bind(2) is non-blocking, so binding happens inline and errors throw
+// synchronously. host must be a numeric IP literal; no DNS is performed.
+class BoundHandle {
+ #handle;
+ #address = {};
+
+ constructor(options = kEmptyObject) {
+ validateObject(options, 'options');
+
+ const port = validatePort(options.port ?? 0, 'options.port');
+
+ const ipv6Only = options.ipv6Only ?? false;
+ validateBoolean(ipv6Only, 'options.ipv6Only');
+
+ const reusePort = options.reusePort ?? false;
+ validateBoolean(reusePort, 'options.reusePort');
+
+ let { host } = options;
+ let addressType;
+ if (host === undefined || host === null) {
+ host = ipv6Only ? DEFAULT_IPV6_ADDR : DEFAULT_IPV4_ADDR;
+ addressType = ipv6Only ? 6 : 4;
+ } else {
+ validateString(host, 'options.host');
+ addressType = isIP(host);
+ if (addressType === 0) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options.host', host,
+ 'must be a numeric IP address; net.BoundHandle does not perform DNS resolution');
+ }
+ }
+
+ let flags = 0;
+ if (ipv6Only) {
+ flags |= TCPConstants.UV_TCP_IPV6ONLY;
+ }
+ if (reusePort) {
+ flags |= TCPConstants.UV_TCP_REUSEPORT;
+ }
+
+ const handle = new TCP(TCPConstants.SOCKET);
+ let err = addressType === 6 ?
+ handle.bind6(host, port, flags) :
+ handle.bind(host, port, flags);
+ // EADDRINUSE is deferred by libuv's uv_tcp_bind(), which returns 0 and
+ // surfaces it on the next getsockname/listen/connect. Force getsockname()
+ // now so conflicts throw synchronously; the address is cached since the
+ // handle is immutable.
+ if (err === 0) {
+ err = handle.getsockname(this.#address);
+ }
+ if (err) {
+ handle.close();
+ throw new ExceptionWithHostPort(err, 'bind', host, port);
+ }
+
+ this.#handle = handle;
+ }
+
+ // The kernel-assigned local address, resolved at construction; reflects the
+ // OS-assigned ephemeral port when the bind requested port 0.
+ address() {
+ if (this.#handle === null) {
+ throw new ERR_SOCKET_HANDLE_ADOPTED();
+ }
+ return this.#address;
+ }
+
+ // The underlying OS file descriptor, or -1 where sockets have none (Windows).
+ // Ownership stays with the BoundHandle until it is adopted, so the caller must
+ // not close it.
+ fd() {
+ if (this.#handle === null) {
+ throw new ERR_SOCKET_HANDLE_ADOPTED();
+ }
+ return this.#handle.fd;
+ }
+
+ // Release the socket if it is never adopted, preventing an fd/handle leak.
+ close() {
+ if (this.#handle === null) {
+ throw new ERR_SOCKET_HANDLE_ADOPTED();
+ }
+ this.#handle.close();
+ this.#handle = null;
+ }
+
+ // Enables `using bound = new net.BoundHandle(...)`: closes an un-adopted
+ // handle and is a no-op once the handle has been adopted or closed.
+ [SymbolDispose]() {
+ if (this.#handle !== null) {
+ this.#handle.close();
+ this.#handle = null;
+ }
+ }
+
+ [kBoundHandleConsume]() {
+ if (this.#handle === null) {
+ throw new ERR_SOCKET_HANDLE_ADOPTED();
+ }
+ const handle = this.#handle;
+ this.#handle = null;
+ return handle;
+ }
+}
function Socket(options) {
if (!(this instanceof Socket)) return new Socket(options);
@@ -420,8 +538,17 @@ function Socket(options) {
options.decodeStrings = false;
stream.Duplex.call(this, options);
+ // An adopted BoundHandle is bound but not connected: defer the read flow
+ // until connect() completes.
+ let boundNotConnected = false;
if (options.handle) {
- this._handle = options.handle; // private
+ if (options.handle instanceof BoundHandle) {
+ this._handle = options.handle[kBoundHandleConsume]();
+ this[kBoundSource] = true;
+ boundNotConnected = true;
+ } else {
+ this._handle = options.handle; // private
+ }
this[async_id_symbol] = getNewAsyncId(this._handle);
} else if (options.fd !== undefined) {
const { fd } = options;
@@ -492,7 +619,7 @@ function Socket(options) {
// If we have a handle, then start the flow of data into the
// buffer. if not, then this will happen when we connect
- if (this._handle && options.readable !== false) {
+ if (this._handle && options.readable !== false && !boundNotConnected) {
if (options.pauseOnCreate) {
// Stop the handle from reading and pause the stream
this._handle.reading = false;
@@ -1391,6 +1518,17 @@ function lookupAndConnect(self, options) {
validateString(host, 'options.host');
+ // An adopted BoundHandle already owns the local endpoint and address family.
+ if (self[kBoundSource]) {
+ if (localAddress !== undefined || localPort !== undefined) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options',
+ options,
+ 'localAddress and localPort cannot be used with an adopted bound handle');
+ }
+ autoSelectFamily = false;
+ }
+
if (localAddress && !isIP(localAddress)) {
throw new ERR_INVALID_IP_ADDRESS(localAddress);
}
@@ -2144,6 +2282,21 @@ Server.prototype.listen = function(...args) {
toNumber(args.length > 1 && args[1]) ||
toNumber(args.length > 2 && args[2]); // (port, host, backlog)
+ // (boundHandle[, ...]) or ({ handle: boundHandle }[, ...]): adopt the bound
+ // handle, transferring ownership so the BoundHandle can no longer close it.
+ let boundHandle = null;
+ if (options instanceof BoundHandle) {
+ boundHandle = options;
+ } else if (options.handle instanceof BoundHandle) {
+ boundHandle = options.handle;
+ }
+ if (boundHandle !== null) {
+ this._handle = boundHandle[kBoundHandleConsume]();
+ this[async_id_symbol] = this._handle.getAsyncId();
+ this._listeningId++;
+ listenInCluster(this, null, -1, -1, backlogFromArgs, undefined, true);
+ return this;
+ }
options = options._handle || options.handle || options;
const flags = getFlags(options);
// Refresh the id to make the previous call invalid
@@ -2573,6 +2726,7 @@ module.exports = {
SocketAddress ??= require('internal/socketaddress').SocketAddress;
return SocketAddress;
},
+ BoundHandle,
connect,
createConnection: connect,
createServer,
diff --git a/test/parallel/test-net-boundhandle.js b/test/parallel/test-net-boundhandle.js
new file mode 100644
index 00000000000000..b293697085433f
--- /dev/null
+++ b/test/parallel/test-net-boundhandle.js
@@ -0,0 +1,232 @@
+'use strict';
+const common = require('../common');
+const assert = require('assert');
+const net = require('net');
+
+// Constructing a BoundHandle binds synchronously and address() reports the
+// resolved address, including the OS-assigned ephemeral port when port is 0.
+{
+ const bound = new net.BoundHandle({ host: '127.0.0.1', port: 0 });
+ const addr = bound.address();
+
+ assert.strictEqual(addr.address, '127.0.0.1');
+ assert.strictEqual(addr.family, 'IPv4');
+ assert.strictEqual(typeof addr.port, 'number');
+ assert.ok(addr.port > 0);
+
+ // fd() exposes the underlying descriptor (a real fd on POSIX, -1 on Windows).
+ const fd = bound.fd();
+ assert.strictEqual(typeof fd, 'number');
+ if (!common.isWindows) {
+ assert.ok(fd >= 0);
+ }
+
+ bound.close();
+}
+
+// Defaults the host to the IPv4 wildcard when omitted.
+{
+ const bound = new net.BoundHandle({ port: 0 });
+ const addr = bound.address();
+ assert.strictEqual(addr.address, '0.0.0.0');
+ assert.strictEqual(addr.family, 'IPv4');
+ assert.ok(addr.port > 0);
+ bound.close();
+}
+
+// No arguments reserves an OS-assigned ephemeral port on the IPv4 wildcard.
+{
+ const bound = new net.BoundHandle();
+ const addr = bound.address();
+ assert.strictEqual(addr.address, '0.0.0.0');
+ assert.strictEqual(addr.family, 'IPv4');
+ assert.ok(addr.port > 0);
+ bound.close();
+}
+
+// Binding to a port held by a live listener throws EADDRINUSE synchronously.
+// libuv defers this error from uv_tcp_bind(), so the constructor forces a
+// getsockname() to surface it eagerly. (Two role-neutral, not-yet-listening
+// binds to the same port instead coexist, since libuv sets SO_REUSEADDR.)
+{
+ const server = net.createServer();
+ server.listen(0, '127.0.0.1', common.mustCall(() => {
+ const { port } = server.address();
+ assert.throws(() => {
+ new net.BoundHandle({ host: '127.0.0.1', port });
+ }, {
+ code: 'EADDRINUSE',
+ syscall: 'bind',
+ });
+ server.close();
+ }));
+}
+
+// Throws synchronously on a non-numeric host (no DNS resolution).
+{
+ assert.throws(() => new net.BoundHandle({ host: 'localhost', port: 0 }), {
+ code: 'ERR_INVALID_ARG_VALUE',
+ name: 'TypeError',
+ });
+}
+
+// Rejects a non-string host and a non-object options argument.
+{
+ assert.throws(() => new net.BoundHandle({ host: 1234 }),
+ { code: 'ERR_INVALID_ARG_TYPE' });
+ assert.throws(() => new net.BoundHandle(0), { code: 'ERR_INVALID_ARG_TYPE' });
+}
+
+// Throws synchronously on a non-local address (EADDRNOTAVAIL). 192.0.2.0/24 is
+// TEST-NET-1 (RFC 5737) and is never assigned to a local interface.
+{
+ assert.throws(() => new net.BoundHandle({ host: '192.0.2.1', port: 0 }), {
+ code: 'EADDRNOTAVAIL',
+ syscall: 'bind',
+ });
+}
+
+// Binding a privileged port without privilege throws EACCES synchronously.
+if (!common.isWindows && process.getuid() !== 0) {
+ assert.throws(() => new net.BoundHandle({ host: '127.0.0.1', port: 1 }), {
+ code: 'EACCES',
+ syscall: 'bind',
+ });
+}
+
+// An un-adopted handle releases its socket cleanly on close(): the port becomes
+// immediately re-bindable.
+{
+ const bound = new net.BoundHandle({ host: '127.0.0.1', port: 0 });
+ const { port } = bound.address();
+ bound.close();
+ const again = new net.BoundHandle({ host: '127.0.0.1', port });
+ assert.strictEqual(again.address().port, port);
+ again.close();
+}
+
+// Server adoption: server.listen(boundHandle), then a client round-trips.
+{
+ const bound = new net.BoundHandle({ host: '127.0.0.1', port: 0 });
+ const { port } = bound.address();
+
+ const server = net.createServer(common.mustCall((socket) => {
+ socket.on('data', common.mustCall((data) => {
+ assert.strictEqual(data.toString(), 'ping');
+ socket.end('pong');
+ }));
+ }));
+
+ server.listen(bound, common.mustCall(() => {
+ assert.strictEqual(server.address().port, port);
+
+ // The bound handle has been adopted: address()/fd()/close() now throw.
+ assert.throws(() => bound.address(), { code: 'ERR_SOCKET_HANDLE_ADOPTED' });
+ assert.throws(() => bound.fd(), { code: 'ERR_SOCKET_HANDLE_ADOPTED' });
+ assert.throws(() => bound.close(), { code: 'ERR_SOCKET_HANDLE_ADOPTED' });
+
+ const client = net.connect({ host: '127.0.0.1', port }, () => {
+ client.end('ping');
+ });
+ client.on('data', common.mustCall((data) => {
+ assert.strictEqual(data.toString(), 'pong');
+ }));
+ client.on('close', common.mustCall(() => server.close()));
+ }));
+}
+
+// Client adoption: new net.Socket({ handle: boundHandle }).connect(...) honors
+// the bound source port and round-trips.
+{
+ const server = net.createServer(common.mustCall((socket) => {
+ socket.on('data', common.mustCall((data) => {
+ assert.strictEqual(data.toString(), 'ping');
+ socket.end('pong');
+ }));
+ }));
+
+ server.listen(0, '127.0.0.1', common.mustCall(() => {
+ const serverPort = server.address().port;
+
+ const bound = new net.BoundHandle({ host: '127.0.0.1', port: 0 });
+ const localPort = bound.address().port;
+
+ const client = new net.Socket({ handle: bound });
+
+ // Adoption consumed the bound handle.
+ assert.throws(() => bound.address(), { code: 'ERR_SOCKET_HANDLE_ADOPTED' });
+
+ client.connect({ host: '127.0.0.1', port: serverPort }, common.mustCall(() => {
+ assert.strictEqual(client.localPort, localPort);
+ client.end('ping');
+ }));
+ client.on('data', common.mustCall((data) => {
+ assert.strictEqual(data.toString(), 'pong');
+ }));
+ client.on('close', common.mustCall(() => server.close()));
+ }));
+}
+
+// connect() rejects localAddress/localPort when adopting a bound handle: the
+// handle already owns the local endpoint.
+{
+ const bound = new net.BoundHandle({ host: '127.0.0.1', port: 0 });
+ const client = new net.Socket({ handle: bound });
+ assert.throws(() => {
+ client.connect({ host: '127.0.0.1', port: 1, localPort: 0 });
+ }, { code: 'ERR_INVALID_ARG_VALUE' });
+ client.destroy();
+}
+
+// reusePort: SO_REUSEPORT permits multiple listeners on the same port. Support
+// is platform-dependent, so probe first.
+{
+ let first;
+ try {
+ first = new net.BoundHandle({ host: '127.0.0.1', port: 0, reusePort: true });
+ } catch {
+ first = null; // SO_REUSEPORT unsupported on this platform.
+ }
+ if (first) {
+ const { port } = first.address();
+ const second = new net.BoundHandle({ host: '127.0.0.1', port, reusePort: true });
+
+ const s1 = net.createServer();
+ const s2 = net.createServer();
+ s1.listen(first, common.mustCall(() => {
+ s2.listen(second, common.mustCall(() => {
+ assert.strictEqual(s1.address().port, port);
+ assert.strictEqual(s2.address().port, port);
+ s1.close();
+ s2.close();
+ }));
+ }));
+ }
+}
+
+// IPv6 binds: loopback, and ipv6Only dual-stack control.
+if (common.hasIPv6) {
+ {
+ const bound = new net.BoundHandle({ host: '::1', port: 0 });
+ const addr = bound.address();
+ assert.strictEqual(addr.address, '::1');
+ assert.strictEqual(addr.family, 'IPv6');
+ assert.ok(addr.port > 0);
+ bound.close();
+ }
+
+ {
+ const bound = new net.BoundHandle({ ipv6Only: true, port: 0 });
+ const addr = bound.address();
+ assert.strictEqual(addr.address, '::');
+ assert.strictEqual(addr.family, 'IPv6');
+ bound.close();
+ }
+
+ // ipv6Only: false (default) binds the IPv6 wildcard as dual-stack.
+ {
+ const bound = new net.BoundHandle({ host: '::', port: 0 });
+ assert.strictEqual(bound.address().family, 'IPv6');
+ bound.close();
+ }
+}