js-libp2p/test/dialing/direct.spec.js
Alex Potsides 902f10d58d
fix: reject connections when not running (#1146)
When the node is shutting down, new connections can still be received.

If this happens we can end up writing into the datastore when it's
been closed which throws an error.

Instead, if we're not running, have the connection manager close new
incoming connections.
2022-01-26 10:52:23 +00:00

638 lines
18 KiB
JavaScript

'use strict'
/* eslint-env mocha */
const { expect } = require('aegir/utils/chai')
const sinon = require('sinon')
const pDefer = require('p-defer')
const pWaitFor = require('p-wait-for')
const delay = require('delay')
const Transport = require('libp2p-websockets')
const filters = require('libp2p-websockets/src/filters')
const Muxer = require('libp2p-mplex')
const { NOISE: Crypto } = require('@chainsafe/libp2p-noise')
const { Multiaddr } = require('multiaddr')
const AggregateError = require('aggregate-error')
const { AbortError } = require('libp2p-interfaces/src/transport/errors')
const { MemoryDatastore } = require('datastore-core/memory')
const { codes: ErrorCodes } = require('../../src/errors')
const Constants = require('../../src/constants')
const Dialer = require('../../src/dialer')
const addressSort = require('libp2p-utils/src/address-sort')
const PeerStore = require('../../src/peer-store')
const TransportManager = require('../../src/transport-manager')
const Libp2p = require('../../src')
const { mockConnectionGater } = require('../utils/mock-connection-gater')
const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser')
const mockUpgrader = require('../utils/mockUpgrader')
const createMockConnection = require('../utils/mockConnection')
const { createPeerId } = require('../utils/creators/peer')
const unsupportedAddr = new Multiaddr('/ip4/127.0.0.1/tcp/9999/ws/p2p/QmckxVrJw1Yo8LqvmDJNUmdAsKtSbiKWmrXJFyKmUraBoN')
const remoteAddr = MULTIADDRS_WEBSOCKETS[0]
describe('Dialing (direct, WebSockets)', () => {
const connectionGater = mockConnectionGater()
let localTM
let peerStore
let peerId
before(async () => {
[peerId] = await createPeerId()
peerStore = new PeerStore({
peerId,
datastore: new MemoryDatastore(),
addressFilter: connectionGater.filterMultiaddrForPeer
})
localTM = new TransportManager({
libp2p: {},
upgrader: mockUpgrader,
onConnection: () => {}
})
localTM.add(Transport.prototype[Symbol.toStringTag], Transport, { filter: filters.all })
})
afterEach(async () => {
await peerStore.delete(peerId)
sinon.restore()
})
it('should have appropriate defaults', () => {
const dialer = new Dialer({
transportManager: localTM,
peerStore,
connectionGater
})
expect(dialer.maxParallelDials).to.equal(Constants.MAX_PARALLEL_DIALS)
expect(dialer.timeout).to.equal(Constants.DIAL_TIMEOUT)
})
it('should limit the number of tokens it provides', () => {
const dialer = new Dialer({
transportManager: localTM,
peerStore,
connectionGater
})
const maxPerPeer = Constants.MAX_PER_PEER_DIALS
expect(dialer.tokens).to.have.length(Constants.MAX_PARALLEL_DIALS)
const tokens = dialer.getTokens(maxPerPeer + 1)
expect(tokens).to.have.length(maxPerPeer)
expect(dialer.tokens).to.have.length(Constants.MAX_PARALLEL_DIALS - maxPerPeer)
})
it('should not return tokens if non are left', () => {
const dialer = new Dialer({
transportManager: localTM,
peerStore,
connectionGater
})
sinon.stub(dialer, 'tokens').value([])
const tokens = dialer.getTokens(1)
expect(tokens.length).to.equal(0)
})
it('should NOT be able to return a token twice', () => {
const dialer = new Dialer({
transportManager: localTM,
peerStore,
connectionGater
})
const tokens = dialer.getTokens(1)
expect(tokens).to.have.length(1)
expect(dialer.tokens).to.have.length(Constants.MAX_PARALLEL_DIALS - 1)
dialer.releaseToken(tokens[0])
dialer.releaseToken(tokens[0])
expect(dialer.tokens).to.have.length(Constants.MAX_PARALLEL_DIALS)
})
it('should be able to connect to a remote node via its multiaddr', async () => {
const dialer = new Dialer({
transportManager: localTM,
peerStore: {
addressBook: {
add: () => {},
getMultiaddrsForPeer: () => [remoteAddr]
}
},
connectionGater
})
const connection = await dialer.connectToPeer(remoteAddr)
expect(connection).to.exist()
await connection.close()
})
it('should be able to connect to a remote node via its stringified multiaddr', async () => {
const dialer = new Dialer({
transportManager: localTM,
peerStore: {
addressBook: {
add: () => {},
getMultiaddrsForPeer: () => [remoteAddr]
}
},
connectionGater
})
const connection = await dialer.connectToPeer(remoteAddr.toString())
expect(connection).to.exist()
await connection.close()
})
it('should fail to connect to an unsupported multiaddr', async () => {
const dialer = new Dialer({
transportManager: localTM,
peerStore,
connectionGater
})
await expect(dialer.connectToPeer(unsupportedAddr))
.to.eventually.be.rejectedWith(AggregateError)
})
it('should be able to connect to a given peer', async () => {
const dialer = new Dialer({
transportManager: localTM,
peerStore: {
addressBook: {
add: () => {},
getMultiaddrsForPeer: () => [remoteAddr]
}
},
connectionGater
})
const connection = await dialer.connectToPeer(peerId)
expect(connection).to.exist()
await connection.close()
})
it('should fail to connect to a given peer with unsupported addresses', async () => {
const dialer = new Dialer({
transportManager: localTM,
peerStore: {
addressBook: {
set: () => {},
getMultiaddrsForPeer: () => [unsupportedAddr]
}
},
connectionGater
})
await expect(dialer.connectToPeer(peerId))
.to.eventually.be.rejectedWith(AggregateError)
})
it('should abort dials on queue task timeout', async () => {
const dialer = new Dialer({
transportManager: localTM,
dialTimeout: 50,
peerStore: {
addressBook: {
add: () => {},
getMultiaddrsForPeer: () => [remoteAddr]
}
},
connectionGater
})
sinon.stub(localTM, 'dial').callsFake(async (addr, options) => {
expect(options.signal).to.exist()
expect(options.signal.aborted).to.equal(false)
expect(addr.toString()).to.eql(remoteAddr.toString())
await delay(60)
expect(options.signal.aborted).to.equal(true)
throw new AbortError()
})
await expect(dialer.connectToPeer(remoteAddr))
.to.eventually.be.rejected()
.and.to.have.property('code', ErrorCodes.ERR_TIMEOUT)
})
it('should throw when a peer advertises more than the allowed number of peers', async () => {
const spy = sinon.spy()
const dialer = new Dialer({
transportManager: localTM,
maxAddrsToDial: 10,
peerStore: {
delete: spy,
addressBook: {
add: () => { },
getMultiaddrsForPeer: () => Array.from({ length: 11 }, (_, i) => new Multiaddr(`/ip4/127.0.0.1/tcp/1500${i}/ws/p2p/12D3KooWHFKTMzwerBtsVmtz4ZZEQy2heafxzWw6wNn5PPYkBxJ5`))
}
},
connectionGater
})
await expect(dialer.connectToPeer(remoteAddr))
.to.eventually.be.rejected()
.and.to.have.property('code', ErrorCodes.ERR_TOO_MANY_ADDRESSES)
expect(spy.calledOnce).to.be.true()
})
it('should sort addresses on dial', async () => {
const peerMultiaddrs = [
new Multiaddr('/ip4/127.0.0.1/tcp/15001/ws'),
new Multiaddr('/ip4/20.0.0.1/tcp/15001/ws'),
new Multiaddr('/ip4/30.0.0.1/tcp/15001/ws')
]
sinon.spy(addressSort, 'publicAddressesFirst')
sinon.stub(localTM, 'dial').callsFake(createMockConnection)
const dialer = new Dialer({
transportManager: localTM,
addressSorter: addressSort.publicAddressesFirst,
maxParallelDials: 3,
peerStore,
connectionGater
})
// Inject data in the AddressBook
await peerStore.addressBook.add(peerId, peerMultiaddrs)
// Perform 3 multiaddr dials
await dialer.connectToPeer(peerId)
expect(addressSort.publicAddressesFirst.callCount).to.eql(1)
const sortedAddresses = addressSort.publicAddressesFirst(peerMultiaddrs.map((m) => ({ multiaddr: m })))
expect(localTM.dial.getCall(0).args[0].equals(sortedAddresses[0].multiaddr))
expect(localTM.dial.getCall(1).args[0].equals(sortedAddresses[1].multiaddr))
expect(localTM.dial.getCall(2).args[0].equals(sortedAddresses[2].multiaddr))
})
it('should dial to the max concurrency', async () => {
const dialer = new Dialer({
transportManager: localTM,
maxParallelDials: 2,
peerStore: {
addressBook: {
set: () => {},
getMultiaddrsForPeer: () => [remoteAddr, remoteAddr, remoteAddr]
}
},
connectionGater
})
expect(dialer.tokens).to.have.length(2)
const deferredDial = pDefer()
sinon.stub(localTM, 'dial').callsFake(() => deferredDial.promise)
// Perform 3 multiaddr dials
dialer.connectToPeer(peerId)
// Let the call stack run
await delay(0)
// We should have 2 in progress, and 1 waiting
expect(dialer.tokens).to.have.length(0)
expect(dialer._pendingDials.size).to.equal(1) // 1 dial request
deferredDial.resolve(await createMockConnection())
// Let the call stack run
await delay(0)
// Only two dials will be run, as the first two succeeded
expect(localTM.dial.callCount).to.equal(2)
expect(dialer.tokens).to.have.length(2)
expect(dialer._pendingDials.size).to.equal(0)
})
it('.destroy should abort pending dials', async () => {
const dialer = new Dialer({
transportManager: localTM,
maxParallelDials: 2,
peerStore: {
addressBook: {
set: () => {},
getMultiaddrsForPeer: () => [remoteAddr, remoteAddr, remoteAddr]
}
},
connectionGater
})
expect(dialer.tokens).to.have.length(2)
sinon.stub(localTM, 'dial').callsFake((_, options) => {
const deferredDial = pDefer()
const onAbort = () => {
options.signal.removeEventListener('abort', onAbort)
deferredDial.reject(new AbortError())
}
options.signal.addEventListener('abort', onAbort)
return deferredDial.promise
})
// Perform 3 multiaddr dials
const dialPromise = dialer.connectToPeer(peerId)
// Let the call stack run
await delay(0)
// We should have 2 in progress, and 1 waiting
expect(dialer.tokens).to.have.length(0)
expect(dialer._pendingDials.size).to.equal(1) // 1 dial request
try {
dialer.destroy()
await dialPromise
expect.fail('should have failed')
} catch (/** @type {any} */ err) {
expect(err).to.be.an.instanceof(AggregateError)
expect(dialer._pendingDials.size).to.equal(0) // 1 dial request
}
})
it('should cancel pending dial targets before proceeding', async () => {
const dialer = new Dialer({
transportManager: localTM,
peerStore: {
addressBook: {
set: () => { }
}
},
connectionGater
})
sinon.stub(dialer, '_createDialTarget').callsFake(() => {
const deferredDial = pDefer()
return deferredDial.promise
})
// Perform dial
const dialPromise = dialer.connectToPeer(peerId)
// Let the call stack run
await delay(0)
dialer.destroy()
await expect(dialPromise)
.to.eventually.be.rejected()
.and.to.have.property('code', 'ABORT_ERR')
})
describe('libp2p.dialer', () => {
const transportKey = Transport.prototype[Symbol.toStringTag]
let libp2p
afterEach(async () => {
sinon.restore()
libp2p && await libp2p.stop()
libp2p = null
})
it('should create a dialer', () => {
libp2p = new Libp2p({
peerId,
modules: {
transport: [Transport],
streamMuxer: [Muxer],
connEncryption: [Crypto]
},
config: {
transport: {
[transportKey]: {
filter: filters.all
}
}
},
connectionGater
})
expect(libp2p.dialer).to.exist()
expect(libp2p.dialer.maxParallelDials).to.equal(Constants.MAX_PARALLEL_DIALS)
expect(libp2p.dialer.maxDialsPerPeer).to.equal(Constants.MAX_PER_PEER_DIALS)
expect(libp2p.dialer.timeout).to.equal(Constants.DIAL_TIMEOUT)
// Ensure the dialer also has the transport manager
expect(libp2p.transportManager).to.equal(libp2p.dialer.transportManager)
})
it('should be able to override dialer options', async () => {
const config = {
peerId,
modules: {
transport: [Transport],
streamMuxer: [Muxer],
connEncryption: [Crypto]
},
dialer: {
maxParallelDials: 10,
maxDialsPerPeer: 1,
dialTimeout: 1e3 // 30 second dial timeout per peer
},
config: {
transport: {
[transportKey]: {
filter: filters.all
}
}
}
}
libp2p = await Libp2p.create(config)
expect(libp2p.dialer).to.exist()
expect(libp2p.dialer.maxParallelDials).to.equal(config.dialer.maxParallelDials)
expect(libp2p.dialer.maxDialsPerPeer).to.equal(config.dialer.maxDialsPerPeer)
expect(libp2p.dialer.timeout).to.equal(config.dialer.dialTimeout)
})
it('should use the dialer for connecting', async () => {
libp2p = new Libp2p({
peerId,
modules: {
transport: [Transport],
streamMuxer: [Muxer],
connEncryption: [Crypto]
},
config: {
transport: {
[transportKey]: {
filter: filters.all
}
}
}
})
sinon.spy(libp2p.dialer, 'connectToPeer')
sinon.spy(libp2p.peerStore.addressBook, 'add')
await libp2p.start()
const connection = await libp2p.dial(remoteAddr)
expect(connection).to.exist()
const { stream, protocol } = await connection.newStream('/echo/1.0.0')
expect(stream).to.exist()
expect(protocol).to.equal('/echo/1.0.0')
await connection.close()
expect(libp2p.dialer.connectToPeer.callCount).to.be.at.least(1)
expect(libp2p.peerStore.addressBook.add.callCount).to.be.at.least(1)
await libp2p.stop()
})
it('should run identify automatically after connecting', async () => {
libp2p = new Libp2p({
peerId,
modules: {
transport: [Transport],
streamMuxer: [Muxer],
connEncryption: [Crypto]
},
config: {
transport: {
[transportKey]: {
filter: filters.all
}
}
}
})
sinon.spy(libp2p.identifyService, 'identify')
sinon.spy(libp2p.upgrader, 'onConnection')
await libp2p.start()
const connection = await libp2p.dial(remoteAddr)
expect(connection).to.exist()
sinon.spy(libp2p.peerStore.protoBook, 'set')
// Wait for onConnection to be called
await pWaitFor(() => libp2p.upgrader.onConnection.callCount === 1)
expect(libp2p.identifyService.identify.callCount).to.equal(1)
await libp2p.identifyService.identify.firstCall.returnValue
expect(libp2p.peerStore.protoBook.set.callCount).to.equal(1)
await libp2p.stop()
})
it('should be able to use hangup to close connections', async () => {
libp2p = new Libp2p({
peerId,
modules: {
transport: [Transport],
streamMuxer: [Muxer],
connEncryption: [Crypto]
},
config: {
transport: {
[transportKey]: {
filter: filters.all
}
}
}
})
await libp2p.start()
const connection = await libp2p.dial(remoteAddr)
expect(connection).to.exist()
expect(connection.stat.timeline.close).to.not.exist()
await libp2p.hangUp(connection.remotePeer)
expect(connection.stat.timeline.close).to.exist()
await libp2p.stop()
})
it('should be able to use hangup when no connection exists', async () => {
libp2p = new Libp2p({
peerId,
modules: {
transport: [Transport],
streamMuxer: [Muxer],
connEncryption: [Crypto]
},
config: {
transport: {
[transportKey]: {
filter: filters.all
}
}
}
})
await libp2p.hangUp(remoteAddr)
})
it('should cancel pending dial targets and stop', async () => {
const [, remotePeerId] = await createPeerId({ number: 2 })
libp2p = new Libp2p({
peerId,
modules: {
transport: [Transport],
streamMuxer: [Muxer],
connEncryption: [Crypto]
},
config: {
transport: {
[transportKey]: {
filter: filters.all
}
}
}
})
sinon.stub(libp2p.dialer, '_createDialTarget').callsFake(() => {
const deferredDial = pDefer()
return deferredDial.promise
})
// Perform dial
const dialPromise = libp2p.dial(remotePeerId)
// Let the call stack run
await delay(0)
await libp2p.stop()
await expect(dialPromise)
.to.eventually.be.rejected()
.and.to.have.property('code', 'ABORT_ERR')
})
it('should abort pending dials on stop', async () => {
libp2p = new Libp2p({
peerId,
modules: {
transport: [Transport],
streamMuxer: [Muxer],
connEncryption: [Crypto]
},
config: {
transport: {
[transportKey]: {
filter: filters.all
}
}
}
})
sinon.spy(libp2p.dialer, 'destroy')
await libp2p.stop()
expect(libp2p.dialer.destroy).to.have.property('callCount', 1)
})
it('should fail to dial self', async () => {
libp2p = new Libp2p({
peerId,
modules: {
transport: [Transport],
streamMuxer: [Muxer],
connEncryption: [Crypto]
}
})
await expect(libp2p.dial(peerId))
.to.eventually.be.rejected()
.and.to.have.property('code', ErrorCodes.ERR_DIALED_SELF)
})
})
})