Source: node/seeder.js

/*!
 * 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;