837 lines
19 KiB
JavaScript
837 lines
19 KiB
JavaScript
|
|
/*!
|
||
|
|
* dns.js - dns server for hsd
|
||
|
|
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
||
|
|
* https://github.com/handshake-org/hsd
|
||
|
|
*/
|
||
|
|
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
const assert = require('bsert');
|
||
|
|
const IP = require('binet');
|
||
|
|
const Logger = require('blgr');
|
||
|
|
const bns = require('bns');
|
||
|
|
const UnboundResolver = require('bns/lib/resolver/unbound');
|
||
|
|
const RecursiveResolver = require('bns/lib/resolver/recursive');
|
||
|
|
const RootResolver = require('bns/lib/resolver/root');
|
||
|
|
const secp256k1 = require('bcrypto/lib/secp256k1');
|
||
|
|
const LRU = require('blru');
|
||
|
|
const base32 = require('bcrypto/lib/encoding/base32');
|
||
|
|
const NameState = require('../covenants/namestate');
|
||
|
|
const rules = require('../covenants/rules');
|
||
|
|
const reserved = require('../covenants/reserved');
|
||
|
|
const {Resource} = require('./resource');
|
||
|
|
const key = require('./key');
|
||
|
|
const nsec = require('./nsec');
|
||
|
|
const {
|
||
|
|
DEFAULT_TTL,
|
||
|
|
TYPE_MAP_ROOT,
|
||
|
|
TYPE_MAP_EMPTY,
|
||
|
|
TYPE_MAP_NS,
|
||
|
|
TYPE_MAP_A,
|
||
|
|
TYPE_MAP_AAAA
|
||
|
|
} = require('./common');
|
||
|
|
|
||
|
|
const {
|
||
|
|
DNSServer,
|
||
|
|
hsig,
|
||
|
|
wire,
|
||
|
|
util
|
||
|
|
} = bns;
|
||
|
|
|
||
|
|
const {
|
||
|
|
Message,
|
||
|
|
Record,
|
||
|
|
ARecord,
|
||
|
|
AAAARecord,
|
||
|
|
NSRecord,
|
||
|
|
SOARecord,
|
||
|
|
types,
|
||
|
|
codes
|
||
|
|
} = wire;
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Constants
|
||
|
|
*/
|
||
|
|
|
||
|
|
const RES_OPT = { inet6: false, tcp: true };
|
||
|
|
const CACHE_TTL = 30 * 60 * 1000;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* RootCache
|
||
|
|
*/
|
||
|
|
|
||
|
|
class RootCache {
|
||
|
|
constructor(size) {
|
||
|
|
this.cache = new LRU(size);
|
||
|
|
}
|
||
|
|
|
||
|
|
set(name, type, msg) {
|
||
|
|
const key = toKey(name, type);
|
||
|
|
const raw = msg.compress();
|
||
|
|
|
||
|
|
this.cache.set(key, {
|
||
|
|
time: Date.now(),
|
||
|
|
raw
|
||
|
|
});
|
||
|
|
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
get(name, type) {
|
||
|
|
const key = toKey(name, type);
|
||
|
|
const item = this.cache.get(key);
|
||
|
|
|
||
|
|
if (!item)
|
||
|
|
return null;
|
||
|
|
|
||
|
|
if (Date.now() > item.time + CACHE_TTL)
|
||
|
|
return null;
|
||
|
|
|
||
|
|
return Message.decode(item.raw);
|
||
|
|
}
|
||
|
|
|
||
|
|
reset() {
|
||
|
|
this.cache.reset();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* RootServer
|
||
|
|
* @extends {DNSServer}
|
||
|
|
*/
|
||
|
|
|
||
|
|
class RootServer extends DNSServer {
|
||
|
|
constructor(options) {
|
||
|
|
super(RES_OPT);
|
||
|
|
|
||
|
|
this.ra = false;
|
||
|
|
this.edns = true;
|
||
|
|
this.dnssec = true;
|
||
|
|
this.noSig0 = false;
|
||
|
|
this.icann = new RootResolver(RES_OPT);
|
||
|
|
|
||
|
|
this.logger = Logger.global;
|
||
|
|
this.key = secp256k1.privateKeyGenerate();
|
||
|
|
this.host = '127.0.0.1';
|
||
|
|
this.port = 5300;
|
||
|
|
this.lookup = null;
|
||
|
|
this.middle = null;
|
||
|
|
this.publicHost = '127.0.0.1';
|
||
|
|
|
||
|
|
// Plugins can add or remove items from
|
||
|
|
// this set before the server is opened.
|
||
|
|
this.blacklist = new Set([
|
||
|
|
'bit', // Namecoin
|
||
|
|
'eth', // ENS
|
||
|
|
'exit', // Tor
|
||
|
|
'gnu', // GNUnet (GNS)
|
||
|
|
'i2p', // Invisible Internet Project
|
||
|
|
'onion', // Tor
|
||
|
|
'tor', // OnioNS
|
||
|
|
'zkey' // GNS
|
||
|
|
]);
|
||
|
|
|
||
|
|
this.cache = new RootCache(3000);
|
||
|
|
|
||
|
|
if (options)
|
||
|
|
this.initOptions(options);
|
||
|
|
|
||
|
|
// Create SYNTH record to use for root zone NS
|
||
|
|
let ip = IP.toBuffer(this.publicHost);
|
||
|
|
if (IP.family(this.publicHost) === 4)
|
||
|
|
ip = ip.slice(12);
|
||
|
|
this.synth = `_${base32.encodeHex(ip)}._synth.`;
|
||
|
|
|
||
|
|
this.initNode();
|
||
|
|
}
|
||
|
|
|
||
|
|
initOptions(options) {
|
||
|
|
assert(options);
|
||
|
|
|
||
|
|
this.parseOptions(options);
|
||
|
|
|
||
|
|
if (options.logger != null) {
|
||
|
|
assert(typeof options.logger === 'object');
|
||
|
|
this.logger = options.logger.context('ns');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.key != null) {
|
||
|
|
assert(Buffer.isBuffer(options.key));
|
||
|
|
assert(options.key.length === 32);
|
||
|
|
this.key = options.key;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.host != null) {
|
||
|
|
assert(typeof options.host === 'string');
|
||
|
|
this.host = IP.normalize(options.host);
|
||
|
|
this.publicHost = this.host;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.port != null) {
|
||
|
|
assert((options.port & 0xffff) === options.port);
|
||
|
|
assert(options.port !== 0);
|
||
|
|
this.port = options.port;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.lookup != null) {
|
||
|
|
assert(typeof options.lookup === 'function');
|
||
|
|
this.lookup = options.lookup;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.noSig0 != null) {
|
||
|
|
assert(typeof options.noSig0 === 'boolean');
|
||
|
|
this.noSig0 = options.noSig0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.publicHost != null) {
|
||
|
|
assert(typeof options.publicHost === 'string');
|
||
|
|
this.publicHost = IP.normalize(options.publicHost);
|
||
|
|
}
|
||
|
|
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
initNode() {
|
||
|
|
this.on('error', (err) => {
|
||
|
|
this.logger.error(err);
|
||
|
|
});
|
||
|
|
|
||
|
|
this.on('query', (req, res) => {
|
||
|
|
this.logMessage('\n\nDNS Request:', req);
|
||
|
|
this.logMessage('\n\nDNS Response:', res);
|
||
|
|
});
|
||
|
|
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
logMessage(prefix, msg) {
|
||
|
|
if (this.logger.level < 5)
|
||
|
|
return;
|
||
|
|
|
||
|
|
const logs = msg.toString().trim().split('\n');
|
||
|
|
|
||
|
|
this.logger.spam(prefix);
|
||
|
|
|
||
|
|
for (const log of logs)
|
||
|
|
this.logger.spam(log);
|
||
|
|
}
|
||
|
|
|
||
|
|
signSize() {
|
||
|
|
if (!this.sig0)
|
||
|
|
return 94;
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
sign(msg, host, port) {
|
||
|
|
if (!this.noSig0)
|
||
|
|
return hsig.sign(msg, this.key);
|
||
|
|
|
||
|
|
return msg;
|
||
|
|
}
|
||
|
|
|
||
|
|
async lookupName(name) {
|
||
|
|
if (!this.lookup)
|
||
|
|
throw new Error('Tree not available.');
|
||
|
|
|
||
|
|
const hash = rules.hashName(name);
|
||
|
|
const data = await this.lookup(hash);
|
||
|
|
|
||
|
|
if (!data)
|
||
|
|
return null;
|
||
|
|
|
||
|
|
const ns = NameState.decode(data);
|
||
|
|
|
||
|
|
if (ns.data.length === 0)
|
||
|
|
return null;
|
||
|
|
|
||
|
|
return ns.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
async response(req, rinfo) {
|
||
|
|
const [qs] = req.question;
|
||
|
|
const name = qs.name.toLowerCase();
|
||
|
|
const type = qs.type;
|
||
|
|
|
||
|
|
// Our root zone.
|
||
|
|
if (name === '.') {
|
||
|
|
const res = new Message();
|
||
|
|
|
||
|
|
res.aa = true;
|
||
|
|
|
||
|
|
switch (type) {
|
||
|
|
case types.ANY:
|
||
|
|
case types.NS:
|
||
|
|
res.answer.push(this.toNS());
|
||
|
|
key.signZSK(res.answer, types.NS);
|
||
|
|
|
||
|
|
if (IP.family(this.publicHost) === 4) {
|
||
|
|
res.additional.push(this.toA());
|
||
|
|
key.signZSK(res.additional, types.A);
|
||
|
|
} else {
|
||
|
|
res.additional.push(this.toAAAA());
|
||
|
|
key.signZSK(res.additional, types.AAAA);
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
case types.SOA:
|
||
|
|
res.answer.push(this.toSOA());
|
||
|
|
key.signZSK(res.answer, types.SOA);
|
||
|
|
|
||
|
|
res.authority.push(this.toNS());
|
||
|
|
key.signZSK(res.authority, types.NS);
|
||
|
|
|
||
|
|
if (IP.family(this.publicHost) === 4) {
|
||
|
|
res.additional.push(this.toA());
|
||
|
|
key.signZSK(res.additional, types.A);
|
||
|
|
} else {
|
||
|
|
res.additional.push(this.toAAAA());
|
||
|
|
key.signZSK(res.additional, types.AAAA);
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
case types.DNSKEY:
|
||
|
|
res.answer.push(key.ksk.deepClone());
|
||
|
|
res.answer.push(key.zsk.deepClone());
|
||
|
|
key.signKSK(res.answer, types.DNSKEY);
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
// Minimally covering NSEC proof:
|
||
|
|
res.authority.push(this.toNSEC());
|
||
|
|
key.signZSK(res.authority, types.NSEC);
|
||
|
|
res.authority.push(this.toSOA());
|
||
|
|
key.signZSK(res.authority, types.SOA);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Process the name.
|
||
|
|
const labels = util.split(name);
|
||
|
|
const tld = util.label(name, labels, -1);
|
||
|
|
|
||
|
|
// Handle reverse pointers.
|
||
|
|
if (tld === '_synth' && labels.length <= 2 && name[0] === '_') {
|
||
|
|
const res = new Message();
|
||
|
|
const rr = new Record();
|
||
|
|
|
||
|
|
res.aa = true;
|
||
|
|
rr.name = name;
|
||
|
|
rr.ttl = 21600;
|
||
|
|
|
||
|
|
// TLD '._synth' is being queried on its own, send SOA
|
||
|
|
// so recursive asks again with complete synth record.
|
||
|
|
if (labels.length === 1) {
|
||
|
|
// Empty non-terminal proof:
|
||
|
|
res.authority.push(
|
||
|
|
nsec.create(
|
||
|
|
'_synth.',
|
||
|
|
'\\000._synth.',
|
||
|
|
TYPE_MAP_EMPTY
|
||
|
|
)
|
||
|
|
);
|
||
|
|
key.signZSK(res.authority, types.NSEC);
|
||
|
|
|
||
|
|
res.authority.push(this.toSOA());
|
||
|
|
key.signZSK(res.authority, types.SOA);
|
||
|
|
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
|
||
|
|
const hash = util.label(name, labels, -2);
|
||
|
|
const ip = IP.map(base32.decodeHex(hash.substring(1)));
|
||
|
|
const synthType = IP.isIPv4(ip) ? types.A : types.AAAA;
|
||
|
|
|
||
|
|
// Query must be for the correct synth version
|
||
|
|
if (type !== synthType) {
|
||
|
|
// SYNTH4/6 proof:
|
||
|
|
const typeMap = synthType === types.A ? TYPE_MAP_A : TYPE_MAP_AAAA;
|
||
|
|
res.authority.push(nsec.create(name, '\\000.' + name, typeMap));
|
||
|
|
key.signZSK(res.authority, types.NSEC);
|
||
|
|
|
||
|
|
res.authority.push(this.toSOA());
|
||
|
|
key.signZSK(res.authority, types.SOA);
|
||
|
|
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (synthType === types.A) {
|
||
|
|
rr.type = types.A;
|
||
|
|
rr.data = new ARecord();
|
||
|
|
} else {
|
||
|
|
rr.type = types.AAAA;
|
||
|
|
rr.data = new AAAARecord();
|
||
|
|
}
|
||
|
|
|
||
|
|
rr.data.address = IP.toString(ip);
|
||
|
|
|
||
|
|
res.answer.push(rr);
|
||
|
|
key.signZSK(res.answer, rr.type);
|
||
|
|
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
|
||
|
|
// REFUSED for invalid names
|
||
|
|
// this simplifies NSEC proofs
|
||
|
|
// by avoiding octets like \000
|
||
|
|
// Also, this decreases load on
|
||
|
|
// the server since it avoids signing
|
||
|
|
// useless proofs for invalid TLDs
|
||
|
|
// (These requests are most
|
||
|
|
// likely bad anyways)
|
||
|
|
if (!rules.verifyName(tld)) {
|
||
|
|
const res = new Message();
|
||
|
|
res.code = codes.REFUSED;
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ask the urkel tree for the name data.
|
||
|
|
const data = !this.blacklist.has(tld)
|
||
|
|
? (await this.lookupName(tld))
|
||
|
|
: null;
|
||
|
|
|
||
|
|
// Non-existent domain.
|
||
|
|
if (!data) {
|
||
|
|
const item = this.getReserved(tld);
|
||
|
|
|
||
|
|
// This name is in the existing root zone.
|
||
|
|
// Fall back to ICANN's servers if not yet
|
||
|
|
// registered on the handshake blockchain.
|
||
|
|
// This is an example of "Dynamic Fallback"
|
||
|
|
// as mentioned in the whitepaper.
|
||
|
|
if (item && item.root) {
|
||
|
|
const res = await this.icann.lookup(tld);
|
||
|
|
|
||
|
|
if (res.ad && res.code !== codes.NXDOMAIN) {
|
||
|
|
// answer must be a referral since lookup
|
||
|
|
// function always asks for NS
|
||
|
|
assert(res.code === codes.NOERROR);
|
||
|
|
assert(res.answer.length === 0);
|
||
|
|
assert(hasValidOwner(res.authority, tld));
|
||
|
|
|
||
|
|
res.ad = false;
|
||
|
|
res.question = [qs];
|
||
|
|
const secure = util.hasType(res.authority, types.DS);
|
||
|
|
|
||
|
|
// no DS referrals for TLDs
|
||
|
|
if (type === types.DS && labels.length === 1) {
|
||
|
|
const dsSet = util.extractSet(res.authority,
|
||
|
|
util.fqdn(tld), types.DS);
|
||
|
|
|
||
|
|
res.aa = true;
|
||
|
|
res.answer = dsSet;
|
||
|
|
key.signZSK(res.answer, types.DS);
|
||
|
|
res.authority = [];
|
||
|
|
res.additional = [];
|
||
|
|
|
||
|
|
if (res.answer.length === 0) {
|
||
|
|
res.authority.push(this.toSOA());
|
||
|
|
key.signZSK(res.authority, types.SOA);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// No DS we must add a minimally covering proof
|
||
|
|
if (!secure) {
|
||
|
|
// Replace any NSEC/NSEC3 records
|
||
|
|
const filterTypes = [types.NSEC, types.NSEC3];
|
||
|
|
res.authority = util.filterSet(res.authority, ...filterTypes);
|
||
|
|
const next = nsec.nextName(tld);
|
||
|
|
const rr = nsec.create(tld, next, TYPE_MAP_NS);
|
||
|
|
res.authority.push(rr);
|
||
|
|
key.signZSK(res.authority, types.NSEC);
|
||
|
|
} else {
|
||
|
|
key.signZSK(res.authority, types.DS);
|
||
|
|
}
|
||
|
|
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const res = new Message();
|
||
|
|
|
||
|
|
res.code = codes.NXDOMAIN;
|
||
|
|
res.aa = true;
|
||
|
|
|
||
|
|
// Doesn't exist.
|
||
|
|
//
|
||
|
|
// We should be giving a real NSEC proof
|
||
|
|
// here, but I don't think it's possible
|
||
|
|
// with the current construction.
|
||
|
|
//
|
||
|
|
// I imagine this would only be possible
|
||
|
|
// if NSEC3 begins to support BLAKE2b for
|
||
|
|
// name hashing. Even then, it's still
|
||
|
|
// not possible for SPV nodes since they
|
||
|
|
// can't arbitrarily iterate over the tree.
|
||
|
|
//
|
||
|
|
// Instead, we give a minimally covering
|
||
|
|
// NSEC record based on rfc4470
|
||
|
|
// https://tools.ietf.org/html/rfc4470
|
||
|
|
|
||
|
|
// Proving the name doesn't exist
|
||
|
|
const prev = nsec.prevName(tld);
|
||
|
|
const next = nsec.nextName(tld);
|
||
|
|
const nameSet = [nsec.create(prev, next, TYPE_MAP_EMPTY)];
|
||
|
|
key.signZSK(nameSet, types.NSEC);
|
||
|
|
|
||
|
|
// Proving a wildcard doesn't exist
|
||
|
|
const wildcardSet = [nsec.create('!.', '+.', TYPE_MAP_EMPTY)];
|
||
|
|
key.signZSK(wildcardSet, types.NSEC);
|
||
|
|
|
||
|
|
res.authority = res.authority.concat(nameSet, wildcardSet);
|
||
|
|
res.authority.push(this.toSOA());
|
||
|
|
key.signZSK(res.authority, types.SOA);
|
||
|
|
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Our resolution.
|
||
|
|
const resource = Resource.decode(data);
|
||
|
|
const res = resource.toDNS(name, type);
|
||
|
|
|
||
|
|
if (res.answer.length === 0 && res.aa) {
|
||
|
|
res.authority.push(this.toSOA());
|
||
|
|
key.signZSK(res.authority, types.SOA);
|
||
|
|
}
|
||
|
|
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
|
||
|
|
async resolve(req, rinfo) {
|
||
|
|
const [qs] = req.question;
|
||
|
|
const {name, type} = qs;
|
||
|
|
const tld = util.from(name, -1);
|
||
|
|
|
||
|
|
// Plugins can insert middleware here and hijack the
|
||
|
|
// lookup for special TLDs before checking Urkel tree.
|
||
|
|
// We also pass the entire question in case a plugin
|
||
|
|
// is able to return an authoritative (non-referral) answer.
|
||
|
|
if (typeof this.middle === 'function') {
|
||
|
|
let res;
|
||
|
|
try {
|
||
|
|
res = await this.middle(tld, req, rinfo);
|
||
|
|
} catch (e) {
|
||
|
|
this.logger.warning(
|
||
|
|
'Root server middleware resolution failed for name: %s',
|
||
|
|
name
|
||
|
|
);
|
||
|
|
this.logger.debug(e.stack);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (res) {
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Hit the cache first.
|
||
|
|
const cache = this.cache.get(name, type);
|
||
|
|
|
||
|
|
if (cache)
|
||
|
|
return cache;
|
||
|
|
|
||
|
|
const res = await this.response(req, rinfo);
|
||
|
|
|
||
|
|
if (!util.equal(tld, '_synth.'))
|
||
|
|
this.cache.set(name, type, res);
|
||
|
|
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
|
||
|
|
async open() {
|
||
|
|
await super.open(this.port, this.host);
|
||
|
|
|
||
|
|
this.logger.info('Root nameserver listening on port %d.', this.port);
|
||
|
|
}
|
||
|
|
|
||
|
|
getReserved(tld) {
|
||
|
|
return reserved.getByName(tld);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Intended to be called by plugin.
|
||
|
|
signRRSet(rrset, type) {
|
||
|
|
key.signZSK(rrset, type);
|
||
|
|
}
|
||
|
|
|
||
|
|
resetCache() {
|
||
|
|
this.cache.reset();
|
||
|
|
}
|
||
|
|
|
||
|
|
serial() {
|
||
|
|
const date = new Date();
|
||
|
|
const y = date.getUTCFullYear() * 1e6;
|
||
|
|
const m = (date.getUTCMonth() + 1) * 1e4;
|
||
|
|
const d = date.getUTCDate() * 1e2;
|
||
|
|
const h = date.getUTCHours();
|
||
|
|
return y + m + d + h;
|
||
|
|
}
|
||
|
|
|
||
|
|
toSOA() {
|
||
|
|
const rr = new Record();
|
||
|
|
const rd = new SOARecord();
|
||
|
|
|
||
|
|
rr.name = '.';
|
||
|
|
rr.type = types.SOA;
|
||
|
|
rr.ttl = 86400;
|
||
|
|
rr.data = rd;
|
||
|
|
rd.ns = '.';
|
||
|
|
rd.mbox = '.';
|
||
|
|
rd.serial = this.serial();
|
||
|
|
rd.refresh = 1800;
|
||
|
|
rd.retry = 900;
|
||
|
|
rd.expire = 604800;
|
||
|
|
rd.minttl = DEFAULT_TTL;
|
||
|
|
|
||
|
|
return rr;
|
||
|
|
}
|
||
|
|
|
||
|
|
toNS() {
|
||
|
|
const rr = new Record();
|
||
|
|
const rd = new NSRecord();
|
||
|
|
rr.name = '.';
|
||
|
|
rr.type = types.NS;
|
||
|
|
rr.ttl = 518400;
|
||
|
|
rr.data = rd;
|
||
|
|
rd.ns = this.synth;
|
||
|
|
return rr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Glue only
|
||
|
|
toA() {
|
||
|
|
const rr = new Record();
|
||
|
|
const rd = new ARecord();
|
||
|
|
rr.name = this.synth;
|
||
|
|
rr.type = types.A;
|
||
|
|
rr.ttl = 518400;
|
||
|
|
rr.data = rd;
|
||
|
|
rd.address = this.publicHost;
|
||
|
|
return rr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Glue only
|
||
|
|
toAAAA() {
|
||
|
|
const rr = new Record();
|
||
|
|
const rd = new AAAARecord();
|
||
|
|
rr.name = this.synth;
|
||
|
|
rr.type = types.AAAA;
|
||
|
|
rr.ttl = 518400;
|
||
|
|
rr.data = rd;
|
||
|
|
rd.address = this.publicHost;
|
||
|
|
return rr;
|
||
|
|
}
|
||
|
|
|
||
|
|
toNSEC() {
|
||
|
|
const next = nsec.nextName('.');
|
||
|
|
return nsec.create('.', next, TYPE_MAP_ROOT);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* RecursiveServer
|
||
|
|
* @extends {DNSServer}
|
||
|
|
*/
|
||
|
|
|
||
|
|
class RecursiveServer extends DNSServer {
|
||
|
|
constructor(options) {
|
||
|
|
super(RES_OPT);
|
||
|
|
|
||
|
|
this.ra = true;
|
||
|
|
this.edns = true;
|
||
|
|
this.dnssec = true;
|
||
|
|
this.noSig0 = false;
|
||
|
|
this.noAny = true;
|
||
|
|
|
||
|
|
this.logger = Logger.global;
|
||
|
|
this.key = secp256k1.privateKeyGenerate();
|
||
|
|
|
||
|
|
this.host = '127.0.0.1';
|
||
|
|
this.port = 5301;
|
||
|
|
this.stubHost = '127.0.0.1';
|
||
|
|
this.stubPort = 5300;
|
||
|
|
|
||
|
|
this.hns = new UnboundResolver({
|
||
|
|
inet6: false,
|
||
|
|
tcp: true,
|
||
|
|
edns: true,
|
||
|
|
dnssec: true,
|
||
|
|
minimize: true
|
||
|
|
});
|
||
|
|
|
||
|
|
if (options)
|
||
|
|
this.initOptions(options);
|
||
|
|
|
||
|
|
this.initNode();
|
||
|
|
|
||
|
|
this.hns.setStub(this.stubHost, this.stubPort, key.ds);
|
||
|
|
}
|
||
|
|
|
||
|
|
initOptions(options) {
|
||
|
|
assert(options);
|
||
|
|
|
||
|
|
this.parseOptions(options);
|
||
|
|
|
||
|
|
if (options.logger != null) {
|
||
|
|
assert(typeof options.logger === 'object');
|
||
|
|
this.logger = options.logger.context('rs');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.key != null) {
|
||
|
|
assert(Buffer.isBuffer(options.key));
|
||
|
|
assert(options.key.length === 32);
|
||
|
|
this.key = options.key;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.host != null) {
|
||
|
|
assert(typeof options.host === 'string');
|
||
|
|
this.host = IP.normalize(options.host);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.port != null) {
|
||
|
|
assert((options.port & 0xffff) === options.port);
|
||
|
|
assert(options.port !== 0);
|
||
|
|
this.port = options.port;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.stubHost != null) {
|
||
|
|
assert(typeof options.stubHost === 'string');
|
||
|
|
|
||
|
|
this.stubHost = IP.normalize(options.stubHost);
|
||
|
|
|
||
|
|
if (this.stubHost === '0.0.0.0' || this.stubHost === '::')
|
||
|
|
this.stubHost = '127.0.0.1';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.stubPort != null) {
|
||
|
|
assert((options.stubPort & 0xffff) === options.stubPort);
|
||
|
|
assert(options.stubPort !== 0);
|
||
|
|
this.stubPort = options.stubPort;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.noSig0 != null) {
|
||
|
|
assert(typeof options.noSig0 === 'boolean');
|
||
|
|
this.noSig0 = options.noSig0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.noUnbound != null) {
|
||
|
|
assert(typeof options.noUnbound === 'boolean');
|
||
|
|
if (options.noUnbound) {
|
||
|
|
this.hns = new RecursiveResolver({
|
||
|
|
inet6: false,
|
||
|
|
tcp: true,
|
||
|
|
edns: true,
|
||
|
|
dnssec: true,
|
||
|
|
minimize: true
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
initNode() {
|
||
|
|
this.hns.on('log', (...args) => {
|
||
|
|
this.logger.debug(...args);
|
||
|
|
});
|
||
|
|
|
||
|
|
this.on('error', (err) => {
|
||
|
|
this.logger.error(err);
|
||
|
|
});
|
||
|
|
|
||
|
|
this.on('query', (req, res) => {
|
||
|
|
this.logMessage('\n\nDNS Request:', req);
|
||
|
|
this.logMessage('\n\nDNS Response:', res);
|
||
|
|
});
|
||
|
|
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
logMessage(prefix, msg) {
|
||
|
|
if (this.logger.level < 5)
|
||
|
|
return;
|
||
|
|
|
||
|
|
const logs = msg.toString().trim().split('\n');
|
||
|
|
|
||
|
|
this.logger.spam(prefix);
|
||
|
|
|
||
|
|
for (const log of logs)
|
||
|
|
this.logger.spam(log);
|
||
|
|
}
|
||
|
|
|
||
|
|
signSize() {
|
||
|
|
if (!this.noSig0)
|
||
|
|
return 94;
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
sign(msg, host, port) {
|
||
|
|
if (!this.noSig0)
|
||
|
|
return hsig.sign(msg, this.key);
|
||
|
|
|
||
|
|
return msg;
|
||
|
|
}
|
||
|
|
|
||
|
|
async open(...args) {
|
||
|
|
await this.hns.open();
|
||
|
|
|
||
|
|
await super.open(this.port, this.host);
|
||
|
|
|
||
|
|
this.logger.info('Recursive server listening on port %d.', this.port);
|
||
|
|
}
|
||
|
|
|
||
|
|
async close() {
|
||
|
|
await super.close();
|
||
|
|
await this.hns.close();
|
||
|
|
}
|
||
|
|
|
||
|
|
async resolve(req, rinfo) {
|
||
|
|
const [qs] = req.question;
|
||
|
|
return this.hns.resolve(qs);
|
||
|
|
}
|
||
|
|
|
||
|
|
async lookup(name, type) {
|
||
|
|
return this.hns.lookup(name, type);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Helpers
|
||
|
|
*/
|
||
|
|
|
||
|
|
function toKey(name, type) {
|
||
|
|
const labels = util.split(name);
|
||
|
|
const label = util.from(name, labels, -1);
|
||
|
|
|
||
|
|
// Ignore type if we're a referral.
|
||
|
|
if (labels.length > 1)
|
||
|
|
return label.toLowerCase();
|
||
|
|
|
||
|
|
let key = '';
|
||
|
|
key += label.toLowerCase();
|
||
|
|
key += ';';
|
||
|
|
key += type.toString(10);
|
||
|
|
|
||
|
|
return key;
|
||
|
|
}
|
||
|
|
|
||
|
|
function hasValidOwner(section, owner) {
|
||
|
|
owner = util.fqdn(owner);
|
||
|
|
|
||
|
|
for (const rr of section) {
|
||
|
|
if (rr.type === types.NS)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
if (!util.equal(rr.name, owner))
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Expose
|
||
|
|
*/
|
||
|
|
|
||
|
|
exports.RootServer = RootServer;
|
||
|
|
exports.RecursiveServer = RecursiveServer;
|