@libp2p/tcp
Version:
A TCP transport for libp2p
232 lines (192 loc) • 7.34 kB
text/typescript
import { InvalidParametersError, TimeoutError } from '@libp2p/interface'
import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr'
import pDefer from 'p-defer'
import { raceEvent } from 'race-event'
import { duplex } from 'stream-to-it'
import { CLOSE_TIMEOUT, SOCKET_TIMEOUT } from './constants.js'
import { multiaddrToNetConfig } from './utils.js'
import type { ComponentLogger, MultiaddrConnection, CounterGroup } from '@libp2p/interface'
import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr'
import type { Socket } from 'net'
import type { DeferredPromise } from 'p-defer'
interface ToConnectionOptions {
listeningAddr?: Multiaddr
remoteAddr?: Multiaddr
localAddr?: Multiaddr
socketInactivityTimeout?: number
socketCloseTimeout?: number
metrics?: CounterGroup
metricPrefix?: string
logger: ComponentLogger
direction: 'inbound' | 'outbound'
}
/**
* Convert a socket into a MultiaddrConnection
* https://github.com/libp2p/interface-transport#multiaddrconnection
*/
export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptions): MultiaddrConnection => {
let closePromise: DeferredPromise<void>
const log = options.logger.forComponent('libp2p:tcp:socket')
const direction = options.direction
const metrics = options.metrics
const metricPrefix = options.metricPrefix ?? ''
const inactivityTimeout = options.socketInactivityTimeout ?? SOCKET_TIMEOUT
const closeTimeout = options.socketCloseTimeout ?? CLOSE_TIMEOUT
let timedOut = false
let errored = false
// Check if we are connected on a unix path
if (options.listeningAddr?.getPath() != null) {
options.remoteAddr = options.listeningAddr
}
if (options.remoteAddr?.getPath() != null) {
options.localAddr = options.remoteAddr
}
// handle socket errors
socket.on('error', err => {
errored = true
if (!timedOut) {
log.error('%s socket error - %e', direction, err)
metrics?.increment({ [`${metricPrefix}error`]: true })
}
socket.destroy()
maConn.timeline.close = Date.now()
})
let remoteAddr: Multiaddr
if (options.remoteAddr != null) {
remoteAddr = options.remoteAddr
} else {
if (socket.remoteAddress == null || socket.remotePort == null) {
// this can be undefined if the socket is destroyed (for example, if the client disconnected)
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketremoteaddress
throw new InvalidParametersError('Could not determine remote address or port')
}
remoteAddr = toMultiaddr(socket.remoteAddress, socket.remotePort)
}
const lOpts = multiaddrToNetConfig(remoteAddr)
const lOptsStr = lOpts.path ?? `${lOpts.host ?? ''}:${lOpts.port ?? ''}`
const { sink, source } = duplex(socket)
// by default there is no timeout
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketsettimeouttimeout-callback
socket.setTimeout(inactivityTimeout)
socket.once('timeout', () => {
timedOut = true
log('%s %s socket read timeout', direction, lOptsStr)
metrics?.increment({ [`${metricPrefix}timeout`]: true })
// if the socket times out due to inactivity we must manually close the connection
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-timeout
socket.destroy(new TimeoutError())
maConn.timeline.close = Date.now()
})
socket.once('close', () => {
// record metric for clean exit
if (!timedOut && !errored) {
log('%s %s socket close', direction, lOptsStr)
metrics?.increment({ [`${metricPrefix}close`]: true })
}
// In instances where `close` was not explicitly called,
// such as an iterable stream ending, ensure we have set the close
// timeline
socket.destroy()
maConn.timeline.close = Date.now()
})
socket.once('end', () => {
// the remote sent a FIN packet which means no more data will be sent
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-end
log('%s %s socket end', direction, lOptsStr)
metrics?.increment({ [`${metricPrefix}end`]: true })
})
const maConn: MultiaddrConnection = {
async sink (source) {
try {
await sink((async function * () {
for await (const buf of source) {
if (buf instanceof Uint8Array) {
yield buf
} else {
yield buf.subarray()
}
}
})())
} catch (err: any) {
// If aborted we can safely ignore
if (err.type !== 'aborted') {
// If the source errored the socket will already have been destroyed by
// duplex(). If the socket errored it will already be
// destroyed. There's nothing to do here except log the error & return.
log.error('%s %s error in sink - %e', direction, lOptsStr, err)
}
}
// we have finished writing, send the FIN message
socket.end()
},
source,
// If the remote address was passed, use it - it may have the peer ID encapsulated
remoteAddr,
timeline: { open: Date.now() },
async close (options: AbortOptions = {}) {
if (socket.closed) {
log('the %s %s socket is already closed', direction, lOptsStr)
return
}
if (socket.destroyed) {
log('the %s %s socket is already destroyed', direction, lOptsStr)
return
}
if (closePromise != null) {
return closePromise.promise
}
try {
closePromise = pDefer()
// close writable end of socket
socket.end()
// convert EventEmitter to EventTarget
const eventTarget = socketToEventTarget(socket)
// don't wait forever to close
const signal = options.signal ?? AbortSignal.timeout(closeTimeout)
// wait for any unsent data to be sent
if (socket.writableLength > 0) {
log('%s %s draining socket', direction, lOptsStr)
await raceEvent(eventTarget, 'drain', signal, {
errorEvent: 'error'
})
log('%s %s socket drained', direction, lOptsStr)
}
await Promise.all([
raceEvent(eventTarget, 'close', signal, {
errorEvent: 'error'
}),
// all bytes have been sent we can destroy the socket
socket.destroy()
])
} catch (err: any) {
this.abort(err)
} finally {
closePromise.resolve()
}
},
abort: (err: Error) => {
log('%s %s socket abort due to error - %e', direction, lOptsStr, err)
// the abortSignalListener may already destroyed the socket with an error
socket.destroy()
// closing a socket is always asynchronous (must wait for "close" event)
// but the tests expect this to be a synchronous operation so we have to
// set the close time here. the tests should be refactored to reflect
// reality.
maConn.timeline.close = Date.now()
},
log
}
return maConn
}
function socketToEventTarget (obj?: any): EventTarget {
const eventTarget = {
addEventListener: (type: any, cb: any) => {
obj.addListener(type, cb)
},
removeEventListener: (type: any, cb: any) => {
obj.removeListener(type, cb)
}
}
// @ts-expect-error partial implementation
return eventTarget
}