diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index e3c6b27789..ca2128416c 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -97,7 +97,7 @@ "@libp2p/peer-store": "^11.0.16", "@libp2p/utils": "^6.5.0", "@multiformats/dns": "^1.0.6", - "@multiformats/multiaddr": "^12.3.3", + "@multiformats/multiaddr": "^12.3.5", "@multiformats/multiaddr-matcher": "^1.6.0", "any-signal": "^4.1.1", "datastore-core": "^10.0.2", diff --git a/packages/libp2p/src/address-manager/index.ts b/packages/libp2p/src/address-manager/index.ts index 60cc8f84ef..f81f529d78 100644 --- a/packages/libp2p/src/address-manager/index.ts +++ b/packages/libp2p/src/address-manager/index.ts @@ -3,7 +3,9 @@ import { isIPv4 } from '@chainsafe/is-ip' import { peerIdFromString } from '@libp2p/peer-id' import { debounce } from '@libp2p/utils/debounce' import { createScalableCuckooFilter } from '@libp2p/utils/filters' +import { isPrivateIp } from '@libp2p/utils/private-ip' import { multiaddr } from '@multiformats/multiaddr' +import { QUICV1, TCP, WebSockets, WebSocketsSecure } from '@multiformats/multiaddr-matcher' import { DNSMappings } from './dns-mappings.js' import { IPMappings } from './ip-mappings.js' import { ObservedAddresses } from './observed-addresses.js' @@ -249,20 +251,42 @@ export class AddressManager implements AddressManagerInterface { addr = stripPeerId(addr, this.components.peerId) let startingConfidence = true - if (options?.type === 'observed' || this.observed.has(addr)) { - startingConfidence = this.observed.confirm(addr, options?.ttl ?? this.addressVerificationTTL) - } - if (options?.type === 'transport' || this.transportAddresses.has(addr)) { - startingConfidence = this.transportAddresses.confirm(addr, options?.ttl ?? this.addressVerificationTTL) + const transportStartingConfidence = this.transportAddresses.confirm(addr, options?.ttl ?? this.addressVerificationTTL) + + if (!transportStartingConfidence && startingConfidence) { + startingConfidence = false + } } if (options?.type === 'dns-mapping' || this.dnsMappings.has(addr)) { - startingConfidence = this.dnsMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL) + const dnsMapingStartingConfidence = this.dnsMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL) + + if (!dnsMapingStartingConfidence && startingConfidence) { + startingConfidence = false + } } if (options?.type === 'ip-mapping' || this.ipMappings.has(addr)) { - startingConfidence = this.ipMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL) + const ipMapingStartingConfidence = this.ipMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL) + + if (!ipMapingStartingConfidence && startingConfidence) { + startingConfidence = false + } + } + + if (options?.type === 'observed' || this.observed.has(addr)) { + // try to match up observed address with local transport listener + if (this.maybeUpgradeToIPMapping(addr)) { + this.ipMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL) + startingConfidence = false + } else { + const observedStartingConfidence = this.observed.confirm(addr, options?.ttl ?? this.addressVerificationTTL) + + if (!observedStartingConfidence && startingConfidence) { + startingConfidence = false + } + } } // only trigger the 'self:peer:update' event if our confidence in an address has changed @@ -277,19 +301,35 @@ export class AddressManager implements AddressManagerInterface { let startingConfidence = false if (this.observed.has(addr)) { - startingConfidence = this.observed.remove(addr) + const observedStartingConfidence = this.observed.remove(addr) + + if (!observedStartingConfidence && startingConfidence) { + startingConfidence = false + } } if (this.transportAddresses.has(addr)) { - startingConfidence = this.transportAddresses.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry) + const transportStartingConfidence = this.transportAddresses.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry) + + if (!transportStartingConfidence && startingConfidence) { + startingConfidence = false + } } if (this.dnsMappings.has(addr)) { - startingConfidence = this.dnsMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry) + const dnsMapingStartingConfidence = this.dnsMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry) + + if (!dnsMapingStartingConfidence && startingConfidence) { + startingConfidence = false + } } if (this.ipMappings.has(addr)) { - startingConfidence = this.ipMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry) + const ipMapingStartingConfidence = this.ipMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry) + + if (!ipMapingStartingConfidence && startingConfidence) { + startingConfidence = false + } } // only trigger the 'self:peer:update' event if our confidence in an address has changed @@ -410,4 +450,82 @@ export class AddressManager implements AddressManagerInterface { this._updatePeerStoreAddresses() } } + + /** + * Where an external service (router, gateway, etc) is forwarding traffic to + * us, attempt to add an IP mapping for the external address - this will + * include the observed mapping in the address list where we also have a DNS + * mapping for the external IP. + * + * Returns true if we added a new mapping + */ + private maybeUpgradeToIPMapping (ma: Multiaddr): boolean { + // this address is already mapped + if (this.ipMappings.has(ma)) { + return false + } + + const maOptions = ma.toOptions() + + // only public IPv4 addresses + if (maOptions.family === 6 || maOptions.host === '127.0.0.1' || isPrivateIp(maOptions.host) === true) { + return false + } + + const listeners = this.components.transportManager.getListeners() + + const transportMatchers: Array<(ma: Multiaddr) => boolean> = [ + (ma: Multiaddr) => WebSockets.exactMatch(ma) || WebSocketsSecure.exactMatch(ma), + (ma: Multiaddr) => TCP.exactMatch(ma), + (ma: Multiaddr) => QUICV1.exactMatch(ma) + ] + + for (const matcher of transportMatchers) { + // is the incoming address the same type as the matcher + if (!matcher(ma)) { + continue + } + + // get the listeners for this transport + const transportListeners = listeners.filter(listener => { + return listener.getAddrs().filter(ma => { + // only IPv4 addresses of the matcher type + return ma.toOptions().family === 4 && matcher(ma) + }).length > 0 + }) + + // because the NAT mapping could be forwarding different external ports to + // internal ones, we can only be sure enough to add a mapping if there is + // a single listener + if (transportListeners.length !== 1) { + continue + } + + // we have one listener which listens on one port so whatever the external + // NAT port mapping is, it should be for this listener + const linkLocalAddr = transportListeners[0].getAddrs().filter(ma => { + return ma.toOptions().host !== '127.0.0.1' + }).pop() + + if (linkLocalAddr == null) { + continue + } + + const linkLocalOptions = linkLocalAddr.toOptions() + + // upgrade observed address to IP mapping + this.observed.remove(ma) + this.ipMappings.add( + linkLocalOptions.host, + linkLocalOptions.port, + maOptions.host, + maOptions.port, + maOptions.transport + ) + + return true + } + + return false + } } diff --git a/packages/libp2p/test/addresses/address-manager.spec.ts b/packages/libp2p/test/addresses/address-manager.spec.ts index fc1480073b..a1a7792cd2 100644 --- a/packages/libp2p/test/addresses/address-manager.spec.ts +++ b/packages/libp2p/test/addresses/address-manager.spec.ts @@ -1,7 +1,7 @@ /* eslint-env mocha */ import { generateKeyPair } from '@libp2p/crypto/keys' -import { TypedEventEmitter, type TypedEventTarget, type Libp2pEvents, type PeerId, type PeerStore, type Peer } from '@libp2p/interface' +import { TypedEventEmitter, type TypedEventTarget, type Libp2pEvents, type PeerId, type PeerStore, type Peer, type Listener } from '@libp2p/interface' import { defaultLogger } from '@libp2p/logger' import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { multiaddr } from '@multiformats/multiaddr' @@ -10,7 +10,7 @@ import delay from 'delay' import Sinon from 'sinon' import { type StubbedInstance, stubInterface } from 'sinon-ts' import { type AddressFilter, AddressManager } from '../../src/address-manager/index.js' -import type { TransportManager } from '@libp2p/interface-internal' +import type { NodeAddress, TransportManager } from '@libp2p/interface-internal' const listenAddresses = ['/ip4/127.0.0.1/tcp/15006/ws', '/ip4/127.0.0.1/tcp/15008/ws'] const announceAddreses = ['/dns4/peer.io'] @@ -200,7 +200,8 @@ describe('Address Manager', () => { const am = new AddressManager({ peerId, transportManager: stubInterface({ - getAddrs: Sinon.stub().returns([]) + getAddrs: Sinon.stub().returns([]), + getListeners: Sinon.stub().returns([]) }), peerStore, events, @@ -714,4 +715,106 @@ describe('Address Manager', () => { multiaddr(`/ip6/${externalIp}/${protocol}/${externalPort}/p2p/${peerId.toString()}`) ]) }) + + it('should upgrade an observed address to an IP mapping when confirming an observed address and there is only a single eligible listener', () => { + const am = new AddressManager({ + peerId, + transportManager: stubInterface({ + getAddrs: Sinon.stub().returns([ + multiaddr('/ip4/127.0.0.1/tcp/1234'), + multiaddr('/ip4/192.168.1.123/tcp/1234') + ]), + getListeners: Sinon.stub().returns([ + stubInterface({ + getAddrs: Sinon.stub().returns([ + multiaddr('/ip4/127.0.0.1/tcp/1234'), + multiaddr('/ip4/192.168.1.123/tcp/1234') + ]) + }), + stubInterface({ + getAddrs: Sinon.stub().returns([ + multiaddr('/ip6/::1/tcp/1234'), + multiaddr('/ip6/2b01:ab15:3c8:3300:10b8:170e:1087:3b0e/tcp/1234') + ]) + }) + ]) + }), + peerStore, + events, + logger: defaultLogger() + }) + + expect(am.getObservedAddrs()).to.be.empty() + + const ma = multiaddr('/ip4/123.123.123.123/tcp/39201') + am.addObservedAddr(ma) + am.confirmObservedAddr(ma) + + expect(am.getAddressesWithMetadata().map(mapAddress)).to.include.deep.members([{ + multiaddr: ma, + verified: true, + type: 'ip-mapping' + }]) + + expect(am.getAddressesWithMetadata().map(mapAddress)).to.not.include.deep.members([{ + multiaddr: ma, + verified: true, + type: 'observed' + }]) + }) + + it('should not upgrade an observed address to an IP mapping when confirming an observed address and there are multiple eligible listeners', () => { + const am = new AddressManager({ + peerId, + transportManager: stubInterface({ + getAddrs: Sinon.stub().returns([ + multiaddr('/ip4/127.0.0.1/tcp/1234'), + multiaddr('/ip4/192.168.1.123/tcp/1234') + ]), + getListeners: Sinon.stub().returns([ + stubInterface({ + getAddrs: Sinon.stub().returns([ + multiaddr('/ip4/127.0.0.1/tcp/1234'), + multiaddr('/ip4/192.168.1.123/tcp/1234') + ]) + }), + stubInterface({ + getAddrs: Sinon.stub().returns([ + multiaddr('/ip4/127.0.0.1/tcp/4567'), + multiaddr('/ip4/192.168.1.123/tcp/4567') + ]) + }) + ]) + }), + peerStore, + events, + logger: defaultLogger() + }) + + expect(am.getObservedAddrs()).to.be.empty() + + const ma = multiaddr('/ip4/123.123.123.123/tcp/39201') + am.addObservedAddr(ma) + am.confirmObservedAddr(ma) + + expect(am.getAddressesWithMetadata().map(mapAddress)).to.not.include.deep.members([{ + multiaddr: ma, + verified: true, + type: 'ip-mapping' + }]) + + expect(am.getAddressesWithMetadata().map(mapAddress)).to.include.deep.members([{ + multiaddr: ma, + verified: true, + type: 'observed' + }]) + }) }) + +function mapAddress (addr: NodeAddress): Pick { + return { + multiaddr: addr.multiaddr, + verified: addr.verified, + type: addr.type + } +}