core/dnsserver: bound DoQ stream read with the server read timeout (#8231)

serveQUICStream acquires a worker from streamProcessPool and then calls
readDOQMessage with no read deadline. A client that opens a QUIC stream
but never (or only slowly) sends its DoQ query blocks that worker
indefinitely. With enough such streams this starves the worker pool and
stalls DoQ service for other clients, a remotely triggerable denial of
service that needs no authentication.

Set a per-stream read deadline before readDOQMessage, bounded by the
server's existing ReadTimeout (the same deadline used to read a query on
TCP, default 3s), so a stalled stream cannot hold a worker forever. A
deadline hit surfaces as a read error handled by the existing error path,
which closes the connection and frees the worker; the normal fast path is
unchanged.

Adds a regression test proving a stalled stream no longer starves the
single-worker pool while a well-behaved query is still served.

Signed-off-by: Omkhar Arasaratnam <omkhar@linkedin.com>
Co-authored-by: Omkhar Arasaratnam <omkhar@linkedin.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Omkhar Arasaratnam
2026-07-09 13:37:57 -04:00
committed by GitHub
parent ab318db7b4
commit 9fac0b6e9e
2 changed files with 139 additions and 0 deletions

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net"
"time"
"github.com/coredns/coredns/plugin/metrics/vars"
clog "github.com/coredns/coredns/plugin/pkg/log"
@@ -186,6 +187,19 @@ func (s *ServerQUIC) serveQUICStream(stream *quic.Stream, conn *quic.Conn) {
s.closeQUICConn(conn, DoQCodeInternalError)
return
}
// A stream is served by a worker acquired from s.streamProcessPool. A
// client that opens a stream but never (or only slowly) sends its DoQ
// query would otherwise block readDOQMessage indefinitely, holding that
// worker and eventually starving the pool. Bound the wait with the
// server's read timeout (the same deadline used for reading a query on
// TCP), so a stalled stream cannot hold a worker forever. A deadline
// hit surfaces as a read error handled by the existing error path below,
// which closes the connection and frees the worker.
if s.ReadTimeout != 0 {
_ = stream.SetReadDeadline(time.Now().Add(s.ReadTimeout))
}
buf, err := readDOQMessage(stream)
// io.EOF does not really mean that there's any error, it is just