1- import type { QUICSocket } from '@matrixai/quic' ;
1+ import type { QUICConnection , QUICSocket } from '@matrixai/quic' ;
22import type { ResourceAcquire } from '@matrixai/resources' ;
33import type { ContextTimed } from '@matrixai/contexts' ;
4+ import type { CertificatePEM } from '../keys/types' ;
45import type KeyRing from '../keys/KeyRing' ;
56import type { Host , Hostname , Port } from '../network/types' ;
67import type NodeGraph from './NodeGraph' ;
@@ -19,7 +20,7 @@ import type { ContextTimedInput } from '@matrixai/contexts/dist/types';
1920import type { RPCStream } from '../rpc/types' ;
2021import type { TLSConfig } from '../network/types' ;
2122import type { QuicConfig } from './types' ;
22- import type { ServerCrypto } from '@matrixai/quic' ;
23+ import type { ServerCrypto , events as QuicEvents } from '@matrixai/quic' ;
2324import { withF } from '@matrixai/resources' ;
2425import Logger from '@matrixai/logger' ;
2526import { 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 /**
0 commit comments