Skip to content

Commit ec403a1

Browse files
committed
proxy: fix quic opcode handling
1 parent e6e0ae0 commit ec403a1

1 file changed

Lines changed: 45 additions & 4 deletions

File tree

proxy/serverquic.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ func (p *Proxy) handleQUICStream(ctx context.Context, stream *quic.Stream, conn
358358
return
359359
}
360360

361-
if !validQUICMsg(req, p.logger) {
361+
if !validQUICMsg(conn, req, p.logger) {
362362
// If a peer encounters such an error condition, it is considered a
363363
// fatal error. It SHOULD forcibly abort the connection using QUIC's
364364
// CONNECTION_CLOSE mechanism and SHOULD use the DoQ error code
@@ -424,8 +424,8 @@ func (p *Proxy) respondQUIC(d *DNSContext) error {
424424
}
425425

426426
// validQUICMsg validates the incoming DNS message and returns false if
427-
// something is wrong with the message.
428-
func validQUICMsg(req *dns.Msg, l *slog.Logger) (ok bool) {
427+
// something is wrong with the message. conn, req, and l must not be nil.
428+
func validQUICMsg(conn *quic.Conn, req *dns.Msg, l *slog.Logger) (ok bool) {
429429
// See https://www.rfc-editor.org/rfc/rfc9250.html#name-protocol-errors
430430

431431
// 1. a client or server receives a message with a non-zero Message ID.
@@ -466,11 +466,52 @@ func validQUICMsg(req *dns.Msg, l *slog.Logger) (ok bool) {
466466

467467
// 7. a server receives a "replayable" transaction in 0-RTT data
468468
//
469-
// The information necessary to validate this is not exposed by quic-go.
469+
// Per RFC 9250 Section 4.5, only QUERY and NOTIFY transactions may be
470+
// processed from 0-RTT early data. Any other transaction received as early
471+
// data is treated as a protocol error and aborts the connection.
472+
if isNonReplayableEarlyData(conn, req) {
473+
l.Debug("client sent non-replayable transaction as 0-rtt data", "opcode", req.Opcode)
474+
475+
return false
476+
}
470477

471478
return true
472479
}
473480

481+
// isNonReplayableEarlyData returns true if req was received as QUIC 0-RTT early
482+
// data and its opcode is not safe to process before the handshake completes.
483+
// Per RFC 9250 Section 4.5, only QUERY and NOTIFY transactions may be processed
484+
// from 0-RTT data. conn and req must not be nil.
485+
//
486+
// See https://www.rfc-editor.org/rfc/rfc9250#section-4.5.
487+
func isNonReplayableEarlyData(conn *quic.Conn, req *dns.Msg) (ok bool) {
488+
if isReplayableOpcode(req.Opcode) {
489+
return false
490+
}
491+
492+
if !conn.ConnectionState().Used0RTT {
493+
return false
494+
}
495+
496+
// The data is early data only if it arrived before the handshake completed
497+
// on a resumed 0-RTT connection.
498+
select {
499+
case <-conn.HandshakeComplete():
500+
return false
501+
default:
502+
return true
503+
}
504+
}
505+
506+
// isReplayableOpcode returns true if a transaction with the given DNS opcode is
507+
// safe to process from QUIC 0-RTT early data. Per RFC 9250 Section 4.5, only
508+
// QUERY and NOTIFY transactions are considered replayable.
509+
//
510+
// See https://www.rfc-editor.org/rfc/rfc9250#section-4.5.
511+
func isReplayableOpcode(opcode int) (ok bool) {
512+
return opcode == dns.OpcodeQuery || opcode == dns.OpcodeNotify
513+
}
514+
474515
// logShortQUICRead is a logging helper for short reads from a QUIC stream.
475516
func logShortQUICRead(ctx context.Context, err error, l *slog.Logger) {
476517
if err == nil {

0 commit comments

Comments
 (0)