From 2ebf446cb4c8d8f0ff465f933eadd1c00e8bbbe0 Mon Sep 17 00:00:00 2001 From: Mathias Buus Date: Fri, 23 Apr 2021 13:50:32 +0200 Subject: [PATCH] add remoteAddress backed by the nat analyzer --- index.js | 15 +++++- lib/nat-analyzer.js | 108 ++++++++++++++++++++++++++++++++++++++++++++ test.js | 6 +++ 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 lib/nat-analyzer.js diff --git a/index.js b/index.js index ee30b60..5c56713 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ const dns = require('dns') const RPC = require('./lib/rpc') const Query = require('./lib/query') +const NatAnalyzer = require('./lib/nat-analyzer') const Table = require('kademlia-routing-table') const TOS = require('time-ordered-set') const FIFO = require('fast-fifo/fixed-size') @@ -73,6 +74,7 @@ class DHT extends EventEmitter { this._refreshTick = this._tick + REFRESH_TICKS this._stableTick = this._tick + STABLE_TICKS this._tickInterval = setInterval(this._ontick.bind(this), TICK_INTERVAL) + this._nat = new NatAnalyzer(opts.natSampleSize || 16) this.table.on('row', (row) => row.on('full', (node) => this._onfullrow(node, row))) } @@ -341,19 +343,20 @@ class DHT extends EventEmitter { if (oldNode) { if (oldNode.port === m.from.port && oldNode.host === m.from.host) { // refresh it - oldNode.to = m.to oldNode.seen = this._tick this.nodes.add(oldNode) } return } + // add a sample of our address from the remote nodes pov + this._nat.add(m.to) + this._addNode({ id: m.nodeId, token: null, port: m.from.port, host: m.from.host, - to: m.to, added: this._tick, seen: this._tick, prev: null, @@ -399,6 +402,10 @@ class DHT extends EventEmitter { return this.rpc.address() } + remoteAddress () { + return this._nat.analyze() + } + _reply (rpc, tid, target, status, value, token, to) { const closerNodes = target ? this.table.closest(target) : null const persistent = !this.ephemeral && rpc === this.rpc @@ -422,6 +429,10 @@ class DHT extends EventEmitter { DHT.OK = 0 DHT.UNKNOWN_COMMAND = 1 DHT.BAD_TOKEN = 2 +DHT.NAT_UNKNOWN = NatAnalyzer.UNKNOWN +DHT.NAT_PORT_CONSISTENT = NatAnalyzer.PORT_CONSISTENT +DHT.NAT_PORT_INCREMENTING = NatAnalyzer.PORT_INCREMENTING +DHT.NAT_PORT_RANDOMIZED = NatAnalyzer.PORT_RANDOMIZED module.exports = DHT diff --git a/lib/nat-analyzer.js b/lib/nat-analyzer.js new file mode 100644 index 0000000..66b1abc --- /dev/null +++ b/lib/nat-analyzer.js @@ -0,0 +1,108 @@ +// 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 + } + + add (addr) { + if (this.length < this.samples.length) this.length++ + this.samples[this.top] = { port: addr.port, host: addr.host, dist: 0 } + this.top = (this.top + 1) & (this.samples.length - 1) + } + + analyze () { + if (this.length <= 2) 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_RANDOM') + +module.exports = NatAnalyzer + +function cmpDist (a, b) { + return a.dist - b.dist +} + +function cmpPort (a, b) { + return a.port - b.port +} diff --git a/test.js b/test.js index 8498467..aa926f1 100644 --- a/test.js +++ b/test.js @@ -40,6 +40,12 @@ tape('make bigger swarm', async function (t) { t.ok(found, 'found target again in ' + messages + ' message(s)') + const { type, host, port } = swarm[490].remoteAddress() + + t.same(type, DHT.NAT_PORT_CONSISTENT) + t.same(port, swarm[490].address().port) + t.ok(host) + destroy(swarm) })