/// import type { ResourceAcquire } from '@matrixai/resources'; import type { ContextTimed, ContextTimedInput } from '@matrixai/contexts'; import type { QUICConnection } from '@matrixai/quic'; import type { ClientManifest, JSONRPCRequest, JSONRPCResponse, ServerManifest } from '@matrixai/rpc'; import type { AuthenticateNetworkForwardCallback, AuthenticateNetworkReverseCallback, NodeId, NodeIdString } from './types.js'; import type { NodesAuthenticateConnectionMessage, SuccessMessage } from './agent/types.js'; import type { AgentClientManifestNodeConnectionManager } from './agent/callers/index.js'; import type KeyRing from '../keys/KeyRing.js'; import type { Host, Hostname, Port, TLSConfig } from '../network/types.js'; import type { JSONValue } from '../types.js'; import { TransformStream } from 'stream/web'; import { events as quicEvents, QUICServer, QUICSocket } from '@matrixai/quic'; import { RPCServer } from '@matrixai/rpc'; import Logger from '@matrixai/logger'; import { Timer } from '@matrixai/timer'; import { startStop } from '@matrixai/async-init'; import { EventAll } from '@matrixai/events'; import { Semaphore } from '@matrixai/async-locks'; import { PromiseCancellable } from '@matrixai/async-cancellable'; import NodeConnection from './NodeConnection.js'; import * as nodesEvents from './events.js'; import RateLimiter from '../utils/ratelimiter/RateLimiter.js'; type ConnectionAndTimer = { connection: NodeConnection; timer: Timer | null; usageCount: number; }; declare enum AuthenticatingState { PENDING = 1, SUCCESS = 2, FAIL = 3 } type ConnectionsEntry = { activeConnection: string; connections: Record>; authenticatedForward: AuthenticatingState; authenticatedReverse: AuthenticatingState; authenticateComplete: boolean; authenticatedP: Promise; authenticatedResolveP: (value: void) => void; authenticatedRejectP: (reason?: Error) => void; }; type ConnectionInfo = { host: Host; hostName: Hostname | undefined; port: Port; timeout: number | undefined; primary: boolean; }; type ActiveConnectionsInfo = { nodeId: NodeId; connections: Record; }; /** * NodeConnectionManager is a server that manages all node connections. * It manages both initiated and received connections. * * It acts like a phone call system. * It can maintain multiple calls to other nodes. * There's no guarantee that we need to make it. * * Node connections make use of the QUIC protocol. * The NodeConnectionManager encapsulates `QUICServer`. * While the NodeConnection encapsulates `QUICClient`. */ interface NodeConnectionManager extends startStop.StartStop { } declare class NodeConnectionManager { /** * Alpha constant for kademlia * The number of the closest nodes to contact initially */ readonly connectionFindConcurrencyLimit: number; /** * Default limit used when getting the closest active connections of a node. * Defaults to the `nodesGraphBucketLimit` */ readonly connectionGetClosestLimit: number; /** * Time used to find a node using `findNodeLocal`. */ readonly connectionFindLocalTimeoutTime: number; /** * Minimum time to wait to garbage collect un-used node connections. */ readonly connectionIdleTimeoutTimeMin: number; /** * Scaling factor to apply to Idle timeout */ readonly connectionIdleTimeoutTimeScale: number; /** * Time used to establish `NodeConnection` */ readonly connectionConnectTimeoutTime: number; /** * Time to keep alive node connection. */ readonly connectionKeepAliveTimeoutTime: number; /** * Time interval for sending keep alive messages. */ readonly connectionKeepAliveIntervalTime: number; /** * Initial delay between punch packets, delay doubles each attempt. */ readonly connectionHolePunchIntervalTime: number; /** * Total number of active bidirectional streams that can be created */ readonly connectionInitialMaxStreamsBidi: number; /** * Total number of active unidirectional streams that can be created */ readonly connectionInitialMaxStreamsUni: number; /** * Max parse buffer size before RPC parser throws a parse error. */ readonly rpcParserBufferSize: number; /** * Default timeout for RPC handlers */ readonly rpcCallTimeoutTime: number; /** * Used to track active hole punching attempts. * Attempts are mapped by a string of `${host}:${port}`. * This is used to coalesce attempts to a target host and port. * Used to cancel and await punch attempts when stopping to prevent orphaned promises. */ protected activeHolePunchPs: Map>; /** * Used to rate limit hole punch attempts per IP Address. * We use a semaphore to track the number of active hole punch attempts to that address. * We Use a semaphore here to allow a limit of 3 attempts per host. * To allow concurrent attempts to the same host while limiting the number of different ports. * This is mainly used to limit requests to a single target host. */ protected activeHolePunchAddresses: Map; /** * Used track the active `nodesConnectionSignalFinal` attempts and prevent orphaned promises. * Used to cancel and await the active `nodesConnectionSignalFinal` when stopping. */ protected activeSignalFinalPs: Set>; /** * Used to limit signalling requests on a per-requester basis. * This is mainly used to limit a single source node making too many requests through a relay. */ protected rateLimiter: RateLimiter; /** * Used to track the active authentication RPC calls */ protected activeForwardAuthenticateCalls: Map>; /** * Callback used to generate authentication data when making the authentication call */ protected authenticateNetworkForwardCallback: AuthenticateNetworkForwardCallback; /** * Callback used to authenticate the peer when processing an authentication request from the peer */ protected authenticateNetworkReverseCallback: AuthenticateNetworkReverseCallback; protected logger: Logger; protected keyRing: KeyRing; protected tlsConfig: TLSConfig; protected quicSocket: QUICSocket; protected quicServer: QUICServer; /** * Data structure to store all NodeConnections. If a connection to a node `N` does * not exist, no entry for `N` will exist in the map. Alternatively, if a * connection is currently being instantiated by some thread, an entry will * exist in the map, but only with the lock (no connection object). Once a * connection is instantiated, the entry in the map is updated to include the * connection object. * A nodeIdString is used for the key here since * NodeIds can't be used to properly retrieve a value from the map. */ protected connections: Map>; protected rpcServer: RPCServer; protected rpcClientManifest: Manifest; /** * Dispatches a `EventNodeConnectionManagerClose` in response to any `NodeConnectionManager` * error event. Will trigger stop of the `NodeConnectionManager` via the * `EventNodeConnectionManagerError` -> `EventNodeConnectionManagerClose` event path. */ protected handleEventNodeConnectionManagerError: (evt: nodesEvents.EventNodeConnectionManagerError) => void; /** * Triggers the destruction of the `NodeConnectionManager`. Since this is only in * response to an underlying problem or close it will force destroy. * Dispatched by the `EventNodeConnectionManagerError` event as the * `EventNodeConnectionManagerError` -> `EventNodeConnectionManagerClose` event path. */ protected handleEventNodeConnectionManagerClose: (_evt: nodesEvents.EventNodeConnectionManagerClose) => Promise; protected handleEventNodeConnectionStream: (evt: nodesEvents.EventNodeConnectionStream) => void; protected handleEventNodeConnectionDestroyed: (evt: nodesEvents.EventNodeConnectionDestroyed) => Promise; /** * Redispatches `QUICSocket` or `QUICServer` error events as `NodeConnectionManager` error events. * This should trigger the destruction of the `NodeConnection` through the * `EventNodeConnectionError` -> `EventNodeConnectionClose` event path. */ protected handleEventQUICError: (evt: quicEvents.EventQUICSocketError) => void; /** * Handle unexpected stoppage of the QUICSocket. Not expected to happen * without error, but we have it just in case. */ protected handleEventQUICSocketStopped: (_evt: quicEvents.EventQUICSocketStopped) => void; /** * Handle unexpected stoppage of the QUICServer. Not expected to happen * without error, but we have it just in case. */ protected handleEventQUICServerStopped: (_evt: quicEvents.EventQUICServerStopped) => void; /** * Handles `EventQUICServerConnection` events. These are reverser or server * peer initiated connections that needs to be handled and added to the * connection map. */ protected handleEventQUICServerConnection: (evt: quicEvents.EventQUICServerConnection) => void; /** * Handles all events and redispatches them upwards */ protected handleEventAll: (evt: EventAll) => void; /** * Constructs the `NodeConnectionManager`. */ constructor({ keyRing, tlsConfig, rpcClientManifest, connectionFindConcurrencyLimit, connectionGetClosestLimit, connectionFindLocalTimeoutTime, connectionIdleTimeoutTimeMin, connectionIdleTimeoutTimeScale, connectionConnectTimeoutTime, connectionKeepAliveTimeoutTime, connectionKeepAliveIntervalTime, connectionHolePunchIntervalTime, connectionInitialMaxStreamsBidi, connectionInitialMaxStreamsUni, rpcParserBufferSize, rpcCallTimeoutTime, authenticateNetworkForwardCallback, authenticateNetworkReverseCallback, logger, }: { keyRing: KeyRing; tlsConfig: TLSConfig; rpcClientManifest: Manifest; connectionFindConcurrencyLimit?: number; connectionGetClosestLimit?: number; connectionFindLocalTimeoutTime?: number; connectionIdleTimeoutTimeMin?: number; connectionIdleTimeoutTimeScale?: number; connectionConnectTimeoutTime?: number; connectionKeepAliveTimeoutTime?: number; connectionKeepAliveIntervalTime?: number; connectionHolePunchIntervalTime?: number; connectionInitialMaxStreamsBidi?: number; connectionInitialMaxStreamsUni?: number; rpcParserBufferSize?: number; rpcCallTimeoutTime?: number; authenticateNetworkForwardCallback?: AuthenticateNetworkForwardCallback; authenticateNetworkReverseCallback?: AuthenticateNetworkReverseCallback; logger?: Logger; }); /** * Get the host that node connection manager is bound to. */ get host(): Host; /** * Get the port that node connection manager is bound to. */ get port(): Port; get type(): 'ipv4' | 'ipv6' | 'ipv4&ipv6'; start({ agentService, host, port, reuseAddr, ipv6Only, }: { agentService: ServerManifest; host?: Host; port?: Port; reuseAddr?: boolean; ipv6Only?: boolean; }): Promise; stop({ force, }?: { force?: boolean; }): Promise; /** * This is the internal acquireConnection for using connections without * authentication. For usage with withF, to acquire a connection. To wait for * authentication, use {@link acquireConnection}. * * This unique acquire function structure of returning the ResourceAcquire * itself is such that we can pass targetNodeId as a parameter (as opposed to * an acquire function with no parameters). * * @param targetNodeId Id of target node to communicate with * @returns ResourceAcquire Resource API for use in with contexts */ acquireConnectionInternal(targetNodeId: NodeId): ResourceAcquire>; /** * This unique acquire function structure of returning the ResourceAcquire * itself is such that we can pass targetNodeId as a parameter (as opposed to * an acquire function with no parameters). It waits for the connection to be * authenticated, otherwise throws an error. See {@link acquireConnectionInternal} * to connect to a node without waiting for authentication. * * If a connection exists but is not authenticated, the authentication is * attempted. Authentication is reattempted if it has failed before but * another attmept is being made to connect to a node. * * For usage with withF, to acquire a connection. * * @param targetNodeId Id of target node to communicate with * @param ctx * @returns ResourceAcquire Resource API for use in with contexts */ acquireConnection(targetNodeId: NodeId, ctx: ContextTimed): ResourceAcquire>; /** * Perform some function on another node over the network with a connection. * Will either retrieve an existing connection, or create a new one if it * doesn't exist. * for use with normal arrow function * @param targetNodeId Id of target node to communicate with * @param ctx * @param f Function to handle communication */ withConnF(targetNodeId: NodeId, ctx: Partial | undefined, f: (conn: NodeConnection) => Promise): Promise; /** * Perform some function on another node over the network with a connection. * Will either retrieve an existing connection, or create a new one if it * doesn't exist. * for use with a generator function * @param targetNodeId Id of target node to communicate with * @param ctx * @param g Generator function to handle communication */ withConnG(targetNodeId: NodeId, ctx: Partial | undefined, g: (conn: NodeConnection) => AsyncGenerator): AsyncGenerator; /** * Starts a connection. This step also attemps to authenticate the connection. */ createConnection(nodeIds: Array, host: Host, port: Port, ctx?: Partial): PromiseCancellable>; /** * Creates multiple connections looking for a single node. Once the connection * has been established then all pending connections are cancelled. * This will return the first connection made or timeout. */ createConnectionMultiple(nodeIds: Array, addresses: Array<[Host, Port]>, ctx?: Partial): PromiseCancellable>; /** * This will start a new connection using a signalling node to coordinate hole punching. */ createConnectionPunch(nodeIdTarget: NodeId, nodeIdSignaller: NodeId, ctx?: Partial): PromiseCancellable>; /** * Adds connection to the connections map. Preforms some checks and lifecycle * hooks. This code sets up the authentication state machine, and must be run * before attempting authentication. * * Multiple connections can be added for a single NodeId, but the connection * with the 'lowest' `connectionId` will be used. The remaining * connections will be left to timeout gracefully. * * @param nodeId The target NodeId to connect to * @param nodeConnection The object corresponding to the node connection */ protected addConnection(nodeId: NodeId, nodeConnection: NodeConnection): ConnectionAndTimer; /** * Gets the existing active connection for the target node */ getConnection(nodeId: NodeId): ConnectionAndTimer | undefined; /** * Removes the connection from the connection map and destroys it. * If the connectionId is specified then just that connection is destroyed. * If no connectionId is specified then all connections for that node are destroyed. * * @param targetNodeId Id of node we are destroying connection to * @param force - if true force the connection to end with error. * @param connectionIdTarget - if specified destroys only the desired connection. */ destroyConnection(targetNodeId: NodeId, force: boolean, connectionIdTarget?: string): Promise; /** * Will determine how long to keep a node around for. * * Timeout is scaled linearly from 1 min to 2 hours based on its bucket. * The value will be symmetric for two nodes, * they will assign the same timeout for each other. */ protected getStickyTimeoutValue(nodeId: NodeId, primary: boolean): number; /** * This takes a reverse initiated QUICConnection, wraps it as a * NodeConnection and adds it to the connection map. */ protected handleConnectionReverse(quicConnection: QUICConnection): void; /** * Open up a port in the NAT by sending packets to the target address. * The packets will be sent in an exponential backoff dialing pattern and * contain random data. * * This is only ever done used in the reverse direction to open up the nat * for the connection to establish from the forward direction. * * This can't know it succeeded, it will continue until timed out or cancelled. * * @param host host of the target client. * @param port port of the target client. * @param ctx */ holePunch(host: Host, port: Port, ctx?: Partial): PromiseCancellable; hasConnection(nodeId: NodeId): boolean; listConnections(): Array<{ nodeId: NodeId; connectionId: string; primary: boolean; address: { host: Host; port: Port; hostname: Hostname | undefined; }; usageCount: number; timeout: number | undefined; authenticated: boolean; }>; /** * Returns the number of active connections */ connectionsActive(): number; updateTlsConfig(tlsConfig: TLSConfig): void; /** * This is used by the `NodesConnectionSignalFinal` to initiate the hole punch * procedure. * * Will validate the message, and initiate hole punching in the background and * return immediately. * Attempts to the same host and port are coalesced. * Attempts to the same host are limited by a semaphore. * Active attempts are tracked inside the `activeHolePunchPs` set and are * cancelled and awaited when the `NodeConnectionManager` stops. */ handleNodesConnectionSignalFinal(host: Host, port: Port): void; /** * This is used by the `NodesConnectionSignalInitial` to initiate a relay * request. Requests can only be relayed to nodes this node is currently * connected to. * * Requests made by the same node are rate limited, when the limit has been * exceeded the request throws an `ErrorNodeConnectionManagerRequestRateExceeded` * error. * * Active relay attempts are tracked in `activeSignalFinalPs` and are cancelled * and awaited when the `NodeConnectionManager` stops. * * @param sourceNodeId NodeId of the node making the request. Used for rate limiting. * @param targetNodeId NodeId of the node that needs to initiate hole punching. * @param address Address the target needs to punch to. * @param requestSignature `base64url` encoded signature * @param ctx */ handleNodesConnectionSignalInitial(sourceNodeId: NodeId, targetNodeId: NodeId, address: { host: Host; port: Port; }, requestSignature: string, ctx?: Partial): Promise<{ host: Host; port: Port; }>; /** * Returns a list of active connections and their address information. */ getClosestConnections(targetNodeId: NodeId, limit?: number): Array; /** * Handles the authentication for two nodes. This is done by triggering a RPC * duplex method. The duplex stream writes messages to a writer and awaits the * responses from a reader in real-time. The authentication follows a strict * protocol. * * SEND Authentication message * RECV Response message (reverse) * RECV Authentication message from Node B * SEND Response message (reverse) * RECV Acknowledgement message * * @param nodeId The NodeId of the target node * @param ctx * @see {@link handleAuthentication} for RPC protocol */ forwardAuthenticate(nodeId: NodeId, ctx?: Partial): PromiseCancellable; handleReverseAuthenticate(nodeId: NodeId, message: NodesAuthenticateConnectionMessage, ctx?: Partial): PromiseCancellable; /** * Will initiate a forward authentication call and coalesce. This method is * idempotent. */ initiateForwardAuthenticate(nodeId: NodeId): void; /** * Handles the authentication for two nodes. This is done by yielding messages * and awaiting response for the messages in real-time. The messages are * yielded by the async generator and the reponse is awaited for via the * async iterator. The authentication follows a strict protocol. * * RECV Authentication message from Node A * SEND Response message * SEND Authentication message from Node B * RECV Response message * SEND Acknowledgement message * * @param requestingNodeId The NodeId of the requesting node * @param inputIterator An iterator yielding responses for the sent messages * @param ctx * @see {@link forwardAuthenticate} for usage example */ handleAuthentication(requestingNodeId: NodeId, inputIterator: AsyncIterableIterator, ctx: ContextTimed): AsyncGenerator; /** * Returns true if the connection has been authenticated */ isAuthenticated(nodeId: NodeId): boolean; /** * Returns a promise that resolves once the connection has authenticated, * otherwise it rejects with the authentication failure * @param nodeId * @param ctx */ isAuthenticatedP(nodeId: NodeId, ctx?: Partial): Promise; protected authenticateFail(targetNodeIdString: NodeIdString, reason: Error): void; protected authenticateSuccess(targetNodeIdString: NodeIdString): void; protected authenticateCancel(targetNodeIdString: NodeIdString, reason: Error): void; setAuthenticateNetworkForwardCallback(authenticateNetworkForwardCallback: AuthenticateNetworkForwardCallback): void; setAuthenticateNetworkReverseCallback(authenticateNetworkReverseCallback: AuthenticateNetworkReverseCallback): void; protected authenticationMiddlewareClient: (_ctx: ContextTimed, _cancel: (reason?: any) => void, meta: Record | undefined) => { forward: TransformStream; reverse: TransformStream, JSONRPCResponse>; }; } export default NodeConnectionManager;