Skip to content

Commit

Permalink
fix: fixed localhost and related lookups (closes #4)
Browse files Browse the repository at this point in the history
  • Loading branch information
titanism committed Mar 9, 2023
1 parent bdad0fe commit af04326
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 43 deletions.
181 changes: 142 additions & 39 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ const { debuglog } = require('node:util');
const { getEventListeners, setMaxListeners } = require('node:events');
const { isIP, isIPv4, isIPv6 } = require('node:net');

const { toASCII } = require('punycode/');

const autoBind = require('auto-bind');
const getStream = require('get-stream');
const hostile = require('hostile');
const ipaddr = require('ipaddr.js');
const mergeOptions = require('merge-options');
const pMap = require('p-map');
Expand All @@ -17,14 +20,20 @@ const pWaitFor = require('p-wait-for');
const packet = require('dns-packet');
const semver = require('semver');
const structuredClone = require('@ungap/structured-clone').default;
const { Hosts } = require('hosts-parser');
const { getService } = require('port-numbers');
// eslint-disable-next-line import/order
const { toASCII } = require('punycode/');

const pkg = require('./package.json');

const debug = debuglog('tangerine');

const hosts = new Hosts(
hostile
.get()
.map((arr) => arr.join(' '))
.join('\n')
);

// dynamically import dohdec
let dohdec;
// eslint-disable-next-line unicorn/prefer-top-level-await
Expand Down Expand Up @@ -148,7 +157,8 @@ class Tangerine extends dns.promises.Resolver {
dns.NOTINITIALIZED,
dns.REFUSED,
dns.SERVFAIL,
dns.TIMEOUT
dns.TIMEOUT,
'EINVAL'
]);

