26
"io"
27
weakrand "math/rand/v2"
28
"mime"
29
+ "net"
30
"net/http"
31
"sync"
32
"time"
36
"go.uber.org/zap/zapcore"
37
"golang.org/x/net/http/httpguts"
38
39
+ "github.com/caddyserver/caddy/v2"
40
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
41
)
42
41
-type h2ReadWriteCloser struct {
43
+type extendedConnectReadWriteCloser struct {
44
io.ReadCloser
45
http.ResponseWriter
46
}
47
46
-func (rwc h2ReadWriteCloser) Write(p []byte) (n int, err error) {
48
+func (rwc extendedConnectReadWriteCloser) Write(p []byte) (n int, err error) {
49
n, err = rwc.ResponseWriter.Write(p)
50
if err != nil {
51
return 0, err
59
return n, nil
60
}
61
60
-func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, rw http.ResponseWriter, req *http.Request, res *http.Response) {
62
+func (h *Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWriter, req *http.Request, res *http.Response, upstreamAddr string) {
63
reqUpType := upgradeType(req.Header)
64
resUpType := upgradeType(res.Header)
65
92
copyHeader(rw.Header(), res.Header)
93
normalizeWebsocketHeaders(rw.Header())
94
95
+ // Capture all h fields needed by the tunnel now, so that the Handler (h)
96
+ // is not referenced after this function returns (for HTTP/1.1 hijacked
97
+ // connections the tunnel runs in a detached goroutine).
98
+ tunnel := h.tunnelTracker
99
+ bufferSize := h.StreamBufferSize
100
+ streamTimeout := time.Duration(h.StreamTimeout)
101
+
102
+ if h.StreamDetached {
103
+ // the return value should be true as it's not hijacked yet,
104
+ // but some middleware may wrap response writers incorrectly
105
+ if !caddyhttp.DetachResponseWriterAfterHijack(rw, true) {
106
+ if c := logger.Check(zap.DebugLevel, "detaching connection failed"); c != nil {
107
+ c.Write(zap.String("tip", "check if your response writers have an Unwrap method or if already hijacked"))
108
+ }
109
+ }
110
+ }
111
+
112
var (
94
- conn io.ReadWriteCloser
95
- brw *bufio.ReadWriter
113
+ conn io.ReadWriteCloser
114
+ brw *bufio.ReadWriter
115
+ detached = h.StreamDetached
116
)
97
- // websocket over http2 or http3 if extended connect is enabled, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
98
- // TODO: once we can reliably detect backend support this, it can be removed for those backends
117
+ // websocket over http2 or http3 if extended connect is enabled,
118
+ // assuming backend doesn't support this, the request will be
119
+ // modified to http1.1 upgrade
120
+ // TODO: once we can reliably detect backend support this, it can
121
+ // be removed for those backends
122
if body, ok := caddyhttp.GetVar(req.Context(), "extended_connect_websocket_body").(io.ReadCloser); ok {
123
+ // websocket over extended connect can't be detached. rw and req.Body
124
+ // are only valid while the handler goroutine is running
125
+ detached = false
126
req.Body = body
127
rw.Header().Del("Upgrade")
128
rw.Header().Del("Connection")
130
rw.WriteHeader(http.StatusOK)
131
132
if c := logger.Check(zap.DebugLevel, "upgrading connection"); c != nil {
107
- c.Write(zap.Int("http_version", 2))
133
+ c.Write(zap.Int("http_version", req.ProtoMajor))
134
}
135
136
//nolint:bodyclose
137
flushErr := http.NewResponseController(rw).Flush()
138
if flushErr != nil {
113
- if c := h.logger.Check(zap.ErrorLevel, "failed to flush http2 websocket response"); c != nil {
139
+ if c := h.logger.Check(zap.ErrorLevel, "failed to flush extended_connect websocket response"); c != nil {
140
c.Write(zap.Error(flushErr))
141
}
142
return
143
}
118
- conn = h2ReadWriteCloser{req.Body, rw}
144
+ conn = extendedConnectReadWriteCloser{req.Body, rw}
145
// bufio is not needed, use minimal buffer
146
brw = bufio.NewReadWriter(bufio.NewReaderSize(conn, 1), bufio.NewWriterSize(conn, 1))
147
} else {
169
}
170
}
171
146
- // adopted from https://github.com/golang/go/commit/8bcf2834afdf6a1f7937390903a41518715ef6f5
147
- backConnCloseCh := make(chan struct{})
148
- go func() {
149
- // Ensure that the cancellation of a request closes the backend.
150
- // See issue https://golang.org/issue/35559.
151
- select {
152
- case <-req.Context().Done():
153
- case <-backConnCloseCh:
154
- }
155
- backConn.Close()
156
- }()
157
- defer close(backConnCloseCh)
158
-
159
- start := time.Now()
160
- defer func() {
161
- conn.Close()
162
- if c := logger.Check(zapcore.DebugLevel, "connection closed"); c != nil {
163
- c.Write(zap.Duration("duration", time.Since(start)))
164
- }
165
- }()
166
-
172
if err := brw.Flush(); err != nil {
173
if c := logger.Check(zapcore.DebugLevel, "response flush"); c != nil {
174
c.Write(zap.Error(err))
189
}
190
}
191
187
- // Ensure the hijacked client connection, and the new connection established
188
- // with the backend, are both closed in the event of a server shutdown. This
189
- // is done by registering them. We also try to gracefully close connections
190
- // we recognize as websockets.
191
- // We need to make sure the client connection messages (i.e. to upstream)
192
- // are masked, so we need to know whether the connection is considered the
193
- // server or the client side of the proxy.
192
+ // Register both connections with the tunnel tracker. We also try to
193
+ // gracefully close connections we recognize as websockets. We need to make
194
+ // sure the client connection messages (i.e. to upstream) are masked, so we
195
+ // need to know whether the connection is considered the server or the
196
+ // client side of the proxy. Note that gracefulClose must not capture h,
197
+ // since the tunnel may outlive the handler instance.
198
gracefulClose := func(conn io.ReadWriteCloser, isClient bool) func() error {
199
if isWebsocket(req) {
200
return func() error {
203
}
204
return nil
205
}
202
- deleteFrontConn := h.registerConnection(conn, gracefulClose(conn, false))
203
- deleteBackConn := h.registerConnection(backConn, gracefulClose(backConn, true))
204
- defer deleteFrontConn()
206
+ deleteFrontConn := tunnel.registerConnection(conn, gracefulClose(conn, false), detached, upstreamAddr)
207
+ deleteBackConn := tunnel.registerConnection(backConn, gracefulClose(backConn, true), detached, upstreamAddr)
208
+ if h.streamLogsSkipHandshake() {
209
+ caddyhttp.SetVar(req.Context(), caddyhttp.LogSkipVar, true)
210
+ }
211
+ repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
212
+ repl.Set("http.reverse_proxy.upgraded", true)
213
+ streamUUID, _ := repl.GetString("http.request.uuid")
214
+ streamFields := makeStreamLogFields(streamUUID)
215
+ streamLogger := h.streamLoggerForRequest(req)
216
+ streamLevel := h.streamLogLevel
217
+ finishMetrics := trackActiveStream(upstreamAddr)
218
+
219
+ start := time.Now()
220
+
221
+ if !detached {
222
+ handleUpgradeTunnel(
223
+ streamLogger,
224
+ streamLevel,
225
+ conn,
226
+ backConn,
227
+ deleteFrontConn,
228
+ deleteBackConn,
229
+ bufferSize,
230
+ streamTimeout,
231
+ start,
232
+ finishMetrics,
233
+ streamFields,
234
+ )
235
+ } else {
236
+ // start a new goroutine
237
+ go handleUpgradeTunnel(
238
+ streamLogger,
239
+ streamLevel,
240
+ conn,
241
+ backConn,
242
+ deleteFrontConn,
243
+ deleteBackConn,
244
+ bufferSize,
245
+ streamTimeout,
246
+ start,
247
+ finishMetrics,
248
+ streamFields,
249
+ )
250
+ }
251
+}
252
+
253
+// handleUpgradeTunnel returns when transfer is done.
254
+func handleUpgradeTunnel(
255
+ streamLogger *zap.Logger,
256
+ streamLevel zapcore.Level,
257
+ conn io.ReadWriteCloser,
258
+ backConn io.ReadWriteCloser,
259
+ deleteFrontConn func(),
260
+ deleteBackConn func(),
261
+ bufferSize int,
262
+ streamTimeout time.Duration,
263
+ start time.Time,
264
+ finishMetrics func(result string, duration time.Duration, toBackend int64, fromBackend int64),
265
+ streamFields []zap.Field,
266
+) {
267
defer deleteBackConn()
268
+ defer deleteFrontConn()
269
+ var (
270
+ wg sync.WaitGroup
271
+ toBackend int64
272
+ fromBackend int64
273
+ result string
274
+ )
275
276
+ // when a stream timeout is encountered, no error will be read from errc
277
+ // a buffer size of 2 will allow both the read and write goroutines to
278
+ // send the error and exit
279
+ // see: https://github.com/caddyserver/caddy/issues/7418
280
+ errc := make(chan error, 2)
281
spc := switchProtocolCopier{
282
user: conn,
283
backend: backConn,
210
- wg: wg,
211
- bufferSize: h.StreamBufferSize,
284
+ wg: &wg,
285
+ bufferSize: bufferSize,
286
+ sent: &toBackend,
287
+ received: &fromBackend,
288
}
289
+ wg.Add(2)
290
214
- // setup the timeout if requested
291
var timeoutc <-chan time.Time
216
- if h.StreamTimeout > 0 {
217
- timer := time.NewTimer(time.Duration(h.StreamTimeout))
292
+ if streamTimeout > 0 {
293
+ timer := time.NewTimer(streamTimeout)
294
defer timer.Stop()
295
timeoutc = timer.C
296
}
297
222
- // when a stream timeout is encountered, no error will be read from errc
223
- // a buffer size of 2 will allow both the read and write goroutines to send the error and exit
224
- // see: https://github.com/caddyserver/caddy/issues/7418
225
- errc := make(chan error, 2)
226
- wg.Add(2)
298
go spc.copyToBackend(errc)
299
go spc.copyFromBackend(errc)
229
-
300
// Wait for both directions to finish. A clean EOF in one direction is a
301
// half-close of that direction only, not the end of the tunnel: the copier
302
// propagates it to the destination with CloseWrite and reports nil, so the
318
//
319
// See https://github.com/caddyserver/caddy/issues/8026, and the same class
320
// fixed upstream in net/http/httputil (https://go.dev/issue/35892).
321
+ result = "closed"
322
+copyLoop:
323
for range 2 {
324
select {
325
case err := <-errc:
326
if err != nil {
255
- if c := logger.Check(zapcore.DebugLevel, "streaming error"); c != nil {
327
+ result = classifyStreamResult(err)
328
+ if c := streamLogger.Check(streamLevel, "streaming error"); c != nil && !errors.Is(err, errCopyDone) {
329
c.Write(zap.Error(err))
330
}
258
- return
331
+ break copyLoop
332
}
260
- case time := <-timeoutc:
261
- if c := logger.Check(zapcore.DebugLevel, "stream timed out"); c != nil {
262
- c.Write(zap.Time("timeout", time))
333
+ case t := <-timeoutc:
334
+ result = "timeout"
335
+ if c := streamLogger.Check(streamLevel, "stream timed out"); c != nil {
336
+ c.Write(zap.Time("timeout", t))
337
}
264
- return
338
+ break copyLoop
339
}
340
}
341
+
342
+ // Close both ends to unblock the still-running copy goroutine,
343
+ // then wait for it so byte counts are final before metrics/logging.
344
+ conn.Close()
345
+ backConn.Close()
346
+ wg.Wait()
347
+
348
+ finishMetrics(result, time.Since(start), toBackend, fromBackend)
349
+ if c := streamLogger.Check(streamLevel, "connection closed"); c != nil {
350
+ fields := append([]zap.Field{}, streamFields...)
351
+ fields = append(fields,
352
+ zap.Duration("duration", time.Since(start)),
353
+ zap.Int64("bytes_to_backend", toBackend),
354
+ zap.Int64("bytes_from_backend", fromBackend),
355
+ )
356
+ c.Write(fields...)
357
+ }
358
+}
359
+
360
+func classifyStreamResult(err error) string {
361
+ if err == nil ||
362
+ errors.Is(err, errCopyDone) ||
363
+ errors.Is(err, io.EOF) ||
364
+ errors.Is(err, net.ErrClosed) ||
365
+ errors.Is(err, context.Canceled) {
366
+ return "closed"
367
+ }
368
+ return "error"
369
+}
370
+
371
+func makeStreamLogFields(streamUUID string) []zap.Field {
372
+ fields := make([]zap.Field, 0, 1)
373
+ if streamUUID != "" {
374
+ fields = append(fields, zap.String("uuid", streamUUID))
375
+ }
376
+ return fields
377
}
378
379
// flushInterval returns the p.FlushInterval value, conditionally
519
}
520
}
521
412
-// registerConnection holds onto conn so it can be closed in the event
413
-// of a server shutdown. This is useful because hijacked connections or
414
-// connections dialed to backends don't close when server is shut down.
415
-// The caller should call the returned delete() function when the
416
-// connection is done to remove it from memory.
417
-func (h *Handler) registerConnection(conn io.ReadWriteCloser, gracefulClose func() error) (del func()) {
418
- h.connectionsMu.Lock()
419
- h.connections[conn] = openConnection{conn, gracefulClose}
420
- h.connectionsMu.Unlock()
522
+// openConnection maps an open connection to an optional function for graceful
523
+// close and records which upstream address the connection is proxying to.
524
+// Also tracks whether the connection is detached, which means it should only be
525
+// closed when the upstream is removed from the config, not on every reload.
526
+type openConnection struct {
527
+ conn io.ReadWriteCloser
528
+ gracefulClose func() error
529
+ detached bool
530
+ upstream string
531
+}
532
+
533
+// tunnelTracker tracks hijacked/upgraded connections for selective cleanup.
534
+// This exists to detach the lifecycle of streaming connections from the proxy
535
+// Handler and config, since we typically want them to survive past config reloads.
536
+// It also allows for selective connection cleanup based on their attachment status.
537
+type tunnelTracker struct {
538
+ connections map[io.ReadWriteCloser]openConnection
539
+ closeTimer *time.Timer
540
+ closeDelay time.Duration
541
+ stopped bool
542
+ mu sync.Mutex
543
+ logger *zap.Logger
544
+}
545
+
546
+func newTunnelTracker(logger *zap.Logger, closeDelay time.Duration) *tunnelTracker {
547
+ return &tunnelTracker{
548
+ connections: make(map[io.ReadWriteCloser]openConnection),
549
+ closeDelay: closeDelay,
550
+ logger: logger,
551
+ }
552
+}
553
+
554
+// registerConnection stores conn in the tracking map. The caller must invoke
555
+// the returned del func when the connection is done.
556
+func (ts *tunnelTracker) registerConnection(conn io.ReadWriteCloser, gracefulClose func() error, detached bool, upstream string) (del func()) {
557
+ ts.mu.Lock()
558
+ ts.connections[conn] = openConnection{conn, gracefulClose, detached, upstream}
559
+ ts.mu.Unlock()
560
return func() {
422
- h.connectionsMu.Lock()
423
- delete(h.connections, conn)
424
- // if there is no connection left before the connections close timer fires
425
- if len(h.connections) == 0 && h.connectionsCloseTimer != nil {
426
- // we release the timer that holds the reference to Handler
427
- if (*h.connectionsCloseTimer).Stop() {
428
- h.logger.Debug("stopped streaming connections close timer - all connections are already closed")
561
+ ts.mu.Lock()
562
+ delete(ts.connections, conn)
563
+ empty := len(ts.connections) == 0 && ts.stopped
564
+ if empty {
565
+ if ts.closeTimer != nil {
566
+ if ts.closeTimer.Stop() {
567
+ ts.logger.Debug("stopped streaming connections close timer - all connections are already closed")
568
+ }
569
+ ts.closeTimer = nil
570
}
430
- h.connectionsCloseTimer = nil
571
}
432
- h.connectionsMu.Unlock()
572
+ ts.mu.Unlock()
573
+ if empty {
574
+ unregisterDetachedTunnelTrackers(ts)
575
+ }
576
}
577
}
578
436
-// closeConnections immediately closes all hijacked connections (both to client and backend).
437
-func (h *Handler) closeConnections() error {
579
+// closeAttachedConnections closes all tracked attached connections.
580
+func (ts *tunnelTracker) closeAttachedConnections() error {
581
var err error
439
- h.connectionsMu.Lock()
440
- defer h.connectionsMu.Unlock()
441
-
442
- for _, oc := range h.connections {
582
+ ts.mu.Lock()
583
+ defer ts.mu.Unlock()
584
+ for _, oc := range ts.connections {
585
+ // detached connections are only closed when the upstream is gone from the config
586
+ if oc.detached {
587
+ continue
588
+ }
589
if oc.gracefulClose != nil {
444
- // this is potentially blocking while we have the lock on the connections
445
- // map, but that should be OK since the server has in theory shut down
446
- // and we are no longer using the connections map
447
- gracefulErr := oc.gracefulClose()
448
- if gracefulErr != nil && err == nil {
590
+ if gracefulErr := oc.gracefulClose(); gracefulErr != nil && err == nil {
591
err = gracefulErr
592
}
593
}
452
- closeErr := oc.conn.Close()
453
- if closeErr != nil && err == nil {
594
+ if closeErr := oc.conn.Close(); closeErr != nil && err == nil {
595
err = closeErr
596
}
597
}
598
return err
599
}
600
460
-// cleanupConnections closes hijacked connections.
461
-// Depending on the value of StreamCloseDelay it does that either immediately
462
-// or sets up a timer that will do that later.
463
-func (h *Handler) cleanupConnections() error {
464
- if h.StreamCloseDelay == 0 {
465
- return h.closeConnections()
466
- }
467
-
468
- h.connectionsMu.Lock()
469
- defer h.connectionsMu.Unlock()
470
- // the handler is shut down, no new connection can appear,
471
- // so we can skip setting up the timer when there are no connections
472
- if len(h.connections) > 0 {
473
- delay := time.Duration(h.StreamCloseDelay)
474
- h.connectionsCloseTimer = time.AfterFunc(delay, func() {
475
- if c := h.logger.Check(zapcore.DebugLevel, "closing streaming connections after delay"); c != nil {
476
- c.Write(zap.Duration("delay", delay))
477
- }
478
- err := h.closeConnections()
479
- if err != nil {
480
- if c := h.logger.Check(zapcore.ErrorLevel, "failed to closed connections after delay"); c != nil {
481
- c.Write(
482
- zap.Error(err),
483
- zap.Duration("delay", delay),
484
- )
485
- }
486
- }
487
- })
601
+// cleanupAttachedConnections closes upgraded attached connections.
602
+// Depending on closeDelay it does that either immediately or after a timer.
603
+func (ts *tunnelTracker) cleanupAttachedConnections() error {
604
+ ts.mu.Lock()
605
+ // Mark the tracker stopped before upstream-removal notifications, even
606
+ // when attached connections will be closed after a delay.
607
+ ts.stopped = true
608
+ if len(ts.connections) == 0 {
609
+ ts.mu.Unlock()
610
+ unregisterDetachedTunnelTrackers(ts)
611
+ return nil
612
+ }
613
+ if ts.closeDelay == 0 {
614
+ ts.mu.Unlock()
615
+ return ts.closeAttachedConnections()
616
}
617
+
618
+ defer ts.mu.Unlock()
619
+ delay := ts.closeDelay
620
+ ts.closeTimer = time.AfterFunc(delay, func() {
621
+ if c := ts.logger.Check(zapcore.DebugLevel, "closing streaming connections after delay"); c != nil {
622
+ c.Write(zap.Duration("delay", delay))
623
+ }
624
+ err := ts.closeAttachedConnections()
625
+ if err != nil {
626
+ if c := ts.logger.Check(zapcore.ErrorLevel, "failed to close connections after delay"); c != nil {
627
+ c.Write(
628
+ zap.Error(err),
629
+ zap.Duration("delay", delay),
630
+ )
631
+ }
632
+ }
633
+ })
634
return nil
635
}
636
746
httpguts.HeaderValuesContainsToken(r.Header["Upgrade"], "websocket")
747
}
748
604
-// openConnection maps an open connection to
605
-// an optional function for graceful close.
606
-type openConnection struct {
607
- conn io.ReadWriteCloser
608
- gracefulClose func() error
749
+// closeConnectionsForUpstream closes detached connections to the given
750
+// upstream on stopped trackers. Attached connections keep their close delay.
751
+func (ts *tunnelTracker) closeConnectionsForUpstream(addr string) error {
752
+ var err error
753
+ ts.mu.Lock()
754
+ defer ts.mu.Unlock()
755
+ if !ts.stopped {
756
+ return nil
757
+ }
758
+ for _, oc := range ts.connections {
759
+ if !oc.detached || oc.upstream != addr {
760
+ continue
761
+ }
762
+ if oc.gracefulClose != nil {
763
+ if gracefulErr := oc.gracefulClose(); gracefulErr != nil && err == nil {
764
+ err = gracefulErr
765
+ }
766
+ }
767
+ if closeErr := oc.conn.Close(); closeErr != nil && err == nil {
768
+ err = closeErr
769
+ }
770
+ }
771
+ return err
772
}
773
774
type maxLatencyWriter struct {
839
user, backend io.ReadWriteCloser
840
wg *sync.WaitGroup
841
bufferSize int
842
+ // sent and received accumulate byte counts for each direction.
843
+ // They are written before wg.Done() and read after wg.Wait(), so no
844
+ // additional synchronization is needed beyond the WaitGroup barrier.
845
+ sent *int64 // bytes copied to backend; must be non-nil
846
+ received *int64 // bytes copied from backend; must be non-nil
847
}
848
849
// errCopyDone is reported by a direction that ended at a clean EOF on a
853
854
func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
855
defer c.wg.Done()
688
- if _, err := io.CopyBuffer(c.user, c.backend, c.buffer()); err != nil {
856
+ n, err := io.CopyBuffer(c.user, c.backend, c.buffer())
857
+ *c.received = n
858
+ if err != nil {
859
errc <- err
860
return
861
}
871
872
func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
873
defer c.wg.Done()
704
- if _, err := io.CopyBuffer(c.backend, c.user, c.buffer()); err != nil {
874
+ n, err := io.CopyBuffer(c.backend, c.user, c.buffer())
875
+ *c.sent = n
876
+ if err != nil {
877
errc <- err
878
return
879
}