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..a5e939e8819bae 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.
@@ -769,6 +773,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 +1637,103 @@ 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`
+
+
+
+A role-neutral wrapper over a synchronously bound TCP socket, mirroring POSIX
+`bind(2)`, which is role-agnostic until `listen()` or `connect()`. It is adopted
+by exactly one server (via [`server.listen()`][]) or socket (via the `handle`
+option of [`new net.Socket()`][`new net.Socket(options)`]). 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({ host: '127.0.0.1', port: 0 });
+const { port } = bound.address();
+
+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`.
+
+Synchronously binds a TCP socket. Because `bind(2)` is a local, non-blocking
+system call, the bind happens inline and errors (such as `EADDRINUSE`,
+`EADDRNOTAVAIL`, `EACCES`, or `EINVAL`) are thrown synchronously. The
+kernel-assigned address, including the ephemeral port chosen when `port` is `0`,
+is available immediately via
+[`boundHandle.address()`][`net.BoundHandle.address()`].
+
+This is the synchronous, role-neutral counterpart to the bind performed
+internally by [`server.listen()`][] and [`socket.connect()`][], analogous to
+[`dgram` `socket.bindSync()`][].
+
+### `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
@@ -2097,10 +2204,14 @@ 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
+[`dgram` `socket.bindSync()`]: dgram.md#socketbindsyncoptions
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
[`dns.lookup()` hints]: dns.md#supported-getaddrinfo-flags
+[`net.BoundHandle.address()`]: #boundhandleaddress
[`net.Server`]: #class-netserver
[`net.Socket`]: #class-netsocket
[`net.connect()`]: #netconnect
@@ -2116,6 +2227,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..ecd68f3935d303 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,127 @@ 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. Used by server.listen() and the Socket constructor.
+const kBoundHandleConsume = Symbol('kBoundHandleConsume');
+
+// A thin, role-neutral wrapper over a synchronously bound libuv TCP handle,
+// mirroring POSIX bind(2): the socket is bound to a local address but has not
+// chosen a role. It neither listens nor connects until it is adopted by exactly
+// one Server (via server.listen()) or Socket (via new net.Socket({ handle })).
+// Adoption transfers ownership of the underlying handle; an un-adopted
+// BoundHandle must be closed by the caller.
+//
+// bind(2) is a local, non-blocking system call, so binding happens inline in
+// the constructor and errors throw synchronously. The host must be a numeric IP
+// literal: no DNS resolution 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(): it stashes the error and
+ // returns 0, surfacing it only on the next getsockname/listen/connect.
+ // Resolve the address now so bind conflicts throw synchronously here; the
+ // result is also cached (a BoundHandle is immutable, so it cannot change).
+ 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 for the bound socket, or -1 on platforms
+ // that do not expose one for sockets (e.g. Windows). Ownership stays with the
+ // BoundHandle, so the caller must not close the descriptor; once the handle is
+ // adopted, the descriptor belongs to the adopting Server/Socket instead.
+ 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 +543,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 +624,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 +1523,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 +2287,22 @@ Server.prototype.listen = function(...args) {
toNumber(args.length > 1 && args[1]) ||
toNumber(args.length > 2 && args[2]); // (port, host, backlog)
+ // (boundHandle[, ...]) or ({ handle: boundHandle }[, ...]) from
+ // net.bindSync(): 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 +2732,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..15579fb890aa80
--- /dev/null
+++ b/test/parallel/test-net-boundhandle.js
@@ -0,0 +1,222 @@
+'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();
+}
+
+// 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();
+ }
+}