diff --git a/connection.go b/connection.go index 7f26ef5e5..1b288a2b3 100644 --- a/connection.go +++ b/connection.go @@ -307,7 +307,7 @@ var newConnection = func( RetrySourceConnectionID: retrySrcConnID, } if s.config.EnableDatagrams { - params.MaxDatagramFrameSize = protocol.MaxDatagramFrameSize + params.MaxDatagramFrameSize = wire.MaxDatagramSize } else { params.MaxDatagramFrameSize = protocol.InvalidByteCount } @@ -414,7 +414,7 @@ var newClientConnection = func( InitialSourceConnectionID: srcConnID, } if s.config.EnableDatagrams { - params.MaxDatagramFrameSize = protocol.MaxDatagramFrameSize + params.MaxDatagramFrameSize = wire.MaxDatagramSize } else { params.MaxDatagramFrameSize = protocol.InvalidByteCount } @@ -1522,7 +1522,7 @@ func (s *connection) handleAckFrame(frame *wire.AckFrame, encLevel protocol.Encr } func (s *connection) handleDatagramFrame(f *wire.DatagramFrame) error { - if f.Length(s.version) > protocol.MaxDatagramFrameSize { + if f.Length(s.version) > wire.MaxDatagramSize { return &qerr.TransportError{ ErrorCode: qerr.ProtocolViolation, ErrorMessage: "DATAGRAM frame too large", @@ -2350,7 +2350,9 @@ func (s *connection) SendDatagram(p []byte) error { f := &wire.DatagramFrame{DataLenPresent: true} if protocol.ByteCount(len(p)) > f.MaxDataLen(s.peerParams.MaxDatagramFrameSize, s.version) { - return errors.New("message too large") + return &DatagramTooLargeError{ + PeerMaxDatagramFrameSize: int64(s.peerParams.MaxDatagramFrameSize), + } } f.Data = make([]byte, len(p)) copy(f.Data, p) diff --git a/errors.go b/errors.go index c9fb0a07b..fda3c9247 100644 --- a/errors.go +++ b/errors.go @@ -61,3 +61,15 @@ func (e *StreamError) Error() string { } return fmt.Sprintf("stream %d canceled by %s with error code %d", e.StreamID, pers, e.ErrorCode) } + +// DatagramTooLargeError is returned from Connection.SendDatagram if the payload is too large to be sent. +type DatagramTooLargeError struct { + PeerMaxDatagramFrameSize int64 +} + +func (e *DatagramTooLargeError) Is(target error) bool { + _, ok := target.(*DatagramTooLargeError) + return ok +} + +func (e *DatagramTooLargeError) Error() string { return "DATAGRAM frame too large" } diff --git a/integrationtests/self/datagram_test.go b/integrationtests/self/datagram_test.go index 55e857716..f9608272b 100644 --- a/integrationtests/self/datagram_test.go +++ b/integrationtests/self/datagram_test.go @@ -19,7 +19,8 @@ import ( ) var _ = Describe("Datagram test", func() { - const num = 100 + const concurrentSends = 100 + const maxDatagramSize = 250 var ( serverConn, clientConn *net.UDPConn @@ -47,11 +48,11 @@ var _ = Describe("Datagram test", func() { if expectDatagramSupport { Expect(conn.ConnectionState().SupportsDatagrams).To(BeTrue()) - if enableDatagram { + f := &wire.DatagramFrame{DataLenPresent: true} var wg sync.WaitGroup - wg.Add(num) - for i := 0; i < num; i++ { + wg.Add(concurrentSends) + for i := 0; i < concurrentSends; i++ { go func(i int) { defer GinkgoRecover() defer wg.Done() @@ -60,6 +61,11 @@ var _ = Describe("Datagram test", func() { Expect(conn.SendDatagram(b)).To(Succeed()) }(i) } + maxDatagramMessageSize := f.MaxDataLen(maxDatagramSize, conn.ConnectionState().Version) + b := make([]byte, maxDatagramMessageSize+1) + Expect(conn.SendDatagram(b)).To(MatchError(&quic.DatagramTooLargeError{ + PeerMaxDatagramFrameSize: int64(maxDatagramMessageSize), + })) wg.Wait() } } else { @@ -103,6 +109,8 @@ var _ = Describe("Datagram test", func() { }) It("sends datagrams", func() { + oldMaxDatagramSize := wire.MaxDatagramSize + wire.MaxDatagramSize = maxDatagramSize proxyPort, close := startServerAndProxy(true, true) defer close() raddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("localhost:%d", proxyPort)) @@ -128,14 +136,15 @@ var _ = Describe("Datagram test", func() { } numDropped := int(dropped.Load()) - expVal := num - numDropped + expVal := concurrentSends - numDropped fmt.Fprintf(GinkgoWriter, "Dropped %d out of %d packets.\n", numDropped, total.Load()) - fmt.Fprintf(GinkgoWriter, "Received %d out of %d sent datagrams.\n", counter, num) + fmt.Fprintf(GinkgoWriter, "Received %d out of %d sent datagrams.\n", counter, concurrentSends) Expect(counter).To(And( BeNumerically(">", expVal*9/10), - BeNumerically("<", num), + BeNumerically("<", concurrentSends), )) Eventually(conn.Context().Done).Should(BeClosed()) + wire.MaxDatagramSize = oldMaxDatagramSize }) It("server can disable datagram", func() { diff --git a/internal/protocol/params.go b/internal/protocol/params.go index 3ca68bf83..28b6da7cd 100644 --- a/internal/protocol/params.go +++ b/internal/protocol/params.go @@ -129,10 +129,6 @@ const MaxPostHandshakeCryptoFrameSize = 1000 // but must ensure that a maximum size ACK frame fits into one packet. const MaxAckFrameSize ByteCount = 1000 -// MaxDatagramFrameSize is the maximum size of a DATAGRAM frame (RFC 9221). -// The size is chosen such that a DATAGRAM frame fits into a QUIC packet. -const MaxDatagramFrameSize ByteCount = 1200 - // DatagramRcvQueueLen is the length of the receive queue for DATAGRAM frames (RFC 9221) const DatagramRcvQueueLen = 128 diff --git a/internal/wire/datagram_frame.go b/internal/wire/datagram_frame.go index e6c451961..4d0010846 100644 --- a/internal/wire/datagram_frame.go +++ b/internal/wire/datagram_frame.go @@ -8,6 +8,12 @@ import ( "github.com/quic-go/quic-go/quicvarint" ) +// MaxDatagramSize is the maximum size of a DATAGRAM frame (RFC 9221). +// By setting it to a large value, we allow all datagrams that fit into a QUIC packet. +// The value is chosen such that it can still be encoded as a 2 byte varint. +// This is a var and not a const so it can be set in tests. +var MaxDatagramSize protocol.ByteCount = 16383 + // A DatagramFrame is a DATAGRAM frame type DatagramFrame struct { DataLenPresent bool diff --git a/internal/wire/transport_parameter_test.go b/internal/wire/transport_parameter_test.go index 2fb795395..a82b6dcf5 100644 --- a/internal/wire/transport_parameter_test.go +++ b/internal/wire/transport_parameter_test.go @@ -503,7 +503,7 @@ var _ = Describe("Transport Parameters", func() { MaxBidiStreamNum: protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))), MaxUniStreamNum: protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))), ActiveConnectionIDLimit: 2 + getRandomValueUpTo(math.MaxInt64-2), - MaxDatagramFrameSize: protocol.ByteCount(getRandomValueUpTo(int64(protocol.MaxDatagramFrameSize))), + MaxDatagramFrameSize: protocol.ByteCount(getRandomValueUpTo(int64(MaxDatagramSize))), } Expect(params.ValidFor0RTT(params)).To(BeTrue()) b := params.MarshalForSessionTicket(nil)