Skip to content

Conversation

@robobun
Copy link
Collaborator

@robobun robobun commented Jan 27, 2026

Summary

  • Fix dns.resolveSrv() failing with ECONNREFUSED on Windows when c-ares falls back to using 127.0.0.1 as the DNS server
  • Add detection for the localhost-only DNS server scenario and fallback to public DNS servers (Cloudflare 1.1.1.1, Google 8.8.8.8/8.8.4.4)

Root Cause

On Windows, c-ares uses IPHLPAPI (GetNetworkParams/GetAdaptersInfo) to detect system DNS servers. This can fail on certain Windows configurations (e.g., fresh installs, certain network setups), causing c-ares to fall back to 127.0.0.1 as the DNS server.

Since 127.0.0.1 typically doesn't run a DNS server, all DNS queries that require c-ares (like SRV records, since Windows' getaddrinfo() doesn't support them) fail with ECONNREFUSED.

On Linux, this issue doesn't occur because c-ares reads DNS servers from /etc/resolv.conf, which is reliably maintained by the system.

Solution

After initializing the c-ares channel on Windows, we check if the only configured DNS server is 127.0.0.1. If so, we replace it with well-known public DNS servers:

  • Cloudflare: 1.1.1.1
  • Google: 8.8.8.8, 8.8.4.4

This allows DNS queries to succeed even when Windows' DNS server detection fails.

Test plan

  • Added regression test in test/regression/issue/26467.test.ts
  • Verified existing DNS tests pass (test/js/node/dns/node-dns.test.js)
  • Needs Windows CI to validate the fix on affected systems

Fixes #26467

🤖 Generated with Claude Code

….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>
@robobun
Copy link
Collaborator Author

robobun commented Jan 27, 2026

Updated 7:43 PM PT - Jan 26th, 2026

❌ Your commit 83bd8103 has 7 failures in Build #35902 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 26468

That installs a local version of the PR into your bun-26468 executable, so you can run:

bun-26468 --bun

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

Walkthrough

Adds Windows-specific DNS fallback logic that detects when system-reported DNS servers default to localhost (127.0.0.1) and automatically replaces them with public DNS servers. A regression test validates DNS resolution and the fallback behavior on affected systems.

Changes

Cohort / File(s) Summary
DNS Fallback Implementation
src/deps/c_ares.zig
Introduces applyWindowsDnsFallback() function that retrieves configured DNS servers and replaces localhost (127.0.0.1) with public fallback servers (1.1.1.1, 8.8.8.8, 8.8.4.4) during channel initialization. Handles edge cases: skips if multiple servers are configured or retrieval fails. +47 lines
DNS Resolution Regression Tests
test/regression/issue/26467.test.ts
New regression test file validating dns.resolveSrv() returns correctly formatted SRV records and dns.getServers() contains at least one non-localhost server, confirming fallback behavior on affected Windows systems. +49 lines

Suggested reviewers

  • cirospaciari
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: implementing a fallback to public DNS servers when Windows c-ares detects only 127.0.0.1.
Description check ✅ Passed The description covers the root cause, solution, and test plan clearly, though it references a template with 'What does this PR do?' and 'How did you verify your code works?' sections that are implicitly answered in the summary and test plan.
Linked Issues check ✅ Passed The PR fully addresses issue #26467 by implementing DNS server detection and fallback logic, adding regression tests, and providing public DNS server substitution for localhost scenarios.
Out of Scope Changes check ✅ Passed All changes (Windows DNS fallback logic in c_ares.zig and regression test) are directly scoped to fixing the #26467 DNS resolution issue on Windows.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/deps/c_ares.zig`:
- Around line 622-659: The applyWindowsDnsFallback function currently
unconditionally replaces a single 127.0.0.1 DNS server; change it to respect
user intent by (1) checking an opt-out environment variable or config flag
(e.g., "CARES_DISABLE_LOCALHOST_FALLBACK") at the start of
applyWindowsDnsFallback and returning early if set, and (2) before calling
ares_set_servers_csv, attempt to verify the localhost resolver is unreachable by
performing a quick query against the channel (or a direct socket probe) and only
apply the fallback if that probe fails; also emit a clear processLogger or debug
log when skipping or applying the fallback so users can discover the behavior.
Ensure references to ares_get_servers_ports, ares_set_servers_csv, and
applyWindowsDnsFallback are used to locate and modify the code.

