Skip to content
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 22 additions & 6 deletions src/cares_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -983,19 +983,31 @@ void ChannelWrap::EnsureServers() {

ares_get_servers_ports(channel_, &servers);

/* if no server or multi-servers, ignore */
/* if no server, ignore */
if (servers == nullptr) return;

/* if multi-servers, mark as non-default and ignore */
if (servers->next != nullptr) {
ares_free_data(servers);
is_servers_default_ = false;
return;
}

/* if the only server is not 127.0.0.1, ignore */
if (servers[0].family != AF_INET ||
servers[0].addr.addr4.s_addr != htonl(INADDR_LOOPBACK) ||
servers[0].tcp_port != 0 ||
servers[0].udp_port != 0) {
/* Check if the only server is a loopback address (IPv4 127.0.0.1 or IPv6
* ::1). Newer c-ares versions may set tcp_port/udp_port to 53 instead of 0,
* so we no longer check port values. */
bool is_loopback = false;
if (servers[0].family == AF_INET) {
is_loopback = (servers[0].addr.addr4.s_addr == htonl(INADDR_LOOPBACK));
} else if (servers[0].family == AF_INET6) {
static const unsigned char kIPv6Loopback[16] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1};
is_loopback =
(memcmp(&servers[0].addr.addr6, kIPv6Loopback, sizeof(kIPv6Loopback)) ==
0);
}

if (!is_loopback) {
ares_free_data(servers);
is_servers_default_ = false;
return;
Expand Down Expand Up @@ -1769,6 +1781,10 @@ static void Query(const FunctionCallbackInfo<Value>& args) {
node::Utf8Value utf8name(args.GetIsolate(), string);
auto plain_name = utf8name.ToStringView();
std::string name = ada::idna::to_ascii(plain_name);

// Ensure c-ares did not fall back to loopback resolver.
channel->EnsureServers();

channel->ModifyActivityQueryCount(1);
int err = wrap->Send(name.c_str());
if (err) {
Expand Down
10 changes: 10 additions & 0 deletions test/common/dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const types = {
PTR: 12,
MX: 15,
TXT: 16,
SRV: 33,
ANY: 255,
CAA: 257,
};
Expand Down Expand Up @@ -279,6 +280,15 @@ function writeDNSPacket(parsed) {
buffers.push(Buffer.from('issue' + rr.issue));
break;
}
case 'SRV':
{
// SRV record format: priority (2) + weight (2) + port (2) + target
const target = writeDomainName(rr.name);
rdLengthBuf[0] = 6 + target.length;
buffers.push(new Uint16Array([rr.priority, rr.weight, rr.port]));
buffers.push(target);
break;
}
default:
throw new Error(`Unknown RR type ${rr.type}`);
}
Expand Down
147 changes: 147 additions & 0 deletions test/parallel/test-dns-resolvesrv-econnrefused.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
'use strict';
// Regression test for SRV record resolution returning ECONNREFUSED.
//
// This test verifies that dns.resolveSrv() properly handles SRV queries
// and doesn't incorrectly return ECONNREFUSED errors when DNS servers
// are reachable but the query format or handling has issues.
//
// Background: In certain Node.js versions, SRV queries could fail with
// ECONNREFUSED even when the DNS server was accessible, affecting
// applications using MongoDB Atlas (mongodb+srv://) and other services
// that rely on SRV record discovery.

const common = require('../common');
const dnstools = require('../common/dns');
const dns = require('dns');
const dnsPromises = dns.promises;
const assert = require('assert');
const dgram = require('dgram');

// Test 1: Basic SRV resolution should succeed, not return ECONNREFUSED
{
const server = dgram.createSocket('udp4');
const srvRecord = {
type: 'SRV',
name: 'mongodb-server.cluster0.example.net',
port: 27017,
priority: 0,
weight: 1,
ttl: 60,
};

server.on('message', common.mustCall((msg, { address, port }) => {
const parsed = dnstools.parseDNSPacket(msg);
const domain = parsed.questions[0].domain;

server.send(dnstools.writeDNSPacket({
id: parsed.id,
questions: parsed.questions,
answers: [Object.assign({ domain }, srvRecord)],
}), port, address);
}));

server.bind(0, common.mustCall(async () => {
const { port } = server.address();
const resolver = new dnsPromises.Resolver();
resolver.setServers([`127.0.0.1:${port}`]);

try {
const result = await resolver.resolveSrv(
'_mongodb._tcp.cluster0.example.net'
);

// Should NOT throw ECONNREFUSED
assert.ok(Array.isArray(result));
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].name, 'mongodb-server.cluster0.example.net');
assert.strictEqual(result[0].port, 27017);
assert.strictEqual(result[0].priority, 0);
assert.strictEqual(result[0].weight, 1);
} catch (err) {
// This is the regression: should NOT get ECONNREFUSED
assert.notStrictEqual(err.code, 'ECONNREFUSED');
throw err;
} finally {
server.close();
}
}));
}

// Test 2: Multiple SRV records (common for MongoDB Atlas clusters)
{
const server = dgram.createSocket('udp4');
const srvRecords = [
{ type: 'SRV', name: 'shard-00-00.cluster.mongodb.net', port: 27017, priority: 0, weight: 1, ttl: 60 },
{ type: 'SRV', name: 'shard-00-01.cluster.mongodb.net', port: 27017, priority: 0, weight: 1, ttl: 60 },
{ type: 'SRV', name: 'shard-00-02.cluster.mongodb.net', port: 27017, priority: 0, weight: 1, ttl: 60 },
];

server.on('message', common.mustCall((msg, { address, port }) => {
const parsed = dnstools.parseDNSPacket(msg);
const domain = parsed.questions[0].domain;

server.send(dnstools.writeDNSPacket({
id: parsed.id,
questions: parsed.questions,
answers: srvRecords.map((r) => Object.assign({ domain }, r)),
}), port, address);
}));

server.bind(0, common.mustCall(async () => {
const { port } = server.address();
const resolver = new dnsPromises.Resolver();
resolver.setServers([`127.0.0.1:${port}`]);

const result = await resolver.resolveSrv('_mongodb._tcp.cluster.mongodb.net');

assert.strictEqual(result.length, 3);

const names = result.map((r) => r.name).sort();
assert.deepStrictEqual(names, [
'shard-00-00.cluster.mongodb.net',
'shard-00-01.cluster.mongodb.net',
'shard-00-02.cluster.mongodb.net',
]);

server.close();
}));
}

// Test 3: Callback-based API should also work
{
const server = dgram.createSocket('udp4');

server.on('message', common.mustCall((msg, { address, port }) => {
const parsed = dnstools.parseDNSPacket(msg);
const domain = parsed.questions[0].domain;

server.send(dnstools.writeDNSPacket({
id: parsed.id,
questions: parsed.questions,
answers: [{
domain,
type: 'SRV',
name: 'service.example.com',
port: 443,
priority: 10,
weight: 5,
ttl: 120,
}],
}), port, address);
}));

server.bind(0, common.mustCall(() => {
const { port } = server.address();
const resolver = new dns.Resolver();
resolver.setServers([`127.0.0.1:${port}`]);

resolver.resolveSrv('_https._tcp.example.com', common.mustSucceed((result) => {
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].name, 'service.example.com');
assert.strictEqual(result[0].port, 443);
assert.strictEqual(result[0].priority, 10);
assert.strictEqual(result[0].weight, 5);
server.close();
}));
}));
}
102 changes: 102 additions & 0 deletions test/parallel/test-dns-resolvesrv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';
// Regression test for dns.resolveSrv() functionality.
// This test ensures SRV record resolution works correctly, which is
// critical for services like MongoDB Atlas that use SRV records for
// connection discovery (mongodb+srv:// URIs).
//
// Related issue: dns.resolveSrv() returning ECONNREFUSED instead of
// properly resolving SRV records.

