mirror of
https://github.com/fluencelabs/js-libp2p
synced 2025-06-06 14:01:21 +00:00
Remove nat port manager
This commit is contained in:
parent
6ac62da025
commit
a9021e4c40
11
package.json
11
package.json
@ -77,6 +77,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"prepare": "npm run build",
|
||||||
"clean": "aegir clean",
|
"clean": "aegir clean",
|
||||||
"lint": "aegir lint",
|
"lint": "aegir lint",
|
||||||
"dep-check": "aegir dep-check",
|
"dep-check": "aegir dep-check",
|
||||||
@ -98,11 +99,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@achingbrain/nat-port-mapper": "^1.0.3",
|
"@achingbrain/nat-port-mapper": "^1.0.3",
|
||||||
"@libp2p/components": "^2.1.0",
|
"@libp2p/components": "^2.0.3",
|
||||||
"@libp2p/connection": "^4.0.2",
|
"@libp2p/connection": "^4.0.1",
|
||||||
"@libp2p/crypto": "^1.0.4",
|
"@libp2p/crypto": "^1.0.3",
|
||||||
"@libp2p/interface-address-manager": "^1.0.3",
|
"@libp2p/interface-address-manager": "^1.0.2",
|
||||||
"@libp2p/interface-connection": "^3.0.2",
|
"@libp2p/interface-connection": "^3.0.1",
|
||||||
"@libp2p/interface-connection-encrypter": "^2.0.1",
|
"@libp2p/interface-connection-encrypter": "^2.0.1",
|
||||||
"@libp2p/interface-connection-manager": "^1.1.1",
|
"@libp2p/interface-connection-manager": "^1.1.1",
|
||||||
"@libp2p/interface-content-routing": "^1.0.2",
|
"@libp2p/interface-content-routing": "^1.0.2",
|
||||||
|
@ -21,7 +21,7 @@ import { DefaultRegistrar } from './registrar.js'
|
|||||||
import { IdentifyService } from './identify/index.js'
|
import { IdentifyService } from './identify/index.js'
|
||||||
import { FetchService } from './fetch/index.js'
|
import { FetchService } from './fetch/index.js'
|
||||||
import { PingService } from './ping/index.js'
|
import { PingService } from './ping/index.js'
|
||||||
import { NatManager } from './nat-manager.js'
|
|
||||||
import { PeerRecordUpdater } from './peer-record-updater.js'
|
import { PeerRecordUpdater } from './peer-record-updater.js'
|
||||||
import { DHTPeerRouting } from './dht/dht-peer-routing.js'
|
import { DHTPeerRouting } from './dht/dht-peer-routing.js'
|
||||||
import { PersistentPeerStore } from '@libp2p/peer-store'
|
import { PersistentPeerStore } from '@libp2p/peer-store'
|
||||||
@ -160,9 +160,6 @@ export class Libp2pNode extends EventEmitter<Libp2pEvents> implements Libp2p {
|
|||||||
...init.keychain
|
...init.keychain
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Create the Nat Manager
|
|
||||||
this.services.push(new NatManager(this.components, init.nat))
|
|
||||||
|
|
||||||
init.transports.forEach((transport) => {
|
init.transports.forEach((transport) => {
|
||||||
this.components.getTransportManager().add(this.configureComponent(transport))
|
this.components.getTransportManager().add(this.configureComponent(transport))
|
||||||
})
|
})
|
||||||
|
@ -1,198 +0,0 @@
|
|||||||
import { upnpNat, NatAPI } from '@achingbrain/nat-port-mapper'
|
|
||||||
import { logger } from '@libp2p/logger'
|
|
||||||
import { fromNodeAddress } 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'
|
|
||||||
import type { Startable } from '@libp2p/interfaces/startable'
|
|
||||||
import type { Components } from '@libp2p/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
|
|
||||||
}
|
|
||||||
|
|
||||||
start () {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to use uPnP to configure port mapping using the current gateway.
|
|
||||||
*
|
|
||||||
* Run after start to ensure the transport manager has all addresses configured.
|
|
||||||
*/
|
|
||||||
afterStart () {
|
|
||||||
if (isBrowser || !this.enabled || this.started) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.started = true
|
|
||||||
|
|
||||||
// done async to not slow down startup
|
|
||||||
void 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(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,232 +0,0 @@
|
|||||||
/* eslint-env mocha */
|
|
||||||
|
|
||||||
import { expect } from 'aegir/chai'
|
|
||||||
import { DefaultAddressManager } from '../../src/address-manager/index.js'
|
|
||||||
import { DefaultTransportManager, FaultTolerance } from '../../src/transport-manager.js'
|
|
||||||
import { TCP } from '@libp2p/tcp'
|
|
||||||
import { mockUpgrader } from '@libp2p/interface-mocks'
|
|
||||||
import { NatManager } from '../../src/nat-manager.js'
|
|
||||||
import delay from 'delay'
|
|
||||||
import Peers from '../fixtures/peers.js'
|
|
||||||
import { codes } from '../../src/errors.js'
|
|
||||||
import { createFromJSON } from '@libp2p/peer-id-factory'
|
|
||||||
import { Components } from '@libp2p/components'
|
|
||||||
import type { NatAPI } from '@achingbrain/nat-port-mapper'
|
|
||||||
import { StubbedInstance, stubInterface } from 'ts-sinon'
|
|
||||||
import { start, stop } from '@libp2p/interfaces/startable'
|
|
||||||
|
|
||||||
const DEFAULT_ADDRESSES = [
|
|
||||||
'/ip4/127.0.0.1/tcp/0',
|
|
||||||
'/ip4/0.0.0.0/tcp/0'
|
|
||||||
]
|
|
||||||
|
|
||||||
describe('Nat Manager (TCP)', () => {
|
|
||||||
const teardown: Array<() => Promise<void>> = []
|
|
||||||
let client: StubbedInstance<NatAPI>
|
|
||||||
|
|
||||||
async function createNatManager (addrs = DEFAULT_ADDRESSES, natManagerOptions = {}) {
|
|
||||||
const components = new Components({
|
|
||||||
peerId: await createFromJSON(Peers[0]),
|
|
||||||
upgrader: mockUpgrader()
|
|
||||||
})
|
|
||||||
components.setAddressManager(new DefaultAddressManager(components, { listen: addrs }))
|
|
||||||
components.setTransportManager(new DefaultTransportManager(components, {
|
|
||||||
faultTolerance: FaultTolerance.NO_FATAL
|
|
||||||
}))
|
|
||||||
|
|
||||||
const natManager = new NatManager(components, {
|
|
||||||
enabled: true,
|
|
||||||
keepAlive: true,
|
|
||||||
...natManagerOptions
|
|
||||||
})
|
|
||||||
|
|
||||||
client = stubInterface<NatAPI>()
|
|
||||||
|
|
||||||
natManager._getClient = async () => {
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
components.getTransportManager().add(new TCP())
|
|
||||||
await components.getTransportManager().listen(components.getAddressManager().getListenAddrs())
|
|
||||||
|
|
||||||
teardown.push(async () => {
|
|
||||||
await stop(natManager)
|
|
||||||
await components.getTransportManager().removeAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
natManager,
|
|
||||||
components
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(async () => await Promise.all(teardown.map(async t => await t())))
|
|
||||||
|
|
||||||
it('should map TCP connections to external ports', async () => {
|
|
||||||
const {
|
|
||||||
natManager,
|
|
||||||
components
|
|
||||||
} = await createNatManager()
|
|
||||||
|
|
||||||
let addressChangedEventFired = false
|
|
||||||
|
|
||||||
components.getAddressManager().addEventListener('change:addresses', () => {
|
|
||||||
addressChangedEventFired = true
|
|
||||||
})
|
|
||||||
|
|
||||||
client.externalIp.resolves('82.3.1.5')
|
|
||||||
|
|
||||||
let observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
|
|
||||||
await start(natManager)
|
|
||||||
|
|
||||||
observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.not.be.empty()
|
|
||||||
|
|
||||||
const internalPorts = components.getTransportManager().getAddrs()
|
|
||||||
.filter(ma => ma.isThinWaistAddress())
|
|
||||||
.map(ma => ma.toOptions())
|
|
||||||
.filter(({ host, transport }) => host !== '127.0.0.1' && transport === 'tcp')
|
|
||||||
.map(({ port }) => port)
|
|
||||||
|
|
||||||
expect(client.map.called).to.be.true()
|
|
||||||
|
|
||||||
internalPorts.forEach(port => {
|
|
||||||
expect(client.map.getCall(0).args[0]).to.include({
|
|
||||||
localPort: port,
|
|
||||||
protocol: 'TCP'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(addressChangedEventFired).to.be.true()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not map TCP connections when double-natted', async () => {
|
|
||||||
const {
|
|
||||||
natManager,
|
|
||||||
components
|
|
||||||
} = await createNatManager()
|
|
||||||
|
|
||||||
client.externalIp.resolves('192.168.1.1')
|
|
||||||
|
|
||||||
let observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
|
|
||||||
await expect(natManager._start()).to.eventually.be.rejectedWith(/double NAT/)
|
|
||||||
|
|
||||||
observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
|
|
||||||
expect(client.map.called).to.be.false()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should do nothing when disabled', async () => {
|
|
||||||
const {
|
|
||||||
natManager
|
|
||||||
} = await createNatManager(DEFAULT_ADDRESSES, {
|
|
||||||
enabled: false
|
|
||||||
})
|
|
||||||
|
|
||||||
await start(natManager)
|
|
||||||
|
|
||||||
await delay(100)
|
|
||||||
|
|
||||||
expect(client.externalIp.called).to.be.false()
|
|
||||||
expect(client.map.called).to.be.false()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not map non-ipv4 connections to external ports', async () => {
|
|
||||||
const {
|
|
||||||
natManager,
|
|
||||||
components
|
|
||||||
} = await createNatManager([
|
|
||||||
'/ip6/::/tcp/0'
|
|
||||||
])
|
|
||||||
|
|
||||||
let observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
|
|
||||||
await start(natManager)
|
|
||||||
|
|
||||||
observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not map non-ipv6 loopback connections to external ports', async () => {
|
|
||||||
const {
|
|
||||||
natManager,
|
|
||||||
components
|
|
||||||
} = await createNatManager([
|
|
||||||
'/ip6/::1/tcp/0'
|
|
||||||
])
|
|
||||||
|
|
||||||
let observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
|
|
||||||
await start(natManager)
|
|
||||||
|
|
||||||
observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not map non-TCP connections to external ports', async () => {
|
|
||||||
const {
|
|
||||||
natManager,
|
|
||||||
components
|
|
||||||
} = await createNatManager([
|
|
||||||
'/ip4/0.0.0.0/utp'
|
|
||||||
])
|
|
||||||
|
|
||||||
let observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
|
|
||||||
await start(natManager)
|
|
||||||
|
|
||||||
observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not map loopback connections to external ports', async () => {
|
|
||||||
const {
|
|
||||||
natManager,
|
|
||||||
components
|
|
||||||
} = await createNatManager([
|
|
||||||
'/ip4/127.0.0.1/tcp/0'
|
|
||||||
])
|
|
||||||
|
|
||||||
let observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
|
|
||||||
await start(natManager)
|
|
||||||
|
|
||||||
observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not map non-thin-waist connections to external ports', async () => {
|
|
||||||
const {
|
|
||||||
natManager,
|
|
||||||
components
|
|
||||||
} = await createNatManager([
|
|
||||||
'/ip4/0.0.0.0/tcp/0/sctp/0'
|
|
||||||
])
|
|
||||||
|
|
||||||
let observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
|
|
||||||
await start(natManager)
|
|
||||||
|
|
||||||
observed = components.getAddressManager().getObservedAddrs().map(ma => ma.toString())
|
|
||||||
expect(observed).to.be.empty()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should specify large enough TTL', async () => {
|
|
||||||
const peerId = await createFromJSON(Peers[0])
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
// @ts-expect-error invalid parameters
|
|
||||||
new NatManager(new Components({ peerId }), { ttl: 5 }) // eslint-disable-line no-new
|
|
||||||
}).to.throw().with.property('code', codes.ERR_INVALID_PARAMETERS)
|
|
||||||
})
|
|
||||||
})
|
|
Loading…
x
Reference in New Issue
Block a user