From 9b0911926ca62b213c72b13886cff65560493d4e Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Fri, 22 May 2026 17:36:58 +0530 Subject: [PATCH] fix: use OS resolver for .local hostnames to fix mDNS resolution (#8072) --- .../src/network/fast-lookup.spec.ts | 52 +++++++++++++++++++ .../bruno-requests/src/network/fast-lookup.ts | 16 ++++++ 2 files changed, 68 insertions(+) diff --git a/packages/bruno-requests/src/network/fast-lookup.spec.ts b/packages/bruno-requests/src/network/fast-lookup.spec.ts index 89205f3e2..ab4ab36c2 100644 --- a/packages/bruno-requests/src/network/fast-lookup.spec.ts +++ b/packages/bruno-requests/src/network/fast-lookup.spec.ts @@ -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')); diff --git a/packages/bruno-requests/src/network/fast-lookup.ts b/packages/bruno-requests/src/network/fast-lookup.ts index 6f103bff8..0f571c83b 100644 --- a/packages/bruno-requests/src/network/fast-lookup.ts +++ b/packages/bruno-requests/src/network/fast-lookup.ts @@ -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