1'use strict'; 2 3const common = require('../common'); 4const { parseDNSPacket, writeDNSPacket } = require('../common/dns'); 5 6const assert = require('assert'); 7const dgram = require('dgram'); 8const { Resolver } = require('dns'); 9const { createConnection, createServer } = require('net'); 10 11// Test that happy eyeballs algorithm is properly implemented. 12 13// Purposely not using setDefaultAutoSelectFamilyAttemptTimeout here to test the 14// parameter is correctly used in options. 15// 16// Some of the windows machines in the CI need more time to establish connection 17const autoSelectFamilyAttemptTimeout = common.platformTimeout(common.isWindows ? 1500 : 250); 18 19function _lookup(resolver, hostname, options, cb) { 20 resolver.resolve(hostname, 'ANY', (err, replies) => { 21 assert.notStrictEqual(options.family, 4); 22 23 if (err) { 24 return cb(err); 25 } 26 27 const hosts = replies 28 .map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 })) 29 .sort((a, b) => b.family - a.family); 30 31 if (options.all === true) { 32 return cb(null, hosts); 33 } 34 35 return cb(null, hosts[0].address, hosts[0].family); 36 }); 37} 38 39function createDnsServer(ipv6Addrs, ipv4Addrs, cb) { 40 if (!Array.isArray(ipv6Addrs)) { 41 ipv6Addrs = [ipv6Addrs]; 42 } 43 44 if (!Array.isArray(ipv4Addrs)) { 45 ipv4Addrs = [ipv4Addrs]; 46 } 47 48 // Create a DNS server which replies with a AAAA and a A record for the same host 49 const socket = dgram.createSocket('udp4'); 50 51 socket.on('message', common.mustCall((msg, { address, port }) => { 52 const parsed = parseDNSPacket(msg); 53 const domain = parsed.questions[0].domain; 54 assert.strictEqual(domain, 'example.org'); 55 56 socket.send(writeDNSPacket({ 57 id: parsed.id, 58 questions: parsed.questions, 59 answers: [ 60 ...ipv6Addrs.map((address) => ({ type: 'AAAA', address, ttl: 123, domain: 'example.org' })), 61 ...ipv4Addrs.map((address) => ({ type: 'A', address, ttl: 123, domain: 'example.org' })), 62 ] 63 }), port, address); 64 })); 65 66 socket.bind(0, () => { 67 const resolver = new Resolver(); 68 resolver.setServers([`127.0.0.1:${socket.address().port}`]); 69 70 cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) }); 71 }); 72} 73 74// Test that IPV4 is reached if IPV6 is not reachable 75{ 76 createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) { 77 const ipv4Server = createServer((socket) => { 78 socket.on('data', common.mustCall(() => { 79 socket.write('response-ipv4'); 80 socket.end(); 81 })); 82 }); 83 84 ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => { 85 const port = ipv4Server.address().port; 86 87 const connection = createConnection({ 88 host: 'example.org', 89 port: port, 90 lookup, 91 autoSelectFamily: true, 92 autoSelectFamilyAttemptTimeout, 93 }); 94 95 let response = ''; 96 connection.setEncoding('utf-8'); 97 98 connection.on('ready', common.mustCall(() => { 99 assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`, `127.0.0.1:${port}`]); 100 })); 101 102 connection.on('data', (chunk) => { 103 response += chunk; 104 }); 105 106 connection.on('end', common.mustCall(() => { 107 assert.strictEqual(response, 'response-ipv4'); 108 ipv4Server.close(); 109 dnsServer.close(); 110 })); 111 112 connection.write('request'); 113 })); 114 })); 115} 116 117// Test that only the last successful connection is established. 118{ 119 createDnsServer( 120 ['2606:4700::6810:85e5', '2606:4700::6810:84e5', '::1'], 121 ['104.20.22.46', '104.20.23.46', '127.0.0.1'], 122 common.mustCall(function({ dnsServer, lookup }) { 123 const ipv4Server = createServer((socket) => { 124 socket.on('data', common.mustCall(() => { 125 socket.write('response-ipv4'); 126 socket.end(); 127 })); 128 }); 129 130 ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => { 131 const port = ipv4Server.address().port; 132 133 const connection = createConnection({ 134 host: 'example.org', 135 port: port, 136 lookup, 137 autoSelectFamily: true, 138 autoSelectFamilyAttemptTimeout, 139 }); 140 141 let response = ''; 142 connection.setEncoding('utf-8'); 143 144 connection.on('ready', common.mustCall(() => { 145 assert.deepStrictEqual( 146 connection.autoSelectFamilyAttemptedAddresses, 147 [ 148 `2606:4700::6810:85e5:${port}`, 149 `104.20.22.46:${port}`, 150 `2606:4700::6810:84e5:${port}`, 151 `104.20.23.46:${port}`, 152 `::1:${port}`, 153 `127.0.0.1:${port}`, 154 ] 155 ); 156 })); 157 158 connection.on('data', (chunk) => { 159 response += chunk; 160 }); 161 162 connection.on('end', common.mustCall(() => { 163 assert.strictEqual(response, 'response-ipv4'); 164 ipv4Server.close(); 165 dnsServer.close(); 166 })); 167 168 connection.write('request'); 169 })); 170 }) 171 ); 172} 173 174// Test that IPV4 is NOT reached if IPV6 is reachable 175if (common.hasIPv6) { 176 createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) { 177 const ipv4Server = createServer((socket) => { 178 socket.on('data', common.mustNotCall(() => { 179 socket.write('response-ipv4'); 180 socket.end(); 181 })); 182 }); 183 184 const ipv6Server = createServer((socket) => { 185 socket.on('data', common.mustCall(() => { 186 socket.write('response-ipv6'); 187 socket.end(); 188 })); 189 }); 190 191 ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => { 192 const port = ipv4Server.address().port; 193 194 ipv6Server.listen(port, '::1', common.mustCall(() => { 195 const connection = createConnection({ 196 host: 'example.org', 197 port, 198 lookup, 199 autoSelectFamily: true, 200 autoSelectFamilyAttemptTimeout, 201 }); 202 203 let response = ''; 204 connection.setEncoding('utf-8'); 205 206 connection.on('ready', common.mustCall(() => { 207 assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`]); 208 })); 209 210 connection.on('data', (chunk) => { 211 response += chunk; 212 }); 213 214 connection.on('end', common.mustCall(() => { 215 assert.strictEqual(response, 'response-ipv6'); 216 ipv4Server.close(); 217 ipv6Server.close(); 218 dnsServer.close(); 219 })); 220 221 connection.write('request'); 222 })); 223 })); 224 })); 225} 226 227// Test that when all errors are returned when no connections succeeded 228{ 229 createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) { 230 const connection = createConnection({ 231 host: 'example.org', 232 port: 10, 233 lookup, 234 autoSelectFamily: true, 235 autoSelectFamilyAttemptTimeout, 236 }); 237 238 connection.on('ready', common.mustNotCall()); 239 connection.on('error', common.mustCall((error) => { 240 assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, ['::1:10', '127.0.0.1:10']); 241 assert.strictEqual(error.constructor.name, 'AggregateError'); 242 assert.strictEqual(error.errors.length, 2); 243 244 const errors = error.errors.map((e) => e.message); 245 assert.ok(errors.includes('connect ECONNREFUSED 127.0.0.1:10')); 246 247 if (common.hasIPv6) { 248 assert.ok(errors.includes('connect ECONNREFUSED ::1:10')); 249 } 250 251 dnsServer.close(); 252 })); 253 })); 254} 255 256// Test that the option can be disabled 257{ 258 createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) { 259 const ipv4Server = createServer((socket) => { 260 socket.on('data', common.mustCall(() => { 261 socket.write('response-ipv4'); 262 socket.end(); 263 })); 264 }); 265 266 ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => { 267 const port = ipv4Server.address().port; 268 269 const connection = createConnection({ 270 host: 'example.org', 271 port, 272 lookup, 273 autoSelectFamily: false, 274 }); 275 276 connection.on('ready', common.mustNotCall()); 277 connection.on('error', common.mustCall((error) => { 278 assert.strictEqual(connection.autoSelectFamilyAttemptedAddresses, undefined); 279 280 if (common.hasIPv6) { 281 assert.strictEqual(error.code, 'ECONNREFUSED'); 282 assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`); 283 } else if (error.code === 'EAFNOSUPPORT') { 284 assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`); 285 } else if (common.isIBMi) { 286 // IBMi returns EUNATCH (ERRNO 42) when IPv6 is disabled 287 // keep this errno assertion until EUNATCH is recognized by libuv 288 assert.strictEqual(error.errno, -42); 289 } else { 290 assert.strictEqual(error.code, 'EADDRNOTAVAIL'); 291 assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`); 292 } 293 294 ipv4Server.close(); 295 dnsServer.close(); 296 })); 297 })); 298 })); 299} 300