Skip to content

Commit

Permalink
quic: use promisified dns lookup
Browse files Browse the repository at this point in the history
PR-URL: #34283
Reviewed-By: Anna Henningsen <anna@addaleax.net>
  • Loading branch information
jasnell committed Jul 16, 2020
1 parent 346aeaf commit fe4e7e4
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 64 deletions.
41 changes: 40 additions & 1 deletion doc/api/quic.md
Expand Up @@ -269,7 +269,8 @@ added: REPLACEME
* `type` {string} Can be one of `'udp4'`, `'upd6'`, or `'udp6-only'` to
use IPv4, IPv6, or IPv6 with dual-stack mode disabled.
**Default**: `'udp4'`.
* `lookup` {Function} A custom DNS lookup function. Default `dns.lookup()`.
* `lookup` {Function} A [custom DNS lookup function][].
**Default**: undefined.
* `maxConnections` {number} The maximum number of total active inbound
connections.
* `maxConnectionsPerHost` {number} The maximum number of inbound connections
Expand Down Expand Up @@ -1505,6 +1506,8 @@ added: REPLACEME
* `type` {string} Can be one of `'udp4'`, `'upd6'`, or `'udp6-only'` to
use IPv4, IPv6, or IPv6 with dual-stack mode disabled.
**Default**: `'udp4'`.
* `lookup` {Function} A [custom DNS lookup function][].
**Default**: undefined.
* Returns: {QuicEndpoint}

Creates and adds a new `QuicEndpoint` to the `QuicSocket` instance. An
Expand Down Expand Up @@ -1661,6 +1664,8 @@ added: REPLACEME
passphrase: <string>]}`. The object form can only occur in an array.
`object.passphrase` is optional. Encrypted keys will be decrypted with
`object.passphrase` if provided, or `options.passphrase` if it is not.
* `lookup` {Function} A [custom DNS lookup function][].
**Default**: undefined.
* `activeConnectionIdLimit` {number} Must be a value between `2` and `8`
(inclusive). Default: `2`.
* `congestionAlgorithm` {string} Must be either `'reno'` or `'cubic'`.
Expand Down Expand Up @@ -1836,6 +1841,8 @@ added: REPLACEME
passphrase: <string>]}`. The object form can only occur in an array.
`object.passphrase` is optional. Encrypted keys will be decrypted with
`object.passphrase` if provided, or `options.passphrase` if it is not.
* `lookup` {Function} A [custom DNS lookup function][].
**Default**: undefined.
* `activeConnectionIdLimit` {number}
* `congestionAlgorithm` {string} Must be either `'reno'` or `'cubic'`.
**Default**: `'reno'`.
Expand Down Expand Up @@ -2411,15 +2418,47 @@ added: REPLACEME

Set to `true` if the `QuicStream` is unidirectional.

## Additional Notes

### Custom DNS Lookup Functions

By default, the QUIC implementation uses the `dns` module's
[promisified version of `lookup()`][] to resolve domains names
into IP addresses. For most typical use cases, this will be
sufficient. However, it is possible to pass a custom `lookup`
function as an option in several places throughout the QUIC API:

* `net.createQuicSocket()`
* `quicsocket.addEndpoint()`
* `quicsocket.connect()`
* `quicsocket.listen()`

The custom `lookup` function must teturn a `Promise` that is
resolved once the lookup is complete. It will be invoked with
two arguments:

* `address` {string|undefined} The host name to resolve, or
`undefined` if no host name was provided.
* `family` {number} One of `4` or `6`, identifying either
IPv4 or IPv6.

```js
async function myCustomLookup(address, type) {
return resolveTheAddressSomehow(address, type);
}
```

