@@ -84,11 +84,58 @@ class QuicTransport(
8484 * peer id we expect that connection to present. A hole punch always targets a known peer, so this
8585 * is never null (see [dialAsListener]).
8686 */
87- private data class PendingHolePunch (
87+ internal data class PendingHolePunch (
8888 val expectedPeerId : PeerId ,
8989 val future : CompletableFuture <QuicChannel >
9090 )
9191
92+ /* *
93+ * Routes a completed inbound (server-side) handshake, applying the hole-punch identity guard
94+ * before the connection is exposed to application handlers. Extracted from the channelActive
95+ * handler in [serverTransportBuilder] so the reject-before-exposure guarantee is testable without
96+ * a live QUIC channel.
97+ *
98+ * If [pending] is a hole punch whose identity does not match the dial target, [closeChannel] is
99+ * invoked and the pending future is completed exceptionally — neither [prepareConnection] nor
100+ * [exposeConnection] runs, so a wrong-identity peer never reaches inbound protocol handlers.
101+ * Otherwise the connection is prepared and then either handed to the waiting hole-punch caller
102+ * ([holePunchChannel]) or exposed via [exposeConnection].
103+ */
104+ internal fun routeInboundHandshake (
105+ remoteIdentity : io.libp2p.security.tls.TlsPeerIdentity ,
106+ pending : PendingHolePunch ? ,
107+ prepareConnection : () -> Unit ,
108+ closeChannel : () -> Unit ,
109+ holePunchChannel : () -> QuicChannel ,
110+ exposeConnection : () -> Unit
111+ ) {
112+ if (pending != null && ! holePunchIdentityMatches(pending.expectedPeerId, remoteIdentity.peerId)) {
113+ logger.warn(
114+ " Hole-punched peer presented libp2p id {} but dial target was {}; closing" ,
115+ remoteIdentity.peerId,
116+ pending.expectedPeerId
117+ )
118+ closeChannel()
119+ pending.future.completeExceptionally(
120+ Libp2pException (
121+ " Remote peer presented libp2p pubkey for ${remoteIdentity.peerId} " +
122+ " but dial target was ${pending.expectedPeerId} "
123+ )
124+ )
125+ return
126+ }
127+
128+ prepareConnection()
129+
130+ if (pending != null ) {
131+ // Route this validated inbound connection to the waiting hole-punch caller.
132+ pending.future.complete(holePunchChannel())
133+ } else {
134+ // Normal path: deliver to the connection handler.
135+ exposeConnection()
136+ }
137+ }
138+
92139 /* * Pending hole punches keyed by the remote address we are trying to reach. */
93140 private val pendingHolePunches = ConcurrentHashMap <InetSocketAddress , PendingHolePunch >()
94141
@@ -427,6 +474,7 @@ class QuicTransport(
427474 " quic-handshake-waiter" ,
428475 object : ChannelInboundHandlerAdapter () {
429476 override fun channelActive (ctx : ChannelHandlerContext ) {
477+ val self = this
430478 // Read peer certificates from this connection's own SSL session
431479 // rather than shared TrustManager state, avoiding a race between
432480 // concurrent handshakes. Both remoteId and remotePubKey come from
@@ -455,50 +503,34 @@ class QuicTransport(
455503 // check closes the channel, giving a wrong-identity peer that
456504 // reached the expected UDP tuple an unauthenticated-by-target
457505 // window into inbound protocols.
458- if (pending != null &&
459- ! holePunchIdentityMatches(pending.expectedPeerId, remoteIdentity.peerId)
460- ) {
461- logger.warn(
462- " Hole-punched peer presented libp2p id {} but dial target was {}; closing" ,
463- remoteIdentity.peerId,
464- pending.expectedPeerId
465- )
466- ctx.close()
467- pending.future.completeExceptionally(
468- Libp2pException (
469- " Remote peer presented libp2p pubkey for ${remoteIdentity.peerId} " +
470- " but dial target was ${pending.expectedPeerId} "
506+ routeInboundHandshake(
507+ remoteIdentity = remoteIdentity,
508+ pending = pending,
509+ prepareConnection = {
510+ connection.setSecureSession(
511+ SecureChannel .Session (
512+ PeerId .fromPubKey(localKey.publicKey()),
513+ remoteIdentity.peerId,
514+ remoteIdentity.pubKey,
515+ null
516+ )
471517 )
472- )
473- return
474- }
475518
476- connection.setSecureSession(
477- SecureChannel .Session (
478- PeerId .fromPubKey(localKey.publicKey()),
479- remoteIdentity.peerId,
480- remoteIdentity.pubKey,
481- null
482- )
519+ // Remove this handler as it's no longer needed
520+ ctx.pipeline().remove(self)
521+
522+ // Warm the address cache while the QuicChannel is still
523+ // live; once closed it frees native state and
524+ // remoteSocketAddress() returns null.
525+ connection.cacheAddresses()
526+ },
527+ closeChannel = { ctx.close() },
528+ holePunchChannel = { ctx.channel() as QuicChannel },
529+ exposeConnection = {
530+ preHandler?.also { visitor -> visitor.visit(connection) }
531+ connHandler.handleConnection(connection)
532+ }
483533 )
484-
485- // Remove this handler as it's no longer needed
486- ctx.pipeline().remove(this )
487-
488- // Warm the address cache while the QuicChannel is still live;
489- // once closed it frees native state and remoteSocketAddress()
490- // returns null.
491- connection.cacheAddresses()
492-
493- if (pending != null ) {
494- // Route this validated inbound connection to the waiting
495- // hole-punch caller.
496- pending.future.complete(ctx.channel() as QuicChannel )
497- } else {
498- // Normal path: deliver to the connection handler
499- preHandler?.also { visitor -> visitor.visit(connection) }
500- connHandler.handleConnection(connection)
501- }
502534 } else {
503535 // This should not happen if channelActive is called after handshake
504536 ctx.close()
0 commit comments