In `@test/regression/issue/26467.test.ts`:
- Around line 33-49: The test's non-localhost assertion should only run on
Windows to avoid false failures on systems using loopback DNS (e.g.,
127.0.0.53); modify the test "dns.getServers should not return only 127.0.0.1
after initialization" to check process.platform === "win32" before asserting
hasNonLocalhost, and on Windows assert that the servers array is not composed
solely of "127.0.0.1" and "::1" (i.e., fail only when every entry is exactly
"127.0.0.1" or "::1"); keep the existing Array.isArray and length checks for all
platforms and only gate the non-localhost check (hasNonLocalhost / servers.some)
behind the Windows platform check.

Comment on lines +622 to +659
/// 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");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the entire function and surrounding context
cat -n src/deps/c_ares.zig | head -700 | tail -100

Repository: oven-sh/bun

Length of output: 5382


🏁 Script executed:

# Search for calls to applyWindowsDnsFallback
rg "applyWindowsDnsFallback" src/

Repository: oven-sh/bun

Length of output: 198


🏁 Script executed:

# Look for any configuration or opt-in mechanisms related to DNS fallback
rg -i "fallback|dns.*opt|opt.*dns" src/deps/c_ares.zig -A 2 -B 2

Repository: oven-sh/bun

Length of output: 4017


🏁 Script executed:

# Check if there's any documentation or comments about Windows DNS issues
rg -i "windows.*dns|iphlpapi|getnetworkparams" src/deps/c_ares.zig -A 3 -B 3

Repository: oven-sh/bun

Length of output: 2553


Public DNS fallback can override intentional localhost resolvers.

The function unconditionally replaces a 127.0.0.1-only DNS configuration with public resolvers on Windows. While defensive checks prevent override when multiple servers are configured, a user with an intentional single localhost resolver (Pi-hole, corporate DNS stub, VPN tunnel) would have it silently replaced. This addresses a real Windows c-ares bug (#26467), but there's no mechanism for users to disable this behavior. Consider either (a) adding an environment variable or configuration flag to opt out, (b) attempting to verify the localhost resolver is actually unreachable before replacing it, or (c) making this behavior more discoverable in logs/documentation.

🤖 Prompt for AI Agents
In `@src/deps/c_ares.zig` around lines 622 - 659, The applyWindowsDnsFallback
function currently unconditionally replaces a single 127.0.0.1 DNS server;
change it to respect user intent by (1) checking an opt-out environment variable
or config flag (e.g., "CARES_DISABLE_LOCALHOST_FALLBACK") at the start of
applyWindowsDnsFallback and returning early if set, and (2) before calling
ares_set_servers_csv, attempt to verify the localhost resolver is unreachable by
performing a quick query against the channel (or a direct socket probe) and only
apply the fallback if that probe fails; also emit a clear processLogger or debug
log when skipping or applying the fallback so users can discover the behavior.
Ensure references to ares_get_servers_ports, ares_set_servers_csv, and
applyWindowsDnsFallback are used to locate and modify the code.

Comment on lines +33 to +49
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);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard the non-localhost assertion to Windows to avoid false failures.
Line 43 treats any 127.* or ::1 entry as a failure, but Linux (systemd-resolved 127.0.0.53) and container DNS (127.0.0.11) legitimately use loopback. This can make the test flaky off Windows. Gate the assertion to Windows and only reject a sole 127.0.0.1/::1 entry.

💡 Proposed fix
-  // 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);
+  // Only assert the localhost-only fallback case on Windows.
+  if (process.platform === "win32") {
+    const onlyLocalhost =
+      servers.length === 1 && (servers[0] === "127.0.0.1" || servers[0] === "::1");
+    expect(onlyLocalhost).toBe(false);
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
});
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);
// Only assert the localhost-only fallback case on Windows.
if (process.platform === "win32") {
const onlyLocalhost =
servers.length === 1 && (servers[0] === "127.0.0.1" || servers[0] === "::1");
expect(onlyLocalhost).toBe(false);
}
});
🤖 Prompt for AI Agents
In `@test/regression/issue/26467.test.ts` around lines 33 - 49, The test's
non-localhost assertion should only run on Windows to avoid false failures on
systems using loopback DNS (e.g., 127.0.0.53); modify the test "dns.getServers
should not return only 127.0.0.1 after initialization" to check process.platform
=== "win32" before asserting hasNonLocalhost, and on Windows assert that the
servers array is not composed solely of "127.0.0.1" and "::1" (i.e., fail only
when every entry is exactly "127.0.0.1" or "::1"); keep the existing
Array.isArray and length checks for all platforms and only gate the
non-localhost check (hasNonLocalhost / servers.some) behind the Windows platform
check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DNS querySrv ECONNREFUSED

2 participants