Skip to content

Commit d795be1

Browse files
authored
fix: upgrade observed address to ip mapping (#2941)
When an observed address with a publicly routable IPv4 host is confirmed by autonat, and there's only one listener it could be associated with, upgrade the observed address to an IP mapping, this allows the DNS mapping to add SNI tuples to corresponding transport addresses with a TLS tuple. This will, for example, detect when external infrastructure such as a router has been configured manually (e.g. not by UPnP) to forward traffic to the current node. Fixes #2929
1 parent d72b511 commit d795be1

File tree

3 files changed

+236
-15
lines changed

3 files changed

+236
-15
lines changed

packages/libp2p/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
"@libp2p/peer-store": "^11.0.16",
9898
"@libp2p/utils": "^6.5.0",
9999
"@multiformats/dns": "^1.0.6",
100-
"@multiformats/multiaddr": "^12.3.3",
100+
"@multiformats/multiaddr": "^12.3.5",
101101
"@multiformats/multiaddr-matcher": "^1.6.0",
102102
"any-signal": "^4.1.1",
103103
"datastore-core": "^10.0.2",

packages/libp2p/src/address-manager/index.ts

+129-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { isIPv4 } from '@chainsafe/is-ip'
33
import { peerIdFromString } from '@libp2p/peer-id'
44
import { debounce } from '@libp2p/utils/debounce'
55
import { createScalableCuckooFilter } from '@libp2p/utils/filters'
6+
import { isPrivateIp } from '@libp2p/utils/private-ip'
67
import { multiaddr } from '@multiformats/multiaddr'
8+
import { QUICV1, TCP, WebSockets, WebSocketsSecure } from '@multiformats/multiaddr-matcher'
79
import { DNSMappings } from './dns-mappings.js'
810
import { IPMappings } from './ip-mappings.js'
911
import { ObservedAddresses } from './observed-addresses.js'
@@ -249,20 +251,42 @@ export class AddressManager implements AddressManagerInterface {
249251
addr = stripPeerId(addr, this.components.peerId)
250252
let startingConfidence = true
251253

252-
if (options?.type === 'observed' || this.observed.has(addr)) {
253-
startingConfidence = this.observed.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
254-
}
255-
256254
if (options?.type === 'transport' || this.transportAddresses.has(addr)) {
257-
startingConfidence = this.transportAddresses.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
255+
const transportStartingConfidence = this.transportAddresses.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
256+
257+
if (!transportStartingConfidence && startingConfidence) {
258+
startingConfidence = false
259+
}
258260
}
259261

260262
if (options?.type === 'dns-mapping' || this.dnsMappings.has(addr)) {
261-
startingConfidence = this.dnsMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
263+
const dnsMapingStartingConfidence = this.dnsMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
264+
265+
if (!dnsMapingStartingConfidence && startingConfidence) {
266+
startingConfidence = false
267+
}
262268
}
263269

264270
if (options?.type === 'ip-mapping' || this.ipMappings.has(addr)) {
265-
startingConfidence = this.ipMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
271+
const ipMapingStartingConfidence = this.ipMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
272+
273+
if (!ipMapingStartingConfidence && startingConfidence) {
274+
startingConfidence = false
275+
}
276+
}
277+
278+
if (options?.type === 'observed' || this.observed.has(addr)) {
279+
// try to match up observed address with local transport listener
280+
if (this.maybeUpgradeToIPMapping(addr)) {
281+
this.ipMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
282+
startingConfidence = false
283+
} else {
284+
const observedStartingConfidence = this.observed.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
285+
286+
if (!observedStartingConfidence && startingConfidence) {
287+
startingConfidence = false
288+
}
289+
}
266290
}
267291

268292
// only trigger the 'self:peer:update' event if our confidence in an address has changed
@@ -277,19 +301,35 @@ export class AddressManager implements AddressManagerInterface {
277301
let startingConfidence = false
278302

279303
if (this.observed.has(addr)) {
280-
startingConfidence = this.observed.remove(addr)
304+
const observedStartingConfidence = this.observed.remove(addr)
305+
306+
if (!observedStartingConfidence && startingConfidence) {
307+
startingConfidence = false
308+
}
281309
}
282310

283311
if (this.transportAddresses.has(addr)) {
284-
startingConfidence = this.transportAddresses.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry)
312+
const transportStartingConfidence = this.transportAddresses.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry)
313+
314+
if (!transportStartingConfidence && startingConfidence) {
315+
startingConfidence = false
316+
}
285317
}
286318

287319
if (this.dnsMappings.has(addr)) {
288-
startingConfidence = this.dnsMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry)
320+
const dnsMapingStartingConfidence = this.dnsMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry)
321+
322+
if (!dnsMapingStartingConfidence && startingConfidence) {
323+
startingConfidence = false
324+
}
289325
}
290326

