From fe4e7e4598fc4396a06cbf2eb2544ae9ce6f8d10 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 10 Jul 2020 14:57:48 -0700 Subject: [PATCH] quic: use promisified dns lookup PR-URL: https://github.com/nodejs/node/pull/34283 Reviewed-By: Anna Henningsen --- doc/api/quic.md | 41 ++++++++++++++++++++++++- lib/internal/quic/core.js | 63 ++++++++++----------------------------- lib/internal/quic/util.js | 30 +++++++++---------- 3 files changed, 70 insertions(+), 64 deletions(-) diff --git a/doc/api/quic.md b/doc/api/quic.md index 671a4c681be3c1..d362e55328b933 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -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 @@ -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 @@ -1661,6 +1664,8 @@ added: REPLACEME passphrase: ]}`. 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'`. @@ -1836,6 +1841,8 @@ added: REPLACEME passphrase: ]}`. 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'`. @@ -2411,6 +2418,36 @@ 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 @@ -2418,8 +2455,10 @@ Set to `true` if the `QuicStream` is unidirectional. [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 diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js index 633a27b1c034f0..43f979cbd837d7 100644 --- a/lib/internal/quic/core.js +++ b/lib/internal/quic/core.js @@ -29,8 +29,6 @@ const { customInspect, getAllowUnauthorized, getSocketType, - lookup4, - lookup6, setTransportParams, toggleListeners, validateNumber, @@ -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'); @@ -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; @@ -594,7 +583,7 @@ function deferredClosePromise(state) { }); } -async function resolvePreferredAddress(state, preferredAddress) { +async function resolvePreferredAddress(lookup, preferredAddress) { if (preferredAddress === undefined) return {}; const { @@ -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 }; } @@ -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; @@ -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 @@ -977,9 +966,6 @@ class QuicSocket extends EventEmitter { // Default configuration for QuicServerSessions server, - // UDP type - type, - // True if address verification should be used. validateAddress, @@ -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) @@ -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) { @@ -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) @@ -1254,6 +1221,7 @@ class QuicSocket extends EventEmitter { // The ALPN protocol identifier is strictly required. const { alpn, + lookup = state.lookup, defaultEncoding, highWaterMark, transportParams, @@ -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 @@ -1348,6 +1316,7 @@ class QuicSocket extends EventEmitter { const { type, address, + lookup = state.lookup } = validateQuicSocketConnectOptions(options); await this[kMaybeBind](); diff --git a/lib/internal/quic/util.js b/lib/internal/quic/util.js index 6e3087ea2938e1..0f61af140140cf 100644 --- a/lib/internal/quic/util.js +++ b/lib/internal/quic/util.js @@ -123,7 +123,7 @@ let dns; function lazyDNS() { if (!dns) - dns = require('dns'); + dns = require('dns').promises; return dns; } @@ -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) { @@ -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) { @@ -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', @@ -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, @@ -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, @@ -622,6 +617,7 @@ function validateQuicSocketListenOptions(options = {}) { return { alpn, + lookup, rejectUnauthorized, requestCert, transportParams, @@ -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 = {}) { @@ -974,8 +973,7 @@ module.exports = { customInspect, getAllowUnauthorized, getSocketType, - lookup4, - lookup6, + defaultLookup, setTransportParams, toggleListeners, validateNumber,