Skip to content

Commit 591b02c

Browse files
blink-so[bot]f0ssel
andcommitted
Fix goroutine leaks in CONNECT connection handling
This fixes three goroutine leaks that were causing CPU and memory usage to build up over time: 1. streamRequestToTarget: Orphaned bidirectional copy goroutines that never terminated properly when connections closed early 2. handleConnectStreaming: Similar issue with bidirectional tunnel goroutines that could hang indefinitely Changes made: - Use errgroup to properly manage and synchronize goroutine lifecycle - Add connection deadlines (5 min) to prevent indefinite blocking on io.Copy - Close write side of connections after copy completes to signal EOF - Add context cancellation monitoring to force cleanup if needed - Improved error handling to ignore expected EOF/ErrClosed errors Co-authored-by: f0ssel <[email protected]>
1 parent 55a44f2 commit 591b02c

File tree

3 files changed

+81
-20
lines changed

3 files changed

+81
-20
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
module github.com/coder/boundary
22

3-
go 1.24
3+
go 1.24.0
44

55
require (
66
github.com/coder/serpent v0.10.0
77
github.com/stretchr/testify v1.8.4
8+
golang.org/x/sync v0.17.0
89
)
910

1011
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
101101
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
102102
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
103103
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
104+
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
105+
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
104106
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
105107
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
106108
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

proxy/proxy.go

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package proxy
22

33
import (
44
"bufio"
5+
"context"
56
"crypto/tls"
67
"errors"
78
"fmt"
@@ -13,9 +14,11 @@ import (
1314
"strings"
1415
"sync"
1516
"sync/atomic"
17+
"time"
1618

1719
"github.com/coder/boundary/audit"
1820
"github.com/coder/boundary/rules"
21+
"golang.org/x/sync/errgroup"
1922
)
2023

2124
// Server handles HTTP and HTTPS requests with rule-based filtering
@@ -658,6 +661,11 @@ func (p *Server) streamRequestToTarget(clientConn *tls.Conn, bufReader *bufio.Re
658661
}
659662
}()
660663

664+
// Set connection deadlines to prevent indefinite blocking
665+
deadline := time.Now().Add(5 * time.Minute)
666+
_ = clientConn.SetDeadline(deadline)
667+
_ = targetConn.SetDeadline(deadline)
668+
661669
// Send HTTP request headers to target
662670
reqLine := fmt.Sprintf("%s %s %s\r\n", req.Method, req.URL.RequestURI(), req.Proto)
663671
_, err = targetConn.Write([]byte(reqLine))
@@ -680,20 +688,40 @@ func (p *Server) streamRequestToTarget(clientConn *tls.Conn, bufReader *bufio.Re
680688
return fmt.Errorf("failed to write headers to target: %v", err)
681689
}
682690

683-
// Stream request body and response bidirectionally
684-
go func() {
685-
// Stream request body: client -> target
691+
// Use errgroup to manage bidirectional streaming and ensure cleanup
692+
g, ctx := errgroup.WithContext(context.Background())
693+
694+
// Stream request body: client -> target
695+
g.Go(func() error {
686696
_, err := io.Copy(targetConn, bufReader)
687-
if err != nil {
688-
p.logger.Error("Error copying request body to target", "error", err)
697+
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
698+
p.logger.Debug("Error copying request body to target", "error", err)
689699
}
690-
}()
700+
// Close write side to signal EOF to target
701+
_ = targetConn.CloseWrite()
702+
return nil
703+
})
691704

692705
// Stream response: target -> client
693-
_, err = io.Copy(clientConn, targetConn)
694-
if err != nil {
695-
p.logger.Error("Error copying response from target to client", "error", err)
696-
}
706+
g.Go(func() error {
707+
_, err := io.Copy(clientConn, targetConn)
708+
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
709+
p.logger.Debug("Error copying response from target to client", "error", err)
710+
}
711+
return nil
712+
})
713+
714+
// Monitor context cancellation to ensure both goroutines exit
715+
g.Go(func() error {
716+
<-ctx.Done()
717+
// Force close connections to unblock any hanging io.Copy
718+
_ = clientConn.Close()
719+
_ = targetConn.Close()
720+
return nil
721+
})
722+
723+
// Wait for all goroutines to complete
724+
_ = g.Wait()
697725

698726
return nil
699727
}
@@ -729,16 +757,46 @@ func (p *Server) handleConnectStreaming(tlsConn *tls.Conn, req *http.Request, ho
729757
}
730758
defer func() { _ = targetConn.Close() }()
731759

732-
// Bidirectional copy
733-
go func() {
760+
// Set connection deadlines to prevent indefinite blocking
761+
deadline := time.Now().Add(5 * time.Minute)
762+
_ = tlsConn.SetDeadline(deadline)
763+
_ = targetConn.SetDeadline(deadline)
764+
765+
// Use errgroup for bidirectional copy with proper cleanup
766+
g, ctx := errgroup.WithContext(context.Background())
767+
768+
// Client to target
769+
g.Go(func() error {
734770
_, err := io.Copy(targetConn, tlsConn)
735-
if err != nil {
736-
p.logger.Error("Error copying from client to target", "error", err)
771+
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
772+
p.logger.Debug("Error copying from client to target", "error", err)
737773
}
738-
}()
739-
_, err = io.Copy(tlsConn, targetConn)
740-
if err != nil {
741-
p.logger.Error("Error copying from target to client", "error", err)
742-
}
774+
// Close write side to signal EOF
775+
if tc, ok := targetConn.(*net.TCPConn); ok {
776+
_ = tc.CloseWrite()
777+
}
778+
return nil
779+
})
780+
781+
// Target to client
782+
g.Go(func() error {
783+
_, err := io.Copy(tlsConn, targetConn)
784+
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
785+
p.logger.Debug("Error copying from target to client", "error", err)
786+
}
787+
return nil
788+
})
789+
790+
// Monitor context cancellation to ensure cleanup
791+
g.Go(func() error {
792+
<-ctx.Done()
793+
// Force close connections to unblock any hanging io.Copy
794+
_ = tlsConn.Close()
795+
_ = targetConn.Close()
796+
return nil
797+
})
798+
799+
// Wait for all goroutines to complete
800+
_ = g.Wait()
743801
p.logger.Debug("CONNECT tunnel closed", "hostname", hostname)
744802
}

0 commit comments

Comments
 (0)