291327
if (this.ipMappings.has(addr)) {
292-
startingConfidence = this.ipMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry)
328+
const ipMapingStartingConfidence = this.ipMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry)
329+
330+
if (!ipMapingStartingConfidence && startingConfidence) {
331+
startingConfidence = false
332+
}
293333
}
294334

295335
// only trigger the 'self:peer:update' event if our confidence in an address has changed
@@ -410,4 +450,82 @@ export class AddressManager implements AddressManagerInterface {
410450
this._updatePeerStoreAddresses()
411451
}
412452
}
453+
454+
/**
455+
* Where an external service (router, gateway, etc) is forwarding traffic to
456+
* us, attempt to add an IP mapping for the external address - this will
457+
* include the observed mapping in the address list where we also have a DNS
458+
* mapping for the external IP.
459+
*
460+
* Returns true if we added a new mapping
461+
*/
462+
private maybeUpgradeToIPMapping (ma: Multiaddr): boolean {
463+
// this address is already mapped
464+
if (this.ipMappings.has(ma)) {
465+
return false
466+
}
467+
468+
const maOptions = ma.toOptions()
469+
470+
// only public IPv4 addresses
471+
if (maOptions.family === 6 || maOptions.host === '127.0.0.1' || isPrivateIp(maOptions.host) === true) {
472+
return false
473+
}
474+
475+
const listeners = this.components.transportManager.getListeners()
476+
477+
const transportMatchers: Array<(ma: Multiaddr) => boolean> = [
478+
(ma: Multiaddr) => WebSockets.exactMatch(ma) || WebSocketsSecure.exactMatch(ma),
479+
(ma: Multiaddr) => TCP.exactMatch(ma),
480+
(ma: Multiaddr) => QUICV1.exactMatch(ma)
481+
]
482+
483+
for (const matcher of transportMatchers) {
484+
// is the incoming address the same type as the matcher
485+
if (!matcher(ma)) {
486+
continue
487+
}
488+
489+
// get the listeners for this transport
490+
const transportListeners = listeners.filter(listener => {
491+
return listener.getAddrs().filter(ma => {
492+
// only IPv4 addresses of the matcher type
493+
return ma.toOptions().family === 4 && matcher(ma)
494+
}).length > 0
495+
})
496+
497+
// because the NAT mapping could be forwarding different external ports to
498+
// internal ones, we can only be sure enough to add a mapping if there is
499+
// a single listener
500+
if (transportListeners.length !== 1) {
501+
continue
502+
}
503+
504+
// we have one listener which listens on one port so whatever the external
505+
// NAT port mapping is, it should be for this listener
506+
const linkLocalAddr = transportListeners[0].getAddrs().filter(ma => {
507+
return ma.toOptions().host !== '127.0.0.1'
508+
}).pop()
509+
510+
if (linkLocalAddr == null) {
511+
continue
512+
}
513+
514+
const linkLocalOptions = linkLocalAddr.toOptions()
515+
516+
// upgrade observed address to IP mapping
517+
this.observed.remove(ma)
518+
this.ipMappings.add(
519+
linkLocalOptions.host,
520+
linkLocalOptions.port,
521+
maOptions.host,
522+
maOptions.port,
523+
maOptions.transport
524+
)
525+
526+
return true
527+
}
528+
529+
return false
530+
}
413531
}

packages/libp2p/test/addresses/address-manager.spec.ts

+106-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-env mocha */
22

33
import { generateKeyPair } from '@libp2p/crypto/keys'
4-
import { TypedEventEmitter, type TypedEventTarget, type Libp2pEvents, type PeerId, type PeerStore, type Peer } from '@libp2p/interface'
4+
import { TypedEventEmitter, type TypedEventTarget, type Libp2pEvents, type PeerId, type PeerStore, type Peer, type Listener } from '@libp2p/interface'
55
import { defaultLogger } from '@libp2p/logger'
66
import { peerIdFromPrivateKey } from '@libp2p/peer-id'
77
import { multiaddr } from '@multiformats/multiaddr'
@@ -10,7 +10,7 @@ import delay from 'delay'
1010
import Sinon from 'sinon'
1111
import { type StubbedInstance, stubInterface } from 'sinon-ts'
1212
import { type AddressFilter, AddressManager } from '../../src/address-manager/index.js'
13-
import type { TransportManager } from '@libp2p/interface-internal'
13+
import type { NodeAddress, TransportManager } from '@libp2p/interface-internal'
1414

