Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v18.x] Backport 48969 to v18.x staging #49016

Closed
10 changes: 10 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@ added: v6.0.0
Enable FIPS-compliant crypto at startup. (Requires Node.js to be built
against FIPS-compatible OpenSSL.)

### `--enable-network-family-autoselection`

<!-- YAML
added: REPLACEME
-->

Enables the family autoselection algorithm unless connection options explicitly
disables it.

### `--enable-source-maps`

<!-- YAML
Expand Down Expand Up @@ -1861,6 +1870,7 @@ Node.js options that are allowed are:
* `--disable-proto`
* `--dns-result-order`
* `--enable-fips`
* `--enable-network-family-autoselection`
* `--enable-source-maps`
* `--experimental-abortcontroller`
* `--experimental-global-customevent`
Expand Down
70 changes: 67 additions & 3 deletions doc/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,20 @@ Returns the bound `address`, the address `family` name and `port` of the
socket as reported by the operating system:
`{ port: 12346, family: 'IPv4', address: '127.0.0.1' }`

### `socket.autoSelectFamilyAttemptedAddresses`

<!-- YAML
added: REPLACEME
-->

* {string\[]}

This property is only present if the family autoselection algorithm is enabled in
[`socket.connect(options)`][] and it is an array of the addresses that have been attempted.

Each address is a string in the form of `$IP:$PORT`. If the connection was successful,
then the last address is the one that the socket is currently connected to.

### `socket.bufferSize`

<!-- YAML
Expand Down Expand Up @@ -854,6 +868,11 @@ behavior.
<!-- YAML
added: v0.1.90
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/45777
description: The default value for autoSelectFamily option can be changed
at runtime using `setDefaultAutoSelectFamily` or via the
command line option `--enable-network-family-autoselection`.
- version: v18.13.0
pr-url: https://github.com/nodejs/node/pull/44731
description: Added the `autoSelectFamily` option.
Expand Down Expand Up @@ -905,16 +924,18 @@ For TCP connections, available `options` are:
that loosely implements section 5 of [RFC 8305][].
The `all` option passed to lookup is set to `true` and the sockets attempts to connect to all
obtained IPv6 and IPv4 addresses, in sequence, until a connection is established.
The first returned AAAA address is tried first, then the first returned A address and so on.
The first returned AAAA address is tried first, then the first returned A address,
then the second returned AAAA address and so on.
Each connection attempt is given the amount of time specified by the `autoSelectFamilyAttemptTimeout`
option before timing out and trying the next address.
Ignored if the `family` option is not `0` or if `localAddress` is set.
Connection errors are not emitted if at least one connection succeeds.
**Default:** `false`.
**Default:** initially `false`, but it can be changed at runtime using [`net.setDefaultAutoSelectFamily(value)`][]
or via the command line option `--enable-network-family-autoselection`.
* `autoSelectFamilyAttemptTimeout` {number}: The amount of time in milliseconds to wait
for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option.
If set to a positive integer less than `10`, then the value `10` will be used instead.
**Default:** `250`.
**Default:** initially `250`, but it can be changed at runtime using [`net.setDefaultAutoSelectFamilyAttemptTimeout(value)`][]

For [IPC][] connections, available `options` are:

