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

UDP/dgram performs unnecessary DNS lookups #39468

Open
schmod opened this issue Jul 20, 2021 · 3 comments · May be fixed by #39473
Open

UDP/dgram performs unnecessary DNS lookups #39468

schmod opened this issue Jul 20, 2021 · 3 comments · May be fixed by #39473
Labels
dgram Issues and PRs related to the dgram subsystem / UDP. dns Issues and PRs related to the dns subsystem.

Comments

@schmod
Copy link

schmod commented Jul 20, 2021

Version

14.x

Platform

All

Subsystem

dgram

What steps will reproduce the bug?

It appears that UDP/dgram connections perform unnecessary DNS lookups when an IP address is provided as the address to connect to.

The issue is easily demonstrated via this snippet:

const dns = require("dns");

const _lookup = dns.lookup;
dns.lookup = function (...args) {
  console.log("Called lookup", args);
  return _lookup(...arguments);
};

const dgram = require("dgram");

const socket = dgram.createSocket({ type: "udp4" });
console.log("Socket created");

socket.connect(1234, "192.168.1.1", (err) => {
  console.log("Socket connected", { err });
});

Output:

$ node index.js

Socket created
Called lookup [ '0.0.0.0', 4, [Function (anonymous)] ]
Called lookup [ '192.168.1.1', 4, [Function: afterDns] ]
Socket connected { err: undefined }

How often does it reproduce? Is there a required condition?

These lookups appear to happen 100% of the time a UDP socket connects to an address that is a raw IP address.

What is the expected behavior?

DNS lookups only occur when the address is a hostname.

What do you see instead?

No response

Additional information

Note that the net module contains code that bypasses dns.lookup() when the target address is a valid IP address. It seems like we could improve efficiency by adding a similar check to dgram without breaking compatibility.

evanlucas added a commit to evanlucas/node that referenced this issue Jul 20, 2021
Previously, we would call dns.lookup even for IP addresses
in Socket#connect. This now skips that to improve performance.

Fixes: nodejs#39468
@evanlucas evanlucas linked a pull request Jul 20, 2021 that will close this issue
evanlucas added a commit to evanlucas/node that referenced this issue Jul 20, 2021
Previously, we would call dns.lookup even for IP addresses
in Socket#connect. This now skips that to improve performance.

Fixes: nodejs#39468
@Ayase-252 Ayase-252 added dgram Issues and PRs related to the dgram subsystem / UDP. dns Issues and PRs related to the dns subsystem. labels Jul 21, 2021
evanlucas added a commit to evanlucas/node that referenced this issue Aug 5, 2021
Previously, we would call dns.lookup even for IP addresses
in Socket#connect. This now skips that to improve performance.

Fixes: nodejs#39468
@renatomariscal
Copy link
Contributor

renatomariscal commented Feb 28, 2022

I suspect there is a misconception here. The dns.lookup is itself the place where NodeJS has implemented the code to prevent actual DNS lookup on IP address:

node/lib/dns.js

Lines 155 to 164 in b3723fa

const matchedFamily = isIP(hostname);
if (matchedFamily) {
if (all) {
process.nextTick(
callback, null, [{ address: hostname, family: matchedFamily }]);
} else {
process.nextTick(callback, null, hostname, matchedFamily);
}
return {};
}

So, the fact that dns.lookup is called doesn't imply on IO.

@renatomariscal
Copy link
Contributor

Trying to come up with a test, the easiest I could find is to get an invalid DNS server, and if it is true that for ip it does lookup, it would fail the same way it fails for a non-ip name.

NodeJS dns.setServers explicitly don't work for dns.lookup, and there is no alternative.

We can use docker with an invalid DNS server:

docker run --dns 127.0.0.1 -it --rm node:12.18.4

Test 1 - call a non-ip name:

Calling foobar:

require("dgram").
    createSocket({ type: "udp4" }).
    connect(1234, "foobar", (err) => {
        console.log("Socket connected", { err });
    });

Result

Socket connected {
  err: Error: getaddrinfo EAI_AGAIN foobar
      at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:66:26)
      at GetAddrInfoReqWrap.callbackTrampoline (internal/async_hooks.js:120:14) {
    errno: 'EAI_AGAIN',
    code: 'EAI_AGAIN',
    syscall: 'getaddrinfo',
    hostname: 'foobar'
  }
}

Test 2 - call an ip address:

Calling 192.168.0.1:

require("dgram").
    createSocket({ type: "udp4" }).
    connect(1234, "192.168.0.1", (err) => {
        console.log("Socket connected", { err });
    });

Result

 Socket connected { err: undefined }

So, I am quite convinced it is working properly.

@schmod
Copy link
Author

schmod commented Mar 1, 2022

To test conclusively, I think you would want to actually run a real DNS server with logging, just in case there's something going on in the Docker image and/or c-ares. However, having looked at the code, I do think that you might be right.

That being said, I think we should still move forward with the change proposed in this ticket.

  • There is still a (minor) performance benefit
  • It would improve consistency between dgram and the net module, and create a more consistent contract for custom resolver functions (ie. for both TCP and UDP, Node will avoid calling the DNS resolver when there's nothing to resolve)
  • It would greatly reduce the number of real-world cases where dns.lookup ends up being a no-op. Many folks (including myself) instrument dns.lookup to monitor performance, and are getting false signals because of the way that drgram invokes it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dgram Issues and PRs related to the dgram subsystem / UDP. dns Issues and PRs related to the dns subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants