/* 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) }) })