Skip to content

Commit

Permalink
feat: add UPnP NAT manager (#810)
Browse files Browse the repository at this point in the history
* feat: add uPnP nat manager

Adds a really basic nat manager that attempts to use UPnP to punch
a hole through your router for any IPV4 tcp addresses you have
configured.

Adds any configured addresses to the node's observed addresses list
and adds observed addresses to `libp2p.multiaddrs` so we exchange
them with peers when performing `identify` and people can dial you.

Adds configuration options under `config.nat`

Hole punching is async to not affect start up time.

Co-authored-by: Vasco Santos <vasco.santos@moxy.studio>
  • Loading branch information
achingbrain and vasco-santos authored Jan 27, 2021
1 parent b5c9e48 commit 0a6bc0d
Show file tree
Hide file tree
Showing 16 changed files with 742 additions and 22 deletions.
5 changes: 4 additions & 1 deletion .aegir.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const before = async () => {
enabled: true,
active: false
}
},
nat: {
enabled: false
}
}
})
Expand All @@ -45,7 +48,7 @@ const after = async () => {
}

module.exports = {
bundlesize: { maxSize: '260kB' },
bundlesize: { maxSize: '215kB' },
hooks: {
pre: before,
post: after
Expand Down
9 changes: 9 additions & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2055,6 +2055,15 @@ This event will be triggered anytime we are disconnected from another peer, rega
- `peerId`: instance of [`PeerId`][peer-id]
- `protocols`: array of known, supported protocols for the peer (string identifiers)

### libp2p.addressManager

#### Our addresses have changed

This could be in response to a peer telling us about addresses they have observed, or
the NatManager performing NAT hole punching.

`libp2p.addressManager.on('change:addresses', () => {})`

## Types

### Stats
Expand Down
37 changes: 37 additions & 0 deletions doc/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
- [Configuring Metrics](#configuring-metrics)
- [Configuring PeerStore](#configuring-peerstore)
- [Customizing Transports](#customizing-transports)
- [Configuring the NAT Manager](#configuring-the-nat-manager)
- [Browser support](#browser-support)
- [UPnP and NAT-PMP](#upnp-and-nat-pmp)
- [Configuration examples](#configuration-examples)

## Overview
Expand Down Expand Up @@ -733,6 +736,40 @@ const node = await Libp2p.create({
})
```
#### Configuring the NAT Manager
Network Address Translation (NAT) is a function performed by your router to enable multiple devices on your local network to share a single IPv4 address. It's done transparently for outgoing connections, ensuring the correct response traffic is routed to your computer, but if you wish to accept incoming connections some configuration is necessary.
The NAT manager can be configured as follows:
```js
const node = await Libp2p.create({
config: {
nat: {
description: 'my-node', // set as the port mapping description on the router, defaults the current libp2p version and your peer id
enabled: true, // defaults to true
gateway: '192.168.1.1', // leave unset to auto-discover
externalIp: '80.1.1.1', // leave unset to auto-discover
ttl: 7200, // TTL for port mappings (min 20 minutes)
keepAlive: true, // Refresh port mapping after TTL expires
pmp: {
enabled: false, // defaults to false
}
}
}
})
```
##### Browser support
Browsers cannot open TCP ports or send the UDP datagrams necessary to configure external port mapping - to accept incoming connections in the browser please use a WebRTC transport.
##### UPnP and NAT-PMP
By default under nodejs libp2p will attempt to use [UPnP](https://en.wikipedia.org/wiki/Universal_Plug_and_Play) to configure your router to allow incoming connections to any TCP transports that have been configured.
[NAT-PMP](http://miniupnp.free.fr/nat-pmp.html) is a feature of some modern routers which performs a similar job to UPnP. NAT-PMP is disabled by default, if enabled libp2p will try to use NAT-PMP and will fall back to UPnP if it fails.
## Configuration examples
As libp2p is designed to be a modular networking library, its usage will vary based on individual project needs. We've included links to some existing project configurations for your reference, in case you wish to replicate their configuration:
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@
"node": ">=12.0.0",
"npm": ">=6.0.0"
},
"browser": {
"@motrix/nat-api": false
},
"dependencies": {
"@motrix/nat-api": "^0.3.1",
"abort-controller": "^3.0.0",
"aggregate-error": "^3.1.0",
"any-signal": "^2.1.1",
Expand Down Expand Up @@ -89,8 +93,11 @@
"node-forge": "^0.10.0",
"p-any": "^3.0.0",
"p-fifo": "^1.0.0",
"p-retry": "^4.2.0",
"p-settle": "^4.0.1",
"peer-id": "^0.14.2",
"private-ip": "^2.0.0",
"promisify-es6": "^1.0.3",
"protons": "^2.0.0",
"retimer": "^2.0.0",
"sanitize-filename": "^1.6.3",
Expand Down Expand Up @@ -132,7 +139,6 @@
"p-defer": "^3.0.0",
"p-times": "^3.0.0",
"p-wait-for": "^3.2.0",
"promisify-es6": "^1.0.3",
"rimraf": "^3.0.2",
"sinon": "^9.2.4",
"uint8arrays": "^2.0.5"
Expand Down
64 changes: 59 additions & 5 deletions src/address-manager/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
'use strict'

/** @typedef {import('../types').EventEmitterFactory} Events */
/** @type Events */
const EventEmitter = require('events')
const multiaddr = require('multiaddr')
const PeerId = require('peer-id')

/**
* @typedef {import('multiaddr')} Multiaddr
Expand All @@ -11,19 +15,30 @@ const multiaddr = require('multiaddr')
* @property {string[]} [listen = []] - list of multiaddrs string representation to listen.
* @property {string[]} [announce = []] - list of multiaddrs string representation to announce.
*/
class AddressManager {

/**
* @fires AddressManager#change:addresses Emitted when a addresses change.
*/
class AddressManager extends EventEmitter {
/**
* Responsible for managing the peer addresses.
* Peers can specify their listen and announce addresses.
* The listen addresses will be used by the libp2p transports to listen for new connections,
* while the announce addresses will be used for the peer addresses' to other peers in the network.
*
* @class
* @param {AddressManagerOptions} [options]
* @param {PeerId} peerId - The Peer ID of the node
* @param {object} [options]
* @param {Array<string>} [options.listen = []] - list of multiaddrs string representation to listen.
* @param {Array<string>} [options.announce = []] - list of multiaddrs string representation to announce.
*/
constructor ({ listen = [], announce = [] } = {}) {
this.listen = new Set(listen)
this.announce = new Set(announce)
constructor (peerId, { listen = [], announce = [] } = {}) {
super()

this.peerId = peerId
this.listen = new Set(listen.map(ma => ma.toString()))
this.announce = new Set(announce.map(ma => ma.toString()))
this.observed = new Set()
}

/**
Expand All @@ -43,6 +58,45 @@ class AddressManager {
getAnnounceAddrs () {
return Array.from(this.announce).map((a) => multiaddr(a))
}

/**
* Get observed multiaddrs.
*
* @returns {Array<Multiaddr>}
*/
getObservedAddrs () {
return Array.from(this.observed).map((a) => multiaddr(a))
}

/**
* Add peer observed addresses
*
* @param {string | Multiaddr} addr
*/
addObservedAddr (addr) {
let ma = multiaddr(addr)
const remotePeer = ma.getPeerId()

// strip our peer id if it has been passed
if (remotePeer) {
const remotePeerId = PeerId.createFromB58String(remotePeer)

// use same encoding for comparison
if (remotePeerId.equals(this.peerId)) {
ma = ma.decapsulate(multiaddr(`/p2p/${this.peerId}`))
}
}

const addrString = ma.toString()

// do not trigger the change:addresses event if we already know about this address
if (this.observed.has(addrString)) {
return
}

this.observed.add(addrString)
this.emit('change:addresses')
}
}

module.exports = AddressManager
10 changes: 10 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ const DefaultConfig = {
timeout: 10e3
}
},
nat: {
enabled: true,
ttl: 7200,
keepAlive: true,
gateway: null,
externalIp: null,
pmp: {
enabled: false
}
},
peerDiscovery: {
autoDial: true
},
Expand Down
4 changes: 3 additions & 1 deletion src/identify/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class IdentifyService {
constructor ({ libp2p }) {
this._libp2p = libp2p
this.peerStore = libp2p.peerStore
this.addressManager = libp2p.addressManager
this.connectionManager = libp2p.connectionManager
this.peerId = libp2p.peerId

Expand Down Expand Up @@ -201,8 +202,9 @@ class IdentifyService {
this.peerStore.protoBook.set(id, protocols)
this.peerStore.metadataBook.set(id, 'AgentVersion', uint8ArrayFromString(message.agentVersion))

// TODO: Track our observed address so that we can score it
// TODO: Score our observed addr
log('received observed address of %s', observedAddr)
this.addressManager.addObservedAddr(observedAddr)
}

/**
Expand Down
48 changes: 40 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const EventEmitter = require('events')

const errCode = require('err-code')
const PeerId = require('peer-id')
const multiaddr = require('multiaddr')

const PeerRouting = require('./peer-routing')
const ContentRouting = require('./content-routing')
Expand All @@ -33,6 +34,8 @@ const Registrar = require('./registrar')
const ping = require('./ping')
const IdentifyService = require('./identify')
const IDENTIFY_PROTOCOLS = IdentifyService.multicodecs
const NatManager = require('./nat-manager')
const { updateSelfPeerRecord } = require('./record/utils')

/**
* @typedef {import('multiaddr')} Multiaddr
Expand Down Expand Up @@ -133,7 +136,14 @@ class Libp2p extends EventEmitter {

// Addresses {listen, announce, noAnnounce}
this.addresses = this._options.addresses
this.addressManager = new AddressManager(this._options.addresses)
this.addressManager = new AddressManager(this.peerId, this._options.addresses)

// when addresses change, update our peer record
this.addressManager.on('change:addresses', () => {
updateSelfPeerRecord(this).catch(err => {
log.error('Error updating self peer record', err)
})
})

this._modules = this._options.modules
this._config = this._options.config
Expand Down Expand Up @@ -187,6 +197,14 @@ class Libp2p extends EventEmitter {
faultTolerance: this._options.transportManager.faultTolerance
})

// Create the Nat Manager
this.natManager = new NatManager({
peerId: this.peerId,
addressManager: this.addressManager,
transportManager: this.transportManager,
...this._options.config.nat
})

// Create the Registrar
this.registrar = new Registrar({
peerStore: this.peerStore,
Expand Down Expand Up @@ -350,6 +368,7 @@ class Libp2p extends EventEmitter {
this.metrics && this.metrics.stop()
])

await this.natManager.stop()
await this.transportManager.close()

ping.unmount(this)
Expand Down Expand Up @@ -445,22 +464,32 @@ class Libp2p extends EventEmitter {
}

/**
* Get peer advertising multiaddrs by concating the addresses used
* by transports to listen with the announce addresses.
* Duplicated addresses and noAnnounce addresses are filtered out.
* Get a deduplicated list of peer advertising multiaddrs by concatenating
* the listen addresses used by transports with any configured
* announce addresses as well as observed addresses reported by peers.
*
* If Announce addrs are specified, configured listen addresses will be
* ignored though observed addresses will still be included.
*
* @returns {Multiaddr[]}
*/
get multiaddrs () {
const announceAddrs = this.addressManager.getAnnounceAddrs()
if (announceAddrs.length) {
return announceAddrs
let addrs = this.addressManager.getAnnounceAddrs().map(ma => ma.toString())

if (!addrs.length) {
// no configured announce addrs, add configured listen addresses
addrs = this.transportManager.getAddrs().map(ma => ma.toString())
}

addrs = addrs.concat(this.addressManager.getObservedAddrs().map(ma => ma.toString()))

const announceFilter = this._options.addresses.announceFilter || ((multiaddrs) => multiaddrs)

// dedupe multiaddrs
const addrSet = new Set(addrs)

// Create advertising list
return announceFilter(this.transportManager.getAddrs())
return announceFilter(Array.from(addrSet).map(str => multiaddr(str)))
}

/**
Expand Down Expand Up @@ -539,6 +568,9 @@ class Libp2p extends EventEmitter {
const addrs = this.addressManager.getListenAddrs()
await this.transportManager.listen(addrs)

// Manage your NATs
this.natManager.start()

// Start PeerStore
await this.peerStore.start()

Expand Down
Loading

0 comments on commit 0a6bc0d

Please sign in to comment.