Repository: quic-go/quic-go
Pull request #4386 (2024-03-23 21:11:28)

add an integration test for 0-RTT GET requests
GitHub still thinks that it's appropriate to automatically close PRs, and not even offer an option to reopen them: #4380. Wish GitHub engineers would finally decide to do their job, but until they do, we'll have to recreate PRs.


Patch:
diff --git a/http3/server.go b/http3/server.go
index aeb956d1..0c8cd5b5 100644
--- a/http3/server.go
+++ b/http3/server.go
@@ -157,10 +157,10 @@ type Server struct {
 	//
 	// Otherwise, if Port is not set and underlying QUIC listeners do not
 	// have valid port numbers, the port part is used in Alt-Svc headers set
-	// with SetQuicHeaders.
+	// with SetQUICHeaders.
 	Addr string
 
-	// Port is used in Alt-Svc response headers set with SetQuicHeaders. If
+	// Port is used in Alt-Svc response headers set with SetQUICHeaders. If
 	// needed Port can be manually set when the Server is created.
 	//
 	// This is useful when a Layer 4 firewall is redirecting UDP traffic and
@@ -176,7 +176,7 @@ type Server struct {
 	// Serve. If nil, it uses reasonable default values.
 	//
 	// Configured versions are also used in Alt-Svc response header set with
-	// SetQuicHeaders.
+	// SetQUICHeaders.
 	QuicConfig *quic.Config
 
 	// Handler is the HTTP request handler to use. If not set, defaults to
@@ -428,7 +428,7 @@ func (s *Server) addListener(l *QUICEarlyListener) error {
 	if port, err := extractPort(laddr.String()); err == nil {
 		s.listeners[l] = listenerInfo{port}
 	} else {
-		s.logger.Errorf("Unable to extract port from listener %s, will not be announced using SetQuicHeaders: %s", laddr, err)
+		s.logger.Errorf("Unable to extract port from listener %s, will not be announced using SetQUICHeaders: %s", laddr, err)
 		s.listeners[l] = listenerInfo{}
 	}
 	s.generateAltSvcHeader()
@@ -709,12 +709,12 @@ func (s *Server) CloseGracefully(timeout time.Duration) error {
 	return nil
 }
 
-// ErrNoAltSvcPort is the error returned by SetQuicHeaders when no port was found
+// ErrNoAltSvcPort is the error returned by SetQUICHeaders when no port was found
 // for Alt-Svc to announce. This can happen if listening on a PacketConn without a port
 // (UNIX socket, for example) and no port is specified in Server.Port or Server.Addr.
 var ErrNoAltSvcPort = errors.New("no port can be announced, specify it explicitly using Server.Port or Server.Addr")
 
-// SetQuicHeaders can be used to set the proper headers that announce that this server supports HTTP/3.
+// SetQUICHeaders can be used to set the proper headers that announce that this server supports HTTP/3.
 // The values set by default advertise all the ports the server is listening on, but can be
 // changed to a specific port by setting Server.Port before launching the server.
 // If no listener's Addr().String() returns an address with a valid port, Server.Addr will be used
@@ -722,7 +722,7 @@ var ErrNoAltSvcPort = errors.New("no port can be announced, specify it explicitl
 // For example, a server launched using ListenAndServe on an address with port 443 would set:
 //
 //	Alt-Svc: h3=":443"; ma=2592000
-func (s *Server) SetQuicHeaders(hdr http.Header) error {
+func (s *Server) SetQUICHeaders(hdr http.Header) error {
 	s.mutex.RLock()
 	defer s.mutex.RUnlock()
 
@@ -734,6 +734,11 @@ func (s *Server) SetQuicHeaders(hdr http.Header) error {
 	return nil
 }
 
+// Deprecated: use SetQUICHeaders instead.
+func (s *Server) SetQuicHeaders(hdr http.Header) error {
+	return s.SetQUICHeaders(hdr)
+}
+
 // ListenAndServeQUIC listens on the UDP network address addr and calls the
 // handler for HTTP/3 requests on incoming connections. http.DefaultServeMux is
 // used when handler is nil.
@@ -791,7 +796,7 @@ func ListenAndServe(addr, certFile, keyFile string, handler http.Handler) error
 	qErr := make(chan error, 1)
 	go func() {
 		hErr <- http.ListenAndServeTLS(addr, certFile, keyFile, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			quicServer.SetQuicHeaders(w.Header())
+			quicServer.SetQUICHeaders(w.Header())
 			handler.ServeHTTP(w, r)
 		}))
 	}()


Test patch:
diff --git a/http3/server_test.go b/http3/server_test.go
index 072d3e17..c9bfa87f 100644
--- a/http3/server_test.go
+++ b/http3/server_test.go
@@ -906,13 +906,13 @@ var _ = Describe("Server", func() {
 
 		checkSetHeaders := func(expected gmtypes.GomegaMatcher) {
 			hdr := http.Header{}
-			Expect(s.SetQuicHeaders(hdr)).To(Succeed())
+			Expect(s.SetQUICHeaders(hdr)).To(Succeed())
 			Expect(hdr).To(expected)
 		}
 
 		checkSetHeaderError := func() {
 			hdr := http.Header{}
-			Expect(s.SetQuicHeaders(hdr)).To(Equal(ErrNoAltSvcPort))
+			Expect(s.SetQUICHeaders(hdr)).To(Equal(ErrNoAltSvcPort))
 		}
 
 		It("sets proper headers with numeric port", func() {
diff --git a/integrationtests/self/http_test.go b/integrationtests/self/http_test.go
index 10564c17..bff99d34 100644
--- a/integrationtests/self/http_test.go
+++ b/integrationtests/self/http_test.go
@@ -5,6 +5,7 @@ import (
 	"bytes"
 	"compress/gzip"
 	"context"
+	"crypto/tls"
 	"errors"
 	"fmt"
 	"io"
@@ -12,12 +13,14 @@ import (
 	"net/http"
 	"os"
 	"strconv"
+	"sync/atomic"
 	"time"
 
 	"golang.org/x/sync/errgroup"
 
 	"github.com/quic-go/quic-go"
 	"github.com/quic-go/quic-go/http3"
+	quicproxy "github.com/quic-go/quic-go/integrationtests/tools/proxy"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
@@ -86,7 +89,7 @@ var _ = Describe("HTTP tests", func() {
 		server = &http3.Server{
 			Handler:    mux,
 			TLSConfig:  getTLSConfig(),
-			QuicConfig: getQuicConfig(nil),
+			QuicConfig: getQuicConfig(&quic.Config{Allow0RTT: true}),
 		}
 
 		addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:0")
@@ -112,9 +115,12 @@ var _ = Describe("HTTP tests", func() {
 
 	BeforeEach(func() {
 		rt = &http3.RoundTripper{
-			TLSClientConfig:    getTLSClientConfigWithoutServerName(),
+			TLSClientConfig: getTLSClientConfigWithoutServerName(),
+			QuicConfig: getQuicConfig(&quic.Config{
+				MaxIdleTimeout: 10 * time.Second,
+				Allow0RTT:      true,
+			}),
 			DisableCompression: true,
-			QuicConfig:         getQuicConfig(&quic.Config{MaxIdleTimeout: 10 * time.Second}),
 		}
 		client = &http.Client{Transport: rt}
 	})
@@ -572,4 +578,67 @@ var _ = Describe("HTTP tests", func() {
 		}})
 		Expect(err).To(MatchError(err))
 	})
+
+	Context("0-RTT", func() {
+		runCountingProxy := func(serverPort int, rtt time.Duration) (*quicproxy.QuicProxy, *atomic.Uint32) {
+			var num0RTTPackets atomic.Uint32
+			proxy, err := quicproxy.NewQuicProxy("localhost:0", &quicproxy.Opts{
+				RemoteAddr: fmt.Sprintf("localhost:%d", serverPort),
+				DelayPacket: func(_ quicproxy.Direction, data []byte) time.Duration {
+					if contains0RTTPacket(data) {
+						num0RTTPackets.Add(1)
+					}
+					return rtt / 2
+				},
+			})
+			Expect(err).ToNot(HaveOccurred())
+			return proxy, &num0RTTPackets
+		}
+
+		It("sends 0-RTT GET requests", func() {
+			proxy, num0RTTPackets := runCountingProxy(port, scaleDuration(50*time.Millisecond))
+			defer proxy.Close()
+
+			tlsConf := getTLSClientConfigWithoutServerName()
+			puts := make(chan string, 10)
+			tlsConf.ClientSessionCache = newClientSessionCache(tls.NewLRUClientSessionCache(10), nil, puts)
+			rt := &http3.RoundTripper{
+				TLSClientConfig: tlsConf,
+				QuicConfig: getQuicConfig(&quic.Config{
+					MaxIdleTimeout: 10 * time.Second,
+					Allow0RTT:      true,
+				}),
+				DisableCompression: true,
+			}
+			defer rt.Close()
+
+			mux.HandleFunc("/0rtt", func(w http.ResponseWriter, r *http.Request) {
+				w.Write([]byte(strconv.FormatBool(!r.TLS.HandshakeComplete)))
+			})
+			req, err := http.NewRequest(http3.MethodGet0RTT, fmt.Sprintf("https://localhost:%d/0rtt", proxy.LocalPort()), nil)
+			Expect(err).ToNot(HaveOccurred())
+			rsp, err := rt.RoundTrip(req)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(rsp.StatusCode).To(BeEquivalentTo(200))
+			data, err := io.ReadAll(rsp.Body)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(string(data)).To(Equal("false"))
+			Expect(num0RTTPackets.Load()).To(BeZero())
+			Eventually(puts).Should(Receive())
+
+			rt2 := &http3.RoundTripper{
+				TLSClientConfig:    rt.TLSClientConfig,
+				QuicConfig:         rt.QuicConfig,
+				DisableCompression: true,
+			}
+			defer rt2.Close()
+			rsp, err = rt2.RoundTrip(req)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(rsp.StatusCode).To(BeEquivalentTo(200))
+			data, err = io.ReadAll(rsp.Body)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(string(data)).To(Equal("true"))
+			Expect(num0RTTPackets.Load()).To(BeNumerically(">", 0))
+		})
+	})
 })
diff --git a/integrationtests/self/resumption_test.go b/integrationtests/self/resumption_test.go
index 23f241be..f8bb3a00 100644
--- a/integrationtests/self/resumption_test.go
+++ b/integrationtests/self/resumption_test.go
@@ -32,13 +32,17 @@ var _ tls.ClientSessionCache = &clientSessionCache{}
 
 func (c *clientSessionCache) Get(sessionKey string) (*tls.ClientSessionState, bool) {
 	session, ok := c.cache.Get(sessionKey)
-	c.gets <- sessionKey
+	if c.gets != nil {
+		c.gets <- sessionKey
+	}
 	return session, ok
 }
 
 func (c *clientSessionCache) Put(sessionKey string, cs *tls.ClientSessionState) {
 	c.cache.Put(sessionKey, cs)
-	c.puts <- sessionKey
+	if c.puts != nil {
+		c.puts <- sessionKey
+	}
 }
 
 var _ = Describe("TLS session resumption", func() {
diff --git a/integrationtests/self/zero_rtt_test.go b/integrationtests/self/zero_rtt_test.go
index 67d1f8af..46b3786d 100644
--- a/integrationtests/self/zero_rtt_test.go
+++ b/integrationtests/self/zero_rtt_test.go
@@ -50,6 +50,23 @@ func (m metadataClientSessionCache) Put(key string, session *tls.ClientSessionSt
 	m.cache.Put(key, session)
 }
 
+// contains0RTTPacket says if a packet contains a 0-RTT long header packet.
+// It correctly handles coalesced packets.
+func contains0RTTPacket(data []byte) bool {
+	for len(data) > 0 {
+		if !wire.IsLongHeaderPacket(data[0]) {
+			return false
+		}
+		hdr, _, rest, err := wire.ParsePacket(data)
+		Expect(err).ToNot(HaveOccurred())
+		if hdr.Type == protocol.PacketType0RTT {
+			return true
+		}
+		data = rest
+	}
+	return false
+}
+
 var _ = Describe("0-RTT", func() {
 	rtt := scaleDuration(5 * time.Millisecond)
 
@@ -58,23 +75,13 @@ var _ = Describe("0-RTT", func() {
 		proxy, err := quicproxy.NewQuicProxy("localhost:0", &quicproxy.Opts{
 			RemoteAddr: fmt.Sprintf("localhost:%d", serverPort),
 			DelayPacket: func(_ quicproxy.Direction, data []byte) time.Duration {
-				for len(data) > 0 {
-					if !wire.IsLongHeaderPacket(data[0]) {
-						break
-					}
-					hdr, _, rest, err := wire.ParsePacket(data)
-					Expect(err).ToNot(HaveOccurred())
-					if hdr.Type == protocol.PacketType0RTT {
-						num0RTTPackets.Add(1)
-						break
-					}
-					data = rest
+				if contains0RTTPacket(data) {
+					num0RTTPackets.Add(1)
 				}
 				return rtt / 2
 			},
 		})
 		Expect(err).ToNot(HaveOccurred())
-
 		return proxy, &num0RTTPackets
 	}