@@ -541,22 +541,48 @@ func newSSHProxyCmd(opts *Options) *cobra.Command {
541541 if err != nil {
542542 return err
543543 }
544- pfKeepAliveCtx , pfKeepAliveCancel := context .WithCancel (context .Background ())
545- startPortForwardKeepalive (pfKeepAliveCtx , usedLocalPort , 10 * time .Second , nil )
546- defer pfKeepAliveCancel ()
547544 if cancelPF != nil {
548545 defer cancelPF ()
549546 }
547+
550548 slog .Debug ("ssh-proxy dialing local port-forward" , "port" , usedLocalPort )
551549 conn , err := waitDialLocal (usedLocalPort , 10 * time .Second )
552550 if err != nil {
553551 slog .Debug ("ssh-proxy dial failed" , "error" , err )
554552 return err
555553 }
556554 defer conn .Close ()
555+ startTime := time .Now ()
556+
557+ // Layer 1: TCP keepalive tuning (5s period, KEEPCNT=2 where supported)
558+ setTCPKeepAliveProxyTuning (conn )
559+
560+ // Layer 3: Port-forward keepalive probe with onDegraded wired up
561+ pfKeepAliveCtx , pfKeepAliveCancel := context .WithCancel (cmd .Context ())
562+ defer pfKeepAliveCancel ()
563+ startPortForwardKeepalive (pfKeepAliveCtx , usedLocalPort , 10 * time .Second , func () {
564+ slog .Info ("ssh-proxy port-forward degraded, closing connection" ,
565+ "port" , usedLocalPort ,
566+ "uptime" , time .Since (startTime ).Round (time .Second ),
567+ )
568+ _ = conn .Close ()
569+ })
570+
557571 slog .Debug ("ssh-proxy connection established" , "localAddr" , conn .LocalAddr (), "remoteAddr" , conn .RemoteAddr ())
558572
559- var wg sync.WaitGroup
573+ // Close conn on context cancellation (SIGINT/SIGTERM) to unblock io.Copy goroutines
574+ go func () {
575+ <- cmd .Context ().Done ()
576+ _ = conn .Close ()
577+ }()
578+
579+ // Layer 2: Data flow monitor
580+ var lastData atomic.Int64
581+ lastData .Store (time .Now ().UnixNano ())
582+ watchdogCtx , watchdogCancel := context .WithCancel (cmd .Context ())
583+ defer watchdogCancel ()
584+ go proxyDataFlowWatchdog (watchdogCtx , conn , & lastData , 5 * time .Second , 15 * time .Second )
585+
560586 var copyErr error
561587 var once sync.Once
562588 done := make (chan struct {})
@@ -567,11 +593,9 @@ func newSSHProxyCmd(opts *Options) *cobra.Command {
567593 slog .Debug ("ssh-proxy copy error" , "error" , err )
568594 once .Do (func () { copyErr = err })
569595 }
570- wg .Add (2 )
571596 go func () {
572- defer wg .Done ()
573597 slog .Debug ("ssh-proxy starting copy: stdin -> conn" )
574- _ , err := io . Copy (conn , os .Stdin )
598+ _ , err := monitoredCopy (conn , os .Stdin , & lastData )
575599 setErr (err )
576600 slog .Debug ("ssh-proxy finished copy: stdin -> conn" )
577601 select {
@@ -581,17 +605,23 @@ func newSSHProxyCmd(opts *Options) *cobra.Command {
581605 }
582606 }()
583607 go func () {
584- defer wg .Done ()
585608 slog .Debug ("ssh-proxy starting copy: conn -> stdout" )
586- _ , err := io . Copy (os .Stdout , conn )
609+ _ , err := monitoredCopy (os .Stdout , conn , & lastData )
587610 setErr (err )
588611 slog .Debug ("ssh-proxy finished copy: conn -> stdout" )
589612 close (done )
590613 _ = conn .Close ()
591614 }()
592615 <- done
593- slog .Debug ("ssh-proxy session finished" , "error" , copyErr )
594- return copyErr
616+ watchdogCancel ()
617+ slog .Info ("ssh-proxy session finished" ,
618+ "error" , copyErr ,
619+ "uptime" , time .Since (startTime ).Round (time .Second ),
620+ )
621+ if copyErr != nil {
622+ return fmt .Errorf ("%w: %v" , ErrProxyHealthDisconnect , copyErr )
623+ }
624+ return nil
595625 })
596626 },
597627 }
0 commit comments