Skip to content

Commit 2dc99d2

Browse files
NotVivek12marco-ippolito
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 6dc2a99 commit 2dc99d2

4 files changed

Lines changed: 281 additions & 6 deletions

File tree

‎src/cares_wrap.cc‎

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

797797
ares_get_servers_ports(channel_, &servers);
798798

799-
/* if no server or multi-servers, ignore */
799+
/* if no server, ignore */
800800
if (servers == nullptr) return;
801+
802+
/* if multi-servers, mark as non-default and ignore */
801803
if (servers->next != nullptr) {
802804
ares_free_data(servers);
803805
is_servers_default_ = false;
804806
return;
805807
}
806808

807-
/* if the only server is not 127.0.0.1, ignore */
808-
if (servers[0].family != AF_INET ||
809-
servers[0].addr.addr4.s_addr != htonl(INADDR_LOOPBACK) ||
810-
servers[0].tcp_port != 0 ||
811-
servers[0].udp_port != 0) {
809+
/* Check if the only server is a loopback address (IPv4 127.0.0.1 or IPv6
810+
* ::1). Newer c-ares versions may set tcp_port/udp_port to 53 instead of 0,
811+
* so we no longer check port values. */
812+
bool is_loopback = false;
813+
if (servers[0].family == AF_INET) {
814+
is_loopback = (servers[0].addr.addr4.s_addr == htonl(INADDR_LOOPBACK));
815+
} elseif (servers[0].family == AF_INET6) {
816+
staticconstunsignedcharkIPv6Loopback[16] = {
817+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1};
818+
is_loopback =
819+
(memcmp(&servers[0].addr.addr6, kIPv6Loopback, sizeof(kIPv6Loopback)) ==
820+
0);
821+
}
822+
823+
if (!is_loopback) {
812824
ares_free_data(servers);
813825
is_servers_default_ = false;
814826
return;
@@ -1413,6 +1425,10 @@ static void Query(const FunctionCallbackInfo<Value>& args) {
14131425
node::Utf8Value utf8name(env->isolate(), string);
14141426
auto plain_name = utf8name.ToStringView();
14151427
std::string name = ada::idna::to_ascii(plain_name);
1428+
1429+
// Ensure c-ares did not fall back to loopback resolver.
1430+
channel->EnsureServers();
1431+
14161432
channel->ModifyActivityQueryCount(1);
14171433
int err = wrap->Send(name.c_str());
14181434
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+
consttarget=writeDomainName(rr.name);
287+
rdLengthBuf[0]=6+target.length;
288+
buffers.push(newUint16Array([rr.priority,rr.weight,rr.port]));
289+
buffers.push(target);
290+
break;
291+
}
282292
default:
283293
thrownewError(`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+
constcommon=require('../common');
14+
constdnstools=require('../common/dns');
15+
constdns=require('dns');
16+
constdnsPromises=dns.promises;
17+
constassert=require('assert');
18+
constdgram=require('dgram');
19+
20+
// Test 1: Basic SRV resolution should succeed, not return ECONNREFUSED
21+
{
22+
constserver=dgram.createSocket('udp4');
23+
constsrvRecord={
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+
constparsed=dnstools.parseDNSPacket(msg);
34+
constdomain=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+
constresolver=newdnsPromises.Resolver();
46+
resolver.setServers([`127.0.0.1:${port}`]);
47+
48+
try{
49+
constresult=awaitresolver.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+
throwerr;
64+
}finally{
65+
server.close();
66+
}
67+
}));
68+
}
69+
70+
// Test 2: Multiple SRV records (common for MongoDB Atlas clusters)
71+
{
72+
constserver=dgram.createSocket('udp4');
73+
constsrvRecords=[
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+
constparsed=dnstools.parseDNSPacket(msg);
81+
constdomain=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+
constresolver=newdnsPromises.Resolver();
93+
resolver.setServers([`127.0.0.1:${port}`]);
94+
95+
constresult=awaitresolver.resolveSrv('_mongodb._tcp.cluster.mongodb.net');
96+
97+
assert.strictEqual(result.length,3);
98+
99+
constnames=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+
constserver=dgram.createSocket('udp4');
113+
114+
server.on('message',common.mustCall((msg,{ address, port })=>{
115+
constparsed=dnstools.parseDNSPacket(msg);
116+
constdomain=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+
constresolver=newdns.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+
constcommon=require('../common');
11+
constdnstools=require('../common/dns');
12+
constdns=require('dns');
13+
constdnsPromises=dns.promises;
14+
constassert=require('assert');
15+
constdgram=require('dgram');
16+
17+
constsrvRecords=[
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+
constserver=dgram.createSocket('udp4');
45+
46+
server.on('message',common.mustCall((msg,{ address, port })=>{
47+
constparsed=dnstools.parseDNSPacket(msg);
48+
constdomain=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+
constaddress=server.address();
60+
constresolver=newdns.Resolver();
61+
constresolverPromises=newdnsPromises.Resolver();
62+
63+
resolver.setServers([`127.0.0.1:${address.port}`]);
64+
resolverPromises.setServers([`127.0.0.1:${address.port}`]);
65+
66+
functionvalidateResult(result){
67+
assert.ok(Array.isArray(result),'Result should be an array');
68+
assert.strictEqual(result.length,3);
69+
70+
for(constrecordofresult){
71+
assert.strictEqual(typeofrecord,'object');
72+
assert.strictEqual(typeofrecord.name,'string');
73+
assert.strictEqual(typeofrecord.port,'number');
74+
assert.strictEqual(typeofrecord.priority,'number');
75+
assert.strictEqual(typeofrecord.weight,'number');
76+
assert.strictEqual(record.port,27017);
77+
}
78+
79+
// Verify we got all expected server names
80+
constnames=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+
constpromiseResult=awaitresolverPromises.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)