Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Root nameserver more flexible for plugins #558

Merged
merged 4 commits into from
Mar 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 46 additions & 13 deletions lib/dns/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -120,8 +109,22 @@ 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
// 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();
Expand Down Expand Up @@ -321,13 +324,13 @@ 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;

// 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
Expand Down Expand Up @@ -395,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') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe hook or resolveHook would be more intuitive name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, appreciate the feedback. I've already written two plugins using this and documented the middle() property here: https://hsd-dev.org/guides/plugins.html 🤷‍♂️

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);

Expand All @@ -415,6 +439,15 @@ class RootServer extends DNSServer {
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();
}
Expand Down
8 changes: 8 additions & 0 deletions lib/node/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
159 changes: 159 additions & 0 deletions test/ns-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -108,3 +111,159 @@ 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);
});
});

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);
});
});