feat: convert to typescript (#1172)
Converts this module to typescript.
- Ecosystem modules renamed from (e.g.) `libp2p-tcp` to `@libp2p/tcp`
- Ecosystem module now have named exports
- Configuration has been updated, now pass instances of modules instead of classes:
- Some configuration keys have been renamed to make them more descriptive. `transport` -> `transports`, `connEncryption` -> `connectionEncryption`. In general where we pass multiple things, the key is now plural, e.g. `streamMuxer` -> `streamMuxers`, `contentRouting` -> `contentRouters`, etc. Where we are configuring a singleton the config key is singular, e.g. `connProtector` -> `connectionProtector` etc.
- Properties of the `modules` config key have been moved to the root
- Properties of the `config` config key have been moved to the root
```js
// before
import Libp2p from 'libp2p'
import TCP from 'libp2p-tcp'
await Libp2p.create({
modules: {
transport: [
TCP
],
}
config: {
transport: {
[TCP.tag]: {
foo: 'bar'
}
},
relay: {
enabled: true,
hop: {
enabled: true,
active: true
}
}
}
})
```
```js
// after
import { createLibp2p } from 'libp2p'
import { TCP } from '@libp2p/tcp'
await createLibp2p({
transports: [
new TCP({ foo: 'bar' })
],
relay: {
enabled: true,
hop: {
enabled: true,
active: true
}
}
})
```
- Use of `enabled` flag has been reduced - previously you could pass a module but disable it with config. Now if you don't want a feature, just don't pass an implementation. Eg:
```js
// before
await Libp2p.create({
modules: {
transport: [
TCP
],
pubsub: Gossipsub
},
config: {
pubsub: {
enabled: false
}
}
})
```
```js
// after
await createLibp2p({
transports: [
new TCP()
]
})
```
- `.multiaddrs` renamed to `.getMultiaddrs()` because it's not a property accessor, work is done by that method to calculate announce addresses, observed addresses, etc
- `/p2p/${peerId}` is now appended to all addresses returned by `.getMultiaddrs()` so they can be used opaquely (every consumer has to append the peer ID to the address to actually use it otherwise). If you need low-level unadulterated addresses, call methods on the address manager.
BREAKING CHANGE: types are no longer hand crafted, this module is now ESM only
2022-03-28 14:30:27 +01:00
|
|
|
import { upnpNat, NatAPI } from '@achingbrain/nat-port-mapper'
|
|
|
|
import { logger } from '@libp2p/logger'
|
|
|
|
import { Multiaddr } from '@multiformats/multiaddr'
|
|
|
|
import { isBrowser } from 'wherearewe'
|
|
|
|
import isPrivateIp from 'private-ip'
|
|
|
|
import * as pkg from './version.js'
|
|
|
|
import errCode from 'err-code'
|
|
|
|
import { codes } from './errors.js'
|
|
|
|
import { isLoopback } from '@libp2p/utils/multiaddr/is-loopback'
|
2022-05-04 16:03:43 +01:00
|
|
|
import type { Startable } from '@libp2p/interfaces/startable'
|
feat: convert to typescript (#1172)
Converts this module to typescript.
- Ecosystem modules renamed from (e.g.) `libp2p-tcp` to `@libp2p/tcp`
- Ecosystem module now have named exports
- Configuration has been updated, now pass instances of modules instead of classes:
- Some configuration keys have been renamed to make them more descriptive. `transport` -> `transports`, `connEncryption` -> `connectionEncryption`. In general where we pass multiple things, the key is now plural, e.g. `streamMuxer` -> `streamMuxers`, `contentRouting` -> `contentRouters`, etc. Where we are configuring a singleton the config key is singular, e.g. `connProtector` -> `connectionProtector` etc.
- Properties of the `modules` config key have been moved to the root
- Properties of the `config` config key have been moved to the root
```js
// before
import Libp2p from 'libp2p'
import TCP from 'libp2p-tcp'
await Libp2p.create({
modules: {
transport: [
TCP
],
}
config: {
transport: {
[TCP.tag]: {
foo: 'bar'
}
},
relay: {
enabled: true,
hop: {
enabled: true,
active: true
}
}
}
})
```
```js
// after
import { createLibp2p } from 'libp2p'
import { TCP } from '@libp2p/tcp'
await createLibp2p({
transports: [
new TCP({ foo: 'bar' })
],
relay: {
enabled: true,
hop: {
enabled: true,
active: true
}
}
})
```
- Use of `enabled` flag has been reduced - previously you could pass a module but disable it with config. Now if you don't want a feature, just don't pass an implementation. Eg:
```js
// before
await Libp2p.create({
modules: {
transport: [
TCP
],
pubsub: Gossipsub
},
config: {
pubsub: {
enabled: false
}
}
})
```
```js
// after
await createLibp2p({
transports: [
new TCP()
]
})
```
- `.multiaddrs` renamed to `.getMultiaddrs()` because it's not a property accessor, work is done by that method to calculate announce addresses, observed addresses, etc
- `/p2p/${peerId}` is now appended to all addresses returned by `.getMultiaddrs()` so they can be used opaquely (every consumer has to append the peer ID to the address to actually use it otherwise). If you need low-level unadulterated addresses, call methods on the address manager.
BREAKING CHANGE: types are no longer hand crafted, this module is now ESM only
2022-03-28 14:30:27 +01:00
|
|
|
import type { Components } from '@libp2p/interfaces/components'
|
|
|
|
|
|
|
|
const log = logger('libp2p:nat')
|
|
|
|
const DEFAULT_TTL = 7200
|
|
|
|
|
|
|
|
function highPort (min = 1024, max = 65535) {
|
|
|
|
return Math.floor(Math.random() * (max - min + 1) + min)
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface PMPOptions {
|
|
|
|
/**
|
|
|
|
* Whether to enable PMP as well as UPnP
|
|
|
|
*/
|
|
|
|
enabled?: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface NatManagerInit {
|
|
|
|
/**
|
|
|
|
* Whether to enable the NAT manager
|
|
|
|
*/
|
|
|
|
enabled: boolean
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pass a value to use instead of auto-detection
|
|
|
|
*/
|
|
|
|
externalAddress?: string
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pass a value to use instead of auto-detection
|
|
|
|
*/
|
|
|
|
localAddress?: string
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A string value to use for the port mapping description on the gateway
|
|
|
|
*/
|
|
|
|
description?: string
|
|
|
|
|
|
|
|
/**
|
|
|
|
* How long UPnP port mappings should last for in seconds (minimum 1200)
|
|
|
|
*/
|
|
|
|
ttl?: number
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether to automatically refresh UPnP port mappings when their TTL is reached
|
|
|
|
*/
|
|
|
|
keepAlive: boolean
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pass a value to use instead of auto-detection
|
|
|
|
*/
|
|
|
|
gateway?: string
|
|
|
|
}
|
|
|
|
|
|
|
|
export class NatManager implements Startable {
|
|
|
|
private readonly components: Components
|
|
|
|
private readonly enabled: boolean
|
|
|
|
private readonly externalAddress?: string
|
|
|
|
private readonly localAddress?: string
|
|
|
|
private readonly description: string
|
|
|
|
private readonly ttl: number
|
|
|
|
private readonly keepAlive: boolean
|
|
|
|
private readonly gateway?: string
|
|
|
|
private started: boolean
|
|
|
|
private client?: NatAPI
|
|
|
|
|
|
|
|
constructor (components: Components, init: NatManagerInit) {
|
|
|
|
this.components = components
|
|
|
|
|
|
|
|
this.started = false
|
|
|
|
this.enabled = init.enabled
|
|
|
|
this.externalAddress = init.externalAddress
|
|
|
|
this.localAddress = init.localAddress
|
|
|
|
this.description = init.description ?? `${pkg.name}@${pkg.version} ${this.components.getPeerId().toString()}`
|
|
|
|
this.ttl = init.ttl ?? DEFAULT_TTL
|
|
|
|
this.keepAlive = init.keepAlive ?? true
|
|
|
|
this.gateway = init.gateway
|
|
|
|
|
|
|
|
if (this.ttl < DEFAULT_TTL) {
|
|
|
|
throw errCode(new Error(`NatManager ttl should be at least ${DEFAULT_TTL} seconds`), codes.ERR_INVALID_PARAMETERS)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
isStarted () {
|
|
|
|
return this.started
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Starts the NAT manager
|
|
|
|
*/
|
|
|
|
start () {
|
|
|
|
if (isBrowser || !this.enabled || this.started) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.started = true
|
|
|
|
|
|
|
|
// done async to not slow down startup
|
|
|
|
this._start().catch((err) => {
|
|
|
|
// hole punching errors are non-fatal
|
|
|
|
log.error(err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async _start () {
|
|
|
|
const addrs = this.components.getTransportManager().getAddrs()
|
|
|
|
|
|
|
|
for (const addr of addrs) {
|
|
|
|
// try to open uPnP ports for each thin waist address
|
|
|
|
const { family, host, port, transport } = addr.toOptions()
|
|
|
|
|
|
|
|
if (!addr.isThinWaistAddress() || transport !== 'tcp') {
|
|
|
|
// only bare tcp addresses
|
|
|
|
// eslint-disable-next-line no-continue
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isLoopback(addr)) {
|
|
|
|
// eslint-disable-next-line no-continue
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if (family !== 4) {
|
|
|
|
// ignore ipv6
|
|
|
|
// eslint-disable-next-line no-continue
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const client = await this._getClient()
|
|
|
|
const publicIp = this.externalAddress ?? await client.externalIp()
|
|
|
|
|
|
|
|
if (isPrivateIp(publicIp)) {
|
|
|
|
throw new Error(`${publicIp} is private - please set config.nat.externalIp to an externally routable IP or ensure you are not behind a double NAT`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const publicPort = highPort()
|
|
|
|
|
|
|
|
log(`opening uPnP connection from ${publicIp}:${publicPort} to ${host}:${port}`)
|
|
|
|
|
|
|
|
await client.map({
|
|
|
|
publicPort,
|
|
|
|
localPort: port,
|
|
|
|
localAddress: this.localAddress,
|
|
|
|
protocol: transport.toUpperCase() === 'TCP' ? 'TCP' : 'UDP'
|
|
|
|
})
|
|
|
|
|
|
|
|
this.components.getAddressManager().addObservedAddr(Multiaddr.fromNodeAddress({
|
|
|
|
family: 4,
|
|
|
|
address: publicIp,
|
|
|
|
port: publicPort
|
|
|
|
}, transport))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async _getClient () {
|
|
|
|
if (this.client != null) {
|
|
|
|
return this.client
|
|
|
|
}
|
|
|
|
|
|
|
|
this.client = await upnpNat({
|
|
|
|
description: this.description,
|
|
|
|
ttl: this.ttl,
|
|
|
|
keepAlive: this.keepAlive,
|
|
|
|
gateway: this.gateway
|
|
|
|
})
|
|
|
|
|
|
|
|
return this.client
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the NAT manager
|
|
|
|
*/
|
|
|
|
async stop () {
|
|
|
|
if (isBrowser || this.client == null) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.client.close()
|
|
|
|
this.client = undefined
|
|
|
|
} catch (err: any) {
|
|
|
|
log.error(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|