diff --git a/cmd/server-main.go b/cmd/server-main.go index 48ed0f87d..c8a3a19ca 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -901,6 +901,8 @@ func serverMain(ctx *cli.Context) { close(globalGridStart) close(globalLockGridStart) + // The HTTP/1 listener preserves absolute header deadlines and renews the + // body read/write idle limits, so transfers may outlast IdleTimeout. httpServer := xhttp.NewServer(getServerListenAddrs()). UseHandler(setCriticalErrorHandler(corsHandler(handler))). UseTLSConfig(newTLSConfig(getCert)). diff --git a/internal/deadlineconn/deadlineconn.go b/internal/deadlineconn/deadlineconn.go index 95bb43eff..5fa5a1403 100644 --- a/internal/deadlineconn/deadlineconn.go +++ b/internal/deadlineconn/deadlineconn.go @@ -34,6 +34,8 @@ type DeadlineConn struct { net.Conn readDeadline time.Duration // sets the read deadline on a connection. readSetAt time.Time + readExplicit time.Time // last deadline requested by the caller. + readDeadlineStrict bool // idle renewal must not extend readExplicit. writeDeadline time.Duration // sets the write deadline on a connection. writeSetAt time.Time abortReads, abortWrites atomic.Bool // A deadline was set to indicate caller wanted the conn to time out. @@ -59,17 +61,31 @@ func (c *DeadlineConn) setReadDeadline() { c.mu.Lock() defer c.mu.Unlock() - if c.abortReads.Load() { + if c.abortReads.Load() || c.infReads.Load() { return } now := time.Now() if now.Sub(c.readSetAt) > updateInterval { - c.Conn.SetReadDeadline(now.Add(c.readDeadline + updateInterval)) + deadline := now.Add(c.readDeadline + updateInterval) + if c.readDeadlineStrict && !c.readExplicit.IsZero() && c.readExplicit.Before(deadline) { + deadline = c.readExplicit + } + c.Conn.SetReadDeadline(deadline) c.readSetAt = now } } +// SetReadDeadlineStrict controls whether idle renewal may extend a deadline set +// by SetReadDeadline or SetDeadline. The default is false. Explicit zero and +// past deadlines retain their disable/cancel semantics in either mode. +func (c *DeadlineConn) SetReadDeadlineStrict(strict bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.readDeadlineStrict = strict + c.readSetAt = time.Time{} +} + func (c *DeadlineConn) setWriteDeadline() { // Do not set a Write deadline, if upstream wants to cancel all reads. if c.writeDeadline <= 0 || c.abortWrites.Load() || c.infWrites.Load() { @@ -115,6 +131,7 @@ func (c *DeadlineConn) SetDeadline(t time.Time) error { defer c.mu.Unlock() c.readSetAt = time.Time{} + c.readExplicit = t c.writeSetAt = time.Time{} c.abortReads.Store(!t.IsZero() && time.Until(t) < 0) c.abortWrites.Store(!t.IsZero() && time.Until(t) < 0) @@ -132,6 +149,7 @@ func (c *DeadlineConn) SetReadDeadline(t time.Time) error { c.abortReads.Store(!t.IsZero() && time.Until(t) < 0) c.infReads.Store(t.IsZero()) c.readSetAt = time.Time{} + c.readExplicit = t return c.Conn.SetReadDeadline(t) } diff --git a/internal/http/listener.go b/internal/http/listener.go index bc6de3af9..14d34f6ea 100644 --- a/internal/http/listener.go +++ b/internal/http/listener.go @@ -70,7 +70,10 @@ func (listener *httpListener) Accept() (conn net.Conn, err error) { if result.err != nil { return nil, result.err } - return deadlineconn.New(result.conn).WithReadDeadline(listener.opts.IdleTimeout).WithWriteDeadline(listener.opts.IdleTimeout), result.err + conn := deadlineconn.New(result.conn).WithReadDeadline(listener.opts.IdleTimeout).WithWriteDeadline(listener.opts.IdleTimeout) + // Server.Init switches to rolling reads only after HTTP/1 headers are read. + conn.SetReadDeadlineStrict(true) + return conn, nil case <-listener.ctxDoneCh: } return nil, syscall.EINVAL diff --git a/internal/http/server.go b/internal/http/server.go index 2934fda6c..d9c19a33f 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -29,6 +29,7 @@ import ( "time" "github.com/dustin/go-humanize" + "github.com/minio/minio/internal/deadlineconn" ) var ( @@ -123,6 +124,32 @@ func (srv *Server) Init(listenCtx context.Context, listenErrCallback func(listen srv.listener = listener srv.listenerMutex.Unlock() + connState := srv.ConnState + srv.ConnState = func(conn net.Conn, state http.ConnState) { + raw := conn + if tlsConn, ok := raw.(*tls.Conn); ok { + if tlsConn.ConnectionState().NegotiatedProtocol == "h2" { + // HTTP/2 owns its stream deadlines; do not change the connection. + raw = nil + } else { + raw = tlsConn.NetConn() + } + } + if dc, ok := raw.(*deadlineconn.DeadlineConn); ok { + switch state { + case http.StateNew, http.StateIdle: + dc.SetReadDeadlineStrict(true) + case http.StateActive: + // net/http has finished reading the headers, including buffered + // requests. Keep ReadTimeout as a rolling idle limit for uploads. + dc.SetReadDeadlineStrict(false) + } + } + if connState != nil { + connState(conn, state) + } + } + var l net.Listener = listener if tlsConfig != nil { l = tls.NewListener(listener, tlsConfig)