fix: use OS resolver for .local hostnames to fix mDNS resolution (#8072)

This commit is contained in:
sanish chirayath
2026-05-22 17:36:58 +05:30
committed by GitHub
parent 8cd7c26648
commit 9b0911926c
2 changed files with 68 additions and 0 deletions

View File

@@ -31,6 +31,58 @@ describe('fastLookup', () => {
});
});
it('should use dns.lookup directly for .local hostnames (mDNS)', (done) => {
mockResolve('resolve4', ['192.0.78.134']); // bogus public-DNS result
mockLookup('192.168.33.254', 4); // correct mDNS result
fastLookup('fhir.local', {}, (err, address, family) => {
expect(err).toBeNull();
expect(address).toBe('192.168.33.254');
expect(family).toBe(4);
expect(dns.resolve4).not.toHaveBeenCalled();
done();
});
});
it('should use dns.lookup for mixed-case .LOCAL hostnames', (done) => {
mockResolve('resolve4', ['192.0.78.134']);
mockLookup('192.168.33.254', 4);
fastLookup('FHIR.LOCAL', {}, (err, address, family) => {
expect(err).toBeNull();
expect(address).toBe('192.168.33.254');
expect(family).toBe(4);
expect(dns.resolve4).not.toHaveBeenCalled();
done();
});
});
it('should use dns.lookup directly for localhost (RFC 6761)', (done) => {
mockResolve('resolve4', ['93.184.216.34']); // hijacked result
mockLookup('127.0.0.1', 4);
fastLookup('localhost', {}, (err, address, family) => {
expect(err).toBeNull();
expect(address).toBe('127.0.0.1');
expect(family).toBe(4);
expect(dns.resolve4).not.toHaveBeenCalled();
done();
});
});
it('should use dns.lookup for .localhost subdomains', (done) => {
mockResolve('resolve4', ['93.184.216.34']);
mockLookup('127.0.0.1', 4);
fastLookup('api.localhost', {}, (err, address, family) => {
expect(err).toBeNull();
expect(address).toBe('127.0.0.1');
expect(family).toBe(4);
expect(dns.resolve4).not.toHaveBeenCalled();
done();
});
});
it('should fall back to dns.lookup when both resolvers fail', (done) => {
mockResolve('resolve4', [], new Error('ENOTFOUND'));
mockResolve('resolve6', [], new Error('ENOTFOUND'));

View File

@@ -6,6 +6,10 @@ import dns from 'node:dns';
* Tries dns.resolve4 then dns.resolve6 (async, c-ares based),
* falls back to dns.lookup for /etc/hosts and mDNS hostnames.
*
* .local hostnames are reserved for mDNS (RFC 6762) and must always use the
* OS resolver (dns.lookup / getaddrinfo) — c-ares doesn't speak mDNS, so
* dns.resolve4 can return bogus public-DNS results for .local names.
*
* NOTE: `options.family` is not currently respected — the function always
* tries IPv4 first regardless of the caller's preference. This is safe today
* because Bruno's HTTP agents use the default family (0), but should be
@@ -16,6 +20,18 @@ export function fastLookup(
options: dns.LookupOptions | undefined,
callback: (err: Error | null, address: string | dns.LookupAddress[], family?: number) => void
): void {
// .local domains use mDNS (RFC 6762 — https://datatracker.ietf.org/doc/html/rfc6762)
// which only the OS resolver understands. c-ares queries public DNS and may
// return wrong results.
// localhost is reserved (RFC 6761 — https://datatracker.ietf.org/doc/html/rfc6761)
// and must always resolve via the OS — c-ares could return hijacked results.
const lower = hostname.toLowerCase();
if (lower.endsWith('.local') || lower === 'localhost' || lower.endsWith('.localhost')) {
return dns.lookup(hostname, options ?? {}, (err, address, family) => {
callback(err, address, family);
});
}
dns.resolve4(hostname, (err4, addresses4) => {
if (!err4 && addresses4?.length) {
return options?.all