mirror of
https://github.com/nodejs/node.git
synced 2026-10-14 10:26:26 -04:00
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: https://github.com/nodejs/node/pull/61453 Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
1 parent
bd8fe4fa71
commit
8c323899ff
4 files changed
+281
-6
No files matched your search
+22
-6
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,6 +13,7 @@ const types = {
|
||||
PTR: 12,
|
||||
MX: 15,
|
||||
TXT: 16,
|
||||
SRV: 33,
|
||||
ANY: 255,
|
||||
CAA: 257,
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
@@ -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();
|
||||
})
|
||||
);
|
||||
}));
|
||||
Reference in new issue
Block a user