Browse Source

infer holepunchability from peers (#18)

* infer holepunchability from peers

* use res.to in pong for forwards compat

* add remoteAddress also

* add initial-nodes event

* docs
v4
Mathias Buus 5 years ago
committed by GitHub
parent
commit
ffc43c2723
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 28
      README.md
  2. 57
      index.js
  3. 12
      lib/io.js

28
README.md

@ -218,6 +218,26 @@ Peer should look like this:
}
```
#### `node.holepunchable()`
Returns `true` if your current network is holepunchable.
Relies on a heuristic interally based on remote node information.
It's usually best to wait for the `initial-nodes` or `ready` event before checking this
as it is more reliable the more routing information the node has.
#### `{ host, port } = node.remoteAddress()`
Returns your remote IP and port.
Relies on a heuristic interally based on remote node information.
If your IP could not be inferred `null` is returned.
If your IP could be inferred but your port not, `{ host, port: 0 }` is returned.
It's usually best to wait for the `initial-nodes` or `ready` event before checking this
as it is more reliable the more routing information the node has.
#### `node.listen([port], [address], [onlistening])`
Explicitly bind the dht node to a certain port/address.
@ -226,6 +246,14 @@ Explicitly bind the dht node to a certain port/address.
Dynamically convert the node into ephemeral (leave the DHT) or non-ephemeral (join the DHT).
#### `node.on('ready')`
Emitted when the node is fully bootstrapped. You can make queries/updates before.
#### `node.on('initial-nodes')`
Emitted when the routing table has been initially populated.
#### `node.on('listening')`
Emitted when the node starts listening on a udp port.

57
index.js

@ -46,6 +46,7 @@ class DHT extends EventEmitter {
this._commands = new Map()
this._tick = 0
this._tickInterval = setInterval(this._ontick.bind(this), 5000)
this._initialNodes = false
process.nextTick(this.bootstrap.bind(this))
}
@ -81,7 +82,7 @@ class DHT extends EventEmitter {
onrequest (type, message, peer) {
if (validateId(message.id)) {
this._addNode(message.id, peer, null)
this._addNode(message.id, peer, null, message.to)
}
switch (message.command) {
@ -163,7 +164,7 @@ class DHT extends EventEmitter {
onresponse (message, peer) {
if (validateId(message.id)) {
this._addNode(message.id, peer, message.roundtripToken)
this._addNode(message.id, peer, message.roundtripToken, message.to)
}
}
@ -183,13 +184,52 @@ class DHT extends EventEmitter {
this._io.query('_ping', null, peer.id, peer, function (err, res) {
if (err) return cb(err)
if (res.error) return cb(new Error(res.error))
const pong = decodePeer(res.value)
const pong = decodePeer(res.to || res.value) // res.value will be deprecated
if (!pong) return cb(new Error('Invalid pong'))
cb(null, pong)
})
}
_addNode (id, peer, token) {
_tally (onlyIp) {
const sum = new Map()
var result = null
var node = this.nodes.latest
var cnt = 0
var good = 0
for (; node && cnt < 10; node = node.prev) {
if (!node.to || node.to.length !== 6) continue
const to = onlyIp ? node.to.toString('hex').slice(0, 8) + '0000' : node.to.toString('hex')
const hits = 1 + (sum.get(to) || 0)
if (hits > good) {
good = hits
result = node.to
}
sum.set(to, hits)
cnt++
}
// We want at least 3 samples all with the same ip:port from
// different remotes (the to field) to be consider it consistent
// If we get >=3 samples with conflicting info we are not (or under attack) (Subject for tweaking)
const bad = cnt - good
return bad < 3 && good >= 3 ? result : null
}
remoteAddress () {
const both = this._tally(false)
if (both) return peers.decode(both)[0]
const onlyIp = this._tally(true)
if (onlyIp) return peers.decode(onlyIp)[0]
return null
}
holepunchable () {
return this._tally(false) !== null
}
_addNode (id, peer, token, to) {
if (id.equals(this.id)) return
var node = this.bucket.get(id)
@ -202,11 +242,18 @@ class DHT extends EventEmitter {
node.host = peer.host
if (token) node.roundtripToken = token
node.tick = this._tick
node.to = to
if (!fresh) this.nodes.remove(node)
this.nodes.add(node)
this.bucket.add(node)
if (fresh) this.emit('add-node', node)
if (fresh) {
this.emit('add-node', node)
if (!this._initialNodes && this.nodes.length >= 5) {
this._initialNodes = true
this.emit('initial-nodes')
}
}
}
_removeNode (node) {

12
lib/io.js

@ -95,6 +95,7 @@ class IO {
_onmessage (buf, rinfo) {
const message = decodeMessage(buf)
if (!message) return
if (message.id && message.id.length !== 32) return
const peer = { port: rinfo.port, host: rinfo.address }
@ -225,6 +226,7 @@ class IO {
const message = {
type: TYPE.RESPONSE,
rid: request.rid,
to: peers.encode([peer]),
id: this.id,
closerNodes,
roundtripToken: this._token(peer, 0),
@ -237,10 +239,11 @@ class IO {
const message = {
type: TYPE.RESPONSE,
rid: request.rid,
to: peers.encode([peer]),
id: this.id,
closerNodes,
error: error.message,
value: value
value
}
this.send(Message.encode(message), peer)
}
@ -251,6 +254,7 @@ class IO {
this._request({
type: TYPE.QUERY,
rid: 0,
to: encodeIP(peer),
id: this.id,
target,
command,
@ -264,6 +268,7 @@ class IO {
this._requestImmediately({
type: TYPE.QUERY,
rid: 0,
to: encodeIP(peer),
id: this.id,
target,
command,
@ -277,6 +282,7 @@ class IO {
this._request({
type: TYPE.UPDATE,
rid: 0,
to: encodeIP(peer),
id: this.id,
roundtripToken: peer.roundtripToken,
target,
@ -306,3 +312,7 @@ function randomBytes (n) {
sodium.randombytes_buf(buf)
return buf
}
function encodeIP (peer) {
return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(peer.host) ? peers.encode([peer]) : null
}

Loading…
Cancel
Save