Skip to content
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