Skip to content

Commit e2f4943

Browse files
authored
fix: ignore failures to listen on IPv6 addresses when IPv4 succeeds (#3001)
If listening on all IPv4 addresses succeeds but listening on all IPv6 addresses fails, we may be on a network that doesn't support IPv6 so ignore the failure. Fixes #2977
1 parent e91a5a4 commit e2f4943

File tree

7 files changed

+161
-38
lines changed

7 files changed

+161
-38
lines changed

packages/libp2p/src/errors.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,6 @@ export class NoValidAddressesError extends Error {
6666
}
6767
}
6868

69-
export class NoSupportedAddressesError extends Error {
70-
constructor (message = 'No supported addresses') {
71-
super(message)
72-
this.name = 'NoSupportedAddressesError'
73-
}
74-
}
75-
7669
export class ConnectionInterceptedError extends Error {
7770
constructor (message = 'Connection intercepted') {
7871
super(message)

packages/libp2p/src/transport-manager.ts

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FaultTolerance, InvalidParametersError, NotStartedError } from '@libp2p/interface'
22
import { trackedMap } from '@libp2p/utils/tracked-map'
3+
import { IP4, IP6 } from '@multiformats/multiaddr-matcher'
34
import { CustomProgressEvent } from 'progress-events'
45
import { NoValidAddressesError, TransportUnavailableError } from './errors.js'
56
import type { Libp2pEvents, ComponentLogger, Logger, Connection, TypedEventTarget, Metrics, Startable, Listener, Transport, Upgrader } from '@libp2p/interface'
@@ -18,6 +19,17 @@ export interface DefaultTransportManagerComponents {
1819
logger: ComponentLogger
1920
}
2021

22+
interface IPStats {
23+
success: number
24+
attempts: number
25+
}
26+
27+
interface ListenStats {
28+
unsupportedAddresses: Set<string>
29+
ipv4: IPStats
30+
ipv6: IPStats
31+
}
32+
2133
export class DefaultTransportManager implements TransportManager, Startable {
2234
private readonly log: Logger
2335
private readonly components: DefaultTransportManagerComponents
@@ -192,11 +204,26 @@ export class DefaultTransportManager implements TransportManager, Startable {
192204
return
193205
}
194206

195-
const couldNotListen = []
207+
// track IPv4/IPv6 results - if we succeed on IPv4 but all IPv6 attempts
208+
// fail then we are probably on a network without IPv6 support
209+
const listenStats: ListenStats = {
210+
unsupportedAddresses: new Set(
211+
addrs.map(ma => ma.toString())
212+
),
213+
ipv4: {
214+
success: 0,
215+
attempts: 0
216+
},
217+
ipv6: {
218+
success: 0,
219+
attempts: 0
220+
}
221+
}
222+
223+
const tasks: Array<Promise<void>> = []
196224

197225
for (const [key, transport] of this.transports.entries()) {
198226
const supportedAddrs = transport.listenFilter(addrs)
199-
const tasks = []
200227

201228
// For each supported multiaddr, create a listener
202229
for (const addr of supportedAddrs) {
@@ -231,36 +258,70 @@ export class DefaultTransportManager implements TransportManager, Startable {
231258
})
232259
})
233260

261+
// track IPv4/IPv6 support
262+
if (IP4.matches(addr)) {
263+
listenStats.ipv4.attempts++
264+
} else if (IP6.matches(addr)) {
265+
listenStats.ipv6.attempts++
266+
}
267+
234268
// We need to attempt to listen on everything
235-
tasks.push(listener.listen(addr))
269+
tasks.push(
270+
listener.listen(addr)
271+
.then(() => {
272+
listenStats.unsupportedAddresses.delete(addr.toString())
273+
274+
if (IP4.matches(addr)) {
275+
listenStats.ipv4.success++
276+
}
277+
278+
if (IP6.matches(addr)) {
279+
listenStats.ipv6.success++
280+
}
281+
}, (err) => {
282+
this.log.error('transport %s could not listen on address %a - %e', key, addr, err)
283+
throw err
284+
})
285+
)
236286
}
287+
}
237288

238-
// Keep track of transports we had no addresses for
239-
if (tasks.length === 0) {
240-
couldNotListen.push(key)
241-
continue
242-
}
289+
const results = await Promise.allSettled(tasks)
243290

244-
const results = await Promise.allSettled(tasks)
245-
// If we are listening on at least 1 address, succeed.
246-
// TODO: we should look at adding a retry (`p-retry`) here to better support
247-
// listening on remote addresses as they may be offline. We could then potentially
248-
// just wait for any (`p-any`) listener to succeed on each transport before returning
249-
const isListening = results.find(r => r.status === 'fulfilled')
250-
if ((isListening == null) && this.faultTolerance !== FaultTolerance.NO_FATAL) {
251-
throw new NoValidAddressesError(`Transport (${key}) could not listen on any available address`)
252-
}
291+
// listening on all addresses, all good
292+
if (results.length > 0 && results.every(res => res.status === 'fulfilled')) {
293+
return
253294
}
254295

