hyperdht
Version:
The DHT powering Hyperswarm
202 lines (159 loc) • 4.69 kB
JavaScript
const { FIREWALL } = require('../lib/constants')
module.exports = class Nat {
constructor (dht, session, socket) {
this._samplesHost = []
this._samplesFull = []
this._visited = new Map()
this._resolve = null
this._minSamples = 4
this._autoSampling = false
this.dht = dht
this.session = session
this.socket = socket
this.sampled = 0
this.firewall = dht.firewalled ? FIREWALL.UNKNOWN : FIREWALL.OPEN
this.addresses = null
this.analyzing = new Promise((resolve) => { this._resolve = resolve })
}
autoSample (retry = true) {
if (this._autoSampling) return
this._autoSampling = true
const self = this
const socket = this.socket
const maxPings = this._minSamples
let skip = this.dht.nodes.length >= 8 ? 5 : 0
let pending = 0
// TODO: it would be best to pick the nodes to help us based on latency to us
// That should reduce connect latency in general. We should investigate tracking that later on.
// TODO 2: try to pick nodes with different IPs as well, as that'll help multi IP cell connections...
// If we expose this from the nat sampler then the DHT should be able to help us filter out scams as well...
for (let node = this.dht.nodes.latest; node && this.sampled + pending < maxPings; node = node.prev) {
if (skip > 0) {
skip--
continue
}
const ref = node.host + ':' + node.port
if (this._visited.has(ref)) continue
this._visited.set(ref, 1)
pending++
this.session.ping(node, { socket, retry: false }).then(onpong, onskip)
}
pending++
onskip()
function onpong (res) {
self.add(res.to, res.from)
onskip()
}
function onskip () {
if (--pending === 0 && self.sampled < self._minSamples) {
if (retry) {
self._autoSampling = false
self.autoSample(false)
return
}
self._resolve()
}
}
}
destroy () {
this._autoSampling = true
this._minSamples = 0
this._resolve()
}
unfreeze () {
this.frozen = false
this._updateFirewall()
this._updateAddresses()
}
freeze () {
this.frozen = true
}
_updateFirewall () {
if (!this.dht.firewalled) {
this.firewall = FIREWALL.OPEN
return
}
if (this.sampled < 3) return
const max = this._samplesFull[0].hits
if (max >= 3) {
this.firewall = FIREWALL.CONSISTENT
return
}
if (max === 1) {
this.firewall = FIREWALL.RANDOM
return
}
// else max === 2
// 1 host, >= 4 total samples ie, 2 bad ones -> random
if (this._samplesHost.length === 1 && this.sampled > 3) {
this.firewall = FIREWALL.RANDOM
return
}
// double hit on two different ips -> assume consistent
if (this._samplesHost.length > 1 && this._samplesFull[1].hits > 1) {
this.firewall = FIREWALL.CONSISTENT
return
}
// (4 is just means - all the samples we expect) - no decision - assume random
if (this.sampled > 4) {
this.firewall = FIREWALL.RANDOM
}
}
_updateAddresses () {
if (this.firewall === FIREWALL.UNKNOWN) {
this.addresses = null
return
}
if (this.firewall === FIREWALL.RANDOM) {
this.addresses = [this._samplesHost[0]]
return
}
if (this.firewall === FIREWALL.CONSISTENT) {
this.addresses = []
for (const addr of this._samplesFull) {
if (addr.hits >= 2 || this.addresses.length < 2) this.addresses.push(addr)
}
}
}
update () {
if (this.dht.firewalled && this.firewall === FIREWALL.OPEN) {
this.firewall = FIREWALL.UNKNOWN
}
this._updateFirewall()
this._updateAddresses()
}
add (addr, from) {
const ref = from.host + ':' + from.port
if (this._visited.get(ref) === 2) return
this._visited.set(ref, 2)
addSample(this._samplesHost, addr.host, 0)
addSample(this._samplesFull, addr.host, addr.port)
if ((++this.sampled >= 3 || !this.dht.firewalled) && !this.frozen) {
this.update()
}
if ((this.firewall === FIREWALL.CONSISTENT || this.firewall === FIREWALL.OPEN)) {
this._resolve()
} else if (this.sampled >= this._minSamples) {
this._resolve()
}
}
}
function addSample (samples, host, port) {
for (let i = 0; i < samples.length; i++) {
const s = samples[i]
if (s.port !== port || s.host !== host) continue
s.hits++
for (; i > 0; i--) {
const prev = samples[i - 1]
if (prev.hits >= s.hits) return
samples[i - 1] = s
samples[i] = prev
}
return
}
samples.push({
host,
port,
hits: 1
})
}