Skip to content

Commit

Permalink
fix: require confirmation of global unicast addresses (#2876)
Browse files Browse the repository at this point in the history
Some routers will allocate global unicast addresses which peers
will treat as publicly routable.  The routers may also firewall
any incoming traffic to these addresses so require confirmation
that the node is reachable before publishing such addresses.
  • Loading branch information
achingbrain authored Dec 6, 2024
1 parent d19974d commit 92cc740
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 33 deletions.
14 changes: 13 additions & 1 deletion packages/interface-internal/src/address-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export interface NodeAddress {
*/
verified: boolean

/**
* The timestamp at which the address was last verified
*/
lastVerified?: number

/**
* A millisecond timestamp after which this address should be reverified
*/
Expand All @@ -40,6 +45,13 @@ export interface NodeAddress {
type: AddressType
}

export interface ConfirmAddressOptions {
/**
* Override the TTL of the observed address verification
*/
ttl?: number
}

export interface AddressManager {
/**
* Get peer listen multiaddrs
Expand All @@ -61,7 +73,7 @@ export interface AddressManager {
* Signal that we have confidence an observed multiaddr is publicly dialable -
* this will make it appear in the output of getAddresses()
*/
confirmObservedAddr(addr: Multiaddr): void
confirmObservedAddr(addr: Multiaddr, options?: ConfirmAddressOptions): void

/**
* Signal that we do not have confidence an observed multiaddr is publicly dialable -
Expand Down
33 changes: 28 additions & 5 deletions packages/libp2p/src/address-manager/dns-mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { Logger } from '@libp2p/interface'
import type { NodeAddress } from '@libp2p/interface-internal'
import type { Multiaddr, StringTuple } from '@multiformats/multiaddr'

const MAX_DATE = 8_640_000_000_000_000

export const defaultValues = {
maxObservedAddresses: 10
}
Expand All @@ -13,6 +15,7 @@ interface DNSMapping {
domain: string
verified: boolean
expires: number
lastVerified?: number
}

const CODEC_TLS = 0x01c0
Expand Down Expand Up @@ -46,13 +49,15 @@ export class DNSMappings {
add (domain: string, addresses: string[]): void {
addresses.forEach(ip => {
this.log('add DNS mapping %s to %s', ip, domain)
// we are only confident if this is an local domain mapping, otherwise
// we will require external validation
const verified = isPrivateIp(ip) === true

this.mappings.set(ip, {
domain,
// we are only confident if this is an local domain mapping, otherwise
// we will require external validation
verified: isPrivateIp(ip) === true,
expires: 0
verified,
expires: verified ? MAX_DATE - Date.now() : 0,
lastVerified: verified ? MAX_DATE - Date.now() : undefined
})
})
}
Expand Down Expand Up @@ -109,7 +114,8 @@ export class DNSMappings {
}`),
verified: mapping.verified,
type: 'dns-mapping',
expires: mapping.expires
expires: mapping.expires,
lastVerified: mapping.lastVerified
})
}
}
Expand Down Expand Up @@ -139,12 +145,29 @@ export class DNSMappings {
startingConfidence = mapping.verified
mapping.verified = true
mapping.expires = Date.now() + ttl
mapping.lastVerified = Date.now()
}
}

return startingConfidence
}

unconfirm (ma: Multiaddr, ttl: number): boolean {
const host = this.findHost(ma)
let wasConfident = false

for (const [ip, mapping] of this.mappings.entries()) {
if (mapping.domain === host) {
this.log('removing verification of %s to %s DNS mapping', ip, mapping.domain)
wasConfident = wasConfident || mapping.verified
mapping.verified = false
mapping.expires = Date.now() + ttl
}
}

return wasConfident
}

private findHost (ma: Multiaddr): string | undefined {
for (const tuple of ma.stringTuples()) {
if (tuple[0] === CODEC_SNI) {
Expand Down
89 changes: 67 additions & 22 deletions packages/libp2p/src/address-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import { multiaddr } from '@multiformats/multiaddr'
import { DNSMappings } from './dns-mappings.js'
import { IPMappings } from './ip-mappings.js'
import { ObservedAddresses } from './observed-addresses.js'
import { TransportAddresses } from './transport-addresses.js'
import type { ComponentLogger, Libp2pEvents, Logger, TypedEventTarget, PeerId, PeerStore } from '@libp2p/interface'
import type { AddressManager as AddressManagerInterface, TransportManager, NodeAddress } from '@libp2p/interface-internal'
import type { AddressManager as AddressManagerInterface, TransportManager, NodeAddress, ConfirmAddressOptions } from '@libp2p/interface-internal'
import type { Filter } from '@libp2p/utils/filters'
import type { Multiaddr } from '@multiformats/multiaddr'

const ONE_MINUTE = 60_000

export const defaultValues = {
maxObservedAddresses: 10,
observedAddressTTL: 60_000 * 10
addressVerificationTTL: ONE_MINUTE * 10,
addressVerificationRetry: ONE_MINUTE * 5
}

export interface AddressManagerInit {
Expand Down Expand Up @@ -50,9 +54,25 @@ export interface AddressManagerInit {
maxObservedAddresses?: number

/**
* How long before each observed address should be reverified
* How long before each public address should be reverified in ms.
*
* Requires `@libp2p/autonat` or some other verification method to be
* configured.
*
* @default 600_000
*/
addressVerificationTTL?: number

/**
* After a transport or mapped address has failed to verify, how long to wait
* before retrying it in ms
*
* Requires `@libp2p/autonat` or some other verification method to be
* configured.
*
* @default 300_000
*/
observedAddressTTL?: number
addressVerificationRetry?: number
}

export interface AddressManagerComponents {
Expand Down Expand Up @@ -103,8 +123,10 @@ export class AddressManager implements AddressManagerInterface {
private readonly observed: ObservedAddresses
private readonly dnsMappings: DNSMappings
private readonly ipMappings: IPMappings
private readonly transportAddresses: TransportAddresses
private readonly observedAddressFilter: Filter
private readonly observedAddressTTL: number
private readonly addressVerificationTTL: number
private readonly addressVerificationRetry: number

/**
* Responsible for managing the peer addresses.
Expand All @@ -123,9 +145,11 @@ export class AddressManager implements AddressManagerInterface {
this.observed = new ObservedAddresses(components, init)
this.dnsMappings = new DNSMappings(components, init)
this.ipMappings = new IPMappings(components, init)
this.transportAddresses = new TransportAddresses(components, init)
this.announceFilter = init.announceFilter ?? defaultAddressFilter
this.observedAddressFilter = createScalableCuckooFilter(1024)
this.observedAddressTTL = init.observedAddressTTL ?? defaultValues.observedAddressTTL
this.addressVerificationTTL = init.addressVerificationTTL ?? defaultValues.addressVerificationTTL
this.addressVerificationRetry = init.addressVerificationRetry ?? defaultValues.addressVerificationRetry

// this method gets called repeatedly on startup when transports start listening so
// debounce it so we don't cause multiple self:peer:update events to be emitted
Expand Down Expand Up @@ -221,20 +245,24 @@ export class AddressManager implements AddressManagerInterface {
this.observed.add(addr)
}

confirmObservedAddr (addr: Multiaddr): void {
confirmObservedAddr (addr: Multiaddr, options?: ConfirmAddressOptions): void {
addr = stripPeerId(addr, this.components.peerId)
let startingConfidence = true

if (this.observed.has(addr)) {
startingConfidence = this.observed.confirm(addr, this.observedAddressTTL)
startingConfidence = this.observed.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
}

if (this.transportAddresses.has(addr)) {
startingConfidence = this.transportAddresses.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
}

if (this.dnsMappings.has(addr)) {
startingConfidence = this.dnsMappings.confirm(addr, this.observedAddressTTL)
startingConfidence = this.dnsMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
}

if (this.ipMappings.has(addr)) {
startingConfidence = this.ipMappings.confirm(addr, this.observedAddressTTL)
startingConfidence = this.ipMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL)
}

// only trigger the 'self:peer:update' event if our confidence in an address has changed
Expand All @@ -243,12 +271,31 @@ export class AddressManager implements AddressManagerInterface {
}
}

removeObservedAddr (addr: Multiaddr): void {
removeObservedAddr (addr: Multiaddr, options?: ConfirmAddressOptions): void {
addr = stripPeerId(addr, this.components.peerId)

this.observed.remove(addr)
this.dnsMappings.remove(addr)
this.ipMappings.remove(addr)
let startingConfidence = false

if (this.observed.has(addr)) {
startingConfidence = this.observed.remove(addr)
}

if (this.transportAddresses.has(addr)) {
startingConfidence = this.transportAddresses.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry)
}

if (this.dnsMappings.has(addr)) {
startingConfidence = this.dnsMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry)
}

if (this.ipMappings.has(addr)) {
startingConfidence = this.ipMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry)
}

// only trigger the 'self:peer:update' event if our confidence in an address has changed
if (startingConfidence) {
this._updatePeerStoreAddresses()
}
}

getAddresses (): Multiaddr[] {
Expand Down Expand Up @@ -299,20 +346,17 @@ export class AddressManager implements AddressManagerInterface {
multiaddr,
verified: true,
type: 'announce',
expires: Date.now() + this.observedAddressTTL
expires: Date.now() + this.addressVerificationTTL,
lastVerified: Date.now()
}))
}

let addresses: NodeAddress[] = []

// add transport addresses
addresses = addresses.concat(
this.components.transportManager.getAddrs().map(multiaddr => ({
multiaddr,
verified: true,
type: 'transport',
expires: Date.now() + this.observedAddressTTL
}))
this.components.transportManager.getAddrs()
.map(multiaddr => this.transportAddresses.get(multiaddr, this.addressVerificationTTL))
)

// add append announce addresses
Expand All @@ -321,7 +365,8 @@ export class AddressManager implements AddressManagerInterface {
multiaddr,
verified: true,
type: 'announce',
expires: Date.now() + this.observedAddressTTL
expires: Date.now() + this.addressVerificationTTL,
lastVerified: Date.now()
}))
)

Expand Down
29 changes: 28 additions & 1 deletion packages/libp2p/src/address-manager/ip-mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface PublicAddressMapping {
protocol: 'tcp' | 'udp'
verified: boolean
expires: number
lastVerified?: number
}

const CODEC_IP4 = 0x04
Expand Down Expand Up @@ -134,7 +135,8 @@ export class IPMappings {
}`),
verified: mapping.verified,
type: 'ip-mapping',
expires: mapping.expires
expires: mapping.expires,
lastVerified: mapping.lastVerified
})
}
}
Expand All @@ -155,10 +157,35 @@ export class IPMappings {
startingConfidence = mapping.verified
mapping.verified = true
mapping.expires = Date.now() + ttl
mapping.lastVerified = Date.now()
}
}
}

return startingConfidence
}

unconfirm (ma: Multiaddr, ttl: number): boolean {
const tuples = ma.stringTuples()
const host = tuples[0][1] ?? ''
const protocol = tuples[1][0] === CODEC_TCP ? 'tcp' : 'udp'
const port = parseInt(tuples[1][1] ?? '0')
let wasConfident = false

for (const mappings of this.mappings.values()) {
for (let i = 0; i < mappings.length; i++) {
const mapping = mappings[i]

if (mapping.externalIp === host && mapping.externalPort === port && mapping.protocol === protocol) {
this.log('removing verification of %s:%s to %s:%s %s IP mapping', mapping.externalIp, mapping.externalPort, host, port, protocol)

wasConfident = wasConfident || mapping.verified
mapping.verified = false
mapping.expires = Date.now() + ttl
}
}
}

return wasConfident
}
}
14 changes: 11 additions & 3 deletions packages/libp2p/src/address-manager/observed-addresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const defaultValues = {
interface ObservedAddressMetadata {
verified: boolean
expires: number
lastVerified?: number
}

export class ObservedAddresses {
Expand Down Expand Up @@ -60,23 +61,30 @@ export class ObservedAddresses {
multiaddr: multiaddr(ma),
verified: metadata.verified,
type: 'observed',
expires: metadata.expires
expires: metadata.expires,
lastVerified: metadata.lastVerified
}))
}

remove (ma: Multiaddr): void {
remove (ma: Multiaddr): boolean {
const startingConfidence = this.addresses.get(ma.toString())?.verified ?? false

this.log('removing observed address %a', ma)
this.addresses.delete(ma.toString())

return startingConfidence
}

confirm (ma: Multiaddr, ttl: number): boolean {
const addrString = ma.toString()
const metadata = this.addresses.get(addrString) ?? {
verified: false,
expires: Date.now() + ttl
expires: Date.now() + ttl,
lastVerified: Date.now()
}
const startingConfidence = metadata.verified
metadata.verified = true
metadata.lastVerified = Date.now()

this.log('marking observed address %a as verified', addrString)
this.addresses.set(addrString, metadata)
Expand Down
Loading

0 comments on commit 92cc740

Please sign in to comment.