255-
// If no transports were able to listen, throw an error. This likely
256-
// means we were given addresses we do not have transports for
257-
if (couldNotListen.length === this.transports.size) {
258-
const message = `no valid addresses were provided for transports [${couldNotListen.join(', ')}]`
259-
if (this.faultTolerance === FaultTolerance.FATAL_ALL) {
260-
throw new NoValidAddressesError(message)
261-
}
262-
this.log(`libp2p in dial mode only: ${message}`)
296+
// detect lack of IPv6 support on the current network - if we tried to
297+
// listen on IPv4 and IPv6 addresses, and all IPv4 addresses succeeded but
298+
// all IPv6 addresses fail, then we can assume there's no IPv6 here
299+
if (this.ipv6Unsupported(listenStats)) {
300+
this.log('all IPv4 addresses succeed but all IPv6 failed')
301+
return
302+
}
303+
304+
if (this.faultTolerance === FaultTolerance.NO_FATAL) {
305+
// ok to be dial-only
306+
this.log('failed to listen on any address but fault tolerance allows this')
307+
return
263308
}
309+
310+
// if a configured address was not able to be listened on, throw an error
311+
throw new NoValidAddressesError(`No configured transport could listen on these addresses, please remove them from your config: ${[
312+
...listenStats.unsupportedAddresses
313+
].join(', ')}`)
314+
}
315+
316+
private ipv6Unsupported (listenStats: ListenStats): boolean {
317+
if (listenStats.ipv4.attempts === 0 || listenStats.ipv6.attempts === 0) {
318+
return false
319+
}
320+
321+
const allIpv4Succeeded = listenStats.ipv4.attempts === listenStats.ipv4.success
322+
const allIpv6Failed = listenStats.ipv6.success === 0
323+
324+
return allIpv4Succeeded && allIpv6Failed
264325
}
265326

266327
/**

packages/libp2p/test/transports/transport-manager.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-env mocha */
22

3+
import { isIPv6 } from '@chainsafe/is-ip'
34
import { generateKeyPair } from '@libp2p/crypto/keys'
45
import { TypedEventEmitter, start, stop, FaultTolerance } from '@libp2p/interface'
56
import { defaultLogger } from '@libp2p/logger'
@@ -150,6 +151,52 @@ describe('Transport Manager', () => {
150151
expect(spyListener.called).to.be.true()
151152
})
152153

