Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: upgrade observed address to ip mapping #2941

Merged
merged 2 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/libp2p/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
140 changes: 129 additions & 11 deletions packages/libp2p/src/address-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
109 changes: 106 additions & 3 deletions packages/libp2p/test/addresses/address-manager.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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']
Expand Down Expand Up @@ -200,7 +200,8 @@ describe('Address Manager', () => {
const am = new AddressManager({
peerId,
transportManager: stubInterface<TransportManager>({
getAddrs: Sinon.stub().returns([])
getAddrs: Sinon.stub().returns([]),
getListeners: Sinon.stub().returns([])
}),
peerStore,
events,
Expand Down Expand Up @@ -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<TransportManager>({
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<Listener>({
getAddrs: Sinon.stub().returns([
multiaddr('/ip4/127.0.0.1/tcp/1234'),
multiaddr('/ip4/192.168.1.123/tcp/1234')
])
}),
stubInterface<Listener>({
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<TransportManager>({
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<Listener>({
getAddrs: Sinon.stub().returns([
multiaddr('/ip4/127.0.0.1/tcp/1234'),
multiaddr('/ip4/192.168.1.123/tcp/1234')
])
}),
stubInterface<Listener>({
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<NodeAddress, 'multiaddr' | 'verified' | 'type'> {
return {
multiaddr: addr.multiaddr,
verified: addr.verified,
type: addr.type
}
}
Loading