diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/proxy/proxy.go | 39 |
1 files changed, 36 insertions, 3 deletions
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 08b8551..216e947 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -45,6 +45,24 @@ import ( // upstream exchange, once dialing has already succeeded. const upstreamTimeout = 60 * time.Second +// clientHeaderTimeout bounds how long a client connection can sit +// sending request headers (or a TLS ClientHello, on the CONNECT-tunnel +// leg) before mitmux gives up on it - a slow-loris style connection +// that opens and then trickles bytes (or never sends a ClientHello at +// all) would otherwise hold a connection and its goroutine open +// indefinitely, with nothing else in the codebase bounding it. Narrow +// on purpose: this only covers the pre-body phase, not overall request +// duration - a legitimately slow multi-minute upload/download must +// still work, so this is deliberately not a blanket ReadTimeout/ +// WriteTimeout on the whole connection. +const clientHeaderTimeout = 30 * time.Second + +// clientIdleTimeout bounds how long a keep-alive client connection can +// sit idle between requests before mitmux closes it - cleans up +// abandoned idle connections without affecting any connection that's +// actively mid-transfer. +const clientIdleTimeout = 120 * time.Second + // hopByHopHeaders are stripped before forwarding a request or response, // per RFC 7230 6.1 - they are meaningful only between a client and its // immediate next hop, not end-to-end. @@ -92,8 +110,10 @@ type Server struct { func New(addrs []string, root *ca.CA, db *store.Store, upstreamProxy string) *Server { s := &Server{Addrs: addrs, ca: root, store: db, UpstreamProxy: upstreamProxy} s.server = &http.Server{ - Handler: http.HandlerFunc(s.handle), - ConnContext: withClientTee, + Handler: http.HandlerFunc(s.handle), + ConnContext: withClientTee, + ReadHeaderTimeout: clientHeaderTimeout, + IdleTimeout: clientIdleTimeout, } return s } @@ -190,11 +210,19 @@ func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) { NextProtos: []string{http2.NextProtoTLS, "http/1.1"}, MinVersion: tls.VersionTLS12, }) + // Bounded the same way the upstream leg already is (see forward's + // conn.SetDeadline): without this, a client that completes CONNECT + // and then never sends a ClientHello at all holds the connection and + // its goroutine open indefinitely. Cleared after a successful + // handshake - the request/response phase that follows has no + // business inheriting a short handshake-only deadline. + client.SetDeadline(time.Now().Add(clientHeaderTimeout)) if err := clientTLS.Handshake(); err != nil { log.Printf("mitm handshake with client for %s: %v", hostname, err) client.Close() return } + client.SetDeadline(time.Time{}) dial := func(ctx context.Context) (net.Conn, string, error) { return dialUpstreamTLS(ctx, hostPort, hostname, s.UpstreamProxy) @@ -208,7 +236,12 @@ func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) { return } - h1 := &http.Server{Handler: handler, ConnContext: withClientTee} + h1 := &http.Server{ + Handler: handler, + ConnContext: withClientTee, + ReadHeaderTimeout: clientHeaderTimeout, + IdleTimeout: clientIdleTimeout, + } err = h1.Serve(newSingleConnListener(clientTLS)) if err != nil && !errors.Is(err, io.EOF) { log.Printf("h1 serve for %s: %v", hostname, err) |