154+
it('should throw if no transports support configured addresses', async () => {
155+
components.addressManager = new AddressManager(components, {
156+
listen: [
157+
'/ip4/0.0.0.0/tcp/0',
158+
'/ip4/0.0.0.0/tcp/0/ws'
159+
]
160+
})
161+
162+
const transportManager = new DefaultTransportManager(components)
163+
164+
const transport = stubInterface<Transport>({
165+
listenFilter: () => []
166+
})
167+
transportManager.add(transport)
168+
169+
await expect(start(transportManager)).to.eventually.be.rejected
170+
.with.property('name', 'NoValidAddressesError')
171+
})
172+
173+
it('should detect lack of IPv6 support', async () => {
174+
components.addressManager = new AddressManager(components, {
175+
listen: [
176+
'/ip4/0.0.0.0/tcp/0',
177+
'/ip6/::/tcp/0'
178+
]
179+
})
180+
181+
const transportManager = new DefaultTransportManager(components)
182+
183+
const transport = stubInterface<Transport>({
184+
listenFilter: (addrs) => addrs,
185+
createListener: () => {
186+
return stubInterface<Listener>({
187+
listen: async (ma) => {
188+
if (isIPv6(ma.toOptions().host)) {
189+
throw new Error('Listen on IPv6 failed')
190+
}
191+
}
192+
})
193+
}
194+
})
195+
transportManager.add(transport)
196+
197+
await start(transportManager)
198+
})
199+
153200
it('should be able to dial', async () => {
154201
tm.add(transport)
155202
await tm.listen(addrs)

packages/transport-webrtc/src/private-to-private/listener.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TypedEventEmitter } from '@libp2p/interface'
22
import { P2P } from '@multiformats/multiaddr-matcher'
33
import { fmt, literal } from '@multiformats/multiaddr-matcher/utils'
4-
import type { PeerId, ListenerEvents, Listener } from '@libp2p/interface'
4+
import type { PeerId, ListenerEvents, Listener, Libp2pEvents, TypedEventTarget } from '@libp2p/interface'
55
import type { TransportManager } from '@libp2p/interface-internal'
66
import type { Multiaddr } from '@multiformats/multiaddr'
77

@@ -10,6 +10,7 @@ const Circuit = fmt(P2P.matchers[0], literal('p2p-circuit'))
1010
export interface WebRTCPeerListenerComponents {
1111
peerId: PeerId
1212
transportManager: TransportManager
13+
events: TypedEventTarget<Libp2pEvents>
1314
}
1415

1516
export interface WebRTCPeerListenerInit {
@@ -19,18 +20,32 @@ export interface WebRTCPeerListenerInit {
1920
export class WebRTCPeerListener extends TypedEventEmitter<ListenerEvents> implements Listener {
2021
private readonly transportManager: TransportManager
2122
private readonly shutdownController: AbortController
23+
private readonly events: TypedEventTarget<Libp2pEvents>
2224

2325
constructor (components: WebRTCPeerListenerComponents, init: WebRTCPeerListenerInit) {
2426
super()
2527

2628
this.transportManager = components.transportManager
29+
this.events = components.events
2730
this.shutdownController = init.shutdownController
31+
32+
this.onTransportListening = this.onTransportListening.bind(this)
2833
}
2934

3035
async listen (): Promise<void> {
31-
queueMicrotask(() => {
36+
this.events.addEventListener('transport:listening', this.onTransportListening)
37+
}
38+
39+
onTransportListening (event: CustomEvent<Listener>): void {
40+
const circuitAddresses = event.detail.getAddrs()
41+
.filter(ma => Circuit.exactMatch(ma))
42+
.map(ma => {
43+
return ma.encapsulate('/webrtc')
44+
})
45+
46+
if (circuitAddresses.length > 0) {
3247
this.safeDispatchEvent('listening')
33-
})
48+
}
3449
}
3550

3651
getAddrs (): Multiaddr[] {
@@ -51,6 +66,8 @@ export class WebRTCPeerListener extends TypedEventEmitter<ListenerEvents> implem
5166
}
5267

5368
async close (): Promise<void> {
69+
this.events.removeEventListener('transport:listening', this.onTransportListening)
70+
5471
this.shutdownController.abort()
5572
queueMicrotask(() => {
5673
this.safeDispatchEvent('close')

packages/transport-webrtc/src/private-to-private/transport.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { initiateConnection } from './initiate-connection.js'
1010
import { WebRTCPeerListener } from './listener.js'
1111
import { handleIncomingStream } from './signaling-stream-handler.js'
1212
import type { DataChannelOptions } from '../index.js'
13-
import type { OutboundConnectionUpgradeEvents, CreateListenerOptions, DialTransportOptions, Transport, Listener, Upgrader, ComponentLogger, Logger, Connection, PeerId, CounterGroup, Metrics, Startable, OpenConnectionProgressEvents, IncomingStreamData } from '@libp2p/interface'
13+
import type { OutboundConnectionUpgradeEvents, CreateListenerOptions, DialTransportOptions, Transport, Listener, Upgrader, ComponentLogger, Logger, Connection, PeerId, CounterGroup, Metrics, Startable, OpenConnectionProgressEvents, IncomingStreamData, Libp2pEvents, TypedEventTarget } from '@libp2p/interface'
1414
import type { Registrar, ConnectionManager, TransportManager } from '@libp2p/interface-internal'
1515
import type { ProgressEvent } from 'progress-events'
1616

@@ -38,6 +38,7 @@ export interface WebRTCTransportComponents {
3838
connectionManager: ConnectionManager
3939
metrics?: Metrics
4040
logger: ComponentLogger
41+
events: TypedEventTarget<Libp2pEvents>
4142
}
4243

4344
export interface WebRTCTransportMetrics {

packages/transport-webrtc/test/listener.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { generateKeyPair } from '@libp2p/crypto/keys'
2+
import { TypedEventEmitter } from '@libp2p/interface'
23
import { peerIdFromPrivateKey } from '@libp2p/peer-id'
34
import { multiaddr } from '@multiformats/multiaddr'
45
import { expect } from 'aegir/chai'
@@ -17,7 +18,8 @@ describe('webrtc private-to-private listener', () => {
1718

1819
const listener = new WebRTCPeerListener({
1920
peerId,
20-
transportManager
21+
transportManager,
22+
events: new TypedEventEmitter()
2123
}, {
2224
shutdownController: new AbortController()
2325
})

packages/transport-webrtc/test/peer.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { generateKeyPair } from '@libp2p/crypto/keys'
2+
import { TypedEventEmitter } from '@libp2p/interface'
23
import { streamPair } from '@libp2p/interface-compliance-tests/mocks'
34
import { defaultLogger, logger } from '@libp2p/logger'
45
import { peerIdFromPrivateKey } from '@libp2p/peer-id'
@@ -252,7 +253,8 @@ describe('webrtc filter', () => {
252253
peerId: Sinon.stub() as any,
253254
registrar: stubInterface<Registrar>(),
254255
upgrader: stubInterface<Upgrader>(),
255-
logger: defaultLogger()
256+
logger: defaultLogger(),
257+
events: new TypedEventEmitter()
256258
})
257259

258260
const valid = [

0 commit comments

Comments
 (0)