diff --git a/lib/dns/server.js b/lib/dns/server.js index 68305b0e..d06432eb 100644 --- a/lib/dns/server.js +++ b/lib/dns/server.js @@ -109,6 +109,7 @@ class RootServer extends DNSServer { 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 @@ -397,6 +398,27 @@ class RootServer extends DNSServer { 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); + } 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); @@ -421,6 +443,11 @@ class RootServer extends DNSServer { return reserved.getByName(tld); } + // Intended to be called by plugin. + signRRSet(rrset, type) { + key.signZSK(rrset, type); + } + resetCache() { this.cache.reset(); } diff --git a/test/ns-test.js b/test/ns-test.js index 5ae1a176..2c782112 100644 --- a/test/ns-test.js +++ b/test/ns-test.js @@ -175,3 +175,95 @@ describe('RootServer Blacklist', function() { assert.strictEqual(res.answer.length, 0); }); }); + +describe('RootServer Plugins', function() { + const ns = new RootServer({ + port: 25349, // regtest + lookup: (hash) => { + // Normally an Urkel Tree goes here. + // Blacklisted names should never get this far. + if (hash.equals(rules.hashName('bit'))) + throw new Error('Blacklisted name!'); + + // For this test all other names have the same record + const namestate = new NameState(); + namestate.data = Resource.fromJSON({ + records: [ + { + type: 'NS', + ns: 'ns1.handshake.' + } + ] + }).encode(); + return namestate.encode(); + } + }); + + before(async () => { + // Plugin inserts middleware before server is opened + ns.middle = (tld, req) => { + const [qs] = req.question; + const name = qs.name.toLowerCase(); + const type = qs.type; + + if (tld === 'bit.') { + // This plugin runs an imaginary Namecoin full node. + // It looks up records and returns an authoritative answer. + // This makes it look like the complete record including + // the subdomain is in the HNS root zone. + const res = new wire.Message(); + res.aa = true; + + // This plugin only returns A records, + // and all Namecoin names have the same IP address. + if (type !== wire.types.A) + return null; + + const rr = new wire.Record(); + const rd = new wire.ARecord(); + rr.name = name; + rr.type = wire.types.A; + rr.ttl = 518400; + rr.data = rd; + rd.address = '4.8.15.16'; + + res.answer.push(rr); + ns.signRRSet(res.answer, wire.types.A); + + return res; + } + + // Plugin doesn't care about this name + return null; + }; + + await ns.open(); + }); + + after(async () => { + await ns.close(); + }); + + it('should hijack lookup for blacklisted name', async () => { + const name = 'decentralize.bit.'; + const req = { + question: [{ + name, + type: wire.types.A + }] + }; + + const res = await ns.resolve(req); + assert.strictEqual(res.authority.length, 0); + assert.strictEqual(res.answer.length, 2); + + const rec = res.answer[0]; + assert.strictEqual(rec.name, name); + assert.strictEqual(rec.type, wire.types.A); + assert.strictEqual(rec.data.address, '4.8.15.16'); + + const sig = res.answer[1]; + assert.strictEqual(sig.name, name); + assert.strictEqual(sig.type, wire.types.RRSIG); + }); +});