From 27fe87e509a97b36cbb0f08c1904f59d4e67cda8 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Tue, 9 Feb 2021 13:42:07 -0500 Subject: [PATCH 1/4] node: reserve 'ns' and 'rs' from plugin ids and add to get() --- lib/node/node.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/node/node.js b/lib/node/node.js index 10d9cc09b..38e872866 100644 --- a/lib/node/node.js +++ b/lib/node/node.js @@ -342,6 +342,8 @@ class Node extends EventEmitter { case 'pool': case 'rpc': case 'http': + case 'ns': + case 'rs': assert(false, `${plugin.id} is already added.`); break; } @@ -401,6 +403,12 @@ class Node extends EventEmitter { case 'http': assert(this.http, 'http is not loaded.'); return this.http; + case 'rs': + assert(this.rs, 'rs is not loaded.'); + return this.rs; + case 'ns': + assert(this.ns, 'ns is not loaded.'); + return this.ns; } return this.plugins[name] || null; From 15168443207fb5352c504af58e09e30549ea5dbd Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Tue, 9 Feb 2021 15:34:42 -0500 Subject: [PATCH 2/4] dns: make blacklist a property of root server class --- lib/dns/server.js | 26 +++++++++--------- test/ns-test.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/lib/dns/server.js b/lib/dns/server.js index ab05ba2ba..8eda43dbb 100644 --- a/lib/dns/server.js +++ b/lib/dns/server.js @@ -51,17 +51,6 @@ const TYPE_MAP = Buffer.from('000722000000000380', 'hex'); const RES_OPT = { inet6: false, tcp: true }; const CACHE_TTL = 30 * 60 * 1000; -const blacklist = new Set([ - 'bit', // Namecoin - 'eth', // ENS - 'exit', // Tor - 'gnu', // GNUnet (GNS) - 'i2p', // Invisible Internet Project - 'onion', // Tor - 'tor', // OnioNS - 'zkey' // GNS -]); - /** * RootCache */ @@ -122,6 +111,19 @@ class RootServer extends DNSServer { this.lookup = 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); this.initNode(); @@ -321,7 +323,7 @@ class RootServer extends DNSServer { } // Ask the urkel tree for the name data. - const data = !blacklist.has(tld) + const data = !this.blacklist.has(tld) ? (await this.lookupName(tld)) : null; diff --git a/test/ns-test.js b/test/ns-test.js index d56767165..5ae1a176f 100644 --- a/test/ns-test.js +++ b/test/ns-test.js @@ -3,6 +3,9 @@ const assert = require('bsert'); const {wire} = require('bns'); const {RootServer} = require('../lib/dns/server'); +const {Resource} = require('../lib/dns/resource'); +const NameState = require('../lib/covenants/namestate'); +const rules = require('../lib/covenants/rules'); describe('RootServer', function() { const ns = new RootServer({ @@ -108,3 +111,67 @@ describe('RootServer', function() { assert.strictEqual(cache.size, 1); }); }); + +describe('RootServer Blacklist', 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 () => { + await ns.open(); + }); + + after(async () => { + await ns.close(); + }); + + it('should look up non-blacklisted name', async () => { + const name = 'icecream.'; + const req = { + question: [{ + name, + type: wire.types.NS + }] + }; + + const res = await ns.resolve(req); + const authority = res.authority; + const rec = authority[0]; + + assert.strictEqual(rec.name, name); + assert.strictEqual(rec.type, wire.types.NS); + assert.strictEqual(rec.data.ns, 'ns1.handshake.'); + }); + + it('should not look up blacklisted name', async () => { + const name = 'bit.'; + const req = { + question: [{ + name, + type: wire.types.NS + }] + }; + + const res = await ns.resolve(req); + assert.strictEqual(res.code, wire.codes.NXDOMAIN); + assert.strictEqual(res.answer.length, 0); + }); +}); From df16daf2292cc576096aec3cc83752384f7d5ca9 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 10 Feb 2021 13:46:37 -0500 Subject: [PATCH 3/4] dns: expose reserved list in root server class --- lib/dns/server.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/dns/server.js b/lib/dns/server.js index 8eda43dbb..68305b0e0 100644 --- a/lib/dns/server.js +++ b/lib/dns/server.js @@ -329,7 +329,7 @@ class RootServer extends DNSServer { // Non-existent domain. if (!data) { - const item = reserved.getByName(tld); + const item = this.getReserved(tld); // This name is in the existing root zone. // Fall back to ICANN's servers if not yet @@ -417,6 +417,10 @@ class RootServer extends DNSServer { this.logger.info('Root nameserver listening on port %d.', this.port); } + getReserved(tld) { + return reserved.getByName(tld); + } + resetCache() { this.cache.reset(); } From 55ee4679ea4c840ba88b5a93b56f169541316f40 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 4 Mar 2021 15:57:25 -0500 Subject: [PATCH 4/4] dns: add middleware property to root server to hijack queries --- lib/dns/server.js | 27 ++++++++++++++ test/ns-test.js | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/lib/dns/server.js b/lib/dns/server.js index 68305b0e0..d06432ebb 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 5ae1a176f..2c782112b 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); + }); +});