Skip to content

Commit

Permalink
feat: multi-host DNS resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
CMCDragonkai committed Oct 31, 2022
1 parent 854ef56 commit 965dd85
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 23 deletions.
8 changes: 4 additions & 4 deletions src/network/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ class ErrorCertChainSignatureInvalid<T> extends ErrorCertChain<T> {
exitCode = sysexits.PROTOCOL;
}

class ErrorHostnameResolutionFailed<T> extends ErrorNetwork<T> {
static description = 'Unable to resolve hostname';
exitCode = sysexits.USAGE;
class ErrorDNSResolver<T> extends ErrorNetwork<T> {
static description = 'DNS resolution failed';
exitCode = sysexits.SOFTWARE;
}

export {
Expand Down Expand Up @@ -161,5 +161,5 @@ export {
ErrorCertChainNameInvalid,
ErrorCertChainKeyInvalid,
ErrorCertChainSignatureInvalid,
ErrorHostnameResolutionFailed,
ErrorDNSResolver,
};
119 changes: 100 additions & 19 deletions src/network/utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type { Socket } from 'net';
import type { TLSSocket } from 'tls';
import type { PromiseCancellable } from '@matrixai/async-cancellable';
import type { Host, Hostname, Port, Address, NetworkMessage } from './types';
import type { Certificate, PublicKey } from '../keys/types';
import type { NodeId } from '../ids/types';
import type { ContextTimed } from '../contexts/types';
import { Buffer } from 'buffer';
import dns from 'dns';
import { IPv4, IPv6, Validator } from 'ip-num';
import timedCancellable from '../contexts/functions/timedCancellable';
import * as networkErrors from './errors';
import * as keysUtils from '../keys/utils';
import * as nodesUtils from '../nodes/utils';
import { isEmptyObject, promisify } from '../utils';
import * as utils from '../utils';

const pingBuffer = serializeNetworkMessage({
type: 'ping',
Expand Down Expand Up @@ -90,25 +93,102 @@ function parseAddress(address: string): [Host, Port] {
}

/**
* Resolves a provided hostname to its respective IP address (type Host)
* Checks if error is software error.
* These error codes would mean there's something broken with DNS.
*/
async function resolveHost(host: Host | Hostname): Promise<Host> {
// If already IPv4/IPv6 address, return it
if (isHost(host)) {
return host as Host;
}
const lookup = promisify(dns.lookup).bind(dns);
let resolvedHost;
try {
// Resolve the hostname and get the IPv4 address
resolvedHost = await lookup(host, 4);
} catch (e) {
throw new networkErrors.ErrorHostnameResolutionFailed(e.message, {
cause: e,
function isDNSError(
e: { code: string }
): boolean {
return e.code === dns.EOF ||
e.code === dns.FILE ||
e.code === dns.NOMEM ||
e.code === dns.DESTRUCTION ||
e.code === dns.BADFLAGS ||
e.code === dns.BADHINTS ||
e.code === dns.NOTINITIALIZED ||
e.code === dns.LOADIPHLPAPI ||
e.code === dns.ADDRGETNETWORKPARAMS;
}

/**
* Resolve a hostname to all IPv4 and IPv6 hosts.
* It does an iterative BFS over any CNAME records.
* This performs proper DNS lookup, it does not use the operating system's
* resolver. However the default set of DNS servers is inherited from the
* operating system configuration.
* The default time limit is practically infinity.
* This means if the DNS server doesn't respond, this function could take
* a very long time.
*/
function resolveHostname(
hostname: Hostname,
ctx?: ContextTimed
): PromiseCancellable<Array<Host>> {
const f = async (ctx: ContextTimed) => {
const hosts: Array<Host> = [];
if (ctx.signal.aborted) {
return hosts;
}
// These settings here practically ensure an infinite resolver
// The `timeout` is the timeout per DNS packet
// The default of `-1` is an exponential backoff starting at 5s
// It doubles from there
// The maximum timeout is `Math.pow(2, 31) - 1`
// The maximum number of tries is `Math.pow(2, 31) - 1`
const resolver = new dns.promises.Resolver({
timeout: -1,
tries: Math.pow(2, 31) - 1
});
// The default DNS servers are inherited from the OS
ctx.signal.addEventListener('abort', () => {
// This will trigger `dns.CANCELLED`
resolver.cancel();
});
// Breadth first search through the CNAME records
const queue = [hostname];
while (queue.length > 0) {
const target = queue.shift()!;
let cnames: Array<Hostname>;
try {
cnames = await resolver.resolveCname(target) as Array<Hostname>;
} catch (e) {
if (!isDNSError(e)) {
cnames = [];
} else {
throw new networkErrors.ErrorDNSResolver(undefined, { cause: e });
}
}
if (cnames.length > 0) {
// Usually only 1 CNAME is used
// but here we can support multiple CNAMEs
queue.push(...cnames);
} else {
let ipv4Hosts: Array<Host>;
try {
ipv4Hosts = await resolver.resolve4(hostname) as Array<Host>;
} catch (e) {
if (!isDNSError(e)) {
ipv4Hosts = [];
} else {
throw new networkErrors.ErrorDNSResolver(undefined, { cause: e });
}
}
let ipv6Hosts: Array<Host>;
try {
ipv6Hosts = await resolver.resolve6(hostname) as Array<Host>;
} catch (e) {
if (!isDNSError(e)) {
ipv6Hosts = [];
} else {
throw new networkErrors.ErrorDNSResolver(undefined, { cause: e });
}
}
hosts.push(...ipv4Hosts, ...ipv6Hosts);
}
}
return hosts;
}
// Returns an array of [ resolved address, family (4 or 6) ]
return resolvedHost[0] as Host;
return timedCancellable(f, true)(ctx);
}

/**
Expand Down Expand Up @@ -153,7 +233,7 @@ function getCertificateChain(socket: TLSSocket): Array<Certificate> {
// The order of certificates is always leaf to root
const certs: Array<Certificate> = [];
let cert_ = socket.getPeerCertificate(true);
if (isEmptyObject(cert_)) {
if (utils.isEmptyObject(cert_)) {
return certs;
}
while (true) {
Expand Down Expand Up @@ -387,7 +467,8 @@ export {
toAuthToken,
buildAddress,
parseAddress,
resolveHost,
isDNSError,
resolveHostname,
resolvesZeroIP,
serializeNetworkMessage,
unserializeNetworkMessage,
Expand Down

0 comments on commit 965dd85

Please sign in to comment.