@@ -202,9 +202,13 @@ internal enum LibSSH2TunnelFactory {
202202 }
203203 }
204204
205- // Clean up any jump hops that were created (reverse order)
205+ // Clean up any jump hops that were created (reverse order).
206+ // Shutdown sockets first to break relay loops, then free resources.
206207 for hop in jumpHops. reversed ( ) {
207208 hop. relayTask? . cancel ( )
209+ shutdown ( hop. socket, SHUT_RDWR)
210+ }
211+ for hop in jumpHops. reversed ( ) {
208212 libssh2_channel_free ( hop. channel)
209213 tablepro_libssh2_session_disconnect ( hop. session, " Error " )
210214 libssh2_session_free ( hop. session)
@@ -452,17 +456,21 @@ internal enum LibSSH2TunnelFactory {
452456 }
453457
454458 /// Start a relay task that copies data between a channel and a socketpair fd.
455- /// All libssh2 calls are dispatched onto the provided `sessionQueue` to serialize access .
459+ /// libssh2 calls use `sessionQueue.sync` for thread safety; I/O loop runs on a concurrent queue .
456460 private static func startChannelRelay(
457461 channel: OpaquePointer ,
458462 socketFD: Int32 ,
459463 sshSocketFD: Int32 ,
460464 session: OpaquePointer ,
461465 sessionQueue: DispatchQueue
462466 ) -> Task < Void , Never > {
463- Task . detached {
467+ let relayQueue = DispatchQueue (
468+ label: " com.TablePro.ssh.hop-relay " ,
469+ qos: . utility
470+ )
471+ return Task . detached {
464472 await withCheckedContinuation { ( continuation: CheckedContinuation < Void , Never > ) in
465- sessionQueue . async {
473+ relayQueue . async {
466474 let bufferSize = 32_768
467475 let buffer = UnsafeMutablePointer< CChar> . allocate( capacity: bufferSize)
468476 defer {
@@ -471,7 +479,7 @@ internal enum LibSSH2TunnelFactory {
471479 continuation. resume ( )
472480 }
473481
474- while true {
482+ while !Task . isCancelled {
475483 var pollFDs = [
476484 pollfd ( fd: socketFD, events: Int16 ( POLLIN) , revents: 0 ) ,
477485 pollfd ( fd: sshSocketFD, events: Int16 ( POLLIN) , revents: 0 ) ,
@@ -480,24 +488,29 @@ internal enum LibSSH2TunnelFactory {
480488 let pollResult = poll ( & pollFDs, 2 , 100 )
481489 if pollResult < 0 { break }
482490
483- // Channel -> socketpair
484- let channelRead = tablepro_libssh2_channel_read ( channel, buffer, bufferSize)
485- if channelRead > 0 {
486- var totalSent = 0
487- while totalSent < Int ( channelRead) {
488- let sent = send (
489- socketFD,
490- buffer. advanced ( by: totalSent) ,
491- Int ( channelRead) - totalSent,
492- 0
493- )
494- if sent <= 0 { return }
495- totalSent += sent
491+ // Channel -> socketpair (serialized libssh2 call)
492+ if pollFDs [ 1 ] . revents & Int16 ( POLLIN) != 0 || pollResult == 0 {
493+ let channelRead : Int = sessionQueue. sync {
494+ Int ( tablepro_libssh2_channel_read ( channel, buffer, bufferSize) )
495+ }
496+ if channelRead > 0 {
497+ var totalSent = 0
498+ while totalSent < channelRead {
499+ let sent = send (
500+ socketFD,
501+ buffer. advanced ( by: totalSent) ,
502+ channelRead - totalSent,
503+ 0
504+ )
505+ if sent <= 0 { return }
506+ totalSent += sent
507+ }
508+ } else if channelRead == 0
509+ || sessionQueue. sync ( { libssh2_channel_eof ( channel) } ) != 0 {
510+ return
511+ } else if channelRead != Int ( LIBSSH2_ERROR_EAGAIN) {
512+ return
496513 }
497- } else if channelRead == 0 || libssh2_channel_eof ( channel) != 0 {
498- return
499- } else if channelRead != Int ( LIBSSH2_ERROR_EAGAIN) {
500- return
501514 }
502515
503516 // Socketpair -> channel
@@ -507,15 +520,16 @@ internal enum LibSSH2TunnelFactory {
507520
508521 var totalWritten = 0
509522 while totalWritten < Int ( socketRead) {
510- let written = tablepro_libssh2_channel_write (
511- channel,
512- buffer. advanced ( by: totalWritten) ,
513- Int ( socketRead) - totalWritten
514- )
523+ let written : Int = sessionQueue. sync {
524+ Int ( tablepro_libssh2_channel_write (
525+ channel,
526+ buffer. advanced ( by: totalWritten) ,
527+ Int ( socketRead) - totalWritten
528+ ) )
529+ }
515530 if written > 0 {
516- totalWritten += Int ( written)
531+ totalWritten += written
517532 } else if written == Int ( LIBSSH2_ERROR_EAGAIN) {
518- // Wait for socket readiness
519533 var writePollFD = pollfd (
520534 fd: sshSocketFD, events: Int16 ( POLLOUT) , revents: 0
521535 )
0 commit comments