const common = require('../common');
const dnstools = require('../common/dns');
const dns = require('dns');
const dnsPromises = dns.promises;
const assert = require('assert');
const dgram = require('dgram');

const srvRecords = [
{
type: 'SRV',
name: 'server1.example.org',
port: 27017,
priority: 0,
weight: 5,
ttl: 300,
},
{
type: 'SRV',
name: 'server2.example.org',
port: 27017,
priority: 0,
weight: 5,
ttl: 300,
},
{
type: 'SRV',
name: 'server3.example.org',
port: 27017,
priority: 1,
weight: 10,
ttl: 300,
},
];

const server = dgram.createSocket('udp4');

server.on('message', common.mustCall((msg, { address, port }) => {
const parsed = dnstools.parseDNSPacket(msg);
const domain = parsed.questions[0].domain;
assert.strictEqual(domain, '_mongodb._tcp.cluster0.example.org');

server.send(dnstools.writeDNSPacket({
id: parsed.id,
questions: parsed.questions,
answers: srvRecords.map((record) => Object.assign({ domain }, record)),
}), port, address);
}, 2)); // Called twice: once for callback, once for promises

server.bind(0, common.mustCall(async () => {
const address = server.address();
const resolver = new dns.Resolver();
const resolverPromises = new dnsPromises.Resolver();

resolver.setServers([`127.0.0.1:${address.port}`]);
resolverPromises.setServers([`127.0.0.1:${address.port}`]);

function validateResult(result) {
assert.ok(Array.isArray(result), 'Result should be an array');
assert.strictEqual(result.length, 3);

for (const record of result) {
assert.strictEqual(typeof record, 'object');
assert.strictEqual(typeof record.name, 'string');
assert.strictEqual(typeof record.port, 'number');
assert.strictEqual(typeof record.priority, 'number');
assert.strictEqual(typeof record.weight, 'number');
assert.strictEqual(record.port, 27017);
}

// Verify we got all expected server names
const names = result.map((r) => r.name).sort();
assert.deepStrictEqual(names, [
'server1.example.org',
'server2.example.org',
'server3.example.org',
]);
}

// Test promises API
const promiseResult = await resolverPromises.resolveSrv(
'_mongodb._tcp.cluster0.example.org'
);
validateResult(promiseResult);

// Test callback API
resolver.resolveSrv(
'_mongodb._tcp.cluster0.example.org',
common.mustSucceed((result) => {
validateResult(result);
server.close();
})
);
}));