///
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;