Skip to content

Commit e71e950

Browse files
NotVivek12aduh95
authored andcommitted
dns: fix Windows SRV ECONNREFUSED by adjusting c-ares fallback detection
Newer c-ares versions set tcp_port/udp_port to 53 instead of 0, which caused the loopback detection to fail. This fix: - Removes the port check from loopback detection - Adds IPv6 loopback (::1) support - Calls EnsureServers() before each DNS query PR-URL: #61453 Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent e90eb1d commit e71e950

File tree

4 files changed

+281
-6
lines changed

4 files changed

+281
-6
lines changed

src/cares_wrap.cc

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -983,19 +983,31 @@ void ChannelWrap::EnsureServers() {
983983

984984
ares_get_servers_ports(channel_, &servers);
985985

986-
/* if no server or multi-servers, ignore */
986+
/* if no server, ignore */
987987
if (servers == nullptr) return;
988+
989+
/* if multi-servers, mark as non-default and ignore */
988990
if (servers->next != nullptr) {
989991
ares_free_data(servers);
990992
is_servers_default_ = false;
991993
return;
992994
}
993995

994-
/* if the only server is not 127.0.0.1, ignore */
995-
if (servers[0].family != AF_INET ||
996-
servers[0].addr.addr4.s_addr != htonl(INADDR_LOOPBACK) ||
997-
servers[0].tcp_port != 0 ||
998-
servers[0].udp_port != 0) {
996+
/* Check if the only server is a loopback address (IPv4 127.0.0.1 or IPv6
997+
* ::1). Newer c-ares versions may set tcp_port/udp_port to 53 instead of 0,
998+
* so we no longer check port values. */
999+
bool is_loopback = false;
1000+
if (servers[0].family == AF_INET) {
1001+
is_loopback = (servers[0].addr.addr4.s_addr == htonl(INADDR_LOOPBACK));
1002+
} else if (servers[0].family == AF_INET6) {
1003+
static const unsigned char kIPv6Loopback[16] = {
1004+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1};
1005+
is_loopback =
1006+
(memcmp(&servers[0].addr.addr6, kIPv6Loopback, sizeof(kIPv6Loopback)) ==
1007+
0);
1008+
}
1009+
1010+
if (!is_loopback) {
9991011
ares_free_data(servers);
10001012
is_servers_default_ = false;
10011013
return;
@@ -1769,6 +1781,10 @@ static void Query(const FunctionCallbackInfo<Value>& args) {
17691781
node::Utf8Value utf8name(args.GetIsolate(), string);
17701782
auto plain_name = utf8name.ToStringView();
17711783
std::string name = ada::idna::to_ascii(plain_name);
1784+
1785+
// Ensure c-ares did not fall back to loopback resolver.
1786+
channel->EnsureServers();
1787+
17721788
channel->ModifyActivityQueryCount(1);
17731789
int err = wrap->Send(name.c_str());
17741790
if (err) {

test/common/dns.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const types = {
1313
PTR: 12,
1414
MX: 15,
1515
TXT: 16,
16+
SRV: 33,
1617
ANY: 255,
1718
CAA: 257,
1819
};
@@ -279,6 +280,15 @@ function writeDNSPacket(parsed) {
279280
buffers.push(Buffer.from('issue' + rr.issue));
280281
break;
281282
}
283+
case 'SRV':
284+
{
285+
// SRV record format: priority (2) + weight (2) + port (2) + target
286+
const target = writeDomainName(rr.name);
287+
rdLengthBuf[0] = 6 + target.length;
288+
buffers.push(new Uint16Array([rr.priority, rr.weight, rr.port]));
289+
buffers.push(target);
290+
break;
291+
}
282292
default:
283293
throw new Error(`Unknown RR type ${rr.type}`);
284294
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
'use strict';
2+
// Regression test for SRV record resolution returning ECONNREFUSED.
3+
//
4+
// This test verifies that dns.resolveSrv() properly handles SRV queries
5+
// and doesn't incorrectly return ECONNREFUSED errors when DNS servers
6+
// are reachable but the query format or handling has issues.
7+
//
8+
// Background: In certain Node.js versions, SRV queries could fail with
9+
// ECONNREFUSED even when the DNS server was accessible, affecting
10+
// applications using MongoDB Atlas (mongodb+srv://) and other services
11+
// that rely on SRV record discovery.
12+
13+
const common = require('../common');
14+
const dnstools = require('../common/dns');
15+
const dns = require('dns');
16+
const dnsPromises = dns.promises;
17+
const assert = require('assert');
18+
const dgram = require('dgram');
19+
20+
// Test 1: Basic SRV resolution should succeed, not return ECONNREFUSED
21+
{
22+
const server = dgram.createSocket('udp4');
23+
const srvRecord = {
24+
type: 'SRV',
25+
name: 'mongodb-server.cluster0.example.net',
26+
port: 27017,
27+
priority: 0,
28+
weight: 1,
29+
ttl: 60,
30+
};
31+
32+
server.on('message', common.mustCall((msg, { address, port }) => {
33+
const parsed = dnstools.parseDNSPacket(msg);
34+
const domain = parsed.questions[0].domain;
35+
36+
server.send(dnstools.writeDNSPacket({
37+
id: parsed.id,
38+
questions: parsed.questions,
39+
answers: [Object.assign({ domain }, srvRecord)],
40+
}), port, address);
41+
}));
42+
43+
server.bind(0, common.mustCall(async () => {
44+
const { port } = server.address();
45+
const resolver = new dnsPromises.Resolver();
46+
resolver.setServers([`127.0.0.1:${port}`]);
47+
48+
try {
49+
const result = await resolver.resolveSrv(
50+
'_mongodb._tcp.cluster0.example.net'
51+
);
52+
53+
// Should NOT throw ECONNREFUSED
54+
assert.ok(Array.isArray(result));
55+
assert.strictEqual(result.length, 1);
56+
assert.strictEqual(result[0].name, 'mongodb-server.cluster0.example.net');
57+
assert.strictEqual(result[0].port, 27017);
58+
assert.strictEqual(result[0].priority, 0);
59+
assert.strictEqual(result[0].weight, 1);
60+
} catch (err) {
61+
// This is the regression: should NOT get ECONNREFUSED
62+
assert.notStrictEqual(err.code, 'ECONNREFUSED');
63+
throw err;
64+
} finally {
65+
server.close();
66+
}
67+
}));
68+
}
69+
70+
// Test 2: Multiple SRV records (common for MongoDB Atlas clusters)
71+
{
72+
const server = dgram.createSocket('udp4');
73+
const srvRecords = [
74+
{ type: 'SRV', name: 'shard-00-00.cluster.mongodb.net', port: 27017, priority: 0, weight: 1, ttl: 60 },
75+
{ type: 'SRV', name: 'shard-00-01.cluster.mongodb.net', port: 27017, priority: 0, weight: 1, ttl: 60 },
76+
{ type: 'SRV', name: 'shard-00-02.cluster.mongodb.net', port: 27017, priority: 0, weight: 1, ttl: 60 },
77+
];
78+
79+
server.on('message', common.mustCall((msg, { address, port }) => {
80+
const parsed = dnstools.parseDNSPacket(msg);
81+
const domain = parsed.questions[0].domain;
82+
83+
server.send(dnstools.writeDNSPacket({
84+
id: parsed.id,
85+
questions: parsed.questions,
86+
answers: srvRecords.map((r) => Object.assign({ domain }, r)),
87+
}), port, address);
88+
}));
89+
90+
server.bind(0, common.mustCall(async () => {
91+
const { port } = server.address();
92+
const resolver = new dnsPromises.Resolver();
93+
resolver.setServers([`127.0.0.1:${port}`]);
94+
95+
const result = await resolver.resolveSrv('_mongodb._tcp.cluster.mongodb.net');
96+
97+
assert.strictEqual(result.length, 3);
98+
99+
const names = result.map((r) => r.name).sort();
100+
assert.deepStrictEqual(names, [
101+
'shard-00-00.cluster.mongodb.net',
102+
'shard-00-01.cluster.mongodb.net',
103+
'shard-00-02.cluster.mongodb.net',
104+
]);
105+
106+
server.close();
107+
}));
108+
}
109+
110+
// Test 3: Callback-based API should also work
111+
{
112+
const server = dgram.createSocket('udp4');
113+
114+
server.on('message', common.mustCall((msg, { address, port }) => {
115+
const parsed = dnstools.parseDNSPacket(msg);
116+
const domain = parsed.questions[0].domain;
117+
118+
server.send(dnstools.writeDNSPacket({
119+
id: parsed.id,
120+
questions: parsed.questions,
121+
answers: [{
122+
domain,
123+
type: 'SRV',
124+
name: 'service.example.com',
125+
port: 443,
126+
priority: 10,
127+
weight: 5,
128+
ttl: 120,
129+
}],
130+
}), port, address);
131+
}));
132+
133+
server.bind(0, common.mustCall(() => {
134+
const { port } = server.address();
135+
const resolver = new dns.Resolver();
136+
resolver.setServers([`127.0.0.1:${port}`]);
137+
138+
resolver.resolveSrv('_https._tcp.example.com', common.mustSucceed((result) => {
139+
assert.strictEqual(result.length, 1);
140+
assert.strictEqual(result[0].name, 'service.example.com');
141+
assert.strictEqual(result[0].port, 443);
142+
assert.strictEqual(result[0].priority, 10);
143+
assert.strictEqual(result[0].weight, 5);
144+
server.close();
145+
}));
146+
}));
147+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use strict';
2+
// Regression test for dns.resolveSrv() functionality.
3+
// This test ensures SRV record resolution works correctly, which is
4+
// critical for services like MongoDB Atlas that use SRV records for
5+
// connection discovery (mongodb+srv:// URIs).
6+
//
7+
// Related issue: dns.resolveSrv() returning ECONNREFUSED instead of
8+
// properly resolving SRV records.
9+
10+
const common = require('../common');
11+
const dnstools = require('../common/dns');
12+
const dns = require('dns');
13+
const dnsPromises = dns.promises;
14+
const assert = require('assert');
15+
const dgram = require('dgram');
16+
17+
const srvRecords = [
18+
{
19+
type: 'SRV',
20+
name: 'server1.example.org',
21+
port: 27017,
22+
priority: 0,
23+
weight: 5,
24+
ttl: 300,
25+
},
26+
{
27+
type: 'SRV',
28+
name: 'server2.example.org',
29+
port: 27017,
30+
priority: 0,
31+
weight: 5,
32+
ttl: 300,
33+
},
34+
{
35+
type: 'SRV',
36+
name: 'server3.example.org',
37+
port: 27017,
38+
priority: 1,
39+
weight: 10,
40+
ttl: 300,
41+
},
42+
];
43+
44+
const server = dgram.createSocket('udp4');
45+
46+
server.on('message', common.mustCall((msg, { address, port }) => {
47+
const parsed = dnstools.parseDNSPacket(msg);
48+
const domain = parsed.questions[0].domain;
49+
assert.strictEqual(domain, '_mongodb._tcp.cluster0.example.org');
50+
51+
server.send(dnstools.writeDNSPacket({
52+
id: parsed.id,
53+
questions: parsed.questions,
54+
answers: srvRecords.map((record) => Object.assign({ domain }, record)),
55+
}), port, address);
56+
}, 2)); // Called twice: once for callback, once for promises
57+
58+
server.bind(0, common.mustCall(async () => {
59+
const address = server.address();
60+
const resolver = new dns.Resolver();
61+
const resolverPromises = new dnsPromises.Resolver();
62+
63+
resolver.setServers([`127.0.0.1:${address.port}`]);
64+
resolverPromises.setServers([`127.0.0.1:${address.port}`]);
65+
66+
function validateResult(result) {
67+
assert.ok(Array.isArray(result), 'Result should be an array');
68+
assert.strictEqual(result.length, 3);
69+
70+
for (const record of result) {
71+
assert.strictEqual(typeof record, 'object');
72+
assert.strictEqual(typeof record.name, 'string');
73+
assert.strictEqual(typeof record.port, 'number');
74+
assert.strictEqual(typeof record.priority, 'number');
75+
assert.strictEqual(typeof record.weight, 'number');
76+
assert.strictEqual(record.port, 27017);
77+
}
78+
79+
// Verify we got all expected server names
80+
const names = result.map((r) => r.name).sort();
81+
assert.deepStrictEqual(names, [
82+
'server1.example.org',
83+
'server2.example.org',
84+
'server3.example.org',
85+
]);
86+
}
87+
88+
// Test promises API
89+
const promiseResult = await resolverPromises.resolveSrv(
90+
'_mongodb._tcp.cluster0.example.org'
91+
);
92+
validateResult(promiseResult);
93+
94+
// Test callback API
95+
resolver.resolveSrv(
96+
'_mongodb._tcp.cluster0.example.org',
97+
common.mustSucceed((result) => {
98+
validateResult(result);
99+
server.close();
100+
})
101+
);
102+
}));

0 commit comments

Comments
 (0)