static DNS_TYPES = new Set([
Expand Down Expand Up @@ -639,50 +649,124 @@ class Tangerine extends dns.promises.Resolver {
}
}

// <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares_getaddrinfo.c#L407>
// <https://www.rfc-editor.org/rfc/rfc6761.html#section-6.3>
//
// > 'localhost and any domains falling within .localhost'
//
// if no system loopback match, then revert to the default
// <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares__addrinfo_localhost.c#L224-L229>
// - IPv4 = '127.0.0.1"
// - IPv6 = "::1"
//
let resolve4;
let resolve6;

// sorted in reverse to match behavior of lookup
for (const rule of hosts._origin.reverse()) {
if (
rule.hostname.toLowerCase() !== name.toLowerCase() &&
rule.ip !== name
)
continue;
const type = isIP(rule.ip);
if (!resolve4 && type === 4) resolve4 = [rule.ip];
else if (!resolve6 && type === 6) resolve6 = [rule.ip];
if (resolve4 && resolve6) break;
}

// if no matches found for resolve4 and resolve6 and it was localhost
// (this is a safeguard in case host file is missing these)
if (
name.toLowerCase() === 'localhost' ||
name.toLowerCase() === 'localhost.'
) {
if (!resolve4) resolve4 = ['127.0.0.1'];
if (!resolve6) resolve6 = ['::1'];
}

if (isIPv4(name)) {
resolve4 = [name];
resolve6 = [];
} else if (isIPv6(name)) {
resolve6 = [name];
resolve4 = [];
}

// resolve the first A or AAAA record (conditionally)
const results = await Promise.all(
[
Array.isArray(resolve4)
? Promise.resolve(resolve4)
: this.resolve4(name, { purgeCache, noThrowOnNODATA: true }),
Array.isArray(resolve6)
? Promise.resolve(resolve6)
: this.resolve6(name, { purgeCache, noThrowOnNODATA: true })
].map((p) => p.catch((err) => err))
);

const errors = [];
let answers = [];

try {
answers = await Promise.all([
this.resolve4(name, { purgeCache, noThrowOnNODATA: true }),
this.resolve6(name, { purgeCache, noThrowOnNODATA: true })
]);
// default node behavior seems to return IPv4 by default always regardless
answers =
answers[0].length > 0 &&
(typeof options.family === 'undefined' || options.family === 0)
? answers[0]
: answers.flat();
} catch (_err) {
debug(_err);
for (const result of results) {
if (result instanceof Error) {
errors.push(result);
} else {
answers.push(result);
}
}

// this will most likely be instanceof AggregateError
if (_err instanceof AggregateError) {
const err = this.constructor.combineErrors(_err.errors);
if (
answers.length === 0 &&
errors.length > 0 &&
errors.every((e) => e.code === errors[0].code)
) {
const err = this.constructor.createError(
name,
'',
errors[0].code === dns.BADNAME ? dns.NOTFOUND : errors[0].code
);
// remap and perform syscall
err.syscall = 'getaddrinfo';
err.message = err.message.replace('query', 'getaddrinfo');
err.errno = -3008;
throw err;
}

/*
//
// NOTE: we probably should handle this differently (?)
// (not sure what native nodejs dns module does for different errors - haven't checked yet)
//
if (errors.every((e) => e.code !== 'ENODATA')) {
const err = this.constructor.combineErrors(errors);
err.hostname = name;
// remap and perform syscall
err.syscall = 'getaddrinfo';
err.message = err.message.replace('query', 'getaddrinfo');
if (!err.code)
err.code = _err.errors.find((e) => e.code)?.code || dns.BADRESP;
err.code = errors.find((e) => e.code)?.code || dns.BADRESP;
if (!err.errno)
err.errno = _err.errors.find((e) => e.errno)?.errno || undefined;

err.errno = errors.find((e) => e.errno)?.errno || undefined;
throw err;
}
*/

const err = this.constructor.createError(name, '', _err.code, _err.errno);
// remap and perform syscall
err.syscall = 'getaddrinfo';
err.error = _err;
throw err;
}
// default node behavior seems to return IPv4 by default always regardless
if (answers.length > 0)
answers =
answers[0].length > 0 &&
(typeof options.family === 'undefined' || options.family === 0)
? answers[0]
: answers.flat();

// if no results then throw ENODATA
if (answers.length === 0) {
const err = this.constructor.createError(name, '', dns.NODATA);
// remap and perform syscall
err.syscall = 'getaddrinfo';
// err.errno = -3008;
err.message = err.message.replace('query', 'getaddrinfo');
err.errno = -3008;
throw err;
}

Expand Down Expand Up @@ -835,15 +919,20 @@ class Tangerine extends dns.promises.Resolver {
}

if (!isIP(ip)) {
const err = this.constructor.createError(ip, '', dns.EINVAL);
const err = this.constructor.createError(ip, '', 'EINVAL');
err.message = `getHostByAddr EINVAL ${err.hostname}`;
err.syscall = 'getHostByAddr';
// err.errno = -22;
err.errno = -22;
if (!ip) delete err.hostname;
throw err;
}

// edge case where localhost IP returns empty
if (ip === '127.0.0.1' || ip === '::1') return [];

// reverse the IP address
if (!dohdec) await pWaitFor(() => Boolean(dohdec));

const name = dohdec.DNSoverHTTPS.reverse(ip);

// perform resolvePTR
Expand Down Expand Up @@ -1270,13 +1359,22 @@ class Tangerine extends dns.promises.Resolver {
);
}

const results = await pMap(
this.constructor.ANY_TYPES,
this.#resolveByType(name, options, abortController),
// <https://developers.cloudflare.com/fundamentals/api/reference/limits/>
{ concurrency: this.options.concurrency, signal: abortController.signal }
);
return results.flat().filter(Boolean);
try {
const results = await pMap(
this.constructor.ANY_TYPES,
this.#resolveByType(name, options, abortController),
// <https://developers.cloudflare.com/fundamentals/api/reference/limits/>
{
concurrency: this.options.concurrency,
signal: abortController.signal
}
);
return results.flat().filter(Boolean);
} catch (err) {
err.syscall = 'queryAny';
err.message = `queryAny ${err.code} ${name}`;
throw err;
}
}

