Comparing version 2.7.3 to 2.7.4-22e62d00f
@@ -65,2 +65,3 @@ import { PeerMap } from '@libp2p/peer-collections'; | ||
dial(peerIdOrMultiaddr: PeerId | Multiaddr | Multiaddr[], options?: OpenConnectionOptions): Promise<Connection>; | ||
private dialPeer; | ||
private calculateMultiaddrs; | ||
@@ -67,0 +68,0 @@ isDialable(multiaddr: Multiaddr | Multiaddr[], options?: IsDialableOptions): Promise<boolean>; |
@@ -138,3 +138,3 @@ /* eslint-disable max-depth */ | ||
return this.queue.add(async (options) => { | ||
options?.onProgress?.(new CustomProgressEvent('dial-queue:start-dial')); | ||
options.onProgress?.(new CustomProgressEvent('dial-queue:start-dial')); | ||
// create abort conditions - need to do this before `calculateMultiaddrs` as | ||
@@ -147,42 +147,98 @@ // we may be about to resolve a dns addr which can time out | ||
setMaxListeners(Infinity, signal); | ||
let addrsToDial; | ||
try { | ||
// load addresses from address book, resolve and dnsaddrs, filter | ||
// undialables, add peer IDs, etc | ||
addrsToDial = await this.calculateMultiaddrs(peerId, options?.multiaddrs, { | ||
...options, | ||
signal | ||
}); | ||
options?.onProgress?.(new CustomProgressEvent('dial-queue:calculated-addresses', addrsToDial)); | ||
addrsToDial.map(({ multiaddr }) => multiaddr.toString()).forEach(addr => { | ||
options?.multiaddrs.add(addr); | ||
}); | ||
return await this.dialPeer(options, signal); | ||
} | ||
catch (err) { | ||
finally { | ||
// clean up abort signals/controllers | ||
signal.clear(); | ||
throw err; | ||
} | ||
try { | ||
let dialed = 0; | ||
const errors = []; | ||
for (const address of addrsToDial) { | ||
if (dialed === this.maxPeerAddrsToDial) { | ||
this.log('dialed maxPeerAddrsToDial (%d) addresses for %p, not trying any others', dialed, peerId); | ||
throw new DialError('Peer had more than maxPeerAddrsToDial'); | ||
} | ||
dialed++; | ||
}, { | ||
peerId, | ||
priority: options.priority ?? DEFAULT_DIAL_PRIORITY, | ||
multiaddrs: new Set(multiaddrs.map(ma => ma.toString())), | ||
signal: options.signal ?? AbortSignal.timeout(this.dialTimeout), | ||
onProgress: options.onProgress | ||
}); | ||
} | ||
async dialPeer(options, signal) { | ||
const peerId = options.peerId; | ||
const multiaddrs = options.multiaddrs; | ||
const failedMultiaddrs = new Set(); | ||
// if we have no multiaddrs, only a peer id, set a flag so we will look the | ||
// peer up in the peer routing to obtain multiaddrs | ||
let forcePeerLookup = options.multiaddrs.size === 0; | ||
let dialed = 0; | ||
let dialIteration = 0; | ||
const errors = []; | ||
this.log('starting dial to %p', peerId); | ||
// repeat this operation in case addresses are added to the dial while we | ||
// resolve multiaddrs, etc | ||
while (forcePeerLookup || multiaddrs.size > 0) { | ||
dialIteration++; | ||
// only perform peer lookup once | ||
forcePeerLookup = false; | ||
// the addresses we will dial | ||
const addrsToDial = []; | ||
// copy the addresses into a new set | ||
const addrs = new Set(options.multiaddrs); | ||
// empty the old set - subsequent dial attempts for the same peer id may | ||
// add more addresses to try | ||
multiaddrs.clear(); | ||
this.log('calculating addrs to dial %p from %s', peerId, [...addrs]); | ||
// load addresses from address book, resolve and dnsaddrs, filter | ||
// undialables, add peer IDs, etc | ||
const calculatedAddrs = await this.calculateMultiaddrs(peerId, addrs, { | ||
...options, | ||
signal | ||
}); | ||
for (const addr of calculatedAddrs) { | ||
// skip any addresses we have previously failed to dial | ||
if (failedMultiaddrs.has(addr.multiaddr.toString())) { | ||
this.log.trace('skipping previously failed multiaddr %a while dialing %p', addr.multiaddr, peerId); | ||
continue; | ||
} | ||
addrsToDial.push(addr); | ||
} | ||
this.log('%s dial to %p with %s', dialIteration === 1 ? 'starting' : 'continuing', peerId, addrsToDial.map(ma => ma.multiaddr.toString())); | ||
options?.onProgress?.(new CustomProgressEvent('dial-queue:calculated-addresses', addrsToDial)); | ||
for (const address of addrsToDial) { | ||
if (dialed === this.maxPeerAddrsToDial) { | ||
this.log('dialed maxPeerAddrsToDial (%d) addresses for %p, not trying any others', dialed, options.peerId); | ||
throw new DialError('Peer had more than maxPeerAddrsToDial'); | ||
} | ||
dialed++; | ||
try { | ||
// try to dial the address | ||
const conn = await this.components.transportManager.dial(address.multiaddr, { | ||
...options, | ||
signal | ||
}); | ||
this.log('dial to %a succeeded', address.multiaddr); | ||
// record the successful dial and the address | ||
try { | ||
const conn = await this.components.transportManager.dial(address.multiaddr, { | ||
...options, | ||
signal | ||
await this.components.peerStore.merge(conn.remotePeer, { | ||
multiaddrs: [ | ||
conn.remoteAddr | ||
], | ||
metadata: { | ||
[LAST_DIAL_SUCCESS_KEY]: uint8ArrayFromString(Date.now().toString()) | ||
} | ||
}); | ||
this.log('dial to %a succeeded', address.multiaddr); | ||
// record the successful dial and the address | ||
} | ||
catch (err) { | ||
this.log.error('could not update last dial failure key for %p', peerId, err); | ||
} | ||
// dial successful, return the connection | ||
return conn; | ||
} | ||
catch (err) { | ||
this.log.error('dial failed to %a', address.multiaddr, err); | ||
// ensure we don't dial it again in this attempt | ||
failedMultiaddrs.add(address.multiaddr.toString()); | ||
if (peerId != null) { | ||
// record the failed dial | ||
try { | ||
await this.components.peerStore.merge(conn.remotePeer, { | ||
multiaddrs: [ | ||
conn.remoteAddr | ||
], | ||
await this.components.peerStore.merge(peerId, { | ||
metadata: { | ||
[LAST_DIAL_SUCCESS_KEY]: uint8ArrayFromString(Date.now().toString()) | ||
[LAST_DIAL_FAILURE_KEY]: uint8ArrayFromString(Date.now().toString()) | ||
} | ||
@@ -194,42 +250,15 @@ }); | ||
} | ||
return conn; | ||
} | ||
catch (err) { | ||
this.log.error('dial failed to %a', address.multiaddr, err); | ||
if (peerId != null) { | ||
// record the failed dial | ||
try { | ||
await this.components.peerStore.merge(peerId, { | ||
metadata: { | ||
[LAST_DIAL_FAILURE_KEY]: uint8ArrayFromString(Date.now().toString()) | ||
} | ||
}); | ||
} | ||
catch (err) { | ||
this.log.error('could not update last dial failure key for %p', peerId, err); | ||
} | ||
} | ||
// the user/dial timeout/shutdown controller signal aborted | ||
if (signal.aborted) { | ||
throw new TimeoutError(err.message); | ||
} | ||
errors.push(err); | ||
// the user/dial timeout/shutdown controller signal aborted | ||
if (signal.aborted) { | ||
throw new TimeoutError(err.message); | ||
} | ||
errors.push(err); | ||
} | ||
if (errors.length === 1) { | ||
throw errors[0]; | ||
} | ||
throw new AggregateError(errors, 'All multiaddr dials failed'); | ||
} | ||
finally { | ||
// clean up abort signals/controllers | ||
signal.clear(); | ||
} | ||
}, { | ||
peerId, | ||
priority: options.priority ?? DEFAULT_DIAL_PRIORITY, | ||
multiaddrs: new Set(multiaddrs.map(ma => ma.toString())), | ||
signal: options.signal ?? AbortSignal.timeout(this.dialTimeout), | ||
onProgress: options.onProgress | ||
}); | ||
} | ||
if (errors.length === 1) { | ||
throw errors[0]; | ||
} | ||
throw new AggregateError(errors, 'All multiaddr dials failed'); | ||
} | ||
@@ -278,5 +307,8 @@ // eslint-disable-next-line complexity | ||
catch (err) { | ||
if (err.name !== 'NoPeerRoutersError') { | ||
this.log.error('looking up multiaddrs for %p in the peer routing failed', peerId, err); | ||
if (err.name === 'NoPeerRoutersError') { | ||
this.log('no peer routers configured', peerId); | ||
} | ||
else { | ||
this.log.error('looking up multiaddrs for %p in the peer routing failed - %e', peerId, err); | ||
} | ||
} | ||
@@ -283,0 +315,0 @@ } |
@@ -29,8 +29,11 @@ export declare enum messages { | ||
} | ||
export declare class NoValidAddressesError extends Error { | ||
export declare class UnsupportedListenAddressError extends Error { | ||
constructor(message?: string); | ||
} | ||
export declare class NoSupportedAddressesError extends Error { | ||
export declare class UnsupportedListenAddressesError extends Error { | ||
constructor(message?: string); | ||
} | ||
export declare class NoValidAddressesError extends Error { | ||
constructor(message?: string); | ||
} | ||
export declare class ConnectionInterceptedError extends Error { | ||
@@ -37,0 +40,0 @@ constructor(message?: string); |
@@ -54,2 +54,14 @@ export var messages; | ||
} | ||
export class UnsupportedListenAddressError extends Error { | ||
constructor(message = 'No transport was configured to listen on this address') { | ||
super(message); | ||
this.name = 'UnsupportedListenAddressError'; | ||
} | ||
} | ||
export class UnsupportedListenAddressesError extends Error { | ||
constructor(message = 'Configured listen addresses could not be listened on') { | ||
super(message); | ||
this.name = 'UnsupportedListenAddressesError'; | ||
} | ||
} | ||
export class NoValidAddressesError extends Error { | ||
@@ -61,8 +73,2 @@ constructor(message = 'No valid addresses') { | ||
} | ||
export class NoSupportedAddressesError extends Error { | ||
constructor(message = 'No supported addresses') { | ||
super(message); | ||
this.name = 'NoSupportedAddressesError'; | ||
} | ||
} | ||
export class ConnectionInterceptedError extends Error { | ||
@@ -69,0 +75,0 @@ constructor(message = 'Connection intercepted') { |
@@ -63,2 +63,3 @@ import { FaultTolerance } from '@libp2p/interface'; | ||
listen(addrs: Multiaddr[]): Promise<void>; | ||
private ipv6Unsupported; | ||
/** | ||
@@ -65,0 +66,0 @@ * Removes the given transport from the manager. |
import { FaultTolerance, InvalidParametersError, NotStartedError } from '@libp2p/interface'; | ||
import { trackedMap } from '@libp2p/utils/tracked-map'; | ||
import { IP4, IP6 } from '@multiformats/multiaddr-matcher'; | ||
import { CustomProgressEvent } from 'progress-events'; | ||
import { NoValidAddressesError, TransportUnavailableError } from './errors.js'; | ||
import { TransportUnavailableError, UnsupportedListenAddressError, UnsupportedListenAddressesError } from './errors.js'; | ||
export class DefaultTransportManager { | ||
@@ -148,6 +149,21 @@ log; | ||
} | ||
const couldNotListen = []; | ||
// track IPv4/IPv6 results - if we succeed on IPv4 but all IPv6 attempts | ||
// fail then we are probably on a network without IPv6 support | ||
const listenStats = { | ||
errors: new Map(), | ||
ipv4: { | ||
success: 0, | ||
attempts: 0 | ||
}, | ||
ipv6: { | ||
success: 0, | ||
attempts: 0 | ||
} | ||
}; | ||
addrs.forEach(ma => { | ||
listenStats.errors.set(ma.toString(), new UnsupportedListenAddressError()); | ||
}); | ||
const tasks = []; | ||
for (const [key, transport] of this.transports.entries()) { | ||
const supportedAddrs = transport.listenFilter(addrs); | ||
const tasks = []; | ||
// For each supported multiaddr, create a listener | ||
@@ -179,30 +195,58 @@ for (const addr of supportedAddrs) { | ||
}); | ||
// track IPv4/IPv6 support | ||
if (IP4.matches(addr)) { | ||
listenStats.ipv4.attempts++; | ||
} | ||
else if (IP6.matches(addr)) { | ||
listenStats.ipv6.attempts++; | ||
} | ||
// We need to attempt to listen on everything | ||
tasks.push(listener.listen(addr)); | ||
tasks.push(listener.listen(addr) | ||
.then(() => { | ||
listenStats.errors.delete(addr.toString()); | ||
if (IP4.matches(addr)) { | ||
listenStats.ipv4.success++; | ||
} | ||
if (IP6.matches(addr)) { | ||
listenStats.ipv6.success++; | ||
} | ||
}, (err) => { | ||
this.log.error('transport %s could not listen on address %a - %e', key, addr, err); | ||
listenStats.errors.set(addr.toString(), err); | ||
throw err; | ||
})); | ||
} | ||
// Keep track of transports we had no addresses for | ||
if (tasks.length === 0) { | ||
couldNotListen.push(key); | ||
continue; | ||
} | ||
const results = await Promise.allSettled(tasks); | ||
// If we are listening on at least 1 address, succeed. | ||
// TODO: we should look at adding a retry (`p-retry`) here to better support | ||
// listening on remote addresses as they may be offline. We could then potentially | ||
// just wait for any (`p-any`) listener to succeed on each transport before returning | ||
const isListening = results.find(r => r.status === 'fulfilled'); | ||
if ((isListening == null) && this.faultTolerance !== FaultTolerance.NO_FATAL) { | ||
throw new NoValidAddressesError(`Transport (${key}) could not listen on any available address`); | ||
} | ||
} | ||
// If no transports were able to listen, throw an error. This likely | ||
// means we were given addresses we do not have transports for | ||
if (couldNotListen.length === this.transports.size) { | ||
const message = `no valid addresses were provided for transports [${couldNotListen.join(', ')}]`; | ||
if (this.faultTolerance === FaultTolerance.FATAL_ALL) { | ||
throw new NoValidAddressesError(message); | ||
} | ||
this.log(`libp2p in dial mode only: ${message}`); | ||
const results = await Promise.allSettled(tasks); | ||
// listening on all addresses, all good | ||
if (results.length > 0 && results.every(res => res.status === 'fulfilled')) { | ||
return; | ||
} | ||
// detect lack of IPv6 support on the current network - if we tried to | ||
// listen on IPv4 and IPv6 addresses, and all IPv4 addresses succeeded but | ||
// all IPv6 addresses fail, then we can assume there's no IPv6 here | ||
if (this.ipv6Unsupported(listenStats)) { | ||
this.log('all IPv4 addresses succeed but all IPv6 failed'); | ||
return; | ||
} | ||
if (this.faultTolerance === FaultTolerance.NO_FATAL) { | ||
// ok to be dial-only | ||
this.log('failed to listen on any address but fault tolerance allows this'); | ||
return; | ||
} | ||
// if a configured address was not able to be listened on, throw an error | ||
throw new UnsupportedListenAddressesError(`Some configured addresses failed to be listened on, you may need to remove one or more listen addresses from your configuration or set \`transportManager.faultTolerance\` to NO_FATAL:\n${[...listenStats.errors.entries()].map(([addr, err]) => { | ||
return ` | ||
${addr}: ${`${err.stack ?? err}`.split('\n').join('\n ')} | ||
`; | ||
}).join('')}`); | ||
} | ||
ipv6Unsupported(listenStats) { | ||
if (listenStats.ipv4.attempts === 0 || listenStats.ipv6.attempts === 0) { | ||
return false; | ||
} | ||
const allIpv4Succeeded = listenStats.ipv4.attempts === listenStats.ipv4.success; | ||
const allIpv6Failed = listenStats.ipv6.success === 0; | ||
return allIpv4Succeeded && allIpv6Failed; | ||
} | ||
/** | ||
@@ -209,0 +253,0 @@ * Removes the given transport from the manager. |
@@ -1,3 +0,3 @@ | ||
export declare const version = "2.7.3"; | ||
export declare const version = "2.7.4-22e62d00f"; | ||
export declare const name = "js-libp2p"; | ||
//# sourceMappingURL=version.d.ts.map |
@@ -1,3 +0,3 @@ | ||
export const version = '2.7.3'; | ||
export const version = '2.7.4-22e62d00f'; | ||
export const name = 'js-libp2p'; | ||
//# sourceMappingURL=version.js.map |
{ | ||
"name": "libp2p", | ||
"version": "2.7.3", | ||
"version": "2.7.4-22e62d00f", | ||
"description": "JavaScript implementation of libp2p, a modular peer to peer network stack", | ||
@@ -95,11 +95,11 @@ "license": "Apache-2.0 OR MIT", | ||
"@chainsafe/netmask": "^2.0.0", | ||
"@libp2p/crypto": "^5.0.13", | ||
"@libp2p/interface": "^2.6.1", | ||
"@libp2p/interface-internal": "^2.3.5", | ||
"@libp2p/logger": "^5.1.10", | ||
"@libp2p/multistream-select": "^6.0.17", | ||
"@libp2p/peer-collections": "^6.0.21", | ||
"@libp2p/peer-id": "^5.0.14", | ||
"@libp2p/peer-store": "^11.0.21", | ||
"@libp2p/utils": "^6.5.5", | ||
"@libp2p/crypto": "5.0.13-22e62d00f", | ||
"@libp2p/interface": "2.6.1-22e62d00f", | ||
"@libp2p/interface-internal": "2.3.5-22e62d00f", | ||
"@libp2p/logger": "5.1.10-22e62d00f", | ||
"@libp2p/multistream-select": "6.0.17-22e62d00f", | ||
"@libp2p/peer-collections": "6.0.21-22e62d00f", | ||
"@libp2p/peer-id": "5.0.14-22e62d00f", | ||
"@libp2p/peer-store": "11.0.21-22e62d00f", | ||
"@libp2p/utils": "6.5.5-22e62d00f", | ||
"@multiformats/dns": "^1.0.6", | ||
@@ -106,0 +106,0 @@ "@multiformats/multiaddr": "^12.3.5", |
@@ -11,6 +11,35 @@ <h1 align="center"> | ||
[](http://webchat.freenode.net/?channels=%23libp2p) | ||
[](https://discord.com/invite/Ae4TbahHaT) | ||
[](https://discord.com/invite/Ae4TbahHaT) | ||
[](https://codecov.io/gh/libp2p/js-libp2p) | ||
[](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amain) | ||
# About | ||
<!-- | ||
!IMPORTANT! | ||
Everything in this README between "# About" and "# Install" is automatically | ||
generated and will be overwritten the next time the doc generator is run. | ||
To make changes to this section, please update the @packageDocumentation section | ||
of src/index.js or src/index.ts | ||
To experiment with formatting, please run "npm run docs" from the root of this | ||
repo and examine the changes made. | ||
--> | ||
Use the `createLibp2p` function to create a libp2p node. | ||
## Example | ||
```typescript | ||
import { createLibp2p } from 'libp2p' | ||
const node = await createLibp2p({ | ||
// ...other options | ||
}) | ||
``` | ||
# Background | ||
@@ -17,0 +46,0 @@ |
@@ -206,3 +206,3 @@ /* eslint-disable max-depth */ | ||
return this.queue.add(async (options) => { | ||
options?.onProgress?.(new CustomProgressEvent('dial-queue:start-dial')) | ||
options.onProgress?.(new CustomProgressEvent('dial-queue:start-dial')) | ||
// create abort conditions - need to do this before `calculateMultiaddrs` as | ||
@@ -216,51 +216,119 @@ // we may be about to resolve a dns addr which can time out | ||
let addrsToDial: Address[] | ||
try { | ||
// load addresses from address book, resolve and dnsaddrs, filter | ||
// undialables, add peer IDs, etc | ||
addrsToDial = await this.calculateMultiaddrs(peerId, options?.multiaddrs, { | ||
...options, | ||
signal | ||
}) | ||
return await this.dialPeer(options, signal) | ||
} finally { | ||
// clean up abort signals/controllers | ||
signal.clear() | ||
} | ||
}, { | ||
peerId, | ||
priority: options.priority ?? DEFAULT_DIAL_PRIORITY, | ||
multiaddrs: new Set(multiaddrs.map(ma => ma.toString())), | ||
signal: options.signal ?? AbortSignal.timeout(this.dialTimeout), | ||
onProgress: options.onProgress | ||
}) | ||
} | ||
options?.onProgress?.(new CustomProgressEvent<Address[]>('dial-queue:calculated-addresses', addrsToDial)) | ||
private async dialPeer (options: DialQueueJobOptions, signal: AbortSignal): Promise<Connection> { | ||
const peerId = options.peerId | ||
const multiaddrs = options.multiaddrs | ||
const failedMultiaddrs = new Set<string>() | ||
addrsToDial.map(({ multiaddr }) => multiaddr.toString()).forEach(addr => { | ||
options?.multiaddrs.add(addr) | ||
}) | ||
} catch (err) { | ||
signal.clear() | ||
throw err | ||
// if we have no multiaddrs, only a peer id, set a flag so we will look the | ||
// peer up in the peer routing to obtain multiaddrs | ||
let forcePeerLookup = options.multiaddrs.size === 0 | ||
let dialed = 0 | ||
let dialIteration = 0 | ||
const errors: Error[] = [] | ||
this.log('starting dial to %p', peerId) | ||
// repeat this operation in case addresses are added to the dial while we | ||
// resolve multiaddrs, etc | ||
while (forcePeerLookup || multiaddrs.size > 0) { | ||
dialIteration++ | ||
// only perform peer lookup once | ||
forcePeerLookup = false | ||
// the addresses we will dial | ||
const addrsToDial: Address[] = [] | ||
// copy the addresses into a new set | ||
const addrs = new Set(options.multiaddrs) | ||
// empty the old set - subsequent dial attempts for the same peer id may | ||
// add more addresses to try | ||
multiaddrs.clear() | ||
this.log('calculating addrs to dial %p from %s', peerId, [...addrs]) | ||
// load addresses from address book, resolve and dnsaddrs, filter | ||
// undialables, add peer IDs, etc | ||
const calculatedAddrs = await this.calculateMultiaddrs(peerId, addrs, { | ||
...options, | ||
signal | ||
}) | ||
for (const addr of calculatedAddrs) { | ||
// skip any addresses we have previously failed to dial | ||
if (failedMultiaddrs.has(addr.multiaddr.toString())) { | ||
this.log.trace('skipping previously failed multiaddr %a while dialing %p', addr.multiaddr, peerId) | ||
continue | ||
} | ||
addrsToDial.push(addr) | ||
} | ||
try { | ||
let dialed = 0 | ||
const errors: Error[] = [] | ||
this.log('%s dial to %p with %s', dialIteration === 1 ? 'starting' : 'continuing', peerId, addrsToDial.map(ma => ma.multiaddr.toString())) | ||
for (const address of addrsToDial) { | ||
if (dialed === this.maxPeerAddrsToDial) { | ||
this.log('dialed maxPeerAddrsToDial (%d) addresses for %p, not trying any others', dialed, peerId) | ||
options?.onProgress?.(new CustomProgressEvent<Address[]>('dial-queue:calculated-addresses', addrsToDial)) | ||
throw new DialError('Peer had more than maxPeerAddrsToDial') | ||
} | ||
for (const address of addrsToDial) { | ||
if (dialed === this.maxPeerAddrsToDial) { | ||
this.log('dialed maxPeerAddrsToDial (%d) addresses for %p, not trying any others', dialed, options.peerId) | ||
dialed++ | ||
throw new DialError('Peer had more than maxPeerAddrsToDial') | ||
} | ||
dialed++ | ||
try { | ||
// try to dial the address | ||
const conn = await this.components.transportManager.dial(address.multiaddr, { | ||
...options, | ||
signal | ||
}) | ||
this.log('dial to %a succeeded', address.multiaddr) | ||
// record the successful dial and the address | ||
try { | ||
const conn = await this.components.transportManager.dial(address.multiaddr, { | ||
...options, | ||
signal | ||
await this.components.peerStore.merge(conn.remotePeer, { | ||
multiaddrs: [ | ||
conn.remoteAddr | ||
], | ||
metadata: { | ||
[LAST_DIAL_SUCCESS_KEY]: uint8ArrayFromString(Date.now().toString()) | ||
} | ||
}) | ||
} catch (err: any) { | ||
this.log.error('could not update last dial failure key for %p', peerId, err) | ||
} | ||
this.log('dial to %a succeeded', address.multiaddr) | ||
// dial successful, return the connection | ||
return conn | ||
} catch (err: any) { | ||
this.log.error('dial failed to %a', address.multiaddr, err) | ||
// record the successful dial and the address | ||
// ensure we don't dial it again in this attempt | ||
failedMultiaddrs.add(address.multiaddr.toString()) | ||
if (peerId != null) { | ||
// record the failed dial | ||
try { | ||
await this.components.peerStore.merge(conn.remotePeer, { | ||
multiaddrs: [ | ||
conn.remoteAddr | ||
], | ||
await this.components.peerStore.merge(peerId, { | ||
metadata: { | ||
[LAST_DIAL_SUCCESS_KEY]: uint8ArrayFromString(Date.now().toString()) | ||
[LAST_DIAL_FAILURE_KEY]: uint8ArrayFromString(Date.now().toString()) | ||
} | ||
@@ -271,45 +339,19 @@ }) | ||
} | ||
} | ||
return conn | ||
} catch (err: any) { | ||
this.log.error('dial failed to %a', address.multiaddr, err) | ||
if (peerId != null) { | ||
// record the failed dial | ||
try { | ||
await this.components.peerStore.merge(peerId, { | ||
metadata: { | ||
[LAST_DIAL_FAILURE_KEY]: uint8ArrayFromString(Date.now().toString()) | ||
} | ||
}) | ||
} catch (err: any) { | ||
this.log.error('could not update last dial failure key for %p', peerId, err) | ||
} | ||
} | ||
// the user/dial timeout/shutdown controller signal aborted | ||
if (signal.aborted) { | ||
throw new TimeoutError(err.message) | ||
} | ||
errors.push(err) | ||
// the user/dial timeout/shutdown controller signal aborted | ||
if (signal.aborted) { | ||
throw new TimeoutError(err.message) | ||
} | ||
} | ||
if (errors.length === 1) { | ||
throw errors[0] | ||
errors.push(err) | ||
} | ||
} | ||
} | ||
throw new AggregateError(errors, 'All multiaddr dials failed') | ||
} finally { | ||
// clean up abort signals/controllers | ||
signal.clear() | ||
} | ||
}, { | ||
peerId, | ||
priority: options.priority ?? DEFAULT_DIAL_PRIORITY, | ||
multiaddrs: new Set(multiaddrs.map(ma => ma.toString())), | ||
signal: options.signal ?? AbortSignal.timeout(this.dialTimeout), | ||
onProgress: options.onProgress | ||
}) | ||
if (errors.length === 1) { | ||
throw errors[0] | ||
} | ||
throw new AggregateError(errors, 'All multiaddr dials failed') | ||
} | ||
@@ -364,4 +406,6 @@ | ||
} catch (err: any) { | ||
if (err.name !== 'NoPeerRoutersError') { | ||
this.log.error('looking up multiaddrs for %p in the peer routing failed', peerId, err) | ||
if (err.name === 'NoPeerRoutersError') { | ||
this.log('no peer routers configured', peerId) | ||
} else { | ||
this.log.error('looking up multiaddrs for %p in the peer routing failed - %e', peerId, err) | ||
} | ||
@@ -368,0 +412,0 @@ } |
@@ -62,16 +62,23 @@ export enum messages { | ||
export class NoValidAddressesError extends Error { | ||
constructor (message = 'No valid addresses') { | ||
export class UnsupportedListenAddressError extends Error { | ||
constructor (message = 'No transport was configured to listen on this address') { | ||
super(message) | ||
this.name = 'NoValidAddressesError' | ||
this.name = 'UnsupportedListenAddressError' | ||
} | ||
} | ||
export class NoSupportedAddressesError extends Error { | ||
constructor (message = 'No supported addresses') { | ||
export class UnsupportedListenAddressesError extends Error { | ||
constructor (message = 'Configured listen addresses could not be listened on') { | ||
super(message) | ||
this.name = 'NoSupportedAddressesError' | ||
this.name = 'UnsupportedListenAddressesError' | ||
} | ||
} | ||
export class NoValidAddressesError extends Error { | ||
constructor (message = 'No valid addresses') { | ||
super(message) | ||
this.name = 'NoValidAddressesError' | ||
} | ||
} | ||
export class ConnectionInterceptedError extends Error { | ||
@@ -78,0 +85,0 @@ constructor (message = 'Connection intercepted') { |
import { FaultTolerance, InvalidParametersError, NotStartedError } from '@libp2p/interface' | ||
import { trackedMap } from '@libp2p/utils/tracked-map' | ||
import { IP4, IP6 } from '@multiformats/multiaddr-matcher' | ||
import { CustomProgressEvent } from 'progress-events' | ||
import { NoValidAddressesError, TransportUnavailableError } from './errors.js' | ||
import { TransportUnavailableError, UnsupportedListenAddressError, UnsupportedListenAddressesError } from './errors.js' | ||
import type { Libp2pEvents, ComponentLogger, Logger, Connection, TypedEventTarget, Metrics, Startable, Listener, Transport, Upgrader } from '@libp2p/interface' | ||
@@ -21,2 +22,13 @@ import type { AddressManager, TransportManager, TransportManagerDialOptions } from '@libp2p/interface-internal' | ||
interface IPStats { | ||
success: number | ||
attempts: number | ||
} | ||
interface ListenStats { | ||
errors: Map<string, Error> | ||
ipv4: IPStats | ||
ipv6: IPStats | ||
} | ||
export class DefaultTransportManager implements TransportManager, Startable { | ||
@@ -196,7 +208,24 @@ private readonly log: Logger | ||
const couldNotListen = [] | ||
// track IPv4/IPv6 results - if we succeed on IPv4 but all IPv6 attempts | ||
// fail then we are probably on a network without IPv6 support | ||
const listenStats: ListenStats = { | ||
errors: new Map(), | ||
ipv4: { | ||
success: 0, | ||
attempts: 0 | ||
}, | ||
ipv6: { | ||
success: 0, | ||
attempts: 0 | ||
} | ||
} | ||
addrs.forEach(ma => { | ||
listenStats.errors.set(ma.toString(), new UnsupportedListenAddressError()) | ||
}) | ||
const tasks: Array<Promise<void>> = [] | ||
for (const [key, transport] of this.transports.entries()) { | ||
const supportedAddrs = transport.listenFilter(addrs) | ||
const tasks = [] | ||
@@ -236,34 +265,73 @@ // For each supported multiaddr, create a listener | ||
// track IPv4/IPv6 support | ||
if (IP4.matches(addr)) { | ||
listenStats.ipv4.attempts++ | ||
} else if (IP6.matches(addr)) { | ||
listenStats.ipv6.attempts++ | ||
} | ||
// We need to attempt to listen on everything | ||
tasks.push(listener.listen(addr)) | ||
} | ||
tasks.push( | ||
listener.listen(addr) | ||
.then(() => { | ||
listenStats.errors.delete(addr.toString()) | ||
// Keep track of transports we had no addresses for | ||
if (tasks.length === 0) { | ||
couldNotListen.push(key) | ||
continue | ||
} | ||
if (IP4.matches(addr)) { | ||
listenStats.ipv4.success++ | ||
} | ||
const results = await Promise.allSettled(tasks) | ||
// If we are listening on at least 1 address, succeed. | ||
// TODO: we should look at adding a retry (`p-retry`) here to better support | ||
// listening on remote addresses as they may be offline. We could then potentially | ||
// just wait for any (`p-any`) listener to succeed on each transport before returning | ||
const isListening = results.find(r => r.status === 'fulfilled') | ||
if ((isListening == null) && this.faultTolerance !== FaultTolerance.NO_FATAL) { | ||
throw new NoValidAddressesError(`Transport (${key}) could not listen on any available address`) | ||
if (IP6.matches(addr)) { | ||
listenStats.ipv6.success++ | ||
} | ||
}, (err) => { | ||
this.log.error('transport %s could not listen on address %a - %e', key, addr, err) | ||
listenStats.errors.set(addr.toString(), err) | ||
throw err | ||
}) | ||
) | ||
} | ||
} | ||
// If no transports were able to listen, throw an error. This likely | ||
// means we were given addresses we do not have transports for | ||
if (couldNotListen.length === this.transports.size) { | ||
const message = `no valid addresses were provided for transports [${couldNotListen.join(', ')}]` | ||
if (this.faultTolerance === FaultTolerance.FATAL_ALL) { | ||
throw new NoValidAddressesError(message) | ||
} | ||
this.log(`libp2p in dial mode only: ${message}`) | ||
const results = await Promise.allSettled(tasks) | ||
// listening on all addresses, all good | ||
if (results.length > 0 && results.every(res => res.status === 'fulfilled')) { | ||
return | ||
} | ||
// detect lack of IPv6 support on the current network - if we tried to | ||
// listen on IPv4 and IPv6 addresses, and all IPv4 addresses succeeded but | ||
// all IPv6 addresses fail, then we can assume there's no IPv6 here | ||
if (this.ipv6Unsupported(listenStats)) { | ||
this.log('all IPv4 addresses succeed but all IPv6 failed') | ||
return | ||
} | ||
if (this.faultTolerance === FaultTolerance.NO_FATAL) { | ||
// ok to be dial-only | ||
this.log('failed to listen on any address but fault tolerance allows this') | ||
return | ||
} | ||
// if a configured address was not able to be listened on, throw an error | ||
throw new UnsupportedListenAddressesError(`Some configured addresses failed to be listened on, you may need to remove one or more listen addresses from your configuration or set \`transportManager.faultTolerance\` to NO_FATAL:\n${ | ||
[...listenStats.errors.entries()].map(([addr, err]) => { | ||
return ` | ||
${addr}: ${`${err.stack ?? err}`.split('\n').join('\n ')} | ||
` | ||
}).join('') | ||
}`) | ||
} | ||
private ipv6Unsupported (listenStats: ListenStats): boolean { | ||
if (listenStats.ipv4.attempts === 0 || listenStats.ipv6.attempts === 0) { | ||
return false | ||
} | ||
const allIpv4Succeeded = listenStats.ipv4.attempts === listenStats.ipv4.success | ||
const allIpv6Failed = listenStats.ipv6.success === 0 | ||
return allIpv4Succeeded && allIpv6Failed | ||
} | ||
/** | ||
@@ -270,0 +338,0 @@ * Removes the given transport from the manager. |
@@ -1,2 +0,2 @@ | ||
export const version = '2.7.3' | ||
export const version = '2.7.4-22e62d00f' | ||
export const name = 'js-libp2p' |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
1009138
12649
191
173
1
+ Added@libp2p/crypto@5.0.13-22e62d00f(transitive)
+ Added@libp2p/interface@2.6.1-22e62d00f(transitive)
+ Added@libp2p/interface-internal@2.3.5-22e62d00f(transitive)
+ Added@libp2p/logger@5.1.10-22e62d00f(transitive)
+ Added@libp2p/multistream-select@6.0.17-22e62d00f(transitive)
+ Added@libp2p/peer-collections@6.0.21-22e62d00f(transitive)
+ Added@libp2p/peer-id@5.0.14-22e62d00f(transitive)
+ Added@libp2p/peer-record@8.0.21-22e62d00f(transitive)
+ Added@libp2p/peer-store@11.0.21-22e62d00f(transitive)
+ Added@libp2p/utils@6.5.5-22e62d00f(transitive)
+ Addedasn1js@3.0.5(transitive)
+ Addedpvtsutils@1.3.6(transitive)
+ Addedpvutils@1.1.3(transitive)
+ Addedtslib@2.8.1(transitive)
- Removed@libp2p/crypto@5.0.15(transitive)
- Removed@libp2p/interface-internal@2.3.7(transitive)
- Removed@libp2p/multistream-select@6.0.19(transitive)
- Removed@libp2p/peer-collections@6.0.23(transitive)
- Removed@libp2p/peer-id@5.0.16(transitive)
- Removed@libp2p/peer-record@8.0.23(transitive)
- Removed@libp2p/peer-store@11.1.0(transitive)
- Removed@libp2p/utils@6.5.7(transitive)