UNPKG

hsd

Version:
837 lines (667 loc) 19.2 kB
/*! * 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;