1843 lines
37 KiB
JavaScript
1843 lines
37 KiB
JavaScript
/*!
|
|
* hostlist.js - address management for hsd
|
|
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
|
* https://github.com/handshake-org/hsd
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('bsert');
|
|
const path = require('path');
|
|
const fs = require('bfile');
|
|
const IP = require('binet');
|
|
const bio = require('bufio');
|
|
const Logger = require('blgr');
|
|
const Hash256 = require('bcrypto/lib/hash256');
|
|
const List = require('blst');
|
|
const rng = require('bcrypto/lib/random');
|
|
const secp256k1 = require('bcrypto/lib/secp256k1');
|
|
const {lookup} = require('./lookup');
|
|
const util = require('../utils/util');
|
|
const Network = require('../protocol/network');
|
|
const NetAddress = require('./netaddress');
|
|
const common = require('./common');
|
|
const seeds = require('./seeds');
|
|
|
|
/**
|
|
* Stochastic address manager based on bitcoin addrman.
|
|
*
|
|
* Design goals:
|
|
* * Keep the address tables in-memory, and asynchronously dump the entire
|
|
* table to hosts.json.
|
|
* * Make sure no (localized) attacker can fill the entire table with his
|
|
* nodes/addresses.
|
|
*
|
|
* To that end:
|
|
* * Addresses are organized into buckets that can each store up
|
|
* to 64 entries (maxEntries).
|
|
* * Addresses to which our node has not successfully connected go into
|
|
* 1024 "fresh" buckets (maxFreshBuckets).
|
|
* * Based on the address range of the source of information
|
|
* 64 buckets are selected at random.
|
|
* * The actual bucket is chosen from one of these, based on the range in
|
|
* which the address itself is located.
|
|
* * The position in the bucket is chosen based on the full address.
|
|
* * One single address can occur in up to 8 different buckets to increase
|
|
* selection chances for addresses that are seen frequently. The chance
|
|
* for increasing this multiplicity decreases exponentially.
|
|
* * When adding a new address to an occupied position of a bucket, it
|
|
* will not replace the existing entry unless that address is also stored
|
|
* in another bucket or it doesn't meet one of several quality criteria
|
|
* (see isStale for exact criteria).
|
|
* * Addresses of nodes that are known to be accessible go into 256 "tried"
|
|
* buckets.
|
|
* * Each address range selects at random 8 of these buckets.
|
|
* * The actual bucket is chosen from one of these, based on the full
|
|
* address.
|
|
* * Bucket selection is based on cryptographic hashing,
|
|
* using a randomly-generated 256-bit key, which should not be observable
|
|
* by adversaries (key).
|
|
*/
|
|
|
|
/**
|
|
* Host List
|
|
* @alias module:net.HostList
|
|
*/
|
|
|
|
class HostList {
|
|
/**
|
|
* Create a host list.
|
|
* @constructor
|
|
* @param {Object} options
|
|
*/
|
|
|
|
constructor(options) {
|
|
this.options = new HostListOptions(options);
|
|
this.network = this.options.network;
|
|
this.logger = this.options.logger.context('hostlist');
|
|
this.address = this.options.address;
|
|
this.brontide = this.options.brontide;
|
|
this.resolve = this.options.resolve;
|
|
this.random = this.options.random;
|
|
|
|
this.key = rng.randomBytes(32);
|
|
this.hash = new Hash256();
|
|
this.hashbuf = Buffer.alloc(4);
|
|
this.portbuf = Buffer.alloc(2);
|
|
|
|
this.dnsSeeds = [];
|
|
this.dnsNodes = [];
|
|
|
|
this.map = new Map();
|
|
this.fresh = [];
|
|
this.totalFresh = 0;
|
|
this.used = [];
|
|
this.totalUsed = 0;
|
|
this.nodes = [];
|
|
this.local = new Map();
|
|
this.banned = new Map();
|
|
|
|
this.maxFreshBuckets = 1024;
|
|
this.maxUsedBuckets = 256;
|
|
this.maxEntries = 64;
|
|
|
|
this.timer = null;
|
|
this.needsFlush = false;
|
|
this.flushing = false;
|
|
this.added = false;
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize list.
|
|
* @private
|
|
*/
|
|
|
|
init() {
|
|
for (let i = 0; i < this.maxFreshBuckets; i++)
|
|
this.fresh.push(new Map());
|
|
|
|
for (let i = 0; i < this.maxUsedBuckets; i++)
|
|
this.used.push(new List());
|
|
}
|
|
|
|
/**
|
|
* Initialize list.
|
|
* @private
|
|
*/
|
|
|
|
initAdd() {
|
|
if (this.added)
|
|
return;
|
|
|
|
const options = this.options;
|
|
const scores = HostList.scores;
|
|
|
|
this.setSeeds(options.seeds);
|
|
this.setNodes(options.nodes);
|
|
|
|
this.pushLocal(this.address, scores.MANUAL);
|
|
this.pushLocal(this.brontide, scores.MANUAL);
|
|
this.addLocal(options.host, options.port, scores.BIND);
|
|
|
|
const hosts = IP.getPublic();
|
|
const port = this.address.port;
|
|
|
|
for (const host of hosts)
|
|
this.addLocal(host, port, scores.IF);
|
|
|
|
this.added = true;
|
|
}
|
|
|
|
/**
|
|
* Open hostlist and read hosts file.
|
|
* @method
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async open() {
|
|
this.initAdd();
|
|
|
|
try {
|
|
await this.loadFile();
|
|
} catch (e) {
|
|
this.logger.warning('Hosts deserialization failed.');
|
|
this.logger.error(e);
|
|
}
|
|
|
|
if (this.size() === 0)
|
|
this.injectSeeds();
|
|
|
|
await this.discoverNodes();
|
|
|
|
this.start();
|
|
}
|
|
|
|
/**
|
|
* Close hostlist.
|
|
* @method
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async close() {
|
|
this.stop();
|
|
await this.flush();
|
|
this.reset();
|
|
}
|
|
|
|
/**
|
|
* Start flush interval.
|
|
*/
|
|
|
|
start() {
|
|
if (this.options.memory)
|
|
return;
|
|
|
|
if (!this.options.filename)
|
|
return;
|
|
|
|
assert(this.timer == null);
|
|
this.timer = setInterval(() => this.flush(), this.options.flushInterval);
|
|
}
|
|
|
|
/**
|
|
* Stop flush interval.
|
|
*/
|
|
|
|
stop() {
|
|
if (this.options.memory)
|
|
return;
|
|
|
|
if (!this.options.filename)
|
|
return;
|
|
|
|
assert(this.timer != null);
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
}
|
|
|
|
/**
|
|
* Read and initialize from hosts file.
|
|
*/
|
|
|
|
injectSeeds() {
|
|
const nodes = seeds.get(this.network.type);
|
|
|
|
for (const node of nodes) {
|
|
const addr = NetAddress.fromHostname(node, this.network);
|
|
|
|
if (this.map.has(addr.hostname))
|
|
continue;
|
|
|
|
if (!addr.isRoutable())
|
|
continue;
|
|
|
|
if (!this.options.onion && addr.isOnion())
|
|
continue;
|
|
|
|
if (this.options.brontideOnly && !addr.hasKey())
|
|
continue;
|
|
|
|
if (addr.port === 0)
|
|
continue;
|
|
|
|
this.add(addr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read and initialize from hosts file.
|
|
* @method
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async loadFile() {
|
|
const filename = this.options.filename;
|
|
|
|
if (fs.unsupported)
|
|
return;
|
|
|
|
if (this.options.memory)
|
|
return;
|
|
|
|
if (!filename)
|
|
return;
|
|
|
|
let data;
|
|
try {
|
|
data = await fs.readFile(filename, 'utf8');
|
|
} catch (e) {
|
|
if (e.code === 'ENOENT')
|
|
return;
|
|
throw e;
|
|
}
|
|
|
|
const json = JSON.parse(data);
|
|
|
|
this.fromJSON(json);
|
|
}
|
|
|
|
/**
|
|
* Flush addrs to hosts file.
|
|
* @method
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async flush() {
|
|
const filename = this.options.filename;
|
|
|
|
if (fs.unsupported)
|
|
return;
|
|
|
|
if (this.options.memory)
|
|
return;
|
|
|
|
if (!filename)
|
|
return;
|
|
|
|
if (!this.needsFlush)
|
|
return;
|
|
|
|
if (this.flushing)
|
|
return;
|
|
|
|
this.needsFlush = false;
|
|
|
|
this.logger.debug('Writing hosts to %s.', filename);
|
|
|
|
const json = this.toJSON();
|
|
const data = JSON.stringify(json);
|
|
|
|
this.flushing = true;
|
|
|
|
try {
|
|
await fs.writeFile(filename, data, 'utf8');
|
|
} catch (e) {
|
|
this.logger.warning('Writing hosts failed.');
|
|
this.logger.error(e);
|
|
}
|
|
|
|
this.flushing = false;
|
|
}
|
|
|
|
/**
|
|
* Get list size.
|
|
* @returns {Number}
|
|
*/
|
|
|
|
size() {
|
|
return this.totalFresh + this.totalUsed;
|
|
}
|
|
|
|
/**
|
|
* Test whether the host list is full.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isFull() {
|
|
return this.totalFresh >= this.maxFreshBuckets * this.maxEntries;
|
|
}
|
|
|
|
/**
|
|
* Reset host list.
|
|
*/
|
|
|
|
reset() {
|
|
this.map.clear();
|
|
|
|
for (const bucket of this.fresh)
|
|
bucket.clear();
|
|
|
|
for (const bucket of this.used)
|
|
bucket.reset();
|
|
|
|
this.totalFresh = 0;
|
|
this.totalUsed = 0;
|
|
|
|
this.nodes.length = 0;
|
|
}
|
|
|
|
/**
|
|
* Mark a peer as banned.
|
|
* @param {String} host
|
|
*/
|
|
|
|
ban(host) {
|
|
this.banned.set(host, util.now());
|
|
}
|
|
|
|
/**
|
|
* Unban host.
|
|
* @param {String} host
|
|
*/
|
|
|
|
unban(host) {
|
|
this.banned.delete(host);
|
|
}
|
|
|
|
/**
|
|
* Clear banned hosts.
|
|
*/
|
|
|
|
clearBanned() {
|
|
this.banned.clear();
|
|
}
|
|
|
|
/**
|
|
* Test whether the host is banned.
|
|
* @param {String} host
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isBanned(host) {
|
|
const time = this.banned.get(host);
|
|
|
|
if (time == null)
|
|
return false;
|
|
|
|
if (util.now() > time + this.options.banTime) {
|
|
this.banned.delete(host);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Allocate a new host.
|
|
* @returns {HostEntry}
|
|
*/
|
|
|
|
getHost() {
|
|
let buckets = null;
|
|
|
|
if (this.totalFresh > 0)
|
|
buckets = this.fresh;
|
|
|
|
if (this.totalUsed > 0) {
|
|
if (this.totalFresh === 0 || this.random(2) === 0)
|
|
buckets = this.used;
|
|
}
|
|
|
|
if (!buckets)
|
|
return null;
|
|
|
|
const now = this.network.now();
|
|
|
|
let factor = 1;
|
|
|
|
for (;;) {
|
|
const i = this.random(buckets.length);
|
|
const bucket = buckets[i];
|
|
|
|
if (bucket.size === 0)
|
|
continue;
|
|
|
|
let index = this.random(bucket.size);
|
|
let entry;
|
|
|
|
if (buckets === this.used) {
|
|
entry = bucket.head;
|
|
while (index--)
|
|
entry = entry.next;
|
|
} else {
|
|
for (entry of bucket.values()) {
|
|
if (index === 0)
|
|
break;
|
|
index -= 1;
|
|
}
|
|
}
|
|
|
|
const num = this.random(1 << 30);
|
|
|
|
if (num < factor * entry.chance(now) * (1 << 30))
|
|
return entry;
|
|
|
|
factor *= 1.2;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get fresh bucket for host.
|
|
* @private
|
|
* @param {HostEntry} entry
|
|
* @param {NetAddress?} [src]
|
|
* @returns {Map}
|
|
*/
|
|
|
|
freshBucket(entry, src) {
|
|
const addr = entry.addr;
|
|
|
|
if (!src)
|
|
src = entry.src;
|
|
|
|
this.hash.init();
|
|
this.hash.update(this.key);
|
|
this.hash.update(addr.getGroup());
|
|
this.hash.update(src.getGroup());
|
|
|
|
const hash1 = this.hash.final();
|
|
const hash32 = bio.readU32(hash1, 0) % 64;
|
|
|
|
bio.writeU32(this.hashbuf, hash32, 0);
|
|
|
|
this.hash.init();
|
|
this.hash.update(this.key);
|
|
this.hash.update(src.getGroup());
|
|
this.hash.update(this.hashbuf);
|
|
|
|
const hash2 = this.hash.final();
|
|
const hash = bio.readU32(hash2, 0);
|
|
const index = hash % this.fresh.length;
|
|
|
|
return this.fresh[index];
|
|
}
|
|
|
|
/**
|
|
* Get used bucket for host.
|
|
* @private
|
|
* @param {HostEntry} entry
|
|
* @returns {List}
|
|
*/
|
|
|
|
usedBucket(entry) {
|
|
const addr = entry.addr;
|
|
|
|
bio.writeU16(this.portbuf, addr.port, 0);
|
|
|
|
this.hash.init();
|
|
this.hash.update(this.key);
|
|
this.hash.update(addr.raw);
|
|
this.hash.update(this.portbuf);
|
|
this.hash.update(addr.key);
|
|
|
|
const hash1 = this.hash.final();
|
|
const hash32 = bio.readU32(hash1, 0) % 8;
|
|
|
|
bio.writeU32(this.hashbuf, hash32, 0);
|
|
|
|
this.hash.init();
|
|
this.hash.update(this.key);
|
|
this.hash.update(addr.getGroup());
|
|
this.hash.update(this.hashbuf);
|
|
|
|
const hash2 = this.hash.final();
|
|
const hash = bio.readU32(hash2, 0);
|
|
const index = hash % this.used.length;
|
|
|
|
return this.used[index];
|
|
}
|
|
|
|
/**
|
|
* Add host to host list.
|
|
* @param {NetAddress} addr
|
|
* @param {NetAddress?} [src]
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
add(addr, src) {
|
|
assert(addr.port !== 0);
|
|
|
|
let entry = this.map.get(addr.hostname);
|
|
|
|
if (entry) {
|
|
let penalty = 2 * 60 * 60;
|
|
let interval = 24 * 60 * 60;
|
|
|
|
// No source means we're inserting
|
|
// this ourselves. No penalty.
|
|
if (!src)
|
|
penalty = 0;
|
|
|
|
// Update services.
|
|
entry.addr.services |= addr.services;
|
|
entry.addr.services >>>= 0;
|
|
|
|
// Online?
|
|
const now = this.network.now();
|
|
if (now - addr.time < 24 * 60 * 60)
|
|
interval = 60 * 60;
|
|
|
|
// Periodically update time.
|
|
if (entry.addr.time < addr.time - interval - penalty) {
|
|
entry.addr.time = addr.time;
|
|
this.needsFlush = true;
|
|
}
|
|
|
|
// Do not update if no new
|
|
// information is present.
|
|
if (entry.addr.time && addr.time <= entry.addr.time)
|
|
return false;
|
|
|
|
// Do not update if the entry was
|
|
// already in the "used" table.
|
|
if (entry.used)
|
|
return false;
|
|
|
|
assert(entry.refCount > 0);
|
|
|
|
// Do not update if the max
|
|
// reference count is reached.
|
|
if (entry.refCount === HostList.MAX_REFS)
|
|
return false;
|
|
|
|
assert(entry.refCount < HostList.MAX_REFS);
|
|
|
|
// Stochastic test: previous refCount
|
|
// N: 2^N times harder to increase it.
|
|
let factor = 1;
|
|
for (let i = 0; i < entry.refCount; i++)
|
|
factor *= 2;
|
|
|
|
if (this.random(factor) !== 0)
|
|
return false;
|
|
} else {
|
|
if (!src)
|
|
src = this.address;
|
|
|
|
entry = new HostEntry(addr, src);
|
|
|
|
this.totalFresh += 1;
|
|
}
|
|
|
|
const bucket = this.freshBucket(entry, src);
|
|
|
|
if (bucket.has(entry.key()))
|
|
return false;
|
|
|
|
if (bucket.size >= this.maxEntries)
|
|
this.evictFresh(bucket);
|
|
|
|
bucket.set(entry.key(), entry);
|
|
entry.refCount += 1;
|
|
|
|
this.map.set(entry.key(), entry);
|
|
this.needsFlush = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Evict a host from fresh bucket.
|
|
* @param {Map} bucket
|
|
*/
|
|
|
|
evictFresh(bucket) {
|
|
let old = null;
|
|
|
|
for (const entry of bucket.values()) {
|
|
if (this.isStale(entry)) {
|
|
bucket.delete(entry.key());
|
|
|
|
if (--entry.refCount === 0) {
|
|
this.map.delete(entry.key());
|
|
this.totalFresh -= 1;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!old) {
|
|
old = entry;
|
|
continue;
|
|
}
|
|
|
|
if (entry.addr.time < old.addr.time)
|
|
old = entry;
|
|
}
|
|
|
|
if (!old)
|
|
return;
|
|
|
|
bucket.delete(old.key());
|
|
|
|
if (--old.refCount === 0) {
|
|
this.map.delete(old.key());
|
|
this.totalFresh -= 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test whether a host is evictable.
|
|
* @param {HostEntry} entry
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isStale(entry) {
|
|
const now = this.network.now();
|
|
|
|
if (entry.lastAttempt && entry.lastAttempt >= now - 60)
|
|
return false;
|
|
|
|
if (entry.addr.time > now + 10 * 60)
|
|
return true;
|
|
|
|
if (entry.addr.time === 0)
|
|
return true;
|
|
|
|
if (now - entry.addr.time > HostList.HORIZON_DAYS * 24 * 60 * 60)
|
|
return true;
|
|
|
|
if (entry.lastSuccess === 0 && entry.attempts >= HostList.RETRIES)
|
|
return true;
|
|
|
|
if (now - entry.lastSuccess > HostList.MIN_FAIL_DAYS * 24 * 60 * 60) {
|
|
if (entry.attempts >= HostList.MAX_FAILURES)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Remove host from host list.
|
|
* @param {String} hostname
|
|
* @returns {NetAddress}
|
|
*/
|
|
|
|
remove(hostname) {
|
|
const entry = this.map.get(hostname);
|
|
|
|
if (!entry)
|
|
return null;
|
|
|
|
if (entry.used) {
|
|
let head = entry;
|
|
|
|
assert(entry.refCount === 0);
|
|
|
|
while (head.prev)
|
|
head = head.prev;
|
|
|
|
for (const bucket of this.used) {
|
|
if (bucket.head === head) {
|
|
bucket.remove(entry);
|
|
this.totalUsed -= 1;
|
|
head = null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
assert(!head);
|
|
} else {
|
|
for (const bucket of this.fresh) {
|
|
if (bucket.delete(entry.key()))
|
|
entry.refCount -= 1;
|
|
}
|
|
|
|
this.totalFresh -= 1;
|
|
assert(entry.refCount === 0);
|
|
}
|
|
|
|
this.map.delete(entry.key());
|
|
|
|
return entry.addr;
|
|
}
|
|
|
|
/**
|
|
* Mark host as failed.
|
|
* @param {String} hostname
|
|
*/
|
|
|
|
markAttempt(hostname) {
|
|
const entry = this.map.get(hostname);
|
|
const now = this.network.now();
|
|
|
|
if (!entry)
|
|
return;
|
|
|
|
entry.attempts += 1;
|
|
entry.lastAttempt = now;
|
|
}
|
|
|
|
/**
|
|
* Mark host as successfully connected.
|
|
* @param {String} hostname
|
|
*/
|
|
|
|
markSuccess(hostname) {
|
|
const entry = this.map.get(hostname);
|
|
const now = this.network.now();
|
|
|
|
if (!entry)
|
|
return;
|
|
|
|
if (now - entry.addr.time > 20 * 60)
|
|
entry.addr.time = now;
|
|
}
|
|
|
|
/**
|
|
* Mark host as successfully ack'd.
|
|
* @param {String} hostname
|
|
* @param {Number} services
|
|
*/
|
|
|
|
markAck(hostname, services) {
|
|
const entry = this.map.get(hostname);
|
|
|
|
if (!entry)
|
|
return;
|
|
|
|
const now = this.network.now();
|
|
|
|
entry.addr.services |= services;
|
|
entry.addr.services >>>= 0;
|
|
|
|
entry.lastSuccess = now;
|
|
entry.lastAttempt = now;
|
|
entry.attempts = 0;
|
|
|
|
if (entry.used)
|
|
return;
|
|
|
|
assert(entry.refCount > 0);
|
|
|
|
// Remove from fresh.
|
|
let old = null;
|
|
for (const bucket of this.fresh) {
|
|
if (bucket.delete(entry.key())) {
|
|
entry.refCount -= 1;
|
|
old = bucket;
|
|
}
|
|
}
|
|
|
|
assert(old);
|
|
assert(entry.refCount === 0);
|
|
this.totalFresh -= 1;
|
|
|
|
// Find room in used bucket.
|
|
const bucket = this.usedBucket(entry);
|
|
|
|
if (bucket.size < this.maxEntries) {
|
|
entry.used = true;
|
|
bucket.push(entry);
|
|
this.totalUsed += 1;
|
|
return;
|
|
}
|
|
|
|
// No room. Evict.
|
|
const evicted = this.evictUsed(bucket);
|
|
|
|
let fresh = this.freshBucket(evicted);
|
|
|
|
// Move to entry's old bucket if no room.
|
|
if (fresh.size >= this.maxEntries)
|
|
fresh = old;
|
|
|
|
// Swap to evicted's used bucket.
|
|
entry.used = true;
|
|
bucket.replace(evicted, entry);
|
|
|
|
// Move evicted to fresh bucket.
|
|
evicted.used = false;
|
|
fresh.set(evicted.key(), evicted);
|
|
assert(evicted.refCount === 0);
|
|
evicted.refCount += 1;
|
|
this.totalFresh += 1;
|
|
}
|
|
|
|
/**
|
|
* Pick used for eviction.
|
|
* @param {List} bucket
|
|
*/
|
|
|
|
evictUsed(bucket) {
|
|
let old = bucket.head;
|
|
|
|
for (let entry = bucket.head; entry; entry = entry.next) {
|
|
if (entry.addr.time < old.addr.time)
|
|
old = entry;
|
|
}
|
|
|
|
return old;
|
|
}
|
|
|
|
/**
|
|
* Convert address list to array.
|
|
* @returns {NetAddress[]}
|
|
*/
|
|
|
|
toArray() {
|
|
const items = [];
|
|
const out = [];
|
|
|
|
for (const entry of this.map.values())
|
|
items.push(entry);
|
|
|
|
for (let i = 0; i < items.length && out.length < 2500; i++) {
|
|
const j = this.random(items.length - i);
|
|
|
|
[items[i], items[i + j]] = [items[i + j], items[i]];
|
|
|
|
const entry = items[i];
|
|
|
|
if (!this.isStale(entry))
|
|
out.push(entry.addr);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Add a preferred seed.
|
|
* @param {String} host
|
|
*/
|
|
|
|
addSeed(host) {
|
|
const ip = IP.fromHostname(host);
|
|
|
|
if (ip.type === IP.types.NONE) {
|
|
// Defer for resolution.
|
|
this.dnsSeeds.push(ip);
|
|
return null;
|
|
}
|
|
|
|
if (ip.port === 0)
|
|
ip.port = ip.key ? this.network.brontidePort : this.network.port;
|
|
|
|
const addr = NetAddress.fromHost(ip.host, ip.port, ip.key, this.network);
|
|
|
|
this.add(addr);
|
|
|
|
return addr;
|
|
}
|
|
|
|
/**
|
|
* Add a priority node.
|
|
* @param {String} host
|
|
* @returns {NetAddress}
|
|
*/
|
|
|
|
addNode(host) {
|
|
const ip = IP.fromHostname(host);
|
|
|
|
if (ip.type === IP.types.NONE) {
|
|
// Defer for resolution.
|
|
this.dnsNodes.push(ip);
|
|
return null;
|
|
}
|
|
|
|
if (ip.port === 0)
|
|
ip.port = ip.key ? this.network.brontidePort : this.network.port;
|
|
|
|
const addr = NetAddress.fromHost(ip.host, ip.port, ip.key, this.network);
|
|
|
|
this.nodes.push(addr);
|
|
this.add(addr);
|
|
|
|
return addr;
|
|
}
|
|
|
|
/**
|
|
* Remove a priority node.
|
|
* @param {String} host
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
removeNode(host) {
|
|
const addr = IP.fromHostname(host);
|
|
|
|
if (addr.port === 0)
|
|
addr.port = addr.key ? this.network.brontidePort : this.network.port;
|
|
|
|
for (let i = 0; i < this.nodes.length; i++) {
|
|
const node = this.nodes[i];
|
|
|
|
if (node.host !== addr.host)
|
|
continue;
|
|
|
|
if (node.port !== addr.port)
|
|
continue;
|
|
|
|
this.nodes.splice(i, 1);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Set initial seeds.
|
|
* @param {String[]} seeds
|
|
*/
|
|
|
|
setSeeds(seeds) {
|
|
this.dnsSeeds.length = 0;
|
|
|
|
for (const host of seeds)
|
|
this.addSeed(host);
|
|
}
|
|
|
|
/**
|
|
* Set priority nodes.
|
|
* @param {String[]} nodes
|
|
*/
|
|
|
|
setNodes(nodes) {
|
|
this.dnsNodes.length = 0;
|
|
this.nodes.length = 0;
|
|
|
|
for (const host of nodes)
|
|
this.addNode(host);
|
|
}
|
|
|
|
/**
|
|
* Add a local address.
|
|
* @param {String} host
|
|
* @param {Number} port
|
|
* @param {Number} score
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
addLocal(host, port, score) {
|
|
const addr = NetAddress.fromHost(host, port, null, this.network);
|
|
addr.services = this.options.services;
|
|
return this.pushLocal(addr, score);
|
|
}
|
|
|
|
/**
|
|
* Add a local address.
|
|
* @param {NetAddress} addr
|
|
* @param {Number} score
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
pushLocal(addr, score) {
|
|
if (!addr.isRoutable())
|
|
return false;
|
|
|
|
if (this.local.has(addr.hostname))
|
|
return false;
|
|
|
|
const local = new LocalAddress(addr, score);
|
|
|
|
this.local.set(addr.hostname, local);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get local address based on reachability.
|
|
* @param {NetAddress?} src
|
|
* @returns {NetAddress}
|
|
*/
|
|
|
|
getLocal(src) {
|
|
let bestReach = -1;
|
|
let bestScore = -1;
|
|
let bestDest = null;
|
|
|
|
if (!src) {
|
|
for (const dest of this.local.values()) {
|
|
// Disable everything except MANUAL
|
|
if (dest.type < HostList.scores.UPNP)
|
|
continue;
|
|
|
|
if (dest.addr.hasKey())
|
|
continue;
|
|
|
|
if (dest.score > bestScore) {
|
|
bestScore = dest.score;
|
|
bestDest = dest.addr;
|
|
}
|
|
}
|
|
|
|
return bestDest;
|
|
}
|
|
|
|
for (const dest of this.local.values()) {
|
|
// Disable everything except MANUAL
|
|
if (dest.type < HostList.scores.UPNP)
|
|
continue;
|
|
|
|
if (dest.addr.hasKey())
|
|
continue;
|
|
|
|
const reach = src.getReachability(dest.addr);
|
|
|
|
if (reach < bestReach)
|
|
continue;
|
|
|
|
if (reach > bestReach || dest.score > bestScore) {
|
|
bestReach = reach;
|
|
bestScore = dest.score;
|
|
bestDest = dest.addr;
|
|
}
|
|
}
|
|
|
|
if (bestDest)
|
|
bestDest.time = this.network.now();
|
|
|
|
return bestDest;
|
|
}
|
|
|
|
/**
|
|
* Mark local address as seen during a handshake.
|
|
* @param {NetAddress} addr
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
markLocal(addr) {
|
|
const local = this.local.get(addr.hostname);
|
|
|
|
if (!local)
|
|
return false;
|
|
|
|
local.score += 1;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Discover hosts from seeds.
|
|
* @method
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async discoverSeeds() {
|
|
const jobs = [];
|
|
|
|
for (const seed of this.dnsSeeds)
|
|
jobs.push(this.populateSeed(seed));
|
|
|
|
return Promise.all(jobs);
|
|
}
|
|
|
|
/**
|
|
* Discover hosts from nodes.
|
|
* @method
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async discoverNodes() {
|
|
const jobs = [];
|
|
|
|
for (const node of this.dnsNodes)
|
|
jobs.push(this.populateNode(node));
|
|
|
|
return Promise.all(jobs);
|
|
}
|
|
|
|
/**
|
|
* Lookup node's domain.
|
|
* @method
|
|
* @param {Object} addr
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async populateNode(addr) {
|
|
const addrs = await this.populate(addr);
|
|
|
|
if (addrs.length === 0)
|
|
return;
|
|
|
|
this.nodes.push(addrs[0]);
|
|
this.add(addrs[0]);
|
|
}
|
|
|
|
/**
|
|
* Populate from seed.
|
|
* @method
|
|
* @param {Object} seed
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async populateSeed(seed) {
|
|
const addrs = await this.populate(seed);
|
|
|
|
for (const addr of addrs)
|
|
this.add(addr);
|
|
}
|
|
|
|
/**
|
|
* Lookup hosts from dns host.
|
|
* @method
|
|
* @param {Object} target
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async populate(target) {
|
|
const addrs = [];
|
|
|
|
assert(target.type === IP.types.NONE, 'Resolved host passed.');
|
|
|
|
this.logger.info('Resolving host: %s.', target.host);
|
|
|
|
let hosts;
|
|
try {
|
|
hosts = await this.resolve(target.host);
|
|
} catch (e) {
|
|
this.logger.error(e);
|
|
return addrs;
|
|
}
|
|
|
|
for (const host of hosts) {
|
|
const addr =
|
|
NetAddress.fromHost(host, this.network.port, null, this.network);
|
|
addrs.push(addr);
|
|
}
|
|
|
|
return addrs;
|
|
}
|
|
|
|
/**
|
|
* Convert host list to json-friendly object.
|
|
* @returns {Object}
|
|
*/
|
|
|
|
toJSON() {
|
|
const addrs = [];
|
|
const fresh = [];
|
|
const used = [];
|
|
|
|
for (const entry of this.map.values())
|
|
addrs.push(entry.toJSON());
|
|
|
|
for (const bucket of this.fresh) {
|
|
const keys = [];
|
|
for (const key of bucket.keys())
|
|
keys.push(key);
|
|
fresh.push(keys);
|
|
}
|
|
|
|
for (const bucket of this.used) {
|
|
const keys = [];
|
|
for (let entry = bucket.head; entry; entry = entry.next)
|
|
keys.push(entry.key());
|
|
used.push(keys);
|
|
}
|
|
|
|
return {
|
|
version: HostList.VERSION,
|
|
network: this.network.type,
|
|
magic: this.network.magic,
|
|
key: this.key.toString('hex'),
|
|
addrs: addrs,
|
|
fresh: fresh,
|
|
used: used
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Inject properties from json object.
|
|
* @private
|
|
* @param {Object} json
|
|
* @returns {HostList}
|
|
*/
|
|
|
|
fromJSON(json) {
|
|
const sources = new Map();
|
|
const map = new Map();
|
|
const fresh = [];
|
|
const used = [];
|
|
|
|
let totalFresh = 0;
|
|
let totalUsed = 0;
|
|
|
|
assert(json && typeof json === 'object');
|
|
|
|
assert(!json.network || json.network === this.network.type,
|
|
'Network mismatch.');
|
|
|
|
assert(json.magic === this.network.magic, 'Magic mismatch.');
|
|
|
|
if (json.version < 4) {
|
|
// Migrate to v4.
|
|
for (const item of json.addrs) {
|
|
const entry = HostEntry.fromJSON(item, this.network);
|
|
const {addr, src} = entry;
|
|
const time = addr.time;
|
|
|
|
if (!entry.lastSuccess)
|
|
continue;
|
|
|
|
this.add(addr, src);
|
|
this.markAttempt(addr.hostname);
|
|
this.markSuccess(addr.hostname);
|
|
this.markAck(addr.hostname, 0);
|
|
|
|
const e = this.map.get(addr.hostname);
|
|
|
|
if (e) {
|
|
e.attempts = entry.attempts;
|
|
e.lastSuccess = entry.lastSuccess;
|
|
e.lastAttempt = entry.lastAttempt;
|
|
e.addr.time = time;
|
|
}
|
|
}
|
|
|
|
this.injectSeeds();
|
|
|
|
return this;
|
|
}
|
|
|
|
assert(json.version === HostList.VERSION,
|
|
'Bad address serialization version.');
|
|
|
|
assert(typeof json.key === 'string');
|
|
assert(Array.isArray(json.addrs));
|
|
|
|
const key = Buffer.from(json.key, 'hex');
|
|
|
|
assert(key.length === 32);
|
|
|
|
for (const addr of json.addrs) {
|
|
const entry = HostEntry.fromJSON(addr, this.network);
|
|
|
|
let src = sources.get(entry.src.hostname);
|
|
|
|
// Save some memory.
|
|
if (!src) {
|
|
src = entry.src;
|
|
sources.set(src.hostname, src);
|
|
}
|
|
|
|
entry.src = src;
|
|
|
|
map.set(entry.key(), entry);
|
|
}
|
|
|
|
assert(Array.isArray(json.fresh));
|
|
assert(json.fresh.length <= this.maxFreshBuckets,
|
|
'Buckets mismatch.');
|
|
|
|
for (const keys of json.fresh) {
|
|
const bucket = new Map();
|
|
|
|
for (const key of keys) {
|
|
const entry = map.get(key);
|
|
assert(entry);
|
|
if (entry.refCount === 0)
|
|
totalFresh += 1;
|
|
entry.refCount += 1;
|
|
bucket.set(key, entry);
|
|
}
|
|
|
|
assert(bucket.size <= this.maxEntries,
|
|
'Bucket size mismatch.');
|
|
|
|
fresh.push(bucket);
|
|
}
|
|
|
|
assert(fresh.length === this.fresh.length,
|
|
'Buckets mismatch.');
|
|
|
|
assert(Array.isArray(json.used));
|
|
assert(json.used.length <= this.maxUsedBuckets,
|
|
'Buckets mismatch.');
|
|
|
|
for (const keys of json.used) {
|
|
const bucket = new List();
|
|
|
|
for (const key of keys) {
|
|
const entry = map.get(key);
|
|
assert(entry);
|
|
assert(entry.refCount === 0);
|
|
assert(!entry.used);
|
|
entry.used = true;
|
|
totalUsed += 1;
|
|
bucket.push(entry);
|
|
}
|
|
|
|
assert(bucket.size <= this.maxEntries,
|
|
'Bucket size mismatch.');
|
|
|
|
used.push(bucket);
|
|
}
|
|
|
|
assert(used.length === this.used.length,
|
|
'Buckets mismatch.');
|
|
|
|
for (const entry of map.values())
|
|
assert(entry.used || entry.refCount > 0);
|
|
|
|
this.key = key;
|
|
this.map = map;
|
|
this.fresh = fresh;
|
|
this.totalFresh = totalFresh;
|
|
this.used = used;
|
|
this.totalUsed = totalUsed;
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Instantiate host list from json object.
|
|
* @param {Object} options
|
|
* @param {Object} json
|
|
* @returns {HostList}
|
|
*/
|
|
|
|
static fromJSON(options, json) {
|
|
return new this(options).fromJSON(json);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Number of days before considering
|
|
* an address stale.
|
|
* @const {Number}
|
|
* @default
|
|
*/
|
|
|
|
HostList.HORIZON_DAYS = 30;
|
|
|
|
/**
|
|
* Number of retries (without success)
|
|
* before considering an address stale.
|
|
* @const {Number}
|
|
* @default
|
|
*/
|
|
|
|
HostList.RETRIES = 3;
|
|
|
|
/**
|
|
* Number of days after reaching
|
|
* MAX_FAILURES to consider an
|
|
* address stale.
|
|
* @const {Number}
|
|
* @default
|
|
*/
|
|
|
|
HostList.MIN_FAIL_DAYS = 7;
|
|
|
|
/**
|
|
* Maximum number of failures
|
|
* allowed before considering
|
|
* an address stale.
|
|
* @const {Number}
|
|
* @default
|
|
*/
|
|
|
|
HostList.MAX_FAILURES = 10;
|
|
|
|
/**
|
|
* Maximum number of references
|
|
* in fresh buckets.
|
|
* @const {Number}
|
|
* @default
|
|
*/
|
|
|
|
HostList.MAX_REFS = 8;
|
|
|
|
/**
|
|
* Serialization version.
|
|
* @const {Number}
|
|
* @default
|
|
*/
|
|
|
|
HostList.VERSION = 4;
|
|
|
|
/**
|
|
* Local address scores.
|
|
* @enum {Number}
|
|
* @default
|
|
*/
|
|
|
|
HostList.scores = {
|
|
NONE: 0,
|
|
IF: 1,
|
|
BIND: 2,
|
|
DNS: 3,
|
|
UPNP: 4,
|
|
MANUAL: 5,
|
|
MAX: 6
|
|
};
|
|
|
|
/**
|
|
* Host Entry
|
|
* @alias module:net.HostEntry
|
|
* @property {NetAddress} addr - host address.
|
|
* @property {NetAddress} src - the first address we discovered this entry
|
|
* from.
|
|
* @property {Boolean} used - is it in the used set.
|
|
* @property {Number} refCount - Reference count in new buckets.
|
|
* @property {Number} attempts - connection attempts since last successful one.
|
|
* @property {Number} lastSuccess - last success timestamp.
|
|
* @property {Number} lastAttempt - last attempt timestamp.
|
|
*/
|
|
|
|
class HostEntry {
|
|
/**
|
|
* Create a host entry.
|
|
* @constructor
|
|
* @param {NetAddress} [addr]
|
|
* @param {NetAddress} [src]
|
|
*/
|
|
|
|
constructor(addr, src) {
|
|
this.addr = addr || new NetAddress();
|
|
this.src = src || new NetAddress();
|
|
this.prev = null;
|
|
this.next = null;
|
|
this.used = false;
|
|
this.refCount = 0;
|
|
this.attempts = 0;
|
|
this.lastSuccess = 0;
|
|
this.lastAttempt = 0;
|
|
|
|
if (addr)
|
|
this.fromOptions(addr, src);
|
|
}
|
|
|
|
/**
|
|
* Inject properties from options.
|
|
* @private
|
|
* @param {NetAddress} addr
|
|
* @param {NetAddress} src
|
|
* @returns {HostEntry}
|
|
*/
|
|
|
|
fromOptions(addr, src) {
|
|
assert(addr instanceof NetAddress);
|
|
assert(src instanceof NetAddress);
|
|
this.addr = addr;
|
|
this.src = src;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Instantiate host entry from options.
|
|
* @param {NetAddress} addr
|
|
* @param {NetAddress} src
|
|
* @returns {HostEntry}
|
|
*/
|
|
|
|
static fromOptions(addr, src) {
|
|
return new this().fromOptions(addr, src);
|
|
}
|
|
|
|
/**
|
|
* Get key suitable for a hash table (hostname).
|
|
* @returns {String}
|
|
*/
|
|
|
|
key() {
|
|
return this.addr.hostname;
|
|
}
|
|
|
|
/**
|
|
* Get host priority.
|
|
* @param {Number} now
|
|
* @returns {Number}
|
|
*/
|
|
|
|
chance(now) {
|
|
let c = 1;
|
|
|
|
if (now - this.lastAttempt < 60 * 10)
|
|
c *= 0.01;
|
|
|
|
c *= Math.pow(0.66, Math.min(this.attempts, 8));
|
|
|
|
return c;
|
|
}
|
|
|
|
/**
|
|
* Inspect host address.
|
|
* @returns {Object}
|
|
*/
|
|
|
|
inspect() {
|
|
return {
|
|
addr: this.addr,
|
|
src: this.src,
|
|
used: this.used,
|
|
refCount: this.refCount,
|
|
attempts: this.attempts,
|
|
lastSuccess: util.date(this.lastSuccess),
|
|
lastAttempt: util.date(this.lastAttempt)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert host entry to json-friendly object.
|
|
* @returns {Object}
|
|
*/
|
|
|
|
toJSON() {
|
|
return {
|
|
addr: this.addr.hostname,
|
|
src: this.src.hostname,
|
|
services: this.addr.services.toString(2),
|
|
time: this.addr.time,
|
|
attempts: this.attempts,
|
|
lastSuccess: this.lastSuccess,
|
|
lastAttempt: this.lastAttempt
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Inject properties from json object.
|
|
* @private
|
|
* @param {Object} json
|
|
* @param {Network} network
|
|
* @returns {HostEntry}
|
|
*/
|
|
|
|
fromJSON(json, network) {
|
|
assert(json && typeof json === 'object');
|
|
assert(typeof json.addr === 'string');
|
|
assert(typeof json.src === 'string');
|
|
|
|
this.addr.fromHostname(json.addr, network);
|
|
|
|
if (json.services != null) {
|
|
assert(typeof json.services === 'string');
|
|
assert(json.services.length > 0);
|
|
assert(json.services.length <= 32);
|
|
const services = parseInt(json.services, 2);
|
|
assert((services >>> 0) === services);
|
|
this.addr.services = services;
|
|
}
|
|
|
|
if (json.time != null) {
|
|
assert(Number.isSafeInteger(json.time));
|
|
assert(json.time >= 0);
|
|
this.addr.time = json.time;
|
|
}
|
|
|
|
if (json.src != null) {
|
|
assert(typeof json.src === 'string');
|
|
this.src.fromHostname(json.src, network);
|
|
}
|
|
|
|
if (json.attempts != null) {
|
|
assert((json.attempts >>> 0) === json.attempts);
|
|
this.attempts = json.attempts;
|
|
}
|
|
|
|
if (json.lastSuccess != null) {
|
|
assert(Number.isSafeInteger(json.lastSuccess));
|
|
assert(json.lastSuccess >= 0);
|
|
this.lastSuccess = json.lastSuccess;
|
|
}
|
|
|
|
if (json.lastAttempt != null) {
|
|
assert(Number.isSafeInteger(json.lastAttempt));
|
|
assert(json.lastAttempt >= 0);
|
|
this.lastAttempt = json.lastAttempt;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Instantiate host entry from json object.
|
|
* @param {Object} json
|
|
* @param {Network} network
|
|
* @returns {HostEntry}
|
|
*/
|
|
|
|
static fromJSON(json, network) {
|
|
return new this().fromJSON(json, network);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Local Address
|
|
* @alias module:net.LocalAddress
|
|
*/
|
|
|
|
class LocalAddress {
|
|
/**
|
|
* Create a local address.
|
|
* @constructor
|
|
* @param {NetAddress} addr
|
|
* @param {Number?} score
|
|
*/
|
|
|
|
constructor(addr, score) {
|
|
this.addr = addr;
|
|
this.type = score || 0;
|
|
this.score = score || 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Host List Options
|
|
* @alias module:net.HostListOptions
|
|
*/
|
|
|
|
class HostListOptions {
|
|
/**
|
|
* Create host list options.
|
|
* @constructor
|
|
* @param {Object?} options
|
|
*/
|
|
|
|
constructor(options) {
|
|
this.network = Network.primary;
|
|
this.logger = Logger.global;
|
|
this.resolve = lookup;
|
|
this.host = '0.0.0.0';
|
|
this.port = this.network.port;
|
|
this.services = common.LOCAL_SERVICES;
|
|
this.onion = false;
|
|
this.brontideOnly = false;
|
|
this.banTime = common.BAN_TIME;
|
|
this.random = random;
|
|
|
|
this.address = new NetAddress();
|
|
this.address.services = this.services;
|
|
this.address.time = this.network.now();
|
|
|
|
this.brontide = new NetAddress();
|
|
this.brontide.services = this.services;
|
|
this.brontide.time = this.network.now();
|
|
|
|
this.seeds = this.network.seeds;
|
|
this.nodes = [];
|
|
|
|
this.prefix = null;
|
|
this.filename = null;
|
|
this.memory = true;
|
|
this.flushInterval = 120000;
|
|
|
|
if (options)
|
|
this.fromOptions(options);
|
|
}
|
|
|
|
/**
|
|
* Inject properties from options.
|
|
* @private
|
|
* @param {Object} options
|
|
*/
|
|
|
|
fromOptions(options) {
|
|
assert(options, 'Options are required.');
|
|
|
|
if (options.network != null) {
|
|
this.network = Network.get(options.network);
|
|
this.seeds = this.network.seeds;
|
|
this.address.port = this.network.port;
|
|
this.brontide.port = this.network.brontidePort;
|
|
this.port = this.network.port;
|
|
}
|
|
|
|
if (options.logger != null) {
|
|
assert(typeof options.logger === 'object');
|
|
this.logger = options.logger;
|
|
}
|
|
|
|
if (options.resolve != null) {
|
|
assert(typeof options.resolve === 'function');
|
|
this.resolve = options.resolve;
|
|
}
|
|
|
|
if (options.banTime != null) {
|
|
assert(options.banTime >= 0);
|
|
this.banTime = options.banTime;
|
|
}
|
|
|
|
if (options.seeds) {
|
|
assert(Array.isArray(options.seeds));
|
|
this.seeds = options.seeds;
|
|
}
|
|
|
|
if (options.nodes) {
|
|
assert(Array.isArray(options.nodes));
|
|
this.nodes = options.nodes;
|
|
}
|
|
|
|
if (options.host != null)
|
|
this.host = IP.normalize(options.host);
|
|
|
|
if (options.port != null) {
|
|
assert((options.port & 0xffff) === options.port);
|
|
this.port = options.port;
|
|
}
|
|
|
|
if (options.publicHost != null) {
|
|
assert(typeof options.publicHost === 'string');
|
|
this.address.setHost(options.publicHost);
|
|
this.brontide.setHost(options.publicHost);
|
|
}
|
|
|
|
if (options.publicPort != null) {
|
|
assert((options.publicPort & 0xffff) === options.publicPort);
|
|
this.address.setPort(options.publicPort);
|
|
}
|
|
|
|
if (options.publicBrontidePort != null) {
|
|
assert((options.publicBrontidePort & 0xffff)
|
|
=== options.publicBrontidePort);
|
|
this.brontide.setPort(options.publicBrontidePort);
|
|
}
|
|
|
|
if (options.identityKey) {
|
|
assert(Buffer.isBuffer(options.identityKey),
|
|
'Identity key must be a buffer.');
|
|
assert(secp256k1.privateKeyVerify(options.identityKey),
|
|
'Invalid identity key.');
|
|
this.brontide.setKey(secp256k1.publicKeyCreate(options.identityKey));
|
|
}
|
|
|
|
if (options.services != null) {
|
|
assert(typeof options.services === 'number');
|
|
this.services = options.services;
|
|
}
|
|
|
|
if (options.onion != null) {
|
|
assert(typeof options.onion === 'boolean');
|
|
this.onion = options.onion;
|
|
}
|
|
|
|
if (options.brontideOnly != null) {
|
|
assert(typeof options.brontideOnly === 'boolean');
|
|
this.brontideOnly = options.brontideOnly;
|
|
}
|
|
|
|
if (options.memory != null) {
|
|
assert(typeof options.memory === 'boolean');
|
|
this.memory = options.memory;
|
|
}
|
|
|
|
if (options.prefix != null) {
|
|
assert(typeof options.prefix === 'string');
|
|
this.prefix = options.prefix;
|
|
this.filename = path.join(this.prefix, 'hosts.json');
|
|
}
|
|
|
|
if (options.filename != null) {
|
|
assert(typeof options.filename === 'string');
|
|
this.filename = options.filename;
|
|
}
|
|
|
|
if (options.flushInterval != null) {
|
|
assert(options.flushInterval >= 0);
|
|
this.flushInterval = options.flushInterval;
|
|
}
|
|
|
|
if (options.random != null) {
|
|
assert(typeof options.random === 'function');
|
|
this.random = options.random;;
|
|
}
|
|
|
|
this.address.time = this.network.now();
|
|
this.address.services = this.services;
|
|
|
|
this.brontide.time = this.network.now();
|
|
this.brontide.services = this.services;
|
|
|
|
return this;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
function random(max) {
|
|
// Fast insecure randomness (a la bitcoin).
|
|
return Math.floor(Math.random() * max);
|
|
}
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
HostList.HostEntry = HostEntry;
|
|
module.exports = HostList;
|