From a9021e4c40205a7ffd9b3e9e1bd1d09f8d73ca36 Mon Sep 17 00:00:00 2001 From: Pavel Murygin Date: Tue, 4 Oct 2022 13:50:41 +0400 Subject: [PATCH] Remove nat port manager --- package.json | 11 +- src/libp2p.ts | 5 +- src/nat-manager.ts | 198 ----------------------- test/nat-manager/nat-manager.node.ts | 232 --------------------------- 4 files changed, 7 insertions(+), 439 deletions(-) delete mode 100644 src/nat-manager.ts delete mode 100644 test/nat-manager/nat-manager.node.ts diff --git a/package.json b/package.json index 5ef0c330..e2cba6a2 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ ] }, "scripts": { + "prepare": "npm run build", "clean": "aegir clean", "lint": "aegir lint", "dep-check": "aegir dep-check", @@ -98,11 +99,11 @@ }, "dependencies": { "@achingbrain/nat-port-mapper": "^1.0.3", - "@libp2p/components": "^2.1.0", - "@libp2p/connection": "^4.0.2", - "@libp2p/crypto": "^1.0.4", - "@libp2p/interface-address-manager": "^1.0.3", - "@libp2p/interface-connection": "^3.0.2", + "@libp2p/components": "^2.0.3", + "@libp2p/connection": "^4.0.1", + "@libp2p/crypto": "^1.0.3", + "@libp2p/interface-address-manager": "^1.0.2", + "@libp2p/interface-connection": "^3.0.1", "@libp2p/interface-connection-encrypter": "^2.0.1", "@libp2p/interface-connection-manager": "^1.1.1", "@libp2p/interface-content-routing": "^1.0.2", diff --git a/src/libp2p.ts b/src/libp2p.ts index 0bc0c995..433300f8 100644 --- a/src/libp2p.ts +++ b/src/libp2p.ts @@ -21,7 +21,7 @@ import { DefaultRegistrar } from './registrar.js' import { IdentifyService } from './identify/index.js' import { FetchService } from './fetch/index.js' import { PingService } from './ping/index.js' -import { NatManager } from './nat-manager.js' + import { PeerRecordUpdater } from './peer-record-updater.js' import { DHTPeerRouting } from './dht/dht-peer-routing.js' import { PersistentPeerStore } from '@libp2p/peer-store' @@ -160,9 +160,6 @@ export class Libp2pNode extends EventEmitter implements Libp2p { ...init.keychain })) - // Create the Nat Manager - this.services.push(new NatManager(this.components, init.nat)) - init.transports.forEach((transport) => { this.components.getTransportManager().add(this.configureComponent(transport)) }) diff --git a/src/nat-manager.ts b/src/nat-manager.ts deleted file mode 100644 index 6da6639e..00000000 --- a/src/nat-manager.ts +++ /dev/null @@ -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) - } - } -} diff --git a/test/nat-manager/nat-manager.node.ts b/test/nat-manager/nat-manager.node.ts deleted file mode 100644 index 01049a2a..00000000 --- a/test/nat-manager/nat-manager.node.ts +++ /dev/null @@ -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> = [] - let client: StubbedInstance - - 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() - - 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) - }) -})