289 lines
5.3 KiB
JavaScript
289 lines
5.3 KiB
JavaScript
/*!
|
|
* seeder.js - dns seed server for hsd
|
|
* Copyright (c) 2020, Christopher Jeffrey (MIT License).
|
|
* https://github.com/handshake-org/hsd
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('bsert');
|
|
const IP = require('binet');
|
|
const bns = require('bns');
|
|
const HostList = require('../net/hostlist');
|
|
|
|
const {
|
|
DNSServer,
|
|
wire,
|
|
util
|
|
} = bns;
|
|
|
|
const {
|
|
Message,
|
|
Record,
|
|
ARecord,
|
|
AAAARecord,
|
|
NSRecord,
|
|
SOARecord,
|
|
types,
|
|
codes
|
|
} = wire;
|
|
|
|
/**
|
|
* Seeder
|
|
*/
|
|
|
|
class Seeder extends DNSServer {
|
|
constructor(options) {
|
|
assert(options != null);
|
|
|
|
super({ inet6: false, tcp: false });
|
|
|
|
this.ra = false;
|
|
this.edns = true;
|
|
this.dnssec = false;
|
|
|
|
this.hosts = new HostList({
|
|
network: options.network,
|
|
prefix: options.prefix,
|
|
filename: options.filename,
|
|
memory: false
|
|
});
|
|
|
|
this.zone = '.';
|
|
this.ns = '.';
|
|
this.ip = null;
|
|
this.host = '127.0.0.1';
|
|
this.port = 53;
|
|
this.lastRefresh = 0;
|
|
this.res4 = new Message();
|
|
this.res6 = new Message();
|
|
|
|
this.initOptions(options);
|
|
}
|
|
|
|
initOptions(options) {
|
|
assert(options != null);
|
|
|
|
this.parseOptions(options);
|
|
|
|
if (options.zone != null)
|
|
this.zone = util.fqdn(options.zone);
|
|
|
|
if (options.ns != null)
|
|
this.ns = util.fqdn(options.ns);
|
|
|
|
if (options.ip != null)
|
|
this.ip = IP.normalize(options.ip);
|
|
|
|
if (options.host != null)
|
|
this.host = IP.normalize(options.host);
|
|
|
|
if (options.port != null) {
|
|
assert((options.port & 0xffff) === options.port);
|
|
assert(options.port !== 0);
|
|
this.port = options.port;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
async resolve(req, rinfo) {
|
|
const [qs] = req.question;
|
|
const {name, type} = qs;
|
|
|
|
if (!util.isSubdomain(this.zone, name))
|
|
throw createError(codes.NOTZONE);
|
|
|
|
if (!util.equal(name, this.zone))
|
|
return this.createEmpty(codes.NXDOMAIN);
|
|
|
|
await this.refresh();
|
|
|
|
switch (type) {
|
|
case types.ANY:
|
|
case types.A:
|
|
return this.res4.clone();
|
|
case types.AAAA:
|
|
return this.res6.clone();
|
|
case types.NS:
|
|
return this.createNS();
|
|
case types.SOA:
|
|
return this.createSOA();
|
|
}
|
|
|
|
return this.createEmpty(codes.SUCCESS);
|
|
}
|
|
|
|
async refresh() {
|
|
if (Date.now() > this.lastRefresh + 10 * 60 * 1000) {
|
|
this.hosts.reset();
|
|
|
|
try {
|
|
await this.hosts.loadFile();
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
this.lastRefresh = Date.now();
|
|
this.res4 = this.createA(types.A);
|
|
this.res6 = this.createA(types.AAAA);
|
|
}
|
|
}
|
|
|
|
createA(type) {
|
|
const res = new Message();
|
|
const items = [];
|
|
|
|
for (const entry of this.hosts.map.values()) {
|
|
const {addr} = entry;
|
|
|
|
if (this.hosts.isStale(entry))
|
|
continue;
|
|
|
|
if (!entry.lastSuccess)
|
|
continue;
|
|
|
|
if (addr.port !== this.hosts.network.port)
|
|
continue;
|
|
|
|
if (addr.hasKey())
|
|
continue;
|
|
|
|
if (!addr.isValid())
|
|
continue;
|
|
|
|
items.push(entry);
|
|
}
|
|
|
|
items.sort((a, b) => {
|
|
return b.lastSuccess - a.lastSuccess;
|
|
});
|
|
|
|
for (const entry of items) {
|
|
const {addr} = entry;
|
|
|
|
switch (type) {
|
|
case types.A:
|
|
if (!addr.isIPv4())
|
|
continue;
|
|
res.answer.push(createA(this.zone, addr.host));
|
|
break;
|
|
case types.AAAA:
|
|
if (!addr.isIPv6())
|
|
continue;
|
|
res.answer.push(createAAAA(this.zone, addr.host));
|
|
break;
|
|
}
|
|
|
|
if (res.answer.length === 50)
|
|
break;
|
|
}
|
|
|
|
if (res.answer.length === 0)
|
|
return this.createEmpty(codes.SUCCESS);
|
|
|
|
return res;
|
|
}
|
|
|
|
createNS() {
|
|
const res = new Message();
|
|
|
|
res.answer.push(createNS(this.zone, this.ns));
|
|
|
|
if (this.ip) {
|
|
const rr = IP.isIPv6String(this.ip)
|
|
? createAAAA(this.ns, this.ip)
|
|
: createA(this.ns, this.ip);
|
|
|
|
res.additional.push(rr);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
createSOA() {
|
|
const res = new Message();
|
|
res.answer.push(createSOA(this.zone, this.ns, this.zone));
|
|
return res;
|
|
}
|
|
|
|
createEmpty(code) {
|
|
const res = new Message();
|
|
res.code = code;
|
|
res.authority.push(createSOA(this.zone, this.ns, this.zone));
|
|
return res;
|
|
}
|
|
|
|
async open() {
|
|
return super.open(this.port, this.host);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
function createSOA(name, ns, mbox) {
|
|
const rr = new Record();
|
|
const rd = new SOARecord();
|
|
|
|
rr.name = name;
|
|
rr.type = types.SOA;
|
|
rr.ttl = 86400;
|
|
rr.data = rd;
|
|
rd.ns = ns;
|
|
rd.mbox = mbox;
|
|
rd.serial = Math.floor(Date.now() / 1000);
|
|
rd.refresh = 604800;
|
|
rd.retry = 86400;
|
|
rd.expire = 2592000;
|
|
rd.minttl = 604800;
|
|
|
|
return rr;
|
|
}
|
|
|
|
function createNS(name, ns) {
|
|
const rr = new Record();
|
|
const rd = new NSRecord();
|
|
rr.name = name;
|
|
rr.type = types.NS;
|
|
rr.ttl = 40000;
|
|
rr.data = rd;
|
|
rd.ns = ns;
|
|
return rr;
|
|
}
|
|
|
|
function createA(name, address) {
|
|
const rr = new Record();
|
|
const rd = new ARecord();
|
|
rr.name = name;
|
|
rr.type = types.A;
|
|
rr.ttl = 3600;
|
|
rr.data = rd;
|
|
rd.address = address;
|
|
return rr;
|
|
}
|
|
|
|
function createAAAA(name, address) {
|
|
const rr = new Record();
|
|
const rd = new AAAARecord();
|
|
rr.name = name;
|
|
rr.type = types.AAAA;
|
|
rr.ttl = 3600;
|
|
rr.data = rd;
|
|
rd.address = address;
|
|
return rr;
|
|
}
|
|
|
|
function createError(code) {
|
|
const err = new Error('Invalid request.');
|
|
err.type = 'DNSError';
|
|
err.errno = code;
|
|
return err;
|
|
}
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
module.exports = Seeder;
|