DoQ: cancel only the stalled stream, not the whole DoQ connection (#8567)

* DoQ: cancel only the stalled stream, not the whole DoQ connection

Signed-off-by: Amila Senadheera <amilaruk1995@gmail.com>

* sent only RESET_STREAM, STOP_SENDING is for client cancelling stream

Signed-off-by: Amila Senadheera <amilaruk1995@gmail.com>

---------

Signed-off-by: Amila Senadheera <amilaruk1995@gmail.com>
This commit is contained in:
Amila Senadheera
2026-09-22 01:19:37 +05:30
committed by GitHub
parent 5cb7cdc914
commit 559e57ec55
2 changed files with 301 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net"
"os"
"time"
"github.com/coredns/coredns/plugin/metrics/vars"
@@ -225,7 +226,7 @@ func (s *ServerQUIC) serveQUICStream(stream *quic.Stream, conn *quic.Conn) {
// 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.
// which frees the worker by cancelling just this stream.
if s.ReadTimeout != 0 {
_ = stream.SetReadDeadline(time.Now().Add(s.ReadTimeout))
}
@@ -236,6 +237,16 @@ func (s *ServerQUIC) serveQUICStream(stream *quic.Stream, conn *quic.Conn) {
// the STREAM FIN indicating that there will be no data to read
// anymore from this stream.
if err != nil && err != io.EOF {
if isTransientStreamError(err) {
// Abandon just this stream, not the whole connection (RFC 9250
// §4.3.3). Only RESET_STREAM (CancelWrite): STOP_SENDING is
// client-only (§4.3.1) and would itself force a connection abort.
stream.CancelWrite(quic.StreamErrorCode(DoQCodeInternalError))
s.countResponse(DoQCodeInternalError)
return
}
s.closeQUICConn(conn, DoQCodeProtocolError)
return
@@ -424,6 +435,27 @@ func readDOQMessage(r io.Reader) ([]byte, error) {
return buf, err
}
// isTransientStreamError reports whether err reflects a condition scoped to
// a single QUIC stream — the server's own read deadline expiring, or the
// peer resetting just that stream — rather than a DoQ message-framing
// violation by the peer. RFC 9250 §4.3.3 requires the latter to abort the
// whole connection; the former must not, since DoQ multiplexes many
// independent queries as separate streams on one connection.
//
// A deadline timeout is identified specifically via os.ErrDeadlineExceeded
// (what stream.SetReadDeadline produces) rather than the broader net.Error
// Timeout() check, because connection-level failures such as
// quic.IdleTimeoutError also report Timeout() == true but must still take
// the existing connection-closing path.
func isTransientStreamError(err error) bool {
if errors.Is(err, os.ErrDeadlineExceeded) {
return true
}
var streamErr *quic.StreamError
return errors.As(err, &streamErr)
}
// isExpectedErr returns true if err is an expected error, likely related to
// the current implementation.
func (s *ServerQUIC) isExpectedErr(err error) bool {