1515
const listenAddresses = ['/ip4/127.0.0.1/tcp/15006/ws', '/ip4/127.0.0.1/tcp/15008/ws']
1616
const announceAddreses = ['/dns4/peer.io']
@@ -200,7 +200,8 @@ describe('Address Manager', () => {
200200
const am = new AddressManager({
201201
peerId,
202202
transportManager: stubInterface<TransportManager>({
203-
getAddrs: Sinon.stub().returns([])
203+
getAddrs: Sinon.stub().returns([]),
204+
getListeners: Sinon.stub().returns([])
204205
}),
205206
peerStore,
206207
events,
@@ -714,4 +715,106 @@ describe('Address Manager', () => {
714715
multiaddr(`/ip6/${externalIp}/${protocol}/${externalPort}/p2p/${peerId.toString()}`)
715716
])
716717
})
718+
719+
it('should upgrade an observed address to an IP mapping when confirming an observed address and there is only a single eligible listener', () => {
720+
const am = new AddressManager({
721+
peerId,
722+
transportManager: stubInterface<TransportManager>({
723+
getAddrs: Sinon.stub().returns([
724+
multiaddr('/ip4/127.0.0.1/tcp/1234'),
725+
multiaddr('/ip4/192.168.1.123/tcp/1234')
726+
]),
727+
getListeners: Sinon.stub().returns([
728+
stubInterface<Listener>({
729+
getAddrs: Sinon.stub().returns([
730+
multiaddr('/ip4/127.0.0.1/tcp/1234'),
731+
multiaddr('/ip4/192.168.1.123/tcp/1234')
732+
])
733+
}),
734+
stubInterface<Listener>({
735+
getAddrs: Sinon.stub().returns([
736+
multiaddr('/ip6/::1/tcp/1234'),
737+
multiaddr('/ip6/2b01:ab15:3c8:3300:10b8:170e:1087:3b0e/tcp/1234')
738+
])
739+
})
740+
])
741+
}),
742+
peerStore,
743+
events,
744+
logger: defaultLogger()
745+
})
746+
747+
expect(am.getObservedAddrs()).to.be.empty()
748+
749+
const ma = multiaddr('/ip4/123.123.123.123/tcp/39201')
750+
am.addObservedAddr(ma)
751+
am.confirmObservedAddr(ma)
752+
753+
expect(am.getAddressesWithMetadata().map(mapAddress)).to.include.deep.members([{
754+
multiaddr: ma,
755+
verified: true,
756+
type: 'ip-mapping'
757+
}])
758+
759+
expect(am.getAddressesWithMetadata().map(mapAddress)).to.not.include.deep.members([{
760+
multiaddr: ma,
761+
verified: true,
762+
type: 'observed'
763+
}])
764+
})
765+
766+
it('should not upgrade an observed address to an IP mapping when confirming an observed address and there are multiple eligible listeners', () => {
767+
const am = new AddressManager({
768+
peerId,
769+
transportManager: stubInterface<TransportManager>({
770+
getAddrs: Sinon.stub().returns([
771+
multiaddr('/ip4/127.0.0.1/tcp/1234'),
772+
multiaddr('/ip4/192.168.1.123/tcp/1234')
773+
]),
774+
getListeners: Sinon.stub().returns([
775+
stubInterface<Listener>({
776+
getAddrs: Sinon.stub().returns([
777+
multiaddr('/ip4/127.0.0.1/tcp/1234'),
778+
multiaddr('/ip4/192.168.1.123/tcp/1234')
779+
])
780+
}),
781+
stubInterface<Listener>({
782+
getAddrs: Sinon.stub().returns([
783+
multiaddr('/ip4/127.0.0.1/tcp/4567'),
784+
multiaddr('/ip4/192.168.1.123/tcp/4567')
785+
])
786+
})
787+
])
788+
}),
789+
peerStore,
790+
events,
791+
logger: defaultLogger()
792+
})
793+
794+
expect(am.getObservedAddrs()).to.be.empty()
795+
796+
const ma = multiaddr('/ip4/123.123.123.123/tcp/39201')
797+
am.addObservedAddr(ma)
798+
am.confirmObservedAddr(ma)
799+
800+
expect(am.getAddressesWithMetadata().map(mapAddress)).to.not.include.deep.members([{
801+
multiaddr: ma,
802+
verified: true,
803+
type: 'ip-mapping'
804+
}])
805+
806+
expect(am.getAddressesWithMetadata().map(mapAddress)).to.include.deep.members([{
807+
multiaddr: ma,
808+
verified: true,
809+
type: 'observed'
810+
}])
811+
})
717812
})
813+
814+
function mapAddress (addr: NodeAddress): Pick<NodeAddress, 'multiaddr' | 'verified' | 'type'> {
815+
return {
816+
multiaddr: addr.multiaddr,
817+
verified: addr.verified,
818+
type: addr.type
819+
}
820+
}

0 commit comments

Comments
 (0)