Mathias Buus
3 years ago
13 changed files with 1061 additions and 1070 deletions
File diff suppressed because it is too large
@ -0,0 +1,39 @@ |
|||
// TODO: move to module so we can have udp+tcp mode also on the same port etc etc
|
|||
|
|||
const dgram = require('dgram') |
|||
|
|||
module.exports = async function bind (port) { |
|||
return new Promise((resolve, reject) => { |
|||
const socket = dgram.createSocket('udp4') |
|||
let tries = 1 |
|||
|
|||
socket.bind(port) |
|||
socket.on('listening', onlistening) |
|||
socket.on('error', onerror) |
|||
|
|||
function onlistening () { |
|||
cleanup() |
|||
resolve(socket) |
|||
} |
|||
|
|||
function onerror (err) { |
|||
if (port === 0 || tries >= 5) { |
|||
cleanup() |
|||
reject(err) |
|||
return |
|||
} |
|||
|
|||
if (++tries < 5) { |
|||
socket.bind(++port) |
|||
} else { |
|||
port = 0 |
|||
socket.bind(0) |
|||
} |
|||
} |
|||
|
|||
function cleanup () { |
|||
socket.removeListener('error', onerror) |
|||
socket.removeListener('listening', onlistening) |
|||
} |
|||
}) |
|||
} |
@ -0,0 +1,8 @@ |
|||
exports.BAD_COMMAND = 1 |
|||
exports.BAD_TOKEN = 2 |
|||
|
|||
exports.TIMEOUT = new Error('Request timed out') |
|||
exports.TIMEOUT.code = 'ETIMEDOUT' |
|||
|
|||
exports.DESTROY = new Error('Request destroyed') |
|||
exports.DESTROY.code = 'EDESTROYED' |
@ -1,25 +0,0 @@ |
|||
const sodium = require('sodium-universal') |
|||
|
|||
const addr = Buffer.alloc(6) |
|||
let i = 0 |
|||
|
|||
module.exports = hash |
|||
|
|||
function num (ip) { |
|||
let n = 0 |
|||
let c = 0 |
|||
while (i < ip.length && (c = ip.charCodeAt(i++)) !== 46) n = n * 10 + (c - 48) |
|||
return n |
|||
} |
|||
|
|||
function hash (ip, port, out = Buffer.allocUnsafe(32)) { |
|||
i = 0 |
|||
addr[0] = num(ip) |
|||
addr[1] = num(ip) |
|||
addr[2] = num(ip) |
|||
addr[3] = num(ip) |
|||
addr[4] = port |
|||
addr[5] = port >>> 8 |
|||
sodium.crypto_generichash(out, addr) |
|||
return out |
|||
} |
@ -0,0 +1,404 @@ |
|||
const FIFO = require('fast-fifo') |
|||
const sodium = require('sodium-universal') |
|||
const c = require('compact-encoding') |
|||
const peer = require('./peer') |
|||
const bind = require('./bind') |
|||
const { BAD_TOKEN, TIMEOUT, DESTROY } = require('./errors') |
|||
|
|||
const VERSION = 0b11 |
|||
const RESPONSE_ID = (0b0001 << 4) | VERSION |
|||
const REQUEST_ID = (0b0000 << 4) | VERSION |
|||
const TMP = Buffer.alloc(32) |
|||
const EMPTY_ARRAY = [] |
|||
|
|||
module.exports = class IO { |
|||
constructor (table, { maxWindow = 80, bind = 0, firewalled = true, onrequest, onresponse = noop, ontimeout = noop } = {}) { |
|||
this.table = table |
|||
this.inflight = [] |
|||
this.clientSocket = null |
|||
this.serverSocket = null |
|||
this.firewalled = firewalled !== false |
|||
this.ephemeral = true |
|||
this.congestion = new CongestionWindow(maxWindow) |
|||
|
|||
this.onrequest = onrequest |
|||
this.onresponse = onresponse |
|||
this.ontimeout = ontimeout |
|||
|
|||
this._pending = new FIFO() |
|||
this._rotateSecrets = 8 |
|||
this._tid = (Math.random() * 65536) | 0 |
|||
this._secrets = null |
|||
this._drainInterval = null |
|||
this._destroying = null |
|||
this._binding = null |
|||
this._bind = bind |
|||
} |
|||
|
|||
onmessage (socket, buffer, rinfo) { |
|||
if (buffer.byteLength < 2) return |
|||
|
|||
const from = { id: null, host: rinfo.address, port: rinfo.port } |
|||
const state = { start: 1, end: buffer.byteLength, buffer } |
|||
const expectedSocket = this.firewalled ? this.clientSocket : this.serverSocket |
|||
const external = socket !== expectedSocket |
|||
|
|||
if (buffer[0] === REQUEST_ID) { |
|||
const req = Request.decode(this, socket, from, state) |
|||
if (req === null) return |
|||
if (req.token !== null && !req.token.equals(this.token(req.from, 1)) && !req.token.equals(this.token(req.from, 0))) { |
|||
req.error(BAD_TOKEN, { token: true }) |
|||
return |
|||
} |
|||
this.onrequest(req, external) |
|||
return |
|||
} |
|||
|
|||
if (buffer[0] === RESPONSE_ID) { |
|||
const res = decodeReply(from, state) |
|||
if (res === null) return |
|||
|
|||
for (let i = 0; i < this.inflight.length; i++) { |
|||
const req = this.inflight[i] |
|||
if (req.tid !== res.tid) continue |
|||
|
|||
if (i === this.inflight.length - 1) this.inflight.pop() |
|||
else this.inflight[i] = this.inflight.pop() |
|||
|
|||
if (req._timeout) { |
|||
clearTimeout(req._timeout) |
|||
req._timeout = null |
|||
} |
|||
|
|||
this.congestion.recv() |
|||
this.onresponse(res, external) |
|||
req.onresponse(res, req) |
|||
break |
|||
} |
|||
} |
|||
} |
|||
|
|||
token (addr, i) { |
|||
if (this._secrets === null) { |
|||
const buf = Buffer.alloc(64) |
|||
this._secrets = [buf.subarray(0, 32), buf.subarray(32, 64)] |
|||
sodium.randombytes_buf(this._secrets[0]) |
|||
sodium.randombytes_buf(this._secrets[1]) |
|||
} |
|||
|
|||
const token = Buffer.allocUnsafe(32) |
|||
sodium.crypto_generichash(token, Buffer.from(addr.host), this._secrets[i]) |
|||
return token |
|||
} |
|||
|
|||
async destroy () { |
|||
if (this._destroying) return this._destroying |
|||
|
|||
// simplifies timing to await the bind here also, although it might be unneeded
|
|||
await this.bind() |
|||
|
|||
if (this._drainInterval) { |
|||
clearInterval(this._drainInterval) |
|||
this._drainInterval = null |
|||
} |
|||
|
|||
while (this.inflight.length) { |
|||
const req = this.inflight.pop() |
|||
if (req._timeout) clearTimeout(req._timeout) |
|||
req._timeout = null |
|||
req.destroyed = true |
|||
req.onerror(DESTROY, req) |
|||
} |
|||
|
|||
this._destroying = new Promise((resolve) => { |
|||
let missing = 2 |
|||
|
|||
this.serverSocket.close(done) |
|||
this.clientSocket.close(done) |
|||
|
|||
function done () { |
|||
if (--missing === 0) resolve() |
|||
} |
|||
}) |
|||
|
|||
return this._destroying |
|||
} |
|||
|
|||
bind () { |
|||
if (this._binding) return this._binding |
|||
this._binding = this._bindSockets() |
|||
return this._binding |
|||
} |
|||
|
|||
async _bindSockets () { |
|||
this.serverSocket = typeof this._bind === 'function' ? this._bind() : await bind(this._bind) |
|||
|
|||
try { |
|||
// TODO: we should reroll the socket is it's close to our preferred range of ports
|
|||
// to avoid it being accidentally opened
|
|||
// We'll prop need additional APIs for that
|
|||
this.clientSocket = await bind(0) |
|||
} catch (err) { |
|||
await new Promise((resolve) => this.serverSocket.close(resolve)) |
|||
this.serverSocket = null |
|||
throw err |
|||
} |
|||
|
|||
this.serverSocket.on('message', this.onmessage.bind(this, this.serverSocket)) |
|||
this.clientSocket.on('message', this.onmessage.bind(this, this.clientSocket)) |
|||
|
|||
if (this._drainInterval === null) { |
|||
this._drainInterval = setInterval(this._drain.bind(this), 750) |
|||
if (this._drainInterval.unref) this._drainInterval.unref() |
|||
} |
|||
|
|||
for (const req of this.inflight) { |
|||
if (!req.socket) req.socket = this.firewalled ? this.clientSocket : this.serverSocket |
|||
req.sent = 0 |
|||
req.send(false) |
|||
} |
|||
} |
|||
|
|||
_drain () { |
|||
if (this._secrets !== null && --this._rotateSecrets === 0) { |
|||
this._rotateSecrets = 8 |
|||
const tmp = this._secrets[0] |
|||
this._secrets[0] = this._secrets[1] |
|||
this._secrets[1] = tmp |
|||
sodium.crypto_generichash(tmp, tmp) |
|||
} |
|||
|
|||
this.congestion.drain() |
|||
|
|||
while (!this.congestion.isFull()) { |
|||
const p = this._pending.shift() |
|||
if (p === undefined) return |
|||
p._sendNow() |
|||
} |
|||
} |
|||
|
|||
createRequest (to, token, command, target, value) { |
|||
if (this._destroying !== null) return null |
|||
|
|||
if (this._tid === 65536) this._tid = 0 |
|||
|
|||
const tid = this._tid++ |
|||
const socket = this.firewalled ? this.clientSocket : this.serverSocket |
|||
|
|||
const req = new Request(this, socket, tid, null, to, token, command, target, value) |
|||
this.inflight.push(req) |
|||
return req |
|||
} |
|||
} |
|||
|
|||
class Request { |
|||
constructor (io, socket, tid, from, to, token, command, target, value) { |
|||
this.socket = socket |
|||
this.tid = tid |
|||
this.from = from |
|||
this.to = to |
|||
this.token = token |
|||
this.command = command |
|||
this.target = target |
|||
this.value = value |
|||
this.sent = 0 |
|||
this.destroyed = false |
|||
|
|||
this.oncycle = noop |
|||
this.onerror = noop |
|||
this.onresponse = noop |
|||
|
|||
this._buffer = null |
|||
this._io = io |
|||
this._timeout = null |
|||
} |
|||
|
|||
static decode (io, socket, from, state) { |
|||
try { |
|||
const flags = c.uint.decode(state) |
|||
const tid = c.uint16.decode(state) |
|||
const to = peer.ipv4.decode(state) |
|||
const id = flags & 1 ? c.fixed32.decode(state) : null |
|||
const token = flags & 2 ? c.fixed32.decode(state) : null |
|||
const command = c.string.decode(state) |
|||
const target = flags & 4 ? c.fixed32.decode(state) : null |
|||
const value = flags & 8 ? c.buffer.decode(state) : null |
|||
|
|||
if (id !== null) from.id = validateId(id, from) |
|||
|
|||
return new Request(io, socket, tid, from, to, token, command, target, value) |
|||
} catch { |
|||
return null |
|||
} |
|||
} |
|||
|
|||
reply (value, opts = {}) { |
|||
this.sendReply(0, value || null, opts.token !== false, this.target !== null && opts.closerNodes !== false) |
|||
} |
|||
|
|||
error (code, opts = {}) { |
|||
this.sendReply(code, null, false, this.target !== null && opts.closerNodes !== false) |
|||
} |
|||
|
|||
send (force = false) { |
|||
if (this.destroyed) return |
|||
|
|||
if (this.socket === null) return |
|||
if (this._buffer === null) this._buffer = this._encodeRequest() |
|||
|
|||
if (!force && this._io.congestion.isFull()) { |
|||
this._io._pending.push(this) |
|||
return |
|||
} |
|||
|
|||
this._sendNow() |
|||
} |
|||
|
|||
_sendNow () { |
|||
if (this.destroyed) return |
|||
this.sent++ |
|||
this._io.congestion.send() |
|||
this.socket.send(this._buffer, 0, this._buffer.byteLength, this.to.port, this.to.host) |
|||
if (this._timeout) clearTimeout(this._timeout) |
|||
this._timeout = setTimeout(oncycle, 1000, this) |
|||
} |
|||
|
|||
destroy (err) { |
|||
if (this.destroyed) return |
|||
this.destroyed = true |
|||
|
|||
const i = this._io.inflight.indexOf(this) |
|||
if (i === -1) return |
|||
|
|||
if (i === this._io.inflight.length - 1) this._io.inflight.pop() |
|||
else this._io.inflight[i] = this._io.inflight.pop() |
|||
|
|||
this.onerror(err || DESTROY, this) |
|||
} |
|||
|
|||
sendReply (error, value, token, hasCloserNodes) { |
|||
if (this.socket === null || this.destroyed) return |
|||
|
|||
const id = this._io.ephemeral === false && this.socket === this._io.serverSocket |
|||
const closerNodes = hasCloserNodes ? this._io.table.closest(this.target) : EMPTY_ARRAY |
|||
const state = { start: 0, end: 1 + 1 + 6 + 2, buffer: null } // (type | version) + flags + to + tid
|
|||
|
|||
if (id) state.end += 32 |
|||
if (token) state.end += 32 |
|||
if (closerNodes.length > 0) peer.ipv4Array.preencode(state, closerNodes) |
|||
if (error > 0) c.uint.preencode(state, error) |
|||
if (value) c.buffer.preencode(state, value) |
|||
|
|||
state.buffer = Buffer.allocUnsafe(state.end) |
|||
state.buffer[state.start++] = RESPONSE_ID |
|||
state.buffer[state.start++] = (id ? 1 : 0) | (token ? 2 : 0) | (closerNodes.length > 0 ? 4 : 0) | (error > 0 ? 8 : 0) | (value ? 16 : 0) |
|||
|
|||
c.uint16.encode(state, this.tid) |
|||
peer.ipv4.encode(state, this.from) |
|||
|
|||
if (id) c.fixed32.encode(state, this._io.table.id) |
|||
if (token) c.fixed32.encode(state, this._io.token(this.to, 1)) |
|||
if (closerNodes.length > 0) peer.ipv4Array.encode(state, closerNodes) |
|||
if (error > 0) c.uint.encode(state, error) |
|||
if (value) c.buffer.encode(state, value) |
|||
|
|||
this.socket.send(state.buffer, 0, state.buffer.byteLength, this.from.port, this.from.host) |
|||
} |
|||
|
|||
_encodeRequest () { |
|||
const id = this._io.ephemeral === false && this.socket === this._io.serverSocket |
|||
const state = { start: 0, end: 1 + 1 + 6 + 2, buffer: null } // (type | version) + flags + to + tid
|
|||
|
|||
if (id) state.end += 32 |
|||
if (this.token) state.end += 32 |
|||
|
|||
c.string.preencode(state, this.command) |
|||
|
|||
if (this.target) state.end += 32 |
|||
if (this.value) c.buffer.preencode(state, this.value) |
|||
|
|||
state.buffer = Buffer.allocUnsafe(state.end) |
|||
state.buffer[state.start++] = REQUEST_ID |
|||
state.buffer[state.start++] = (id ? 1 : 0) | (this.token ? 2 : 0) | (this.target ? 4 : 0) | (this.value ? 8 : 0) |
|||
|
|||
c.uint16.encode(state, this.tid) |
|||
peer.ipv4.encode(state, this.to) |
|||
|
|||
if (id) c.fixed32.encode(state, this._io.table.id) |
|||
if (this.token) c.fixed32.encode(state, this.token) |
|||
|
|||
c.string.encode(state, this.command) |
|||
|
|||
if (this.target) c.fixed32.encode(state, this.target) |
|||
if (this.value) c.buffer.encode(state, this.value) |
|||
|
|||
return state.buffer |
|||
} |
|||
} |
|||
|
|||
class CongestionWindow { |
|||
constructor (maxWindow) { |
|||
this._i = 0 |
|||
this._total = 0 |
|||
this._window = [0, 0, 0, 0] |
|||
this._maxWindow = maxWindow |
|||
} |
|||
|
|||
isFull () { |
|||
return this._total >= 2 * this._maxWindow || this._window[this._i] >= this._maxWindow |
|||
} |
|||
|
|||
recv () { |
|||
if (this._window[this._i] > 0) { |
|||
this._window[this._i]-- |
|||
this._total-- |
|||
} |
|||
} |
|||
|
|||
send () { |
|||
this._total++ |
|||
this._window[this._i]++ |
|||
} |
|||
|
|||
drain () { |
|||
this._i = (this._i + 1) & 3 |
|||
this._total -= this._window[this._i] |
|||
this._window[this._i] = 0 // clear oldest
|
|||
} |
|||
} |
|||
|
|||
function noop () {} |
|||
|
|||
function oncycle (req) { |
|||
req._timeout = null |
|||
req.oncycle(req) |
|||
if (req.sent === 3) { |
|||
req.destroy(TIMEOUT) |
|||
req._io.ontimeout(req) |
|||
} else { |
|||
req.send() |
|||
} |
|||
} |
|||
|
|||
function decodeReply (from, state) { |
|||
const flags = c.uint.decode(state) |
|||
const tid = c.uint16.decode(state) |
|||
const to = peer.ipv4.decode(state) |
|||
const id = flags & 1 ? c.fixed32.decode(state) : null |
|||
const token = flags & 2 ? c.fixed32.decode(state) : null |
|||
const closerNodes = flags & 4 ? peer.ipv4Array.decode(state) : null |
|||
const error = flags & 8 ? c.uint.decode(state) : 0 |
|||
const value = flags & 16 ? c.buffer.decode(state) : null |
|||
|
|||
if (id !== null) from.id = validateId(id, from) |
|||
|
|||
try { |
|||
return { tid, from, to, token, closerNodes, error, value } |
|||
} catch { |
|||
return null |
|||
} |
|||
} |
|||
|
|||
function validateId (id, from) { |
|||
return peer.id(from.host, from.port, TMP).equals(id) ? id : null |
|||
} |
@ -1,107 +0,0 @@ |
|||
const cenc = require('compact-encoding') |
|||
|
|||
const IPv4 = exports.IPv4 = { |
|||
preencode (state, ip) { |
|||
state.end += 4 |
|||
}, |
|||
encode (state, ip) { // TODO: move over fast parser from ./id.js
|
|||
const nums = ip.split('.') |
|||
state.buffer[state.start++] = Number(nums[0]) || 0 |
|||
state.buffer[state.start++] = Number(nums[1]) || 0 |
|||
state.buffer[state.start++] = Number(nums[2]) || 0 |
|||
state.buffer[state.start++] = Number(nums[3]) || 0 |
|||
}, |
|||
decode (state) { |
|||
if (state.end - state.start < 4) throw new Error('Out of bounds') |
|||
return state.buffer[state.start++] + '.' + state.buffer[state.start++] + '.' + state.buffer[state.start++] + '.' + state.buffer[state.start++] |
|||
} |
|||
} |
|||
|
|||
const peerIPv4 = exports.peerIPv4 = { |
|||
preencode (state, peer) { |
|||
state.end += 6 |
|||
}, |
|||
encode (state, peer) { |
|||
IPv4.encode(state, peer.host) |
|||
cenc.uint16.encode(state, peer.port) |
|||
}, |
|||
decode (state) { |
|||
return { |
|||
host: IPv4.decode(state), |
|||
port: cenc.uint16.decode(state) |
|||
} |
|||
} |
|||
} |
|||
|
|||
const peerIPv4Array = exports.peerIPv4Array = cenc.array(peerIPv4) |
|||
|
|||
const IS_REQUEST = 0b0001 |
|||
const HAS_ID = 0b0010 |
|||
const HAS_TOKEN = 0b0100 |
|||
const ROUTE_INFO = 0b1000 | IS_REQUEST |
|||
const HAS_TARGET = ROUTE_INFO | IS_REQUEST |
|||
const HAS_CLOSER_NODES = ROUTE_INFO ^ IS_REQUEST |
|||
|
|||
exports.message = { |
|||
preencode (state, m) { |
|||
state.end += 1 // version
|
|||
state.end += 1 // flags
|
|||
state.end += 2 // tid
|
|||
state.end += 6 // to
|
|||
|
|||
if (m.id) state.end += 32 |
|||
if (m.token) state.end += 32 |
|||
if (m.target) state.end += 32 |
|||
if (m.closerNodes && m.closerNodes.length) peerIPv4Array.preencode(state, m.closerNodes) |
|||
if (m.command) cenc.string.preencode(state, m.command) |
|||
else cenc.uint.preencode(state, m.status) |
|||
|
|||
cenc.buffer.preencode(state, m.value) |
|||
}, |
|||
encode (state, m) { |
|||
const closerNodes = m.closerNodes || [] |
|||
const flags = (m.id ? HAS_ID : 0) | |
|||
(m.token ? HAS_TOKEN : 0) | |
|||
(closerNodes.length ? HAS_CLOSER_NODES : 0) | |
|||
(m.target ? HAS_TARGET : 0) | |
|||
(m.command ? IS_REQUEST : 0) |
|||
|
|||
state.buffer[state.start++] = 2 |
|||
state.buffer[state.start++] = flags |
|||
|
|||
cenc.uint16.encode(state, m.tid) |
|||
peerIPv4.encode(state, m.to) |
|||
|
|||
if ((flags & HAS_ID) === HAS_ID) cenc.fixed32.encode(state, m.id) |
|||
if ((flags & HAS_TOKEN) === HAS_TOKEN) cenc.fixed32.encode(state, m.token) |
|||
if ((flags & ROUTE_INFO) === HAS_TARGET) cenc.fixed32.encode(state, m.target) |
|||
if ((flags & ROUTE_INFO) === HAS_CLOSER_NODES) peerIPv4Array.encode(state, closerNodes) |
|||
if ((flags & IS_REQUEST) === IS_REQUEST) cenc.string.encode(state, m.command) |
|||
if ((flags & IS_REQUEST) === 0) cenc.uint.encode(state, m.status) |
|||
|
|||
cenc.buffer.encode(state, m.value) |
|||
}, |
|||
decode (state) { |
|||
const version = state.buffer[state.start++] |
|||
|
|||
if (version !== 2) { |
|||
throw new Error('Incompatible version') |
|||
} |
|||
|
|||
const flags = cenc.uint.decode(state) |
|||
|
|||
return { |
|||
version: 2, |
|||
tid: cenc.uint16.decode(state), |
|||
from: null, // populated in caller
|
|||
to: peerIPv4.decode(state), |
|||
id: (flags & HAS_ID) === HAS_ID ? cenc.fixed32.decode(state) : null, |
|||
token: (flags & HAS_TOKEN) === HAS_TOKEN ? cenc.fixed32.decode(state) : null, |
|||
target: ((flags & ROUTE_INFO) === HAS_TARGET) ? cenc.fixed32.decode(state) : null, |
|||
closerNodes: ((flags & ROUTE_INFO) === HAS_CLOSER_NODES) ? peerIPv4Array.decode(state) : null, |
|||
command: ((flags & IS_REQUEST) === IS_REQUEST) ? cenc.string.decode(state) : null, |
|||
status: ((flags & IS_REQUEST) === 0) ? cenc.uint.decode(state) : 0, |
|||
value: cenc.buffer.decode(state) |
|||
} |
|||
} |
|||
} |
@ -1,117 +0,0 @@ |
|||
// how far can the port median distance be?
|
|||
const INCREMENTING_THRESHOLD = 200 |
|||
|
|||
class NatAnalyzer { |
|||
constructor (sampleSize) { |
|||
// sampleSize must be 2^n
|
|||
this.samples = new Array(sampleSize) |
|||
this.length = 0 |
|||
this.top = 0 |
|||
} |
|||
|
|||
sample (referrer) { |
|||
for (let i = 0; i < this.length; i++) { |
|||
const s = this.samples[i] |
|||
const r = s.referrer |
|||
if (r.port === referrer.port && r.host === referrer.host) return s |
|||
} |
|||
return null |
|||
} |
|||
|
|||
add (addr, referrer) { |
|||
if (this.length < this.samples.length) this.length++ |
|||
this.samples[this.top] = { port: addr.port, host: addr.host, dist: 0, referrer } |
|||
this.top = (this.top + 1) & (this.samples.length - 1) |
|||
} |
|||
|
|||
analyze (minSamples = 3) { |
|||
if (this.length < minSamples) return { type: NatAnalyzer.UNKNOWN, host: null, port: 0 } |
|||
|
|||
const samples = this.samples.slice(0, this.length) |
|||
const hosts = new Map() |
|||
|
|||
let bestHost = null |
|||
let bestHits = 0 |
|||
|
|||
for (let i = 0; i < samples.length; i++) { |
|||
const host = samples[i].host |
|||
const hits = (hosts.get(host) || 0) + 1 |
|||
|
|||
hosts.set(host, hits) |
|||
|
|||
if (hits > bestHits) { |
|||
bestHits = hits |
|||
bestHost = host |
|||
} |
|||
} |
|||
|
|||
if (bestHits < (samples.length >> 1)) { |
|||
return { type: NatAnalyzer.UNKNOWN, host: null, port: 0 } |
|||
} |
|||
|
|||
samples.sort(cmpPort) |
|||
|
|||
let start = 0 |
|||
let end = samples.length |
|||
let mid = samples[samples.length >> 1].port |
|||
|
|||
// remove the 3 biggest outliers from the median if we have more than 6 samples
|
|||
if (samples.length >= 6) { |
|||
for (let i = 0; i < 3; i++) { |
|||
const s = samples[start] |
|||
const e = samples[end - 1] |
|||
|
|||
if (Math.abs(mid - s.port) < Math.abs(mid - e.port)) end-- |
|||
else start++ |
|||
} |
|||
} |
|||
|
|||
const len = end - start |
|||
mid = samples[len >> 1].port |
|||
|
|||
for (let i = 0; i < samples.length; i++) { |
|||
samples[i].dist = Math.abs(mid - samples[i].port) |
|||
} |
|||
|
|||
// note that still sorts with the outliers which is why we just start=0, end=len-1 below
|
|||
samples.sort(cmpDist) |
|||
mid = samples[len >> 1].dist |
|||
|
|||
if (samples[0].dist === 0 && samples[len - 1].dist === 0) { |
|||
return { |
|||
type: NatAnalyzer.PORT_CONSISTENT, |
|||
host: bestHost, |
|||
port: samples[0].port |
|||
} |
|||
} |
|||
|
|||
if (mid < INCREMENTING_THRESHOLD) { |
|||
return { |
|||
type: NatAnalyzer.PORT_INCREMENTING, |
|||
host: bestHost, |
|||
port: 0 |
|||
} |
|||
} |
|||
|
|||
return { |
|||
type: NatAnalyzer.PORT_RANDOMIZED, |
|||
host: bestHost, |
|||
port: 0 |
|||
} |
|||
} |
|||
} |
|||
|
|||
NatAnalyzer.UNKNOWN = Symbol.for('NAT_UNKNOWN') |
|||
NatAnalyzer.PORT_CONSISTENT = Symbol.for('NAT_PORT_CONSISTENT') |
|||
NatAnalyzer.PORT_INCREMENTING = Symbol.for('NAT_PORT_INCREMENTING') |
|||
NatAnalyzer.PORT_RANDOMIZED = Symbol.for('NAT_PORT_RANDOMIZED') |
|||
|
|||
module.exports = NatAnalyzer |
|||
|
|||
function cmpDist (a, b) { |
|||
return a.dist - b.dist |
|||
} |
|||
|
|||
function cmpPort (a, b) { |
|||
return a.port - b.port |
|||
} |
@ -0,0 +1,49 @@ |
|||
const sodium = require('sodium-universal') |
|||
const c = require('compact-encoding') |
|||
|
|||
const addr = Buffer.alloc(6) |
|||
let i = 0 |
|||
|
|||
const ipv4 = { |
|||
preencode (state, p) { |
|||
state.end += 6 |
|||
}, |
|||
encode (state, p) { |
|||
i = 0 |
|||
state.buffer[state.start++] = num(p.host) |
|||
state.buffer[state.start++] = num(p.host) |
|||
state.buffer[state.start++] = num(p.host) |
|||
state.buffer[state.start++] = num(p.host) |
|||
state.buffer[state.start++] = p.port |
|||
state.buffer[state.start++] = p.port >>> 8 |
|||
}, |
|||
decode (state) { |
|||
if (state.end - state.start < 6) throw new Error('Out of bounds') |
|||
return { |
|||
id: null, // populated elsewhere
|
|||
host: state.buffer[state.start++] + '.' + state.buffer[state.start++] + '.' + state.buffer[state.start++] + '.' + state.buffer[state.start++], |
|||
port: state.buffer[state.start++] + 256 * state.buffer[state.start++] |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { id, ipv4, ipv4Array: c.array(ipv4) } |
|||
|
|||
function num (ip) { |
|||
let n = 0 |
|||
let c = 0 |
|||
while (i < ip.length && (c = ip.charCodeAt(i++)) !== 46) n = n * 10 + (c - 48) |
|||
return n |
|||
} |
|||
|
|||
function id (ip, port, out = Buffer.allocUnsafe(32)) { |
|||
i = 0 |
|||
addr[0] = num(ip) |
|||
addr[1] = num(ip) |
|||
addr[2] = num(ip) |
|||
addr[3] = num(ip) |
|||
addr[4] = port |
|||
addr[5] = port >>> 8 |
|||
sodium.crypto_generichash(out, addr) |
|||
return out |
|||
} |
@ -1,16 +0,0 @@ |
|||
module.exports = async function race (p, min = 1, max = p.length) { |
|||
let errors = 0 |
|||
const results = [] |
|||
// avoid unhandled rejections after early return/throw
|
|||
for (const promise of p) promise.catch(() => {}) |
|||
for (let i = 0; i < p.length; i++) { |
|||
try { |
|||
const res = await p[i] |
|||
if (results.length < max) results.push(res) |
|||
if (results.length >= max) return results |
|||
if (results.length + errors === p.length) return results |
|||
} catch { |
|||
if ((p.length - ++errors) < min) throw new Error('Too many requests failed') |
|||
} |
|||
} |
|||
} |
@ -1,255 +0,0 @@ |
|||
const dgram = require('dgram') |
|||
const { message } = require('./messages') |
|||
|
|||
module.exports = class RPC { |
|||
constructor (opts = {}) { |
|||
this._pendingSends = [] |
|||
this._tid = (Math.random() * 65536) | 0 |
|||
this._drainInterval = null |
|||
this._tick = 0 |
|||
this._w = 0 |
|||
this._win = [0, 0, 0, 0] |
|||
this._bind = opts.bind || 0 |
|||
this._bound = false |
|||
this._binding = null |
|||
|
|||
this.maxWindow = opts.maxWindow || 80 // ~100 per second burst, ~80 per second avg
|
|||
this.maxRetries = 3 |
|||
this.destroyed = false |
|||
this.inflight = [] |
|||
this.onrequest = opts.onrequest || noop |
|||
this.onresponse = opts.onresponse || noop |
|||
this.onwarning = opts.onwarning || noop |
|||
this.socket = opts.socket || dgram.createSocket('udp4') |
|||
this.socket.on('message', this.onmessage.bind(this, true)) |
|||
} |
|||
|
|||
get inflightRequests () { |
|||
return this.inflight.length |
|||
} |
|||
|
|||
send (m, socket = this.socket) { |
|||
const state = { start: 0, end: 0, buffer: null } |
|||
|
|||
message.preencode(state, m) |
|||
state.buffer = Buffer.allocUnsafe(state.end) |
|||
message.encode(state, m) |
|||
|
|||
this._send(socket, state.buffer, m.to) |
|||
} |
|||
|
|||
reply (req, reply, socket = this.socket) { |
|||
reply.tid = req.tid |
|||
reply.to = req.from |
|||
this.send(reply, socket) |
|||
} |
|||
|
|||
address () { |
|||
return this.socket.address() |
|||
} |
|||
|
|||
bind (port = this._bind) { |
|||
if (this._binding) return this._binding |
|||
|
|||
const self = this |
|||
|
|||
this._binding = new Promise((resolve, reject) => { |
|||
const s = this.socket |
|||
|
|||
s.bind(port) |
|||
s.on('listening', onlistening) |
|||
s.on('error', onerror) |
|||
|
|||
function onlistening () { |
|||
self._bound = true |
|||
|
|||
s.removeListener('listening', onlistening) |
|||
s.removeListener('error', onerror) |
|||
resolve(s.address().port) |
|||
} |
|||
|
|||
function onerror (err) { |
|||
// retry on any port if preferred port is unavail
|
|||
if (port === self._bind && port !== 0) { |
|||
port = 0 |
|||
s.bind(0) |
|||
return |
|||
} |
|||
|
|||
s.removeListener('listening', onlistening) |
|||
s.removeListener('error', onerror) |
|||
reject(err) |
|||
} |
|||
}) |
|||
|
|||
return this._binding |
|||
} |
|||
|
|||
destroy () { |
|||
if (this.destroyed) return |
|||
this.unwrap(true) |
|||
this.socket.close() |
|||
} |
|||
|
|||
unwrap (closing = false) { |
|||
if (this.destroyed) return |
|||
this.destroyed = true |
|||
|
|||
clearInterval(this._drainInterval) |
|||
this.socket.removeAllListeners() |
|||
|
|||
for (const req of this.inflight) { |
|||
req.reject(new Error('RPC socket destroyed')) |
|||
} |
|||
|
|||
this.inflight = [] |
|||
return this.socket |
|||
} |
|||
|
|||
async request (m, opts) { |
|||
if (this.destroyed) throw new Error('RPC socket destroyed') |
|||
|
|||
const socket = (opts && opts.socket) || this.socket |
|||
|
|||
if (!this._bound && socket === this.socket) await this.bind() |
|||
|
|||
if (this._drainInterval === null) { |
|||
this._drainInterval = setInterval(this._drain.bind(this), 750) |
|||
if (this._drainInterval.unref) this._drainInterval.unref() |
|||
} |
|||
|
|||
m.tid = this._tid++ |
|||
if (this._tid === 65536) this._tid = 0 |
|||
|
|||
const state = { start: 0, end: 0, buffer: null } |
|||
|
|||
message.preencode(state, m) |
|||
state.buffer = Buffer.allocUnsafe(state.end) |
|||
message.encode(state, m) |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
const total = this._win[0] + this._win[1] + this._win[2] + this._win[3] |
|||
const req = { |
|||
socket, |
|||
timeout: 2, |
|||
expectOk: !!(opts && opts.expectOk !== false), |
|||
tries: (opts && opts.retry === false) ? this.maxRetries : 0, |
|||
tid: m.tid, |
|||
buffer: state.buffer, |
|||
to: m.to, |
|||
resolve, |
|||
reject |
|||
} |
|||
|
|||
this.inflight.push(req) |
|||
|
|||
if (total < 2 * this.maxWindow && this._win[this._w] < this.maxWindow) { |
|||
this._win[this._w]++ |
|||
req.tries++ |
|||
this._send(req.socket, req.buffer, req.to) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
onmessage (sample, buffer, rinfo) { |
|||
const from = { host: rinfo.address, port: rinfo.port } |
|||
if (!from.port) return |
|||
if (buffer.byteLength <= 1) return |
|||
|
|||
const state = { start: 0, end: buffer.byteLength, buffer } |
|||
let m = null |
|||
|
|||
try { |
|||
m = message.decode(state) |
|||
} catch (err) { |
|||
this.onwarning(err) |
|||
return |
|||
} |
|||
|
|||
m.from = from |
|||
|
|||
if (m.command !== null) { // request
|
|||
if (this.onrequest === noop) return |
|||
this.onrequest(m, sample) |
|||
return |
|||
} |
|||
|
|||
const req = this._dequeue(m.tid) |
|||
|
|||
if (req === null) return |
|||
if (m.id && (req.to.port !== from.port || req.to.host !== from.host)) m.id = null |
|||
|
|||
// decrement the inflight window as this is an "ack"
|
|||
if (this._win[this._w] > 0) this._win[this._w]-- |
|||
this.onresponse(m, sample) |
|||
|
|||
if (m.status === 0 || req.expectOk === false) { |
|||
req.resolve(m) |
|||
} else { |
|||
req.reject(createStatusError(m.status)) |
|||
} |
|||
} |
|||
|
|||
_send (socket, buf, addr) { |
|||
if (this.destroyed) return |
|||
socket.send(buf, 0, buf.byteLength, addr.port, addr.host) |
|||
} |
|||
|
|||
_dequeue (tid) { |
|||
for (let i = 0; i < this.inflight.length; i++) { |
|||
const req = this.inflight[i] |
|||
|
|||
if (req.tid === tid) { |
|||
if (i === this.inflight.length - 1) this.inflight.pop() |
|||
else this.inflight[i] = this.inflight.pop() |
|||
return req |
|||
} |
|||
} |
|||
|
|||
return null |
|||
} |
|||
|
|||
_drain () { |
|||
let total = this._win[0] + this._win[1] + this._win[2] + this._win[3] |
|||
|
|||
for (let i = 0; i < this.inflight.length; i++) { |
|||
const req = this.inflight[i] |
|||
|
|||
if (req.tries > 0 && --req.timeout >= 0) continue |
|||
req.timeout = 2 |
|||
|
|||
if (req.tries++ > this.maxRetries) { |
|||
if (i === this.inflight.length - 1) this.inflight.pop() |
|||
else this.inflight[i] = this.inflight.pop() |
|||
req.reject(createTimeoutError()) |
|||
continue |
|||
} |
|||
|
|||
if (total >= 2 * this.maxWindow || this._win[this._w] >= this.maxWindow) { |
|||
req.tries-- |
|||
continue |
|||
} |
|||
|
|||
total++ |
|||
this._win[this._w]++ |
|||
this._send(req.socket, req.buffer, req.to) |
|||
} |
|||
|
|||
this._w = (this._w + 1) & 3 |
|||
this._win[this._w] = 0 // clear oldest
|
|||
} |
|||
} |
|||
|
|||
function createTimeoutError () { |
|||
const err = new Error('Request timed out') |
|||
err.status = 0 |
|||
return err |
|||
} |
|||
|
|||
function createStatusError (status) { |
|||
const err = new Error('Request failed with status ' + status) |
|||
err.status = status |
|||
return err |
|||
} |
|||
|
|||
function noop () {} |
Loading…
Reference in new issue