Files
Kévin Dunglas 0cf03d32f7 caddyhttp: mitigate slowloris via idle read/write deadlines (#7913)
* caddyhttp: mitigate slowloris via idle read/write deadlines

ReadTimeout and WriteTimeout previously applied as a single hard
deadline over the whole body/response through http.Server, so any
non-zero value also killed large transfers from legitimately slow
clients. Reset the deadline on every successful read/write instead
(via http.ResponseController), and give both a sane 1m default now
that doing so no longer penalizes slow-but-progressing clients.

* caddyhttp: split idle read/write timeouts from the existing hard ones

Reworking ReadTimeout/WriteTimeout's own semantics was an unwanted
behavior change for existing configs relying on the hard deadline.
Leave them untouched and add ReadIdleTimeout/WriteIdleTimeout instead,
reset on every successful read/write; both default to 1m since,
being new, no existing config could have depended on a different
value. Combining an idle timeout with its hard counterpart now gives
the same base+ceiling shape as Apache's mod_reqtimeout, for free.

* caddyhttp: cap idle-reset deadlines at the hard timeout ceiling

Deadlines are a single absolute value on the connection, not a min of
several: ReadTimeout/WriteTimeout's own hard deadline, set once by
net/http before the handler runs, was silently getting overwritten by
the first idle-reset Read/Write, voiding it entirely. Clamp the
idle-reset deadline to the hard one when both are set, so combining
them actually behaves like the advertised base+ceiling.

* caddyhttp: add ReadMinRate/WriteMinRate, Apache MinRate equivalent

Pure idle-reset alone doesn't bound a trickle that sends just enough
to never go idle. ReadMinRate/WriteMinRate (bytes/second) grow the
allowed deadline from a fixed start based on bytes transferred so far
instead of resetting to a flat window on every call, so a transfer
that doesn't sustain the configured rate falls behind real time and
gets cut, matching Apache mod_reqtimeout's MinRate. Zero (default)
keeps the existing flat idle-reset behavior unchanged.

* caddyhttp: use named return and consistent blank lines in idleDeadline

Matches the named-return style already used by ResponseWriterWrapper.ReadFrom.

* caddyhttp: chunk idleTimeoutWriter's Write/ReadFrom, cap at 64 KiB

SetWriteDeadline bounds the whole call it precedes, not just a stall
within it. net.Conn.Write loops internally until a buffer is fully
sent (unlike Read, which returns after one syscall), and
ResponseWriter.ReadFrom hands the entire remaining source to the
connection in one call. A single large Write, or any body copied via
io.Copy triggering the ReadFrom fast path (http.ServeContent, static
file serving), had its whole transfer bounded by one deadline,
silently truncating a slow-but-healthy transfer exactly like a hard
WriteTimeout would - the same bug found and fixed the same way in
FrankenPHP's go_ub_write (php/frankenphp#2574).

Cap each underlying call at 64 KiB and reset the deadline between
chunks instead. net/sendfile.go special-cases *io.LimitedReader, so
chunking ReadFrom still uses the sendfile fast path per chunk.

* caddyhttp: export idle-timeout types, add configurable MaxWriteChunk

Export IdleTimeoutReader/IdleTimeoutWriter/IdleDeadline so other
packages (request_body next) can reuse the same idle-reset mechanism
instead of reimplementing it, and turn the hardcoded 64 KiB write
chunk size into a configurable MaxWriteChunk field defaulting to the
same value - nginx's sendfile_max_chunk exists for the identical
reason and is admin-tunable rather than fixed.

* requestbody: idle-reset ReadTimeout/WriteTimeout, add MinRate/MaxWriteChunk

ReadTimeout/WriteTimeout set a single deadline once, so any transfer
running longer than the timeout got cut regardless of whether it was
actually stalled - the same bug the server-wide timeouts had before
switching to idle-reset. Reuse caddyhttp.IdleTimeoutReader/Writer here
too, giving per-route granularity nginx/Apache have via location/
directory scoping and Caddy's server-wide timeouts don't: a route
matching this handler can now set its own idle window independently
from the rest of the server block.

* caddyhttp: fold read/write min_rate into the idle-timeout directive

Two directives per rate (read_body_idle + read_body_min_rate) for a
value that's meaningless without the other. Fold min_rate into the
idle-timeout directive as an optional second argument instead.

* caddyhttp: split write pacing out of request_body into new timeouts handler

request_body is a request-body concern (max_size, set); ReadTimeout/
WriteTimeout/MinRate/MaxWriteChunk pace both directions, and write
pacing has nothing to do with the request body. Move all of it to a
dedicated http.handlers.timeouts module instead, mirroring the
server-wide timeouts option one level down.
2026-08-21 21:17:26 -06:00

128 lines
4.0 KiB
Go

// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package timeouts
import (
"net/http"
"time"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
caddy.RegisterModule(Timeouts{})
}
// Timeouts is a middleware that applies read/write idle timeouts, minimum
// transfer rates, and a write chunk size cap to routes matching this
// handler, independent of the server-wide equivalents.
type Timeouts struct {
// How long to allow a read from the request body to stall before
// aborting the connection, reset on every successful read (like the
// server-wide read_idle_timeout, but scoped to routes matching this
// handler). If zero, no idle timeout is applied here.
// EXPERIMENTAL. Subject to change/removal.
ReadTimeout time.Duration `json:"read_timeout,omitempty"`
// ReadMinRate requires the client to sustain at least this many
// bytes/second, averaged from the start of the read, or the
// connection is aborted (Apache mod_reqtimeout's MinRate). Only
// takes effect if ReadTimeout is also set.
// EXPERIMENTAL. Subject to change/removal.
ReadMinRate int64 `json:"read_min_rate,omitempty"`
// How long to allow a write to the client to stall before aborting
// the connection, reset on every successful write (like the
// server-wide write_idle_timeout, but scoped to routes matching this
// handler). If zero, no idle timeout is applied here.
// EXPERIMENTAL. Subject to change/removal.
WriteTimeout time.Duration `json:"write_timeout,omitempty"`
// WriteMinRate is like ReadMinRate, but for writes to the client.
// Only takes effect if WriteTimeout is also set.
// EXPERIMENTAL. Subject to change/removal.
WriteMinRate int64 `json:"write_min_rate,omitempty"`
// MaxWriteChunk bounds how many bytes a single underlying write
// operation is allowed to cover, so WriteTimeout/WriteMinRate can
// actually apply between chunks of a large response instead of
// being bounded by one deadline for the whole thing. If zero,
// caddyhttp.DefaultMaxWriteChunk is used.
// EXPERIMENTAL. Subject to change/removal.
MaxWriteChunk int `json:"max_write_chunk,omitempty"`
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
func (Timeouts) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.timeouts",
New: func() caddy.Module { return new(Timeouts) },
}
}
func (t *Timeouts) Provision(ctx caddy.Context) error {
t.logger = ctx.Logger()
return nil
}
func (t Timeouts) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
if t.ReadTimeout <= 0 && t.WriteTimeout <= 0 {
return next.ServeHTTP(w, r)
}
//nolint:bodyclose
rc := http.NewResponseController(w)
start := time.Now()
if t.ReadTimeout > 0 && r.Body != nil {
r.Body = &caddyhttp.IdleTimeoutReader{
ReadCloser: r.Body,
Ctrl: rc,
Deadline: caddyhttp.IdleDeadline{
Start: start,
Timeout: t.ReadTimeout,
MinRate: t.ReadMinRate,
},
Logger: t.logger,
}
}
if t.WriteTimeout > 0 {
w = &caddyhttp.IdleTimeoutWriter{
ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
Ctrl: rc,
Deadline: caddyhttp.IdleDeadline{
Start: start,
Timeout: t.WriteTimeout,
MinRate: t.WriteMinRate,
},
MaxChunk: t.MaxWriteChunk,
Logger: t.logger,
}
}
return next.ServeHTTP(w, r)
}
// Interface guards
var (
_ caddy.Provisioner = (*Timeouts)(nil)
_ caddyhttp.MiddlewareHandler = (*Timeouts)(nil)
)