Skip to content

Commit b75ec6f

Browse files
committed
feat: NodeConnectionManager handles reverse intimated connections
* Related #527 [ci skip]
1 parent fcc3315 commit b75ec6f

3 files changed

Lines changed: 141 additions & 35 deletions

File tree

src/nodes/NodeConnection.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ContextTimed } from '@matrixai/contexts';
22
import type { PromiseCancellable } from '@matrixai/async-cancellable';
33
import type { NodeId, QuicConfig } from './types';
44
import type { Host, Hostname, Port, TLSConfig } from '../network/types';
5-
import type { CertificatePEM } from '../keys/types';
5+
import type { Certificate, CertificatePEM } from '../keys/types';
66
import type { ClientManifest, RPCStream } from '../rpc/types';
77
import type {
88
QUICSocket,
@@ -22,6 +22,7 @@ import RPCClient from '../rpc/RPCClient';
2222
import * as networkUtils from '../network/utils';
2323
import * as rpcUtils from '../rpc/utils';
2424
import * as keysUtils from '../keys/utils';
25+
import * as nodesUtils from '../nodes/utils';
2526
import { never } from '../utils';
2627

2728
/**
@@ -183,6 +184,7 @@ class NodeConnection<M extends ClientManifest> extends EventTarget {
183184
if (certChain == null) never();
184185
const nodeId = keysUtils.certNodeId(certChain[0]);
185186
if (nodeId == null) never();
187+
const newLogger = logger.getParent() ?? new Logger(this.name);
186188
const nodeConnection = new this<M>({
187189
validatedNodeId,
188190
nodeId,
@@ -195,7 +197,11 @@ class NodeConnection<M extends ClientManifest> extends EventTarget {
195197
quicClient,
196198
quicConnection,
197199
rpcClient,
198-
logger,
200+
logger: newLogger.getChild(
201+
`${this.name} [${nodesUtils.encodeNodeId(nodeId)}@${
202+
quicConnection.remoteHost
203+
}:${quicConnection.remotePort}]`,
204+
),
199205
});
200206
quicClient.addEventListener(
201207
'clientDestroy',
@@ -211,25 +217,20 @@ class NodeConnection<M extends ClientManifest> extends EventTarget {
211217

212218
static async createNodeConnectionReverse<M extends ClientManifest>({
213219
handleStream,
220+
certChain,
221+
nodeId,
214222
quicConnection,
215223
manifest,
216224
logger = new Logger(this.name),
217225
}: {
218226
handleStream: (stream: RPCStream<Uint8Array, Uint8Array>) => void;
227+
certChain: Array<Certificate>;
228+
nodeId: NodeId;
219229
quicConnection: QUICConnection;
220230
manifest: M;
221231
logger?: Logger;
222232
}): Promise<NodeConnection<M>> {
223233
logger.info(`Creating ${this.name}`);
224-
// No specific error here, validation is handled by the QUICServer
225-
const certChain = quicConnection.getRemoteCertsChain().map((pem) => {
226-
const cert = keysUtils.certFromPEM(pem as CertificatePEM);
227-
if (cert == null) never();
228-
return cert;
229-
});
230-
if (certChain == null) never();
231-
const nodeId = keysUtils.certNodeId(certChain[0]);
232-
if (nodeId == null) never();
233234
// Creating RPCClient
234235
const rpcClient = await RPCClient.createRPCClient<M>({
235236
manifest,

src/nodes/NodeConnectionManager.ts

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type { QUICSocket } from '@matrixai/quic';
1+
import type { QUICConnection, QUICSocket } from '@matrixai/quic';
22
import type { ResourceAcquire } from '@matrixai/resources';
33
import type { ContextTimed } from '@matrixai/contexts';
4+
import type { CertificatePEM } from '../keys/types';
45
import type KeyRing from '../keys/KeyRing';
56
import type { Host, Hostname, Port } from '../network/types';
67
import type NodeGraph from './NodeGraph';
@@ -19,7 +20,7 @@ import type { ContextTimedInput } from '@matrixai/contexts/dist/types';
1920
import type { RPCStream } from '../rpc/types';
2021
import type { TLSConfig } from '../network/types';
2122
import type { QuicConfig } from './types';
22-
import type { ServerCrypto } from '@matrixai/quic';
23+
import type { ServerCrypto, events as QuicEvents } from '@matrixai/quic';
2324
import { withF } from '@matrixai/resources';
2425
import Logger from '@matrixai/logger';
2526
import { ready, StartStop } from '@matrixai/async-init/dist/StartStop';
@@ -84,7 +85,7 @@ class NodeConnectionManager {
8485
public readonly connectionHolePunchIntervalTime: number;
8586

8687
protected handleStream: (stream: RPCStream<Uint8Array, Uint8Array>) => void =
87-
() => never();
88+
() => never() as (stream: RPCStream<Uint8Array, Uint8Array>) => void;
8889
protected logger: Logger;
8990
protected nodeGraph: NodeGraph;
9091
protected keyRing: KeyRing;
@@ -115,6 +116,12 @@ class NodeConnectionManager {
115116
protected tlsConfig: TLSConfig;
116117
protected quicConfig: QuicConfig;
117118
protected crypto: ServerCrypto & ClientCrypto;
119+
protected serverConnectionHandler = async (
120+
connectionEvent: QuicEvents.QUICServerConnectionEvent,
121+
) => {
122+
const quicConnection = connectionEvent.detail;
123+
await this.handleConnectionReverse(quicConnection);
124+
};
118125

119126
public constructor({
120127
keyRing,
@@ -211,12 +218,19 @@ class NodeConnectionManager {
211218
// Starting QUICServer
212219
// No host or port is provided here, it's configured in the shared QUICSocket.
213220
await this.quicServer.start();
214-
221+
this.quicServer.addEventListener(
222+
'serverConnection',
223+
this.serverConnectionHandler,
224+
);
215225
this.logger.info(`Started ${this.constructor.name}`);
216226
}
217227

218228
public async stop() {
219229
this.logger.info(`Stopping ${this.constructor.name}`);
230+
this.quicServer.removeEventListener(
231+
'serverConnection',
232+
this.serverConnectionHandler,
233+
);
220234
this.nodeManager = undefined;
221235
const destroyProms: Array<Promise<void>> = [];
222236
for (const [nodeId, connAndTimer] of this.connections) {
@@ -577,7 +591,7 @@ class NodeConnectionManager {
577591
},
578592
connectionsResults: Map<NodeIdString, ConnectionAndTimer>,
579593
ctx: ContextTimed,
580-
) {
594+
): Promise<void> {
581595
// TODO: do we bother with a concurrency limit for now? It's simple to use a semaphore.
582596
// TODO: if all connections fail then this needs to throw. Or does it? Do we just report the allSettled result?
583597
// 1. attempt connection to an address
@@ -629,27 +643,102 @@ class NodeConnectionManager {
629643
// Return;
630644
}
631645
// Final setup
646+
const newConnAndTimer = this.addConnection(nodeId, connection);
647+
// We can assume connection was established and destination was valid, we can add the target to the nodeGraph
648+
await this.nodeManager?.setNode(nodeId, {
649+
host: address.host,
650+
port: address.port,
651+
});
652+
connectionsResults.set(nodeIdString, newConnAndTimer);
653+
this.logger.debug(
654+
`Created NodeConnection for ${nodesUtils.encodeNodeId(
655+
nodeId,
656+
)} on ${address}`,
657+
);
658+
}
659+
660+
/**
661+
* This will take a `QUICConnection` emitted by the `QUICServer` and handle adding it to the connection map
662+
*/
663+
@ready(new nodesErrors.ErrorNodeConnectionManagerNotRunning())
664+
protected async handleConnectionReverse(quicConnection: QUICConnection) {
665+
// Checking NodeId
666+
// No specific error here, validation is handled by the QUICServer
667+
const certChain = quicConnection.getRemoteCertsChain().map((pem) => {
668+
const cert = keysUtils.certFromPEM(pem as CertificatePEM);
669+
if (cert == null) never();
670+
return cert;
671+
});
672+
if (certChain == null) never();
673+
const nodeId = keysUtils.certNodeId(certChain[0]);
674+
if (nodeId == null) never();
675+
const nodeIdString = nodeId.toString() as NodeIdString;
676+
// TODO: A connection can fail while awaiting lock. We should abort early in this case.
677+
return await this.connectionLocks.withF(
678+
[nodeIdString, Lock],
679+
async (): Promise<void> => {
680+
// Check if the connection already exists under that nodeId and reject the connection if so
681+
if (this.connections.has(nodeIdString)) {
682+
// Reject and return early.
683+
await quicConnection.stop({
684+
applicationError: true,
685+
errorCode: 42,
686+
errorMessage: 'Connection already exists, forcing close',
687+
force: true,
688+
});
689+
return;
690+
}
691+
const nodeConnection =
692+
await NodeConnection.createNodeConnectionReverse<AgentClientManifest>(
693+
{
694+
handleStream: this.handleStream,
695+
nodeId,
696+
certChain,
697+
manifest: agentClientManifest,
698+
quicConnection: quicConnection,
699+
logger: this.logger.getChild(
700+
`${NodeConnection.name} [${nodesUtils.encodeNodeId(nodeId)}@${
701+
quicConnection.remoteHost
702+
}:${quicConnection.remotePort}]`,
703+
),
704+
},
705+
);
706+
// Final setup
707+
this.addConnection(nodeId, nodeConnection);
708+
// We can add the target to the nodeGraph
709+
await this.nodeManager?.setNode(nodeId, {
710+
host: nodeConnection.host,
711+
port: nodeConnection.port,
712+
});
713+
},
714+
);
715+
}
716+
717+
/**
718+
* Adds connection to the connections map. Preforms some checks and lifecycle hooks.
719+
* This code is shared between the reverse and forward connection creation.
720+
*/
721+
protected addConnection(
722+
nodeId: NodeId,
723+
nodeConnection: NodeConnection<AgentClientManifest>,
724+
): ConnectionAndTimer {
725+
const nodeIdString = nodeId.toString() as NodeIdString;
726+
// Check if exists in map, this should never happen but better safe than sorry.
727+
if (this.connections.has(nodeIdString)) never();
632728
const handleDestroy = async () => {
633729
this.logger.debug('stream destroyed event');
634730
// To avoid deadlock only in the case where this is called
635731
// we want to check for destroying connection and read lock
636-
const connAndTimer = this.connections.get(nodeIdString);
637-
// If the connection is calling destroyCallback then it SHOULD
638-
// exist in the connection map
639-
if (connAndTimer == null) return;
732+
// If the connection is calling destroyCallback then it SHOULD exist in the connection map.
733+
if (!this.connections.has(nodeIdString)) return;
640734
// Already locked so already destroying
641735
if (this.connectionLocks.isLocked(nodeIdString)) return;
642736
await this.destroyConnection(nodeId);
643737
};
644-
connection.addEventListener('destroy', handleDestroy, {
738+
nodeConnection.addEventListener('destroy', handleDestroy, {
645739
once: true,
646740
});
647-
// We can assume connection was established and destination was valid,
648-
// we can add the target to the nodeGraph
649-
await this.nodeManager?.setNode(nodeId, {
650-
host: address.host,
651-
port: address.port,
652-
});
741+
653742
// Creating TTL timeout.
654743
// We don't create a TTL for seed nodes.
655744
const timeToLiveTimer = !this.isSeedNode(nodeId)
@@ -660,17 +749,12 @@ class NodeConnectionManager {
660749
: null;
661750
// Add to map
662751
const newConnAndTimer: ConnectionAndTimer = {
663-
connection,
752+
connection: nodeConnection,
664753
timer: timeToLiveTimer,
665754
usageCount: 0,
666755
};
667756
this.connections.set(nodeIdString, newConnAndTimer);
668-
connectionsResults.set(nodeIdString, newConnAndTimer);
669-
this.logger.debug(
670-
`Created NodeConnection for ${nodesUtils.encodeNodeId(
671-
nodeId,
672-
)} on ${address}`,
673-
);
757+
return newConnAndTimer;
674758
}
675759

676760
/**

tests/nodes/NodeConnection.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Host, Port, TLSConfig } from '@/network/types';
22
import type * as quicEvents from '@matrixai/quic/dist/events';
33
import type { NodeId, NodeIdEncoded } from '@/ids';
44
import type { RPCStream } from '@/rpc/types';
5+
import type { CertificatePEM } from '@/keys/types';
56
import { QUICServer, QUICSocket } from '@matrixai/quic';
67
import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger';
78
import { errors as quicErrors } from '@matrixai/quic';
@@ -10,7 +11,7 @@ import * as nodesUtils from '@/nodes/utils';
1011
import * as keysUtils from '@/keys/utils';
1112
import RPCServer from '@/rpc/RPCServer';
1213
import NodeConnection from '@/nodes/NodeConnection';
13-
import { promise } from '@/utils';
14+
import { never, promise } from '@/utils';
1415
import * as networkUtils from '@/network/utils';
1516
import * as tlsTestUtils from '../utils/tls';
1617

@@ -370,9 +371,19 @@ describe(`${NodeConnection.name}`, () => {
370371
'serverConnection',
371372
async (event: quicEvents.QUICServerConnectionEvent) => {
372373
const quicConnection = event.detail;
374+
const certChain = quicConnection.getRemoteCertsChain().map((pem) => {
375+
const cert = keysUtils.certFromPEM(pem as CertificatePEM);
376+
if (cert == null) never();
377+
return cert;
378+
});
379+
if (certChain == null) never();
380+
const nodeId = keysUtils.certNodeId(certChain[0]);
381+
if (nodeId == null) never();
373382
const nodeConnection = await NodeConnection.createNodeConnectionReverse(
374383
{
375384
handleStream: () => {},
385+
nodeId,
386+
certChain,
376387
manifest: {},
377388
quicConnection,
378389
logger,
@@ -411,11 +422,21 @@ describe(`${NodeConnection.name}`, () => {
411422
'serverConnection',
412423
async (event: quicEvents.QUICServerConnectionEvent) => {
413424
const quicConnection = event.detail;
425+
const certChain = quicConnection.getRemoteCertsChain().map((pem) => {
426+
const cert = keysUtils.certFromPEM(pem as CertificatePEM);
427+
if (cert == null) never();
428+
return cert;
429+
});
430+
if (certChain == null) never();
431+
const nodeId = keysUtils.certNodeId(certChain[0]);
432+
if (nodeId == null) never();
414433
const nodeConnection = await NodeConnection.createNodeConnectionReverse(
415434
{
416435
handleStream: (stream) => {
417436
reverseStreamProm.resolveP(stream);
418437
},
438+
nodeId,
439+
certChain,
419440
manifest: {},
420441
quicConnection,
421442
logger,

0 commit comments

Comments
 (0)