diff --git a/src/node/internal/internal_dns.ts b/src/node/internal/internal_dns.ts index 005339b04b0..70c3a8ed842 100644 --- a/src/node/internal/internal_dns.ts +++ b/src/node/internal/internal_dns.ts @@ -154,10 +154,10 @@ export function lookup( address: answer.data, family: 4, })) ?? []; - const ipv6Addresses: { address: string; family: 4 }[] = + const ipv6Addresses: { address: string; family: 6 }[] = ipv6Response.Answer?.map((answer) => ({ address: answer.data, - family: 4, + family: 6, })) ?? []; // No addresses found @@ -178,10 +178,43 @@ export function lookup( .catch((error: unknown): void => { process.nextTick(callback, error); }); + } else if (family === 0) { + // family=0, all=false: query both A and AAAA, return first result based on dnsOrder + Promise.all([ + sendDnsRequest(hostname, 'A').catch(() => ({ Answer: [] })), + sendDnsRequest(hostname, 'AAAA').catch(() => ({ Answer: [] })), + ]) + .then(([ipv4Response, ipv6Response]): void => { + const ipv4 = ipv4Response.Answer?.at(0)?.data; + const ipv6 = ipv6Response.Answer?.at(0)?.data; + + if (ipv4 == null && ipv6 == null) { + callback(new DnsError(hostname, errorCodes.NOTFOUND, 'queryA')); + return; + } + + // Return the preferred address based on dnsOrder + if (dnsOrder === 'ipv6first') { + if (ipv6 != null) { + callback(null, ipv6, 6); + } else { + callback(null, ipv4 as string, 4); + } + } else { + if (ipv4 != null) { + callback(null, ipv4, 4); + } else { + callback(null, ipv6 as string, 6); + } + } + }) + .catch((error: unknown): void => { + process.nextTick(callback, error); + }); } else { const requestType = family === 4 ? 'A' : 'AAAA'; - // Single request for all other cases (including when all=true but family is specified) + // Single request when family is specified (with or without all=true) sendDnsRequest(hostname, requestType) .then((json): void => { validateAnswer(json.Answer, hostname, `query${requestType}`); diff --git a/src/node/internal/internal_dns_promises.ts b/src/node/internal/internal_dns_promises.ts index 872ea4d0781..a9a1c2daacf 100644 --- a/src/node/internal/internal_dns_promises.ts +++ b/src/node/internal/internal_dns_promises.ts @@ -126,7 +126,7 @@ export class Resolver implements dns.Resolver { return resolveNs(name); } - esolvePtr(name: string): Promise { + resolvePtr(name: string): Promise { return resolvePtr(name); } diff --git a/src/workerd/api/node/tests/dns-nodejs-test.js b/src/workerd/api/node/tests/dns-nodejs-test.js index b9fb0cc6002..af787cd6078 100644 --- a/src/workerd/api/node/tests/dns-nodejs-test.js +++ b/src/workerd/api/node/tests/dns-nodejs-test.js @@ -371,6 +371,96 @@ export const getServers = { }, }; +// Regression: dns.lookup() with all:true and family:0 must return family:6 for +// IPv6 addresses (was returning family:4 due to copy-paste bug). +export const lookupAllFamilyZeroIPv6Family = { + async test() { + const results = await dnsPromises.lookup(addresses.INET_HOST, { + all: true, + family: 0, + }); + ok(Array.isArray(results), 'expected array of addresses'); + ok(results.length > 0, 'expected at least one address'); + + for (const entry of results) { + strictEqual(typeof entry.address, 'string'); + ok( + entry.family === 4 || entry.family === 6, + `family must be 4 or 6, got ${entry.family}` + ); + } + + // If any IPv6 addresses are returned, they must have family:6 + const ipv6Entries = results.filter((e) => e.address.includes(':')); + for (const entry of ipv6Entries) { + strictEqual( + entry.family, + 6, + `IPv6 address ${entry.address} should have family:6 but got family:${entry.family}` + ); + } + + // If any IPv4 addresses are returned, they must have family:4 + const ipv4Entries = results.filter((e) => !e.address.includes(':')); + for (const entry of ipv4Entries) { + strictEqual( + entry.family, + 4, + `IPv4 address ${entry.address} should have family:4 but got family:${entry.family}` + ); + } + }, +}; + +// Regression: dns.lookup() with default family (0) and all:false must resolve +// hosts that only have A records (was only querying AAAA, failing for IPv4-only). +export const lookupDefaultFamilyResolvesIPv4 = { + async test() { + // Default options (family:0, all:false) — should return an address + const result = await dnsPromises.lookup(addresses.INET4_HOST); + ok(result != null, 'expected a result'); + strictEqual(typeof result.address, 'string'); + ok(result.address.length > 0, 'expected non-empty address'); + ok( + result.family === 4 || result.family === 6, + `family must be 4 or 6, got ${result.family}` + ); + + // Also verify via callback API + const { promise, resolve, reject } = Promise.withResolvers(); + dns.lookup(addresses.INET4_HOST, (error, address, family) => { + if (error) { + reject(error); + return; + } + strictEqual(typeof address, 'string'); + ok(address.length > 0, 'expected non-empty address from callback'); + ok(family === 4 || family === 6, `family must be 4 or 6, got ${family}`); + resolve(); + }); + await promise; + }, +}; + +// Regression: Resolver.resolvePtr must exist (was misspelled as 'esolvePtr'). +export const resolverResolvePtrExists = { + async test() { + const resolver = new dnsPromises.Resolver(); + strictEqual( + typeof resolver.resolvePtr, + 'function', + 'Resolver.resolvePtr should be a function' + ); + // Actually call it to verify it works end-to-end + const result = await resolver.resolvePtr(addresses.PTR_HOST); + ok(Array.isArray(result), 'expected array result'); + ok(result.length > 0, 'expected at least one PTR record'); + for (const item of result) { + strictEqual(typeof item, 'string'); + } + }, +}; + // Tests are taken from // https://github.com/nodejs/node/blob/3153c8333e3a8f2015b795642def4d81ec7cd7b3/test/parallel/test-dns-lookup.js export const testDnsLookup = {