Expand Down Expand Up @@ -1599,6 +1620,47 @@ Use `nc` to connect to a Unix domain socket server:
$ nc -U /tmp/echo.sock
```

## `net.getDefaultAutoSelectFamily()`

<!-- YAML
added: v19.4.0
-->

Gets the current default value of the `autoSelectFamily` option of [`socket.connect(options)`][].

* Returns: {boolean} The current default value of the `autoSelectFamily` option.

## `net.setDefaultAutoSelectFamily(value)`

<!-- YAML
added: v19.4.0
-->

Sets the default value of the `autoSelectFamily` option of [`socket.connect(options)`][].

* `value` {boolean} The new default value. The initial default value is `false`.

## `net.getDefaultAutoSelectFamilyAttemptTimeout()`

<!-- YAML
added: REPLACEME
-->

Gets the current default value of the `autoSelectFamilyAttemptTimeout` option of [`socket.connect(options)`][].

* Returns: {number} The current default value of the `autoSelectFamilyAttemptTimeout` option.

## `net.setDefaultAutoSelectFamilyAttemptTimeout(value)`

<!-- YAML
added: REPLACEME
-->

Sets the default value of the `autoSelectFamilyAttemptTimeout` option of [`socket.connect(options)`][].

* `value` {number} The new default value, which must be a positive number. If the number is less than `10`,
the value `10` is used insted The initial default value is `250`.

## `net.isIP(input)`

<!-- YAML
Expand Down Expand Up @@ -1683,6 +1745,8 @@ net.isIPv6('fhqwhgads'); // returns false
[`net.createConnection(path)`]: #netcreateconnectionpath-connectlistener
[`net.createConnection(port, host)`]: #netcreateconnectionport-host-connectlistener
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
[`net.setDefaultAutoSelectFamily(value)`]: #netsetdefaultautoselectfamilyvalue
[`net.setDefaultAutoSelectFamilyAttemptTimeout(value)`]: #netsetdefaultautoselectfamilyattempttimeoutvalue
[`new net.Socket(options)`]: #new-netsocketoptions
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
[`server.close()`]: #serverclosecallback
Expand Down
5 changes: 3 additions & 2 deletions lib/_http_outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,9 @@ function _storeHeader(firstLine, headers) {

// keep-alive logic
if (this._removedConnection) {
this._last = true;
this.shouldKeepAlive = false;
// shouldKeepAlive is generally true for HTTP/1.1. In that common case,
// even if the connection header isn't sent, we still persist by default.
this._last = !this.shouldKeepAlive;
} else if (!state.connection) {
const shouldSendKeepAlive = this.shouldKeepAlive &&
(state.contLen || this.useChunkedEncodingByDefault || this.agent);
Expand Down
136 changes: 93 additions & 43 deletions lib/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const EE = require('events');
const net = require('net');
const tls = require('tls');
const common = require('_tls_common');
const { kWrapConnectedHandle } = require('internal/net');
const { kReinitializeHandle } = require('internal/net');
const JSStreamSocket = require('internal/js_stream_socket');
const { Buffer } = require('buffer');
let debug = require('internal/util/debuglog').debuglog('tls', (fn) => {
Expand Down Expand Up @@ -502,25 +502,36 @@ function TLSSocket(socket, opts) {
this[kPendingSession] = null;

let wrap;
if ((socket instanceof net.Socket && socket._handle) || !socket) {
// 1. connected socket
// 2. no socket, one will be created with net.Socket().connect
wrap = socket;
let handle;
let wrapHasActiveWriteFromPrevOwner;

if (socket) {
if (socket instanceof net.Socket && socket._handle) {
// 1. connected socket
wrap = socket;
} else {
// 2. socket has no handle so it is js not c++
// 3. unconnected sockets are wrapped
// TLS expects to interact from C++ with a net.Socket that has a C++ stream
// handle, but a JS stream doesn't have one. Wrap it up to make it look like
// a socket.
wrap = new JSStreamSocket(socket);
}

handle = wrap._handle;
wrapHasActiveWriteFromPrevOwner = wrap.writableLength > 0;
} else {
// 3. socket has no handle so it is js not c++
// 4. unconnected sockets are wrapped
// TLS expects to interact from C++ with a net.Socket that has a C++ stream
// handle, but a JS stream doesn't have one. Wrap it up to make it look like
// a socket.
wrap = new JSStreamSocket(socket);
// 4. no socket, one will be created with net.Socket().connect
wrap = null;
wrapHasActiveWriteFromPrevOwner = false;
}

// Just a documented property to make secure sockets
// distinguishable from regular ones.
this.encrypted = true;

ReflectApply(net.Socket, this, [{
handle: this._wrapHandle(wrap),
handle: this._wrapHandle(wrap, handle, wrapHasActiveWriteFromPrevOwner),
allowHalfOpen: socket ? socket.allowHalfOpen : tlsOptions.allowHalfOpen,
pauseOnCreate: tlsOptions.pauseOnConnect,
manualStart: true,
Expand All @@ -539,6 +550,21 @@ function TLSSocket(socket, opts) {
if (enableTrace && this._handle)
this._handle.enableTrace();

if (wrapHasActiveWriteFromPrevOwner) {
// `wrap` is a streams.Writable in JS. This empty write will be queued
// and hence finish after all existing writes, which is the timing
// we want to start to send any tls data to `wrap`.
wrap.write('', (err) => {
if (err) {
debug('error got before writing any tls data to the underlying stream');
this.destroy(err);
return;
}

this._handle.writesIssuedByPrevListenerDone();
});
}

// Read on next tick so the caller has a chance to setup listeners
process.nextTick(initRead, this, socket);
}
Expand Down Expand Up @@ -599,11 +625,14 @@ TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() {
this[kDisableRenegotiation] = true;
};

TLSSocket.prototype._wrapHandle = function(wrap, handle) {
if (!handle && wrap) {
handle = wrap._handle;
}

/**
*
* @param {null|net.Socket} wrap
* @param {null|object} handle
* @param {boolean} wrapHasActiveWriteFromPrevOwner
* @returns {object}
*/
TLSSocket.prototype._wrapHandle = function(wrap, handle, wrapHasActiveWriteFromPrevOwner) {
const options = this._tlsOptions;
if (!handle) {
handle = options.pipe ?
Expand All @@ -620,7 +649,10 @@ TLSSocket.prototype._wrapHandle = function(wrap, handle) {
if (!(context.context instanceof NativeSecureContext)) {
throw new ERR_TLS_INVALID_CONTEXT('context');
}
const res = tls_wrap.wrap(handle, context.context, !!options.isServer);

const res = tls_wrap.wrap(handle, context.context,
!!options.isServer,
wrapHasActiveWriteFromPrevOwner);
res._parent = handle; // C++ "wrap" object: TCPWrap, JSStream, ...
res._parentWrap = wrap; // JS object: net.Socket, JSStreamSocket, ...
res._secureContext = context;
Expand All @@ -633,14 +665,27 @@ TLSSocket.prototype._wrapHandle = function(wrap, handle) {
return res;
};

TLSSocket.prototype[kWrapConnectedHandle] = function(handle) {
this._handle = this._wrapHandle(null, handle);
TLSSocket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
const originalServername = this._handle.getServername();
const originalSession = this._handle.getSession();

this.handle = this._wrapHandle(null, handle, false);
this.ssl = this._handle;

net.Socket.prototype[kReinitializeHandle].call(this, this.handle);
this._init();

if (this._tlsOptions.enableTrace) {
this._handle.enableTrace();
}

if (originalSession) {
this.setSession(originalSession);
}

if (originalServername) {
this.setServername(originalServername);
}
};

// This eliminates a cyclic reference to TLSWrap
Expand Down Expand Up @@ -679,6 +724,30 @@ TLSSocket.prototype._destroySSL = function _destroySSL() {
this[kIsVerified] = false;
};

function keylogNewListener(event) {
if (event !== 'keylog')
return;

// Guard against enableKeylogCallback after destroy
if (!this._handle) return;
this._handle.enableKeylogCallback();

// Remove this listener since it's no longer needed.
this.removeListener('newListener', keylogNewListener);
}

function newListener(event) {
if (event !== 'session')
return;

// Guard against enableSessionCallbacks after destroy
if (!this._handle) return;
this._handle.enableSessionCallbacks();

// Remove this listener since it's no longer needed.
this.removeListener('newListener', newListener);
}

// Constructor guts, arbitrarily factored out.
let warnOnTlsKeylog = true;
let warnOnTlsKeylogError = true;
Expand All @@ -704,18 +773,9 @@ TLSSocket.prototype._init = function(socket, wrap) {

// Only call .onkeylog if there is a keylog listener.
ssl.onkeylog = onkeylog;
this.on('newListener', keylogNewListener);

function keylogNewListener(event) {
if (event !== 'keylog')
return;

// Guard against enableKeylogCallback after destroy
if (!this._handle) return;
this._handle.enableKeylogCallback();

// Remove this listener since it's no longer needed.
this.removeListener('newListener', keylogNewListener);
if (this.listenerCount('newListener', keylogNewListener) === 0) {
this.on('newListener', keylogNewListener);
}

if (options.isServer) {
Expand Down Expand Up @@ -750,18 +810,8 @@ TLSSocket.prototype._init = function(socket, wrap) {
ssl.onnewsession = onnewsessionclient;

// Only call .onnewsession if there is a session listener.
this.on('newListener', newListener);

function newListener(event) {
if (event !== 'session')
return;

// Guard against enableSessionCallbacks after destroy
if (!this._handle) return;
this._handle.enableSessionCallbacks();

// Remove this listener since it's no longer needed.
this.removeListener('newListener', newListener);
if (this.listenerCount('newListener', newListener) === 0) {
this.on('newListener', newListener);
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/internal/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function makeSyncWrite(fd) {
}

module.exports = {
kWrapConnectedHandle: Symbol('wrapConnectedHandle'),
kReinitializeHandle: Symbol('reinitializeHandle'),
isIP,
isIPv4,
isIPv6,
Expand Down
Loading