feat: skip SRV lookup for IP addresses and localhost

Performing DNS SRV lookups for hosts that are already direct IP
addresses or special-case hostnames like 'localhost' is
unnecessary.

In addition, several related test suite improvements
have been included:

- A bug in the Bedrock mock packet creation was fixed where the magic
  GUID was being written to an incorrect buffer offset.
- The DNS mocking strategy in the Java tests has been refactored for
  better accuracy by mocking the `dns.promises.Resolver` class.
- An error test for the Bedrock pinger was corrected to properly
  handle a promise rejection instead of a synchronous throw.
This commit is contained in:
2025-06-25 01:46:53 +03:00
parent 1e6b5b3973
commit 371fb9daa7
3 changed files with 69 additions and 35 deletions

View File

@ -5,7 +5,7 @@
"use strict";
import net from "node:net";
import { createConnection, isIP } from "node:net";
import { Resolver } from "node:dns/promises";
import createDebug from "debug";
import * as varint from "./varint.js";
@ -147,39 +147,60 @@ export async function pingJava(host, options = {}) {
let targetHost = host;
let targetPort = fallbackPort;
try {
// A list of hostnames that should never have an SRV lookup.
const nonSrvLookableHostnames = ["localhost"];
// Check if the host is a valid IP address (v4 or v6).
// net.isIP() returns 0 for invalid IPs, 4 for IPv4, and 6 for IPv6.
const isDirectIp = isIP(host) !== 0;
const isNonLookableHostname = nonSrvLookableHostnames.includes(
host.toLowerCase()
);
if (isDirectIp || isNonLookableHostname) {
debug(
"attempting SRV lookup for _minecraft._tcp.%s with %dms timeout",
host,
timeout
"host '%s' is a direct IP or a non-lookable hostname, skipping SRV lookup.",
host
);
const resolver = new Resolver({ timeout, tries: 3 });
const srvRecords = await resolver.resolveSrv(`_minecraft._tcp.${host}`);
if (srvRecords.length > 0) {
targetHost = srvRecords[0].name;
targetPort = srvRecords[0].port;
debug("SRV lookup successful, new target: %s:%d", targetHost, targetPort);
}
} catch (err) {
// Common errors like ENODATA, ENOTFOUND, or a DNS timeout (ETIMEOUT) are expected
// when a server does not have an SRV record, so we ignore them and proceed.
const nonFatalDnsCodes = ["ENODATA", "ENOTFOUND", "ETIMEOUT"];
if (
err instanceof Error &&
"code" in err &&
nonFatalDnsCodes.includes(err.code)
) {
debug("SRV lookup for %s failed (%s), using fallback.", host, err.code);
} else {
// Re-throw anything else to fail the operation
debug("SRV lookup for %s failed unexpectedly, re-throwing.", host, err);
throw err;
} else {
try {
debug(
"attempting SRV lookup for _minecraft._tcp.%s with %dms timeout",
host,
timeout
);
const resolver = new Resolver({ timeout, tries: 3 });
const srvRecords = await resolver.resolveSrv(`_minecraft._tcp.${host}`);
if (srvRecords.length > 0) {
targetHost = srvRecords[0].name;
targetPort = srvRecords[0].port;
debug(
"SRV lookup successful, new target: %s:%d",
targetHost,
targetPort
);
}
} catch (err) {
// Common errors like ENODATA, ENOTFOUND, or a DNS timeout (ETIMEOUT) are expected
// when a server does not have an SRV record, so we ignore them and proceed.
const nonFatalDnsCodes = ["ENODATA", "ENOTFOUND", "ETIMEOUT"];
if (
err instanceof Error &&
"code" in err &&
nonFatalDnsCodes.includes(err.code)
) {
debug("SRV lookup for %s failed (%s), using fallback.", host, err.code);
} else {
// Re-throw anything else to fail the operation
debug("SRV lookup for %s failed unexpectedly, re-throwing.", host, err);
throw err;
}
}
}
return new Promise((resolve, reject) => {
debug("creating TCP connection to %s:%d", targetHost, targetPort);
const socket = net.createConnection({ host: targetHost, port: targetPort });
const socket = createConnection({ host: targetHost, port: targetPort });
// Prevent cleanup tasks from running more than once
// in case of multiple error callbacks