You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
117 lines
2.8 KiB
117 lines
2.8 KiB
// 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
|
|
}
|
|
|