diff --git a/benchmark/dgram/send-to-ip.js b/benchmark/dgram/send-to-ip.js new file mode 100644 index 00000000000000..8704462cf8d8bd --- /dev/null +++ b/benchmark/dgram/send-to-ip.js @@ -0,0 +1,43 @@ +// Measure the send rate to a literal IP destination. The destination needs no +// name resolution, so this isolates the per-send overhead the default lookup +// pays before the packet reaches the socket. +'use strict'; + +const common = require('../common.js'); +const dgram = require('dgram'); +const PORT = common.PORT; + +// `n` is the number of send requests queued each round. Keep it high (>10) so +// the measurement reflects send overhead rather than event loop cycles. +const bench = common.createBenchmark(main, { + n: [100], + dur: [5], +}); + +function main({ dur, n }) { + const chunk = Buffer.allocUnsafe(1); + let sent = 0; + const socket = dgram.createSocket('udp4'); + + function onsend() { + if (sent++ % n === 0) { + setImmediate(() => { + for (let i = 0; i < n; i++) { + socket.send(chunk, PORT, '127.0.0.1', onsend); + } + }); + } + } + + socket.on('listening', () => { + bench.start(); + onsend(); + + setTimeout(() => { + bench.end(sent); + process.exit(0); + }, dur * 1000); + }); + + socket.bind(PORT); +} diff --git a/doc/api/dgram.md b/doc/api/dgram.md index 3143774fb2b183..2113cbc442269f 100644 --- a/doc/api/dgram.md +++ b/doc/api/dgram.md @@ -1039,6 +1039,8 @@ changes: * `recvBufferSize` {number} Sets the `SO_RCVBUF` socket value. * `sendBufferSize` {number} Sets the `SO_SNDBUF` socket value. * `lookup` {Function} Custom lookup function. **Default:** [`dns.lookup()`][]. + When the default is used, a literal IP address of the socket's family + resolves to itself without calling [`dns.lookup()`][]. * `signal` {AbortSignal} An AbortSignal that may be used to close a socket. * `receiveBlockList` {net.BlockList} `receiveBlockList` can be used for discarding inbound datagram to specific IP addresses, IP ranges, or IP subnets. This does not diff --git a/lib/internal/dgram.js b/lib/internal/dgram.js index bae5da1c1f0def..3e477a6a798f9c 100644 --- a/lib/internal/dgram.js +++ b/lib/internal/dgram.js @@ -8,6 +8,7 @@ const { const { codes: { ERR_SOCKET_BAD_TYPE, } } = require('internal/errors'); +const { isIP } = require('internal/net'); const { UDP } = internalBinding('udp_wrap'); const { guessHandleType } = require('internal/util'); const { @@ -28,13 +29,22 @@ function lookup6(lookup, address, callback) { return lookup(address || '::1', 6, callback); } +// A literal IP of the socket's family resolves to itself, so skip dns.lookup(). +// Defer with nextTick to keep the callback async (e.g. bind()'s 'listening'). +function defaultLookup(address, family, callback) { + if (isIP(address) === family) { + process.nextTick(callback, null, address, family); + return; + } + if (dns === undefined) { + dns = require('dns'); + } + return dns.lookup(address, family, callback); +} + function newHandle(type, lookup) { if (lookup === undefined) { - if (dns === undefined) { - dns = require('dns'); - } - - lookup = dns.lookup; + lookup = defaultLookup; } else { validateFunction(lookup, 'lookup'); } diff --git a/test/parallel/test-dgram-custom-lookup.js b/test/parallel/test-dgram-custom-lookup.js index 4f80451c526625..7a2bf92187bda8 100644 --- a/test/parallel/test-dgram-custom-lookup.js +++ b/test/parallel/test-dgram-custom-lookup.js @@ -4,10 +4,12 @@ const assert = require('assert'); const dgram = require('dgram'); const dns = require('dns'); +const originalLookup = dns.lookup; + { // Verify that the provided lookup function is called. const lookup = common.mustCall((host, family, callback) => { - dns.lookup(host, family, callback); + originalLookup(host, family, callback); }); const socket = dgram.createSocket({ type: 'udp4', lookup }); @@ -18,17 +20,17 @@ const dns = require('dns'); } { - // Verify that lookup defaults to dns.lookup(). - const originalLookup = dns.lookup; - + // Verify that the default lookup forwards host names to dns.lookup(). dns.lookup = common.mustCall((host, family, callback) => { dns.lookup = originalLookup; - originalLookup(host, family, callback); + assert.strictEqual(host, 'example.invalid'); + assert.strictEqual(family, 4); + callback(null, '127.0.0.1', 4); }); const socket = dgram.createSocket({ type: 'udp4' }); - socket.bind(common.mustCall(() => { + socket.bind(0, 'example.invalid', common.mustCall(() => { socket.close(); })); } diff --git a/test/parallel/test-dgram-default-lookup-ip.js b/test/parallel/test-dgram-default-lookup-ip.js new file mode 100644 index 00000000000000..826ac7582aac1b --- /dev/null +++ b/test/parallel/test-dgram-default-lookup-ip.js @@ -0,0 +1,80 @@ +'use strict'; + +// The default dgram lookup resolves a literal IP address of the socket's own +// family to itself, without calling dns.lookup(). Each case below stubs the +// process-global dns.lookup(), so they run sequentially to keep one case from +// observing another's stub. + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const dns = require('dns'); + +const originalLookup = dns.lookup; + +function ipv4SendSkipsLookup(next) { + dns.lookup = common.mustNotCall('dns.lookup() ran for an IPv4 literal'); + + const receiver = dgram.createSocket('udp4'); + const sender = dgram.createSocket('udp4'); + + receiver.on('message', common.mustCall((msg) => { + assert.strictEqual(msg.toString(), 'payload'); + dns.lookup = originalLookup; + receiver.close(); + sender.close(); + next(); + })); + + receiver.bind(0, '127.0.0.1', common.mustCall(() => { + sender.send('payload', receiver.address().port, '127.0.0.1', common.mustCall()); + })); +} + +function ipv6BindSkipsLookup(next) { + if (!common.hasIPv6) { + next(); + return; + } + + dns.lookup = common.mustNotCall('dns.lookup() ran for an IPv6 literal'); + + const socket = dgram.createSocket('udp6'); + + socket.bind(0, '::1', common.mustCall(() => { + dns.lookup = originalLookup; + socket.close(); + next(); + })); +} + +function mismatchedFamilyFallsThrough(next) { + // '::1' is not an IPv4 literal, so a udp4 socket still resolves it via + // dns.lookup() rather than short-circuiting. + dns.lookup = common.mustCall((host, family, callback) => { + dns.lookup = originalLookup; + assert.strictEqual(host, '::1'); + assert.strictEqual(family, 4); + callback(null, '127.0.0.1', 4); + }); + + const socket = dgram.createSocket('udp4'); + + socket.bind(0, '::1', common.mustCall(() => { + socket.close(); + next(); + })); +} + +const cases = [ + ipv4SendSkipsLookup, + ipv6BindSkipsLookup, + mismatchedFamilyFallsThrough, +]; + +(function runNext() { + const testCase = cases.shift(); + if (testCase !== undefined) { + testCase(runNext); + } +})(); diff --git a/test/sequential/test-dgram-implicit-bind-failure.js b/test/sequential/test-dgram-implicit-bind-failure.js index 89da00d5766fb3..ec00022c693730 100644 --- a/test/sequential/test-dgram-implicit-bind-failure.js +++ b/test/sequential/test-dgram-implicit-bind-failure.js @@ -4,19 +4,20 @@ const common = require('../common'); const assert = require('assert'); const EventEmitter = require('events'); const dgram = require('dgram'); -const dns = require('dns'); const { kStateSymbol } = require('internal/dgram'); const mockError = new Error('fake DNS'); -// Monkey patch dns.lookup() so that it always fails. -dns.lookup = function(address, family, callback) { +const socket = dgram.createSocket('udp4'); + +// Fail the implicit bind by making the handle's address resolution fail. A +// literal bind address is not passed to dns.lookup(), so patching dns.lookup() +// would not be observed here. +socket[kStateSymbol].handle.lookup = function(address, callback) { process.nextTick(() => { callback(mockError); }); }; -const socket = dgram.createSocket('udp4'); - socket.on(EventEmitter.errorMonitor, common.mustCall((err) => { - // The DNS lookup should fail since it is monkey patched. At that point in + // The bind should fail since the lookup is monkey patched. At that point in // time, the send queue should be populated with the send() operation. assert.strictEqual(err, mockError); assert(Array.isArray(socket[kStateSymbol].queue));