fix(dns): fallback to public DNS servers when c-ares detects only 127.0.0.1 on Windows

On Windows, c-ares uses IPHLPAPI (GetNetworkParams/GetAdaptersInfo) to
detect system DNS servers. This can fail on some Windows configurations,
causing c-ares to fall back to 127.0.0.1, which doesn't respond to DNS
queries. This results in ECONNREFUSED errors for dns.resolveSrv() and
other DNS record queries that must use c-ares.

This fix detects when c-ares has only 127.0.0.1 configured as a DNS
server after initialization and replaces it with public DNS servers
(Cloudflare 1.1.1.1, Google 8.8.8.8/8.8.4.4) as a fallback.

Fixes #26467

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-27 02:44:57 +00:00
parent bfe40e8760
commit 83bd8103e6
2 changed files with 96 additions and 0 deletions

View File

@@ -607,10 +607,57 @@ pub const Channel = opaque {
return err;
}
// On Windows, c-ares may fail to detect DNS servers from the system
// and fall back to 127.0.0.1. This causes DNS queries to fail with
// ECONNREFUSED. Detect this case and use public DNS servers as fallback.
// See: https://github.com/oven-sh/bun/issues/26467
if (bun.Environment.isWindows) {
channel.applyWindowsDnsFallback();
}
this.channel = channel;
return null;
}
/// On Windows, c-ares uses IPHLPAPI (GetNetworkParams/GetAdaptersInfo) to detect
/// system DNS servers. This can fail on some Windows configurations, causing
/// c-ares to fall back to 127.0.0.1, which doesn't respond to DNS queries.
/// This function detects that case and sets public DNS servers as a fallback.
fn applyWindowsDnsFallback(channel: *Channel) void {
var servers: ?*struct_ares_addr_port_node = null;
if (Error.get(ares_get_servers_ports(channel, &servers))) |_| {
// Failed to get servers, can't apply fallback
return;
}
defer ares_free_data(servers);
// Check if the only configured server is localhost (127.0.0.1)
const first_server = servers orelse return;
if (first_server.next != null) {
// Multiple servers configured, don't override
return;
}
// Check if it's an IPv4 address
if (first_server.family != AF.INET) {
return;
}
// Check if it's 127.0.0.1 (localhost)
// struct_in_addr is std.posix.sockaddr.in which contains .sa (sockaddr struct)
// The s_addr is the first 4 bytes in network byte order
const addr_bytes: *const [4]u8 = @ptrCast(&first_server.addr.addr4);
const is_localhost = addr_bytes[0] == 127 and addr_bytes[1] == 0 and addr_bytes[2] == 0 and addr_bytes[3] == 1;
if (!is_localhost) {
return;
}
// The only DNS server is 127.0.0.1, which likely won't work.
// Set fallback public DNS servers: Cloudflare (1.1.1.1) and Google (8.8.8.8, 8.8.4.4)
_ = ares_set_servers_csv(channel, "1.1.1.1,8.8.8.8,8.8.4.4");
}
pub fn deinit(this: *Channel) void {
ares_destroy(this);
}

View File

@@ -0,0 +1,49 @@
import { beforeAll, expect, setDefaultTimeout, test } from "bun:test";
import dns from "dns";
beforeAll(() => {
setDefaultTimeout(30_000);
});
// https://github.com/oven-sh/bun/issues/26467
// On Windows, dns.resolveSrv() was failing with ECONNREFUSED when c-ares
// fell back to using 127.0.0.1 as the DNS server due to IPHLPAPI failures.
// The fix adds a fallback to public DNS servers when only 127.0.0.1 is configured.
test("dns.resolveSrv should resolve SRV records", async () => {
// Use a known SRV record for testing
const hostname = "_test._tcp.test.socketify.dev";
const results = await dns.promises.resolveSrv(hostname);
expect(Array.isArray(results)).toBe(true);
expect(results.length).toBeGreaterThan(0);
// Verify the SRV record structure
const record = results[0];
expect(record).toHaveProperty("name");
expect(record).toHaveProperty("port");
expect(record).toHaveProperty("priority");
expect(record).toHaveProperty("weight");
expect(typeof record.name).toBe("string");
expect(typeof record.port).toBe("number");
expect(typeof record.priority).toBe("number");
expect(typeof record.weight).toBe("number");
});
test("dns.getServers should not return only 127.0.0.1 after initialization", () => {
// After the fix, if c-ares detects only 127.0.0.1 on Windows,
// it should have been replaced with public DNS servers.
const servers = dns.getServers();
expect(Array.isArray(servers)).toBe(true);
expect(servers.length).toBeGreaterThan(0);
// The servers should contain at least one non-localhost server
// If the fix worked, we shouldn't see only "127.0.0.1" as the sole server
const hasNonLocalhost = servers.some(server => !server.startsWith("127.") && server !== "::1");
// Note: This test may pass even without the fix on systems with proper DNS configuration.
// The fix specifically targets Windows systems where IPHLPAPI fails to detect DNS servers.
// On Linux/macOS, this should always pass since /etc/resolv.conf is reliable.
expect(hasNonLocalhost).toBe(true);
});