Skip to content
Open
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
23 changes: 20 additions & 3 deletions packages/protocol-autonat-v2/src/autonat.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { serviceCapabilities, serviceDependencies, start, stop } from '@libp2p/interface'
import { TypedEventEmitter } from 'main-event'
import { AutoNATv2Client } from './client.ts'
import { DIAL_BACK, DIAL_REQUEST, PROTOCOL_NAME, PROTOCOL_PREFIX, PROTOCOL_VERSION } from './constants.ts'
import { AutoNATv2Server } from './server.ts'
import type { AutoNATv2Components, AutoNATv2ServiceInit } from './index.ts'
import type { AutoNATv2Components, AutoNATv2Events, AutoNATv2ServiceInit } from './index.ts'
import type { Startable } from '@libp2p/interface'
import type { Multiaddr } from '@multiformats/multiaddr'

export class AutoNATv2Service implements Startable {
export class AutoNATv2Service extends TypedEventEmitter<AutoNATv2Events> implements Startable {
private readonly client: AutoNATv2Client
private readonly server: AutoNATv2Server

constructor (components: AutoNATv2Components, init: AutoNATv2ServiceInit) {
super()

const dialRequestProtocol = `/${init.protocolPrefix ?? PROTOCOL_PREFIX}/${PROTOCOL_NAME}/${PROTOCOL_VERSION}/${DIAL_REQUEST}`
const dialBackProtocol = `/${init.protocolPrefix ?? PROTOCOL_PREFIX}/${PROTOCOL_NAME}/${PROTOCOL_VERSION}/${DIAL_BACK}`

this.client = new AutoNATv2Client(components, {
...init,
dialRequestProtocol,
dialBackProtocol
dialBackProtocol,
safeDispatchEvent: this.safeDispatchEvent.bind(this)
})
this.server = new AutoNATv2Server(components, {
...init,
Expand All @@ -25,6 +30,18 @@ export class AutoNATv2Service implements Startable {
})
}

get verifying (): Multiaddr[] {
return this.client.verifying
}

get reachable (): Multiaddr[] {
return this.client.reachable
}

get unreachable (): Multiaddr[] {
return this.client.unreachable
}

readonly [Symbol.toStringTag] = '@libp2p/autonat-v2'

readonly [serviceCapabilities]: string[] = [
Expand Down
83 changes: 72 additions & 11 deletions packages/protocol-autonat-v2/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import { setMaxListeners } from 'main-event'
import { DEFAULT_CONNECTION_THRESHOLD, DIAL_DATA_CHUNK_SIZE, MAX_DIAL_DATA_BYTES, MAX_INBOUND_STREAMS, MAX_MESSAGE_SIZE, MAX_OUTBOUND_STREAMS, TIMEOUT } from './constants.ts'
import { DialBack, DialBackResponse, DialResponse, DialStatus, Message } from './pb/index.ts'
import { randomNumber } from './utils.ts'
import type { AutoNATv2Components, AutoNATv2ServiceInit } from './index.ts'
import type { AutoNATv2Components, AutoNATv2Events, AutoNATv2ServiceInit } from './index.ts'
import type { Logger, Connection, Startable, AbortOptions, Stream } from '@libp2p/interface'
import type { AddressType } from '@libp2p/interface-internal'
import type { PeerSet } from '@libp2p/peer-collections'
import type { Filter, RepeatingTask } from '@libp2p/utils'
import type { Multiaddr } from '@multiformats/multiaddr'
import type { TypedEventEmitter } from 'main-event'

export type DispatchAutoNATv2Event = TypedEventEmitter<AutoNATv2Events>['safeDispatchEvent']

// if more than 3 peers manage to dial us on what we believe to be our external
// IP then we are convinced that it is, in fact, our external IP
Expand Down Expand Up @@ -68,6 +71,12 @@ export interface AutoNATv2ClientInit extends AutoNATv2ServiceInit {
dialBackProtocol: string
maxDialDataBytes?: bigint
dialDataChunkSize?: number
safeDispatchEvent: DispatchAutoNATv2Event
}

interface Verdict {
addr: Multiaddr
state: 'verifying' | 'reachable' | 'unreachable'
}

export class AutoNATv2Client implements Startable {
Expand All @@ -84,14 +93,17 @@ export class AutoNATv2Client implements Startable {
private readonly log: Logger
private topologyId?: string
private readonly dialResults: Map<string, DialResults>
private readonly verdicts: Map<string, Verdict>
private readonly findPeers: RepeatingTask
private readonly addressFilter: Filter
private readonly connectionThreshold: number
private readonly queue: PeerQueue
private readonly nonces: Set<bigint>
private readonly safeDispatchEvent: DispatchAutoNATv2Event

constructor (components: AutoNATv2Components, init: AutoNATv2ClientInit) {
this.components = components
this.safeDispatchEvent = init.safeDispatchEvent
this.log = components.logger.forComponent('libp2p:auto-nat-v2:client')
this.started = false
this.dialRequestProtocol = init.dialRequestProtocol
Expand All @@ -105,6 +117,7 @@ export class AutoNATv2Client implements Startable {
name: 'libp2p_autonat_v2_dial_results',
metrics: components.metrics
})
this.verdicts = new Map()
this.findPeers = repeatingTask(this.findRandomPeers.bind(this), 60_000)
this.addressFilter = createScalableCuckooFilter(1024)
this.queue = new PeerQueue({
Expand All @@ -117,6 +130,24 @@ export class AutoNATv2Client implements Startable {
this.nonces = new Set()
}

get verifying (): Multiaddr[] {
return [...this.verdicts.values()]
.filter(v => v.state === 'verifying')
.map(v => v.addr)
}

get reachable (): Multiaddr[] {
return [...this.verdicts.values()]
.filter(v => v.state === 'reachable')
.map(v => v.addr)
}

get unreachable (): Multiaddr[] {
return [...this.verdicts.values()]
.filter(v => v.state === 'unreachable')
.map(v => v.addr)
}

readonly [Symbol.toStringTag] = '@libp2p/autonat-v2'

readonly [serviceCapabilities]: string[] = [
Expand Down Expand Up @@ -170,6 +201,7 @@ export class AutoNATv2Client implements Startable {
}

this.dialResults.clear()
this.verdicts.clear()
this.findPeers.stop()
this.started = false
}
Expand Down Expand Up @@ -358,6 +390,14 @@ export class AutoNATv2Client implements Startable {
}

this.dialResults.set(addrString, results)

// First-ever probe of this address — surface as verifying. Re-probes
// of an address that already has a verdict stay silent: the verdicts
// entry persists across re-probes and only flips on an actual change.
if (!this.verdicts.has(addrString)) {
this.verdicts.set(addrString, { addr: addr.multiaddr, state: 'verifying' })
this.safeDispatchEvent('address:verifying', { detail: { addr: addr.multiaddr } })
}
}

output.push(results)
Expand All @@ -368,26 +408,31 @@ export class AutoNATv2Client implements Startable {

/**
* Removes any multiaddr result objects created for old multiaddrs that we are
* no longer waiting on
* no longer waiting on, and prunes verdicts for addresses no longer tracked
* by the AddressManager (emitting `address:removed` for each).
*/
private removeOutdatedMultiaddrResults (): void {
const unverifiedMultiaddrs = new Set(this.components.addressManager.getAddressesWithMetadata()
.filter(({ expires }) => {
if (expires < Date.now()) {
return true
}

return false
})
const allAddresses = this.components.addressManager.getAddressesWithMetadata()
const allKeys = new Set(allAddresses.map(({ multiaddr }) => multiaddr.toString()))
const unverifiedKeys = new Set(allAddresses
.filter(({ expires }) => expires < Date.now())
.map(({ multiaddr }) => multiaddr.toString())
)

for (const multiaddr of this.dialResults.keys()) {
if (!unverifiedMultiaddrs.has(multiaddr)) {
if (!unverifiedKeys.has(multiaddr)) {
this.log.trace('remove results for %a', multiaddr)
this.dialResults.delete(multiaddr)
}
}

for (const [key, verdict] of this.verdicts) {
if (!allKeys.has(key)) {
this.log.trace('verdict no longer applies for %a', verdict.addr)
this.verdicts.delete(key)
this.safeDispatchEvent('address:removed', { detail: { addr: verdict.addr } })
}
}
}

/**
Expand Down Expand Up @@ -605,6 +650,8 @@ export class AutoNATv2Client implements Startable {

// abort & remove any outstanding verification jobs for this multiaddr
results.result = true

this.recordVerdict(results.multiaddr, 'reachable')
}

private unconfirmAddress (results: DialResults): void {
Expand All @@ -615,6 +662,20 @@ export class AutoNATv2Client implements Startable {

// abort & remove any outstanding verification jobs for this multiaddr
results.result = false

this.recordVerdict(results.multiaddr, 'unreachable')
}

private recordVerdict (addr: Multiaddr, state: 'reachable' | 'unreachable'): void {
const key = addr.toString()
const previous = this.verdicts.get(key)?.state

if (previous === state) {
return
}

this.verdicts.set(key, { addr, state })
this.safeDispatchEvent(`address:${state}`, { detail: { addr } })
}

private getNetworkSegment (ma: Multiaddr): string {
Expand Down
12 changes: 12 additions & 0 deletions packages/protocol-autonat-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@
import { AutoNATv2Service } from './autonat.ts'
import type { ComponentLogger, Metrics, PeerStore } from '@libp2p/interface'
import type { AddressManager, ConnectionManager, RandomWalk, Registrar } from '@libp2p/interface-internal'
import type { Multiaddr } from '@multiformats/multiaddr'

export interface AddressReachabilityChange {
addr: Multiaddr
}

export interface AutoNATv2Events {
'address:verifying': CustomEvent<AddressReachabilityChange>
'address:reachable': CustomEvent<AddressReachabilityChange>
'address:unreachable': CustomEvent<AddressReachabilityChange>
'address:removed': CustomEvent<AddressReachabilityChange>
}

export interface AutoNATv2ServiceInit {
/**
Expand Down
Loading
Loading