[`crypto.getCurves()`]: crypto.html#crypto_crypto_getcurves
[`stream.Readable`]: #stream_class_stream_readable
[`tls.DEFAULT_ECDH_CURVE`]: #tls_tls_default_ecdh_curve
[`tls.getCiphers()`]: tls.html#tls_tls_getciphers
[ALPN]: https://tools.ietf.org/html/rfc7301
[RFC 4007]: https://tools.ietf.org/html/rfc4007
[Certificate Object]: https://nodejs.org/dist/latest-v12.x/docs/api/tls.html#tls_certificate_object
[custom DNS lookup function]: #custom_dns_lookups
[modifying the default cipher suite]: tls.html#tls_modifying_the_default_tls_cipher_suite
[OpenSSL Options]: crypto.html#crypto_openssl_options
[Perfect Forward Secrecy]: #tls_perfect_forward_secrecy
[promisified version of `lookup()`]: dns.html#dns_dnspromises_lookup_hostname_options
['qlog']: #quic_quicsession_qlog
[qlog standard]: https://tools.ietf.org/id/draft-marx-qlog-event-definitions-quic-h3-00.html
63 changes: 16 additions & 47 deletions lib/internal/quic/core.js
Expand Up @@ -29,8 +29,6 @@ const {
customInspect,
getAllowUnauthorized,
getSocketType,
lookup4,
lookup6,
setTransportParams,
toggleListeners,
validateNumber,
Expand Down Expand Up @@ -202,17 +200,12 @@ const {

const emit = EventEmitter.prototype.emit;

// TODO(@jasnell): Temporary while converting to Promises-based API
const { lookup } = require('dns').promises;

const kAfterPreferredAddressLookup = Symbol('kAfterPreferredAddressLookup');
const kAddSession = Symbol('kAddSession');
const kAddStream = Symbol('kAddStream');
const kBind = Symbol('kBind');
const kClose = Symbol('kClose');
const kCert = Symbol('kCert');
const kClientHello = Symbol('kClientHello');
const kCompleteListen = Symbol('kCompleteListen');
const kDestroy = Symbol('kDestroy');
const kEndpointBound = Symbol('kEndpointBound');
const kEndpointClose = Symbol('kEndpointClose');
Expand Down Expand Up @@ -579,10 +572,6 @@ function addressOrLocalhost(address, type) {
return address || (type === AF_INET6 ? '::' : '0.0.0.0');
}

function lookupOrDefault(lookup, type) {
return lookup || (type === AF_INET6 ? lookup6 : lookup4);
}

function deferredClosePromise(state) {
return state.closePromise = new Promise((resolve, reject) => {
state.closePromiseResolve = resolve;
Expand All @@ -594,7 +583,7 @@ function deferredClosePromise(state) {
});
}

async function resolvePreferredAddress(state, preferredAddress) {
async function resolvePreferredAddress(lookup, preferredAddress) {
if (preferredAddress === undefined)
return {};
const {
Expand All @@ -603,7 +592,9 @@ async function resolvePreferredAddress(state, preferredAddress) {
type = 'udp4'
} = { ...preferredAddress };
const [typeVal] = getSocketType(type);
const { address: ip } = await lookup(address, typeVal === AF_INET6 ? 6 : 4);
const {
address: ip
} = await lookup(address, typeVal === AF_INET6 ? 6 : 4);
return { ip, port, type };
}

Expand Down Expand Up @@ -641,7 +632,7 @@ class QuicEndpoint {
const state = this[kInternalState];
state.socket = socket;
state.address = addressOrLocalhost(address, type);
state.lookup = lookupOrDefault(lookup, type);
state.lookup = lookup;
state.ipv6Only = ipv6Only;
state.port = port;
state.reuseAddr = reuseAddr;
Expand Down Expand Up @@ -711,11 +702,9 @@ class QuicEndpoint {

state.state = kSocketPending;

// TODO(@jasnell): Use passed in lookup function once everything
// has been converted to Promises-based API
const {
address: ip
} = await lookup(state.address, state.type === AF_INET6 ? 6 : 4);
} = await state.lookup(state.address, state.type === AF_INET6 ? 6 : 4);

// It's possible for the QuicEndpoint to have been destroyed while
// we were waiting for the DNS lookup to complete. If so, reject
Expand Down Expand Up @@ -977,9 +966,6 @@ class QuicSocket extends EventEmitter {
// Default configuration for QuicServerSessions
server,

// UDP type
type,

// True if address verification should be used.
validateAddress,

Expand All @@ -1001,7 +987,7 @@ class QuicSocket extends EventEmitter {

state.client = client;
state.server = server;
state.lookup = lookupOrDefault(lookup, type);
state.lookup = lookup;

let socketOptions = 0;
if (validateAddress)
Expand All @@ -1020,14 +1006,7 @@ class QuicSocket extends EventEmitter {
statelessResetSecret,
disableStatelessReset));

this.addEndpoint({
lookup: state.lookup,
// Keep the lookup and ...endpoint in this order
// to allow the passed in endpoint options to
// override the lookup specifically for that endpoint
...endpoint,
preferred: true
});
this.addEndpoint({ ...endpoint, preferred: true });
}

[kRejections](err, eventname, ...args) {
Expand Down Expand Up @@ -1205,28 +1184,16 @@ class QuicSocket extends EventEmitter {
if (state.state !== kSocketUnbound)
throw new ERR_INVALID_STATE('QuicSocket is already being bound');

options = {
lookup: state.lookup,
...options
};

const endpoint = new QuicEndpoint(this, options);
state.endpoints.add(endpoint);
return endpoint;
}

// Used only from within the [kContinueListen] function. When a preferred
// address has been provided, the hostname given must be resolved into an
// IP address, which must be passed on to #completeListen or the QuicSocket
// needs to be destroyed.
static [kAfterPreferredAddressLookup](
transportParams,
port,
type,
err,
address) {
if (err) {
this.destroy(err);
return;
}
this[kCompleteListen](transportParams, { address, port, type });
}

listen(options) {
const state = this[kInternalState];
if (state.listenPromise !== undefined)
Expand Down Expand Up @@ -1254,6 +1221,7 @@ class QuicSocket extends EventEmitter {
// The ALPN protocol identifier is strictly required.
const {
alpn,
lookup = state.lookup,
defaultEncoding,
highWaterMark,
transportParams,
Expand Down Expand Up @@ -1285,7 +1253,7 @@ class QuicSocket extends EventEmitter {
ip,
port,
type
} = await resolvePreferredAddress(state, transportParams.preferredAddress);
} = await resolvePreferredAddress(lookup, transportParams.preferredAddress);

// It's possible that the QuicSocket was destroyed or closed while
// the preferred address resolution was pending. Check for that and handle
Expand Down Expand Up @@ -1348,6 +1316,7 @@ class QuicSocket extends EventEmitter {
const {
type,
address,
lookup = state.lookup
} = validateQuicSocketConnectOptions(options);

await this[kMaybeBind]();
Expand Down
30 changes: 14 additions & 16 deletions lib/internal/quic/util.js
Expand Up @@ -123,7 +123,7 @@ let dns;

function lazyDNS() {
if (!dns)
dns = require('dns');
dns = require('dns').promises;
return dns;
}

Expand All @@ -147,14 +147,9 @@ function getSocketType(type = 'udp4') {
throw new ERR_INVALID_ARG_VALUE('options.type', type);
}

function lookup4(address, callback) {
function defaultLookup(address, type) {
const { lookup } = lazyDNS();
lookup(address || '127.0.0.1', 4, callback);
}

function lookup6(address, callback) {
const { lookup } = lazyDNS();
lookup(address || '::1', 6, callback);
return lookup(address || (type === 6 ? '::1' : '127.0.0.1'), type);
}

function validateCloseCode(code) {
Expand All @@ -174,7 +169,7 @@ function validateCloseCode(code) {

function validateLookup(lookup) {
if (lookup && typeof lookup !== 'function')
throw new ERR_INVALID_ARG_TYPE('options.lookup', 'Function', lookup);
throw new ERR_INVALID_ARG_TYPE('options.lookup', 'function', lookup);
}

function validatePreferredAddress(address) {
Expand Down Expand Up @@ -360,8 +355,6 @@ function validateTransportParams(params) {
}

function validateQuicClientSessionOptions(options = {}) {
if (options !== null && typeof options !== 'object')
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
const {
autoStart = true,
address = 'localhost',
Expand Down Expand Up @@ -525,7 +518,7 @@ function validateQuicSocketOptions(options = {}) {
client = {},
disableStatelessReset = false,
endpoint = { port: 0, type: 'udp4' },
lookup,
lookup = defaultLookup,
maxConnections = DEFAULT_MAX_CONNECTIONS,
maxConnectionsPerHost = DEFAULT_MAX_CONNECTIONS_PER_HOST,
maxStatelessResetsPerHost = DEFAULT_MAX_STATELESS_RESETS_PER_HOST,
Expand Down Expand Up @@ -607,13 +600,15 @@ function validateQuicSocketListenOptions(options = {}) {
highWaterMark,
requestCert,
rejectUnauthorized,
lookup,
} = options;
validateString(alpn, 'options.alpn');
if (rejectUnauthorized !== undefined)
validateBoolean(rejectUnauthorized, 'options.rejectUnauthorized');
if (requestCert !== undefined)
validateBoolean(requestCert, 'options.requestCert');

if (lookup !== undefined)
validateLookup(lookup);
const transportParams =
validateTransportParams(
options,
Expand All @@ -622,6 +617,7 @@ function validateQuicSocketListenOptions(options = {}) {

return {
alpn,
lookup,
rejectUnauthorized,
requestCert,
transportParams,
Expand All @@ -634,11 +630,14 @@ function validateQuicSocketConnectOptions(options = {}) {
const {
type = 'udp4',
address,
lookup,
} = options;
if (address !== undefined)
validateString(address, 'options.address');
if (lookup !== undefined)
validateLookup(lookup);
const [typeVal] = getSocketType(type);
return { type: typeVal, address };
return { type: typeVal, address, lookup };
}

function validateCreateSecureContextOptions(options = {}) {
Expand Down Expand Up @@ -974,8 +973,7 @@ module.exports = {
customInspect,
getAllowUnauthorized,
getSocketType,
lookup4,
lookup6,
defaultLookup,
setTransportParams,
toggleListeners,
validateNumber,
Expand Down

0 comments on commit fe4e7e4

Please sign in to comment.