mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
net: early TCP binding via synchronous net.BoundSocket
Signed-off-by: Guy Bedford <guybedford@gmail.com> PR-URL: https://github.com/nodejs/node/pull/63951 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
1 parent
c27921ba06
commit
e3cdb14678
5 files changed
+513
-5
No files matched your search
@@ -2986,6 +2986,14 @@ disconnected socket.
|
||||
|
||||
A call was made and the UDP subsystem was not running.
|
||||
|
||||
<a id="ERR_SOCKET_HANDLE_ADOPTED"></a>
|
||||
|
||||
### `ERR_SOCKET_HANDLE_ADOPTED`
|
||||
|
||||
An operation was attempted on a [`BoundSocket`][] that had already been adopted
|
||||
by a [`net.Server`][] or [`net.Socket`][]. Once a bound socket is adopted, its
|
||||
`address()` and `close()` methods can no longer be used.
|
||||
|
||||
<a id="ERR_SOURCE_MAP_CORRUPT"></a>
|
||||
|
||||
### `ERR_SOURCE_MAP_CORRUPT`
|
||||
@@ -4566,6 +4574,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
|
||||
[`BoundSocket`]: net.md#class-netboundsocket
|
||||
[`Class: assert.AssertionError`]: assert.md#class-assertassertionerror
|
||||
[`ERR_INCOMPATIBLE_OPTION_PAIR`]: #err_incompatible_option_pair
|
||||
[`ERR_INVALID_ARG_TYPE`]: #err_invalid_arg_type
|
||||
@@ -4609,7 +4618,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
|
||||
|
||||
+112
-3
@@ -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 [`BoundSocket`][], or an object with an `fd`
|
||||
member that is a valid file descriptor.
|
||||
|
||||
When `handle` is a [`BoundSocket`][], the server adopts the already-bound
|
||||
socket and starts listening on it. Adoption consumes the bound socket (see
|
||||
[ownership transfer][`BoundSocket`]).
|
||||
|
||||
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.BoundSocket} A pre-bound [`BoundSocket`][]. The server adopts
|
||||
the already-bound socket and listens on it, ignoring `host`, `port`, and
|
||||
`path`. Adoption consumes the bound socket (see
|
||||
[ownership transfer][`BoundSocket`]).
|
||||
* `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.BoundSocket} If specified, wrap around the bound socket from a
|
||||
[`BoundSocket`][]. A subsequent
|
||||
[`socket.connect()`][`socket.connect()`] uses the bound socket as the
|
||||
connection's source binding (honoring the bound local address and port).
|
||||
Adoption consumes the bound socket (see
|
||||
[ownership transfer][`BoundSocket`]).
|
||||
* `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.BoundSocket`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
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.BoundSocket();
|
||||
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.BoundSocket([options])`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `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`.
|
||||
|
||||
### `boundSocket.address()`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* 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.
|
||||
|
||||
### `boundSocket.fd()`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* 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
|
||||
`BoundSocket`, 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`][].
|
||||
|
||||
### `boundSocket.close()`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
Releases the bound socket. Only needed when the handle is never adopted.
|
||||
|
||||
### `boundSocket[Symbol.dispose]()`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
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.BoundSocket} A pre-bound [`BoundSocket`][] used as the
|
||||
connection's source binding, honoring its local address and port. Adoption
|
||||
consumes the bound socket (see [ownership transfer][`BoundSocket`]).
|
||||
* `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
|
||||
[`BoundSocket`]: #class-netboundsocket
|
||||
[`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
|
||||
|
||||
@@ -1790,6 +1790,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 socket 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',
|
||||
|
||||
+156
-2
@@ -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,
|
||||
validateStringWithoutNullBytes,
|
||||
@@ -363,6 +365,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 BoundSocket.
|
||||
const kBoundSource = Symbol('kBoundSource');
|
||||
|
||||
// Internal: adopt the underlying handle, transferring ownership to a
|
||||
// Server/Socket.
|
||||
const kBoundSocketConsume = Symbol('kBoundSocketConsume');
|
||||
|
||||
// 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 BoundSocket {
|
||||
#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.BoundSocket 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 BoundSocket 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.BoundSocket(...)`: 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;
|
||||
}
|
||||
}
|
||||
|
||||
[kBoundSocketConsume]() {
|
||||
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);
|
||||
@@ -421,8 +539,17 @@ function Socket(options) {
|
||||
options.decodeStrings = false;
|
||||
stream.Duplex.call(this, options);
|
||||
|
||||
// An adopted BoundSocket 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 BoundSocket) {
|
||||
this._handle = options.handle[kBoundSocketConsume]();
|
||||
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;
|
||||
@@ -493,7 +620,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;
|
||||
@@ -1392,6 +1519,17 @@ function lookupAndConnect(self, options) {
|
||||
|
||||
validateStringWithoutNullBytes(host, 'options.host');
|
||||
|
||||
// An adopted BoundSocket 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 socket');
|
||||
}
|
||||
autoSelectFamily = false;
|
||||
}
|
||||
|
||||
if (localAddress && !isIP(localAddress)) {
|
||||
throw new ERR_INVALID_IP_ADDRESS(localAddress);
|
||||
}
|
||||
@@ -2145,6 +2283,21 @@ Server.prototype.listen = function(...args) {
|
||||
toNumber(args.length > 1 && args[1]) ||
|
||||
toNumber(args.length > 2 && args[2]); // (port, host, backlog)
|
||||
|
||||
// (boundSocket[, ...]) or ({ handle: boundSocket }[, ...]): adopt the bound
|
||||
// handle, transferring ownership so the BoundSocket can no longer close it.
|
||||
let boundSocket = null;
|
||||
if (options instanceof BoundSocket) {
|
||||
boundSocket = options;
|
||||
} else if (options.handle instanceof BoundSocket) {
|
||||
boundSocket = options.handle;
|
||||
}
|
||||
if (boundSocket !== null) {
|
||||
this._handle = boundSocket[kBoundSocketConsume]();
|
||||
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
|
||||
@@ -2574,6 +2727,7 @@ module.exports = {
|
||||
SocketAddress ??= require('internal/socketaddress').SocketAddress;
|
||||
return SocketAddress;
|
||||
},
|
||||
BoundSocket,
|
||||
connect,
|
||||
createConnection: connect,
|
||||
createServer,
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const net = require('net');
|
||||
|
||||
// Constructing a BoundSocket binds synchronously and address() reports the
|
||||
// resolved address, including the OS-assigned ephemeral port when port is 0.
|
||||
{
|
||||
const bound = new net.BoundSocket({ 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.BoundSocket({ 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.BoundSocket();
|
||||
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.BoundSocket({ 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.BoundSocket({ 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.BoundSocket({ host: 1234 }),
|
||||
{ code: 'ERR_INVALID_ARG_TYPE' });
|
||||
assert.throws(() => new net.BoundSocket(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.BoundSocket({ 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.BoundSocket({ 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.BoundSocket({ host: '127.0.0.1', port: 0 });
|
||||
const { port } = bound.address();
|
||||
bound.close();
|
||||
const again = new net.BoundSocket({ host: '127.0.0.1', port });
|
||||
assert.strictEqual(again.address().port, port);
|
||||
again.close();
|
||||
}
|
||||
|
||||
// Server adoption: server.listen(boundSocket), then a client round-trips.
|
||||
{
|
||||
const bound = new net.BoundSocket({ 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 socket 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: boundSocket }).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.BoundSocket({ host: '127.0.0.1', port: 0 });
|
||||
const localPort = bound.address().port;
|
||||
|
||||
const client = new net.Socket({ handle: bound });
|
||||
|
||||
// Adoption consumed the bound socket.
|
||||
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 socket: the
|
||||
// handle already owns the local endpoint.
|
||||
{
|
||||
const bound = new net.BoundSocket({ 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.BoundSocket({ 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.BoundSocket({ 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.BoundSocket({ 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.BoundSocket({ 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.BoundSocket({ host: '::', port: 0 });
|
||||
assert.strictEqual(bound.address().family, 'IPv6');
|
||||
bound.close();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user