617 lines
12 KiB
JavaScript
617 lines
12 KiB
JavaScript
/*!
|
|
* netaddress.js - network address object for hsd
|
|
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
|
* https://github.com/handshake-org/hsd
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('bsert');
|
|
const bio = require('bufio');
|
|
const IP = require('binet');
|
|
const base32 = require('bcrypto/lib/encoding/base32');
|
|
const Network = require('../protocol/network');
|
|
const util = require('../utils/util');
|
|
const common = require('./common');
|
|
|
|
/** @typedef {import('net').Socket} NetSocket */
|
|
/** @typedef {import('../types').NetworkType} NetworkType */
|
|
/** @typedef {import('../types').BufioWriter} BufioWriter */
|
|
|
|
/*
|
|
* Constants
|
|
*/
|
|
|
|
const ZERO_KEY = Buffer.alloc(33, 0x00);
|
|
|
|
/**
|
|
* Net Address
|
|
* Represents a network address.
|
|
* @alias module:net.NetAddress
|
|
* @property {Host} host
|
|
* @property {Number} port
|
|
* @property {Number} services
|
|
* @property {Number} time
|
|
*/
|
|
|
|
class NetAddress extends bio.Struct {
|
|
/**
|
|
* Create a network address.
|
|
* @constructor
|
|
* @param {Object} [options]
|
|
* @param {Number?} options.time - Timestamp.
|
|
* @param {Number?} options.services - Service bits.
|
|
* @param {String?} options.host - IP address (IPv6 or IPv4).
|
|
* @param {Number?} options.port - Port.
|
|
*/
|
|
|
|
constructor(options) {
|
|
super();
|
|
|
|
this.host = '0.0.0.0';
|
|
this.port = 0;
|
|
this.services = 0;
|
|
this.time = 0;
|
|
this.hostname = '0.0.0.0:0';
|
|
this.raw = IP.ZERO_IPV4;
|
|
this.key = ZERO_KEY;
|
|
|
|
if (options)
|
|
this.fromOptions(options);
|
|
}
|
|
|
|
/**
|
|
* Inject properties from options object.
|
|
* @param {Object} options
|
|
*/
|
|
|
|
fromOptions(options) {
|
|
assert(typeof options.host === 'string',
|
|
'NetAddress requires host string.');
|
|
assert(typeof options.port === 'number',
|
|
'NetAddress requires port number.');
|
|
assert(options.port >= 0 && options.port <= 0xffff,
|
|
'port number is incorrect.');
|
|
|
|
this.raw = IP.toBuffer(options.host);
|
|
this.host = IP.toString(this.raw);
|
|
this.port = options.port;
|
|
|
|
if (options.services) {
|
|
assert(typeof options.services === 'number',
|
|
'services must be a number.');
|
|
this.services = options.services;
|
|
}
|
|
|
|
if (options.time) {
|
|
assert(typeof options.time === 'number',
|
|
'time must be a number.');
|
|
this.time = options.time;
|
|
}
|
|
|
|
if (options.key) {
|
|
assert(Buffer.isBuffer(options.key), 'key must be a buffer.');
|
|
assert(options.key.length === 33, 'key length must be 33.');
|
|
this.key = options.key;
|
|
}
|
|
|
|
this.hostname = IP.toHostname(this.host, this.port, this.key);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Test whether required services are available.
|
|
* @param {Number} services
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
hasServices(services) {
|
|
return (this.services & services) === services;
|
|
}
|
|
|
|
/**
|
|
* Test whether the address is IPv4.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isIPv4() {
|
|
return IP.isIPv4(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the address is IPv6.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isIPv6() {
|
|
return IP.isIPv6(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the address is RFC3964.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isRFC3964() {
|
|
return IP.isRFC3964(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the address is RFC4380.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isRFC4380() {
|
|
return IP.isRFC4380(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the address is RFC6052.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isRFC6052() {
|
|
return IP.isRFC6052(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the address is RFC6145.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isRFC6145() {
|
|
return IP.isRFC6145(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the host is null.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isNull() {
|
|
return IP.isNull(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the host is a local address.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isLocal() {
|
|
return IP.isLocal(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the host is valid.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isValid() {
|
|
return IP.isValid(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the host is routable.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isRoutable() {
|
|
return IP.isRoutable(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the host is an onion address.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
isOnion() {
|
|
return IP.isOnion(this.raw);
|
|
}
|
|
|
|
/**
|
|
* Test whether the peer has a key.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
hasKey() {
|
|
return !this.key.equals(ZERO_KEY);
|
|
}
|
|
|
|
/**
|
|
* Compare against another network address.
|
|
* @param {NetAddress} addr
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
equal(addr) {
|
|
return this.compare(addr) === 0;
|
|
}
|
|
|
|
/**
|
|
* Compare against another network address.
|
|
* @param {NetAddress} addr
|
|
* @returns {Number}
|
|
*/
|
|
|
|
compare(addr) {
|
|
const cmp = this.raw.compare(addr.raw);
|
|
|
|
if (cmp !== 0)
|
|
return cmp;
|
|
|
|
return this.port - addr.port;
|
|
}
|
|
|
|
/**
|
|
* Get reachable score to destination.
|
|
* @param {NetAddress} dest
|
|
* @returns {Number}
|
|
*/
|
|
|
|
getReachability(dest) {
|
|
return IP.getReachability(this.raw, dest.raw);
|
|
}
|
|
|
|
/**
|
|
* Get the canonical identifier of our network group
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
getGroup() {
|
|
return groupKey(this);
|
|
}
|
|
|
|
/**
|
|
* Set null host.
|
|
*/
|
|
|
|
setNull() {
|
|
this.raw = IP.ZERO_IPV4;
|
|
this.host = '0.0.0.0';
|
|
this.key = ZERO_KEY;
|
|
this.hostname = IP.toHostname(this.host, this.port, this.key);
|
|
}
|
|
|
|
/**
|
|
* Set host.
|
|
* @param {String} host
|
|
*/
|
|
|
|
setHost(host) {
|
|
this.raw = IP.toBuffer(host);
|
|
this.host = IP.toString(this.raw);
|
|
this.hostname = IP.toHostname(this.host, this.port, this.key);
|
|
}
|
|
|
|
/**
|
|
* Set port.
|
|
* @param {Number} port
|
|
*/
|
|
|
|
setPort(port) {
|
|
assert(port >= 0 && port <= 0xffff);
|
|
this.port = port;
|
|
this.hostname = IP.toHostname(this.host, this.port, this.key);
|
|
}
|
|
|
|
/**
|
|
* Set key.
|
|
* @param {Buffer} key
|
|
*/
|
|
|
|
setKey(key) {
|
|
if (key == null)
|
|
key = ZERO_KEY;
|
|
|
|
assert(Buffer.isBuffer(key) && key.length === 33);
|
|
|
|
this.key = key;
|
|
this.hostname = IP.toHostname(this.host, this.port, this.key);
|
|
}
|
|
|
|
/**
|
|
* Get key.
|
|
* @param {String} enc
|
|
* @returns {String|Buffer}
|
|
*/
|
|
|
|
getKey(enc) {
|
|
if (!this.hasKey())
|
|
return null;
|
|
|
|
if (enc === 'base32')
|
|
return base32.encode(this.key);
|
|
|
|
if (enc === 'hex')
|
|
return this.key.toString('hex');
|
|
|
|
return this.key;
|
|
}
|
|
|
|
/**
|
|
* Inject properties from host, port, and network.
|
|
* @param {String} host
|
|
* @param {Number} port
|
|
* @param {Buffer} [key]
|
|
* @param {(Network|NetworkType)?} [network]
|
|
*/
|
|
|
|
fromHost(host, port, key, network) {
|
|
network = Network.get(network);
|
|
|
|
assert(port >= 0 && port <= 0xffff);
|
|
assert(!key || Buffer.isBuffer(key));
|
|
assert(!key || key.length === 33);
|
|
|
|
this.raw = IP.toBuffer(host);
|
|
this.host = IP.toString(this.raw);
|
|
this.port = port;
|
|
this.services = NetAddress.DEFAULT_SERVICES;
|
|
this.time = network.now();
|
|
this.key = key || ZERO_KEY;
|
|
this.hostname = IP.toHostname(this.host, this.port, this.key);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Instantiate a network address
|
|
* from a host and port.
|
|
* @param {String} host
|
|
* @param {Number} port
|
|
* @param {Buffer} [key]
|
|
* @param {(Network|NetworkType)?} [network]
|
|
* @returns {NetAddress}
|
|
*/
|
|
|
|
static fromHost(host, port, key, network) {
|
|
return new this().fromHost(host, port, key, network);
|
|
}
|
|
|
|
/**
|
|
* Inject properties from hostname and network.
|
|
* @param {String} hostname
|
|
* @param {(Network|NetworkType)?} [network]
|
|
*/
|
|
|
|
fromHostname(hostname, network) {
|
|
network = Network.get(network);
|
|
|
|
const addr = IP.fromHostname(hostname);
|
|
|
|
if (addr.port === 0)
|
|
addr.port = addr.key ? network.brontidePort : network.port;
|
|
|
|
return this.fromHost(addr.host, addr.port, addr.key, network);
|
|
}
|
|
|
|
/**
|
|
* Instantiate a network address
|
|
* from a hostname (i.e. 127.0.0.1:8333).
|
|
* @param {String} hostname
|
|
* @param {(Network|NetworkType)?} [network]
|
|
* @returns {NetAddress}
|
|
*/
|
|
|
|
static fromHostname(hostname, network) {
|
|
return new this().fromHostname(hostname, network);
|
|
}
|
|
|
|
/**
|
|
* Inject properties from socket.
|
|
* @param {NetSocket} socket
|
|
* @param {(Network|NetworkType)?} [network]
|
|
*/
|
|
|
|
fromSocket(socket, network) {
|
|
const host = socket.remoteAddress;
|
|
const port = socket.remotePort;
|
|
assert(typeof host === 'string');
|
|
assert(typeof port === 'number');
|
|
return this.fromHost(IP.normalize(host), port, null, network);
|
|
}
|
|
|
|
/**
|
|
* Instantiate a network address
|
|
* from a socket.
|
|
* @param {NetSocket} socket
|
|
* @param {(Network|NetworkType)?} [network]
|
|
* @returns {NetAddress}
|
|
*/
|
|
|
|
static fromSocket(socket, network) {
|
|
return new this().fromSocket(socket, network);
|
|
}
|
|
|
|
/**
|
|
* Calculate serialization size of address.
|
|
* @returns {Number}
|
|
*/
|
|
|
|
getSize() {
|
|
return 88;
|
|
}
|
|
|
|
/**
|
|
* Write network address to a buffer writer.
|
|
* @param {BufioWriter} bw
|
|
* @returns {BufioWriter}
|
|
*/
|
|
|
|
write(bw) {
|
|
bw.writeU64(this.time);
|
|
bw.writeU32(this.services);
|
|
bw.writeU32(0);
|
|
bw.writeU8(0);
|
|
bw.writeBytes(this.raw);
|
|
bw.fill(0, 20); // reserved
|
|
bw.writeU16(this.port);
|
|
bw.writeBytes(this.key);
|
|
return bw;
|
|
}
|
|
|
|
/**
|
|
* Inject properties from buffer reader.
|
|
* @param {bio.BufferReader} br
|
|
*/
|
|
|
|
read(br) {
|
|
this.time = br.readU64();
|
|
this.services = br.readU32();
|
|
|
|
// Note: hi service bits
|
|
// are currently unused.
|
|
br.readU32();
|
|
|
|
if (br.readU8() === 0) {
|
|
this.raw = br.readBytes(16);
|
|
br.seek(20);
|
|
} else {
|
|
this.raw = Buffer.alloc(16, 0x00);
|
|
br.seek(36);
|
|
}
|
|
|
|
this.port = br.readU16();
|
|
this.key = br.readBytes(33);
|
|
|
|
this.host = IP.toString(this.raw);
|
|
this.hostname = IP.toHostname(this.host, this.port, this.key);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Convert net address to json-friendly object.
|
|
* @returns {Object}
|
|
*/
|
|
|
|
getJSON() {
|
|
return {
|
|
host: this.host,
|
|
port: this.port,
|
|
services: this.services,
|
|
time: this.time,
|
|
key: this.key.toString('hex')
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Inject properties from json object.
|
|
* @param {Object} json
|
|
* @returns {this}
|
|
*/
|
|
|
|
fromJSON(json) {
|
|
assert((json.port & 0xffff) === json.port);
|
|
assert((json.services >>> 0) === json.services);
|
|
assert((json.time >>> 0) === json.time);
|
|
assert(typeof json.key === 'string');
|
|
this.raw = IP.toBuffer(json.host);
|
|
this.host = json.host;
|
|
this.port = json.port;
|
|
this.services = json.services;
|
|
this.time = json.time;
|
|
this.key = Buffer.from(json.key, 'hex');
|
|
this.hostname = IP.toHostname(this.host, this.port, this.key);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Inspect the network address.
|
|
* @returns {Object}
|
|
*/
|
|
|
|
format() {
|
|
return '<NetAddress:'
|
|
+ ` hostname=${this.hostname}`
|
|
+ ` services=${this.services.toString(2)}`
|
|
+ ` date=${util.date(this.time)}`
|
|
+ '>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Default services for
|
|
* unknown outbound peers.
|
|
* @const {Number}
|
|
* @default
|
|
*/
|
|
|
|
NetAddress.DEFAULT_SERVICES = 0
|
|
| common.services.NETWORK
|
|
| common.services.BLOOM;
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
/**
|
|
* @param {NetAddress} addr
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
function groupKey(addr) {
|
|
const raw = addr.raw;
|
|
|
|
// See: https://github.com/bitcoin/bitcoin/blob/e258ce7/src/netaddress.cpp#L413
|
|
// Todo: Use IP->ASN mapping, see:
|
|
// https://github.com/bitcoin/bitcoin/blob/adea5e1/src/addrman.h#L274
|
|
let type = IP.networks.INET6; // NET_IPV6
|
|
let start = 0;
|
|
let bits = 16;
|
|
let i = 0;
|
|
|
|
if (addr.isLocal()) {
|
|
type = 255; // NET_LOCAL
|
|
bits = 0;
|
|
} else if (!addr.isRoutable()) {
|
|
type = IP.networks.NONE; // NET_UNROUTABLE
|
|
bits = 0;
|
|
} else if (addr.isIPv4() || addr.isRFC6145() || addr.isRFC6052()) {
|
|
type = IP.networks.INET4; // NET_IPV4
|
|
start = 12;
|
|
} else if (addr.isRFC3964()) {
|
|
type = IP.networks.INET4; // NET_IPV4
|
|
start = 2;
|
|
} else if (addr.isRFC4380()) {
|
|
const buf = Buffer.alloc(3);
|
|
buf[0] = IP.networks.INET4; // NET_IPV4
|
|
buf[1] = raw[12] ^ 0xff;
|
|
buf[2] = raw[13] ^ 0xff;
|
|
return buf;
|
|
} else if (addr.isOnion()) {
|
|
type = IP.networks.ONION; // NET_ONION
|
|
start = 6;
|
|
bits = 4;
|
|
} else if (raw[0] === 0x20
|
|
&& raw[1] === 0x01
|
|
&& raw[2] === 0x04
|
|
&& raw[3] === 0x70) {
|
|
bits = 36;
|
|
} else {
|
|
bits = 32;
|
|
}
|
|
|
|
const out = Buffer.alloc(1 + ((bits + 7) >>> 3));
|
|
|
|
out[i++] = type;
|
|
|
|
while (bits >= 8) {
|
|
out[i++] = raw[start++];
|
|
bits -= 8;
|
|
}
|
|
|
|
if (bits > 0)
|
|
out[i++] = raw[start] | ((1 << (8 - bits)) - 1);
|
|
|
|
assert(i === out.length);
|
|
|
|
return out;
|
|
}
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
module.exports = NetAddress;
|