fix: close hijacks racing with shutdown

This commit is contained in:
MiMoCode
2026-07-10 19:43:41 +08:00
parent 62675e400b
commit f8abacf2e5
2 changed files with 89 additions and 0 deletions
+9
View File
@@ -33,6 +33,8 @@ type Handler struct {
connMu sync.Mutex
conns map[net.Conn]struct{}
connChanged chan struct{}
closing bool
closed bool
}
// Options 控制反代连接上游时的网络行为。
@@ -253,7 +255,9 @@ func (h *Handler) serveRequestBodyError(cw *captureWriter, r *http.Request, st *
func (h *Handler) Shutdown(ctx context.Context) error {
for {
h.connMu.Lock()
h.closing = true
if len(h.conns) == 0 {
h.closed = true
h.connMu.Unlock()
return nil
}
@@ -284,6 +288,11 @@ func (h *Handler) trackConn(conn net.Conn) net.Conn {
h.connMu.Unlock()
}
h.connMu.Lock()
if h.closing || h.closed {
h.connMu.Unlock()
_ = conn.Close()
return conn
}
h.conns[tracked] = struct{}{}
close(h.connChanged)
h.connChanged = make(chan struct{})
+80
View File
@@ -11,6 +11,7 @@ import (
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"
"time"
@@ -45,6 +46,30 @@ type failingWriteConn struct{ net.Conn }
func (failingWriteConn) Write([]byte) (int, error) { return 0, errors.New("handshake write failed") }
type closeNotifyConn struct {
net.Conn
closed chan struct{}
once sync.Once
}
func (c *closeNotifyConn) Close() error {
err := c.Conn.Close()
c.once.Do(func() { close(c.closed) })
return err
}
type blockingHijackResponseWriter struct {
*hijackResponseWriter
hijacked chan struct{}
release chan struct{}
}
func (w *blockingHijackResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
close(w.hijacked)
<-w.release
return w.hijackResponseWriter.Hijack()
}
// fakeUpstream 模拟一个最简 WebSocket 升级:
// 收到 GET + Upgrade: websocket 后回 101,然后做字节回声直到对端关闭。
func fakeUpstream(t *testing.T) *httptest.Server {
@@ -161,6 +186,61 @@ func TestWebSocketHandshakeCapturedAndShutdownClosesConnection(t *testing.T) {
}
}
func TestShutdownClosesConnectionHijackedBeforeRegistration(t *testing.T) {
upstream := fakeUpstream(t)
defer upstream.Close()
u, _ := url.Parse(upstream.URL)
filter, err := config.NewFilter(config.FilterDisabled, nil)
if err != nil {
t.Fatal(err)
}
h := proxy.New(u, filter, noopSubmitter{}, 1024)
downstream, peer := net.Pipe()
defer peer.Close()
closed := make(chan struct{})
conn := &closeNotifyConn{Conn: downstream, closed: closed}
w := &blockingHijackResponseWriter{
hijackResponseWriter: &hijackResponseWriter{header: make(http.Header), conn: conn},
hijacked: make(chan struct{}),
release: make(chan struct{}),
}
req := httptest.NewRequest(http.MethodGet, "http://proxy.test/v1/realtime", nil)
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "Upgrade")
serveDone := make(chan struct{})
var releaseOnce sync.Once
releaseHijack := func() { releaseOnce.Do(func() { close(w.release) }) }
go func() {
h.ServeHTTP(w, req)
close(serveDone)
}()
defer func() {
releaseHijack()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
_ = h.Shutdown(ctx)
<-serveDone
}()
select {
case <-w.hijacked:
case <-time.After(time.Second):
t.Fatal("downstream connection was not hijacked")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := h.Shutdown(ctx); err != nil {
t.Fatal(err)
}
releaseHijack()
select {
case <-closed:
case <-time.After(100 * time.Millisecond):
t.Fatal("connection hijacked during Shutdown remained open")
}
}
func TestWebSocketHandshakeWriteFailureIsNotCaptured(t *testing.T) {
upstream := fakeUpstream(t)
defer upstream.Close()