setDefaultResultOrder(dnsOrder) {
Expand Down Expand Up @@ -1330,6 +1428,11 @@ class Tangerine extends dns.promises.Resolver {
throw err;
}

// edge case where c-ares detects "." as start of string
// <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares_getaddrinfo.c#L829>
if (name.startsWith('.') || name.includes('..'))
throw this.constructor.createError(name, rrtype, dns.BADNAME);

// purge cache support
let purgeCache;
if (options?.purgeCache) {
Expand Down Expand Up @@ -1712,7 +1815,7 @@ class Tangerine extends dns.promises.Resolver {
: obj.certificate_type.toString();
return obj;
} catch (err) {
console.error(err);
this.options.logger.error(err, { name, rrtype, options, answer });
throw err;
}
});
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"dns-packet": "^5.4.0",
"dohdec": "^5.0.3",
"get-stream": "6",
"hostile": "^1.3.3",
"hosts-parser": "^0.3.2",
"ipaddr.js": "^2.0.1",
"merge-options": "3.0.4",
"p-map": "4",
Expand Down
85 changes: 81 additions & 4 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ function compareResults(t, type, r1, r2) {
}

if (_.isError(r1) || _.isError(r2)) {
t.log(r1);
t.log(r2);
t.deepEqual(r1, r2);
} else {
// r1/r2 = [ { type: 'TXT', value: 'blah' }, ... ] }
Expand Down Expand Up @@ -222,6 +224,25 @@ function compareResults(t, type, r1, r2) {
// NOTE: need to test all options
//
for (const host of [
'localhost',
'localhost.',
'..localhost',
'localhost..',
'beep..',
'beep.com..',
'beep..com..',
'foo..com',
'..foo.com',
'..foo..com.',
'.foo.com.',
'foo..localhost',
'foo..localhost..localhost',
'.localhost',
'foo.localhost',
'foo.bar.localhost',
'::1',
'::0',
'127.0.0.1',
'forwardemail.net',
'cloudflare.com',
'stackoverflow.com',
Expand Down Expand Up @@ -265,12 +286,53 @@ for (const host of [
});
*/

// tangerine.reverse
test(`reverse("${host}")`, async (t) => {
const tangerine = new Tangerine();
const resolver = new Resolver();
if (!t.context.isBlackholed) resolver.setServers(tangerine.getServers());

let r1;
let r2;
try {
r1 = await tangerine.reverse(host);
} catch (err) {
r1 = err;
}

try {
r2 = await resolver.reverse(host);
} catch (err) {
r2 = err;
}

t.log(r1);
t.log(r2);

compareResults(t, 'reverse', r1, r2);
});

// tangerine.lookup"${host}"[, options])
test(`lookup("${host}")`, async (t) => {
// returns { address: IP , family: 4 || 6 }
const tangerine = new Tangerine();
let r1 = await tangerine.lookup(host);
let r2 = await dns.promises.lookup(host);
let r1;
let r2;
try {
r1 = await tangerine.lookup(host);
} catch (err) {
r1 = err;
}

try {
r2 = await dns.promises.lookup(host);
} catch (err) {
r2 = err;
}

t.log(r1);
t.log(r2);

if (_.isPlainObject(r1)) r1 = [r1];
if (_.isPlainObject(r2)) r2 = [r2];
if (!_.isError(r1)) r1 = r1.every((o) => isIP(o.address) === o.family);
Expand All @@ -283,8 +345,23 @@ for (const host of [
const tangerine = new Tangerine();
const resolver = new Resolver();
if (!t.context.isBlackholed) resolver.setServers(tangerine.getServers());
let r1 = await tangerine.resolve(host);
let r2 = await resolver.resolve(host);
let r1;
let r2;
try {
r1 = await tangerine.resolve(host);
} catch (err) {
r1 = err;
}

try {
r2 = await resolver.resolve(host);
} catch (err) {
r2 = err;
}

t.log(r1);
t.log(r2);

// see explanation below regarding this under "A" and "AAAA" in switch/case
if (!_.isError(r1)) r1 = r1.every((o) => isIPv4(o) || isIPv6(o));
if (!_.isError(r2)) r2 = r2.every((o) => isIPv4(o) || isIPv6(o));
Expand Down

0 comments on commit af04326

Please sign in to comment.