Skip to content
Open
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
93 changes: 82 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 @@ -368,26 +400,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 @@ -439,6 +476,10 @@ export class AutoNATv2Client implements Startable {
return
}

for (const result of results) {
this.recordVerifying(result.multiaddr)
}

this.queue.add(async (options: AbortOptions) => {
const signal = anySignal([options.signal, AbortSignal.timeout(this.timeout)])
const nonce = BigInt(randomNumber(0, Number.MAX_SAFE_INTEGER))
Expand Down Expand Up @@ -605,6 +646,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 +658,34 @@ 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 recordVerifying (addr: Multiaddr): void {
const key = addr.toString()

// 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(key)) {
return
}

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

private getNetworkSegment (ma: Multiaddr): string {
Expand Down
20 changes: 18 additions & 2 deletions packages/protocol-autonat-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,24 @@
*/

import { AutoNATv2Service } from './autonat.ts'
import type { ComponentLogger, Metrics, PeerStore } from '@libp2p/interface'
import type { ComponentLogger, Metrics, PeerStore, Startable } from '@libp2p/interface'
import type { AddressManager, ConnectionManager, RandomWalk, Registrar } from '@libp2p/interface-internal'
import type { Multiaddr } from '@multiformats/multiaddr'
import type { TypedEventEmitter } from 'main-event'

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 AutoNATv2 extends Startable, TypedEventEmitter<AutoNATv2Events> {
}

export interface AutoNATv2ServiceInit {
/**
Expand Down Expand Up @@ -103,7 +119,7 @@ export interface AutoNATv2Components {
metrics?: Metrics
}

export function autoNATv2 (init: AutoNATv2ServiceInit = {}): (components: AutoNATv2Components) => unknown {
export function autoNATv2 (init: AutoNATv2ServiceInit = {}): (components: AutoNATv2Components) => AutoNATv2 {
return (components) => {
return new AutoNATv2Service(components, init)
}
Expand Down
Loading
Loading